diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 1f937e1837..3cecb0d07c 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -15,13 +15,13 @@ ] }, "codefilesanity": { - "version": "0.0.36", + "version": "0.0.37", "commands": [ "CodeFileSanity" ] }, "ppy.localisationanalyser.tools": { - "version": "2022.809.0", + "version": "2023.712.0", "commands": [ "localisation" ] diff --git a/.editorconfig b/.editorconfig index 67c47000d3..c249e5e9b3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,6 +9,9 @@ indent_style = space indent_size = 2 trim_trailing_whitespace = true +[g_*.cs] +generated_code = true + [*.cs] end_of_line = crlf insert_final_newline = true diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index b85862270b..d35d4be412 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -6,3 +6,5 @@ 212d78865a6b5f091173a347bad5686834d1d5fe # Add partial specs in mobile projects too 00c11b2b4e389e48f3995d63484a6bc66a7afbdb +# Mass NRT enabling +0ab0c52ad577b3e7b406d09fa6056a56ff997c3e diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index 2c6ec17e18..e7c628e365 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -1,206 +1,380 @@ -# Listens for new PR comments containing !pp check [id], and runs a diffcalc comparison against master. -# Usage: -# !pp check 0 | Runs only the osu! ruleset. -# !pp check 0 2 | Runs only the osu! and catch rulesets. +# ## Description # +# Uses [diffcalc-sheet-generator](https://github.com/smoogipoo/diffcalc-sheet-generator) to run two builds of osu and generate an SR/PP/Score comparison spreadsheet. +# +# ## Requirements +# +# Self-hosted runner with installed: +# - `docker >= 20.10.16` +# - `docker-compose >= 2.5.1` +# - `lbzip2` +# - `jq` +# +# ## Usage +# +# The workflow can be run in two ways: +# 1. Via workflow dispatch. +# 2. By an owner of the repository posting a pull request or issue comment containing `!diffcalc`. +# For pull requests, the workflow will assume the pull request as the target to compare against (i.e. the `OSU_B` variable). +# Any lines in the comment of the form `KEY=VALUE` are treated as variables for the generator. +# +# ## Google Service Account +# +# Spreadsheets are uploaded to a Google Service Account, and exposed with read-only permissions to the wider audience. +# +# 1. Create a project at https://console.cloud.google.com +# 2. Enable the `Google Sheets` and `Google Drive` APIs. +# 3. Create a Service Account +# 4. Generate a key in the JSON format. +# 5. Encode the key as base64 and store as an **actions secret** with name **`DIFFCALC_GOOGLE_CREDENTIALS`** +# +# ## Environment variables +# +# The default environment may be configured via **actions variables**. +# +# Refer to [the sample environment](https://github.com/smoogipoo/diffcalc-sheet-generator/blob/master/.env.sample), and prefix each variable with `DIFFCALC_` (e.g. `DIFFCALC_THREADS`, `DIFFCALC_INNODB_BUFFER_SIZE`, etc...). + +name: Run difficulty calculation comparison + +run-name: "${{ github.event_name == 'workflow_dispatch' && format('Manual run: {0}', inputs.osu-b) || 'Automatic comment trigger' }}" -name: Difficulty Calculation on: issue_comment: types: [ created ] + workflow_dispatch: + inputs: + osu-b: + description: "The target build of ppy/osu" + type: string + required: true + ruleset: + description: "The ruleset to process" + type: choice + required: true + options: + - osu + - taiko + - catch + - mania + converts: + description: "Include converted beatmaps" + type: boolean + required: false + default: true + ranked-only: + description: "Only ranked beatmaps" + type: boolean + required: false + default: true + generators: + description: "Comma-separated list of generators (available: [sr, pp, score])" + type: string + required: false + default: 'pp,sr' + osu-a: + description: "The source build of ppy/osu" + type: string + required: false + default: 'latest' + difficulty-calculator-a: + description: "The source build of ppy/osu-difficulty-calculator" + type: string + required: false + default: 'latest' + difficulty-calculator-b: + description: "The target build of ppy/osu-difficulty-calculator" + type: string + required: false + default: 'latest' + score-processor-a: + description: "The source build of ppy/osu-queue-score-statistics" + type: string + required: false + default: 'latest' + score-processor-b: + description: "The target build of ppy/osu-queue-score-statistics" + type: string + required: false + default: 'latest' + +permissions: + pull-requests: write env: - CONCURRENCY: 4 - ALLOW_DOWNLOAD: 1 - SAVE_DOWNLOADED: 1 - SKIP_INSERT_ATTRIBUTES: 1 + EXECUTION_ID: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }} jobs: - metadata: - name: Check for requests + check-permissions: + name: Check permissions + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.comment.body, '!diffcalc') }} + steps: + - name: Check permissions + if: ${{ github.event_name != 'workflow_dispatch' }} + uses: actions-cool/check-user-permission@a0668c9aec87f3875fc56170b6452a453e9dd819 # v2.2.0 + with: + require: 'write' + + create-comment: + name: Create PR comment + needs: check-permissions + runs-on: ubuntu-latest + if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }} + steps: + - name: Create comment + uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2 + with: + comment_tag: ${{ env.EXECUTION_ID }} + message: | + Difficulty calculation queued -- please wait! (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + + *This comment will update on completion* + + directory: + name: Prepare directory + needs: check-permissions runs-on: self-hosted - if: github.event.issue.pull_request && contains(github.event.comment.body, '!pp check') && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') outputs: - matrix: ${{ steps.generate-matrix.outputs.matrix }} - continue: ${{ steps.generate-matrix.outputs.continue }} + 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: Construct build matrix - id: generate-matrix + - name: Checkout diffcalc-sheet-generator + uses: actions/checkout@v3 + with: + path: ${{ env.EXECUTION_ID }} + repository: 'smoogipoo/diffcalc-sheet-generator' + + - name: Set outputs + id: set-outputs run: | - if [[ "${{ github.event.comment.body }}" =~ "osu" ]] ; then - MATRIX_PROJECTS_JSON+='{ "name": "osu", "id": 0 },' - fi - if [[ "${{ github.event.comment.body }}" =~ "taiko" ]] ; then - MATRIX_PROJECTS_JSON+='{ "name": "taiko", "id": 1 },' - fi - if [[ "${{ github.event.comment.body }}" =~ "catch" ]] ; then - MATRIX_PROJECTS_JSON+='{ "name": "catch", "id": 2 },' - fi - if [[ "${{ github.event.comment.body }}" =~ "mania" ]] ; then - MATRIX_PROJECTS_JSON+='{ "name": "mania", "id": 3 },' - fi + 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}" - if [[ "${MATRIX_PROJECTS_JSON}" != "" ]]; then - MATRIX_JSON="{ \"ruleset\": [ ${MATRIX_PROJECTS_JSON} ] }" - echo "${MATRIX_JSON}" - CONTINUE="yes" - else - CONTINUE="no" - fi - - echo "continue=${CONTINUE}" >> $GITHUB_OUTPUT - echo "matrix=${MATRIX_JSON}" >> $GITHUB_OUTPUT - diffcalc: - name: Run + environment: + name: Setup environment + needs: directory runs-on: self-hosted - timeout-minutes: 1440 - if: needs.metadata.outputs.continue == 'yes' - needs: metadata - strategy: - matrix: ${{ fromJson(needs.metadata.outputs.matrix) }} + env: + VARS_JSON: ${{ toJSON(vars) }} steps: - - name: Verify MySQL connection from host + - name: Add base environment run: | - mysql -e "SHOW DATABASES" + # Required by diffcalc-sheet-generator + cp '${{ needs.directory.outputs.GENERATOR_DIR }}/.env.sample' "${{ needs.directory.outputs.GENERATOR_ENV }}" - - name: Drop previous databases - run: | - for db in osu_master osu_pr - do - mysql -e "DROP DATABASE IF EXISTS $db" + # 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: Create directory structure + - name: Add pull-request environment + if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }} run: | - mkdir -p $GITHUB_WORKSPACE/master/ - mkdir -p $GITHUB_WORKSPACE/pr/ + sed -i "s;^OSU_B=.*$;OSU_B=${{ github.event.issue.pull_request.html_url }};" "${{ needs.directory.outputs.GENERATOR_ENV }}" - - name: Get upstream branch # https://akaimo.hatenablog.jp/entry/2020/05/16/101251 - id: upstreambranch - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Add comment environment + if: ${{ github.event_name == 'issue_comment' }} run: | - echo "branchname=$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.ref' | sed 's/\"//g')" >> $GITHUB_OUTPUT - echo "repo=$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.repo.full_name' | sed 's/\"//g')" >> $GITHUB_OUTPUT - - # Checkout osu - - name: Checkout osu (master) - uses: actions/checkout@v3 - with: - path: 'master/osu' - - name: Checkout osu (pr) - uses: actions/checkout@v3 - with: - path: 'pr/osu' - repository: ${{ steps.upstreambranch.outputs.repo }} - ref: ${{ steps.upstreambranch.outputs.branchname }} - - - name: Checkout osu-difficulty-calculator (master) - uses: actions/checkout@v3 - with: - repository: ppy/osu-difficulty-calculator - path: 'master/osu-difficulty-calculator' - - name: Checkout osu-difficulty-calculator (pr) - uses: actions/checkout@v3 - with: - repository: ppy/osu-difficulty-calculator - path: 'pr/osu-difficulty-calculator' - - - name: Install .NET 5.0.x - uses: actions/setup-dotnet@v3 - with: - dotnet-version: "5.0.x" - - # Sanity checks to make sure diffcalc is not run when incompatible. - - name: Build diffcalc (master) - run: | - cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator - ./UseLocalOsu.sh - dotnet build - - name: Build diffcalc (pr) - run: | - cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator - ./UseLocalOsu.sh - dotnet build - - - name: Download + import data - run: | - PERFORMANCE_DATA_NAME=$(curl https://data.ppy.sh/ | grep performance_${{ matrix.ruleset.name }}_top_1000 | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g') - BEATMAPS_DATA_NAME=$(curl https://data.ppy.sh/ | grep osu_files | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g') - - # Set env variable for further steps. - echo "BEATMAPS_PATH=$GITHUB_WORKSPACE/$BEATMAPS_DATA_NAME" >> $GITHUB_ENV - - cd $GITHUB_WORKSPACE - - echo "Downloading database dump $PERFORMANCE_DATA_NAME.." - wget -q -nc https://data.ppy.sh/$PERFORMANCE_DATA_NAME.tar.bz2 - echo "Extracting.." - tar -xf $PERFORMANCE_DATA_NAME.tar.bz2 - - echo "Downloading beatmap dump $BEATMAPS_DATA_NAME.." - wget -q -nc https://data.ppy.sh/$BEATMAPS_DATA_NAME.tar.bz2 - echo "Extracting.." - tar -xf $BEATMAPS_DATA_NAME.tar.bz2 - - cd $PERFORMANCE_DATA_NAME - - for db in osu_master osu_pr - do - echo "Setting up database $db.." - - mysql -e "CREATE DATABASE $db" - - echo "Importing beatmaps.." - cat osu_beatmaps.sql | mysql $db - echo "Importing beatmapsets.." - cat osu_beatmapsets.sql | mysql $db - - echo "Creating table structure.." - mysql $db -e 'CREATE TABLE `osu_beatmap_difficulty` ( - `beatmap_id` int unsigned NOT NULL, - `mode` tinyint NOT NULL DEFAULT 0, - `mods` int unsigned NOT NULL, - `diff_unified` float NOT NULL, - `last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (`beatmap_id`,`mode`,`mods`), - KEY `diff_sort` (`mode`,`mods`,`diff_unified`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;' + # Add comment environment + echo '${{ github.event.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: Run diffcalc (master) - env: - DB_NAME: osu_master + - name: Add dispatch environment + if: ${{ github.event_name == 'workflow_dispatch' }} run: | - cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator/osu.Server.DifficultyCalculator - dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }} - - name: Run diffcalc (pr) - env: - DB_NAME: osu_pr - run: | - cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator/osu.Server.DifficultyCalculator - dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }} + 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 }}" - - name: Print diffs - run: | - mysql -e " - SELECT - m.beatmap_id, - m.mods, - b.filename, - m.diff_unified as 'sr_master', - p.diff_unified as 'sr_pr', - (p.diff_unified - m.diff_unified) as 'diff' - FROM osu_master.osu_beatmap_difficulty m - JOIN osu_pr.osu_beatmap_difficulty p - ON m.beatmap_id = p.beatmap_id - AND m.mode = p.mode - AND m.mods = p.mods - JOIN osu_pr.osu_beatmaps b - ON b.beatmap_id = p.beatmap_id - WHERE abs(m.diff_unified - p.diff_unified) > 0.1 - ORDER BY abs(m.diff_unified - p.diff_unified) - DESC - LIMIT 10000;" + if [[ '${{ inputs.osu-a }}' != 'latest' ]]; then + sed -i 's;^OSU_A=.*$;OSU_A=${{ inputs.osu-a }};' "${{ needs.directory.outputs.GENERATOR_ENV }}" + fi - # Todo: Run ppcalc + if [[ '${{ inputs.difficulty-calculator-a }}' != 'latest' ]]; then + sed -i 's;^DIFFICULTY_CALCULATOR_A=.*$;DIFFICULTY_CALCULATOR_A=${{ inputs.difficulty-calculator-a }};' "${{ needs.directory.outputs.GENERATOR_ENV }}" + fi + + if [[ '${{ inputs.difficulty-calculator-b }}' != 'latest' ]]; then + sed -i 's;^DIFFICULTY_CALCULATOR_B=.*$;DIFFICULTY_CALCULATOR_B=${{ inputs.difficulty-calculator-b }};' "${{ needs.directory.outputs.GENERATOR_ENV }}" + fi + + if [[ '${{ inputs.score-processor-a }}' != 'latest' ]]; then + sed -i 's;^SCORE_PROCESSOR_A=.*$;SCORE_PROCESSOR_A=${{ inputs.score-processor-a }};' "${{ needs.directory.outputs.GENERATOR_ENV }}" + fi + + if [[ '${{ inputs.score-processor-b }}' != 'latest' ]]; then + sed -i 's;^SCORE_PROCESSOR_B=.*$;SCORE_PROCESSOR_B=${{ inputs.score-processor-b }};' "${{ needs.directory.outputs.GENERATOR_ENV }}" + fi + + if [[ '${{ inputs.converts }}' == 'true' ]]; then + sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=0/' "${{ needs.directory.outputs.GENERATOR_ENV }}" + else + sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=1/' "${{ needs.directory.outputs.GENERATOR_ENV }}" + fi + + if [[ '${{ inputs.ranked-only }}' == 'true' ]]; then + sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=1/' "${{ needs.directory.outputs.GENERATOR_ENV }}" + else + sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=0/' "${{ needs.directory.outputs.GENERATOR_ENV }}" + fi + + scores: + name: Setup scores + needs: [ directory, environment ] + runs-on: self-hosted + steps: + - name: Query latest data + id: query + run: | + ruleset=$(cat ${{ needs.directory.outputs.GENERATOR_ENV }} | grep -E '^RULESET=' | cut -d '=' -f2-) + performance_data_name=$(curl -s "https://data.ppy.sh/" | grep "performance_${ruleset}_top_1000\b" | tail -1 | awk -F "'" '{print $2}' | sed 's/\.tar\.bz2//g') + + echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/sql/${ruleset}" >> "${GITHUB_OUTPUT}" + echo "DATA_NAME=${performance_data_name}" >> "${GITHUB_OUTPUT}" + + - name: Restore cache + id: restore-cache + uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1 + with: + path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2 + key: ${{ steps.query.outputs.DATA_NAME }} + + - name: Download + if: steps.restore-cache.outputs.cache-hit != 'true' + run: | + wget -q -nc "https://data.ppy.sh/${{ steps.query.outputs.DATA_NAME }}.tar.bz2" + + - name: Extract + run: | + tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_NAME }}.tar.bz2" + rm -r "${{ steps.query.outputs.TARGET_DIR }}" + mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}" + + beatmaps: + name: Setup beatmaps + needs: directory + runs-on: self-hosted + steps: + - name: Query latest data + id: query + run: | + beatmaps_data_name=$(curl -s "https://data.ppy.sh/" | grep "osu_files" | tail -1 | awk -F "'" '{print $2}' | sed 's/\.tar\.bz2//g') + + echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/beatmaps" >> "${GITHUB_OUTPUT}" + echo "DATA_NAME=${beatmaps_data_name}" >> "${GITHUB_OUTPUT}" + + - name: Restore cache + id: restore-cache + uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1 + with: + path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2 + key: ${{ steps.query.outputs.DATA_NAME }} + + - name: Download + if: steps.restore-cache.outputs.cache-hit != 'true' + run: | + wget -q -nc "https://data.ppy.sh/${{ steps.query.outputs.DATA_NAME }}.tar.bz2" + + - name: Extract + run: | + tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_NAME }}.tar.bz2" + rm -r "${{ steps.query.outputs.TARGET_DIR }}" + mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}" + + generator: + name: Run generator + needs: [ directory, environment, scores, beatmaps ] + runs-on: self-hosted + timeout-minutes: 720 + outputs: + TARGET: ${{ steps.run.outputs.TARGET }} + SPREADSHEET_LINK: ${{ steps.run.outputs.SPREADSHEET_LINK }} + steps: + - name: Run + id: run + run: | + # Add the GitHub token. This needs to be done here because it's unique per-job. + sed -i 's/^GH_TOKEN=.*$/GH_TOKEN=${{ github.token }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}" + + cd "${{ needs.directory.outputs.GENERATOR_DIR }}" + docker-compose up --build generator + + link=$(docker-compose logs generator -n 10 | grep 'http' | sed -E 's/^.*(http.*)$/\1/') + target=$(cat "${{ needs.directory.outputs.GENERATOR_ENV }}" | grep -E '^OSU_B=' | cut -d '=' -f2-) + + echo "TARGET=${target}" >> "${GITHUB_OUTPUT}" + echo "SPREADSHEET_LINK=${link}" >> "${GITHUB_OUTPUT}" + + - name: Shutdown + if: ${{ always() }} + run: | + cd "${{ needs.directory.outputs.GENERATOR_DIR }}" + docker-compose down -v + + output-cli: + name: Output info + needs: generator + 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 }}" + + update-comment: + name: Update PR comment + needs: [ create-comment, generator ] + runs-on: ubuntu-latest + if: ${{ always() && needs.create-comment.result == 'success' }} + steps: + - name: Update comment on success + if: ${{ needs.generator.result == 'success' }} + uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2 + with: + comment_tag: ${{ env.EXECUTION_ID }} + mode: upsert + create_if_not_exists: false + message: | + Target: ${{ needs.generator.outputs.TARGET }} + Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }} + + - name: Update comment on failure + if: ${{ needs.generator.result == 'failure' }} + uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2 + with: + comment_tag: ${{ env.EXECUTION_ID }} + mode: upsert + create_if_not_exists: false + message: | + Difficulty calculation failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + + - name: Update comment on cancellation + if: ${{ needs.generator.result == 'cancelled' }} + uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2 + with: + comment_tag: ${{ env.EXECUTION_ID }} + mode: delete + message: '.' # Appears to be required by this action for non-error status code. diff --git a/.gitignore b/.gitignore index 0c7a18b437..525b3418cd 100644 --- a/.gitignore +++ b/.gitignore @@ -339,6 +339,5 @@ inspectcode # Fody (pulled in by Realm) - schema file FodyWeavers.xsd -**/FodyWeavers.xml .idea/.idea.osu.Desktop/.idea/misc.xml \ No newline at end of file diff --git a/README.md b/README.md index cf7ce35791..d5dc0723af 100644 --- a/README.md +++ b/README.md @@ -12,45 +12,48 @@ A free-to-win rhythm game. Rhythm is just a *click* away! -The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge. +This is the future – and final – iteration of the [osu!](https://osu.ppy.sh) game client which marks the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge. ## Status -This project is under constant development, but we aim to keep things in a stable state. Users are encouraged to try it out and keep it installed alongside the stable *osu!* client. It will continue to evolve to the point of eventually replacing the existing stable client as an update. +This project is under constant development, but we do our best to keep things in a stable state. Players are encouraged to install from a release alongside their stable *osu!* client. This project will continue to evolve until we eventually reach the point where most users prefer it over the previous "osu!stable" release. -**IMPORTANT:** Gameplay mechanics (and other features which you may have come to know and love) are in a constant state of flux. Game balance and final quality-of-life passes come at the end of development, preceded by experimentation and changes which may potentially **reduce playability or usability**. This is done in order to allow us to move forward as developers and designers more efficiently. If this offends you, please consider sticking to a [stable release](https://osu.ppy.sh/home/download) of osu!. We are not yet open to heated discussion over game mechanics and will not be using github as a forum for such discussions just yet. - -We are accepting bug reports (please report with as much detail as possible and follow the existing issue templates). Feature requests are also welcome, but understand that our focus is on completing the game to feature parity before adding new features. A few resources are available as starting points to getting involved and understanding the project: +A few resources are available as starting points to getting involved and understanding the project: - Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer). - You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management). +- Track our current efforts [towards full "ranked play" support](https://github.com/orgs/ppy/projects/13?query=is%3Aopen+sort%3Aupdated-desc). ## Running osu! -If you are looking to install or test osu! without setting up a development environment, you can consume our [releases](https://github.com/ppy/osu/releases). You can also generally download a version for your current device from the [osu! site](https://osu.ppy.sh/home/download). Failing that, you may use the links below to download the latest version for your operating system of choice: +If you are just looking to give the game a whirl, you can grab the latest release for your platform: -**Latest release:** +### Latest release: | [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 10.15+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 13.4+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) | | ------------- | ------------- | ------------- | ------------- | ------------- | -- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets. +You can also generally download a version for your current device from the [osu! site](https://osu.ppy.sh/home/download). If your platform is not listed above, there is still a chance you can manually build it by following the instructions below. +**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores in early 2024. + ## Developing a custom ruleset -osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates). +osu! is designed to allow user-created gameplay variations, called "rulesets". Building one of these allows a developer to harness the power of the osu! beatmap library, game engine, and general UX for a new style of gameplay. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates). You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/discussions/13096). ## Developing osu! +### Prerequisites + Please make sure you have the following prerequisites: - A desktop platform with the [.NET 6.0 SDK](https://dotnet.microsoft.com/download) installed. -When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/). +When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Visual Studio Code](https://code.visualstudio.com/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) plugin installed. ### Downloading the source code @@ -69,9 +72,19 @@ git pull ### Building -Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing). +#### From an IDE -- Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln`. This will allow access to template run configurations. +You should load the solution via one of the platform-specific `.slnf` files, rather than the main `.sln`. This will reduce dependencies and hide platforms that you don't care about. Valid `.slnf` files are: + +- `osu.Desktop.slnf` (most common) +- `osu.Android.slnf` +- `osu.iOS.slnf` + +Run configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `osu! (Tests)` project/configuration. More information on this is provided [below](#contributing). + +To build for mobile platforms, you will likely need to run `sudo dotnet workload restore` if you haven't done so previously. This will install Android/iOS tooling required to complete the build. + +#### From CLI You can also build and run *osu!* from the command-line with a single command: @@ -79,12 +92,10 @@ You can also build and run *osu!* from the command-line with a single command: dotnet run --project osu.Desktop ``` -If you are not interested in debugging *osu!*, you can add `-c Release` to gain performance. In this case, you must replace `Debug` with `Release` in any commands mentioned in this document. +When running locally to do any kind of performance testing, make sure to add `-c Release` to the build command, as the overhead of running with the default `Debug` configuration can be large (especially when testing with local framework modifications as below). If the build fails, try to restore NuGet packages with `dotnet restore`. -_Due to a historical feature gap between .NET Core and Xamarin, running `dotnet` CLI from the root directory will not work for most commands. This can be resolved by specifying a target `.csproj` or the helper project at `build/Desktop.proj`. Configurations have been provided to work around this issue for all supported IDEs mentioned above._ - ### Testing with resource/framework modifications Sometimes it may be necessary to cross-test changes in [osu-resources](https://github.com/ppy/osu-resources) or [osu-framework](https://github.com/ppy/osu-framework). This can be quickly achieved using included commands: diff --git a/Templates/README.md b/Templates/README.md index cf25a89273..28aaee3290 100644 --- a/Templates/README.md +++ b/Templates/README.md @@ -7,7 +7,7 @@ Templates for use when creating osu! dependent projects. Create a fully-testable ```bash # install (or update) templates package. # this only needs to be done once -dotnet new -i ppy.osu.Game.Templates +dotnet new install ppy.osu.Game.Templates # create an empty freeform ruleset dotnet new ruleset -n MyCoolRuleset diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj index a1c53ece03..2baa7ee0e0 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -9,9 +9,9 @@ false - + - + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 683e9fd5e8..a2308e6dfc 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -9,9 +9,9 @@ false - + - + diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj index b7a7fff18a..e839d2657c 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -9,9 +9,9 @@ false - + - + diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 683e9fd5e8..a2308e6dfc 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -9,9 +9,9 @@ false - + - + diff --git a/app.manifest b/app.manifest index 533c6ff208..088ad1dde7 100644 --- a/app.manifest +++ b/app.manifest @@ -1,6 +1,7 @@  + 1 @@ -14,33 +15,10 @@ - - - - - - - + - - - true - - - - - - - diff --git a/osu.Android.props b/osu.Android.props index c88bea8265..2870696c03 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -8,13 +8,9 @@ true true - manifestmerger.jar - - - - + + + \ No newline at end of file diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs index 3c39a820cc..e5fc354db7 100644 --- a/osu.Android/GameplayScreenRotationLocker.cs +++ b/osu.Android/GameplayScreenRotationLocker.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 Android.Content.PM; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -13,10 +11,10 @@ namespace osu.Android { public partial class GameplayScreenRotationLocker : Component { - private Bindable localUserPlaying; + private Bindable localUserPlaying = null!; [Resolved] - private OsuGameActivity gameActivity { get; set; } + private OsuGameActivity gameActivity { get; set; } = null!; [BackgroundDependencyLoader] private void load(OsuGame game) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index f0a6e4733c..33ffed432e 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.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.Collections.Generic; using System.Linq; @@ -15,6 +13,7 @@ using Android.Graphics; using Android.OS; using Android.Views; using osu.Framework.Android; +using osu.Framework.Extensions.ObjectExtensions; using osu.Game.Database; using Debug = System.Diagnostics.Debug; using Uri = Android.Net.Uri; @@ -51,11 +50,11 @@ namespace osu.Android /// Adjusted on startup to match expected UX for the current device type (phone/tablet). public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified; - private OsuGameAndroid game; + private OsuGameAndroid game = null!; protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this); - protected override void OnCreate(Bundle savedInstanceState) + protected override void OnCreate(Bundle? savedInstanceState) { base.OnCreate(savedInstanceState); @@ -92,15 +91,15 @@ namespace osu.Android Assembly.Load("osu.Game.Rulesets.Mania"); } - protected override void OnNewIntent(Intent intent) => handleIntent(intent); + protected override void OnNewIntent(Intent? intent) => handleIntent(intent); - private void handleIntent(Intent intent) + private void handleIntent(Intent? intent) { - switch (intent.Action) + switch (intent?.Action) { case Intent.ActionDefault: if (intent.Scheme == ContentResolver.SchemeContent) - handleImportFromUris(intent.Data); + handleImportFromUris(intent.Data.AsNonNull()); else if (osu_url_schemes.Contains(intent.Scheme)) game.HandleLink(intent.DataString); break; @@ -114,7 +113,7 @@ namespace osu.Android { var content = intent.ClipData?.GetItemAt(i); if (content != null) - uris.Add(content.Uri); + uris.Add(content.Uri.AsNonNull()); } handleImportFromUris(uris.ToArray()); diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 0227d2aec2..dea70e6b27 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.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 Android.App; using Microsoft.Maui.Devices; using osu.Framework.Allocation; using osu.Framework.Android.Input; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Input.Handlers; using osu.Framework.Platform; using osu.Game; @@ -32,7 +31,7 @@ namespace osu.Android { get { - var packageInfo = Application.Context.ApplicationContext.PackageManager.GetPackageInfo(Application.Context.ApplicationContext.PackageName, 0); + var packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull(); try { @@ -45,7 +44,7 @@ namespace osu.Android // Basic conversion format (as done in Fastfile): 2020.606.0 -> 202006060 // https://stackoverflow.com/questions/52977079/android-sdk-28-versioncode-in-packageinfo-has-been-deprecated - string versionName = string.Empty; + string versionName; if (OperatingSystem.IsAndroidVersionAtLeast(28)) { @@ -68,7 +67,7 @@ namespace osu.Android { } - return new Version(packageInfo.VersionName); + return new Version(packageInfo.VersionName.AsNonNull()); } } diff --git a/osu.Android/Properties/AndroidManifestOverlay.xml b/osu.Android/Properties/AndroidManifestOverlay.xml deleted file mode 100644 index 815f935383..0000000000 --- a/osu.Android/Properties/AndroidManifestOverlay.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/osu.Android/Properties/AssemblyInfo.cs b/osu.Android/Properties/AssemblyInfo.cs index f65b1b239f..1632087fb1 100644 --- a/osu.Android/Properties/AssemblyInfo.cs +++ b/osu.Android/Properties/AssemblyInfo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Android; using Android.App; diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index fe3e08537e..caf0a1d9fd 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -54,9 +54,6 @@ namespace osu.Desktop client.OnReady += onReady; - // safety measure for now, until we performance test / improve backoff for failed connections. - client.OnConnectionFailed += (_, _) => client.Deinitialize(); - client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network); config.BindWith(OsuSetting.DiscordRichPresence, privacyMode); @@ -187,7 +184,7 @@ namespace osu.Desktop return edit.BeatmapInfo.ToString() ?? string.Empty; case UserActivity.WatchingReplay watching: - return watching.BeatmapInfo.ToString(); + return watching.BeatmapInfo?.ToString() ?? string.Empty; case UserActivity.InLobby lobby: return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value; diff --git a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs index 5d950eef55..d1ac42f22b 100644 --- a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs +++ b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs @@ -75,7 +75,7 @@ namespace osu.Desktop.LegacyIpc case LegacyIpcDifficultyCalculationRequest req: try { - WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile); + WorkingBeatmap beatmap = new FlatWorkingBeatmap(req.BeatmapFile); var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance(); Mod[] mods = ruleset.ConvertFromLegacyMods((LegacyMods)req.Mods).ToArray(); diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index d92fea27bf..a0db896f46 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -2,12 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.Versioning; -using System.Threading.Tasks; using Microsoft.Win32; using osu.Desktop.Security; using osu.Framework.Platform; @@ -17,9 +15,9 @@ using osu.Framework; using osu.Framework.Logging; using osu.Game.Updater; using osu.Desktop.Windows; -using osu.Framework.Threading; using osu.Game.IO; using osu.Game.IPC; +using osu.Game.Online.Multiplayer; using osu.Game.Utils; using SDL2; @@ -111,6 +109,25 @@ namespace osu.Desktop } } + public override bool RestartAppWhenExited() + { + switch (RuntimeInfo.OS) + { + case RuntimeInfo.Platform.Windows: + Debug.Assert(OperatingSystem.IsWindows()); + + // Of note, this is an async method in squirrel that adds an arbitrary delay before returning + // likely to ensure the external process is in a good state. + // + // We're not waiting on that here, but the outro playing before the actual exit should be enough + // to cover this. + Squirrel.UpdateManager.RestartAppWhenExited().FireAndForget(); + return true; + } + + return base.RestartAppWhenExited(); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -130,60 +147,16 @@ namespace osu.Desktop { base.SetHost(host); - var desktopWindow = (SDL2DesktopWindow)host.Window; - var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico"); if (iconStream != null) - desktopWindow.SetIconFromStream(iconStream); + host.Window.SetIconFromStream(iconStream); - desktopWindow.CursorState |= CursorState.Hidden; - desktopWindow.Title = Name; - desktopWindow.DragDrop += f => - { - // on macOS, URL associations are handled via SDL_DROPFILE events. - if (f.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) - { - HandleLink(f); - return; - } - - fileDrop(new[] { f }); - }; + host.Window.CursorState |= CursorState.Hidden; + host.Window.Title = Name; } protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo(); - private readonly List importableFiles = new List(); - private ScheduledDelegate? importSchedule; - - private void fileDrop(string[] filePaths) - { - lock (importableFiles) - { - importableFiles.AddRange(filePaths); - - Logger.Log($"Adding {filePaths.Length} files for import"); - - // File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms. - // In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch. - importSchedule?.Cancel(); - importSchedule = Scheduler.AddDelayed(handlePendingImports, 100); - } - } - - private void handlePendingImports() - { - lock (importableFiles) - { - Logger.Log($"Handling batch import of {importableFiles.Count} files"); - - string[] paths = importableFiles.ToArray(); - importableFiles.Clear(); - - Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning); - } - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 5a1373e040..a33e845f5b 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -85,7 +85,7 @@ namespace osu.Desktop } } - using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = !tournamentClient })) { if (!host.IsPrimaryInstance) { diff --git a/osu.Desktop/app.manifest b/osu.Desktop/app.manifest deleted file mode 100644 index a11cee132c..0000000000 --- a/osu.Desktop/app.manifest +++ /dev/null @@ -1,21 +0,0 @@ - - - - 1 - - - - - - - - - - - - - - true - - - diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index f1b9c92429..1d43e118a3 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -8,7 +8,6 @@ osu! osu!(lazer) lazer.ico - app.manifest 0.0.0 0.0.0 @@ -27,7 +26,7 @@ - + diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index 4719d54138..5de21a68d0 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -7,9 +7,9 @@ - + - + diff --git a/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml index bf7c0bfeca..52b34959b9 100644 --- a/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml +++ b/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests.Android/MainActivity.cs b/osu.Game.Rulesets.Catch.Tests.Android/MainActivity.cs index 64c71c9ecd..d8b729576d 100644 --- a/osu.Game.Rulesets.Catch.Tests.Android/MainActivity.cs +++ b/osu.Game.Rulesets.Catch.Tests.Android/MainActivity.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 Android.App; using osu.Framework.Android; using osu.Game.Tests; diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist index 5ace6c07f5..f87043e1d1 100644 --- a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist @@ -5,7 +5,7 @@ CFBundleName osu.Game.Rulesets.Catch.Tests.iOS CFBundleIdentifier - ppy.osu-Game-Rulesets-Catch-Tests-iOS + sh.ppy.catch-ruleset-tests CFBundleShortVersionString 1.0 CFBundleVersion @@ -42,4 +42,4 @@ CADisableMinimumFrameDurationOnPhone - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs index b6cb351c1e..baca8166d1 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.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.Collections.Generic; using Newtonsoft.Json; diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs index cf030f6e13..880316f177 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty; diff --git a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs index b9d6f28228..dacfd649ef 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.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; using NUnit.Framework; using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Rulesets.Mods; using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Catch.Tests @@ -26,7 +25,8 @@ namespace osu.Game.Rulesets.Catch.Tests new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } }, new object[] { LegacyMods.Flashlight, new[] { typeof(CatchModFlashlight) } }, new object[] { LegacyMods.Autoplay, new[] { typeof(CatchModAutoplay) } }, - new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } } + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } }, + new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } }, }; [TestCaseSource(nameof(catch_mod_mapping))] diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs index f30b216d8d..72011042bc 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.IO.Stores; using osu.Game.Rulesets.Catch.Skinning; diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs index 2af851a561..4306cc7d9d 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Catch.Tests diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchEditorTestSceneContainer.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchEditorTestSceneContainer.cs index 39508359a4..058d4eb6b9 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchEditorTestSceneContainer.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchEditorTestSceneContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs index 033dca587e..6dfc74e75c 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs index 2db4102513..ed37ff4ef3 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Testing; diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchDistanceSnapGrid.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchDistanceSnapGrid.cs index 1e057cf3fb..8052b8e3f7 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchDistanceSnapGrid.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs index 5593f3d319..c9ba127569 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Tests.Visual; diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs index 93b24d92fb..75d3c3753a 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Utils; diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs index 2426f8c886..d010bb02ad 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1])); AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0])); AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1])); - AddAssert("default slider velocity", () => lastObject.SliderVelocityBindable.IsDefault); + AddAssert("default slider velocity", () => lastObject.SliderVelocityMultiplierBindable.IsDefault); } [Test] @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor addPlacementSteps(times, positions); addPathCheckStep(times, positions); - AddAssert("slider velocity changed", () => !lastObject.SliderVelocityBindable.IsDefault); + AddAssert("slider velocity changed", () => !lastObject.SliderVelocityMultiplierBindable.IsDefault); } [Test] diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs index beba5811fe..05d7a38a95 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs @@ -108,11 +108,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor double[] times = { 100, 300 }; float[] positions = { 200, 300 }; addBlueprintStep(times, positions); - AddAssert("default slider velocity", () => hitObject.SliderVelocityBindable.IsDefault); + AddAssert("default slider velocity", () => hitObject.SliderVelocityMultiplierBindable.IsDefault); addDragStartStep(times[1], positions[1]); AddMouseMoveStep(times[1], 400); - AddAssert("slider velocity changed", () => !hitObject.SliderVelocityBindable.IsDefault); + AddAssert("slider velocity changed", () => !hitObject.SliderVelocityMultiplierBindable.IsDefault); } [Test] diff --git a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs index 0de992c1df..95b4fdc07e 100644 --- a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModFloatingFruits.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModFloatingFruits.cs new file mode 100644 index 0000000000..73579d1c22 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModFloatingFruits.cs @@ -0,0 +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 NUnit.Framework; +using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Catch.Tests.Mods +{ + public partial class TestSceneCatchModFloatingFruits : ModTestScene + { + protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); + + [Test] + public void TestFloating() => CreateModTest(new ModTestData + { + Mod = new CatchModFloatingFruits(), + PassCondition = () => true + }); + } +} diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail@2x.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-0@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-0.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-0@2x.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-0.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-1@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-1.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-1@2x.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-1.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-2@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-2.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-2@2x.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-2.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-3@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-3.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-3@2x.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-3.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-4@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-4.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-4@2x.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-4.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-5@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-5.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-5@2x.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-5.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai@2x.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai.png diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs index f21825668f..3261fb656e 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.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.Collections.Generic; using System.Linq; using osu.Game.Audio; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs index 402f8f548d..569c69a633 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs index 05d3361dc3..a44575a46e 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; using NUnit.Framework; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs index 01cce88d9d..a82edc1df8 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.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 NUnit.Framework; using osu.Game.Tests.Visual; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs index 4c1ba33aa2..5406230359 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Extensions.IEnumerableExtensions; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs index cbf900ebc0..1d2ea4610d 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs index 75ab4ad9d2..e2fc31d869 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Catch.Tests private float getCaughtObjectPosition(Fruit fruit) { var caughtObject = catcher.ChildrenOfType().Single(c => c.HitObject == fruit); - return caughtObject.Parent.ToSpaceOfOtherDrawable(caughtObject.Position, catcher).X; + return caughtObject.Parent!.ToSpaceOfOtherDrawable(caughtObject.Position, catcher).X; } private void catchFruit(Fruit fruit, float x) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs index c8979381fe..af38956002 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs index 007f309f3f..23fcd49863 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Rulesets.Catch.Mods; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index 223c4e57fc..fda4136a37 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.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 NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs index 995daaceb1..8e7f77285c 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; @@ -28,6 +26,8 @@ namespace osu.Game.Rulesets.Catch.Tests AddSliderStep("start time", 500, 600, 0, x => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = x; + drawableFruit.RefreshStateTransforms(); + drawableBanana.RefreshStateTransforms(); }); } @@ -46,6 +46,8 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("Initialize start time", () => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; + drawableFruit.RefreshStateTransforms(); + drawableBanana.RefreshStateTransforms(); fruitRotation = drawableFruit.DisplayRotation; bananaRotation = drawableBanana.DisplayRotation; @@ -56,6 +58,8 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("change start time", () => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = another_start_time; + drawableFruit.RefreshStateTransforms(); + drawableBanana.RefreshStateTransforms(); }); AddAssert("fruit rotation is changed", () => drawableFruit.DisplayRotation != fruitRotation); @@ -66,6 +70,8 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("reset start time", () => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; + drawableFruit.RefreshStateTransforms(); + drawableBanana.RefreshStateTransforms(); }); AddAssert("rotation and size restored", () => diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs index 4b2873e0a8..1534d91e77 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.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 osu.Framework.Bindables; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index f8c43a221e..3c222662f5 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.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 NUnit.Framework; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs index c91f07891c..c31a7ca99f 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Beatmaps; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs index aa66fc8741..871da28142 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; @@ -21,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests public partial class TestSceneLegacyBeatmapSkin : LegacyBeatmapSkinColourTest { [Resolved] - private AudioManager audio { get; set; } + private AudioManager audio { get; set; } = null!; [BackgroundDependencyLoader] private void load(OsuConfigManager config) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs new file mode 100644 index 0000000000..dfdde0a325 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs @@ -0,0 +1,157 @@ +// 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 NUnit.Framework; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Beatmaps; +using osu.Game.Rulesets.Catch.Judgements; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Scoring; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Tests.Visual.Gameplay; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [TestFixture] + public partial class TestSceneScoring : ScoringTestScene + { + public TestSceneScoring() + : base(supportsNonPerfectJudgements: false) + { + } + + private Bindable scoreMultiplier { get; } = new BindableDouble + { + Default = 4, + Value = 4 + }; + + protected override IBeatmap CreateBeatmap(int maxCombo) + { + var beatmap = new CatchBeatmap(); + for (int i = 0; i < maxCombo; ++i) + beatmap.HitObjects.Add(new Fruit()); + return beatmap; + } + + protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1 { ScoreMultiplier = { BindTarget = scoreMultiplier } }; + + protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo); + + protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new CatchProcessorBasedScoringAlgorithm(beatmap, mode); + + [Test] + public void TestBasicScenarios() + { + AddStep("set up score multiplier", () => + { + scoreMultiplier.BindValueChanged(_ => Rerun()); + }); + AddStep("set max combo to 100", () => MaxCombo.Value = 100); + AddStep("set perfect score", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + }); + AddStep("set score with misses", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + MissLocations.AddRange(new[] { 24d, 49 }); + }); + AddSliderStep("adjust score multiplier", 0, 10, (int)scoreMultiplier.Default, multiplier => scoreMultiplier.Value = multiplier); + } + + private const int base_great = 300; + + private class ScoreV1 : IScoringAlgorithm + { + private int currentCombo; + + public BindableDouble ScoreMultiplier { get; } = new BindableDouble(); + + public void ApplyHit() => applyHitV1(base_great); + + public void ApplyNonPerfect() => throw new NotSupportedException("catch does not have \"non-perfect\" judgements."); + + public void ApplyMiss() => applyHitV1(0); + + private void applyHitV1(int baseScore) + { + if (baseScore == 0) + { + currentCombo = 0; + return; + } + + TotalScore += baseScore; + + // combo multiplier + // ReSharper disable once PossibleLossOfFraction + TotalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * ScoreMultiplier.Value)); + + currentCombo++; + } + + public long TotalScore { get; private set; } + } + + private class ScoreV2 : IScoringAlgorithm + { + private int currentCombo; + private double comboPortion; + + private readonly double comboPortionMax; + + private const double combo_base = 4; + private const int combo_cap = 200; + + public ScoreV2(int maxCombo) + { + for (int i = 0; i < maxCombo; i++) + ApplyHit(); + + comboPortionMax = comboPortion; + + currentCombo = 0; + comboPortion = 0; + } + + public void ApplyHit() => applyHitV2(base_great); + + public void ApplyNonPerfect() => throw new NotSupportedException("catch does not have \"non-perfect\" judgements."); + + private void applyHitV2(int baseScore) + { + comboPortion += baseScore * Math.Min(Math.Max(0.5, Math.Log(++currentCombo, combo_base)), Math.Log(combo_cap, combo_base)); + } + + public void ApplyMiss() + { + currentCombo = 0; + } + + public long TotalScore + => (int)Math.Round(1000000 * comboPortion / comboPortionMax); // vast simplification, as we're not doing ticks here. + } + + private class CatchProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm + { + public CatchProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode) + : base(beatmap, mode) + { + } + + protected override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(); + + protected override JudgementResult CreatePerfectJudgementResult() => new CatchJudgementResult(new Fruit(), new CatchJudgement()) { Type = HitResult.Great }; + + protected override JudgementResult CreateNonPerfectJudgementResult() => throw new NotSupportedException("catch does not have \"non-perfect\" judgements."); + + protected override JudgementResult CreateMissJudgementResult() => new CatchJudgementResult(new Fruit(), new CatchJudgement()) { Type = HitResult.Miss }; + } + } +} diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index 01922b2a96..c45c85833c 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -1,9 +1,9 @@  - + - + WinExe diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 2c8ef9eae0..6a24c26844 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -41,9 +41,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps X = xPositionData?.X ?? 0, NewCombo = comboData?.NewCombo ?? false, ComboOffset = comboData?.ComboOffset ?? 0, - LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0, LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y, - SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1 + SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1 }.Yield(); case IHasDuration endTime: diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 8a0b8250d5..9ceb78893e 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -25,7 +25,10 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.UI; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch @@ -51,7 +54,7 @@ namespace osu.Game.Rulesets.Catch new KeyBinding(InputKey.X, CatchAction.MoveRight), new KeyBinding(InputKey.Right, CatchAction.MoveRight), new KeyBinding(InputKey.Shift, CatchAction.Dash), - new KeyBinding(InputKey.Shift, CatchAction.Dash), + new KeyBinding(InputKey.MouseLeft, CatchAction.Dash), }; public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) @@ -91,6 +94,9 @@ namespace osu.Game.Rulesets.Catch if (mods.HasFlagFast(LegacyMods.Relax)) yield return new CatchModRelax(); + + if (mods.HasFlagFast(LegacyMods.ScoreV2)) + yield return new ModScoreV2(); } public override IEnumerable GetModsFor(ModType type) @@ -140,6 +146,12 @@ namespace osu.Game.Rulesets.Catch new CatchModNoScope(), }; + case ModType.System: + return new Mod[] + { + new ModScoreV2(), + }; + default: return Array.Empty(); } @@ -202,10 +214,24 @@ namespace osu.Game.Rulesets.Catch public int LegacyID => 2; + public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new CatchLegacyScoreSimulator(); + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame(); public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this); public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier(); + + public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) + { + return new[] + { + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }), + }; + } } } diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs index 2d01153f98..5c64643fd4 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs @@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty // Todo: osu!catch should not output star rating in the 'aim' attribute. yield return (ATTRIB_ID_AIM, StarRating); yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate); - yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -36,7 +35,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty StarRating = values[ATTRIB_ID_AIM]; ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; - MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; } } } diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 42cfde268e..b826c1f546 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -38,13 +38,15 @@ namespace osu.Game.Rulesets.Catch.Difficulty // this is the same as osu!, so there's potential to share the implementation... maybe double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; - return new CatchDifficultyAttributes + CatchDifficultyAttributes attributes = new CatchDifficultyAttributes { StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor, Mods = mods, ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0, MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)), }; + + return attributes; } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs new file mode 100644 index 0000000000..746f5713e4 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs @@ -0,0 +1,192 @@ +// 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.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; + +namespace osu.Game.Rulesets.Catch.Difficulty +{ + internal class CatchLegacyScoreSimulator : ILegacyScoreSimulator + { + private int legacyBonusScore; + private int standardisedBonusScore; + private int combo; + + private double scoreMultiplier; + + public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap) + { + IBeatmap baseBeatmap = workingBeatmap.Beatmap; + + int countNormal = 0; + int countSlider = 0; + int countSpinner = 0; + + foreach (HitObject obj in baseBeatmap.HitObjects) + { + switch (obj) + { + case IHasPath: + countSlider++; + break; + + case IHasDuration: + countSpinner++; + break; + + default: + countNormal++; + break; + } + } + + int objectCount = countNormal + countSlider + countSpinner; + + int drainLength = 0; + + if (baseBeatmap.HitObjects.Count > 0) + { + int breakLength = baseBeatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum(); + drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000; + } + + int difficultyPeppyStars = (int)Math.Round( + (baseBeatmap.Difficulty.DrainRate + + baseBeatmap.Difficulty.OverallDifficulty + + baseBeatmap.Difficulty.CircleSize + + Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5); + + scoreMultiplier = difficultyPeppyStars; + + LegacyScoreAttributes attributes = new LegacyScoreAttributes(); + + foreach (var obj in playableBeatmap.HitObjects) + simulateHit(obj, ref attributes); + + attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore; + + return attributes; + } + + private void simulateHit(HitObject hitObject, ref LegacyScoreAttributes attributes) + { + bool increaseCombo = true; + bool addScoreComboMultiplier = false; + + bool isBonus = false; + HitResult bonusResult = HitResult.None; + + int scoreIncrease = 0; + + switch (hitObject) + { + case TinyDroplet: + scoreIncrease = 10; + increaseCombo = false; + break; + + case Droplet: + scoreIncrease = 100; + break; + + case Fruit: + scoreIncrease = 300; + addScoreComboMultiplier = true; + increaseCombo = true; + break; + + case Banana: + scoreIncrease = 1100; + increaseCombo = false; + isBonus = true; + bonusResult = HitResult.LargeBonus; + break; + + case JuiceStream: + foreach (var nested in hitObject.NestedHitObjects) + simulateHit(nested, ref attributes); + return; + + case BananaShower: + foreach (var nested in hitObject.NestedHitObjects) + simulateHit(nested, ref attributes); + return; + } + + if (addScoreComboMultiplier) + { + // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) + attributes.ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier)); + } + + if (isBonus) + { + legacyBonusScore += scoreIncrease; + standardisedBonusScore += Judgement.ToNumericResult(bonusResult); + } + else + attributes.AccuracyScore += scoreIncrease; + + if (increaseCombo) + combo++; + } + + public double GetLegacyScoreMultiplier(IReadOnlyList mods, LegacyBeatmapConversionDifficultyInfo difficulty) + { + bool scoreV2 = mods.Any(m => m is ModScoreV2); + + double multiplier = 1.0; + + foreach (var mod in mods) + { + switch (mod) + { + case CatchModNoFail: + multiplier *= scoreV2 ? 1.0 : 0.5; + break; + + case CatchModEasy: + multiplier *= 0.5; + break; + + case CatchModHalfTime: + case CatchModDaycore: + multiplier *= 0.3; + break; + + case CatchModHidden: + multiplier *= scoreV2 ? 1.0 : 1.06; + break; + + case CatchModHardRock: + multiplier *= 1.12; + break; + + case CatchModDoubleTime: + case CatchModNightcore: + multiplier *= 1.06; + break; + + case CatchModFlashlight: + multiplier *= 1.12; + break; + + case CatchModRelax: + return 0; + } + } + + return multiplier; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs index d2d605a6fe..1a2990e4ac 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs @@ -6,7 +6,6 @@ using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; -using osuTK; namespace osu.Game.Rulesets.Catch.Edit.Blueprints { @@ -24,7 +23,5 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints : base(new THitObject()) { } - - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs index 7a577f8a83..df76bf0a8c 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs @@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components public void UpdateHitObjectFromPath(JuiceStream hitObject) { // The SV setting may need to be changed for the current path. - var svBindable = hitObject.SliderVelocityBindable; + var svBindable = hitObject.SliderVelocityMultiplierBindable; double svToVelocityFactor = hitObject.Velocity / svBindable.Value; double requiredVelocity = path.ComputeRequiredVelocity(); diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBeatSnapGrid.cs b/osu.Game.Rulesets.Catch/Edit/CatchBeatSnapGrid.cs index 6862696b3a..40bd08455f 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchBeatSnapGrid.cs @@ -1,180 +1,19 @@ // 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.Collections.Generic; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Caching; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.UI.Scrolling; -using osu.Game.Screens.Edit; -using osuTK.Graphics; +using osu.Game.Screens.Edit.Compose.Components; namespace osu.Game.Rulesets.Catch.Edit { - /// - /// A grid which displays coloured beat divisor lines in proximity to the selection or placement cursor. - /// - /// - /// This class heavily borrows from osu!mania's implementation (ManiaBeatSnapGrid). - /// If further changes are to be made, they should also be applied there. - /// If the scale of the changes are large enough, abstracting may be a good path. - /// - public partial class CatchBeatSnapGrid : Component + public partial class CatchBeatSnapGrid : BeatSnapGrid { - private const double visible_range = 750; - - /// - /// The range of time values of the current selection. - /// - public (double start, double end)? SelectionTimeRange + protected override IEnumerable GetTargetContainers(HitObjectComposer composer) => new[] { - set - { - if (value == selectionTimeRange) - return; - - selectionTimeRange = value; - lineCache.Invalidate(); - } - } - - [Resolved] - private EditorBeatmap beatmap { get; set; } = null!; - - [Resolved] - private OsuColour colours { get; set; } = null!; - - [Resolved] - private BindableBeatDivisor beatDivisor { get; set; } = null!; - - private readonly Cached lineCache = new Cached(); - - private (double start, double end)? selectionTimeRange; - - private ScrollingHitObjectContainer lineContainer = null!; - - [BackgroundDependencyLoader] - private void load(HitObjectComposer composer) - { - lineContainer = new ScrollingHitObjectContainer(); - - ((CatchPlayfield)composer.Playfield).UnderlayElements.Add(lineContainer); - - beatDivisor.BindValueChanged(_ => createLines(), true); - } - - protected override void Update() - { - base.Update(); - - if (!lineCache.IsValid) - { - lineCache.Validate(); - createLines(); - } - } - - private readonly Stack availableLines = new Stack(); - - private void createLines() - { - foreach (var line in lineContainer.Objects.OfType()) - availableLines.Push(line); - - lineContainer.Clear(); - - if (selectionTimeRange == null) - return; - - var range = selectionTimeRange.Value; - - var timingPoint = beatmap.ControlPointInfo.TimingPointAt(range.start - visible_range); - - double time = timingPoint.Time; - int beat = 0; - - // progress time until in the visible range. - while (time < range.start - visible_range) - { - time += timingPoint.BeatLength / beatDivisor.Value; - beat++; - } - - while (time < range.end + visible_range) - { - var nextTimingPoint = beatmap.ControlPointInfo.TimingPointAt(time); - - // switch to the next timing point if we have reached it. - if (nextTimingPoint.Time > timingPoint.Time) - { - beat = 0; - time = nextTimingPoint.Time; - timingPoint = nextTimingPoint; - } - - Color4 colour = BindableBeatDivisor.GetColourFor( - BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value), colours); - - if (!availableLines.TryPop(out var line)) - line = new DrawableGridLine(); - - line.HitObject.StartTime = time; - line.Colour = colour; - - lineContainer.Add(line); - - beat++; - time += timingPoint.BeatLength / beatDivisor.Value; - } - - // required to update ScrollingHitObjectContainer's cache. - lineContainer.UpdateSubTree(); - - foreach (var line in lineContainer.Objects.OfType()) - { - time = line.HitObject.StartTime; - - if (time >= range.start && time <= range.end) - line.Alpha = 1; - else - { - double timeSeparation = time < range.start ? range.start - time : time - range.end; - line.Alpha = (float)Math.Max(0, 1 - timeSeparation / visible_range); - } - } - } - - private partial class DrawableGridLine : DrawableHitObject - { - public DrawableGridLine() - : base(new HitObject()) - { - RelativeSizeAxes = Axes.X; - Height = 2; - - AddInternal(new Box { RelativeSizeAxes = Axes.Both }); - } - - [BackgroundDependencyLoader] - private void load() - { - Origin = Anchor.BottomLeft; - Anchor = Anchor.BottomLeft; - } - - protected override void UpdateInitialTransforms() - { - // don't perform any fading – we are handling that ourselves. - LifetimeEnd = HitObject.StartTime + visible_range; - } - } + ((CatchPlayfield)composer.Playfield).UnderlayElements + }; } } diff --git a/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs new file mode 100644 index 0000000000..c3103bd204 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs @@ -0,0 +1,26 @@ +// 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.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Catch.Edit +{ + public partial class CatchDistanceSnapProvider : ComposerDistanceSnapProvider + { + protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after) + { + // osu!catch's distance snap implementation is limited, in that a custom spacing cannot be specified. + // Therefore this functionality is not currently used. + // + // The implementation below is probably correct but should be checked if/when exposed via controls. + + float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime()); + float actualDistance = Math.Abs(((CatchHitObject)before).EffectiveX - ((CatchHitObject)after).EffectiveX); + + return actualDistance / expectedDistance; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfieldAdjustmentContainer.cs index 0271005dd1..36aee792a8 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfieldAdjustmentContainer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Edit { base.Update(); - Scale = new Vector2(Math.Min(Parent.ChildSize.X / CatchPlayfield.WIDTH, Parent.ChildSize.Y / CatchPlayfield.HEIGHT)); + Scale = new Vector2(Math.Min(Parent!.ChildSize.X / CatchPlayfield.WIDTH, Parent!.ChildSize.Y / CatchPlayfield.HEIGHT)); Height = 1 / Scale.Y; } } diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index f2877572e8..4172720ada 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -1,14 +1,13 @@ // 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.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; -using osu.Framework.Input; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; @@ -20,27 +19,27 @@ using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose.Components; using osuTK; namespace osu.Game.Rulesets.Catch.Edit { - public partial class CatchHitObjectComposer : DistancedHitObjectComposer + public partial class CatchHitObjectComposer : ScrollingHitObjectComposer, IKeyBindingHandler { private const float distance_snap_radius = 50; private CatchDistanceSnapGrid distanceSnapGrid = null!; - private InputManager inputManager = null!; - - private CatchBeatSnapGrid beatSnapGrid = null!; - private readonly BindableDouble timeRangeMultiplier = new BindableDouble(1) { MinValue = 1, MaxValue = 10, }; + [Cached(typeof(IDistanceSnapProvider))] + protected readonly CatchDistanceSnapProvider DistanceSnapProvider = new CatchDistanceSnapProvider(); + public CatchHitObjectComposer(CatchRuleset ruleset) : base(ruleset) { @@ -49,8 +48,11 @@ namespace osu.Game.Rulesets.Catch.Edit [BackgroundDependencyLoader] private void load() { + AddInternal(DistanceSnapProvider); + DistanceSnapProvider.AttachToToolbox(RightToolbox); + // todo: enable distance spacing once catch supports applying it to its existing distance snap grid implementation. - DistanceSpacingMultiplier.Disabled = true; + DistanceSnapProvider.DistanceSpacingMultiplier.Disabled = true; LayerBelowRuleset.Add(new PlayfieldBorder { @@ -67,61 +69,30 @@ namespace osu.Game.Rulesets.Catch.Edit Catcher.BASE_DASH_SPEED, -Catcher.BASE_DASH_SPEED, Catcher.BASE_WALK_SPEED, -Catcher.BASE_WALK_SPEED, })); - - AddInternal(beatSnapGrid = new CatchBeatSnapGrid()); } - protected override void LoadComplete() - { - base.LoadComplete(); + protected override IEnumerable CreateTernaryButtons() + => base.CreateTernaryButtons() + .Concat(DistanceSnapProvider.CreateTernaryButtons()); - inputManager = GetContainingInputManager(); - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - if (BlueprintContainer.CurrentTool is SelectTool) + protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) => + new DrawableCatchEditorRuleset(ruleset, beatmap, mods) { - if (EditorBeatmap.SelectedHitObjects.Any()) - { - beatSnapGrid.SelectionTimeRange = (EditorBeatmap.SelectedHitObjects.Min(h => h.StartTime), EditorBeatmap.SelectedHitObjects.Max(h => h.GetEndTime())); - } - else - beatSnapGrid.SelectionTimeRange = null; - } - else - { - var result = FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position); - if (result.Time is double time) - beatSnapGrid.SelectionTimeRange = (time, time); - else - beatSnapGrid.SelectionTimeRange = null; - } - } + TimeRangeMultiplier = { BindTarget = timeRangeMultiplier, } + }; - protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after) + protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this); + + protected override BeatSnapGrid CreateBeatSnapGrid() => new CatchBeatSnapGrid(); + + protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] { - // osu!catch's distance snap implementation is limited, in that a custom spacing cannot be specified. - // Therefore this functionality is not currently used. - // - // The implementation below is probably correct but should be checked if/when exposed via controls. + new FruitCompositionTool(), + new JuiceStreamCompositionTool(), + new BananaShowerCompositionTool() + }; - float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime()); - float actualDistance = Math.Abs(((CatchHitObject)before).EffectiveX - ((CatchHitObject)after).EffectiveX); - - return actualDistance / expectedDistance; - } - - protected override void Update() - { - base.Update(); - - updateDistanceSnapGrid(); - } - - public override bool OnPressed(KeyBindingPressEvent e) + public bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) { @@ -130,28 +101,19 @@ namespace osu.Game.Rulesets.Catch.Edit // May be worth considering standardising "zoom" behaviour with what the timeline uses (ie. alt-wheel) but that may cause new conflicts. case GlobalAction.IncreaseScrollSpeed: this.TransformBindableTo(timeRangeMultiplier, timeRangeMultiplier.Value - 1, 200, Easing.OutQuint); - break; + return true; case GlobalAction.DecreaseScrollSpeed: this.TransformBindableTo(timeRangeMultiplier, timeRangeMultiplier.Value + 1, 200, Easing.OutQuint); - break; + return true; } - return base.OnPressed(e); + return false; } - protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods = null) => - new DrawableCatchEditorRuleset(ruleset, beatmap, mods) - { - TimeRangeMultiplier = { BindTarget = timeRangeMultiplier, } - }; - - protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] + public void OnReleased(KeyBindingReleaseEvent e) { - new FruitCompositionTool(), - new JuiceStreamCompositionTool(), - new BananaShowerCompositionTool() - }; + } public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) { @@ -171,8 +133,6 @@ namespace osu.Game.Rulesets.Catch.Edit return result; } - protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this); - private PalpableCatchHitObject? getLastSnappableHitObject(double time) { var hitObject = EditorBeatmap.HitObjects.OfType().LastOrDefault(h => h.GetEndTime() < time && !(h is BananaShower)); @@ -213,7 +173,7 @@ namespace osu.Game.Rulesets.Catch.Edit return null; } - double timeAtCursor = ((CatchPlayfield)Playfield).TimeAtScreenSpacePosition(inputManager.CurrentState.Mouse.Position); + double timeAtCursor = ((CatchPlayfield)Playfield).TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position); return getLastSnappableHitObject(timeAtCursor); default: @@ -221,9 +181,16 @@ namespace osu.Game.Rulesets.Catch.Edit } } + protected override void Update() + { + base.Update(); + + updateDistanceSnapGrid(); + } + private void updateDistanceSnapGrid() { - if (DistanceSnapToggle.Value != TernaryState.True) + if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True) { distanceSnapGrid.Hide(); return; diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs index e12181d051..9d88c90576 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.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 osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Rulesets.Catch.Objects; @@ -21,10 +20,8 @@ namespace osu.Game.Rulesets.Catch.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - drawableRuleset.Anchor = Anchor.Centre; - drawableRuleset.Origin = Anchor.Centre; - - drawableRuleset.Scale = new Vector2(1, -1); + drawableRuleset.PlayfieldAdjustmentContainer.Scale = new Vector2(1, -1); + drawableRuleset.PlayfieldAdjustmentContainer.Y = 1 - drawableRuleset.PlayfieldAdjustmentContainer.Y; } } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs index 93eadcc13e..62fded0980 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs @@ -1,6 +1,7 @@ // 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.Rulesets.Mods; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Beatmaps; @@ -16,5 +17,13 @@ namespace osu.Game.Rulesets.Catch.Mods var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor; catchProcessor.HardRockOffsets = true; } + + public override void ApplyToDifficulty(BeatmapDifficulty difficulty) + { + base.ApplyToDifficulty(difficulty); + + difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio. + difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f); + } } } diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index f4bd515995..b9fef6bf8c 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -8,6 +8,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osuTK; @@ -151,7 +152,7 @@ namespace osu.Game.Rulesets.Catch.Objects TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); - Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2; + Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize); } protected override HitWindows CreateHitWindows() => HitWindows.Empty; diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 169e99c90c..fb1a86d8c0 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -28,17 +28,17 @@ namespace osu.Game.Rulesets.Catch.Objects public int RepeatCount { get; set; } - public BindableNumber SliderVelocityBindable { get; } = new BindableDouble(1) + public BindableNumber SliderVelocityMultiplierBindable { get; } = new BindableDouble(1) { Precision = 0.01, MinValue = 0.1, MaxValue = 10 }; - public double SliderVelocity + public double SliderVelocityMultiplier { - get => SliderVelocityBindable.Value; - set => SliderVelocityBindable.Value = value; + get => SliderVelocityMultiplierBindable.Value; + set => SliderVelocityMultiplierBindable.Value = value; } [JsonIgnore] @@ -48,10 +48,10 @@ namespace osu.Game.Rulesets.Catch.Objects private double tickDistanceFactor; [JsonIgnore] - public double Velocity => velocityFactor * SliderVelocity; + public double Velocity => velocityFactor * SliderVelocityMultiplier; [JsonIgnore] - public double TickDistance => tickDistanceFactor * SliderVelocity; + public double TickDistance => tickDistanceFactor * SliderVelocityMultiplier; /// /// The length of one span of this . @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Catch.Objects int nodeIndex = 0; SliderEventDescriptor? lastEvent = null; - foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken)) + foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), cancellationToken)) { // generate tiny droplets since the last point if (lastEvent != null) @@ -104,8 +104,8 @@ namespace osu.Game.Rulesets.Catch.Objects } } - // this also includes LegacyLastTick and this is used for TinyDroplet generation above. - // this means that the final segment of TinyDroplets are increasingly mistimed where LegacyLastTickOffset is being applied. + // this also includes LastTick and this is used for TinyDroplet generation above. + // this means that the final segment of TinyDroplets are increasingly mistimed where LastTick is being applied. lastEvent = e; switch (e.Type) @@ -162,7 +162,5 @@ namespace osu.Game.Rulesets.Catch.Objects public double Distance => Path.Distance; public IList> NodeSamples { get; set; } = new List>(); - - public double? LegacyLastTickOffset { get; set; } } } diff --git a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonCatcher.cs b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonCatcher.cs index 82374085c8..5a788a26fb 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonCatcher.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonCatcher.cs @@ -15,7 +15,11 @@ namespace osu.Game.Rulesets.Catch.Skinning.Argon [BackgroundDependencyLoader] private void load() { - RelativeSizeAxes = Axes.Both; + Anchor = Anchor.TopCentre; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; InternalChildren = new Drawable[] { diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/DefaultCatcher.cs b/osu.Game.Rulesets.Catch/Skinning/Default/DefaultCatcher.cs index 72208b763b..bcd4c73f04 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/DefaultCatcher.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/DefaultCatcher.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Rulesets.Catch.UI; +using osuTK; namespace osu.Game.Rulesets.Catch.Skinning.Default { @@ -22,6 +23,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default public DefaultCatcher() { + Anchor = Anchor.TopCentre; RelativeSizeAxes = Axes.Both; InternalChild = sprite = new Sprite { @@ -32,6 +34,15 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default }; } + protected override void Update() + { + base.Update(); + + // matches stable's origin position since we're using the same catcher sprite. + // see LegacyCatcher for more information. + OriginPosition = new Vector2(DrawWidth / 2, 16f); + } + [BackgroundDependencyLoader] private void load(TextureStore store, Bindable currentState) { diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyBananaPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyBananaPiece.cs index 26832b7271..ae530e94fc 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyBananaPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyBananaPiece.cs @@ -2,17 +2,21 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Textures; +using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Catch.Skinning.Legacy { public partial class LegacyBananaPiece : LegacyCatchHitObjectPiece { + private static readonly Vector2 banana_max_size = new Vector2(160); + protected override void LoadComplete() { base.LoadComplete(); - Texture? texture = Skin.GetTexture("fruit-bananas"); - Texture? overlayTexture = Skin.GetTexture("fruit-bananas-overlay"); + Texture? texture = Skin.GetTexture("fruit-bananas")?.WithMaximumSize(banana_max_size); + Texture? overlayTexture = Skin.GetTexture("fruit-bananas-overlay")?.WithMaximumSize(banana_max_size); SetTexture(texture, overlayTexture); } diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs index eba837a52d..f38b9b430e 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs @@ -2,8 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Catch.UI; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy /// /// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter. /// - public partial class LegacyCatchComboCounter : CompositeDrawable, ICatchComboCounter + public partial class LegacyCatchComboCounter : UprightAspectMaintainingContainer, ICatchComboCounter { private readonly LegacyRollingCounter counter; @@ -59,7 +60,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy lastDisplayedCombo = combo; - if (Time.Elapsed < 0) + if ((Clock as IGameplayClock)?.IsRewinding == true) { // needs more work to make rewind somehow look good. // basically we want the previous increment to play... or turning off RemoveCompletedTransforms (not feasible from a performance angle). diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcher.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcher.cs new file mode 100644 index 0000000000..6cd8b14191 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcher.cs @@ -0,0 +1,33 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Skinning.Legacy +{ + public abstract partial class LegacyCatcher : CompositeDrawable + { + protected LegacyCatcher() + { + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + + // in stable, catcher sprites are displayed in their raw size. stable also has catcher sprites displayed with the following scale factors applied: + // 1. 0.5x, affecting all sprites in the playfield, computed here based on lazer's catch playfield dimensions (see WIDTH/HEIGHT constants in CatchPlayfield), + // source: https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/GameplayElements/HitObjectManager.cs#L483-L494 + // 2. 0.7x, a constant scale applied to all catcher sprites on construction. + AutoSizeAxes = Axes.Both; + Scale = new Vector2(0.5f * 0.7f); + } + + protected override void Update() + { + base.Update(); + + // stable sets the Y origin position of the catcher to 16px in order for the catching range and OD scaling to align with the top of the catcher's plate in the default skin. + OriginPosition = new Vector2(DrawWidth / 2, 16f); + } + } +} diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs index f6b2c52498..54d555b22a 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs @@ -7,14 +7,12 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Catch.UI; using osu.Game.Skinning; -using osuTK; namespace osu.Game.Rulesets.Catch.Skinning.Legacy { - public partial class LegacyCatcherNew : CompositeDrawable + public partial class LegacyCatcherNew : LegacyCatcher { [Resolved] private Bindable currentState { get; set; } = null!; @@ -23,25 +21,12 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy private Drawable currentDrawable = null!; - public LegacyCatcherNew() - { - RelativeSizeAxes = Axes.Both; - } - [BackgroundDependencyLoader] private void load(ISkinSource skin) { foreach (var state in Enum.GetValues()) { - AddInternal(drawables[state] = getDrawableFor(state).With(d => - { - d.Anchor = Anchor.TopCentre; - d.Origin = Anchor.TopCentre; - d.RelativeSizeAxes = Axes.Both; - d.Size = Vector2.One; - d.FillMode = FillMode.Fit; - d.Alpha = 0; - })); + AddInternal(drawables[state] = getDrawableFor(state).With(d => d.Alpha = 0)); } currentDrawable = drawables[CatcherAnimationState.Idle]; diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherOld.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherOld.cs index 1e21d8eab1..012200eedf 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherOld.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherOld.cs @@ -3,30 +3,21 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Skinning; -using osuTK; namespace osu.Game.Rulesets.Catch.Skinning.Legacy { - public partial class LegacyCatcherOld : CompositeDrawable + public partial class LegacyCatcherOld : LegacyCatcher { public LegacyCatcherOld() { - RelativeSizeAxes = Axes.Both; + AutoSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load(ISkinSource skin) { - InternalChild = (skin.GetAnimation(@"fruit-ryuuta", true, true, true) ?? Empty()).With(d => - { - d.Anchor = Anchor.TopCentre; - d.Origin = Anchor.TopCentre; - d.RelativeSizeAxes = Axes.Both; - d.Size = Vector2.One; - d.FillMode = FillMode.Fit; - }); + InternalChild = skin.GetAnimation(@"fruit-ryuuta", true, true, true) ?? Empty(); } } } diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs index 7ffd682698..a121d20d3d 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs @@ -2,12 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Textures; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Catch.Skinning.Legacy { public partial class LegacyDropletPiece : LegacyCatchHitObjectPiece { + private static readonly Vector2 droplet_max_size = new Vector2(160); + public LegacyDropletPiece() { Scale = new Vector2(0.8f); @@ -17,8 +20,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy { base.LoadComplete(); - Texture? texture = Skin.GetTexture("fruit-drop"); - Texture? overlayTexture = Skin.GetTexture("fruit-drop-overlay"); + Texture? texture = Skin.GetTexture("fruit-drop")?.WithMaximumSize(droplet_max_size); + Texture? overlayTexture = Skin.GetTexture("fruit-drop-overlay")?.WithMaximumSize(droplet_max_size); SetTexture(texture, overlayTexture); } diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs index 85b60561dd..3a8b5b427a 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs @@ -2,11 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Catch.Skinning.Legacy { internal partial class LegacyFruitPiece : LegacyCatchHitObjectPiece { + private static readonly Vector2 fruit_max_size = new Vector2(160); + protected override void LoadComplete() { base.LoadComplete(); @@ -22,21 +26,26 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy switch (visualRepresentation) { case FruitVisualRepresentation.Pear: - SetTexture(Skin.GetTexture("fruit-pear"), Skin.GetTexture("fruit-pear-overlay")); + setTextures("pear"); break; case FruitVisualRepresentation.Grape: - SetTexture(Skin.GetTexture("fruit-grapes"), Skin.GetTexture("fruit-grapes-overlay")); + setTextures("grapes"); break; case FruitVisualRepresentation.Pineapple: - SetTexture(Skin.GetTexture("fruit-apple"), Skin.GetTexture("fruit-apple-overlay")); + setTextures("apple"); break; case FruitVisualRepresentation.Raspberry: - SetTexture(Skin.GetTexture("fruit-orange"), Skin.GetTexture("fruit-orange-overlay")); + setTextures("orange"); break; } + + void setTextures(string fruitName) => SetTexture( + Skin.GetTexture($"fruit-{fruitName}")?.WithMaximumSize(fruit_max_size), + Skin.GetTexture($"fruit-{fruitName}-overlay")?.WithMaximumSize(fruit_max_size) + ); } } } diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs index 74cbc665c0..11531011ee 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs @@ -17,12 +17,13 @@ namespace osu.Game.Rulesets.Catch.UI public CatchPlayfieldAdjustmentContainer() { - // because we are using centre anchor/origin, we will need to limit visibility in the future - // to ensure tall windows do not get a readability advantage. - // it may be possible to bake the catch-specific offsets (-100..340 mentioned below) into new values - // which are compatible with TopCentre alignment. - Anchor = Anchor.Centre; - Origin = Anchor.Centre; + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + + // playfields in stable are positioned vertically at three fourths the difference between the playfield height and the window height in stable. + // we can match that in lazer by using relative coordinates for Y and considering window height to be 1, and playfield height to be 0.8. + RelativePositionAxes = Axes.Y; + Y = (1 - playfield_size_adjust) / 4 * 3; Size = new Vector2(playfield_size_adjust); @@ -42,18 +43,28 @@ namespace osu.Game.Rulesets.Catch.UI /// private partial class ScalingContainer : Container { + public ScalingContainer() + { + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + } + protected override void Update() { base.Update(); - // in stable, fruit fall vertically from -100 to 340. - // to emulate this, we want to make our playfield 440 gameplay pixels high. - // we then offset it -100 vertically in the position set below. - const float stable_v_offset_ratio = 440 / 384f; + // in stable, fruit fall vertically from 100 pixels above the playfield top down to the catcher's Y position (i.e. -100 to 340), + // see: https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/GameplayElements/HitObjects/Fruits/HitCircleFruits.cs#L65 + // we already have the playfield positioned similar to stable (see CatchPlayfieldAdjustmentContainer constructor), + // so we only need to increase this container's height 100 pixels above the playfield, and offset it to have the bottom at 340 rather than 384. + const float stable_fruit_start_position = -100; + const float stable_catcher_y_position = 340; + const float playfield_v_size_adjustment = (stable_catcher_y_position - stable_fruit_start_position) / CatchPlayfield.HEIGHT; + const float playfield_v_catcher_offset = stable_catcher_y_position - CatchPlayfield.HEIGHT; - Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.WIDTH); - Position = new Vector2(0, -100 * stable_v_offset_ratio + Scale.X); - Size = Vector2.Divide(new Vector2(1, stable_v_offset_ratio), Scale); + Scale = new Vector2(Parent!.ChildSize.X / CatchPlayfield.WIDTH); + Position = new Vector2(0f, playfield_v_catcher_offset * Scale.Y); + Size = Vector2.Divide(new Vector2(1, playfield_v_size_adjustment), Scale); } } } diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index f77dab56c8..0c2c157d10 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Skinning; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -29,6 +30,13 @@ namespace osu.Game.Rulesets.Catch.UI /// /// The size of the catcher at 1x scale. /// + /// + /// This is mainly used to compute catching range, the actual catcher size may differ based on skin implementation and sprite textures. + /// This is also equivalent to the "catcherWidth" property in osu-stable when the game field and beatmap difficulty are set to default values. + /// + /// + /// + /// public const float BASE_SIZE = 106.75f; /// @@ -175,11 +183,6 @@ namespace osu.Game.Rulesets.Catch.UI /// public Drawable CreateProxiedContent() => caughtObjectContainer.CreateProxy(); - /// - /// Calculates the scale of the catcher based off the provided beatmap difficulty. - /// - private static Vector2 calculateScale(IBeatmapDifficultyInfo difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); - /// /// Calculates the width of the area used for attempting catches in gameplay. /// @@ -464,6 +467,11 @@ namespace osu.Game.Rulesets.Catch.UI d.Expire(); } + /// + /// Calculates the scale of the catcher based off the provided beatmap difficulty. + /// + private static Vector2 calculateScale(IBeatmapDifficultyInfo difficulty) => new Vector2(LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize) * 2); + private enum DroppedObjectAnimation { Drop, diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 1b99270b65..567c288b47 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Rulesets.Catch.UI @@ -96,7 +97,7 @@ namespace osu.Game.Rulesets.Catch.UI comboDisplay.X = Catcher.X; - if (Time.Elapsed <= 0) + if ((Clock as IGameplayClock)?.IsRewinding == true) { // This is probably a wrong value, but currently the true value is not recorded. // Setting `true` will prevent generation of false-positive after-images (with more false-negatives). diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index 7930a07551..f0a327d7ac 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -21,8 +21,6 @@ namespace osu.Game.Rulesets.Catch.UI { public partial class DrawableCatchRuleset : DrawableScrollingRuleset { - protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Constant; - protected override bool UserScrollSpeedAdjustment => false; public DrawableCatchRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods = null) @@ -30,6 +28,7 @@ namespace osu.Game.Rulesets.Catch.UI { Direction.Value = ScrollingDirection.Down; TimeRange.Value = GetTimeRange(beatmap.Difficulty.ApproachRate); + VisualisationMethod = ScrollVisualisationMethod.Constant; } [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs b/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs index bcc59a5e4f..107b79c88e 100644 --- a/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs +++ b/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs @@ -6,7 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Skinning.Default; using osu.Game.Skinning; -using osuTK; namespace osu.Game.Rulesets.Catch.UI { @@ -26,8 +25,8 @@ namespace osu.Game.Rulesets.Catch.UI : base(new CatchSkinComponentLookup(CatchSkinComponents.Catcher), _ => new DefaultCatcher()) { Anchor = Anchor.TopCentre; - // Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling. - OriginPosition = new Vector2(0.5f, 0.06f) * Catcher.BASE_SIZE; + Origin = Anchor.TopCentre; + CentreComponent = false; } } } diff --git a/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml index 4a1545a423..f5a49210ea 100644 --- a/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml +++ b/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests.Android/MainActivity.cs b/osu.Game.Rulesets.Mania.Tests.Android/MainActivity.cs index 789fc9e22d..518071fd49 100644 --- a/osu.Game.Rulesets.Mania.Tests.Android/MainActivity.cs +++ b/osu.Game.Rulesets.Mania.Tests.Android/MainActivity.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 Android.App; using osu.Framework.Android; using osu.Game.Tests; diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist index ff5dde856e..740036309f 100644 --- a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist @@ -5,7 +5,7 @@ CFBundleName osu.Game.Rulesets.Mania.Tests.iOS CFBundleIdentifier - ppy.osu-Game-Rulesets-Mania-Tests-iOS + sh.ppy.mania-ruleset-tests CFBundleShortVersionString 1.0 CFBundleVersion @@ -42,4 +42,4 @@ CADisableMinimumFrameDurationOnPhone - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs index 2fda012f07..80e1b753ea 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Graphics; @@ -19,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { protected override Container Content => blueprints ?? base.Content; - private readonly Container blueprints; + private readonly Container? blueprints; [Cached(typeof(Playfield))] public Playfield Playfield { get; } diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs index 0a21098d0d..762238be47 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs index 4b332c3faa..b79bcb7682 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Objects; diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs index a65f949cec..c75095237e 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mania.Edit.Blueprints; diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index aca555552f..fbc0ed1785 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; using osu.Framework.Allocation; diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs index a1f4b234c4..8f623d1fc6 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; @@ -25,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor public partial class TestSceneManiaComposeScreen : EditorClockTestScene { [Resolved] - private SkinManager skins { get; set; } + private SkinManager skins { get; set; } = null!; [Cached] private EditorClipboard clipboard = new EditorClipboard(); diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs index 86e87e7486..9d56d31329 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mania.Edit.Blueprints; diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs index 4ae6cb9c7c..7b0171a9ee 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs index c85583c1fd..62591ce4ca 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -14,7 +12,8 @@ namespace osu.Game.Rulesets.Mania.Tests { public abstract partial class ManiaInputTestScene : OsuTestScene { - private readonly Container content; + private readonly Container? content; + protected override Container Content => content ?? base.Content; protected ManiaInputTestScene(int keys) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs index 9dee861e66..cb2abc1595 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.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; using NUnit.Framework; using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mods; using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Mania.Tests @@ -38,7 +37,8 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { LegacyMods.Key3, new[] { typeof(ManiaModKey3) } }, new object[] { LegacyMods.Key2, new[] { typeof(ManiaModKey2) } }, new object[] { LegacyMods.Mirror, new[] { typeof(ManiaModMirror) } }, - new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } } + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } }, + new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } }, }; [TestCaseSource(nameof(mania_mod_mapping))] diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs index 7d1a934456..641631d05e 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Replays; diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaSpecialColumnTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaSpecialColumnTest.cs index 3bd654e75e..ff1f9e6894 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaSpecialColumnTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaSpecialColumnTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Game.Rulesets.Mania.Beatmaps; using NUnit.Framework; diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModConstantSpeed.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModConstantSpeed.cs index dc4f660a45..474430414c 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModConstantSpeed.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModConstantSpeed.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods PassCondition = () => { var hitObject = Player.ChildrenOfType().FirstOrDefault(); - return hitObject?.Dependencies.Get().Algorithm is ConstantScrollAlgorithm; + return hitObject?.Dependencies.Get().Algorithm.Value is ConstantScrollAlgorithm; } }); } diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs new file mode 100644 index 0000000000..c717f03f51 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs @@ -0,0 +1,74 @@ +// 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 NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Replays; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests.Mods +{ + public partial class TestSceneManiaModDoubleTime : ModTestScene + { + private const double offset = 18; + + protected override bool AllowFail => true; + + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + + [Test] + public void TestHitWindowWithoutDoubleTime() => CreateModTest(new ModTestData + { + PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 + && Player.ScoreProcessor.Accuracy.Value == 1 + && Player.ScoreProcessor.TotalScore.Value == 1_000_000, + Autoplay = false, + Beatmap = new Beatmap + { + BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }, + Difficulty = { OverallDifficulty = 10 }, + HitObjects = new List + { + new Note { StartTime = 1000 } + }, + }, + ReplayFrames = new List + { + new ManiaReplayFrame(1000 + offset, ManiaAction.Key1) + } + }); + + [Test] + public void TestHitWindowWithDoubleTime() + { + var doubleTime = new ManiaModDoubleTime(); + + CreateModTest(new ModTestData + { + Mod = doubleTime, + PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 + && Player.ScoreProcessor.Accuracy.Value == 1 + && Player.ScoreProcessor.TotalScore.Value == (long)(1_000_010 * doubleTime.ScoreMultiplier), + Autoplay = false, + Beatmap = new Beatmap + { + BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }, + Difficulty = { OverallDifficulty = 10 }, + HitObjects = new List + { + new Note { StartTime = 1000 } + }, + }, + ReplayFrames = new List + { + new ManiaReplayFrame(1000 + offset, ManiaAction.Key1) + } + }); + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs index 3011a93755..f5117b61af 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs @@ -6,7 +6,6 @@ using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Mods; using osu.Game.Tests.Visual; -using System.Collections.Generic; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mania.Beatmaps; @@ -24,21 +23,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods Assert.False(testBeatmap.HitObjects.OfType().Any()); } - [Test] - public void TestCorrectNoteValues() - { - var testBeatmap = createRawBeatmap(); - var noteValues = new List(testBeatmap.HitObjects.OfType().Count()); - - foreach (HoldNote h in testBeatmap.HitObjects.OfType()) - { - noteValues.Add(ManiaModHoldOff.GetNoteDurationInBeatLength(h, testBeatmap)); - } - - noteValues.Sort(); - Assert.AreEqual(noteValues, new List { 0.125, 0.250, 0.500, 1.000, 2.000 }); - } - [Test] public void TestCorrectObjectCount() { @@ -47,25 +31,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods var rawBeatmap = createRawBeatmap(); var testBeatmap = createModdedBeatmap(); - // Calculate expected number of objects - int expectedObjectCount = 0; - - foreach (ManiaHitObject h in rawBeatmap.HitObjects) - { - // Both notes and hold notes account for at least one object - expectedObjectCount++; - - if (h.GetType() == typeof(HoldNote)) - { - double noteValue = ManiaModHoldOff.GetNoteDurationInBeatLength((HoldNote)h, rawBeatmap); - - if (noteValue >= ManiaModHoldOff.END_NOTE_ALLOW_THRESHOLD) - { - // Should generate an end note if it's longer than the minimum note value - expectedObjectCount++; - } - } - } + // Both notes and hold notes account for at least one object + int expectedObjectCount = rawBeatmap.HitObjects.Count; Assert.That(testBeatmap.HitObjects.Count == expectedObjectCount); } diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-0.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-0.png new file mode 100644 index 0000000000..5044c2d15f Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-0.png differ diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-1.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-1.png new file mode 100644 index 0000000000..0a3d614739 Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-1.png differ diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-2.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-2.png new file mode 100644 index 0000000000..f0aa0b3a02 Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-2.png differ diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-3.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-3.png new file mode 100644 index 0000000000..8f9155b8f7 Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-light-3.png differ diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini index 7c51036d69..3a9d465f8d 100644 --- a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini +++ b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini @@ -4,6 +4,7 @@ Version: 2.5 [Mania] Keys: 4 ColumnLineWidth: 3,1,3,1,1 +LightFramePerSecond: 15 // some skins found in the wild had configuration keys where the @2x suffix was included in the values. // the expected compatibility behaviour is that the presence of the @2x suffix shouldn't change anything // if @2x assets are present. @@ -15,5 +16,6 @@ Hit300: mania/hit300@2x Hit300g: mania/hit300g@2x StageLeft: mania/stage-left StageRight: mania/stage-right +StageLight: mania/stage-light NoteImage0L: LongNoteTailWang NoteImage1L: LongNoteTailWang diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs index 0c55cebf0d..465d4a49f0 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs index 25e120edc5..dd494dfc82 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs index 30bd600d9d..abf01aa4a4 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -60,7 +58,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning IBindable IScrollingInfo.Direction => Direction; IBindable IScrollingInfo.TimeRange { get; } = new Bindable(5000); - IScrollAlgorithm IScrollingInfo.Algorithm { get; } = new ConstantScrollAlgorithm(); + IBindable IScrollingInfo.Algorithm { get; } = new Bindable(new ConstantScrollAlgorithm()); } } } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs index 3881aae22e..47923d0733 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs index 9cccc2dd86..d4bbc8acb6 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs index 2a9727dbd4..c993ba0e0a 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Extensions; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs index 30dd83123d..a0833ff91f 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; using NUnit.Framework; @@ -41,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { c.Add(hitExplosionPools[poolIndex].Get(e => { - e.Apply(new JudgementResult(new HitObject(), runCount % 6 == 0 ? new HoldNoteTickJudgement() : new ManiaJudgement())); + e.Apply(new JudgementResult(new HitObject(), new ManiaJudgement())); e.Anchor = Anchor.Centre; e.Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs index 0b9ca42af8..a9d18ba401 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; using NUnit.Framework; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneNote.cs index d049d88ea8..2c978c1148 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneNote.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneNote.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mania.Objects; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs index f85e303940..29c47ca93a 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs @@ -1,13 +1,12 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI; +using osuTK; namespace osu.Game.Rulesets.Mania.Tests.Skinning { @@ -25,22 +24,35 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning new StageDefinition(2) }; - SetContents(_ => new ManiaPlayfield(stageDefinitions)); + SetContents(_ => new ManiaInputManager(new ManiaRuleset().RulesetInfo, 2) + { + Child = new ManiaPlayfield(stageDefinitions) + }); }); } - [Test] - public void TestDualStages() + [TestCase(2)] + [TestCase(3)] + [TestCase(5)] + public void TestDualStages(int columnCount) { AddStep("create stage", () => { stageDefinitions = new List { - new StageDefinition(2), - new StageDefinition(2) + new StageDefinition(columnCount), + new StageDefinition(columnCount) }; - SetContents(_ => new ManiaPlayfield(stageDefinitions)); + SetContents(_ => new ManiaInputManager(new ManiaRuleset().RulesetInfo, (int)PlayfieldType.Dual + 2 * columnCount) + { + Child = new ManiaPlayfield(stageDefinitions) + { + // bit of a hack to make sure the dual stages fit on screen without overlapping each other. + Size = new Vector2(1.5f), + Scale = new Vector2(1 / 1.5f) + } + }); }); } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs index 25e24929c9..d44a38fdec 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs index 0557a201c8..11c3ab3cd3 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.UI.Components; diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs index 9fdd93bcc9..e3846e8213 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Testing; diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs index b96fab9ec0..cb9fcca5b0 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.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.Collections.Generic; using NUnit.Framework; diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 77db1b0bd8..044ce37832 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -54,7 +54,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); assertNoteJudgement(HitResult.IgnoreMiss); } @@ -73,7 +72,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Perfect); assertNoteJudgement(HitResult.IgnoreHit); } @@ -92,7 +90,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Miss); assertNoteJudgement(HitResult.IgnoreMiss); } @@ -111,7 +108,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); } @@ -129,7 +125,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); } @@ -149,7 +144,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Miss); } @@ -169,7 +163,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Perfect); } @@ -188,10 +181,33 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); } + /// + /// -----[ ]----- + /// xox o + /// + [Test] + public void TestPressAtStartThenReleaseAndImmediatelyRepress() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_head + 1), + new ManiaReplayFrame(time_head + 2, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + // judgement combo offset by perfect bonus judgement. see logic in DrawableNote.CheckForResult. + assertComboAtJudgement(1, 1); + assertTailJudgement(HitResult.Meh); + assertComboAtJudgement(2, 0); + // judgement combo offset by perfect bonus judgement. see logic in DrawableNote.CheckForResult. + assertComboAtJudgement(4, 1); + } + /// /// -----[ ]----- /// xo x o @@ -208,7 +224,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Miss); } @@ -228,7 +243,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Meh); } @@ -246,7 +260,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Miss); } @@ -264,7 +277,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Meh); } @@ -358,7 +370,6 @@ namespace osu.Game.Rulesets.Mania.Tests }, beatmap); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); assertHitObjectJudgement(note, HitResult.Good); @@ -371,7 +382,8 @@ namespace osu.Game.Rulesets.Mania.Tests [Test] public void TestPressAndReleaseJustAfterTailWithNearbyNote() { - Note note; + // Next note within tail lenience + Note note = new Note { StartTime = time_tail + 50 }; var beatmap = new Beatmap { @@ -383,13 +395,7 @@ namespace osu.Game.Rulesets.Mania.Tests Duration = time_tail - time_head, Column = 0, }, - { - // Next note within tail lenience - note = new Note - { - StartTime = time_tail + 50 - } - } + note }, BeatmapInfo = { @@ -405,7 +411,6 @@ namespace osu.Game.Rulesets.Mania.Tests }, beatmap); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); assertHitObjectJudgement(note, HitResult.Great); @@ -425,7 +430,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Meh); } @@ -476,42 +480,6 @@ namespace osu.Game.Rulesets.Mania.Tests .All(j => j.Type.IsHit())); } - [Test] - public void TestHitTailBeforeLastTick() - { - const int tick_rate = 8; - const double tick_spacing = TimingControlPoint.DEFAULT_BEAT_LENGTH / tick_rate; - const double time_last_tick = time_head + tick_spacing * (int)((time_tail - time_head) / tick_spacing - 1); - - var beatmap = new Beatmap - { - HitObjects = - { - new HoldNote - { - StartTime = time_head, - Duration = time_tail - time_head, - Column = 0, - } - }, - BeatmapInfo = - { - Difficulty = new BeatmapDifficulty { SliderTickRate = tick_rate }, - Ruleset = new ManiaRuleset().RulesetInfo - }, - }; - - performTest(new List - { - new ManiaReplayFrame(time_head, ManiaAction.Key1), - new ManiaReplayFrame(time_last_tick - 5) - }, beatmap); - - assertHeadJudgement(HitResult.Perfect); - assertLastTickJudgement(HitResult.LargeTickMiss); - assertTailJudgement(HitResult.Ok); - } - [Test] public void TestZeroLength() { @@ -551,11 +519,8 @@ namespace osu.Game.Rulesets.Mania.Tests private void assertNoteJudgement(HitResult result) => AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type, () => Is.EqualTo(result)); - private void assertTickJudgement(HitResult result) - => AddAssert($"any tick judged as {result}", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Select(j => j.Type), () => Does.Contain(result)); - - private void assertLastTickJudgement(HitResult result) - => AddAssert($"last tick judged as {result}", () => judgementResults.Last(j => j.HitObject is HoldNoteTick).Type, () => Is.EqualTo(result)); + private void assertComboAtJudgement(int judgementIndex, int combo) + => AddAssert($"combo at judgement {judgementIndex} is {combo}", () => judgementResults.ElementAt(judgementIndex).ComboAfterJudgement, () => Is.EqualTo(combo)); private ScoreAccessibleReplayPlayer currentPlayer = null!; diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs index 7021c081b7..36ecbdb098 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Reflection; using NUnit.Framework; using osu.Framework.IO.Stores; diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs index 4e50fd924c..073bef5061 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Extensions.ObjectExtensions; using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.UI; diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs new file mode 100644 index 0000000000..edf866952b --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs @@ -0,0 +1,147 @@ +// 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.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public partial class TestSceneMaximumScore : RateAdjustedBeatmapTestScene + { + private ScoreAccessibleReplayPlayer currentPlayer = null!; + + private List judgementResults = new List(); + + [Test] + public void TestSimultaneousTickAndNote() + { + performTest( + new List + { + new HoldNote + { + StartTime = 1000, + Duration = 2000, + Column = 0, + }, + new Note + { + StartTime = 2000, + Column = 1 + } + }, + new List + { + new ManiaReplayFrame(1000, ManiaAction.Key1), + new ManiaReplayFrame(2000, ManiaAction.Key1, ManiaAction.Key2), + new ManiaReplayFrame(2001, ManiaAction.Key1), + new ManiaReplayFrame(3000) + }); + + AddAssert("all objects perfectly judged", + () => judgementResults.Select(result => result.Type), + () => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult))); + AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_030)); + } + + [Test] + public void TestSimultaneousLongNotes() + { + performTest( + new List + { + new HoldNote + { + StartTime = 1000, + Duration = 2000, + Column = 0, + }, + new HoldNote + { + StartTime = 2000, + Duration = 2000, + Column = 1 + } + }, + new List + { + new ManiaReplayFrame(1000, ManiaAction.Key1), + new ManiaReplayFrame(2000, ManiaAction.Key1, ManiaAction.Key2), + new ManiaReplayFrame(3000, ManiaAction.Key2), + new ManiaReplayFrame(4000) + }); + + AddAssert("all objects perfectly judged", + () => judgementResults.Select(result => result.Type), + () => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult))); + AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_040)); + } + + private void performTest(List hitObjects, List frames) + { + var beatmap = new Beatmap + { + HitObjects = hitObjects, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty { SliderTickRate = 4 }, + Ruleset = new ManiaRuleset().RulesetInfo + }, + }; + + beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f }); + + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(beatmap); + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + }; + + LoadScreen(currentPlayer = p); + judgementResults = new List(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); + } + + private partial class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs index f497c88bcc..2a8dc715f9 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneScoring.cs new file mode 100644 index 0000000000..ae3ea861ea --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneScoring.cs @@ -0,0 +1,175 @@ +// 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 NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Judgements; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Scoring; +using osu.Game.Rulesets.Scoring; +using osu.Game.Tests.Visual.Gameplay; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [TestFixture] + public partial class TestSceneScoring : ScoringTestScene + { + protected override IBeatmap CreateBeatmap(int maxCombo) + { + var beatmap = new ManiaBeatmap(new StageDefinition(5)); + for (int i = 0; i < maxCombo; ++i) + beatmap.HitObjects.Add(new Note()); + return beatmap; + } + + protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1(MaxCombo.Value); + protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo); + protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new ManiaProcessorBasedScoringAlgorithm(beatmap, mode); + + [Test] + public void TestBasicScenarios() + { + AddStep("set max combo to 100", () => MaxCombo.Value = 100); + AddStep("set perfect score", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + }); + AddStep("set score with misses", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + MissLocations.AddRange(new[] { 24d, 49 }); + }); + AddStep("set score with misses and OKs", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + + NonPerfectLocations.AddRange(new[] { 9d, 19, 29, 39, 59, 69, 79, 89, 99 }); + MissLocations.AddRange(new[] { 24d, 49 }); + }); + } + + private class ScoreV1 : IScoringAlgorithm + { + private int currentCombo; + private double comboAddition = 100; + private double totalScoreDouble; + private readonly double scoreMultiplier; + + public ScoreV1(int maxCombo) + { + scoreMultiplier = 500000d / maxCombo; + } + + public void ApplyHit() => applyHitV1(320, add => add + 2, 32); + public void ApplyNonPerfect() => applyHitV1(100, add => add - 24, 8); + public void ApplyMiss() => applyHitV1(0, _ => -56, 0); + + private void applyHitV1(int scoreIncrease, Func comboAdditionFunc, int delta) + { + comboAddition = comboAdditionFunc(comboAddition); + if (currentCombo != 0 && currentCombo % 384 == 0) + comboAddition = 100; + comboAddition = Math.Max(0, Math.Min(comboAddition, 100)); + double scoreIncreaseD = Math.Sqrt(comboAddition) * delta * scoreMultiplier / 320; + + TotalScore = (long)totalScoreDouble; + + scoreIncreaseD += scoreIncrease * scoreMultiplier / 320; + scoreIncrease = (int)scoreIncreaseD; + + TotalScore += scoreIncrease; + totalScoreDouble += scoreIncreaseD; + + if (scoreIncrease > 0) + currentCombo++; + } + + public long TotalScore { get; private set; } + } + + private class ScoreV2 : IScoringAlgorithm + { + private int currentCombo; + private double comboPortion; + private double currentBaseScore; + private double maxBaseScore; + private int currentHits; + + private readonly double comboPortionMax; + private readonly int maxCombo; + + private const double combo_base = 4; + + public ScoreV2(int maxCombo) + { + this.maxCombo = maxCombo; + + for (int i = 0; i < this.maxCombo; i++) + ApplyHit(); + + comboPortionMax = comboPortion; + + currentCombo = 0; + comboPortion = 0; + currentBaseScore = 0; + maxBaseScore = 0; + currentHits = 0; + } + + public void ApplyHit() => applyHitV2(305, 300); + public void ApplyNonPerfect() => applyHitV2(100, 100); + + private void applyHitV2(int hitValue, int baseHitValue) + { + maxBaseScore += 305; + currentBaseScore += hitValue; + comboPortion += baseHitValue * Math.Min(Math.Max(0.5, Math.Log(++currentCombo, combo_base)), Math.Log(400, combo_base)); + + currentHits++; + } + + public void ApplyMiss() + { + currentHits++; + maxBaseScore += 305; + currentCombo = 0; + } + + public long TotalScore + { + get + { + float accuracy = (float)(currentBaseScore / maxBaseScore); + + return (int)Math.Round + ( + 200000 * comboPortion / comboPortionMax + + 800000 * Math.Pow(accuracy, 2 + 2 * accuracy) * ((double)currentHits / maxCombo) + ); + } + } + } + + private class ManiaProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm + { + public ManiaProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode) + : base(beatmap, mode) + { + } + + protected override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(); + + protected override JudgementResult CreatePerfectJudgementResult() => new JudgementResult(new Note(), new ManiaJudgement()) { Type = HitResult.Perfect }; + + protected override JudgementResult CreateNonPerfectJudgementResult() => new JudgementResult(new Note(), new ManiaJudgement()) { Type = HitResult.Ok }; + + protected override JudgementResult CreateMissJudgementResult() => new JudgementResult(new Note(), new ManiaJudgement()) { Type = HitResult.Miss }; + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs index 86499a7c6e..fee3ba3e39 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs @@ -117,18 +117,16 @@ namespace osu.Game.Rulesets.Mania.Tests private void createBarLine(bool major) { - foreach (var stage in stages) + var obj = new BarLine { - var obj = new BarLine - { - StartTime = Time.Current + 2000, - Major = major, - }; + StartTime = Time.Current + 2000, + Major = major, + }; - obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + foreach (var stage in stages) stage.Add(obj); - } } private ScrollingTestContainer createStage(ScrollingDirection direction, ManiaAction action) diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index 027bf60a0c..b991db408c 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -1,9 +1,9 @@  - + - + WinExe diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs index b5655a4579..28cdf8907e 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.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.Collections.Generic; using System.Linq; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 632b7cdcc7..aaef69f119 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Mania.Beatmaps.Patterns; using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy; +using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Utils; using osuTK; @@ -43,39 +44,41 @@ namespace osu.Game.Rulesets.Mania.Beatmaps : base(beatmap, ruleset) { IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo); + TargetColumns = GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap)); - double roundedCircleSize = Math.Round(beatmap.Difficulty.CircleSize); - double roundedOverallDifficulty = Math.Round(beatmap.Difficulty.OverallDifficulty); - - if (IsForCurrentRuleset) + if (IsForCurrentRuleset && TargetColumns > ManiaRuleset.MAX_STAGE_KEYS) { - TargetColumns = GetColumnCountForNonConvert(beatmap.BeatmapInfo); - - if (TargetColumns > ManiaRuleset.MAX_STAGE_KEYS) - { - TargetColumns /= 2; - Dual = true; - } - } - else - { - float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasDuration) / beatmap.HitObjects.Count; - if (percentSliderOrSpinner < 0.2) - TargetColumns = 7; - else if (percentSliderOrSpinner < 0.3 || roundedCircleSize >= 5) - TargetColumns = roundedOverallDifficulty > 5 ? 7 : 6; - else if (percentSliderOrSpinner > 0.6) - TargetColumns = roundedOverallDifficulty > 4 ? 5 : 4; - else - TargetColumns = Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7)); + TargetColumns /= 2; + Dual = true; } originalTargetColumns = TargetColumns; } - public static int GetColumnCountForNonConvert(BeatmapInfo beatmapInfo) + public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty) { - double roundedCircleSize = Math.Round(beatmapInfo.Difficulty.CircleSize); + if (new ManiaRuleset().RulesetInfo.Equals(difficulty.SourceRuleset)) + return GetColumnCountForNonConvert(difficulty); + + double roundedCircleSize = Math.Round(difficulty.CircleSize); + double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty); + + int countSliderOrSpinner = difficulty.TotalObjectCount - difficulty.CircleCount; + float percentSpecialObjects = (float)countSliderOrSpinner / difficulty.TotalObjectCount; + + if (percentSpecialObjects < 0.2) + return 7; + if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5) + return roundedOverallDifficulty > 5 ? 7 : 6; + if (percentSpecialObjects > 0.6) + return roundedOverallDifficulty > 4 ? 5 : 4; + + return Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7)); + } + + public static int GetColumnCountForNonConvert(IBeatmapDifficultyInfo difficulty) + { + double roundedCircleSize = Math.Round(difficulty.CircleSize); return (int)Math.Max(1, roundedCircleSize); } @@ -119,14 +122,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps yield return obj; } - private readonly List prevNoteTimes = new List(max_notes_for_density); + private readonly LimitedCapacityQueue prevNoteTimes = new LimitedCapacityQueue(max_notes_for_density); private double density = int.MaxValue; private void computeDensity(double newNoteTime) { - if (prevNoteTimes.Count == max_notes_for_density) - prevNoteTimes.RemoveAt(0); - prevNoteTimes.Add(newNoteTime); + prevNoteTimes.Enqueue(newNoteTime); if (prevNoteTimes.Count >= 2) density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index 91b7be6e8f..cce0944564 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -10,10 +10,11 @@ using System.Linq; using osu.Framework.Extensions.EnumExtensions; using osu.Game.Audio; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; -using osu.Game.Rulesets.Mania.Objects; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Legacy; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Utils; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy @@ -50,10 +51,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); double beatLength; - if (hitObject.LegacyBpmMultiplier.HasValue) - beatLength = timingPoint.BeatLength * hitObject.LegacyBpmMultiplier.Value; - else if (hitObject is IHasSliderVelocity hasSliderVelocity) - beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity; + + if (hitObject is IHasSliderVelocity hasSliderVelocity) + beatLength = LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(hasSliderVelocity, timingPoint, ManiaRuleset.SHORT_NAME); else beatLength = timingPoint.BeatLength; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs index 630fdf7ae2..2265d3d347 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.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.Collections.Generic; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs index 912cac4fe4..27cb681300 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternType.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternType.cs index bf54dc3179..e4a28167ec 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternType.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternType.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; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs index 931673f337..3d3c35773b 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.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.Collections.Generic; using osu.Game.Rulesets.Objects; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs index 898b558eb3..48b3ce010f 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.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 osu.Game.Rulesets.Mania.UI; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs index d259c2af8e..db60e757e1 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs @@ -24,7 +24,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty foreach (var v in base.ToDatabaseAttributes()) yield return v; - yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); yield return (ATTRIB_ID_DIFFICULTY, StarRating); yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); } @@ -33,7 +32,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty { base.FromDatabaseAttributes(values, onlineInfo); - MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; StarRating = values[ATTRIB_ID_DIFFICULTY]; GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 63e61f17e3..6bb6879052 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; @@ -48,15 +46,17 @@ namespace osu.Game.Rulesets.Mania.Difficulty HitWindows hitWindows = new ManiaHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); - return new ManiaDifficultyAttributes + ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes { StarRating = skills[0].DifficultyValue() * star_scaling_factor, Mods = mods, // In osu-stable mania, rate-adjustment mods don't affect the hit window. // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being. GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate), - MaxCombo = beatmap.HitObjects.Sum(maxComboForObject) + MaxCombo = beatmap.HitObjects.Sum(maxComboForObject), }; + + return attributes; } private static int maxComboForObject(HitObject hitObject) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs new file mode 100644 index 0000000000..ddb4b868a3 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs @@ -0,0 +1,66 @@ +// 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.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring.Legacy; + +namespace osu.Game.Rulesets.Mania.Difficulty +{ + internal class ManiaLegacyScoreSimulator : ILegacyScoreSimulator + { + public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap) + { + return new LegacyScoreAttributes { ComboScore = 1000000 }; + } + + public double GetLegacyScoreMultiplier(IReadOnlyList mods, LegacyBeatmapConversionDifficultyInfo difficulty) + { + bool scoreV2 = mods.Any(m => m is ModScoreV2); + + double multiplier = 1.0; + + foreach (var mod in mods) + { + switch (mod) + { + case ManiaModNoFail: + multiplier *= scoreV2 ? 1.0 : 0.5; + break; + + case ManiaModEasy: + multiplier *= 0.5; + break; + + case ManiaModHalfTime: + case ManiaModDaycore: + multiplier *= 0.5; + break; + } + } + + if (new ManiaRuleset().RulesetInfo.Equals(difficulty.SourceRuleset)) + return multiplier; + + // Apply key mod multipliers. + + int originalColumns = ManiaBeatmapConverter.GetColumnCount(difficulty); + int actualColumns = originalColumns; + + actualColumns = mods.OfType().SingleOrDefault()?.KeyCount ?? actualColumns; + if (mods.Any(m => m is ManiaModDualStages)) + actualColumns *= 2; + + if (actualColumns > originalColumns) + multiplier *= 0.9; + else if (actualColumns < originalColumns) + multiplier *= 0.9 - 0.04 * (originalColumns - actualColumns); + + return multiplier; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs index 01474e6e00..64f8b026c2 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.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.Collections.Generic; using Newtonsoft.Json; using osu.Game.Rulesets.Difficulty; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index 440dec82af..d9f9479247 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.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.Collections.Generic; using System.Linq; diff --git a/osu.Game.Rulesets.Mania/Difficulty/Preprocessing/ManiaDifficultyHitObject.cs b/osu.Game.Rulesets.Mania/Difficulty/Preprocessing/ManiaDifficultyHitObject.cs index df95654319..a67d38b29f 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Preprocessing/ManiaDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Preprocessing/ManiaDifficultyHitObject.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Mania.Objects; diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index 2c7c84de97..a24fcaad8d 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Utils; using osu.Game.Rulesets.Difficulty.Preprocessing; @@ -16,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills { private const double individual_decay_base = 0.125; private const double overall_decay_base = 0.30; - private const double release_threshold = 24; + private const double release_threshold = 30; protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 1; @@ -52,10 +50,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills for (int i = 0; i < endTimes.Length; ++i) { // The current note is overlapped if a previous note or end is overlapping the current note body - isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) && Precision.DefinitelyBigger(endTime, endTimes[i], 1); + isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) && + Precision.DefinitelyBigger(endTime, endTimes[i], 1) && + Precision.DefinitelyBigger(startTime, startTimes[i], 1); // We give a slight bonus to everything if something is held meanwhile - if (Precision.DefinitelyBigger(endTimes[i], endTime, 1)) + if (Precision.DefinitelyBigger(endTimes[i], endTime, 1) && + Precision.DefinitelyBigger(startTime, startTimes[i], 1)) holdFactor = 1.25; closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - endTimes[i])); @@ -72,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills // 0.0 +--------+-+---------------> Release Difference / ms // release_threshold if (isOverlapping) - holdAddition = 1 / (1 + Math.Exp(0.5 * (release_threshold - closestEndTime))); + holdAddition = 1 / (1 + Math.Exp(0.27 * (release_threshold - closestEndTime))); // Decay and increase individualStrains in own column individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base); diff --git a/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs index 262247e244..e9d26b4aa1 100644 --- a/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs +++ b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; using osu.Framework.Input.Bindings; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs index be1cc9a7fe..6a12ec5088 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs index ef7ce9073c..48dde29a9f 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index 381af8be7f..02ad1655b5 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; @@ -23,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints private readonly EditNotePiece tailPiece; [Resolved] - private IScrollingInfo scrollingInfo { get; set; } + private IScrollingInfo scrollingInfo { get; set; } = null!; protected override bool IsValidForPlacement => HitObject.Duration > 0; @@ -46,8 +44,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints if (Column != null) { - headPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.StartTime)).Y; - tailPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.EndTime)).Y; + headPiece.Y = Parent!.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.StartTime)).Y; + tailPiece.Y = Parent!.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.EndTime)).Y; switch (scrollingInfo.Direction.Value) { diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs index cf4bca0030..1ae65dd8c0 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Rulesets.Edit; @@ -17,10 +15,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints where T : ManiaHitObject { [Resolved] - private Playfield playfield { get; set; } + private Playfield playfield { get; set; } = null!; [Resolved] - private IScrollingInfo scrollingInfo { get; set; } + private IScrollingInfo scrollingInfo { get; set; } = null!; protected ScrollingHitObjectContainer HitObjectContainer => ((ManiaPlayfield)playfield).GetColumn(HitObject.Column).HitObjectContainer; @@ -39,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints foreach (var child in InternalChildren) child.Anchor = child.Origin = anchor; - Position = Parent.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition; + Position = Parent!.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition; Width = HitObjectContainer.DrawWidth; } } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs index d77abca350..b3ec3ef3e4 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs index a1392f09fa..01c7bd502a 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.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 osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; diff --git a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs index 4a070e70b4..8d34373f82 100644 --- a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs +++ b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs @@ -1,28 +1,37 @@ // 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.Bindables; using osu.Framework.Graphics; -using osuTK; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; +using osuTK; namespace osu.Game.Rulesets.Mania.Edit { - public partial class DrawableManiaEditorRuleset : DrawableManiaRuleset + public partial class DrawableManiaEditorRuleset : DrawableManiaRuleset, ISupportConstantAlgorithmToggle { + public BindableBool ShowSpeedChanges { get; } = new BindableBool(); + public new IScrollingInfo ScrollingInfo => base.ScrollingInfo; - public DrawableManiaEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) + public DrawableManiaEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods) : base(ruleset, beatmap, mods) { } + protected override void LoadComplete() + { + base.LoadComplete(); + + ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Sequential : ScrollVisualisationMethod.Constant, true); + } + protected override Playfield CreatePlayfield() => new ManiaEditorPlayfield(Beatmap.Stages) { Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs b/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs index 960a08eeeb..99e1ce04b1 100644 --- a/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs +++ b/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 2d4b5f718c..61c730912f 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -1,206 +1,23 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Caching; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.UI.Scrolling; -using osu.Game.Screens.Edit; -using osuTK.Graphics; +using osu.Game.Screens.Edit.Compose.Components; namespace osu.Game.Rulesets.Mania.Edit { - /// - /// A grid which displays coloured beat divisor lines in proximity to the selection or placement cursor. - /// - public partial class ManiaBeatSnapGrid : Component + public partial class ManiaBeatSnapGrid : BeatSnapGrid { - private const double visible_range = 750; - - /// - /// The range of time values of the current selection. - /// - public (double start, double end)? SelectionTimeRange + protected override IEnumerable GetTargetContainers(HitObjectComposer composer) { - set - { - if (value == selectionTimeRange) - return; - - selectionTimeRange = value; - lineCache.Invalidate(); - } - } - - [Resolved] - private EditorBeatmap beatmap { get; set; } - - [Resolved] - private OsuColour colours { get; set; } - - [Resolved] - private BindableBeatDivisor beatDivisor { get; set; } - - private readonly List grids = new List(); - - private readonly Cached lineCache = new Cached(); - - private (double start, double end)? selectionTimeRange; - - [BackgroundDependencyLoader] - private void load(HitObjectComposer composer) - { - foreach (var stage in ((ManiaPlayfield)composer.Playfield).Stages) - { - foreach (var column in stage.Columns) - { - var lineContainer = new ScrollingHitObjectContainer(); - - grids.Add(lineContainer); - column.UnderlayElements.Add(lineContainer); - } - } - - beatDivisor.BindValueChanged(_ => createLines(), true); - } - - protected override void Update() - { - base.Update(); - - if (!lineCache.IsValid) - { - lineCache.Validate(); - createLines(); - } - } - - private readonly Stack availableLines = new Stack(); - - private void createLines() - { - foreach (var grid in grids) - { - foreach (var line in grid.Objects.OfType()) - availableLines.Push(line); - - grid.Clear(); - } - - if (selectionTimeRange == null) - return; - - var range = selectionTimeRange.Value; - - var timingPoint = beatmap.ControlPointInfo.TimingPointAt(range.start - visible_range); - - double time = timingPoint.Time; - int beat = 0; - - // progress time until in the visible range. - while (time < range.start - visible_range) - { - time += timingPoint.BeatLength / beatDivisor.Value; - beat++; - } - - while (time < range.end + visible_range) - { - var nextTimingPoint = beatmap.ControlPointInfo.TimingPointAt(time); - - // switch to the next timing point if we have reached it. - if (nextTimingPoint.Time > timingPoint.Time) - { - beat = 0; - time = nextTimingPoint.Time; - timingPoint = nextTimingPoint; - } - - Color4 colour = BindableBeatDivisor.GetColourFor( - BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value), colours); - - foreach (var grid in grids) - { - if (!availableLines.TryPop(out var line)) - line = new DrawableGridLine(); - - line.HitObject.StartTime = time; - line.Colour = colour; - - grid.Add(line); - } - - beat++; - time += timingPoint.BeatLength / beatDivisor.Value; - } - - foreach (var grid in grids) - { - // required to update ScrollingHitObjectContainer's cache. - grid.UpdateSubTree(); - - foreach (var line in grid.Objects.OfType()) - { - time = line.HitObject.StartTime; - - if (time >= range.start && time <= range.end) - line.Alpha = 1; - else - { - double timeSeparation = time < range.start ? range.start - time : time - range.end; - line.Alpha = (float)Math.Max(0, 1 - timeSeparation / visible_range); - } - } - } - } - - private partial class DrawableGridLine : DrawableHitObject - { - [Resolved] - private IScrollingInfo scrollingInfo { get; set; } - - private readonly IBindable direction = new Bindable(); - - public DrawableGridLine() - : base(new HitObject()) - { - RelativeSizeAxes = Axes.X; - Height = 2; - - AddInternal(new Box { RelativeSizeAxes = Axes.Both }); - } - - [BackgroundDependencyLoader] - private void load() - { - direction.BindTo(scrollingInfo.Direction); - direction.BindValueChanged(onDirectionChanged, true); - } - - private void onDirectionChanged(ValueChangedEvent direction) - { - Origin = Anchor = direction.NewValue == ScrollingDirection.Up - ? Anchor.TopLeft - : Anchor.BottomLeft; - } - - protected override void UpdateInitialTransforms() - { - // don't perform any fading – we are handling that ourselves. - LifetimeEnd = HitObject.StartTime + visible_range; - } + return ((ManiaPlayfield)composer.Playfield) + .Stages + .SelectMany(stage => stage.Columns) + .Select(column => column.UnderlayElements); } } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs index 05d8ccc73f..d0eb8c1e6e 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Objects; @@ -18,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Edit { } - public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) + public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject) { switch (hitObject) { diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaEditorPlayfield.cs b/osu.Game.Rulesets.Mania/Edit/ManiaEditorPlayfield.cs index 0a697ca986..77e372d1d6 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaEditorPlayfield.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaEditorPlayfield.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 osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI; using System.Collections.Generic; diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 5e577a2964..b9db4168f4 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -5,15 +5,12 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit.Compose.Components; @@ -21,35 +18,15 @@ using osuTK; namespace osu.Game.Rulesets.Mania.Edit { - public partial class ManiaHitObjectComposer : HitObjectComposer + public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer { private DrawableManiaEditorRuleset drawableRuleset; - private ManiaBeatSnapGrid beatSnapGrid; - private InputManager inputManager; public ManiaHitObjectComposer(Ruleset ruleset) : base(ruleset) { } - [BackgroundDependencyLoader] - private void load() - { - AddInternal(beatSnapGrid = new ManiaBeatSnapGrid()); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - inputManager = GetContainingInputManager(); - } - - private DependencyContainer dependencies; - - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - public new ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield); public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo; @@ -57,48 +34,20 @@ namespace osu.Game.Rulesets.Mania.Edit protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => Playfield.GetColumnByPosition(screenSpacePosition); - protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) - { + protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) => drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods); - // This is the earliest we can cache the scrolling info to ourselves, before masks are added to the hierarchy and inject it - dependencies.CacheAs(drawableRuleset.ScrollingInfo); - - return drawableRuleset; - } - protected override ComposeBlueprintContainer CreateBlueprintContainer() => new ManiaBlueprintContainer(this); + protected override BeatSnapGrid CreateBeatSnapGrid() => new ManiaBeatSnapGrid(); + protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] { new NoteCompositionTool(), new HoldNoteCompositionTool() }; - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - if (BlueprintContainer.CurrentTool is SelectTool) - { - if (EditorBeatmap.SelectedHitObjects.Any()) - { - beatSnapGrid.SelectionTimeRange = (EditorBeatmap.SelectedHitObjects.Min(h => h.StartTime), EditorBeatmap.SelectedHitObjects.Max(h => h.GetEndTime())); - } - else - beatSnapGrid.SelectionTimeRange = null; - } - else - { - var result = FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position); - if (result.Time is double time) - beatSnapGrid.SelectionTimeRange = (time, time); - else - beatSnapGrid.SelectionTimeRange = null; - } - } - public override string ConvertSelectionToString() => string.Join(',', EditorBeatmap.SelectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}")); } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index 5e6ae9bb11..8fdbada04f 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; @@ -16,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Edit public partial class ManiaSelectionHandler : EditorSelectionHandler { [Resolved] - private HitObjectComposer composer { get; set; } + private HitObjectComposer composer { get; set; } = null!; public override bool HandleMovement(MoveSelectionEvent moveEvent) { diff --git a/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs b/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs index 179f920c2f..08ee05ad3f 100644 --- a/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs +++ b/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs new file mode 100644 index 0000000000..4f983debea --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs @@ -0,0 +1,22 @@ +// 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.Game.Resources.Localisation.Web; +using osu.Game.Screens.Edit.Setup; + +namespace osu.Game.Rulesets.Mania.Edit.Setup +{ + public partial class ManiaDifficultySection : DifficultySection + { + [BackgroundDependencyLoader] + private void load() + { + CircleSizeSlider.Label = BeatmapsetsStrings.ShowStatsCsMania; + CircleSizeSlider.Description = "The number of columns in the beatmap"; + if (CircleSizeSlider.Current is BindableNumber circleSizeFloat) + circleSizeFloat.Precision = 1; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteBodyJudgement.cs similarity index 55% rename from osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs rename to osu.Game.Rulesets.Mania/Judgements/HoldNoteBodyJudgement.cs index 4b94198c4d..6719665cbe 100644 --- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteBodyJudgement.cs @@ -1,14 +1,13 @@ // 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 osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Judgements { - public class HoldNoteTickJudgement : ManiaJudgement + public class HoldNoteBodyJudgement : ManiaJudgement { - public override HitResult MaxResult => HitResult.LargeTickHit; + public override HitResult MaxResult => HitResult.IgnoreHit; + public override HitResult MinResult => HitResult.ComboBreak; } } diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs index 32f9689d7e..2b75bcdc3d 100644 --- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -14,12 +12,6 @@ namespace osu.Game.Rulesets.Mania.Judgements { switch (result) { - case HitResult.LargeTickHit: - return DEFAULT_MAX_HEALTH_INCREASE * 0.1; - - case HitResult.LargeTickMiss: - return -DEFAULT_MAX_HEALTH_INCREASE * 0.1; - case HitResult.Meh: return -DEFAULT_MAX_HEALTH_INCREASE * 0.5; diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index c8832dfdfb..7f8a00bf88 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania public bool Matches(BeatmapInfo beatmapInfo) { - return !keys.HasFilter || (beatmapInfo.Ruleset.OnlineID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo))); + return !keys.HasFilter || (beatmapInfo.Ruleset.OnlineID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo.Difficulty))); } public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) diff --git a/osu.Game.Rulesets.Mania/ManiaInputManager.cs b/osu.Game.Rulesets.Mania/ManiaInputManager.cs index 9ad24d6256..a41e72660b 100644 --- a/osu.Game.Rulesets.Mania/ManiaInputManager.cs +++ b/osu.Game.Rulesets.Mania/ManiaInputManager.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.ComponentModel; using osu.Framework.Allocation; using osu.Framework.Input.Bindings; diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index e8fda3ec80..0055e10ada 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -33,6 +33,7 @@ using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; @@ -157,6 +158,9 @@ namespace osu.Game.Rulesets.Mania if (mods.HasFlagFast(LegacyMods.Mirror)) yield return new ManiaModMirror(); + + if (mods.HasFlagFast(LegacyMods.ScoreV2)) + yield return new ModScoreV2(); } public override LegacyMods ConvertToLegacyMods(Mod[] mods) @@ -285,6 +289,12 @@ namespace osu.Game.Rulesets.Mania new ModAdaptiveSpeed() }; + case ModType.System: + return new Mod[] + { + new ModScoreV2(), + }; + default: return Array.Empty(); } @@ -302,6 +312,8 @@ namespace osu.Game.Rulesets.Mania public int LegacyID => 3; + public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new ManiaLegacyScoreSimulator(); + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame(); public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new ManiaRulesetConfigManager(settings, RulesetInfo); @@ -374,21 +386,11 @@ namespace osu.Game.Rulesets.Mania HitResult.Ok, HitResult.Meh, - HitResult.LargeTickHit, + // HitResult.SmallBonus is used for awarding perfect bonus score but is not included here as + // it would be a bit redundant to show this to the user. }; } - public override LocalisableString GetDisplayNameForHitResult(HitResult result) - { - switch (result) - { - case HitResult.LargeTickHit: - return "hold tick"; - } - - return base.GetDisplayNameForHitResult(result); - } - public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] { new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) @@ -401,7 +403,7 @@ namespace osu.Game.Rulesets.Mania RelativeSizeAxes = Axes.X, Height = 250 }, true), - new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem("Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[] { new AverageHitError(score.HitEvents), new UnstableRate(score.HitEvents) @@ -414,6 +416,8 @@ namespace osu.Game.Rulesets.Mania } public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection(); + + public override DifficultySection CreateEditorDifficultySection() => new ManiaDifficultySection(); } public enum PlayfieldType diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index 065534eec4..30eca0636c 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs @@ -41,6 +41,7 @@ namespace osu.Game.Rulesets.Mania }, new SettingsCheckbox { + Keywords = new[] { "color" }, LabelText = RulesetSettingsStrings.TimingBasedColouring, Current = config.GetBindable(ManiaRulesetSetting.TimingBasedNoteColouring), } diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs index c9ee5af809..44120e16e6 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs @@ -33,5 +33,6 @@ namespace osu.Game.Rulesets.Mania HitExplosion, StageBackground, StageForeground, + BarLine } } diff --git a/osu.Game.Rulesets.Mania/Mods/IManiaRateAdjustmentMod.cs b/osu.Game.Rulesets.Mania/Mods/IManiaRateAdjustmentMod.cs new file mode 100644 index 0000000000..ea01bd4436 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/IManiaRateAdjustmentMod.cs @@ -0,0 +1,47 @@ +// 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.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Scoring; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mania.Mods +{ + /// + /// May be attached to rate-adjustment mods to adjust hit windows adjust relative to gameplay rate. + /// + /// + /// Historically, in osu!mania, hit windows are expected to adjust relative to the gameplay rate such that the real-world hit window remains the same. + /// + public interface IManiaRateAdjustmentMod : IApplicableToDifficulty, IApplicableToHitObject + { + BindableNumber SpeedChange { get; } + + HitWindows HitWindows { get; set; } + + void IApplicableToDifficulty.ApplyToDifficulty(BeatmapDifficulty difficulty) + { + HitWindows = new ManiaHitWindows(SpeedChange.Value); + HitWindows.SetDifficulty(difficulty.OverallDifficulty); + } + + void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject) + { + switch (hitObject) + { + case Note: + hitObject.HitWindows = HitWindows; + break; + + case HoldNote hold: + hold.Head.HitWindows = HitWindows; + hold.Tail.HitWindows = HitWindows; + break; + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs index 66269f5572..d8e6bcd424 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { var maniaRuleset = (DrawableManiaRuleset)drawableRuleset; - maniaRuleset.ScrollMethod = ScrollVisualisationMethod.Constant; + maniaRuleset.VisualisationMethod = ScrollVisualisationMethod.Constant; } } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs index 309393b664..dbe2a9a9fc 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.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 osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModDaycore : ModDaycore + public class ManiaModDaycore : ModDaycore, IManiaRateAdjustmentMod { + public HitWindows HitWindows { get; set; } = new ManiaHitWindows(); } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs index f4b9cf3b88..a841a8ab37 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.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 osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModDoubleTime : ModDoubleTime + public class ManiaModDoubleTime : ModDoubleTime, IManiaRateAdjustmentMod { + public HitWindows HitWindows { get; set; } = new ManiaHitWindows(); } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs index 8d48e3acde..b0fbb11396 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.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 osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModHalfTime : ModHalfTime + public class ManiaModHalfTime : ModHalfTime, IManiaRateAdjustmentMod { + public HitWindows HitWindows { get; set; } = new ManiaHitWindows(); } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs index 2b0098744f..4e6cc4f1d6 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs @@ -29,8 +29,6 @@ namespace osu.Game.Rulesets.Mania.Mods public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert) }; - public const double END_NOTE_ALLOW_THRESHOLD = 0.5; - public void ApplyToBeatmap(IBeatmap beatmap) { var maniaBeatmap = (ManiaBeatmap)beatmap; @@ -46,28 +44,9 @@ namespace osu.Game.Rulesets.Mania.Mods StartTime = h.StartTime, Samples = h.GetNodeSamples(0) }); - - // Don't add an end note if the duration is shorter than the threshold - double noteValue = GetNoteDurationInBeatLength(h, maniaBeatmap); // 1/1, 1/2, 1/4, etc. - - if (noteValue >= END_NOTE_ALLOW_THRESHOLD) - { - newObjects.Add(new Note - { - Column = h.Column, - StartTime = h.EndTime, - Samples = h.GetNodeSamples((h.NodeSamples?.Count - 1) ?? 1) - }); - } } maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType().Concat(newObjects).OrderBy(h => h.StartTime).ToList(); } - - public static double GetNoteDurationInBeatLength(HoldNote holdNote, ManiaBeatmap beatmap) - { - double beatLength = beatmap.ControlPointInfo.TimingPointAt(holdNote.StartTime).BeatLength; - return holdNote.Duration / beatLength; - } } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs index 748725af9f..f64f7ae31a 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs @@ -2,11 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModNightcore : ModNightcore + public class ManiaModNightcore : ModNightcore, IManiaRateAdjustmentMod { + public HitWindows HitWindows { get; set; } = new ManiaHitWindows(); } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs index 09abe8d7f4..bc76c5cfe9 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Mods foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) { HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer; - Container hocParent = (Container)hoc.Parent; + Container hocParent = (Container)hoc.Parent!; hocParent.Remove(hoc, false); hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c => diff --git a/osu.Game.Rulesets.Mania/Objects/BarLine.cs b/osu.Game.Rulesets.Mania/Objects/BarLine.cs index 09a746042b..cf576239ed 100644 --- a/osu.Game.Rulesets.Mania/Objects/BarLine.cs +++ b/osu.Game.Rulesets.Mania/Objects/BarLine.cs @@ -1,6 +1,7 @@ // 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.Bindables; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -8,7 +9,15 @@ namespace osu.Game.Rulesets.Mania.Objects { public class BarLine : ManiaHitObject, IBarLine { - public bool Major { get; set; } + private HitObjectProperty major; + + public Bindable MajorBindable => major.Bindable; + + public bool Major + { + get => major.Value; + set => major.Value = value; + } public override Judgement CreateJudgement() => new IgnoreJudgement(); } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs index 8381b8b24b..25fed1a84c 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs @@ -1,9 +1,11 @@ // 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.Shapes; -using osuTK; +using osu.Game.Rulesets.Mania.Skinning.Default; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -13,45 +15,41 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// public partial class DrawableBarLine : DrawableManiaHitObject { + public readonly Bindable Major = new Bindable(); + + public DrawableBarLine() + : this(null!) + { + } + public DrawableBarLine(BarLine barLine) : base(barLine) { RelativeSizeAxes = Axes.X; - Height = barLine.Major ? 1.7f : 1.2f; + } - AddInternal(new Box + [BackgroundDependencyLoader] + private void load() + { + AddInternal(new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.BarLine), _ => new DefaultBarLine()) { - Name = "Bar line", - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.Both, - Alpha = barLine.Major ? 0.5f : 0.2f + Anchor = Anchor.Centre, + Origin = Anchor.Centre, }); - if (barLine.Major) - { - Vector2 size = new Vector2(22, 6); - const float line_offset = 4; + Major.BindValueChanged(major => Height = major.NewValue ? 1.7f : 1.2f, true); + } - AddInternal(new Circle - { - Name = "Left line", - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreRight, + protected override void OnApply() + { + base.OnApply(); + Major.BindTo(HitObject.MajorBindable); + } - Size = size, - X = -line_offset, - }); - - AddInternal(new Circle - { - Name = "Right line", - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreLeft, - Size = size, - X = line_offset, - }); - } + protected override void OnFree() + { + base.OnFree(); + Major.UnbindFrom(HitObject.MajorBindable); } protected override void UpdateStartTimeStateTransforms() => this.FadeOut(150); diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 3f91328128..3490d50871 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; @@ -34,10 +35,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public DrawableHoldNoteHead Head => headContainer.Child; public DrawableHoldNoteTail Tail => tailContainer.Child; + public DrawableHoldNoteBody Body => bodyContainer.Child; private Container headContainer; private Container tailContainer; - private Container tickContainer; + private Container bodyContainer; private PausableSkinnableSound slidingSample; @@ -59,12 +61,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public double? HoldStartTime { get; private set; } /// - /// Time at which the hold note has been broken, i.e. released too early, resulting in a reduced score. - /// - public double? HoldBrokenTime { get; private set; } - - /// - /// Whether the hold note has been released potentially without having caused a break. + /// Used to decide whether to visually clamp the hold note to the judgement line. /// private double? releaseTime; @@ -102,6 +99,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables headContainer = new Container { RelativeSizeAxes = Axes.Both } } }, + bodyContainer = new Container { RelativeSizeAxes = Axes.Both }, bodyPiece = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.HoldNoteBody), _ => new DefaultBodyPiece { RelativeSizeAxes = Axes.Both, @@ -109,15 +107,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { RelativeSizeAxes = Axes.X }, - tickContainer = new Container { RelativeSizeAxes = Axes.Both }, tailContainer = new Container { RelativeSizeAxes = Axes.Both }, - slidingSample = new PausableSkinnableSound { Looping = true } + slidingSample = new PausableSkinnableSound + { + Looping = true, + MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME, + } }); maskedContents.AddRange(new[] { bodyPiece.CreateProxy(), - tickContainer.CreateProxy(), tailContainer.CreateProxy(), }); } @@ -135,7 +135,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables sizingContainer.Size = Vector2.One; HoldStartTime = null; - HoldBrokenTime = null; releaseTime = null; } @@ -153,8 +152,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables tailContainer.Child = tail; break; - case DrawableHoldNoteTick tick: - tickContainer.Add(tick); + case DrawableHoldNoteBody body: + bodyContainer.Child = body; break; } } @@ -164,7 +163,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables base.ClearNestedHitObjects(); headContainer.Clear(false); tailContainer.Clear(false); - tickContainer.Clear(false); + bodyContainer.Clear(false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) @@ -177,8 +176,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables case HeadNote head: return new DrawableHoldNoteHead(head); - case HoldNoteTick tick: - return new DrawableHoldNoteTick(tick); + case HoldNoteBody body: + return new DrawableHoldNoteBody(body); } return base.CreateNestedHitObject(hitObject); @@ -242,38 +241,38 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2; bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2; - // As the note is being held, adjust the size of the sizing container. This has two effects: - // 1. The contained masking container will mask the body and ticks. - // 2. The head note will move along with the new "head position" in the container. - // - // As per stable, this should not apply for early hits, waiting until the object starts to touch the - // judgement area first. - if (Head.IsHit && releaseTime == null && DrawHeight > 0 && Time.Current >= HitObject.StartTime) + if (Time.Current >= HitObject.StartTime) { - // How far past the hit target this hold note is. - float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y; - sizingContainer.Height = 1 - yOffset / DrawHeight; + // As the note is being held, adjust the size of the sizing container. This has two effects: + // 1. The contained masking container will mask the body and ticks. + // 2. The head note will move along with the new "head position" in the container. + // + // As per stable, this should not apply for early hits, waiting until the object starts to touch the + // judgement area first. + if (Head.IsHit && releaseTime == null && DrawHeight > 0) + { + // How far past the hit target this hold note is. + float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y; + sizingContainer.Height = 1 - yOffset / DrawHeight; + } } + else + sizingContainer.Height = 1; } protected override void CheckForResult(bool userTriggered, double timeOffset) { if (Tail.AllJudged) { - foreach (var tick in tickContainer) - { - if (!tick.Judged) - tick.MissForcefully(); - } - if (Tail.IsHit) ApplyResult(r => r.Type = r.Judgement.MaxResult); else MissForcefully(); } - if (Tail.Judged && !Tail.IsHit) - HoldBrokenTime = Time.Current; + // Make sure that the hold note is fully judged by giving the body a judgement. + if (Tail.AllJudged && !Body.AllJudged) + Body.TriggerResult(Tail.IsHit); } public override void MissForcefully() @@ -293,7 +292,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables return false; // do not run any of this logic when rewinding, as it inverts order of presses/releases. - if (Time.Elapsed < 0) + if ((Clock as IGameplayClock)?.IsRewinding == true) return false; if (CheckHittable?.Invoke(this, Time.Current) == false) @@ -327,22 +326,22 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (e.Action != Action.Value) return; - // Make sure a hold was started - if (HoldStartTime == null) - return; - // do not run any of this logic when rewinding, as it inverts order of presses/releases. - if (Time.Elapsed < 0) + if ((Clock as IGameplayClock)?.IsRewinding == true) return; - Tail.UpdateResult(); - endHold(); + // When our action is released and we are in the middle of a hold, there's a chance that + // the user has released too early (before the tail). + // + // In such a case, we want to record this against the DrawableHoldNoteBody. + if (HoldStartTime != null) + { + Tail.UpdateResult(); + Body.TriggerResult(Tail.IsHit); - // If the key has been released too early, the user should not receive full score for the release - if (!Tail.IsHit) - HoldBrokenTime = Time.Current; - - releaseTime = Time.Current; + endHold(); + releaseTime = Time.Current; + } } private void endHold() diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs new file mode 100644 index 0000000000..1b2efbafdf --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +namespace osu.Game.Rulesets.Mania.Objects.Drawables +{ + public partial class DrawableHoldNoteBody : DrawableManiaHitObject + { + public bool HasHoldBreak => AllJudged && !IsHit; + + public override bool DisplayResult => false; + + public DrawableHoldNoteBody() + : this(null) + { + } + + public DrawableHoldNoteBody(HoldNoteBody hitObject) + : base(hitObject) + { + } + + internal void TriggerResult(bool hit) + { + if (AllJudged) return; + + ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index e7326df07d..79002b3819 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -3,7 +3,6 @@ #nullable disable -using System.Diagnostics; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Rulesets.Scoring; @@ -33,33 +32,19 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public void UpdateResult() => base.UpdateResult(true); - protected override void CheckForResult(bool userTriggered, double timeOffset) - { - Debug.Assert(HitObject.HitWindows != null); - + protected override void CheckForResult(bool userTriggered, double timeOffset) => // Factor in the release lenience - timeOffset /= TailNote.RELEASE_WINDOW_LENIENCE; + base.CheckForResult(userTriggered, timeOffset / TailNote.RELEASE_WINDOW_LENIENCE); - if (!userTriggered) - { - if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(r => r.Type = r.Judgement.MinResult); + protected override HitResult GetCappedResult(HitResult result) + { + // If the head wasn't hit or the hold note was broken, cap the max score to Meh. + bool hasComboBreak = !HoldNote.Head.IsHit || HoldNote.Body.HasHoldBreak; - return; - } + if (result > HitResult.Meh && hasComboBreak) + return HitResult.Meh; - var result = HitObject.HitWindows.ResultFor(timeOffset); - if (result == HitResult.None) - return; - - ApplyResult(r => - { - // If the head wasn't hit or the hold note was broken, cap the max score to Meh. - if (result > HitResult.Meh && (!HoldNote.Head.IsHit || HoldNote.HoldBrokenTime != null)) - result = HitResult.Meh; - - r.Type = result; - }); + return result; } public override bool OnPressed(KeyBindingPressEvent e) => false; // Handled by the hold note diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs deleted file mode 100644 index ce6a83f79f..0000000000 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs +++ /dev/null @@ -1,110 +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 System; -using System.Diagnostics; -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Shapes; - -namespace osu.Game.Rulesets.Mania.Objects.Drawables -{ - /// - /// Visualises a hit object. - /// - public partial class DrawableHoldNoteTick : DrawableManiaHitObject - { - /// - /// References the time at which the user started holding the hold note. - /// - private Func holdStartTime; - - private Container glowContainer; - - public DrawableHoldNoteTick() - : this(null) - { - } - - public DrawableHoldNoteTick(HoldNoteTick hitObject) - : base(hitObject) - { - Anchor = Anchor.TopCentre; - Origin = Anchor.TopCentre; - - RelativeSizeAxes = Axes.X; - } - - [BackgroundDependencyLoader] - private void load() - { - AddInternal(glowContainer = new CircularContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - } - } - }); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - AccentColour.BindValueChanged(colour => - { - glowContainer.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Radius = 2f, - Roundness = 15f, - Colour = colour.NewValue.Opacity(0.3f) - }; - }, true); - } - - protected override void OnApply() - { - base.OnApply(); - - Debug.Assert(ParentHitObject != null); - - var holdNote = (DrawableHoldNote)ParentHitObject; - holdStartTime = () => holdNote.HoldStartTime; - } - - protected override void OnFree() - { - base.OnFree(); - - holdStartTime = null; - } - - protected override void CheckForResult(bool userTriggered, double timeOffset) - { - if (Time.Current < HitObject.StartTime) - return; - - double? startTime = holdStartTime?.Invoke(); - - if (startTime == null || startTime > HitObject.StartTime) - ApplyResult(r => r.Type = r.Judgement.MinResult); - else - ApplyResult(r => r.Type = r.Judgement.MaxResult); - } - } -} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 0819e8401c..c70dfcb761 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -13,6 +13,8 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Skinning.Default; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit; @@ -38,6 +40,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private Drawable headPiece; + private DrawableNotePerfectBonus perfectBonus; + public DrawableNote() : this(null) { @@ -89,7 +93,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) + { + perfectBonus.TriggerResult(false); ApplyResult(r => r.Type = r.Judgement.MinResult); + } + return; } @@ -97,9 +105,23 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (result == HitResult.None) return; + result = GetCappedResult(result); + + perfectBonus.TriggerResult(result == HitResult.Perfect); ApplyResult(r => r.Type = result); } + public override void MissForcefully() + { + perfectBonus.TriggerResult(false); + base.MissForcefully(); + } + + /// + /// Some objects in mania may want to limit the max result. + /// + protected virtual HitResult GetCappedResult(HitResult result) => result; + public virtual bool OnPressed(KeyBindingPressEvent e) { if (e.Action != Action.Value) @@ -115,6 +137,32 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { } + protected override void AddNestedHitObject(DrawableHitObject hitObject) + { + switch (hitObject) + { + case DrawableNotePerfectBonus bonus: + AddInternal(perfectBonus = bonus); + break; + } + } + + protected override void ClearNestedHitObjects() + { + RemoveInternal(perfectBonus, false); + } + + protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) + { + switch (hitObject) + { + case NotePerfectBonus bonus: + return new DrawableNotePerfectBonus(bonus); + } + + return base.CreateNestedHitObject(hitObject); + } + private void updateSnapColour() { if (beatmap == null || HitObject == null) return; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNotePerfectBonus.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNotePerfectBonus.cs new file mode 100644 index 0000000000..70ddb60296 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNotePerfectBonus.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Mania.Objects.Drawables +{ + public partial class DrawableNotePerfectBonus : DrawableManiaHitObject + { + public override bool DisplayResult => false; + + public DrawableNotePerfectBonus() + : this(null!) + { + } + + public DrawableNotePerfectBonus(NotePerfectBonus hitObject) + : base(hitObject) + { + } + + /// + /// Apply a judgement result. + /// + /// Whether this tick was reached. + internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult); + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/HeadNote.cs b/osu.Game.Rulesets.Mania/Objects/HeadNote.cs index e69cc62aed..a2e89ea560 100644 --- a/osu.Game.Rulesets.Mania/Objects/HeadNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HeadNote.cs @@ -3,6 +3,9 @@ namespace osu.Game.Rulesets.Mania.Objects { + /// + /// The head note of a . + /// public class HeadNote : Note { } diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index c367886efe..3f930a310b 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -6,8 +6,6 @@ using System.Collections.Generic; using System.Threading; using osu.Game.Audio; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; @@ -81,27 +79,18 @@ namespace osu.Game.Rulesets.Mania.Objects /// public TailNote Tail { get; private set; } - public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset; - /// - /// The time between ticks of this hold. + /// The body of the hold. + /// This is an invisible and silent object that tracks the holding state of the . /// - private double tickSpacing = 50; + public HoldNoteBody Body { get; private set; } - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) - { - base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - - TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate; - } + public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset; protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { base.CreateNestedHitObjects(cancellationToken); - createTicks(cancellationToken); - AddNested(Head = new HeadNote { StartTime = StartTime, @@ -115,23 +104,12 @@ namespace osu.Game.Rulesets.Mania.Objects Column = Column, Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1), }); - } - private void createTicks(CancellationToken cancellationToken) - { - if (tickSpacing == 0) - return; - - for (double t = StartTime + tickSpacing; t <= EndTime - tickSpacing; t += tickSpacing) + AddNested(Body = new HoldNoteBody { - cancellationToken.ThrowIfCancellationRequested(); - - AddNested(new HoldNoteTick - { - StartTime = t, - Column = Column - }); - } + StartTime = StartTime, + Column = Column + }); } public override Judgement CreateJudgement() => new IgnoreJudgement(); diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs b/osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs new file mode 100644 index 0000000000..47163d0d81 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs @@ -0,0 +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.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Judgements; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mania.Objects +{ + /// + /// The body of a . + /// Mostly a dummy hitobject that provides the judgement for the "holding" state.
+ /// On hit - the hold note was held correctly for the full duration.
+ /// On miss - the hold note was released at some point during its judgement period. + ///
+ public class HoldNoteBody : ManiaHitObject + { + public override Judgement CreateJudgement() => new HoldNoteBodyJudgement(); + protected override HitWindows CreateHitWindows() => HitWindows.Empty; + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs index ebff5cf4e9..25ad6b997d 100644 --- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.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 osu.Framework.Bindables; using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Objects; diff --git a/osu.Game.Rulesets.Mania/Objects/Note.cs b/osu.Game.Rulesets.Mania/Objects/Note.cs index 578b46a7aa..5914132624 100644 --- a/osu.Game.Rulesets.Mania/Objects/Note.cs +++ b/osu.Game.Rulesets.Mania/Objects/Note.cs @@ -1,8 +1,7 @@ // 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.Threading; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Judgements; @@ -14,5 +13,12 @@ namespace osu.Game.Rulesets.Mania.Objects public class Note : ManiaHitObject { public override Judgement CreateJudgement() => new ManiaJudgement(); + + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) + { + base.CreateNestedHitObjects(cancellationToken); + + AddNested(new NotePerfectBonus { StartTime = StartTime }); + } } } diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/NotePerfectBonus.cs similarity index 57% rename from osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs rename to osu.Game.Rulesets.Mania/Objects/NotePerfectBonus.cs index 9117c60dcd..def4c01268 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs +++ b/osu.Game.Rulesets.Mania/Objects/NotePerfectBonus.cs @@ -1,21 +1,20 @@ // 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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Objects { - /// - /// A scoring tick of a hold note. - /// - public class HoldNoteTick : ManiaHitObject + public class NotePerfectBonus : ManiaHitObject { - public override Judgement CreateJudgement() => new HoldNoteTickJudgement(); - + public override Judgement CreateJudgement() => new NotePerfectBonusJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; + + public class NotePerfectBonusJudgement : ManiaJudgement + { + public override HitResult MaxResult => HitResult.SmallBonus; + } } } diff --git a/osu.Game.Rulesets.Mania/Objects/TailNote.cs b/osu.Game.Rulesets.Mania/Objects/TailNote.cs index d6dc25079a..def32880f1 100644 --- a/osu.Game.Rulesets.Mania/Objects/TailNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/TailNote.cs @@ -1,13 +1,14 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Judgements; namespace osu.Game.Rulesets.Mania.Objects { + /// + /// The tail note of a . + /// public class TailNote : Note { /// diff --git a/osu.Game.Rulesets.Mania/Properties/AssemblyInfo.cs b/osu.Game.Rulesets.Mania/Properties/AssemblyInfo.cs index 1bc20f7ef3..ca1f7036c7 100644 --- a/osu.Game.Rulesets.Mania/Properties/AssemblyInfo.cs +++ b/osu.Game.Rulesets.Mania/Properties/AssemblyInfo.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.Runtime.CompilerServices; // We publish our internal attributes to other sub-projects of the framework. diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs index 16f7af0d0a..e63a037ca9 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs index c46a1b5ab6..627f48f391 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs @@ -1,14 +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.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Scoring { public class ManiaHitWindows : HitWindows { + private readonly double multiplier; + + public ManiaHitWindows() + : this(1) + { + } + + public ManiaHitWindows(double multiplier) + { + this.multiplier = multiplier; + } + public override bool IsHitResultAllowed(HitResult result) { switch (result) @@ -24,5 +35,12 @@ namespace osu.Game.Rulesets.Mania.Scoring return false; } + + protected override DifficultyRange[] GetRanges() => base.GetRanges().Select(r => + new DifficultyRange( + r.Result, + r.Min * multiplier, + r.Average * multiplier, + r.Max * multiplier)).ToArray(); } } diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 6292ed75cd..c53f3c3e07 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -2,7 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Scoring @@ -16,6 +21,9 @@ namespace osu.Game.Rulesets.Mania.Scoring { } + protected override IEnumerable EnumerateHitObjects(IBeatmap beatmap) + => base.EnumerateHitObjects(beatmap).OrderBy(ho => ho, JudgementOrderComparer.DEFAULT); + protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) { return 10000 * comboProgress @@ -25,5 +33,29 @@ namespace osu.Game.Rulesets.Mania.Scoring protected override double GetComboScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base)); + + private class JudgementOrderComparer : IComparer + { + public static readonly JudgementOrderComparer DEFAULT = new JudgementOrderComparer(); + + public int Compare(HitObject? x, HitObject? y) + { + if (ReferenceEquals(x, y)) return 0; + if (ReferenceEquals(x, null)) return -1; + if (ReferenceEquals(y, null)) return 1; + + int result = x.GetEndTime().CompareTo(y.GetEndTime()); + if (result != 0) + return result; + + // due to the way input is handled in mania, notes take precedence over ticks in judging order. + if (x is Note && y is not Note) return -1; + if (x is not Note && y is Note) return 1; + + return x is ManiaHitObject maniaX && y is ManiaHitObject maniaY + ? maniaX.Column.CompareTo(maniaY.Column) + : 0; + } + } } } diff --git a/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs index 765fd11dd5..44ffeb5ec2 100644 --- a/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs +++ b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Input.Bindings; diff --git a/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs new file mode 100644 index 0000000000..ef75e9df11 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs @@ -0,0 +1,82 @@ +// 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.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Objects.Drawables; +using osuTK; + +namespace osu.Game.Rulesets.Mania.Skinning.Default +{ + public partial class DefaultBarLine : CompositeDrawable + { + private Bindable major = null!; + + private Drawable mainLine = null!; + private Drawable leftAnchor = null!; + private Drawable rightAnchor = null!; + + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableHitObject) + { + RelativeSizeAxes = Axes.Both; + + // Avoid flickering due to no anti-aliasing of boxes by default. + var edgeSmoothness = new Vector2(0.3f); + + AddInternal(mainLine = new Box + { + Name = "Bar line", + EdgeSmoothness = edgeSmoothness, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Both, + }); + + const float major_extension = 10; + + AddInternal(leftAnchor = new Box + { + Name = "Left anchor", + EdgeSmoothness = edgeSmoothness, + Blending = BlendingParameters.Additive, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreRight, + Width = major_extension, + RelativeSizeAxes = Axes.Y, + Colour = ColourInfo.GradientHorizontal(Colour4.Transparent, Colour4.White), + }); + + AddInternal(rightAnchor = new Box + { + Name = "Right anchor", + EdgeSmoothness = edgeSmoothness, + Blending = BlendingParameters.Additive, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreLeft, + Width = major_extension, + RelativeSizeAxes = Axes.Y, + Colour = ColourInfo.GradientHorizontal(Colour4.White, Colour4.Transparent), + }); + + major = ((DrawableBarLine)drawableHitObject).Major.GetBoundCopy(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + major.BindValueChanged(updateMajor, true); + } + + private void updateMajor(ValueChangedEvent major) + { + mainLine.Alpha = major.NewValue ? 0.5f : 0.2f; + leftAnchor.Alpha = rightAnchor.Alpha = major.NewValue ? mainLine.Alpha * 0.3f : 0; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs index eb51179cea..3e0fe8ed4b 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs @@ -35,10 +35,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default var stage = beatmap.GetStageForColumnIndex(column); - if (stage.IsSpecialColumn(column)) + int columnInStage = column % stage.Columns; + + if (stage.IsSpecialColumn(columnInStage)) return SkinUtils.As(new Bindable(colourSpecial)); - int distanceToEdge = Math.Min(column, (stage.Columns - 1) - column); + int distanceToEdge = Math.Min(columnInStage, (stage.Columns - 1) - columnInStage); return SkinUtils.As(new Bindable(distanceToEdge % 2 == 0 ? colourOdd : colourEven)); } } diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index ef4810c40d..ee274fc45e 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs @@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy direction.BindTo(scrollingInfo.Direction); isHitting.BindTo(holdNote.IsHitting); - bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true).With(d => + bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true, frameLength: 30).With(d => { if (d == null) return; @@ -123,9 +123,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state) { - // ensure that the hold note is also faded out when the head/tail/any tick is missed. - if (state == ArmedState.Miss) - missFadeTime.Value ??= hitObject.HitStateUpdateTime; + switch (hitObject) + { + // Ensure that the hold note is also faded out when the head/tail/body is missed. + // Importantly, we filter out unrelated objects like DrawableNotePerfectBonus. + case DrawableHoldNoteTail: + case DrawableHoldNoteHead: + case DrawableHoldNoteBody: + if (state == ArmedState.Miss) + missFadeTime.Value ??= hitObject.HitStateUpdateTime; + + break; + } } private void onIsHittingChanged(ValueChangedEvent isHitting) @@ -209,7 +218,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy protected override void Update() { base.Update(); - missFadeTime.Value ??= holdNote.HoldBrokenTime; + + if (holdNote.Body.HasHoldBreak) + missFadeTime.Value = holdNote.Body.Result.TimeAbsolute; int scaleDirection = (direction.Value == ScrollingDirection.Down ? 1 : -1); diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyColumnBackground.cs index ab996519a7..914ed79234 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyColumnBackground.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Rulesets.UI.Scrolling; @@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy private readonly IBindable direction = new Bindable(); private Container lightContainer = null!; - private Sprite light = null!; + private Drawable light = null!; public LegacyColumnBackground() { @@ -39,6 +38,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy Color4 lightColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value ?? Color4.White; + int lightFramePerSecond = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LightFramePerSecond)?.Value ?? 60; + InternalChildren = new[] { lightContainer = new Container @@ -46,16 +47,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy Origin = Anchor.BottomCentre, RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Bottom = lightPosition }, - Child = light = new Sprite + Child = light = skin.GetAnimation(lightImage, true, true, frameLength: 1000d / lightFramePerSecond)?.With(l => { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Colour = LegacyColourCompatibility.DisallowZeroAlpha(lightColour), - Texture = skin.GetTexture(lightImage), - RelativeSizeAxes = Axes.X, - Width = 1, - Alpha = 0 - } + l.Anchor = Anchor.BottomCentre; + l.Origin = Anchor.BottomCentre; + l.Colour = LegacyColourCompatibility.DisallowZeroAlpha(lightColour); + l.RelativeSizeAxes = Axes.X; + l.Width = 1; + l.Alpha = 0; + }) ?? Empty(), } }; diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs index 6c56db613c..1ec218644c 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs @@ -7,7 +7,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; @@ -69,9 +68,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy public void Animate(JudgementResult result) { - if (result.Judgement is HoldNoteTickJudgement) - return; - (explosion as IFramedAnimation)?.GotoFrame(0); explosion?.FadeInFromZero(FADE_IN_DURATION) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index f8519beb22..73c521b2ed 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -119,6 +119,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy case ManiaSkinComponents.StageForeground: return new LegacyStageForeground(); + case ManiaSkinComponents.BarLine: + return null; // Not yet implemented. + default: throw new UnsupportedSkinComponentException(lookup); } @@ -135,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy string filename = this.GetManiaSkinConfig(hit_result_mapping[result])?.Value ?? default_hit_result_skin_filenames[result]; - var animation = this.GetAnimation(filename, true, true); + var animation = this.GetAnimation(filename, true, true, frameLength: 1000 / 20d); return animation == null ? null : new LegacyManiaJudgementPiece(result, animation); } diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs index 6c39ffdcc3..aa50aeb130 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs @@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mania.Skinning Lookup = lookup; ColumnIndex = columnIndex; } + + public override string ToString() => $"[{nameof(ManiaSkinConfigurationLookup)} lookup:{Lookup} col:{ColumnIndex}]"; } } diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index f38571a6d3..9489281176 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -109,10 +109,11 @@ namespace osu.Game.Rulesets.Mania.UI TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy()); RegisterPool(10, 50); + RegisterPool(10, 50); RegisterPool(10, 50); RegisterPool(10, 50); RegisterPool(10, 50); - RegisterPool(50, 250); + RegisterPool(10, 50); } private void onSourceChanged() diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index c93be91a84..91e0f2c19b 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.UI; diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs index ef34fc04ee..a5c6f10907 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs index 41b2dba173..2ad6e4f076 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs index e0663e9878..e588951624 100644 --- a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.Skinning.Default; using osu.Game.Rulesets.UI.Scrolling; using osuTK; @@ -150,9 +149,6 @@ namespace osu.Game.Rulesets.Mania.UI // scale roughly in-line with visual appearance of notes Vector2 scale = new Vector2(1, 0.6f); - if (result.Judgement is HoldNoteTickJudgement) - scale *= 0.5f; - this.ScaleTo(scale); largeFaint diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 2d373c0471..9169599798 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -3,7 +3,6 @@ #nullable disable -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -14,7 +13,6 @@ using osu.Framework.Graphics; using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Configuration; using osu.Game.Input.Handlers; using osu.Game.Replays; using osu.Game.Rulesets.Mania.Beatmaps; @@ -52,22 +50,6 @@ namespace osu.Game.Rulesets.Mania.UI protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config; - public ScrollVisualisationMethod ScrollMethod - { - get => scrollMethod; - set - { - if (IsLoaded) - throw new InvalidOperationException($"Can't alter {nameof(ScrollMethod)} after ruleset is already loaded"); - - scrollMethod = value; - } - } - - private ScrollVisualisationMethod scrollMethod = ScrollVisualisationMethod.Sequential; - - protected override ScrollVisualisationMethod VisualisationMethod => scrollMethod; - private readonly Bindable configDirection = new Bindable(); private readonly BindableInt configScrollSpeed = new BindableInt(); private double smoothTimeRange; diff --git a/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs index 74ddceeeef..bbae055b84 100644 --- a/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Judgements; namespace osu.Game.Rulesets.Mania.UI diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index e3ebadc836..314d199944 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Graphics.Primitives; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects; @@ -25,6 +26,22 @@ namespace osu.Game.Rulesets.Mania.UI private readonly List stages = new List(); + public override Quad SkinnableComponentScreenSpaceDrawQuad + { + get + { + RectangleF totalArea = RectangleF.Empty; + + for (int i = 0; i < Stages.Count; ++i) + { + var stageArea = Stages[i].ScreenSpaceDrawQuad.AABBFloat; + totalArea = i == 0 ? stageArea : RectangleF.Union(totalArea, stageArea); + } + + return totalArea; + } + } + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos)); public ManiaPlayfield(List stageDefinitions) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs index d4621ab8f3..1183b616f5 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Game.Rulesets.UI; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs b/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs index 56ac38a737..65dc43af0b 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Replays; diff --git a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs index c39e21bace..b6e8fb7191 100644 --- a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Mania.Objects.Drawables; diff --git a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs index 46cba01771..92f471e36b 100644 --- a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs +++ b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index 879c704450..fa9af6d157 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -136,6 +136,8 @@ namespace osu.Game.Rulesets.Mania.UI columnFlow.SetContentForColumn(i, column); AddNested(column); } + + RegisterPool(50, 200); } private ISkinSource currentSkin; @@ -186,17 +188,13 @@ namespace osu.Game.Rulesets.Mania.UI public override bool Remove(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Remove(h); - public void Add(BarLine barLine) => base.Add(new DrawableBarLine(barLine)); + public void Add(BarLine barLine) => base.Add(barLine); internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) { if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; - // Tick judgements should not display text. - if (judgedObject is DrawableHoldNoteTick) - return; - judgements.Clear(false); judgements.Add(judgementPool.Get(j => { diff --git a/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml index 45d27dda70..ed4725dd94 100644 --- a/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml +++ b/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests.Android/MainActivity.cs b/osu.Game.Rulesets.Osu.Tests.Android/MainActivity.cs index 9b4226d5b6..46c60f06a5 100644 --- a/osu.Game.Rulesets.Osu.Tests.Android/MainActivity.cs +++ b/osu.Game.Rulesets.Osu.Tests.Android/MainActivity.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 Android.App; using osu.Framework.Android; using osu.Game.Tests; diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist index 1e33f2ff16..7f489874e7 100644 --- a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist @@ -5,7 +5,7 @@ CFBundleName osu.Game.Rulesets.Osu.Tests.iOS CFBundleIdentifier - ppy.osu-Game-Rulesets-Osu-Tests-iOS + sh.ppy.osu-ruleset-tests CFBundleShortVersionString 1.0 CFBundleVersion @@ -42,4 +42,4 @@ CADisableMinimumFrameDurationOnPhone - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs index 587bd2de44..a49afd82f3 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index b05c755bfd..8d8386cae1 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); moveMouseToHitObject(1); - AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection")); + AddAssert("merge option available", () => selectionHandler.ContextMenuItems?.Any(o => o.Text.Value == "Merge selection") == true); mergeSelection(); @@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); moveMouseToHitObject(1); - AddAssert("merge option not available", () => selectionHandler.ContextMenuItems.Length > 0 && selectionHandler.ContextMenuItems.All(o => o.Text.Value != "Merge selection")); + AddAssert("merge option not available", () => selectionHandler.ContextMenuItems?.Length > 0 && selectionHandler.ContextMenuItems.All(o => o.Text.Value != "Merge selection")); mergeSelection(); AddAssert("circles not merged", () => circle1 is not null && circle2 is not null && EditorBeatmap.HitObjects.Contains(circle1) && EditorBeatmap.HitObjects.Contains(circle2)); @@ -222,7 +222,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); moveMouseToHitObject(1); - AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection")); + AddAssert("merge option available", () => selectionHandler.ContextMenuItems?.Any(o => o.Text.Value == "Merge selection") == true); mergeSelection(); diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs index 8641663ce8..623cefff6b 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs @@ -25,6 +25,35 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + [Test] + public void TestSelectAfterFadedOut() + { + var slider = new Slider + { + StartTime = 0, + Position = new Vector2(100, 100), + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint(), + new PathControlPoint(new Vector2(100)) + } + } + }; + AddStep("add slider", () => EditorBeatmap.Add(slider)); + + moveMouseToObject(() => slider); + + AddStep("seek after end", () => EditorClock.Seek(750)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("slider not selected", () => EditorBeatmap.SelectedHitObjects.Count == 0); + + AddStep("seek to visible", () => EditorClock.Seek(650)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddUntilStep("slider selected", () => EditorBeatmap.SelectedHitObjects.Single() == slider); + } + [Test] public void TestContextMenuShownCorrectlyForSelectedSlider() { diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index 7579e8077b..b70f932913 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -9,6 +9,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; @@ -46,8 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Cached] private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); - [Cached(typeof(IDistanceSnapProvider))] - private readonly OsuHitObjectComposer snapProvider = new OsuHitObjectComposer(new OsuRuleset()) + private readonly TestHitObjectComposer composer = new TestHitObjectComposer { // Just used for the snap implementation, so let's hide from vision. AlwaysPresent = true, @@ -70,12 +70,19 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor base.Content.Children = new Drawable[] { editorClock = new EditorClock(editorBeatmap), - snapProvider, + new PopoverContainer { Child = composer }, Content }; } - protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies.CacheAs(composer.DistanceSnapProvider); + return dependencies; + } + + protected override Container Content { get; } = new PopoverContainer { RelativeSizeAxes = Axes.Both }; [SetUp] public void Setup() => Schedule(() => @@ -83,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor editorBeatmap.Difficulty.SliderMultiplier = 1; editorBeatmap.ControlPointInfo.Clear(); editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length }); - snapProvider.DistanceSpacingMultiplier.Value = 1; + composer.DistanceSnapProvider.DistanceSpacingMultiplier.Value = 1; Children = new Drawable[] { @@ -115,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [TestCase(0.5f)] public void TestDistanceSpacing(float multiplier) { - AddStep($"set distance spacing = {multiplier}", () => snapProvider.DistanceSpacingMultiplier.Value = multiplier); + AddStep($"set distance spacing = {multiplier}", () => composer.DistanceSnapProvider.DistanceSpacingMultiplier.Value = multiplier); } [Test] @@ -152,7 +159,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [TestCase(2f, beat_length * 2)] public void TestDistanceSpacingAdjust(float multiplier, float expectedDistance) { - AddStep($"Set distance spacing to {multiplier}", () => snapProvider.DistanceSpacingMultiplier.Value = multiplier); + AddStep($"Set distance spacing to {multiplier}", () => composer.DistanceSnapProvider.DistanceSpacingMultiplier.Value = multiplier); AddStep("move mouse to point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2(beat_length, 0) * 2))); assertSnappedDistance(expectedDistance); @@ -185,7 +192,18 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("Ensure cursor is on a grid line", () => { - return grid.ChildrenOfType().Any(p => Precision.AlmostEquals(p.ScreenSpaceDrawQuad.TopRight.X, grid.ToScreenSpace(cursor.LastSnappedPosition).X)); + return grid.ChildrenOfType().Any(ring => + { + // the grid rings are actually slightly _larger_ than the snapping radii. + // this is done such that the snapping radius falls right in the middle of each grid ring thickness-wise, + // but it does however complicate the following calculations slightly. + + // we want to calculate the coordinates of the rightmost point on the grid line, which is in the exact middle of the ring thickness-wise. + // for the X component, we take the entire width of the ring, minus one half of the inner radius (since we want the middle of the line on the right side). + // for the Y component, we just take 0.5f. + var rightMiddleOfGridLine = ring.ToScreenSpace(ring.DrawSize * new Vector2(1 - ring.InnerRadius / 2, 0.5f)); + return Precision.AlmostEquals(rightMiddleOfGridLine.X, grid.ToScreenSpace(cursor.LastSnappedPosition).X); + }); }); } @@ -254,5 +272,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor cursor.Position = LastSnappedPosition = GetSnapPosition.Invoke(inputManager.CurrentState.Mouse.Position); } } + + private partial class TestHitObjectComposer : OsuHitObjectComposer + { + public new IDistanceSnapProvider DistanceSnapProvider => base.DistanceSnapProvider; + + public TestHitObjectComposer() + : base(new OsuRuleset()) + { + } + } } } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs index 41a099e6e9..03ab7ebbf7 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.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 NUnit.Framework; using osu.Game.Tests.Visual; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs index 59146bc05e..d14e593587 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Testing; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs index bb29504ec3..37a109de18 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 37561fda85..c267cd1f63 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep($"move mouse to control point {index}", () => { Vector2 position = slider.Path.ControlPoints[index].Position; - InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position)); + InputManager.MoveMouseTo(visualiser.Pieces[0].Parent!.ToScreenSpace(position)); }); } @@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { MenuItem item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == "Curve type")?.Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText); - item?.Action?.Value(); + item?.Action.Value?.Invoke(); }); } } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs new file mode 100644 index 0000000000..d7dd30d608 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs @@ -0,0 +1,95 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Edit; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Screens.Edit.Components.RadioButtons; +using osu.Game.Tests.Beatmaps; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + public partial class TestScenePreciseRotation : TestSceneOsuEditor + { + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset); + + [Test] + public void TestHotkeyHandling() + { + AddStep("select single circle", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType().First())); + AddStep("press rotate hotkey", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.R); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddUntilStep("no popover present", () => this.ChildrenOfType().Count(), () => Is.Zero); + + AddStep("select first three objects", () => + { + EditorBeatmap.SelectedHitObjects.Clear(); + EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects.Take(3)); + }); + AddStep("press rotate hotkey", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.R); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddUntilStep("popover present", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); + AddStep("press rotate hotkey", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.R); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddUntilStep("no popover present", () => this.ChildrenOfType().Count(), () => Is.Zero); + } + + [Test] + public void TestRotateCorrectness() + { + AddStep("replace objects", () => + { + EditorBeatmap.Clear(); + EditorBeatmap.AddRange(new HitObject[] + { + new HitCircle { Position = new Vector2(100) }, + new HitCircle { Position = new Vector2(200) }, + }); + }); + AddStep("select both circles", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); + AddStep("press rotate hotkey", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.R); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddUntilStep("popover present", getPopover, () => Is.Not.Null); + + AddStep("rotate by 180deg", () => getPopover().ChildrenOfType().Single().Current.Value = "180"); + AddAssert("first object rotated 180deg around playfield centre", + () => EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, + () => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(100))); + AddAssert("second object rotated 180deg around playfield centre", + () => EditorBeatmap.HitObjects.OfType().ElementAt(1).Position, + () => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(200))); + + AddStep("change rotation origin", () => getPopover().ChildrenOfType().ElementAt(1).TriggerClick()); + AddAssert("first object rotated 90deg around selection centre", + () => EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, () => Is.EqualTo(new Vector2(200, 200))); + AddAssert("second object rotated 90deg around selection centre", + () => EditorBeatmap.HitObjects.OfType().ElementAt(1).Position, () => Is.EqualTo(new Vector2(100, 100))); + + PreciseRotationPopover? getPopover() => this.ChildrenOfType().SingleOrDefault(); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs index db9eea4127..408205d6b2 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs @@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep($"move mouse to {relativePosition}", () => { Vector2 position = slider.Position + relativePosition; - InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position)); + InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position)); }); [Test] @@ -331,7 +331,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep($"move mouse to {relativePosition}", () => { Vector2 position = slider.Position + relativePosition; - InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position)); + InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position)); }); } @@ -340,7 +340,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep($"move mouse to control point {index}", () => { Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position; - InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position)); + InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position)); }); } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 7542e00a94..7d29670daa 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -61,6 +61,20 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointType(0, PathType.Linear); } + [Test] + public void TestPlaceWithMouseMovementOutsidePlayfield() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + AddStep("move mouse out of screen", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + Vector2.One)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(2); + assertControlPointType(0, PathType.Linear); + } + [Test] public void TestPlaceNormalControlPoint() { diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs new file mode 100644 index 0000000000..9c5eb83e3c --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs @@ -0,0 +1,110 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Tests.Beatmaps; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + public partial class TestSceneSliderReversal : TestSceneOsuEditor + { + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false); + + private readonly PathControlPoint[][] paths = + { + createPathSegment( + PathType.PerfectCurve, + new Vector2(200, -50), + new Vector2(250, 0) + ), + createPathSegment( + PathType.Linear, + new Vector2(100, 0), + new Vector2(100, 100) + ) + }; + + private static PathControlPoint[] createPathSegment(PathType type, params Vector2[] positions) + { + return positions.Select(p => new PathControlPoint + { + Position = p + }).Prepend(new PathControlPoint + { + Type = type + }).ToArray(); + } + + private Slider selectedSlider => (Slider)EditorBeatmap.SelectedHitObjects[0]; + + [TestCase(0, 250)] + [TestCase(0, 200)] + [TestCase(1, 120)] + [TestCase(1, 80)] + public void TestSliderReversal(int pathIndex, double length) + { + var controlPoints = paths[pathIndex]; + + Vector2 oldStartPos = default; + Vector2 oldEndPos = default; + double oldDistance = default; + var oldControlPointTypes = controlPoints.Select(p => p.Type); + + AddStep("Add slider", () => + { + var slider = new Slider + { + Position = new Vector2(OsuPlayfield.BASE_SIZE.X / 2, OsuPlayfield.BASE_SIZE.Y / 2), + Path = new SliderPath(controlPoints) + { + ExpectedDistance = { Value = length } + } + }; + + EditorBeatmap.Add(slider); + + oldStartPos = slider.Position; + oldEndPos = slider.EndPosition; + oldDistance = slider.Path.Distance; + }); + + AddStep("Select slider", () => + { + var slider = (Slider)EditorBeatmap.HitObjects[0]; + EditorBeatmap.SelectedHitObjects.Add(slider); + }); + + AddStep("Reverse slider", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.G); + InputManager.ReleaseKey(Key.LControl); + }); + + AddAssert("Slider has correct length", () => + Precision.AlmostEquals(selectedSlider.Path.Distance, oldDistance)); + + AddAssert("Slider has correct start position", () => + Vector2.Distance(selectedSlider.Position, oldEndPos) < 1); + + AddAssert("Slider has correct end position", () => + Vector2.Distance(selectedSlider.EndPosition, oldStartPos) < 1); + + AddAssert("Control points have correct types", () => + { + var newControlPointTypes = selectedSlider.Path.ControlPoints.Select(p => p.Type).ToArray(); + + return oldControlPointTypes.Take(newControlPointTypes.Length).SequenceEqual(newControlPointTypes); + }); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs index 8ed77d45d7..413a3c3dfd 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs @@ -187,7 +187,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep($"move mouse to control point {index}", () => { Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position; - InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position)); + InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position)); }); } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs index f262a4334a..0ae14bdde8 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs @@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { var firstPiece = this.ChildrenOfType>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[0]); var pos = slider.Path.PositionAt(0.25d) + slider.Position; - InputManager.MoveMouseTo(firstPiece.Parent.ToScreenSpace(pos)); + InputManager.MoveMouseTo(firstPiece.Parent!.ToScreenSpace(pos)); }); AddStep("move slider end", () => { diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs index 605771fb20..ad37258c9b 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs @@ -163,7 +163,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor slider = new Slider { Position = new Vector2(0, 50), - LegacyLastTickOffset = 36, // This is necessary for undo to retain the sample control point Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero, PathType.PerfectCurve), @@ -232,7 +231,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor if (slider is null || visualiser is null) return; Vector2 position = slider.Path.ControlPoints[index].Position + slider.Position; - InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position)); + InputManager.MoveMouseTo(visualiser.Pieces[0].Parent!.ToScreenSpace(position)); }); } @@ -242,9 +241,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { if (visualiser is null) return; - MenuItem? item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText); + MenuItem? item = visualiser.ContextMenuItems?.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText); - item?.Action?.Value(); + item?.Action.Value?.Invoke(); }); } } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs index bb8c52bdfc..175cbeca6e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs @@ -1,9 +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.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Input; @@ -24,15 +21,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public partial class TestSceneSliderVelocityAdjust : OsuGameTestScene { - private Screens.Edit.Editor editor => Game.ScreenStack.CurrentScreen as Screens.Edit.Editor; + private Screens.Edit.Editor? editor => Game.ScreenStack.CurrentScreen as Screens.Edit.Editor; - private EditorBeatmap editorBeatmap => editor.ChildrenOfType().FirstOrDefault(); + private EditorBeatmap editorBeatmap => editor.ChildrenOfType().FirstOrDefault()!; - private EditorClock editorClock => editor.ChildrenOfType().FirstOrDefault(); + private EditorClock editorClock => editor.ChildrenOfType().FirstOrDefault()!; - private Slider slider => editorBeatmap.HitObjects.OfType().FirstOrDefault(); + private Slider? slider => editorBeatmap.HitObjects.OfType().FirstOrDefault(); - private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType().FirstOrDefault(); + private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType().FirstOrDefault()!; private DifficultyPointPiece difficultyPointPiece => blueprint.ChildrenOfType().First(); @@ -46,6 +43,55 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { double? velocity = null; + AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader())); + AddUntilStep("wait for editor load", () => editor?.ReadyForUse, () => Is.True); + + AddStep("seek to first control point", () => editorClock.Seek(editorBeatmap.ControlPointInfo.TimingPoints.First().Time)); + AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3)); + + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(editor.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre)); + AddStep("start placement", () => InputManager.Click(MouseButton.Left)); + + AddStep("move mouse to bottom right", () => InputManager.MoveMouseTo(editor.ChildrenOfType().First().ScreenSpaceDrawQuad.BottomRight - new Vector2(10))); + AddStep("end placement", () => InputManager.Click(MouseButton.Right)); + + AddStep("exit placement mode", () => InputManager.Key(Key.Number1)); + + AddAssert("slider placed", () => slider, () => Is.Not.Null); + + AddStep("select slider", () => editorBeatmap.SelectedHitObjects.Add(slider)); + + AddAssert("ensure one slider placed", () => slider, () => Is.Not.Null); + + AddStep("store velocity", () => velocity = slider!.Velocity); + + if (adjustVelocity) + { + AddStep("open velocity adjust panel", () => difficultyPointPiece.TriggerClick()); + AddStep("change velocity", () => velocityTextBox.Current.Value = 2); + + AddAssert("velocity adjusted", () => slider!.Velocity, + () => Is.EqualTo(velocity!.Value * 2).Within(Precision.DOUBLE_EPSILON)); + + AddStep("store velocity", () => velocity = slider!.Velocity); + } + + AddStep("save", () => InputManager.Keys(PlatformAction.Save)); + AddStep("exit", () => InputManager.Key(Key.Escape)); + + AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader())); + AddUntilStep("wait for editor load", () => editor?.ReadyForUse, () => Is.True); + + AddStep("seek to slider", () => editorClock.Seek(slider!.StartTime)); + AddAssert("slider has correct velocity", () => slider!.Velocity, () => Is.EqualTo(velocity)); + } + + [Test] + public void TestVelocityUndo() + { + double? velocityBefore = null; + double? durationBefore = null; + AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader())); AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true); @@ -60,36 +106,29 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("exit placement mode", () => InputManager.Key(Key.Number1)); - AddAssert("slider placed", () => slider != null); - + AddAssert("slider placed", () => slider, () => Is.Not.Null); AddStep("select slider", () => editorBeatmap.SelectedHitObjects.Add(slider)); - AddAssert("ensure one slider placed", () => slider != null); - - AddStep("store velocity", () => velocity = slider.Velocity); - - if (adjustVelocity) + AddStep("store velocity", () => { - AddStep("open velocity adjust panel", () => difficultyPointPiece.TriggerClick()); - AddStep("change velocity", () => velocityTextBox.Current.Value = 2); + velocityBefore = slider!.Velocity; + durationBefore = slider.Duration; + }); - AddAssert("velocity adjusted", () => - { - Debug.Assert(velocity != null); - return Precision.AlmostEquals(velocity.Value * 2, slider.Velocity); - }); + AddStep("open velocity adjust panel", () => difficultyPointPiece.TriggerClick()); + AddStep("change velocity", () => velocityTextBox.Current.Value = 2); - AddStep("store velocity", () => velocity = slider.Velocity); - } + AddAssert("velocity adjusted", () => slider!.Velocity, () => Is.EqualTo(velocityBefore!.Value * 2).Within(Precision.DOUBLE_EPSILON)); - AddStep("save", () => InputManager.Keys(PlatformAction.Save)); - AddStep("exit", () => InputManager.Key(Key.Escape)); + AddStep("undo", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Z); + InputManager.ReleaseKey(Key.ControlLeft); + }); - AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader())); - AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true); - - AddStep("seek to slider", () => editorClock.Seek(slider.StartTime)); - AddAssert("slider has correct velocity", () => slider.Velocity == velocity); + AddAssert("slider has correct velocity", () => slider!.Velocity, () => Is.EqualTo(velocityBefore)); + AddAssert("slider has correct duration", () => slider!.Duration, () => Is.EqualTo(durationBefore)); } } } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs index 6378097800..0e8673319e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs index c899f58c5a..8468995e86 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; diff --git a/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs b/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs index baaa24959f..5ad268a77b 100644 --- a/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests Child = piece = new TestLegacyMainCirclePiece(priorityLookup), }; - var sprites = this.ChildrenOfType().Where(s => !string.IsNullOrEmpty(s.Texture.AssetName)).DistinctBy(s => s.Texture.AssetName).ToArray(); + var sprites = this.ChildrenOfType().Where(s => !string.IsNullOrEmpty(s.Texture?.AssetName)).DistinctBy(s => s.Texture.AssetName).ToArray(); Debug.Assert(sprites.Length <= 2); }); @@ -103,8 +103,8 @@ namespace osu.Game.Rulesets.Osu.Tests private partial class TestLegacyMainCirclePiece : LegacyMainCirclePiece { - public new Sprite? CircleSprite => base.CircleSprite.ChildrenOfType().DistinctBy(s => s.Texture.AssetName).SingleOrDefault(); - public new Sprite? OverlaySprite => base.OverlaySprite.ChildrenOfType().DistinctBy(s => s.Texture.AssetName).SingleOrDefault(); + public new Sprite? CircleSprite => base.CircleSprite.ChildrenOfType().Where(s => !string.IsNullOrEmpty(s.Texture?.AssetName)).DistinctBy(s => s.Texture.AssetName).SingleOrDefault(); + public new Sprite? OverlaySprite => base.OverlaySprite.ChildrenOfType().Where(s => !string.IsNullOrEmpty(s.Texture?.AssetName)).DistinctBy(s => s.Texture.AssetName).SingleOrDefault(); public TestLegacyMainCirclePiece(string? priorityLookupPrefix) : base(priorityLookupPrefix, false) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs index 616a9c362d..ace7f23989 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs @@ -1,22 +1,29 @@ // 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.Framework.Extensions.ObjectExtensions; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Scoring; +using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods { public partial class TestSceneOsuModAutoplay : OsuModTestScene { + protected override bool AllowFail => true; + [Test] public void TestCursorPositionStoredToJudgement() { @@ -44,6 +51,37 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods FinalRate = { Value = 1.3 } }); + [TestCase(6.25f)] + [TestCase(20)] + public void TestPerfectScoreOnShortSliderWithRepeat(float pathLength) + { + AddStep("set score to standardised", () => LocalConfig.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised)); + + CreateModTest(new ModTestData + { + Autoplay = true, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Slider + { + StartTime = 500, + Position = new Vector2(256, 192), + Path = new SliderPath(new[] + { + new PathControlPoint(), + new PathControlPoint(new Vector2(0, pathLength)) + }), + RepeatCount = 1, + SliderVelocityMultiplier = 10 + } + } + }, + PassCondition = () => Player.ScoreProcessor.TotalScore.Value == 1_000_000 + }); + } + private void runSpmTest(Mod mod) { SpinnerSpmCalculator? spmCalculator = null; diff --git a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs index bee46da1ba..3e0a86d39c 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs @@ -1,10 +1,9 @@ // 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.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Utils; using osu.Game.Rulesets.Objects; @@ -21,10 +20,13 @@ namespace osu.Game.Rulesets.Osu.Tests [TestCase("basic")] [TestCase("colinear-perfect-curve")] [TestCase("slider-ticks")] + [TestCase("slider-ticks-edge-case")] + [TestCase("slider-paths-edge-case")] [TestCase("repeat-slider")] [TestCase("uneven-repeat-slider")] [TestCase("old-stacking")] [TestCase("multi-segment-slider")] + [TestCase("nan-slider")] public void Test(string name) => base.Test(name); protected override IEnumerable CreateConvertValue(HitObject hitObject) @@ -32,8 +34,21 @@ namespace osu.Game.Rulesets.Osu.Tests switch (hitObject) { case Slider slider: + var objects = new List(); + foreach (var nested in slider.NestedHitObjects) - yield return createConvertValue((OsuHitObject)nested); + objects.Add(createConvertValue((OsuHitObject)nested, slider)); + + // stable does slider tail leniency by offsetting the last tick 36ms back. + // based on player feedback, we're doing this a little different in lazer, + // and the lazer method does not require offsetting the last tick + // (see `DrawableSliderTail.CheckForResult()`). + // however, in conversion tests, just so the output matches, we're bringing + // the 36ms offset back locally. + // in particular, on some sliders, this may rearrange nested objects, + // so we sort them again by start time to prevent test failures. + foreach (var obj in objects.OrderBy(cv => cv.StartTime)) + yield return obj; break; @@ -43,13 +58,29 @@ namespace osu.Game.Rulesets.Osu.Tests break; } - static ConvertValue createConvertValue(OsuHitObject obj) => new ConvertValue + static ConvertValue createConvertValue(OsuHitObject obj, OsuHitObject? parent = null) { - StartTime = obj.StartTime, - EndTime = obj.GetEndTime(), - X = obj.StackedPosition.X, - Y = obj.StackedPosition.Y - }; + double startTime = obj.StartTime; + double endTime = obj.GetEndTime(); + + // as stated in the inline comment above, this is locally bringing back + // the stable treatment of the "legacy last tick" just to make sure + // that the conversion output matches. + // compare: `SliderEventGenerator.Generate()`, and the calculation of `legacyLastTickTime`. + if (obj is SliderTailCircle && parent is Slider slider) + { + startTime = Math.Max(startTime + SliderEventGenerator.TAIL_LENIENCY, slider.StartTime + slider.Duration / 2); + endTime = Math.Max(endTime + SliderEventGenerator.TAIL_LENIENCY, slider.StartTime + slider.Duration / 2); + } + + return new ConvertValue + { + StartTime = startTime, + EndTime = endTime, + X = obj.StackedPosition.X, + Y = obj.StackedPosition.Y + }; + } } protected override Ruleset CreateRuleset() => new OsuRuleset(); diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 7e995f2dde..7b7deb9c67 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; @@ -17,18 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; - [TestCase(6.7115569159190587d, 206, "diffcalc-test")] - [TestCase(1.4391311903612753d, 45, "zero-length-sliders")] + [TestCase(6.710442985146793d, 206, "diffcalc-test")] + [TestCase(1.4386882251130073d, 45, "zero-length-sliders")] + [TestCase(0.42506480230838789d, 2, "very-fast-slider")] + [TestCase(0.14102693012101306d, 1, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(8.9757300665532966d, 206, "diffcalc-test")] - [TestCase(1.7437232654020756d, 45, "zero-length-sliders")] + [TestCase(8.9742952703071666d, 206, "diffcalc-test")] + [TestCase(0.55071082800473514d, 2, "very-fast-slider")] + [TestCase(1.743180218215227d, 45, "zero-length-sliders")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.7115569159190587d, 239, "diffcalc-test")] - [TestCase(1.4391311903612753d, 54, "zero-length-sliders")] + [TestCase(6.710442985146793d, 239, "diffcalc-test")] + [TestCase(0.42506480230838789d, 4, "very-fast-slider")] + [TestCase(1.4386882251130073d, 54, "zero-length-sliders")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); diff --git a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs index b4727b3c02..2cf9842c83 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs @@ -1,11 +1,10 @@ // 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 NUnit.Framework; using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Tests.Beatmaps; @@ -30,7 +29,8 @@ namespace osu.Game.Rulesets.Osu.Tests new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } }, new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } }, new object[] { LegacyMods.Target, new[] { typeof(OsuModTargetPractice) } }, - new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } } + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } }, + new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } }, }; [TestCaseSource(nameof(osu_mod_mapping))] diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/reversearrow.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/reversearrow.png new file mode 100644 index 0000000000..7ebdec37d3 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/reversearrow.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-0@2x.png new file mode 100644 index 0000000000..67d2e2cf04 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-0@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-1@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-1@2x.png new file mode 100644 index 0000000000..2df10655ef Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-1@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-2@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-2@2x.png new file mode 100644 index 0000000000..eeb8ec0edf Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-2@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-3@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-3@2x.png new file mode 100644 index 0000000000..4ee73f503a Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-3@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-4@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-4@2x.png new file mode 100644 index 0000000000..eab26e36a0 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-4@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-5@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-5@2x.png new file mode 100644 index 0000000000..8056248a4b Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-5@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-6@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-6@2x.png new file mode 100644 index 0000000000..f7ca3de273 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-6@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-7@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-7@2x.png new file mode 100644 index 0000000000..49541c4228 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-7@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-8@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-8@2x.png new file mode 100644 index 0000000000..343b60f475 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-8@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-9@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-9@2x.png new file mode 100644 index 0000000000..53b687fdea Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-9@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini index 49ac2cf80d..9d16267d73 100644 --- a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini +++ b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini @@ -1,3 +1,4 @@ [General] Version: latest HitCircleOverlayAboveNumber: 0 +HitCirclePrefix: display \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs b/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs new file mode 100644 index 0000000000..e6dc72033a --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs @@ -0,0 +1,111 @@ +// 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.Collections.Generic; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class SpinFramesGenerator + { + /// + /// A small amount to spin beyond a given angle to mitigate floating-point precision errors. + /// + public const float SPIN_ERROR = MathF.PI / 8; + + /// + /// The offset from the centre of the spinner at which to spin. + /// + private const float centre_spin_offset = 50; + + private readonly double startTime; + private readonly float startAngle; + private readonly List<(float deltaAngle, double duration)> sequences = new List<(float deltaAngle, double duration)>(); + + /// + /// Creates a new that can be used to generate spinner spin frames. + /// + /// The time at which to start spinning. + /// The angle, in radians, at which to start spinning from. Defaults to the positive-y-axis. + public SpinFramesGenerator(double startTime, float startAngle = -MathF.PI / 2f) + { + this.startTime = startTime; + this.startAngle = startAngle; + } + + /// + /// Performs a single spin. + /// + /// The amount of degrees to spin. + /// The time to spend to perform the spin. + /// This . + public SpinFramesGenerator Spin(float delta, double duration) + { + sequences.Add((delta / 360 * 2 * MathF.PI, duration)); + return this; + } + + /// + /// Constructs the replay frames. + /// + /// The replay frames. + public List Build() + { + List frames = new List(); + + double lastTime = startTime; + float lastAngle = startAngle; + int lastDirection = 0; + + for (int i = 0; i < sequences.Count; i++) + { + var seq = sequences[i]; + + int seqDirection = Math.Sign(seq.deltaAngle); + float seqError = SPIN_ERROR * seqDirection; + + if (seqDirection == lastDirection) + { + // Spinning in the same direction, but the error was already added in the last rotation. + seqError = 0; + } + else if (lastDirection != 0) + { + // Spinning in a different direction, we need to account for the error of the start angle, so double it. + seqError *= 2; + } + + double seqStartTime = lastTime; + double seqEndTime = lastTime + seq.duration; + float seqStartAngle = lastAngle; + float seqEndAngle = seqStartAngle + seq.deltaAngle + seqError; + + // Intermediate spin frames. + for (; lastTime < seqEndTime; lastTime += 10) + frames.Add(new OsuReplayFrame(lastTime, calcOffsetAt((lastTime - seqStartTime) / (seqEndTime - seqStartTime), seqStartAngle, seqEndAngle), OsuAction.LeftButton)); + + // Final frame at the end of the current spin. + frames.Add(new OsuReplayFrame(seqEndTime, calcOffsetAt(1, seqStartAngle, seqEndAngle), OsuAction.LeftButton)); + + lastTime = seqEndTime; + lastAngle = seqEndAngle; + lastDirection = seqDirection; + } + + // Key release frame. + if (frames.Count > 0) + frames.Add(new OsuReplayFrame(frames[^1].Time, ((OsuReplayFrame)frames[^1]).Position)); + + return frames; + } + + private static Vector2 calcOffsetAt(double p, float startAngle, float endAngle) + { + float angle = startAngle + (endAngle - startAngle) * (float)p; + return new Vector2(256, 192) + centre_spin_offset * new Vector2(MathF.Cos(angle), MathF.Sin(angle)); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/SpinnerSpinHistoryTest.cs b/osu.Game.Rulesets.Osu.Tests/SpinnerSpinHistoryTest.cs new file mode 100644 index 0000000000..cd54873d36 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/SpinnerSpinHistoryTest.cs @@ -0,0 +1,135 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public class SpinnerSpinHistoryTest + { + private SpinnerSpinHistory history = null!; + + [SetUp] + public void Setup() + { + history = new SpinnerSpinHistory(); + } + + [TestCase(0, 0)] + [TestCase(10, 10)] + [TestCase(180, 180)] + [TestCase(350, 350)] + [TestCase(360, 360)] + [TestCase(370, 370)] + [TestCase(540, 540)] + [TestCase(720, 720)] + // --- + [TestCase(-0, 0)] + [TestCase(-10, 10)] + [TestCase(-180, 180)] + [TestCase(-350, 350)] + [TestCase(-360, 360)] + [TestCase(-370, 370)] + [TestCase(-540, 540)] + [TestCase(-720, 720)] + public void TestSpinOneDirection(float spin, float expectedRotation) + { + history.ReportDelta(500, spin); + Assert.That(history.TotalRotation, Is.EqualTo(expectedRotation)); + } + + [TestCase(0, 0, 0, 0)] + // --- + [TestCase(10, -10, 0, 10)] + [TestCase(-10, 10, 0, 10)] + // --- + [TestCase(10, -20, 0, 10)] + [TestCase(-10, 20, 0, 10)] + // --- + [TestCase(20, -10, 0, 20)] + [TestCase(-20, 10, 0, 20)] + // --- + [TestCase(10, -360, 0, 350)] + [TestCase(-10, 360, 0, 350)] + // --- + [TestCase(360, -10, 0, 370)] + [TestCase(360, 10, 0, 370)] + [TestCase(-360, 10, 0, 370)] + [TestCase(-360, -10, 0, 370)] + // --- + [TestCase(10, 10, 10, 30)] + [TestCase(10, 10, -10, 20)] + [TestCase(10, -10, 10, 10)] + [TestCase(-10, -10, -10, 30)] + [TestCase(-10, -10, 10, 20)] + [TestCase(-10, 10, 10, 10)] + // --- + [TestCase(10, -20, -350, 360)] + [TestCase(10, -20, 350, 340)] + [TestCase(-10, 20, 350, 360)] + [TestCase(-10, 20, -350, 340)] + public void TestSpinMultipleDirections(float spin1, float spin2, float spin3, float expectedRotation) + { + history.ReportDelta(500, spin1); + history.ReportDelta(1000, spin2); + history.ReportDelta(1500, spin3); + Assert.That(history.TotalRotation, Is.EqualTo(expectedRotation)); + } + + // One spin + [TestCase(370, -50, 320)] + [TestCase(-370, 50, 320)] + // Two spins + [TestCase(740, -420, 320)] + [TestCase(-740, 420, 320)] + public void TestRemoveAndCrossFullSpin(float deltaToAdd, float deltaToRemove, float expectedRotation) + { + history.ReportDelta(1000, deltaToAdd); + history.ReportDelta(500, deltaToRemove); + Assert.That(history.TotalRotation, Is.EqualTo(expectedRotation)); + } + + // One spin + partial + [TestCase(400, -30, -50, 320)] + [TestCase(-400, 30, 50, 320)] + // Two spins + partial + [TestCase(800, -430, -50, 320)] + [TestCase(-800, 430, 50, 320)] + public void TestRemoveAndCrossFullAndPartialSpins(float deltaToAdd1, float deltaToAdd2, float deltaToRemove, float expectedRotation) + { + history.ReportDelta(1000, deltaToAdd1); + history.ReportDelta(1500, deltaToAdd2); + history.ReportDelta(500, deltaToRemove); + Assert.That(history.TotalRotation, Is.EqualTo(expectedRotation)); + } + + [Test] + public void TestRewindMultipleFullSpins() + { + history.ReportDelta(500, 360); + history.ReportDelta(1000, 720); + + Assert.That(history.TotalRotation, Is.EqualTo(1080)); + + history.ReportDelta(250, -900); + + Assert.That(history.TotalRotation, Is.EqualTo(180)); + } + + [Test] + public void TestRewindOverDirectionChange() + { + history.ReportDelta(1000, 40); // max is now CW 40 degrees + Assert.That(history.TotalRotation, Is.EqualTo(40)); + history.ReportDelta(1100, -90); // max is now CCW 50 degrees + Assert.That(history.TotalRotation, Is.EqualTo(50)); + history.ReportDelta(1200, 110); // max is now CW 60 degrees + Assert.That(history.TotalRotation, Is.EqualTo(60)); + + history.ReportDelta(1000, -20); + Assert.That(history.TotalRotation, Is.EqualTo(40)); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/StackingTest.cs b/osu.Game.Rulesets.Osu.Tests/StackingTest.cs index a2ab7b564c..e370807bca 100644 --- a/osu.Game.Rulesets.Osu.Tests/StackingTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/StackingTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.IO; using System.Linq; diff --git a/osu.Game.Rulesets.Osu.Tests/TestPlayfieldBorder.cs b/osu.Game.Rulesets.Osu.Tests/TestPlayfieldBorder.cs index 5366a86bc0..323df75daf 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestPlayfieldBorder.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestPlayfieldBorder.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs index 9582ee491b..421a32b9eb 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Tests { RelativeSizeAxes = Axes.Both, Size = new Vector2(0.8f), - Child = new MovingCursorInputManager { Child = createContent?.Invoke() } + Child = new MovingCursorInputManager { Child = createContent() } }); }); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs index ff71300733..874130233a 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.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.Collections.Generic; using System.Linq; @@ -25,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Tests public partial class TestSceneDrawableJudgement : OsuSkinnableTestScene { [Resolved] - private OsuConfigManager config { get; set; } + private OsuConfigManager config { get; set; } = null!; private readonly List> pools; @@ -77,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Tests pool = pools[poolIndex]; // We need to make sure neither the pool nor the judgement get disposed when new content is set, and they both share the same parent. - ((Container)pool.Parent).Clear(false); + ((Container)pool.Parent!).Clear(false); } var container = new Container diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index 50f9c5e775..30b0451a3b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.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; @@ -25,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Tests private int depthIndex; [Resolved] - private OsuConfigManager config { get; set; } + private OsuConfigManager config { get; set; } = null!; [Test] public void TestHits() @@ -36,6 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Hit Big Stream", () => SetContents(_ => testStream(2, true))); AddStep("Hit Medium Stream", () => SetContents(_ => testStream(5, true))); AddStep("Hit Small Stream", () => SetContents(_ => testStream(7, true))); + AddStep("High combo index", () => SetContents(_ => testSingle(2, true, comboIndex: 15))); } [Test] @@ -68,12 +67,12 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true))); } - private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null) + private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null, int comboIndex = 0) { var playfield = new TestOsuPlayfield(); for (double t = timeOffset; t < timeOffset + 60000; t += 2000) - playfield.Add(createSingle(circleSize, auto, t, positionOffset)); + playfield.Add(createSingle(circleSize, auto, t, positionOffset, comboIndex: comboIndex)); return playfield; } @@ -86,14 +85,14 @@ namespace osu.Game.Rulesets.Osu.Tests for (int i = 0; i <= 1000; i += 100) { - playfield.Add(createSingle(circleSize, auto, i, pos, hitOffset)); + playfield.Add(createSingle(circleSize, auto, i, pos, hitOffset, i / 100 - 1)); pos.X += 50; } return playfield; } - private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset, double hitOffset = 0) + private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset, double hitOffset = 0, int comboIndex = 0) { positionOffset ??= Vector2.Zero; @@ -101,6 +100,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + 1000 + timeOffset, Position = OsuPlayfield.BASE_SIZE / 4 + positionOffset.Value, + IndexInCurrentCombo = comboIndex, }; circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize }); @@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Osu.Tests protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current) != false) + if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current, HitResult.Great) == ClickAction.Hit) { // force success ApplyResult(r => r.Type = HitResult.Great); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleComboChange.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleComboChange.cs index 34c67e8b9c..b7c3097864 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleComboChange.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleComboChange.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Game.Rulesets.Osu.Objects; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleHidden.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleHidden.cs index b3498b9651..c113993d31 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleHidden.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 NUnit.Framework; using osu.Game.Rulesets.Osu.Mods; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs index 3c32b4fa65..483155e646 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Scoring; using osu.Game.Tests.Visual; using osuTK; @@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Tests private float? alphaAtMiss; [Test] - public void TestHitCircleClassicMod() + public void TestHitCircleClassicModMiss() { AddStep("Create hit circle", () => { @@ -61,8 +62,27 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("Transparent when missed", () => alphaAtMiss == 0); } + /// + /// No early fade is expected to be applied if the hit circle has been hit. + /// [Test] - public void TestHitCircleNoMod() + public void TestHitCircleClassicModHit() + { + TestDrawableHitCircle circle = null!; + + AddStep("Create hit circle", () => + { + SelectedMods.Value = new Mod[] { new OsuModClassic() }; + circle = createCircle(true); + }); + + AddUntilStep("Wait until circle is hit", () => circle.Result?.Type == HitResult.Great); + AddUntilStep("Wait for miss window", () => Clock.CurrentTime, () => Is.GreaterThanOrEqualTo(circle.HitObject.StartTime + circle.HitObject.HitWindows.WindowFor(HitResult.Miss))); + AddAssert("Check circle is still visible", () => circle.Alpha, () => Is.GreaterThan(0)); + } + + [Test] + public void TestHitCircleNoModMiss() { AddStep("Create hit circle", () => { @@ -74,6 +94,16 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("Opaque when missed", () => alphaAtMiss == 1); } + [Test] + public void TestHitCircleNoModHit() + { + AddStep("Create hit circle", () => + { + SelectedMods.Value = Array.Empty(); + createCircle(true); + }); + } + [Test] public void TestSliderClassicMod() { @@ -100,27 +130,33 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("Head circle opaque when missed", () => alphaAtMiss == 1); } - private void createCircle() + private TestDrawableHitCircle createCircle(bool shouldHit = false) { alphaAtMiss = null; - DrawableHitCircle drawableHitCircle = new DrawableHitCircle(new HitCircle + TestDrawableHitCircle drawableHitCircle = new TestDrawableHitCircle(new HitCircle { StartTime = Time.Current + 500, - Position = new Vector2(250) - }); + Position = new Vector2(250), + }, shouldHit); + drawableHitCircle.Scale = new Vector2(2f); + + LoadComponent(drawableHitCircle); foreach (var mod in SelectedMods.Value.OfType()) mod.ApplyToDrawableHitObject(drawableHitCircle); drawableHitCircle.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - drawableHitCircle.OnNewResult += (_, _) => + drawableHitCircle.OnNewResult += (_, result) => { - alphaAtMiss = drawableHitCircle.Alpha; + if (!result.IsHit) + alphaAtMiss = drawableHitCircle.Alpha; }; Child = drawableHitCircle; + + return drawableHitCircle; } private void createSlider() @@ -138,6 +174,8 @@ namespace osu.Game.Rulesets.Osu.Tests }) }); + drawableSlider.Scale = new Vector2(2f); + drawableSlider.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); drawableSlider.OnLoadComplete += _ => @@ -145,12 +183,36 @@ namespace osu.Game.Rulesets.Osu.Tests foreach (var mod in SelectedMods.Value.OfType()) mod.ApplyToDrawableHitObject(drawableSlider.HeadCircle); - drawableSlider.HeadCircle.OnNewResult += (_, _) => + drawableSlider.HeadCircle.OnNewResult += (_, result) => { - alphaAtMiss = drawableSlider.HeadCircle.Alpha; + if (!result.IsHit) + alphaAtMiss = drawableSlider.HeadCircle.Alpha; }; }; + Child = drawableSlider; } + + protected partial class TestDrawableHitCircle : DrawableHitCircle + { + private readonly bool shouldHit; + + public TestDrawableHitCircle(HitCircle h, bool shouldHit) + : base(h) + { + this.shouldHit = shouldHit; + } + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + if (shouldHit && !userTriggered && timeOffset >= 0) + { + // force success + ApplyResult(r => r.Type = HitResult.Great); + } + else + base.CheckForResult(userTriggered, timeOffset); + } + } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs index 93f1123341..bfb31d5b31 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs new file mode 100644 index 0000000000..fa6aa580a3 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs @@ -0,0 +1,891 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using NUnit.Framework; +using osu.Framework.Extensions; +using osu.Framework.Extensions.TypeExtensions; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Formats; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public partial class TestSceneLegacyHitPolicy : RateAdjustedBeatmapTestScene + { + private readonly OsuHitWindows referenceHitWindows; + + /// + /// This is provided as a convenience for testing note lock behaviour against osu!stable. + /// Setting this field to a non-null path will cause beatmap files and replays used in all test cases + /// to be exported to disk so that they can be cross-checked against stable. + /// + private readonly string? exportLocation = null; + + public TestSceneLegacyHitPolicy() + { + referenceHitWindows = new OsuHitWindows(); + referenceHitWindows.SetDifficulty(0); + } + + /// + /// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged. + /// + [Test] + public void TestClickSecondCircleBeforeFirstCircleTime() + { + const double time_first_circle = 1500; + const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); + + var hitObjects = new List + { + new HitCircle + { + StartTime = time_first_circle, + Position = positionFirstCircle + }, + new HitCircle + { + StartTime = time_second_circle, + Position = positionSecondCircle + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } + }); + + addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[1], HitResult.Miss); + // note lock prevented the object from being hit, so the judgement offset should be very late. + addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh)); + addClickActionAssert(0, ClickAction.Shake); + } + + /// + /// Tests clicking a future circle at the first circle's start time, while the first circle HAS NOT been judged. + /// + [Test] + public void TestClickSecondCircleAtFirstCircleTime() + { + const double time_first_circle = 1500; + const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); + + var hitObjects = new List + { + new HitCircle + { + StartTime = time_first_circle, + Position = positionFirstCircle + }, + new HitCircle + { + StartTime = time_second_circle, + Position = positionSecondCircle + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } + }); + + addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[1], HitResult.Miss); + // note lock prevented the object from being hit, so the judgement offset should be very late. + addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh)); + addClickActionAssert(0, ClickAction.Shake); + } + + /// + /// Tests clicking a future circle after the first circle's start time, while the first circle HAS NOT been judged. + /// + [Test] + public void TestClickSecondCircleAfterFirstCircleTime() + { + const double time_first_circle = 1500; + const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); + + var hitObjects = new List + { + new HitCircle + { + StartTime = time_first_circle, + Position = positionFirstCircle + }, + new HitCircle + { + StartTime = time_second_circle, + Position = positionSecondCircle + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_circle + 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } + }); + + addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[1], HitResult.Miss); + // note lock prevented the object from being hit, so the judgement offset should be very late. + addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh)); + addClickActionAssert(0, ClickAction.Shake); + } + + /// + /// Tests clicking a future circle before the first circle's start time, while the first circle HAS been judged. + /// + [Test] + public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged() + { + const double time_first_circle = 1500; + const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); + + var hitObjects = new List + { + new HitCircle + { + StartTime = time_first_circle, + Position = positionFirstCircle + }, + new HitCircle + { + StartTime = time_second_circle, + Position = positionSecondCircle + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_circle - 190, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_first_circle - 90, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } + }); + + addJudgementAssert(hitObjects[0], HitResult.Meh); + addJudgementAssert(hitObjects[1], HitResult.Meh); + addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190 + addJudgementOffsetAssert(hitObjects[1], -190); // time_second_circle - first_circle_time - 90 + addClickActionAssert(0, ClickAction.Hit); + addClickActionAssert(1, ClickAction.Hit); + } + + /// + /// Tests clicking a future circle after the first circle's start time, while the first circle HAS been judged. + /// + [Test] + public void TestClickSecondCircleAfterFirstCircleTimeWithFirstCircleJudged() + { + const double time_first_circle = 1500; + const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); + + var hitObjects = new List + { + new HitCircle + { + StartTime = time_first_circle, + Position = positionFirstCircle + }, + new HitCircle + { + StartTime = time_second_circle, + Position = positionSecondCircle + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_circle - 190, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } + }); + + addJudgementAssert(hitObjects[0], HitResult.Meh); + addJudgementAssert(hitObjects[1], HitResult.Ok); + addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190 + addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time + addClickActionAssert(0, ClickAction.Hit); + addClickActionAssert(1, ClickAction.Hit); + } + + /// + /// Tests clicking a future circle after a slider's start time, but hitting the slider head and all slider ticks. + /// + [Test] + public void TestHitCircleBeforeSliderHead() + { + const double time_slider = 1500; + const double time_circle = 1510; + Vector2 positionCircle = Vector2.Zero; + Vector2 positionSlider = new Vector2(80); + + var hitObjects = new List + { + new HitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + new Slider + { + StartTime = time_slider, + Position = positionSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(50, 0), + }) + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_slider, Position = positionCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } } + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit); + addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); + addClickActionAssert(0, ClickAction.Hit); + addClickActionAssert(1, ClickAction.Hit); + } + + /// + /// Tests clicking hitting future slider ticks before a circle. + /// + [Test] + public void TestHitSliderTicksBeforeCircle() + { + const double time_slider = 1500; + const double time_circle = 1510; + Vector2 positionCircle = Vector2.Zero; + Vector2 positionSlider = new Vector2(30); + + var hitObjects = new List + { + new HitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + new Slider + { + StartTime = time_slider, + Position = positionSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(50, 0), + }) + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_circle + referenceHitWindows.WindowFor(HitResult.Meh) - 100, Position = positionCircle, Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_circle + referenceHitWindows.WindowFor(HitResult.Meh) - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + }); + + addJudgementAssert(hitObjects[0], HitResult.Ok); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit); + addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); + addClickActionAssert(0, ClickAction.Hit); + addClickActionAssert(1, ClickAction.Hit); + } + + /// + /// Tests clicking a future circle before a spinner. + /// + [Test] + public void TestHitCircleBeforeSpinner() + { + const double time_spinner = 1500; + const double time_circle = 1600; + Vector2 positionCircle = Vector2.Zero; + + var hitObjects = new List + { + new TestSpinner + { + StartTime = time_spinner, + Position = new Vector2(256, 192), + EndTime = time_spinner + 1000, + }, + new HitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + }; + + List frames = new List + { + new OsuReplayFrame { Time = time_spinner - 90, Position = positionCircle, Actions = { OsuAction.LeftButton } }, + }; + + frames.AddRange(new SpinFramesGenerator(time_spinner + 10) + .Spin(360, 500) + .Build()); + + performTest(hitObjects, frames); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Meh); + addClickActionAssert(0, ClickAction.Hit); + } + + [Test] + public void TestHitSliderHeadBeforeHitCircle() + { + const double time_circle = 1000; + const double time_slider = 1200; + Vector2 positionCircle = Vector2.Zero; + Vector2 positionSlider = new Vector2(80); + + var hitObjects = new List + { + new HitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + new Slider + { + StartTime = time_slider, + Position = positionSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_circle - 100, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_circle, Position = positionCircle, Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + addClickActionAssert(0, ClickAction.Shake); + addClickActionAssert(1, ClickAction.Hit); + addClickActionAssert(2, ClickAction.Hit); + } + + [Test] + public void TestOverlappingSliders() + { + const double time_first_slider = 1000; + const double time_second_slider = 1200; + Vector2 positionFirstSlider = new Vector2(100, 50); + Vector2 positionSecondSlider = new Vector2(100, 80); + var midpoint = (positionFirstSlider + positionSecondSlider) / 2; + + var hitObjects = new List + { + new Slider + { + StartTime = time_first_slider, + Position = positionFirstSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + }, + new Slider + { + StartTime = time_second_slider, + Position = positionSecondSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint, Actions = { OsuAction.LeftButton, OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_first_slider + 50, Position = midpoint }, + new OsuReplayFrame { Time = time_second_slider, Position = positionSecondSlider + new Vector2(0, 10), Actions = { OsuAction.LeftButton } }, + }); + + addJudgementAssert(hitObjects[0], HitResult.Ok); + addJudgementAssert(hitObjects[1], HitResult.Great); + addClickActionAssert(0, ClickAction.Hit); + addClickActionAssert(1, ClickAction.Hit); + } + + [Test] + public void TestStacksDoNotShake() + { + const double time_stack_start = 1000; + Vector2 position = new Vector2(80); + + var hitObjects = Enumerable.Range(0, 20).Select(i => new HitCircle + { + StartTime = time_stack_start + i * 100, + Position = position + }).Cast().ToList(); + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_stack_start - 450, Position = new Vector2(55), Actions = { OsuAction.LeftButton } }, + }); + + addClickActionAssert(0, ClickAction.Ignore); + } + + [Test] + public void TestAutopilotReducesHittableRange() + { + const double time_circle = 1500; + Vector2 positionCircle = Vector2.Zero; + + var hitObjects = new List + { + new HitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_circle - 250, Position = positionCircle, Actions = { OsuAction.LeftButton } } + }, new Mod[] { new OsuModAutopilot() }); + + addJudgementAssert(hitObjects[0], HitResult.Miss); + // note lock prevented the object from being hit, so the judgement offset should be very late. + addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh)); + addClickActionAssert(0, ClickAction.Shake); + } + + [Test] + public void TestInputDoesNotFallThroughOverlappingSliders() + { + const double time_first_slider = 1000; + const double time_second_slider = 1250; + Vector2 positionFirstSlider = new Vector2(100, 50); + Vector2 positionSecondSlider = new Vector2(100, 80); + var midpoint = (positionFirstSlider + positionSecondSlider) / 2; + + var hitObjects = new List + { + new Slider + { + StartTime = time_first_slider, + Position = positionFirstSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + }, + new Slider + { + StartTime = time_second_slider, + Position = positionSecondSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_first_slider + 50, Position = midpoint }, + }); + + addJudgementAssert(hitObjects[0], HitResult.Ok); + addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0); + addJudgementAssert(hitObjects[1], HitResult.Miss); + // the slider head of the first slider prevents the second slider's head from being hit, so the judgement offset should be very late. + // this is not strictly done by the hit policy implementation itself (see `OsuModClassic.blockInputToObjectsUnderSliderHead()`), + // but we're testing this here anyways to just keep everything related to input handling and note lock in one place. + addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, referenceHitWindows.WindowFor(HitResult.Meh)); + addClickActionAssert(0, ClickAction.Hit); + } + + [Test] + public void TestOverlappingSlidersDontBlockEachOtherWhenFullyJudged() + { + const double time_first_slider = 1000; + const double time_second_slider = 1600; + Vector2 positionFirstSlider = new Vector2(100, 50); + Vector2 positionSecondSlider = new Vector2(100, 80); + var midpoint = (positionFirstSlider + positionSecondSlider) / 2; + + var hitObjects = new List + { + new Slider + { + StartTime = time_first_slider, + Position = positionFirstSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + }, + new Slider + { + StartTime = time_second_slider, + Position = positionSecondSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint }, + // this frame doesn't do anything on lazer, but is REQUIRED for correct playback on stable, + // because stable during replay playback only updates game state _when it encounters a replay frame_ + new OsuReplayFrame { Time = 1250, Position = midpoint }, + new OsuReplayFrame { Time = time_second_slider + 50, Position = midpoint, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_second_slider + 75, Position = midpoint }, + }); + + addJudgementAssert(hitObjects[0], HitResult.Ok); + addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0); + addJudgementAssert(hitObjects[1], HitResult.Ok); + addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, 50); + addClickActionAssert(0, ClickAction.Hit); + addClickActionAssert(1, ClickAction.Hit); + } + + [Test] + public void TestOverlappingHitCirclesDontBlockEachOtherWhenBothVisible() + { + const double time_first_circle = 1000; + const double time_second_circle = 1200; + Vector2 positionFirstCircle = new Vector2(100); + Vector2 positionSecondCircle = new Vector2(120); + var midpoint = (positionFirstCircle + positionSecondCircle) / 2; + + var hitObjects = new List + { + new HitCircle + { + StartTime = time_first_circle, + Position = positionFirstCircle, + }, + new HitCircle + { + StartTime = time_second_circle, + Position = positionSecondCircle, + }, + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_circle, Position = midpoint, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_first_circle + 25, Position = midpoint }, + new OsuReplayFrame { Time = time_first_circle + 50, Position = midpoint, Actions = { OsuAction.RightButton } }, + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementOffsetAssert(hitObjects[0], 0); + + addJudgementAssert(hitObjects[1], HitResult.Meh); + addJudgementOffsetAssert(hitObjects[1], -150); + } + + [Test] + public void TestOverlappingHitCirclesDontBlockEachOtherWhenFullyFadedOut() + { + const double time_first_circle = 1000; + const double time_second_circle = 1200; + const double time_third_circle = 1400; + Vector2 positionFirstCircle = new Vector2(100); + Vector2 positionSecondCircle = new Vector2(200); + + var hitObjects = new List + { + new HitCircle + { + StartTime = time_first_circle, + Position = positionFirstCircle, + }, + new HitCircle + { + StartTime = time_second_circle, + Position = positionSecondCircle, + }, + new HitCircle + { + StartTime = time_third_circle, + Position = positionFirstCircle, + }, + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_first_circle + 50, Position = positionFirstCircle }, + new OsuReplayFrame { Time = time_second_circle - 50, Position = positionSecondCircle }, + new OsuReplayFrame { Time = time_second_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_second_circle + 50, Position = positionSecondCircle }, + new OsuReplayFrame { Time = time_third_circle - 50, Position = positionFirstCircle }, + new OsuReplayFrame { Time = time_third_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_third_circle + 50, Position = positionFirstCircle }, + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementOffsetAssert(hitObjects[0], 0); + + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementOffsetAssert(hitObjects[1], 0); + + addJudgementAssert(hitObjects[2], HitResult.Great); + addJudgementOffsetAssert(hitObjects[2], 0); + } + + private void addJudgementAssert(OsuHitObject hitObject, HitResult result) + { + AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}", + () => judgementResults.Single(r => r.HitObject == hitObject).Type, () => Is.EqualTo(result)); + } + + private void addJudgementAssert(string name, Func hitObject, HitResult result) + { + AddAssert($"{name} judgement is {result}", + () => judgementResults.Single(r => r.HitObject == hitObject()).Type, () => Is.EqualTo(result)); + } + + private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset) + { + AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}", + () => judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, () => Is.EqualTo(offset).Within(50)); + } + + private void addJudgementOffsetAssert(string name, Func hitObject, double offset) + { + AddAssert($"{name} @ judged at {offset}", + () => judgementResults.Single(r => r.HitObject == hitObject()).TimeOffset, () => Is.EqualTo(offset).Within(50)); + } + + private void addClickActionAssert(int inputIndex, ClickAction action) + => AddAssert($"input #{inputIndex} resulted in {action}", () => testPolicy.ClickActions[inputIndex], () => Is.EqualTo(action)); + + private ScoreAccessibleReplayPlayer currentPlayer = null!; + private List judgementResults = null!; + private TestLegacyHitPolicy testPolicy = null!; + + private void performTest(List hitObjects, List frames, IEnumerable? extraMods = null, [CallerMemberName] string testCaseName = "") + { + List mods = null!; + IBeatmap playableBeatmap = null!; + Score score = null!; + + AddStep("set up mods", () => + { + mods = new List { new OsuModClassic() }; + + if (extraMods != null) + mods.AddRange(extraMods); + }); + + AddStep("create beatmap", () => + { + var cpi = new ControlPointInfo(); + cpi.Add(0, new TimingControlPoint { BeatLength = 1000 }); + Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + Metadata = + { + Title = testCaseName + }, + HitObjects = hitObjects, + Difficulty = new BeatmapDifficulty + { + OverallDifficulty = 0, + SliderTickRate = 3 + }, + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo, + BeatmapVersion = LegacyBeatmapEncoder.FIRST_LAZER_VERSION // for correct offset treatment by score encoder + }, + ControlPointInfo = cpi + }); + playableBeatmap = Beatmap.Value.GetPlayableBeatmap(new OsuRuleset().RulesetInfo); + }); + + AddStep("create score", () => + { + score = new Score + { + Replay = new Replay + { + Frames = new List + { + // required for correct playback in stable + new OsuReplayFrame(0, new Vector2(256, -500)), + new OsuReplayFrame(0, new Vector2(256, -500)) + }.Concat(frames).ToList() + }, + ScoreInfo = + { + Ruleset = new OsuRuleset().RulesetInfo, + BeatmapInfo = playableBeatmap.BeatmapInfo, + Mods = mods.ToArray() + } + }; + }); + + if (exportLocation != null) + { + AddStep("export beatmap", () => + { + var beatmapEncoder = new LegacyBeatmapEncoder(playableBeatmap, null); + + using (var stream = File.Open(Path.Combine(exportLocation, $"{testCaseName}.osu"), FileMode.Create)) + { + var memoryStream = new MemoryStream(); + using (var writer = new StreamWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) + beatmapEncoder.Encode(writer); + + memoryStream.Seek(0, SeekOrigin.Begin); + memoryStream.CopyTo(stream); + memoryStream.Seek(0, SeekOrigin.Begin); + playableBeatmap.BeatmapInfo.MD5Hash = memoryStream.ComputeMD5Hash(); + } + }); + + AddStep("export score", () => + { + using var stream = File.Open(Path.Combine(exportLocation, $"{testCaseName}.osr"), FileMode.Create); + var encoder = new LegacyScoreEncoder(score, playableBeatmap); + encoder.Encode(stream); + }); + } + + AddStep("load player", () => + { + SelectedMods.Value = mods.ToArray(); + + var p = new ScoreAccessibleReplayPlayer(score); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + }; + + LoadScreen(currentPlayer = p); + judgementResults = new List(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddStep("Substitute hit policy", () => + { + var playfield = currentPlayer.ChildrenOfType().Single(); + var currentPolicy = playfield.HitPolicy; + playfield.HitPolicy = testPolicy = new TestLegacyHitPolicy(currentPolicy); + }); + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); + } + + private class TestSpinner : Spinner + { + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) + { + base.ApplyDefaultsToSelf(controlPointInfo, difficulty); + SpinsRequired = 1; + } + } + + private partial class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + } + } + + private class TestLegacyHitPolicy : LegacyHitPolicy + { + private readonly IHitPolicy currentPolicy; + + public TestLegacyHitPolicy(IHitPolicy currentPolicy) + { + this.currentPolicy = currentPolicy; + } + + public List ClickActions { get; } = new List(); + + public override ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result) + { + var action = currentPolicy.CheckHittable(hitObject, time, result); + ClickActions.Add(action); + return action; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs index 0d3fd77568..c37660831b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Beatmaps; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoSpinnerStacking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoSpinnerStacking.cs index 1f0e264cf7..30d9aff83a 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoSpinnerStacking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoSpinnerStacking.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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs deleted file mode 100644 index ee70441688..0000000000 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ /dev/null @@ -1,493 +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 System; -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Extensions.TypeExtensions; -using osu.Framework.Screens; -using osu.Framework.Utils; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Replays; -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; -using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Replays; -using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; -using osu.Game.Screens.Play; -using osu.Game.Tests.Visual; -using osuTK; - -namespace osu.Game.Rulesets.Osu.Tests -{ - public partial class TestSceneObjectOrderedHitPolicy : RateAdjustedBeatmapTestScene - { - private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss - private const double late_miss_window = 500; // time after +500 is considered a miss - - /// - /// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged. - /// - [Test] - public void TestClickSecondCircleBeforeFirstCircleTime() - { - const double time_first_circle = 1500; - const double time_second_circle = 1600; - Vector2 positionFirstCircle = Vector2.Zero; - Vector2 positionSecondCircle = new Vector2(80); - - var hitObjects = new List - { - new TestHitCircle - { - StartTime = time_first_circle, - Position = positionFirstCircle - }, - new TestHitCircle - { - StartTime = time_second_circle, - Position = positionSecondCircle - } - }; - - performTest(hitObjects, new List - { - new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } - }); - - addJudgementAssert(hitObjects[0], HitResult.Miss); - addJudgementAssert(hitObjects[1], HitResult.Miss); - addJudgementOffsetAssert(hitObjects[0], late_miss_window); - } - - /// - /// Tests clicking a future circle at the first circle's start time, while the first circle HAS NOT been judged. - /// - [Test] - public void TestClickSecondCircleAtFirstCircleTime() - { - const double time_first_circle = 1500; - const double time_second_circle = 1600; - Vector2 positionFirstCircle = Vector2.Zero; - Vector2 positionSecondCircle = new Vector2(80); - - var hitObjects = new List - { - new TestHitCircle - { - StartTime = time_first_circle, - Position = positionFirstCircle - }, - new TestHitCircle - { - StartTime = time_second_circle, - Position = positionSecondCircle - } - }; - - performTest(hitObjects, new List - { - new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } - }); - - addJudgementAssert(hitObjects[0], HitResult.Miss); - addJudgementAssert(hitObjects[1], HitResult.Miss); - addJudgementOffsetAssert(hitObjects[0], late_miss_window); - } - - /// - /// Tests clicking a future circle after the first circle's start time, while the first circle HAS NOT been judged. - /// - [Test] - public void TestClickSecondCircleAfterFirstCircleTime() - { - const double time_first_circle = 1500; - const double time_second_circle = 1600; - Vector2 positionFirstCircle = Vector2.Zero; - Vector2 positionSecondCircle = new Vector2(80); - - var hitObjects = new List - { - new TestHitCircle - { - StartTime = time_first_circle, - Position = positionFirstCircle - }, - new TestHitCircle - { - StartTime = time_second_circle, - Position = positionSecondCircle - } - }; - - performTest(hitObjects, new List - { - new OsuReplayFrame { Time = time_first_circle + 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } - }); - - addJudgementAssert(hitObjects[0], HitResult.Miss); - addJudgementAssert(hitObjects[1], HitResult.Miss); - addJudgementOffsetAssert(hitObjects[0], late_miss_window); - } - - /// - /// Tests clicking a future circle before the first circle's start time, while the first circle HAS been judged. - /// - [Test] - public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged() - { - const double time_first_circle = 1500; - const double time_second_circle = 1600; - Vector2 positionFirstCircle = Vector2.Zero; - Vector2 positionSecondCircle = new Vector2(80); - - var hitObjects = new List - { - new TestHitCircle - { - StartTime = time_first_circle, - Position = positionFirstCircle - }, - new TestHitCircle - { - StartTime = time_second_circle, - Position = positionSecondCircle - } - }; - - performTest(hitObjects, new List - { - new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } - }); - - addJudgementAssert(hitObjects[0], HitResult.Great); - addJudgementAssert(hitObjects[1], HitResult.Great); - addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 - addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100 - } - - /// - /// Tests clicking a future circle after the first circle's start time, while the first circle HAS been judged. - /// - [Test] - public void TestClickSecondCircleAfterFirstCircleTimeWithFirstCircleJudged() - { - const double time_first_circle = 1500; - const double time_second_circle = 1600; - Vector2 positionFirstCircle = Vector2.Zero; - Vector2 positionSecondCircle = new Vector2(80); - - var hitObjects = new List - { - new TestHitCircle - { - StartTime = time_first_circle, - Position = positionFirstCircle - }, - new TestHitCircle - { - StartTime = time_second_circle, - Position = positionSecondCircle - } - }; - - performTest(hitObjects, new List - { - new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } - }); - - addJudgementAssert(hitObjects[0], HitResult.Great); - addJudgementAssert(hitObjects[1], HitResult.Great); - addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 - addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time - } - - /// - /// Tests clicking a future circle after a slider's start time, but hitting all slider ticks. - /// - [Test] - public void TestMissSliderHeadAndHitAllSliderTicks() - { - const double time_slider = 1500; - const double time_circle = 1510; - Vector2 positionCircle = Vector2.Zero; - Vector2 positionSlider = new Vector2(80); - - var hitObjects = new List - { - new TestHitCircle - { - StartTime = time_circle, - Position = positionCircle - }, - new TestSlider - { - StartTime = time_slider, - Position = positionSlider, - Path = new SliderPath(PathType.Linear, new[] - { - Vector2.Zero, - new Vector2(25, 0), - }) - } - }; - - performTest(hitObjects, new List - { - new OsuReplayFrame { Time = time_slider, Position = positionCircle, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } } - }); - - addJudgementAssert(hitObjects[0], HitResult.Miss); - addJudgementAssert(hitObjects[1], HitResult.Great); - addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit); - addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); - } - - /// - /// Tests clicking hitting future slider ticks before a circle. - /// - [Test] - public void TestHitSliderTicksBeforeCircle() - { - const double time_slider = 1500; - const double time_circle = 1510; - Vector2 positionCircle = Vector2.Zero; - Vector2 positionSlider = new Vector2(30); - - var hitObjects = new List - { - new TestHitCircle - { - StartTime = time_circle, - Position = positionCircle - }, - new TestSlider - { - StartTime = time_slider, - Position = positionSlider, - Path = new SliderPath(PathType.Linear, new[] - { - Vector2.Zero, - new Vector2(25, 0), - }) - } - }; - - performTest(hitObjects, new List - { - new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_circle + late_miss_window - 100, Position = positionCircle, Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_circle + late_miss_window - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } }, - }); - - addJudgementAssert(hitObjects[0], HitResult.Great); - addJudgementAssert(hitObjects[1], HitResult.Great); - addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit); - addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); - } - - /// - /// Tests clicking a future circle before a spinner. - /// - [Test] - public void TestHitCircleBeforeSpinner() - { - const double time_spinner = 1500; - const double time_circle = 1800; - Vector2 positionCircle = Vector2.Zero; - - var hitObjects = new List - { - new TestSpinner - { - StartTime = time_spinner, - Position = new Vector2(256, 192), - EndTime = time_spinner + 1000, - }, - new TestHitCircle - { - StartTime = time_circle, - Position = positionCircle - }, - }; - - performTest(hitObjects, new List - { - new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, - }); - - addJudgementAssert(hitObjects[0], HitResult.Great); - addJudgementAssert(hitObjects[1], HitResult.Great); - } - - [Test] - public void TestHitSliderHeadBeforeHitCircle() - { - const double time_circle = 1000; - const double time_slider = 1200; - Vector2 positionCircle = Vector2.Zero; - Vector2 positionSlider = new Vector2(80); - - var hitObjects = new List - { - new TestHitCircle - { - StartTime = time_circle, - Position = positionCircle - }, - new TestSlider - { - StartTime = time_slider, - Position = positionSlider, - Path = new SliderPath(PathType.Linear, new[] - { - Vector2.Zero, - new Vector2(25, 0), - }) - } - }; - - performTest(hitObjects, new List - { - new OsuReplayFrame { Time = time_circle - 100, Position = positionSlider, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_circle, Position = positionCircle, Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } }, - }); - - addJudgementAssert(hitObjects[0], HitResult.Great); - addJudgementAssert(hitObjects[1], HitResult.Great); - } - - private void addJudgementAssert(OsuHitObject hitObject, HitResult result) - { - AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}", - () => judgementResults.Single(r => r.HitObject == hitObject).Type, () => Is.EqualTo(result)); - } - - private void addJudgementAssert(string name, Func hitObject, HitResult result) - { - AddAssert($"{name} judgement is {result}", - () => judgementResults.Single(r => r.HitObject == hitObject()).Type == result); - } - - private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset) - { - AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}", - () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100)); - } - - private ScoreAccessibleReplayPlayer currentPlayer; - private List judgementResults; - - private void performTest(List hitObjects, List frames) - { - AddStep("load player", () => - { - Beatmap.Value = CreateWorkingBeatmap(new Beatmap - { - HitObjects = hitObjects, - Difficulty = new BeatmapDifficulty { SliderTickRate = 3 }, - BeatmapInfo = - { - Ruleset = new OsuRuleset().RulesetInfo - }, - }); - - SelectedMods.Value = new[] { new OsuModClassic() }; - - var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); - - p.OnLoadComplete += _ => - { - p.ScoreProcessor.NewJudgement += result => - { - if (currentPlayer == p) judgementResults.Add(result); - }; - }; - - LoadScreen(currentPlayer = p); - judgementResults = new List(); - }); - - AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); - AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); - AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); - } - - private class TestHitCircle : HitCircle - { - protected override HitWindows CreateHitWindows() => new TestHitWindows(); - } - - private class TestSlider : Slider - { - public TestSlider() - { - SliderVelocity = 0.1f; - - DefaultsApplied += _ => - { - HeadCircle.HitWindows = new TestHitWindows(); - TailCircle.HitWindows = new TestHitWindows(); - - HeadCircle.HitWindows.SetDifficulty(0); - TailCircle.HitWindows.SetDifficulty(0); - }; - } - } - - private class TestSpinner : Spinner - { - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) - { - base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - SpinsRequired = 1; - } - } - - private class TestHitWindows : HitWindows - { - private static readonly DifficultyRange[] ranges = - { - new DifficultyRange(HitResult.Great, 500, 500, 500), - new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window), - }; - - public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss; - - protected override DifficultyRange[] GetRanges() => ranges; - } - - private partial class ScoreAccessibleReplayPlayer : ReplayPlayer - { - public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; - - protected override bool PauseOnFocusLost => false; - - public ScoreAccessibleReplayPlayer(Score score) - : base(score, new PlayerConfiguration - { - AllowPause = false, - ShowResults = false, - }) - { - } - } - } -} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHitObjectSamples.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHitObjectSamples.cs index 4d0b2cc406..61cc10f284 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHitObjectSamples.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHitObjectSamples.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Reflection; using NUnit.Framework; using osu.Framework.IO.Stores; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs index 53c4e49807..44efc94d7b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Tests.Visual; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs index bb424eb587..2e62689e2c 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs @@ -49,6 +49,9 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Create tests", () => { + InputTrigger triggerLeft; + InputTrigger triggerRight; + Children = new Drawable[] { osuInputManager = new OsuInputManager(new OsuRuleset().RulesetInfo) @@ -59,29 +62,39 @@ namespace osu.Game.Rulesets.Osu.Tests Origin = Anchor.Centre, Children = new Drawable[] { - leftKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.LeftButton)) - { - Anchor = Anchor.Centre, - Origin = Anchor.CentreRight, - Depth = float.MinValue, - X = -100, - }, - rightKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.RightButton)) - { - Anchor = Anchor.Centre, - Origin = Anchor.CentreLeft, - Depth = float.MinValue, - X = 100, - }, new OsuCursorContainer { Depth = float.MinValue, + }, + triggerLeft = new TestActionKeyCounterTrigger(OsuAction.LeftButton) + { + Depth = float.MinValue + }, + triggerRight = new TestActionKeyCounterTrigger(OsuAction.RightButton) + { + Depth = float.MinValue } }, - } + }, }, new TouchVisualiser(), }; + + mainContent.AddRange(new[] + { + leftKeyCounter = new DefaultKeyCounter(triggerLeft) + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + X = -100, + }, + rightKeyCounter = new DefaultKeyCounter(triggerRight) + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft, + X = 100, + }, + }); }); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs index b66974d4b1..0bb27cff0f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs new file mode 100644 index 0000000000..bb09328ab7 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs @@ -0,0 +1,176 @@ +// 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 NUnit.Framework; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Scoring; +using osu.Game.Tests.Visual.Gameplay; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public partial class TestSceneScoring : ScoringTestScene + { + private Bindable scoreMultiplier { get; } = new BindableDouble + { + Default = 4, + Value = 4 + }; + + protected override IBeatmap CreateBeatmap(int maxCombo) + { + var beatmap = new OsuBeatmap(); + for (int i = 0; i < maxCombo; i++) + beatmap.HitObjects.Add(new HitCircle()); + return beatmap; + } + + protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1 { ScoreMultiplier = { BindTarget = scoreMultiplier } }; + protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo); + protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new OsuProcessorBasedScoringAlgorithm(beatmap, mode); + + [Test] + public void TestBasicScenarios() + { + AddStep("set up score multiplier", () => + { + scoreMultiplier.BindValueChanged(_ => Rerun()); + }); + AddStep("set max combo to 100", () => MaxCombo.Value = 100); + AddStep("set perfect score", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + }); + AddStep("set score with misses", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + MissLocations.AddRange(new[] { 24d, 49 }); + }); + AddStep("set score with misses and OKs", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + + NonPerfectLocations.AddRange(new[] { 9d, 19, 29, 39, 59, 69, 79, 89, 99 }); + MissLocations.AddRange(new[] { 24d, 49 }); + }); + AddSliderStep("adjust score multiplier", 0, 10, (int)scoreMultiplier.Default, multiplier => scoreMultiplier.Value = multiplier); + } + + private const int base_great = 300; + private const int base_ok = 100; + + private class ScoreV1 : IScoringAlgorithm + { + private int currentCombo; + + public BindableDouble ScoreMultiplier { get; } = new BindableDouble(); + + public void ApplyHit() => applyHitV1(base_great); + public void ApplyNonPerfect() => applyHitV1(base_ok); + public void ApplyMiss() => applyHitV1(0); + + private void applyHitV1(int baseScore) + { + if (baseScore == 0) + { + currentCombo = 0; + return; + } + + TotalScore += baseScore; + + // combo multiplier + // ReSharper disable once PossibleLossOfFraction + TotalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * ScoreMultiplier.Value)); + + currentCombo++; + } + + public long TotalScore { get; private set; } + } + + private class ScoreV2 : IScoringAlgorithm + { + private int currentCombo; + private double comboPortion; + private double currentBaseScore; + private double maxBaseScore; + private int currentHits; + + private readonly double comboPortionMax; + private readonly int maxCombo; + + public ScoreV2(int maxCombo) + { + this.maxCombo = maxCombo; + + for (int i = 0; i < this.maxCombo; i++) + ApplyHit(); + + comboPortionMax = comboPortion; + + currentCombo = 0; + comboPortion = 0; + currentBaseScore = 0; + maxBaseScore = 0; + currentHits = 0; + } + + public void ApplyHit() => applyHitV2(base_great); + public void ApplyNonPerfect() => applyHitV2(base_ok); + + private void applyHitV2(int baseScore) + { + maxBaseScore += base_great; + currentBaseScore += baseScore; + comboPortion += baseScore * (1 + ++currentCombo / 10.0); + + currentHits++; + } + + public void ApplyMiss() + { + currentHits++; + maxBaseScore += base_great; + currentCombo = 0; + } + + public long TotalScore + { + get + { + double accuracy = currentBaseScore / maxBaseScore; + + return (int)Math.Round + ( + 700000 * comboPortion / comboPortionMax + + 300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo) + ); + } + } + } + + private class OsuProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm + { + public OsuProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode) + : base(beatmap, mode) + { + } + + protected override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); + protected override JudgementResult CreatePerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }; + protected override JudgementResult CreateNonPerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }; + protected override JudgementResult CreateMissJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }; + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs index bee7831625..0599517899 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Diagnostics; using osu.Framework.Threading; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index 4ad78a3190..b805e7ed63 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Audio; @@ -19,7 +20,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Testing; -using osu.Game.Beatmaps.Legacy; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Judgements; @@ -27,6 +27,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Configuration; +using osu.Game.Rulesets.Osu.Mods; namespace osu.Game.Rulesets.Osu.Tests { @@ -35,16 +36,26 @@ namespace osu.Game.Rulesets.Osu.Tests { private int depthIndex; - private readonly BindableBool snakingIn = new BindableBool(); - private readonly BindableBool snakingOut = new BindableBool(); + private readonly BindableBool snakingIn = new BindableBool(true); + private readonly BindableBool snakingOut = new BindableBool(true); - [SetUpSteps] - public void SetUpSteps() + private float progressToHit; + + protected override void LoadComplete() { - AddToggleStep("toggle snaking", v => + base.LoadComplete(); + + AddToggleStep("disable snaking", v => { - snakingIn.Value = v; - snakingOut.Value = v; + snakingIn.Value = !v; + snakingOut.Value = !v; + }); + + AddToggleStep("toggle hidden", hiddenActive => SelectedMods.Value = hiddenActive ? new[] { new OsuModHidden() } : Array.Empty()); + + AddSliderStep("hit at", 0f, 1f, 0f, v => + { + progressToHit = v; }); } @@ -56,6 +67,18 @@ namespace osu.Game.Rulesets.Osu.Tests config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut); } + protected override void Update() + { + base.Update(); + + foreach (var slider in this.ChildrenOfType()) + { + double completionProgress = Math.Clamp((Time.Current - slider.HitObject.StartTime) / slider.HitObject.Duration, 0, 1); + if (completionProgress > progressToHit && !slider.IsHit) + slider.HeadCircle.HitArea.Hit(); + } + } + [Test] public void TestVariousSliders() { @@ -206,7 +229,7 @@ namespace osu.Game.Rulesets.Osu.Tests StackHeight = 10 }; - return createDrawable(slider, 2, 2); + return createDrawable(slider, 2); } private Drawable testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats); @@ -229,6 +252,7 @@ namespace osu.Game.Rulesets.Osu.Tests { var slider = new Slider { + SliderVelocityMultiplier = speedMultiplier, StartTime = Time.Current + time_offset, Position = new Vector2(0, -(distance / 2)), Path = new SliderPath(PathType.PerfectCurve, new[] @@ -240,7 +264,7 @@ namespace osu.Game.Rulesets.Osu.Tests StackHeight = stackHeight }; - return createDrawable(slider, circleSize, speedMultiplier); + return createDrawable(slider, circleSize); } private Drawable testPerfect(int repeats = 0) @@ -258,7 +282,7 @@ namespace osu.Game.Rulesets.Osu.Tests RepeatCount = repeats, }; - return createDrawable(slider, 2, 3); + return createDrawable(slider, 2); } private Drawable testLinear(int repeats = 0) => createLinear(repeats); @@ -281,7 +305,7 @@ namespace osu.Game.Rulesets.Osu.Tests RepeatCount = repeats, }; - return createDrawable(slider, 2, 3); + return createDrawable(slider, 2); } private Drawable testBezier(int repeats = 0) => createBezier(repeats); @@ -303,7 +327,7 @@ namespace osu.Game.Rulesets.Osu.Tests RepeatCount = repeats, }; - return createDrawable(slider, 2, 3); + return createDrawable(slider, 2); } private Drawable testLinearOverlapping(int repeats = 0) => createOverlapping(repeats); @@ -326,7 +350,7 @@ namespace osu.Game.Rulesets.Osu.Tests RepeatCount = repeats, }; - return createDrawable(slider, 2, 3); + return createDrawable(slider, 2); } private Drawable testCatmull(int repeats = 0) => createCatmull(repeats); @@ -352,15 +376,12 @@ namespace osu.Game.Rulesets.Osu.Tests NodeSamples = repeatSamples }; - return createDrawable(slider, 3, 1); + return createDrawable(slider, 3); } - private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier) + private Drawable createDrawable(Slider slider, float circleSize) { - var cpi = new LegacyControlPointInfo(); - cpi.Add(0, new DifficultyControlPoint { SliderVelocity = speedMultiplier }); - - slider.ApplyDefaults(cpi, new BeatmapDifficulty + slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize, SliderTickRate = 3 diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderComboChange.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderComboChange.cs index dc8842a20a..863cc24920 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderComboChange.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderComboChange.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs index fc2e6d1f72..d4bb789a12 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs @@ -10,6 +10,7 @@ using osu.Game.Beatmaps; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; @@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Tests { const double time_slider_start = 1000; - float circleRadius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (circleSize - 5) / 5) / 2; + float circleRadius = OsuHitObject.OBJECT_RADIUS * LegacyRulesetExtensions.CalculateScaleFromCircleSize(circleSize, true); float followCircleRadius = circleRadius * 1.2f; performTest(new Beatmap @@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_slider_start, Position = new Vector2(0, 0), - SliderVelocity = velocity, + SliderVelocityMultiplier = velocity, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderHidden.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderHidden.cs index eb13995ad0..671285831f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderHidden.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 NUnit.Framework; using osu.Game.Rulesets.Osu.Mods; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index d83926ab9b..f718a5069f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.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.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -33,7 +32,140 @@ namespace osu.Game.Rulesets.Osu.Tests private const double time_during_slide_4 = 3800; private const double time_slider_end = 4000; - private List judgementResults; + private ScoreAccessibleReplayPlayer currentPlayer = null!; + + private const float slider_path_length = 25; + + private readonly List judgementResults = new List(); + + [TestCase(30, 0)] + [TestCase(30, 1)] + [TestCase(40, 0)] + [TestCase(40, 1)] + [TestCase(50, 1)] + [TestCase(60, 1)] + [TestCase(70, 1)] + [TestCase(80, 1)] + [TestCase(80, 0)] + [TestCase(80, 10)] + [TestCase(90, 1)] + [Ignore("headless test doesn't run at high enough precision for this to always enter a tracking state in time.")] + public void TestVeryShortSliderMissHead(float sliderLength, int repeatCount) + { + performTest(new List + { + new OsuReplayFrame { Position = new Vector2(50, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start - 10 }, + new OsuReplayFrame { Position = new Vector2(50, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 2000 }, + }, new Slider + { + StartTime = time_slider_start, + Position = new Vector2(0, 0), + SliderVelocityMultiplier = 10f, + RepeatCount = repeatCount, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(sliderLength, 0), + }), + }, 240, 1); + + AddAssert("Head judgement is first", () => judgementResults[0].HitObject is SliderHeadCircle); + AddAssert("Tail judgement is second last", () => judgementResults[^2].HitObject is SliderTailCircle); + AddAssert("Slider judgement is last", () => judgementResults[^1].HitObject is Slider); + } + + // Making these too short causes breakage from frames not being processed fast enough. + // To keep things simple, these tests are crafted to always be >16ms length. + // If sliders shorter than this are ever used in gameplay it will probably break things and we can revisit. + [TestCase(30, 0)] + [TestCase(30, 1)] + [TestCase(40, 0)] + [TestCase(40, 1)] + [TestCase(50, 1)] + [TestCase(60, 1)] + [TestCase(70, 1)] + [TestCase(80, 1)] + [TestCase(80, 0)] + [TestCase(80, 10)] + [TestCase(90, 1)] + [Ignore("headless test doesn't run at high enough precision for this to always enter a tracking state in time.")] + public void TestVeryShortSlider(float sliderLength, int repeatCount) + { + Slider slider; + + performTest(new List + { + new OsuReplayFrame { Position = new Vector2(10, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start - 10 }, + new OsuReplayFrame { Position = new Vector2(10, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 2000 }, + }, slider = new Slider + { + StartTime = time_slider_start, + Position = new Vector2(0, 0), + SliderVelocityMultiplier = 10f, + RepeatCount = repeatCount, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(sliderLength, 0), + }), + }, 240, 1); + + assertAllMaxJudgements(); + + AddAssert("Head judgement is first", () => judgementResults.First().HitObject is SliderHeadCircle); + + // Even if the last tick is hit early, the slider should always execute its final judgement at its endtime. + // If not, hitsounds will not play on time. + AddAssert("Judgement offset is zero", () => judgementResults.Last().TimeOffset == 0); + AddAssert("Slider judged at end time", () => judgementResults.Last().TimeAbsolute, () => Is.EqualTo(slider.EndTime)); + + AddAssert("Slider is last judgement", () => judgementResults[^1].HitObject, Is.TypeOf); + AddAssert("Tail is second last judgement", () => judgementResults[^2].HitObject, Is.TypeOf); + } + + [TestCase(300, false)] + [TestCase(200, true)] + [TestCase(150, true)] + [TestCase(120, true)] + [TestCase(60, true)] + [TestCase(10, true)] + [TestCase(0, true)] + [TestCase(-30, false)] + [Ignore("headless test doesn't run at high enough precision for this to always enter a tracking state in time.")] + public void TestTailLeniency(float finalPosition, bool hit) + { + Slider slider; + + performTest(new List + { + new OsuReplayFrame { Position = Vector2.Zero, Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start }, + new OsuReplayFrame { Position = new Vector2(finalPosition, slider_path_length * 3), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 20 }, + }, slider = new Slider + { + StartTime = time_slider_start, + Position = new Vector2(0, 0), + SliderVelocityMultiplier = 10f, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(slider_path_length * 10, 0), + new Vector2(slider_path_length * 10, slider_path_length * 3), + new Vector2(0, slider_path_length * 3), + }), + }, 240, 1); + + if (hit) + assertAllMaxJudgements(); + else + AddAssert("Tracking dropped", assertMidSliderJudgementFail); + + AddAssert("Head judgement is first", () => judgementResults.First().HitObject is SliderHeadCircle); + + // Even if the last tick is hit early, the slider should always execute its final judgement at its endtime. + // If not, hitsounds will not play on time. + AddAssert("Judgement offset is zero", () => judgementResults.Last().TimeOffset == 0); + AddAssert("Slider judged at end time", () => judgementResults.Last().TimeAbsolute, () => Is.EqualTo(slider.EndTime)); + } [Test] public void TestPressBothKeysSimultaneouslyAndReleaseOne() @@ -44,7 +176,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = Vector2.Zero, Actions = { OsuAction.RightButton }, Time = time_during_slide_1 }, }); - AddAssert("Tracking retained", assertMaxJudge); + assertAllMaxJudgements(); } /// @@ -86,7 +218,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_2 }, }); - AddAssert("Tracking retained", assertMaxJudge); + assertAllMaxJudgements(); } /// @@ -107,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_1 }, }); - AddAssert("Tracking retained", assertMaxJudge); + assertAllMaxJudgements(); } /// @@ -128,7 +260,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_1 }, }); - AddAssert("Tracking retained", assertMaxJudge); + assertAllMaxJudgements(); } /// @@ -301,7 +433,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(slider_path_length, OsuHitObject.OBJECT_RADIUS * 1.199f), Actions = { OsuAction.LeftButton }, Time = time_slider_end }, }); - AddAssert("Tracking kept", assertMaxJudge); + assertAllMaxJudgements(); } /// @@ -325,7 +457,13 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("Tracking dropped", assertMidSliderJudgementFail); } - private bool assertMaxJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == t.Judgement.MaxResult); + private void assertAllMaxJudgements() + { + AddAssert("All judgements max", () => + { + return judgementResults.Select(j => (j.HitObject, j.Type)); + }, () => Is.EqualTo(judgementResults.Select(j => (j.HitObject, j.Judgement.MaxResult)))); + } private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.SmallTickHit && !judgementResults.First().IsHit; @@ -333,35 +471,36 @@ namespace osu.Game.Rulesets.Osu.Tests private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.SmallTickMiss; - private ScoreAccessibleReplayPlayer currentPlayer; - - private const float slider_path_length = 25; - - private void performTest(List frames) + private void performTest(List frames, Slider? slider = null, double? bpm = null, int? tickRate = null) { + slider ??= new Slider + { + StartTime = time_slider_start, + Position = new Vector2(0, 0), + SliderVelocityMultiplier = 0.1f, + Path = new SliderPath(PathType.PerfectCurve, new[] + { + Vector2.Zero, + new Vector2(slider_path_length, 0), + }, slider_path_length), + }; + AddStep("load player", () => { + var cpi = new ControlPointInfo(); + + if (bpm != null) + cpi.Add(0, new TimingControlPoint { BeatLength = 60000 / bpm.Value }); + Beatmap.Value = CreateWorkingBeatmap(new Beatmap { - HitObjects = - { - new Slider - { - StartTime = time_slider_start, - Position = new Vector2(0, 0), - SliderVelocity = 0.1f, - Path = new SliderPath(PathType.PerfectCurve, new[] - { - Vector2.Zero, - new Vector2(slider_path_length, 0), - }, slider_path_length), - } - }, + HitObjects = { slider }, BeatmapInfo = { - Difficulty = new BeatmapDifficulty { SliderTickRate = 3 }, - Ruleset = new OsuRuleset().RulesetInfo + Difficulty = new BeatmapDifficulty { SliderTickRate = tickRate ?? 3 }, + Ruleset = new OsuRuleset().RulesetInfo, }, + ControlPointInfo = cpi, }); var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); @@ -375,7 +514,7 @@ namespace osu.Game.Rulesets.Osu.Tests }; LoadScreen(currentPlayer = p); - judgementResults = new List(); + judgementResults.Clear(); }); AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index 630049f408..13166c2b6b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.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.Collections.Generic; using System.Linq; @@ -33,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Tests public partial class TestSceneSliderSnaking : TestSceneOsuPlayer { [Resolved] - private AudioManager audioManager { get; set; } + private AudioManager audioManager { get; set; } = null!; protected override bool Autoplay => autoplay; private bool autoplay; @@ -41,12 +39,12 @@ namespace osu.Game.Rulesets.Osu.Tests private readonly BindableBool snakingIn = new BindableBool(); private readonly BindableBool snakingOut = new BindableBool(); - private IBeatmap beatmap; + private IBeatmap beatmap = null!; private const double duration_of_span = 3605; private const double fade_in_modifier = -1200; - protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) => new ClockBackedTestWorkingBeatmap(this.beatmap = beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); [BackgroundDependencyLoader] @@ -57,15 +55,8 @@ namespace osu.Game.Rulesets.Osu.Tests config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut); } - private Slider slider; - private DrawableSlider drawableSlider; - - [SetUp] - public void Setup() => Schedule(() => - { - slider = null; - drawableSlider = null; - }); + private Slider slider = null!; + private DrawableSlider? drawableSlider; protected override bool HasCustomSteps => true; @@ -135,9 +126,9 @@ namespace osu.Game.Rulesets.Osu.Tests } [Test] - public void TestRepeatArrowDoesNotMoveWhenHit() + public void TestRepeatArrowDoesNotMove([Values] bool useAutoplay) { - AddStep("enable autoplay", () => autoplay = true); + AddStep($"set autoplay to {useAutoplay}", () => autoplay = useAutoplay); setSnaking(true); CreateTest(); // repeat might have a chance to update its position depending on where in the frame its hit, @@ -145,21 +136,12 @@ namespace osu.Game.Rulesets.Osu.Tests addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame); } - [Test] - public void TestRepeatArrowMovesWhenNotHit() - { - AddStep("disable autoplay", () => autoplay = false); - setSnaking(true); - CreateTest(); - addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased); - } - private void retrieveSlider(int index) { AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]); addSeekStep(() => slider.StartTime); AddUntilStep("retrieve drawable slider", () => - (drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null); + (drawableSlider = (DrawableSlider?)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null); } private void addEnsureSnakingInSteps(Func startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionIncreased); @@ -179,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Tests private Func timeAtRepeat(Func startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex; private Func positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? getSliderStart : getSliderEnd; - private List getSliderCurve() => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; + private List getSliderCurve() => ((PlaySliderBody)drawableSlider!.Body.Drawable).CurrentCurve; private Vector2 getSliderStart() => getSliderCurve().First(); private Vector2 getSliderEnd() => getSliderCurve().Last(); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index 8cfd674f88..ea57a6a1b5 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Audio; @@ -26,6 +27,15 @@ namespace osu.Game.Rulesets.Osu.Tests private TestDrawableSpinner drawableSpinner; + private readonly BindableDouble spinRate = new BindableDouble(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddSliderStep("Spin rate", 0.5, 5, 1, val => spinRate.Value = val); + } + [TestCase(true)] [TestCase(false)] public void TestVariousSpinners(bool autoplay) @@ -43,7 +53,8 @@ namespace osu.Game.Rulesets.Osu.Tests AddUntilStep("Pitch starts low", () => getSpinningSample().Frequency.Value < 0.8); AddUntilStep("Pitch increases", () => getSpinningSample().Frequency.Value > 0.8); - PausableSkinnableSound getSpinningSample() => drawableSpinner.ChildrenOfType().FirstOrDefault(s => s.Samples.Any(i => i.LookupNames.Any(l => l.Contains("spinnerspin")))); + PausableSkinnableSound getSpinningSample() => + drawableSpinner.ChildrenOfType().FirstOrDefault(s => s.Samples.Any(i => i.LookupNames.Any(l => l.Contains("spinnerspin")))); } [TestCase(false)] @@ -64,6 +75,39 @@ namespace osu.Game.Rulesets.Osu.Tests AddUntilStep("Short spinner implicitly completes", () => drawableSpinner.Progress == 1); } + [TestCase(0, 4, 6)] + [TestCase(5, 7, 10)] + [TestCase(10, 11, 8)] + public void TestSpinnerSpinRequirements(int od, int normalTicks, int bonusTicks) + { + Spinner spinner = null; + + AddStep("add spinner", () => SetContents(_ => + { + spinner = new Spinner + { + StartTime = Time.Current, + EndTime = Time.Current + 3000, + Samples = new List + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL) + } + }; + + spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { OverallDifficulty = od }); + + return drawableSpinner = new TestDrawableSpinner(spinner, true, spinRate) + { + Anchor = Anchor.Centre, + Depth = depthIndex++, + Scale = new Vector2(0.75f) + }; + })); + + AddAssert("number of normal ticks matches", () => spinner.SpinsRequired, () => Is.EqualTo(normalTicks)); + AddAssert("number of bonus ticks matches", () => spinner.MaximumBonusSpins, () => Is.EqualTo(bonusTicks)); + } + private Drawable testSingle(float circleSize, bool auto = false, double length = 3000) { const double delay = 2000; @@ -80,7 +124,7 @@ namespace osu.Game.Rulesets.Osu.Tests spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize }); - drawableSpinner = new TestDrawableSpinner(spinner, auto) + drawableSpinner = new TestDrawableSpinner(spinner, auto, spinRate) { Anchor = Anchor.Centre, Depth = depthIndex++, @@ -96,18 +140,20 @@ namespace osu.Game.Rulesets.Osu.Tests private partial class TestDrawableSpinner : DrawableSpinner { private readonly bool auto; + private readonly BindableDouble spinRate; - public TestDrawableSpinner(Spinner s, bool auto) + public TestDrawableSpinner(Spinner s, bool auto, BindableDouble spinRate) : base(s) { this.auto = auto; + this.spinRate = spinRate; } protected override void Update() { base.Update(); if (auto) - RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * 2)); + RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * spinRate.Value)); } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs index 1ae17432be..dae81f4cff 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); AddStep("rotate some", () => dho.RotationTracker.AddRotation(180)); - AddAssert("rotation is set", () => dho.Result.RateAdjustedRotation == 180); + AddAssert("rotation is set", () => dho.Result.TotalRotation == 180); AddStep("apply new spinner", () => dho.Apply(prepareObject(new Spinner { @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Tests Duration = 1000, }))); - AddAssert("rotation is reset", () => dho.Result.RateAdjustedRotation == 0); + AddAssert("rotation is reset", () => dho.Result.TotalRotation == 0); } private Spinner prepareObject(Spinner circle) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerHidden.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerHidden.cs index 1aaba23e56..42cbb96813 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerHidden.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 NUnit.Framework; using osu.Game.Rulesets.Osu.Mods; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs new file mode 100644 index 0000000000..75bcd809c8 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs @@ -0,0 +1,350 @@ +// 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.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Storyboards; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public partial class TestSceneSpinnerInput : RateAdjustedBeatmapTestScene + { + private const int centre_x = 256; + private const int centre_y = 192; + private const double time_spinner_start = 1500; + private const double time_spinner_end = 8000; + + private readonly List judgementResults = new List(); + + private ScoreAccessibleReplayPlayer currentPlayer = null!; + private ManualClock? manualClock; + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) + { + return manualClock == null + ? base.CreateWorkingBeatmap(beatmap, storyboard) + : new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(manualClock), Audio); + } + + [SetUp] + public void Setup() => Schedule(() => + { + manualClock = null; + SelectedMods.Value = Array.Empty(); + }); + + /// + /// While off-centre, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms. + /// + [Test] + public void TestVibrateWithoutSpinningOffCentre() + { + List frames = new List(); + + const int vibrate_time = 50; + const float y_pos = centre_y - 50; + + int direction = -1; + + for (double i = time_spinner_start; i <= time_spinner_end; i += vibrate_time) + { + frames.Add(new OsuReplayFrame(i, new Vector2(centre_x + direction * 50, y_pos), OsuAction.LeftButton)); + frames.Add(new OsuReplayFrame(i + vibrate_time, new Vector2(centre_x - direction * 50, y_pos), OsuAction.LeftButton)); + + direction *= -1; + } + + performTest(frames); + + assertTicksHit(0); + assertSpinnerHit(false); + } + + /// + /// While centred on the slider, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms. + /// + [Test] + public void TestVibrateWithoutSpinningOnCentre() + { + List frames = new List(); + + const int vibrate_time = 50; + + int direction = -1; + + for (double i = time_spinner_start; i <= time_spinner_end; i += vibrate_time) + { + frames.Add(new OsuReplayFrame(i, new Vector2(centre_x + direction * 50, centre_y), OsuAction.LeftButton)); + frames.Add(new OsuReplayFrame(i + vibrate_time, new Vector2(centre_x - direction * 50, centre_y), OsuAction.LeftButton)); + + direction *= -1; + } + + performTest(frames); + + assertTicksHit(0); + assertSpinnerHit(false); + } + + [Test] + public void TestVibrateWithoutSpinningOnCentreWithDoubleTime() + { + List frames = new List(); + + const int rate = 2; + // the track clock is going to be playing twice as fast, + // so the vibration time in clock time needs to be twice as long + // to keep constant speed in real time. + const int vibrate_time = 50 * rate; + + int direction = -1; + + for (double i = time_spinner_start; i <= time_spinner_end; i += vibrate_time) + { + frames.Add(new OsuReplayFrame(i, new Vector2(centre_x + direction * 50, centre_y), OsuAction.LeftButton)); + frames.Add(new OsuReplayFrame(i + vibrate_time, new Vector2(centre_x - direction * 50, centre_y), OsuAction.LeftButton)); + + direction *= -1; + } + + AddStep("set DT", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = rate } } }); + performTest(frames); + + assertSpinnerHit(false); + } + + /// + /// Spins in a single direction. + /// + [TestCase(180, 0)] + [TestCase(-180, 0)] + [TestCase(360, 1)] + [TestCase(-360, 1)] + [TestCase(540, 1)] + [TestCase(-540, 1)] + [TestCase(720, 2)] + [TestCase(-720, 2)] + public void TestSpinSingleDirection(float amount, int expectedTicks) + { + performTest(new SpinFramesGenerator(time_spinner_start) + .Spin(amount, 500) + .Build()); + + assertTicksHit(expectedTicks); + assertSpinnerHit(false); + } + + /// + /// Spin half-way clockwise then perform one full spin counter-clockwise. + /// No ticks should be hit since the total rotation is -0.5 (0.5 CW + 1 CCW = 0.5 CCW). + /// + [Test] + public void TestSpinHalfBothDirections() + { + performTest(new SpinFramesGenerator(time_spinner_start) + .Spin(180, 500) // Rotate to +0.5. + .Spin(-360, 500) // Rotate to -0.5 + .Build()); + + assertTicksHit(0); + assertSpinnerHit(false); + } + + /// + /// Spin in one direction then spin in the other. + /// + [TestCase(180, -540, 1)] + [TestCase(-180, 540, 1)] + [TestCase(180, -900, 2)] + [TestCase(-180, 900, 2)] + public void TestSpinOneDirectionThenChangeDirection(float direction1, float direction2, int expectedTicks) + { + performTest(new SpinFramesGenerator(time_spinner_start) + .Spin(direction1, 500) + .Spin(direction2, 500) + .Build()); + + assertTicksHit(expectedTicks); + assertSpinnerHit(false); + } + + [Test] + public void TestRewind() + { + AddStep("set manual clock", () => manualClock = new ManualClock + { + // Avoids interpolation trying to run ahead during testing. + Rate = 0 + }); + + List frames = + new SpinFramesGenerator(time_spinner_start) + // 1500ms start + .Spin(360, 500) + // 2000ms -> 1 full CW spin + .Spin(-180, 500) + // 2500ms -> 1 full CW spin + 0.5 CCW spins + .Spin(90, 500) + // 3000ms -> 1 full CW spin + 0.25 CCW spins + .Spin(450, 500) + // 3500ms -> 2 full CW spins + .Spin(180, 500) + // 4000ms -> 2 full CW spins + 0.5 CW spins + .Build(); + + loadPlayer(frames); + + GameplayClockContainer clock = null!; + DrawableRuleset drawableRuleset = null!; + AddStep("get gameplay objects", () => + { + clock = currentPlayer.ChildrenOfType().Single(); + drawableRuleset = currentPlayer.ChildrenOfType().Single(); + }); + + addSeekStep(frames.Last().Time); + + DrawableSpinner drawableSpinner = null!; + AddUntilStep("get spinner", () => (drawableSpinner = currentPlayer.ChildrenOfType().Single()) != null); + + assertFinalRotationCorrect(); + assertTotalRotation(3750, 810); + assertTotalRotation(3500, 720); + assertTotalRotation(3250, 530); + assertTotalRotation(3000, 450); + assertTotalRotation(2750, 540); + assertTotalRotation(2500, 540); + assertTotalRotation(2250, 450); + assertTotalRotation(2000, 360); + assertTotalRotation(1500, 0); + + // same thing but always returning to final time to check. + assertFinalRotationCorrect(); + assertTotalRotation(3750, 810); + assertFinalRotationCorrect(); + assertTotalRotation(3500, 720); + assertFinalRotationCorrect(); + assertTotalRotation(3250, 530); + assertFinalRotationCorrect(); + assertTotalRotation(3000, 450); + assertFinalRotationCorrect(); + assertTotalRotation(2750, 540); + assertFinalRotationCorrect(); + assertTotalRotation(2500, 540); + assertFinalRotationCorrect(); + assertTotalRotation(2250, 450); + assertFinalRotationCorrect(); + assertTotalRotation(2000, 360); + assertFinalRotationCorrect(); + assertTotalRotation(1500, 0); + + void assertTotalRotation(double time, float expected) + { + addSeekStep(time); + AddAssert($"total rotation @ {time} is {expected}", () => drawableSpinner.Result.TotalRotation, + () => Is.EqualTo(expected).Within(MathHelper.RadiansToDegrees(SpinFramesGenerator.SPIN_ERROR * 2))); + } + + void addSeekStep(double time) + { + AddStep($"seek to {time}", () => clock.Seek(time)); + // Lenience is required due to interpolation running slightly ahead on a stalled clock. + AddUntilStep("wait for seek to finish", () => drawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time)); + } + + void assertFinalRotationCorrect() => assertTotalRotation(4000, 900); + } + + private void assertTicksHit(int count) + { + AddAssert($"{count} ticks hit", () => judgementResults.Where(r => r.HitObject is SpinnerTick).Count(r => r.IsHit), () => Is.EqualTo(count)); + } + + private void assertSpinnerHit(bool shouldBeHit) + { + AddAssert($"spinner is {(shouldBeHit ? "hit" : "missed")}", () => judgementResults.Single(r => r.HitObject is Spinner).IsHit, () => Is.EqualTo(shouldBeHit)); + } + + private void loadPlayer(List frames) + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + HitObjects = + { + new Spinner + { + StartTime = time_spinner_start, + EndTime = time_spinner_end, + Position = new Vector2(centre_x, centre_y) + } + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty(), + Ruleset = new OsuRuleset().RulesetInfo + }, + }); + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + }; + + LoadScreen(currentPlayer = p); + judgementResults.Clear(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + } + + private void performTest(List frames) + { + loadPlayer(frames); + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); + } + + private partial class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs new file mode 100644 index 0000000000..8d8c2e9639 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs @@ -0,0 +1,127 @@ +// 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.Framework.Extensions.TypeExtensions; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public partial class TestSceneSpinnerJudgement : RateAdjustedBeatmapTestScene + { + private const double time_spinner_start = 2000; + private const double time_spinner_end = 4000; + + private List judgementResults = new List(); + private ScoreAccessibleReplayPlayer currentPlayer = null!; + + [Test] + public void TestHitNothing() + { + performTest(new List()); + + AddAssert("all min judgements", () => judgementResults.All(result => result.Type == result.Judgement.MinResult)); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(5)] + public void TestNumberOfSpins(int spins) + { + performTest(generateReplay(spins)); + + for (int i = 0; i < spins; ++i) + assertResult(i, HitResult.SmallBonus); + + assertResult(spins, HitResult.IgnoreMiss); + } + + [Test] + public void TestHitEverything() + { + performTest(generateReplay(20)); + + AddAssert("all max judgements", () => judgementResults.All(result => result.Type == result.Judgement.MaxResult)); + } + + private static List generateReplay(int spins) => new SpinFramesGenerator(time_spinner_start) + .Spin(spins * 360, time_spinner_end - time_spinner_start) + .Build(); + + private void performTest(List frames) + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + HitObjects = + { + new Spinner + { + StartTime = time_spinner_start, + EndTime = time_spinner_end, + Position = OsuPlayfield.BASE_SIZE / 2 + } + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty(), + Ruleset = new OsuRuleset().RulesetInfo + }, + }); + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + }; + + LoadScreen(currentPlayer = p); + judgementResults = new List(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); + } + + private void assertResult(int index, HitResult expectedResult) + { + AddAssert($"{typeof(T).ReadableName()} ({index}) judged as {expectedResult}", + () => judgementResults.Where(j => j.HitObject is T).OrderBy(j => j.HitObject.StartTime).ElementAt(index).Type, + () => Is.EqualTo(expectedResult)); + } + + private partial class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 116c974f32..8711aa9c09 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -63,11 +63,11 @@ namespace osu.Game.Rulesets.Osu.Tests trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f); }); AddAssert("is disc rotation not almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.Not.EqualTo(0).Within(100)); - AddAssert("is disc rotation absolute not almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.Not.EqualTo(0).Within(100)); + AddAssert("is disc rotation absolute not almost 0", () => drawableSpinner.Result.TotalRotation, () => Is.Not.EqualTo(0).Within(100)); addSeekStep(0); AddAssert("is disc rotation almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(0).Within(trackerRotationTolerance)); - AddAssert("is disc rotation absolute almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(0).Within(100)); + AddAssert("is disc rotation absolute almost 0", () => drawableSpinner.Result.TotalRotation, () => Is.EqualTo(0).Within(100)); } [Test] @@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Osu.Tests finalTrackerRotation = drawableSpinner.RotationTracker.Rotation; trackerRotationTolerance = Math.Abs(finalTrackerRotation * 0.05f); }); - AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation); + AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.TotalRotation); addSeekStep(spinner_start_time + 2500); AddAssert("disc rotation rewound", @@ -92,13 +92,13 @@ namespace osu.Game.Rulesets.Osu.Tests () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation / 2).Within(trackerRotationTolerance)); AddAssert("is cumulative rotation rewound", // cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error. - () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation / 2).Within(100)); + () => drawableSpinner.Result.TotalRotation, () => Is.EqualTo(finalCumulativeTrackerRotation / 2).Within(100)); addSeekStep(spinner_start_time + 5000); AddAssert("is disc rotation almost same", () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation).Within(trackerRotationTolerance)); AddAssert("is cumulative rotation almost same", - () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation).Within(100)); + () => drawableSpinner.Result.TotalRotation, () => Is.EqualTo(finalCumulativeTrackerRotation).Within(100)); } [Test] @@ -135,7 +135,7 @@ namespace osu.Game.Rulesets.Osu.Tests { // multipled by 2 to nullify the score multiplier. (autoplay mod selected) long totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; - return totalScore == (int)(drawableSpinner.Result.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; + return totalScore == (int)(drawableSpinner.Result.TotalRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; }); addSeekStep(0); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs index f4257a9ee7..3475680c71 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs @@ -284,15 +284,16 @@ namespace osu.Game.Rulesets.Osu.Tests }, }; - performTest(hitObjects, new List + List frames = new List { new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, - }); + }; + + frames.AddRange(new SpinFramesGenerator(time_spinner + 10) + .Spin(360, 500) + .Build()); + + performTest(hitObjects, frames); addJudgementAssert(hitObjects[0], HitResult.Great); addJudgementAssert(hitObjects[1], HitResult.Great); @@ -336,6 +337,52 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementAssert(hitObjects[1], HitResult.IgnoreHit); } + [Test] + public void TestInputFallsThroughJudgedSliders() + { + const double time_first_slider = 1000; + const double time_second_slider = 1250; + Vector2 positionFirstSlider = new Vector2(100, 50); + Vector2 positionSecondSlider = new Vector2(100, 80); + var midpoint = (positionFirstSlider + positionSecondSlider) / 2; + + var hitObjects = new List + { + new TestSlider + { + StartTime = time_first_slider, + Position = positionFirstSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + }, + new TestSlider + { + StartTime = time_second_slider, + Position = positionSecondSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_first_slider + 50, Position = midpoint }, + }); + + addJudgementAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, HitResult.Great); + addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0); + addJudgementAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Great); + addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, -200); + } + private void addJudgementAssert(OsuHitObject hitObject, HitResult result) { AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}", @@ -354,6 +401,12 @@ namespace osu.Game.Rulesets.Osu.Tests () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100)); } + private void addJudgementOffsetAssert(string name, Func hitObject, double offset) + { + AddAssert($"{name} @ judged at {offset}", + () => judgementResults.Single(r => r.HitObject == hitObject()).TimeOffset, () => Is.EqualTo(offset).Within(50)); + } + private ScoreAccessibleReplayPlayer currentPlayer; private List judgementResults; @@ -399,7 +452,7 @@ namespace osu.Game.Rulesets.Osu.Tests { public TestSlider() { - SliderVelocity = 0.1f; + SliderVelocityMultiplier = 0.1f; DefaultsApplied += _ => { diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index 57900bffd7..ea033cda45 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -1,10 +1,10 @@  - + - + WinExe diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs index df146a9511..a5282877ee 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.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.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index d03ee81f0d..3c051a6bb1 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.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 osuTK; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; @@ -46,12 +44,11 @@ namespace osu.Game.Rulesets.Osu.Beatmaps Position = positionData?.Position ?? Vector2.Zero, NewCombo = comboData?.NewCombo ?? false, ComboOffset = comboData?.ComboOffset ?? 0, - LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset, // prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance. // this results in more (or less) ticks being generated in . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics; using osu.Game.Beatmaps; @@ -216,17 +214,24 @@ namespace osu.Game.Rulesets.Osu.Beatmaps ? currSlider.Position + currSlider.Path.PositionAt(1) : currHitObject.Position; + // Note the use of `StartTime` in the code below doesn't match stable's use of `EndTime`. + // This is because in the stable implementation, `UpdateCalculations` is not called on the inner-loop hitobject (j) + // and therefore it does not have a correct `EndTime`, but instead the default of `EndTime = StartTime`. + // + // Effects of this can be seen on https://osu.ppy.sh/beatmapsets/243#osu/1146 at sliders around 86647 ms, where + // if we use `EndTime` here it would result in unexpected stacking. + if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.Position) < stack_distance) { currHitObject.StackHeight++; - startTime = beatmap.HitObjects[j].GetEndTime(); + startTime = beatmap.HitObjects[j].StartTime; } else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, position2) < stack_distance) { // Case for sliders - bump notes down and right, rather than up and left. sliderStack++; beatmap.HitObjects[j].StackHeight -= sliderStack; - startTime = beatmap.HitObjects[j].GetEndTime(); + startTime = beatmap.HitObjects[j].StartTime; } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index 6d1b4d1a15..3d1939acac 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; @@ -26,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators /// and slider difficulty. /// /// - public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliders) + public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliderTravelDistance) { if (current.BaseObject is Spinner || current.Index <= 1 || current.Previous(0).BaseObject is Spinner) return 0; @@ -39,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime; // But if the last object is a slider, then we extend the travel velocity through the slider into the current object. - if (osuLastObj.BaseObject is Slider && withSliders) + if (osuLastObj.BaseObject is Slider && withSliderTravelDistance) { double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime; // calculate the slider velocity from slider head to slider end. double movementVelocity = osuCurrObj.MinimumJumpDistance / osuCurrObj.MinimumJumpTime; // calculate the movement velocity from slider end to current object @@ -50,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // As above, do the same for the previous hitobject. double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime; - if (osuLastLastObj.BaseObject is Slider && withSliders) + if (osuLastLastObj.BaseObject is Slider && withSliderTravelDistance) { double travelVelocity = osuLastLastObj.TravelDistance / osuLastLastObj.TravelTime; double movementVelocity = osuLastObj.MinimumJumpDistance / osuLastObj.MinimumJumpTime; @@ -124,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier); // Add in additional slider velocity bonus. - if (withSliders) + if (withSliderTravelDistance) aimStrain += sliderBonus * slider_multiplier; return aimStrain; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index dabbfcd2fb..5cb5a8f934 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index 3bec2346ce..05939bb3ab 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index c98f875eb5..2df383aaa8 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; @@ -32,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // derive strainTime for calculation var osuCurrObj = (OsuDifficultyHitObject)current; var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null; - var osuNextObj = (OsuDifficultyHitObject)current.Next(0); + var osuNextObj = (OsuDifficultyHitObject?)current.Next(0); double strainTime = osuCurrObj.StrainTime; double doubletapness = 1; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 03540abddb..24d5635104 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -93,7 +93,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_SPEED, SpeedDifficulty); yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty); yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate); - yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); yield return (ATTRIB_ID_DIFFICULTY, StarRating); if (ShouldSerializeFlashlightRating()) @@ -111,7 +110,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty SpeedDifficulty = values[ATTRIB_ID_SPEED]; OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY]; ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; - MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; StarRating = values[ATTRIB_ID_DIFFICULTY]; FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT); SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR]; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 1e83d6d820..3b580a5b59 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -71,7 +71,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1 ); - double starRating = basePerformance > 0.00001 ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; + double starRating = basePerformance > 0.00001 + ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) + : 0; double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double drainRate = beatmap.Difficulty.DrainRate; @@ -86,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate; - return new OsuDifficultyAttributes + OsuDifficultyAttributes attributes = new OsuDifficultyAttributes { StarRating = starRating, Mods = mods, @@ -103,6 +105,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty SliderCount = sliderCount, SpinnerCount = spinnerCount, }; + + return attributes; } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs new file mode 100644 index 0000000000..3a905d77b1 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs @@ -0,0 +1,234 @@ +// 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.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; + +namespace osu.Game.Rulesets.Osu.Difficulty +{ + internal class OsuLegacyScoreSimulator : ILegacyScoreSimulator + { + private int legacyBonusScore; + private int standardisedBonusScore; + private int combo; + + private double scoreMultiplier; + + public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap) + { + IBeatmap baseBeatmap = workingBeatmap.Beatmap; + + int countNormal = 0; + int countSlider = 0; + int countSpinner = 0; + + foreach (HitObject obj in workingBeatmap.Beatmap.HitObjects) + { + switch (obj) + { + case IHasPath: + countSlider++; + break; + + case IHasDuration: + countSpinner++; + break; + + default: + countNormal++; + break; + } + } + + int objectCount = countNormal + countSlider + countSpinner; + + int drainLength = 0; + + if (baseBeatmap.HitObjects.Count > 0) + { + int breakLength = baseBeatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum(); + drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000; + } + + int difficultyPeppyStars = (int)Math.Round( + (baseBeatmap.Difficulty.DrainRate + + baseBeatmap.Difficulty.OverallDifficulty + + baseBeatmap.Difficulty.CircleSize + + Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5); + + scoreMultiplier = difficultyPeppyStars; + + LegacyScoreAttributes attributes = new LegacyScoreAttributes(); + + foreach (var obj in playableBeatmap.HitObjects) + simulateHit(obj, ref attributes); + + attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore; + + return attributes; + } + + private void simulateHit(HitObject hitObject, ref LegacyScoreAttributes attributes) + { + bool increaseCombo = true; + bool addScoreComboMultiplier = false; + + bool isBonus = false; + HitResult bonusResult = HitResult.None; + + int scoreIncrease = 0; + + switch (hitObject) + { + case SliderHeadCircle: + case SliderTailCircle: + case SliderRepeat: + scoreIncrease = 30; + break; + + case SliderTick: + scoreIncrease = 10; + break; + + case SpinnerBonusTick: + scoreIncrease = 1100; + increaseCombo = false; + isBonus = true; + bonusResult = HitResult.LargeBonus; + break; + + case SpinnerTick: + scoreIncrease = 100; + increaseCombo = false; + isBonus = true; + bonusResult = HitResult.SmallBonus; + break; + + case HitCircle: + scoreIncrease = 300; + addScoreComboMultiplier = true; + break; + + case Slider: + foreach (var nested in hitObject.NestedHitObjects) + simulateHit(nested, ref attributes); + + scoreIncrease = 300; + increaseCombo = false; + addScoreComboMultiplier = true; + break; + + case Spinner spinner: + // The spinner object applies a lenience because gameplay mechanics differ from osu-stable. + // We'll redo the calculations to match osu-stable here... + const double maximum_rotations_per_second = 477.0 / 60; + + // Normally, this value depends on the final overall difficulty. For simplicity, we'll only consider the worst case that maximises bonus score. + // As we're primarily concerned with computing the maximum theoretical final score, + // this will have the final effect of slightly underestimating bonus score achieved on stable when converting from score V1. + const double minimum_rotations_per_second = 3; + + double secondsDuration = spinner.Duration / 1000; + + // The total amount of half spins possible for the entire spinner. + int totalHalfSpinsPossible = (int)(secondsDuration * maximum_rotations_per_second * 2); + // The amount of half spins that are required to successfully complete the spinner (i.e. get a 300). + int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimum_rotations_per_second); + // To be able to receive bonus points, the spinner must be rotated another 1.5 times. + int halfSpinsRequiredBeforeBonus = halfSpinsRequiredForCompletion + 3; + + for (int i = 0; i <= totalHalfSpinsPossible; i++) + { + if (i > halfSpinsRequiredBeforeBonus && (i - halfSpinsRequiredBeforeBonus) % 2 == 0) + simulateHit(new SpinnerBonusTick(), ref attributes); + else if (i > 1 && i % 2 == 0) + simulateHit(new SpinnerTick(), ref attributes); + } + + scoreIncrease = 300; + addScoreComboMultiplier = true; + break; + } + + if (addScoreComboMultiplier) + { + // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) + attributes.ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier)); + } + + if (isBonus) + { + legacyBonusScore += scoreIncrease; + standardisedBonusScore += Judgement.ToNumericResult(bonusResult); + } + else + attributes.AccuracyScore += scoreIncrease; + + if (increaseCombo) + combo++; + } + + public double GetLegacyScoreMultiplier(IReadOnlyList mods, LegacyBeatmapConversionDifficultyInfo difficulty) + { + bool scoreV2 = mods.Any(m => m is ModScoreV2); + + double multiplier = 1.0; + + foreach (var mod in mods) + { + switch (mod) + { + case OsuModNoFail: + multiplier *= scoreV2 ? 1.0 : 0.5; + break; + + case OsuModEasy: + multiplier *= 0.5; + break; + + case OsuModHalfTime: + case OsuModDaycore: + multiplier *= 0.3; + break; + + case OsuModHidden: + multiplier *= 1.06; + break; + + case OsuModHardRock: + multiplier *= scoreV2 ? 1.10 : 1.06; + break; + + case OsuModDoubleTime: + case OsuModNightcore: + multiplier *= scoreV2 ? 1.20 : 1.12; + break; + + case OsuModFlashlight: + multiplier *= 1.12; + break; + + case OsuModSpunOut: + multiplier *= 0.9; + break; + + case OsuModRelax: + case OsuModAutopilot: + return 0; + } + } + + return multiplier; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index efb3ade220..0aeaf7669f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.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.Collections.Generic; using Newtonsoft.Json; using osu.Game.Rulesets.Difficulty; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 30b56ff769..b31f4ff519 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 6aea00fd35..488d1e2e9f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -1,10 +1,9 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Mods; @@ -84,13 +83,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing /// public double HitWindowGreat { get; private set; } - private readonly OsuHitObject lastLastObject; + private readonly OsuHitObject? lastLastObject; private readonly OsuHitObject lastObject; - public OsuDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, List objects, int index) + public OsuDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject? lastLastObject, double clockRate, List objects, int index) : base(hitObject, lastObject, clockRate, objects, index) { - this.lastLastObject = (OsuHitObject)lastLastObject; + this.lastLastObject = lastLastObject as OsuHitObject; this.lastObject = (OsuHitObject)lastObject; // Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects. @@ -216,7 +215,45 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing if (slider.LazyEndPosition != null) return; - slider.LazyTravelTime = slider.NestedHitObjects[^1].StartTime - slider.StartTime; + // TODO: This commented version is actually correct by the new lazer implementation, but intentionally held back from + // difficulty calculator to preserve known behaviour. + // double trackingEndTime = Math.Max( + // // SliderTailCircle always occurs at the final end time of the slider, but the player only needs to hold until within a lenience before it. + // slider.Duration + SliderEventGenerator.TAIL_LENIENCY, + // // There's an edge case where one or more ticks/repeats fall within that leniency range. + // // In such a case, the player needs to track until the final tick or repeat. + // slider.NestedHitObjects.LastOrDefault(n => n is not SliderTailCircle)?.StartTime ?? double.MinValue + // ); + + double trackingEndTime = Math.Max( + slider.StartTime + slider.Duration + SliderEventGenerator.TAIL_LENIENCY, + slider.StartTime + slider.Duration / 2 + ); + + IList nestedObjects = slider.NestedHitObjects; + + SliderTick? lastRealTick = slider.NestedHitObjects.OfType().LastOrDefault(); + + if (lastRealTick?.StartTime > trackingEndTime) + { + trackingEndTime = lastRealTick.StartTime; + + // When the last tick falls after the tracking end time, we need to re-sort the nested objects + // based on time. This creates a somewhat weird ordering which is counter to how a user would + // understand the slider, but allows a zero-diff with known diffcalc output. + // + // To reiterate, this is definitely not correct from a difficulty calculation perspective + // and should be revisited at a later date (likely by replacing this whole code with the commented + // version above). + List reordered = nestedObjects.ToList(); + + reordered.Remove(lastRealTick); + reordered.Add(lastRealTick); + + nestedObjects = reordered; + } + + slider.LazyTravelTime = trackingEndTime - slider.StartTime; double endTimeMin = slider.LazyTravelTime / slider.SpanDuration; if (endTimeMin % 2 >= 1) @@ -225,12 +262,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing endTimeMin %= 1; slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived. - var currCursorPosition = slider.StackedPosition; + + Vector2 currCursorPosition = slider.StackedPosition; + double scalingFactor = NORMALISED_RADIUS / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used. - for (int i = 1; i < slider.NestedHitObjects.Count; i++) + for (int i = 1; i < nestedObjects.Count; i++) { - var currMovementObj = (OsuHitObject)slider.NestedHitObjects[i]; + var currMovementObj = (OsuHitObject)nestedObjects[i]; Vector2 currMovement = Vector2.Subtract(currMovementObj.StackedPosition, currCursorPosition); double currMovementLength = scalingFactor * currMovement.Length; @@ -238,7 +277,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing // Amount of movement required so that the cursor position needs to be updated. double requiredMovement = assumed_slider_radius; - if (i == slider.NestedHitObjects.Count - 1) + if (i == nestedObjects.Count - 1) { // The end of a slider has special aim rules due to the relaxed time constraint on position. // There is both a lazy end position as well as the actual end slider position. We assume the player takes the simpler movement. @@ -265,7 +304,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing slider.LazyTravelDistance += (float)currMovementLength; } - if (i == slider.NestedHitObjects.Count - 1) + if (i == nestedObjects.Count - 1) slider.LazyEndPosition = currCursorPosition; } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 38e0e5b677..3f6b22bbb1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.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 osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 40448c444c..3d6d3f99c1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.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.Game.Rulesets.Difficulty.Preprocessing; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index d6683ade05..15b20a5572 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.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.Collections.Generic; using osu.Game.Rulesets.Difficulty.Skills; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index efe0e136bf..40aac013ab 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.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 osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/BlueprintPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/BlueprintPiece.cs index 41ab5a81b8..cdd11c53d2 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/BlueprintPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/BlueprintPiece.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Objects; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs index e5cc8595d1..3cba0610a1 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components { Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; InternalChild = content = new Container { diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs index 1fed19da03..c585f09b00 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; @@ -18,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components { Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; CornerRadius = Size.X / 2; CornerExponent = 2; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 26d18c7a17..20ad99baa2 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs index 1b3e7f3a8f..0608f8c929 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.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 osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs index d6409279a4..8e8972a665 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -17,14 +15,20 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints where T : OsuHitObject { [Resolved] - private EditorClock editorClock { get; set; } + private EditorClock editorClock { get; set; } = null!; protected new DrawableOsuHitObject DrawableObject => (DrawableOsuHitObject)base.DrawableObject; protected override bool AlwaysShowWhenSelected => true; protected override bool ShouldBeAlive => base.ShouldBeAlive - || (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION); + || (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime + && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION); + + public override bool IsSelectable => + // Bypass fade out extension from hit markers for selection purposes. + // This is to match stable, where even when the afterimage hit markers are still visible, objects are not selectable. + base.ShouldBeAlive; protected OsuSelectionBlueprint(T hitObject) : base(hitObject) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index c56ffcb140..f891d23bbd 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -47,7 +47,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public Action> SplitControlPointsRequested; [Resolved(CanBeNull = true)] - private IDistanceSnapProvider snapProvider { get; set; } + private IPositionSnapProvider positionSnapProvider { get; set; } + + [Resolved(CanBeNull = true)] + private IDistanceSnapProvider distanceSnapProvider { get; set; } public PathControlPointVisualiser(T hitObject, bool allowSelection) { @@ -288,10 +291,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (selectedControlPoints.Contains(hitObject.Path.ControlPoints[0])) { // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account - Vector2 newHeadPosition = Parent.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); - var result = snapProvider?.FindSnappedPositionAndTime(newHeadPosition); + Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); + var result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition); - Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position; + Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position; hitObject.Position += movementDelta; hitObject.StartTime = result?.Time ?? hitObject.StartTime; @@ -309,9 +312,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } else { - var result = snapProvider?.FindSnappedPositionAndTime(Parent.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids); + var result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids); - Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? Parent.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; + Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; for (int i = 0; i < controlPoints.Count; ++i) { @@ -322,7 +325,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } // Snap the path to the current beat divisor before checking length validity. - hitObject.SnapTo(snapProvider); + hitObject.SnapTo(distanceSnapProvider); if (!hitObject.Path.HasValidLength) { @@ -332,7 +335,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components hitObject.Position = oldPosition; hitObject.StartTime = oldStartTime; // Snap the path length again to undo the invalid length. - hitObject.SnapTo(snapProvider); + hitObject.SnapTo(distanceSnapProvider); return; } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs index 68a44eb2f8..075e9e6aa1 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Game.Graphics; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs index 3341a632c1..d47cf6bf23 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 966092c6fe..9b6adc04cf 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -39,7 +39,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private int currentSegmentLength; [Resolved(CanBeNull = true)] - private IDistanceSnapProvider snapProvider { get; set; } + private IPositionSnapProvider positionSnapProvider { get; set; } + + [Resolved(CanBeNull = true)] + private IDistanceSnapProvider distanceSnapProvider { get; set; } protected override bool IsValidForPlacement => HitObject.Path.HasValidLength; @@ -85,9 +88,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders BeginPlacement(); double? nearestSliderVelocity = (editorBeatmap.HitObjects - .LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocity; + .LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocityMultiplier; - HitObject.SliderVelocity = nearestSliderVelocity ?? 1; + HitObject.SliderVelocityMultiplier = nearestSliderVelocity ?? 1; HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); // Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation. @@ -198,7 +201,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } // Update the cursor position. - var result = snapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.Body ? SnapType.GlobalGrids : SnapType.All); + var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.Body ? SnapType.GlobalGrids : SnapType.All); cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position; } else if (cursor != null) @@ -230,7 +233,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void updateSlider() { - HitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; + HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; bodyPiece.UpdateFrom(HitObject); headCirclePiece.UpdateFrom(HitObject.HeadCircle); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 6685507ee0..80c4cee7f2 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -40,7 +40,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders protected PathControlPointVisualiser ControlPointVisualiser { get; private set; } [Resolved(CanBeNull = true)] - private IDistanceSnapProvider snapProvider { get; set; } + private IPositionSnapProvider positionSnapProvider { get; set; } + + [Resolved(CanBeNull = true)] + private IDistanceSnapProvider distanceSnapProvider { get; set; } [Resolved(CanBeNull = true)] private IPlacementHandler placementHandler { get; set; } @@ -194,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { if (placementControlPoint != null) { - var result = snapProvider?.FindSnappedPositionAndTime(ToScreenSpace(e.MousePosition)); + var result = positionSnapProvider?.FindSnappedPositionAndTime(ToScreenSpace(e.MousePosition)); placementControlPoint.Position = ToLocalSpace(result?.ScreenSpacePosition ?? ToScreenSpace(e.MousePosition)) - HitObject.Position; } } @@ -245,7 +248,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders // Move the control points from the insertion index onwards to make room for the insertion controlPoints.Insert(insertionIndex, pathControlPoint); - HitObject.SnapTo(snapProvider); + HitObject.SnapTo(distanceSnapProvider); return pathControlPoint; } @@ -267,7 +270,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } // Snap the slider to the current beat divisor before checking length validity. - HitObject.SnapTo(snapProvider); + HitObject.SnapTo(distanceSnapProvider); // If there are 0 or 1 remaining control points, or the slider has an invalid length, it is in a degenerate form and should be deleted if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength) @@ -315,7 +318,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders StartTime = HitObject.StartTime, Position = HitObject.Position + splitControlPoints[0].Position, NewCombo = HitObject.NewCombo, - LegacyLastTickOffset = HitObject.LegacyLastTickOffset, Samples = HitObject.Samples.Select(s => s.With()).ToList(), RepeatCount = HitObject.RepeatCount, NodeSamples = HitObject.NodeSamples.Select(n => (IList)n.Select(s => s.With()).ToList()).ToList(), diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs index cc58acdc80..17e838b4e7 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerSelectionBlueprint.cs index a80ec68c10..b273292f8a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerSelectionBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components; using osu.Game.Rulesets.Osu.Objects; using osuTK; diff --git a/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs index 69187875d7..c41ae10b2e 100644 --- a/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs +++ b/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs index 2c67f0bf97..325e9ed4cb 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs index 173a664902..54c54fca17 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; @@ -22,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Edit protected override SelectionHandler CreateSelectionHandler() => new OsuSelectionHandler(); - public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) + public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject) { switch (hitObject) { diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs new file mode 100644 index 0000000000..522943df7d --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public partial class OsuDistanceSnapProvider : ComposerDistanceSnapProvider + { + protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after) + { + float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime()); + float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position); + + return actualDistance / expectedDistance; + } + + protected override bool AdjustDistanceSpacing(GlobalAction action, float amount) + { + // To allow better visualisation, ensure that the spacing grid is visible before adjusting. + DistanceSnapToggle.Value = TernaryState.True; + + return base.AdjustDistanceSpacing(action, amount); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index aac5f6ffb1..0f8c960b65 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -17,7 +17,6 @@ using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; -using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; @@ -30,14 +29,14 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Edit { - public partial class OsuHitObjectComposer : DistancedHitObjectComposer + public partial class OsuHitObjectComposer : HitObjectComposer { public OsuHitObjectComposer(Ruleset ruleset) : base(ruleset) { } - protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) => new DrawableOsuEditorRuleset(ruleset, beatmap, mods); protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] @@ -49,25 +48,29 @@ namespace osu.Game.Rulesets.Osu.Edit private readonly Bindable rectangularGridSnapToggle = new Bindable(); - protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[] - { - new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th }) - }); + protected override IEnumerable CreateTernaryButtons() + => base.CreateTernaryButtons() + .Concat(DistanceSnapProvider.CreateTernaryButtons()) + .Concat(new[] + { + new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th }) + }); private BindableList selectedHitObjects; private Bindable placementObject; + [Cached(typeof(IDistanceSnapProvider))] + protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider(); + [BackgroundDependencyLoader] private void load() { + AddInternal(DistanceSnapProvider); + DistanceSnapProvider.AttachToToolbox(RightToolbox); + // Give a bit of breathing room around the playfield content. - PlayfieldContentContainer.Padding = new MarginPadding - { - Vertical = 10, - Left = TOOLBOX_CONTRACTED_SIZE_LEFT + 10, - Right = TOOLBOX_CONTRACTED_SIZE_RIGHT + 10, - }; + PlayfieldContentContainer.Padding = new MarginPadding(10); LayerBelowRuleset.AddRange(new Drawable[] { @@ -86,10 +89,15 @@ namespace osu.Game.Rulesets.Osu.Edit placementObject = EditorBeatmap.PlacementObject.GetBoundCopy(); placementObject.ValueChanged += _ => updateDistanceSnapGrid(); - DistanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid(); + DistanceSnapProvider.DistanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid(); // we may be entering the screen with a selection already active updateDistanceSnapGrid(); + + RightToolbox.Add(new TransformToolboxGroup + { + RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler + }); } protected override ComposeBlueprintContainer CreateBlueprintContainer() @@ -106,14 +114,6 @@ namespace osu.Game.Rulesets.Osu.Edit private RectangularPositionSnapGrid rectangularPositionSnapGrid; - protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after) - { - float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime()); - float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position); - - return actualDistance / expectedDistance; - } - protected override void Update() { base.Update(); @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Edit // We want to ensure that in this particular case, the time-snapping component of distance snap is still applied. // The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over // the time value if the proposed positions are roughly the same. - if (snapType.HasFlagFast(SnapType.RelativeGrids) && DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) + if (snapType.HasFlagFast(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) { (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition)); if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)) @@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Edit if (snapType.HasFlagFast(SnapType.RelativeGrids)) { - if (DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) + if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) { (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); @@ -220,7 +220,7 @@ namespace osu.Game.Rulesets.Osu.Edit distanceSnapGridCache.Invalidate(); distanceSnapGrid = null; - if (DistanceSnapToggle.Value != TernaryState.True) + if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True) return; switch (BlueprintContainer.CurrentTool) @@ -262,14 +262,6 @@ namespace osu.Game.Rulesets.Osu.Edit base.OnKeyUp(e); } - protected override bool AdjustDistanceSpacing(GlobalAction action, float amount) - { - // To allow better visualisation, ensure that the spacing grid is visible before adjusting. - DistanceSnapToggle.Value = TernaryState.True; - - return base.AdjustDistanceSpacing(action, amount); - } - private bool gridSnapMomentary; private void handleToggleViaKey(KeyboardEvent key) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs index 3234b03a3e..efc6668ebf 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Input.Bindings; @@ -22,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Edit private int currentGridSizeIndex = grid_sizes.Length - 1; [Resolved] - private EditorBeatmap editorBeatmap { get; set; } + private EditorBeatmap editorBeatmap { get; set; } = null!; public OsuRectangularPositionSnapGrid() : base(OsuPlayfield.BASE_SIZE / 2) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 2a6d6ce4c3..e81941d254 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Utils; using osuTK; using osuTK.Input; @@ -27,11 +28,6 @@ namespace osu.Game.Rulesets.Osu.Edit [Resolved(CanBeNull = true)] private IDistanceSnapProvider? snapProvider { get; set; } - /// - /// During a transform, the initial origin is stored so it can be used throughout the operation. - /// - private Vector2? referenceOrigin; - /// /// During a transform, the initial path types of a single selected slider are stored so they /// can be maintained throughout the operation. @@ -42,9 +38,8 @@ namespace osu.Game.Rulesets.Osu.Edit { base.OnSelectionChanged(); - Quad quad = selectedMovableObjects.Length > 0 ? getSurroundingQuad(selectedMovableObjects) : new Quad(); + Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad(); - SelectionBox.CanRotate = quad.Width > 0 || quad.Height > 0; SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0; SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0; SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider); @@ -53,7 +48,6 @@ namespace osu.Game.Rulesets.Osu.Edit protected override void OnOperationEnded() { base.OnOperationEnded(); - referenceOrigin = null; referencePathTypes = null; } @@ -109,13 +103,13 @@ namespace osu.Game.Rulesets.Osu.Edit { var hitObjects = selectedMovableObjects; - var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : getSurroundingQuad(hitObjects); + var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : GeometryUtils.GetSurroundingQuad(hitObjects); bool didFlip = false; foreach (var h in hitObjects) { - var flippedPosition = GetFlippedPosition(direction, flipQuad, h.Position); + var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipQuad, h.Position); if (!Precision.AlmostEquals(flippedPosition, h.Position)) { @@ -169,34 +163,13 @@ namespace osu.Game.Rulesets.Osu.Edit if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y; } - public override bool HandleRotation(float delta) - { - var hitObjects = selectedMovableObjects; - - Quad quad = getSurroundingQuad(hitObjects); - - referenceOrigin ??= quad.Centre; - - foreach (var h in hitObjects) - { - h.Position = RotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta); - - if (h is IHasPath path) - { - foreach (PathControlPoint cp in path.Path.ControlPoints) - cp.Position = RotatePointAroundOrigin(cp.Position, Vector2.Zero, delta); - } - } - - // this isn't always the case but let's be lenient for now. - return true; - } + public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler(); private void scaleSlider(Slider slider, Vector2 scale) { referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList(); - Quad sliderQuad = GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position)); + Quad sliderQuad = GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position)); // Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0. scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size; @@ -222,7 +195,7 @@ namespace osu.Game.Rulesets.Osu.Edit slider.SnapTo(snapProvider); //if sliderhead or sliderend end up outside playfield, revert scaling. - Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider }); + Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider }); (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); if (xInBounds && yInBounds && slider.Path.HasValidLength) @@ -238,10 +211,10 @@ namespace osu.Game.Rulesets.Osu.Edit private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale) { scale = getClampedScale(hitObjects, reference, scale); - Quad selectionQuad = getSurroundingQuad(hitObjects); + Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects); foreach (var h in hitObjects) - h.Position = GetScaledPosition(reference, scale, selectionQuad, h.Position); + h.Position = GeometryUtils.GetScaledPosition(reference, scale, selectionQuad, h.Position); } private (bool X, bool Y) isQuadInBounds(Quad quad) @@ -256,7 +229,7 @@ namespace osu.Game.Rulesets.Osu.Edit { var hitObjects = selectedMovableObjects; - Quad quad = getSurroundingQuad(hitObjects); + Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects); Vector2 delta = Vector2.Zero; @@ -286,7 +259,7 @@ namespace osu.Game.Rulesets.Osu.Edit float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0; float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0; - Quad selectionQuad = getSurroundingQuad(hitObjects); + Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects); //todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead. Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y); @@ -311,26 +284,6 @@ namespace osu.Game.Rulesets.Osu.Edit return scale; } - /// - /// Returns a gamefield-space quad surrounding the provided hit objects. - /// - /// The hit objects to calculate a quad for. - private Quad getSurroundingQuad(OsuHitObject[] hitObjects) => - GetSurroundingQuad(hitObjects.SelectMany(h => - { - if (h is IHasPath path) - { - return new[] - { - h.Position, - // can't use EndPosition for reverse slider cases. - h.Position + path.Path.PositionAt(1) - }; - } - - return new[] { h.Position }; - })); - /// /// All osu! hitobjects which can be moved/rotated/scaled. /// diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs new file mode 100644 index 0000000000..21fb8a67de --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs @@ -0,0 +1,107 @@ +// 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.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public partial class OsuSelectionRotationHandler : SelectionRotationHandler + { + [Resolved] + private IEditorChangeHandler? changeHandler { get; set; } + + private BindableList selectedItems { get; } = new BindableList(); + + [BackgroundDependencyLoader] + private void load(EditorBeatmap editorBeatmap) + { + selectedItems.BindTo(editorBeatmap.SelectedHitObjects); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedItems.CollectionChanged += (_, __) => updateState(); + updateState(); + } + + private void updateState() + { + var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects); + CanRotate.Value = quad.Width > 0 || quad.Height > 0; + } + + private OsuHitObject[]? objectsInRotation; + + private Vector2? defaultOrigin; + private Dictionary? originalPositions; + private Dictionary? originalPathControlPointPositions; + + public override void Begin() + { + if (objectsInRotation != null) + throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!"); + + changeHandler?.BeginChange(); + + objectsInRotation = selectedMovableObjects.ToArray(); + defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation).Centre; + originalPositions = objectsInRotation.ToDictionary(obj => obj, obj => obj.Position); + originalPathControlPointPositions = objectsInRotation.OfType().ToDictionary( + obj => obj, + obj => obj.Path.ControlPoints.Select(point => point.Position).ToArray()); + } + + public override void Update(float rotation, Vector2? origin = null) + { + if (objectsInRotation == null) + throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!"); + + Debug.Assert(originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null); + + Vector2 actualOrigin = origin ?? defaultOrigin.Value; + + foreach (var ho in objectsInRotation) + { + ho.Position = GeometryUtils.RotatePointAroundOrigin(originalPositions[ho], actualOrigin, rotation); + + if (ho is IHasPath withPath) + { + var originalPath = originalPathControlPointPositions[withPath]; + + for (int i = 0; i < withPath.Path.ControlPoints.Count; ++i) + withPath.Path.ControlPoints[i].Position = GeometryUtils.RotatePointAroundOrigin(originalPath[i], Vector2.Zero, rotation); + } + } + } + + public override void Commit() + { + if (objectsInRotation == null) + throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!"); + + changeHandler?.EndChange(); + + objectsInRotation = null; + originalPositions = null; + originalPathControlPointPositions = null; + defaultOrigin = null; + } + + private IEnumerable selectedMovableObjects => selectedItems.Cast() + .Where(h => h is not Spinner); + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs new file mode 100644 index 0000000000..f09d6b78e6 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -0,0 +1,107 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Screens.Edit.Components.RadioButtons; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public partial class PreciseRotationPopover : OsuPopover + { + private readonly SelectionRotationHandler rotationHandler; + + private readonly Bindable rotationInfo = new Bindable(new PreciseRotationInfo(0, RotationOrigin.PlayfieldCentre)); + + private SliderWithTextBoxInput angleInput = null!; + private EditorRadioButtonCollection rotationOrigin = null!; + + public PreciseRotationPopover(SelectionRotationHandler rotationHandler) + { + this.rotationHandler = rotationHandler; + + AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight }; + } + + [BackgroundDependencyLoader] + private void load() + { + Child = new FillFlowContainer + { + Width = 220, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(20), + Children = new Drawable[] + { + angleInput = new SliderWithTextBoxInput("Angle (degrees):") + { + Current = new BindableNumber + { + MinValue = -360, + MaxValue = 360, + Precision = 1 + }, + Instantaneous = true + }, + rotationOrigin = new EditorRadioButtonCollection + { + RelativeSizeAxes = Axes.X, + Items = new[] + { + new RadioButton("Playfield centre", + () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre }, + () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), + new RadioButton("Selection centre", + () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.SelectionCentre }, + () => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare }) + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ScheduleAfterChildren(() => angleInput.TakeFocus()); + angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }); + rotationOrigin.Items.First().Select(); + + rotationInfo.BindValueChanged(rotation => + { + rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null); + }); + } + + protected override void PopIn() + { + base.PopIn(); + rotationHandler.Begin(); + } + + protected override void PopOut() + { + base.PopOut(); + + if (IsLoaded) + rotationHandler.Commit(); + } + } + + public enum RotationOrigin + { + PlayfieldCentre, + SelectionCentre + } + + public record PreciseRotationInfo(float Degrees, RotationOrigin Origin); +} diff --git a/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs index 0a3fc176ad..676205c8d7 100644 --- a/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs +++ b/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs index 3c0cf34010..c8160617c9 100644 --- a/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs +++ b/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs new file mode 100644 index 0000000000..3da9f5b69b --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -0,0 +1,80 @@ +// 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.Sprites; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Input.Bindings; +using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit.Components; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler + { + private readonly Bindable canRotate = new BindableBool(); + + private EditorToolButton rotateButton = null!; + + public SelectionRotationHandler RotationHandler { get; init; } = null!; + + public TransformToolboxGroup() + : base("transform") + { + } + + [BackgroundDependencyLoader] + private void load() + { + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(5), + Children = new Drawable[] + { + rotateButton = new EditorToolButton("Rotate", + () => new SpriteIcon { Icon = FontAwesome.Solid.Undo }, + () => new PreciseRotationPopover(RotationHandler)), + // TODO: scale + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // bindings to `Enabled` on the buttons are decoupled on purpose + // due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set. + canRotate.BindTo(RotationHandler.CanRotate); + canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true); + } + + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Repeat) return false; + + switch (e.Action) + { + case GlobalAction.EditorToggleRotateControl: + { + rotateButton.TriggerClick(); + return true; + } + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu/Judgements/ComboResult.cs b/osu.Game.Rulesets.Osu/Judgements/ComboResult.cs index 9762c676c5..556eb94f38 100644 --- a/osu.Game.Rulesets.Osu/Judgements/ComboResult.cs +++ b/osu.Game.Rulesets.Osu/Judgements/ComboResult.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.ComponentModel; namespace osu.Game.Rulesets.Osu.Judgements diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs index 5f9faaceb2..7a9a868b9b 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs index 1bdb74cd3b..7c7f16779e 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Judgements diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs index a5503d3273..1a88e2a8b2 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuJudgementResult.cs index 50d73fa19d..3bc2cacb43 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuJudgementResult.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuJudgementResult.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs index 4229c87b58..e8fc1d99bc 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs @@ -1,11 +1,10 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Judgements { @@ -17,28 +16,15 @@ namespace osu.Game.Rulesets.Osu.Judgements public Spinner Spinner => (Spinner)HitObject; /// - /// The total rotation performed on the spinner disc, disregarding the spin direction, - /// adjusted for the track's playback rate. + /// The total amount that the spinner was rotated. /// - /// - /// - /// This value is always non-negative and is monotonically increasing with time - /// (i.e. will only increase if time is passing forward, but can decrease during rewind). - /// - /// - /// The rotation from each frame is multiplied by the clock's current playback rate. - /// The reason this is done is to ensure that spinners give the same score and require the same number of spins - /// regardless of whether speed-modifying mods are applied. - /// - /// - /// - /// Assuming no speed-modifying mods are active, - /// if the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise, - /// this property will return the value of 720 (as opposed to 0). - /// If Double Time is active instead (with a speed multiplier of 1.5x), - /// in the same scenario the property will return 720 * 1.5 = 1080. - /// - public float RateAdjustedRotation; + public float TotalRotation => History.TotalRotation; + + /// + /// Stores the spinning history of the spinner.
+ /// Instants of movement deltas may be added or removed from this in order to calculate the total rotation for the spinner. + ///
+ public readonly SpinnerSpinHistory History = new SpinnerSpinHistory(); /// /// Time instant at which the spin was started (the first user input which caused an increase in spin). diff --git a/osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs index 270c1f31fb..21b024a720 100644 --- a/osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs +++ b/osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Judgements diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs index 2e2d320313..8b0adbe50f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs @@ -139,12 +139,12 @@ namespace osu.Game.Rulesets.Osu.Mods if (Precision.AlmostEquals(restrictTo.Rotation, 0)) { - start = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopLeft).X; - end = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopRight).X; + start = Parent!.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopLeft).X; + end = Parent!.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopRight).X; } else { - float center = restrictTo.ToSpaceOfOtherDrawable(restrictTo.OriginPosition, Parent).X; + float center = restrictTo.ToSpaceOfOtherDrawable(restrictTo.OriginPosition, Parent!).X; float halfDiagonal = (restrictTo.DrawSize / 2).LengthFast; start = center - halfDiagonal; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs index b74b722bad..9c0e43e96f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs @@ -61,10 +61,12 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { + OsuHitObject firstObject = drawableRuleset.Beatmap.HitObjects.First(); + // Multiplying by 2 results in an initial size that is too large, hence 1.90 has been chosen // Also avoids the HitObject bleeding around the edges of the bubble drawable at minimum size - bubbleSize = (float)(drawableRuleset.Beatmap.HitObjects.OfType().First().Radius * 1.90f); - bubbleFade = drawableRuleset.Beatmap.HitObjects.OfType().First().TimePreempt * 2; + bubbleSize = (float)firstObject.Radius * 1.90f; + bubbleFade = firstObject.TimePreempt * 2; // We want to hide the judgements since they are obscured by the BubbleDrawable (due to layering) drawableRuleset.Playfield.DisplayJudgements.Value = false; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 250d97c537..8930b4ad70 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -24,9 +25,6 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")] public Bindable NoSliderHeadAccuracy { get; } = new BindableBool(true); - [SettingSource("No slider head movement", "Pins slider heads at their starting position, regardless of time.")] - public Bindable NoSliderHeadMovement { get; } = new BindableBool(true); - [SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")] public Bindable ClassicNoteLock { get; } = new BindableBool(true); @@ -57,7 +55,10 @@ namespace osu.Game.Rulesets.Osu.Mods var osuRuleset = (DrawableOsuRuleset)drawableRuleset; if (ClassicNoteLock.Value) - osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy(); + { + double hittableRange = OsuHitWindows.MISS_WINDOW - (drawableRuleset.Mods.OfType().Any() ? 200 : 0); + osuRuleset.Playfield.HitPolicy = new LegacyHitPolicy(hittableRange); + } usingHiddenFading = drawableRuleset.Mods.OfType().SingleOrDefault()?.OnlyFadeApproachCircles.Value == false; } @@ -67,9 +68,12 @@ namespace osu.Game.Rulesets.Osu.Mods switch (obj) { case DrawableSliderHead head: - head.TrackFollowCircle = !NoSliderHeadMovement.Value; if (FadeHitCircleEarly.Value && !usingHiddenFading) applyEarlyFading(head); + + if (ClassicNoteLock.Value) + blockInputToObjectsUnderSliderHead(head); + break; case DrawableSliderTail tail: @@ -79,19 +83,39 @@ namespace osu.Game.Rulesets.Osu.Mods case DrawableHitCircle circle: if (FadeHitCircleEarly.Value && !usingHiddenFading) applyEarlyFading(circle); + break; } } + /// + /// On stable, slider heads that have already been hit block input from reaching objects that may be underneath them + /// until the sliders they're part of have been fully judged. + /// The purpose of this method is to restore that behaviour. + /// In order to avoid introducing yet another confusing config option, this behaviour is roped into the general notion of "note lock". + /// + private static void blockInputToObjectsUnderSliderHead(DrawableSliderHead slider) + { + var oldHitAction = slider.HitArea.Hit; + slider.HitArea.Hit = () => + { + oldHitAction?.Invoke(); + return !slider.DrawableSlider.AllJudged; + }; + } + private void applyEarlyFading(DrawableHitCircle circle) { - circle.ApplyCustomUpdateState += (o, _) => + circle.ApplyCustomUpdateState += (dho, state) => { - using (o.BeginAbsoluteSequence(o.StateUpdateTime)) + using (dho.BeginAbsoluteSequence(dho.StateUpdateTime)) { - double okWindow = o.HitObject.HitWindows.WindowFor(HitResult.Ok); - double lateMissFadeTime = o.HitObject.HitWindows.WindowFor(HitResult.Meh) - okWindow; - o.Delay(okWindow).FadeOut(lateMissFadeTime); + if (state != ArmedState.Hit) + { + double okWindow = dho.HitObject.HitWindows.WindowFor(HitResult.Ok); + double lateMissFadeTime = dho.HitObject.HitWindows.WindowFor(HitResult.Meh) - okWindow; + dho.Delay(okWindow).FadeOut(lateMissFadeTime); + } } }; } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 3a6b232f9f..f35b1abc42 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -2,13 +2,19 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModDifficultyAdjust : ModDifficultyAdjust + public partial class OsuModDifficultyAdjust : ModDifficultyAdjust { [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))] public DifficultyBindable CircleSize { get; } = new DifficultyBindable @@ -20,12 +26,13 @@ namespace osu.Game.Rulesets.Osu.Mods ReadCurrentFromDifficulty = diff => diff.CircleSize, }; - [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))] + [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(ApproachRateSettingsControl))] public DifficultyBindable ApproachRate { get; } = new DifficultyBindable { Precision = 0.1f, MinValue = 0, MaxValue = 10, + ExtendedMinValue = -10, ExtendedMaxValue = 11, ReadCurrentFromDifficulty = diff => diff.ApproachRate, }; @@ -53,5 +60,34 @@ namespace osu.Game.Rulesets.Osu.Mods if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.Value.Value; if (ApproachRate.Value != null) difficulty.ApproachRate = ApproachRate.Value.Value; } + + private partial class ApproachRateSettingsControl : DifficultyAdjustSettingsControl + { + protected override RoundedSliderBar CreateSlider(BindableNumber current) => + new ApproachRateSlider + { + RelativeSizeAxes = Axes.X, + Current = current, + KeyboardStep = 0.1f, + }; + + /// + /// A slider bar with more detailed approach rate info for its given value + /// + public partial class ApproachRateSlider : RoundedSliderBar + { + public override LocalisableString TooltipText => + (Current as BindableNumber)?.MinValue < 0 + ? $"{base.TooltipText} ({getPreemptTime(Current.Value):0} ms)" + : base.TooltipText; + + private double getPreemptTime(float approachRate) + { + var hitCircle = new HitCircle(); + hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { ApproachRate = approachRate }); + return hitCircle.TimePreempt; + } + } + } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index efeac9a180..252d7e2762 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -68,8 +68,8 @@ namespace osu.Game.Rulesets.Osu.Mods public void OnSliderTrackingChange(ValueChangedEvent e) { - // If a slider is in a tracking state, a further dim should be applied to the (remaining) visible portion of the playfield over a brief duration. - this.TransformTo(nameof(FlashlightDim), e.NewValue ? 0.8f : 0.0f, 50); + // If a slider is in a tracking state, a further dim should be applied to the (remaining) visible portion of the playfield. + FlashlightDim = e.NewValue ? 0.8f : 0.0f; } protected override bool OnMouseMove(MouseMoveEvent e) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs index 0a1aab9ef1..f1197ce0cd 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override LocalisableString Description => "Burn the notes into your memory."; //Alters the transforms of the approach circles, breaking the effects of these mods. - public override Type[] IncompatibleMods => new[] { typeof(OsuModApproachDifferent) }; + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform) }).ToArray(); public override ModType Type => ModType.Fun; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs index 19d4a1bf83..d24597eeed 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; @@ -22,5 +23,13 @@ namespace osu.Game.Rulesets.Osu.Mods OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(osuObject); } + + public override void ApplyToDifficulty(BeatmapDifficulty difficulty) + { + base.ApplyToDifficulty(difficulty); + + difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio. + difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f); + } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 996ee1cddb..dd2befef4e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -98,6 +98,9 @@ namespace osu.Game.Rulesets.Osu.Mods // only apply to circle piece – reverse arrow is not affected by hidden. sliderRepeat.CirclePiece.FadeOut(fadeDuration); + using (drawableObject.BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) + sliderRepeat.FadeOut(); + break; case DrawableHitCircle circle: diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index 72031b4958..c465ab8732 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -96,14 +96,13 @@ namespace osu.Game.Rulesets.Osu.Mods Position = original.Position; NewCombo = original.NewCombo; ComboOffset = original.ComboOffset; - LegacyLastTickOffset = original.LegacyLastTickOffset; TickDistanceMultiplier = original.TickDistanceMultiplier; - SliderVelocity = original.SliderVelocity; + SliderVelocityMultiplier = original.SliderVelocityMultiplier; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { - var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken); + var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), cancellationToken); foreach (var e in sliderEvents) { @@ -130,7 +129,7 @@ namespace osu.Game.Rulesets.Osu.Mods }); break; - case SliderEventType.LegacyLastTick: + case SliderEventType.Tail: AddNested(TailCircle = new StrictTrackingSliderTailCircle(this) { RepeatIndex = e.SpanIndex, diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs new file mode 100644 index 0000000000..e1123807cd --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.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 osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Screens.Edit; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Mods +{ + /// + /// Mod that colours s based on the musical division they are on + /// + public class OsuModSynesthesia : ModSynesthesia, IApplicableToBeatmap, IApplicableToDrawableHitObject + { + private readonly OsuColour colours = new OsuColour(); + + private IBeatmap? currentBeatmap { get; set; } + + public void ApplyToBeatmap(IBeatmap beatmap) + { + //Store a reference to the current beatmap to look up the beat divisor when notes are drawn + if (currentBeatmap != beatmap) + currentBeatmap = beatmap; + } + + public void ApplyToDrawableHitObject(DrawableHitObject d) + { + if (currentBeatmap == null) return; + + Color4? timingBasedColour = null; + + d.HitObjectApplied += _ => + { + // slider tails are a painful edge case, as their start time is offset 36ms back (see `LastTick`). + // to work around this, look up the slider tail's parenting slider's end time instead to ensure proper snap. + double snapTime = d is DrawableSliderTail tail + ? tail.Slider.GetEndTime() + : d.HitObject.StartTime; + timingBasedColour = BindableBeatDivisor.GetColourFor(currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(snapTime), colours); + }; + + // Need to set this every update to ensure it doesn't get overwritten by DrawableHitObject.OnApply() -> UpdateComboColour(). + d.OnUpdate += _ => + { + if (timingBasedColour != null) + d.AccentColour.Value = timingBasedColour.Value; + }; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs index 2354cd50ae..92a499e735 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; @@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override LocalisableString Description => "Everything rotates. EVERYTHING."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel) }; + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame) }).ToArray(); private float theta; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs index d588127cb9..52edfb1422 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.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 osu.Framework.Bindables; using osuTK; using osuTK.Graphics; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 3458069dd1..e87a075a11 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Collections.Generic; using System.Diagnostics; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -18,6 +19,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osuTK; @@ -33,6 +35,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public HitReceptor HitArea { get; private set; } public SkinnableDrawable CirclePiece { get; private set; } + protected override IEnumerable DimmablePieces => new[] + { + CirclePiece, + }; + Drawable IHasApproachCircle.ApproachCircle => ApproachCircle; private Container scaleContainer; @@ -154,12 +161,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } var result = ResultFor(timeOffset); + var clickAction = CheckHittable?.Invoke(this, Time.Current, result); - if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false) - { + if (clickAction == ClickAction.Shake) Shake(); + + if (result == HitResult.None || clickAction != ClickAction.Hit) return; - } ApplyResult(r => { @@ -189,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables CirclePiece.FadeInFromZero(HitObject.TimeFadeIn); - ApproachCircle.FadeIn(Math.Min(HitObject.TimeFadeIn * 2, HitObject.TimePreempt)); + ApproachCircle.FadeTo(0.9f, Math.Min(HitObject.TimeFadeIn * 2, HitObject.TimePreempt)); ApproachCircle.ScaleTo(1f, HitObject.TimePreempt); ApproachCircle.Expire(true); } @@ -242,7 +250,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public HitReceptor() { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -259,7 +267,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables case OsuAction.RightButton: if (IsHovered && (Hit?.Invoke() ?? false)) { - HitAction = e.Action; + HitAction ??= e.Action; return true; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index df0ba344d8..bdd818cf18 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -4,6 +4,8 @@ #nullable disable using System; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -12,6 +14,8 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Scoring; using osuTK; using osuTK.Graphics; @@ -30,10 +34,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override float SamplePlaybackPosition => CalculateDrawableRelativePosition(this); /// - /// Whether this can be hit, given a time value. - /// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false. + /// What action this should take in response to a + /// click at the given time value. + /// If non-null, judgements will be ignored for return values of + /// and , and this hit object will be shaken for return values of + /// . /// - public Func CheckHittable; + public Func CheckHittable; protected DrawableOsuHitObject(OsuHitObject hitObject) : base(hitObject) @@ -66,20 +73,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ScaleBindable.UnbindFrom(HitObject.ScaleBindable); } + protected virtual IEnumerable DimmablePieces => Enumerable.Empty(); + protected override void UpdateInitialTransforms() { base.UpdateInitialTransforms(); - // Dim should only be applied at a top level, as it will be implicitly applied to nested objects. - if (ParentHitObject == null) + foreach (var piece in DimmablePieces) { - // Of note, no one noticed this was missing for years, but it definitely feels like it should still exist. - // For now this is applied across all skins, and matches stable. - // For simplicity, dim colour is applied to the DrawableHitObject itself. - // We may need to make a nested container setup if this even causes a usage conflict (ie. with a mod). - this.FadeColour(new Color4(195, 195, 195, 255)); - using (BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW)) - this.FadeColour(Color4.White, 100); + piece.FadeColour(new Color4(195, 195, 195, 255)); + using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW)) + piece.FadeColour(Color4.White, 100); } } @@ -98,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult); - private RectangleF parentScreenSpaceRectangle => ((DrawableOsuHitObject)ParentHitObject)?.parentScreenSpaceRectangle ?? Parent.ScreenSpaceDrawQuad.AABBFloat; + private RectangleF parentScreenSpaceRectangle => ((DrawableOsuHitObject)ParentHitObject)?.parentScreenSpaceRectangle ?? Parent!.ScreenSpaceDrawQuad.AABBFloat; /// /// Calculates the position of the given relative to the playfield area. diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 664a8146e7..cdfe888c99 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -35,6 +36,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private ShakeContainer shakeContainer; + protected override IEnumerable DimmablePieces => new Drawable[] + { + HeadCircle, + TailCircle, + repeatContainer, + Body, + }; + /// /// A target container which can be used to add top level elements to the slider's display. /// Intended to be used for proxy purposes only. @@ -75,25 +84,35 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables [BackgroundDependencyLoader] private void load() { + tailContainer = new Container { RelativeSizeAxes = Axes.Both }; + AddRangeInternal(new Drawable[] { shakeContainer = new ShakeContainer { ShakeDuration = 30, RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Children = new[] { Body = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling), - tailContainer = new Container { RelativeSizeAxes = Axes.Both }, + // proxied here so that the tail is drawn under repeats/ticks - legacy skins rely on this + tailContainer.CreateProxy(), tickContainer = new Container { RelativeSizeAxes = Axes.Both }, repeatContainer = new Container { RelativeSizeAxes = Axes.Both }, + // actual tail container is placed here to ensure that tail hitobjects are processed after ticks/repeats. + // this is required for the correct operation of Score V2. + tailContainer, } }, // slider head is not included in shake as it handles hit detection, and handles its own shaking. headContainer = new Container { RelativeSizeAxes = Axes.Both }, OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, }, Ball, - slidingSample = new PausableSkinnableSound { Looping = true } + slidingSample = new PausableSkinnableSound + { + Looping = true, + MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME, + } }); PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); @@ -222,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1); Ball.UpdateProgress(completionProgress); - SliderBody?.UpdateProgress(completionProgress); + SliderBody?.UpdateProgress(HeadCircle.IsHit ? completionProgress : 0); foreach (DrawableHitObject hitObject in NestedHitObjects) { @@ -250,7 +269,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (userTriggered || Time.Current < HitObject.EndTime) + if (userTriggered || !TailCircle.Judged || Time.Current < HitObject.EndTime) return; // If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes. @@ -282,7 +301,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public override void PlaySamples() { // rather than doing it this way, we should probably attach the sample to the tail circle. - // this can only be done after we stop using LegacyLastTick. + // this can only be done if we stop using LastTick. if (!TailCircle.SamplePlaysOnlyOnHit || TailCircle.IsHit) base.PlaySamples(); } @@ -311,7 +330,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables switch (state) { case ArmedState.Hit: - if (SliderBody?.SnakingOut.Value == true) + if (HeadCircle.IsHit && SliderBody?.SnakingOut.Value == true) Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear. break; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs index e1766adc20..292f2ffd7d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs @@ -11,9 +11,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Input.Events; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; @@ -36,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Children = new[] { @@ -152,9 +154,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Tracking = // in valid time range - Time.Current >= drawableSlider.HitObject.StartTime && Time.Current < drawableSlider.HitObject.EndTime && + Time.Current >= drawableSlider.HitObject.StartTime + // even in an edge case where current time has exceeded the slider's time, we may not have finished judging. + // we don't want to potentially update from Tracking=true to Tracking=false at this point. + && (!drawableSlider.AllJudged || Time.Current <= drawableSlider.HitObject.GetEndTime()) // in valid position range - lastScreenSpaceMousePosition.HasValue && followCircleReceptor.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) && + && lastScreenSpaceMousePosition.HasValue && followCircleReceptor.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) && // valid action (actions?.Any(isValidTrackingAction) ?? false); @@ -179,16 +184,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Vector2? lastPosition; - private bool rewinding; - public void UpdateProgress(double completionProgress) { Position = drawableSlider.HitObject.CurvePositionAt(completionProgress); var diff = lastPosition.HasValue ? lastPosition.Value - Position : Position - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f); - if (Clock.ElapsedFrameTime != 0) - rewinding = Clock.ElapsedFrameTime < 0; + bool rewinding = (Clock as IGameplayClock)?.IsRewinding == true; // Ensure the value is substantially high enough to allow for Atan2 to get a valid angle. if (diff.LengthFast < 0.01f) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index b8a1efabe0..be6c322668 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -3,11 +3,9 @@ #nullable disable -using System; using System.Diagnostics; -using JetBrains.Annotations; using osu.Framework.Bindables; -using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -16,19 +14,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public new SliderHeadCircle HitObject => (SliderHeadCircle)base.HitObject; - [CanBeNull] - public Slider Slider => DrawableSlider?.HitObject; - public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult; - /// - /// Makes this track the follow circle when the start time is reached. - /// If false, this will be pinned to its initial position in the slider. - /// - public bool TrackFollowCircle = true; - private readonly IBindable pathVersion = new Bindable(); protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle; @@ -60,24 +49,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables pathVersion.BindTo(DrawableSlider.PathVersion); - CheckHittable = (d, t) => DrawableSlider.CheckHittable?.Invoke(d, t) ?? true; - } - - protected override void Update() - { - base.Update(); - - Debug.Assert(Slider != null); - Debug.Assert(HitObject != null); - - if (TrackFollowCircle) - { - double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1); - - //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice. - if (!IsHit) - Position = Slider.CurvePositionAt(completionProgress); - } + CheckHittable = (d, t, r) => DrawableSlider.CheckHittable?.Invoke(d, t, r) ?? ClickAction.Hit; } protected override HitResult ResultFor(double timeOffset) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index fc4863f164..cdfd96514e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -17,7 +17,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public partial class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking + public partial class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking, IRequireTracking { public new SliderRepeat HitObject => (SliderRepeat)base.HitObject; @@ -30,12 +30,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public SkinnableDrawable CirclePiece { get; private set; } - public ReverseArrowPiece Arrow { get; private set; } + public SkinnableDrawable Arrow { get; private set; } private Drawable scaleContainer; public override bool DisplayResult => false; + public bool Tracking { get; set; } + public DrawableSliderRepeat() : base(null) { @@ -50,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private void load() { Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; AddInternal(scaleContainer = new Container { @@ -65,7 +67,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - Arrow = new ReverseArrowPiece(), + Arrow = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.ReverseArrow), _ => new DefaultReverseArrow()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, } }); @@ -81,8 +87,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (HitObject.StartTime <= Time.Current) - ApplyResult(r => r.Type = DrawableSlider.Tracking.Value ? r.Judgement.MaxResult : r.Judgement.MinResult); + // shared implementation with DrawableSliderTick. + if (timeOffset >= 0) + { + // Attempt to preserve correct ordering of judgements as best we can by forcing + // an un-judged head to be missed when the user has clearly skipped it. + // + // This check is applied to all nested slider objects apart from the head (ticks, repeats, tail). + if (Tracking && !DrawableSlider.HeadCircle.Judged) + DrawableSlider.HeadCircle.MissForcefully(); + + ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult); + } } protected override void UpdateInitialTransforms() @@ -114,11 +130,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables case ArmedState.Hit: this.FadeOut(animDuration, Easing.Out); - - const float final_scale = 1.5f; - - Arrow.ScaleTo(Scale * final_scale, animDuration, Easing.Out); - CirclePiece.ScaleTo(Scale * final_scale, animDuration, Easing.Out); break; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index d9501f7d58..e3ed12a648 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -4,10 +4,12 @@ #nullable disable using System.Diagnostics; +using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; @@ -55,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private void load() { Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; AddRangeInternal(new Drawable[] { @@ -125,8 +127,33 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (!userTriggered && timeOffset >= 0) - ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult); + if (userTriggered) + return; + + // Ensure the tail can only activate after all previous ticks/repeats already have. + // + // This covers the edge case where the lenience may allow the tail to activate before + // the last tick, changing ordering of score/combo awarding. + var lastTick = DrawableSlider.NestedHitObjects.LastOrDefault(o => o.HitObject is SliderTick || o.HitObject is SliderRepeat); + if (lastTick?.Judged == false) + return; + + if (timeOffset < SliderEventGenerator.TAIL_LENIENCY) + return; + + // Attempt to preserve correct ordering of judgements as best we can by forcing + // an un-judged head to be missed when the user has clearly skipped it. + // + // This check is applied to all nested slider objects apart from the head (ticks, repeats, tail). + if (Tracking && !DrawableSlider.HeadCircle.Judged) + DrawableSlider.HeadCircle.MissForcefully(); + + // The player needs to have engaged in tracking at any point after the tail leniency cutoff. + // An actual tick miss should only occur if reaching the tick itself. + if (Tracking) + ApplyResult(r => r.Type = r.Judgement.MaxResult); + else if (timeOffset > 0) + ApplyResult(r => r.Type = r.Judgement.MinResult); } protected override void OnApply() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 6d0ae93e62..172dca356e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables [BackgroundDependencyLoader] private void load() { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Origin = Anchor.Centre; AddInternal(scaleContainer = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.SliderScorePoint), _ => new CircularContainer @@ -75,8 +75,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { + // shared implementation with DrawableSliderRepeat. if (timeOffset >= 0) + { + // Attempt to preserve correct ordering of judgements as best we can by forcing + // an un-judged head to be missed when the user has clearly skipped it. + // + // This check is applied to all nested slider objects apart from the head (ticks, repeats, tail). + if (Tracking && !DrawableSlider.HeadCircle.Judged) + DrawableSlider.HeadCircle.MissForcefully(); + ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult); + } } protected override void UpdateInitialTransforms() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 0ceda1d4b0..c092b4dd4b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -48,9 +48,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// The amount of bonus score gained from spinning after the required number of spins, for display purposes. /// - public IBindable GainedBonus => gainedBonus; + public double CurrentBonusScore => score_per_tick * Math.Clamp(completedFullSpins.Value - HitObject.SpinsRequiredForBonus, 0, HitObject.MaximumBonusSpins); - private readonly Bindable gainedBonus = new BindableDouble(); + /// + /// The maximum amount of bonus score which can be achieved from extra spins. + /// + public double MaximumBonusScore => score_per_tick * HitObject.MaximumBonusSpins; + + public IBindable CompletedFullSpins => completedFullSpins; + + private readonly Bindable completedFullSpins = new Bindable(); /// /// The number of spins per minute this spinner is spinning at, for display purposes. @@ -99,6 +106,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables spinningSample = new PausableSkinnableSound { Volume = { Value = 0 }, + MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME, Looping = true, Frequency = { Value = spinning_sample_initial_frequency } } @@ -218,7 +226,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // these become implicitly hit. return 1; - return Math.Clamp(Result.RateAdjustedRotation / 360 / HitObject.SpinsRequired, 0, 1); + return Math.Clamp(Result.TotalRotation / 360 / HitObject.SpinsRequired, 0, 1); } } @@ -279,43 +287,35 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // don't update after end time to avoid the rate display dropping during fade out. // this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period. if (Time.Current <= HitObject.EndTime) - spmCalculator.SetRotation(Result.RateAdjustedRotation); + spmCalculator.SetRotation(Result.TotalRotation); updateBonusScore(); } private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult; - private int wholeSpins; - private void updateBonusScore() { if (ticks.Count == 0) return; - int spins = (int)(Result.RateAdjustedRotation / 360); + int spins = (int)(Result.TotalRotation / 360); - if (spins < wholeSpins) + if (spins < completedFullSpins.Value) { // rewinding, silently handle - wholeSpins = spins; + completedFullSpins.Value = spins; return; } - while (wholeSpins != spins) + while (completedFullSpins.Value != spins) { var tick = ticks.FirstOrDefault(t => !t.Result.HasResult); // tick may be null if we've hit the spin limit. - if (tick != null) - { - tick.TriggerResult(true); + tick?.TriggerResult(true); - if (tick is DrawableSpinnerBonusTick) - gainedBonus.Value = score_per_tick * (spins - HitObject.SpinsRequired); - } - - wholeSpins++; + completedFullSpins.Value++; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs index f1f4ec983e..889a9bd816 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Rulesets.Osu.Objects.Drawables { public partial class DrawableSpinnerBonusTick : DrawableSpinnerTick diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index 34253e3d4f..a5785dd1f6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -25,6 +25,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre; } + protected override void OnApply() + { + base.OnApply(); + + // the tick can be theoretically judged at any point in the spinner's duration, + // so it must be alive throughout the spinner's entire lifetime. + // this mostly matters for correct sample playback. + LifetimeStart = DrawableSpinner.HitObject.StartTime; + } + /// /// Apply a judgement result. /// diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/ITrackSnaking.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/ITrackSnaking.cs index 9e8035a1ee..cae2a7c36d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/ITrackSnaking.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/ITrackSnaking.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 osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerSpinHistory.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerSpinHistory.cs new file mode 100644 index 0000000000..1c6c5b5d02 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerSpinHistory.cs @@ -0,0 +1,146 @@ +// 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.Collections.Generic; +using System.Diagnostics; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables +{ + /// + /// Stores the spinning history of a single spinner.
+ /// Instants of movement deltas may be added or removed from this in order to calculate the total rotation for the spinner. + ///
+ /// + /// A single, full rotation of the spinner is defined as a 360-degree rotation of the spinner, starting from 0, going in a single direction.
+ ///
+ /// + /// If the player spins 90-degrees clockwise, then changes direction, they need to spin 90-degrees counter-clockwise to return to 0 + /// and then continue rotating the spinner for another 360-degrees in the same direction. + /// + public class SpinnerSpinHistory + { + /// + /// The sum of all complete spins and any current partial spin, in degrees. + /// + /// + /// This is the final scoring value. + /// + public float TotalRotation => 360 * completedSpins.Count + currentSpinMaxRotation; + + private readonly Stack completedSpins = new Stack(); + + /// + /// The total accumulated (absolute) rotation. + /// + private float totalAccumulatedRotation; + + private float totalAccumulatedRotationAtLastCompletion; + + /// + /// For the current spin, represents the maximum absolute rotation (from 0..360) achieved by the user. + /// + /// + /// This is used to report in the case a user spins backwards. + /// Basically it allows us to not reduce the total rotation in such a case. + /// + /// This also stops spinner "cheese" where a user may rapidly change directions and cause an increase + /// in rotations. + /// + private float currentSpinMaxRotation; + + /// + /// The current spin, from -360..360. + /// + private float currentSpinRotation => totalAccumulatedRotation - totalAccumulatedRotationAtLastCompletion; + + private double lastReportTime = double.NegativeInfinity; + + /// + /// Report a delta update based on user input. + /// + /// The current time. + /// The delta of the angle moved through since the last report. + public void ReportDelta(double currentTime, float delta) + { + if (delta == 0) + return; + + // Importantly, outside of tests the max delta entering here is 180 degrees. + // If it wasn't for tests, we could add this line: + // + // Debug.Assert(Math.Abs(delta) < 180); + // + // For this to be 101% correct, we need to add the ability for important frames to be + // created based on gameplay intrinsics (ie. there should be one frame for any spinner delta 90 < n < 180 degrees). + // + // But this can come later. + + totalAccumulatedRotation += delta; + + if (currentTime >= lastReportTime) + { + currentSpinMaxRotation = Math.Max(currentSpinMaxRotation, Math.Abs(currentSpinRotation)); + + // Handle the case where the user has completed another spin. + // Note that this does could be an `if` rather than `while` if the above assertion held true. + // It is a `while` loop to handle tests which throw larger values at this method. + while (currentSpinMaxRotation >= 360) + { + int direction = Math.Sign(currentSpinRotation); + + completedSpins.Push(new CompletedSpin(currentTime, direction)); + + // Incrementing the last completion point will cause `currentSpinRotation` to + // hold the remaining spin that needs to be considered. + totalAccumulatedRotationAtLastCompletion += direction * 360; + + // Reset the current max as we are entering a new spin. + // Importantly, carry over the remainder (which is now stored in `currentSpinRotation`). + currentSpinMaxRotation = Math.Abs(currentSpinRotation); + } + } + else + { + // When rewinding, the main thing we care about is getting `totalAbsoluteRotationsAtLastCompletion` + // to the correct value. We can used the stored history for this. + while (completedSpins.TryPeek(out var segment) && segment.CompletionTime > currentTime) + { + completedSpins.Pop(); + totalAccumulatedRotationAtLastCompletion -= segment.Direction * 360; + } + + // This is a best effort. We may not have enough data to match this 1:1, but that's okay. + // We know that the player is somewhere in a spin. + // In the worst case, this will be lower than expected, and recover in forward playback. + currentSpinMaxRotation = Math.Abs(currentSpinRotation); + } + + lastReportTime = currentTime; + } + + /// + /// Represents a single completed spin. + /// + private class CompletedSpin + { + /// + /// The time at which this spin completion occurred. + /// + public readonly double CompletionTime; + + /// + /// The direction this spin completed in. + /// + public readonly int Direction; + + public CompletedSpin(double completionTime, int direction) + { + Debug.Assert(direction == -1 || direction == 1); + + CompletionTime = completionTime; + Direction = direction; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/HitCircle.cs b/osu.Game.Rulesets.Osu/Objects/HitCircle.cs index 5f43e57ed8..d652db0fd4 100644 --- a/osu.Game.Rulesets.Osu/Objects/HitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/HitCircle.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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 7b98fc48e0..d74d28c748 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -1,14 +1,13 @@ // 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.Bindables; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; @@ -23,6 +22,11 @@ namespace osu.Game.Rulesets.Osu.Objects ///
public const float OBJECT_RADIUS = 64; + /// + /// The width and height any element participating in display of a hitcircle (or similarly sized object) should be. + /// + public static readonly Vector2 OBJECT_DIMENSIONS = new Vector2(OBJECT_RADIUS * 2); + /// /// Scoring distance with a speed-adjusted beat length of 1 second (ie. the speed slider balls move through their track). /// @@ -152,7 +156,7 @@ namespace osu.Game.Rulesets.Osu.Objects // This adjustment is necessary for AR>10, otherwise TimePreempt can become smaller leading to hitcircles not fully fading in. TimeFadeIn = 400 * Math.Min(1, TimePreempt / PREEMPT_MIN); - Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2; + Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize, true); } protected override HitWindows CreateHitWindows() => new OsuHitWindows(); diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 4189f8ba1e..3cb9b96090 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -16,6 +16,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; @@ -48,13 +49,9 @@ namespace osu.Game.Rulesets.Osu.Objects set { path.ControlPoints.Clear(); - path.ExpectedDistance.Value = null; + path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position, c.Type))); - if (value != null) - { - path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position, c.Type))); - path.ExpectedDistance.Value = value.ExpectedDistance.Value; - } + path.ExpectedDistance.Value = value.ExpectedDistance.Value; } } @@ -70,8 +67,6 @@ namespace osu.Game.Rulesets.Osu.Objects } } - public double? LegacyLastTickOffset { get; set; } - /// /// The position of the cursor at the point of completion of this if it was hit /// with as few movements as possible. This is set and used by difficulty calculation. @@ -113,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Objects public double SpanDuration => Duration / this.SpanCount(); /// - /// Velocity of this . + /// The computed velocity of this . This is the amount of path distance travelled in 1 ms. /// public double Velocity { get; private set; } @@ -134,17 +129,16 @@ namespace osu.Game.Rulesets.Osu.Objects /// public bool OnlyJudgeNestedObjects = true; - public BindableNumber SliderVelocityBindable { get; } = new BindableDouble(1) + public BindableNumber SliderVelocityMultiplierBindable { get; } = new BindableDouble(1) { - Precision = 0.01, MinValue = 0.1, MaxValue = 10 }; - public double SliderVelocity + public double SliderVelocityMultiplier { - get => SliderVelocityBindable.Value; - set => SliderVelocityBindable.Value = value; + get => SliderVelocityMultiplierBindable.Value; + set => SliderVelocityMultiplierBindable.Value = value; } public bool GenerateTicks { get; set; } = true; @@ -167,9 +161,11 @@ namespace osu.Game.Rulesets.Osu.Objects TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * SliderVelocity; + Velocity = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier / LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(this, timingPoint, OsuRuleset.SHORT_NAME); + // WARNING: this is intentionally not computed as `BASE_SCORING_DISTANCE * difficulty.SliderMultiplier` + // for backwards compatibility reasons (intentionally introducing floating point errors to match stable). + double scoringDistance = Velocity * timingPoint.BeatLength; - Velocity = scoringDistance / timingPoint.BeatLength; TickDistance = GenerateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity; } @@ -177,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Objects { base.CreateNestedHitObjects(cancellationToken); - var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken); + var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), cancellationToken); foreach (var e in sliderEvents) { @@ -204,10 +200,7 @@ namespace osu.Game.Rulesets.Osu.Objects }); break; - case SliderEventType.LegacyLastTick: - // we need to use the LegacyLastTick here for compatibility reasons (difficulty). - // it is *okay* to use this because the TailCircle is not used for any meaningful purpose in gameplay. - // if this is to change, we should revisit this. + case SliderEventType.Tail: AddNested(TailCircle = new SliderTailCircle(this) { RepeatIndex = e.SpanIndex, @@ -262,7 +255,9 @@ namespace osu.Game.Rulesets.Osu.Objects if (HeadCircle != null) HeadCircle.Samples = this.GetNodeSamples(0); - // The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to. + // The samples should be attached to the slider tail, however this can only be done if LastTick is removed otherwise they would play earlier than they're intended to. + // (see mapping logic in `CreateNestedHitObjects` above) + // // For now, the samples are played by the slider itself at the correct end time. TailSamples = this.GetNodeSamples(repeatCount + 1); } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs index f52c3ab382..ddbbb300ca 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.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 osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs index 2a84b04030..73c222653e 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; diff --git a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs index 7b9316f8ac..cca86361c2 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index 87c8117b6b..54d2afb444 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -1,19 +1,12 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { - /// - /// Note that this should not be used for timing correctness. - /// See usage in for more information. - /// public class SliderTailCircle : SliderEndCircle { public SliderTailCircle(Slider slider) diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs index 676ff62455..74ec4d6eb3 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.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 osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index ba0981e781..e3dfe8e69a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.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.Collections.Generic; using System.Linq; @@ -20,6 +18,16 @@ namespace osu.Game.Rulesets.Osu.Objects { public class Spinner : OsuHitObject, IHasDuration { + /// + /// The RPM required to clear the spinner at ODs [ 0, 5, 10 ]. + /// + private static readonly (int min, int mid, int max) clear_rpm_range = (90, 150, 225); + + /// + /// The RPM required to complete the spinner and receive full score at ODs [ 0, 5, 10 ]. + /// + private static readonly (int min, int mid, int max) complete_rpm_range = (250, 380, 430); + public double EndTime { get => StartTime + Duration; @@ -33,6 +41,16 @@ namespace osu.Game.Rulesets.Osu.Objects ///
public int SpinsRequired { get; protected set; } = 1; + /// + /// The number of spins required to start receiving bonus score. The first bonus is awarded on this spin count. + /// + public int SpinsRequiredForBonus => SpinsRequired + bonus_spins_gap; + + /// + /// The gap between spinner completion and the first bonus-awarding spin. + /// + private const int bonus_spins_gap = 2; + /// /// Number of spins available to give bonus, beyond . /// @@ -44,25 +62,26 @@ namespace osu.Game.Rulesets.Osu.Objects { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - // spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being. - const double stable_matching_fudge = 0.6; + // The average RPS required over the length of the spinner to clear the spinner. + double minRps = IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, clear_rpm_range) / 60; - // close to 477rpm - const double maximum_rotations_per_second = 8; + // The RPS required over the length of the spinner to receive full score (all normal + bonus ticks). + double maxRps = IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, complete_rpm_range) / 60; double secondsDuration = Duration / 1000; - double minimumRotationsPerSecond = stable_matching_fudge * IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5); + // Allow a 0.1ms floating point precision error in the calculation of the duration. + const double duration_error = 0.0001; - SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond); - MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration); + SpinsRequired = (int)(minRps * secondsDuration + duration_error); + MaximumBonusSpins = Math.Max(0, (int)(maxRps * secondsDuration + duration_error) - SpinsRequired - bonus_spins_gap); } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { base.CreateNestedHitObjects(cancellationToken); - int totalSpins = MaximumBonusSpins + SpinsRequired; + int totalSpins = MaximumBonusSpins + SpinsRequired + bonus_spins_gap; for (int i = 0; i < totalSpins; i++) { @@ -70,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.Objects double startTime = StartTime + (float)(i + 1) / totalSpins * Duration; - AddNested(i < SpinsRequired + AddNested(i < SpinsRequiredForBonus ? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration } : new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration, Samples = new[] { CreateHitSampleInfo("spinnerbonus") } }); } diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs index 00ceccaf7b..8d53100529 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs index c890f3771b..7989c9b7ff 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 8ce55d78dd..607b83d379 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -33,6 +33,7 @@ using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; @@ -113,6 +114,9 @@ namespace osu.Game.Rulesets.Osu if (mods.HasFlagFast(LegacyMods.TouchDevice)) yield return new OsuModTouchDevice(); + + if (mods.HasFlagFast(LegacyMods.ScoreV2)) + yield return new ModScoreV2(); } public override LegacyMods ConvertToLegacyMods(Mod[] mods) @@ -204,13 +208,15 @@ namespace osu.Game.Rulesets.Osu new MultiMod(new OsuModMagnetised(), new OsuModRepel()), new ModAdaptiveSpeed(), new OsuModFreezeFrame(), - new OsuModBubbles() + new OsuModBubbles(), + new OsuModSynesthesia() }; case ModType.System: return new Mod[] { new OsuModTouchDevice(), + new ModScoreV2(), }; default: @@ -252,6 +258,8 @@ namespace osu.Game.Rulesets.Osu public int LegacyID => 0; + public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new OsuLegacyScoreSimulator(); + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame(); public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new OsuRulesetConfigManager(settings, RulesetInfo); @@ -312,7 +320,7 @@ namespace osu.Game.Rulesets.Osu RelativeSizeAxes = Axes.X, Height = 250 }, true), - new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem("Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[] { new AverageHitError(timedHitEvents), new UnstableRate(timedHitEvents) diff --git a/osu.Game.Rulesets.Osu/Properties/AssemblyInfo.cs b/osu.Game.Rulesets.Osu/Properties/AssemblyInfo.cs index 7fffb1871f..c842874635 100644 --- a/osu.Game.Rulesets.Osu/Properties/AssemblyInfo.cs +++ b/osu.Game.Rulesets.Osu/Properties/AssemblyInfo.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.Runtime.CompilerServices; // We publish our internal attributes to other sub-projects of the framework. diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider-expected-conversion.json new file mode 100755 index 0000000000..86a4a278f1 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":77497.0,"Objects":[{"StartTime":77497.0,"EndTime":77497.0,"X":298.0,"Y":290.0},{"StartTime":77533.0,"EndTime":77533.0,"X":276.162567,"Y":293.0336}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider.osu new file mode 100755 index 0000000000..fa545a7614 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider.osu @@ -0,0 +1,18 @@ +osu file format v14 + +[Difficulty] +HPDrainRate:5.8 +CircleSize:4 +OverallDifficulty:9.6 +ApproachRate:10 +SliderMultiplier:2 +SliderTickRate:1 + +[TimingPoints] +77211,-100,4,3,50,70,0,0 +77497,8.40402703648439,4,3,51,70,1,8 +77497,NaN,4,3,51,70,0,8 +77498,285.714285714286,4,3,51,70,1,0 + +[HitObjects] +298,290,77497,6,0,B|234:298|192:279|192:279|180:299|180:299|205:311|238:318|238:318|230:347|217:371|217:371|137:370|80:340|80:340|65:259|73:143|102:68|102:68|149:49|199:34|199:34|213:54|213:54|267:38|324:40|324:40|332:18|332:18|385:20|435:27|435:27|480:93|517:204|521:286|521:286|474:329|396:350|396:350|377:329|363:302|363:302|393:287|415:271|415:271|398:254|398:254|362:282|299:290,1,1723.66345596313,10|0,1:0|3:0,3:0:0:0: diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-paths-edge-case-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-paths-edge-case-expected-conversion.json new file mode 100644 index 0000000000..5b04027cd6 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-paths-edge-case-expected-conversion.json @@ -0,0 +1,94 @@ +{ + "Mappings": [ + { + "StartTime": 46060.0, + "Objects": [ + { + "StartTime": 46060.0, + "EndTime": 46060.0, + "X": 160.0, + "Y": 208.0, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 46398.0, + "EndTime": 46398.0, + "X": 160.980164, + "Y": 317.779083, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 46737.0, + "EndTime": 46737.0, + "X": 268.887268, + "Y": 320.0, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 47040.0, + "EndTime": 47040.0, + "X": 378.995544, + "Y": 320.0, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + } + ] + }, + { + "StartTime": 123348.0, + "Objects": [ + { + "StartTime": 123348.0, + "EndTime": 123348.0, + "X": 352.0, + "Y": 160.0, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 123686.0, + "EndTime": 123686.0, + "X": 351.019836, + "Y": 50.2209129, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 124025.0, + "EndTime": 124025.0, + "X": 243.112747, + "Y": 48.0, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 124328.0, + "EndTime": 124328.0, + "X": 133.004471, + "Y": 48.0, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + } + ] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-paths-edge-case.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-paths-edge-case.osu new file mode 100644 index 0000000000..1b6cd0417b --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-paths-edge-case.osu @@ -0,0 +1,19 @@ +osu file format v6 + +[General] +StackLeniency: 0.7 + +[Difficulty] +HPDrainRate:1 +CircleSize:3 +OverallDifficulty:1 +SliderMultiplier:1.1 +SliderTickRate:1 + +[TimingPoints] +-41,338.983050847458,4,1,0,70,1,0 +93648,-100,4,1,0,70,0,0 + +[HitObjects] +160,208,46060,6,0,B|161:320|161:320|271:320|271:320,1,330,8|0 +352,160,123348,6,0,B|351:48|351:48|241:48|241:48,1,330,8|0 diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks-edge-case-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks-edge-case-expected-conversion.json new file mode 100644 index 0000000000..6d97b643b1 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks-edge-case-expected-conversion.json @@ -0,0 +1,39 @@ +{ + "Mappings": [ + { + "StartTime": 7493.0, + "Objects": [ + { + "StartTime": 7493.0, + "EndTime": 7493.0, + "X": 130.0, + "Y": 232.0, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 7817.0, + "EndTime": 7817.0, + "X": 30.9946651, + "Y": 208.5157, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 7843.0, + "EndTime": 7843.0, + "X": 33.7820168, + "Y": 208.9957, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + } + ] + } + ] +} diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks-edge-case.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks-edge-case.osu new file mode 100644 index 0000000000..daf35e1d2b --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks-edge-case.osu @@ -0,0 +1,31 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 + +[Difficulty] +HPDrainRate:3 +CircleSize:3.4 +OverallDifficulty:4 +ApproachRate:5.5 +SliderMultiplier:1.1 +SliderTickRate:1 + +[Events] +//Background and Video events +0,0,"aa.jpg",0,0 +//Break Periods +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Layer 4 (Overlay) +//Storyboard Sound Samples + +[TimingPoints] +6967,350.877192982456,6,2,1,55,1,0 +6967,-100,3,2,1,55,0,0 +7493,-111.111111111111,3,2,1,55,0,0 + +[HitObjects] +130,232,7493,6,0,P|78:218|28:208,1,101.82149697876,0|0,3:2|2:0,2:0:0:0: diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/very-fast-slider.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/very-fast-slider.osu new file mode 100644 index 0000000000..58ef36e70c --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/very-fast-slider.osu @@ -0,0 +1,21 @@ +osu file format v128 + +[Difficulty] +HPDrainRate: 3 +CircleSize: 4 +OverallDifficulty: 9 +ApproachRate: 9.3 +SliderMultiplier: 3.59999990463257 +SliderTickRate: 1 + +[TimingPoints] +812,342.857142857143,4,1,1,70,1,0 +57383,-28.5714285714286,4,1,1,70,0,0 + +[HitObjects] +// Taken from https://osu.ppy.sh/beatmapsets/881996#osu/1844019 +// This slider is 42 ms in length, triggering the LegacyLastTick edge case. +// The tick will be at 21.5 ms (sliderDuration / 2) instead of 6 ms (sliderDuration - LAST_TICK_LENIENCE). +416,41,57383,6,0,L|467:217,1,157.499997329712,2|0,3:3|3:0,3:0:0:0: +// Include the next slider as well to cover the jump back to the start position. +407,73,57469,2,0,L|470:215,1,129.599999730835,2|0,0:0|0:0,0:0:0:0: diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs index 1c5cf49625..7508a689d2 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs @@ -48,21 +48,26 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon private Bindable configHitLighting = null!; + private static readonly Vector2 circle_size = OsuHitObject.OBJECT_DIMENSIONS; + [Resolved] private DrawableHitObject drawableObject { get; set; } = null!; public ArgonMainCirclePiece(bool withOuterFill) { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = circle_size; Anchor = Anchor.Centre; Origin = Anchor.Centre; InternalChildren = new Drawable[] { - outerFill = new Circle // renders white outer border and dark fill + outerFill = new Circle // renders dark fill { - Size = Size, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + // Slightly inset to prevent bleeding outside the ring + Size = circle_size - new Vector2(1), Alpha = withOuterFill ? 1 : 0, }, outerGradient = new Circle // renders the outer bright gradient @@ -88,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon Masking = true, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = Size, + Size = circle_size, Child = new KiaiFlash { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs index f93e26b2ca..87b89a07cf 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs @@ -1,15 +1,19 @@ // 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.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osuTK; using osuTK.Graphics; @@ -17,38 +21,98 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon { public partial class ArgonReverseArrow : CompositeDrawable { + private DrawableSliderRepeat drawableRepeat { get; set; } = null!; + private Bindable accentColour = null!; private SpriteIcon icon = null!; + private Container main = null!; + private Sprite side = null!; [BackgroundDependencyLoader] - private void load(DrawableHitObject hitObject) + private void load(DrawableHitObject drawableObject, TextureStore textures) { + drawableRepeat = (DrawableSliderRepeat)drawableObject; + Anchor = Anchor.Centre; Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; InternalChildren = new Drawable[] { - new Circle + main = new Container { - Size = new Vector2(40, 20), - Colour = Color4.White, + RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, + Children = new Drawable[] + { + new Circle + { + Size = new Vector2(40, 20), + Colour = Color4.White, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + icon = new SpriteIcon + { + Icon = FontAwesome.Solid.AngleDoubleRight, + Size = new Vector2(16), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } }, - icon = new SpriteIcon + side = new Sprite { - Icon = FontAwesome.Solid.AngleDoubleRight, - Size = new Vector2(16), Anchor = Anchor.Centre, Origin = Anchor.Centre, - }, + Texture = textures.Get("Gameplay/osu/repeat-edge-piece"), + Size = new Vector2(ArgonMainCirclePiece.OUTER_GRADIENT_SIZE), + } }; - accentColour = hitObject.AccentColour.GetBoundCopy(); + accentColour = drawableRepeat.AccentColour.GetBoundCopy(); accentColour.BindValueChanged(accent => icon.Colour = accent.NewValue.Darken(4), true); + + drawableRepeat.ApplyCustomUpdateState += updateStateTransforms; + } + + private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state) + { + const float move_distance = -12; + const double move_out_duration = 35; + const double move_in_duration = 250; + const double total = 300; + + switch (state) + { + case ArmedState.Idle: + main.ScaleTo(1.3f, move_out_duration, Easing.Out) + .Then() + .ScaleTo(1f, move_in_duration, Easing.Out) + .Loop(total - (move_in_duration + move_out_duration)); + side + .MoveToX(move_distance, move_out_duration, Easing.Out) + .Then() + .MoveToX(0, move_in_duration, Easing.Out) + .Loop(total - (move_in_duration + move_out_duration)); + break; + + case ArmedState.Hit: + double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration); + this.ScaleTo(1.5f, animDuration, Easing.Out); + break; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (drawableRepeat.IsNotNull()) + drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms; } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs index 461b4a3b45..325f0a22ad 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs @@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon base.Update(); //undo rotation on layers which should not be rotated. - float appliedRotation = Parent.Rotation; + float appliedRotation = Parent!.Rotation; fill.Rotation = -appliedRotation; } diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinner.cs index d5a9cf46c5..ee9f228137 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinner.cs @@ -35,14 +35,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon InternalChildren = new Drawable[] { - bonusCounter = new OsuSpriteText - { - Alpha = 0, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.Default.With(size: 24), - Y = -120, - }, new ArgonSpinnerDisc { RelativeSizeAxes = Axes.Both, @@ -85,19 +77,33 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon }; } - private IBindable gainedBonus = null!; + private IBindable completedSpins = null!; private IBindable spinsPerMinute = null!; protected override void LoadComplete() { base.LoadComplete(); - gainedBonus = drawableSpinner.GainedBonus.GetBoundCopy(); - gainedBonus.BindValueChanged(bonus => + completedSpins = drawableSpinner.CompletedFullSpins.GetBoundCopy(); + completedSpins.BindValueChanged(_ => { - bonusCounter.Text = bonus.NewValue.ToString(NumberFormatInfo.InvariantInfo); - bonusCounter.FadeOutFromOne(1500); - bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint); + if (drawableSpinner.CurrentBonusScore <= 0) + return; + + if (drawableSpinner.CurrentBonusScore == drawableSpinner.MaximumBonusScore) + { + bonusCounter.Text = "MAX"; + bonusCounter.ScaleTo(1.5f).Then().ScaleTo(2.8f, 1000, Easing.OutQuint); + + bonusCounter.FlashColour(Colour4.FromHex("FC618F"), 400); + bonusCounter.FadeOutFromOne(500); + } + else + { + bonusCounter.Text = drawableSpinner.CurrentBonusScore.ToString(NumberFormatInfo.InvariantInfo); + bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint); + bonusCounter.FadeOutFromOne(1500); + } }); spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy(); diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs index bdc93eb63f..079758c21e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon { get { - int rotations = (int)(drawableSpinner.Result.RateAdjustedRotation / 360); + int rotations = (int)(drawableSpinner.Result.TotalRotation / 360); if (wholeRotationCount == rotations) return false; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs index f4761e0ea8..65a7b1328b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; -using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Default { @@ -22,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default public CirclePiece() { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Masking = true; CornerRadius = Size.X / 2; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultReverseArrow.cs new file mode 100644 index 0000000000..ad49150d81 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultReverseArrow.cs @@ -0,0 +1,76 @@ +// 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.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Skinning.Default +{ + public partial class DefaultReverseArrow : CompositeDrawable + { + private DrawableSliderRepeat drawableRepeat { get; set; } = null!; + + public DefaultReverseArrow() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Size = OsuHitObject.OBJECT_DIMENSIONS; + + InternalChild = new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Icon = FontAwesome.Solid.ChevronRight, + Size = new Vector2(0.35f), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableObject) + { + drawableRepeat = (DrawableSliderRepeat)drawableObject; + drawableRepeat.ApplyCustomUpdateState += updateStateTransforms; + } + + private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state) + { + const double move_out_duration = 35; + const double move_in_duration = 250; + const double total = 300; + + switch (state) + { + case ArmedState.Idle: + InternalChild.ScaleTo(1.3f, move_out_duration, Easing.Out) + .Then() + .ScaleTo(1f, move_in_duration, Easing.Out) + .Loop(total - (move_in_duration + move_out_duration)); + break; + + case ArmedState.Hit: + double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration); + InternalChild.ScaleTo(1.5f, animDuration, Easing.Out); + break; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (drawableRepeat.IsNotNull()) + drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs index 071fbe6add..4a76a1aec4 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs @@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default private Container spmContainer = null!; private OsuSpriteText spmCounter = null!; + [Resolved] + private OsuColour colours { get; set; } = null!; + public DefaultSpinner() { RelativeSizeAxes = Axes.Both; @@ -80,19 +83,33 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default }); } - private IBindable gainedBonus = null!; + private IBindable completedSpins = null!; private IBindable spinsPerMinute = null!; protected override void LoadComplete() { base.LoadComplete(); - gainedBonus = drawableSpinner.GainedBonus.GetBoundCopy(); - gainedBonus.BindValueChanged(bonus => + completedSpins = drawableSpinner.CompletedFullSpins.GetBoundCopy(); + completedSpins.BindValueChanged(bonus => { - bonusCounter.Text = bonus.NewValue.ToString(NumberFormatInfo.InvariantInfo); - bonusCounter.FadeOutFromOne(1500); - bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint); + if (drawableSpinner.CurrentBonusScore <= 0) + return; + + if (drawableSpinner.CurrentBonusScore == drawableSpinner.MaximumBonusScore) + { + bonusCounter.Text = "MAX"; + bonusCounter.ScaleTo(1.5f).Then().ScaleTo(2.8f, 1000, Easing.OutQuint); + + bonusCounter.FlashColour(colours.YellowLight, 400); + bonusCounter.FadeOutFromOne(500); + } + else + { + bonusCounter.Text = drawableSpinner.CurrentBonusScore.ToString(NumberFormatInfo.InvariantInfo); + bonusCounter.FadeOutFromOne(1500); + bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint); + } }); spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy(); diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs index 75f3247448..b498975a83 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs @@ -200,7 +200,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { get { - int rotations = (int)(drawableSpinner.Result.RateAdjustedRotation / 360); + int rotations = (int)(drawableSpinner.Result.TotalRotation / 360); if (wholeRotationCount == rotations) return false; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/ExplodePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/ExplodePiece.cs index 91bf75617a..7beb16f7d7 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/ExplodePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/ExplodePiece.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; -using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Default { @@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default public ExplodePiece() { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/FlashPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/FlashPiece.cs index 789137117e..86087ac50d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/FlashPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/FlashPiece.cs @@ -5,7 +5,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Osu.Objects; -using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Default { @@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { public FlashPiece() { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs index 20fa4e5342..bcea33f63c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Default @@ -25,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default public MainCirclePiece() { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs index 539777dd6b..aa507cbaf0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs @@ -46,22 +46,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default BorderSize = skin.GetConfig(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1; BorderColour = skin.GetConfig(OsuSkinColour.SliderBorder)?.Value ?? Color4.White; - - drawableObject.HitObjectApplied += onHitObjectApplied; - } - - private void onHitObjectApplied(DrawableHitObject obj) - { - var drawableSlider = (DrawableSlider)obj; - if (drawableSlider.HitObject == null) - return; - - // When not tracking the follow circle, unbind from the config and forcefully disable snaking out - it looks better that way. - if (!drawableSlider.HeadCircle.TrackFollowCircle) - { - SnakingOut.UnbindFrom(configSnakingOut); - SnakingOut.Value = false; - } } protected virtual Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour) => diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/ReverseArrowPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/ReverseArrowPiece.cs deleted file mode 100644 index 3fe7872ff7..0000000000 --- a/osu.Game.Rulesets.Osu/Skinning/Default/ReverseArrowPiece.cs +++ /dev/null @@ -1,51 +0,0 @@ -// 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.Audio.Track; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics.Containers; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Skinning; -using osuTK; - -namespace osu.Game.Rulesets.Osu.Skinning.Default -{ - public partial class ReverseArrowPiece : BeatSyncedContainer - { - [Resolved] - private DrawableHitObject drawableRepeat { get; set; } = null!; - - public ReverseArrowPiece() - { - Divisor = 2; - MinimumBeatLength = 200; - - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); - - Child = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.ReverseArrow), _ => new SpriteIcon - { - RelativeSizeAxes = Axes.Both, - Blending = BlendingParameters.Additive, - Icon = FontAwesome.Solid.ChevronRight, - Size = new Vector2(0.35f) - }) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }; - } - - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) - { - if (!drawableRepeat.IsHit) - Child.ScaleTo(1.3f).ScaleTo(1f, timingPoint.BeatLength, Easing.Out); - } - } -} diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs index 46d48f62e7..c3bbd89ab6 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs @@ -5,7 +5,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Osu.Objects; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Default @@ -14,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { public RingPiece(float thickness = 9) { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index bf06f513b7..1d75663fd9 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; @@ -22,11 +23,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default private readonly DrawableSpinner drawableSpinner; - private Vector2 mousePosition; + private Vector2? mousePosition; + private float? lastAngle; - private float lastAngle; private float currentRotation; - private bool rotationTransferred; [Resolved(canBeNull: true)] @@ -56,25 +56,31 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default protected override bool OnMouseMove(MouseMoveEvent e) { - mousePosition = Parent.ToLocalSpace(e.ScreenSpaceMousePosition); + mousePosition = Parent!.ToLocalSpace(e.ScreenSpaceMousePosition); return base.OnMouseMove(e); } protected override void Update() { base.Update(); - float thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2)); - float delta = thisAngle - lastAngle; + if (mousePosition is Vector2 pos) + { + float thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(pos.X - DrawSize.X / 2, pos.Y - DrawSize.Y / 2)); + float delta = lastAngle == null ? 0 : thisAngle - lastAngle.Value; - if (Tracking) - AddRotation(delta); + // Normalise the delta to -180 .. 180 + if (delta > 180) delta -= 360; + if (delta < -180) delta += 360; - lastAngle = thisAngle; + if (Tracking) + AddRotation(delta); - IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation / 2 - Rotation) > 5f; + lastAngle = thisAngle; + } - Rotation = (float)Interpolation.Damp(Rotation, currentRotation / 2, 0.99, Math.Abs(Time.Elapsed)); + IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f; + Rotation = (float)Interpolation.Damp(Rotation, currentRotation, 0.99, Math.Abs(Time.Elapsed)); } /// @@ -83,41 +89,35 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default /// /// Will be a no-op if not a valid time to spin. /// - /// The delta angle. - public void AddRotation(float angle) + /// The delta angle. + public void AddRotation(float delta) { if (!isSpinnableTime) return; if (!rotationTransferred) { - currentRotation = Rotation * 2; + currentRotation = Rotation; rotationTransferred = true; } - if (angle > 180) - { - lastAngle += 360; - angle -= 360; - } - else if (-angle > 180) - { - lastAngle -= 360; - angle += 360; - } + Debug.Assert(Math.Abs(delta) <= 180); - currentRotation += angle; - // rate has to be applied each frame, because it's not guaranteed to be constant throughout playback - // (see: ModTimeRamp) - drawableSpinner.Result.RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate)); + double rate = gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate; + delta = (float)(delta * Math.Abs(rate)); + + currentRotation += delta; + drawableSpinner.Result.History.ReportDelta(Time.Current, delta); } private void resetState(DrawableHitObject obj) { Tracking = false; IsSpinning.Value = false; - mousePosition = default; - lastAngle = currentRotation = Rotation = 0; + mousePosition = null; + lastAngle = null; + currentRotation = 0; + Rotation = 0; rotationTransferred = false; } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs index e9342bbdbb..eea6606233 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs @@ -5,12 +5,14 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { + // todo: this should probably not be a SkinnableSprite, as this is always created for legacy skins and is recreated on skin change. public partial class LegacyApproachCircle : SkinnableSprite { private readonly IBindable accentColour = new Bindable(); @@ -19,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private DrawableHitObject drawableObject { get; set; } = null!; public LegacyApproachCircle() - : base("Gameplay/osu/approachcircle") + : base("Gameplay/osu/approachcircle", OsuHitObject.OBJECT_DIMENSIONS * 2) { } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index cadac4d319..d8d86d1802 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy this.priorityLookupPrefix = priorityLookupPrefix; this.hasNumber = hasNumber; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; } [BackgroundDependencyLoader] @@ -63,12 +63,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy // otherwise fall back to the default prefix "hitcircle". string circleName = (priorityLookupPrefix != null && skin.GetTexture(priorityLookupPrefix) != null) ? priorityLookupPrefix : @"hitcircle"; + Vector2 maxSize = OsuHitObject.OBJECT_DIMENSIONS * 2; + // at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it. // the conditional above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist. // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png. InternalChildren = new[] { - CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) }) + CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(maxSize) }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -77,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d)) + Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs index e6166e9441..780084115d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs @@ -1,12 +1,16 @@ // 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 osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK.Graphics; @@ -15,8 +19,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public partial class LegacyReverseArrow : CompositeDrawable { - [Resolved(canBeNull: true)] - private DrawableHitObject? drawableHitObject { get; set; } + private DrawableSliderRepeat drawableRepeat { get; set; } = null!; private Drawable proxy = null!; @@ -26,17 +29,31 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private Drawable arrow = null!; + private bool shouldRotate; + [BackgroundDependencyLoader] - private void load(ISkinSource skinSource) + private void load(DrawableHitObject drawableObject, ISkinSource skinSource) { + drawableRepeat = (DrawableSliderRepeat)drawableObject; + AutoSizeAxes = Axes.Both; string lookupName = new OsuSkinComponentLookup(OsuSkinComponents.ReverseArrow).LookupName; var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null); - InternalChild = arrow = (skin?.GetAnimation(lookupName, true, true) ?? Empty()); + InternalChild = arrow = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = skin?.GetTexture(lookupName)?.WithMaximumSize(maxSize: OsuHitObject.OBJECT_DIMENSIONS * 2), + }; + textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin; + + drawableObject.ApplyCustomUpdateState += updateStateTransforms; + + shouldRotate = skinSource.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value <= 1; } protected override void LoadComplete() @@ -45,17 +62,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy proxy = CreateProxy(); - if (drawableHitObject != null) - { - drawableHitObject.HitObjectApplied += onHitObjectApplied; - onHitObjectApplied(drawableHitObject); + drawableRepeat.HitObjectApplied += onHitObjectApplied; + onHitObjectApplied(drawableRepeat); - accentColour = drawableHitObject.AccentColour.GetBoundCopy(); - accentColour.BindValueChanged(c => - { - arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > (600 / 255f) ? Color4.Black : Color4.White; - }, true); - } + accentColour = drawableRepeat.AccentColour.GetBoundCopy(); + accentColour.BindValueChanged(c => + { + arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > (600 / 255f) ? Color4.Black : Color4.White; + }, true); } private void onHitObjectApplied(DrawableHitObject drawableObject) @@ -63,15 +77,51 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Debug.Assert(proxy.Parent == null); // see logic in LegacySliderHeadHitCircle. - (drawableObject as DrawableSliderRepeat)?.DrawableSlider - .OverlayElementContainer.Add(proxy); + drawableRepeat.DrawableSlider.OverlayElementContainer.Add(proxy); + } + + private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state) + { + const double duration = 300; + const float rotation = 5.625f; + + switch (state) + { + case ArmedState.Idle: + if (shouldRotate) + { + InternalChild.ScaleTo(1.3f) + .RotateTo(rotation) + .Then() + .ScaleTo(1f, duration) + .RotateTo(-rotation, duration) + .Loop(); + } + else + { + InternalChild.ScaleTo(1.3f).Then() + .ScaleTo(1f, duration, Easing.Out) + .Loop(); + } + + break; + + case ArmedState.Hit: + double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration); + InternalChild.ScaleTo(1.4f, animDuration, Easing.Out); + break; + } } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - if (drawableHitObject != null) - drawableHitObject.HitObjectApplied -= onHitObjectApplied; + + if (drawableRepeat.IsNotNull()) + { + drawableRepeat.HitObjectApplied -= onHitObjectApplied; + drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms; + } } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs index 2aa843581e..45c59e970b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs @@ -1,35 +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; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public partial class LegacySliderBall : CompositeDrawable { - private readonly Drawable animationContent; - private readonly ISkin skin; [Resolved(canBeNull: true)] private DrawableHitObject? parentObject { get; set; } - public Color4 BallColour => animationContent.Colour; - private Sprite layerNd = null!; private Sprite layerSpec = null!; - public LegacySliderBall(Drawable animationContent, ISkin skin) + private TextureAnimation ballAnimation = null!; + private Texture[] ballTextures = null!; + + public Color4 BallColour => ballAnimation.Colour; + + public LegacySliderBall(ISkin skin) { - this.animationContent = animationContent; this.skin = skin; AutoSizeAxes = Axes.Both; @@ -38,30 +42,39 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy [BackgroundDependencyLoader] private void load() { - var ballColour = skin.GetConfig(OsuSkinColour.SliderBall)?.Value ?? Color4.White; + Vector2 maxSize = OsuLegacySkinTransformer.MAX_FOLLOW_CIRCLE_AREA_SIZE; - InternalChildren = new[] + var ballColour = skin.GetConfig(OsuSkinColour.SliderBall)?.Value ?? Color4.White; + ballTextures = skin.GetTextures("sliderb", default, default, true, "", maxSize, out _); + + InternalChildren = new Drawable[] { layerNd = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = skin.GetTexture("sliderb-nd"), + Texture = skin.GetTexture("sliderb-nd")?.WithMaximumSize(maxSize), Colour = new Color4(5, 5, 5, 255), }, - LegacyColourCompatibility.ApplyWithDoubledAlpha(animationContent.With(d => + ballAnimation = new LegacySkinExtensions.SkinnableTextureAnimation { - d.Anchor = Anchor.Centre; - d.Origin = Anchor.Centre; - }), ballColour), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = ballColour, + }, layerSpec = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = skin.GetTexture("sliderb-spec"), + Texture = skin.GetTexture("sliderb-spec")?.WithMaximumSize(maxSize), Blending = BlendingParameters.Additive, }, }; + + if (parentObject != null) + parentObject.HitObjectApplied += onHitObjectApplied; + + onHitObjectApplied(parentObject); } private readonly IBindable accentColour = new Bindable(); @@ -78,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy if (skin.GetConfig(SkinConfiguration.LegacySetting.AllowSliderBallTint)?.Value == true) { accentColour.BindTo(parentObject.AccentColour); - accentColour.BindValueChanged(a => animationContent.Colour = a.NewValue, true); + accentColour.BindValueChanged(a => ballAnimation.Colour = a.NewValue, true); } } } @@ -88,12 +101,32 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy base.UpdateAfterChildren(); //undo rotation on layers which should not be rotated. - float appliedRotation = Parent.Rotation; + float appliedRotation = Parent!.Rotation; layerNd.Rotation = -appliedRotation; layerSpec.Rotation = -appliedRotation; } + private void onHitObjectApplied(DrawableHitObject? drawableObject = null) + { + double frameDelay; + + if (drawableObject?.HitObject != null) + { + DrawableSlider drawableSlider = (DrawableSlider)drawableObject; + + frameDelay = Math.Max( + 0.15 / drawableSlider.HitObject.Velocity * LegacySkinExtensions.SIXTY_FRAME_TIME, + LegacySkinExtensions.SIXTY_FRAME_TIME); + } + else + frameDelay = LegacySkinExtensions.SIXTY_FRAME_TIME; + + ballAnimation.ClearFrames(); + foreach (var texture in ballTextures) + ballAnimation.AddFrame(texture, frameDelay); + } + private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState _) { // Gets called by slider ticks, tails, etc., leading to duplicated @@ -114,7 +147,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy base.Dispose(isDisposing); if (parentObject != null) + { + parentObject.HitObjectApplied -= onHitObjectApplied; parentObject.ApplyCustomUpdateState -= updateStateTransforms; + } } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index d8f837ae5e..5a95eac0f1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Origin = Anchor.Centre, Scale = new Vector2(SPRITE_SCALE), Y = SPINNER_TOP_OFFSET + 299, - }.With(s => s.Font = s.Font.With(fixedWidth: false)), + }, spmBackground = new Sprite { Anchor = Anchor.TopCentre, @@ -102,12 +102,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Origin = Anchor.TopRight, Scale = new Vector2(SPRITE_SCALE * 0.9f), Position = new Vector2(80, 448 + spm_hide_offset), - }.With(s => s.Font = s.Font.With(fixedWidth: false)), + }, } }); } - private IBindable gainedBonus = null!; + private IBindable completedSpins = null!; private IBindable spinsPerMinute = null!; private readonly Bindable completed = new Bindable(); @@ -116,12 +116,24 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { base.LoadComplete(); - gainedBonus = DrawableSpinner.GainedBonus.GetBoundCopy(); - gainedBonus.BindValueChanged(bonus => + completedSpins = DrawableSpinner.CompletedFullSpins.GetBoundCopy(); + completedSpins.BindValueChanged(bonus => { - bonusCounter.Text = bonus.NewValue.ToString(NumberFormatInfo.InvariantInfo); - bonusCounter.FadeOutFromOne(800, Easing.Out); - bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out); + if (DrawableSpinner.CurrentBonusScore <= 0) + return; + + bonusCounter.Text = DrawableSpinner.CurrentBonusScore.ToString(NumberFormatInfo.InvariantInfo); + + if (DrawableSpinner.CurrentBonusScore == DrawableSpinner.MaximumBonusScore) + { + bonusCounter.ScaleTo(1.4f).Then().ScaleTo(1.8f, 1000, Easing.Out); + bonusCounter.FadeOutFromOne(500, Easing.Out); + } + else + { + bonusCounter.FadeOutFromOne(800, Easing.Out); + bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out); + } }); spinsPerMinute = DrawableSpinner.SpinsPerMinute.GetBoundCopy(); diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index f049aa088f..c01d28c8e1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Skinning; using osuTK; @@ -20,7 +21,17 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy /// Their hittable area is 128px, but the actual circle portion is 118px. /// We must account for some gameplay elements such as slider bodies, where this padding is not present. /// - public const float LEGACY_CIRCLE_RADIUS = 64 - 5; + public const float LEGACY_CIRCLE_RADIUS = OsuHitObject.OBJECT_RADIUS - 5; + + /// + /// The maximum allowed size of sprites that reside in the follow circle area of a slider. + /// + /// + /// The reason this is extracted out to a constant, rather than be inlined in the follow circle sprite retrieval, + /// is that some skins will use `sliderb` elements to emulate a slider follow circle with slightly different visual effects applied + /// (`sliderb` is always shown and doesn't pulsate; `sliderfollowcircle` isn't always shown and pulsates). + /// + public static readonly Vector2 MAX_FOLLOW_CIRCLE_AREA_SIZE = OsuHitObject.OBJECT_DIMENSIONS * 3; public OsuLegacySkinTransformer(ISkin skin) : base(skin) @@ -35,26 +46,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy switch (osuComponent.Component) { case OsuSkinComponents.FollowPoint: - return this.GetAnimation("followpoint", true, true, true, startAtCurrentTime: false); + return this.GetAnimation("followpoint", true, true, true, startAtCurrentTime: false, maxSize: new Vector2(OsuHitObject.OBJECT_RADIUS * 2, OsuHitObject.OBJECT_RADIUS)); case OsuSkinComponents.SliderScorePoint: - return this.GetAnimation("sliderscorepoint", false, false); + return this.GetAnimation("sliderscorepoint", false, false, maxSize: OsuHitObject.OBJECT_DIMENSIONS); case OsuSkinComponents.SliderFollowCircle: - var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true); + var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true, maxSize: MAX_FOLLOW_CIRCLE_AREA_SIZE); if (followCircleContent != null) return new LegacyFollowCircle(followCircleContent); return null; case OsuSkinComponents.SliderBall: - var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: ""); - - // todo: slider ball has a custom frame delay based on velocity - // Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME); - - if (sliderBallContent != null) - return new LegacySliderBall(sliderBallContent, this); + if (GetTexture("sliderb") != null || GetTexture("sliderb0") != null) + return new LegacySliderBall(this); return null; @@ -138,10 +144,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy if (!this.HasFont(LegacyFont.HitCircle)) return null; + const float hitcircle_text_scale = 0.8f; return new LegacySpriteText(LegacyFont.HitCircle) { // stable applies a blanket 0.8x scale to hitcircle fonts - Scale = new Vector2(0.8f), + Scale = new Vector2(hitcircle_text_scale), + MaxSizePerGlyph = OsuHitObject.OBJECT_DIMENSIONS * 2 / hitcircle_text_scale, }; case OsuSkinComponents.SpinnerBody: diff --git a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs index 9d64c354e2..d818c8baee 100644 --- a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs +++ b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs @@ -257,7 +257,7 @@ namespace osu.Game.Rulesets.Osu.Skinning texture.Bind(); for (int i = 0; i < points.Count; i++) - drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex); + drawPointQuad(renderer, points[i], textureRect, i + firstVisiblePointIndex); UnbindTextureShader(renderer); renderer.PopLocalMatrix(); @@ -325,7 +325,7 @@ namespace osu.Game.Rulesets.Osu.Skinning private float getRotation(int index) => max_rotation * (StatelessRNG.NextSingle(rotationSeed, index) * 2 - 1); - private void drawPointQuad(SmokePoint point, RectangleF textureRect, int index) + private void drawPointQuad(IRenderer renderer, SmokePoint point, RectangleF textureRect, int index) { Debug.Assert(quadBatch != null); @@ -347,25 +347,25 @@ namespace osu.Game.Rulesets.Osu.Skinning var localBotLeft = point.Position + ortho - dir; var localBotRight = point.Position + ortho + dir; - quadBatch.Add(new TexturedVertex2D + quadBatch.Add(new TexturedVertex2D(renderer) { Position = localTopLeft, TexturePosition = textureRect.TopLeft, Colour = Color4Extensions.Multiply(ColourAtPosition(localTopLeft), colour), }); - quadBatch.Add(new TexturedVertex2D + quadBatch.Add(new TexturedVertex2D(renderer) { Position = localTopRight, TexturePosition = textureRect.TopRight, Colour = Color4Extensions.Multiply(ColourAtPosition(localTopRight), colour), }); - quadBatch.Add(new TexturedVertex2D + quadBatch.Add(new TexturedVertex2D(renderer) { Position = localBotRight, TexturePosition = textureRect.BottomRight, Colour = Color4Extensions.Multiply(ColourAtPosition(localBotRight), colour), }); - quadBatch.Add(new TexturedVertex2D + quadBatch.Add(new TexturedVertex2D(renderer) { Position = localBotLeft, TexturePosition = textureRect.BottomLeft, diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 5d2f6a14c7..83bab7dc01 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -11,7 +11,9 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Scoring; using osuTK; @@ -120,18 +122,22 @@ namespace osu.Game.Rulesets.Osu.Statistics new OsuSpriteText { Text = "Overshoot", + Font = OsuFont.GetFont(size: 12), Anchor = Anchor.Centre, - Origin = Anchor.BottomCentre, - Padding = new MarginPadding(3), + Origin = Anchor.BottomLeft, + Padding = new MarginPadding(2), + Rotation = -rotation, RelativePositionAxes = Axes.Both, Y = -(inner_portion + line_extension) / 2, }, new OsuSpriteText { Text = "Undershoot", + Font = OsuFont.GetFont(size: 12), Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - Padding = new MarginPadding(3), + Origin = Anchor.TopRight, + Rotation = -rotation, + Padding = new MarginPadding(2), RelativePositionAxes = Axes.Both, Y = (inner_portion + line_extension) / 2, }, @@ -203,8 +209,7 @@ namespace osu.Game.Rulesets.Osu.Statistics if (score.HitEvents.Count == 0) return; - // Todo: This should probably not be done like this. - float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.Difficulty.CircleSize - 5) / 5) / 2; + float radius = OsuHitObject.OBJECT_RADIUS * LegacyRulesetExtensions.CalculateScaleFromCircleSize(playableBeatmap.Difficulty.CircleSize, true); foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle))) { diff --git a/osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.cs index afa54c2dfb..2c6895d7ec 100644 --- a/osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.cs @@ -1,9 +1,8 @@ // 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 osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.UI @@ -13,9 +12,9 @@ namespace osu.Game.Rulesets.Osu.UI ///
public class AnyOrderHitPolicy : IHitPolicy { - public IHitObjectContainer HitObjectContainer { get; set; } + public IHitObjectContainer HitObjectContainer { get; set; } = null!; - public bool IsHittable(DrawableHitObject hitObject, double time) => true; + public ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result) => ClickAction.Hit; public void HandleHit(DrawableHitObject hitObject) { diff --git a/osu.Game.Rulesets.Osu/UI/ClickAction.cs b/osu.Game.Rulesets.Osu/UI/ClickAction.cs new file mode 100644 index 0000000000..2b00f5acce --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ClickAction.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.UI +{ + /// + /// An action that an recommends be taken in response to a click + /// on a . + /// + public enum ClickAction + { + Ignore, + Shake, + Hit + } +} diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs index 076d97d06a..52486b701a 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs @@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { new RingPiece(3) { - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2), + Size = OsuHitObject.OBJECT_DIMENSIONS, Alpha = 0.1f, } }; diff --git a/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs index 0dac3307c2..44d3b37408 100644 --- a/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs @@ -1,10 +1,9 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.UI @@ -21,8 +20,9 @@ namespace osu.Game.Rulesets.Osu.UI ///
/// The to check. /// The time to check. + /// The result that the object would be judged with if hit. /// Whether can be hit at the given . - bool IsHittable(DrawableHitObject hitObject, double time); + ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result); /// /// Handles a being hit. diff --git a/osu.Game.Rulesets.Osu/UI/LegacyHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/LegacyHitPolicy.cs new file mode 100644 index 0000000000..daf498581e --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/LegacyHitPolicy.cs @@ -0,0 +1,72 @@ +// 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.Linq; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Osu.UI +{ + /// + /// Ensures that s are hit in order of appearance. The classic note lock. + /// + /// Hits will be blocked until the previous s have been judged. + /// + /// + public class LegacyHitPolicy : IHitPolicy + { + public IHitObjectContainer? HitObjectContainer { get; set; } + + private readonly double hittableRange; + + public LegacyHitPolicy(double hittableRange = OsuHitWindows.MISS_WINDOW) + { + this.hittableRange = hittableRange; + } + + public void HandleHit(DrawableHitObject hitObject) + { + } + + public virtual ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result) + { + if (HitObjectContainer == null) + throw new InvalidOperationException($"{nameof(HitObjectContainer)} should be set before {nameof(CheckHittable)} is called."); + + var aliveObjects = HitObjectContainer.AliveObjects.ToList(); + int index = aliveObjects.IndexOf(hitObject); + + if (index > 0) + { + var previousHitObject = (DrawableOsuHitObject)aliveObjects[index - 1]; + if (previousHitObject.HitObject.StackHeight > 0 && !previousHitObject.AllJudged) + return ClickAction.Ignore; + } + + if (result == HitResult.None) + return ClickAction.Shake; + + foreach (DrawableHitObject testObject in aliveObjects) + { + if (testObject.AllJudged) + continue; + + // if we found the object being checked, we can move on to the final timing test. + if (testObject == hitObject) + break; + + // for all other objects, we check for validity and block the hit if any are still valid. + // 3ms of extra leniency to account for slightly unsnapped objects. + if (testObject.HitObject.GetEndTime() + 3 < hitObject.HitObject.StartTime) + return ClickAction.Shake; + } + + return Math.Abs(hitObject.HitObject.StartTime - time) < hittableRange ? ClickAction.Hit : ClickAction.Shake; + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs deleted file mode 100644 index 6330208d37..0000000000 --- a/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs +++ /dev/null @@ -1,56 +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 System.Collections.Generic; -using System.Linq; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.UI; - -namespace osu.Game.Rulesets.Osu.UI -{ - /// - /// Ensures that s are hit in order of appearance. The classic note lock. - /// - /// Hits will be blocked until the previous s have been judged. - /// - /// - public class ObjectOrderedHitPolicy : IHitPolicy - { - public IHitObjectContainer HitObjectContainer { get; set; } - - public bool IsHittable(DrawableHitObject hitObject, double time) => enumerateHitObjectsUpTo(hitObject.HitObject.StartTime).All(obj => obj.AllJudged); - - public void HandleHit(DrawableHitObject hitObject) - { - } - - private IEnumerable enumerateHitObjectsUpTo(double targetTime) - { - foreach (var obj in HitObjectContainer.AliveObjects) - { - if (obj.HitObject.StartTime >= targetTime) - yield break; - - switch (obj) - { - case DrawableSpinner: - continue; - - case DrawableSlider slider: - yield return slider.HeadCircle; - - break; - - default: - yield return obj; - - break; - } - } - } - } -} diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index ed02284a4b..15ca0a90de 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.UI protected override void OnNewDrawableHitObject(DrawableHitObject drawable) { - ((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable; + ((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.CheckHittable; Debug.Assert(!drawable.IsLoaded, $"Already loaded {nameof(DrawableHitObject)} is added to {nameof(OsuPlayfield)}"); drawable.OnLoadComplete += onDrawableHitObjectLoaded; diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs index b45d552c7f..b139296c61 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.UI; @@ -62,12 +60,12 @@ namespace osu.Game.Rulesets.Osu.UI // game_size = DrawSizePreservingFillContainer.TargetSize = new Vector2(1024, 768) // // Parent is a 4:3 aspect enforced, using height as the constricting dimension - // Parent.ChildSize.X = min(game_size.X, game_size.Y * (4 / 3)) * playfield_size_adjust - // Parent.ChildSize.X = 819.2 + // Parent!.ChildSize.X = min(game_size.X, game_size.Y * (4 / 3)) * playfield_size_adjust + // Parent!.ChildSize.X = 819.2 // // Scale = 819.2 / 512 // Scale = 1.6 - Scale = new Vector2(Parent.ChildSize.X / OsuPlayfield.BASE_SIZE.X); + Scale = new Vector2(Parent!.ChildSize.X / OsuPlayfield.BASE_SIZE.X); Position = new Vector2(0, (PlayfieldShift ? 8f : 0f) * Scale.X); // Size = 0.625 Size = Vector2.Divide(Vector2.One, Scale); diff --git a/osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs b/osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs index 66a4f467a9..545b31bf29 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Replays; diff --git a/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs index edc3ba0818..2b24fb9398 100644 --- a/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.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.Collections.Generic; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.UI @@ -22,11 +21,14 @@ namespace osu.Game.Rulesets.Osu.UI /// public class StartTimeOrderedHitPolicy : IHitPolicy { - public IHitObjectContainer HitObjectContainer { get; set; } + public IHitObjectContainer? HitObjectContainer { get; set; } - public bool IsHittable(DrawableHitObject hitObject, double time) + public ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult _) { - DrawableHitObject blockingObject = null; + if (HitObjectContainer == null) + throw new InvalidOperationException($"{nameof(HitObjectContainer)} should be set before {nameof(CheckHittable)} is called."); + + DrawableHitObject? blockingObject = null; foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime)) { @@ -36,22 +38,25 @@ namespace osu.Game.Rulesets.Osu.UI // If there is no previous hitobject, allow the hit. if (blockingObject == null) - return true; + return ClickAction.Hit; // A hit is allowed if: // 1. The last blocking hitobject has been judged. // 2. The current time is after the last hitobject's start time. // Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245). - return blockingObject.Judged || time >= blockingObject.HitObject.StartTime; + return (blockingObject.Judged || time >= blockingObject.HitObject.StartTime) ? ClickAction.Hit : ClickAction.Shake; } public void HandleHit(DrawableHitObject hitObject) { + if (HitObjectContainer == null) + throw new InvalidOperationException($"{nameof(HitObjectContainer)} should be set before {nameof(HandleHit)} is called."); + // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners). if (!hitObjectCanBlockFutureHits(hitObject)) return; - if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) + if (CheckHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset, hitObject.Result.Type) != ClickAction.Hit) throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!"); // Miss all hitobjects prior to the hit one. @@ -74,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.UI private IEnumerable enumerateHitObjectsUpTo(double targetTime) { - foreach (var obj in HitObjectContainer.AliveObjects) + foreach (var obj in HitObjectContainer!.AliveObjects) { if (obj.HitObject.StartTime >= targetTime) yield break; diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs index aa4cd0af14..e936c24c08 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Extensions.IEnumerableExtensions; diff --git a/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml index 452b9683ec..cc88d3080a 100644 --- a/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml +++ b/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko.Tests.Android/MainActivity.cs b/osu.Game.Rulesets.Taiko.Tests.Android/MainActivity.cs index a55b461876..e4f4bbfd53 100644 --- a/osu.Game.Rulesets.Taiko.Tests.Android/MainActivity.cs +++ b/osu.Game.Rulesets.Taiko.Tests.Android/MainActivity.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 Android.App; using osu.Framework.Android; using osu.Game.Tests; diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist index 76cb3c0db0..162ee75c22 100644 --- a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist @@ -5,7 +5,7 @@ CFBundleName osu.Game.Rulesets.Taiko.Tests.iOS CFBundleIdentifier - ppy.osu-Game-Rulesets-Taiko-Tests-iOS + sh.ppy.taiko-ruleset-tests CFBundleShortVersionString 1.0 CFBundleVersion @@ -42,4 +42,4 @@ CADisableMinimumFrameDurationOnPhone - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs index 157a96eec8..68517166e7 100644 --- a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs index 747c599721..878f451fd2 100644 --- a/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Input.Events; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditor.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditor.cs index 3ee9171e7e..822219db29 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditor.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditor.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Tests.Visual; diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoEditorSaving.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoEditorSaving.cs index 93b26624de..af7db2251b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoEditorSaving.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoEditorSaving.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Utils; using osu.Game.Rulesets.Taiko.Beatmaps; diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs index ed73730c4a..64a29ce866 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs index eb2d96ec51..f3e37736b2 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs @@ -1,6 +1,7 @@ // 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.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -10,6 +11,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; @@ -36,11 +38,12 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements () => Is.EqualTo(expectedResult)); } - protected void PerformTest(List frames, Beatmap? beatmap = null) + protected void PerformTest(List frames, Beatmap? beatmap = null, Mod[]? mods = null) { AddStep("load player", () => { Beatmap.Value = CreateWorkingBeatmap(beatmap); + SelectedMods.Value = mods ?? Array.Empty(); var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs index 21f2b8f1be..2f9f5e0a37 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs @@ -75,6 +75,25 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements AssertResult(0, HitResult.IgnoreHit); } + [Test] + public void TestHitNoneStrongDrumRoll() + { + PerformTest(new List + { + new TaikoReplayFrame(0), + }, CreateBeatmap(createDrumRoll(true))); + + AssertJudgementCount(12); + + for (int i = 0; i < 5; ++i) + { + AssertResult(i, HitResult.IgnoreMiss); + AssertResult(i, HitResult.IgnoreMiss); + } + + AssertResult(0, HitResult.IgnoreHit); + } + [Test] public void TestHitAllStrongDrumRollWithOneKey() { diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs index 3bf94eb62e..6fe61e78b7 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs @@ -4,10 +4,14 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Replays; +using osu.Game.Rulesets.Taiko.Scoring; namespace osu.Game.Rulesets.Taiko.Tests.Judgements { @@ -32,6 +36,28 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements AssertResult(0, HitResult.Great); } + [Test] + public void TestHitWithBothKeysOnSameFrameDoesNotFallThroughToNextObject() + { + PerformTest(new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(1000, TaikoAction.LeftCentre, TaikoAction.RightCentre), + }, CreateBeatmap(new Hit + { + Type = HitType.Centre, + StartTime = 1000, + }, new Hit + { + Type = HitType.Centre, + StartTime = 1020 + })); + + AssertJudgementCount(2); + AssertResult(0, HitResult.Great); + AssertResult(1, HitResult.Miss); + } + [Test] public void TestHitRimHit() { @@ -157,5 +183,58 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements AssertJudgementCount(1); AssertResult(0, HitResult.Ok); } + + [Test] + public void TestStrongHitOneKeyWithHidden() + { + const double hit_time = 1000; + + var beatmap = CreateBeatmap(new Hit + { + Type = HitType.Centre, + StartTime = hit_time, + IsStrong = true + }); + + var hitWindows = new TaikoHitWindows(); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + + PerformTest(new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) - 1, TaikoAction.LeftCentre), + }, beatmap, new Mod[] { new TaikoModHidden() }); + + AssertJudgementCount(2); + AssertResult(0, HitResult.Ok); + AssertResult(0, HitResult.IgnoreMiss); + } + + [Test] + public void TestStrongHitTwoKeysWithHidden() + { + const double hit_time = 1000; + + var beatmap = CreateBeatmap(new Hit + { + Type = HitType.Centre, + StartTime = hit_time, + IsStrong = true + }); + + var hitWindows = new TaikoHitWindows(); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + + PerformTest(new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) - 1, TaikoAction.LeftCentre), + new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) + DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW - 2, TaikoAction.LeftCentre, TaikoAction.RightCentre), + }, beatmap, new Mod[] { new TaikoModHidden() }); + + AssertJudgementCount(2); + AssertResult(0, HitResult.Ok); + AssertResult(0, HitResult.LargeBonus); + } } } diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs index ccc829f09e..6e42ae7eb5 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs @@ -114,5 +114,117 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements AddAssert("all tick offsets are 0", () => JudgementResults.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0)); } + + [Test] + public void TestAtMostOneSwellTickJudgedPerFrame() + { + const double swell_time = 1000; + + Swell swell = new Swell + { + StartTime = swell_time, + Duration = 1000, + RequiredHits = 10 + }; + + List frames = new List + { + new TaikoReplayFrame(1000), + new TaikoReplayFrame(1250, TaikoAction.LeftCentre, TaikoAction.LeftRim), + new TaikoReplayFrame(1251), + new TaikoReplayFrame(1500, TaikoAction.LeftCentre, TaikoAction.LeftRim, TaikoAction.RightCentre, TaikoAction.RightRim), + new TaikoReplayFrame(1501), + new TaikoReplayFrame(2000), + }; + + PerformTest(frames, CreateBeatmap(swell)); + + AssertJudgementCount(11); + + // this is a charitable interpretation of the inputs. + // + // for the frame at time 1250, we only count either one of the input actions - simple. + // + // for the frame at time 1500, we give the user the benefit of the doubt, + // and we ignore actions that wouldn't otherwise cause a hit due to not alternating, + // but we still count one (just one) of the actions that _would_ normally cause a hit. + // this is done as a courtesy to avoid stuff like key chattering after press blocking legitimate inputs. + for (int i = 0; i < 2; i++) + AssertResult(i, HitResult.IgnoreHit); + for (int i = 2; i < swell.RequiredHits; i++) + AssertResult(i, HitResult.IgnoreMiss); + + AssertResult(0, HitResult.IgnoreMiss); + } + + /// + /// Ensure input is correctly sent to subsequent hits if a swell is fully completed. + /// + [Test] + public void TestHitSwellThenHitHit() + { + const double swell_time = 1000; + const double hit_time = 1150; + + Swell swell = new Swell + { + StartTime = swell_time, + Duration = 100, + RequiredHits = 1 + }; + + Hit hit = new Hit + { + StartTime = hit_time + }; + + List frames = new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(swell_time, TaikoAction.LeftRim), + new TaikoReplayFrame(hit_time, TaikoAction.RightCentre), + }; + + PerformTest(frames, CreateBeatmap(swell, hit)); + + AssertJudgementCount(3); + + AssertResult(0, HitResult.IgnoreHit); + AssertResult(0, HitResult.LargeBonus); + AssertResult(0, HitResult.Great); + } + + [Test] + public void TestMissSwellThenHitHit() + { + const double swell_time = 1000; + const double hit_time = 1150; + + Swell swell = new Swell + { + StartTime = swell_time, + Duration = 100, + RequiredHits = 1 + }; + + Hit hit = new Hit + { + StartTime = hit_time + }; + + List frames = new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(hit_time, TaikoAction.RightCentre), + }; + + PerformTest(frames, CreateBeatmap(swell, hit)); + + AssertJudgementCount(3); + + AssertResult(0, HitResult.IgnoreMiss); + AssertResult(0, HitResult.IgnoreMiss); + AssertResult(0, HitResult.Great); + } } } diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModHidden.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModHidden.cs index edc53429b1..6e6be26e43 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModHidden.cs @@ -1,24 +1,71 @@ // 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.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Taiko.Mods; +using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Tests.Mods { public partial class TestSceneTaikoModHidden : TaikoModTestScene { + private Func checkAllMaxResultJudgements(int count) => () + => Player.ScoreProcessor.JudgedHits >= count + && Player.Results.All(result => result.Type == result.Judgement.MaxResult); + [Test] public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData { Mod = new TaikoModHidden(), Autoplay = true, - PassCondition = checkSomeAutoplayHits + PassCondition = checkAllMaxResultJudgements(4), }); - private bool checkSomeAutoplayHits() - => Player.ScoreProcessor.JudgedHits >= 4 - && Player.Results.All(result => result.Type == result.Judgement.MaxResult); + [Test] + public void TestHitTwoNotesWithinShortPeriod() + { + const double hit_time = 1; + + var beatmap = new Beatmap + { + HitObjects = new List + { + new Hit + { + Type = HitType.Rim, + StartTime = hit_time, + }, + new Hit + { + Type = HitType.Centre, + StartTime = hit_time * 2, + }, + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty + { + SliderTickRate = 4, + OverallDifficulty = 0, + }, + Ruleset = new TaikoRuleset().RulesetInfo + }, + }; + + beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f }); + + CreateModTest(new ModTestData + { + Mod = new TaikoModHidden(), + Autoplay = true, + PassCondition = checkAllMaxResultJudgements(2), + Beatmap = beatmap, + }); + } } } diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TaikoSkinnableTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TaikoSkinnableTestScene.cs index 38530282b7..b11501535f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TaikoSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TaikoSkinnableTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests.Skinning diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs index 7fd90685e3..497c788ce8 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs index 3b2f4f2fb2..bef8be4cb5 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs index 9567eac80f..863ca4a07d 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs index 924f903ce9..f8dd53c834 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs index cb5d0d1f91..3c6319ddf9 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs index 5f98f2f27a..fbdc4132ec 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs index eb2762cb2d..c89e2b727b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs index 826cf2acab..2535058c29 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Testing; using osu.Framework.Timing; diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs index 781a686700..a528a7f9d1 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.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.Collections.Generic; using NUnit.Framework; diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index 5685ac0f60..09d6540f72 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs index c86f8cb8d2..5f7a78ddf1 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs @@ -1,11 +1,10 @@ // 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 NUnit.Framework; using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Tests.Beatmaps; @@ -27,7 +26,8 @@ namespace osu.Game.Rulesets.Taiko.Tests new object[] { LegacyMods.Flashlight, new[] { typeof(TaikoModFlashlight) } }, new object[] { LegacyMods.Autoplay, new[] { typeof(TaikoModAutoplay) } }, new object[] { LegacyMods.Random, new[] { typeof(TaikoModRandom) } }, - new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } } + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } }, + new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } }, }; [TestCaseSource(nameof(taiko_mod_mapping))] diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs index 00292d5473..44bc611d92 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs index b01bd11149..0a178ec69f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs index 287d90b406..6c925f566b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs @@ -6,7 +6,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Timing; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -17,14 +16,13 @@ using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Play; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests { public partial class TestSceneDrumSampleTriggerSource : OsuTestScene { - private readonly ManualClock manualClock = new ManualClock(); - [Cached(typeof(IScrollingInfo))] private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo { @@ -34,23 +32,25 @@ namespace osu.Game.Rulesets.Taiko.Tests private ScrollingHitObjectContainer hitObjectContainer = null!; private TestDrumSampleTriggerSource triggerSource = null!; + private readonly ManualClock manualClock = new TestManualClock(); + private GameplayClockContainer gameplayClock = null!; [SetUp] public void SetUp() => Schedule(() => { - hitObjectContainer = new ScrollingHitObjectContainer(); - manualClock.CurrentTime = 0; - - Child = new Container + gameplayClock = new GameplayClockContainer(manualClock, false, false) { - Clock = new FramedClock(manualClock), RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - hitObjectContainer, + hitObjectContainer = new ScrollingHitObjectContainer(), triggerSource = new TestDrumSampleTriggerSource(hitObjectContainer) } }; + gameplayClock.Reset(0); + + hitObjectContainer.Clock = gameplayClock; + Child = gameplayClock; }); [Test] @@ -72,13 +72,13 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); - AddStep("seek past hit", () => manualClock.CurrentTime = 200); + seekTo(200); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); } [Test] @@ -100,13 +100,68 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); - AddStep("seek past hit", () => manualClock.CurrentTime = 200); + seekTo(200); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + } + + [Test] + public void TestBetweenHits() + { + Hit first = null!, second = null!; + + AddStep("add hit with normal samples", () => + { + first = new Hit + { + StartTime = 100, + Samples = new List + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL) + } + }; + first.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + var drawableHit = new DrawableHit(first); + hitObjectContainer.Add(drawableHit); + }); + AddStep("add hit with soft samples", () => + { + second = new Hit + { + StartTime = 500, + Samples = new List + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT), + new HitSampleInfo(HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT) + } + }; + second.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + var drawableHit = new DrawableHit(second); + hitObjectContainer.Add(drawableHit); + }); + + AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first)); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); + + seekTo(120); + AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first)); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); + + seekTo(480); + AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second)); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + + seekTo(700); + AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second)); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); } [Test] @@ -119,8 +174,8 @@ namespace osu.Game.Rulesets.Taiko.Tests StartTime = 100, Samples = new List { - new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"), - new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM), + new HitSampleInfo(HitSampleInfo.HIT_FINISH, HitSampleInfo.BANK_DRUM) // implies strong } }; hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); @@ -128,14 +183,14 @@ namespace osu.Game.Rulesets.Taiko.Tests hitObjectContainer.Add(drawableHit); }); - AddAssert("most valid object is strong nested hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + AddAssert("most valid object is nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); + checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); - AddStep("seek past hit", () => manualClock.CurrentTime = 200); + seekTo(200); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); } [Test] @@ -158,18 +213,18 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); - AddStep("seek to middle of drum roll", () => manualClock.CurrentTime = 600); + seekTo(600); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); - AddStep("seek past drum roll", () => manualClock.CurrentTime = 1200); + seekTo(1200); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); } [Test] @@ -192,18 +247,18 @@ namespace osu.Game.Rulesets.Taiko.Tests }); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); - AddStep("seek to middle of drum roll", () => manualClock.CurrentTime = 600); + seekTo(600); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); - AddStep("seek past drum roll", () => manualClock.CurrentTime = 1200); + seekTo(1200); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); } [Test] @@ -217,8 +272,8 @@ namespace osu.Game.Rulesets.Taiko.Tests EndTime = 1100, Samples = new List { - new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"), - new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM), + new HitSampleInfo(HitSampleInfo.HIT_FINISH, HitSampleInfo.BANK_DRUM) // implies strong } }; drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); @@ -226,19 +281,19 @@ namespace osu.Game.Rulesets.Taiko.Tests hitObjectContainer.Add(drawableDrumRoll); }); - AddAssert("most valid object is drum roll tick's nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); + checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); - AddStep("seek to middle of drum roll", () => manualClock.CurrentTime = 600); - AddAssert("most valid object is drum roll tick's nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + seekTo(600); + AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); + checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); - AddStep("seek past drum roll", () => manualClock.CurrentTime = 1200); + seekTo(1200); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM); } [Test] @@ -260,19 +315,22 @@ namespace osu.Game.Rulesets.Taiko.Tests hitObjectContainer.Add(drawableSwell); }); - AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); - - AddStep("seek to middle of swell", () => manualClock.CurrentTime = 600); - AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); - - AddStep("seek past swell", () => manualClock.CurrentTime = 1200); + // You might think that this should be a SwellTick since we're before the swell, but SwellTicks get no StartTime (ie. they are zero). + // This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits. + // But for sample playback purposes they can be ignored as noise. AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + + seekTo(600); + AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); + + seekTo(1200); + AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); } [Test] @@ -286,7 +344,7 @@ namespace osu.Game.Rulesets.Taiko.Tests EndTime = 1100, Samples = new List { - new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum") + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM) } }; swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); @@ -294,28 +352,34 @@ namespace osu.Game.Rulesets.Taiko.Tests hitObjectContainer.Add(drawableSwell); }); - AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); - - AddStep("seek to middle of swell", () => manualClock.CurrentTime = 600); - AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); - - AddStep("seek past swell", () => manualClock.CurrentTime = 1200); + // You might think that this should be a SwellTick since we're before the swell, but SwellTicks get no StartTime (ie. they are zero). + // This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits. + // But for sample playback purposes they can be ignored as noise. AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); - checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); - checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); + + seekTo(600); + AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); + + seekTo(1200); + AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf); + checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); + checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); } - private void checkSound(HitType hitType, string expectedName, string expectedBank) + private void checkSamples(HitType hitType, bool strong, string expectedSamplesCsv, string expectedBank) { - AddStep($"hit {hitType}", () => triggerSource.Play(hitType)); - AddAssert($"last played sample is {expectedName}", () => triggerSource.LastPlayedSamples!.OfType().Single().Name, () => Is.EqualTo(expectedName)); - AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType().Single().Bank, () => Is.EqualTo(expectedBank)); + AddStep($"hit {hitType}", () => triggerSource.Play(hitType, strong)); + AddAssert($"last played sample is {expectedSamplesCsv}", () => string.Join(',', triggerSource.LastPlayedSamples!.OfType().Select(s => s.Name)), + () => Is.EqualTo(expectedSamplesCsv)); + AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType().First().Bank, () => Is.EqualTo(expectedBank)); } + private void seekTo(double time) => AddStep($"seek to {time}", () => gameplayClock.Seek(time)); + private partial class TestDrumSampleTriggerSource : DrumSampleTriggerSource { public ISampleInfo[]? LastPlayedSamples { get; private set; } @@ -331,7 +395,33 @@ namespace osu.Game.Rulesets.Taiko.Tests LastPlayedSamples = samples; } - public new HitObject GetMostValidObject() => base.GetMostValidObject(); + public new HitObject? GetMostValidObject() => base.GetMostValidObject(); + } + + private class TestManualClock : ManualClock, IAdjustableClock + { + public TestManualClock() + { + IsRunning = true; + } + + public void Start() => IsRunning = true; + + public void Stop() => IsRunning = false; + + public bool Seek(double position) + { + CurrentTime = position; + return true; + } + + public void Reset() + { + } + + public void ResetSpeedAdjustments() + { + } } } } diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs index e0ff617b59..0e10f75378 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Tests private void addFlyingHit(HitType hitType) { - var tick = new DrumRollTick { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current }; + var tick = new DrumRollTick(new DrumRoll()) { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current }; DrawableDrumRollTick h; DrawableRuleset.Playfield.Add(h = new DrawableDrumRollTick(tick) { JudgementType = hitType }); diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs index 301620edc9..24aa4f2ef0 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index 91209e5ec5..fd850a9a67 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.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.Collections.Generic; using System.Linq; diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs index dcdda6014c..a548a14d88 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs @@ -1,19 +1,18 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; +using NUnit.Framework; using osu.Framework.Testing; -using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.UI; namespace osu.Game.Rulesets.Taiko.Tests { /// - /// Taiko has some interesting rules for legacy mappings. + /// Taiko doesn't output any samples. They are all handled externally by . /// [HeadlessTest] public partial class TestSceneSampleOutput : TestSceneTaikoPlayer @@ -28,10 +27,10 @@ namespace osu.Game.Rulesets.Taiko.Tests string.Empty, string.Empty, string.Empty, - HitSampleInfo.HIT_FINISH, - HitSampleInfo.HIT_WHISTLE, - HitSampleInfo.HIT_WHISTLE, - HitSampleInfo.HIT_WHISTLE, + string.Empty, + string.Empty, + string.Empty, + string.Empty, }; var actualSampleNames = new List(); @@ -48,7 +47,7 @@ namespace osu.Game.Rulesets.Taiko.Tests AddUntilStep("all samples collected", () => actualSampleNames.Count == expectedSampleNames.Length); - AddAssert("samples are correct", () => actualSampleNames.SequenceEqual(expectedSampleNames)); + AddAssert("samples are correct", () => actualSampleNames, () => Is.EqualTo(expectedSampleNames)); } protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TaikoBeatmapConversionTest().GetBeatmap("sample-to-type-conversions"); diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneScoring.cs new file mode 100644 index 0000000000..e065070822 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneScoring.cs @@ -0,0 +1,185 @@ +// 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 NUnit.Framework; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Rulesets.Taiko.Judgements; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Scoring; +using osu.Game.Tests.Visual.Gameplay; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [TestFixture] + public partial class TestSceneScoring : ScoringTestScene + { + private Bindable scoreMultiplier { get; } = new BindableDouble + { + Default = 4, + Value = 4 + }; + + protected override IBeatmap CreateBeatmap(int maxCombo) + { + var beatmap = new TaikoBeatmap(); + for (int i = 0; i < maxCombo; ++i) + beatmap.HitObjects.Add(new Hit()); + return beatmap; + } + + protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1 { ScoreMultiplier = { BindTarget = scoreMultiplier } }; + protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo); + + protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new TaikoProcessorBasedScoringAlgorithm(beatmap, mode); + + [Test] + public void TestBasicScenarios() + { + AddStep("set up score multiplier", () => + { + scoreMultiplier.BindValueChanged(_ => Rerun()); + }); + AddStep("set max combo to 100", () => MaxCombo.Value = 100); + AddStep("set perfect score", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + }); + AddStep("set score with misses", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + MissLocations.AddRange(new[] { 24d, 49 }); + }); + AddStep("set score with misses and OKs", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + + NonPerfectLocations.AddRange(new[] { 9d, 19, 29, 39, 59, 69, 79, 89, 99 }); + MissLocations.AddRange(new[] { 24d, 49 }); + }); + AddSliderStep("adjust score multiplier", 0, 10, (int)scoreMultiplier.Default, multiplier => scoreMultiplier.Value = multiplier); + } + + private const int base_great = 300; + private const int base_ok = 150; + + private class ScoreV1 : IScoringAlgorithm + { + private int currentCombo; + + public BindableDouble ScoreMultiplier { get; } = new BindableDouble(); + + public void ApplyHit() => applyHitV1(base_great); + + public void ApplyNonPerfect() => applyHitV1(base_ok); + + public void ApplyMiss() => applyHitV1(0); + + private void applyHitV1(int baseScore) + { + if (baseScore == 0) + { + currentCombo = 0; + return; + } + + TotalScore += baseScore; + + // combo multiplier + // ReSharper disable once PossibleLossOfFraction + TotalScore += (int)((baseScore / 35) * 2 * (ScoreMultiplier.Value + 1)) * (Math.Min(100, currentCombo) / 10); + + currentCombo++; + } + + public long TotalScore { get; private set; } + } + + private class ScoreV2 : IScoringAlgorithm + { + private int currentCombo; + private double comboPortion; + private double currentBaseScore; + private double maxBaseScore; + private int currentHits; + + private readonly double comboPortionMax; + private readonly int maxCombo; + + private const double combo_base = 4; + + public ScoreV2(int maxCombo) + { + this.maxCombo = maxCombo; + + for (int i = 0; i < this.maxCombo; i++) + ApplyHit(); + + comboPortionMax = comboPortion; + + currentCombo = 0; + comboPortion = 0; + currentBaseScore = 0; + maxBaseScore = 0; + currentHits = 0; + } + + public void ApplyHit() => applyHitV2(base_great); + + public void ApplyNonPerfect() => applyHitV2(base_ok); + + private void applyHitV2(int baseScore) + { + maxBaseScore += base_great; + currentBaseScore += baseScore; + + currentHits++; + + // `base_great` is INTENTIONALLY used above here instead of `baseScore` + // see `BaseHitValue` override in `ScoreChangeTaiko` on stable + comboPortion += base_great * Math.Min(Math.Max(0.5, Math.Log(++currentCombo, combo_base)), Math.Log(400, combo_base)); + } + + public void ApplyMiss() + { + currentHits++; + maxBaseScore += base_great; + currentCombo = 0; + } + + public long TotalScore + { + get + { + double accuracy = currentBaseScore / maxBaseScore; + + return (int)Math.Round + ( + 250000 * comboPortion / comboPortionMax + + 750000 * Math.Pow(accuracy, 3.6) * ((double)currentHits / maxCombo) + ); + } + } + } + + private class TaikoProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm + { + public TaikoProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode) + : base(beatmap, mode) + { + } + + protected override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(); + protected override JudgementResult CreatePerfectJudgementResult() => new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }; + protected override JudgementResult CreateNonPerfectJudgementResult() => new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok }; + protected override JudgementResult CreateMissJudgementResult() => new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }; + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayer.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayer.cs index 8c903f748c..d2cf691c7f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayer.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs index 08c06b08f2..9e45197b04 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Game.Beatmaps; diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index 0c39ad988b..48465bb119 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -1,9 +1,9 @@  - + - + WinExe diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs index 6ff5cdf7e5..41fe63a553 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.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.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 1c2e7abafe..e46e2ec09c 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.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 osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -16,6 +14,7 @@ using JetBrains.Annotations; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Formats; +using osu.Game.Rulesets.Objects.Legacy; namespace osu.Game.Rulesets.Taiko.Beatmaps { @@ -66,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { if (hitObject is not IHasSliderVelocity hasSliderVelocity) continue; - double nextScrollSpeed = hasSliderVelocity.SliderVelocity; + double nextScrollSpeed = hasSliderVelocity.SliderVelocityMultiplier; EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime); if (!Precision.AlmostEquals(lastScrollSpeed, nextScrollSpeed, acceptableDifference: currentEffectPoint.ScrollSpeedBindable.Precision)) @@ -141,7 +140,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps StartTime = obj.StartTime, Samples = obj.Samples, Duration = taikoDuration, - SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1 }; } @@ -189,10 +187,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime); double beatLength; - if (obj.LegacyBpmMultiplier.HasValue) - beatLength = timingPoint.BeatLength * obj.LegacyBpmMultiplier.Value; - else if (obj is IHasSliderVelocity hasSliderVelocity) - beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity; + + if (obj is IHasSliderVelocity hasSliderVelocity) + beatLength = LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(hasSliderVelocity, timingPoint, TaikoRuleset.SHORT_NAME); else beatLength = timingPoint.BeatLength; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index ff187a133a..e76af13686 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.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 osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index d04c028fec..e528c70699 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.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 osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index 72452e27b3..1664c941f8 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -48,7 +48,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty foreach (var v in base.ToDatabaseAttributes()) yield return v; - yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); yield return (ATTRIB_ID_DIFFICULTY, StarRating); yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); } @@ -57,7 +56,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { base.FromDatabaseAttributes(values, onlineInfo); - MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; StarRating = values[ATTRIB_ID_DIFFICULTY]; GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 24b5f5939a..ab193caaa3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; @@ -86,7 +84,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty HitWindows hitWindows = new TaikoHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); - return new TaikoDifficultyAttributes + TaikoDifficultyAttributes attributes = new TaikoDifficultyAttributes { StarRating = starRating, Mods = mods, @@ -97,6 +95,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), }; + + return attributes; } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs new file mode 100644 index 0000000000..6a3eb68a22 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs @@ -0,0 +1,243 @@ +// 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.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; +using osu.Game.Rulesets.Taiko.Mods; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Difficulty +{ + internal class TaikoLegacyScoreSimulator : ILegacyScoreSimulator + { + private int legacyBonusScore; + private int standardisedBonusScore; + private int combo; + + private int difficultyPeppyStars; + private IBeatmap playableBeatmap = null!; + + public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap) + { + this.playableBeatmap = playableBeatmap; + + IBeatmap baseBeatmap = workingBeatmap.Beatmap; + + int countNormal = 0; + int countSlider = 0; + int countSpinner = 0; + + foreach (HitObject obj in baseBeatmap.HitObjects) + { + switch (obj) + { + case IHasPath: + countSlider++; + break; + + case IHasDuration: + countSpinner++; + break; + + default: + countNormal++; + break; + } + } + + int objectCount = countNormal + countSlider + countSpinner; + + int drainLength = 0; + + if (baseBeatmap.HitObjects.Count > 0) + { + int breakLength = baseBeatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum(); + drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000; + } + + difficultyPeppyStars = (int)Math.Round( + (baseBeatmap.Difficulty.DrainRate + + baseBeatmap.Difficulty.OverallDifficulty + + baseBeatmap.Difficulty.CircleSize + + Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5); + + LegacyScoreAttributes attributes = new LegacyScoreAttributes(); + + foreach (var obj in playableBeatmap.HitObjects) + simulateHit(obj, ref attributes); + + attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore; + + return attributes; + } + + private void simulateHit(HitObject hitObject, ref LegacyScoreAttributes attributes) + { + bool increaseCombo = true; + bool addScoreComboMultiplier = false; + + bool isBonus = false; + HitResult bonusResult = HitResult.None; + + int scoreIncrease = 0; + + switch (hitObject) + { + case SwellTick: + scoreIncrease = 300; + increaseCombo = false; + break; + + case DrumRollTick: + scoreIncrease = 300; + increaseCombo = false; + isBonus = true; + bonusResult = HitResult.SmallBonus; + break; + + case Swell swell: + // The taiko swell generally does not match the osu-stable implementation in any way. + // We'll redo the calculations to match osu-stable here... + + // Normally, this value depends on the final overall difficulty. For simplicity, we'll only consider the worst case that maximises rotations. + const double minimum_rotations_per_second = 7.5; + + // The amount of half spins that are required to successfully complete the spinner (i.e. get a 300). + int halfSpinsRequiredForCompletion = (int)(swell.Duration / 1000 * minimum_rotations_per_second); + halfSpinsRequiredForCompletion = (int)Math.Max(1, halfSpinsRequiredForCompletion * 1.65f); + + // + // Normally, this multiplier depends on the active mods (DT = 0.75, HT = 1.5). For simplicity, we'll only consider the worst case that maximises rotations. + // This way, scores remain beatable at the cost of the conversion being slightly inaccurate. + // - A perfect DT/NM score will have less than 1M total score (excluding bonus). + // - A perfect HT score will have 1M total score (excluding bonus). + // + halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 1.5f)); + + for (int i = 0; i <= halfSpinsRequiredForCompletion; i++) + simulateHit(new SwellTick(), ref attributes); + + scoreIncrease = 300; + addScoreComboMultiplier = true; + increaseCombo = false; + isBonus = true; + bonusResult = HitResult.LargeBonus; + break; + + case Hit: + scoreIncrease = 300; + addScoreComboMultiplier = true; + break; + + case DrumRoll: + foreach (var nested in hitObject.NestedHitObjects) + simulateHit(nested, ref attributes); + return; + } + + if (hitObject is DrumRollTick tick) + { + if (playableBeatmap.ControlPointInfo.EffectPointAt(tick.Parent.StartTime).KiaiMode) + scoreIncrease = (int)(scoreIncrease * 1.2f); + + if (tick.IsStrong) + scoreIncrease += scoreIncrease / 5; + } + + // The score increase directly contributed to by the combo-multiplied portion. + int comboScoreIncrease = 0; + + if (addScoreComboMultiplier) + { + int oldScoreIncrease = scoreIncrease; + + scoreIncrease += scoreIncrease / 35 * 2 * (difficultyPeppyStars + 1) * (Math.Min(100, combo) / 10); + + if (hitObject is Swell) + { + if (playableBeatmap.ControlPointInfo.EffectPointAt(hitObject.GetEndTime()).KiaiMode) + scoreIncrease = (int)(scoreIncrease * 1.2f); + } + else + { + if (playableBeatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode) + scoreIncrease = (int)(scoreIncrease * 1.2f); + } + + comboScoreIncrease = scoreIncrease - oldScoreIncrease; + } + + if (hitObject is Swell || (hitObject is TaikoStrongableHitObject strongable && strongable.IsStrong)) + { + scoreIncrease *= 2; + comboScoreIncrease *= 2; + } + + scoreIncrease -= comboScoreIncrease; + + if (addScoreComboMultiplier) + attributes.ComboScore += comboScoreIncrease; + + if (isBonus) + { + legacyBonusScore += scoreIncrease; + standardisedBonusScore += Judgement.ToNumericResult(bonusResult); + } + else + attributes.AccuracyScore += scoreIncrease; + + if (increaseCombo) + combo++; + } + + public double GetLegacyScoreMultiplier(IReadOnlyList mods, LegacyBeatmapConversionDifficultyInfo difficulty) + { + bool scoreV2 = mods.Any(m => m is ModScoreV2); + + double multiplier = 1.0; + + foreach (var mod in mods) + { + switch (mod) + { + case TaikoModNoFail: + multiplier *= scoreV2 ? 1.0 : 0.5; + break; + + case TaikoModEasy: + multiplier *= 0.5; + break; + + case TaikoModHalfTime: + case TaikoModDaycore: + multiplier *= 0.3; + break; + + case TaikoModHidden: + case TaikoModHardRock: + multiplier *= 1.06; + break; + + case TaikoModDoubleTime: + case TaikoModNightcore: + case TaikoModFlashlight: + multiplier *= 1.12; + break; + + case TaikoModRelax: + return 0; + } + } + + return multiplier; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs index b61c13a2df..b12c0ca29d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.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.Collections.Generic; using Newtonsoft.Json; using osu.Game.Rulesets.Difficulty; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 2d1b2903c9..ac4462c18b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.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.Collections.Generic; using System.Linq; @@ -44,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss; // TODO: The detection of rulesets is temporary until the leftover old skills have been reworked. - bool isConvert = score.BeatmapInfo.Ruleset.OnlineID != 1; + bool isConvert = score.BeatmapInfo!.Ruleset.OnlineID != 1; double multiplier = 1.13; diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/DrumRollPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/DrumRollPlacementBlueprint.cs index 4b4e2b5847..2f3c722fda 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/DrumRollPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/DrumRollPlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Edit.Blueprints diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs index 84bc547372..3401f944e4 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs index af02522a05..a56f92a026 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/SwellPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/SwellPlacementBlueprint.cs index 2080293428..9b5ef640d7 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/SwellPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/SwellPlacementBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Edit.Blueprints diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs index 34695cbdd6..2a824d1f68 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -32,8 +30,8 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints var topLeft = new Vector2(float.MaxValue, float.MaxValue); var bottomRight = new Vector2(float.MinValue, float.MinValue); - topLeft = Vector2.ComponentMin(topLeft, Parent.ToLocalSpace(DrawableObject.ScreenSpaceDrawQuad.TopLeft)); - bottomRight = Vector2.ComponentMax(bottomRight, Parent.ToLocalSpace(DrawableObject.ScreenSpaceDrawQuad.BottomRight)); + topLeft = Vector2.ComponentMin(topLeft, Parent!.ToLocalSpace(DrawableObject.ScreenSpaceDrawQuad.TopLeft)); + bottomRight = Vector2.ComponentMax(bottomRight, Parent!.ToLocalSpace(DrawableObject.ScreenSpaceDrawQuad.BottomRight)); Size = bottomRight - topLeft; Position = topLeft; diff --git a/osu.Game.Rulesets.Taiko/Edit/DrawableTaikoEditorRuleset.cs b/osu.Game.Rulesets.Taiko/Edit/DrawableTaikoEditorRuleset.cs new file mode 100644 index 0000000000..3c7a97c864 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/DrawableTaikoEditorRuleset.cs @@ -0,0 +1,37 @@ +// 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 osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Rulesets.UI.Scrolling; + +namespace osu.Game.Rulesets.Taiko.Edit +{ + public partial class DrawableTaikoEditorRuleset : DrawableTaikoRuleset, ISupportConstantAlgorithmToggle + { + public BindableBool ShowSpeedChanges { get; } = new BindableBool(); + + public DrawableTaikoEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) + : base(ruleset, beatmap, mods) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Overlapping : ScrollVisualisationMethod.Constant, true); + } + + protected override double ComputeTimeRange() + { + // Adjust when we're using constant algorithm to not be sluggish. + double multiplier = ShowSpeedChanges.Value ? 1 : 4; + return base.ComputeTimeRange() / multiplier; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs index acb17fc455..f332441875 100644 --- a/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs +++ b/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs index e52dae4b0c..fa50841893 100644 --- a/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs +++ b/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs index dd0ff61c10..4d4ee8effe 100644 --- a/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs +++ b/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoBeatSnapGrid.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoBeatSnapGrid.cs new file mode 100644 index 0000000000..1b6794974f --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoBeatSnapGrid.cs @@ -0,0 +1,19 @@ +// 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 osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Screens.Edit.Compose.Components; + +namespace osu.Game.Rulesets.Taiko.Edit +{ + public partial class TaikoBeatSnapGrid : BeatSnapGrid + { + protected override IEnumerable GetTargetContainers(HitObjectComposer composer) => new[] + { + ((TaikoPlayfield)composer.Playfield).UnderlayElements + }; + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs index 6be22f3af0..027723c02c 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Edit.Blueprints; diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs index cff5731181..6020f6e04c 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs @@ -2,15 +2,20 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Compose.Components; namespace osu.Game.Rulesets.Taiko.Edit { - public partial class TaikoHitObjectComposer : HitObjectComposer + public partial class TaikoHitObjectComposer : ScrollingHitObjectComposer { + protected override bool ApplyHorizontalCentering => false; + public TaikoHitObjectComposer(TaikoRuleset ruleset) : base(ruleset) { @@ -23,7 +28,12 @@ namespace osu.Game.Rulesets.Taiko.Edit new SwellCompositionTool() }; + protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) => + new DrawableTaikoEditorRuleset(ruleset, beatmap, mods); + protected override ComposeBlueprintContainer CreateBlueprintContainer() => new TaikoBlueprintContainer(this); + + protected override BeatSnapGrid CreateBeatSnapGrid() => new TaikoBeatSnapGrid(); } } diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index b727c0a61b..7ab8a54b02 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; using osu.Framework.Allocation; diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs index de56c76f56..4fc455fb23 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.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 osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.Judgements diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs index f8e3303752..e272c1a4ef 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs index bafe7dfbaf..1fc50c8751 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.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 osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.Judgements diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs index 146621997d..d22ac6bf5e 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.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 osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.Judgements diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index 4708ef9bf0..2c3b4a8d18 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -62,6 +62,8 @@ namespace osu.Game.Rulesets.Taiko.Mods hitObject.LifetimeEnd = state == ArmedState.Idle || !hitObject.AllJudged ? hitObject.HitObject.GetEndTime() + hitObject.HitObject.HitWindows.WindowFor(HitResult.Miss) : hitObject.HitStateUpdateTime; + // extend the lifetime end of the object in order to allow its nested strong hit (if any) to be judged. + hitObject.LifetimeEnd += DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW; } break; diff --git a/osu.Game.Rulesets.Taiko/Objects/BarLine.cs b/osu.Game.Rulesets.Taiko/Objects/BarLine.cs index d2eba0eb54..46b3f13501 100644 --- a/osu.Game.Rulesets.Taiko/Objects/BarLine.cs +++ b/osu.Game.Rulesets.Taiko/Objects/BarLine.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 osu.Framework.Bindables; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 005d2ab1ac..2bf0c04adf 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -195,14 +195,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); } - public override void OnKilled() - { - base.OnKilled(); - - if (Time.Current > ParentHitObject.HitObject.GetEndTime() && !Judged) - ApplyResult(r => r.Type = r.Judgement.MinResult); - } - public override bool OnPressed(KeyBindingPressEvent e) => false; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index abecd19545..c900165d34 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -108,14 +108,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); } - public override void OnKilled() - { - base.OnKilled(); - - if (Time.Current > ParentHitObject.HitObject.GetEndTime() && !Judged) - ApplyResult(r => r.Type = r.Judgement.MinResult); - } - public override bool OnPressed(KeyBindingPressEvent e) => false; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs index 0cd265ecab..a039ce3407 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 62c8457c58..1ef426854e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -4,14 +4,12 @@ #nullable disable using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Events; -using osu.Game.Audio; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Skinning.Default; @@ -37,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private bool validActionPressed; - private bool pressHandledThisFrame; + private double? lastPressHandleTime; private readonly Bindable type = new Bindable(); @@ -78,7 +76,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables HitActions = null; HitAction = null; - validActionPressed = pressHandledThisFrame = false; + validActionPressed = false; + lastPressHandleTime = null; } private void updateActionsFromType() @@ -93,40 +92,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ? new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit) : new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit); - public override IEnumerable GetSamples() - { - // normal and claps are always handled by the drum (see DrumSampleMapping). - // in addition, whistles are excluded as they are an alternative rim marker. - - var samples = HitObject.Samples.Where(s => - s.Name != HitSampleInfo.HIT_NORMAL - && s.Name != HitSampleInfo.HIT_CLAP - && s.Name != HitSampleInfo.HIT_WHISTLE); - - if (HitObject.Type == HitType.Rim && HitObject.IsStrong) - { - // strong + rim always maps to whistle. - // TODO: this should really be in the legacy decoder, but can't be because legacy encoding parity would be broken. - // when we add a taiko editor, this is probably not going to play nice. - - var corrected = samples.ToList(); - - for (int i = 0; i < corrected.Count; i++) - { - var s = corrected[i]; - - if (s.Name != HitSampleInfo.HIT_FINISH) - continue; - - corrected[i] = s.With(HitSampleInfo.HIT_WHISTLE); - } - - return corrected; - } - - return samples; - } - protected override void CheckForResult(bool userTriggered, double timeOffset) { Debug.Assert(HitObject.HitWindows != null); @@ -150,7 +115,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool OnPressed(KeyBindingPressEvent e) { - if (pressHandledThisFrame) + if (lastPressHandleTime == Time.Current) return true; if (Judged) return false; @@ -164,7 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables // Regardless of whether we've hit or not, any secondary key presses in the same frame should be discarded // E.g. hitting a non-strong centre as a strong should not fall through and perform a hit on the next note - pressHandledThisFrame = true; + lastPressHandleTime = Time.Current; return result; } @@ -175,15 +140,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables base.OnReleased(e); } - protected override void Update() - { - base.Update(); - - // The input manager processes all input prior to us updating, so this is the perfect time - // for us to remove the extra press blocking, before input is handled in the next frame - pressHandledThisFrame = false; - } - protected override void UpdateHitStateTransforms(ArmedState state) { Debug.Assert(HitObject.HitWindows != null); @@ -231,7 +187,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// The lenience for the second key press. /// This does not adjust by map difficulty in ScoreV2 yet. /// - private const double second_hit_window = 30; + public const double SECOND_HIT_WINDOW = 30; public StrongNestedHit() : this(null) @@ -259,12 +215,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!userTriggered) { - if (timeOffset - ParentHitObject.Result.TimeOffset > second_hit_window) + if (timeOffset - ParentHitObject.Result.TimeOffset > SECOND_HIT_WINDOW) ApplyResult(r => r.Type = r.Judgement.MinResult); return; } - if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= second_hit_window) + if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= SECOND_HIT_WINDOW) ApplyResult(r => r.Type = r.Judgement.MaxResult); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs index 4ea30453d1..724d59edcd 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs @@ -1,9 +1,7 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -13,11 +11,23 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ///
public abstract partial class DrawableStrongNestedHit : DrawableTaikoHitObject { - public new DrawableTaikoHitObject ParentHitObject => (DrawableTaikoHitObject)base.ParentHitObject; + public new DrawableTaikoHitObject? ParentHitObject => base.ParentHitObject as DrawableTaikoHitObject; - protected DrawableStrongNestedHit([CanBeNull] StrongNestedHitObject nestedHit) + protected DrawableStrongNestedHit(StrongNestedHitObject? nestedHit) : base(nestedHit) { } + + public override void OnKilled() + { + base.OnKilled(); + + // usually, the strong nested hit isn't judged itself, it is judged by its parent object. + // however, in rare cases (see: drum rolls, hits with hidden active), + // it can happen that the hit window of the nested strong hit extends past the lifetime of the parent object. + // this is a safety to prevent such cases from causing the nested hit to never be judged and as such prevent gameplay from completing. + if (!Judged && Time.Current > ParentHitObject?.HitObject.GetEndTime()) + ApplyResult(r => r.Type = r.Judgement.MinResult); + } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 8441e3a749..e4a083f218 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Skinning.Default; +using osu.Game.Screens.Play; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -38,6 +39,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; + private double? lastPressHandleTime; + public override bool DisplayResult => false; public DrawableSwell() @@ -140,6 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables UnproxyContent(); lastWasCentre = null; + lastPressHandleTime = null; } protected override void AddNestedHitObject(DrawableHitObject hitObject) @@ -257,7 +261,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); @@ -266,6 +270,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ProxyContent(); else UnproxyContent(); + + if ((Clock as IGameplayClock)?.IsRewinding == true) + lastPressHandleTime = null; } private bool? lastWasCentre; @@ -276,13 +283,23 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (Time.Current < HitObject.StartTime) return false; + if (AllJudged) + return false; + bool isCentre = e.Action == TaikoAction.LeftCentre || e.Action == TaikoAction.RightCentre; // Ensure alternating centre and rim hits if (lastWasCentre == isCentre) return false; + // If we've already successfully judged a tick this frame, do not judge more. + // Note that the ordering is important here - this is intentionally placed after the alternating check. + // That is done to prevent accidental double inputs blocking simultaneous but legitimate hits from registering. + if (lastPressHandleTime == Time.Current) + return true; + lastWasCentre = isCentre; + lastPressHandleTime = Time.Current; UpdateResult(true); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 1b5d641612..3f4694d71d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -119,8 +119,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool RemoveWhenNotAlive => false; } - // Most osu!taiko hitsounds are managed by the drum (see DrumSampleTriggerSource). - public override IEnumerable GetSamples() => Enumerable.Empty(); + // osu!taiko hitsounds are managed by the drum (see DrumSampleTriggerSource). + public sealed override IEnumerable GetSamples() => Enumerable.Empty(); } public abstract partial class DrawableTaikoHitObject : DrawableTaikoHitObject diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 79d17b4a1f..5f47d486e6 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -3,7 +3,6 @@ using osu.Game.Rulesets.Objects.Types; using System.Threading; -using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Formats; @@ -14,7 +13,7 @@ using osuTK; namespace osu.Game.Rulesets.Taiko.Objects { - public class DrumRoll : TaikoStrongableHitObject, IHasPath, IHasSliderVelocity + public class DrumRoll : TaikoStrongableHitObject, IHasPath { /// /// Drum roll distance that results in a duration of 1 speed-adjusted beat length. @@ -34,19 +33,6 @@ namespace osu.Game.Rulesets.Taiko.Objects /// public double Velocity { get; private set; } - public BindableNumber SliderVelocityBindable { get; } = new BindableDouble(1) - { - Precision = 0.01, - MinValue = 0.1, - MaxValue = 10 - }; - - public double SliderVelocity - { - get => SliderVelocityBindable.Value; - set => SliderVelocityBindable.Value = value; - } - /// /// Numer of ticks per beat length. /// @@ -63,8 +49,9 @@ namespace osu.Game.Rulesets.Taiko.Objects base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); + EffectControlPoint effectPoint = controlPointInfo.EffectPointAt(StartTime); - double scoringDistance = base_distance * difficulty.SliderMultiplier * SliderVelocity; + double scoringDistance = base_distance * difficulty.SliderMultiplier * effectPoint.ScrollSpeed; Velocity = scoringDistance / timingPoint.BeatLength; TickRate = difficulty.SliderTickRate == 3 ? 3 : 4; @@ -90,7 +77,7 @@ namespace osu.Game.Rulesets.Taiko.Objects { cancellationToken.ThrowIfCancellationRequested(); - AddNested(new DrumRollTick + AddNested(new DrumRollTick(this) { FirstTick = first, TickSpacing = tickSpacing, diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs index 206e8ecb5a..dc082ffd21 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Judgements; @@ -11,6 +9,8 @@ namespace osu.Game.Rulesets.Taiko.Objects { public class DrumRollTick : TaikoStrongableHitObject { + public readonly DrumRoll Parent; + /// /// Whether this is the first (initial) tick of the slider. /// @@ -27,6 +27,11 @@ namespace osu.Game.Rulesets.Taiko.Objects ///
public double HitWindow => TickSpacing / 2; + public DrumRollTick(DrumRoll parent) + { + Parent = parent; + } + public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; diff --git a/osu.Game.Rulesets.Taiko/Objects/Hit.cs b/osu.Game.Rulesets.Taiko/Objects/Hit.cs index ec23079ed9..156e890607 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Hit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Hit.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 osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; diff --git a/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs b/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs index 18f47b7cff..302f940ef4 100644 --- a/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.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 osu.Game.Rulesets.Judgements; namespace osu.Game.Rulesets.Taiko.Objects diff --git a/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs index 316115f44d..14cbe338ed 100644 --- a/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Judgements; diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs index 9ad783ba7e..a8db8df021 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Swell.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.Threading; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Judgements; diff --git a/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs index 43830cb528..41fb9cac7e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs index 3aba5c571b..1a1fde1990 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs index 479ad8369a..228179f94b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Threading; using osu.Framework.Bindables; diff --git a/osu.Game.Rulesets.Taiko/Properties/AssemblyInfo.cs b/osu.Game.Rulesets.Taiko/Properties/AssemblyInfo.cs index 5b66e18a6d..ca7d04876e 100644 --- a/osu.Game.Rulesets.Taiko/Properties/AssemblyInfo.cs +++ b/osu.Game.Rulesets.Taiko/Properties/AssemblyInfo.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.Runtime.CompilerServices; // We publish our internal attributes to other sub-projects of the framework. diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index 7c70beb0a4..d75906f3aa 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Game.Beatmaps; diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs index 896af24772..cf806c0c97 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.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 osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.Scoring diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonBarLine.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonBarLine.cs index 32afb8e6ac..6cb55f1111 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonBarLine.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonBarLine.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Objects.Drawables; @@ -14,53 +15,54 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { public partial class ArgonBarLine : CompositeDrawable { - private Container majorEdgeContainer = null!; - private Bindable major = null!; + private Box mainLine = null!; + private Drawable topAnchor = null!; + private Drawable bottomAnchor = null!; + [BackgroundDependencyLoader] private void load(DrawableHitObject drawableHitObject) { RelativeSizeAxes = Axes.Both; - const float line_offset = 8; - var majorPieceSize = new Vector2(6, 20); + // Avoid flickering due to no anti-aliasing of boxes by default. + var edgeSmoothness = new Vector2(0.3f); - InternalChildren = new Drawable[] + AddInternal(mainLine = new Box { - line = new Box - { - RelativeSizeAxes = Axes.Both, - EdgeSmoothness = new Vector2(0.5f, 0), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - majorEdgeContainer = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Children = new[] - { - new Circle - { - Name = "Top line", - Anchor = Anchor.TopCentre, - Origin = Anchor.BottomCentre, - Size = majorPieceSize, - Y = -line_offset, - }, - new Circle - { - Name = "Bottom line", - Anchor = Anchor.BottomCentre, - Origin = Anchor.TopCentre, - Size = majorPieceSize, - Y = line_offset, - }, - } - } - }; + Name = "Bar line", + EdgeSmoothness = edgeSmoothness, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }); + + const float major_extension = 10; + + AddInternal(topAnchor = new Box + { + Name = "Top anchor", + EdgeSmoothness = edgeSmoothness, + Blending = BlendingParameters.Additive, + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + Height = major_extension, + RelativeSizeAxes = Axes.X, + Colour = ColourInfo.GradientVertical(Colour4.Transparent, Colour4.White), + }); + + AddInternal(bottomAnchor = new Box + { + Name = "Bottom anchor", + EdgeSmoothness = edgeSmoothness, + Blending = BlendingParameters.Additive, + Anchor = Anchor.BottomCentre, + Origin = Anchor.TopCentre, + Height = major_extension, + RelativeSizeAxes = Axes.X, + Colour = ColourInfo.GradientVertical(Colour4.White, Colour4.Transparent), + }); major = ((DrawableBarLine)drawableHitObject).Major.GetBoundCopy(); } @@ -71,13 +73,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon major.BindValueChanged(updateMajor, true); } - private Box line = null!; - private void updateMajor(ValueChangedEvent major) { - line.Alpha = major.NewValue ? 1f : 0.5f; - line.Width = major.NewValue ? 1 : 0.5f; - majorEdgeContainer.Alpha = major.NewValue ? 1 : 0; + mainLine.Alpha = major.NewValue ? 1f : 0.5f; + topAnchor.Alpha = bottomAnchor.Alpha = major.NewValue ? mainLine.Alpha * 0.3f : 0; } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs index d7e37899ce..cecb99c690 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon private const double pre_beat_transition_time = 80; - private const float flash_opacity = 0.3f; + private const float kiai_flash_opacity = 0.15f; private ColourInfo accentColour; @@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon if (drawableHitObject.State.Value == ArmedState.Idle) { flash - .FadeTo(flash_opacity) + .FadeTo(kiai_flash_opacity) .Then() .FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs new file mode 100644 index 0000000000..2ff36ef9bf --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSamplePlayer.cs @@ -0,0 +1,42 @@ +// 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.Game.Audio; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Rulesets.UI; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + internal partial class ArgonDrumSamplePlayer : DrumSamplePlayer + { + private ArgonFlourishTriggerSource argonFlourishTrigger = null!; + + [BackgroundDependencyLoader] + private void load(Playfield playfield, IPooledSampleProvider sampleProvider) + { + var hitObjectContainer = playfield.HitObjectContainer; + + // Warm up pools for non-standard samples. + sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_NORMAL), true)); + sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_CLAP), true)); + sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_FLOURISH), true)); + + // We want to play back flourishes in an isolated source as to not have them cancelled. + AddInternal(argonFlourishTrigger = new ArgonFlourishTriggerSource(hitObjectContainer)); + } + + protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) => + new ArgonDrumSampleTriggerSource(hitObjectContainer, balance); + + protected override void Play(DrumSampleTriggerSource triggerSource, HitType hitType, bool strong) + { + base.Play(triggerSource, hitType, strong); + + // This won't always play something, but the logic for flourish playback is contained within. + argonFlourishTrigger.Play(hitType, strong); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs new file mode 100644 index 0000000000..fb4c1067e3 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonDrumSampleTriggerSource.cs @@ -0,0 +1,46 @@ +// 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 osu.Framework.Allocation; +using osu.Game.Audio; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Rulesets.UI; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public partial class ArgonDrumSampleTriggerSource : DrumSampleTriggerSource + { + [Resolved] + private ISkinSource skinSource { get; set; } = null!; + + public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) + : base(hitObjectContainer, balance) + { + } + + public override void Play(HitType hitType, bool strong) + { + TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; + + if (hitObject == null) + return; + + var originalSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); + + // If the sample is provided by a legacy skin, we should not try and do anything special. + if (skinSource.FindProvider(s => s.GetSample(originalSample) != null) is LegacySkinTransformer) + { + base.Play(hitType, strong); + return; + } + + // let the magic begin... + var samplesToPlay = new List { new VolumeAwareHitSampleInfo(originalSample, strong) }; + + PlaySamples(samplesToPlay.ToArray()); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonElongatedCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonElongatedCirclePiece.cs index 17386cc659..1ac2822a1b 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonElongatedCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonElongatedCirclePiece.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon protected override void Update() { base.Update(); - Width = Parent.DrawSize.X + DrawHeight; + Width = Parent!.DrawSize.X + DrawHeight; } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs new file mode 100644 index 0000000000..8dfe31b55d --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonFlourishTriggerSource.cs @@ -0,0 +1,79 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Game.Audio; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Rulesets.UI; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + internal partial class ArgonFlourishTriggerSource : DrumSampleTriggerSource + { + private readonly HitObjectContainer hitObjectContainer; + + [Resolved] + private ISkinSource skinSource { get; set; } = null!; + + /// + /// The minimum time to leave between flourishes that are added to strong rim hits. + /// + private const double time_between_flourishes = 2000; + + public ArgonFlourishTriggerSource(HitObjectContainer hitObjectContainer) + : base(hitObjectContainer) + { + this.hitObjectContainer = hitObjectContainer; + } + + public override void Play(HitType hitType, bool strong) + { + TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; + + if (hitObject == null) + return; + + var originalSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); + + // If the sample is provided by a legacy skin, we should not try and do anything special. + if (skinSource.FindProvider(s => s.GetSample(originalSample) != null) is LegacySkinTransformer) + return; + + if (strong && hitType == HitType.Rim && canPlayFlourish(hitObject)) + PlaySamples(new ISampleInfo[] { new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(HitSampleInfo.HIT_FLOURISH), true) }); + } + + private bool canPlayFlourish(TaikoHitObject hitObject) + { + double? lastFlourish = null; + + var hitObjects = hitObjectContainer.AliveObjects + .Reverse() + .Select(d => d.HitObject) + .OfType() + .Where(h => h.IsStrong && h.Type == HitType.Rim); + + // Add an additional 'flourish' sample to strong rim hits (that are at least `time_between_flourishes` apart). + // This is applied to hitobjects in reverse order, as to sound more musically coherent by biasing towards to + // end of groups/combos of strong rim hits instead of the start. + foreach (var h in hitObjects) + { + bool canFlourish = lastFlourish == null || lastFlourish - h.StartTime >= time_between_flourishes; + + if (canFlourish) + lastFlourish = h.StartTime; + + // hitObject can be either the strong hit itself (if hit late), or its nested strong object (if hit early) + // due to `GetMostValidObject()` idiosyncrasies. + // whichever it is, if we encounter it during iteration, stop looking. + if (h == hitObject || h.NestedHitObjects.Contains(hitObject)) + return canFlourish; + } + + return false; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index 780018af4e..9fcecd2b1a 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -60,6 +60,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon // the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield. return Drawable.Empty().With(d => d.Expire()); + case TaikoSkinComponents.DrumSamplePlayer: + return new ArgonDrumSamplePlayer(); + case TaikoSkinComponents.TaikoExplosionGreat: case TaikoSkinComponents.TaikoExplosionMiss: case TaikoSkinComponents.TaikoExplosionOk: diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/VolumeAwareHitSampleInfo.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/VolumeAwareHitSampleInfo.cs new file mode 100644 index 0000000000..3ca4b5a3c7 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/VolumeAwareHitSampleInfo.cs @@ -0,0 +1,52 @@ +// 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 osu.Game.Audio; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public class VolumeAwareHitSampleInfo : HitSampleInfo + { + public const int SAMPLE_VOLUME_THRESHOLD_HARD = 90; + public const int SAMPLE_VOLUME_THRESHOLD_MEDIUM = 60; + + public VolumeAwareHitSampleInfo(HitSampleInfo sampleInfo, bool isStrong = false) + : base(sampleInfo.Name, isStrong ? BANK_STRONG : getBank(sampleInfo.Bank, sampleInfo.Name, sampleInfo.Volume), sampleInfo.Suffix, sampleInfo.Volume) + { + } + + public override IEnumerable LookupNames + { + get + { + foreach (string name in base.LookupNames) + yield return name.Insert(name.LastIndexOf('/') + 1, "Argon/taiko-"); + } + } + + private static string getBank(string originalBank, string sampleName, int volume) + { + // So basically we're overwriting mapper's bank intentions here. + // The rationale is that most taiko beatmaps only use a single bank, but regularly adjust volume. + + switch (sampleName) + { + case HIT_NORMAL: + case HIT_CLAP: + { + if (volume >= SAMPLE_VOLUME_THRESHOLD_HARD) + return BANK_DRUM; + + if (volume >= SAMPLE_VOLUME_THRESHOLD_MEDIUM) + return BANK_NORMAL; + + return BANK_SOFT; + } + + default: + return originalBank; + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs index bde502bbed..b3833d372c 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default private const double pre_beat_transition_time = 80; - private const float flash_opacity = 0.3f; + private const float kiai_flash_opacity = 0.15f; [Resolved] private DrawableHitObject drawableHitObject { get; set; } = null!; @@ -187,7 +187,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default if (drawableHitObject.State.Value == ArmedState.Idle) { flashBox - .FadeTo(flash_opacity) + .FadeTo(kiai_flash_opacity) .Then() .FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultInputDrum.cs index 6d19db999c..60bacf6413 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultInputDrum.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultInputDrum.cs @@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default back = rim; } - if (target != null) + if (target != null && back != null) { const float scale_amount = 0.05f; const float alpha_amount = 0.5f; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/ElongatedCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/ElongatedCirclePiece.cs index 11d82a3714..c756b0fb66 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/ElongatedCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/ElongatedCirclePiece.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default protected override void Update() { base.Update(); - Width = Parent.DrawSize.X + DrawHeight; + Width = Parent!.DrawSize.X + DrawHeight; } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs index 37eb95b86f..dbc8718f02 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs @@ -1,6 +1,7 @@ // 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; @@ -8,6 +9,7 @@ using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; @@ -20,17 +22,21 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public partial class LegacyCirclePiece : CompositeDrawable, IHasAccentColour { + private static readonly Vector2 circle_piece_size = new Vector2(128); + private static readonly Vector2 max_circle_sprite_size = new Vector2(160); + private Drawable backgroundLayer = null!; private Drawable? foregroundLayer; private Bindable currentCombo { get; } = new BindableInt(); private int animationFrame; - private double beatLength; // required for editor blueprints (not sure why these circle pieces are zero size). public override Quad ScreenSpaceDrawQuad => backgroundLayer.ScreenSpaceDrawQuad; + private TimingControlPoint timingPoint = TimingControlPoint.DEFAULT; + public LegacyCirclePiece() { RelativeSizeAxes = Axes.Both; @@ -39,41 +45,48 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy [Resolved(canBeNull: true)] private GameplayState? gameplayState { get; set; } - [Resolved(canBeNull: true)] - private IBeatSyncProvider? beatSyncProvider { get; set; } - [BackgroundDependencyLoader] - private void load(ISkinSource skin, DrawableHitObject drawableHitObject) + private void load(ISkinSource skin, DrawableHitObject drawableHitObject, IBeatSyncProvider? beatSyncProvider) { - Drawable? getDrawableFor(string lookup) + Drawable? getDrawableFor(string lookup, bool animatable) { const string normal_hit = "taikohit"; const string big_hit = "taikobig"; string prefix = ((drawableHitObject.HitObject as TaikoStrongableHitObject)?.IsStrong ?? false) ? big_hit : normal_hit; - return skin.GetAnimation($"{prefix}{lookup}", true, false) ?? + return skin.GetAnimation($"{prefix}{lookup}", animatable, false, maxSize: max_circle_sprite_size) ?? // fallback to regular size if "big" version doesn't exist. - skin.GetAnimation($"{normal_hit}{lookup}", true, false); + skin.GetAnimation($"{normal_hit}{lookup}", animatable, false, maxSize: max_circle_sprite_size); } // backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer. - AddInternal(backgroundLayer = new LegacyKiaiFlashingDrawable(() => getDrawableFor("circle"))); - - foregroundLayer = getDrawableFor("circleoverlay"); - if (foregroundLayer != null) - AddInternal(foregroundLayer); - - // Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat). - // For now just stop at first frame for sanity. - foreach (var c in InternalChildren) + AddInternal(backgroundLayer = new LegacyKiaiFlashingDrawable(() => getDrawableFor("circle", false)) { - (c as IFramedAnimation)?.Stop(); + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); - c.Anchor = Anchor.Centre; - c.Origin = Anchor.Centre; + foregroundLayer = getDrawableFor("circleoverlay", true); + + if (foregroundLayer != null) + { + foregroundLayer.Anchor = Anchor.Centre; + foregroundLayer.Origin = Anchor.Centre; + + // Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat). + // For now just stop at first frame for sanity. + if (foregroundLayer is IFramedAnimation animatedForegroundLayer) + animatedForegroundLayer.Stop(); + + AddInternal(foregroundLayer); } + drawableHitObject.StartTimeBindable.BindValueChanged(startTime => + { + timingPoint = beatSyncProvider?.ControlPoints?.TimingPointAt(startTime.NewValue) ?? TimingControlPoint.DEFAULT; + }, true); + if (gameplayState != null) currentCombo.BindTo(gameplayState.ScoreProcessor.Combo); } @@ -91,13 +104,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy // Not all skins (including the default osu-stable) have similar sizes for "hitcircle" and "hitcircleoverlay". // This ensures they are scaled relative to each other but also match the expected DrawableHit size. foreach (var c in InternalChildren) - c.Scale = new Vector2(DrawHeight / 128); + c.Scale = new Vector2(DrawHeight / circle_piece_size.Y); - if (foregroundLayer is IFramedAnimation animatableForegroundLayer) - animateForegroundLayer(animatableForegroundLayer); + if (foregroundLayer is IFramedAnimation animatedForegroundLayer) + animateForegroundLayer(animatedForegroundLayer); } - private void animateForegroundLayer(IFramedAnimation animatableForegroundLayer) + private void animateForegroundLayer(IFramedAnimation animation) { int multiplier; @@ -111,18 +124,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } else { - animatableForegroundLayer.GotoFrame(0); + animation.GotoFrame(0); return; } - if (beatSyncProvider?.ControlPoints != null) - { - beatLength = beatSyncProvider.ControlPoints.TimingPointAt(Time.Current).BeatLength; - - animationFrame = Time.Current % ((beatLength * 2) / multiplier) >= beatLength / multiplier ? 0 : 1; - - animatableForegroundLayer.GotoFrame(animationFrame); - } + animationFrame = Math.Abs(Time.Current - timingPoint.Time) % ((timingPoint.BeatLength * 2) / multiplier) >= timingPoint.BeatLength / multiplier ? 0 : 1; + animation.GotoFrame(animationFrame); } private Color4 accentColour; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index d61f9ac35d..894b91e9ce 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -52,6 +52,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy return null; + case TaikoSkinComponents.DrumSamplePlayer: + return null; + case TaikoSkinComponents.CentreHit: case TaikoSkinComponents.RimHit: if (hasHitCircle) diff --git a/osu.Game.Rulesets.Taiko/TaikoInputManager.cs b/osu.Game.Rulesets.Taiko/TaikoInputManager.cs index ca06a0a77e..4292d461bf 100644 --- a/osu.Game.Rulesets.Taiko/TaikoInputManager.cs +++ b/osu.Game.Rulesets.Taiko/TaikoInputManager.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.ComponentModel; using osu.Framework.Allocation; using osu.Framework.Input.Bindings; diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index d6824109b3..072653dcbf 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -34,6 +34,7 @@ using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; using osu.Game.Rulesets.Configuration; using osu.Game.Configuration; +using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.Taiko.Configuration; namespace osu.Game.Rulesets.Taiko @@ -116,6 +117,9 @@ namespace osu.Game.Rulesets.Taiko if (mods.HasFlagFast(LegacyMods.Random)) yield return new TaikoModRandom(); + + if (mods.HasFlagFast(LegacyMods.ScoreV2)) + yield return new ModScoreV2(); } public override LegacyMods ConvertToLegacyMods(Mod[] mods) @@ -176,6 +180,12 @@ namespace osu.Game.Rulesets.Taiko new ModAdaptiveSpeed() }; + case ModType.System: + return new Mod[] + { + new ModScoreV2(), + }; + default: return Array.Empty(); } @@ -197,6 +207,8 @@ namespace osu.Game.Rulesets.Taiko public int LegacyID => 1; + public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new TaikoLegacyScoreSimulator(); + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TaikoRulesetConfigManager(settings, RulesetInfo); @@ -245,7 +257,7 @@ namespace osu.Game.Rulesets.Taiko RelativeSizeAxes = Axes.X, Height = 250 }, true), - new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem("Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[] { new AverageHitError(timedHitEvents), new UnstableRate(timedHitEvents) diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index b8e3313e1b..28133ffcb2 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -21,6 +21,7 @@ namespace osu.Game.Rulesets.Taiko TaikoExplosionKiai, Scroller, Mascot, - KiaiGlow + KiaiGlow, + DrumSamplePlayer } } diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index 64d406a308..2af4c0c2e8 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -35,8 +35,6 @@ namespace osu.Game.Rulesets.Taiko.UI public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager; - protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping; - protected override bool UserScrollSpeedAdjustment => false; private SkinnableDrawable scroller; @@ -45,6 +43,7 @@ namespace osu.Game.Rulesets.Taiko.UI : base(ruleset, beatmap, mods) { Direction.Value = ScrollingDirection.Left; + VisualisationMethod = ScrollVisualisationMethod.Overlapping; } [BackgroundDependencyLoader] @@ -65,6 +64,11 @@ namespace osu.Game.Rulesets.Taiko.UI { base.Update(); + TimeRange.Value = ComputeTimeRange(); + } + + protected virtual double ComputeTimeRange() + { // Taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened. const float scroll_rate = 10; @@ -73,7 +77,7 @@ namespace osu.Game.Rulesets.Taiko.UI // We clamp the ratio to the maximum aspect ratio to keep scroll speed consistent on widths lower than the default. float ratio = Math.Max(DrawSize.X / 768f, TaikoPlayfieldAdjustmentContainer.MAXIMUM_ASPECT); - TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate; + return (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate; } protected override void UpdateAfterChildren() diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs index 6454fb5afa..57067ac666 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSamplePlayer.cs @@ -1,57 +1,151 @@ // 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.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; namespace osu.Game.Rulesets.Taiko.UI { internal partial class DrumSamplePlayer : CompositeDrawable, IKeyBindingHandler { - private readonly DrumSampleTriggerSource leftRimSampleTriggerSource; - private readonly DrumSampleTriggerSource leftCentreSampleTriggerSource; - private readonly DrumSampleTriggerSource rightCentreSampleTriggerSource; - private readonly DrumSampleTriggerSource rightRimSampleTriggerSource; + private DrumSampleTriggerSource leftCentreTrigger = null!; + private DrumSampleTriggerSource rightCentreTrigger = null!; + private DrumSampleTriggerSource leftRimTrigger = null!; + private DrumSampleTriggerSource rightRimTrigger = null!; + private DrumSampleTriggerSource strongCentreTrigger = null!; + private DrumSampleTriggerSource strongRimTrigger = null!; - public DrumSamplePlayer(HitObjectContainer hitObjectContainer) + private double lastHitTime; + private TaikoAction? lastAction; + + [BackgroundDependencyLoader] + private void load(Playfield playfield) { + var hitObjectContainer = playfield.HitObjectContainer; InternalChildren = new Drawable[] { - leftRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer), - leftCentreSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer), - rightCentreSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer), - rightRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer), + leftCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Left), + rightCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Right), + leftRimTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Left), + rightRimTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Right), + strongCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Centre), + strongRimTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Centre) }; } + protected virtual DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) + => new DrumSampleTriggerSource(hitObjectContainer); + public bool OnPressed(KeyBindingPressEvent e) { + if ((Clock as IGameplayClock)?.IsRewinding == true) + return false; + + HitType hitType; + + DrumSampleTriggerSource triggerSource; + + bool strong = checkStrongValidity(e.Action, lastAction, Time.Current - lastHitTime); + switch (e.Action) { - case TaikoAction.LeftRim: - leftRimSampleTriggerSource.Play(HitType.Rim); - break; - case TaikoAction.LeftCentre: - leftCentreSampleTriggerSource.Play(HitType.Centre); + hitType = HitType.Centre; + triggerSource = strong ? strongCentreTrigger : leftCentreTrigger; break; case TaikoAction.RightCentre: - rightCentreSampleTriggerSource.Play(HitType.Centre); + hitType = HitType.Centre; + triggerSource = strong ? strongCentreTrigger : rightCentreTrigger; + break; + + case TaikoAction.LeftRim: + hitType = HitType.Rim; + triggerSource = strong ? strongRimTrigger : leftRimTrigger; break; case TaikoAction.RightRim: - rightRimSampleTriggerSource.Play(HitType.Rim); + hitType = HitType.Rim; + triggerSource = strong ? strongRimTrigger : rightRimTrigger; break; + + default: + return false; } + if (strong) + { + switch (hitType) + { + case HitType.Centre: + flushCenterTriggerSources(); + break; + + case HitType.Rim: + flushRimTriggerSources(); + break; + } + } + + Play(triggerSource, hitType, strong); + + lastHitTime = Time.Current; + lastAction = e.Action; + return false; } + protected virtual void Play(DrumSampleTriggerSource triggerSource, HitType hitType, bool strong) => + triggerSource.Play(hitType, strong); + + private bool checkStrongValidity(TaikoAction newAction, TaikoAction? lastAction, double timeBetweenActions) + { + if (lastAction == null) + return false; + + if (timeBetweenActions < 0 || timeBetweenActions > DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW) + return false; + + switch (newAction) + { + case TaikoAction.LeftCentre: + return lastAction == TaikoAction.RightCentre; + + case TaikoAction.RightCentre: + return lastAction == TaikoAction.LeftCentre; + + case TaikoAction.LeftRim: + return lastAction == TaikoAction.RightRim; + + case TaikoAction.RightRim: + return lastAction == TaikoAction.LeftRim; + + default: + return false; + } + } + + private void flushCenterTriggerSources() + { + leftCentreTrigger.StopAllPlayback(); + rightCentreTrigger.StopAllPlayback(); + strongCentreTrigger.StopAllPlayback(); + } + + private void flushRimTriggerSources() + { + leftRimTrigger.StopAllPlayback(); + rightRimTrigger.StopAllPlayback(); + strongRimTrigger.StopAllPlayback(); + } + public void OnReleased(KeyBindingReleaseEvent e) { } diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs index 92f2b74568..1de16c2294 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs @@ -2,30 +2,74 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; +using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.UI; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.UI { public partial class DrumSampleTriggerSource : GameplaySampleTriggerSource { - public DrumSampleTriggerSource(HitObjectContainer hitObjectContainer) + private const double stereo_separation = 0.2; + + public DrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance = SampleBalance.Centre) : base(hitObjectContainer) { + switch (balance) + { + case SampleBalance.Left: + AudioContainer.Balance.Value = -stereo_separation; + break; + + case SampleBalance.Centre: + AudioContainer.Balance.Value = 0; + break; + + case SampleBalance.Right: + AudioContainer.Balance.Value = stereo_separation; + break; + } } - public void Play(HitType hitType) + public virtual void Play(HitType hitType, bool strong) { - var hitSample = GetMostValidObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); + TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject; - if (hitSample == null) + if (hitObject == null) return; - PlaySamples(new ISampleInfo[] { new HitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL, hitSample.Bank, volume: hitSample.Volume) }); + var baseSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL); + + if (strong) + { + PlaySamples(new ISampleInfo[] + { + baseSample, + hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH) + }); + } + else + { + PlaySamples(new ISampleInfo[] { baseSample }); + } } public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead"); + + protected override void ApplySampleInfo(SkinnableSound hitSound, ISampleInfo[] samples) + { + base.ApplySampleInfo(hitSound, samples); + + hitSound.Balance.Value = -0.05 + RNG.NextDouble(0.1); + } + } + + public enum SampleBalance + { + Left, + Centre, + Right } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 9f9debe7d7..31f8171290 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Taiko.UI ///
public Bindable ClassicHitTargetPosition = new BindableBool(); + public Container UnderlayElements { get; private set; } = null!; + private Container hitExplosionContainer; private Container kiaiExplosionContainer; private JudgementContainer judgementContainer; @@ -130,7 +132,14 @@ namespace osu.Game.Rulesets.Taiko.UI { Name = "Bar line content", RelativeSizeAxes = Axes.Both, - Child = barLinePlayfield = new BarLinePlayfield(), + Children = new Drawable[] + { + UnderlayElements = new Container + { + RelativeSizeAxes = Axes.Both, + }, + barLinePlayfield = new BarLinePlayfield(), + } }, hitObjectContent = new Container { @@ -170,7 +179,10 @@ namespace osu.Game.Rulesets.Taiko.UI RelativeSizeAxes = Axes.Both, }, drumRollHitContainer.CreateProxy(), - new DrumSamplePlayer(HitObjectContainer), + new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumSamplePlayer), _ => new DrumSamplePlayer()) + { + RelativeSizeAxes = Axes.Both, + }, // this is added at the end of the hierarchy to receive input before taiko objects. // but is proxied below everything to not cover visual effects such as hit explosions. inputDrum, diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs index 3587783104..54608b77de 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.UI // This is still a bit weird, because readability changes with window size, but it is what it is. if (LockPlayfieldAspectRange.Value) { - float currentAspect = Parent.ChildSize.X / Parent.ChildSize.Y; + float currentAspect = Parent!.ChildSize.X / Parent!.ChildSize.Y; if (currentAspect > MAXIMUM_ASPECT) height *= currentAspect / MAXIMUM_ASPECT; diff --git a/osu.Game.Tests.Android/AndroidManifest.xml b/osu.Game.Tests.Android/AndroidManifest.xml index f25b2e5328..6f91fb928c 100644 --- a/osu.Game.Tests.Android/AndroidManifest.xml +++ b/osu.Game.Tests.Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/osu.Game.Tests.Android/MainActivity.cs b/osu.Game.Tests.Android/MainActivity.cs index bdb947fbb4..d25e46f3c5 100644 --- a/osu.Game.Tests.Android/MainActivity.cs +++ b/osu.Game.Tests.Android/MainActivity.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.Reflection; using Android.App; using Android.OS; @@ -15,7 +13,7 @@ namespace osu.Game.Tests.Android { protected override Framework.Game CreateGame() => new OsuTestBrowser(); - protected override void OnCreate(Bundle savedInstanceState) + protected override void OnCreate(Bundle? savedInstanceState) { base.OnCreate(savedInstanceState); diff --git a/osu.Game.Tests.iOS/Info.plist b/osu.Game.Tests.iOS/Info.plist index ac661f6263..d2d0583e46 100644 --- a/osu.Game.Tests.iOS/Info.plist +++ b/osu.Game.Tests.iOS/Info.plist @@ -5,7 +5,7 @@ CFBundleName osu.Game.Tests.iOS CFBundleIdentifier - ppy.osu-Game-Tests-iOS + sh.ppy.osu-tests CFBundleShortVersionString 1.0 CFBundleVersion @@ -42,4 +42,4 @@ CADisableMinimumFrameDurationOnPhone - + \ No newline at end of file diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 970b6aaf60..66151a51e6 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestDecodeBeatmapVersion() { - using (var resStream = TestResources.OpenResource("beatmap-version.osu")) + using (var resStream = TestResources.OpenResource("beatmap-version-6.osu")) using (var stream = new LineBufferedReader(resStream)) { var decoder = Decoder.GetDecoder(stream); @@ -45,6 +45,25 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [TestCase(false)] + [TestCase(true)] + public void TestPreviewPointWithOffsets(bool applyOffsets) + { + using (var resStream = TestResources.OpenResource("beatmap-version-4.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var decoder = Decoder.GetDecoder(stream); + ((LegacyBeatmapDecoder)decoder).ApplyOffsets = applyOffsets; + var working = new TestWorkingBeatmap(decoder.Decode(stream)); + + Assert.AreEqual(4, working.BeatmapInfo.BeatmapVersion); + Assert.AreEqual(4, working.Beatmap.BeatmapInfo.BeatmapVersion); + Assert.AreEqual(4, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty()).BeatmapInfo.BeatmapVersion); + + Assert.AreEqual(-1, working.BeatmapInfo.Metadata.PreviewTime); + } + } + [Test] public void TestDecodeBeatmapGeneral() { @@ -621,6 +640,38 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestInvalidBankDefaultsToNormal() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("invalid-bank.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var hitObjects = decoder.Decode(stream).HitObjects; + + assertObjectHasBanks(hitObjects[0], HitSampleInfo.BANK_DRUM); + assertObjectHasBanks(hitObjects[1], HitSampleInfo.BANK_NORMAL); + assertObjectHasBanks(hitObjects[2], HitSampleInfo.BANK_SOFT); + assertObjectHasBanks(hitObjects[3], HitSampleInfo.BANK_DRUM); + assertObjectHasBanks(hitObjects[4], HitSampleInfo.BANK_NORMAL); + + assertObjectHasBanks(hitObjects[5], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_DRUM); + assertObjectHasBanks(hitObjects[6], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_NORMAL); + assertObjectHasBanks(hitObjects[7], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_SOFT); + assertObjectHasBanks(hitObjects[8], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_DRUM); + assertObjectHasBanks(hitObjects[9], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_NORMAL); + } + + void assertObjectHasBanks(HitObject hitObject, string normalBank, string? additionsBank = null) + { + Assert.AreEqual(normalBank, hitObject.Samples[0].Bank); + + if (additionsBank != null) + Assert.AreEqual(additionsBank, hitObject.Samples[1].Bank); + } + } + [Test] public void TestFallbackDecoderForCorruptedHeader() { @@ -883,10 +934,11 @@ namespace osu.Game.Tests.Beatmaps.Formats } } - [Test] - public void TestLegacyDefaultsPreserved() + [TestCase(false)] + [TestCase(true)] + public void TestLegacyDefaultsPreserved(bool applyOffsets) { - var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = applyOffsets }; using (var memoryStream = new MemoryStream()) using (var stream = new LineBufferedReader(memoryStream)) @@ -1024,10 +1076,21 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(controlPoints.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1)); Assert.That(controlPoints.DifficultyPointAt(3000).SliderVelocity, Is.EqualTo(1)); -#pragma warning disable 618 - Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(2000)).GenerateTicks, Is.False); - Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(3000)).GenerateTicks, Is.True); -#pragma warning restore 618 + Assert.That(controlPoints.DifficultyPointAt(2000).GenerateTicks, Is.False); + Assert.That(controlPoints.DifficultyPointAt(3000).GenerateTicks, Is.True); + } + } + + [Test] + public void TestSamplePointLeniency() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("sample-point-leniency.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var hitObject = decoder.Decode(stream).HitObjects.Single(); + Assert.That(hitObject.Samples.Select(s => s.Volume), Has.All.EqualTo(70)); } } } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index fac5e098b9..5d9049ead7 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections; using System.Collections.Generic; diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyDecoderTest.cs index c1c9e0d118..39bb616563 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyDecoderTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Beatmaps.Formats; diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 93cda34ef7..ab88be1511 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -87,6 +87,34 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestDecodeLegacyOnlineID() + { + var decoder = new TestLegacyScoreDecoder(); + + using (var resourceStream = TestResources.OpenResource("Replays/taiko-replay-with-legacy-online-id.osr")) + { + var score = decoder.Parse(resourceStream); + + Assert.That(score.ScoreInfo.OnlineID, Is.EqualTo(-1)); + Assert.That(score.ScoreInfo.LegacyOnlineID, Is.EqualTo(255)); + } + } + + [Test] + public void TestDecodeNewOnlineID() + { + var decoder = new TestLegacyScoreDecoder(); + + using (var resourceStream = TestResources.OpenResource("Replays/taiko-replay-with-new-online-id.osr")) + { + var score = decoder.Parse(resourceStream); + + Assert.That(score.ScoreInfo.OnlineID, Is.EqualTo(258)); + Assert.That(score.ScoreInfo.LegacyOnlineID, Is.EqualTo(-1)); + } + } + [TestCase(3, true)] [TestCase(6, false)] [TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)] diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 34ff8bfd84..647c0aed75 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -287,5 +287,26 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(manyTimes.EndTime, Is.EqualTo(9000 + loop_duration)); } } + + [Test] + public void TestVideoAndBackgroundEventsDoNotAffectStoryboardBounds() + { + var decoder = new LegacyStoryboardDecoder(); + + using var resStream = TestResources.OpenResource("video-background-events-ignored.osb"); + using var stream = new LineBufferedReader(resStream); + + var storyboard = decoder.Decode(stream); + + Assert.Multiple(() => + { + Assert.That(storyboard.GetLayer(@"Video").Elements, Has.Count.EqualTo(1)); + Assert.That(storyboard.GetLayer(@"Video").Elements.Single(), Is.InstanceOf()); + Assert.That(storyboard.GetLayer(@"Video").Elements.Single().StartTime, Is.EqualTo(-5678)); + + Assert.That(storyboard.EarliestEventTime, Is.Null); + Assert.That(storyboard.LatestEventTime, Is.Null); + }); + } } } diff --git a/osu.Game.Tests/Beatmaps/IO/LineBufferedReaderTest.cs b/osu.Game.Tests/Beatmaps/IO/LineBufferedReaderTest.cs index 8f20fd7a68..5e37f01c81 100644 --- a/osu.Game.Tests/Beatmaps/IO/LineBufferedReaderTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/LineBufferedReaderTest.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.IO; using System.Text; diff --git a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs index 04eb9a3fa2..810ea5dbd0 100644 --- a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.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.IO; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs index d30ab3dea1..c7cf3fe956 100644 --- a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs +++ b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Game.Rulesets.Objects; @@ -18,7 +16,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestSingleSpan() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -33,7 +31,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestRepeat() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -54,7 +52,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestNonEvenTicks() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -85,12 +83,12 @@ namespace osu.Game.Tests.Beatmaps } [Test] - public void TestLegacyLastTickOffset() + public void TestLastTickOffset() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1).ToArray(); Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LegacyLastTick)); - Assert.That(events[2].Time, Is.EqualTo(900)); + Assert.That(events[2].Time, Is.EqualTo(span_duration + SliderEventGenerator.TAIL_LENIENCY)); } [Test] @@ -99,7 +97,7 @@ namespace osu.Game.Tests.Beatmaps const double velocity = 5; const double min_distance = velocity * 10; - var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2).ToArray(); Assert.Multiple(() => { diff --git a/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs b/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs index b4a205b478..f3c05d8970 100644 --- a/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs +++ b/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Models; diff --git a/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs b/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs index 89b8c8927d..237fe758b5 100644 --- a/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs +++ b/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestCachedRetrievalWithFiles() => AddStep("run test", () => { - var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID).Detach()); + var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID)!.Detach()); Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0)); @@ -90,7 +90,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestForcedRefetchRetrievalWithFiles() => AddStep("run test", () => { - var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID).Detach()); + var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID)!.Detach()); Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0)); @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestSavePreservesCollections() => AddStep("run test", () => { - var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID).Detach()); + var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID)!.Detach()); var working = beatmaps.GetWorkingBeatmap(beatmap); diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 3c35dc311f..1baa737a9c 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -32,8 +32,26 @@ namespace osu.Game.Tests.Chat Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a gopher://really-old-protocol we don't support." }); Assert.AreEqual(result.Content, result.DisplayContent); - Assert.AreEqual(1, result.Links.Count); - Assert.AreEqual("gopher://really-old-protocol", result.Links[0].Url); + Assert.AreEqual(0, result.Links.Count); + } + + [Test] + public void TestFakeProtocolLink() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a osunotarealprotocol://completely-made-up-protocol we don't support." }); + + Assert.AreEqual(result.Content, result.DisplayContent); + Assert.AreEqual(0, result.Links.Count); + } + + [Test] + public void TestSupportedProtocolLinkParsing() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "forgotspacehttps://dev.ppy.sh joinmyosump://12345 jointheosu://chan/#english" }); + + Assert.AreEqual("https://dev.ppy.sh", result.Links[0].Url); + Assert.AreEqual("osump://12345", result.Links[1].Url); + Assert.AreEqual("osu://chan/#english", result.Links[2].Url); } [Test] @@ -478,8 +496,8 @@ namespace osu.Game.Tests.Chat Content = "This is a [http://www.simple-test.com simple test] with some [traps] and [[wiki links]]. Don't forget to visit https://osu.ppy.sh (now!)[http://google.com]\uD83D\uDE12" }); - Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now!\0\0\0", result.DisplayContent); - Assert.AreEqual(5, result.Links.Count); + Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now![emoji]", result.DisplayContent); + Assert.AreEqual(4, result.Links.Count); Link f = result.Links.Find(l => l.Url == "https://dev.ppy.sh/wiki/wiki links"); Assert.That(f, Is.Not.Null); @@ -500,27 +518,22 @@ namespace osu.Game.Tests.Chat Assert.That(f, Is.Not.Null); Assert.AreEqual(78, f.Index); Assert.AreEqual(18, f.Length); - - f = result.Links.Find(l => l.Url == "\uD83D\uDE12"); - Assert.That(f, Is.Not.Null); - Assert.AreEqual(101, f.Index); - Assert.AreEqual(3, f.Length); } [Test] public void TestEmoji() { - Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" }); - Assert.AreEqual("Hello world\0\0\0<--This is an emoji,There are more:\0\0\0\0\0\0,\0\0\0", result.DisplayContent); - Assert.AreEqual(result.Links.Count, 4); - Assert.AreEqual(result.Links[0].Index, 11); - Assert.AreEqual(result.Links[1].Index, 49); - Assert.AreEqual(result.Links[2].Index, 52); - Assert.AreEqual(result.Links[3].Index, 56); - Assert.AreEqual(result.Links[0].Url, "\uD83D\uDE12"); - Assert.AreEqual(result.Links[1].Url, "\uD83D\uDE10"); - Assert.AreEqual(result.Links[2].Url, "\uD83D\uDE00"); - Assert.AreEqual(result.Links[3].Url, "\uD83D\uDE20"); + Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more emojis among us:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" }); + Assert.AreEqual("Hello world[emoji]<--This is an emoji,There are more emojis among us:[emoji][emoji],[emoji]", result.DisplayContent); + Assert.AreEqual(result.Links.Count, 0); + } + + [Test] + public void TestEmojiWithSuccessiveParens() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "\uD83D\uDE10(let's hope this doesn't accidentally turn into a link)" }); + Assert.AreEqual("[emoji](let's hope this doesn't accidentally turn into a link)", result.DisplayContent); + Assert.AreEqual(result.Links.Count, 0); } [Test] diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs index 9079ecdc48..d034e69957 100644 --- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs +++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.IO; using System.Linq; diff --git a/osu.Game.Tests/Database/BackgroundBeatmapProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs similarity index 57% rename from osu.Game.Tests/Database/BackgroundBeatmapProcessorTests.cs rename to osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index ddb60606ec..e65088ca2e 100644 --- a/osu.Game.Tests/Database/BackgroundBeatmapProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -8,6 +8,9 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; using osu.Game.Screens.Play; using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Visual; @@ -15,7 +18,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Tests.Database { [HeadlessTest] - public partial class BackgroundBeatmapProcessorTests : OsuTestScene, ILocalUserPlayInfo + public partial class BackgroundDataStoreProcessorTests : OsuTestScene, ILocalUserPlayInfo { public IBindable IsPlaying => isPlaying; @@ -42,7 +45,7 @@ namespace osu.Game.Tests.Database { return Realm.Run(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); }); }); @@ -51,7 +54,7 @@ namespace osu.Game.Tests.Database { Realm.Write(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; foreach (var b in beatmapSetInfo.Beatmaps) b.StarRating = -1; }); @@ -59,14 +62,14 @@ namespace osu.Game.Tests.Database AddStep("Run background processor", () => { - Add(new TestBackgroundBeatmapProcessor()); + Add(new TestBackgroundDataStoreProcessor()); }); AddUntilStep("wait for difficulties repopulated", () => { return Realm.Run(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); }); }); @@ -79,7 +82,7 @@ namespace osu.Game.Tests.Database { return Realm.Run(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); }); }); @@ -90,7 +93,7 @@ namespace osu.Game.Tests.Database { Realm.Write(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; foreach (var b in beatmapSetInfo.Beatmaps) b.StarRating = -1; }); @@ -98,7 +101,7 @@ namespace osu.Game.Tests.Database AddStep("Run background processor", () => { - Add(new TestBackgroundBeatmapProcessor()); + Add(new TestBackgroundDataStoreProcessor()); }); AddWaitStep("wait some", 500); @@ -107,7 +110,7 @@ namespace osu.Game.Tests.Database { return Realm.Run(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; return beatmapSetInfo.Beatmaps.All(b => b.StarRating == -1); }); }); @@ -118,13 +121,65 @@ namespace osu.Game.Tests.Database { return Realm.Run(r => { - var beatmapSetInfo = r.Find(importedSet.ID); + var beatmapSetInfo = r.Find(importedSet.ID)!; return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); }); }); } - public partial class TestBackgroundBeatmapProcessor : BackgroundBeatmapProcessor + [TestCase(30000002)] + [TestCase(30000003)] + public void TestScoreUpgradeSuccess(int scoreVersion) + { + ScoreInfo scoreInfo = null!; + + AddStep("Add score which requires upgrade (and has beatmap)", () => + { + Realm.Write(r => + { + r.Add(scoreInfo = new ScoreInfo(ruleset: r.All().First(), beatmap: r.All().First()) + { + TotalScoreVersion = scoreVersion, + LegacyTotalScore = 123456, + IsLegacyScore = true, + }); + }); + }); + + AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor())); + + AddUntilStep("Score version upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(LegacyScoreEncoder.LATEST_VERSION)); + AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False); + } + + [Test] + public void TestScoreUpgradeFailed() + { + ScoreInfo scoreInfo = null!; + + AddStep("Add score which requires upgrade (but has no beatmap)", () => + { + Realm.Write(r => + { + r.Add(scoreInfo = new ScoreInfo(ruleset: r.All().First(), beatmap: new BeatmapInfo + { + BeatmapSet = new BeatmapSetInfo(), + Ruleset = r.All().First(), + }) + { + TotalScoreVersion = 30000002, + IsLegacyScore = true, + }); + }); + }); + + AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor())); + + AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True); + AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000002)); + } + + public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor { protected override int TimeToSleepDuringGameplay => 10; } diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 446eb72b04..0eac70f9c8 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -18,6 +18,7 @@ using osu.Game.Extensions; using osu.Game.Models; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; +using osu.Game.Scoring; using osu.Game.Tests.Resources; using Realms; using SharpCompress.Archives; @@ -416,6 +417,108 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestImport_Modify_Revert() + { + RunTestWithRealmAsync(async (realm, storage) => + { + var importer = new BeatmapImporter(storage, realm); + using var store = new RealmRulesetStore(realm, storage); + + var imported = await LoadOszIntoStore(importer, realm.Realm); + + await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First()); + + var score = realm.Run(r => r.All().Single()); + + string originalHash = imported.Beatmaps.First().Hash; + const string modified_hash = "new_hash"; + + Assert.That(imported.Beatmaps.First().Scores.Single(), Is.EqualTo(score)); + + Assert.That(score.BeatmapHash, Is.EqualTo(originalHash)); + Assert.That(score.BeatmapInfo, Is.EqualTo(imported.Beatmaps.First())); + + // imitate making local changes via editor + // ReSharper disable once MethodHasAsyncOverload + realm.Write(r => + { + BeatmapInfo beatmap = imported.Beatmaps.First(); + beatmap.Hash = modified_hash; + beatmap.ResetOnlineInfo(); + beatmap.UpdateLocalScores(r); + }); + + Assert.That(!imported.Beatmaps.First().Scores.Any()); + + Assert.That(score.BeatmapInfo, Is.Null); + Assert.That(score.BeatmapHash, Is.EqualTo(originalHash)); + + // imitate reverting the local changes made above + // ReSharper disable once MethodHasAsyncOverload + realm.Write(r => + { + BeatmapInfo beatmap = imported.Beatmaps.First(); + beatmap.Hash = originalHash; + beatmap.ResetOnlineInfo(); + beatmap.UpdateLocalScores(r); + }); + + Assert.That(imported.Beatmaps.First().Scores.Single(), Is.EqualTo(score)); + + Assert.That(score.BeatmapHash, Is.EqualTo(originalHash)); + Assert.That(score.BeatmapInfo, Is.EqualTo(imported.Beatmaps.First())); + }); + } + + [Test] + public void TestImport_ThenModifyMapWithScore_ThenImport() + { + RunTestWithRealmAsync(async (realm, storage) => + { + var importer = new BeatmapImporter(storage, realm); + using var store = new RealmRulesetStore(realm, storage); + + string? temp = TestResources.GetTestBeatmapForImport(); + + var imported = await LoadOszIntoStore(importer, realm.Realm); + + await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First()); + + Assert.That(imported.Beatmaps.First().Scores.Any()); + + // imitate making local changes via editor + // ReSharper disable once MethodHasAsyncOverload + realm.Write(r => + { + BeatmapInfo beatmap = imported.Beatmaps.First(); + beatmap.Hash = "new_hash"; + beatmap.ResetOnlineInfo(); + beatmap.UpdateLocalScores(r); + }); + + Assert.That(!imported.Beatmaps.First().Scores.Any()); + + var importedSecondTime = await importer.Import(new ImportTask(temp)); + + EnsureLoaded(realm.Realm); + + // check the newly "imported" beatmap is not the original. + Assert.NotNull(importedSecondTime); + Debug.Assert(importedSecondTime != null); + Assert.That(imported.ID != importedSecondTime.ID); + + var importedFirstTimeBeatmap = imported.Beatmaps.First(); + var importedSecondTimeBeatmap = importedSecondTime.PerformRead(s => s.Beatmaps.First()); + + Assert.That(importedFirstTimeBeatmap.ID != importedSecondTimeBeatmap.ID); + Assert.That(importedFirstTimeBeatmap.Hash != importedSecondTimeBeatmap.Hash); + Assert.That(!importedFirstTimeBeatmap.Scores.Any()); + Assert.That(importedSecondTimeBeatmap.Scores.Count() == 1); + Assert.That(importedSecondTimeBeatmap.Scores.Single().BeatmapInfo, Is.EqualTo(importedSecondTimeBeatmap)); + }); + } + [Test] public void TestImportThenImportWithChangedFile() { @@ -1074,18 +1177,16 @@ namespace osu.Game.Tests.Database Assert.IsTrue(realm.All().First(_ => true).DeletePending); } - private static Task createScoreForBeatmap(Realm realm, BeatmapInfo beatmap) - { - // TODO: reimplement when we have score support in realm. - // return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo - // { - // OnlineID = 2, - // Beatmap = beatmap, - // BeatmapInfoID = beatmap.ID - // }, new ImportScoreTest.TestArchiveReader()); - - return Task.CompletedTask; - } + private static Task createScoreForBeatmap(Realm realm, BeatmapInfo beatmap) => + realm.WriteAsync(() => + { + realm.Add(new ScoreInfo + { + OnlineID = 2, + BeatmapInfo = beatmap, + BeatmapHash = beatmap.Hash + }); + }); private static void checkBeatmapSetCount(Realm realm, int expected, bool includeDeletePending = false) { diff --git a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs index b94cff2a9a..d30b3c089e 100644 --- a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs @@ -323,7 +323,7 @@ namespace osu.Game.Tests.Database var beatmapInfo = s.Beatmaps.First(b => b.File?.Filename != removedFilename); scoreTargetBeatmapHash = beatmapInfo.Hash; - s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All().First(), new RealmUser())); + s.Realm!.Add(new ScoreInfo(beatmapInfo, s.Realm.All().First(), new RealmUser())); }); realm.Run(r => r.Refresh()); @@ -347,6 +347,73 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestDanglingScoreTransferred() + { + RunTestWithRealmAsync(async (realm, storage) => + { + var importer = new BeatmapImporter(storage, realm); + using var rulesets = new RealmRulesetStore(realm, storage); + + using var __ = getBeatmapArchive(out string pathOriginal); + using var _ = getBeatmapArchive(out string pathOnlineCopy); + + var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal)); + + Assert.That(importBeforeUpdate, Is.Not.Null); + Debug.Assert(importBeforeUpdate != null); + + string scoreTargetBeatmapHash = string.Empty; + + // set a score on the beatmap + importBeforeUpdate.PerformWrite(s => + { + var beatmapInfo = s.Beatmaps.First(); + + scoreTargetBeatmapHash = beatmapInfo.Hash; + + s.Realm!.Add(new ScoreInfo(beatmapInfo, s.Realm.All().First(), new RealmUser())); + }); + + // locally modify beatmap + const string new_beatmap_hash = "new_hash"; + importBeforeUpdate.PerformWrite(s => + { + var beatmapInfo = s.Beatmaps.First(b => b.Hash == scoreTargetBeatmapHash); + + beatmapInfo.Hash = new_beatmap_hash; + beatmapInfo.ResetOnlineInfo(); + beatmapInfo.UpdateLocalScores(s.Realm!); + }); + + realm.Run(r => r.Refresh()); + + // making changes to a beatmap doesn't remove the score from realm, but should disassociate the beatmap. + checkCount(realm, 1); + Assert.That(realm.Run(r => r.All().First().BeatmapInfo), Is.Null); + + // reimport the original beatmap before local modifications + var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOnlineCopy), importBeforeUpdate.Value); + + Assert.That(importAfterUpdate, Is.Not.Null); + Debug.Assert(importAfterUpdate != null); + + realm.Run(r => r.Refresh()); + + // both original and locally modified versions present + checkCount(realm, count_beatmaps + 1); + checkCount(realm, count_beatmaps + 1); + checkCount(realm, 2); + + // score is preserved + checkCount(realm, 1); + + // score is transferred to new beatmap + Assert.That(importBeforeUpdate.Value.Beatmaps.First(b => b.Hash == new_beatmap_hash).Scores, Has.Count.EqualTo(0)); + Assert.That(importAfterUpdate.Value.Beatmaps.First(b => b.Hash == scoreTargetBeatmapHash).Scores, Has.Count.EqualTo(1)); + }); + } + [Test] public void TestScoreLostOnModification() { @@ -368,7 +435,7 @@ namespace osu.Game.Tests.Database { var beatmapInfo = s.Beatmaps.Last(); scoreTargetFilename = beatmapInfo.File?.Filename; - s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All().First(), new RealmUser())); + s.Realm!.Add(new ScoreInfo(beatmapInfo, s.Realm.All().First(), new RealmUser())); }); realm.Run(r => r.Refresh()); @@ -461,7 +528,7 @@ namespace osu.Game.Tests.Database importBeforeUpdate.PerformWrite(s => { - var beatmapCollection = s.Realm.Add(new BeatmapCollection("test collection")); + var beatmapCollection = s.Realm!.Add(new BeatmapCollection("test collection")); beatmapsToAddToCollection = s.Beatmaps.Count - (allOriginalBeatmapsInCollection ? 0 : 1); for (int i = 0; i < beatmapsToAddToCollection; i++) @@ -476,7 +543,7 @@ namespace osu.Game.Tests.Database importAfterUpdate.PerformRead(updated => { - updated.Realm.Refresh(); + updated.Realm!.Refresh(); string[] hashes = updated.Realm.All().Single().BeatmapMD5Hashes.ToArray(); @@ -526,7 +593,7 @@ namespace osu.Game.Tests.Database importBeforeUpdate.PerformWrite(s => { - var beatmapCollection = s.Realm.Add(new BeatmapCollection("test collection")); + var beatmapCollection = s.Realm!.Add(new BeatmapCollection("test collection")); originalHash = s.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash; beatmapCollection.BeatmapMD5Hashes.Add(originalHash); @@ -540,7 +607,7 @@ namespace osu.Game.Tests.Database importAfterUpdate.PerformRead(updated => { - updated.Realm.Refresh(); + updated.Realm!.Refresh(); string[] hashes = updated.Realm.All().Single().BeatmapMD5Hashes.ToArray(); string updatedHash = updated.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash; diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index fd0b391d0d..b8073a65bc 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -128,7 +128,7 @@ namespace osu.Game.Tests.Database realm.RegisterCustomSubscription(r => { - var subscription = r.All().QueryAsyncWithNotifications((_, _, _) => + var subscription = r.All().QueryAsyncWithNotifications((_, _) => { realm.Run(_ => { diff --git a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs index e7fdb52d2f..5f722e381c 100644 --- a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs +++ b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs @@ -1,21 +1,23 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Linq; using NUnit.Framework; using osu.Framework.Platform; using osu.Framework.Testing; +using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.IO; +using osu.Game.Tests.Resources; namespace osu.Game.Tests.Database { [TestFixture] - public class LegacyBeatmapImporterTest + public class LegacyBeatmapImporterTest : RealmTest { private readonly TestLegacyBeatmapImporter importer = new TestLegacyBeatmapImporter(); @@ -42,17 +44,23 @@ namespace osu.Game.Tests.Database createFile(subdirectory2, Path.Combine("beatmap5", "beatmap.osu")); createFile(subdirectory2, Path.Combine("beatmap6", "beatmap.osu")); + // songs subdirectory with random file + var subdirectory3 = songsStorage.GetStorageForDirectory("subdirectory3"); + createFile(subdirectory3, "silly readme.txt"); + createFile(subdirectory3, Path.Combine("beatmap7", "beatmap.osu")); + // empty songs subdirectory songsStorage.GetStorageForDirectory("subdirectory3"); string[] paths = importer.GetStableImportPaths(songsStorage).ToArray(); - Assert.That(paths.Length, Is.EqualTo(6)); + Assert.That(paths.Length, Is.EqualTo(7)); Assert.That(paths.Contains(songsStorage.GetFullPath("beatmap1"))); Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory", "beatmap2")))); Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory", "beatmap3")))); Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory", "sub-subdirectory", "beatmap4")))); Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory2", "beatmap5")))); Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory2", "beatmap6")))); + Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory3", "beatmap7")))); } static void createFile(Storage storage, string path) @@ -62,6 +70,33 @@ namespace osu.Game.Tests.Database } } + [Test] + public void TestStableDateAddedApplied() + { + RunTestWithRealmAsync(async (realm, storage) => + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (var tmpStorage = new TemporaryNativeStorage("stable-songs-folder")) + { + var stableStorage = new StableStorage(tmpStorage.GetFullPath(""), host); + var songsStorage = stableStorage.GetStorageForDirectory(StableStorage.STABLE_DEFAULT_SONGS_PATH); + + ZipFile.ExtractToDirectory(TestResources.GetQuickTestBeatmapForImport(), songsStorage.GetFullPath("renatus")); + + string[] beatmaps = Directory.GetFiles(songsStorage.GetFullPath("renatus"), "*.osu", SearchOption.TopDirectoryOnly); + + File.SetLastWriteTimeUtc(beatmaps[beatmaps.Length / 2], new DateTime(2000, 1, 1, 12, 0, 0)); + + await new LegacyBeatmapImporter(new BeatmapImporter(storage, realm)).ImportFromStableAsync(stableStorage); + + var importedSet = realm.Realm.All().Single(); + + Assert.NotNull(importedSet); + Assert.AreEqual(new DateTimeOffset(new DateTime(2000, 1, 1, 12, 0, 0, DateTimeKind.Utc)), importedSet.DateAdded); + } + }); + } + private class TestLegacyBeatmapImporter : LegacyBeatmapImporter { public TestLegacyBeatmapImporter() diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index d853e75db0..cea30acf3f 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -355,7 +355,7 @@ namespace osu.Game.Tests.Database return null; }); - void gotChange(IRealmCollection sender, ChangeSet changes, Exception error) + void gotChange(IRealmCollection sender, ChangeSet? changes) { changesTriggered++; } diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs index 4ee302bbd0..45842a952a 100644 --- a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs +++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.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.Collections.Generic; using System.Linq; using System.Threading; @@ -54,7 +53,7 @@ namespace osu.Game.Tests.Database registration.Dispose(); }); - void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error) + void onChanged(IRealmCollection sender, ChangeSet? changes) { lastChanges = changes; @@ -92,7 +91,7 @@ namespace osu.Game.Tests.Database registration.Dispose(); }); - void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error) => lastChanges = changes; + void onChanged(IRealmCollection sender, ChangeSet? changes) => lastChanges = changes; } [Test] @@ -185,7 +184,7 @@ namespace osu.Game.Tests.Database } }); - void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error) + void onChanged(IRealmCollection sender, ChangeSet? changes) { if (changes == null) resolvedItems = sender; diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs index d2779e3038..2812a97268 100644 --- a/osu.Game.Tests/Database/RealmTest.cs +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -35,6 +35,7 @@ namespace osu.Game.Tests.Database Logger.Log($"Running test using realm file {testStorage.GetFullPath(realm.Filename)}"); testAction(realm, testStorage); + // ReSharper disable once DisposeOnUsingVariable realm.Dispose(); Logger.Log($"Final database size: {getFileSize(testStorage, realm)}"); @@ -58,6 +59,7 @@ namespace osu.Game.Tests.Database Logger.Log($"Running test using realm file {testStorage.GetFullPath(realm.Filename)}"); await testAction(realm, testStorage); + // ReSharper disable once DisposeOnUsingVariable realm.Dispose(); Logger.Log($"Final database size: {getFileSize(testStorage, realm)}"); diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs index a5662fa121..8b4c6e2411 100644 --- a/osu.Game.Tests/Database/RulesetStoreTests.cs +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -76,12 +76,12 @@ namespace osu.Game.Tests.Database Available = true, })); - Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.True); + Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True); // Availability is updated on construction of a RealmRulesetStore var _ = new RealmRulesetStore(realm, storage); - Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.False); + Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.False); }); } @@ -101,18 +101,18 @@ namespace osu.Game.Tests.Database Available = true, })); - Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.True); + Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True); // Availability is updated on construction of a RealmRulesetStore var _ = new RealmRulesetStore(realm, storage); - Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.False); + Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.False); // Simulate the ruleset getting updated LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION; var __ = new RealmRulesetStore(realm, storage); - Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.True); + Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True); }); } diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index f4467867db..e2774cef00 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Database realm.Run(innerRealm => { - var binding = innerRealm.ResolveReference(tsr); + var binding = innerRealm.ResolveReference(tsr)!; innerRealm.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace)); }); diff --git a/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs b/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs new file mode 100644 index 0000000000..28556566ba --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs @@ -0,0 +1,183 @@ +// 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.Timing; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Editing.Checks +{ + public class CheckBreaksTest + { + private CheckBreaks check = null!; + + [SetUp] + public void Setup() + { + check = new CheckBreaks(); + } + + [Test] + public void TestBreakTooShort() + { + var beatmap = new Beatmap + { + Breaks = new List + { + new BreakPeriod(0, 649) + } + }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateTooShort); + } + + [Test] + public void TestBreakStartsEarly() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 1_200 } + }, + Breaks = new List + { + new BreakPeriod(100, 751) + } + }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateEarlyStart); + } + + [Test] + public void TestBreakEndsLate() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 1_298 } + }, + Breaks = new List + { + new BreakPeriod(200, 850) + } + }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateLateEnd); + } + + [Test] + public void TestBreakAfterLastObjectStartsEarly() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 1200 } + }, + Breaks = new List + { + new BreakPeriod(1398, 2300) + } + }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateEarlyStart); + } + + [Test] + public void TestBreakBeforeFirstObjectEndsLate() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 1100 }, + new HitCircle { StartTime = 1500 } + }, + Breaks = new List + { + new BreakPeriod(0, 652) + } + }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateLateEnd); + } + + [Test] + public void TestBreakMultipleObjectsEarly() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 1_297 }, + new HitCircle { StartTime = 1_298 } + }, + Breaks = new List + { + new BreakPeriod(200, 850) + } + }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateLateEnd); + } + + [Test] + public void TestBreaksCorrect() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 1_300 } + }, + Breaks = new List + { + new BreakPeriod(200, 850) + } + }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Is.Empty); + } + } +} diff --git a/osu.Game.Tests/Editing/Checks/CheckDrainLengthTest.cs b/osu.Game.Tests/Editing/Checks/CheckDrainLengthTest.cs new file mode 100644 index 0000000000..1b5c5c398f --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckDrainLengthTest.cs @@ -0,0 +1,85 @@ +// 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.Timing; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Editing.Checks +{ + public class CheckDrainLengthTest + { + private CheckDrainLength check = null!; + + [SetUp] + public void Setup() + { + check = new CheckDrainLength(); + } + + [Test] + public void TestDrainTimeShort() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 29_999 } + } + }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckDrainLength.IssueTemplateTooShort); + } + + [Test] + public void TestDrainTimeBreak() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 40_000 } + }, + Breaks = new List + { + new BreakPeriod(10_000, 21_000) + } + }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckDrainLength.IssueTemplateTooShort); + } + + [Test] + public void TestDrainTimeCorrect() + { + var hitObjects = new List(); + + for (int i = 0; i <= 30; ++i) + hitObjects.Add(new HitCircle { StartTime = 1000 * i }); + + var beatmap = new Beatmap { HitObjects = hitObjects }; + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Is.Empty); + } + } +} diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index 6399507aa0..463287fb35 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -3,9 +3,9 @@ using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Editing [Cached(typeof(IBeatSnapProvider))] private readonly EditorBeatmap editorBeatmap; - protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; + protected override Container Content { get; } = new PopoverContainer { RelativeSizeAxes = Axes.Both }; public TestSceneHitObjectComposerDistanceSnapping() { @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Editing { assertSnapDistance(100, new Slider { - SliderVelocity = multiplier + SliderVelocityMultiplier = multiplier }, false); } @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Editing { assertSnapDistance(100 * multiplier, new Slider { - SliderVelocity = multiplier + SliderVelocityMultiplier = multiplier }, true); } @@ -111,7 +111,7 @@ namespace osu.Game.Tests.Editing var referenceObject = new Slider { - SliderVelocity = slider_velocity + SliderVelocityMultiplier = slider_velocity }; assertSnapDistance(base_distance * slider_velocity, referenceObject, true); @@ -229,25 +229,25 @@ namespace osu.Game.Tests.Editing } private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity) - => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(referenceObject ?? new HitObject(), includeSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistanceAt(referenceObject ?? new HitObject(), includeSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); private void assertDurationToDistance(double duration, float expectedDistance, HitObject? referenceObject = null) - => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(referenceObject ?? new HitObject(), duration), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DistanceSnapProvider.DurationToDistance(referenceObject ?? new HitObject(), duration), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); private void assertDistanceToDuration(float distance, double expectedDuration, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); private void assertSnappedDuration(float distance, double expectedDuration, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.FindSnappedDistance(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); private partial class TestHitObjectComposer : OsuHitObjectComposer { public new EditorBeatmap EditorBeatmap => base.EditorBeatmap; - public new Bindable DistanceSpacingMultiplier => base.DistanceSpacingMultiplier; + public new IDistanceSnapProvider DistanceSnapProvider => base.DistanceSnapProvider; public TestHitObjectComposer() : base(new OsuRuleset()) diff --git a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs index 04fc4cafbd..10dbede2e0 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; @@ -80,7 +81,9 @@ namespace osu.Game.Tests.Gameplay { TestLifetimeEntry entry = null; AddStep("Create entry", () => entry = new TestLifetimeEntry(new HitObject()) { LifetimeStart = 1 }); + assertJudged(() => entry, false); AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty())); + assertJudged(() => entry, false); AddAssert("Lifetime is updated", () => entry.LifetimeStart == -TestLifetimeEntry.INITIAL_LIFETIME_OFFSET); TestDrawableHitObject dho = null; @@ -91,6 +94,7 @@ namespace osu.Game.Tests.Gameplay }); AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty())); AddAssert("Lifetime is correct", () => dho.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY && entry.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY); + assertJudged(() => entry, false); } [Test] @@ -138,6 +142,29 @@ namespace osu.Game.Tests.Gameplay AddAssert("DHO state is correct", () => dho.State.Value == ArmedState.Miss); } + [Test] + public void TestJudgedStateThroughLifetime() + { + TestDrawableHitObject dho = null; + HitObjectLifetimeEntry lifetimeEntry = null; + + AddStep("Create lifetime entry", () => lifetimeEntry = new HitObjectLifetimeEntry(new HitObject { StartTime = Time.Current })); + + assertJudged(() => lifetimeEntry, false); + + AddStep("Create DHO and apply entry", () => + { + Child = dho = new TestDrawableHitObject(); + dho.Apply(lifetimeEntry); + }); + + assertJudged(() => lifetimeEntry, false); + + AddStep("Apply result", () => dho.MissForcefully()); + + assertJudged(() => lifetimeEntry, true); + } + [Test] public void TestResultSetBeforeLoadComplete() { @@ -154,15 +181,20 @@ namespace osu.Game.Tests.Gameplay } }; }); + assertJudged(() => lifetimeEntry, true); AddStep("Create DHO and apply entry", () => { dho = new TestDrawableHitObject(); dho.Apply(lifetimeEntry); Child = dho; }); + assertJudged(() => lifetimeEntry, true); AddAssert("DHO state is correct", () => dho.State.Value, () => Is.EqualTo(ArmedState.Hit)); } + private void assertJudged(Func entry, bool val) => + AddAssert(val ? "Is judged" : "Not judged", () => entry().Judged, () => Is.EqualTo(val)); + private partial class TestDrawableHitObject : DrawableHitObject { public const double INITIAL_LIFETIME_OFFSET = 100; diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs index 769ca1f9a9..d198ef5074 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.IO.Stores; using osu.Game.Rulesets; diff --git a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs index 393217f371..1368b42a3c 100644 --- a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs +++ b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs @@ -108,6 +108,28 @@ namespace osu.Game.Tests.Gameplay AddAssert("gameplay clock time = 10000", () => gameplayClockContainer.CurrentTime, () => Is.EqualTo(10000).Within(10f)); } + [Test] + public void TestStopUsingBeatmapClock() + { + ClockBackedTestWorkingBeatmap working = null; + MasterGameplayClockContainer gameplayClockContainer = null; + BindableDouble frequencyAdjustment = new BindableDouble(2); + + AddStep("create container", () => + { + working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio); + Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0); + + gameplayClockContainer.Reset(startClock: true); + }); + + AddStep("apply frequency adjustment", () => gameplayClockContainer.AdjustmentsFromMods.AddAdjustment(AdjustableProperty.Frequency, frequencyAdjustment)); + AddAssert("track frequency changed", () => working.Track.AggregateFrequency.Value, () => Is.EqualTo(2)); + + AddStep("stop using beatmap clock", () => gameplayClockContainer.StopUsingBeatmapClock()); + AddAssert("frequency adjustment unapplied", () => working.Track.AggregateFrequency.Value, () => Is.EqualTo(1)); + } + protected override void Dispose(bool isDisposing) { localConfig?.Dispose(); diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs index a261185473..1cf72cf937 100644 --- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 2cad7d33c2..4fb9db845b 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -126,7 +126,7 @@ namespace osu.Game.Tests.Gameplay public void TestSamplePlaybackWithBeatmapHitsoundsOff() { GameplayClockContainer gameplayContainer = null; - TestDrawableStoryboardSample sample = null; + DrawableStoryboardSample sample = null; AddStep("disable beatmap hitsounds", () => config.SetValue(OsuSetting.BeatmapHitsounds, false)); @@ -141,7 +141,7 @@ namespace osu.Game.Tests.Gameplay Child = beatmapSkinSourceContainer }); - beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) + beatmapSkinSourceContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) { Clock = gameplayContainer }); @@ -199,14 +199,6 @@ namespace osu.Game.Tests.Gameplay protected internal override ISkin GetSkin() => new TestSkin("test-sample", resources); } - private partial class TestDrawableStoryboardSample : DrawableStoryboardSample - { - public TestDrawableStoryboardSample(StoryboardSampleInfo sampleInfo) - : base(sampleInfo) - { - } - } - #region IResourceStorageProvider public IRenderer Renderer => host.Renderer; diff --git a/osu.Game.Tests/ImportTest.cs b/osu.Game.Tests/ImportTest.cs index 9e2c9cd7e0..27b8d3f21e 100644 --- a/osu.Game.Tests/ImportTest.cs +++ b/osu.Game.Tests/ImportTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Threading; using System.Threading.Tasks; @@ -11,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Game.Database; +using osu.Game.Online.API; using osu.Game.Tests.Resources; namespace osu.Game.Tests @@ -48,12 +47,15 @@ namespace osu.Game.Tests public partial class TestOsuGameBase : OsuGameBase { public RealmAccess Realm => Dependencies.Get(); + public new IAPIProvider API => base.API; private readonly bool withBeatmap; public TestOsuGameBase(bool withBeatmap) { this.withBeatmap = withBeatmap; + + base.API = new DummyAPIAccess(); } [BackgroundDependencyLoader] diff --git a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs index 3c296b2ff5..6b43ab83c5 100644 --- a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs +++ b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Configuration; @@ -18,7 +16,7 @@ namespace osu.Game.Tests.Input public partial class ConfineMouseTrackerTest : OsuGameTestScene { [Resolved] - private FrameworkConfigManager frameworkConfigManager { get; set; } + private FrameworkConfigManager frameworkConfigManager { get; set; } = null!; [TestCase(WindowMode.Windowed)] [TestCase(WindowMode.Borderless)] diff --git a/osu.Game.Tests/Input/RealmKeyBindingStoreTest.cs b/osu.Game.Tests/Input/RealmKeyBindingStoreTest.cs new file mode 100644 index 0000000000..ce31a9ea9d --- /dev/null +++ b/osu.Game.Tests/Input/RealmKeyBindingStoreTest.cs @@ -0,0 +1,117 @@ +// 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 NUnit.Framework; +using osu.Framework.Input.Bindings; +using osu.Game.Input; +using osu.Game.Input.Bindings; +using osuTK.Input; + +namespace osu.Game.Tests.Input +{ + [TestFixture] + public class RealmKeyBindingStoreTest + { + [Test] + public void TestBindingsWithoutDuplicatesAreNotModified() + { + var bindings = new List + { + new RealmKeyBinding(GlobalAction.Back, KeyCombination.FromKey(Key.Escape)), + new RealmKeyBinding(GlobalAction.Back, KeyCombination.FromMouseButton(MouseButton.Button1)), + new RealmKeyBinding(GlobalAction.MusicPrev, KeyCombination.FromKey(Key.F1)), + new RealmKeyBinding(GlobalAction.MusicNext, KeyCombination.FromKey(Key.F5)) + }; + + int countCleared = RealmKeyBindingStore.ClearDuplicateBindings(bindings); + + Assert.Multiple(() => + { + Assert.That(countCleared, Is.Zero); + + Assert.That(bindings[0].Action, Is.EqualTo((int)GlobalAction.Back)); + Assert.That(bindings[0].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.Escape))); + + Assert.That(bindings[1].Action, Is.EqualTo((int)GlobalAction.Back)); + Assert.That(bindings[1].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.ExtraMouseButton1))); + + Assert.That(bindings[2].Action, Is.EqualTo((int)GlobalAction.MusicPrev)); + Assert.That(bindings[2].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.F1))); + + Assert.That(bindings[3].Action, Is.EqualTo((int)GlobalAction.MusicNext)); + Assert.That(bindings[3].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.F5))); + }); + } + + [Test] + public void TestDuplicateBindingsAreCleared() + { + var bindings = new List + { + new RealmKeyBinding(GlobalAction.Back, KeyCombination.FromKey(Key.Escape)), + new RealmKeyBinding(GlobalAction.Back, KeyCombination.FromMouseButton(MouseButton.Button1)), + new RealmKeyBinding(GlobalAction.MusicPrev, KeyCombination.FromKey(Key.F1)), + new RealmKeyBinding(GlobalAction.IncreaseVolume, KeyCombination.FromKey(Key.Escape)), + new RealmKeyBinding(GlobalAction.MusicNext, KeyCombination.FromKey(Key.F5)), + new RealmKeyBinding(GlobalAction.ExportReplay, KeyCombination.FromKey(Key.F1)), + new RealmKeyBinding(GlobalAction.TakeScreenshot, KeyCombination.FromKey(Key.PrintScreen)), + }; + + int countCleared = RealmKeyBindingStore.ClearDuplicateBindings(bindings); + + Assert.Multiple(() => + { + Assert.That(countCleared, Is.EqualTo(4)); + + Assert.That(bindings[0].Action, Is.EqualTo((int)GlobalAction.Back)); + Assert.That(bindings[0].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.None))); + + Assert.That(bindings[1].Action, Is.EqualTo((int)GlobalAction.Back)); + Assert.That(bindings[1].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.ExtraMouseButton1))); + + Assert.That(bindings[2].Action, Is.EqualTo((int)GlobalAction.MusicPrev)); + Assert.That(bindings[2].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.None))); + + Assert.That(bindings[3].Action, Is.EqualTo((int)GlobalAction.IncreaseVolume)); + Assert.That(bindings[3].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.None))); + + Assert.That(bindings[4].Action, Is.EqualTo((int)GlobalAction.MusicNext)); + Assert.That(bindings[4].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.F5))); + + Assert.That(bindings[5].Action, Is.EqualTo((int)GlobalAction.ExportReplay)); + Assert.That(bindings[5].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.None))); + + Assert.That(bindings[6].Action, Is.EqualTo((int)GlobalAction.TakeScreenshot)); + Assert.That(bindings[6].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.PrintScreen))); + }); + } + + [Test] + public void TestDuplicateBindingsAllowedIfBoundToSameAction() + { + var bindings = new List + { + new RealmKeyBinding(GlobalAction.Back, KeyCombination.FromKey(Key.Escape)), + new RealmKeyBinding(GlobalAction.Back, KeyCombination.FromKey(Key.Escape)), + new RealmKeyBinding(GlobalAction.MusicPrev, KeyCombination.FromKey(Key.F1)), + }; + + int countCleared = RealmKeyBindingStore.ClearDuplicateBindings(bindings); + + Assert.Multiple(() => + { + Assert.That(countCleared, Is.EqualTo(0)); + + Assert.That(bindings[0].Action, Is.EqualTo((int)GlobalAction.Back)); + Assert.That(bindings[0].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.Escape))); + + Assert.That(bindings[1].Action, Is.EqualTo((int)GlobalAction.Back)); + Assert.That(bindings[1].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.Escape))); + + Assert.That(bindings[2].Action, Is.EqualTo((int)GlobalAction.MusicPrev)); + Assert.That(bindings[2].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.F1))); + }); + } + } +} diff --git a/osu.Game.Tests/Input/RealmKeyBindingTest.cs b/osu.Game.Tests/Input/RealmKeyBindingTest.cs new file mode 100644 index 0000000000..366d5ea825 --- /dev/null +++ b/osu.Game.Tests/Input/RealmKeyBindingTest.cs @@ -0,0 +1,48 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Input.Bindings; +using osu.Framework.Testing; +using osu.Game.Input.Bindings; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; +using osu.Game.Tests.Visual; +using osuTK.Input; + +namespace osu.Game.Tests.Input +{ + [HeadlessTest] + public partial class RealmKeyBindingTest : OsuTestScene + { + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + [Test] + public void TestUnmapGlobalAction() + { + var keyBinding = new RealmKeyBinding(GlobalAction.ToggleReplaySettings, KeyCombination.FromKey(Key.Z)); + + AddAssert("action is integer", () => keyBinding.Action, () => Is.EqualTo((int)GlobalAction.ToggleReplaySettings)); + AddAssert("action unmaps correctly", () => keyBinding.GetAction(rulesets), () => Is.EqualTo(GlobalAction.ToggleReplaySettings)); + } + + [TestCase(typeof(OsuRuleset), OsuAction.Smoke, null)] + [TestCase(typeof(TaikoRuleset), TaikoAction.LeftCentre, null)] + [TestCase(typeof(CatchRuleset), CatchAction.MoveRight, null)] + [TestCase(typeof(ManiaRuleset), ManiaAction.Key7, 7)] + public void TestUnmapRulesetActions(Type rulesetType, object action, int? variant) + { + string rulesetName = ((Ruleset)Activator.CreateInstance(rulesetType)!).ShortName; + var keyBinding = new RealmKeyBinding(action, KeyCombination.FromKey(Key.Z), rulesetName, variant); + + AddAssert("action is integer", () => keyBinding.Action, () => Is.EqualTo((int)action)); + AddAssert("action unmaps correctly", () => keyBinding.GetAction(rulesets), () => Is.EqualTo(action)); + } + } +} diff --git a/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs b/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs index d01eaca714..9926acf772 100644 --- a/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs +++ b/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.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 NUnit.Framework; using osu.Game.Beatmaps; diff --git a/osu.Game.Tests/Models/DisplayStringTest.cs b/osu.Game.Tests/Models/DisplayStringTest.cs index d585a0eb9f..b5303e1dd6 100644 --- a/osu.Game.Tests/Models/DisplayStringTest.cs +++ b/osu.Game.Tests/Models/DisplayStringTest.cs @@ -87,10 +87,10 @@ namespace osu.Game.Tests.Models var mock = new Mock(); mock.Setup(m => m.User).Returns(new APIUser { Username = "user" }); // TODO: temporary. - mock.Setup(m => m.Beatmap.Metadata.Artist).Returns("artist"); - mock.Setup(m => m.Beatmap.Metadata.Title).Returns("title"); - mock.Setup(m => m.Beatmap.Metadata.Author.Username).Returns("author"); - mock.Setup(m => m.Beatmap.DifficultyName).Returns("difficulty"); + mock.Setup(m => m.Beatmap!.Metadata.Artist).Returns("artist"); + mock.Setup(m => m.Beatmap!.Metadata.Title).Returns("title"); + mock.Setup(m => m.Beatmap!.Metadata.Author.Username).Returns("author"); + mock.Setup(m => m.Beatmap!.DifficultyName).Returns("difficulty"); Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("user playing artist - title (author) [difficulty]")); } diff --git a/osu.Game.Tests/Mods/SettingSourceAttributeTest.cs b/osu.Game.Tests/Mods/SettingSourceAttributeTest.cs index 5da303d3a7..b1d8b7e7fa 100644 --- a/osu.Game.Tests/Mods/SettingSourceAttributeTest.cs +++ b/osu.Game.Tests/Mods/SettingSourceAttributeTest.cs @@ -42,22 +42,22 @@ namespace osu.Game.Tests.Mods private class ClassWithSettings { [SettingSource("Unordered setting", "Should be last")] - public BindableFloat UnorderedSetting { get; set; } = new BindableFloat(); + public BindableFloat UnorderedSetting { get; } = new BindableFloat(); [SettingSource("Second setting", "Another description", 2)] - public BindableBool SecondSetting { get; set; } = new BindableBool(); + public BindableBool SecondSetting { get; } = new BindableBool(); [SettingSource("First setting", "A description", 1)] - public BindableDouble FirstSetting { get; set; } = new BindableDouble(); + public BindableDouble FirstSetting { get; } = new BindableDouble(); [SettingSource("Third setting", "Yet another description", 3)] - public BindableInt ThirdSetting { get; set; } = new BindableInt(); + public BindableInt ThirdSetting { get; } = new BindableInt(); } private class ClassWithCustomSettingControl { [SettingSource("Custom setting", "Should be a custom control", SettingControlType = typeof(CustomSettingsControl))] - public BindableInt UnorderedSetting { get; set; } = new BindableInt(); + public BindableInt UnorderedSetting { get; } = new BindableInt(); } private partial class CustomSettingsControl : SettingsItem diff --git a/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs b/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs index e7827a7398..0f5c13ca0e 100644 --- a/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs +++ b/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Utils; diff --git a/osu.Game.Tests/NonVisual/ClosestBeatDivisorTest.cs b/osu.Game.Tests/NonVisual/ClosestBeatDivisorTest.cs index 8ebf34b1ca..8a53759323 100644 --- a/osu.Game.Tests/NonVisual/ClosestBeatDivisorTest.cs +++ b/osu.Game.Tests/NonVisual/ClosestBeatDivisorTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Beatmaps; diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs index a2ded643fa..2d5d425ee8 100644 --- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs +++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Game.Beatmaps.ControlPoints; diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs index 6637d640b2..6b1b883ce7 100644 --- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.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.Collections.Generic; using System.Linq; diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs index 33204d33a7..c7a32ebbc4 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.NonVisual.Filtering Artist = "The Artist", ArtistUnicode = "check unicode too", Title = "Title goes here", - TitleUnicode = "Title goes here", + TitleUnicode = "TitleUnicode goes here", Author = { Username = "The Author" }, Source = "unit tests", Tags = "look for tags too", @@ -159,6 +159,34 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual(filtered, carouselItem.Filtered.Value); } + [Test] + [TestCase("\"artist\"", false)] + [TestCase("\"arti\"", true)] + [TestCase("\"artist title author\"", true)] + [TestCase("\"artist\" \"title\" \"author\"", false)] + [TestCase("\"an artist\"", true)] + [TestCase("\"tags too\"", false)] + [TestCase("\"tags to\"", true)] + [TestCase("\"version\"", false)] + [TestCase("\"an auteur\"", true)] + [TestCase("\"Artist\"!", true)] + [TestCase("\"The Artist\"!", false)] + [TestCase("\"the artist\"!", false)] + [TestCase("\"\\\"", true)] // nasty case, covers properly escaping user input in underlying regex. + public void TestCriteriaMatchingExactTerms(string terms, bool filtered) + { + var exampleBeatmapInfo = getExampleBeatmap(); + var criteria = new FilterCriteria + { + Ruleset = new RulesetInfo { OnlineID = 6 }, + AllowConvertedBeatmaps = true, + SearchText = terms + }; + var carouselItem = new CarouselBeatmap(exampleBeatmapInfo); + carouselItem.Filter(criteria); + Assert.AreEqual(filtered, carouselItem.Filtered.Value); + } + [Test] [TestCase("", false)] [TestCase("The", false)] @@ -179,6 +207,27 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual(filtered, carouselItem.Filtered.Value); } + [Test] + [TestCase("", false)] + [TestCase("Goes", false)] + [TestCase("GOES", false)] + [TestCase("goes", false)] + [TestCase("title goes", false)] + [TestCase("title goes AND then something else", true)] + [TestCase("titleunicode", false)] + [TestCase("unknown", true)] + public void TestCriteriaMatchingTitle(string titleName, bool filtered) + { + var exampleBeatmapInfo = getExampleBeatmap(); + var criteria = new FilterCriteria + { + Title = new FilterCriteria.OptionalTextFilter { SearchTerm = titleName } + }; + var carouselItem = new CarouselBeatmap(exampleBeatmapInfo); + carouselItem.Filter(criteria); + Assert.AreEqual(filtered, carouselItem.Filtered.Value); + } + [Test] [TestCase("", false)] [TestCase("The", false)] @@ -188,6 +237,9 @@ namespace osu.Game.Tests.NonVisual.Filtering [TestCase("the artist AND then something else", true)] [TestCase("unicode too", false)] [TestCase("unknown", true)] + [TestCase("\"Artist\"!", true)] + [TestCase("\"The Artist\"!", false)] + [TestCase("\"the artist\"!", false)] public void TestCriteriaMatchingArtist(string artistName, bool filtered) { var exampleBeatmapInfo = getExampleBeatmap(); diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index da32edb8fb..739a72df08 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -2,10 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Filter; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Filter; namespace osu.Game.Tests.NonVisual.Filtering @@ -23,6 +25,63 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual(4, filterCriteria.SearchTerms.Length); } + [Test] + public void TestApplyQueriesBareWordsWithExactMatch() + { + const string query = "looking for \"a beatmap\"! like \"this\""; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("looking for \"a beatmap\"! like \"this\"", filterCriteria.SearchText); + Assert.AreEqual(5, filterCriteria.SearchTerms.Length); + + Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("a beatmap")); + Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.FullPhrase)); + + Assert.That(filterCriteria.SearchTerms[1].SearchTerm, Is.EqualTo("this")); + Assert.That(filterCriteria.SearchTerms[1].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase)); + + Assert.That(filterCriteria.SearchTerms[2].SearchTerm, Is.EqualTo("looking")); + Assert.That(filterCriteria.SearchTerms[2].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring)); + + Assert.That(filterCriteria.SearchTerms[3].SearchTerm, Is.EqualTo("for")); + Assert.That(filterCriteria.SearchTerms[3].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring)); + + Assert.That(filterCriteria.SearchTerms[4].SearchTerm, Is.EqualTo("like")); + Assert.That(filterCriteria.SearchTerms[4].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring)); + } + + [Test] + public void TestApplyFullPhraseQueryWithExclamationPointInTerm() + { + const string query = "looking for \"circles!\"!"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("looking for \"circles!\"!", filterCriteria.SearchText); + Assert.AreEqual(3, filterCriteria.SearchTerms.Length); + + Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("circles!")); + Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.FullPhrase)); + + Assert.That(filterCriteria.SearchTerms[1].SearchTerm, Is.EqualTo("looking")); + Assert.That(filterCriteria.SearchTerms[1].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring)); + + Assert.That(filterCriteria.SearchTerms[2].SearchTerm, Is.EqualTo("for")); + Assert.That(filterCriteria.SearchTerms[2].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring)); + } + + [Test] + public void TestApplyBrokenFullPhraseQuery() + { + const string query = "\"!"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("\"!", filterCriteria.SearchText); + Assert.AreEqual(1, filterCriteria.SearchTerms.Length); + + Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("!")); + Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase)); + } + /* * The following tests have been written a bit strangely (they don't check exact * bound equality with what the filter says). @@ -226,6 +285,18 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual("my_fav", filterCriteria.Creator.SearchTerm); } + [Test] + public void TestApplyTitleQueries() + { + const string query = "find me songs with title=\"a certain title\" please"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("find me songs with please", filterCriteria.SearchText.Trim()); + Assert.AreEqual(5, filterCriteria.SearchTerms.Length); + Assert.AreEqual("a certain title", filterCriteria.Title.SearchTerm); + Assert.That(filterCriteria.Title.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase)); + } + [Test] public void TestApplyArtistQueries() { @@ -235,6 +306,7 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual("find me songs by please", filterCriteria.SearchText.Trim()); Assert.AreEqual(5, filterCriteria.SearchTerms.Length); Assert.AreEqual("singer", filterCriteria.Artist.SearchTerm); + Assert.That(filterCriteria.Artist.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring)); } [Test] @@ -246,6 +318,19 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual("really like yes", filterCriteria.SearchText.Trim()); Assert.AreEqual(3, filterCriteria.SearchTerms.Length); Assert.AreEqual("name with space", filterCriteria.Artist.SearchTerm); + Assert.That(filterCriteria.Artist.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase)); + } + + [Test] + public void TestApplyArtistQueriesWithSpacesFullPhrase() + { + const string query = "artist=\"The Only One\"!"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.That(filterCriteria.SearchText.Trim(), Is.Empty); + Assert.AreEqual(0, filterCriteria.SearchTerms.Length); + Assert.AreEqual("The Only One", filterCriteria.Artist.SearchTerm); + Assert.That(filterCriteria.Artist.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.FullPhrase)); } [Test] @@ -299,6 +384,57 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual("unrecognised=keyword", filterCriteria.SearchText.Trim()); } + [TestCase("[1]", new[] { 0 })] + [TestCase("[1", new[] { 0 })] + [TestCase("My[Favourite", new[] { 2 })] + [TestCase("My[Favourite]", new[] { 2 })] + [TestCase("My[Favourite]Song", new[] { 2 })] + [TestCase("Favourite]", new[] { 2 })] + [TestCase("[Diff", new[] { 0, 1, 3, 4, 6 })] + [TestCase("[Diff]", new[] { 0, 1, 3, 4, 6 })] + [TestCase("[Favourite]", new[] { 3 })] + [TestCase("Title1 [Diff]", new[] { 0, 1 })] + [TestCase("Title1[Diff]", new int[] { })] + [TestCase("[diff ]with]", new[] { 4 })] + [TestCase("[diff ]with [[ brackets]]]]", new[] { 4 })] + [TestCase("[Diff in title]", new int[] { })] + [TestCase("[Diff in diff]", new[] { 6 })] + [TestCase("diff=Diff", new[] { 0, 1, 3, 4, 6 })] + [TestCase("diff=Diff1", new[] { 0 })] + [TestCase("diff=\"Diff\"", new[] { 3, 4, 6 })] + [TestCase("diff=!\"Diff\"", new int[] { })] + public void TestDifficultySearch(string query, int[] expectedBeatmapIndexes) + { + var carouselBeatmaps = (((string title, string difficultyName)[])new[] + { + ("Title1", "Diff1"), + ("Title1", "Diff2"), + ("My[Favourite]Song", "Expert"), + ("Title", "My Favourite Diff"), + ("Another One", "diff ]with [[ brackets]]]"), + ("Diff in title", "a"), + ("a", "Diff in diff"), + }).Select(info => new CarouselBeatmap(new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Title = info.title + }, + DifficultyName = info.difficultyName + })).ToList(); + + var criteria = new FilterCriteria(); + + FilterQueryParser.ApplyQueries(criteria, query); + carouselBeatmaps.ForEach(b => b.Filter(criteria)); + + int[] visibleBeatmaps = carouselBeatmaps + .Where(b => !b.Filtered.Value) + .Select(b => carouselBeatmaps.IndexOf(b)).ToArray(); + + Assert.That(visibleBeatmaps, Is.EqualTo(expectedBeatmapIndexes)); + } + private class CustomFilterCriteria : IRulesetFilterCriteria { public string? CustomValue { get; set; } diff --git a/osu.Game.Tests/NonVisual/FormatUtilsTest.cs b/osu.Game.Tests/NonVisual/FormatUtilsTest.cs index 4d2fc53bc3..a12658bd8b 100644 --- a/osu.Game.Tests/NonVisual/FormatUtilsTest.cs +++ b/osu.Game.Tests/NonVisual/FormatUtilsTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Utils; diff --git a/osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs b/osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs index d67a3cb824..07de4d1a01 100644 --- a/osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs +++ b/osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Audio; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Timing; using osu.Game.Screens.Play; @@ -16,16 +17,16 @@ namespace osu.Game.Tests.NonVisual [TestCase(1)] public void TestTrueGameplayRateWithGameplayAdjustment(double underlyingClockRate) { - var framedClock = new FramedClock(new ManualClock { Rate = underlyingClockRate }); - var gameplayClock = new TestGameplayClockContainer(framedClock); + var trackVirtual = new TrackVirtual(60000) { Frequency = { Value = underlyingClockRate } }; + var gameplayClock = new TestGameplayClockContainer(trackVirtual); Assert.That(gameplayClock.GetTrueGameplayRate(), Is.EqualTo(2)); } private partial class TestGameplayClockContainer : GameplayClockContainer { - public TestGameplayClockContainer(IFrameBasedClock underlyingClock) - : base(underlyingClock) + public TestGameplayClockContainer(IClock underlyingClock) + : base(underlyingClock, false, false) { AdjustmentsFromMods.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(2.0)); } diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index ae6a76f6cd..b4bbe274a5 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Humanizer; using NUnit.Framework; diff --git a/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs b/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs index b589f7c9f1..664a499cc3 100644 --- a/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs +++ b/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; diff --git a/osu.Game.Tests/NonVisual/RulesetInfoOrderingTest.cs b/osu.Game.Tests/NonVisual/RulesetInfoOrderingTest.cs index 8654abd49d..335e7d25a2 100644 --- a/osu.Game.Tests/NonVisual/RulesetInfoOrderingTest.cs +++ b/osu.Game.Tests/NonVisual/RulesetInfoOrderingTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Game.Rulesets; diff --git a/osu.Game.Tests/NonVisual/ScoreInfoTest.cs b/osu.Game.Tests/NonVisual/ScoreInfoTest.cs index dcc4f91dba..ad3b5b6f66 100644 --- a/osu.Game.Tests/NonVisual/ScoreInfoTest.cs +++ b/osu.Game.Tests/NonVisual/ScoreInfoTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Online.API; using osu.Game.Rulesets.Mania; diff --git a/osu.Game.Tests/NonVisual/TimeDisplayExtensionTest.cs b/osu.Game.Tests/NonVisual/TimeDisplayExtensionTest.cs index 861e342cdb..10d592364d 100644 --- a/osu.Game.Tests/NonVisual/TimeDisplayExtensionTest.cs +++ b/osu.Game.Tests/NonVisual/TimeDisplayExtensionTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Extensions; diff --git a/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs b/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs index 13f1ed5c57..e4118a23b4 100644 --- a/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs +++ b/osu.Game.Tests/Online/Chat/MessageNotifierTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Online.Chat; diff --git a/osu.Game.Tests/Online/TestMultiplayerMessagePackSerialization.cs b/osu.Game.Tests/Online/TestMultiplayerMessagePackSerialization.cs index aea579a82d..c440f375fd 100644 --- a/osu.Game.Tests/Online/TestMultiplayerMessagePackSerialization.cs +++ b/osu.Game.Tests/Online/TestMultiplayerMessagePackSerialization.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 MessagePack; using NUnit.Framework; using osu.Game.Online; diff --git a/osu.Game.Tests/Online/TestSoloScoreInfoJsonSerialization.cs b/osu.Game.Tests/Online/TestSoloScoreInfoJsonSerialization.cs index 8ff0b67b5b..19bc96c677 100644 --- a/osu.Game.Tests/Online/TestSoloScoreInfoJsonSerialization.cs +++ b/osu.Game.Tests/Online/TestSoloScoreInfoJsonSerialization.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Newtonsoft.Json; using NUnit.Framework; using osu.Game.IO.Serialization; diff --git a/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs b/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs index 73ed2bb868..274681b413 100644 --- a/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs +++ b/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs index 1d568a9dc2..7b0b211899 100644 --- a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs @@ -15,6 +15,9 @@ using osu.Game.Tests.Visual; namespace osu.Game.Tests.OnlinePlay { + // NOTE: This test scene never calls ProcessFrame on clocks. + // The current tests are fine without this as they are testing very static scenarios, but it's worth knowing + // if adding further tests to this class. [HeadlessTest] public partial class TestSceneCatchUpSyncManager : OsuTestScene { @@ -28,7 +31,7 @@ namespace osu.Game.Tests.OnlinePlay [SetUp] public void Setup() { - syncManager = new SpectatorSyncManager(master = new GameplayClockContainer(new TestManualClock())); + syncManager = new SpectatorSyncManager(master = new GameplayClockContainer(new TestManualClock(), false, false)); player1 = syncManager.CreateManagedClock(); player2 = syncManager.CreateManagedClock(); @@ -188,6 +191,8 @@ namespace osu.Game.Tests.OnlinePlay public void Reset() { + IsRunning = false; + CurrentTime = 0; } public void ResetSpeedAdjustments() diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-pro-20230618.osk b/osu.Game.Tests/Resources/Archives/modified-argon-pro-20230618.osk new file mode 100644 index 0000000000..dd25e06c06 Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/modified-argon-pro-20230618.osk differ diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-pro-20231001.osk b/osu.Game.Tests/Resources/Archives/modified-argon-pro-20231001.osk new file mode 100644 index 0000000000..081bb73b9e Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/modified-argon-pro-20231001.osk differ diff --git a/osu.Game.Tests/Resources/Replays/taiko-replay-with-legacy-online-id.osr b/osu.Game.Tests/Resources/Replays/taiko-replay-with-legacy-online-id.osr new file mode 100644 index 0000000000..85ad28c7ca Binary files /dev/null and b/osu.Game.Tests/Resources/Replays/taiko-replay-with-legacy-online-id.osr differ diff --git a/osu.Game.Tests/Resources/Replays/taiko-replay-with-new-online-id.osr b/osu.Game.Tests/Resources/Replays/taiko-replay-with-new-online-id.osr new file mode 100644 index 0000000000..63e05f5fcd Binary files /dev/null and b/osu.Game.Tests/Resources/Replays/taiko-replay-with-new-online-id.osr differ diff --git a/osu.Game.Tests/Resources/beatmap-version-4.osu b/osu.Game.Tests/Resources/beatmap-version-4.osu new file mode 100644 index 0000000000..bba4ed46f1 --- /dev/null +++ b/osu.Game.Tests/Resources/beatmap-version-4.osu @@ -0,0 +1,4 @@ +osu file format v4 + +[General] +PreviewTime: -1 diff --git a/osu.Game.Tests/Resources/beatmap-version.osu b/osu.Game.Tests/Resources/beatmap-version-6.osu similarity index 100% rename from osu.Game.Tests/Resources/beatmap-version.osu rename to osu.Game.Tests/Resources/beatmap-version-6.osu diff --git a/osu.Game.Tests/Resources/invalid-bank.osu b/osu.Game.Tests/Resources/invalid-bank.osu new file mode 100644 index 0000000000..8c554cc17f --- /dev/null +++ b/osu.Game.Tests/Resources/invalid-bank.osu @@ -0,0 +1,19 @@ +osu file format v14 + +[General] +SampleSet: Normal + +[TimingPoints] +0,500,4,3,0,100,1,0 + +[HitObjects] +256,192,1000,5,0,0:0:0:0: +256,192,2000,1,0,1:0:0:0: +256,192,3000,1,0,2:0:0:0: +256,192,4000,1,0,3:0:0:0: +256,192,5000,1,0,42:0:0:0: +256,192,6000,5,4,0:0:0:0: +256,192,7000,1,4,0:1:0:0: +256,192,8000,1,4,0:2:0:0: +256,192,9000,1,4,0:3:0:0: +256,192,10000,1,4,0:42:0:0: diff --git a/osu.Game.Tests/Resources/sample-point-leniency.osu b/osu.Game.Tests/Resources/sample-point-leniency.osu new file mode 100644 index 0000000000..a44838007c --- /dev/null +++ b/osu.Game.Tests/Resources/sample-point-leniency.osu @@ -0,0 +1,10 @@ +osu file format v14 + +# extracted from https://osu.ppy.sh/beatmapsets/1859679#osu/3823636 + +[TimingPoints] +39166,-117.647058823529,4,2,1,5,0,0 +39262,-117.647058823529,4,2,1,70,0,0 + +[HitObjects] +440,70,39260,1,10,0:2:0:0: diff --git a/osu.Game.Tests/Resources/video-background-events-ignored.osb b/osu.Game.Tests/Resources/video-background-events-ignored.osb new file mode 100644 index 0000000000..7525b1fee9 --- /dev/null +++ b/osu.Game.Tests/Resources/video-background-events-ignored.osb @@ -0,0 +1,5 @@ +osu file format v14 + +[Events] +0,-1234,"BG.jpg",0,0 +Video,-5678,"Video.avi",0,0 \ No newline at end of file diff --git a/osu.Game.Tests/Rulesets/Scoring/HitResultTest.cs b/osu.Game.Tests/Rulesets/Scoring/HitResultTest.cs new file mode 100644 index 0000000000..68d7335055 --- /dev/null +++ b/osu.Game.Tests/Rulesets/Scoring/HitResultTest.cs @@ -0,0 +1,36 @@ +// 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.Linq; +using NUnit.Framework; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Tests.Rulesets.Scoring +{ + [TestFixture] + public class HitResultTest + { + [TestCase(new[] { HitResult.Perfect, HitResult.Great, HitResult.Good, HitResult.Ok, HitResult.Meh }, new[] { HitResult.Miss })] + [TestCase(new[] { HitResult.LargeTickHit }, new[] { HitResult.LargeTickMiss })] + [TestCase(new[] { HitResult.SmallTickHit }, new[] { HitResult.SmallTickMiss })] + [TestCase(new[] { HitResult.LargeBonus, HitResult.SmallBonus }, new[] { HitResult.IgnoreMiss })] + [TestCase(new[] { HitResult.IgnoreHit }, new[] { HitResult.IgnoreMiss, HitResult.ComboBreak })] + public void TestValidResultPairs(HitResult[] maxResults, HitResult[] minResults) + { + HitResult[] unsupportedResults = HitResultExtensions.ALL_TYPES.Where(t => !minResults.Contains(t)).ToArray(); + + Assert.Multiple(() => + { + foreach (var max in maxResults) + { + foreach (var min in minResults) + Assert.DoesNotThrow(() => HitResultExtensions.ValidateHitResultPair(max, min), $"{max} + {min} should be supported."); + + foreach (var unsupported in unsupportedResults) + Assert.Throws(() => HitResultExtensions.ValidateHitResultPair(max, unsupported), $"{max} + {unsupported} should not be supported."); + } + }); + } + } +} diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index e5e96d2033..cba90b2ebe 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -45,9 +45,9 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Standardised, HitResult.Meh, 116_667)] [TestCase(ScoringMode.Standardised, HitResult.Ok, 233_338)] [TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)] - [TestCase(ScoringMode.Classic, HitResult.Meh, 0)] - [TestCase(ScoringMode.Classic, HitResult.Ok, 2)] - [TestCase(ScoringMode.Classic, HitResult.Great, 36)] + [TestCase(ScoringMode.Classic, HitResult.Meh, 11_670)] + [TestCase(ScoringMode.Classic, HitResult.Ok, 23_341)] + [TestCase(ScoringMode.Classic, HitResult.Great, 100_033)] public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore) { scoreProcessor.ApplyBeatmap(beatmap); @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] [TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 79_333)] [TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 158_667)] - [TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 302_402)] + [TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 317_626)] [TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 492_894)] [TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 492_894)] [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] @@ -84,17 +84,17 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)] [TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)] [TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] - [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 4)] - [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 15)] - [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 53)] - [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 140)] - [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 140)] + [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 7_975)] + [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 15_949)] + [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 31_928)] + [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 49_546)] + [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 49_546)] [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] - [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 11)] + [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 54_189)] [TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] - [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 9)] - [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 36)] - [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 36)] + [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 49_289)] + [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 100_003)] + [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 100_015)] public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore) { var minResult = new TestJudgement(hitResult).MinResult; @@ -107,7 +107,7 @@ namespace osu.Game.Tests.Rulesets.Scoring for (int i = 0; i < 4; i++) { - var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], new TestJudgement(maxResult)) + var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], fourObjectBeatmap.HitObjects[i].CreateJudgement()) { Type = i == 2 ? minResult : hitResult }; @@ -259,6 +259,41 @@ namespace osu.Game.Tests.Rulesets.Scoring } #pragma warning restore CS0618 + [Test] + public void TestComboBreak() + { + Assert.That(HitResult.ComboBreak.IncreasesCombo(), Is.False); + Assert.That(HitResult.ComboBreak.BreaksCombo(), Is.True); + Assert.That(HitResult.ComboBreak.AffectsCombo(), Is.True); + Assert.That(HitResult.ComboBreak.AffectsAccuracy(), Is.False); + Assert.That(HitResult.ComboBreak.IsBasic(), Is.False); + Assert.That(HitResult.ComboBreak.IsTick(), Is.False); + Assert.That(HitResult.ComboBreak.IsBonus(), Is.False); + Assert.That(HitResult.ComboBreak.IsHit(), Is.False); + Assert.That(HitResult.ComboBreak.IsScorable(), Is.True); + Assert.That(HitResultExtensions.ALL_TYPES, Does.Contain(HitResult.ComboBreak)); + + beatmap = new TestBeatmap(new RulesetInfo()) + { + HitObjects = new List + { + new TestHitObject(HitResult.Great), + new TestHitObject(HitResult.IgnoreHit, HitResult.ComboBreak), + } + }; + + scoreProcessor = new TestScoreProcessor(); + scoreProcessor.ApplyBeatmap(beatmap); + + scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].CreateJudgement()) { Type = HitResult.Great }); + Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1)); + Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1)); + + scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].CreateJudgement()) { Type = HitResult.ComboBreak }); + Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0)); + Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1)); + } + [Test] public void TestAccuracyWhenNearPerfect() { @@ -275,7 +310,7 @@ namespace osu.Game.Tests.Rulesets.Scoring for (int i = 0; i < beatmap.HitObjects.Count; i++) { - scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[i], new TestJudgement(HitResult.Great)) + scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[i], beatmap.HitObjects[i].CreateJudgement()) { Type = i == 0 ? HitResult.Miss : HitResult.Great }); @@ -293,24 +328,31 @@ namespace osu.Game.Tests.Rulesets.Scoring { public override HitResult MaxResult { get; } - public TestJudgement(HitResult maxResult) + public override HitResult MinResult => minResult ?? base.MinResult; + + private readonly HitResult? minResult; + + public TestJudgement(HitResult maxResult, HitResult? minResult = null) { MaxResult = maxResult; + this.minResult = minResult; } } private class TestHitObject : HitObject { private readonly HitResult maxResult; + private readonly HitResult? minResult; public override Judgement CreateJudgement() { - return new TestJudgement(maxResult); + return new TestJudgement(maxResult, minResult); } - public TestHitObject(HitResult maxResult) + public TestHitObject(HitResult maxResult, HitResult? minResult = null) { this.maxResult = maxResult; + this.minResult = minResult; } } diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 892ceea185..dd724d268e 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -11,7 +11,10 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.IO.Archives; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -67,6 +70,116 @@ namespace osu.Game.Tests.Scores.IO } } + [TestCase(false)] + [TestCase(true)] + public void TestLastPlayedUpdate(bool isLocalUser) + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = LoadOsuIntoHost(host, true); + + if (!isLocalUser) + osu.API.Logout(); + + var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely(); + var beatmapInfo = beatmap.Beatmaps.First(); + + DateTimeOffset replayDate = DateTimeOffset.Now; + + var toImport = new ScoreInfo + { + Rank = ScoreRank.B, + TotalScore = 987654, + Accuracy = 0.8, + MaxCombo = 500, + Combo = 250, + User = new APIUser + { + Username = "Test user", + Id = DummyAPIAccess.DUMMY_USER_ID, + }, + Date = replayDate, + OnlineID = 12345, + Ruleset = new OsuRuleset().RulesetInfo, + BeatmapInfo = beatmapInfo + }; + + var imported = LoadScoreIntoOsu(osu, toImport); + + Assert.AreEqual(toImport.Rank, imported.Rank); + Assert.AreEqual(toImport.TotalScore, imported.TotalScore); + Assert.AreEqual(toImport.Accuracy, imported.Accuracy); + Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo); + Assert.AreEqual(toImport.User.Username, imported.User.Username); + Assert.AreEqual(toImport.Date, imported.Date); + Assert.AreEqual(toImport.OnlineID, imported.OnlineID); + + if (isLocalUser) + Assert.That(imported.BeatmapInfo!.LastPlayed, Is.EqualTo(replayDate)); + else + Assert.That(imported.BeatmapInfo!.LastPlayed, Is.Null); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public void TestLastPlayedNotUpdatedDueToNewerPlays() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = LoadOsuIntoHost(host, true); + + var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely(); + var beatmapInfo = beatmap.Beatmaps.First(); + + var realmAccess = osu.Dependencies.Get(); + realmAccess.Write(r => r.Find(beatmapInfo.ID)!.LastPlayed = new DateTimeOffset(2023, 10, 30, 0, 0, 0, TimeSpan.Zero)); + + var toImport = new ScoreInfo + { + Rank = ScoreRank.B, + TotalScore = 987654, + Accuracy = 0.8, + MaxCombo = 500, + Combo = 250, + User = new APIUser + { + Username = "Test user", + Id = DummyAPIAccess.DUMMY_USER_ID, + }, + Date = new DateTimeOffset(2023, 10, 27, 0, 0, 0, TimeSpan.Zero), + OnlineID = 12345, + Ruleset = new OsuRuleset().RulesetInfo, + BeatmapInfo = beatmapInfo + }; + + var imported = LoadScoreIntoOsu(osu, toImport); + + Assert.AreEqual(toImport.Rank, imported.Rank); + Assert.AreEqual(toImport.TotalScore, imported.TotalScore); + Assert.AreEqual(toImport.Accuracy, imported.Accuracy); + Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo); + Assert.AreEqual(toImport.User.Username, imported.User.Username); + Assert.AreEqual(toImport.Date, imported.Date); + Assert.AreEqual(toImport.OnlineID, imported.OnlineID); + + Assert.That(imported.BeatmapInfo!.LastPlayed, Is.EqualTo(new DateTimeOffset(2023, 10, 30, 0, 0, 0, TimeSpan.Zero))); + } + finally + { + host.Exit(); + } + } + } + [Test] public void TestImportMods() { diff --git a/osu.Game.Tests/Scores/IO/TestScoreEquality.cs b/osu.Game.Tests/Scores/IO/TestScoreEquality.cs index d44fd786d7..5aa07260ef 100644 --- a/osu.Game.Tests/Scores/IO/TestScoreEquality.cs +++ b/osu.Game.Tests/Scores/IO/TestScoreEquality.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Scoring; diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index 0c25934d52..ab3e099c3a 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.IO; using System.Linq; @@ -29,7 +27,7 @@ namespace osu.Game.Tests.Skins.IO var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk")); // When the import filename doesn't match, it should be appended (and update the skin.ini). - assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu); + assertCorrectMetadata(import1, "test skin [skin]", "skinner", 1.0m, osu); }); [Test] @@ -38,7 +36,7 @@ namespace osu.Game.Tests.Skins.IO var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner", iniFilename: "Skin.InI"), "skin.osk")); // When the import filename doesn't match, it should be appended (and update the skin.ini). - assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu); + assertCorrectMetadata(import1, "test skin [skin]", "skinner", 1.0m, osu); }); [Test] @@ -47,7 +45,7 @@ namespace osu.Game.Tests.Skins.IO var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner", includeSectionHeader: false), "skin.osk")); // When the import filename doesn't match, it should be appended (and update the skin.ini). - assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu); + assertCorrectMetadata(import1, "test skin [skin]", "skinner", 1.0m, osu); }); [Test] @@ -56,7 +54,7 @@ namespace osu.Game.Tests.Skins.IO var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "test skin.osk")); // When the import filename matches it shouldn't be appended. - assertCorrectMetadata(import1, "test skin", "skinner", osu); + assertCorrectMetadata(import1, "test skin", "skinner", 1.0m, osu); }); [Test] @@ -65,7 +63,7 @@ namespace osu.Game.Tests.Skins.IO var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithNonIniFile(), "test skin.osk")); // When the import filename matches it shouldn't be appended. - assertCorrectMetadata(import1, "test skin", "Unknown", osu); + assertCorrectMetadata(import1, "test skin", "Unknown", SkinConfiguration.LATEST_VERSION, osu); }); [Test] @@ -74,7 +72,7 @@ namespace osu.Game.Tests.Skins.IO var import1 = await loadSkinIntoOsu(osu, new ImportTask(createEmptyOsk(), "test skin.osk")); // When the import filename matches it shouldn't be appended. - assertCorrectMetadata(import1, "test skin", "Unknown", osu); + assertCorrectMetadata(import1, "test skin", "Unknown", SkinConfiguration.LATEST_VERSION, osu); }); #endregion @@ -104,7 +102,7 @@ namespace osu.Game.Tests.Skins.IO public Task TestImportUpperCasedOskArchive() => runSkinTest(async osu => { var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "name 1.OsK")); - assertCorrectMetadata(import1, "name 1", "author 1", osu); + assertCorrectMetadata(import1, "name 1", "author 1", 1.0m, osu); var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "name 1.oSK")); @@ -117,14 +115,14 @@ namespace osu.Game.Tests.Skins.IO MemoryStream exportStream = new MemoryStream(); var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "custom.osk")); - assertCorrectMetadata(import1, "name 1 [custom]", "author 1", osu); + assertCorrectMetadata(import1, "name 1 [custom]", "author 1", 1.0m, osu); await new LegacySkinExporter(osu.Dependencies.Get()).ExportToStreamAsync(import1, exportStream); string exportFilename = import1.GetDisplayString(); var import2 = await loadSkinIntoOsu(osu, new ImportTask(exportStream, $"{exportFilename}.osk")); - assertCorrectMetadata(import2, "name 1 [custom]", "author 1", osu); + assertCorrectMetadata(import2, "name 1 [custom]", "author 1", 1.0m, osu); assertImportedOnce(import1, import2); }); @@ -135,14 +133,14 @@ namespace osu.Game.Tests.Skins.IO MemoryStream exportStream = new MemoryStream(); var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 『1』", "author 1"), "custom.osk")); - assertCorrectMetadata(import1, "name 『1』 [custom]", "author 1", osu); + assertCorrectMetadata(import1, "name 『1』 [custom]", "author 1", 1.0m, osu); await new LegacySkinExporter(osu.Dependencies.Get()).ExportToStreamAsync(import1, exportStream); string exportFilename = import1.GetDisplayString().GetValidFilename(); var import2 = await loadSkinIntoOsu(osu, new ImportTask(exportStream, $"{exportFilename}.osk")); - assertCorrectMetadata(import2, "name 『1』 [custom]", "author 1", osu); + assertCorrectMetadata(import2, "name 『1』 [custom]", "author 1", 1.0m, osu); }); [Test] @@ -152,7 +150,7 @@ namespace osu.Game.Tests.Skins.IO var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 1"), batchImport); assertImportedOnce(import1, import2); - assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", osu); + assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", 1.0m, osu); }); #endregion @@ -185,8 +183,8 @@ namespace osu.Game.Tests.Skins.IO var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin v2.1", "skinner"), "skin.osk")); assertImportedBoth(import1, import2); - assertCorrectMetadata(import1, "test skin v2 [skin]", "skinner", osu); - assertCorrectMetadata(import2, "test skin v2.1 [skin]", "skinner", osu); + assertCorrectMetadata(import1, "test skin v2 [skin]", "skinner", 1.0m, osu); + assertCorrectMetadata(import2, "test skin v2.1 [skin]", "skinner", 1.0m, osu); }); [Test] @@ -196,8 +194,8 @@ namespace osu.Game.Tests.Skins.IO var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 2")); assertImportedBoth(import1, import2); - assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", osu); - assertCorrectMetadata(import2, "name 1 [my custom skin 2]", "author 1", osu); + assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", 1.0m, osu); + assertCorrectMetadata(import2, "name 1 [my custom skin 2]", "author 1", 1.0m, osu); }); [Test] @@ -266,7 +264,7 @@ namespace osu.Game.Tests.Skins.IO #endregion - private void assertCorrectMetadata(Live import1, string name, string creator, OsuGameBase osu) + private void assertCorrectMetadata(Live import1, string name, string creator, decimal version, OsuGameBase osu) { import1.PerformRead(i => { @@ -278,6 +276,7 @@ namespace osu.Game.Tests.Skins.IO Assert.That(instance.Configuration.SkinInfo.Name, Is.EqualTo(name)); Assert.That(instance.Configuration.SkinInfo.Creator, Is.EqualTo(creator)); + Assert.That(instance.Configuration.LegacyVersion, Is.EqualTo(version)); }); } diff --git a/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs index 6da335a9b7..b96bf09255 100644 --- a/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs +++ b/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.IO; using osu.Game.Skinning; diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index bd8088cfb6..98008a003d 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -51,6 +51,10 @@ namespace osu.Game.Tests.Skins "Archives/modified-default-20230117.osk", // Covers player avatar and flag. "Archives/modified-argon-20230305.osk", + // Covers key counters + "Archives/modified-argon-pro-20230618.osk", + // Covers "Argon" health display + "Archives/modified-argon-pro-20231001.osk" }; /// diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 5df5337a96..566ccd6bd5 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -125,13 +125,13 @@ namespace osu.Game.Tests.Visual.Background createFakeStoryboard(); AddStep("Enable Storyboard", () => { - player.ReplacesBackground.Value = true; + player.StoryboardReplacesBackground.Value = true; player.StoryboardEnabled.Value = true; }); - AddUntilStep("Background is invisible, storyboard is visible", () => songSelect.IsBackgroundInvisible() && player.IsStoryboardVisible); + AddUntilStep("Background is black, storyboard is visible", () => songSelect.IsBackgroundVisible() && songSelect.IsBackgroundBlack() && player.IsStoryboardVisible); AddStep("Disable Storyboard", () => { - player.ReplacesBackground.Value = false; + player.StoryboardReplacesBackground.Value = false; player.StoryboardEnabled.Value = false; }); AddUntilStep("Background is visible, storyboard is invisible", () => songSelect.IsBackgroundVisible() && !player.IsStoryboardVisible); @@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Background createFakeStoryboard(); AddStep("Enable Storyboard", () => { - player.ReplacesBackground.Value = true; + player.StoryboardReplacesBackground.Value = true; player.StoryboardEnabled.Value = true; }); AddStep("Enable user dim", () => player.DimmableStoryboard.IgnoreUserSettings.Value = false); @@ -188,7 +188,7 @@ namespace osu.Game.Tests.Visual.Background { performFullSetup(); createFakeStoryboard(); - AddStep("Enable replacing background", () => player.ReplacesBackground.Value = true); + AddStep("Enable replacing background", () => player.StoryboardReplacesBackground.Value = true); AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible); AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible()); @@ -199,11 +199,11 @@ namespace osu.Game.Tests.Visual.Background player.DimmableStoryboard.IgnoreUserSettings.Value = true; }); AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible); - AddUntilStep("Background is invisible", () => songSelect.IsBackgroundInvisible()); + AddUntilStep("Background is dimmed", () => songSelect.IsBackgroundVisible() && songSelect.IsBackgroundBlack()); - AddStep("Disable background replacement", () => player.ReplacesBackground.Value = false); + AddStep("Disable background replacement", () => player.StoryboardReplacesBackground.Value = false); AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible); - AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible()); + AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible() && !songSelect.IsBackgroundBlack()); } /// @@ -257,7 +257,7 @@ namespace osu.Game.Tests.Visual.Background private void createFakeStoryboard() => AddStep("Create storyboard", () => { player.StoryboardEnabled.Value = false; - player.ReplacesBackground.Value = false; + player.StoryboardReplacesBackground.Value = false; player.DimmableStoryboard.Add(new OsuSpriteText { Size = new Vector2(500, 50), @@ -323,6 +323,8 @@ namespace osu.Game.Tests.Visual.Background config.BindWith(OsuSetting.BlurLevel, BlurLevel); } + public bool IsBackgroundBlack() => background.CurrentColour == OsuColour.Gray(0); + public bool IsBackgroundDimmed() => background.CurrentColour == OsuColour.Gray(1f - background.CurrentDim); public bool IsBackgroundUndimmed() => background.CurrentColour == Color4.White; @@ -331,8 +333,6 @@ namespace osu.Game.Tests.Visual.Background public bool IsUserBlurDisabled() => background.CurrentBlur == new Vector2(0); - public bool IsBackgroundInvisible() => background.CurrentAlpha == 0; - public bool IsBackgroundVisible() => background.CurrentAlpha == 1; public bool IsBackgroundBlur() => Precision.AlmostEquals(background.CurrentBlur, new Vector2(BACKGROUND_BLUR), 0.1f); @@ -367,7 +367,7 @@ namespace osu.Game.Tests.Visual.Background { base.OnEntering(e); - ApplyToBackground(b => ReplacesBackground.BindTo(b.StoryboardReplacesBackground)); + ApplyToBackground(b => StoryboardReplacesBackground.BindTo(b.StoryboardReplacesBackground)); } public new DimmableStoryboard DimmableStoryboard => base.DimmableStoryboard; @@ -376,7 +376,7 @@ namespace osu.Game.Tests.Visual.Background public bool BlockLoad; public Bindable StoryboardEnabled; - public readonly Bindable ReplacesBackground = new Bindable(); + public readonly Bindable StoryboardReplacesBackground = new Bindable(); public readonly Bindable IsPaused = new Bindable(); public LoadBlockingTestPlayer(bool allowPause = true) diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs index d4018be7fc..fed26d8acb 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs @@ -260,6 +260,12 @@ namespace osu.Game.Tests.Visual.Beatmaps AddStep($"set {scheme} scheme", () => Child = createContent(scheme, creationFunc)); } + [Test] + public void TestNano() + { + createTestCase(beatmapSetInfo => new BeatmapCardNano(beatmapSetInfo)); + } + [Test] public void TestNormal() { diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDifficultyList.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDifficultyList.cs index 9f3e36ad76..9e5bd53b13 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDifficultyList.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDifficultyList.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs index 8a11d60875..dcc4654437 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; @@ -49,7 +47,35 @@ namespace osu.Game.Tests.Visual.Beatmaps pill.AutoSizeAxes = Axes.Y; pill.Width = 90; })); + AddStep("unset fixed width", () => statusPills.ForEach(pill => pill.AutoSizeAxes = Axes.Both)); } + + [Test] + public void TestChangeLabels() + { + AddStep("Change labels", () => + { + foreach (var pill in this.ChildrenOfType()) + { + switch (pill.Status) + { + // cycle at end + case BeatmapOnlineStatus.Loved: + pill.Status = BeatmapOnlineStatus.LocallyModified; + break; + + // skip none + case BeatmapOnlineStatus.LocallyModified: + pill.Status = BeatmapOnlineStatus.Graveyard; + break; + + default: + pill.Status = (pill.Status + 1); + break; + } + } + }); + } } } diff --git a/osu.Game.Tests/Visual/Colours/TestSceneStarDifficultyColours.cs b/osu.Game.Tests/Visual/Colours/TestSceneStarDifficultyColours.cs index 55a2efa89d..7f07563dfd 100644 --- a/osu.Game.Tests/Visual/Colours/TestSceneStarDifficultyColours.cs +++ b/osu.Game.Tests/Visual/Colours/TestSceneStarDifficultyColours.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; @@ -19,7 +17,7 @@ namespace osu.Game.Tests.Visual.Colours public partial class TestSceneStarDifficultyColours : OsuTestScene { [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; [Test] public void TestColours() diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs index c7b6d984ed..f2b3351533 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.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.Diagnostics; using System.Linq; @@ -23,8 +21,7 @@ namespace osu.Game.Tests.Visual.Editing { public partial class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene { - private BeatDivisorControl beatDivisorControl; - private BindableBeatDivisor bindableBeatDivisor; + private BeatDivisorControl beatDivisorControl = null!; private SliderBar tickSliderBar => beatDivisorControl.ChildrenOfType>().Single(); private Triangle tickMarkerHead => tickSliderBar.ChildrenOfType().Single(); @@ -32,13 +29,19 @@ namespace osu.Game.Tests.Visual.Editing [Cached] private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + [Cached] + private readonly BindableBeatDivisor bindableBeatDivisor = new BindableBeatDivisor(16); + [SetUp] public void SetUp() => Schedule(() => { + bindableBeatDivisor.ValidDivisors.SetDefault(); + bindableBeatDivisor.SetDefault(); + Child = new PopoverContainer { RelativeSizeAxes = Axes.Both, - Child = beatDivisorControl = new BeatDivisorControl(bindableBeatDivisor = new BindableBeatDivisor(16)) + Child = beatDivisorControl = new BeatDivisorControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -169,9 +172,11 @@ namespace osu.Game.Tests.Visual.Editing switchPresets(1); assertPreset(BeatDivisorType.Triplets); + assertBeatSnap(6); switchPresets(1); assertPreset(BeatDivisorType.Common); + assertBeatSnap(4); switchPresets(-1); assertPreset(BeatDivisorType.Triplets); @@ -187,6 +192,7 @@ namespace osu.Game.Tests.Visual.Editing setDivisorViaInput(15); assertPreset(BeatDivisorType.Custom, 15); + assertBeatSnap(15); switchBeatSnap(-1); assertBeatSnap(5); @@ -196,12 +202,14 @@ namespace osu.Game.Tests.Visual.Editing setDivisorViaInput(5); assertPreset(BeatDivisorType.Custom, 15); + assertBeatSnap(5); switchPresets(1); assertPreset(BeatDivisorType.Common); switchPresets(-1); - assertPreset(BeatDivisorType.Triplets); + assertPreset(BeatDivisorType.Custom, 15); + assertBeatSnap(15); } private void switchBeatSnap(int direction) => AddRepeatStep($"move snap {(direction > 0 ? "forward" : "backward")}", () => @@ -225,7 +233,7 @@ namespace osu.Game.Tests.Visual.Editing private void assertPreset(BeatDivisorType type, int? maxDivisor = null) { - AddAssert($"preset is {type}", () => bindableBeatDivisor.ValidDivisors.Value.Type == type); + AddAssert($"preset is {type}", () => bindableBeatDivisor.ValidDivisors.Value.Type, () => Is.EqualTo(type)); if (type == BeatDivisorType.Custom) { @@ -243,7 +251,7 @@ namespace osu.Game.Tests.Visual.Editing InputManager.Click(MouseButton.Left); }); - BeatDivisorControl.CustomDivisorPopover popover = null; + BeatDivisorControl.CustomDivisorPopover? popover = null; AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType().SingleOrDefault()) != null && popover.IsLoaded); AddStep($"set divisor to {divisor}", () => { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs index 8b598a6a24..fdb3513e66 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Testing; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 7a0b3d0c1a..80c69aacf6 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -3,8 +3,11 @@ #nullable disable +using System; using System.Linq; +using JetBrains.Annotations; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; @@ -20,6 +23,14 @@ namespace osu.Game.Tests.Visual.Editing private Container selectionArea; private SelectionBox selectionBox; + [Cached(typeof(SelectionRotationHandler))] + private TestSelectionRotationHandler rotationHandler; + + public TestSceneComposeSelectBox() + { + rotationHandler = new TestSelectionRotationHandler(() => selectionArea); + } + [SetUp] public void SetUp() => Schedule(() => { @@ -34,13 +45,11 @@ namespace osu.Game.Tests.Visual.Editing { RelativeSizeAxes = Axes.Both, - CanRotate = true, CanScaleX = true, CanScaleY = true, CanFlipX = true, CanFlipY = true, - OnRotation = handleRotation, OnScale = handleScale } } @@ -71,11 +80,48 @@ namespace osu.Game.Tests.Visual.Editing return true; } - private bool handleRotation(float angle) + private partial class TestSelectionRotationHandler : SelectionRotationHandler { - // kinda silly and wrong, but just showing that the drag handles work. - selectionArea.Rotation += angle; - return true; + private readonly Func getTargetContainer; + + public TestSelectionRotationHandler(Func getTargetContainer) + { + this.getTargetContainer = getTargetContainer; + + CanRotate.Value = true; + } + + [CanBeNull] + private Container targetContainer; + + private float? initialRotation; + + public override void Begin() + { + if (targetContainer != null) + throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!"); + + targetContainer = getTargetContainer(); + initialRotation = targetContainer!.Rotation; + } + + public override void Update(float rotation, Vector2? origin = null) + { + if (targetContainer == null) + throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!"); + + // kinda silly and wrong, but just showing that the drag handles work. + targetContainer.Rotation = initialRotation!.Value + rotation; + } + + public override void Commit() + { + if (targetContainer == null) + throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!"); + + targetContainer = null; + initialRotation = null; + } } [Test] diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index b14025c9d8..3884a3108f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -1,26 +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; using System.Linq; using NUnit.Framework; -using osu.Framework.Testing; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; -using osu.Game.Tests.Beatmaps; using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Tests.Beatmaps; using osuTK; using osuTK.Input; @@ -82,7 +80,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestNudgeSelection() { - HitCircle[] addedObjects = null; + HitCircle[] addedObjects = null!; AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] { @@ -101,6 +99,64 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 100); } + [Test] + public void TestRotateHotkeys() + { + HitCircle[] addedObjects = null!; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] + { + new HitCircle { StartTime = 100 }, + new HitCircle { StartTime = 200, Position = new Vector2(100) }, + new HitCircle { StartTime = 300, Position = new Vector2(200) }, + new HitCircle { StartTime = 400, Position = new Vector2(300) }, + })); + + AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects)); + + AddStep("rotate clockwise", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Period); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddAssert("objects rotated clockwise", () => addedObjects[0].Position == new Vector2(300, 0)); + + AddStep("rotate counterclockwise", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Comma); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddAssert("objects reverted to original position", () => addedObjects[0].Position == new Vector2(0)); + } + + [Test] + public void TestGlobalFlipHotkeys() + { + HitCircle addedObject = null!; + + AddStep("add hitobjects", () => EditorBeatmap.Add(addedObject = new HitCircle { StartTime = 100 })); + + AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.Add(addedObject)); + + AddStep("flip horizontally across playfield", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.H); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddAssert("objects flipped horizontally", () => addedObject.Position == new Vector2(OsuPlayfield.BASE_SIZE.X, 0)); + + AddStep("flip vertically across playfield", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.J); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddAssert("objects flipped vertically", () => addedObject.Position == OsuPlayfield.BASE_SIZE); + } + [Test] public void TestBasicSelect() { @@ -159,11 +215,173 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1])); } + [Test] + public void TestNearestSelection() + { + var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 }; + var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject })); + + moveMouseToObject(() => firstObject); + + AddStep("seek near first", () => EditorClock.Seek(100)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + + AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear()); + + AddStep("seek near second", () => EditorClock.Seek(500)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + + AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear()); + + AddStep("seek halfway", () => EditorClock.Seek(300)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + } + + [Test] + public void TestNearestSelectionWithEndTime() + { + var firstObject = new Slider + { + Position = new Vector2(256, 192), + StartTime = 0, + Path = new SliderPath(new[] + { + new PathControlPoint(), + new PathControlPoint(new Vector2(50, 0)), + }) + }; + + var secondObject = new HitCircle + { + Position = new Vector2(256, 192), + StartTime = 600 + }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new HitObject[] { firstObject, secondObject })); + + moveMouseToObject(() => firstObject); + + AddStep("seek near first", () => EditorClock.Seek(100)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + + AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear()); + + AddStep("seek near second", () => EditorClock.Seek(500)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + + AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear()); + + AddStep("seek roughly halfway", () => EditorClock.Seek(350)); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + // Slider gets priority due to end time. + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + } + + [Test] + public void TestCyclicSelection() + { + var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 }; + var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 300 }; + var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject })); + + moveMouseToObject(() => firstObject); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject)); + + // cycle around + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + } + + [Test] + public void TestCyclicSelectionOutwards() + { + var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 }; + var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 300 }; + var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject })); + + moveMouseToObject(() => firstObject); + + AddStep("seek near second", () => EditorClock.Seek(320)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + + // cycle around + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + } + + [Test] + public void TestCyclicSelectionBackwards() + { + var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 }; + var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 200 }; + var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 400 }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject })); + + moveMouseToObject(() => firstObject); + + AddStep("seek to third", () => EditorClock.Seek(350)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + + // cycle around + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject)); + } + + [Test] + public void TestDoubleClickToSeek() + { + var hitCircle = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { hitCircle })); + + moveMouseToObject(() => hitCircle); + + AddRepeatStep("double click", () => InputManager.Click(MouseButton.Left), 2); + + AddUntilStep("seeked to circle", () => EditorClock.CurrentTime, () => Is.EqualTo(600)); + } + [TestCase(false)] [TestCase(true)] public void TestMultiSelectFromDrag(bool alreadySelectedBeforeDrag) { - HitCircle[] addedObjects = null; + HitCircle[] addedObjects = null!; AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] { @@ -262,7 +480,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestQuickDeleteRemovesSliderControlPoint() { - Slider slider = null; + Slider slider = null!; PathControlPoint[] points = { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs index 280e6de97e..12e00c4485 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs @@ -72,9 +72,13 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null); AddStep("confirm", () => InputManager.Key(Key.Number1)); - AddAssert($"difficulty {i} is deleted", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Select(b => b.ID), () => Does.Not.Contain(deletedDifficultyID)); - AddAssert("count decreased by one", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, () => Is.EqualTo(countBeforeDeletion - 1)); + AddAssert($"difficulty {i} is unattached from set", + () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Select(b => b.ID), () => Does.Not.Contain(deletedDifficultyID)); + AddAssert("beatmap set difficulty count decreased by one", + () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, () => Is.EqualTo(countBeforeDeletion - 1)); AddAssert("set hash changed", () => Beatmap.Value.BeatmapSetInfo.Hash, () => Is.Not.EqualTo(beatmapSetHashBefore)); + AddAssert($"difficulty {i} is deleted from realm", + () => Realm.Run(r => r.Find(deletedDifficultyID)), () => Is.Null); } } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index 70e4420a45..f2a015402a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -187,11 +187,9 @@ namespace osu.Game.Tests.Visual.Editing private class SnapProvider : IDistanceSnapProvider { - public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.AllGrids) => new SnapResult(screenSpacePosition, 0); - public Bindable DistanceSpacingMultiplier { get; } = new BindableDouble(1); - IBindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier; + Bindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier; public float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true) => beat_snap_distance; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 5aa2dd2ebf..7a2ed23cae 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -18,6 +18,7 @@ using osu.Game.Database; using osu.Game.Overlays.Dialog; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; @@ -91,25 +92,6 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - [FlakyTest] - /* - * Fail rate around 1.2%. - * - * Failing with realm refetch occasionally being null. - * My only guess is that the WorkingBeatmap at SetupScreen is dummy instead of the true one. - * If it's something else, we have larger issues with realm, but I don't think that's the case. - * - * at osu.Framework.Logging.ThrowingTraceListener.Fail(String message1, String message2) - * at System.Diagnostics.TraceInternal.Fail(String message, String detailMessage) - * at System.Diagnostics.TraceInternal.TraceProvider.Fail(String message, String detailMessage) - * at System.Diagnostics.Debug.Fail(String message, String detailMessage) - * at osu.Game.Database.ModelManager`1.<>c__DisplayClass8_0.b__0(Realm realm) ModelManager.cs:line 50 - * at osu.Game.Database.RealmExtensions.Write(Realm realm, Action`1 function) RealmExtensions.cs:line 14 - * at osu.Game.Database.ModelManager`1.performFileOperation(TModel item, Action`1 operation) ModelManager.cs:line 47 - * at osu.Game.Database.ModelManager`1.AddFile(TModel item, Stream contents, String filename) ModelManager.cs:line 37 - * at osu.Game.Screens.Edit.Setup.ResourcesSection.ChangeAudioTrack(FileInfo source) ResourcesSection.cs:line 115 - * at osu.Game.Tests.Visual.Editing.TestSceneEditorBeatmapCreation.b__11_0() TestSceneEditorBeatmapCreation.cs:line 101 - */ public void TestAddAudioTrack() { AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual); @@ -143,7 +125,7 @@ namespace osu.Game.Tests.Visual.Editing }); AddAssert("track is not virtual", () => Beatmap.Value.Track is not TrackVirtual); - AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000); + AddUntilStep("track length changed", () => Beatmap.Value.Track.Length > 60000); AddStep("test play", () => Editor.TestGameplay()); @@ -200,7 +182,7 @@ namespace osu.Game.Tests.Visual.Editing if (sameRuleset) { AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); - AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog.PerformOkAction()); + AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); } AddUntilStep("wait for created", () => @@ -287,7 +269,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); - AddStep("confirm creation as a copy", () => DialogOverlay.CurrentDialog.Buttons.ElementAt(1).TriggerClick()); + AddStep("confirm creation as a copy", () => DialogOverlay.CurrentDialog!.Buttons.ElementAt(1).TriggerClick()); AddUntilStep("wait for created", () => { @@ -360,7 +342,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); - AddStep("confirm creation as a copy", () => DialogOverlay.CurrentDialog.Buttons.ElementAt(1).TriggerClick()); + AddStep("confirm creation as a copy", () => DialogOverlay.CurrentDialog!.Buttons.ElementAt(1).TriggerClick()); AddUntilStep("wait for created", () => { @@ -398,7 +380,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("try to create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); - AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog.PerformOkAction()); + AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); AddUntilStep("wait for created", () => { @@ -433,7 +415,7 @@ namespace osu.Game.Tests.Visual.Editing if (sameRuleset) { AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); - AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog.PerformOkAction()); + AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); } AddUntilStep("wait for created", () => @@ -453,6 +435,51 @@ namespace osu.Game.Tests.Visual.Editing }); } + [Test] + public void TestExitBlockedWhenSavingBeatmapWithSameNamedDifficulties() + { + Guid setId = Guid.Empty; + const string duplicate_difficulty_name = "duplicate"; + + AddStep("retrieve set ID", () => setId = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID); + AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name); + AddStep("save beatmap", () => Editor.Save()); + AddAssert("new beatmap persisted", () => + { + var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); + return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1); + }); + + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new CatchRuleset().RulesetInfo)); + + AddUntilStep("wait for created", () => + { + string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName != duplicate_difficulty_name; + }); + AddUntilStep("wait for editor load", () => Editor.IsLoaded && DialogOverlay.IsLoaded); + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] + { + new Fruit + { + StartTime = 0 + }, + new Fruit + { + StartTime = 1000 + } + })); + + AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name); + AddUntilStep("wait for has unsaved changes", () => Editor.HasUnsavedChanges); + + AddStep("exit", () => Editor.Exit()); + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is PromptForSaveDialog); + AddStep("attempt to save", () => DialogOverlay.CurrentDialog!.PerformOkAction()); + AddAssert("editor is still current", () => Editor.IsCurrentScreen()); + } + [Test] public void TestCreateNewDifficultyForInconvertibleRuleset() { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs index 82d2542190..ed58c59ff0 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("seek near end", () => EditorClock.Seek(EditorClock.TrackLength - 250)); AddUntilStep("clock stops", () => !EditorClock.IsRunning); - AddUntilStep("clock stopped at end", () => EditorClock.CurrentTime - EditorClock.TotalAppliedOffset, () => Is.EqualTo(EditorClock.TrackLength)); + AddUntilStep("clock stopped at end", () => EditorClock.CurrentTime, () => Is.EqualTo(EditorClock.TrackLength)); AddStep("start clock again", () => EditorClock.Start()); AddAssert("clock looped to start", () => EditorClock.IsRunning && EditorClock.CurrentTime < 500); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs index e11d2e9dbf..2a822b3f1f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs index 699b99c57f..dbcf66f005 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs index a9d054881b..c0d64f4030 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs index b2b3dd9632..da4f159cae 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs index f255dd08a8..ddca2f8553 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index 007716bd6c..bbd7123f20 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Editing InputManager.MoveMouseTo(button); InputManager.Click(MouseButton.Left); }); - AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveBeforeGameplayTestDialog); + AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog); AddStep("dismiss prompt", () => { @@ -165,7 +165,7 @@ namespace osu.Game.Tests.Visual.Editing InputManager.MoveMouseTo(button); InputManager.Click(MouseButton.Left); }); - AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveBeforeGameplayTestDialog); + AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog); AddStep("save changes", () => DialogOverlay.CurrentDialog.PerformOkAction()); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs index 9bdb9a513c..ed3bffe5c2 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs @@ -8,7 +8,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -117,9 +117,9 @@ namespace osu.Game.Tests.Visual.Editing AddStep("move mouse to overlapping toggle button", () => { var playfield = hitObjectComposer.Playfield.ScreenSpaceDrawQuad; - var button = toolboxContainer.ChildrenOfType().First(b => playfield.Contains(b.ScreenSpaceDrawQuad.Centre)); + var button = toolboxContainer.ChildrenOfType().First(b => playfield.Contains(getOverlapPoint(b))); - InputManager.MoveMouseTo(button); + InputManager.MoveMouseTo(getOverlapPoint(button)); }); AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0); @@ -127,6 +127,12 @@ namespace osu.Game.Tests.Visual.Editing AddStep("attempt place circle", () => InputManager.Click(MouseButton.Left)); AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0); + + Vector2 getOverlapPoint(DrawableTernaryButton ternaryButton) + { + var quad = ternaryButton.ScreenSpaceDrawQuad; + return quad.TopLeft + new Vector2(quad.Width * 9 / 10, quad.Height / 2); + } } [Test] @@ -172,7 +178,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5); } - public partial class EditorBeatmapContainer : Container + public partial class EditorBeatmapContainer : PopoverContainer { private readonly IWorkingBeatmap working; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs index c874b39028..530b3db208 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Humanizer; using NUnit.Framework; @@ -61,7 +59,7 @@ namespace osu.Game.Tests.Visual.Editing new PathControlPoint(new Vector2(100, 0)) } }, - SliderVelocity = 2 + SliderVelocityMultiplier = 2 }); }); } @@ -112,7 +110,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("unify slider velocity", () => { foreach (var h in EditorBeatmap.HitObjects.OfType()) - h.SliderVelocity = 1.5; + h.SliderVelocityMultiplier = 1.5; }); AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); @@ -196,7 +194,7 @@ namespace osu.Game.Tests.Visual.Editing private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () => { var h = EditorBeatmap.HitObjects.ElementAt(objectIndex); - return h is IHasSliderVelocity hasSliderVelocity && hasSliderVelocity.SliderVelocity == velocity; + return h is IHasSliderVelocity hasSliderVelocity && hasSliderVelocity.SliderVelocityMultiplier == velocity; }); } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index b0b51a5dbd..1415ff4b0f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Collections.Generic; using Humanizer; @@ -360,7 +358,7 @@ namespace osu.Game.Tests.Visual.Editing var popover = this.ChildrenOfType().SingleOrDefault(); var textBox = popover?.ChildrenOfType().First(); - return textBox?.Current.Value == bank && string.IsNullOrEmpty(textBox?.PlaceholderText.ToString()); + return textBox?.Current.Value == bank && string.IsNullOrEmpty(textBox.PlaceholderText.ToString()); }); private void samplePopoverHasIndeterminateBank() => AddUntilStep("sample popover has indeterminate bank", () => diff --git a/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs index c0e681b8b4..534b813ddc 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs index b63296a48d..79ca8ee20c 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Screens.Edit.Compose.Components.Timeline; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs index 08e036248b..7a5243f6e8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.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.Graphics; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs index 50eeb9a54b..d8219ff36e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs @@ -7,8 +7,10 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; @@ -44,6 +46,47 @@ namespace osu.Game.Tests.Visual.Editing }); } + [Test] + public void TestContextMenuWithObjectBehind() + { + TimelineHitObjectBlueprint blueprint; + + AddStep("add object", () => + { + EditorBeatmap.Add(new HitCircle { StartTime = 3000 }); + }); + + AddStep("enter slider placement", () => + { + InputManager.Key(Key.Number3); + InputManager.MoveMouseTo(ScreenSpaceDrawQuad.Centre); + }); + + AddStep("start conflicting slider", () => + { + InputManager.Click(MouseButton.Left); + + blueprint = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(blueprint.ScreenSpaceDrawQuad.TopLeft - new Vector2(10, 0)); + }); + + AddStep("end conflicting slider", () => + { + InputManager.Click(MouseButton.Right); + }); + + AddStep("click object", () => + { + InputManager.Key(Key.Number1); + blueprint = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(blueprint); + InputManager.Click(MouseButton.Left); + }); + + AddStep("right click", () => InputManager.Click(MouseButton.Right)); + AddAssert("context menu open", () => this.ChildrenOfType().SingleOrDefault()?.State == MenuState.Open); + } + [Test] public void TestNudgeSelection() { @@ -139,7 +182,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("click away", () => { - InputManager.MoveMouseTo(Editor.ChildrenOfType().Single().ScreenSpaceDrawQuad.TopLeft + Vector2.One); + InputManager.MoveMouseTo(Editor.ChildrenOfType().First().ScreenSpaceDrawQuad.TopLeft + new Vector2(5)); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs index 41fb3ed8b9..18bd6d840a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -25,7 +23,7 @@ namespace osu.Game.Tests.Visual.Editing { BeatDivisor.Value = 4; - Add(new BeatDivisorControl(BeatDivisor) + Add(new BeatDivisorControl { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs index 19f4678c15..b493845ad4 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs index a141e4d431..1c8a18e131 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs @@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Editing public void TestWidthUpdatesOnDrawSizeChanges() { AddStep("Shrink scroll container", () => scrollContainer.Width = 0.5f); - AddAssert("Scroll container width shrunk", () => scrollContainer.DrawWidth == scrollContainer.Parent.DrawWidth / 2); + AddAssert("Scroll container width shrunk", () => scrollContainer.DrawWidth == scrollContainer.Parent!.DrawWidth / 2); AddAssert("Inner container width matches scroll container", () => innerBox.DrawWidth == scrollContainer.DrawWidth); } diff --git a/osu.Game.Tests/Visual/Gameplay/OsuPlayerTestScene.cs b/osu.Game.Tests/Visual/Gameplay/OsuPlayerTestScene.cs index 7ff059ff77..7252fbc474 100644 --- a/osu.Game.Tests/Visual/Gameplay/OsuPlayerTestScene.cs +++ b/osu.Game.Tests/Visual/Gameplay/OsuPlayerTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets; using osu.Game.Rulesets.Osu; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs index e86302bbd1..63fc4e47f9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs @@ -67,6 +67,11 @@ namespace osu.Game.Tests.Visual.Gameplay private Player loadPlayerFor(RulesetInfo rulesetInfo) { + // if a player screen is present already, we must exit that before loading another one, + // otherwise it'll crash on SpectatorClient.BeginPlaying being called while client is in "playing" state already. + if (Stack.CurrentScreen is Player) + Stack.Exit(); + Ruleset.Value = rulesetInfo; var ruleset = rulesetInfo.CreateInstance(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs new file mode 100644 index 0000000000..7bad623d7f --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs @@ -0,0 +1,91 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public partial class TestSceneArgonHealthDisplay : OsuTestScene + { + [Cached(typeof(HealthProcessor))] + private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); + + private ArgonHealthDisplay healthDisplay = null!; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep(@"Reset all", delegate + { + healthProcessor.Health.Value = 1; + healthProcessor.Failed += () => false; // health won't be updated if the processor gets into a "fail" state. + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Gray, + }, + healthDisplay = new ArgonHealthDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + }; + }); + + AddSliderStep("Width", 0, 1f, 1f, val => + { + if (healthDisplay.IsNotNull()) + healthDisplay.BarLength.Value = val; + }); + + AddSliderStep("Height", 0, 64, 0, val => + { + if (healthDisplay.IsNotNull()) + healthDisplay.BarHeight.Value = val; + }); + } + + [Test] + public void TestHealthDisplayIncrementing() + { + AddRepeatStep("apply miss judgement", delegate + { + healthProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }); + }, 5); + + AddRepeatStep(@"decrease hp slightly", delegate + { + healthProcessor.Health.Value -= 0.01f; + }, 10); + + AddRepeatStep(@"increase hp without flash", delegate + { + healthProcessor.Health.Value += 0.1f; + }, 3); + + AddRepeatStep(@"increase hp with flash", delegate + { + healthProcessor.Health.Value += 0.1f; + healthProcessor.ApplyResult(new JudgementResult(new HitCircle(), new OsuJudgement()) + { + Type = HitResult.Perfect + }); + }, 3); + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index f3f942b74b..3ac4d25028 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -35,14 +35,14 @@ namespace osu.Game.Tests.Visual.Gameplay var referenceBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0); - AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Counters.Any(kc => kc.CountPresses.Value > 2)); + AddUntilStep("key counter counted keys", () => Player.HUDOverlay.InputCountController.Triggers.Any(kc => kc.ActivationCount.Value > 2)); seekTo(referenceBeatmap.Breaks[0].StartTime); - AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting.Value); + AddAssert("keys not counting", () => !Player.HUDOverlay.InputCountController.IsCounting.Value); AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType().Single().AccuracyDisplay.Current.Value == 1); AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); - AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Counters.All(kc => kc.CountPresses.Value == 0)); + AddUntilStep("key counter reset", () => Player.HUDOverlay.InputCountController.Triggers.All(kc => kc.ActivationCount.Value == 0)); seekTo(referenceBeatmap.HitObjects[^1].GetEndTime()); AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index 514a2d7e84..a2ce62105e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; using osu.Game.Storyboards; @@ -68,7 +69,7 @@ namespace osu.Game.Tests.Visual.Gameplay var expectedComponentsAdjustmentContainer = new DependencyProvidingContainer { - Position = actualComponentsContainer.Parent.ToSpaceOfOtherDrawable(actualComponentsContainer.DrawPosition, Content), + Position = actualComponentsContainer.Parent!.ToSpaceOfOtherDrawable(actualComponentsContainer.DrawPosition, Content), Size = actualComponentsContainer.DrawSize, Child = expectedComponentsContainer, // proxy the same required dependencies that `actualComponentsContainer` is using. @@ -77,7 +78,8 @@ namespace osu.Game.Tests.Visual.Gameplay (typeof(ScoreProcessor), actualComponentsContainer.Dependencies.Get()), (typeof(HealthProcessor), actualComponentsContainer.Dependencies.Get()), (typeof(GameplayState), actualComponentsContainer.Dependencies.Get()), - (typeof(IGameplayClock), actualComponentsContainer.Dependencies.Get()) + (typeof(IGameplayClock), actualComponentsContainer.Dependencies.Get()), + (typeof(InputCountController), actualComponentsContainer.Dependencies.Get()) }, }; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs index 6b8e0e1088..db06329d74 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs @@ -17,7 +17,7 @@ namespace osu.Game.Tests.Visual.Gameplay { public partial class TestSceneClicksPerSecondCalculator : OsuTestScene { - private ClicksPerSecondCalculator calculator = null!; + private ClicksPerSecondController controller = null!; private TestGameplayClock manualGameplayClock = null!; @@ -34,11 +34,11 @@ namespace osu.Game.Tests.Visual.Gameplay CachedDependencies = new (Type, object)[] { (typeof(IGameplayClock), manualGameplayClock) }, Children = new Drawable[] { - calculator = new ClicksPerSecondCalculator(), + controller = new ClicksPerSecondController(), new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, - CachedDependencies = new (Type, object)[] { (typeof(ClicksPerSecondCalculator), calculator) }, + CachedDependencies = new (Type, object)[] { (typeof(ClicksPerSecondController), controller) }, Child = new ClicksPerSecondCounter { Anchor = Anchor.Centre, @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.Gameplay checkClicksPerSecondValue(6); } - private void checkClicksPerSecondValue(int i) => AddAssert("clicks/s is correct", () => calculator.Value, () => Is.EqualTo(i)); + private void checkClicksPerSecondValue(int i) => AddAssert("clicks/s is correct", () => controller.Value, () => Is.EqualTo(i)); private void seekClockImmediately(double time) => manualGameplayClock.CurrentTime = time; @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.Gameplay foreach (double timestamp in inputs) { seekClockImmediately(timestamp); - calculator.AddInputTimestamp(); + controller.AddInputTimestamp(); } seekClockImmediately(baseTime); @@ -125,6 +125,7 @@ namespace osu.Game.Tests.Visual.Gameplay public IEnumerable NonGameplayAdjustments => throw new NotImplementedException(); public IBindable IsPaused => throw new NotImplementedException(); + public bool IsRewinding => false; } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index 287b7d43b4..4c898feb48 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -311,14 +311,13 @@ namespace osu.Game.Tests.Visual.Gameplay protected override bool RelativeScaleBeatLengths => RelativeScaleBeatLengthsOverride; - protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping; - public new Bindable TimeRange => base.TimeRange; public TestDrawableScrollingRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) : base(ruleset, beatmap, mods) { TimeRange.Value = time_range; + VisualisationMethod = ScrollVisualisationMethod.Overlapping; } public override DrawableHitObject CreateDrawableRepresentation(TestHitObject h) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs index ec4bb1a86b..32693c2bb2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs @@ -2,17 +2,23 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Storyboards; using osu.Game.Storyboards.Drawables; +using osu.Game.Tests.Resources; using osuTK; namespace osu.Game.Tests.Visual.Gameplay @@ -21,17 +27,21 @@ namespace osu.Game.Tests.Visual.Gameplay { protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); - [Cached] - private Storyboard storyboard { get; set; } = new Storyboard(); + [Cached(typeof(Storyboard))] + private TestStoryboard storyboard { get; set; } = new TestStoryboard(); private IEnumerable sprites => this.ChildrenOfType(); + private const string lookup_name = "hitcircleoverlay"; + [Test] public void TestSkinSpriteDisallowedByDefault() { - const string lookup_name = "hitcircleoverlay"; - - AddStep("allow skin lookup", () => storyboard.UseSkinSprites = false); + AddStep("disallow all lookups", () => + { + storyboard.UseSkinSprites = false; + storyboard.AlwaysProvideTexture = false; + }); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); @@ -40,11 +50,13 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestAllowLookupFromSkin() + public void TestLookupFromStoryboard() { - const string lookup_name = "hitcircleoverlay"; - - AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true); + AddStep("allow storyboard lookup", () => + { + storyboard.UseSkinSprites = false; + storyboard.AlwaysProvideTexture = true; + }); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); @@ -52,16 +64,54 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("sprite found texture", () => sprites.Any(sprite => sprite.ChildrenOfType().All(s => s.Texture != null))); - AddAssert("skinnable sprite has correct size", () => - sprites.Any(sprite => sprite.ChildrenOfType().All(s => s.Size == new Vector2(128)))); + assertStoryboardSourced(); + } + + [Test] + public void TestSkinLookupPreferredOverStoryboard() + { + AddStep("allow all lookups", () => + { + storyboard.UseSkinSprites = true; + storyboard.AlwaysProvideTexture = true; + }); + + AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); + + // Only checking for at least one sprite that succeeded, as not all skins in this test provide the hitcircleoverlay texture. + AddAssert("sprite found texture", () => + sprites.Any(sprite => sprite.ChildrenOfType().All(s => s.Texture != null))); + + assertSkinSourced(); + } + + [Test] + public void TestAllowLookupFromSkin() + { + AddStep("allow skin lookup", () => + { + storyboard.UseSkinSprites = true; + storyboard.AlwaysProvideTexture = false; + }); + + AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); + + // Only checking for at least one sprite that succeeded, as not all skins in this test provide the hitcircleoverlay texture. + AddAssert("sprite found texture", () => + sprites.Any(sprite => sprite.ChildrenOfType().All(s => s.Texture != null))); + + assertSkinSourced(); } [Test] public void TestFlippedSprite() { - const string lookup_name = "hitcircleoverlay"; + AddStep("allow all lookups", () => + { + storyboard.UseSkinSprites = true; + storyboard.AlwaysProvideTexture = true; + }); - AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); AddStep("flip sprites", () => sprites.ForEach(s => { @@ -74,9 +124,12 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestZeroScale() { - const string lookup_name = "hitcircleoverlay"; + AddStep("allow all lookups", () => + { + storyboard.UseSkinSprites = true; + storyboard.AlwaysProvideTexture = true; + }); - AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); AddAssert("sprites present", () => sprites.All(s => s.IsPresent)); AddStep("scale sprite", () => sprites.ForEach(s => s.VectorScale = new Vector2(0, 1))); @@ -86,9 +139,12 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestNegativeScale() { - const string lookup_name = "hitcircleoverlay"; + AddStep("allow all lookups", () => + { + storyboard.UseSkinSprites = true; + storyboard.AlwaysProvideTexture = true; + }); - AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); AddStep("scale sprite", () => sprites.ForEach(s => s.VectorScale = new Vector2(-1))); AddAssert("origin flipped", () => sprites.All(s => s.Origin == Anchor.BottomRight)); @@ -97,9 +153,12 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestNegativeScaleWithFlippedSprite() { - const string lookup_name = "hitcircleoverlay"; + AddStep("allow all lookups", () => + { + storyboard.UseSkinSprites = true; + storyboard.AlwaysProvideTexture = true; + }); - AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); AddStep("scale sprite", () => sprites.ForEach(s => s.VectorScale = new Vector2(-1))); AddAssert("origin flipped", () => sprites.All(s => s.Origin == Anchor.BottomRight)); @@ -111,13 +170,78 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("origin back", () => sprites.All(s => s.Origin == Anchor.TopLeft)); } - private DrawableStoryboardSprite createSprite(string lookupName, Anchor origin, Vector2 initialPosition) - => new DrawableStoryboardSprite( - new StoryboardSprite(lookupName, origin, initialPosition) - ).With(s => + private DrawableStoryboard createSprite(string lookupName, Anchor origin, Vector2 initialPosition) + { + var layer = storyboard.GetLayer("Background"); + + var sprite = new StoryboardSprite(lookupName, origin, initialPosition); + sprite.AddLoop(Time.Current, 100).Alpha.Add(Easing.None, 0, 10000, 1, 1); + + layer.Elements.Clear(); + layer.Add(sprite); + + return storyboard.CreateDrawable().With(s => s.RelativeSizeAxes = Axes.Both); + } + + private void assertStoryboardSourced() + { + AddAssert("sprite came from storyboard", () => + sprites.Any(sprite => sprite.ChildrenOfType().All(s => s.Size == new Vector2(200)))); + } + + private void assertSkinSourced() + { + AddAssert("sprite came from skin", () => + sprites.Any(sprite => sprite.ChildrenOfType().All(s => s.Size == new Vector2(128)))); + } + + private partial class TestStoryboard : Storyboard + { + public override DrawableStoryboard CreateDrawable(IReadOnlyList? mods = null) { - s.LifetimeStart = double.MinValue; - s.LifetimeEnd = double.MaxValue; - }); + return new TestDrawableStoryboard(this, mods); + } + + public bool AlwaysProvideTexture { get; set; } + + public override string GetStoragePathFromStoryboardPath(string path) => AlwaysProvideTexture ? path : string.Empty; + + private partial class TestDrawableStoryboard : DrawableStoryboard + { + private readonly bool alwaysProvideTexture; + + public TestDrawableStoryboard(TestStoryboard storyboard, IReadOnlyList? mods) + : base(storyboard, mods) + { + alwaysProvideTexture = storyboard.AlwaysProvideTexture; + } + + protected override IResourceStore CreateResourceLookupStore() => alwaysProvideTexture + ? new AlwaysReturnsTextureStore() + : new ResourceStore(); + + internal class AlwaysReturnsTextureStore : IResourceStore + { + private const string test_image = "Resources/Textures/test-image.png"; + + private readonly DllResourceStore store; + + public AlwaysReturnsTextureStore() + { + store = TestResources.GetStore(); + } + + public void Dispose() => store.Dispose(); + + public byte[] Get(string name) => store.Get(test_image); + + public Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => store.GetAsync(test_image, cancellationToken); + + public Stream GetStream(string name) => store.GetStream(test_image); + + public IEnumerable GetAvailableResources() => store.GetAvailableResources(); + } + } + } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs index 6cb1101173..b251253b7c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs index e779c6c1cb..6297b062dd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Game.Rulesets; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index d4000c07e7..193e8b2571 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -3,14 +3,17 @@ #nullable disable +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Extensions.PolygonExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Screens.Play.HUD; using osuTK; @@ -139,6 +142,37 @@ namespace osu.Game.Tests.Visual.Gameplay => AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (GameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount); } + [Test] + public void TestFriendScore() + { + APIUser friend = new APIUser { Username = "my friend", Id = 10000 }; + + createLeaderboard(); + addLocalPlayer(); + + AddStep("Add friend to API", () => + { + var api = (DummyAPIAccess)API; + + api.Friends.Clear(); + api.Friends.Add(friend); + }); + + int playerNumber = 1; + + AddRepeatStep("add 3 other players", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 3); + AddUntilStep("no pink color scores", + () => leaderboard.ChildrenOfType().Select(b => ((Colour4)b.Colour).ToHex()), + () => Does.Not.Contain("#FF549A")); + + AddRepeatStep("add 3 friend score", () => createRandomScore(friend), 3); + AddUntilStep("at least one friend score is pink", + () => leaderboard.GetAllScoresForUsername("my friend") + .SelectMany(score => score.ChildrenOfType()) + .Select(b => ((Colour4)b.Colour).ToHex()), + () => Does.Contain("#FF549A")); + } + private void addLocalPlayer() { AddStep("add local player", () => @@ -179,6 +213,9 @@ namespace osu.Game.Tests.Visual.Gameplay return scoreItem != null && scoreItem.ScorePosition == expectedPosition; } + + public IEnumerable GetAllScoresForUsername(string username) + => Flow.Where(i => i.User?.Username == username); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs index 751aeb4e13..0cb74ecde6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs @@ -7,8 +7,8 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Utils; using osu.Framework.Timing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu.Objects; @@ -31,11 +31,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); addSeekStep(3000); AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged)); - AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Counters.Select(kc => kc.CountPresses.Value).Sum() == 15); + AddUntilStep("key counter counted keys", () => Player.HUDOverlay.InputCountController.Triggers.Select(kc => kc.ActivationCount.Value).Sum() == 15); AddStep("clear results", () => Player.Results.Clear()); addSeekStep(0); AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged)); - AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Counters.All(kc => kc.CountPresses.Value == 0)); + AddUntilStep("key counters reset", () => Player.HUDOverlay.InputCountController.Triggers.All(kc => kc.ActivationCount.Value == 0)); AddAssert("no results triggered", () => Player.Results.Count == 0); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index e52ec6f8cc..0f16d3f394 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Storyboards; using osuTK; @@ -62,25 +63,30 @@ namespace osu.Game.Tests.Visual.Gameplay { new HitCircle { + HitWindows = new HitWindows(), StartTime = t += spacing, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } }, new HitCircle { + HitWindows = new HitWindows(), StartTime = t += spacing, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) } }, new HitCircle { + HitWindows = new HitWindows(), StartTime = t += spacing, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT) }, }, new HitCircle { + HitWindows = new HitWindows(), StartTime = t += spacing, }, new Slider { + HitWindows = new HitWindows(), StartTime = t += spacing, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }), Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT) }, @@ -101,7 +107,7 @@ namespace osu.Game.Tests.Visual.Gameplay { base.SetUpSteps(); - AddStep("Add trigger source", () => Player.HUDOverlay.Add(sampleTriggerSource = new TestGameplaySampleTriggerSource(Player.DrawableRuleset.Playfield.HitObjectContainer))); + AddStep("Add trigger source", () => Player.DrawableRuleset.FrameStableComponents.Add(sampleTriggerSource = new TestGameplaySampleTriggerSource(Player.DrawableRuleset.Playfield.HitObjectContainer))); } [Test] @@ -131,7 +137,12 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("first object hit", () => getNextAliveObject()?.Entry?.Result?.HasResult == true); - checkValidObjectIndex(1); + // next object is too far away, so we still use the already hit object. + checkValidObjectIndex(0); + + // still too far away. + seekBeforeIndex(1, 400); + checkValidObjectIndex(0); // Still object 1 as it's not hit yet. seekBeforeIndex(1); @@ -142,6 +153,14 @@ namespace osu.Game.Tests.Visual.Gameplay waitForAliveObjectIndex(2); checkValidObjectIndex(2); + // test rewinding + seekBeforeIndex(1); + waitForAliveObjectIndex(1); + checkValidObjectIndex(1); + + seekBeforeIndex(1, 400); + checkValidObjectIndex(0); + seekBeforeIndex(3); waitForAliveObjectIndex(3); checkValidObjectIndex(3); @@ -168,9 +187,9 @@ namespace osu.Game.Tests.Visual.Gameplay checkValidObjectIndex(4); } - private void seekBeforeIndex(int index) + private void seekBeforeIndex(int index, double amount = 100) { - AddStep($"seek to just before object {index}", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects[index].StartTime - 100)); + AddStep($"seek to {amount} ms before object {index}", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects[index].StartTime - amount)); waitForCatchUp(); } @@ -186,7 +205,7 @@ namespace osu.Game.Tests.Visual.Gameplay } private void checkValidObjectIndex(int index) => - AddAssert($"check valid object is {index}", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[index])); + AddAssert($"check object at index {index} is correct", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[index])); private DrawableHitObject? getNextAliveObject() => Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault(); @@ -204,7 +223,7 @@ namespace osu.Game.Tests.Visual.Gameplay { } - public new HitObject GetMostValidObject() => base.GetMostValidObject(); + public new HitObject? GetMostValidObject() => base.GetMostValidObject(); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index f97019e466..f8226eb21d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -6,11 +6,11 @@ using System.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; -using osu.Framework.Timing; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Mods; @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay private HUDOverlay hudOverlay = null!; [Cached(typeof(ScoreProcessor))] - private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor; + private ScoreProcessor scoreProcessor { get; set; } [Cached(typeof(HealthProcessor))] private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); @@ -41,11 +41,16 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached(typeof(IGameplayClock))] - private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock()); + private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new TrackVirtual(60000), false, false); // best way to check without exposing. - private Drawable hideTarget => hudOverlay.KeyCounter; - private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single(); + private Drawable hideTarget => hudOverlay.ChildrenOfType().First(); + private Drawable keyCounterFlow => hudOverlay.ChildrenOfType().First().ChildrenOfType>().Single(); + + public TestSceneHUDOverlay() + { + scoreProcessor = gameplayState.ScoreProcessor; + } [BackgroundDependencyLoader] private void load() @@ -73,7 +78,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("showhud is set", () => hudOverlay.ShowHud.Value); - AddAssert("hidetarget is visible", () => hideTarget.IsPresent); + AddAssert("hidetarget is visible", () => hideTarget.Alpha, () => Is.GreaterThan(0)); AddAssert("key counter flow is visible", () => keyCounterFlow.IsPresent); AddAssert("pause button is visible", () => hudOverlay.HoldToQuit.IsPresent); } @@ -95,7 +100,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); - AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); + AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0)); AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent); // Key counter flow container should not be affected by this, only the key counter display will be hidden as checked above. @@ -109,13 +114,13 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set hud to never show", () => localConfig.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never)); - AddUntilStep("wait for fade", () => !hideTarget.IsPresent); + AddUntilStep("wait for fade", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0)); AddStep("trigger momentary show", () => InputManager.PressKey(Key.ControlLeft)); - AddUntilStep("wait for visible", () => hideTarget.IsPresent); + AddUntilStep("wait for visible", () => hideTarget.Alpha, () => Is.GreaterThan(0)); AddStep("stop trigering", () => InputManager.ReleaseKey(Key.ControlLeft)); - AddUntilStep("wait for fade", () => !hideTarget.IsPresent); + AddUntilStep("wait for fade", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0)); } [Test] @@ -138,16 +143,18 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("hide key overlay", () => { localConfig.SetValue(OsuSetting.KeyOverlay, false); - hudOverlay.KeyCounter.AlwaysVisible.Value = false; + var kcd = hudOverlay.ChildrenOfType().FirstOrDefault(); + if (kcd != null) + kcd.AlwaysVisible.Value = false; }); AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); - AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); - AddAssert("key counters hidden", () => !keyCounterFlow.IsPresent); + AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0)); + AddUntilStep("key counters hidden", () => !keyCounterFlow.IsPresent); AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true); - AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent); - AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent); + AddUntilStep("hidetarget is visible", () => hideTarget.Alpha, () => Is.GreaterThan(0)); + AddUntilStep("key counters still hidden", () => !keyCounterFlow.IsPresent); } [Test] @@ -169,7 +176,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); - AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); + AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0)); AddStep("attempt activate", () => { @@ -209,11 +216,11 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); - AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); + AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0)); AddStep("attempt seek", () => { - InputManager.MoveMouseTo(getSongProgress()); + InputManager.MoveMouseTo(getSongProgress().AsNonNull()); InputManager.Click(MouseButton.Left); }); @@ -234,7 +241,6 @@ namespace osu.Game.Tests.Visual.Gameplay createNew(); - AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded); AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType().Single().Alpha == 0); AddUntilStep("wait for hud load", () => hudOverlay.ChildrenOfType().All(c => c.ComponentsLoaded)); @@ -253,7 +259,6 @@ namespace osu.Game.Tests.Visual.Gameplay createNew(); - AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded); AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType().Single().Alpha == 0); AddStep("reload components", () => hudOverlay.ChildrenOfType().Single().Reload()); @@ -267,7 +272,7 @@ namespace osu.Game.Tests.Visual.Gameplay hudOverlay = new HUDOverlay(null, Array.Empty()); // Add any key just to display the key counter visually. - hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space)); + hudOverlay.InputCountController.Add(new KeyCounterKeyboardTrigger(Key.Space)); scoreProcessor.Combo.Value = 1; @@ -275,6 +280,9 @@ namespace osu.Game.Tests.Visual.Gameplay Child = hudOverlay; }); + + AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded); + AddUntilStep("wait for components present", () => hudOverlay.ChildrenOfType().FirstOrDefault() != null); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs index 5a802e0d36..dae6dc7b4b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs @@ -11,8 +11,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD.JudgementCounter; @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Gameplay public partial class TestSceneJudgementCounter : OsuTestScene { private ScoreProcessor scoreProcessor = null!; - private JudgementTally judgementTally = null!; + private JudgementCountController judgementCountController = null!; private TestJudgementCounterDisplay counterDisplay = null!; private DependencyProvidingContainer content = null!; @@ -47,17 +47,17 @@ namespace osu.Game.Tests.Visual.Gameplay CachedDependencies = new (Type, object)[] { (typeof(ScoreProcessor), scoreProcessor), (typeof(Ruleset), ruleset) }, Children = new Drawable[] { - judgementTally = new JudgementTally(), + judgementCountController = new JudgementCountController(), content = new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, - CachedDependencies = new (Type, object)[] { (typeof(JudgementTally), judgementTally) }, + CachedDependencies = new (Type, object)[] { (typeof(JudgementCountController), judgementCountController) }, } }, }; }); - protected override Ruleset CreateRuleset() => new ManiaRuleset(); + protected override Ruleset CreateRuleset() => new OsuRuleset(); private void applyOneJudgement(HitResult result) { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 22f7111f68..5a66a5c7a6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -1,11 +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 NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; @@ -17,63 +17,66 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public partial class TestSceneKeyCounter : OsuManualInputManagerTestScene { + [Cached] + private readonly InputCountController controller; + public TestSceneKeyCounter() { - KeyCounterDisplay defaultDisplay = new DefaultKeyCounterDisplay + Children = new Drawable[] { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Position = new Vector2(0, 72.7f) + controller = new InputCountController(), + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(72.7f), + Children = new KeyCounterDisplay[] + { + new DefaultKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + }, + new ArgonKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + } + } + } }; - KeyCounterDisplay argonDisplay = new ArgonKeyCounterDisplay - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Position = new Vector2(0, -72.7f) - }; - - defaultDisplay.AddRange(new InputTrigger[] + var inputTriggers = new InputTrigger[] { new KeyCounterKeyboardTrigger(Key.X), new KeyCounterKeyboardTrigger(Key.X), new KeyCounterMouseTrigger(MouseButton.Left), new KeyCounterMouseTrigger(MouseButton.Right), - }); + }; - argonDisplay.AddRange(new InputTrigger[] - { - new KeyCounterKeyboardTrigger(Key.X), - new KeyCounterKeyboardTrigger(Key.X), - new KeyCounterMouseTrigger(MouseButton.Left), - new KeyCounterMouseTrigger(MouseButton.Right), - }); - - var testCounter = (DefaultKeyCounter)defaultDisplay.Counters.First(); + AddRange(inputTriggers); + controller.AddRange(inputTriggers); AddStep("Add random", () => { Key key = (Key)((int)Key.A + RNG.Next(26)); - defaultDisplay.Add(new KeyCounterKeyboardTrigger(key)); - argonDisplay.Add(new KeyCounterKeyboardTrigger(key)); + var trigger = new KeyCounterKeyboardTrigger(key); + Add(trigger); + controller.Add(trigger); }); - Key testKey = ((KeyCounterKeyboardTrigger)defaultDisplay.Counters.First().Trigger).Key; + InputTrigger testTrigger = controller.Triggers.First(); + Key testKey = ((KeyCounterKeyboardTrigger)testTrigger).Key; addPressKeyStep(); - AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 1); + AddAssert($"Check {testKey} counter after keypress", () => testTrigger.ActivationCount.Value == 1); addPressKeyStep(); - AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 2); - AddStep("Disable counting", () => - { - argonDisplay.IsCounting.Value = false; - defaultDisplay.IsCounting.Value = false; - }); + AddAssert($"Check {testKey} counter after keypress", () => testTrigger.ActivationCount.Value == 2); + AddStep("Disable counting", () => controller.IsCounting.Value = false); addPressKeyStep(); - AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses.Value == 2); - - Add(defaultDisplay); - Add(argonDisplay); + AddAssert($"Check {testKey} count has not changed", () => testTrigger.ActivationCount.Value == 2); void addPressKeyStep() => AddStep($"Press {testKey} key", () => InputManager.Key(testKey)); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs index 626406e4d2..71ed0a14a2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Overlays; using osu.Game.Users; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs index 84334ba0a9..0e03f253a9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps.Timing; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs index c73d57dc2b..8fb34883bb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs @@ -69,13 +69,13 @@ namespace osu.Game.Tests.Visual.Gameplay spewer.Clock = new FramedClock(testClock); }); AddStep("start spewer", () => spewer.Active.Value = true); - AddAssert("spawned first particle", () => spewer.TotalCreatedParticles == 1); + AddAssert("spawned first particle", () => spewer.TotalCreatedParticles, () => Is.EqualTo(1)); AddStep("move clock forward", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * 3); - AddAssert("spawned second particle", () => spewer.TotalCreatedParticles == 2); + AddAssert("spawned second particle", () => spewer.TotalCreatedParticles, () => Is.EqualTo(2)); AddStep("move clock backwards", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * -1); - AddAssert("spawned third particle", () => spewer.TotalCreatedParticles == 3); + AddAssert("spawned third particle", () => spewer.TotalCreatedParticles, () => Is.EqualTo(3)); } private TestParticleSpewer createSpewer() => diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index b072ce191e..ec3b3e0822 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestPauseWithLargeOffset() { - double lastTime; + double lastStopTime; bool alwaysGoingForward = true; AddStep("force large offset", () => @@ -84,20 +84,24 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("add time forward check hook", () => { - lastTime = double.MinValue; + lastStopTime = double.MinValue; alwaysGoingForward = true; Player.OnUpdate += _ => { - double currentTime = Player.GameplayClockContainer.CurrentTime; - bool goingForward = currentTime >= lastTime - 500; + var masterClock = (MasterGameplayClockContainer)Player.GameplayClockContainer; + + double currentTime = masterClock.CurrentTime; + + bool goingForward = currentTime >= (masterClock.LastStopTime ?? lastStopTime); alwaysGoingForward &= goingForward; if (!goingForward) - Logger.Log($"Backwards time occurred ({currentTime:N1} -> {lastTime:N1})"); + Logger.Log($"Went too far backwards (last stop: {lastStopTime:N1} current: {currentTime:N1})"); - lastTime = currentTime; + if (masterClock.LastStopTime != null) + lastStopTime = masterClock.LastStopTime.Value; }; }); diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs index 80c4e4bce9..0dd544bb30 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.IO; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -14,6 +15,9 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -21,9 +25,12 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Tests.Resources; +using osu.Game.Users; +using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { @@ -95,6 +102,7 @@ namespace osu.Game.Tests.Visual.Gameplay { DateTimeOffset? getLastPlayed() => Realm.Run(r => r.Find(Beatmap.Value.BeatmapInfo.ID)?.LastPlayed); + AddStep("reset last played", () => Realm.Write(r => r.Find(Beatmap.Value.BeatmapInfo.ID)!.LastPlayed = null)); AddAssert("last played is null", () => getLastPlayed() == null); CreateTest(); @@ -131,7 +139,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("results screen score has matching", () => (Player.GetChildScreen() as ResultsScreen)?.Score.Mods.First(), () => Is.EqualTo(playerMods.First())); AddUntilStep("score in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) != null)); - AddUntilStep("databased score has correct mods", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID)).Mods.First(), () => Is.EqualTo(playerMods.First())); + AddUntilStep("databased score has correct mods", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID))!.Mods.First(), () => Is.EqualTo(playerMods.First())); } [Test] @@ -147,6 +155,80 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("score in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) != null)); } + [Test] + public void TestGuestScoreIsStoredAsGuest() + { + AddStep("set up API", () => ((DummyAPIAccess)API).HandleRequest = req => + { + switch (req) + { + case GetUserRequest userRequest: + userRequest.TriggerSuccess(new APIUser + { + Username = "Guest", + CountryCode = CountryCode.JP, + Id = 1234 + }); + return true; + + default: + return false; + } + }); + + AddStep("log out", () => API.Logout()); + CreateTest(); + + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); + AddStep("log back in", () => API.Login("username", "password")); + + AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); + + AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen); + AddUntilStep("score in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) != null)); + AddAssert("score is not associated with online user", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID))!.UserID == APIUser.SYSTEM_USER_ID); + } + + [Test] + public void TestReplayExport() + { + CreateTest(); + + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); + + AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); + + AddUntilStep("results displayed", () => (Player.GetChildScreen() as ResultsScreen)?.IsLoaded == true); + AddUntilStep("score in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) != null)); + + AddUntilStep("wait for button clickable", () => ((OsuScreen)Player.GetChildScreen()) + .ChildrenOfType().FirstOrDefault()? + .ChildrenOfType().FirstOrDefault()? + .Enabled.Value == true); + + AddAssert("no export files", () => !LocalStorage.GetFiles("exports").Any()); + + AddStep("Export replay", () => InputManager.PressKey(Key.F2)); + + string? filePath = null; + + // Files starting with _ are temporary, created by CreateFileSafely call. + AddUntilStep("wait for export file", () => filePath = LocalStorage.GetFiles("exports").SingleOrDefault(f => !Path.GetFileName(f).StartsWith("_", StringComparison.Ordinal)), () => Is.Not.Null); + AddUntilStep("filesize is non-zero", () => + { + try + { + using (var stream = LocalStorage.GetStream(filePath)) + return stream.Length; + } + catch (IOException) + { + // file move may still be in progress. + return 0; + } + }, () => Is.Not.Zero); + } + [Test] public void TestScoreStoredLocallyCustomRuleset() { diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs new file mode 100644 index 0000000000..53a4abdd07 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs @@ -0,0 +1,139 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using osu.Framework.Testing; +using osu.Game.IO; +using osu.Game.Rulesets; +using osu.Game.Screens.Play; +using osu.Game.Skinning; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +namespace osu.Game.Tests.Visual.Gameplay +{ + /// + /// Upscales all gameplay sprites by a huge amount, to aid in manually checking skin texture size limits + /// on individual elements. + /// + /// + /// The HUD is hidden as it does't really affect game balance if HUD elements are larger than they should be. + /// + [Ignore("This test is for visual testing, and has no value in being run in standard CI runs.")] + public partial class TestScenePlayerMaxDimensions : TestSceneAllRulesetPlayers + { + // scale textures to 4 times their size. + private const int scale_factor = 4; + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + // for now this only applies to legacy skins, as modern skins don't have texture-based gameplay elements yet. + dependencies.CacheAs(new UpscaledLegacySkin(dependencies.Get())); + + return dependencies; + } + + protected override void AddCheckSteps() + { + } + + protected override Player CreatePlayer(Ruleset ruleset) + { + var player = base.CreatePlayer(ruleset); + player.OnLoadComplete += _ => + { + // this test scene focuses on gameplay elements, so let's hide the hud. + var hudOverlay = player.ChildrenOfType().Single(); + hudOverlay.ShowHud.Value = false; + hudOverlay.ShowHud.Disabled = true; + }; + return player; + } + + private class UpscaledLegacySkin : DefaultLegacySkin, ISkinSource + { + public UpscaledLegacySkin(IStorageResourceProvider resources) + : base(resources) + { + } + + public event Action? SourceChanged + { + add { } + remove { } + } + + public ISkin FindProvider(Func lookupFunction) => this; + public IEnumerable AllSources => new[] { this }; + + protected override IResourceStore CreateTextureLoaderStore(IStorageResourceProvider resources, IResourceStore storage) + => new UpscaledTextureLoaderStore(base.CreateTextureLoaderStore(resources, storage)); + + private class UpscaledTextureLoaderStore : IResourceStore + { + private readonly IResourceStore? textureStore; + + public UpscaledTextureLoaderStore(IResourceStore? textureStore) + { + this.textureStore = textureStore; + } + + public void Dispose() + { + textureStore?.Dispose(); + } + + public TextureUpload Get(string name) + { + var textureUpload = textureStore?.Get(name); + + // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp. + if (textureUpload == null) + return null!; + + return upscale(textureUpload); + } + + public async Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) + { + // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp. + if (textureStore == null) + return null!; + + var textureUpload = await textureStore.GetAsync(name, cancellationToken).ConfigureAwait(false); + + if (textureUpload == null) + return null!; + + return await Task.Run(() => upscale(textureUpload), cancellationToken).ConfigureAwait(false); + } + + private TextureUpload upscale(TextureUpload textureUpload) + { + var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height); + + // The original texture upload will no longer be returned or used. + textureUpload.Dispose(); + + image.Mutate(i => i.Resize(new Size(textureUpload.Width, textureUpload.Height) * scale_factor)); + return new TextureUpload(image); + } + + public Stream? GetStream(string name) => textureStore?.GetStream(name); + + public IEnumerable GetAvailableResources() => textureStore?.GetAvailableResources() ?? Array.Empty(); + } + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index d16f51f36e..fea7456472 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -170,7 +170,16 @@ namespace osu.Game.Tests.Visual.Gameplay ManualClock clock = null; var beatmap = new Beatmap(); - beatmap.HitObjects.Add(new TestHitObjectWithNested { Duration = 40 }); + beatmap.HitObjects.Add(new TestHitObjectWithNested + { + Duration = 40, + NestedObjects = new HitObject[] + { + new PooledNestedHitObject { StartTime = 10 }, + new PooledNestedHitObject { StartTime = 20 }, + new PooledNestedHitObject { StartTime = 30 } + } + }); createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock())); @@ -209,6 +218,49 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("object judged", () => playfield.JudgedObjects.Count == 1); } + [Test] + public void TestPooledObjectWithNonPooledNesteds() + { + ManualClock clock = null; + TestHitObjectWithNested hitObjectWithNested; + + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(hitObjectWithNested = new TestHitObjectWithNested + { + Duration = 40, + NestedObjects = new HitObject[] + { + new PooledNestedHitObject { StartTime = 10 }, + new NonPooledNestedHitObject { StartTime = 20 }, + new NonPooledNestedHitObject { StartTime = 30 } + } + }); + + createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock())); + + AddAssert("hitobject entry has all nesteds", () => playfield.HitObjectContainer.Entries.Single().NestedEntries, () => Has.Count.EqualTo(3)); + + AddStep("skip to middle of object", () => clock.CurrentTime = (hitObjectWithNested.StartTime + hitObjectWithNested.GetEndTime()) / 2); + AddAssert("2 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(2)); + AddAssert("entry not all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.False); + + AddStep("skip to before end of object", () => clock.CurrentTime = hitObjectWithNested.GetEndTime() - 1); + AddAssert("3 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3)); + AddAssert("entry not all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.False); + + AddStep("removing object doesn't crash", () => playfield.Remove(hitObjectWithNested)); + AddStep("clear judged", () => playfield.JudgedObjects.Clear()); + + AddStep("add object back", () => playfield.Add(hitObjectWithNested)); + AddAssert("entry not all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.False); + + AddStep("skip to long past object", () => clock.CurrentTime = 100_000); + // the parent entry should still be linked to nested entries of pooled objects that are managed externally + // but not contain synthetic entries that were created for the non-pooled objects. + AddAssert("entry still has non-synthetic nested entries", () => playfield.HitObjectContainer.Entries.Single().NestedEntries, () => Has.Count.EqualTo(1)); + AddAssert("entry all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.True); + } + private void createTest(IBeatmap beatmap, int poolSize, Func createClock = null) { AddStep("create test", () => @@ -289,7 +341,7 @@ namespace osu.Game.Tests.Visual.Gameplay RegisterPool(poolSize); RegisterPool(poolSize); RegisterPool(poolSize); - RegisterPool(poolSize); + RegisterPool(poolSize); } protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); @@ -422,16 +474,22 @@ namespace osu.Game.Tests.Visual.Gameplay private class TestHitObjectWithNested : TestHitObject { + public IEnumerable NestedObjects { get; init; } = Array.Empty(); + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { base.CreateNestedHitObjects(cancellationToken); - for (int i = 0; i < 3; ++i) - AddNested(new NestedHitObject { StartTime = (float)Duration * (i + 1) / 4 }); + foreach (var ho in NestedObjects) + AddNested(ho); } } - private class NestedHitObject : ConvertHitObject + private class PooledNestedHitObject : ConvertHitObject + { + } + + private class NonPooledNestedHitObject : ConvertHitObject { } @@ -482,6 +540,9 @@ namespace osu.Game.Tests.Visual.Gameplay nestedContainer.Clear(false); } + protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) + => hitObject is NonPooledNestedHitObject nonPooled ? new DrawableNestedHitObject(nonPooled) : null; + protected override void CheckForResult(bool userTriggered, double timeOffset) { base.CheckForResult(userTriggered, timeOffset); @@ -490,25 +551,30 @@ namespace osu.Game.Tests.Visual.Gameplay } } - private partial class DrawableNestedHitObject : DrawableHitObject + private partial class DrawableNestedHitObject : DrawableHitObject { public DrawableNestedHitObject() - : this(null) { } - public DrawableNestedHitObject(NestedHitObject hitObject) + public DrawableNestedHitObject(PooledNestedHitObject hitObject) + : base(hitObject) + { + } + + public DrawableNestedHitObject(NonPooledNestedHitObject hitObject) : base(hitObject) { - Size = new Vector2(15); - Colour = Colour4.White; - RelativePositionAxes = Axes.Both; - Origin = Anchor.Centre; } [BackgroundDependencyLoader] private void load() { + Size = new Vector2(15); + Colour = Colour4.White; + RelativePositionAxes = Axes.Both; + Origin = Anchor.Centre; + AddInternal(new Circle { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs index bf9b13b320..94a25d064c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { AddUntilStep("score above zero", () => ((ScoreAccessibleReplayPlayer)Player).ScoreProcessor.TotalScore.Value > 0); - AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Counters.Any(kc => kc.CountPresses.Value > 0)); + AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.InputCountController.Triggers.Any(kc => kc.ActivationCount.Value > 0)); AddAssert("cannot fail", () => !((ScoreAccessibleReplayPlayer)Player).AllowFail); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index 6ccf73d8ff..5b32f380b9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -213,7 +213,7 @@ namespace osu.Game.Tests.Visual.Gameplay OnlineID = hasOnlineId ? online_score_id : 0, Ruleset = new OsuRuleset().RulesetInfo, BeatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(), - Hash = replayAvailable ? "online" : string.Empty, + HasOnlineReplay = replayAvailable, User = new APIUser { Id = 39828, diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index 65b409a6f7..c51883b221 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -220,7 +220,7 @@ namespace osu.Game.Tests.Visual.Gameplay public partial class TestInputConsumer : CompositeDrawable, IKeyBindingHandler { - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent.ReceivePositionalInputAt(screenSpacePos); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent!.ReceivePositionalInputAt(screenSpacePos); private readonly Box box; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs deleted file mode 100644 index 2b378c8013..0000000000 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs +++ /dev/null @@ -1,499 +0,0 @@ -// 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.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays.Settings; -using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Beatmaps; -using osu.Game.Rulesets.Osu.Judgements; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring.Legacy; -using osuTK; -using osuTK.Graphics; -using osuTK.Input; - -namespace osu.Game.Tests.Visual.Gameplay -{ - public partial class TestSceneScoring : OsuTestScene - { - private GraphContainer graphs = null!; - private SettingsSlider sliderMaxCombo = null!; - - private FillFlowContainer legend = null!; - - [Test] - public void TestBasic() - { - AddStep("setup tests", () => - { - Children = new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] - { - graphs = new GraphContainer - { - RelativeSizeAxes = Axes.Both, - }, - }, - new Drawable[] - { - legend = new FillFlowContainer - { - Padding = new MarginPadding(20), - Direction = FillDirection.Full, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }, - }, - new Drawable[] - { - new FillFlowContainer - { - Padding = new MarginPadding(20), - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Full, - Children = new Drawable[] - { - sliderMaxCombo = new SettingsSlider - { - Width = 0.5f, - TransferValueOnCommit = true, - Current = new BindableInt(1024) - { - MinValue = 96, - MaxValue = 8192, - }, - LabelText = "max combo", - }, - new OsuTextFlowContainer - { - RelativeSizeAxes = Axes.X, - Width = 0.5f, - AutoSizeAxes = Axes.Y, - Text = $"Left click to add miss\nRight click to add OK/{base_ok}" - } - } - }, - }, - } - } - }; - - sliderMaxCombo.Current.BindValueChanged(_ => rerun()); - - graphs.MissLocations.BindCollectionChanged((_, __) => rerun()); - graphs.NonPerfectLocations.BindCollectionChanged((_, __) => rerun()); - - graphs.MaxCombo.BindTo(sliderMaxCombo.Current); - - rerun(); - }); - } - - private const int base_great = 300; - private const int base_ok = 100; - - private void rerun() - { - graphs.Clear(); - legend.Clear(); - - runForProcessor("lazer-standardised", Color4.YellowGreen, new ScoreProcessor(new OsuRuleset()), ScoringMode.Standardised); - runForProcessor("lazer-classic", Color4.MediumPurple, new ScoreProcessor(new OsuRuleset()), ScoringMode.Classic); - - runScoreV1(); - runScoreV2(); - } - - private void runScoreV1() - { - int totalScore = 0; - int currentCombo = 0; - - void applyHitV1(int baseScore) - { - if (baseScore == 0) - { - currentCombo = 0; - return; - } - - const float score_multiplier = 1; - - totalScore += baseScore; - - // combo multiplier - // ReSharper disable once PossibleLossOfFraction - totalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * score_multiplier)); - - currentCombo++; - } - - runForAlgorithm("ScoreV1 (classic)", Color4.Purple, - () => applyHitV1(base_great), - () => applyHitV1(base_ok), - () => applyHitV1(0), - () => - { - // Arbitrary value chosen towards the upper range. - const double score_multiplier = 4; - - return (int)(totalScore * score_multiplier); - }); - } - - private void runScoreV2() - { - int maxCombo = sliderMaxCombo.Current.Value; - - int currentCombo = 0; - double comboPortion = 0; - double currentBaseScore = 0; - double maxBaseScore = 0; - int currentHits = 0; - - for (int i = 0; i < maxCombo; i++) - applyHitV2(base_great); - - double comboPortionMax = comboPortion; - - currentCombo = 0; - comboPortion = 0; - currentBaseScore = 0; - maxBaseScore = 0; - currentHits = 0; - - void applyHitV2(int baseScore) - { - maxBaseScore += base_great; - currentBaseScore += baseScore; - comboPortion += baseScore * (1 + ++currentCombo / 10.0); - - currentHits++; - } - - runForAlgorithm("ScoreV2", Color4.OrangeRed, - () => applyHitV2(base_great), - () => applyHitV2(base_ok), - () => - { - currentHits++; - maxBaseScore += base_great; - currentCombo = 0; - }, () => - { - double accuracy = currentBaseScore / maxBaseScore; - - return (int)Math.Round - ( - 700000 * comboPortion / comboPortionMax + - 300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo) - ); - }); - } - - private void runForProcessor(string name, Color4 colour, ScoreProcessor processor, ScoringMode mode) - { - int maxCombo = sliderMaxCombo.Current.Value; - - var beatmap = new OsuBeatmap(); - for (int i = 0; i < maxCombo; i++) - beatmap.HitObjects.Add(new HitCircle()); - - processor.ApplyBeatmap(beatmap); - - runForAlgorithm(name, colour, - () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }), - () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }), - () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }), - () => processor.GetDisplayScore(mode)); - } - - private void runForAlgorithm(string name, Color4 colour, Action applyHit, Action applyNonPerfect, Action applyMiss, Func getTotalScore) - { - int maxCombo = sliderMaxCombo.Current.Value; - - List results = new List(); - - for (int i = 0; i < maxCombo; i++) - { - if (graphs.MissLocations.Contains(i)) - applyMiss(); - else if (graphs.NonPerfectLocations.Contains(i)) - applyNonPerfect(); - else - applyHit(); - - results.Add(getTotalScore()); - } - - graphs.Add(new LineGraph - { - Name = name, - RelativeSizeAxes = Axes.Both, - LineColour = colour, - Values = results - }); - - legend.Add(new OsuSpriteText - { - Colour = colour, - RelativeSizeAxes = Axes.X, - Width = 0.5f, - Text = $"{FontAwesome.Solid.Circle.Icon} {name}" - }); - - legend.Add(new OsuSpriteText - { - Colour = colour, - RelativeSizeAxes = Axes.X, - Width = 0.5f, - Text = $"final score {getTotalScore():#,0}" - }); - } - } - - public partial class GraphContainer : Container, IHasCustomTooltip> - { - public readonly BindableList MissLocations = new BindableList(); - public readonly BindableList NonPerfectLocations = new BindableList(); - - public Bindable MaxCombo = new Bindable(); - - protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; - - private readonly Box hoverLine; - - private readonly Container missLines; - private readonly Container verticalGridLines; - - public int CurrentHoverCombo { get; private set; } - - public GraphContainer() - { - InternalChild = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = OsuColour.Gray(0.1f), - RelativeSizeAxes = Axes.Both, - }, - verticalGridLines = new Container - { - RelativeSizeAxes = Axes.Both, - }, - hoverLine = new Box - { - Colour = Color4.Yellow, - RelativeSizeAxes = Axes.Y, - Origin = Anchor.TopCentre, - Alpha = 0, - Width = 1, - }, - missLines = new Container - { - Alpha = 0.6f, - RelativeSizeAxes = Axes.Both, - }, - Content, - } - }; - - MissLocations.BindCollectionChanged((_, _) => updateMissLocations()); - NonPerfectLocations.BindCollectionChanged((_, _) => updateMissLocations()); - - MaxCombo.BindValueChanged(_ => - { - updateMissLocations(); - updateVerticalGridLines(); - }, true); - } - - private void updateVerticalGridLines() - { - verticalGridLines.Clear(); - - for (int i = 0; i < MaxCombo.Value; i++) - { - if (i % 100 == 0) - { - verticalGridLines.AddRange(new Drawable[] - { - new Box - { - Colour = OsuColour.Gray(0.2f), - Origin = Anchor.TopCentre, - Width = 1, - RelativeSizeAxes = Axes.Y, - RelativePositionAxes = Axes.X, - X = (float)i / MaxCombo.Value, - }, - new OsuSpriteText - { - RelativePositionAxes = Axes.X, - X = (float)i / MaxCombo.Value, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Text = $"{i:#,0}", - Rotation = -30, - Y = -20, - } - }); - } - } - } - - private void updateMissLocations() - { - missLines.Clear(); - - foreach (int miss in MissLocations) - { - missLines.Add(new Box - { - Colour = Color4.Red, - Origin = Anchor.TopCentre, - Width = 1, - RelativeSizeAxes = Axes.Y, - RelativePositionAxes = Axes.X, - X = (float)miss / MaxCombo.Value, - }); - } - - foreach (int miss in NonPerfectLocations) - { - missLines.Add(new Box - { - Colour = Color4.Orange, - Origin = Anchor.TopCentre, - Width = 1, - RelativeSizeAxes = Axes.Y, - RelativePositionAxes = Axes.X, - X = (float)miss / MaxCombo.Value, - }); - } - } - - protected override bool OnHover(HoverEvent e) - { - hoverLine.Show(); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - hoverLine.Hide(); - base.OnHoverLost(e); - } - - protected override bool OnMouseMove(MouseMoveEvent e) - { - CurrentHoverCombo = (int)(e.MousePosition.X / DrawWidth * MaxCombo.Value); - - hoverLine.X = e.MousePosition.X; - return base.OnMouseMove(e); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - if (e.Button == MouseButton.Left) - MissLocations.Add(CurrentHoverCombo); - else - NonPerfectLocations.Add(CurrentHoverCombo); - - return true; - } - - private GraphTooltip? tooltip; - - public ITooltip> GetCustomTooltip() => tooltip ??= new GraphTooltip(this); - - public IEnumerable TooltipContent => Content.OfType(); - - public partial class GraphTooltip : CompositeDrawable, ITooltip> - { - private readonly GraphContainer graphContainer; - - private readonly OsuTextFlowContainer textFlow; - - public GraphTooltip(GraphContainer graphContainer) - { - this.graphContainer = graphContainer; - AutoSizeAxes = Axes.Both; - - Masking = true; - CornerRadius = 10; - - InternalChildren = new Drawable[] - { - new Box - { - Colour = OsuColour.Gray(0.15f), - RelativeSizeAxes = Axes.Both, - }, - textFlow = new OsuTextFlowContainer - { - Colour = Color4.White, - AutoSizeAxes = Axes.Both, - Padding = new MarginPadding(10), - } - }; - } - - private int? lastContentCombo; - - public void SetContent(IEnumerable content) - { - int relevantCombo = graphContainer.CurrentHoverCombo; - - if (lastContentCombo == relevantCombo) - return; - - lastContentCombo = relevantCombo; - textFlow.Clear(); - - textFlow.AddParagraph($"At combo {relevantCombo}:"); - - foreach (var graph in content) - { - float valueAtHover = graph.Values.ElementAt(relevantCombo); - float ofTotal = valueAtHover / graph.Values.Last(); - - textFlow.AddParagraph($"{graph.Name}: {valueAtHover:#,0} ({ofTotal * 100:N0}% of final)\n", st => st.Colour = graph.LineColour); - } - } - - public void Move(Vector2 pos) => this.MoveTo(pos); - } - } -} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 7b37b6624d..4e5db5d46e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -138,24 +138,28 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestCyclicSelection() { - SkinBlueprint[] blueprints = null!; + List blueprints = new List(); - AddStep("Add big black boxes", () => + AddStep("clear list", () => blueprints.Clear()); + + for (int i = 0; i < 3; i++) { - InputManager.MoveMouseTo(skinEditor.ChildrenOfType().First()); - InputManager.Click(MouseButton.Left); - InputManager.Click(MouseButton.Left); - InputManager.Click(MouseButton.Left); - }); + AddStep("Add big black box", () => + { + InputManager.MoveMouseTo(skinEditor.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + + AddStep("store box", () => + { + // Add blueprints one-by-one so we have a stable order for testing reverse cyclic selection against. + blueprints.Add(skinEditor.ChildrenOfType().Single(s => s.IsSelected)); + }); + } AddAssert("Three black boxes added", () => targetContainer.Components.OfType().Count(), () => Is.EqualTo(3)); - AddStep("Store black box blueprints", () => - { - blueprints = skinEditor.ChildrenOfType().Where(b => b.Item is BigBlackBox).ToArray(); - }); - - AddAssert("Selection is black box 1", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item)); + AddAssert("Selection is last", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[2].Item)); AddStep("move cursor to black box", () => { @@ -164,13 +168,13 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left)); - AddAssert("Selection is black box 2", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[1].Item)); + AddAssert("Selection is second last", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[1].Item)); AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left)); - AddAssert("Selection is black box 3", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[2].Item)); + AddAssert("Selection is last", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item)); AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left)); - AddAssert("Selection is black box 1", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item)); + AddAssert("Selection is first", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[2].Item)); AddStep("select all boxes", () => { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs index 2ae5e6f998..b7b2a6c175 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -10,6 +8,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Skinning; namespace osu.Game.Tests.Visual.Gameplay { @@ -21,7 +20,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestToggleEditor() { - AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox + var skinComponentsContainer = new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.SongSelect)); + + AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox(skinComponentsContainer, null) { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index 4ae115a68d..656873e9ed 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -4,10 +4,10 @@ #nullable disable using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; -using osu.Framework.Timing; using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Gameplay public partial class TestSceneSkinEditorMultipleSkins : SkinnableTestScene { [Cached(typeof(ScoreProcessor))] - private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor; + private ScoreProcessor scoreProcessor { get; set; } [Cached(typeof(HealthProcessor))] private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); @@ -32,11 +32,16 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached(typeof(IGameplayClock))] - private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock()); + private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new TrackVirtual(60000), false, false); [Cached] public readonly EditorClipboard Clipboard = new EditorClipboard(); + public TestSceneSkinEditorMultipleSkins() + { + scoreProcessor = gameplayState.ScoreProcessor; + } + [SetUpSteps] public void SetUpSteps() { @@ -58,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay }; // Add any key just to display the key counter visually. - hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space)); + hudOverlay.InputCountController.Add(new KeyCounterKeyboardTrigger(Key.Space)); scoreProcessor.Combo.Value = 1; return new Container diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs index 6f079778c5..59a1f938e6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs index 93fa953ef4..0c351a93bb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index 89432940ba..4cb0d5c0ff 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -8,17 +8,18 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; -using osu.Framework.Timing; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; +using osu.Game.Skinning; using osu.Game.Tests.Gameplay; using osuTK.Input; @@ -29,7 +30,7 @@ namespace osu.Game.Tests.Visual.Gameplay private HUDOverlay hudOverlay; [Cached(typeof(ScoreProcessor))] - private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor; + private ScoreProcessor scoreProcessor { get; set; } [Cached(typeof(HealthProcessor))] private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); @@ -38,13 +39,18 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached(typeof(IGameplayClock))] - private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock()); + private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new TrackVirtual(60000), false, false); private IEnumerable hudOverlays => CreatedDrawables.OfType(); // best way to check without exposing. - private Drawable hideTarget => hudOverlay.KeyCounter; - private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single(); + private Drawable hideTarget => hudOverlay.ChildrenOfType().First(); + private Drawable keyCounterFlow => hudOverlay.ChildrenOfType().First().ChildrenOfType>().Single(); + + public TestSceneSkinnableHUDOverlay() + { + scoreProcessor = gameplayState.ScoreProcessor; + } [Test] public void TestComboCounterIncrementing() @@ -62,7 +68,6 @@ namespace osu.Game.Tests.Visual.Gameplay float? initialAlpha = null; createNew(h => h.OnLoadComplete += _ => initialAlpha = hideTarget.Alpha); - AddUntilStep("wait for load", () => hudOverlay.IsAlive); AddAssert("initial alpha was less than 1", () => initialAlpha < 1); } @@ -73,7 +78,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false)); - AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); + AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0)); AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent); // Key counter flow container should not be affected by this, only the key counter display will be hidden as checked above. @@ -89,13 +94,16 @@ namespace osu.Game.Tests.Visual.Gameplay hudOverlay = new HUDOverlay(null, Array.Empty()); // Add any key just to display the key counter visually. - hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space)); + hudOverlay.InputCountController.Add(new KeyCounterKeyboardTrigger(Key.Space)); action?.Invoke(hudOverlay); return hudOverlay; }); }); + AddUntilStep("HUD overlay loaded", () => hudOverlay.IsAlive); + AddUntilStep("components container loaded", + () => hudOverlay.ChildrenOfType().Any(scc => scc.ComponentsLoaded)); } protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs index 7f6c9d7804..8d3eee2445 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs @@ -1,18 +1,18 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Tests.Visual.Gameplay { @@ -21,8 +21,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(HealthProcessor))] private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); - protected override Drawable CreateDefaultImplementation() => new DefaultHealthDisplay(); - protected override Drawable CreateLegacyImplementation() => new LegacyHealthDisplay(); + protected override Drawable CreateArgonImplementation() => new ArgonHealthDisplay { Scale = new Vector2(0.6f) }; + protected override Drawable CreateDefaultImplementation() => new DefaultHealthDisplay { Scale = new Vector2(0.6f) }; + protected override Drawable CreateLegacyImplementation() => new LegacyHealthDisplay { Scale = new Vector2(0.6f) }; [SetUpSteps] public void SetUpSteps() @@ -30,15 +31,21 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep(@"Reset all", delegate { healthProcessor.Health.Value = 1; + healthProcessor.Failed += () => false; // health won't be updated if the processor gets into a "fail" state. }); } [Test] public void TestHealthDisplayIncrementing() { - AddRepeatStep(@"decrease hp", delegate + AddRepeatStep("apply miss judgement", delegate { - healthProcessor.Health.Value -= 0.08f; + healthProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }); + }, 5); + + AddRepeatStep(@"decrease hp slightly", delegate + { + healthProcessor.Health.Value -= 0.01f; }, 10); AddRepeatStep(@"increase hp without flash", delegate diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs index 2cb3303dd6..7079b93d3e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs index dfa9fdf03b..635d9f9604 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs @@ -185,6 +185,37 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("shorten to -10 length", () => path.ExpectedDistance.Value = -10); } + [Test] + public void TestGetSegmentEnds() + { + var positions = new[] + { + Vector2.Zero, + new Vector2(100, 0), + new Vector2(100), + new Vector2(200, 100), + }; + double[] distances = { 100d, 200d, 300d }; + + AddStep("create path", () => path.ControlPoints.AddRange(positions.Select(p => new PathControlPoint(p, PathType.Linear)))); + AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 300))); + AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(positions.Skip(1))); + + AddStep("lengthen last segment", () => path.ExpectedDistance.Value = 400); + AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 400))); + AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(positions.Skip(1))); + + AddStep("shorten last segment", () => path.ExpectedDistance.Value = 150); + AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 150))); + // see remarks in `GetSegmentEnds()` xmldoc (`SliderPath.PositionAt()` clamps progress to [0,1]). + AddAssert("segment end positions not recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(new[] + { + positions[1], + new Vector2(100, 50), + new Vector2(100, 50), + })); + } + private List createSegment(PathType type, params Vector2[] controlPoints) { var points = controlPoints.Select(p => new PathControlPoint { Position = p }).ToList(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs index 5855838d3c..e975a85401 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs @@ -7,12 +7,15 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; +using osuTK.Graphics; namespace osu.Game.Tests.Visual.Gameplay { @@ -21,6 +24,8 @@ namespace osu.Game.Tests.Visual.Gameplay { private GameplayClockContainer gameplayClockContainer = null!; + private Box background = null!; + private const double skip_target_time = -2000; [BackgroundDependencyLoader] @@ -30,11 +35,20 @@ namespace osu.Game.Tests.Visual.Gameplay FrameStabilityContainer frameStabilityContainer; - Add(gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time) + AddRange(new Drawable[] { - Child = frameStabilityContainer = new FrameStabilityContainer + background = new Box { - MaxCatchUpFrames = 1 + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue + }, + gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time) + { + Child = frameStabilityContainer = new FrameStabilityContainer + { + MaxCatchUpFrames = 1 + } } }); @@ -71,9 +85,20 @@ namespace osu.Game.Tests.Visual.Gameplay applyToArgonProgress(s => s.ShowGraph.Value = b); }); + AddStep("set white background", () => background.FadeColour(Color4.White, 200, Easing.OutQuint)); + AddStep("randomise background colour", () => background.FadeColour(new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1), 200, Easing.OutQuint)); + AddStep("stop", gameplayClockContainer.Stop); } + [Test] + public void TestSeekToKnownTime() + { + AddStep("seek to known time", () => gameplayClockContainer.Seek(60000)); + AddWaitStep("wait some for seek", 15); + AddStep("stop", () => gameplayClockContainer.Stop()); + } + private void applyToArgonProgress(Action action) => this.ChildrenOfType().ForEach(action); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs index 1c09c29748..f1ee5cc414 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Screens; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 794860b9ec..3a5b3864af 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -259,7 +259,7 @@ namespace osu.Game.Tests.Visual.Gameplay public partial class TestInputConsumer : CompositeDrawable, IKeyBindingHandler { - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent.ReceivePositionalInputAt(screenSpacePos); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent!.ReceivePositionalInputAt(screenSpacePos); private readonly Box box; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs index 699c8ea20a..b002e90bb0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Utils; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index a6663f3086..893b9f11f4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -106,14 +106,12 @@ namespace osu.Game.Tests.Visual.Gameplay if (storyboard != null) storyboardContainer.Remove(storyboard, true); - var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; - storyboardContainer.Clock = decoupledClock; + storyboardContainer.Clock = new FramedClock(Beatmap.Value.Track); storyboard = toLoad.CreateDrawable(SelectedMods.Value); storyboard.Passing = false; storyboardContainer.Add(storyboard); - decoupledClock.ChangeSource(Beatmap.Value.Track); } private void loadStoryboard(string filename, Action? setUpStoryboard = null) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs index a9d4508f70..11dc0f9c30 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs @@ -41,6 +41,7 @@ namespace osu.Game.Tests.Visual.Gameplay backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: -7000, volume: 20)); backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: -5000, volume: 20)); backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 0, volume: 20)); + backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 2000, volume: 20)); } [SetUp] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index 283866bef2..98825b27d4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -72,12 +72,12 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestStoryboardExitDuringOutroStillExits() + public void TestStoryboardExitDuringOutroProgressesToResults() { CreateTest(); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddStep("exit via pause", () => Player.ExitViaPause()); - AddAssert("player exited", () => !Player.IsCurrentScreen() && Player.GetChildScreen() == null); + AddUntilStep("reached results screen", () => Stack.CurrentScreen is ResultsScreen); } [TestCase(false)] @@ -171,7 +171,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("disable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, false)); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddStep("exit via pause", () => Player.ExitViaPause()); - AddAssert("player exited", () => Stack.CurrentScreen == null); + AddUntilStep("reached results screen", () => Stack.CurrentScreen is ResultsScreen); } [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneUnknownMod.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneUnknownMod.cs index cb5631e599..b02e180715 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneUnknownMod.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneUnknownMod.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -22,7 +20,6 @@ namespace osu.Game.Tests.Visual.Gameplay { CreateModTest(new ModTestData { - Beatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo).Beatmap, Mod = new UnknownMod("WNG"), PassCondition = () => Player.IsLoaded && !Player.LoadedBeatmapSuccessfully }); diff --git a/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs b/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs index 45e5a7c270..fb82b0df80 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs index 0c024248ea..c0ced9057a 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.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 NUnit.Framework; using osu.Game.Screens.Menu; diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs index 23373892d1..e8859400d5 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Screens.Menu; diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs index 2ccf184525..cb4a52a3b9 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.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 NUnit.Framework; using osu.Framework.Utils; using osu.Game.Screens.Menu; diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs new file mode 100644 index 0000000000..0bc71924ce --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs @@ -0,0 +1,89 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Overlays; +using osu.Game.Users.Drawables; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Menus +{ + [TestFixture] + public partial class TestSceneLoginOverlay : OsuManualInputManagerTestScene + { + private LoginOverlay loginOverlay = null!; + + [BackgroundDependencyLoader] + private void load() + { + Child = loginOverlay = new LoginOverlay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("show login overlay", () => loginOverlay.Show()); + } + + [Test] + public void TestLoginSuccess() + { + AddStep("logout", () => API.Logout()); + + AddStep("enter password", () => loginOverlay.ChildrenOfType().First().Text = "password"); + AddStep("submit", () => loginOverlay.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); + } + + [Test] + public void TestLoginFailure() + { + AddStep("logout", () => + { + API.Logout(); + ((DummyAPIAccess)API).FailNextLogin(); + }); + + AddStep("enter password", () => loginOverlay.ChildrenOfType().First().Text = "password"); + AddStep("submit", () => loginOverlay.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); + } + + [Test] + public void TestLoginConnecting() + { + AddStep("logout", () => + { + API.Logout(); + ((DummyAPIAccess)API).PauseOnConnectingNextLogin(); + }); + + AddStep("enter password", () => loginOverlay.ChildrenOfType().First().Text = "password"); + AddStep("submit", () => loginOverlay.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); + } + + [Test] + public void TestClickingOnFlagClosesOverlay() + { + AddStep("logout", () => API.Logout()); + AddStep("enter password", () => loginOverlay.ChildrenOfType().First().Text = "password"); + AddStep("submit", () => loginOverlay.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); + + AddStep("click on flag", () => + { + InputManager.MoveMouseTo(loginOverlay.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("login overlay is hidden", () => loginOverlay.State.Value == Visibility.Hidden); + } + } +} diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs deleted file mode 100644 index 738220f5ce..0000000000 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs +++ /dev/null @@ -1,78 +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 System.Linq; -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Testing; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API; -using osu.Game.Overlays.Login; -using osu.Game.Users.Drawables; -using osuTK.Input; - -namespace osu.Game.Tests.Visual.Menus -{ - [TestFixture] - public partial class TestSceneLoginPanel : OsuManualInputManagerTestScene - { - private LoginPanel loginPanel; - private int hideCount; - - [SetUpSteps] - public void SetUpSteps() - { - AddStep("create login dialog", () => - { - Add(loginPanel = new LoginPanel - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 0.5f, - RequestHide = () => hideCount++, - }); - }); - } - - [Test] - public void TestLoginSuccess() - { - AddStep("logout", () => API.Logout()); - - AddStep("enter password", () => loginPanel.ChildrenOfType().First().Text = "password"); - AddStep("submit", () => loginPanel.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); - } - - [Test] - public void TestLoginFailure() - { - AddStep("logout", () => - { - API.Logout(); - ((DummyAPIAccess)API).FailNextLogin(); - }); - - AddStep("enter password", () => loginPanel.ChildrenOfType().First().Text = "password"); - AddStep("submit", () => loginPanel.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); - } - - [Test] - public void TestClickingOnFlagClosesPanel() - { - AddStep("reset hide count", () => hideCount = 0); - - AddStep("logout", () => API.Logout()); - AddStep("enter password", () => loginPanel.ChildrenOfType().First().Text = "password"); - AddStep("submit", () => loginPanel.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); - - AddStep("click on flag", () => - { - InputManager.MoveMouseTo(loginPanel.ChildrenOfType().First()); - InputManager.Click(MouseButton.Left); - }); - AddAssert("hide requested", () => hideCount == 1); - } - } -} diff --git a/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs b/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs index e5e092b382..4f01dcffd9 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; diff --git a/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs b/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs index c54c66df7e..e4add64da2 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays; diff --git a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs new file mode 100644 index 0000000000..bb327e5962 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual.Menus +{ + [TestFixture] + public partial class TestSceneStarFountain : OsuTestScene + { + [SetUpSteps] + public void SetUpSteps() + { + AddStep("make fountains", () => + { + Children = new[] + { + new StarFountain + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + X = 200, + }, + new StarFountain + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + X = -200, + }, + }; + }); + } + + [Test] + public void TestPew() + { + AddRepeatStep("activate fountains sometimes", () => + { + foreach (var fountain in Children.OfType()) + { + if (RNG.NextSingle() > 0.8f) + fountain.Shoot(RNG.Next(-1, 2)); + } + }, 150); + } + } +} diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs index 22c7bb64b2..ce9f80a84f 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using Moq; @@ -250,6 +251,8 @@ namespace osu.Game.Tests.Visual.Menus } public virtual IBindable UnreadCount => null; + + public IEnumerable AllNotifications => Enumerable.Empty(); } } } diff --git a/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs b/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs index 49256c7a01..f4732234a7 100644 --- a/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs +++ b/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs index 45f671618e..66ba908879 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs @@ -8,6 +8,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Testing; @@ -57,11 +58,36 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("customisation area not expanded", () => this.ChildrenOfType().Single().Height == 0); } + [Test] + public void TestSelectAllButtonUpdatesStateWhenSearchTermChanged() + { + createFreeModSelect(); + + AddStep("apply search term", () => freeModSelectOverlay.SearchTerm = "ea"); + + AddAssert("select all button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + + AddStep("click select all button", navigateAndClick); + AddAssert("select all button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + + AddStep("change search term", () => freeModSelectOverlay.SearchTerm = "e"); + + AddAssert("select all button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + + void navigateAndClick() where T : Drawable + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + } + } + [Test] public void TestSelectDeselectAllViaKeyboard() { createFreeModSelect(); + AddStep("kill search bar focus", () => freeModSelectOverlay.SearchTextBox.KillFocus()); + AddStep("press ctrl+a", () => InputManager.Keys(PlatformAction.SelectAll)); AddUntilStep("all mods selected", assertAllAvailableModsSelected); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs index 979cb4424e..d1a914300f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs @@ -84,12 +84,12 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] - public void TestFocusOnTabKeyWhenExpanded() + public void TestFocusOnEnterKeyWhenExpanded() { setLocalUserPlaying(true); assertChatFocused(false); - AddStep("press tab", () => InputManager.Key(Key.Tab)); + AddStep("press enter", () => InputManager.Key(Key.Enter)); assertChatFocused(true); } @@ -99,19 +99,19 @@ namespace osu.Game.Tests.Visual.Multiplayer setLocalUserPlaying(true); assertChatFocused(false); - AddStep("press tab", () => InputManager.Key(Key.Tab)); + AddStep("press enter", () => InputManager.Key(Key.Enter)); assertChatFocused(true); AddStep("press escape", () => InputManager.Key(Key.Escape)); assertChatFocused(false); } [Test] - public void TestFocusOnTabKeyWhenNotExpanded() + public void TestFocusOnEnterKeyWhenNotExpanded() { AddStep("set not expanded", () => chatDisplay.Expanded.Value = false); AddUntilStep("is not visible", () => !chatDisplay.IsPresent); - AddStep("press tab", () => InputManager.Key(Key.Tab)); + AddStep("press enter", () => InputManager.Key(Key.Enter)); assertChatFocused(true); AddUntilStep("is visible", () => chatDisplay.IsPresent); @@ -120,21 +120,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("is not visible", () => !chatDisplay.IsPresent); } - [Test] - public void TestFocusToggleViaAction() - { - AddStep("set not expanded", () => chatDisplay.Expanded.Value = false); - AddUntilStep("is not visible", () => !chatDisplay.IsPresent); - - AddStep("press tab", () => InputManager.Key(Key.Tab)); - assertChatFocused(true); - AddUntilStep("is visible", () => chatDisplay.IsPresent); - - AddStep("press tab", () => InputManager.Key(Key.Tab)); - assertChatFocused(false); - AddUntilStep("is not visible", () => !chatDisplay.IsPresent); - } - private void assertChatFocused(bool isFocused) => AddAssert($"chat {(isFocused ? "focused" : "not focused")}", () => textBox.HasFocus == isFocused); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index 63a0ada3dc..24d1b51ff8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Game.Online.API; using osu.Game.Online.Rooms; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs index defb3006cc..ea8fe8873d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Graphics; using osu.Game.Online.API; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs index 3d85a47ca9..46d409e6f1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.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 NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Screens; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 049c02ffde..4bf2ebc1a4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -49,6 +49,8 @@ namespace osu.Game.Tests.Visual.Multiplayer LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray()) { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Expanded = { Value = true } }, Add); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index e09496b6e9..cebc75f90c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -65,6 +65,47 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("clear playing users", () => playingUsers.Clear()); } + [TestCase(1)] + [TestCase(4)] + [TestCase(9)] + public void TestGeneral(int count) + { + int[] userIds = getPlayerIds(count); + + start(userIds); + loadSpectateScreen(); + + sendFrames(userIds, 1000); + AddWaitStep("wait a bit", 20); + } + + [TestCase(2)] + [TestCase(16)] + public void TestTeams(int count) + { + int[] userIds = getPlayerIds(count); + + start(userIds, teams: true); + loadSpectateScreen(); + + sendFrames(userIds, 1000); + AddWaitStep("wait a bit", 20); + } + + [Test] + public void TestMultipleStartRequests() + { + int[] userIds = getPlayerIds(2); + + start(userIds); + loadSpectateScreen(); + + sendFrames(userIds, 1000); + AddWaitStep("wait a bit", 20); + + start(userIds); + } + [Test] public void TestDelayedStart() { @@ -88,18 +129,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType().Count() == 2); } - [Test] - public void TestGeneral() - { - int[] userIds = getPlayerIds(4); - - start(userIds); - loadSpectateScreen(); - - sendFrames(userIds, 1000); - AddWaitStep("wait a bit", 20); - } - [Test] public void TestSpectatorPlayerInteractiveElementsHidden() { @@ -434,16 +463,18 @@ namespace osu.Game.Tests.Visual.Multiplayer private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId); - private void start(int[] userIds, int? beatmapId = null, APIMod[]? mods = null) + private void start(int[] userIds, int? beatmapId = null, APIMod[]? mods = null, bool teams = false) { AddStep("start play", () => { - foreach (int id in userIds) + for (int i = 0; i < userIds.Length; i++) { + int id = userIds[i]; var user = new MultiplayerRoomUser(id) { User = new APIUser { Id = id }, Mods = mods ?? Array.Empty(), + MatchState = teams ? new TeamVersusUserState { TeamID = i % 2 } : null, }; OnlinePlayDependencies.MultiplayerClient.AddUser(user, true); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 09624f63b7..16030d568b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -693,7 +693,9 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] - [FlakyTest] // See above + [Ignore("Failing too often, needs revisiting in some future.")] + // This test is failing even after 10 retries (see https://github.com/ppy/osu/actions/runs/6700910613/job/18208272419) + // Something is stopping the ready button from changing states, over multiple runs. public void TestGameplayExitFlow() { Bindable? holdDelay = null; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index a612167d57..bafe373d57 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs index 48f74cf308..37662ffce8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Graphics; using osu.Game.Online.Multiplayer; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs index d636373fbd..c2d3b17ccb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 9b130071cc..8dc41cd707 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -67,6 +67,14 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded); } + [Test] + public void TestSelectFreeMods() + { + 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()); + } + [Test] public void TestBeatmapConfirmed() { @@ -94,6 +102,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [TestCase(typeof(OsuModHidden), typeof(OsuModTraceable))] // Incompatible. public void TestAllowedModDeselectedWhenRequired(Type allowedMod, Type requiredMod) { + 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) }); @@ -102,17 +111,17 @@ namespace osu.Game.Tests.Visual.Multiplayer // A previous test's mod overlay could still be fading out. AddUntilStep("wait for only one freemod overlay", () => this.ChildrenOfType().Count() == 1); - assertHasFreeModButton(allowedMod, false); - assertHasFreeModButton(requiredMod, false); + assertFreeModNotShown(allowedMod); + assertFreeModNotShown(requiredMod); } - private void assertHasFreeModButton(Type type, bool hasButton = true) + private void assertFreeModNotShown(Type type) { - AddAssert($"{type.ReadableName()} {(hasButton ? "displayed" : "not displayed")} in freemod overlay", + AddAssert($"{type.ReadableName()} not displayed in freemod overlay", () => this.ChildrenOfType() .Single() .ChildrenOfType() - .Where(panel => !panel.Filtered.Value) + .Where(panel => panel.Visible) .All(b => b.Mod.GetType() != type)); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 8816787ceb..a41eff067b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -203,7 +203,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("mod select contains only double time mod", () => this.ChildrenOfType().Single().UserModsSelectOverlay .ChildrenOfType() - .SingleOrDefault(panel => !panel.Filtered.Value)?.Mod is OsuModDoubleTime); + .SingleOrDefault(panel => panel.Visible)?.Mod is OsuModDoubleTime); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 2da29ccc95..95ae4c5e80 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -107,6 +107,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestBeatmapDownloadingStates() { + AddStep("set to unknown", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Unknown())); AddStep("set to no map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded())); AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); @@ -382,6 +383,8 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddUntilStep("wait for list to load", () => participantsList?.IsLoaded == true); + + AddStep("set beatmap available", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable())); } private void checkProgressBarVisibility(bool visible) => diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index 45c5c67fff..cbeff770c9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -11,6 +11,7 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual.Multiplayer { @@ -28,6 +29,11 @@ namespace osu.Game.Tests.Visual.Multiplayer Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); }); + AddStep("Start track playing", () => + { + Beatmap.Value.Track.Start(); + }); + AddStep("initialise gameplay", () => { Stack.Push(player = new MultiplayerPlayer(MultiplayerClient.ServerAPIRoom, new PlaylistItem(Beatmap.Value.BeatmapInfo) @@ -37,7 +43,14 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddUntilStep("wait for player to be current", () => player.IsCurrentScreen() && player.IsLoaded); + + AddAssert("gameplay clock is paused", () => player.ChildrenOfType().Single().IsPaused.Value); + AddAssert("gameplay clock is not running", () => !player.ChildrenOfType().Single().IsRunning); + AddStep("start gameplay", () => ((IMultiplayerClient)MultiplayerClient).GameplayStarted()); + + AddUntilStep("gameplay clock is not paused", () => !player.ChildrenOfType().Single().IsPaused.Value); + AddAssert("gameplay clock is running", () => player.ChildrenOfType().Single().IsRunning); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs index aaf1a850af..d5f53bc354 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; using Moq; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs index e46ae978d7..b53a61f881 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Beatmaps; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index 5483be5676..d0fa5fc737 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; @@ -16,6 +17,7 @@ using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Menu; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; using osu.Game.Tests.Resources; using osuTK.Input; @@ -170,6 +172,66 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("time is correct", () => getEditor().ChildrenOfType().First().CurrentTime, () => Is.EqualTo(1234)); } + [Test] + public void TestAttemptGlobalMusicOperationFromEditor() + { + BeatmapSetInfo beatmapSet = null!; + + AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()); + AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()); + + AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet)); + AddUntilStep("wait for song select", + () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) + && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect + && songSelect.IsLoaded); + + AddUntilStep("wait for music playing", () => Game.MusicController.IsPlaying); + AddStep("user request stop", () => Game.MusicController.Stop(requestedByUser: true)); + AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying); + + AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); + AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + + AddUntilStep("music still stopped", () => !Game.MusicController.IsPlaying); + AddStep("user request play", () => Game.MusicController.Play(requestedByUser: true)); + AddUntilStep("music still stopped", () => !Game.MusicController.IsPlaying); + + AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield())); + AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); + + AddUntilStep("wait for music playing", () => Game.MusicController.IsPlaying); + AddStep("user request stop", () => Game.MusicController.Stop(requestedByUser: true)); + AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying); + } + + [TestCase(SortMode.Title)] + [TestCase(SortMode.Difficulty)] + public void TestSelectionRetainedOnExit(SortMode sortMode) + { + BeatmapSetInfo beatmapSet = null!; + + AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()); + AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()); + + AddStep($"set sort mode to {sortMode}", () => Game.LocalConfig.SetValue(OsuSetting.SongSelectSortingMode, sortMode)); + AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet)); + AddUntilStep("wait for song select", + () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) + && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect + && songSelect.IsLoaded); + + AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); + AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + + AddStep("exit editor", () => InputManager.Key(Key.Escape)); + AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor); + + AddUntilStep("selection retained on song select", + () => Game.Beatmap.Value.BeatmapInfo.ID, + () => Is.EqualTo(beatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0).ID)); + } + private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType().Single(); private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneButtonSystemNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneButtonSystemNavigation.cs index 64ea6003bc..c6d67f2bc6 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneButtonSystemNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneButtonSystemNavigation.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Testing; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs index 224e7e411e..3a3af43cb1 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs @@ -78,11 +78,11 @@ namespace osu.Game.Tests.Visual.Navigation private KeyBindingsSubsection osuBindingSubsection => keyBindingPanel .ChildrenOfType() - .FirstOrDefault(s => s.Ruleset.ShortName == "osu"); + .FirstOrDefault(s => s.Ruleset!.ShortName == "osu"); private OsuButton configureBindingsButton => Game.Settings .ChildrenOfType().SingleOrDefault()? - .ChildrenOfType()? + .ChildrenOfType() .First(b => b.Text.ToString() == "Configure"); private KeyBindingPanel keyBindingPanel => Game.Settings diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs b/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs index bd75825da2..1633a778ab 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs index 1c8fa775b9..5fe4bb9340 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Overlays; +using osu.Game.Rulesets.Mods; using osu.Game.Screens; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; @@ -86,6 +87,29 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("did perform", () => actionPerformed); } + [Test] + public void TestPerformAtMenuFromPlayerLoaderWithAutoplayShortcut() + { + importAndWaitForSongSelect(); + + AddStep("press ctrl+enter", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Enter); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + AddUntilStep("Wait for new screen", () => Game.ScreenStack.CurrentScreen is PlayerLoader); + + AddAssert("Mods include autoplay", () => Game.SelectedMods.Value.Any(m => m is ModAutoplay)); + + AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true)); + AddUntilStep("returned to main menu", () => Game.ScreenStack.CurrentScreen is MainMenu); + AddAssert("did perform", () => actionPerformed); + + AddAssert("Mods don't include autoplay", () => !Game.SelectedMods.Value.Any(m => m is ModAutoplay)); + } + [Test] public void TestPerformEnsuresScreenIsLoaded() { diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 18aef99ccd..9e743ef336 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -7,6 +7,7 @@ using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -16,12 +17,14 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Configuration; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.Mods; +using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Toolbar; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; @@ -33,6 +36,7 @@ using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Options; using osu.Game.Tests.Beatmaps.IO; @@ -164,6 +168,41 @@ namespace osu.Game.Tests.Visual.Navigation ConfirmAtMainMenu(); } + [Test] + public void TestSongSelectScrollHandling() + { + TestPlaySongSelect songSelect = null; + double scrollPosition = 0; + + AddStep("set game volume to max", () => Game.Dependencies.Get().SetValue(FrameworkSetting.VolumeUniversal, 1d)); + AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType().SingleOrDefault()?.State.Value, () => Is.EqualTo(Visibility.Hidden)); + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("store scroll position", () => scrollPosition = getCarouselScrollPosition()); + + AddStep("move to left side", () => InputManager.MoveMouseTo( + songSelect.ChildrenOfType().Single().ScreenSpaceDrawQuad.TopLeft + new Vector2(1))); + AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1)); + AddAssert("carousel didn't move", getCarouselScrollPosition, () => Is.EqualTo(scrollPosition)); + + AddRepeatStep("alt-scroll down", () => + { + InputManager.PressKey(Key.AltLeft); + InputManager.ScrollVerticalBy(-1); + InputManager.ReleaseKey(Key.AltLeft); + }, 5); + AddAssert("game volume decreased", () => Game.Dependencies.Get().Get(FrameworkSetting.VolumeUniversal), () => Is.LessThan(1)); + + AddStep("move to carousel", () => InputManager.MoveMouseTo(songSelect.ChildrenOfType().Single())); + AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1)); + AddAssert("carousel moved", getCarouselScrollPosition, () => Is.Not.EqualTo(scrollPosition)); + + double getCarouselScrollPosition() => Game.ChildrenOfType>().Single().Current; + } + /// /// This tests that the F1 key will open the mod select overlay, and not be handled / blocked by the music controller (which has the same default binding /// but should be handled *after* song select). @@ -208,12 +247,32 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("end spectator before retry", () => Game.SpectatorClient.EndPlaying(player.GameplayState)); AddStep("attempt to retry", () => player.ChildrenOfType().First().Action()); + AddAssert("old player score marked failed", () => player.Score.ScoreInfo.Rank, () => Is.EqualTo(ScoreRank.F)); AddUntilStep("wait for old player gone", () => Game.ScreenStack.CurrentScreen != player); AddUntilStep("get new player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); AddAssert("retry count is 1", () => player.RestartCount == 1); } + [Test] + public void TestRetryImmediatelyAfterCompletion() + { + var getOriginalPlayer = playToCompletion(); + + AddStep("attempt to retry", () => getOriginalPlayer().ChildrenOfType().First().Action()); + AddAssert("original play isn't failed", () => getOriginalPlayer().Score.ScoreInfo.Rank, () => Is.Not.EqualTo(ScoreRank.F)); + AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player); + } + + [Test] + public void TestExitImmediatelyAfterCompletion() + { + var player = playToCompletion(); + + AddStep("attempt to exit", () => player().ChildrenOfType().First().Action()); + AddUntilStep("wait for results", () => Game.ScreenStack.CurrentScreen is ResultsScreen); + } + [Test] public void TestRetryFromResults() { @@ -683,6 +742,68 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("center cursor", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre)); } + [Test] + public void TestExitWithOperationInProgress() + { + AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); + + ProgressNotification progressNotification = null!; + + AddStep("start ongoing operation", () => + { + progressNotification = new ProgressNotification + { + Text = "Something is still running", + Progress = 0.5f, + State = ProgressNotificationState.Active, + }; + Game.Notifications.Post(progressNotification); + }); + + AddStep("Hold escape", () => InputManager.PressKey(Key.Escape)); + AddUntilStep("confirmation dialog shown", () => Game.ChildrenOfType().Single().CurrentDialog is ConfirmExitDialog); + AddStep("Release escape", () => InputManager.ReleaseKey(Key.Escape)); + + AddStep("cancel exit", () => InputManager.Key(Key.Escape)); + AddAssert("dialog dismissed", () => Game.ChildrenOfType().Single().CurrentDialog == null); + + AddStep("complete operation", () => + { + progressNotification.Progress = 100; + progressNotification.State = ProgressNotificationState.Completed; + }); + + AddStep("Hold escape", () => InputManager.PressKey(Key.Escape)); + AddUntilStep("Wait for intro", () => Game.ScreenStack.CurrentScreen is IntroScreen); + AddStep("Release escape", () => InputManager.ReleaseKey(Key.Escape)); + + AddUntilStep("Wait for game exit", () => Game.ScreenStack.CurrentScreen == null); + } + + [Test] + public void TestForceExitWithOperationInProgress() + { + AddStep("set hold delay to 0", () => Game.LocalConfig.SetValue(OsuSetting.UIHoldActivationDelay, 0.0)); + AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); + + AddStep("start ongoing operation", () => + { + Game.Notifications.Post(new ProgressNotification + { + Text = "Something is still running", + Progress = 0.5f, + State = ProgressNotificationState.Active, + }); + }); + + AddStep("attempt exit", () => + { + for (int i = 0; i < 2; ++i) + Game.ScreenStack.CurrentScreen.Exit(); + }); + AddUntilStep("stopped at exit confirm", () => Game.ChildrenOfType().Single().CurrentDialog is ConfirmExitDialog); + } + [Test] public void TestExitGameFromSongSelect() { @@ -699,7 +820,7 @@ namespace osu.Game.Tests.Visual.Navigation } [Test] - public void TestRapidBackButtonExit() + public void TestExitWithHoldDisabled() { AddStep("set hold delay to 0", () => Game.LocalConfig.SetValue(OsuSetting.UIHoldActivationDelay, 0.0)); @@ -711,10 +832,17 @@ namespace osu.Game.Tests.Visual.Navigation pushEscape(); - AddAssert("exit dialog is shown", () => Game.Dependencies.Get().CurrentDialog != null); + AddAssert("exit dialog is shown", () => Game.Dependencies.Get().CurrentDialog is ConfirmExitDialog); } private Func playToResults() + { + var player = playToCompletion(); + AddUntilStep("wait for results", () => (Game.ScreenStack.CurrentScreen as ResultsScreen)?.IsLoaded == true); + return player; + } + + private Func playToCompletion() { Player player = null; @@ -740,7 +868,8 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for track playing", () => beatmap().Track.IsRunning); AddStep("seek to near end", () => player.ChildrenOfType().First().Seek(beatmap().Beatmap.HitObjects[^1].StartTime - 1000)); - AddUntilStep("wait for pass", () => (Game.ScreenStack.CurrentScreen as ResultsScreen)?.IsLoaded == true); + AddUntilStep("wait for complete", () => player.GameplayState.HasPassed); + return () => player; } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index bedb2ceaa1..c17a9ddf5f 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -5,6 +5,7 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; @@ -18,6 +19,7 @@ using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD.HitErrorMeters; +using osu.Game.Skinning; using osu.Game.Tests.Beatmaps.IO; using osuTK; using osuTK.Input; @@ -31,7 +33,7 @@ namespace osu.Game.Tests.Visual.Navigation private SkinEditor skinEditor => Game.ChildrenOfType().FirstOrDefault(); [Test] - public void TestEditComponentDuringGameplay() + public void TestEditComponentFromGameplayScene() { advanceToSongSelect(); openSkinEditor(); @@ -69,6 +71,28 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default); } + [Test] + public void TestMutateProtectedSkinDuringGameplay() + { + advanceToSongSelect(); + AddStep("set default skin", () => Game.Dependencies.Get().CurrentSkinInfo.SetDefault()); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("enable NF", () => Game.SelectedMods.Value = new[] { new OsuModNoFail() }); + AddStep("enter gameplay", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + return Game.ScreenStack.CurrentScreen is Player; + }); + + openSkinEditor(); + AddUntilStep("current skin is mutable", () => !Game.Dependencies.Get().CurrentSkin.Value.SkinInfo.Value.Protected); + } + [Test] public void TestComponentsDeselectedOnSkinEditorHide() { @@ -148,7 +172,7 @@ namespace osu.Game.Tests.Visual.Navigation { advanceToSongSelect(); openSkinEditor(); - AddStep("select no fail and spun out", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail(), new OsuModSpunOut() }); + AddStep("select relax and spun out", () => Game.SelectedMods.Value = new Mod[] { new OsuModRelax(), new OsuModSpunOut() }); switchToGameplayScene(); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs index f885c2f44c..0bfe02cb16 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupRuleset.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupRuleset.cs index 621dabe869..d70eaf16f6 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneStartupRuleset.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupRuleset.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 NUnit.Framework; using osu.Framework.Development; using osu.Game.Configuration; diff --git a/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs b/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs index c32aa7f5f9..820df02b66 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Utils; using osu.Game.Configuration; diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapAvailability.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapAvailability.cs index 36f8d2d9bd..ccd3d7f4d6 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapAvailability.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapAvailability.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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 5e49cb633e..8c32135cfd 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -188,7 +188,7 @@ namespace osu.Game.Tests.Visual.Online AddUntilStep("placeholder shown", () => { var notFoundDrawable = overlay.ChildrenOfType().SingleOrDefault(); - return notFoundDrawable != null && notFoundDrawable.IsPresent && notFoundDrawable.Parent.DrawHeight > 0; + return notFoundDrawable != null && notFoundDrawable.IsPresent && notFoundDrawable.Parent!.DrawHeight > 0; }); } diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs index 36c3576da6..599eee7d0c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Online selector.BeatmapSet = new APIBeatmapSet { - Beatmaps = selector.BeatmapSet.Beatmaps + Beatmaps = selector.BeatmapSet!.Beatmaps .Where(b => b.Ruleset.OnlineID != ruleset) .Concat(Enumerable.Range(0, count).Select(_ => new APIBeatmap { RulesetID = ruleset })) .ToArray(), diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index a27c4ddad2..60fb6b8c86 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.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 NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Beatmaps; @@ -39,7 +37,7 @@ namespace osu.Game.Tests.Visual.Online } [Resolved] - private IRulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } = null!; [SetUp] public void SetUp() => Schedule(() => SelectedMods.Value = Array.Empty()); @@ -86,6 +84,7 @@ namespace osu.Game.Tests.Visual.Online StarRating = 9.99, DifficultyName = @"TEST", Length = 456000, + HitLength = 400000, RulesetID = 3, CircleSize = 1, DrainRate = 2.3f, diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs index 8d61c5df9f..040b903636 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -154,7 +154,14 @@ namespace osu.Game.Tests.Visual.Online Type = ChangelogEntryType.Misc, Category = "Code quality", Title = "Clean up another thing" - } + }, + new APIChangelogEntry + { + Type = ChangelogEntryType.Add, + Category = "osu!", + Title = "Add entry with news url", + Url = "https://osu.ppy.sh/home/news/2023-07-27-summer-splash" + }, } }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogSupporterPromo.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogSupporterPromo.cs index 96996db940..fbd3b3a728 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogSupporterPromo.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogSupporterPromo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLineTruncation.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLineTruncation.cs index 32d95ec8dc..d1c380e2c7 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLineTruncation.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLineTruncation.cs @@ -38,10 +38,17 @@ namespace osu.Game.Tests.Visual.Online private void clear() => AddStep("clear messages", textContainer.Clear); - private void addMessageWithChecks(string text, bool isAction = false, bool isImportant = false, string username = null) + private void addMessageWithChecks(string text, bool isAction = false, bool isImportant = false, string username = null, Colour4? color = null) { int index = textContainer.Count + 1; - var newLine = new ChatLine(new DummyMessage(text, isAction, isImportant, index, username)); + + var newLine = color != null + ? new ChatLine(new DummyMessage(text, isAction, isImportant, index, username)) + { + UsernameColour = color.Value + } + : new ChatLine(new DummyMessage(text, isAction, isImportant, index, username)); + textContainer.Add(newLine); } @@ -51,6 +58,7 @@ namespace osu.Game.Tests.Visual.Online addMessageWithChecks($"Wide {a} character username.", username: new string('w', a)); addMessageWithChecks("Short name with spaces.", username: "sho rt name"); addMessageWithChecks("Long name with spaces.", username: "long name with s p a c e s"); + addMessageWithChecks("message with custom color", username: "I have custom color", color: Colour4.Green); } private class DummyMessage : Message diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index f5cf4c1ff2..7616b9b83c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -64,6 +64,9 @@ namespace osu.Game.Tests.Visual.Online addMessageWithChecks("test!"); addMessageWithChecks("dev.ppy.sh!"); addMessageWithChecks("https://dev.ppy.sh!", 1, expectedActions: LinkAction.External); + addMessageWithChecks("http://dev.ppy.sh!", 1, expectedActions: LinkAction.External); + addMessageWithChecks("forgothttps://dev.ppy.sh!", 1, expectedActions: LinkAction.External); + addMessageWithChecks("forgothttp://dev.ppy.sh!", 1, expectedActions: LinkAction.External); addMessageWithChecks("00:12:345 (1,2) - Test?", 1, expectedActions: LinkAction.OpenEditorTimestamp); addMessageWithChecks("Wiki link for tasty [[Performance Points]]", 1, expectedActions: LinkAction.OpenWiki); addMessageWithChecks("(osu forums)[https://dev.ppy.sh/forum] (old link format)", 1, expectedActions: LinkAction.External); @@ -84,11 +87,14 @@ namespace osu.Game.Tests.Visual.Online addMessageWithChecks("feels important", 0, true, true); addMessageWithChecks("likes to post this [https://dev.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External); addMessageWithChecks("Join my multiplayer game osump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch); + addMessageWithChecks("Join my multiplayer gameosump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch); addMessageWithChecks("Join my [multiplayer game](osump://12346).", 1, expectedActions: LinkAction.JoinMultiplayerMatch); addMessageWithChecks($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", 1, expectedActions: LinkAction.OpenChannel); addMessageWithChecks($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel); + addMessageWithChecks($"Join my{OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel); addMessageWithChecks("Join my #english or #japanese channels.", 2, expectedActions: new[] { LinkAction.OpenChannel, LinkAction.OpenChannel }); addMessageWithChecks("Join my #english or #nonexistent #hashtag channels.", 1, expectedActions: LinkAction.OpenChannel); + addMessageWithChecks("Hello world\uD83D\uDE12(<--This is an emoji). There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20"); void addMessageWithChecks(string text, int linkAmount = 0, bool isAction = false, bool isImportant = false, params LinkAction[] expectedActions) { diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs b/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs index 1e80acd56b..8c5475223c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs @@ -3,11 +3,13 @@ #nullable disable +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; @@ -18,7 +20,7 @@ using osu.Game.Overlays.Chat; namespace osu.Game.Tests.Visual.Online { [TestFixture] - public partial class TestSceneChatTextBox : OsuTestScene + public partial class TestSceneChatTextBox : OsuManualInputManagerTestScene { [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); @@ -30,6 +32,8 @@ namespace osu.Game.Tests.Visual.Online private OsuSpriteText searchText; private ChatTextBar bar; + private ChatTextBox textBox => bar.ChildrenOfType().Single(); + [SetUp] public void SetUp() { @@ -115,6 +119,36 @@ namespace osu.Game.Tests.Visual.Online AddStep("Chat Mode Search", () => bar.ShowSearch.Value = true); } + [Test] + public void TestLengthLimit() + { + var firstChannel = new Channel + { + Name = "#test1", + Type = ChannelType.Public, + Id = 4567, + MessageLengthLimit = 20 + }; + var secondChannel = new Channel + { + Name = "#test2", + Type = ChannelType.Public, + Id = 5678, + MessageLengthLimit = 5 + }; + + AddStep("switch to channel with 20 char length limit", () => currentChannel.Value = firstChannel); + AddStep("type a message", () => textBox.Current.Value = "abcdefgh"); + + AddStep("switch to channel with 5 char length limit", () => currentChannel.Value = secondChannel); + AddAssert("text box empty", () => textBox.Current.Value, () => Is.Empty); + AddStep("type too much", () => textBox.Current.Value = "123456"); + AddAssert("text box has 5 chars", () => textBox.Current.Value, () => Has.Length.EqualTo(5)); + + AddStep("switch back to channel with 20 char length limit", () => currentChannel.Value = firstChannel); + AddAssert("unsent message preserved without truncation", () => textBox.Current.Value, () => Is.EqualTo("abcdefgh")); + } + private static Channel createPublicChannel(string name) => new Channel { Name = name, Type = ChannelType.Public, Id = 1234 }; diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsHeader.cs index 43d80ee0ac..89b8a8c079 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCommentsHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsHeader.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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs index 4f825e1191..5237238f63 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.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 System.Threading; @@ -24,8 +22,8 @@ namespace osu.Game.Tests.Visual.Online { private readonly APIUser streamingUser = new APIUser { Id = 2, Username = "Test user" }; - private TestSpectatorClient spectatorClient; - private CurrentlyPlayingDisplay currentlyPlaying; + private TestSpectatorClient spectatorClient = null!; + private CurrentlyPlayingDisplay currentlyPlaying = null!; [SetUpSteps] public void SetUpSteps() @@ -61,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online public void TestBasicDisplay() { AddStep("Add playing user", () => spectatorClient.SendStartPlay(streamingUser.Id, 0)); - AddUntilStep("Panel loaded", () => currentlyPlaying.ChildrenOfType()?.FirstOrDefault()?.User.Id == 2); + AddUntilStep("Panel loaded", () => currentlyPlaying.ChildrenOfType().FirstOrDefault()?.User.Id == 2); AddStep("Remove playing user", () => spectatorClient.SendEndPlay(streamingUser.Id)); AddUntilStep("Panel no longer present", () => !currentlyPlaying.ChildrenOfType().Any()); } @@ -88,13 +86,13 @@ namespace osu.Game.Tests.Visual.Online "pishifat" }; - protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) + protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) { // tests against failed lookups if (lookup == 13) - return Task.FromResult(null); + return Task.FromResult(null); - return Task.FromResult(new APIUser + return Task.FromResult(new APIUser { Id = lookup, Username = usernames[lookup % usernames.Length], diff --git a/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs index 504be45b44..9407941da6 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.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 NUnit.Framework; using osu.Game.Overlays; diff --git a/osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs b/osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs index ac80463d3a..97e1cae11c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs @@ -64,7 +64,8 @@ namespace osu.Game.Tests.Visual.Online new[] { "Plain", "This is plain comment" }, new[] { "Pinned", "This is pinned comment" }, new[] { "Link", "Please visit https://osu.ppy.sh" }, - + new[] { "Big Image", "![](Backgrounds/bg1)" }, + new[] { "Small Image", "![](Cursor/cursortrail)" }, new[] { "Heading", @"# Heading 1 diff --git a/osu.Game.Tests/Visual/Online/TestSceneExternalLinkButton.cs b/osu.Game.Tests/Visual/Online/TestSceneExternalLinkButton.cs index 90ec3160d8..84f245afb6 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneExternalLinkButton.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneExternalLinkButton.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 osu.Framework.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game.Tests/Visual/Online/TestSceneGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneGraph.cs index 357ed7548c..4f19003638 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneGraph.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneGraph.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 NUnit.Framework; diff --git a/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs index a58845ca7e..5c726bd69e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.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 osu.Framework.Graphics.Containers; using osu.Framework.Graphics; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game.Tests/Visual/Online/TestSceneLeaderboardScopeSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneLeaderboardScopeSelector.cs index 0231775189..4c67f778a2 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneLeaderboardScopeSelector.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneLeaderboardScopeSelector.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 osu.Game.Overlays.BeatmapSet; using osu.Framework.Graphics; using osu.Framework.Bindables; diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs index 001e6d925e..2d91df8a6d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsCard.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 osu.Framework.Graphics.Containers; using osu.Framework.Graphics; using osu.Game.Overlays.News; diff --git a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs index 10c2b2b9e1..fb36580a42 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs @@ -10,6 +10,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Chat; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Users; @@ -32,7 +33,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestGenericActivity() { - AddStep("Set activity", () => api.Activity.Value = new UserActivity.InLobby(null)); + AddStep("Set activity", () => api.Activity.Value = new UserActivity.InLobby(new Room())); AddStep("Run command", () => Add(new NowPlayingCommand(new Channel()))); @@ -63,7 +64,7 @@ namespace osu.Game.Tests.Visual.Online [TestCase(false)] public void TestLinkPresence(bool hasOnlineId) { - AddStep("Set activity", () => api.Activity.Value = new UserActivity.InLobby(null)); + AddStep("Set activity", () => api.Activity.Value = new UserActivity.InLobby(new Room())); AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null) { diff --git a/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapListingOverlay.cs index ecfa76f395..f332575fb4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapListingOverlay.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 osu.Game.Overlays; using NUnit.Framework; diff --git a/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapSetOverlay.cs index 01b0b39661..3a4d0b97db 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapSetOverlay.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 NUnit.Framework; using osu.Game.Overlays; diff --git a/osu.Game.Tests/Visual/Online/TestSceneOnlineViewContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneOnlineViewContainer.cs index 6c8430e955..df29b33f17 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneOnlineViewContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneOnlineViewContainer.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 NUnit.Framework; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsCountryFilter.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsCountryFilter.cs index dfefcd735e..3b38cf47d8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneRankingsCountryFilter.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsCountryFilter.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 osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Overlays.Rankings; diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs index 5aef91bef1..eb2a451c9b 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Overlays; diff --git a/osu.Game.Tests/Visual/Online/TestSceneReplayMissingBeatmap.cs b/osu.Game.Tests/Visual/Online/TestSceneReplayMissingBeatmap.cs new file mode 100644 index 0000000000..60197e0eb7 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneReplayMissingBeatmap.cs @@ -0,0 +1,97 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using System.Net; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.Online +{ + public partial class TestSceneReplayMissingBeatmap : OsuGameTestScene + { + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + + [Test] + public void TestSceneMissingBeatmapWithOnlineAvailable() + { + var beatmap = new APIBeatmap + { + OnlineBeatmapSetID = 173612, + BeatmapSet = new APIBeatmapSet + { + Title = "FREEDOM Dive", + Artist = "xi", + Covers = new BeatmapSetOnlineCovers + { + Card = "https://assets.ppy.sh/beatmaps/173612/covers/card@2x.jpg" + }, + OnlineID = 173612 + } + }; + + setupBeatmapResponse(beatmap); + + AddStep("import score", () => + { + using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr")) + { + var importTask = new ImportTask(resourceStream, "replay.osr"); + + Game.ScoreManager.Import(new[] { importTask }); + } + }); + + AddUntilStep("Replay missing notification show", () => Game.Notifications.ChildrenOfType().Any()); + } + + [Test] + public void TestSceneMissingBeatmapWithOnlineUnavailable() + { + setupFailedResponse(); + + AddStep("import score", () => + { + using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr")) + { + var importTask = new ImportTask(resourceStream, "replay.osr"); + + Game.ScoreManager.Import(new[] { importTask }); + } + }); + + AddUntilStep("Replay missing notification not show", () => !Game.Notifications.ChildrenOfType().Any()); + } + + private void setupBeatmapResponse(APIBeatmap b) + => AddStep("setup response", () => + { + dummyAPI.HandleRequest = request => + { + if (request is GetBeatmapRequest getBeatmapRequest) + { + getBeatmapRequest.TriggerSuccess(b); + return true; + } + + return false; + }; + }); + + private void setupFailedResponse() + => AddStep("setup failed response", () => + { + dummyAPI.HandleRequest = request => + { + request.TriggerFailure(new WebException()); + return true; + }; + }); + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneSpotlightsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneSpotlightsLayout.cs index 4cbcaaac85..5ce82a8766 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneSpotlightsLayout.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneSpotlightsLayout.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Online/TestSceneTotalCommentsCounter.cs b/osu.Game.Tests/Visual/Online/TestSceneTotalCommentsCounter.cs index 8af87dd597..cbd8ffa91c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneTotalCommentsCounter.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneTotalCommentsCounter.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 osu.Framework.Graphics; using osu.Framework.Bindables; using osu.Game.Overlays.Comments; diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index a047e2f0c5..c61b572d8c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -9,8 +9,10 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Tests.Beatmaps; using osu.Game.Users; @@ -116,9 +118,9 @@ namespace osu.Game.Tests.Visual.Online AddStep("solo (osu!catch)", () => activity.Value = soloGameStatusForRuleset(2)); AddStep("solo (osu!mania)", () => activity.Value = soloGameStatusForRuleset(3)); AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap()); - AddStep("editing beatmap", () => activity.Value = new UserActivity.EditingBeatmap(null)); - AddStep("modding beatmap", () => activity.Value = new UserActivity.ModdingBeatmap(null)); - AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(null, null)); + AddStep("editing beatmap", () => activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo())); + AddStep("modding beatmap", () => activity.Value = new UserActivity.ModdingBeatmap(new BeatmapInfo())); + AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(new BeatmapInfo(), new OsuRuleset().RulesetInfo)); } [Test] @@ -134,7 +136,7 @@ namespace osu.Game.Tests.Visual.Online AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent); } - private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(null, rulesetStore.GetRuleset(rulesetId)); + private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!); private ScoreInfo createScore(string name) => new ScoreInfo(new TestBeatmap(Ruleset.Value).BeatmapInfo) { diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs index 640e895b6c..c9e5a3315c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs @@ -5,8 +5,10 @@ using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Configuration; +using osu.Game.Graphics.Containers; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Profile; @@ -28,7 +30,14 @@ namespace osu.Game.Tests.Visual.Online [SetUpSteps] public void SetUpSteps() { - AddStep("create header", () => Child = header = new ProfileHeader()); + AddStep("create header", () => + { + Child = new OsuScrollContainer(Direction.Vertical) + { + RelativeSizeAxes = Axes.Both, + Child = header = new ProfileHeader() + }; + }); } [Test] @@ -110,5 +119,286 @@ namespace osu.Game.Tests.Visual.Online } }, new OsuRuleset().RulesetInfo)); } + + [Test] + public void TestPreviousUsernames() + { + AddStep("Show user w/ previous usernames", () => header.User.Value = new UserProfileData(new APIUser + { + Id = 727, + Username = "SomeoneIndecisive", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg", + Groups = new[] + { + new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" }, + }, + Statistics = new UserStatistics + { + IsRanked = false, + // web will sometimes return non-empty rank history even for unranked users. + RankHistory = new APIRankHistory + { + Mode = @"osu", + Data = Enumerable.Range(2345, 85).ToArray() + }, + }, + PreviousUsernames = new[] { "tsrk.", "quoicoubeh", "apagnan", "epita" } + }, new OsuRuleset().RulesetInfo)); + } + + [Test] + public void TestManyTournamentBanners() + { + AddStep("Show user w/ many tournament banners", () => header.User.Value = new UserProfileData(new APIUser + { + Id = 728, + Username = "Certain Guy", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", + Statistics = new UserStatistics + { + IsRanked = false, + // web will sometimes return non-empty rank history even for unranked users. + RankHistory = new APIRankHistory + { + Mode = @"osu", + Data = Enumerable.Range(2345, 85).ToArray() + }, + }, + TournamentBanners = new[] + { + new TournamentBanner + { + Id = 15329, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK@2x.jpg" + }, + new TournamentBanner + { + Id = 15588, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN@2x.jpg" + }, + new TournamentBanner + { + Id = 15589, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH@2x.jpg" + }, + new TournamentBanner + { + Id = 15590, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL@2x.jpg" + }, + new TournamentBanner + { + Id = 15591, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_JP.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_JP@2x.jpg" + }, + new TournamentBanner + { + Id = 15592, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RU.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RU@2x.jpg" + }, + new TournamentBanner + { + Id = 15593, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KR.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KR@2x.jpg" + }, + new TournamentBanner + { + Id = 15594, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NZ.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NZ@2x.jpg" + }, + new TournamentBanner + { + Id = 15595, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TH.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TH@2x.jpg" + }, + new TournamentBanner + { + Id = 15596, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TW.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TW@2x.jpg" + }, + new TournamentBanner + { + Id = 15603, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ID.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ID@2x.jpg" + }, + new TournamentBanner + { + Id = 15604, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KZ.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KZ@2x.jpg" + }, + new TournamentBanner + { + Id = 15605, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AR.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AR@2x.jpg" + }, + new TournamentBanner + { + Id = 15606, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_BR.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_BR@2x.jpg" + }, + new TournamentBanner + { + Id = 15607, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL@2x.jpg" + }, + new TournamentBanner + { + Id = 15639, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_MX.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_MX@2x.jpg" + }, + new TournamentBanner + { + Id = 15640, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AU.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AU@2x.jpg" + }, + new TournamentBanner + { + Id = 15641, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_IT.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_IT@2x.jpg" + }, + new TournamentBanner + { + Id = 15642, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_UA.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_UA@2x.jpg" + }, + new TournamentBanner + { + Id = 15643, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NL.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NL@2x.jpg" + }, + new TournamentBanner + { + Id = 15644, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FI.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FI@2x.jpg" + }, + new TournamentBanner + { + Id = 15645, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RO.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RO@2x.jpg" + }, + new TournamentBanner + { + Id = 15646, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SG.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SG@2x.jpg" + }, + new TournamentBanner + { + Id = 15647, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_DE.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_DE@2x.jpg" + }, + new TournamentBanner + { + Id = 15648, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ES.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ES@2x.jpg" + }, + new TournamentBanner + { + Id = 15649, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SE.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SE@2x.jpg" + }, + new TournamentBanner + { + Id = 15650, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CA.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CA@2x.jpg" + }, + new TournamentBanner + { + Id = 15651, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NO.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NO@2x.jpg" + }, + new TournamentBanner + { + Id = 15652, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_GB.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_GB@2x.jpg" + }, + new TournamentBanner + { + Id = 15653, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_US.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_US@2x.jpg" + }, + new TournamentBanner + { + Id = 15654, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL@2x.jpg" + }, + new TournamentBanner + { + Id = 15655, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FR.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FR@2x.jpg" + }, + new TournamentBanner + { + Id = 15686, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK@2x.jpg" + } + } + }, new OsuRuleset().RulesetInfo)); + } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index 4278c46d6a..a321a194a9 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -121,12 +121,29 @@ namespace osu.Game.Tests.Visual.Online Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray() }, }, - TournamentBanner = new TournamentBanner + TournamentBanners = new[] { - Id = 13926, - TournamentId = 35, - ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2022/profile/winner_US.jpg", - Image = "https://assets.ppy.sh/tournament-banners/official/owc2022/profile/winner_US@2x.jpg", + new TournamentBanner + { + Id = 15588, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN@2x.jpg" + }, + new TournamentBanner + { + Id = 15589, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH@2x.jpg" + }, + new TournamentBanner + { + Id = 15590, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL@2x.jpg" + } }, Badges = new[] { @@ -134,14 +151,16 @@ namespace osu.Game.Tests.Visual.Online { AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569), Description = "Outstanding help by being a voluntary test subject.", - ImageUrl = "https://assets.ppy.sh/profile-badges/contributor.jpg", + ImageUrl = "https://assets.ppy.sh/profile-badges/contributor-new@2x.png", + ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor-new.png", Url = "https://osu.ppy.sh/wiki/en/People/Community_Contributors", }, new Badge { AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569), Description = "Badge without a url.", - ImageUrl = "https://assets.ppy.sh/profile-badges/contributor.jpg", + ImageUrl = "https://assets.ppy.sh/profile-badges/contributor@2x.png", + ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor.png", }, }, Title = "osu!volunteer", diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernamesDisplay.cs similarity index 76% rename from osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs rename to osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernamesDisplay.cs index 921738d331..c1140f60af 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernamesDisplay.cs @@ -5,20 +5,29 @@ using System; using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; using osu.Game.Overlays.Profile.Header.Components; namespace osu.Game.Tests.Visual.Online { [TestFixture] - public partial class TestSceneUserProfilePreviousUsernames : OsuTestScene + public partial class TestSceneUserProfilePreviousUsernamesDisplay : OsuTestScene { - private PreviousUsernames container = null!; + private PreviousUsernamesDisplay container = null!; + private OverlayColourProvider colourProvider = null!; [SetUp] public void SetUp() => Schedule(() => { - Child = container = new PreviousUsernames + colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); + Child = new DependencyProvidingContainer { + Child = container = new PreviousUsernamesDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + CachedDependencies = new (Type, object)[] { (typeof(OverlayColourProvider), colourProvider) }, Anchor = Anchor.Centre, Origin = Anchor.Centre, }; diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMainPage.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMainPage.cs index 8876f0fd3b..9967be73e8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiMainPage.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMainPage.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs index c4a1200cb1..3b60c28dc0 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index 6c732f4295..2f66309f04 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Playlists }); }); - AddUntilStep("Progress details are hidden", () => match.ChildrenOfType().FirstOrDefault()?.Parent.Alpha == 0); + AddUntilStep("Progress details are hidden", () => match.ChildrenOfType().FirstOrDefault()?.Parent!.Alpha == 0); AddUntilStep("Leaderboard shows two aggregate scores", () => match.ChildrenOfType().Count(s => s.ScoreText.Text != "0") == 2); @@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual.Playlists }); }); - AddUntilStep("Progress details are visible", () => match.ChildrenOfType().FirstOrDefault()?.Parent.Alpha == 1); + AddUntilStep("Progress details are visible", () => match.ChildrenOfType().FirstOrDefault()?.Parent!.Alpha == 1); } [Test] diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs index 71e284ecfe..891dd3bb1a 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.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 NUnit.Framework; namespace osu.Game.Tests.Visual.Playlists diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index bf18bd3e51..03b168c72c 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Extensions.Color4Extensions; diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs index e92e74598d..0f17b08b7b 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; @@ -35,7 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("show excess mods score", () => { var score = TestResources.CreateTestScoreInfo(); - score.Mods = score.BeatmapInfo.Ruleset.CreateInstance().CreateAllMods().ToArray(); + score.Mods = score.BeatmapInfo!.Ruleset.CreateInstance().CreateAllMods().ToArray(); showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), score); }); } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index bd7a11b4bb..d71c72f4ec 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Ranking var author = new RealmUser { Username = "mapper_name" }; var score = TestResources.CreateTestScoreInfo(createTestBeatmap(author)); - score.Mods = score.BeatmapInfo.Ruleset.CreateInstance().CreateAllMods().ToArray(); + score.Mods = score.BeatmapInfo!.Ruleset.CreateInstance().CreateAllMods().ToArray(); showPanel(score); }); @@ -93,7 +93,7 @@ namespace osu.Game.Tests.Visual.Ranking private BeatmapInfo createTestBeatmap([NotNull] RealmUser author) { - var beatmap = new TestBeatmap(rulesetStore.GetRuleset(0)).BeatmapInfo; + var beatmap = new TestBeatmap(rulesetStore.GetRuleset(0)!).BeatmapInfo; beatmap.Metadata.Author = author; beatmap.Metadata.Title = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap title"; diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs index be7be6d4f1..27d66ea2a2 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 42068ff117..ab2e867255 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -69,6 +69,35 @@ namespace osu.Game.Tests.Visual.Ranking })); } + private int onlineScoreID = 1; + + [TestCase(1, ScoreRank.X)] + [TestCase(0.9999, ScoreRank.S)] + [TestCase(0.975, ScoreRank.S)] + [TestCase(0.925, ScoreRank.A)] + [TestCase(0.85, ScoreRank.B)] + [TestCase(0.75, ScoreRank.C)] + [TestCase(0.5, ScoreRank.D)] + [TestCase(0.2, ScoreRank.D)] + public void TestResultsWithPlayer(double accuracy, ScoreRank rank) + { + TestResultsScreen screen = null; + + loadResultsScreen(() => + { + var score = TestResources.CreateTestScoreInfo(); + + score.OnlineID = onlineScoreID++; + score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); + score.Accuracy = accuracy; + score.Rank = rank; + + return screen = createResultsScreen(score); + }); + AddUntilStep("wait for loaded", () => screen.IsLoaded); + AddAssert("retry overlay present", () => screen.RetryOverlay != null); + } + [Test] public void TestResultsWithoutPlayer() { @@ -82,34 +111,14 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both }; - stack.Push(screen = createResultsScreen()); + var score = TestResources.CreateTestScoreInfo(); + + stack.Push(screen = createResultsScreen(score)); }); AddUntilStep("wait for loaded", () => screen.IsLoaded); AddAssert("retry overlay not present", () => screen.RetryOverlay == null); } - [TestCase(0.2, ScoreRank.D)] - [TestCase(0.5, ScoreRank.D)] - [TestCase(0.75, ScoreRank.C)] - [TestCase(0.85, ScoreRank.B)] - [TestCase(0.925, ScoreRank.A)] - [TestCase(0.975, ScoreRank.S)] - [TestCase(0.9999, ScoreRank.S)] - [TestCase(1, ScoreRank.X)] - public void TestResultsWithPlayer(double accuracy, ScoreRank rank) - { - TestResultsScreen screen = null; - - var score = TestResources.CreateTestScoreInfo(); - - score.Accuracy = accuracy; - score.Rank = rank; - - loadResultsScreen(() => screen = createResultsScreen(score)); - AddUntilStep("wait for loaded", () => screen.IsLoaded); - AddAssert("retry overlay present", () => screen.RetryOverlay != null); - } - [Test] public void TestResultsForUnranked() { @@ -328,13 +337,14 @@ namespace osu.Game.Tests.Visual.Ranking } } - private partial class TestResultsScreen : ResultsScreen + private partial class TestResultsScreen : SoloResultsScreen { public HotkeyRetryOverlay RetryOverlay; public TestResultsScreen(ScoreInfo score) : base(score, true) { + ShowUserStatistics = true; } protected override void LoadComplete() @@ -352,7 +362,7 @@ namespace osu.Game.Tests.Visual.Ranking { var score = TestResources.CreateTestScoreInfo(); score.TotalScore += 10 - i; - score.Hash = $"test{i}"; + score.HasOnlineReplay = true; scores.Add(score); } @@ -405,7 +415,7 @@ namespace osu.Game.Tests.Visual.Ranking public UnrankedSoloResultsScreen(ScoreInfo score) : base(score, true) { - Score.BeatmapInfo.OnlineID = 0; + Score.BeatmapInfo!.OnlineID = 0; Score.BeatmapInfo.Status = BeatmapOnlineStatus.Pending; } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 67211a3b72..93005271a9 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Online.Solo; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; @@ -23,6 +24,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Tests.Resources; +using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Ranking @@ -30,19 +32,20 @@ namespace osu.Game.Tests.Visual.Ranking public partial class TestSceneStatisticsPanel : OsuTestScene { [Test] - public void TestScoreWithTimeStatistics() + public void TestScoreWithPositionStatistics() { var score = TestResources.CreateTestScoreInfo(); - score.HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); + score.OnlineID = 1234; + score.HitEvents = CreatePositionDistributedHitEvents(); loadPanel(score); } [Test] - public void TestScoreWithPositionStatistics() + public void TestScoreWithTimeStatistics() { var score = TestResources.CreateTestScoreInfo(); - score.HitEvents = createPositionDistributedHitEvents(); + score.HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); loadPanel(score); } @@ -79,28 +82,67 @@ namespace osu.Game.Tests.Visual.Ranking private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { - Child = new StatisticsPanel + Child = new SoloStatisticsPanel(score) { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, - Score = { Value = score } + Score = { Value = score }, + StatisticsUpdate = + { + Value = new SoloStatisticsUpdate(score, new UserStatistics + { + Level = new UserStatistics.LevelInfo + { + Current = 5, + Progress = 20, + }, + GlobalRank = 38000, + CountryRank = 12006, + PP = 2134, + RankedScore = 21123849, + Accuracy = 0.985, + PlayCount = 13375, + PlayTime = 354490, + TotalScore = 128749597, + TotalHits = 0, + MaxCombo = 1233, + }, new UserStatistics + { + Level = new UserStatistics.LevelInfo + { + Current = 5, + Progress = 30, + }, + GlobalRank = 36000, + CountryRank = 12000, + PP = (decimal)2134.5, + RankedScore = 23897015, + Accuracy = 0.984, + PlayCount = 13376, + PlayTime = 35789, + TotalScore = 132218497, + TotalHits = 0, + MaxCombo = 1233, + }) + } }; }); - private static List createPositionDistributedHitEvents() + public static List CreatePositionDistributedHitEvents() { - var hitEvents = new List(); + var hitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(); + // Use constant seed for reproducibility var random = new Random(0); - for (int i = 0; i < 500; i++) + for (int i = 0; i < hitEvents.Count; i++) { double angle = random.NextDouble() * 2 * Math.PI; double radius = random.NextDouble() * 0.5f * OsuHitObject.OBJECT_RADIUS; var position = new Vector2((float)(radius * Math.Cos(angle)), (float)(radius * Math.Sin(angle))); - hitEvents.Add(new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), position)); + hitEvents[i] = hitEvents[i].With(position); } return hitEvents; diff --git a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs index ce6973aacf..3ef0ffc13a 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Tests.Visual.UserInterface; diff --git a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs index f61e3ca557..e8f74a2f1b 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -16,7 +14,7 @@ namespace osu.Game.Tests.Visual.Settings public partial class TestSceneFileSelector : ThemeComparisonTestScene { [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; [Test] public void TestJpgFilesOnly() diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingConflictPopover.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingConflictPopover.cs new file mode 100644 index 0000000000..03f74fa35c --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingConflictPopover.cs @@ -0,0 +1,63 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; +using osu.Game.Overlays.Settings.Sections.Input; +using osu.Game.Rulesets.Osu; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Settings +{ + public partial class TestSceneKeyBindingConflictPopover : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + [Test] + public void TestAppearance() + { + ButtonWithConflictPopover button = null!; + + AddStep("create content", () => + { + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = button = new ButtonWithConflictPopover + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Open popover", + Width = 300 + } + }; + }); + AddStep("show popover", () => button.TriggerClick()); + } + + private partial class ButtonWithConflictPopover : RoundedButton, IHasPopover + { + [BackgroundDependencyLoader] + private void load() + { + Action = this.ShowPopover; + } + + public Popover GetPopover() => new KeyBindingConflictPopover( + new KeyBindingRow.KeyBindingConflictInfo( + new KeyBindingRow.ConflictingKeyBinding(Guid.NewGuid(), OsuAction.LeftButton, KeyCombination.FromKey(Key.X), new KeyCombination(InputKey.None)), + new KeyBindingRow.ConflictingKeyBinding(Guid.NewGuid(), OsuAction.RightButton, KeyCombination.FromKey(Key.Z), KeyCombination.FromKey(Key.X)) + ) + ); + } + } +} diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index da48086717..1c4e89e1a2 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -10,8 +10,12 @@ using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; using osu.Game.Overlays; +using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings.Sections.Input; +using osu.Game.Rulesets.Taiko; using osuTK.Input; namespace osu.Game.Tests.Visual.Settings @@ -154,7 +158,9 @@ namespace osu.Game.Tests.Visual.Settings clickClearButton(); - AddAssert("first binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType().First().Text.Text.ToString())); + AddAssert("first binding cleared", + () => multiBindingRow.ChildrenOfType().First().Text.Text, + () => Is.EqualTo(InputSettingsStrings.ActionHasNoKeyBinding)); AddStep("click second binding", () => { @@ -166,7 +172,9 @@ namespace osu.Game.Tests.Visual.Settings clickClearButton(); - AddAssert("second binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType().ElementAt(1).Text.Text.ToString())); + AddAssert("second binding cleared", + () => multiBindingRow.ChildrenOfType().ElementAt(1).Text.Text, + () => Is.EqualTo(InputSettingsStrings.ActionHasNoKeyBinding)); void clickClearButton() { @@ -195,19 +203,19 @@ namespace osu.Game.Tests.Visual.Settings InputManager.ReleaseKey(Key.P); }); - AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha > 0); + AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha > 0); AddStep("click reset button for bindings", () => { - var resetButton = settingsKeyBindingRow.ChildrenOfType>().First(); + var resetButton = settingsKeyBindingRow.ChildrenOfType>().First(); resetButton.TriggerClick(); }); - AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0); + AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0); AddAssert("binding cleared", - () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0))); + () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.Value.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0))); } [Test] @@ -225,7 +233,7 @@ namespace osu.Game.Tests.Visual.Settings InputManager.ReleaseKey(Key.P); }); - AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha > 0); + AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha > 0); AddStep("click reset button for bindings", () => { @@ -234,10 +242,10 @@ namespace osu.Game.Tests.Visual.Settings resetButton.TriggerClick(); }); - AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0); + AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0); AddAssert("binding cleared", - () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0))); + () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.Value.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0))); } [Test] @@ -288,6 +296,106 @@ namespace osu.Game.Tests.Visual.Settings AddUntilStep("all reset section bindings buttons shown", () => panel.ChildrenOfType().All(button => button.Alpha == 1)); } + [Test] + public void TestBindingConflictResolvedByRollback() + { + AddStep("reset taiko section to default", () => + { + var section = panel.ChildrenOfType().First(section => new TaikoRuleset().RulesetInfo.Equals(section.Ruleset)); + section.ChildrenOfType().Single().TriggerClick(); + }); + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre)); + AddUntilStep("wait for collapsed", () => panel.ChildrenOfType().Single().Expanded.Value, () => Is.False); + scrollToAndStartBinding("Left (rim)"); + AddStep("attempt to bind M1 to two keys", () => InputManager.Click(MouseButton.Left)); + + KeyBindingConflictPopover popover = null; + AddUntilStep("wait for popover", () => popover = panel.ChildrenOfType().SingleOrDefault(), () => Is.Not.Null); + AddStep("click first button", () => popover.ChildrenOfType().First().TriggerClick()); + checkBinding("Left (centre)", "M1"); + checkBinding("Left (rim)", "M2"); + } + + [Test] + public void TestBindingConflictResolvedByOverwrite() + { + AddStep("reset taiko section to default", () => + { + var section = panel.ChildrenOfType().First(section => new TaikoRuleset().RulesetInfo.Equals(section.Ruleset)); + section.ChildrenOfType().Single().TriggerClick(); + }); + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre)); + AddUntilStep("wait for collapsed", () => panel.ChildrenOfType().Single().Expanded.Value, () => Is.False); + scrollToAndStartBinding("Left (rim)"); + AddStep("attempt to bind M1 to two keys", () => InputManager.Click(MouseButton.Left)); + + KeyBindingConflictPopover popover = null; + AddUntilStep("wait for popover", () => popover = panel.ChildrenOfType().SingleOrDefault(), () => Is.Not.Null); + AddStep("click second button", () => popover.ChildrenOfType().ElementAt(1).TriggerClick()); + checkBinding("Left (centre)", InputSettingsStrings.ActionHasNoKeyBinding.ToString()); + checkBinding("Left (rim)", "M1"); + } + + [Test] + public void TestBindingConflictCausedByResetToDefaultOfSingleRow() + { + AddStep("reset taiko section to default", () => + { + var section = panel.ChildrenOfType().First(section => new TaikoRuleset().RulesetInfo.Equals(section.Ruleset)); + section.ChildrenOfType().Single().TriggerClick(); + }); + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre)); + AddUntilStep("wait for collapsed", () => panel.ChildrenOfType().Single().Expanded.Value, () => Is.False); + scrollToAndStartBinding("Left (centre)"); + AddStep("clear binding", () => + { + var row = panel.ChildrenOfType().First(r => r.ChildrenOfType().Any(s => s.Text.ToString() == "Left (centre)")); + row.ChildrenOfType().Single().TriggerClick(); + }); + scrollToAndStartBinding("Left (rim)"); + AddStep("bind M1", () => InputManager.Click(MouseButton.Left)); + + AddStep("reset Left (centre) to default", () => + { + var row = panel.ChildrenOfType().First(r => r.ChildrenOfType().Any(s => s.Text.ToString() == "Left (centre)")); + row.ChildrenOfType>().Single().TriggerClick(); + }); + + KeyBindingConflictPopover popover = null; + AddUntilStep("wait for popover", () => popover = panel.ChildrenOfType().SingleOrDefault(), () => Is.Not.Null); + AddStep("click second button", () => popover.ChildrenOfType().ElementAt(1).TriggerClick()); + checkBinding("Left (centre)", "M1"); + checkBinding("Left (rim)", InputSettingsStrings.ActionHasNoKeyBinding.ToString()); + } + + [Test] + public void TestResettingEntireSectionDoesNotCauseBindingConflicts() + { + AddStep("reset taiko section to default", () => + { + var section = panel.ChildrenOfType().First(section => new TaikoRuleset().RulesetInfo.Equals(section.Ruleset)); + section.ChildrenOfType().Single().TriggerClick(); + }); + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre)); + AddUntilStep("wait for collapsed", () => panel.ChildrenOfType().Single().Expanded.Value, () => Is.False); + scrollToAndStartBinding("Left (centre)"); + AddStep("clear binding", () => + { + var row = panel.ChildrenOfType().First(r => r.ChildrenOfType().Any(s => s.Text.ToString() == "Left (centre)")); + row.ChildrenOfType().Single().TriggerClick(); + }); + scrollToAndStartBinding("Left (rim)"); + AddStep("bind M1", () => InputManager.Click(MouseButton.Left)); + + AddStep("reset taiko section to default", () => + { + var section = panel.ChildrenOfType().First(section => new TaikoRuleset().RulesetInfo.Equals(section.Ruleset)); + section.ChildrenOfType().Single().TriggerClick(); + }); + AddWaitStep("wait a bit", 3); + AddUntilStep("conflict popover not shown", () => panel.ChildrenOfType().SingleOrDefault(), () => Is.Null); + } + private void checkBinding(string name, string keyName) { AddAssert($"Check {name} is bound to {keyName}", () => diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingRow.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingRow.cs new file mode 100644 index 0000000000..ff996a9ca1 --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingRow.cs @@ -0,0 +1,60 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; +using osu.Framework.Testing; +using osu.Game.Input.Bindings; +using osu.Game.Overlays; +using osu.Game.Overlays.Settings.Sections.Input; + +namespace osu.Game.Tests.Visual.Settings +{ + public partial class TestSceneKeyBindingRow : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + [Test] + public void TestChangesAfterConstruction() + { + KeyBindingRow row = null!; + + AddStep("create row", () => Child = new Container + { + Width = 500, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = row = new KeyBindingRow(GlobalAction.Back) + { + Defaults = new[] + { + new KeyCombination(InputKey.Escape), + new KeyCombination(InputKey.ExtraMouseButton1) + } + } + }); + + AddStep("change key bindings", () => + { + row.KeyBindings.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.Escape))); + row.KeyBindings.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.ExtraMouseButton1))); + }); + AddUntilStep("revert to default button not shown", () => row.ChildrenOfType>().Single().Alpha, () => Is.Zero); + + AddStep("change key bindings", () => + { + row.KeyBindings.Clear(); + row.KeyBindings.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.X))); + row.KeyBindings.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.Z))); + row.KeyBindings.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.I))); + }); + AddUntilStep("revert to default button not shown", () => row.ChildrenOfType>().Single().Alpha, () => Is.Not.Zero); + } + } +} diff --git a/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs b/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs index 91320fdb1c..22f185cbd3 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.IO; using System.Threading; using NUnit.Framework; diff --git a/osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs b/osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs deleted file mode 100644 index 6e52881f5e..0000000000 --- a/osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs +++ /dev/null @@ -1,65 +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 NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; -using osu.Game.Overlays; -using osuTK; - -namespace osu.Game.Tests.Visual.Settings -{ - public partial class TestSceneRestoreDefaultValueButton : OsuTestScene - { - [Resolved] - private OsuColour colours { get; set; } - - private float scale = 1; - - private readonly Bindable current = new Bindable - { - Default = default, - Value = 1, - }; - - [Test] - public void TestBasic() - { - RestoreDefaultValueButton restoreDefaultValueButton = null; - - AddStep("create button", () => Child = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colours.GreySeaFoam - }, - restoreDefaultValueButton = new RestoreDefaultValueButton - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(scale), - Current = current, - } - } - }); - AddSliderStep("set scale", 1, 4, 1, scale => - { - this.scale = scale; - if (restoreDefaultValueButton != null) - restoreDefaultValueButton.Scale = new Vector2(scale); - }); - AddToggleStep("toggle default state", state => current.Value = state ? default : 1); - AddToggleStep("toggle disabled state", state => current.Disabled = state); - } - } -} diff --git a/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs b/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs new file mode 100644 index 0000000000..d081f663ba --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneRevertToDefaultButton.cs @@ -0,0 +1,53 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Overlays; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.Settings +{ + public partial class TestSceneRevertToDefaultButton : ThemeComparisonTestScene + { + private float scale = 1; + + private readonly Bindable current = new Bindable + { + Default = default, + Value = 1, + }; + + protected override Drawable CreateContent() => new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = new RevertToDefaultButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(scale), + Current = current, + } + }; + + [Test] + public void TestStates() + { + AddStep("create content", () => CreateThemedContent(OverlayColourScheme.Purple)); + AddSliderStep("set scale", 1, 4, 1, scale => + { + this.scale = scale; + foreach (var revertToDefaultButton in this.ChildrenOfType>()) + revertToDefaultButton.Parent!.Scale = new Vector2(scale); + }); + AddToggleStep("toggle default state", state => current.Value = state ? default : 1); + AddToggleStep("toggle disabled state", state => current.Disabled = state); + } + } +} diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs index 384508f375..2926b11067 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Settings public void TestRestoreDefaultValueButtonVisibility() { SettingsTextBox textBox = null; - RestoreDefaultValueButton restoreDefaultValueButton = null; + RevertToDefaultButton revertToDefaultButton = null; AddStep("create settings item", () => { @@ -33,22 +33,22 @@ namespace osu.Game.Tests.Visual.Settings }; }); AddUntilStep("wait for loaded", () => textBox.IsLoaded); - AddStep("retrieve restore default button", () => restoreDefaultValueButton = textBox.ChildrenOfType>().Single()); + AddStep("retrieve restore default button", () => revertToDefaultButton = textBox.ChildrenOfType>().Single()); - AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); + AddAssert("restore button hidden", () => revertToDefaultButton.Alpha == 0); AddStep("change value from default", () => textBox.Current.Value = "non-default"); - AddUntilStep("restore button shown", () => restoreDefaultValueButton.Alpha > 0); + AddUntilStep("restore button shown", () => revertToDefaultButton.Alpha > 0); AddStep("restore default", () => textBox.Current.SetDefault()); - AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); + AddUntilStep("restore button hidden", () => revertToDefaultButton.Alpha == 0); } [Test] public void TestSetAndClearLabelText() { SettingsTextBox textBox = null; - RestoreDefaultValueButton restoreDefaultValueButton = null; + RevertToDefaultButton revertToDefaultButton = null; OsuTextBox control = null; AddStep("create settings item", () => @@ -61,25 +61,25 @@ namespace osu.Game.Tests.Visual.Settings AddUntilStep("wait for loaded", () => textBox.IsLoaded); AddStep("retrieve components", () => { - restoreDefaultValueButton = textBox.ChildrenOfType>().Single(); + revertToDefaultButton = textBox.ChildrenOfType>().Single(); control = textBox.ChildrenOfType().Single(); }); - AddStep("set non-default value", () => restoreDefaultValueButton.Current.Value = "non-default"); - AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1)); + AddStep("set non-default value", () => revertToDefaultButton.Current.Value = "non-default"); + AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(revertToDefaultButton.Parent!.DrawHeight, control.DrawHeight, 1)); AddStep("set label", () => textBox.LabelText = "label text"); AddAssert("default value button centre aligned to label size", () => { var label = textBox.ChildrenOfType().Single(spriteText => spriteText.Text == "label text"); - return Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, label.DrawHeight, 1); + return Precision.AlmostEquals(revertToDefaultButton.Parent!.DrawHeight, label.DrawHeight, 1); }); AddStep("clear label", () => textBox.LabelText = default); - AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1)); + AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(revertToDefaultButton.Parent!.DrawHeight, control.DrawHeight, 1)); AddStep("set warning text", () => textBox.SetNoticeText("This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator...", true)); - AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1)); + AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(revertToDefaultButton.Parent!.DrawHeight, control.DrawHeight, 1)); } /// @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Settings { BindableFloat current = null; SettingsSlider sliderBar = null; - RestoreDefaultValueButton restoreDefaultValueButton = null; + RevertToDefaultButton revertToDefaultButton = null; AddStep("create settings item", () => { @@ -107,15 +107,15 @@ namespace osu.Game.Tests.Visual.Settings }; }); AddUntilStep("wait for loaded", () => sliderBar.IsLoaded); - AddStep("retrieve restore default button", () => restoreDefaultValueButton = sliderBar.ChildrenOfType>().Single()); + AddStep("retrieve restore default button", () => revertToDefaultButton = sliderBar.ChildrenOfType>().Single()); - AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); + AddAssert("restore button hidden", () => revertToDefaultButton.Alpha == 0); AddStep("change value to next closest", () => sliderBar.Current.Value += current.Precision * 0.6f); - AddUntilStep("restore button shown", () => restoreDefaultValueButton.Alpha > 0); + AddUntilStep("restore button shown", () => revertToDefaultButton.Alpha > 0); AddStep("restore default", () => sliderBar.Current.SetDefault()); - AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); + AddUntilStep("restore button hidden", () => revertToDefaultButton.Alpha == 0); } [Test] diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs index 24c2eee783..69e489b247 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs @@ -140,6 +140,17 @@ namespace osu.Game.Tests.Visual.Settings AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType().FirstOrDefault()?.HasFocus == true); } + [Test] + public void TestSearchTextBoxSelectedOnShow() + { + SearchTextBox searchTextBox = null!; + + AddStep("set text", () => (searchTextBox = settings.SectionsContainer.ChildrenOfType().First()).Current.Value = "some text"); + AddAssert("no text selected", () => searchTextBox.SelectedText == string.Empty); + AddRepeatStep("toggle visibility", () => settings.ToggleVisibility(), 2); + AddAssert("search text selected", () => searchTextBox.SelectedText == searchTextBox.Current.Value); + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs index 30811bab32..309438e51c 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.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 NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 61a8322ee3..c509d40e07 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -39,6 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelect private BeatmapInfo currentSelection => carousel.SelectedBeatmapInfo; private const int set_count = 5; + private const int diff_count = 3; [BackgroundDependencyLoader] private void load(RulesetStore rulesets) @@ -111,7 +112,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestScrollPositionMaintainedOnAdd() { - loadBeatmaps(count: 1, randomDifficulties: false); + loadBeatmaps(setCount: 1); for (int i = 0; i < 10; i++) { @@ -124,7 +125,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestDeletion() { - loadBeatmaps(count: 5, randomDifficulties: true); + loadBeatmaps(setCount: 5, randomDifficulties: true); AddStep("remove first set", () => carousel.RemoveBeatmapSet(carousel.Items.Select(item => item.Item).OfType().First().BeatmapSet)); AddUntilStep("4 beatmap sets visible", () => this.ChildrenOfType().Count(set => set.Alpha > 0) == 4); @@ -133,7 +134,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestScrollPositionMaintainedOnDelete() { - loadBeatmaps(count: 50, randomDifficulties: false); + loadBeatmaps(setCount: 50); for (int i = 0; i < 10; i++) { @@ -150,7 +151,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestManyPanels() { - loadBeatmaps(count: 5000, randomDifficulties: true); + loadBeatmaps(setCount: 5000, randomDifficulties: true); } [Test] @@ -453,6 +454,25 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false)); } + [Test] + public void TestRewindToDeletedBeatmap() + { + loadBeatmaps(); + + var firstAdded = TestResources.CreateTestBeatmapSetInfo(); + + AddStep("add new set", () => carousel.UpdateBeatmapSet(firstAdded)); + AddStep("select set", () => carousel.SelectBeatmap(firstAdded.Beatmaps.First())); + + nextRandom(); + + AddStep("delete set", () => carousel.RemoveBeatmapSet(firstAdded)); + + prevRandom(); + + AddAssert("deleted set not selected", () => carousel.SelectedBeatmapSet?.Equals(firstAdded) == false); + } + /// /// Test adding and removing beatmap sets /// @@ -482,6 +502,34 @@ namespace osu.Game.Tests.Visual.SongSelect waitForSelection(set_count); } + [Test] + public void TestAddRemoveDifficultySort() + { + const int local_set_count = 2; + const int local_diff_count = 2; + + loadBeatmaps(setCount: local_set_count, diffCount: local_diff_count); + + AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false)); + + checkVisibleItemCount(false, local_set_count * local_diff_count); + + var firstAdded = TestResources.CreateTestBeatmapSetInfo(local_diff_count); + firstAdded.Status = BeatmapOnlineStatus.Loved; + + AddStep("Add new set", () => carousel.UpdateBeatmapSet(firstAdded)); + + checkVisibleItemCount(false, (local_set_count + 1) * local_diff_count); + + AddStep("Remove set", () => carousel.RemoveBeatmapSet(firstAdded)); + + checkVisibleItemCount(false, (local_set_count) * local_diff_count); + + setSelected(local_set_count, 1); + + waitForSelection(local_set_count); + } + [Test] public void TestSelectionEnteringFromEmptyRuleset() { @@ -643,7 +691,7 @@ namespace osu.Game.Tests.Visual.SongSelect for (int i = 0; i < 3; i++) { - var set = TestResources.CreateTestBeatmapSetInfo(3); + var set = TestResources.CreateTestBeatmapSetInfo(diff_count); // only need to set the first as they are a shared reference. var beatmap = set.Beatmaps.First(); @@ -690,7 +738,7 @@ namespace osu.Game.Tests.Visual.SongSelect for (int i = 0; i < 3; i++) { - var set = TestResources.CreateTestBeatmapSetInfo(3); + var set = TestResources.CreateTestBeatmapSetInfo(diff_count); // only need to set the first as they are a shared reference. var beatmap = set.Beatmaps.First(); @@ -739,32 +787,54 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - public void TestSortingWithFiltered() + public void TestSortingWithDifficultyFiltered() { + const int local_diff_count = 3; + const int local_set_count = 2; + List sets = new List(); AddStep("Populuate beatmap sets", () => { sets.Clear(); - for (int i = 0; i < 3; i++) + for (int i = 0; i < local_set_count; i++) { - var set = TestResources.CreateTestBeatmapSetInfo(3); + var set = TestResources.CreateTestBeatmapSetInfo(local_diff_count); set.Beatmaps[0].StarRating = 3 - i; - set.Beatmaps[2].StarRating = 6 + i; + set.Beatmaps[1].StarRating = 6 + i; sets.Add(set); } }); loadBeatmaps(sets); + AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false)); + + checkVisibleItemCount(false, local_set_count * local_diff_count); + checkVisibleItemCount(true, 1); + AddStep("Filter to normal", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Normal" }, false)); - AddAssert("Check first set at end", () => carousel.BeatmapSets.First().Equals(sets.Last())); - AddAssert("Check last set at start", () => carousel.BeatmapSets.Last().Equals(sets.First())); + checkVisibleItemCount(false, local_set_count); + checkVisibleItemCount(true, 1); + + AddUntilStep("Check all visible sets have one normal", () => + { + return carousel.Items.OfType() + .Where(p => p.IsPresent) + .Count(p => ((CarouselBeatmapSet)p.Item)!.Beatmaps.Single().BeatmapInfo.DifficultyName.StartsWith("Normal", StringComparison.Ordinal)) == local_set_count; + }); AddStep("Filter to insane", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Insane" }, false)); - AddAssert("Check first set at start", () => carousel.BeatmapSets.First().Equals(sets.First())); - AddAssert("Check last set at end", () => carousel.BeatmapSets.Last().Equals(sets.Last())); + checkVisibleItemCount(false, local_set_count); + checkVisibleItemCount(true, 1); + + AddUntilStep("Check all visible sets have one insane", () => + { + return carousel.Items.OfType() + .Where(p => p.IsPresent) + .Count(p => ((CarouselBeatmapSet)p.Item)!.Beatmaps.Single().BeatmapInfo.DifficultyName.StartsWith("Insane", StringComparison.Ordinal)) == local_set_count; + }); } [Test] @@ -819,7 +889,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("create hidden set", () => { - hidingSet = TestResources.CreateTestBeatmapSetInfo(3); + hidingSet = TestResources.CreateTestBeatmapSetInfo(diff_count); hidingSet.Beatmaps[1].Hidden = true; hiddenList.Clear(); @@ -866,7 +936,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("add mixed ruleset beatmapset", () => { - testMixed = TestResources.CreateTestBeatmapSetInfo(3); + testMixed = TestResources.CreateTestBeatmapSetInfo(diff_count); for (int i = 0; i <= 2; i++) { @@ -888,7 +958,7 @@ namespace osu.Game.Tests.Visual.SongSelect BeatmapSetInfo testSingle = null; AddStep("add single ruleset beatmapset", () => { - testSingle = TestResources.CreateTestBeatmapSetInfo(3); + testSingle = TestResources.CreateTestBeatmapSetInfo(diff_count); testSingle.Beatmaps.ForEach(b => { b.Ruleset = rulesets.AvailableRulesets.ElementAt(1); @@ -911,7 +981,7 @@ namespace osu.Game.Tests.Visual.SongSelect manySets.Clear(); for (int i = 1; i <= 50; i++) - manySets.Add(TestResources.CreateTestBeatmapSetInfo(3)); + manySets.Add(TestResources.CreateTestBeatmapSetInfo(diff_count)); }); loadBeatmaps(manySets); @@ -936,6 +1006,43 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1); } + [Test] + public void TestCarouselRemembersSelectionDifficultySort() + { + List manySets = new List(); + + AddStep("Populate beatmap sets", () => + { + manySets.Clear(); + + for (int i = 1; i <= 50; i++) + manySets.Add(TestResources.CreateTestBeatmapSetInfo(diff_count)); + }); + + loadBeatmaps(manySets); + + AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false)); + + advanceSelection(direction: 1, diff: false); + + for (int i = 0; i < 5; i++) + { + AddStep("Toggle non-matching filter", () => + { + carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false); + }); + + AddStep("Restore no filter", () => + { + carousel.Filter(new FilterCriteria(), false); + eagerSelectedIDs.Add(carousel.SelectedBeatmapSet!.ID); + }); + } + + // always returns to same selection as long as it's available. + AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1); + } + [Test] public void TestFilteringByUserStarDifficulty() { @@ -1062,20 +1169,26 @@ namespace osu.Game.Tests.Visual.SongSelect } } - private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null, Action carouselAdjust = null, int? count = null, - bool randomDifficulties = false) + private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null, Action carouselAdjust = null, + int? setCount = null, int? diffCount = null, bool randomDifficulties = false) { bool changed = false; if (beatmapSets == null) { beatmapSets = new List(); + var statuses = Enum.GetValues() + .Except(new[] { BeatmapOnlineStatus.None }) // make sure a badge is always shown. + .ToArray(); - for (int i = 1; i <= (count ?? set_count); i++) + for (int i = 1; i <= (setCount ?? set_count); i++) { - beatmapSets.Add(randomDifficulties + var set = randomDifficulties ? TestResources.CreateTestBeatmapSetInfo() - : TestResources.CreateTestBeatmapSetInfo(3)); + : TestResources.CreateTestBeatmapSetInfo(diffCount ?? diff_count); + set.Status = statuses[RNG.Next(statuses.Length)]; + + beatmapSets.Add(set); } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index a470ed47d4..7cd4f06bce 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -15,6 +15,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Sprites; +using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; @@ -188,7 +189,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddUntilStep($"displayed bpm is {target}", () => { - var label = infoWedge.DisplayedContent.ChildrenOfType().Single(l => l.Statistic.Name == "BPM"); + var label = infoWedge.DisplayedContent.ChildrenOfType().Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsBpm); return label.Statistic.Content == target; }); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs new file mode 100644 index 0000000000..2a3269ea0a --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs @@ -0,0 +1,219 @@ +// 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.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Legacy; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Screens.Select; +using osuTK; + +namespace osu.Game.Tests.Visual.SongSelect +{ + [TestFixture] + public partial class TestSceneBeatmapInfoWedgeV2 : OsuTestScene + { + private RulesetStore rulesets = null!; + private TestBeatmapInfoWedgeV2 infoWedge = null!; + private readonly List beatmaps = new List(); + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + this.rulesets = rulesets; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddRange(new Drawable[] + { + // This exists only to make the wedge more visible in the test scene + new Box + { + Y = -20, + Colour = Colour4.Cornsilk.Darken(0.2f), + Height = BeatmapInfoWedgeV2.WEDGE_HEIGHT + 40, + Width = 0.65f, + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Top = 20, Left = -10 } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 20 }, + Child = infoWedge = new TestBeatmapInfoWedgeV2 + { + Width = 0.6f, + RelativeSizeAxes = Axes.X, + }, + } + }); + + AddSliderStep("change star difficulty", 0, 11.9, 5.55, v => + { + foreach (var hasCurrentValue in infoWedge.ChildrenOfType>()) + hasCurrentValue.Current.Value = new StarDifficulty(v, 0); + }); + } + + [Test] + public void TestRulesetChange() + { + selectBeatmap(Beatmap.Value.Beatmap); + + AddWaitStep("wait for select", 3); + + foreach (var rulesetInfo in rulesets.AvailableRulesets) + { + var instance = rulesetInfo.CreateInstance(); + var testBeatmap = createTestBeatmap(rulesetInfo); + + beatmaps.Add(testBeatmap); + + setRuleset(rulesetInfo); + + selectBeatmap(testBeatmap); + + testBeatmapLabels(instance); + } + } + + [Test] + public void TestWedgeVisibility() + { + AddStep("hide", () => { infoWedge.Hide(); }); + AddWaitStep("wait for hide", 3); + AddAssert("check visibility", () => infoWedge.Alpha == 0); + AddStep("show", () => { infoWedge.Show(); }); + AddWaitStep("wait for show", 1); + AddAssert("check visibility", () => infoWedge.Alpha > 0); + } + + private void testBeatmapLabels(Ruleset ruleset) + { + AddAssert("check title", () => infoWedge.Info!.TitleLabel.Current.Value == $"{ruleset.ShortName}Title"); + AddAssert("check artist", () => infoWedge.Info!.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist"); + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("reset mods", () => SelectedMods.SetDefault()); + } + + [Test] + public void TestTruncation() + { + selectBeatmap(createLongMetadata()); + } + + [Test] + public void TestNullBeatmapWithBackground() + { + selectBeatmap(null); + AddAssert("check default title", () => infoWedge.Info!.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title); + AddAssert("check default artist", () => infoWedge.Info!.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist); + AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType().Any()); + } + + private void setRuleset(RulesetInfo rulesetInfo) + { + Container? containerBefore = null; + + AddStep("set ruleset", () => + { + // wedge content is only refreshed if the ruleset changes, so only wait for load in that case. + if (!rulesetInfo.Equals(Ruleset.Value)) + containerBefore = infoWedge.DisplayedContent; + + Ruleset.Value = rulesetInfo; + }); + + AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore); + } + + private void selectBeatmap(IBeatmap? b) + { + Container? containerBefore = null; + + AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () => + { + containerBefore = infoWedge.DisplayedContent; + infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b); + infoWedge.Show(); + }); + + AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore); + } + + private IBeatmap createTestBeatmap(RulesetInfo ruleset) + { + List objects = new List(); + for (double i = 0; i < 50000; i += 1000) + objects.Add(new TestHitObject { StartTime = i }); + + return new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Author = { Username = $"{ruleset.ShortName}Author" }, + Artist = $"{ruleset.ShortName}Artist", + Source = $"{ruleset.ShortName}Source", + Title = $"{ruleset.ShortName}Title" + }, + Ruleset = ruleset, + StarRating = 6, + DifficultyName = $"{ruleset.ShortName}Version", + Difficulty = new BeatmapDifficulty() + }, + HitObjects = objects + }; + } + + private IBeatmap createLongMetadata() + { + return new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Author = { Username = "WWWWWWWWWWWWWWW" }, + Artist = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Artist", + Source = "Verrrrry long Source", + Title = "Verrrrry long Title" + }, + DifficultyName = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Version", + Status = BeatmapOnlineStatus.Graveyard, + }, + }; + } + + private partial class TestBeatmapInfoWedgeV2 : BeatmapInfoWedgeV2 + { + public new Container? DisplayedContent => base.DisplayedContent; + public new WedgeInfoText? Info => base.Info; + } + + private class TestHitObject : ConvertHitObject, IHasPosition + { + public float X => 0; + public float Y => 0; + public Vector2 Position { get; } = Vector2.Zero; + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs index c2537cff79..379bd838cd 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs @@ -1,10 +1,9 @@ // 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.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -24,10 +23,10 @@ namespace osu.Game.Tests.Visual.SongSelect { public partial class TestSceneBeatmapMetadataDisplay : OsuTestScene { - private BeatmapMetadataDisplay display; + private BeatmapMetadataDisplay display = null!; [Resolved] - private BeatmapManager manager { get; set; } + private BeatmapManager manager { get; set; } = null!; [Cached(typeof(BeatmapDifficultyCache))] private readonly TestBeatmapDifficultyCache testDifficultyCache = new TestBeatmapDifficultyCache(); @@ -121,7 +120,7 @@ namespace osu.Game.Tests.Visual.SongSelect private partial class TestBeatmapDifficultyCache : BeatmapDifficultyCache { - private TaskCompletionSource calculationBlocker; + private TaskCompletionSource? calculationBlocker; private bool blockCalculation; @@ -142,10 +141,13 @@ namespace osu.Game.Tests.Visual.SongSelect } } - public override async Task GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo rulesetInfo = null, IEnumerable mods = null, CancellationToken cancellationToken = default) + public override async Task GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, IEnumerable? mods = null, CancellationToken cancellationToken = default) { if (blockCalculation) + { + Debug.Assert(calculationBlocker != null); await calculationBlocker.Task.ConfigureAwait(false); + } return await base.GetDifficultyAsync(beatmapInfo, rulesetInfo, mods, cancellationToken).ConfigureAwait(false); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs index 46a26d2e98..fa4981c137 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.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.ComponentModel; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 64e2447cca..00a0d4a849 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -210,7 +210,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Click(MouseButton.Left); }); - AddAssert("collection filter still selected", () => control.CreateCriteria().CollectionBeatmapMD5Hashes.Any()); + AddAssert("collection filter still selected", () => control.CreateCriteria().CollectionBeatmapMD5Hashes?.Any() == true); AddAssert("filter request not fired", () => !received); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index f094d40caa..7313bde8fe 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -13,6 +13,7 @@ using osu.Framework.Extensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; @@ -25,6 +26,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; @@ -163,7 +165,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Key(Key.Enter); }); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddAssert("ensure selection changed", () => selected != Beatmap.Value); } @@ -186,7 +188,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Key(Key.Down); }); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddAssert("ensure selection didn't change", () => selected == Beatmap.Value); } @@ -215,7 +217,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Key(Key.Enter); }); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddAssert("ensure selection changed", () => selected != Beatmap.Value); } @@ -244,7 +246,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.ReleaseButton(MouseButton.Left); }); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddAssert("ensure selection didn't change", () => selected == Beatmap.Value); } @@ -257,7 +259,7 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child"))); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddStep("return", () => songSelect!.MakeCurrent()); AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen()); @@ -275,7 +277,7 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child"))); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); @@ -292,7 +294,7 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child"))); - AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddStep("update beatmap", () => { @@ -311,7 +313,9 @@ namespace osu.Game.Tests.Visual.SongSelect { createSongSelect(); - addRulesetImportStep(0); + // We need to use one real beatmap to trigger the "same-track-transfer" logic that we're looking to test here. + // See `SongSelect.ensurePlayingSelected` and `WorkingBeatmap.TryTransferTrack`. + AddStep("import test beatmap", () => manager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).WaitSafely()); addRulesetImportStep(0); checkMusicPlaying(true); @@ -320,6 +324,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("manual pause", () => music.TogglePause()); checkMusicPlaying(false); + + // Track should not have changed, so music should still not be playing. AddStep("select next difficulty", () => songSelect!.Carousel.SelectNext(skipDifficulties: false)); checkMusicPlaying(false); @@ -825,6 +831,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Click on a filtered difficulty", () => { + Debug.Assert(filteredIcon != null); + InputManager.MoveMouseTo(filteredIcon); InputManager.Click(MouseButton.Left); @@ -918,6 +926,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Click on a difficulty", () => { + Debug.Assert(difficultyIcon != null); + InputManager.MoveMouseTo(difficultyIcon); InputManager.Click(MouseButton.Left); @@ -1007,7 +1017,7 @@ namespace osu.Game.Tests.Visual.SongSelect }); }); - AddUntilStep("wait for results screen presented", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(getPresentBeatmap())); AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0); @@ -1036,7 +1046,7 @@ namespace osu.Game.Tests.Visual.SongSelect songSelect!.PresentScore(TestResources.CreateTestScoreInfo(getPresentBeatmap())); }); - AddUntilStep("wait for results screen presented", () => !songSelect!.IsCurrentScreen()); + waitForDismissed(); AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(getPresentBeatmap())); AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0); @@ -1083,6 +1093,42 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("0 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "0 matches"); } + [Test] + public void TestDeleteHotkey() + { + createSongSelect(); + + addRulesetImportStep(0); + AddAssert("3 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "3 matches"); + + AddStep("press shift-delete", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.Key(Key.Delete); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + AddUntilStep("delete dialog shown", () => DialogOverlay.CurrentDialog, Is.InstanceOf); + AddStep("confirm deletion", () => DialogOverlay.CurrentDialog!.PerformAction()); + AddAssert("0 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "0 matches"); + } + + [Test] + public void TestCutInFilterTextBox() + { + createSongSelect(); + + AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType().First().Text = "nonono"); + AddStep("select all", () => InputManager.Keys(PlatformAction.SelectAll)); + AddStep("press ctrl-x", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.X); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + AddAssert("filter text cleared", () => songSelect!.FilterControl.ChildrenOfType().First().Text, () => Is.Empty); + } + private void waitForInitialSelection() { AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault); @@ -1157,6 +1203,8 @@ namespace osu.Game.Tests.Visual.SongSelect rulesets.Dispose(); } + private void waitForDismissed() => AddUntilStep("wait for not current", () => !songSelect.AsNonNull().IsCurrentScreen()); + private partial class TestSongSelect : PlaySongSelect { public Action? StartRequested; diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs index 72adbfc104..013bad55bc 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs @@ -6,9 +6,11 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Game.Overlays; using osu.Game.Overlays.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Screens.Select.FooterV2; using osuTK.Input; @@ -37,10 +39,10 @@ namespace osu.Game.Tests.Visual.SongSelect Children = new Drawable[] { - footer = new FooterV2 + new PopoverContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre + RelativeSizeAxes = Axes.Both, + Child = footer = new FooterV2(), }, overlay = new DummyOverlay() }; @@ -56,6 +58,24 @@ namespace osu.Game.Tests.Visual.SongSelect overlay.Hide(); }); + [SetUpSteps] + public void SetUpSteps() + { + AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo))); + } + + [Test] + public void TestShowOptions() + { + AddStep("enable options", () => + { + var optionsButton = this.ChildrenOfType().Last(); + + optionsButton.Enabled.Value = true; + optionsButton.TriggerClick(); + }); + } + [Test] public void TestState() { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs index 11d55bc0bd..6d97be730b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs @@ -15,6 +15,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; +using osu.Game.Screens.Select.Filter; using osu.Game.Tests.Online; using osu.Game.Tests.Resources; using osuTK.Input; @@ -192,6 +193,57 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("release mouse button", () => InputManager.ReleaseButton(MouseButton.Left)); } + [Test] + public void TestSplitDisplay() + { + ArchiveDownloadRequest? downloadRequest = null; + + AddStep("set difficulty sort mode", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty })); + AddStep("update online hash", () => + { + testBeatmapSetInfo.Beatmaps.First().OnlineMD5Hash = "different hash"; + testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now; + + carousel.UpdateBeatmapSet(testBeatmapSetInfo); + }); + + AddUntilStep("multiple \"sets\" visible", () => carousel.ChildrenOfType().Count(), () => Is.GreaterThan(1)); + AddUntilStep("update button visible", getUpdateButton, () => Is.Not.Null); + + AddStep("click button", () => getUpdateButton()?.TriggerClick()); + + AddUntilStep("wait for download started", () => + { + downloadRequest = beatmapDownloader.GetExistingDownload(testBeatmapSetInfo); + return downloadRequest != null; + }); + + AddUntilStep("wait for button disabled", () => getUpdateButton()?.Enabled.Value == false); + + AddUntilStep("progress download to completion", () => + { + if (downloadRequest is TestSceneOnlinePlayBeatmapAvailabilityTracker.TestDownloadRequest testRequest) + { + testRequest.SetProgress(testRequest.Progress + 0.1f); + + if (testRequest.Progress >= 1) + { + testRequest.TriggerSuccess(); + + // usually this would be done by the import process. + testBeatmapSetInfo.Beatmaps.First().MD5Hash = "different hash"; + testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now; + + // usually this would be done by a realm subscription. + carousel.UpdateBeatmapSet(testBeatmapSetInfo); + return true; + } + } + + return false; + }); + } + private BeatmapCarousel createCarousel() { return carousel = new BeatmapCarousel @@ -199,7 +251,7 @@ namespace osu.Game.Tests.Visual.SongSelect RelativeSizeAxes = Axes.Both, BeatmapSets = new List { - (testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo()), + (testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(5)), } }; } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs index 0476198e41..30f1803795 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs b/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs index 7f01a67903..6b39717354 100644 --- a/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs +++ b/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs @@ -56,38 +56,38 @@ namespace osu.Game.Tests.Visual public void AllowTrackAdjustmentsTest() { AddStep("push allowing screen", () => stack.Push(loadNewScreen())); - AddAssert("allows adjustments 1", () => musicController.AllowTrackAdjustments); + AddAssert("allows adjustments 1", () => musicController.ApplyModTrackAdjustments); AddStep("push inheriting screen", () => stack.Push(loadNewScreen())); - AddAssert("allows adjustments 2", () => musicController.AllowTrackAdjustments); + AddAssert("allows adjustments 2", () => musicController.ApplyModTrackAdjustments); AddStep("push disallowing screen", () => stack.Push(loadNewScreen())); - AddAssert("disallows adjustments 3", () => !musicController.AllowTrackAdjustments); + AddAssert("disallows adjustments 3", () => !musicController.ApplyModTrackAdjustments); AddStep("push inheriting screen", () => stack.Push(loadNewScreen())); - AddAssert("disallows adjustments 4", () => !musicController.AllowTrackAdjustments); + AddAssert("disallows adjustments 4", () => !musicController.ApplyModTrackAdjustments); AddStep("push inheriting screen", () => stack.Push(loadNewScreen())); - AddAssert("disallows adjustments 5", () => !musicController.AllowTrackAdjustments); + AddAssert("disallows adjustments 5", () => !musicController.ApplyModTrackAdjustments); AddStep("push allowing screen", () => stack.Push(loadNewScreen())); - AddAssert("allows adjustments 6", () => musicController.AllowTrackAdjustments); + AddAssert("allows adjustments 6", () => musicController.ApplyModTrackAdjustments); // Now start exiting from screens AddStep("exit screen", () => stack.Exit()); - AddAssert("disallows adjustments 7", () => !musicController.AllowTrackAdjustments); + AddAssert("disallows adjustments 7", () => !musicController.ApplyModTrackAdjustments); AddStep("exit screen", () => stack.Exit()); - AddAssert("disallows adjustments 8", () => !musicController.AllowTrackAdjustments); + AddAssert("disallows adjustments 8", () => !musicController.ApplyModTrackAdjustments); AddStep("exit screen", () => stack.Exit()); - AddAssert("disallows adjustments 9", () => !musicController.AllowTrackAdjustments); + AddAssert("disallows adjustments 9", () => !musicController.ApplyModTrackAdjustments); AddStep("exit screen", () => stack.Exit()); - AddAssert("allows adjustments 10", () => musicController.AllowTrackAdjustments); + AddAssert("allows adjustments 10", () => musicController.ApplyModTrackAdjustments); AddStep("exit screen", () => stack.Exit()); - AddAssert("allows adjustments 11", () => musicController.AllowTrackAdjustments); + AddAssert("allows adjustments 11", () => musicController.ApplyModTrackAdjustments); } public partial class TestScreen : ScreenWithBeatmapBackground @@ -129,12 +129,12 @@ namespace osu.Game.Tests.Visual private partial class AllowScreen : OsuScreen { - public override bool? AllowTrackAdjustments => true; + public override bool? ApplyModTrackAdjustments => true; } public partial class DisallowScreen : OsuScreen { - public override bool? AllowTrackAdjustments => false; + public override bool? ApplyModTrackAdjustments => false; } private partial class InheritScreen : OsuScreen diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs index 837de60053..494268b158 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 5d97714ab5..c723610614 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Audio.Track; @@ -283,8 +282,6 @@ namespace osu.Game.Tests.Visual.UserInterface if (ReferenceEquals(timingPoints[^1], current)) { - Debug.Assert(BeatSyncSource.Clock != null); - return (int)Math.Ceiling((BeatSyncSource.Clock.CurrentTime - current.Time) / current.BeatLength); } @@ -295,8 +292,6 @@ namespace osu.Game.Tests.Visual.UserInterface { base.Update(); - Debug.Assert(BeatSyncSource.Clock != null); - timeUntilNextBeat.Value = TimeUntilNextBeat; timeSinceLastBeat.Value = TimeSinceLastBeat; currentTime.Value = BeatSyncSource.Clock.CurrentTime; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs index dd7bf48791..343378ccfb 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.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; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs index 7f7ba6966b..5e22450ba8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBreadcrumbControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBreadcrumbControl.cs index eeb2d1e70f..92bf2448b1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBreadcrumbControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBreadcrumbControl.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; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneColourPicker.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneColourPicker.cs index d2acf89dc8..bc3f1d0070 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneColourPicker.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneColourPicker.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs index e7840d4a2a..b17024ae8f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Overlays.Comments; using osuTK; @@ -25,6 +26,7 @@ namespace osu.Game.Tests.Visual.UserInterface private TestCommentEditor commentEditor = null!; private TestCancellableCommentEditor cancellableCommentEditor = null!; + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; [SetUp] public void SetUp() => Schedule(() => @@ -96,12 +98,43 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("button is not loading", () => !commentEditor.IsSpinnerShown); } + [Test] + public void TestLoggingInAndOut() + { + void assertLoggedInState() + { + AddAssert("commit button visible", () => commentEditor.ButtonsContainer[0].Alpha == 1); + AddAssert("login button hidden", () => commentEditor.ButtonsContainer[1].Alpha == 0); + AddAssert("text box editable", () => !commentEditor.TextBox.ReadOnly); + } + + void assertLoggedOutState() + { + AddAssert("commit button hidden", () => commentEditor.ButtonsContainer[0].Alpha == 0); + AddAssert("login button visible", () => commentEditor.ButtonsContainer[1].Alpha == 1); + AddAssert("text box readonly", () => commentEditor.TextBox.ReadOnly); + } + + // there's also the case of starting logged out, but more annoying to test. + + // starting logged in + assertLoggedInState(); + + // moving from logged in -> logged out + AddStep("log out", () => dummyAPI.Logout()); + assertLoggedOutState(); + + // moving from logged out -> logged in + AddStep("log back in", () => dummyAPI.Login("username", "password")); + assertLoggedInState(); + } + [Test] public void TestCancelAction() { AddStep("click cancel button", () => { - InputManager.MoveMouseTo(cancellableCommentEditor.ButtonsContainer[1]); + InputManager.MoveMouseTo(cancellableCommentEditor.ButtonsContainer[2]); InputManager.Click(MouseButton.Left); }); @@ -112,6 +145,7 @@ namespace osu.Game.Tests.Visual.UserInterface { public new Bindable Current => base.Current; public new FillFlowContainer ButtonsContainer => base.ButtonsContainer; + public new TextBox TextBox => base.TextBox; public string CommittedText { get; private set; } = string.Empty; @@ -125,8 +159,12 @@ namespace osu.Game.Tests.Visual.UserInterface } protected override LocalisableString FooterText => @"Footer text. And it is pretty long. Cool."; - protected override LocalisableString CommitButtonText => @"Commit"; - protected override LocalisableString TextBoxPlaceholder => @"This text box is empty"; + + protected override LocalisableString GetButtonText(bool isLoggedIn) => + isLoggedIn ? @"Commit" : "You're logged out!"; + + protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => + isLoggedIn ? @"This text box is empty" : "Still empty, but now you can't type in it."; } private partial class TestCancellableCommentEditor : CancellableCommentEditor @@ -146,8 +184,8 @@ namespace osu.Game.Tests.Visual.UserInterface { } - protected override LocalisableString CommitButtonText => @"Save"; - protected override LocalisableString TextBoxPlaceholder => @"Multiline textboxes soon"; + protected override LocalisableString GetButtonText(bool isLoggedIn) => @"Save"; + protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => @"Multiline textboxes soon"; } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs index 1bfa389a25..eaaf40fb36 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.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 osu.Game.Overlays.Comments.Buttons; using osu.Framework.Graphics; using osu.Framework.Allocation; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs index 3491b7dbc1..7b80549854 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.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.Collections.Generic; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs index 01d4eb83f3..a8eaabe758 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.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 NUnit.Framework; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs index 6092f35050..77658dc482 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.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 osu.Framework.Graphics.Containers; using osu.Framework.Graphics; using osu.Game.Overlays.Dashboard.Home; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDrawableDate.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDrawableDate.cs index 108ad8b7c1..b590abf4e5 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDrawableDate.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDrawableDate.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneEditorSidebar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneEditorSidebar.cs index 72dacb7558..7d1b3a4bb7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneEditorSidebar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneEditorSidebar.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingBar.cs index 9d850c0fc5..ed0fd340e7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingBar.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBehaviour.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBehaviour.cs index ec8ef0ad50..4b589ffe4c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBehaviour.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBehaviour.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Screens; using osu.Game.Overlays; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBundledBeatmaps.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBundledBeatmaps.cs index e9460e45d3..30a36652c2 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBundledBeatmaps.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBundledBeatmaps.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Screens; using osu.Game.Overlays; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenImportFromStable.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenImportFromStable.cs index e6fc889a70..680b54f637 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenImportFromStable.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenImportFromStable.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Threading; using System.Threading.Tasks; using Moq; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenUIScale.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenUIScale.cs index 8ba94cf9ae..2dee57f4cb 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenUIScale.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenUIScale.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Screens; using osu.Game.Overlays; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs index 77ed97e3ed..9275f9755f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs @@ -214,6 +214,8 @@ namespace osu.Game.Tests.Visual.UserInterface } public virtual IBindable UnreadCount => null; + + public IEnumerable AllNotifications => Enumerable.Empty(); } // interface mocks break hot reload, mocking this stub implementation instead works around it. diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs index 24b4060a42..4e1bf1390a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToExitGameOverlay.cs similarity index 87% rename from osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs rename to osu.Game.Tests/Visual/UserInterface/TestSceneHoldToExitGameOverlay.cs index 801bef62c8..df423268b6 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToExitGameOverlay.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 osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -10,11 +8,11 @@ using osu.Game.Screens.Menu; namespace osu.Game.Tests.Visual.UserInterface { - public partial class TestSceneHoldToConfirmOverlay : OsuTestScene + public partial class TestSceneHoldToExitGameOverlay : OsuTestScene { protected override double TimePerAction => 100; // required for the early exit test, since hold-to-confirm delay is 200ms - public TestSceneHoldToConfirmOverlay() + public TestSceneHoldToExitGameOverlay() { bool fired = false; @@ -27,7 +25,7 @@ namespace osu.Game.Tests.Visual.UserInterface Alpha = 0, }; - var overlay = new TestHoldToConfirmOverlay + var overlay = new TestHoldToExitGameOverlay { Action = () => { @@ -59,7 +57,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("wait until fired again", () => overlay.Fired); } - private partial class TestHoldToConfirmOverlay : ExitConfirmOverlay + private partial class TestHoldToExitGameOverlay : HoldToExitGameOverlay { public void Begin() => BeginConfirm(); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneIconButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneIconButton.cs index 454fa7cd05..a1910570dd 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneIconButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneIconButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDropdown.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDropdown.cs index a2cfae3c7f..300b451cf5 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDropdown.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDropdown.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterfaceV2; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs index 6181891e13..726f13861b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSwitchButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSwitchButton.cs index c4af47bd0f..bec517af2c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSwitchButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSwitchButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs index 8046554819..a9f6b812df 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingSpinner.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingSpinner.cs index 40e786592a..bd36be846b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingSpinner.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingSpinner.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 osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs index f9d92aabc6..863e59f6a7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs index 5926f07a11..57ea4ee58e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs @@ -269,8 +269,8 @@ namespace osu.Game.Tests.Visual.UserInterface if (!logoFacade.Transforms.Any() && !transferContainer.Transforms.Any()) { Random random = new Random(); - trackingContainer.Delay(500).MoveTo(new Vector2(random.Next(0, (int)logo.Parent.DrawWidth), random.Next(0, (int)logo.Parent.DrawHeight)), 300); - transferContainer.Delay(500).MoveTo(new Vector2(random.Next(0, (int)logo.Parent.DrawWidth), random.Next(0, (int)logo.Parent.DrawHeight)), 300); + trackingContainer.Delay(500).MoveTo(new Vector2(random.Next(0, (int)logo.Parent!.DrawWidth), random.Next(0, (int)logo.Parent!.DrawHeight)), 300); + transferContainer.Delay(500).MoveTo(new Vector2(random.Next(0, (int)logo.Parent!.DrawWidth), random.Next(0, (int)logo.Parent!.DrawHeight)), 300); } if (randomPositions) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMissingBeatmapNotification.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneMissingBeatmapNotification.cs new file mode 100644 index 0000000000..f5506edf3b --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneMissingBeatmapNotification.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Database; +using osu.Game.Overlays; +using osu.Game.Tests.Scores.IO; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public partial class TestSceneMissingBeatmapNotification : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + [BackgroundDependencyLoader] + private void load() + { + Child = new Container + { + Width = 280, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = new MissingBeatmapNotification(CreateAPIBeatmapSet(Ruleset.Value).Beatmaps.First(), new ImportScoreTest.TestArchiveReader(), "deadbeef") + }; + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index a11000214c..18739c0275 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -106,26 +106,26 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase))); - AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => !panel.Filtered.Value) == 2); + AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => panel.Visible) == 2); clickToggle(); AddUntilStep("wait for animation", () => !column.SelectionAnimationRunning); - AddAssert("only visible items selected", () => column.ChildrenOfType().Where(panel => panel.Active.Value).All(panel => !panel.Filtered.Value)); + AddAssert("only visible items selected", () => column.ChildrenOfType().Where(panel => panel.Active.Value).All(panel => panel.Visible)); AddStep("unset filter", () => setFilter(null)); - AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value)); + AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => panel.Visible)); AddAssert("checkbox not selected", () => !column.ChildrenOfType().Single().Current.Value); AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase))); - AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => !panel.Filtered.Value) == 2); + AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => panel.Visible) == 2); AddAssert("checkbox selected", () => column.ChildrenOfType().Single().Current.Value); AddStep("filter out everything", () => setFilter(_ => false)); - AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => panel.Filtered.Value)); + AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => !panel.Visible)); AddUntilStep("checkbox hidden", () => !column.ChildrenOfType().Single().IsPresent); AddStep("inset filter", () => setFilter(null)); - AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value)); + AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => panel.Visible)); AddUntilStep("checkbox visible", () => column.ChildrenOfType().Single().IsPresent); void clickToggle() => AddStep("click toggle", () => @@ -288,10 +288,53 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("no change", () => this.ChildrenOfType().Count(panel => panel.Active.Value) == 2); } + [Test] + public void TestApplySearchTerms() + { + Mod hidden = getExampleModsFor(ModType.DifficultyIncrease).Where(modState => modState.Mod is ModHidden).Select(modState => modState.Mod).Single(); + + ModColumn column = null!; + AddStep("create content", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(30), + Child = column = new ModColumn(ModType.DifficultyIncrease, false) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AvailableMods = getExampleModsFor(ModType.DifficultyIncrease) + } + }); + + applySearchAndAssert(hidden.Name); + + clearSearch(); + + applySearchAndAssert(hidden.Acronym); + + clearSearch(); + + applySearchAndAssert(hidden.Description.ToString()); + + void applySearchAndAssert(string searchTerm) + { + AddStep("search by mod name", () => column.SearchTerm = searchTerm); + + AddAssert("only hidden is visible", () => column.ChildrenOfType().Where(panel => panel.Visible).All(panel => panel.Mod is ModHidden)); + } + + void clearSearch() + { + AddStep("clear search", () => column.SearchTerm = string.Empty); + + AddAssert("all mods are visible", () => column.ChildrenOfType().All(panel => panel.Visible)); + } + } + private void setFilter(Func? filter) { foreach (var modState in this.ChildrenOfType().Single().AvailableMods) - modState.Filtered.Value = filter?.Invoke(modState.Mod) == false; + modState.ValidForSelection.Value = filter?.Invoke(modState.Mod) != false; } private partial class TestModColumn : ModColumn diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModDisplay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModDisplay.cs index bd5a0d8645..1bb83eeddf 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModDisplay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Rulesets.Mods; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModEffectPreviewPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModEffectPreviewPanel.cs new file mode 100644 index 0000000000..b3ad5a499e --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModEffectPreviewPanel.cs @@ -0,0 +1,131 @@ +// 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.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public partial class TestSceneModEffectPreviewPanel : OsuTestScene + { + [Cached(typeof(BeatmapDifficultyCache))] + private TestBeatmapDifficultyCache difficultyCache = new TestBeatmapDifficultyCache(); + + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + private Container content = null!; + protected override Container Content => content; + + private BeatmapAttributesDisplay panel = null!; + + [BackgroundDependencyLoader] + private void load() + { + base.Content.AddRange(new Drawable[] + { + difficultyCache, + content = new Container + { + RelativeSizeAxes = Axes.Both + } + }); + } + + [Test] + public void TestDisplay() + { + OsuModDifficultyAdjust difficultyAdjust = new OsuModDifficultyAdjust(); + OsuModDoubleTime doubleTime = new OsuModDoubleTime(); + + AddStep("create display", () => Child = panel = new BeatmapAttributesDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + AddStep("set beatmap", () => + { + var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo) + { + BeatmapInfo = + { + BPM = 120 + } + }; + + Ruleset.Value = beatmap.BeatmapInfo.Ruleset; + panel.BeatmapInfo.Value = beatmap.BeatmapInfo; + }); + + AddSliderStep("change star rating", 0, 10d, 5, stars => + { + if (panel.IsNotNull()) + previewStarRating(stars); + }); + AddStep("preview ridiculously high SR", () => previewStarRating(1234)); + + AddStep("add DA to mods", () => SelectedMods.Value = new[] { difficultyAdjust }); + + AddSliderStep("change AR", 0, 10f, 5, ar => + { + if (panel.IsNotNull()) + difficultyAdjust.ApproachRate.Value = ar; + }); + AddSliderStep("change CS", 0, 10f, 5, cs => + { + if (panel.IsNotNull()) + difficultyAdjust.CircleSize.Value = cs; + }); + AddSliderStep("change HP", 0, 10f, 5, hp => + { + if (panel.IsNotNull()) + difficultyAdjust.DrainRate.Value = hp; + }); + AddSliderStep("change OD", 0, 10f, 5, od => + { + if (panel.IsNotNull()) + difficultyAdjust.OverallDifficulty.Value = od; + }); + + AddStep("add DT to mods", () => SelectedMods.Value = new Mod[] { difficultyAdjust, doubleTime }); + AddSliderStep("change rate", 1.01d, 2d, 1.5d, rate => + { + if (panel.IsNotNull()) + doubleTime.SpeedChange.Value = rate; + }); + + AddToggleStep("toggle collapsed", collapsed => panel.Collapsed.Value = collapsed); + } + + private void previewStarRating(double stars) + { + difficultyCache.Difficulty = new StarDifficulty(stars, 0); + panel.BeatmapInfo.TriggerChange(); + } + + private partial class TestBeatmapDifficultyCache : BeatmapDifficultyCache + { + public StarDifficulty? Difficulty { get; set; } + + public override Task GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, IEnumerable? mods = null, + CancellationToken cancellationToken = default) + => Task.FromResult(Difficulty); + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs index 897d5fd9f5..11cd122c99 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs @@ -1,10 +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.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; @@ -25,6 +29,53 @@ namespace osu.Game.Tests.Visual.UserInterface ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods().Select(m => new ModIcon(m)), }; }); + + AddStep("toggle selected", () => + { + foreach (var icon in this.ChildrenOfType()) + icon.Selected.Toggle(); + }); + } + + [Test] + public void TestShowRateAdjusts() + { + AddStep("create mod icons", () => + { + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Full, + ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods() + .OfType() + .SelectMany(m => + { + List icons = new List { new ModIcon(m) }; + + for (double i = m.SpeedChange.MinValue; i < m.SpeedChange.MaxValue; i += m.SpeedChange.Precision * 10) + { + m = (ModRateAdjust)m.DeepClone(); + m.SpeedChange.Value = i; + icons.Add(new ModIcon(m)); + } + + return icons; + }), + }; + }); + + AddStep("adjust rates", () => + { + foreach (var icon in this.ChildrenOfType()) + { + if (icon.Mod is ModRateAdjust rateAdjust) + { + rateAdjust.SpeedChange.Value = RNG.NextDouble() > 0.9 + ? rateAdjust.SpeedChange.Default + : RNG.NextDouble(rateAdjust.SpeedChange.MinValue, rateAdjust.SpeedChange.MaxValue); + } + } + }); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index 3efdba8754..1779b240cc 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -9,8 +9,8 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Testing; using osu.Framework.Graphics.Cursor; +using osu.Framework.Testing; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; @@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.UserInterface var testPresets = createTestPresets(); foreach (var preset in testPresets) - preset.Ruleset = realm.Find(preset.Ruleset.ShortName); + preset.Ruleset = realm.Find(preset.Ruleset.ShortName)!; realm.Add(testPresets); }); @@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.UserInterface new ManiaModNightcore(), new ManiaModHardRock() }, - Ruleset = r.Find("mania") + Ruleset = r.Find("mania")! }))); AddUntilStep("2 panels visible", () => this.ChildrenOfType().Count() == 2); @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.UserInterface new OsuModHidden(), new OsuModHardRock() }, - Ruleset = r.Find("osu") + Ruleset = r.Find("osu")! }))); AddUntilStep("2 panels visible", () => this.ChildrenOfType().Count() == 2); @@ -304,11 +304,8 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("preset is not changed", () => panel.Preset.Value.Name == presetName); AddUntilStep("popover is unchanged", () => this.ChildrenOfType().FirstOrDefault() == popover); AddStep("edit preset name", () => popover.ChildrenOfType().First().Current.Value = "something new"); - AddStep("attempt preset edit", () => - { - InputManager.MoveMouseTo(popover.ChildrenOfType().ElementAt(1)); - InputManager.Click(MouseButton.Left); - }); + AddStep("commit changes to textbox", () => InputManager.Key(Key.Enter)); + AddStep("attempt preset edit via select binding", () => InputManager.Key(Key.Enter)); AddUntilStep("popover closed", () => !this.ChildrenOfType().Any()); AddAssert("preset is changed", () => panel.Preset.Value.Name != presetName); } @@ -392,6 +389,28 @@ namespace osu.Game.Tests.Visual.UserInterface new HashSet(this.ChildrenOfType().First().Preset.Value.Mods).SetEquals(mods)); } + [Test] + public void TestTextFiltering() + { + ModPresetColumn modPresetColumn = null!; + + AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); + AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded); + + AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0)); + AddStep("set text filter", () => modPresetColumn.SearchTerm = "First"); + AddUntilStep("one panel visible", () => modPresetColumn.ChildrenOfType().Count(panel => panel.IsPresent), () => Is.EqualTo(1)); + + AddStep("set mania ruleset", () => Ruleset.Value = rulesets.GetRuleset(3)); + AddUntilStep("no panels visible", () => modPresetColumn.ChildrenOfType().Count(panel => panel.IsPresent), () => Is.EqualTo(0)); + } + private ICollection createTestPresets() => new[] { new ModPreset diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 5cf24c1960..f0822ce2a8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -51,6 +51,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("clear contents", Clear); AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0)); AddStep("reset mods", () => SelectedMods.SetDefault()); + AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo)); AddStep("set up presets", () => { Realm.Write(r => @@ -60,7 +61,7 @@ namespace osu.Game.Tests.Visual.UserInterface { Name = "AR0", Description = "Too... many... circles...", - Ruleset = r.Find(OsuRuleset.SHORT_NAME), + Ruleset = r.Find(OsuRuleset.SHORT_NAME)!, Mods = new[] { new OsuModDifficultyAdjust @@ -73,7 +74,7 @@ namespace osu.Game.Tests.Visual.UserInterface { Name = "Half Time 0.5x", Description = "Very slow", - Ruleset = r.Find(OsuRuleset.SHORT_NAME), + Ruleset = r.Find(OsuRuleset.SHORT_NAME)!, Mods = new[] { new OsuModHalfTime @@ -92,6 +93,7 @@ namespace osu.Game.Tests.Visual.UserInterface { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, + Beatmap = Beatmap.Value, SelectedMods = { BindTarget = SelectedMods } }); waitForColumnLoad(); @@ -113,7 +115,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("mod multiplier correct", () => { double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier); - return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType().Single().Current.Value); + return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType().Single().Current.Value); }); assertCustomisationToggleState(disabled: false, active: false); AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType().Any()); @@ -128,7 +130,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("mod multiplier correct", () => { double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier); - return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType().Single().Current.Value); + return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType().Single().Current.Value); }); assertCustomisationToggleState(disabled: false, active: false); AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType().Any()); @@ -218,7 +220,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("dismiss mod customisation via toggle", () => { - InputManager.MoveMouseTo(modSelectOverlay.CustomisationButton); + InputManager.MoveMouseTo(modSelectOverlay.CustomisationButton.AsNonNull()); InputManager.Click(MouseButton.Left); }); assertCustomisationToggleState(disabled: false, active: false); @@ -490,15 +492,15 @@ namespace osu.Game.Tests.Visual.UserInterface createScreen(); changeRuleset(0); - AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value)); + AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.Visible)); AddStep("make double time invalid", () => modSelectOverlay.IsValidMod = m => !(m is OsuModDoubleTime)); - AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => panel.Filtered.Value)); - AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value)); + AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => !panel.Visible)); + AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModNightcore).Any(panel => panel.Visible)); AddStep("make double time valid again", () => modSelectOverlay.IsValidMod = _ => true); - AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value)); - AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(b => b.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value)); + AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.Visible)); + AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(b => b.Mod is OsuModNightcore).Any(panel => panel.Visible)); } [Test] @@ -524,7 +526,57 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("set ruleset", () => Ruleset.Value = testRuleset.RulesetInfo); waitForColumnLoad(); - AddAssert("unimplemented mod panel is filtered", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value); + AddAssert("unimplemented mod panel is filtered", () => !getPanelForMod(typeof(TestUnimplementedMod)).Visible); + } + + [Test] + public void TestFirstModSelectDeselect() + { + createScreen(); + + AddStep("apply search", () => modSelectOverlay.SearchTerm = "HD"); + + AddStep("press enter", () => InputManager.Key(Key.Enter)); + AddAssert("hidden selected", () => getPanelForMod(typeof(OsuModHidden)).Active.Value); + + AddStep("press enter again", () => InputManager.Key(Key.Enter)); + AddAssert("hidden deselected", () => !getPanelForMod(typeof(OsuModHidden)).Active.Value); + + AddStep("clear search", () => modSelectOverlay.SearchTerm = string.Empty); + AddStep("press enter", () => InputManager.Key(Key.Enter)); + AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden); + } + + [Test] + public void TestSearchFocusChangeViaClick() + { + createScreen(); + + AddStep("click on search", navigateAndClick); + AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus); + + AddStep("click on mod column", navigateAndClick); + AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus); + + void navigateAndClick() where T : Drawable + { + InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + } + } + + [Test] + public void TestSearchFocusChangeViaKey() + { + createScreen(); + + const Key focus_switch_key = Key.Tab; + + AddStep("press tab", () => InputManager.Key(focus_switch_key)); + AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus); + + AddStep("press tab", () => InputManager.Key(focus_switch_key)); + AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus); } [Test] @@ -533,6 +585,8 @@ namespace osu.Game.Tests.Visual.UserInterface createScreen(); changeRuleset(0); + AddStep("kill search bar focus", () => modSelectOverlay.SearchTextBox.KillFocus()); + AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() }); AddAssert("DT + HD selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value) == 2); @@ -540,6 +594,26 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any()); } + [Test] + public void TestDeselectAllViaKey_WithSearchApplied() + { + createScreen(); + changeRuleset(0); + + AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() }); + AddStep("focus on search", () => modSelectOverlay.SearchTextBox.TakeFocus()); + AddStep("apply search", () => modSelectOverlay.SearchTerm = "Easy"); + AddAssert("DT + HD selected and hidden", () => modSelectOverlay.ChildrenOfType().Count(panel => !panel.Visible && panel.Active.Value) == 2); + + AddStep("press backspace", () => InputManager.Key(Key.BackSpace)); + AddAssert("DT + HD still selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value) == 2); + AddAssert("search term changed", () => modSelectOverlay.SearchTerm == "Eas"); + + AddStep("kill focus", () => modSelectOverlay.SearchTextBox.KillFocus()); + AddStep("press backspace", () => InputManager.Key(Key.BackSpace)); + AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any()); + } + [Test] public void TestDeselectAllViaButton() { @@ -561,6 +635,31 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("deselect all button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); } + [Test] + public void TestDeselectAllViaButton_WithSearchApplied() + { + createScreen(); + changeRuleset(0); + + AddAssert("deselect all button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + + AddStep("select DT + HD + RD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModRandom() }); + AddAssert("DT + HD + RD selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value) == 3); + AddAssert("deselect all button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + + AddStep("apply search", () => modSelectOverlay.SearchTerm = "Easy"); + AddAssert("DT + HD + RD are hidden and selected", () => modSelectOverlay.ChildrenOfType().Count(panel => !panel.Visible && panel.Active.Value) == 3); + AddAssert("deselect all button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + + AddStep("click deselect all button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any()); + AddAssert("deselect all button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + } + [Test] public void TestCloseViaBackButton() { @@ -580,8 +679,11 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden); } + /// + /// Covers columns hiding/unhiding on changes of . + /// [Test] - public void TestColumnHiding() + public void TestColumnHidingOnIsValidChange() { AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay { @@ -610,6 +712,56 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("3 columns visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 3); } + /// + /// Covers columns hiding/unhiding on changes of . + /// + [Test] + public void TestColumnHidingOnTextFilterChange() + { + AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + SelectedMods = { BindTarget = SelectedMods } + }); + waitForColumnLoad(); + changeRuleset(0); + + AddAssert("all columns visible", () => this.ChildrenOfType().All(col => col.IsPresent)); + + AddStep("set search", () => modSelectOverlay.SearchTerm = "HD"); + AddAssert("one column visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 1); + + AddStep("filter out everything", () => modSelectOverlay.SearchTerm = "Some long search term with no matches"); + AddAssert("no columns visible", () => this.ChildrenOfType().All(col => !col.IsPresent)); + + AddStep("clear search bar", () => modSelectOverlay.SearchTerm = ""); + AddAssert("all columns visible", () => this.ChildrenOfType().All(col => col.IsPresent)); + } + + [Test] + public void TestHidingOverlayClearsTextSearch() + { + AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + SelectedMods = { BindTarget = SelectedMods } + }); + waitForColumnLoad(); + changeRuleset(0); + + AddAssert("all columns visible", () => this.ChildrenOfType().All(col => col.IsPresent)); + + AddStep("set search", () => modSelectOverlay.SearchTerm = "fail"); + AddAssert("one column visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 2); + + AddStep("hide", () => modSelectOverlay.Hide()); + AddStep("show", () => modSelectOverlay.Show()); + + AddAssert("all columns visible", () => this.ChildrenOfType().All(col => col.IsPresent)); + } + [Test] public void TestColumnHidingOnRulesetChange() { @@ -635,7 +787,7 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.MoveMouseTo(this.ChildrenOfType().Single(preset => preset.Preset.Value.Name == "Half Time 0.5x")); InputManager.Click(MouseButton.Left); }); - AddAssert("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(0.5)); + AddAssert("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(0.5)); // this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation, // it is instrumental in the reproduction of the failure scenario that this test is supposed to cover. @@ -643,12 +795,15 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick()); AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType().Single() - .ChildrenOfType>().Single().TriggerClick()); - AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(0.7)); + .ChildrenOfType>().Single().TriggerClick()); + AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(0.7)); } - private void waitForColumnLoad() => AddUntilStep("all column content loaded", - () => modSelectOverlay.ChildrenOfType().Any() && modSelectOverlay.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded)); + private void waitForColumnLoad() => AddUntilStep("all column content loaded", () => + modSelectOverlay.ChildrenOfType().Any() + && modSelectOverlay.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded) + && modSelectOverlay.ChildrenOfType().Any() + && modSelectOverlay.ChildrenOfType().All(column => column.IsLoaded)); private void changeRuleset(int id) { @@ -688,12 +843,10 @@ namespace osu.Game.Tests.Visual.UserInterface { public override string ShortName => "unimplemented"; - public override IEnumerable GetModsFor(ModType type) - { - if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() }); - - return base.GetModsFor(type); - } + public override IEnumerable GetModsFor(ModType type) => + type == ModType.Conversion + ? base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() }) + : base.GetModsFor(type); } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchSmall.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchSmall.cs index 07312379b3..83dc96d0d7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchSmall.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchSmall.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs index 34dd139428..b7cf3fef9b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs @@ -1,19 +1,20 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.UI; @@ -36,6 +37,49 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestMania() => createSwitchTestFor(new ManiaRuleset()); + [Test] + public void TestShowRateAdjusts() + { + AddStep("create mod icons", () => + { + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Full, + ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods() + .OfType() + .SelectMany(m => + { + List icons = new List { new TestModSwitchTiny(m) }; + + for (double i = m.SpeedChange.MinValue; i < m.SpeedChange.MaxValue; i += m.SpeedChange.Precision * 10) + { + m = (ModRateAdjust)m.DeepClone(); + m.SpeedChange.Value = i; + icons.Add(new TestModSwitchTiny(m, true)); + } + + return icons; + }), + }; + }); + + AddStep("adjust rates", () => + { + foreach (var icon in this.ChildrenOfType()) + { + if (icon.Mod is ModRateAdjust rateAdjust) + { + rateAdjust.SpeedChange.Value = RNG.NextDouble() > 0.9 + ? rateAdjust.SpeedChange.Default + : RNG.NextDouble(rateAdjust.SpeedChange.MinValue, rateAdjust.SpeedChange.MaxValue); + } + } + }); + + AddToggleStep("toggle active", active => this.ChildrenOfType().ForEach(s => s.Active.Value = active)); + } + private void createSwitchTestFor(Ruleset ruleset) { AddStep("no colour scheme", () => Child = createContent(ruleset, null)); @@ -45,7 +89,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep($"{scheme} colour scheme", () => Child = createContent(ruleset, scheme)); } - AddToggleStep("toggle active", active => this.ChildrenOfType().ForEach(s => s.Active.Value = active)); + AddToggleStep("toggle active", active => this.ChildrenOfType().ForEach(s => s.Active.Value = active)); } private static Drawable createContent(Ruleset ruleset, OverlayColourScheme? colourScheme) @@ -64,7 +108,7 @@ namespace osu.Game.Tests.Visual.UserInterface AutoSizeAxes = Axes.Y, Direction = FillDirection.Full, Spacing = new Vector2(5), - ChildrenEnumerable = group.Select(mod => new ModSwitchTiny(mod)) + ChildrenEnumerable = group.Select(mod => new TestModSwitchTiny(mod)) }) }; @@ -83,5 +127,15 @@ namespace osu.Game.Tests.Visual.UserInterface return switchFlow; } + + private partial class TestModSwitchTiny : ModSwitchTiny + { + public new IMod Mod => base.Mod; + + public TestModSwitchTiny(IMod mod, bool showExtendedInformation = false) + : base(mod, showExtendedInformation) + { + } + } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModsEffectDisplay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModsEffectDisplay.cs deleted file mode 100644 index a1c8bef1de..0000000000 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModsEffectDisplay.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Linq; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; -using osu.Framework.Testing; -using osu.Game.Graphics; -using osu.Game.Overlays; -using osu.Game.Overlays.Mods; -using osu.Game.Rulesets.Mods; -using osuTK.Graphics; - -namespace osu.Game.Tests.Visual.UserInterface -{ - [TestFixture] - public partial class TestSceneModsEffectDisplay : OsuTestScene - { - [Cached] - private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); - - [Resolved] - private OsuColour colours { get; set; } = null!; - - [Test] - public void TestModsEffectDisplay() - { - TestDisplay testDisplay = null!; - Box background = null!; - - AddStep("add display", () => - { - Add(testDisplay = new TestDisplay - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - var boxes = testDisplay.ChildrenOfType(); - background = boxes.First(); - }); - - AddStep("set value to default", () => testDisplay.Current.Value = 50); - AddUntilStep("colours are correct", () => testDisplay.Container.Colour == Color4.White && background.Colour == colourProvider.Background3); - - AddStep("set value to less", () => testDisplay.Current.Value = 40); - AddUntilStep("colours are correct", () => testDisplay.Container.Colour == colourProvider.Background5 && background.Colour == colours.ForModType(ModType.DifficultyReduction)); - - AddStep("set value to bigger", () => testDisplay.Current.Value = 60); - AddUntilStep("colours are correct", () => testDisplay.Container.Colour == colourProvider.Background5 && background.Colour == colours.ForModType(ModType.DifficultyIncrease)); - } - - private partial class TestDisplay : ModsEffectDisplay - { - public Container Container => Content; - - protected override LocalisableString Label => "Test display"; - - public TestDisplay() - { - Current.Default = 50; - } - } - } -} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index 3cd5daf7a1..c584c7dba0 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -5,11 +5,13 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Database; using osu.Game.Graphics.Sprites; using osu.Game.Online.Multiplayer; using osu.Game.Overlays; @@ -31,6 +33,8 @@ namespace osu.Game.Tests.Visual.UserInterface public double TimeToCompleteProgress { get; set; } = 2000; + private readonly UserLookupCache userLookupCache = new TestUserLookupCache(); + [SetUp] public void SetUp() => Schedule(() => { @@ -52,6 +56,33 @@ namespace osu.Game.Tests.Visual.UserInterface notificationOverlay.UnreadCount.ValueChanged += count => { displayedCount.Text = $"unread count: {count.NewValue}"; }; }); + [Test] + public void TestBasicFlow() + { + setState(Visibility.Visible); + AddStep(@"simple #1", sendHelloNotification); + AddStep(@"simple #2", sendAmazingNotification); + AddStep(@"progress #1", sendUploadProgress); + AddStep(@"progress #2", sendDownloadProgress); + AddStep(@"User notification", sendUserNotification); + + checkProgressingCount(2); + + setState(Visibility.Hidden); + + AddRepeatStep(@"add many simple", sendManyNotifications, 3); + + waitForCompletion(); + + AddStep(@"progress #3", sendUploadProgress); + + checkProgressingCount(1); + + checkDisplayedCount(33); + + waitForCompletion(); + } + [Test] public void TestForwardWithFlingRight() { @@ -411,32 +442,6 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("wait for update applied", () => applyUpdate); } - [Test] - public void TestBasicFlow() - { - setState(Visibility.Visible); - AddStep(@"simple #1", sendHelloNotification); - AddStep(@"simple #2", sendAmazingNotification); - AddStep(@"progress #1", sendUploadProgress); - AddStep(@"progress #2", sendDownloadProgress); - - checkProgressingCount(2); - - setState(Visibility.Hidden); - - AddRepeatStep(@"add many simple", sendManyNotifications, 3); - - waitForCompletion(); - - AddStep(@"progress #3", sendUploadProgress); - - checkProgressingCount(1); - - checkDisplayedCount(33); - - waitForCompletion(); - } - [Test] public void TestImportantWhileClosed() { @@ -537,6 +542,16 @@ namespace osu.Game.Tests.Visual.UserInterface progressingNotifications.Add(n); } + private void sendUserNotification() + { + var user = userLookupCache.GetUserAsync(0).GetResultSafely(); + if (user == null) return; + + var n = new UserAvatarNotification(user, $"{user.Username} invited you to a multiplayer match!"); + + notificationOverlay.Post(n); + } + private void sendUploadProgress() { var n = new ProgressNotification diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs index f2123061e5..4bd3a883f1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Configuration; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs index 770b9dece1..b0548d7e9f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs index 24a27f71e8..62a493815b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Screens.Menu; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuPopover.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuPopover.cs index 4ff7befe71..98ae50c915 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuPopover.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuPopover.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs index 929537e675..69fe8ad105 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs index d9c2774611..bb94912c83 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs @@ -101,6 +101,10 @@ namespace osu.Game.Tests.Visual.UserInterface }, }; } + + protected override void PopIn() + { + } } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs index e90041774e..a927b0931b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.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 osu.Framework.Graphics.Containers; using osu.Game.Overlays; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeaderBackground.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeaderBackground.cs index 7a445427f5..5a43c5ae69 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeaderBackground.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeaderBackground.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 osu.Framework.Graphics.Containers; using osu.Game.Overlays; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayRulesetSelector.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayRulesetSelector.cs index 432e448038..d83e922edf 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayRulesetSelector.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayRulesetSelector.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 osu.Framework.Graphics; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePageSelector.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePageSelector.cs index b9e3592389..3f3b6d8267 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePageSelector.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePageSelector.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; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneParallaxContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneParallaxContainer.cs index 92d4981d4a..38f65b3a17 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneParallaxContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneParallaxContainer.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 osu.Framework.Graphics; using osu.Framework.Screens; using osu.Game.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRankingsSortTabControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRankingsSortTabControl.cs index f364a48616..59600e639f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneRankingsSortTabControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRankingsSortTabControl.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Overlays; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs index cbf67c49a6..8c2651f71d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Bindables; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs new file mode 100644 index 0000000000..66d54c8562 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs @@ -0,0 +1,74 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneRoundedSliderBar : OsuManualInputManagerTestScene + { + [Cached] + private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple); + + private RoundedSliderBar slider = null!; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create slider", () => Child = slider = new RoundedSliderBar + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = new BindableDouble(5) + { + Precision = 0.1, + MinValue = 0, + MaxValue = 15 + }, + RelativeSizeAxes = Axes.X, + Width = 0.4f + }); + } + + [Test] + public void TestNubDoubleClickRevertToDefault() + { + AddStep("set slider to 1", () => slider.Current.Value = 1); + + AddStep("move mouse to nub", () => InputManager.MoveMouseTo(slider.ChildrenOfType().Single())); + + AddStep("double click nub", () => + { + InputManager.Click(MouseButton.Left); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("slider is default", () => slider.Current.IsDefault); + } + + [Test] + public void TestNubDoubleClickOnDisabledSliderDoesNothing() + { + AddStep("set slider to 1", () => slider.Current.Value = 1); + AddStep("disable slider", () => slider.Current.Disabled = true); + + AddStep("move mouse to nub", () => InputManager.MoveMouseTo(slider.ChildrenOfType().Single())); + + AddStep("double click nub", () => + { + InputManager.Click(MouseButton.Left); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("slider is still at 1", () => slider.Current.Value, () => Is.EqualTo(1)); + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDifficultyMultiplierDisplay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScoreMultiplierDisplay.cs similarity index 79% rename from osu.Game.Tests/Visual/UserInterface/TestSceneDifficultyMultiplierDisplay.cs rename to osu.Game.Tests/Visual/UserInterface/TestSceneScoreMultiplierDisplay.cs index 890c7295b4..c2ddd814b7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDifficultyMultiplierDisplay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScoreMultiplierDisplay.cs @@ -1,10 +1,9 @@ // 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 NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -12,17 +11,17 @@ using osu.Game.Overlays.Mods; namespace osu.Game.Tests.Visual.UserInterface { [TestFixture] - public partial class TestSceneDifficultyMultiplierDisplay : OsuTestScene + public partial class TestSceneScoreMultiplierDisplay : OsuTestScene { [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); [Test] - public void TestDifficultyMultiplierDisplay() + public void TestBasic() { - DifficultyMultiplierDisplay multiplierDisplay = null; + ScoreMultiplierDisplay multiplierDisplay = null!; - AddStep("create content", () => Child = multiplierDisplay = new DifficultyMultiplierDisplay + AddStep("create content", () => Child = multiplierDisplay = new ScoreMultiplierDisplay { Anchor = Anchor.Centre, Origin = Anchor.Centre @@ -34,7 +33,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddSliderStep("set multiplier", 0, 2, 1d, multiplier => { - if (multiplierDisplay != null) + if (multiplierDisplay.IsNotNull()) multiplierDisplay.Current.Value = multiplier; }); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenBreadcrumbControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenBreadcrumbControl.cs index 3d35f2c9cc..968cf9f9db 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenBreadcrumbControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenBreadcrumbControl.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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs index a0fe5fce32..1101f27139 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedOverlayHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedOverlayHeader.cs index aeea0681eb..27b128e709 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedOverlayHeader.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedOverlayHeader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSearchTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSearchTextBox.cs index 0072864335..f3a7f1481a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSearchTextBox.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSearchTextBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs index 766f22d867..c3038ddb3d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs @@ -1,37 +1,74 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; +using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { - public partial class TestSceneShearedSliderBar : OsuTestScene + public partial class TestSceneShearedSliderBar : OsuManualInputManagerTestScene { [Cached] private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple); - private readonly BindableDouble current = new BindableDouble(5) - { - Precision = 0.1f, - MinValue = 0, - MaxValue = 15 - }; + private ShearedSliderBar slider = null!; - [BackgroundDependencyLoader] - private void load() + [SetUpSteps] + public void SetUpSteps() { - Child = new ShearedSliderBar + AddStep("create slider", () => Child = slider = new ShearedSliderBar { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Current = current, + Current = new BindableDouble(5) + { + Precision = 0.1, + MinValue = 0, + MaxValue = 15 + }, RelativeSizeAxes = Axes.X, Width = 0.4f - }; + }); + } + + [Test] + public void TestNubDoubleClickRevertToDefault() + { + AddStep("set slider to 1", () => slider.Current.Value = 1); + + AddStep("move mouse to nub", () => InputManager.MoveMouseTo(slider.ChildrenOfType().Single())); + + AddStep("double click nub", () => + { + InputManager.Click(MouseButton.Left); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("slider is default", () => slider.Current.IsDefault); + } + + [Test] + public void TestNubDoubleClickOnDisabledSliderDoesNothing() + { + AddStep("set slider to 1", () => slider.Current.Value = 1); + AddStep("disable slider", () => slider.Current.Disabled = true); + + AddStep("move mouse to nub", () => InputManager.MoveMouseTo(slider.ChildrenOfType().Single())); + + AddStep("double click nub", () => + { + InputManager.Click(MouseButton.Left); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("slider is still at 1", () => slider.Current.Value, () => Is.EqualTo(1)); } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSliderWithTextBoxInput.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSliderWithTextBoxInput.cs new file mode 100644 index 0000000000..d23fcebae3 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSliderWithTextBoxInput.cs @@ -0,0 +1,130 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneSliderWithTextBoxInput : OsuManualInputManagerTestScene + { + private SliderWithTextBoxInput sliderWithTextBoxInput = null!; + + private OsuSliderBar slider => sliderWithTextBoxInput.ChildrenOfType>().Single(); + private Nub nub => sliderWithTextBoxInput.ChildrenOfType().Single(); + private OsuTextBox textBox => sliderWithTextBoxInput.ChildrenOfType().Single(); + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create slider", () => Child = sliderWithTextBoxInput = new SliderWithTextBoxInput("Test Slider") + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + Current = new BindableFloat + { + MinValue = -5, + MaxValue = 5, + Precision = 0.2f + } + }); + } + + [Test] + public void TestNonInstantaneousMode() + { + AddStep("set instantaneous to false", () => sliderWithTextBoxInput.Instantaneous = false); + + AddStep("focus textbox", () => InputManager.ChangeFocus(textBox)); + AddStep("change text", () => textBox.Text = "3"); + AddAssert("slider not moved", () => slider.Current.Value, () => Is.Zero); + AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.Zero); + + AddStep("commit text", () => InputManager.Key(Key.Enter)); + AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3)); + AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3)); + + AddStep("move mouse to nub", () => InputManager.MoveMouseTo(nub)); + AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left)); + AddStep("move mouse to minimum", () => InputManager.MoveMouseTo(sliderWithTextBoxInput.ScreenSpaceDrawQuad.BottomLeft)); + AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("3")); + AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3)); + + AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left)); + AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5")); + AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); + + AddStep("focus textbox", () => InputManager.ChangeFocus(textBox)); + AddStep("set text to invalid", () => textBox.Text = "garbage"); + AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); + AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); + + AddStep("commit text", () => InputManager.Key(Key.Enter)); + AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5")); + AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); + AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); + + AddStep("focus textbox", () => InputManager.ChangeFocus(textBox)); + AddStep("set text to invalid", () => textBox.Text = "garbage"); + AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); + AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); + + AddStep("lose focus", () => InputManager.ChangeFocus(null)); + AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5")); + AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); + AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); + } + + [Test] + public void TestInstantaneousMode() + { + AddStep("set instantaneous to true", () => sliderWithTextBoxInput.Instantaneous = true); + + AddStep("focus textbox", () => InputManager.ChangeFocus(textBox)); + AddStep("change text", () => textBox.Text = "3"); + AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3)); + AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3)); + + AddStep("commit text", () => InputManager.Key(Key.Enter)); + AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(3)); + AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3)); + + AddStep("move mouse to nub", () => InputManager.MoveMouseTo(nub)); + AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left)); + AddStep("move mouse to minimum", () => InputManager.MoveMouseTo(sliderWithTextBoxInput.ScreenSpaceDrawQuad.BottomLeft)); + AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5")); + AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); + + AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left)); + AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("-5")); + AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); + + AddStep("focus textbox", () => InputManager.ChangeFocus(textBox)); + AddStep("set text to invalid", () => textBox.Text = "garbage"); + AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); + AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); + + AddStep("commit text", () => InputManager.Key(Key.Enter)); + AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5")); + AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); + AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); + + AddStep("focus textbox", () => InputManager.ChangeFocus(textBox)); + AddStep("set text to invalid", () => textBox.Text = "garbage"); + AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); + AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); + + AddStep("lose focus", () => InputManager.ChangeFocus(null)); + AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5")); + AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); + AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneTabControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneTabControl.cs index 24c4ed79b1..94117ff7e3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneTabControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneTabControl.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.ComponentModel; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneToggleMenuItem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneToggleMenuItem.cs index 41a6f35624..f564f561ec 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneToggleMenuItem.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneToggleMenuItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneTwoLayerButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneTwoLayerButton.cs index 20b0ab5801..524119f34d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneTwoLayerButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneTwoLayerButton.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 osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapSetCover.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapSetCover.cs index a1a546d4a7..54532001a9 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapSetCover.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapSetCover.cs @@ -99,16 +99,18 @@ namespace osu.Game.Tests.Visual.UserInterface { TestUpdateableOnlineBeatmapSetCover updateableCover = null; - AddStep("setup cover", () => Child = updateableCover = new TestUpdateableOnlineBeatmapSetCover + AddStep("setup cover", () => Child = updateableCover = new TestUpdateableOnlineBeatmapSetCover(400) { OnlineInfo = CreateAPIBeatmapSet(), RelativeSizeAxes = Axes.Both, Masking = true, }); - AddStep("change model", () => updateableCover.OnlineInfo = null); - AddWaitStep("wait some", 5); - AddAssert("no cover added", () => !updateableCover.ChildrenOfType().Any()); + AddStep("change model to null", () => updateableCover.OnlineInfo = null); + + AddUntilStep("wait for load", () => updateableCover.DelayedLoadFinished); + + AddAssert("no cover added", () => !updateableCover.ChildrenOfType().Any()); } [Test] @@ -143,11 +145,19 @@ namespace osu.Game.Tests.Visual.UserInterface { private readonly int loadDelay; + public bool DelayedLoadFinished; + public TestUpdateableOnlineBeatmapSetCover(int loadDelay = 10000) { this.loadDelay = loadDelay; } + protected override void OnLoadFinished() + { + base.OnLoadFinished(); + DelayedLoadFinished = true; + } + protected override Drawable CreateDrawable(IBeatmapSetOnlineInfo model) { if (model == null) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.cs index 8737f7312e..a373fbbc51 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneVolumePieces.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneVolumePieces.cs index 7cedef96e3..311bae0d50 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneVolumePieces.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneVolumePieces.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 osu.Framework.Graphics; using osu.Game.Overlays.Volume; using osuTK; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneWaveContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneWaveContainer.cs index 7851571b36..e1f8357a36 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneWaveContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneWaveContainer.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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; diff --git a/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs index 05ffd1fbef..2c894eacab 100644 --- a/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs +++ b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 59a786a11d..ef6c16f2c4 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -2,10 +2,10 @@ - + - + diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDateTextBox.cs b/osu.Game.Tournament.Tests/Components/TestSceneDateTextBox.cs index f547acd635..d018a7ed5c 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneDateTextBox.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneDateTextBox.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 NUnit.Framework; using osu.Game.Tests.Visual; using osu.Game.Tournament.Components; @@ -13,7 +11,7 @@ namespace osu.Game.Tournament.Tests.Components { public partial class TestSceneDateTextBox : OsuManualInputManagerTestScene { - private DateTextBox textBox; + private DateTextBox textBox = null!; [SetUp] public void Setup() => Schedule(() => diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentMatch.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentMatch.cs index cb923a1f9a..18c42b3086 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentMatch.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentMatch.cs @@ -1,10 +1,11 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.Ladder.Components; @@ -12,60 +13,67 @@ namespace osu.Game.Tournament.Tests.Components { public partial class TestSceneDrawableTournamentMatch : TournamentTestScene { - public TestSceneDrawableTournamentMatch() + [Test] + public void TestBasic() { - Container level1; - Container level2; + Container level1 = null!; + Container level2 = null!; - var match1 = new TournamentMatch( - new TournamentTeam { FlagName = { Value = "AU" }, FullName = { Value = "Australia" }, }, - new TournamentTeam { FlagName = { Value = "JP" }, FullName = { Value = "Japan" }, Acronym = { Value = "JPN" } }) + TournamentMatch match1 = null!; + TournamentMatch match2 = null!; + + AddStep("setup test", () => { - Team1Score = { Value = 4 }, - Team2Score = { Value = 1 }, - }; - - var match2 = new TournamentMatch( - new TournamentTeam + match1 = new TournamentMatch( + new TournamentTeam { FlagName = { Value = "AU" }, FullName = { Value = "Australia" }, }, + new TournamentTeam { FlagName = { Value = "JP" }, FullName = { Value = "Japan" }, Acronym = { Value = "JPN" } }) { - FlagName = { Value = "RO" }, - FullName = { Value = "Romania" }, - } - ); + Team1Score = { Value = 4 }, + Team2Score = { Value = 1 }, + }; - Child = new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = new Drawable[] + match2 = new TournamentMatch( + new TournamentTeam + { + FlagName = { Value = "RO" }, + FullName = { Value = "Romania" }, + } + ); + + Child = new FillFlowContainer { - level1 = new FillFlowContainer + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] { - AutoSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - Children = new[] + level1 = new FillFlowContainer { - new DrawableTournamentMatch(match1), - new DrawableTournamentMatch(match2), - new DrawableTournamentMatch(new TournamentMatch()), - } - }, - level2 = new FillFlowContainer - { - AutoSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - Margin = new MarginPadding(20), - Children = new[] + AutoSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Children = new[] + { + new DrawableTournamentMatch(match1), + new DrawableTournamentMatch(match2), + new DrawableTournamentMatch(new TournamentMatch()), + } + }, + level2 = new FillFlowContainer { - new DrawableTournamentMatch(new TournamentMatch()), - new DrawableTournamentMatch(new TournamentMatch()) + AutoSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Margin = new MarginPadding(20), + Children = new[] + { + new DrawableTournamentMatch(new TournamentMatch()), + new DrawableTournamentMatch(new TournamentMatch()) + } } } - } - }; + }; - level1.Children[0].Match.Progression.Value = level2.Children[0].Match; - level1.Children[1].Match.Progression.Value = level2.Children[0].Match; + level1.Children[0].Match.Progression.Value = level2.Children[0].Match; + level1.Children[1].Match.Progression.Value = level2.Children[0].Match; + }); AddRepeatStep("change scores", () => match1.Team2Score.Value++, 4); AddStep("add new team", () => match2.Team2.Value = new TournamentTeam { FlagName = { Value = "PT" }, FullName = { Value = "Portugal" } }); @@ -80,6 +88,9 @@ namespace osu.Game.Tournament.Tests.Components AddRepeatStep("change scores", () => level2.Children[0].Match.Team1Score.Value++, 5); AddRepeatStep("change scores", () => level2.Children[0].Match.Team2Score.Value++, 4); + + AddStep("select as current", () => match1.Current.Value = true); + AddStep("select as editing", () => this.ChildrenOfType().Last().Selected = true); } } } diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs index dd7c613c6c..43adcc61bf 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs @@ -1,8 +1,7 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Tests.Visual; @@ -16,13 +15,19 @@ namespace osu.Game.Tournament.Tests.Components { public partial class TestSceneDrawableTournamentTeam : OsuGridTestScene { + [Cached] + protected LadderInfo Ladder { get; private set; } = new LadderInfo(); + public TestSceneDrawableTournamentTeam() : base(4, 3) { + AddToggleStep("toggle seed view", v => Ladder.DisplayTeamSeeds.Value = v); + var team = new TournamentTeam { FlagName = { Value = "AU" }, FullName = { Value = "Australia" }, + Seed = { Value = "#5" }, Players = { new TournamentUser { Username = "ASecretBox" }, @@ -32,7 +37,7 @@ namespace osu.Game.Tournament.Tests.Components new TournamentUser { Username = "Parkes" }, new TournamentUser { Username = "Shiroha" }, new TournamentUser { Username = "Jordan The Bear" }, - } + }, }; var match = new TournamentMatch { Team1 = { Value = team } }; diff --git a/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs b/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs index 2347c84ba8..ae4c5ec685 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; diff --git a/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs index 9b1fc17591..3007232077 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Utils; diff --git a/osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs index cb22e7e7c7..431b6eff63 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; diff --git a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs index f793c33878..95d6b6d107 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs @@ -1,28 +1,36 @@ // 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 NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Beatmaps.Legacy; -using osu.Game.Tests.Visual; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; namespace osu.Game.Tournament.Tests.Components { [TestFixture] - public partial class TestSceneSongBar : OsuTestScene + public partial class TestSceneSongBar : TournamentTestScene { - [Cached] - private readonly LadderInfo ladder = new LadderInfo(); + private SongBar songBar = null!; + private TournamentBeatmap ladderBeatmap = null!; - [Test] - public void TestSongBar() + [SetUpSteps] + public override void SetUpSteps() { - SongBar songBar = null; + base.SetUpSteps(); + + AddStep("setup picks bans", () => + { + ladderBeatmap = CreateSampleBeatmap(); + Ladder.CurrentMatch.Value!.PicksBans.Add(new BeatmapChoice + { + BeatmapID = ladderBeatmap.OnlineID, + Team = TeamColour.Red, + Type = ChoiceType.Pick, + }); + }); AddStep("create bar", () => Child = songBar = new SongBar { @@ -31,22 +39,33 @@ namespace osu.Game.Tournament.Tests.Components Origin = Anchor.Centre }); AddUntilStep("wait for loaded", () => songBar.IsLoaded); + } + [Test] + public void TestSongBar() + { AddStep("set beatmap", () => { var beatmap = CreateAPIBeatmap(Ruleset.Value); + beatmap.CircleSize = 3.4f; beatmap.ApproachRate = 6.8f; beatmap.OverallDifficulty = 5.5f; beatmap.StarRating = 4.56f; beatmap.Length = 123456; beatmap.BPM = 133; + beatmap.OnlineID = ladderBeatmap.OnlineID; songBar.Beatmap = new TournamentBeatmap(beatmap); }); + AddStep("set mods to HR", () => songBar.Mods = LegacyMods.HardRock); AddStep("set mods to DT", () => songBar.Mods = LegacyMods.DoubleTime); AddStep("unset mods", () => songBar.Mods = LegacyMods.None); + + AddToggleStep("toggle expanded", expanded => songBar.Expanded = expanded); + + AddStep("set null beatmap", () => songBar.Beatmap = null); } } } diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs index 057566d426..4fa90437c8 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Online.API; @@ -21,7 +19,7 @@ namespace osu.Game.Tournament.Tests.Components /// It cannot be trivially replaced because setting to causes to no longer be usable. /// [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs index d9ae8df651..de91a66e56 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs @@ -1,13 +1,14 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; -using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; +using osu.Game.Overlays.Chat; using osu.Game.Tests.Visual; using osu.Game.Tournament.Components; using osu.Game.Tournament.IPC; @@ -39,6 +40,12 @@ namespace osu.Game.Tournament.Tests.Components OnlineID = 4, }; + private readonly TournamentUser blueUserWithCustomColour = new TournamentUser + { + Username = "nekodex", + OnlineID = 5, + }; + [Cached] private LadderInfo ladderInfo = new LadderInfo(); @@ -55,18 +62,6 @@ namespace osu.Game.Tournament.Tests.Components Origin = Anchor.Centre, }); - ladderInfo.CurrentMatch.Value = new TournamentMatch - { - Team1 = - { - Value = new TournamentTeam { Players = new BindableList { redUser } } - }, - Team2 = - { - Value = new TournamentTeam { Players = new BindableList { blueUser } } - } - }; - chatDisplay.Channel.Value = testChannel; } @@ -80,12 +75,27 @@ namespace osu.Game.Tournament.Tests.Components Content = "I am a wang!" })); + AddStep("set current match", () => ladderInfo.CurrentMatch.Value = new TournamentMatch + { + Team1 = + { + Value = new TournamentTeam { Players = { redUser } } + }, + Team2 = + { + Value = new TournamentTeam { Players = { blueUser, blueUserWithCustomColour } } + } + }); + AddStep("message from team red", () => testChannel.AddNewMessages(new Message(nextMessageId()) { Sender = redUser.ToAPIUser(), Content = "I am team red." })); + AddUntilStep("message from team red is red color", () => + this.ChildrenOfType().Last().AccentColour, () => Is.EqualTo(TournamentGame.COLOUR_RED)); + AddStep("message from team red", () => testChannel.AddNewMessages(new Message(nextMessageId()) { Sender = redUser.ToAPIUser(), @@ -98,6 +108,24 @@ namespace osu.Game.Tournament.Tests.Components Content = "Not on my watch. Prepare to eat saaaaaaaaaand. Lots and lots of saaaaaaand." })); + AddUntilStep("message from team blue is blue color", () => + this.ChildrenOfType().Last().AccentColour, () => Is.EqualTo(TournamentGame.COLOUR_BLUE)); + + var userWithCustomColour = blueUserWithCustomColour.ToAPIUser(); + userWithCustomColour.Colour = "#e45678"; + + AddStep("message from team blue with custom colour", () => testChannel.AddNewMessages(new Message(nextMessageId()) + { + Sender = userWithCustomColour, + Content = "Not on my watch. Prepare to eat saaaaaaaaaand. Lots and lots of saaaaaaand." + })); + + AddUntilStep("message from team blue is blue color", () => + this.ChildrenOfType().Last().AccentColour, () => Is.EqualTo(TournamentGame.COLOUR_BLUE)); + + AddUntilStep("message from user with custom colour is inverted", () => + this.ChildrenOfType().Last().Inverted, () => Is.EqualTo(true)); + AddStep("message from admin", () => testChannel.AddNewMessages(new Message(nextMessageId()) { Sender = admin, diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs index cea4306ff8..0b69da78a4 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -19,12 +17,12 @@ namespace osu.Game.Tournament.Tests.Components public partial class TestSceneTournamentModDisplay : TournamentTestScene { [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; [Resolved] - private IRulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } = null!; - private FillFlowContainer fillFlow; + private FillFlowContainer fillFlow = null!; [BackgroundDependencyLoader] private void load() @@ -45,7 +43,7 @@ namespace osu.Game.Tournament.Tests.Components private void success(APIBeatmap beatmap) { - var ruleset = rulesets.GetRuleset(Ladder.Ruleset.Value.OnlineID); + var ruleset = rulesets.GetRuleset(Ladder.Ruleset.Value?.OnlineID ?? -1); if (ruleset == null) return; diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index 45dffdc94a..3ae1400a99 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.IO; using System.Linq; using NUnit.Framework; diff --git a/osu.Game.Tournament.Tests/NonVisual/DataLoadTest.cs b/osu.Game.Tournament.Tests/NonVisual/DataLoadTest.cs index 256a984a7c..e8c9f65608 100644 --- a/osu.Game.Tournament.Tests/NonVisual/DataLoadTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/DataLoadTest.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.IO; using System.Threading.Tasks; @@ -81,11 +79,11 @@ namespace osu.Game.Tournament.Tests.NonVisual public partial class TestTournament : TournamentGameBase { private readonly bool resetRuleset; - private readonly Action runOnLoadComplete; + private readonly Action? runOnLoadComplete; public new Task BracketLoadTask => base.BracketLoadTask; - public TestTournament(bool resetRuleset = false, [InstantHandle] Action runOnLoadComplete = null) + public TestTournament(bool resetRuleset = false, [InstantHandle] Action? runOnLoadComplete = null) { this.resetRuleset = resetRuleset; this.runOnLoadComplete = runOnLoadComplete; diff --git a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs index ca6354cb48..6ee7808099 100644 --- a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.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.IO; using System.Linq; using NUnit.Framework; @@ -36,11 +34,11 @@ namespace osu.Game.Tournament.Tests.NonVisual { var osu = LoadTournament(host); TournamentStorage storage = (TournamentStorage)osu.Dependencies.Get(); - FileBasedIPC ipc = null; + FileBasedIPC? ipc = null; WaitForOrAssert(() => (ipc = osu.Dependencies.Get() as FileBasedIPC)?.IsLoaded == true, @"ipc could not be populated in a reasonable amount of time"); - Assert.True(ipc.SetIPCLocation(testStableInstallDirectory)); + Assert.True(ipc!.SetIPCLocation(testStableInstallDirectory)); Assert.True(storage.AllTournaments.Exists("stable.json")); } finally diff --git a/osu.Game.Tournament.Tests/NonVisual/LadderInfoSerialisationTest.cs b/osu.Game.Tournament.Tests/NonVisual/LadderInfoSerialisationTest.cs index f1e0966293..acb4a87ffc 100644 --- a/osu.Game.Tournament.Tests/NonVisual/LadderInfoSerialisationTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/LadderInfoSerialisationTest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Newtonsoft.Json; using NUnit.Framework; using osu.Game.Tournament.Models; @@ -37,8 +35,8 @@ namespace osu.Game.Tournament.Tests.NonVisual PlayersPerTeam = { Value = 4 }, Teams = { - match.Team1.Value, - match.Team2.Value, + match.Team1.Value!, + match.Team2.Value!, }, Rounds = { diff --git a/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs b/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs index 8dc0946432..e4a35913cc 100644 --- a/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.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.Threading; using System.Threading.Tasks; @@ -13,7 +11,7 @@ namespace osu.Game.Tournament.Tests.NonVisual { public abstract class TournamentHostTest { - public static TournamentGameBase LoadTournament(GameHost host, TournamentGameBase tournament = null) + public static TournamentGameBase LoadTournament(GameHost host, TournamentGameBase? tournament = null) { tournament ??= new TournamentGameBase(); Task.Factory.StartNew(() => host.Run(tournament), TaskCreationOptions.LongRunning) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneDrawingsScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneDrawingsScreen.cs index 10ed850002..df11859b71 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneDrawingsScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneDrawingsScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.IO; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -12,7 +10,7 @@ using osu.Game.Tournament.Screens.Drawings; namespace osu.Game.Tournament.Tests.Screens { - public partial class TestSceneDrawingsScreen : TournamentTestScene + public partial class TestSceneDrawingsScreen : TournamentScreenTestScene { [BackgroundDependencyLoader] private void load(Storage storage) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs index f127a930a6..31583bf8b7 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.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; @@ -15,11 +13,25 @@ using osu.Game.Tournament.Screens.Gameplay.Components; namespace osu.Game.Tournament.Tests.Screens { - public partial class TestSceneGameplayScreen : TournamentTestScene + public partial class TestSceneGameplayScreen : TournamentScreenTestScene { [Cached] private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay { Width = 0.5f }; + [Test] + public void TestWarmup() + { + createScreen(); + + checkScoreVisibility(false); + + toggleWarmup(); + checkScoreVisibility(true); + + toggleWarmup(); + checkScoreVisibility(false); + } + [Test] public void TestStartupState([Values] TourneyState state) { @@ -35,20 +47,6 @@ namespace osu.Game.Tournament.Tests.Screens createScreen(); } - [Test] - public void TestWarmup() - { - createScreen(); - - checkScoreVisibility(false); - - toggleWarmup(); - checkScoreVisibility(true); - - toggleWarmup(); - checkScoreVisibility(false); - } - private void createScreen() { AddStep("setup screen", () => diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneLadderEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneLadderEditorScreen.cs index 5c4e1b2a5a..b9b150d264 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneLadderEditorScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneLadderEditorScreen.cs @@ -1,25 +1,100 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; +using System; +using System.Linq; +using Newtonsoft.Json; +using NUnit.Framework; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Tournament.Screens.Editors; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Dialog; +using osu.Game.Tournament.Screens.Editors.Components; +using osuTK; +using osuTK.Input; namespace osu.Game.Tournament.Tests.Screens { - public partial class TestSceneLadderEditorScreen : TournamentTestScene + public partial class TestSceneLadderEditorScreen : TournamentScreenTestScene { - [BackgroundDependencyLoader] - private void load() + private LadderEditorScreen ladderEditorScreen = null!; + private OsuContextMenuContainer? osuContextMenuContainer; + + [SetUp] + public void Setup() => Schedule(() => { - Add(new OsuContextMenuContainer + ladderEditorScreen = new LadderEditorScreen(); + Add(osuContextMenuContainer = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, - Child = new LadderEditorScreen() + Child = ladderEditorScreen = new LadderEditorScreen() }); + }); + + [Test] + public void TestResetBracketTeamsCancelled() + { + Bindable matchBeforeReset = new Bindable(); + AddStep("save current match state", () => + { + matchBeforeReset.Value = JsonConvert.SerializeObject(Ladder.CurrentMatch.Value); + }); + AddStep("pull up context menu", () => + { + InputManager.MoveMouseTo(ladderEditorScreen); + InputManager.Click(MouseButton.Right); + }); + AddStep("click Reset teams button", () => + { + InputManager.MoveMouseTo(osuContextMenuContainer.ChildrenOfType().Last(p => + ((OsuMenuItem)p.Item).Type == MenuItemType.Destructive), new Vector2(5, 0)); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("dialog displayed", () => DialogOverlay.CurrentDialog is LadderResetTeamsDialog); + AddStep("click cancel", () => + { + InputManager.MoveMouseTo(DialogOverlay.CurrentDialog.ChildrenOfType().Last()); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("dialog dismissed", () => DialogOverlay.CurrentDialog is not LadderResetTeamsDialog); + + AddAssert("assert ladder teams unchanged", () => string.Equals(matchBeforeReset.Value, JsonConvert.SerializeObject(Ladder.CurrentMatch.Value), StringComparison.Ordinal)); + } + + [Test] + public void TestResetBracketTeams() + { + AddStep("pull up context menu", () => + { + InputManager.MoveMouseTo(ladderEditorScreen); + InputManager.Click(MouseButton.Right); + }); + + AddStep("click Reset teams button", () => + { + InputManager.MoveMouseTo(osuContextMenuContainer.ChildrenOfType().Last(p => + ((OsuMenuItem)p.Item).Type == MenuItemType.Destructive), new Vector2(5, 0)); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("dialog displayed", () => DialogOverlay.CurrentDialog is LadderResetTeamsDialog); + + AddStep("click confirmation", () => + { + InputManager.MoveMouseTo(DialogOverlay.CurrentDialog.ChildrenOfType().First()); + InputManager.PressButton(MouseButton.Left); + }); + + AddUntilStep("dialog dismissed", () => DialogOverlay.CurrentDialog is not LadderResetTeamsDialog); + + AddStep("release mouse button", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddAssert("assert ladder teams reset", () => Ladder.CurrentMatch.Value?.Team1.Value == null && Ladder.CurrentMatch.Value?.Team2.Value == null); } } } diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneLadderScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneLadderScreen.cs index 20f729bb8d..6605e055f8 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneLadderScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneLadderScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics.Cursor; @@ -10,7 +8,7 @@ using osu.Game.Tournament.Screens.Ladder; namespace osu.Game.Tournament.Tests.Screens { - public partial class TestSceneLadderScreen : TournamentTestScene + public partial class TestSceneLadderScreen : TournamentScreenTestScene { [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs index 5695cb5574..7b2c1ba336 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.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; @@ -14,9 +12,9 @@ using osu.Game.Tournament.Screens.MapPool; namespace osu.Game.Tournament.Tests.Screens { - public partial class TestSceneMapPoolScreen : TournamentTestScene + public partial class TestSceneMapPoolScreen : TournamentScreenTestScene { - private MapPoolScreen screen; + private MapPoolScreen screen = null!; [BackgroundDependencyLoader] private void load() @@ -24,12 +22,15 @@ namespace osu.Game.Tournament.Tests.Screens Add(screen = new MapPoolScreen { Width = 0.7f }); } + [SetUp] + public void SetUp() => Schedule(() => Ladder.SplitMapPoolByMods.Value = true); + [Test] public void TestFewMaps() { AddStep("load few maps", () => { - Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); + Ladder.CurrentMatch.Value!.Round.Value!.Beatmaps.Clear(); for (int i = 0; i < 8; i++) addBeatmap(); @@ -49,7 +50,7 @@ namespace osu.Game.Tournament.Tests.Screens { AddStep("load just enough maps", () => { - Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); + Ladder.CurrentMatch.Value!.Round.Value!.Beatmaps.Clear(); for (int i = 0; i < 18; i++) addBeatmap(); @@ -69,7 +70,7 @@ namespace osu.Game.Tournament.Tests.Screens { AddStep("load many maps", () => { - Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); + Ladder.CurrentMatch.Value!.Round.Value!.Beatmaps.Clear(); for (int i = 0; i < 19; i++) addBeatmap(); @@ -89,10 +90,10 @@ namespace osu.Game.Tournament.Tests.Screens { AddStep("load many maps", () => { - Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); + Ladder.CurrentMatch.Value!.Round.Value!.Beatmaps.Clear(); for (int i = 0; i < 11; i++) - addBeatmap(i > 4 ? $"M{i}" : "NM"); + addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM"); }); AddStep("reset match", () => @@ -115,10 +116,10 @@ namespace osu.Game.Tournament.Tests.Screens { AddStep("load many maps", () => { - Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); + Ladder.CurrentMatch.Value!.Round.Value!.Beatmaps.Clear(); for (int i = 0; i < 12; i++) - addBeatmap(i > 4 ? $"M{i}" : "NM"); + addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM"); }); AddStep("reset match", () => @@ -130,9 +131,29 @@ namespace osu.Game.Tournament.Tests.Screens assertThreeWide(); } - private void addBeatmap(string mods = "nm") + [Test] + public void TestSplitMapPoolByMods() { - Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Add(new RoundBeatmap + AddStep("load many maps", () => + { + Ladder.CurrentMatch.Value!.Round.Value!.Beatmaps.Clear(); + + for (int i = 0; i < 12; i++) + addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM"); + }); + + AddStep("disable splitting map pool by mods", () => Ladder.SplitMapPoolByMods.Value = false); + + AddStep("reset match", () => + { + Ladder.CurrentMatch.Value = new TournamentMatch(); + Ladder.CurrentMatch.Value = Ladder.Matches.First(); + }); + } + + private void addBeatmap(string mods = "NM") + { + Ladder.CurrentMatch.Value!.Round.Value!.Beatmaps.Add(new RoundBeatmap { Beatmap = CreateSampleBeatmap(), Mods = mods diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneRoundEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneRoundEditorScreen.cs index ebeb69012d..c5e03f7abd 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneRoundEditorScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneRoundEditorScreen.cs @@ -1,15 +1,15 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Game.Tournament.Screens.Editors; namespace osu.Game.Tournament.Tests.Screens { - public partial class TestSceneRoundEditorScreen : TournamentTestScene + public partial class TestSceneRoundEditorScreen : TournamentScreenTestScene { - public TestSceneRoundEditorScreen() + [BackgroundDependencyLoader] + private void load() { Add(new RoundEditorScreen { diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs index fd0de3d63a..a58f09d13a 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.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 NUnit.Framework; using osu.Framework.Allocation; @@ -12,8 +10,15 @@ using osu.Game.Tournament.Screens.Schedule; namespace osu.Game.Tournament.Tests.Screens { - public partial class TestSceneScheduleScreen : TournamentTestScene + public partial class TestSceneScheduleScreen : TournamentScreenTestScene { + public override void SetUpSteps() + { + AddStep("clear matches", () => Ladder.Matches.Clear()); + + base.SetUpSteps(); + } + [BackgroundDependencyLoader] private void load() { @@ -36,6 +41,36 @@ namespace osu.Game.Tournament.Tests.Screens AddStep("Set null current match", () => Ladder.CurrentMatch.Value = null); } + [Test] + public void TestUpcomingMatches() + { + AddStep("Add upcoming match", () => + { + var tournamentMatch = CreateSampleMatch(); + + tournamentMatch.Date.Value = DateTimeOffset.UtcNow.AddMinutes(5); + tournamentMatch.Completed.Value = false; + + Ladder.Matches.Add(tournamentMatch); + }); + } + + [Test] + public void TestRecentMatches() + { + AddStep("Add recent match", () => + { + var tournamentMatch = CreateSampleMatch(); + + tournamentMatch.Date.Value = DateTimeOffset.UtcNow; + tournamentMatch.Completed.Value = true; + tournamentMatch.Team1Score.Value = tournamentMatch.PointsToWin; + tournamentMatch.Team2Score.Value = tournamentMatch.PointsToWin / 2; + + Ladder.Matches.Add(tournamentMatch); + }); + } + private void setMatchDate(TimeSpan relativeTime) // Humanizer cannot handle negative timespans. => AddStep($"start time is {relativeTime}", () => diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs index cfb533149d..a7ef4d7a29 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs @@ -1,24 +1,24 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.Editors; namespace osu.Game.Tournament.Tests.Screens { - public partial class TestSceneSeedingEditorScreen : TournamentTestScene + public partial class TestSceneSeedingEditorScreen : TournamentScreenTestScene { [Cached] private readonly LadderInfo ladder = new LadderInfo(); - public TestSceneSeedingEditorScreen() + [BackgroundDependencyLoader] + private void load() { var match = CreateSampleMatch(); - Add(new SeedingEditorScreen(match.Team1.Value, new TeamEditorScreen()) + Add(new SeedingEditorScreen(match.Team1.Value.AsNonNull(), new TeamEditorScreen()) { Width = 0.85f // create room for control panel }); diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs index c9620bc0b9..a3890bbff0 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; @@ -14,7 +12,7 @@ using osu.Game.Tournament.Screens.TeamIntro; namespace osu.Game.Tournament.Tests.Screens { - public partial class TestSceneSeedingScreen : TournamentTestScene + public partial class TestSceneSeedingScreen : TournamentScreenTestScene { [Cached] private readonly LadderInfo ladder = new LadderInfo diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.cs index 84c8b9a141..71ae17d3eb 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.cs @@ -1,14 +1,12 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Game.Tournament.Screens.Setup; namespace osu.Game.Tournament.Tests.Screens { - public partial class TestSceneSetupScreen : TournamentTestScene + public partial class TestSceneSetupScreen : TournamentScreenTestScene { [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneShowcaseScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneShowcaseScreen.cs index 6287679c27..21e4d0267f 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneShowcaseScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneShowcaseScreen.cs @@ -1,14 +1,12 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Game.Tournament.Screens.Showcase; namespace osu.Game.Tournament.Tests.Screens { - public partial class TestSceneShowcaseScreen : TournamentTestScene + public partial class TestSceneShowcaseScreen : TournamentScreenTestScene { [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs index dbd9cb2817..3dc595cbb2 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs @@ -1,13 +1,11 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Tournament.Screens.Setup; namespace osu.Game.Tournament.Tests.Screens { - public partial class TestSceneStablePathSelectScreen : TournamentTestScene + public partial class TestSceneStablePathSelectScreen : TournamentScreenTestScene { public TestSceneStablePathSelectScreen() { diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneTeamEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneTeamEditorScreen.cs index 63c08800ad..71f1afbb87 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneTeamEditorScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneTeamEditorScreen.cs @@ -1,15 +1,15 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Game.Tournament.Screens.Editors; namespace osu.Game.Tournament.Tests.Screens { - public partial class TestSceneTeamEditorScreen : TournamentTestScene + public partial class TestSceneTeamEditorScreen : TournamentScreenTestScene { - public TestSceneTeamEditorScreen() + [BackgroundDependencyLoader] + private void load() { Add(new TeamEditorScreen { diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneTeamIntroScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneTeamIntroScreen.cs index 5c26bc203c..b76e0d7521 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneTeamIntroScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneTeamIntroScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Graphics; @@ -11,7 +9,7 @@ using osu.Game.Tournament.Screens.TeamIntro; namespace osu.Game.Tournament.Tests.Screens { - public partial class TestSceneTeamIntroScreen : TournamentTestScene + public partial class TestSceneTeamIntroScreen : TournamentScreenTestScene { [Cached] private readonly LadderInfo ladder = new LadderInfo(); diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs index 43e16873c6..1e3ff72d43 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Graphics; @@ -10,16 +8,16 @@ using osu.Game.Tournament.Screens.TeamWin; namespace osu.Game.Tournament.Tests.Screens { - public partial class TestSceneTeamWinScreen : TournamentTestScene + public partial class TestSceneTeamWinScreen : TournamentScreenTestScene { [Test] public void TestBasic() { AddStep("set up match", () => { - var match = Ladder.CurrentMatch.Value; + var match = Ladder.CurrentMatch.Value!; - match.Round.Value = Ladder.Rounds.FirstOrDefault(g => g.Name.Value == "Finals"); + match.Round.Value = Ladder.Rounds.First(g => g.Name.Value == "Quarterfinals"); match.Completed.Value = true; }); diff --git a/osu.Game.Tournament.Tests/TestSceneTournamentSceneManager.cs b/osu.Game.Tournament.Tests/TestSceneTournamentSceneManager.cs index 859d0591c3..f580b2e455 100644 --- a/osu.Game.Tournament.Tests/TestSceneTournamentSceneManager.cs +++ b/osu.Game.Tournament.Tests/TestSceneTournamentSceneManager.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; namespace osu.Game.Tournament.Tests diff --git a/osu.Game.Tournament.Tests/TournamentScreenTestScene.cs b/osu.Game.Tournament.Tests/TournamentScreenTestScene.cs new file mode 100644 index 0000000000..e8cca00c92 --- /dev/null +++ b/osu.Game.Tournament.Tests/TournamentScreenTestScene.cs @@ -0,0 +1,38 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osuTK; + +namespace osu.Game.Tournament.Tests +{ + public abstract partial class TournamentScreenTestScene : TournamentTestScene + { + protected override Container Content { get; } = new TournamentScalingContainer(); + + [BackgroundDependencyLoader] + private void load() + { + base.Content.Add(Content); + } + + private partial class TournamentScalingContainer : DrawSizePreservingFillContainer + { + public TournamentScalingContainer() + { + TargetDrawSize = new Vector2(1024, 768); + RelativeSizeAxes = Axes.Both; + } + + protected override void Update() + { + base.Update(); + + Scale = new Vector2(Math.Min(1, Content.DrawWidth / (1920 + TournamentSceneManager.CONTROL_AREA_WIDTH))); + } + } + } +} diff --git a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs index f29272fbb8..037afd8690 100644 --- a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs +++ b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; diff --git a/osu.Game.Tournament.Tests/TournamentTestRunner.cs b/osu.Game.Tournament.Tests/TournamentTestRunner.cs index f95fcbf487..5f642b14f5 100644 --- a/osu.Game.Tournament.Tests/TournamentTestRunner.cs +++ b/osu.Game.Tournament.Tests/TournamentTestRunner.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework; using osu.Framework.Platform; @@ -14,7 +12,7 @@ namespace osu.Game.Tournament.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu-development", new HostOptions { BindIPC = true })) { host.Run(new TournamentTestBrowser()); return 0; diff --git a/osu.Game.Tournament.Tests/TournamentTestScene.cs b/osu.Game.Tournament.Tests/TournamentTestScene.cs index cab78422a2..4106556ee1 100644 --- a/osu.Game.Tournament.Tests/TournamentTestScene.cs +++ b/osu.Game.Tournament.Tests/TournamentTestScene.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 System.Threading; using osu.Framework.Allocation; @@ -10,6 +8,7 @@ using osu.Framework.Platform; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Tests.Visual; using osu.Game.Tournament.IO; @@ -18,19 +17,22 @@ using osu.Game.Tournament.Models; namespace osu.Game.Tournament.Tests { - public abstract partial class TournamentTestScene : OsuTestScene + public abstract partial class TournamentTestScene : OsuManualInputManagerTestScene { - private TournamentMatch match; + [Cached(typeof(IDialogOverlay))] + protected readonly DialogOverlay DialogOverlay = new DialogOverlay { Depth = float.MinValue }; [Cached] protected LadderInfo Ladder { get; private set; } = new LadderInfo(); - [Resolved] - private RulesetStore rulesetStore { get; set; } - [Cached] protected MatchIPCInfo IPCInfo { get; private set; } = new MatchIPCInfo(); + [Resolved] + private RulesetStore rulesetStore { get; set; } = null!; + + private TournamentMatch match = null!; + [BackgroundDependencyLoader] private void load(TournamentStorage storage) { @@ -38,13 +40,15 @@ namespace osu.Game.Tournament.Tests match = CreateSampleMatch(); - Ladder.Rounds.Add(match.Round.Value); + Ladder.Rounds.Add(match.Round.Value!); Ladder.Matches.Add(match); - Ladder.Teams.Add(match.Team1.Value); - Ladder.Teams.Add(match.Team2.Value); + Ladder.Teams.Add(match.Team1.Value!); + Ladder.Teams.Add(match.Team2.Value!); Ruleset.BindTo(Ladder.Ruleset); Dependencies.CacheAs(new StableInfo(storage)); + + Add(DialogOverlay); } [SetUpSteps] @@ -63,7 +67,7 @@ namespace osu.Game.Tournament.Tests FlagName = { Value = "JP" }, FullName = { Value = "Japan" }, LastYearPlacing = { Value = 10 }, - Seed = { Value = "Low" }, + Seed = { Value = "#12" }, SeedingResults = { new SeedingResult @@ -136,6 +140,7 @@ namespace osu.Game.Tournament.Tests Acronym = { Value = "USA" }, FlagName = { Value = "US" }, FullName = { Value = "United States" }, + Seed = { Value = "#3" }, Players = { new TournamentUser { Username = "Hello" }, @@ -148,7 +153,7 @@ namespace osu.Game.Tournament.Tests }, Round = { - Value = new TournamentRound { Name = { Value = "Quarterfinals" } } + Value = new TournamentRound { Name = { Value = "Quarterfinals" } }, } }; @@ -167,7 +172,7 @@ namespace osu.Game.Tournament.Tests public partial class TournamentTestSceneTestRunner : TournamentGameBase, ITestSceneTestRunner { - private TestSceneTestRunner.TestRunner runner; + private TestSceneTestRunner.TestRunner runner = null!; protected override void LoadAsyncComplete() { diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 5847079161..2cc07dd9ed 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -4,9 +4,9 @@ osu.Game.Tournament.Tests.TournamentTestRunner - + - + WinExe diff --git a/osu.Game.Tournament/Components/ControlPanel.cs b/osu.Game.Tournament/Components/ControlPanel.cs index c3e66e80eb..b5912349a0 100644 --- a/osu.Game.Tournament/Components/ControlPanel.cs +++ b/osu.Game.Tournament/Components/ControlPanel.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game.Tournament/Components/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs index 192d8c9fd1..dd70d5856d 100644 --- a/osu.Game.Tournament/Components/DateTextBox.cs +++ b/osu.Game.Tournament/Components/DateTextBox.cs @@ -1,9 +1,8 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Globalization; using osu.Framework.Bindables; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; @@ -12,29 +11,26 @@ namespace osu.Game.Tournament.Components { public partial class DateTextBox : SettingsTextBox { - public new Bindable Current + private readonly BindableWithCurrent current = new BindableWithCurrent(DateTimeOffset.Now); + + public new Bindable? Current { get => current; - set - { - current = value.GetBoundCopy(); - current.BindValueChanged(dto => - base.Current.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"), true); - } + set => current.Current = value!; } - // hold a reference to the provided bindable so we don't have to in every settings section. - private Bindable current = new Bindable(); - public DateTextBox() { base.Current = new Bindable(string.Empty); + current.BindValueChanged(dto => + base.Current.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ", DateTimeFormatInfo.InvariantInfo), true); + ((OsuTextBox)Control).OnCommit += (sender, _) => { try { - current.Value = DateTimeOffset.Parse(sender.Text); + current.Value = DateTimeOffset.Parse(sender.Text, DateTimeFormatInfo.InvariantInfo); } catch { diff --git a/osu.Game.Tournament/Components/DrawableTeamFlag.cs b/osu.Game.Tournament/Components/DrawableTeamFlag.cs index 317d685ee7..aef854bb8d 100644 --- a/osu.Game.Tournament/Components/DrawableTeamFlag.cs +++ b/osu.Game.Tournament/Components/DrawableTeamFlag.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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -17,14 +15,14 @@ namespace osu.Game.Tournament.Components { public partial class DrawableTeamFlag : Container { - private readonly TournamentTeam team; + private readonly TournamentTeam? team; [UsedImplicitly] - private Bindable flag; + private Bindable? flag; - private Sprite flagSprite; + private Sprite? flagSprite; - public DrawableTeamFlag(TournamentTeam team) + public DrawableTeamFlag(TournamentTeam? team) { this.team = team; } diff --git a/osu.Game.Tournament/Components/DrawableTeamHeader.cs b/osu.Game.Tournament/Components/DrawableTeamHeader.cs index 1648e7373b..e9ce9f3759 100644 --- a/osu.Game.Tournament/Components/DrawableTeamHeader.cs +++ b/osu.Game.Tournament/Components/DrawableTeamHeader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Tournament.Models; using osuTK; diff --git a/osu.Game.Tournament/Components/DrawableTeamSeed.cs b/osu.Game.Tournament/Components/DrawableTeamSeed.cs new file mode 100644 index 0000000000..077185f5c0 --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTeamSeed.cs @@ -0,0 +1,45 @@ +// 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.Game.Tournament.Models; + +namespace osu.Game.Tournament.Components +{ + public partial class DrawableTeamSeed : TournamentSpriteTextWithBackground + { + private readonly TournamentTeam? team; + + private IBindable seed = null!; + private Bindable displaySeed = null!; + + public DrawableTeamSeed(TournamentTeam? team) + { + this.team = team; + } + + [Resolved] + private LadderInfo ladder { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + Text.Font = Text.Font.With(size: 36); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (team == null) + return; + + seed = team.Seed.GetBoundCopy(); + seed.BindValueChanged(s => Text.Text = s.NewValue, true); + + displaySeed = ladder.DisplayTeamSeeds.GetBoundCopy(); + displaySeed.BindValueChanged(v => Alpha = v.NewValue ? 1 : 0, true); + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTeamTitle.cs b/osu.Game.Tournament/Components/DrawableTeamTitle.cs index 68cc46be19..85b3e5419c 100644 --- a/osu.Game.Tournament/Components/DrawableTeamTitle.cs +++ b/osu.Game.Tournament/Components/DrawableTeamTitle.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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -12,12 +10,12 @@ namespace osu.Game.Tournament.Components { public partial class DrawableTeamTitle : TournamentSpriteTextWithBackground { - private readonly TournamentTeam team; + private readonly TournamentTeam? team; [UsedImplicitly] - private Bindable acronym; + private Bindable? acronym; - public DrawableTeamTitle(TournamentTeam team) + public DrawableTeamTitle(TournamentTeam? team) { this.team = team; } diff --git a/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs b/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs index 27113b0d21..7d8fc847d4 100644 --- a/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs +++ b/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Tournament.Models; @@ -12,7 +10,7 @@ namespace osu.Game.Tournament.Components { public partial class DrawableTeamTitleWithHeader : CompositeDrawable { - public DrawableTeamTitleWithHeader(TournamentTeam team, TeamColour colour) + public DrawableTeamTitleWithHeader(TournamentTeam? team, TeamColour colour) { AutoSizeAxes = Axes.Both; @@ -20,11 +18,12 @@ namespace osu.Game.Tournament.Components { AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 10), + Spacing = new Vector2(0, 5), Children = new Drawable[] { new DrawableTeamHeader(colour), new DrawableTeamTitle(team), + new DrawableTeamSeed(team), } }; } diff --git a/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs b/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs index 9606670ad8..4f0c7d6b72 100644 --- a/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs +++ b/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Graphics; using osu.Framework.Graphics.Containers; @@ -15,7 +13,7 @@ namespace osu.Game.Tournament.Components { public partial class DrawableTeamWithPlayers : CompositeDrawable { - public DrawableTeamWithPlayers(TournamentTeam team, TeamColour colour) + public DrawableTeamWithPlayers(TournamentTeam? team, TeamColour colour) { AutoSizeAxes = Axes.Both; diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs index c83fceb01d..671d2ffb65 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs index 7a1f448cb4..ef576f5b02 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tournament/Components/DrawableTournamentTeam.cs b/osu.Game.Tournament/Components/DrawableTournamentTeam.cs index 0036f5f115..9583682e8b 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentTeam.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentTeam.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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -14,15 +12,15 @@ namespace osu.Game.Tournament.Components { public abstract partial class DrawableTournamentTeam : CompositeDrawable { - public readonly TournamentTeam Team; + public readonly TournamentTeam? Team; protected readonly Container Flag; protected readonly TournamentSpriteText AcronymText; [UsedImplicitly] - private Bindable acronym; + private Bindable? acronym; - protected DrawableTournamentTeam(TournamentTeam team) + protected DrawableTournamentTeam(TournamentTeam? team) { Team = team; @@ -36,7 +34,8 @@ namespace osu.Game.Tournament.Components [BackgroundDependencyLoader] private void load() { - if (Team == null) return; + if (Team == null) + return; (acronym = Team.Acronym.GetBoundCopy()).BindValueChanged(_ => AcronymText.Text = Team?.Acronym.Value?.ToUpperInvariant() ?? string.Empty, true); } diff --git a/osu.Game.Tournament/Components/IPCErrorDialog.cs b/osu.Game.Tournament/Components/IPCErrorDialog.cs index 995bbffffc..07aeb65ee6 100644 --- a/osu.Game.Tournament/Components/IPCErrorDialog.cs +++ b/osu.Game.Tournament/Components/IPCErrorDialog.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics.Sprites; using osu.Game.Overlays.Dialog; diff --git a/osu.Game.Tournament/Components/RoundDisplay.cs b/osu.Game.Tournament/Components/RoundDisplay.cs index 6018cc6ffb..4c72209d12 100644 --- a/osu.Game.Tournament/Components/RoundDisplay.cs +++ b/osu.Game.Tournament/Components/RoundDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index aeceece160..cc1d00f62f 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -16,7 +14,6 @@ using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Rulesets; using osu.Game.Screens.Menu; -using osu.Game.Tournament.Models; using osuTK; using osuTK.Graphics; @@ -24,14 +21,14 @@ namespace osu.Game.Tournament.Components { public partial class SongBar : CompositeDrawable { - private TournamentBeatmap beatmap; + private IBeatmapInfo? beatmap; public const float HEIGHT = 145 / 2f; [Resolved] - private IBindable ruleset { get; set; } + private IBindable ruleset { get; set; } = null!; - public TournamentBeatmap Beatmap + public IBeatmapInfo? Beatmap { set { @@ -39,7 +36,7 @@ namespace osu.Game.Tournament.Components return; beatmap = value; - update(); + refreshContent(); } } @@ -51,11 +48,11 @@ namespace osu.Game.Tournament.Components set { mods = value; - update(); + refreshContent(); } } - private FillFlowContainer flow; + private FillFlowContainer flow = null!; private bool expanded; @@ -73,19 +70,26 @@ namespace osu.Game.Tournament.Components protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false; [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + Masking = true; + CornerRadius = 5; + InternalChildren = new Drawable[] { + new Box + { + Colour = colours.Gray3, + RelativeSizeAxes = Axes.Both, + Alpha = 0.4f, + }, flow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - LayoutDuration = 500, - LayoutEasing = Easing.OutQuint, Direction = FillDirection.Full, Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, @@ -95,7 +99,7 @@ namespace osu.Game.Tournament.Components Expanded = true; } - private void update() + private void refreshContent() { if (beatmap == null) { @@ -188,7 +192,7 @@ namespace osu.Game.Tournament.Components Children = new Drawable[] { new DiffPiece(stats), - new DiffPiece(("Star Rating", $"{beatmap.StarRating:0.##}{srExtra}")) + new DiffPiece(("Star Rating", $"{beatmap.StarRating:0.00}{srExtra}")) } }, new FillFlowContainer diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 1157b50377..4e0adb30ac 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.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.Collections.Specialized; using System.Linq; @@ -11,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; @@ -21,19 +20,18 @@ namespace osu.Game.Tournament.Components { public partial class TournamentBeatmapPanel : CompositeDrawable { - public readonly TournamentBeatmap Beatmap; + public readonly IBeatmapInfo? Beatmap; private readonly string mod; public const float HEIGHT = 50; - private readonly Bindable currentMatch = new Bindable(); - private Box flash; + private readonly Bindable currentMatch = new Bindable(); - public TournamentBeatmapPanel(TournamentBeatmap beatmap, string mod = null) + private Box flash = null!; + + public TournamentBeatmapPanel(IBeatmapInfo? beatmap, string mod = "") { - ArgumentNullException.ThrowIfNull(beatmap); - Beatmap = beatmap; this.mod = mod; @@ -56,11 +54,11 @@ namespace osu.Game.Tournament.Components RelativeSizeAxes = Axes.Both, Colour = Color4.Black, }, - new UpdateableOnlineBeatmapSetCover + new NoUnloadBeatmapSetCover { RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(0.5f), - OnlineInfo = Beatmap, + OnlineInfo = (Beatmap as IBeatmapSetOnlineInfo), }, new FillFlowContainer { @@ -73,7 +71,7 @@ namespace osu.Game.Tournament.Components { new TournamentSpriteText { - Text = Beatmap.GetDisplayTitleRomanisable(false, false), + Text = Beatmap?.GetDisplayTitleRomanisable(false, false) ?? (LocalisableString)@"unknown", Font = OsuFont.Torus.With(weight: FontWeight.Bold), }, new FillFlowContainer @@ -90,7 +88,7 @@ namespace osu.Game.Tournament.Components }, new TournamentSpriteText { - Text = Beatmap.Metadata.Author.Username, + Text = Beatmap?.Metadata.Author.Username ?? "unknown", Padding = new MarginPadding { Right = 20 }, Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14) }, @@ -102,7 +100,7 @@ namespace osu.Game.Tournament.Components }, new TournamentSpriteText { - Text = Beatmap.DifficultyName, + Text = Beatmap?.DifficultyName ?? "unknown", Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14) }, } @@ -131,36 +129,42 @@ namespace osu.Game.Tournament.Components } } - private void matchChanged(ValueChangedEvent match) + private void matchChanged(ValueChangedEvent match) { if (match.OldValue != null) match.OldValue.PicksBans.CollectionChanged -= picksBansOnCollectionChanged; - match.NewValue.PicksBans.CollectionChanged += picksBansOnCollectionChanged; - updateState(); + if (match.NewValue != null) + match.NewValue.PicksBans.CollectionChanged += picksBansOnCollectionChanged; + + Scheduler.AddOnce(updateState); } - private void picksBansOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - => updateState(); + private void picksBansOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + => Scheduler.AddOnce(updateState); - private BeatmapChoice choice; + private BeatmapChoice? choice; private void updateState() { - var found = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == Beatmap.OnlineID); - - bool doFlash = found != choice; - choice = found; - - if (found != null) + if (currentMatch.Value == null) { - if (doFlash) - flash?.FadeOutFromOne(500).Loop(0, 10); + return; + } + + var newChoice = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == Beatmap?.OnlineID); + + bool shouldFlash = newChoice != choice; + + if (newChoice != null) + { + if (shouldFlash) + flash.FadeOutFromOne(500).Loop(0, 10); BorderThickness = 6; - BorderColour = TournamentGame.GetTeamColour(found.Team); + BorderColour = TournamentGame.GetTeamColour(newChoice.Team); - switch (found.Type) + switch (newChoice.Type) { case ChoiceType.Pick: Colour = Color4.White; @@ -179,6 +183,18 @@ namespace osu.Game.Tournament.Components BorderThickness = 0; Alpha = 1; } + + choice = newChoice; + } + + private partial class NoUnloadBeatmapSetCover : UpdateableOnlineBeatmapSetCover + { + // As covers are displayed on stream, we want them to load as soon as possible. + protected override double LoadDelay => 0; + + // Use DelayedLoadWrapper to avoid content unloading when switching away to another screen. + protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func createContentFunc, double timeBeforeLoad) + => new DelayedLoadWrapper(createContentFunc, timeBeforeLoad); } } } diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index 8a0dd6e336..0998e606e9 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.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 osu.Framework.Allocation; using osu.Framework.Bindables; @@ -19,7 +17,10 @@ namespace osu.Game.Tournament.Components { private readonly Bindable chatChannel = new Bindable(); - private ChannelManager manager; + private ChannelManager? manager; + + [Resolved] + private LadderInfo ladderInfo { get; set; } = null!; public TournamentMatchChatDisplay() { @@ -31,8 +32,8 @@ namespace osu.Game.Tournament.Components CornerRadius = 0; } - [BackgroundDependencyLoader(true)] - private void load(MatchIPCInfo ipc, IAPIProvider api) + [BackgroundDependencyLoader] + private void load(MatchIPCInfo? ipc, IAPIProvider api) { if (ipc != null) { @@ -71,7 +72,7 @@ namespace osu.Game.Tournament.Components public void Contract() => this.FadeOut(200); - protected override ChatLine CreateMessage(Message message) => new MatchMessage(message); + protected override ChatLine CreateMessage(Message message) => new MatchMessage(message, ladderInfo); protected override StandAloneDrawableChannel CreateDrawableChannel(Channel channel) => new MatchChannel(channel); @@ -86,19 +87,16 @@ namespace osu.Game.Tournament.Components protected partial class MatchMessage : StandAloneMessage { - public MatchMessage(Message message) + public MatchMessage(Message message, LadderInfo info) : base(message) { - } - - private void load(LadderInfo info) - { - // if (info.CurrentMatch.Value.Team1.Value.Players.Any(u => u.Id == Message.Sender.Id)) - // SenderText.Colour = TournamentGame.COLOUR_RED; - // else if (info.CurrentMatch.Value.Team2.Value.Players.Any(u => u.Id == Message.Sender.Id)) - // SenderText.Colour = TournamentGame.COLOUR_BLUE; - // else if (Message.Sender.Colour != null) - // SenderText.Colour = ColourBox.Colour = Color4Extensions.FromHex(Message.Sender.Colour); + if (info.CurrentMatch.Value is TournamentMatch match) + { + if (match.Team1.Value?.Players.Any(u => u.OnlineID == Message.Sender.OnlineID) == true) + UsernameColour = TournamentGame.COLOUR_RED; + else if (match.Team2.Value?.Players.Any(u => u.OnlineID == Message.Sender.OnlineID) == true) + UsernameColour = TournamentGame.COLOUR_BLUE; + } } } } diff --git a/osu.Game.Tournament/Components/TournamentModIcon.cs b/osu.Game.Tournament/Components/TournamentModIcon.cs index 76b6151519..28114e64fe 100644 --- a/osu.Game.Tournament/Components/TournamentModIcon.cs +++ b/osu.Game.Tournament/Components/TournamentModIcon.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -23,7 +21,7 @@ namespace osu.Game.Tournament.Components private readonly string modAcronym; [Resolved] - private IRulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } = null!; public TournamentModIcon(string modAcronym) { diff --git a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs index 3a16662463..21439482e3 100644 --- a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs +++ b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -32,7 +30,7 @@ namespace osu.Game.Tournament.Components Colour = TournamentGame.ELEMENT_FOREGROUND_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.SemiBold, size: 50), Padding = new MarginPadding { Left = 10, Right = 20 }, - Text = text + Text = text, } }; } diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index b9ce84b735..6e45c7556b 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -19,8 +17,8 @@ namespace osu.Game.Tournament.Components { private readonly string filename; private readonly bool drawFallbackGradient; - private Video video; - private ManualClock manualClock; + private Video? video; + private ManualClock? manualClock; public bool VideoAvailable => video != null; diff --git a/osu.Game.Tournament/Configuration/TournamentConfigManager.cs b/osu.Game.Tournament/Configuration/TournamentConfigManager.cs index 8f256ba9c3..b76b18cb5f 100644 --- a/osu.Game.Tournament/Configuration/TournamentConfigManager.cs +++ b/osu.Game.Tournament/Configuration/TournamentConfigManager.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Configuration; using osu.Framework.Platform; diff --git a/osu.Game.Tournament/CountryExtensions.cs b/osu.Game.Tournament/CountryExtensions.cs index f2a583c8a5..c66727bc77 100644 --- a/osu.Game.Tournament/CountryExtensions.cs +++ b/osu.Game.Tournament/CountryExtensions.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 osu.Game.Users; namespace osu.Game.Tournament @@ -519,9 +518,6 @@ namespace osu.Game.Tournament case CountryCode.KE: return "KEN"; - case CountryCode.SS: - return "SSD"; - case CountryCode.SR: return "SUR"; @@ -763,7 +759,7 @@ namespace osu.Game.Tournament return "MOZ"; default: - throw new ArgumentOutOfRangeException(nameof(country)); + return country.ToString(); } } } diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index e59f90a45e..7c5f3e44a7 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -1,7 +1,9 @@ // 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.Collections.Generic; +using System.Linq; using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Framework.Platform; @@ -43,6 +45,6 @@ namespace osu.Game.Tournament.IO Logger.Log("Changing tournament storage: " + GetFullPath(string.Empty)); } - public IEnumerable ListTournaments() => AllTournaments.GetDirectories(string.Empty); + public IEnumerable ListTournaments() => AllTournaments.GetDirectories(string.Empty).OrderBy(directory => directory, StringComparer.CurrentCultureIgnoreCase); } } diff --git a/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs b/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs index ba584f1d3e..ba0e593eae 100644 --- a/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs +++ b/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.IO.Stores; using osu.Framework.Platform; diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 7babb3ea5a..5407c21079 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -1,12 +1,9 @@ // 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.IO; using System.Linq; -using JetBrains.Annotations; using Microsoft.Win32; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; @@ -24,36 +21,35 @@ namespace osu.Game.Tournament.IPC { public partial class FileBasedIPC : MatchIPCInfo { - public Storage IPCStorage { get; private set; } + public Storage? IPCStorage { get; private set; } [Resolved] - protected IAPIProvider API { get; private set; } + protected IAPIProvider API { get; private set; } = null!; [Resolved] - protected IRulesetStore Rulesets { get; private set; } + protected IRulesetStore Rulesets { get; private set; } = null!; [Resolved] - private GameHost host { get; set; } + private GameHost host { get; set; } = null!; [Resolved] - private LadderInfo ladder { get; set; } + private LadderInfo ladder { get; set; } = null!; [Resolved] - private StableInfo stableInfo { get; set; } + private StableInfo stableInfo { get; set; } = null!; private int lastBeatmapId; - private ScheduledDelegate scheduled; - private GetBeatmapRequest beatmapLookupRequest; + private ScheduledDelegate? scheduled; + private GetBeatmapRequest? beatmapLookupRequest; [BackgroundDependencyLoader] private void load() { - string stablePath = stableInfo.StablePath ?? findStablePath(); + string? stablePath = stableInfo.StablePath ?? findStablePath(); initialiseIPCStorage(stablePath); } - [CanBeNull] - private Storage initialiseIPCStorage(string path) + private Storage? initialiseIPCStorage(string? path) { scheduled?.Cancel(); @@ -89,14 +85,23 @@ namespace osu.Game.Tournament.IPC lastBeatmapId = beatmapId; - var existing = ladder.CurrentMatch.Value?.Round.Value?.Beatmaps.FirstOrDefault(b => b.ID == beatmapId && b.Beatmap != null); + var existing = ladder.CurrentMatch.Value?.Round.Value?.Beatmaps.FirstOrDefault(b => b.ID == beatmapId); if (existing != null) Beatmap.Value = existing.Beatmap; else { beatmapLookupRequest = new GetBeatmapRequest(new APIBeatmap { OnlineID = beatmapId }); - beatmapLookupRequest.Success += b => Beatmap.Value = new TournamentBeatmap(b); + beatmapLookupRequest.Success += b => + { + if (lastBeatmapId == beatmapId) + Beatmap.Value = new TournamentBeatmap(b); + }; + beatmapLookupRequest.Failure += _ => + { + if (lastBeatmapId == beatmapId) + Beatmap.Value = null; + }; API.Queue(beatmapLookupRequest); } } @@ -114,7 +119,7 @@ namespace osu.Game.Tournament.IPC using (var stream = IPCStorage.GetStream(file_ipc_channel_filename)) using (var sr = new StreamReader(stream)) { - ChatChannel.Value = sr.ReadLine(); + ChatChannel.Value = sr.ReadLine().AsNonNull(); } } catch (Exception) @@ -140,8 +145,8 @@ namespace osu.Game.Tournament.IPC using (var stream = IPCStorage.GetStream(file_ipc_scores_filename)) using (var sr = new StreamReader(stream)) { - Score1.Value = int.Parse(sr.ReadLine()); - Score2.Value = int.Parse(sr.ReadLine()); + Score1.Value = int.Parse(sr.ReadLine().AsNonNull()); + Score2.Value = int.Parse(sr.ReadLine().AsNonNull()); } } catch (Exception) @@ -164,7 +169,7 @@ namespace osu.Game.Tournament.IPC /// /// Path to the IPC directory /// Whether the supplied path was a valid IPC directory. - public bool SetIPCLocation(string path) + public bool SetIPCLocation(string? path) { if (path == null || !ipcFileExistsInDirectory(path)) return false; @@ -184,29 +189,28 @@ namespace osu.Game.Tournament.IPC /// Whether an IPC directory was successfully auto-detected. public bool AutoDetectIPCLocation() => SetIPCLocation(findStablePath()); - private static bool ipcFileExistsInDirectory(string p) => p != null && File.Exists(Path.Combine(p, "ipc.txt")); + private static bool ipcFileExistsInDirectory(string? p) => p != null && File.Exists(Path.Combine(p, "ipc.txt")); - [CanBeNull] - private string findStablePath() + private string? findStablePath() { - string stableInstallPath = findFromEnvVar() ?? - findFromRegistry() ?? - findFromLocalAppData() ?? - findFromDotFolder(); + string? stableInstallPath = findFromEnvVar() ?? + findFromRegistry() ?? + findFromLocalAppData() ?? + findFromDotFolder(); Logger.Log($"Stable path for tourney usage: {stableInstallPath}"); return stableInstallPath; } - private string findFromEnvVar() + private string? findFromEnvVar() { try { Logger.Log("Trying to find stable with environment variables"); - string stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH"); + string? stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH"); if (ipcFileExistsInDirectory(stableInstallPath)) - return stableInstallPath; + return stableInstallPath!; } catch { @@ -215,7 +219,7 @@ namespace osu.Game.Tournament.IPC return null; } - private string findFromLocalAppData() + private string? findFromLocalAppData() { Logger.Log("Trying to find stable in %LOCALAPPDATA%"); string stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); @@ -226,7 +230,7 @@ namespace osu.Game.Tournament.IPC return null; } - private string findFromDotFolder() + private string? findFromDotFolder() { Logger.Log("Trying to find stable in dotfolders"); string stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu"); @@ -237,16 +241,16 @@ namespace osu.Game.Tournament.IPC return null; } - private string findFromRegistry() + private string? findFromRegistry() { Logger.Log("Trying to find stable in registry"); try { - string stableInstallPath; + string? stableInstallPath; #pragma warning disable CA1416 - using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) + using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu")) stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", ""); #pragma warning restore CA1416 diff --git a/osu.Game.Tournament/IPC/MatchIPCInfo.cs b/osu.Game.Tournament/IPC/MatchIPCInfo.cs index 3bf790d58e..b4575144e7 100644 --- a/osu.Game.Tournament/IPC/MatchIPCInfo.cs +++ b/osu.Game.Tournament/IPC/MatchIPCInfo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps.Legacy; @@ -12,11 +10,11 @@ namespace osu.Game.Tournament.IPC { public partial class MatchIPCInfo : Component { - public Bindable Beatmap { get; } = new Bindable(); + public Bindable Beatmap { get; } = new Bindable(); public Bindable Mods { get; } = new Bindable(); public Bindable State { get; } = new Bindable(); public Bindable ChatChannel { get; } = new Bindable(); - public BindableInt Score1 { get; } = new BindableInt(); - public BindableInt Score2 { get; } = new BindableInt(); + public BindableLong Score1 { get; } = new BindableLong(); + public BindableLong Score2 { get; } = new BindableLong(); } } diff --git a/osu.Game.Tournament/JsonPointConverter.cs b/osu.Game.Tournament/JsonPointConverter.cs index d3b40a3526..a58ec47612 100644 --- a/osu.Game.Tournament/JsonPointConverter.cs +++ b/osu.Game.Tournament/JsonPointConverter.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.Diagnostics; using System.Drawing; @@ -28,7 +26,7 @@ namespace osu.Game.Tournament if (reader.TokenType != JsonToken.StartObject) { // if there's no object present then this is using string representation (System.Drawing.Point serializes to "x,y") - string str = (string)reader.Value; + string? str = (string?)reader.Value; Debug.Assert(str != null); @@ -45,9 +43,12 @@ namespace osu.Game.Tournament if (reader.TokenType == JsonToken.PropertyName) { - string name = reader.Value?.ToString(); + string? name = reader.Value?.ToString(); int? val = reader.ReadAsInt32(); + if (name == null) + continue; + if (val == null) continue; diff --git a/osu.Game.Tournament/Models/BeatmapChoice.cs b/osu.Game.Tournament/Models/BeatmapChoice.cs index ddd4597722..c8ba989fa7 100644 --- a/osu.Game.Tournament/Models/BeatmapChoice.cs +++ b/osu.Game.Tournament/Models/BeatmapChoice.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Newtonsoft.Json; using Newtonsoft.Json.Converters; diff --git a/osu.Game.Tournament/Models/LadderEditorInfo.cs b/osu.Game.Tournament/Models/LadderEditorInfo.cs index 84ebeff3db..7b36096e3f 100644 --- a/osu.Game.Tournament/Models/LadderEditorInfo.cs +++ b/osu.Game.Tournament/Models/LadderEditorInfo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; namespace osu.Game.Tournament.Models diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs index 6b64a1156e..f96dae8044 100644 --- a/osu.Game.Tournament/Models/LadderInfo.cs +++ b/osu.Game.Tournament/Models/LadderInfo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using Newtonsoft.Json; @@ -17,7 +15,7 @@ namespace osu.Game.Tournament.Models [Serializable] public class LadderInfo { - public Bindable Ruleset = new Bindable(); + public Bindable Ruleset = new Bindable(); public BindableList Matches = new BindableList(); public BindableList Rounds = new BindableList(); @@ -27,7 +25,7 @@ namespace osu.Game.Tournament.Models public List Progressions = new List(); [JsonIgnore] // updated manually in TournamentGameBase - public Bindable CurrentMatch = new Bindable(); + public Bindable CurrentMatch = new Bindable(); public Bindable ChromaKeyWidth = new BindableInt(1024) { @@ -42,5 +40,9 @@ namespace osu.Game.Tournament.Models }; public Bindable AutoProgressScreens = new BindableBool(true); + + public Bindable SplitMapPoolByMods = new BindableBool(true); + + public Bindable DisplayTeamSeeds = new BindableBool(); } } diff --git a/osu.Game.Tournament/Models/RoundBeatmap.cs b/osu.Game.Tournament/Models/RoundBeatmap.cs index 65ef77e53d..b03b28b3b8 100644 --- a/osu.Game.Tournament/Models/RoundBeatmap.cs +++ b/osu.Game.Tournament/Models/RoundBeatmap.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 Newtonsoft.Json; namespace osu.Game.Tournament.Models @@ -10,9 +8,9 @@ namespace osu.Game.Tournament.Models public class RoundBeatmap { public int ID; - public string Mods; + public string Mods = string.Empty; [JsonProperty("BeatmapInfo")] - public TournamentBeatmap Beatmap; + public TournamentBeatmap? Beatmap; } } diff --git a/osu.Game.Tournament/Models/SeedingResult.cs b/osu.Game.Tournament/Models/SeedingResult.cs index 2a404153e6..865f65294e 100644 --- a/osu.Game.Tournament/Models/SeedingResult.cs +++ b/osu.Game.Tournament/Models/SeedingResult.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Bindables; diff --git a/osu.Game.Tournament/Models/StableInfo.cs b/osu.Game.Tournament/Models/StableInfo.cs index 1ae80d4596..7ee0b4a361 100644 --- a/osu.Game.Tournament/Models/StableInfo.cs +++ b/osu.Game.Tournament/Models/StableInfo.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.IO; using Newtonsoft.Json; @@ -20,12 +18,12 @@ namespace osu.Game.Tournament.Models /// /// Path to the IPC directory used by the stable (cutting-edge) install. /// - public string StablePath { get; set; } + public string? StablePath { get; set; } /// /// Fired whenever stable info is successfully saved to file. /// - public event Action OnStableInfoSaved; + public event Action? OnStableInfoSaved; private const string config_path = "stable.json"; diff --git a/osu.Game.Tournament/Models/TournamentMatch.cs b/osu.Game.Tournament/Models/TournamentMatch.cs index 97c2060f2c..0a700eb4d6 100644 --- a/osu.Game.Tournament/Models/TournamentMatch.cs +++ b/osu.Game.Tournament/Models/TournamentMatch.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.Collections.Generic; using System.Collections.ObjectModel; @@ -33,16 +31,16 @@ namespace osu.Game.Tournament.Models } [JsonIgnore] - public readonly Bindable Team1 = new Bindable(); + public readonly Bindable Team1 = new Bindable(); - public string Team1Acronym; + public string? Team1Acronym; public readonly Bindable Team1Score = new Bindable(); [JsonIgnore] - public readonly Bindable Team2 = new Bindable(); + public readonly Bindable Team2 = new Bindable(); - public string Team2Acronym; + public string? Team2Acronym; public readonly Bindable Team2Score = new Bindable(); @@ -53,13 +51,13 @@ namespace osu.Game.Tournament.Models public readonly ObservableCollection PicksBans = new ObservableCollection(); [JsonIgnore] - public readonly Bindable Round = new Bindable(); + public readonly Bindable Round = new Bindable(); [JsonIgnore] - public readonly Bindable Progression = new Bindable(); + public readonly Bindable Progression = new Bindable(); [JsonIgnore] - public readonly Bindable LosersProgression = new Bindable(); + public readonly Bindable LosersProgression = new Bindable(); /// /// Should not be set directly. Use LadderInfo.CurrentMatch.Value = this instead. @@ -79,7 +77,7 @@ namespace osu.Game.Tournament.Models Team2.BindValueChanged(t => Team2Acronym = t.NewValue?.Acronym.Value, true); } - public TournamentMatch(TournamentTeam team1 = null, TournamentTeam team2 = null) + public TournamentMatch(TournamentTeam? team1 = null, TournamentTeam? team2 = null) : this() { Team1.Value = team1; @@ -87,10 +85,10 @@ namespace osu.Game.Tournament.Models } [JsonIgnore] - public TournamentTeam Winner => !Completed.Value ? null : Team1Score.Value > Team2Score.Value ? Team1.Value : Team2.Value; + public TournamentTeam? Winner => !Completed.Value ? null : Team1Score.Value > Team2Score.Value ? Team1.Value : Team2.Value; [JsonIgnore] - public TournamentTeam Loser => !Completed.Value ? null : Team1Score.Value > Team2Score.Value ? Team2.Value : Team1.Value; + public TournamentTeam? Loser => !Completed.Value ? null : Team1Score.Value > Team2Score.Value ? Team2.Value : Team1.Value; public TeamColour WinnerColour => Winner == Team1.Value ? TeamColour.Red : TeamColour.Blue; diff --git a/osu.Game.Tournament/Models/TournamentProgression.cs b/osu.Game.Tournament/Models/TournamentProgression.cs index 6c3ba1922a..f119605026 100644 --- a/osu.Game.Tournament/Models/TournamentProgression.cs +++ b/osu.Game.Tournament/Models/TournamentProgression.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; namespace osu.Game.Tournament.Models diff --git a/osu.Game.Tournament/Models/TournamentRound.cs b/osu.Game.Tournament/Models/TournamentRound.cs index 480d6c37c3..a92bab690e 100644 --- a/osu.Game.Tournament/Models/TournamentRound.cs +++ b/osu.Game.Tournament/Models/TournamentRound.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using Newtonsoft.Json; diff --git a/osu.Game.Tournament/Models/TournamentTeam.cs b/osu.Game.Tournament/Models/TournamentTeam.cs index 1beea517d5..95858240a8 100644 --- a/osu.Game.Tournament/Models/TournamentTeam.cs +++ b/osu.Game.Tournament/Models/TournamentTeam.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 Newtonsoft.Json; @@ -39,7 +37,7 @@ namespace osu.Game.Tournament.Models { int[] ranks = Players.Select(p => p.Rank) .Where(i => i.HasValue) - .Select(i => i.Value) + .Select(i => i!.Value) .ToArray(); if (ranks.Length == 0) @@ -53,12 +51,12 @@ namespace osu.Game.Tournament.Models public Bindable LastYearPlacing = new BindableInt { - MinValue = 1, + MinValue = 0, MaxValue = 256 }; [JsonProperty] - public BindableList Players { get; set; } = new BindableList(); + public BindableList Players { get; } = new BindableList(); public TournamentTeam() { @@ -66,14 +64,14 @@ namespace osu.Game.Tournament.Models { // use a sane default flag name based on acronym. if (val.OldValue.StartsWith(FlagName.Value, StringComparison.InvariantCultureIgnoreCase)) - FlagName.Value = val.NewValue.Length >= 2 ? val.NewValue?.Substring(0, 2).ToUpperInvariant() : string.Empty; + FlagName.Value = val.NewValue?.Length >= 2 ? val.NewValue.Substring(0, 2).ToUpperInvariant() : string.Empty; }; FullName.ValueChanged += val => { // use a sane acronym based on full name. if (val.OldValue.StartsWith(Acronym.Value, StringComparison.InvariantCultureIgnoreCase)) - Acronym.Value = val.NewValue.Length >= 3 ? val.NewValue?.Substring(0, 3).ToUpperInvariant() : string.Empty; + Acronym.Value = val.NewValue?.Length >= 3 ? val.NewValue.Substring(0, 3).ToUpperInvariant() : string.Empty; }; } diff --git a/osu.Game.Tournament/Properties/AssemblyInfo.cs b/osu.Game.Tournament/Properties/AssemblyInfo.cs index 2eb8c3e1d6..70e42bcafb 100644 --- a/osu.Game.Tournament/Properties/AssemblyInfo.cs +++ b/osu.Game.Tournament/Properties/AssemblyInfo.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.Runtime.CompilerServices; // We publish our internal attributes to other sub-projects of the framework. diff --git a/osu.Game.Tournament/SaveChangesOverlay.cs b/osu.Game.Tournament/SaveChangesOverlay.cs index 6db8605808..0b5194a51f 100644 --- a/osu.Game.Tournament/SaveChangesOverlay.cs +++ b/osu.Game.Tournament/SaveChangesOverlay.cs @@ -7,13 +7,16 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; using osuTK; namespace osu.Game.Tournament { - internal partial class SaveChangesOverlay : CompositeDrawable + internal partial class SaveChangesOverlay : CompositeDrawable, IKeyBindingHandler { [Resolved] private TournamentGame tournamentGame { get; set; } = null!; @@ -25,12 +28,11 @@ namespace osu.Game.Tournament { RelativeSizeAxes = Axes.Both; - InternalChild = new Container + InternalChild = new CircularContainer { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Position = new Vector2(5), - CornerRadius = 10, + Position = new Vector2(-5), Masking = true, AutoSizeAxes = Axes.Both, Children = new Drawable[] @@ -43,18 +45,10 @@ namespace osu.Game.Tournament saveChangesButton = new TourneyButton { Text = "Save Changes", + RelativeSizeAxes = Axes.None, Width = 140, Height = 50, - Padding = new MarginPadding - { - Top = 10, - Left = 10, - }, - Margin = new MarginPadding - { - Right = 10, - Bottom = 10, - }, + Margin = new MarginPadding(10), Action = saveChanges, // Enabled = { Value = false }, }, @@ -87,6 +81,21 @@ namespace osu.Game.Tournament scheduleNextCheck(); } + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Action == PlatformAction.Save && !e.Repeat) + { + saveChangesButton.TriggerClick(); + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + private void scheduleNextCheck() => Scheduler.AddDelayed(() => checkForChanges().FireAndForget(), 1000); private void saveChanges() diff --git a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs index 6f7234b8c3..32a50d2222 100644 --- a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs +++ b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -39,7 +37,7 @@ namespace osu.Game.Tournament.Screens SongBar.Mods = mods.NewValue; } - private void beatmapChanged(ValueChangedEvent beatmap) + private void beatmapChanged(ValueChangedEvent beatmap) { SongBar.FadeInFromZero(300, Easing.OutQuint); SongBar.Beatmap = beatmap.NewValue; diff --git a/osu.Game.Tournament/Screens/Drawings/Components/DrawingsConfigManager.cs b/osu.Game.Tournament/Screens/Drawings/Components/DrawingsConfigManager.cs index ac1d599851..1a2f5a1ff4 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/DrawingsConfigManager.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/DrawingsConfigManager.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 osu.Framework.Configuration; using osu.Framework.Platform; diff --git a/osu.Game.Tournament/Screens/Drawings/Components/Group.cs b/osu.Game.Tournament/Screens/Drawings/Components/Group.cs index b397f807f0..9d4474a58c 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/Group.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/Group.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.Collections.Generic; using System.Linq; using System.Text; @@ -86,7 +84,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components public bool ContainsTeam(string fullName) { - return allTeams.Any(t => t.Team.FullName.Value == fullName); + return allTeams.Any(t => t.Team?.FullName.Value == fullName); } public bool RemoveTeam(TournamentTeam team) @@ -114,7 +112,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components { StringBuilder sb = new StringBuilder(); foreach (GroupTeam gt in allTeams) - sb.AppendLine(gt.Team.FullName.Value); + sb.AppendLine(gt.Team?.FullName.Value); return sb.ToString(); } } diff --git a/osu.Game.Tournament/Screens/Drawings/Components/GroupContainer.cs b/osu.Game.Tournament/Screens/Drawings/Components/GroupContainer.cs index 37e15b7e45..0a2fa3f207 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/GroupContainer.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/GroupContainer.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.Collections.Generic; using System.Linq; diff --git a/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs b/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs index 167a576424..137003a126 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; diff --git a/osu.Game.Tournament/Screens/Drawings/Components/ITeamList.cs b/osu.Game.Tournament/Screens/Drawings/Components/ITeamList.cs index 7e0ac89c83..09208818a9 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/ITeamList.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/ITeamList.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.Collections.Generic; using osu.Game.Tournament.Models; diff --git a/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs b/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs index c2b15dd3e9..d4e0f29852 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.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; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -22,8 +21,8 @@ namespace osu.Game.Tournament.Screens.Drawings.Components { public partial class ScrollingTeamContainer : Container { - public event Action OnScrollStarted; - public event Action OnSelected; + public event Action? OnScrollStarted; + public event Action? OnSelected; private readonly List availableTeams = new List(); @@ -42,7 +41,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components private double lastTime; - private ScheduledDelegate delayedStateChangeDelegate; + private ScheduledDelegate? delayedStateChangeDelegate; public ScrollingTeamContainer() { @@ -117,7 +116,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components if (!Children.Any()) break; - ScrollingTeam closest = null; + ScrollingTeam? closest = null; foreach (var c in Children) { @@ -137,9 +136,8 @@ namespace osu.Game.Tournament.Screens.Drawings.Components closest = stc; } - Trace.Assert(closest != null, "closest != null"); + Debug.Assert(closest != null, "closest != null"); - // ReSharper disable once PossibleNullReferenceException offset += DrawWidth / 2f - (closest.Position.X + closest.DrawWidth / 2f); ScrollingTeam st = closest; @@ -147,7 +145,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components availableTeams.RemoveAll(at => at == st.Team); st.Selected = true; - OnSelected?.Invoke(st.Team); + OnSelected?.Invoke(st.Team.AsNonNull()); delayedStateChangeDelegate = Scheduler.AddDelayed(() => setScrollState(ScrollState.Idle), 10000); break; @@ -174,7 +172,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components setScrollState(ScrollState.Idle); } - public void AddTeams(IEnumerable teams) + public void AddTeams(IEnumerable? teams) { if (teams == null) return; @@ -311,6 +309,8 @@ namespace osu.Game.Tournament.Screens.Drawings.Components public partial class ScrollingTeam : DrawableTournamentTeam { + public new TournamentTeam Team => base.Team.AsNonNull(); + public const float WIDTH = 58; public const float HEIGHT = 44; diff --git a/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs b/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs index 74afb42c1a..e13462b9bd 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.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.Collections.Generic; using System.IO; @@ -39,7 +37,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components { while (sr.Peek() != -1) { - string line = sr.ReadLine()?.Trim(); + string? line = sr.ReadLine()?.Trim(); if (string.IsNullOrEmpty(line)) continue; @@ -56,7 +54,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components teams.Add(new TournamentTeam { FullName = { Value = split[1], }, - Acronym = { Value = split.Length >= 3 ? split[2] : null, }, + Acronym = { Value = split.Length >= 3 ? split[2] : string.Empty, }, FlagName = { Value = split[0] } }); } diff --git a/osu.Game.Tournament/Screens/Drawings/Components/VisualiserContainer.cs b/osu.Game.Tournament/Screens/Drawings/Components/VisualiserContainer.cs index 676eec14cd..d5e39e3f44 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/VisualiserContainer.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/VisualiserContainer.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.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -72,7 +70,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components private float leftPos => -(float)((Time.Current + Offset) / CycleTime) + expiredCount; - private Texture texture; + private Texture texture = null!; private int expiredCount; diff --git a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs index 23d0edf26e..fc59b486fe 100644 --- a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs +++ b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs @@ -1,14 +1,13 @@ // 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.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -28,28 +27,29 @@ namespace osu.Game.Tournament.Screens.Drawings { private const string results_filename = "drawings_results.txt"; - private ScrollingTeamContainer teamsContainer; - private GroupContainer groupsContainer; - private TournamentSpriteText fullTeamNameText; + private ScrollingTeamContainer teamsContainer = null!; + private GroupContainer groupsContainer = null!; + private TournamentSpriteText fullTeamNameText = null!; private readonly List allTeams = new List(); - private DrawingsConfigManager drawingsConfig; + private DrawingsConfigManager drawingsConfig = null!; - private Task writeOp; + private Task? writeOp; - private Storage storage; + private Storage storage = null!; - public ITeamList TeamList; + public ITeamList TeamList = null!; [BackgroundDependencyLoader] private void load(Storage storage) { - RelativeSizeAxes = Axes.Both; - this.storage = storage; - TeamList ??= new StorageBackedTeamList(storage); + RelativeSizeAxes = Axes.Both; + + if (TeamList.IsNull()) + TeamList = new StorageBackedTeamList(storage); if (!TeamList.Teams.Any()) { @@ -251,7 +251,7 @@ namespace osu.Game.Tournament.Screens.Drawings using (Stream stream = storage.GetStream(results_filename, FileAccess.Read, FileMode.Open)) using (StreamReader sr = new StreamReader(stream)) { - string line; + string? line; while ((line = sr.ReadLine()?.Trim()) != null) { @@ -261,8 +261,7 @@ namespace osu.Game.Tournament.Screens.Drawings if (line.ToUpperInvariant().StartsWith("GROUP", StringComparison.Ordinal)) continue; - // ReSharper disable once AccessToModifiedClosure - TournamentTeam teamToAdd = allTeams.FirstOrDefault(t => t.FullName.Value == line); + TournamentTeam? teamToAdd = allTeams.FirstOrDefault(t => t.FullName.Value == line); if (teamToAdd == null) continue; diff --git a/osu.Game.Tournament/Screens/Editors/Components/DeleteRoundDialog.cs b/osu.Game.Tournament/Screens/Editors/Components/DeleteRoundDialog.cs new file mode 100644 index 0000000000..769412bf94 --- /dev/null +++ b/osu.Game.Tournament/Screens/Editors/Components/DeleteRoundDialog.cs @@ -0,0 +1,20 @@ +// 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.Graphics.Sprites; +using osu.Game.Overlays.Dialog; +using osu.Game.Tournament.Models; + +namespace osu.Game.Tournament.Screens.Editors.Components +{ + public partial class DeleteRoundDialog : DangerousActionDialog + { + public DeleteRoundDialog(TournamentRound round, Action action) + { + HeaderText = round.Name.Value.Length > 0 ? $@"Delete round ""{round.Name.Value}""?" : @"Delete unnamed round?"; + Icon = FontAwesome.Solid.Trash; + DangerousAction = action; + } + } +} diff --git a/osu.Game.Tournament/Screens/Editors/Components/DeleteTeamDialog.cs b/osu.Game.Tournament/Screens/Editors/Components/DeleteTeamDialog.cs new file mode 100644 index 0000000000..65fb47cf94 --- /dev/null +++ b/osu.Game.Tournament/Screens/Editors/Components/DeleteTeamDialog.cs @@ -0,0 +1,22 @@ +// 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.Graphics.Sprites; +using osu.Game.Overlays.Dialog; +using osu.Game.Tournament.Models; + +namespace osu.Game.Tournament.Screens.Editors.Components +{ + public partial class DeleteTeamDialog : DangerousActionDialog + { + public DeleteTeamDialog(TournamentTeam team, Action action) + { + HeaderText = team.FullName.Value.Length > 0 ? $@"Delete team ""{team.FullName.Value}""?" : + team.Acronym.Value.Length > 0 ? $@"Delete team ""{team.Acronym.Value}""?" : + @"Delete unnamed team?"; + Icon = FontAwesome.Solid.Trash; + DangerousAction = action; + } + } +} diff --git a/osu.Game.Tournament/Screens/Editors/Components/LadderResetTeamsDialog.cs b/osu.Game.Tournament/Screens/Editors/Components/LadderResetTeamsDialog.cs new file mode 100644 index 0000000000..8ed1b381f6 --- /dev/null +++ b/osu.Game.Tournament/Screens/Editors/Components/LadderResetTeamsDialog.cs @@ -0,0 +1,19 @@ +// 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.Graphics.Sprites; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Tournament.Screens.Editors.Components +{ + public partial class LadderResetTeamsDialog : DangerousActionDialog + { + public LadderResetTeamsDialog(Action action) + { + HeaderText = @"Reset teams?"; + Icon = FontAwesome.Solid.Undo; + DangerousAction = action; + } + } +} diff --git a/osu.Game.Tournament/Screens/Editors/Components/TournamentClearAllDialog.cs b/osu.Game.Tournament/Screens/Editors/Components/TournamentClearAllDialog.cs new file mode 100644 index 0000000000..1dc7c8231d --- /dev/null +++ b/osu.Game.Tournament/Screens/Editors/Components/TournamentClearAllDialog.cs @@ -0,0 +1,19 @@ +// 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.Graphics.Sprites; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Tournament.Screens.Editors.Components +{ + public partial class TournamentClearAllDialog : DangerousActionDialog + { + public TournamentClearAllDialog(Action action) + { + HeaderText = @"Clear all?"; + Icon = FontAwesome.Solid.Trash; + DangerousAction = action; + } + } +} diff --git a/osu.Game.Tournament/Screens/Editors/IModelBacked.cs b/osu.Game.Tournament/Screens/Editors/IModelBacked.cs index ca59afa2cb..867055f9ea 100644 --- a/osu.Game.Tournament/Screens/Editors/IModelBacked.cs +++ b/osu.Game.Tournament/Screens/Editors/IModelBacked.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Tournament.Screens.Editors { /// diff --git a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs index 4ee3108034..4074e681f9 100644 --- a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.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.Drawing; using System.Linq; @@ -14,7 +12,11 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Input.States; using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Tournament.Components; +using osu.Game.Overlays; using osu.Game.Tournament.Models; +using osu.Game.Tournament.Screens.Editors.Components; using osu.Game.Tournament.Screens.Ladder; using osu.Game.Tournament.Screens.Ladder.Components; using osuTK; @@ -25,29 +27,59 @@ namespace osu.Game.Tournament.Screens.Editors [Cached] public partial class LadderEditorScreen : LadderScreen, IHasContextMenu { + public const float GRID_SPACING = 10; + [Cached] private LadderEditorInfo editorInfo = new LadderEditorInfo(); - private WarningBox rightClickMessage; + private WarningBox rightClickMessage = null!; + + private RectangularPositionSnapGrid grid = null!; + + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } protected override bool DrawLoserPaths => true; [BackgroundDependencyLoader] private void load() { - Content.Add(new LadderEditorSettings + AddInternal(new ControlPanel { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Margin = new MarginPadding(5) + Child = new LadderEditorSettings(), }); AddInternal(rightClickMessage = new WarningBox("Right click to place and link matches")); + ScrollContent.Add(grid = new RectangularPositionSnapGrid(Vector2.Zero) + { + Spacing = new Vector2(GRID_SPACING), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + BypassAutoSizeAxes = Axes.Both, + Depth = float.MaxValue + }); + LadderInfo.Matches.CollectionChanged += (_, _) => updateMessage(); updateMessage(); } + protected override void Update() + { + base.Update(); + + // Expand grid with the content to allow going beyond the bounds of the screen. + grid.Size = ScrollContent.Size + new Vector2(GRID_SPACING * 2); + } + + private Vector2 lastMatchesContainerMouseDownPosition; + + protected override bool OnMouseDown(MouseDownEvent e) + { + lastMatchesContainerMouseDownPosition = MatchesContainer.ToLocalSpace(e.ScreenSpaceMouseDownPosition); + return base.OnMouseDown(e); + } + private void updateMessage() { rightClickMessage.Alpha = LadderInfo.Matches.Count > 0 ? 0 : 1; @@ -58,28 +90,28 @@ namespace osu.Game.Tournament.Screens.Editors ScrollContent.Add(new JoinVisualiser(MatchesContainer, match, losers, UpdateLayout)); } - public MenuItem[] ContextMenuItems - { - get + public MenuItem[] ContextMenuItems => + new MenuItem[] { - if (editorInfo == null) - return Array.Empty(); - - return new MenuItem[] + new OsuMenuItem("Create new match", MenuItemType.Highlighted, () => { - new OsuMenuItem("Create new match", MenuItemType.Highlighted, () => - { - var pos = MatchesContainer.ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position); - LadderInfo.Matches.Add(new TournamentMatch { Position = { Value = new Point((int)pos.X, (int)pos.Y) } }); - }), - new OsuMenuItem("Reset teams", MenuItemType.Destructive, () => + Vector2 pos = MatchesContainer.Count == 0 ? Vector2.Zero : lastMatchesContainerMouseDownPosition; + + TournamentMatch newMatch = new TournamentMatch { Position = { Value = new Point((int)pos.X, (int)pos.Y) } }; + + LadderInfo.Matches.Add(newMatch); + + editorInfo.Selected.Value = newMatch; + }), + new OsuMenuItem("Reset teams", MenuItemType.Destructive, () => + { + dialogOverlay?.Push(new LadderResetTeamsDialog(() => { foreach (var p in MatchesContainer) p.Match.Reset(); - }) - }; - } - } + })); + }) + }; public void Remove(TournamentMatch match) { @@ -91,11 +123,11 @@ namespace osu.Game.Tournament.Screens.Editors private readonly Container matchesContainer; public readonly TournamentMatch Source; private readonly bool losers; - private readonly Action complete; + private readonly Action? complete; - private ProgressionPath path; + private ProgressionPath? path; - public JoinVisualiser(Container matchesContainer, TournamentMatch source, bool losers, Action complete) + public JoinVisualiser(Container matchesContainer, TournamentMatch source, bool losers, Action? complete) { this.matchesContainer = matchesContainer; RelativeSizeAxes = Axes.Both; @@ -109,7 +141,7 @@ namespace osu.Game.Tournament.Screens.Editors Source.Progression.Value = null; } - private DrawableTournamentMatch findTarget(InputState state) + private DrawableTournamentMatch? findTarget(InputState state) { return matchesContainer.FirstOrDefault(d => d.ReceivePositionalInputAt(state.Mouse.Position)); } diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs index 75131c282d..f887c41749 100644 --- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Bindables; @@ -13,9 +11,11 @@ using osu.Game.Graphics; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; using osu.Game.Overlays.Settings; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; +using osu.Game.Tournament.Screens.Editors.Components; using osuTK; namespace osu.Game.Tournament.Screens.Editors @@ -29,7 +29,10 @@ namespace osu.Game.Tournament.Screens.Editors public TournamentRound Model { get; } [Resolved] - private LadderInfo ladderInfo { get; set; } + private LadderInfo ladderInfo { get; set; } = null!; + + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } public RoundRow(TournamentRound round) { @@ -101,11 +104,11 @@ namespace osu.Game.Tournament.Screens.Editors RelativeSizeAxes = Axes.None, Width = 150, Text = "Delete Round", - Action = () => + Action = () => dialogOverlay?.Push(new DeleteRoundDialog(Model, () => { Expire(); ladderInfo.Rounds.Remove(Model); - }, + })) } }; @@ -136,9 +139,11 @@ namespace osu.Game.Tournament.Screens.Editors public void CreateNew() { - var user = new RoundBeatmap(); - round.Beatmaps.Add(user); - flow.Add(new RoundBeatmapRow(round, user)); + var b = new RoundBeatmap(); + + round.Beatmaps.Add(b); + + flow.Add(new RoundBeatmapRow(round, b)); } public partial class RoundBeatmapRow : CompositeDrawable @@ -146,7 +151,7 @@ namespace osu.Game.Tournament.Screens.Editors public RoundBeatmap Model { get; } [Resolved] - protected IAPIProvider API { get; private set; } + protected IAPIProvider API { get; private set; } = null!; private readonly Bindable beatmapId = new Bindable(); diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index a4358b4396..9927dd56a0 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Bindables; @@ -140,7 +138,7 @@ namespace osu.Game.Tournament.Screens.Editors public SeedingBeatmap Model { get; } [Resolved] - protected IAPIProvider API { get; private set; } + protected IAPIProvider API { get; private set; } = null!; private readonly Bindable beatmapId = new Bindable(); diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index c9d897ca11..250d5acaae 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; @@ -12,11 +10,14 @@ using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics; -using osu.Game.Online.API; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; using osu.Game.Overlays.Settings; -using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; +using osu.Game.Tournament.Screens.Editors.Components; +using osu.Game.Tournament.Screens.Drawings.Components; using osu.Game.Users; using osuTK; @@ -61,13 +62,14 @@ namespace osu.Game.Tournament.Screens.Editors { public TournamentTeam Model { get; } - private readonly Container drawableContainer; - - [Resolved(canBeNull: true)] - private TournamentSceneManager sceneManager { get; set; } + [Resolved] + private TournamentSceneManager? sceneManager { get; set; } [Resolved] - private LadderInfo ladderInfo { get; set; } + private IDialogOverlay? dialogOverlay { get; set; } + + [Resolved] + private LadderInfo ladderInfo { get; set; } = null!; public TeamRow(TournamentTeam team, TournamentScreen parent) { @@ -76,10 +78,10 @@ namespace osu.Game.Tournament.Screens.Editors Masking = true; CornerRadius = 10; - PlayerEditor playerEditor = new PlayerEditor(Model) - { - Width = 0.95f - }; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + PlayerEditor playerEditor = new PlayerEditor(Model); InternalChildren = new Drawable[] { @@ -88,17 +90,17 @@ namespace osu.Game.Tournament.Screens.Editors Colour = OsuColour.Gray(0.1f), RelativeSizeAxes = Axes.Both, }, - drawableContainer = new Container + new GroupTeam(team) { - Size = new Vector2(100, 50), - Margin = new MarginPadding(10), + Margin = new MarginPadding(16), + Scale = new Vector2(2), Anchor = Anchor.TopRight, Origin = Anchor.TopRight, }, new FillFlowContainer { - Margin = new MarginPadding(5), Spacing = new Vector2(5), + Padding = new MarginPadding(10), Direction = FillDirection.Full, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -128,32 +130,13 @@ namespace osu.Game.Tournament.Screens.Editors Width = 0.2f, Current = Model.Seed }, - new SettingsSlider + new SettingsSlider { LabelText = "Last Year Placement", Width = 0.33f, Current = Model.LastYearPlacing }, new SettingsButton - { - Width = 0.11f, - Margin = new MarginPadding(10), - Text = "Add player", - Action = () => playerEditor.CreateNew() - }, - new DangerousSettingsButton - { - Width = 0.11f, - Text = "Delete Team", - Margin = new MarginPadding(10), - Action = () => - { - Expire(); - ladderInfo.Teams.Remove(Model); - }, - }, - playerEditor, - new SettingsButton { Width = 0.2f, Margin = new MarginPadding(10), @@ -163,19 +146,40 @@ namespace osu.Game.Tournament.Screens.Editors sceneManager?.SetScreen(new SeedingEditorScreen(team, parent)); } }, + playerEditor, + new SettingsButton + { + Text = "Add player", + Action = () => playerEditor.CreateNew() + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new DangerousSettingsButton + { + Width = 0.2f, + Text = "Delete Team", + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Action = () => dialogOverlay?.Push(new DeleteTeamDialog(Model, () => + { + Expire(); + ladderInfo.Teams.Remove(Model); + })), + }, + } + }, } }, }; - - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - - Model.FlagName.BindValueChanged(updateDrawable, true); } - private void updateDrawable(ValueChangedEvent flag) + private partial class LastYearPlacementSlider : RoundedSliderBar { - drawableContainer.Child = new DrawableTeamFlag(Model); + public override LocalisableString TooltipText => Current.Value == 0 ? "N/A" : base.TooltipText; } public partial class PlayerEditor : CompositeDrawable @@ -195,6 +199,8 @@ namespace osu.Game.Tournament.Screens.Editors RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, + Padding = new MarginPadding(5), + Spacing = new Vector2(5), ChildrenEnumerable = team.Players.Select(p => new PlayerRow(team, p)) }; } @@ -211,26 +217,21 @@ namespace osu.Game.Tournament.Screens.Editors private readonly TournamentUser user; [Resolved] - protected IAPIProvider API { get; private set; } - - [Resolved] - private TournamentGameBase game { get; set; } + private TournamentGameBase game { get; set; } = null!; private readonly Bindable playerId = new Bindable(); - private readonly Container drawableContainer; + private readonly Container userPanelContainer; public PlayerRow(TournamentTeam team, TournamentUser user) { this.user = user; - Margin = new MarginPadding(10); - RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; Masking = true; - CornerRadius = 5; + CornerRadius = 10; InternalChildren = new Drawable[] { @@ -242,10 +243,11 @@ namespace osu.Game.Tournament.Screens.Editors new FillFlowContainer { Margin = new MarginPadding(5), - Padding = new MarginPadding { Right = 160 }, + Padding = new MarginPadding { Right = 60 }, Spacing = new Vector2(5), Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Children = new Drawable[] { new SettingsNumberBox @@ -255,9 +257,10 @@ namespace osu.Game.Tournament.Screens.Editors Width = 200, Current = playerId, }, - drawableContainer = new Container + userPanelContainer = new Container { - Size = new Vector2(100, 70), + Width = 400, + RelativeSizeAxes = Axes.Y, }, } }, @@ -300,7 +303,12 @@ namespace osu.Game.Tournament.Screens.Editors private void updatePanel() => Scheduler.AddOnce(() => { - drawableContainer.Child = new UserGridPanel(user.ToAPIUser()) { Width = 300 }; + userPanelContainer.Child = new UserListPanel(user.ToAPIUser()) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Scale = new Vector2(1f), + }; }); } } diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs index 0fb6c1367b..aa3718150c 100644 --- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.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.Collections.Specialized; using System.Diagnostics; using System.Linq; @@ -15,8 +13,9 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Overlays.Settings; +using osu.Game.Overlays; using osu.Game.Tournament.Components; +using osu.Game.Tournament.Screens.Editors.Components; using osuTK; namespace osu.Game.Tournament.Screens.Editors @@ -27,23 +26,27 @@ namespace osu.Game.Tournament.Screens.Editors { protected abstract BindableList Storage { get; } - private FillFlowContainer flow; + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } - [Resolved(canBeNull: true)] - private TournamentSceneManager sceneManager { get; set; } + private FillFlowContainer flow = null!; - protected ControlPanel ControlPanel; + [Resolved] + private TournamentSceneManager? sceneManager { get; set; } - private readonly TournamentScreen parentScreen; - private BackButton backButton; + protected ControlPanel ControlPanel = null!; - protected TournamentEditorScreen(TournamentScreen parentScreen = null) + private readonly TournamentScreen? parentScreen; + + private BackButton backButton = null!; + + protected TournamentEditorScreen(TournamentScreen? parentScreen = null) { this.parentScreen = parentScreen; } [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours) { AddRangeInternal(new Drawable[] { @@ -63,6 +66,7 @@ namespace osu.Game.Tournament.Screens.Editors RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(20), + Padding = new MarginPadding(20), }, }, ControlPanel = new ControlPanel @@ -75,11 +79,15 @@ namespace osu.Game.Tournament.Screens.Editors Text = "Add new", Action = () => Storage.Add(new TModel()) }, - new DangerousSettingsButton + new TourneyButton { RelativeSizeAxes = Axes.X, + BackgroundColour = colours.DangerousButtonColour, Text = "Clear all", - Action = Storage.Clear + Action = () => + { + dialogOverlay?.Push(new TournamentClearAllDialog(() => Storage.Clear())); + } }, } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index 8f7484980d..69f150c8ac 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -14,9 +12,9 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { public partial class MatchHeader : Container { - private TeamScoreDisplay teamDisplay1; - private TeamScoreDisplay teamDisplay2; - private DrawableTournamentHeaderLogo logo; + private TeamScoreDisplay teamDisplay1 = null!; + private TeamScoreDisplay teamDisplay2 = null!; + private DrawableTournamentHeaderLogo logo = null!; private bool showScores = true; diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs index d2b61220f0..bd23317e1f 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Tournament.Components; @@ -12,7 +10,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { public partial class MatchRoundDisplay : TournamentSpriteTextWithBackground { - private readonly Bindable currentMatch = new Bindable(); + private readonly Bindable currentMatch = new Bindable(); [BackgroundDependencyLoader] private void load(LadderInfo ladder) @@ -21,7 +19,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components currentMatch.BindTo(ladder.CurrentMatch); } - private void matchChanged(ValueChangedEvent match) => + private void matchChanged(ValueChangedEvent match) => Text.Text = match.NewValue?.Round.Value?.Name.Value ?? "Unknown Round"; } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index 60d1678326..3eec67c639 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -16,7 +14,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { private readonly TeamScore score; - private readonly TournamentSpriteTextWithBackground teamText; + private readonly TournamentSpriteTextWithBackground teamNameText; private readonly Bindable teamName = new Bindable("???"); @@ -37,7 +35,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components } } - public TeamDisplay(TournamentTeam team, TeamColour colour, Bindable currentTeamScore, int pointsToWin) + public TeamDisplay(TournamentTeam? team, TeamColour colour, Bindable currentTeamScore, int pointsToWin) : base(team) { AutoSizeAxes = Axes.Both; @@ -97,7 +95,13 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components } } }, - teamText = new TournamentSpriteTextWithBackground + teamNameText = new TournamentSpriteTextWithBackground + { + Scale = new Vector2(0.5f), + Origin = anchor, + Anchor = anchor, + }, + new DrawableTeamSeed(Team) { Scale = new Vector2(0.5f), Origin = anchor, @@ -121,7 +125,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components if (Team != null) teamName.BindTo(Team.FullName); - teamName.BindValueChanged(name => teamText.Text.Text = name.NewValue, true); + teamName.BindValueChanged(name => teamNameText.Text.Text = name.NewValue, true); } private void updateDisplay() diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs index 8b3786fa1f..c154e4fcef 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs index 57fe1c7312..c7fcfae602 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -17,16 +15,22 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { private readonly TeamColour teamColour; - private readonly Bindable currentMatch = new Bindable(); - private readonly Bindable currentTeam = new Bindable(); + private readonly Bindable currentMatch = new Bindable(); + private readonly Bindable currentTeam = new Bindable(); private readonly Bindable currentTeamScore = new Bindable(); - private TeamDisplay teamDisplay; + private TeamDisplay? teamDisplay; public bool ShowScore { - get => teamDisplay.ShowScore; - set => teamDisplay.ShowScore = value; + get => teamDisplay?.ShowScore ?? false; + set + { + if (teamDisplay != null) + { + teamDisplay.ShowScore = value; + } + } } public TeamScoreDisplay(TeamColour teamColour) @@ -48,7 +52,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components updateMatch(); } - private void matchChanged(ValueChangedEvent match) + private void matchChanged(ValueChangedEvent match) { currentTeamScore.UnbindBindings(); currentTeam.UnbindBindings(); @@ -78,7 +82,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components switch (e.Button) { case MouseButton.Left: - if (currentTeamScore.Value < currentMatch.Value.PointsToWin) + if (currentTeamScore.Value < currentMatch.Value?.PointsToWin) currentTeamScore.Value++; return true; @@ -91,7 +95,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components return base.OnMouseDown(e); } - private void teamChanged(ValueChangedEvent team) + private void teamChanged(ValueChangedEvent team) { bool wasShowingScores = teamDisplay?.ShowScore ?? false; diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs index bd1f3a2dd0..f8de34a511 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs @@ -1,158 +1,19 @@ // 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 osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Play.HUD; using osu.Game.Tournament.IPC; -using osuTK; namespace osu.Game.Tournament.Screens.Gameplay.Components { - // TODO: Update to derive from osu-side class? - public partial class TournamentMatchScoreDisplay : CompositeDrawable + public partial class TournamentMatchScoreDisplay : MatchScoreDisplay { - private const float bar_height = 18; - - private readonly BindableInt score1 = new BindableInt(); - private readonly BindableInt score2 = new BindableInt(); - - private readonly MatchScoreCounter score1Text; - private readonly MatchScoreCounter score2Text; - - private readonly Drawable score1Bar; - private readonly Drawable score2Bar; - - public TournamentMatchScoreDisplay() - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - - InternalChildren = new[] - { - new Box - { - Name = "top bar red (static)", - RelativeSizeAxes = Axes.X, - Height = bar_height / 4, - Width = 0.5f, - Colour = TournamentGame.COLOUR_RED, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopRight - }, - new Box - { - Name = "top bar blue (static)", - RelativeSizeAxes = Axes.X, - Height = bar_height / 4, - Width = 0.5f, - Colour = TournamentGame.COLOUR_BLUE, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopLeft - }, - score1Bar = new Box - { - Name = "top bar red", - RelativeSizeAxes = Axes.X, - Height = bar_height, - Width = 0, - Colour = TournamentGame.COLOUR_RED, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopRight - }, - score1Text = new MatchScoreCounter - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - }, - score2Bar = new Box - { - Name = "top bar blue", - RelativeSizeAxes = Axes.X, - Height = bar_height, - Width = 0, - Colour = TournamentGame.COLOUR_BLUE, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopLeft - }, - score2Text = new MatchScoreCounter - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - }, - }; - } - [BackgroundDependencyLoader] private void load(MatchIPCInfo ipc) { - score1.BindValueChanged(_ => updateScores()); - score1.BindTo(ipc.Score1); - - score2.BindValueChanged(_ => updateScores()); - score2.BindTo(ipc.Score2); - } - - private void updateScores() - { - score1Text.Current.Value = score1.Value; - score2Text.Current.Value = score2.Value; - - var winningText = score1.Value > score2.Value ? score1Text : score2Text; - var losingText = score1.Value <= score2.Value ? score1Text : score2Text; - - winningText.Winning = true; - losingText.Winning = false; - - var winningBar = score1.Value > score2.Value ? score1Bar : score2Bar; - var losingBar = score1.Value <= score2.Value ? score1Bar : score2Bar; - - int diff = Math.Max(score1.Value, score2.Value) - Math.Min(score1.Value, score2.Value); - - losingBar.ResizeWidthTo(0, 400, Easing.OutQuint); - winningBar.ResizeWidthTo(Math.Min(0.4f, MathF.Pow(diff / 1500000f, 0.5f) / 2), 400, Easing.OutQuint); - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - score1Text.X = -Math.Max(5 + score1Text.DrawWidth / 2, score1Bar.DrawWidth); - score2Text.X = Math.Max(5 + score2Text.DrawWidth / 2, score2Bar.DrawWidth); - } - - private partial class MatchScoreCounter : CommaSeparatedScoreCounter - { - private OsuSpriteText displayedSpriteText; - - public MatchScoreCounter() - { - Margin = new MarginPadding { Top = bar_height, Horizontal = 10 }; - } - - public bool Winning - { - set => updateFont(value); - } - - protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => - { - displayedSpriteText = s; - displayedSpriteText.Spacing = new Vector2(-6); - updateFont(false); - }); - - private void updateFont(bool winning) - => displayedSpriteText.Font = winning - ? OsuFont.Torus.With(weight: FontWeight.Bold, size: 50, fixedWidth: true) - : OsuFont.Torus.With(weight: FontWeight.Regular, size: 40, fixedWidth: true); + Team1Score.BindTo(ipc.Score1); + Team2Score.BindTo(ipc.Score2); } } } diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index f2a2e97bcc..b2152eaf3d 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -26,19 +24,19 @@ namespace osu.Game.Tournament.Screens.Gameplay private readonly BindableBool warmup = new BindableBool(); public readonly Bindable State = new Bindable(); - private OsuButton warmupButton; - private MatchIPCInfo ipc; - - [Resolved(canBeNull: true)] - private TournamentSceneManager sceneManager { get; set; } + private OsuButton warmupButton = null!; + private MatchIPCInfo ipc = null!; [Resolved] - private TournamentMatchChatDisplay chat { get; set; } + private TournamentSceneManager? sceneManager { get; set; } - private Drawable chroma; + [Resolved] + private TournamentMatchChatDisplay chat { get; set; } = null!; + + private Drawable chroma = null!; [BackgroundDependencyLoader] - private void load(LadderInfo ladder, MatchIPCInfo ipc) + private void load(MatchIPCInfo ipc) { this.ipc = ipc; @@ -51,7 +49,7 @@ namespace osu.Game.Tournament.Screens.Gameplay }, header = new MatchHeader { - ShowLogo = false + ShowLogo = false, }, new Container { @@ -120,12 +118,12 @@ namespace osu.Game.Tournament.Screens.Gameplay LabelText = "Players per team", Current = LadderInfo.PlayersPerTeam, KeyboardStep = 1, - } + }, } } }); - ladder.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true); + LadderInfo.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true); warmup.BindValueChanged(w => { @@ -139,10 +137,10 @@ namespace osu.Game.Tournament.Screens.Gameplay base.LoadComplete(); State.BindTo(ipc.State); - State.BindValueChanged(stateChanged, true); + State.BindValueChanged(_ => updateState(), true); } - protected override void CurrentMatchChanged(ValueChangedEvent match) + protected override void CurrentMatchChanged(ValueChangedEvent match) { base.CurrentMatchChanged(match); @@ -150,20 +148,53 @@ namespace osu.Game.Tournament.Screens.Gameplay return; warmup.Value = match.NewValue.Team1Score.Value + match.NewValue.Team2Score.Value == 0; - scheduledOperation?.Cancel(); + scheduledScreenChange?.Cancel(); } - private ScheduledDelegate scheduledOperation; - private TournamentMatchScoreDisplay scoreDisplay; + private ScheduledDelegate? scheduledScreenChange; + private ScheduledDelegate? scheduledContract; + + private TournamentMatchScoreDisplay scoreDisplay = null!; private TourneyState lastState; - private MatchHeader header; + private MatchHeader header = null!; - private void stateChanged(ValueChangedEvent state) + private void contract() + { + if (!IsLoaded) + return; + + scheduledContract?.Cancel(); + + SongBar.Expanded = false; + scoreDisplay.FadeOut(100); + using (chat.BeginDelayedSequence(500)) + chat.Expand(); + } + + private void expand() + { + if (!IsLoaded) + return; + + scheduledContract?.Cancel(); + + chat.Contract(); + + using (BeginDelayedSequence(300)) + { + scoreDisplay.FadeIn(100); + SongBar.Expanded = true; + } + } + + private void updateState() { try { - if (state.NewValue == TourneyState.Ranking) + scheduledScreenChange?.Cancel(); + + if (State.Value == TourneyState.Ranking) { if (warmup.Value || CurrentMatch.Value == null) return; @@ -173,28 +204,7 @@ namespace osu.Game.Tournament.Screens.Gameplay CurrentMatch.Value.Team2Score.Value++; } - scheduledOperation?.Cancel(); - - void expand() - { - chat?.Contract(); - - using (BeginDelayedSequence(300)) - { - scoreDisplay.FadeIn(100); - SongBar.Expanded = true; - } - } - - void contract() - { - SongBar.Expanded = false; - scoreDisplay.FadeOut(100); - using (chat?.BeginDelayedSequence(500)) - chat?.Expand(); - } - - switch (state.NewValue) + switch (State.Value) { case TourneyState.Idle: contract(); @@ -208,34 +218,45 @@ namespace osu.Game.Tournament.Screens.Gameplay if (lastState == TourneyState.Ranking && !warmup.Value) { if (CurrentMatch.Value?.Completed.Value == true) - scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(TeamWinScreen)); }, delay_before_progression); + scheduledScreenChange = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(TeamWinScreen)); }, delay_before_progression); else if (CurrentMatch.Value?.Completed.Value == false) - scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(MapPoolScreen)); }, delay_before_progression); + scheduledScreenChange = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(MapPoolScreen)); }, delay_before_progression); } } break; case TourneyState.Ranking: - scheduledOperation = Scheduler.AddDelayed(contract, 10000); + scheduledContract = Scheduler.AddDelayed(contract, 10000); break; default: - chat.Contract(); expand(); break; } } finally { - lastState = state.NewValue; + lastState = State.Value; } } + public override void Hide() + { + scheduledScreenChange?.Cancel(); + base.Hide(); + } + + public override void Show() + { + updateState(); + base.Show(); + } + private partial class ChromaArea : CompositeDrawable { [Resolved] - private LadderInfo ladder { get; set; } + private LadderInfo ladder { get; set; } = null!; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs index 2b66df1a31..637591c6f6 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.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 osu.Framework.Allocation; using osu.Framework.Bindables; @@ -28,20 +26,20 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { private readonly TournamentMatch match; private readonly bool losers; - private TournamentSpriteText scoreText; - private Box background; - private Box backgroundRight; + private TournamentSpriteText scoreText = null!; + private Box background = null!; + private Box backgroundRight = null!; private readonly Bindable score = new Bindable(); private readonly BindableBool completed = new BindableBool(); private Color4 colourWinner; - private readonly Func isWinner; - private LadderEditorScreen ladderEditor; + private readonly Func? isWinner; + private LadderEditorScreen ladderEditor = null!; - [Resolved(canBeNull: true)] - private LadderInfo ladderInfo { get; set; } + [Resolved] + private LadderInfo? ladderInfo { get; set; } private void setCurrent() { @@ -55,10 +53,10 @@ namespace osu.Game.Tournament.Screens.Ladder.Components ladderInfo.CurrentMatch.Value.Current.Value = true; } - [Resolved(CanBeNull = true)] - private LadderEditorInfo editorInfo { get; set; } + [Resolved] + private LadderEditorInfo? editorInfo { get; set; } - public DrawableMatchTeam(TournamentTeam team, TournamentMatch match, bool losers) + public DrawableMatchTeam(TournamentTeam? team, TournamentMatch match, bool losers) : base(team) { this.match = match; @@ -72,14 +70,11 @@ namespace osu.Game.Tournament.Screens.Ladder.Components AcronymText.Padding = new MarginPadding { Left = 50 }; AcronymText.Font = OsuFont.Torus.With(size: 22, weight: FontWeight.Bold); - if (match != null) - { - isWinner = () => match.Winner == Team; + isWinner = () => match.Winner == Team; - completed.BindTo(match.Completed); - if (team != null) - score.BindTo(team == match.Team1.Value ? match.Team1Score : match.Team2Score); - } + completed.BindTo(match.Completed); + if (team != null) + score.BindTo(team == match.Team1.Value ? match.Team1Score : match.Team2Score); } [BackgroundDependencyLoader(true)] diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs index 33e383482f..4de47d7c7f 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs @@ -1,10 +1,9 @@ // 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.Collections.Generic; +using System.Diagnostics; using System.Drawing; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -13,6 +12,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Tournament.Models; +using osu.Game.Tournament.Screens.Editors; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -25,14 +25,14 @@ namespace osu.Game.Tournament.Screens.Ladder.Components private readonly bool editor; protected readonly FillFlowContainer Flow; private readonly Drawable selectionBox; - protected readonly Drawable CurrentMatchSelectionBox; - private Bindable globalSelection; + private readonly Drawable currentMatchSelectionBox; + private Bindable? globalSelection; - [Resolved(CanBeNull = true)] - private LadderEditorInfo editorInfo { get; set; } + [Resolved] + private LadderEditorInfo? editorInfo { get; set; } - [Resolved(CanBeNull = true)] - private LadderInfo ladderInfo { get; set; } + [Resolved] + private LadderInfo? ladderInfo { get; set; } public DrawableTournamentMatch(TournamentMatch match, bool editor = false) { @@ -41,35 +41,60 @@ namespace osu.Game.Tournament.Screens.Ladder.Components AutoSizeAxes = Axes.Both; - Margin = new MarginPadding(5); + const float border_thickness = 5; + const float spacing = 2; - InternalChildren = new[] + Margin = new MarginPadding(10); + + InternalChildren = new Drawable[] { - selectionBox = new Container - { - Scale = new Vector2(1.1f), - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Alpha = 0, - Colour = Color4.YellowGreen, - Child = new Box { RelativeSizeAxes = Axes.Both } - }, - CurrentMatchSelectionBox = new Container - { - Scale = new Vector2(1.05f, 1.1f), - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Alpha = 0, - Colour = Color4.White, - Child = new Box { RelativeSizeAxes = Axes.Both } - }, Flow = new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Spacing = new Vector2(2) + Spacing = new Vector2(spacing) + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(-10), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = selectionBox = new Container + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Masking = true, + BorderColour = Color4.YellowGreen, + BorderThickness = border_thickness, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, + Alpha = 0, + } + }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(-(spacing + border_thickness)), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = currentMatchSelectionBox = new Container + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + BorderColour = Color4.White, + BorderThickness = border_thickness, + Masking = true, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, + Alpha = 0, + } + }, } }; @@ -97,14 +122,13 @@ namespace osu.Game.Tournament.Screens.Ladder.Components Position = new Vector2(pos.NewValue.X, pos.NewValue.Y); Changed?.Invoke(); }, true); - updateTeams(); } /// - /// Fired when somethign changed that requires a ladder redraw. + /// Fired when something changed that requires a ladder redraw. /// - public Action Changed; + public Action? Changed; private readonly List refBindables = new List(); @@ -126,9 +150,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components private void updateCurrentMatch() { if (Match.Current.Value) - CurrentMatchSelectionBox.Show(); + currentMatchSelectionBox.Show(); else - CurrentMatchSelectionBox.Hide(); + currentMatchSelectionBox.Hide(); } private bool selected; @@ -176,20 +200,22 @@ namespace osu.Game.Tournament.Screens.Ladder.Components } else { - transferProgression(Match.Progression?.Value, Match.Winner); - transferProgression(Match.LosersProgression?.Value, Match.Loser); + Debug.Assert(Match.Winner != null); + transferProgression(Match.Progression.Value, Match.Winner); + Debug.Assert(Match.Loser != null); + transferProgression(Match.LosersProgression.Value, Match.Loser); } Changed?.Invoke(); } - private void transferProgression(TournamentMatch destination, TournamentTeam team) + private void transferProgression(TournamentMatch? destination, TournamentTeam team) { if (destination == null) return; bool progressionAbove = destination.ID < Match.ID; - Bindable destinationTeam; + Bindable destinationTeam; // check for the case where we have already transferred out value if (destination.Team1.Value == team) @@ -213,7 +239,8 @@ namespace osu.Game.Tournament.Screens.Ladder.Components int instantWinAmount = Match.Round.Value.BestOf.Value / 2; Match.Completed.Value = Match.Round.Value.BestOf.Value > 0 - && (Match.Team1Score.Value + Match.Team2Score.Value >= Match.Round.Value.BestOf.Value || Match.Team1Score.Value > instantWinAmount || Match.Team2Score.Value > instantWinAmount); + && (Match.Team1Score.Value + Match.Team2Score.Value >= Match.Round.Value.BestOf.Value || Match.Team1Score.Value > instantWinAmount + || Match.Team2Score.Value > instantWinAmount); } protected override void LoadComplete() @@ -224,10 +251,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components if (editorInfo != null) { globalSelection = editorInfo.Selected.GetBoundCopy(); - globalSelection.BindValueChanged(s => - { - if (s.NewValue != Match) Selected = false; - }); + globalSelection.BindValueChanged(s => Selected = s.NewValue == Match, true); } } @@ -245,8 +269,8 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { foreach (var conditional in Match.ConditionalMatches) { - bool team1Match = conditional.Acronyms.Contains(Match.Team1Acronym); - bool team2Match = conditional.Acronyms.Contains(Match.Team2Acronym); + bool team1Match = Match.Team1Acronym != null && conditional.Acronyms.Contains(Match.Team1Acronym); + bool team2Match = Match.Team2Acronym != null && conditional.Acronyms.Contains(Match.Team2Acronym); if (team1Match && team2Match) Match.Date.Value = conditional.Date.Value; @@ -265,8 +289,6 @@ namespace osu.Game.Tournament.Screens.Ladder.Components protected override bool OnMouseDown(MouseDownEvent e) => e.Button == MouseButton.Left && editorInfo != null; - protected override bool OnDragStart(DragStartEvent e) => editorInfo != null; - protected override bool OnKeyDown(KeyDownEvent e) { if (Selected && editorInfo != null && e.Key == Key.Delete) @@ -287,23 +309,45 @@ namespace osu.Game.Tournament.Screens.Ladder.Components return true; } + private Vector2 positionAtStartOfDrag; + + protected override bool OnDragStart(DragStartEvent e) + { + if (editorInfo != null) + { + positionAtStartOfDrag = Position; + return true; + } + + return false; + } + protected override void OnDrag(DragEvent e) { base.OnDrag(e); Selected = true; - this.MoveToOffset(e.Delta); - var pos = Position; - Match.Position.Value = new Point((int)pos.X, (int)pos.Y); + this.MoveTo(snapToGrid(positionAtStartOfDrag + (e.MousePosition - e.MouseDownPosition))); + + Match.Position.Value = new Point((int)Position.X, (int)Position.Y); } + private Vector2 snapToGrid(Vector2 pos) => + new Vector2( + (int)(pos.X / LadderEditorScreen.GRID_SPACING) * LadderEditorScreen.GRID_SPACING, + (int)(pos.Y / LadderEditorScreen.GRID_SPACING) * LadderEditorScreen.GRID_SPACING + ); + public void Remove() { Selected = false; Match.Progression.Value = null; Match.LosersProgression.Value = null; + if (ladderInfo == null) + return; + ladderInfo.Matches.Remove(Match); foreach (var m in ladderInfo.Matches) diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs index 4b2a29247b..216e0a5a3e 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.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 JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -52,7 +50,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components name.BindValueChanged(_ => textName.Text = ((losers ? "Losers " : "") + round.Name).ToUpperInvariant(), true); description = round.Description.GetBoundCopy(); - description.BindValueChanged(_ => textDescription.Text = round.Description.Value?.ToUpperInvariant(), true); + description.BindValueChanged(_ => textDescription.Text = round.Description.Value?.ToUpperInvariant() ?? string.Empty, true); } } } diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs index 5aea551c00..9f0fa19915 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.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.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; @@ -11,43 +9,50 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Overlays.Settings; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; +using osuTK; namespace osu.Game.Tournament.Screens.Ladder.Components { - public partial class LadderEditorSettings : PlayerSettingsGroup + public partial class LadderEditorSettings : CompositeDrawable { - private SettingsDropdown roundDropdown; - private PlayerCheckbox losersCheckbox; - private DateTextBox dateTimeBox; - private SettingsTeamDropdown team1Dropdown; - private SettingsTeamDropdown team2Dropdown; + private SettingsDropdown roundDropdown = null!; + private PlayerCheckbox losersCheckbox = null!; + private DateTextBox dateTimeBox = null!; + private SettingsTeamDropdown team1Dropdown = null!; + private SettingsTeamDropdown team2Dropdown = null!; [Resolved] - private LadderEditorInfo editorInfo { get; set; } + private LadderEditorInfo editorInfo { get; set; } = null!; [Resolved] - private LadderInfo ladderInfo { get; set; } - - public LadderEditorSettings() - : base("ladder") - { - } + private LadderInfo ladderInfo { get; set; } = null!; [BackgroundDependencyLoader] private void load() { - Children = new Drawable[] + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new FillFlowContainer { - team1Dropdown = new SettingsTeamDropdown(ladderInfo.Teams) { LabelText = "Team 1" }, - team2Dropdown = new SettingsTeamDropdown(ladderInfo.Teams) { LabelText = "Team 2" }, - roundDropdown = new SettingsRoundDropdown(ladderInfo.Rounds) { LabelText = "Round" }, - losersCheckbox = new PlayerCheckbox { LabelText = "Losers Bracket" }, - dateTimeBox = new DateTextBox { LabelText = "Match Time" }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Children = new Drawable[] + { + team1Dropdown = new SettingsTeamDropdown(ladderInfo.Teams) { LabelText = "Team 1" }, + team2Dropdown = new SettingsTeamDropdown(ladderInfo.Teams) { LabelText = "Team 2" }, + roundDropdown = new SettingsRoundDropdown(ladderInfo.Rounds) { LabelText = "Round" }, + losersCheckbox = new PlayerCheckbox { LabelText = "Losers Bracket" }, + dateTimeBox = new DateTextBox { LabelText = "Match Time" }, + }, }; editorInfo.Selected.ValueChanged += selection => @@ -55,22 +60,28 @@ namespace osu.Game.Tournament.Screens.Ladder.Components // ensure any ongoing edits are committed out to the *current* selection before changing to a new one. GetContainingInputManager().TriggerFocusContention(null); - roundDropdown.Current = selection.NewValue?.Round; - losersCheckbox.Current = selection.NewValue?.Losers; - dateTimeBox.Current = selection.NewValue?.Date; + // Required to avoid cyclic failure in BindableWithCurrent (TriggerChange called during the Current_Set process). + // Arguable a framework issue but since we haven't hit it anywhere else a local workaround seems best. + roundDropdown.Current.ValueChanged -= roundDropdownChanged; - team1Dropdown.Current = selection.NewValue?.Team1; - team2Dropdown.Current = selection.NewValue?.Team2; + roundDropdown.Current = selection.NewValue.Round; + losersCheckbox.Current = selection.NewValue.Losers; + dateTimeBox.Current = selection.NewValue.Date; + + team1Dropdown.Current = selection.NewValue.Team1; + team2Dropdown.Current = selection.NewValue.Team2; + + roundDropdown.Current.ValueChanged += roundDropdownChanged; }; + } - roundDropdown.Current.ValueChanged += round => + private void roundDropdownChanged(ValueChangedEvent round) + { + if (editorInfo.Selected.Value?.Date.Value < round.NewValue?.StartDate.Value) { - if (editorInfo.Selected.Value?.Date.Value < round.NewValue?.StartDate.Value) - { - editorInfo.Selected.Value.Date.Value = round.NewValue.StartDate.Value; - editorInfo.Selected.TriggerChange(); - } - }; + editorInfo.Selected.Value.Date.Value = round.NewValue.StartDate.Value; + editorInfo.Selected.TriggerChange(); + } } protected override void LoadComplete() @@ -88,11 +99,11 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { } - private partial class SettingsRoundDropdown : SettingsDropdown + private partial class SettingsRoundDropdown : SettingsDropdown { public SettingsRoundDropdown(BindableList rounds) { - Current = new Bindable(); + Current = new Bindable(); foreach (var r in rounds.Prepend(new TournamentRound())) add(r); diff --git a/osu.Game.Tournament/Screens/Ladder/Components/ProgressionPath.cs b/osu.Game.Tournament/Screens/Ladder/Components/ProgressionPath.cs index c79dbc26be..90efac30fd 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/ProgressionPath.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/ProgressionPath.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Graphics; using osu.Framework.Graphics.Lines; @@ -62,8 +60,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components var topLeft = new Vector2(minX, minY); - Position = Parent.ToLocalSpace(topLeft); - Vertices = points.Select(p => Parent.ToLocalSpace(p) - Parent.ToLocalSpace(topLeft)).ToList(); + OriginPosition = new Vector2(PathRadius); + Position = Parent!.ToLocalSpace(topLeft); + Vertices = points.Select(p => Parent!.ToLocalSpace(p) - Parent!.ToLocalSpace(topLeft)).ToList(); } } } diff --git a/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs index f7a42e4f50..7e35190e2e 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Collections.Specialized; using System.Diagnostics; @@ -14,7 +12,7 @@ using osu.Game.Tournament.Models; namespace osu.Game.Tournament.Screens.Ladder.Components { - public partial class SettingsTeamDropdown : SettingsDropdown + public partial class SettingsTeamDropdown : SettingsDropdown { public SettingsTeamDropdown(BindableList teams) { diff --git a/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs b/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs index 10d58612f4..3a2db4fc71 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -38,8 +36,8 @@ namespace osu.Game.Tournament.Screens.Ladder { float newScale = Math.Clamp(scale + e.ScrollDelta.Y / 15 * scale, min_scale, max_scale); - this.MoveTo(target -= e.MousePosition * (newScale - scale), 2000, Easing.OutQuint); - this.ScaleTo(scale = newScale, 2000, Easing.OutQuint); + this.MoveTo(target -= e.MousePosition * (newScale - scale), 1000, Easing.OutQuint); + this.ScaleTo(scale = newScale, 1000, Easing.OutQuint); return true; } diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs index 176c06c0e5..4f56a2fcc9 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.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.Collections.Specialized; using System.Diagnostics; using System.Linq; @@ -22,13 +20,13 @@ namespace osu.Game.Tournament.Screens.Ladder { public partial class LadderScreen : TournamentScreen { - protected Container MatchesContainer; - private Container paths; - private Container headings; + protected Container MatchesContainer = null!; + private Container paths = null!; + private Container headings = null!; - protected LadderDragContainer ScrollContent; + protected LadderDragContainer ScrollContent = null!; - protected Container Content; + protected Container Content = null!; [BackgroundDependencyLoader] private void load() @@ -41,6 +39,7 @@ namespace osu.Game.Tournament.Screens.Ladder InternalChild = Content = new Container { RelativeSizeAxes = Axes.Both, + Masking = true, Children = new Drawable[] { new TourneyVideo("ladder") @@ -56,12 +55,15 @@ namespace osu.Game.Tournament.Screens.Ladder }, ScrollContent = new LadderDragContainer { - RelativeSizeAxes = Axes.Both, + AutoSizeAxes = Axes.Both, Children = new Drawable[] { paths = new Container { RelativeSizeAxes = Axes.Both }, headings = new Container { RelativeSizeAxes = Axes.Both }, - MatchesContainer = new Container { RelativeSizeAxes = Axes.Both }, + MatchesContainer = new Container + { + AutoSizeAxes = Axes.Both + }, } }, } diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index f0e34d78c3..f80f43bb77 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.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 osu.Framework.Allocation; using osu.Framework.Bindables; @@ -24,20 +22,23 @@ namespace osu.Game.Tournament.Screens.MapPool { public partial class MapPoolScreen : TournamentMatchScreen { - private readonly FillFlowContainer> mapFlows; + private FillFlowContainer> mapFlows = null!; - [Resolved(canBeNull: true)] - private TournamentSceneManager sceneManager { get; set; } + [Resolved] + private TournamentSceneManager? sceneManager { get; set; } private TeamColour pickColour; private ChoiceType pickType; - private readonly OsuButton buttonRedBan; - private readonly OsuButton buttonBlueBan; - private readonly OsuButton buttonRedPick; - private readonly OsuButton buttonBluePick; + private OsuButton buttonRedBan = null!; + private OsuButton buttonBlueBan = null!; + private OsuButton buttonRedPick = null!; + private OsuButton buttonBluePick = null!; - public MapPoolScreen() + private ScheduledDelegate? scheduledScreenChange; + + [BackgroundDependencyLoader] + private void load(MatchIPCInfo ipc) { InternalChildren = new Drawable[] { @@ -98,24 +99,35 @@ namespace osu.Game.Tournament.Screens.MapPool Action = reset }, new ControlPanel.Spacer(), + new OsuCheckbox + { + LabelText = "Split display by mods", + Current = LadderInfo.SplitMapPoolByMods, + }, }, } }; - } - [BackgroundDependencyLoader] - private void load(MatchIPCInfo ipc) - { ipc.Beatmap.BindValueChanged(beatmapChanged); } - private void beatmapChanged(ValueChangedEvent beatmap) + private Bindable? splitMapPoolByMods; + + protected override void LoadComplete() + { + base.LoadComplete(); + + splitMapPoolByMods = LadderInfo.SplitMapPoolByMods.GetBoundCopy(); + splitMapPoolByMods.BindValueChanged(_ => updateDisplay()); + } + + private void beatmapChanged(ValueChangedEvent beatmap) { if (CurrentMatch.Value == null || CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) < 2) return; - // if bans have already been placed, beatmap changes result in a selection being made autoamtically - if (beatmap.NewValue.OnlineID > 0) + // if bans have already been placed, beatmap changes result in a selection being made automatically + if (beatmap.NewValue?.OnlineID > 0) addForBeatmap(beatmap.NewValue.OnlineID); } @@ -134,6 +146,9 @@ namespace osu.Game.Tournament.Screens.MapPool private void setNextMode() { + if (CurrentMatch.Value == null) + return; + const TeamColour roll_winner = TeamColour.Red; //todo: draw from match var nextColour = (CurrentMatch.Value.PicksBans.LastOrDefault()?.Team ?? roll_winner) == TeamColour.Red ? TeamColour.Blue : TeamColour.Red; @@ -151,15 +166,15 @@ namespace osu.Game.Tournament.Screens.MapPool if (map != null) { - if (e.Button == MouseButton.Left && map.Beatmap.OnlineID > 0) + if (e.Button == MouseButton.Left && map.Beatmap?.OnlineID > 0) addForBeatmap(map.Beatmap.OnlineID); else { - var existing = CurrentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.Beatmap.OnlineID); + var existing = CurrentMatch.Value?.PicksBans.FirstOrDefault(p => p.BeatmapID == map.Beatmap?.OnlineID); if (existing != null) { - CurrentMatch.Value.PicksBans.Remove(existing); + CurrentMatch.Value?.PicksBans.Remove(existing); setNextMode(); } } @@ -172,18 +187,16 @@ namespace osu.Game.Tournament.Screens.MapPool private void reset() { - CurrentMatch.Value.PicksBans.Clear(); + CurrentMatch.Value?.PicksBans.Clear(); setNextMode(); } - private ScheduledDelegate scheduledChange; - private void addForBeatmap(int beatmapId) { - if (CurrentMatch.Value == null) + if (CurrentMatch.Value?.Round.Value == null) return; - if (CurrentMatch.Value.Round.Value.Beatmaps.All(b => b.Beatmap.OnlineID != beatmapId)) + if (CurrentMatch.Value.Round.Value.Beatmaps.All(b => b.Beatmap?.OnlineID != beatmapId)) // don't attempt to add if the beatmap isn't in our pool return; @@ -204,33 +217,42 @@ namespace osu.Game.Tournament.Screens.MapPool { if (pickType == ChoiceType.Pick && CurrentMatch.Value.PicksBans.Any(i => i.Type == ChoiceType.Pick)) { - scheduledChange?.Cancel(); - scheduledChange = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(GameplayScreen)); }, 10000); + scheduledScreenChange?.Cancel(); + scheduledScreenChange = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(GameplayScreen)); }, 10000); } } } - protected override void CurrentMatchChanged(ValueChangedEvent match) + public override void Hide() + { + scheduledScreenChange?.Cancel(); + base.Hide(); + } + + protected override void CurrentMatchChanged(ValueChangedEvent match) { base.CurrentMatchChanged(match); + updateDisplay(); + } + private void updateDisplay() + { mapFlows.Clear(); - if (match.NewValue == null) + if (CurrentMatch.Value == null) return; int totalRows = 0; - if (match.NewValue.Round.Value != null) + if (CurrentMatch.Value.Round.Value != null) { - FillFlowContainer currentFlow = null; - string currentMod = null; - + FillFlowContainer? currentFlow = null; + string? currentMods = null; int flowCount = 0; - foreach (var b in match.NewValue.Round.Value.Beatmaps) + foreach (var b in CurrentMatch.Value.Round.Value.Beatmaps) { - if (currentFlow == null || currentMod != b.Mods) + if (currentFlow == null || (LadderInfo.SplitMapPoolByMods.Value && currentMods != b.Mods)) { mapFlows.Add(currentFlow = new FillFlowContainer { @@ -240,7 +262,7 @@ namespace osu.Game.Tournament.Screens.MapPool AutoSizeAxes = Axes.Y }); - currentMod = b.Mods; + currentMods = b.Mods; totalRows++; flowCount = 0; diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 8d5547c749..d02559d6b7 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -1,9 +1,8 @@ // 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.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -21,9 +20,10 @@ namespace osu.Game.Tournament.Screens.Schedule { public partial class ScheduleScreen : TournamentScreen { - private readonly Bindable currentMatch = new Bindable(); - private Container mainContainer; - private LadderInfo ladder; + private readonly BindableList allMatches = new BindableList(); + private readonly Bindable currentMatch = new Bindable(); + private Container mainContainer = null!; + private LadderInfo ladder = null!; [BackgroundDependencyLoader] private void load(LadderInfo ladder) @@ -103,19 +103,34 @@ namespace osu.Game.Tournament.Screens.Schedule { base.LoadComplete(); + allMatches.BindTo(ladder.Matches); + allMatches.BindCollectionChanged((_, _) => refresh()); + currentMatch.BindTo(ladder.CurrentMatch); - currentMatch.BindValueChanged(matchChanged, true); + currentMatch.BindValueChanged(_ => refresh(), true); } - private void matchChanged(ValueChangedEvent match) + private void refresh() { - var upcoming = ladder.Matches.Where(p => !p.Completed.Value && p.Team1.Value != null && p.Team2.Value != null && Math.Abs(p.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < 4); - var conditionals = ladder - .Matches.Where(p => !p.Completed.Value && (p.Team1.Value == null || p.Team2.Value == null) && Math.Abs(p.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < 4) - .SelectMany(m => m.ConditionalMatches.Where(cp => m.Acronyms.TrueForAll(a => cp.Acronyms.Contains(a)))); + const int days_for_displays = 4; - upcoming = upcoming.Concat(conditionals); - upcoming = upcoming.OrderBy(p => p.Date.Value).Take(8); + IEnumerable conditionals = + allMatches + .Where(m => !m.Completed.Value && (m.Team1.Value == null || m.Team2.Value == null) && Math.Abs(m.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < days_for_displays) + .SelectMany(m => m.ConditionalMatches.Where(cp => m.Acronyms.TrueForAll(a => cp.Acronyms.Contains(a)))); + + IEnumerable upcoming = + allMatches + .Where(m => !m.Completed.Value && m.Team1.Value != null && m.Team2.Value != null && Math.Abs(m.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < days_for_displays) + .Concat(conditionals) + .OrderBy(m => m.Date.Value) + .Take(8); + + var recent = + allMatches + .Where(m => m.Completed.Value && m.Team1.Value != null && m.Team2.Value != null && Math.Abs(m.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < days_for_displays) + .OrderByDescending(m => m.Date.Value) + .Take(8); ScheduleContainer comingUpNext; @@ -139,12 +154,7 @@ namespace osu.Game.Tournament.Screens.Schedule { RelativeSizeAxes = Axes.Both, Width = 0.4f, - ChildrenEnumerable = ladder.Matches - .Where(p => p.Completed.Value && p.Team1.Value != null && p.Team2.Value != null - && Math.Abs(p.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < 4) - .OrderByDescending(p => p.Date.Value) - .Take(8) - .Select(p => new ScheduleMatch(p)) + ChildrenEnumerable = recent.Select(p => new ScheduleMatch(p)) }, new ScheduleContainer("upcoming matches") { @@ -163,7 +173,7 @@ namespace osu.Game.Tournament.Screens.Schedule } }; - if (match.NewValue != null) + if (currentMatch.Value != null) { comingUpNext.Child = new FillFlowContainer { @@ -172,12 +182,12 @@ namespace osu.Game.Tournament.Screens.Schedule Spacing = new Vector2(30), Children = new Drawable[] { - new ScheduleMatch(match.NewValue, false) + new ScheduleMatch(currentMatch.Value, false) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, - new TournamentSpriteTextWithBackground(match.NewValue.Round.Value?.Name.Value) + new TournamentSpriteTextWithBackground(currentMatch.Value.Round.Value?.Name.Value ?? string.Empty) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -187,7 +197,7 @@ namespace osu.Game.Tournament.Screens.Schedule { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = match.NewValue.Team1.Value?.FullName + " vs " + match.NewValue.Team2.Value?.FullName, + Text = currentMatch.Value.Team1.Value?.FullName + " vs " + currentMatch.Value.Team2.Value?.FullName, Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold) }, new FillFlowContainer @@ -198,7 +208,7 @@ namespace osu.Game.Tournament.Screens.Schedule Origin = Anchor.CentreLeft, Children = new Drawable[] { - new ScheduleMatchDate(match.NewValue.Date.Value) + new ScheduleMatchDate(currentMatch.Value.Date.Value) { Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular) } @@ -218,8 +228,6 @@ namespace osu.Game.Tournament.Screens.Schedule Scale = new Vector2(0.8f); - CurrentMatchSelectionBox.Scale = new Vector2(1.02f, 1.15f); - bool conditional = match is ConditionalTournamentMatch; if (conditional) @@ -286,6 +294,7 @@ namespace osu.Game.Tournament.Screens.Schedule { Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.Both, + Spacing = new Vector2(0, -6), Margin = new MarginPadding(10) }, } diff --git a/osu.Game.Tournament/Screens/Setup/ActionableInfo.cs b/osu.Game.Tournament/Screens/Setup/ActionableInfo.cs index e3fe2170ba..62f945e50d 100644 --- a/osu.Game.Tournament/Screens/Setup/ActionableInfo.cs +++ b/osu.Game.Tournament/Screens/Setup/ActionableInfo.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -15,7 +13,15 @@ namespace osu.Game.Tournament.Screens.Setup { internal partial class ActionableInfo : LabelledDrawable { - protected OsuButton Button; + public const float BUTTON_SIZE = 120; + + public Action? Action; + + protected FillFlowContainer FlowContainer = null!; + + protected OsuButton Button = null!; + + private TournamentSpriteText valueText = null!; public ActionableInfo() : base(true) @@ -37,11 +43,6 @@ namespace osu.Game.Tournament.Screens.Setup set => valueText.Colour = value ? Color4.Red : Color4.White; } - public Action Action; - - private TournamentSpriteText valueText; - protected FillFlowContainer FlowContainer; - protected override Drawable CreateComponent() => new Container { AutoSizeAxes = Axes.Y, @@ -63,7 +64,7 @@ namespace osu.Game.Tournament.Screens.Setup { Button = new RoundedButton { - Size = new Vector2(100, 40), + Size = new Vector2(BUTTON_SIZE, 40), Action = () => Action?.Invoke() } } diff --git a/osu.Game.Tournament/Screens/Setup/ResolutionSelector.cs b/osu.Game.Tournament/Screens/Setup/ResolutionSelector.cs index e6ab6f143a..c700e3bfdd 100644 --- a/osu.Game.Tournament/Screens/Setup/ResolutionSelector.cs +++ b/osu.Game.Tournament/Screens/Setup/ResolutionSelector.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 osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; @@ -14,9 +12,9 @@ namespace osu.Game.Tournament.Screens.Setup private const int minimum_window_height = 480; private const int maximum_window_height = 2160; - public new Action Action; + public new Action? Action; - private OsuNumberBox numberBox; + private OsuNumberBox? numberBox; protected override Drawable CreateComponent() { diff --git a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs index ceddd4d1a1..fed9d625ee 100644 --- a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/Setup/SetupScreen.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.Drawing; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -11,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.API; using osu.Game.Overlays; @@ -23,27 +22,27 @@ namespace osu.Game.Tournament.Screens.Setup { public partial class SetupScreen : TournamentScreen { - private FillFlowContainer fillFlow; + private FillFlowContainer fillFlow = null!; - private LoginOverlay loginOverlay; - private ResolutionSelector resolution; + private LoginOverlay? loginOverlay; + private ResolutionSelector resolution = null!; [Resolved] - private MatchIPCInfo ipc { get; set; } + private MatchIPCInfo ipc { get; set; } = null!; [Resolved] - private StableInfo stableInfo { get; set; } + private StableInfo stableInfo { get; set; } = null!; [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; [Resolved] - private RulesetStore rulesets { get; set; } + private RulesetStore rulesets { get; set; } = null!; - [Resolved(canBeNull: true)] - private TournamentSceneManager sceneManager { get; set; } + [Resolved] + private TournamentSceneManager? sceneManager { get; set; } - private Bindable windowSize; + private Bindable windowSize = null!; [BackgroundDependencyLoader] private void load(FrameworkConfigManager frameworkConfig) @@ -57,14 +56,18 @@ namespace osu.Game.Tournament.Screens.Setup RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(0.2f), }, - fillFlow = new FillFlowContainer + new OsuScrollContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Padding = new MarginPadding(10), - Spacing = new Vector2(10), - } + RelativeSizeAxes = Axes.Both, + Child = fillFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding(10), + Spacing = new Vector2(10), + }, + }, }; api.LocalUser.BindValueChanged(_ => Schedule(reload)); @@ -106,11 +109,11 @@ namespace osu.Game.Tournament.Screens.Setup loginOverlay.State.Value = Visibility.Visible; }, - Value = api?.LocalUser.Value.Username, - Failing = api?.IsLoggedIn != true, + Value = api.LocalUser.Value.Username, + Failing = api.IsLoggedIn != true, Description = "In order to access the API and display metadata, signing in is required." }, - new LabelledDropdown + new LabelledDropdown { Label = "Ruleset", Description = "Decides what stats are displayed and which ranks are retrieved for players. This requires a restart to reload data for an existing bracket.", @@ -137,6 +140,12 @@ namespace osu.Game.Tournament.Screens.Setup Description = "Screens will progress automatically from gameplay -> results -> map pool", Current = LadderInfo.AutoProgressScreens, }, + new LabelledSwitchButton + { + Label = "Display team seeds", + Description = "Team seeds will display alongside each team at the top in gameplay/map pool screens.", + Current = LadderInfo.DisplayTeamSeeds, + }, }; } diff --git a/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs index 463b012b77..74404e06f8 100644 --- a/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.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.IO; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -23,20 +21,20 @@ namespace osu.Game.Tournament.Screens.Setup { public partial class StablePathSelectScreen : TournamentScreen { - [Resolved(canBeNull: true)] - private TournamentSceneManager sceneManager { get; set; } + [Resolved] + private TournamentSceneManager? sceneManager { get; set; } [Resolved] - private MatchIPCInfo ipc { get; set; } + private MatchIPCInfo ipc { get; set; } = null!; - private OsuDirectorySelector directorySelector; - private DialogOverlay overlay; + private OsuDirectorySelector directorySelector = null!; + private DialogOverlay? overlay; [BackgroundDependencyLoader(true)] private void load(Storage storage, OsuColour colours) { var initialStorage = (ipc as FileBasedIPC)?.IPCStorage ?? storage; - string initialPath = new DirectoryInfo(initialStorage.GetFullPath(string.Empty)).Parent?.FullName; + string? initialPath = new DirectoryInfo(initialStorage.GetFullPath(string.Empty)).Parent?.FullName; AddRangeInternal(new Drawable[] { diff --git a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs index 7a8b03a7aa..e55cbc2dbb 100644 --- a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs +++ b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; @@ -13,11 +11,12 @@ namespace osu.Game.Tournament.Screens.Setup { internal partial class TournamentSwitcher : ActionableInfo { - private OsuDropdown dropdown; - private OsuButton folderButton; + private OsuDropdown dropdown = null!; + private OsuButton folderButton = null!; + private OsuButton reloadTournamentsButton = null!; [Resolved] - private TournamentGameBase game { get; set; } + private TournamentGameBase game { get; set; } = null!; [BackgroundDependencyLoader] private void load(TournamentStorage storage) @@ -28,7 +27,13 @@ namespace osu.Game.Tournament.Screens.Setup dropdown.Items = storage.ListTournaments(); dropdown.Current.BindValueChanged(v => Button.Enabled.Value = v.NewValue != startupTournament, true); - Action = () => game.AttemptExit(); + reloadTournamentsButton.Action = () => dropdown.Items = storage.ListTournaments(); + + Action = () => + { + game.RestartAppWhenExited(); + game.AttemptExit(); + }; folderButton.Action = () => storage.PresentExternally(); ButtonText = "Close osu!"; @@ -41,10 +46,16 @@ namespace osu.Game.Tournament.Screens.Setup FlowContainer.Insert(-1, folderButton = new RoundedButton { Text = "Open folder", - Width = 100 + Width = BUTTON_SIZE }); - FlowContainer.Insert(-2, dropdown = new OsuDropdown + FlowContainer.Insert(-2, reloadTournamentsButton = new RoundedButton + { + Text = "Refresh", + Width = BUTTON_SIZE + }); + + FlowContainer.Insert(-3, dropdown = new OsuDropdown { Width = 510 }); diff --git a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs index 35d63f4fcf..ae2ec0c291 100644 --- a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs +++ b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -44,7 +42,7 @@ namespace osu.Game.Tournament.Screens.Showcase }); } - protected override void CurrentMatchChanged(ValueChangedEvent match) + protected override void CurrentMatchChanged(ValueChangedEvent match) { // showcase screen doesn't care about a match being selected. // base call intentionally omitted to not show match warning. diff --git a/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs b/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs index d04059118f..5d9ab5fa8f 100644 --- a/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs +++ b/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs index b07a0a65dd..899d462e4e 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.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.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -22,12 +20,12 @@ namespace osu.Game.Tournament.Screens.TeamIntro { public partial class SeedingScreen : TournamentMatchScreen { - private Container mainContainer; + private Container mainContainer = null!; - private readonly Bindable currentTeam = new Bindable(); + private readonly Bindable currentTeam = new Bindable(); - private TourneyButton showFirstTeamButton; - private TourneyButton showSecondTeamButton; + private TourneyButton showFirstTeamButton = null!; + private TourneyButton showSecondTeamButton = null!; [BackgroundDependencyLoader] private void load() @@ -53,13 +51,13 @@ namespace osu.Game.Tournament.Screens.TeamIntro { RelativeSizeAxes = Axes.X, Text = "Show first team", - Action = () => currentTeam.Value = CurrentMatch.Value.Team1.Value, + Action = () => currentTeam.Value = CurrentMatch.Value?.Team1.Value, }, showSecondTeamButton = new TourneyButton { RelativeSizeAxes = Axes.X, Text = "Show second team", - Action = () => currentTeam.Value = CurrentMatch.Value.Team2.Value, + Action = () => currentTeam.Value = CurrentMatch.Value?.Team2.Value, }, new SettingsTeamDropdown(LadderInfo.Teams) { @@ -73,7 +71,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro currentTeam.BindValueChanged(teamChanged, true); } - private void teamChanged(ValueChangedEvent team) => updateTeamDisplay(); + private void teamChanged(ValueChangedEvent team) => updateTeamDisplay(); public override void Show() { @@ -84,7 +82,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro updateTeamDisplay(); } - protected override void CurrentMatchChanged(ValueChangedEvent match) + protected override void CurrentMatchChanged(ValueChangedEvent match) { base.CurrentMatchChanged(match); @@ -256,7 +254,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro private partial class LeftInfo : CompositeDrawable { - public LeftInfo(TournamentTeam team) + public LeftInfo(TournamentTeam? team) { FillFlowContainer fill; @@ -276,7 +274,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro new TeamDisplay(team) { Margin = new MarginPadding { Bottom = 30 } }, new RowDisplay("Average Rank:", $"#{team.AverageRank:#,0}"), new RowDisplay("Seed:", team.Seed.Value), - new RowDisplay("Last year's placing:", team.LastYearPlacing.Value > 0 ? $"#{team.LastYearPlacing:#,0}" : "0"), + new RowDisplay("Last year's placing:", team.LastYearPlacing.Value > 0 ? $"#{team.LastYearPlacing:#,0}" : "N/A"), new Container { Margin = new MarginPadding { Bottom = 30 } }, } }, @@ -315,7 +313,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro private partial class TeamDisplay : DrawableTournamentTeam { - public TeamDisplay(TournamentTeam team) + public TeamDisplay(TournamentTeam? team) : base(team) { AutoSizeAxes = Axes.Both; diff --git a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs index 950a63808c..2280f21d47 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -15,7 +13,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro { public partial class TeamIntroScreen : TournamentMatchScreen { - private Container mainContainer; + private Container mainContainer = null!; [BackgroundDependencyLoader] private void load() @@ -36,7 +34,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro }; } - protected override void CurrentMatchChanged(ValueChangedEvent match) + protected override void CurrentMatchChanged(ValueChangedEvent match) { base.CurrentMatchChanged(match); diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs index 9206de1dc2..af21613541 100644 --- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs +++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -16,12 +14,12 @@ namespace osu.Game.Tournament.Screens.TeamWin { public partial class TeamWinScreen : TournamentMatchScreen { - private Container mainContainer; + private Container mainContainer = null!; private readonly Bindable currentCompleted = new Bindable(); - private TourneyVideo blueWinVideo; - private TourneyVideo redWinVideo; + private TourneyVideo blueWinVideo = null!; + private TourneyVideo redWinVideo = null!; [BackgroundDependencyLoader] private void load() @@ -51,7 +49,7 @@ namespace osu.Game.Tournament.Screens.TeamWin currentCompleted.BindValueChanged(_ => update()); } - protected override void CurrentMatchChanged(ValueChangedEvent match) + protected override void CurrentMatchChanged(ValueChangedEvent match) { base.CurrentMatchChanged(match); @@ -70,7 +68,7 @@ namespace osu.Game.Tournament.Screens.TeamWin { var match = CurrentMatch.Value; - if (match.Winner == null) + if (match?.Winner == null) { mainContainer.Clear(); return; diff --git a/osu.Game.Tournament/Screens/TournamentMatchScreen.cs b/osu.Game.Tournament/Screens/TournamentMatchScreen.cs index 58444d0c1b..5a9b9d05ed 100644 --- a/osu.Game.Tournament/Screens/TournamentMatchScreen.cs +++ b/osu.Game.Tournament/Screens/TournamentMatchScreen.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 osu.Framework.Bindables; using osu.Game.Tournament.Models; @@ -10,8 +8,8 @@ namespace osu.Game.Tournament.Screens { public abstract partial class TournamentMatchScreen : TournamentScreen { - protected readonly Bindable CurrentMatch = new Bindable(); - private WarningBox noMatchWarning; + protected readonly Bindable CurrentMatch = new Bindable(); + private WarningBox? noMatchWarning; protected override void LoadComplete() { @@ -21,7 +19,7 @@ namespace osu.Game.Tournament.Screens CurrentMatch.BindValueChanged(CurrentMatchChanged, true); } - protected virtual void CurrentMatchChanged(ValueChangedEvent match) + protected virtual void CurrentMatchChanged(ValueChangedEvent match) { if (match.NewValue == null) { diff --git a/osu.Game.Tournament/Screens/TournamentScreen.cs b/osu.Game.Tournament/Screens/TournamentScreen.cs index 02903a637c..1e119e0336 100644 --- a/osu.Game.Tournament/Screens/TournamentScreen.cs +++ b/osu.Game.Tournament/Screens/TournamentScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -15,7 +13,7 @@ namespace osu.Game.Tournament.Screens public const double FADE_DELAY = 200; [Resolved] - protected LadderInfo LadderInfo { get; private set; } + protected LadderInfo LadderInfo { get; private set; } = null!; protected TournamentScreen() { diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index beef1e197d..25dc8ae1e5 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -1,9 +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.Drawing; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -17,6 +14,7 @@ using osu.Framework.Platform; using osu.Game.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; using osu.Game.Tournament.Models; using osuTK.Graphics; @@ -34,15 +32,21 @@ namespace osu.Game.Tournament public static readonly Color4 ELEMENT_FOREGROUND_COLOUR = Color4Extensions.FromHex("#000"); public static readonly Color4 TEXT_COLOUR = Color4Extensions.FromHex("#fff"); - private Drawable heightWarning; - private Bindable windowSize; - private Bindable windowMode; - private LoadingSpinner loadingSpinner; + private Drawable heightWarning = null!; + + private Bindable windowMode = null!; + private readonly BindableSize windowSize = new BindableSize(); + + private LoadingSpinner loadingSpinner = null!; + + [Cached(typeof(IDialogOverlay))] + private readonly DialogOverlay dialogOverlay = new DialogOverlay(); [BackgroundDependencyLoader] private void load(FrameworkConfigManager frameworkConfig, GameHost host) { - windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize); + frameworkConfig.BindWith(FrameworkSetting.WindowedSize, windowSize); + windowMode = frameworkConfig.GetBindable(FrameworkSetting.WindowMode); Add(loadingSpinner = new LoadingSpinner(true, true) @@ -90,12 +94,12 @@ namespace osu.Game.Tournament { RelativeSizeAxes = Axes.Both, Child = new TournamentSceneManager() - } + }, + dialogOverlay }, drawables => { loadingSpinner.Hide(); loadingSpinner.Expire(); - AddRange(drawables); windowSize.BindValueChanged(size => ScheduleAfterChildren(() => diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 634cc87a9f..eecd097a97 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -1,24 +1,23 @@ // 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.IO; using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Online; using osu.Game.Online.API.Requests; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Tournament.IO; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; @@ -31,10 +30,11 @@ namespace osu.Game.Tournament public partial class TournamentGameBase : OsuGameBase { public const string BRACKET_FILENAME = @"bracket.json"; - private LadderInfo ladder; - private TournamentStorage storage; - private DependencyContainer dependencies; - private FileBasedIPC ipc; + private LadderInfo ladder = new LadderInfo(); + private TournamentStorage storage = null!; + private DependencyContainer dependencies = null!; + private FileBasedIPC ipc = null!; + private BeatmapLookupCache beatmapCache = null!; protected Task BracketLoadTask => bracketLoadTaskCompletionSource.Task; @@ -53,7 +53,7 @@ namespace osu.Game.Tournament return new ProductionEndpointConfiguration(); } - private TournamentSpriteText initialisationText; + private TournamentSpriteText initialisationText = null!; [BackgroundDependencyLoader] private void load(Storage baseStorage) @@ -75,6 +75,8 @@ namespace osu.Game.Tournament Textures.AddTextureSource(new TextureLoaderStore(new StorageBackedResourceStore(storage))); dependencies.CacheAs(new StableInfo(storage)); + + beatmapCache = dependencies.Get(); } protected override void LoadComplete() @@ -89,7 +91,7 @@ namespace osu.Game.Tournament Task.Run(readBracket); } - private void readBracket() + private async Task readBracket() { try { @@ -97,11 +99,11 @@ namespace osu.Game.Tournament { using (Stream stream = storage.GetStream(BRACKET_FILENAME, FileAccess.Read, FileMode.Open)) using (var sr = new StreamReader(stream)) - ladder = JsonConvert.DeserializeObject(sr.ReadToEnd(), new JsonPointConverter()); + { + ladder = JsonConvert.DeserializeObject(await sr.ReadToEndAsync().ConfigureAwait(false), new JsonPointConverter()) ?? ladder; + } } - ladder ??= new LadderInfo(); - var resolvedRuleset = ladder.Ruleset.Value != null ? RulesetStore.GetRuleset(ladder.Ruleset.Value.ShortName) : RulesetStore.AvailableRulesets.First(); @@ -161,8 +163,8 @@ namespace osu.Game.Tournament } addedInfo |= addPlayers(); - addedInfo |= addRoundBeatmaps(); - addedInfo |= addSeedingBeatmaps(); + addedInfo |= await addRoundBeatmaps().ConfigureAwait(false); + addedInfo |= await addSeedingBeatmaps().ConfigureAwait(false); if (addedInfo) saveChanges(); @@ -228,11 +230,11 @@ namespace osu.Game.Tournament /// /// Add missing beatmap info based on beatmap IDs /// - private bool addRoundBeatmaps() + private async Task addRoundBeatmaps() { var beatmapsRequiringPopulation = ladder.Rounds .SelectMany(r => r.Beatmaps) - .Where(b => b.Beatmap?.OnlineID == 0 && b.ID > 0).ToList(); + .Where(b => (b.Beatmap == null || b.Beatmap?.OnlineID == 0) && b.ID > 0).ToList(); if (beatmapsRequiringPopulation.Count == 0) return false; @@ -241,9 +243,9 @@ namespace osu.Game.Tournament { var b = beatmapsRequiringPopulation[i]; - var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = b.ID }); - API.Perform(req); - b.Beatmap = new TournamentBeatmap(req.Response ?? new APIBeatmap()); + var populated = await beatmapCache.GetBeatmapAsync(b.ID).ConfigureAwait(false); + if (populated != null) + b.Beatmap = new TournamentBeatmap(populated); updateLoadProgressMessage($"Populating round beatmaps ({i} / {beatmapsRequiringPopulation.Count})"); } @@ -254,7 +256,7 @@ namespace osu.Game.Tournament /// /// Add missing beatmap info based on beatmap IDs /// - private bool addSeedingBeatmaps() + private async Task addSeedingBeatmaps() { var beatmapsRequiringPopulation = ladder.Teams .SelectMany(r => r.SeedingResults) @@ -268,9 +270,9 @@ namespace osu.Game.Tournament { var b = beatmapsRequiringPopulation[i]; - var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = b.ID }); - API.Perform(req); - b.Beatmap = new TournamentBeatmap(req.Response ?? new APIBeatmap()); + var populated = await beatmapCache.GetBeatmapAsync(b.ID).ConfigureAwait(false); + if (populated != null) + b.Beatmap = new TournamentBeatmap(populated); updateLoadProgressMessage($"Populating seeding beatmaps ({i} / {beatmapsRequiringPopulation.Count})"); } @@ -280,7 +282,7 @@ namespace osu.Game.Tournament private void updateLoadProgressMessage(string s) => Schedule(() => initialisationText.Text = s); - public void PopulatePlayer(TournamentUser user, Action success = null, Action failure = null, bool immediate = false) + public void PopulatePlayer(TournamentUser user, Action? success = null, Action? failure = null, bool immediate = false) { var req = new GetUserRequest(user.OnlineID, ladder.Ruleset.Value); @@ -345,8 +347,8 @@ namespace osu.Game.Tournament foreach (var r in ladder.Rounds) r.Matches = ladder.Matches.Where(p => p.Round.Value == r).Select(p => p.ID).ToList(); - ladder.Progressions = ladder.Matches.Where(p => p.Progression.Value != null).Select(p => new TournamentProgression(p.ID, p.Progression.Value.ID)).Concat( - ladder.Matches.Where(p => p.LosersProgression.Value != null).Select(p => new TournamentProgression(p.ID, p.LosersProgression.Value.ID, true))) + ladder.Progressions = ladder.Matches.Where(p => p.Progression.Value != null).Select(p => new TournamentProgression(p.ID, p.Progression.Value.AsNonNull().ID)).Concat( + ladder.Matches.Where(p => p.LosersProgression.Value != null).Select(p => new TournamentProgression(p.ID, p.LosersProgression.Value.AsNonNull().ID, true))) .ToList(); return JsonConvert.SerializeObject(ladder, diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs index abfe69b97b..c69b76ae29 100644 --- a/osu.Game.Tournament/TournamentSceneManager.cs +++ b/osu.Game.Tournament/TournamentSceneManager.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; @@ -35,20 +33,23 @@ namespace osu.Game.Tournament [Cached] public partial class TournamentSceneManager : CompositeDrawable { - private Container screens; - private TourneyVideo video; + private Container screens = null!; + private TourneyVideo video = null!; - public const float CONTROL_AREA_WIDTH = 160; + public const int CONTROL_AREA_WIDTH = 200; - public const float STREAM_AREA_WIDTH = 1366; + public const int STREAM_AREA_WIDTH = 1366; + public const int STREAM_AREA_HEIGHT = (int)(STREAM_AREA_WIDTH / ASPECT_RATIO); - public const double REQUIRED_WIDTH = CONTROL_AREA_WIDTH * 2 + STREAM_AREA_WIDTH; + public const float ASPECT_RATIO = 16 / 9f; + + public const int REQUIRED_WIDTH = CONTROL_AREA_WIDTH * 2 + STREAM_AREA_WIDTH; [Cached] private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay(); - private Container chatContainer; - private FillFlowContainer buttons; + private Container chatContainer = null!; + private FillFlowContainer buttons = null!; public TournamentSceneManager() { @@ -65,13 +66,20 @@ namespace osu.Game.Tournament RelativeSizeAxes = Axes.Y, X = CONTROL_AREA_WIDTH, FillMode = FillMode.Fit, - FillAspectRatio = 16 / 9f, + FillAspectRatio = ASPECT_RATIO, Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, Width = STREAM_AREA_WIDTH, //Masking = true, Children = new Drawable[] { + new Box + { + Colour = new Color4(20, 20, 20, 255), + Anchor = Anchor.TopRight, + RelativeSizeAxes = Axes.Both, + Width = 10, + }, video = new TourneyVideo("main", true) { Loop = true, @@ -156,10 +164,10 @@ namespace osu.Game.Tournament private float depth; - private Drawable currentScreen; - private ScheduledDelegate scheduledHide; + private Drawable? currentScreen; + private ScheduledDelegate? scheduledHide; - private Drawable temporaryScreen; + private Drawable? temporaryScreen; public void SetScreen(Drawable screen) { @@ -252,14 +260,13 @@ namespace osu.Game.Tournament if (shortcutKey != null) { - Add(new Container + Add(new CircularContainer { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Size = new Vector2(24), Margin = new MarginPadding(5), Masking = true, - CornerRadius = 4, Alpha = 0.5f, Blending = BlendingParameters.Additive, Children = new Drawable[] @@ -275,7 +282,7 @@ namespace osu.Game.Tournament Y = -2, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = shortcutKey.ToString(), + Text = shortcutKey.Value.ToString(), } } }); @@ -295,7 +302,7 @@ namespace osu.Game.Tournament private bool isSelected; - public Action RequestSelection; + public Action? RequestSelection; public bool IsSelected { diff --git a/osu.Game.Tournament/TournamentSpriteText.cs b/osu.Game.Tournament/TournamentSpriteText.cs index 7ecb31ff15..227231a310 100644 --- a/osu.Game.Tournament/TournamentSpriteText.cs +++ b/osu.Game.Tournament/TournamentSpriteText.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Graphics; using osu.Game.Graphics.Sprites; diff --git a/osu.Game.Tournament/TourneyButton.cs b/osu.Game.Tournament/TourneyButton.cs index 558bd476c3..b874c5c37c 100644 --- a/osu.Game.Tournament/TourneyButton.cs +++ b/osu.Game.Tournament/TourneyButton.cs @@ -1,20 +1,21 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings; namespace osu.Game.Tournament { - public partial class TourneyButton : OsuButton + public partial class TourneyButton : SettingsButton { public new Box Background => base.Background; - public TourneyButton() - : base(null) + [BackgroundDependencyLoader] + private void load() { + Padding = new MarginPadding(); } } } diff --git a/osu.Game.Tournament/WarningBox.cs b/osu.Game.Tournament/WarningBox.cs index 4a196446f6..a79e97914b 100644 --- a/osu.Game.Tournament/WarningBox.cs +++ b/osu.Game.Tournament/WarningBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 9573a9a4aa..24cb1730bf 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -24,6 +24,12 @@ namespace osu.Game.Audio public const string BANK_SOFT = @"soft"; public const string BANK_DRUM = @"drum"; + // new sample used exclusively by taiko for now. + public const string HIT_FLOURISH = "hitflourish"; + + // new bank used exclusively by taiko for now. + public const string BANK_STRONG = @"strong"; + /// /// All valid sample addition constants. /// diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/BackgroundBeatmapProcessor.cs deleted file mode 100644 index b8c89d8822..0000000000 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ /dev/null @@ -1,197 +0,0 @@ -// 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.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Logging; -using osu.Game.Beatmaps; -using osu.Game.Database; -using osu.Game.Rulesets; -using osu.Game.Scoring; -using osu.Game.Screens.Play; - -namespace osu.Game -{ - public partial class BackgroundBeatmapProcessor : Component - { - [Resolved] - private RulesetStore rulesetStore { get; set; } = null!; - - [Resolved] - private ScoreManager scoreManager { get; set; } = null!; - - [Resolved] - private RealmAccess realmAccess { get; set; } = null!; - - [Resolved] - private BeatmapUpdater beatmapUpdater { get; set; } = null!; - - [Resolved] - private IBindable gameBeatmap { get; set; } = null!; - - [Resolved] - private ILocalUserPlayInfo? localUserPlayInfo { get; set; } - - protected virtual int TimeToSleepDuringGameplay => 30000; - - protected override void LoadComplete() - { - base.LoadComplete(); - - Task.Run(() => - { - Logger.Log("Beginning background beatmap processing.."); - checkForOutdatedStarRatings(); - processBeatmapSetsWithMissingMetrics(); - processScoresWithMissingStatistics(); - }).ContinueWith(t => - { - if (t.Exception?.InnerException is ObjectDisposedException) - { - Logger.Log("Finished background aborted during shutdown"); - return; - } - - Logger.Log("Finished background beatmap processing!"); - }); - } - - /// - /// Check whether the databased difficulty calculation version matches the latest ruleset provided version. - /// If it doesn't, clear out any existing difficulties so they can be incrementally recalculated. - /// - private void checkForOutdatedStarRatings() - { - foreach (var ruleset in rulesetStore.AvailableRulesets) - { - // beatmap being passed in is arbitrary here. just needs to be non-null. - int currentVersion = ruleset.CreateInstance().CreateDifficultyCalculator(gameBeatmap.Value).Version; - - if (ruleset.LastAppliedDifficultyVersion < currentVersion) - { - Logger.Log($"Resetting star ratings for {ruleset.Name} (difficulty calculation version updated from {ruleset.LastAppliedDifficultyVersion} to {currentVersion})"); - - int countReset = 0; - - realmAccess.Write(r => - { - foreach (var b in r.All()) - { - if (b.Ruleset.ShortName == ruleset.ShortName) - { - b.StarRating = -1; - countReset++; - } - } - - r.Find(ruleset.ShortName).LastAppliedDifficultyVersion = currentVersion; - }); - - Logger.Log($"Finished resetting {countReset} beatmap sets for {ruleset.Name}"); - } - } - } - - private void processBeatmapSetsWithMissingMetrics() - { - HashSet beatmapSetIds = new HashSet(); - - Logger.Log("Querying for beatmap sets to reprocess..."); - - realmAccess.Run(r => - { - foreach (var b in r.All().Where(b => b.StarRating < 0 || (b.OnlineID > 0 && b.LastOnlineUpdate == null))) - { - Debug.Assert(b.BeatmapSet != null); - beatmapSetIds.Add(b.BeatmapSet.ID); - } - }); - - Logger.Log($"Found {beatmapSetIds.Count} beatmap sets which require reprocessing."); - - int i = 0; - - foreach (var id in beatmapSetIds) - { - while (localUserPlayInfo?.IsPlaying.Value == true) - { - Logger.Log("Background processing sleeping due to active gameplay..."); - Thread.Sleep(TimeToSleepDuringGameplay); - } - - realmAccess.Run(r => - { - var set = r.Find(id); - - if (set != null) - { - try - { - Logger.Log($"Background processing {set} ({++i} / {beatmapSetIds.Count})"); - beatmapUpdater.Process(set); - } - catch (Exception e) - { - Logger.Log($"Background processing failed on {set}: {e}"); - } - } - }); - } - } - - private void processScoresWithMissingStatistics() - { - HashSet scoreIds = new HashSet(); - - Logger.Log("Querying for scores to reprocess..."); - - realmAccess.Run(r => - { - foreach (var score in r.All()) - { - if (score.Statistics.Sum(kvp => kvp.Value) > 0 && score.MaximumStatistics.Sum(kvp => kvp.Value) == 0) - scoreIds.Add(score.ID); - } - }); - - Logger.Log($"Found {scoreIds.Count} scores which require reprocessing."); - - foreach (var id in scoreIds) - { - while (localUserPlayInfo?.IsPlaying.Value == true) - { - Logger.Log("Background processing sleeping due to active gameplay..."); - Thread.Sleep(TimeToSleepDuringGameplay); - } - - try - { - var score = scoreManager.Query(s => s.ID == id); - - scoreManager.PopulateMaximumStatistics(score); - - // Can't use async overload because we're not on the update thread. - // ReSharper disable once MethodHasAsyncOverload - realmAccess.Write(r => - { - r.Find(id).MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics); - }); - - Logger.Log($"Populated maximum statistics for score {id}"); - } - catch (Exception e) - { - Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}"); - } - } - } - } -} diff --git a/osu.Game/BackgroundDataStoreProcessor.cs b/osu.Game/BackgroundDataStoreProcessor.cs new file mode 100644 index 0000000000..90e55dea6d --- /dev/null +++ b/osu.Game/BackgroundDataStoreProcessor.cs @@ -0,0 +1,321 @@ +// 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.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Logging; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Rulesets; +using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; +using osu.Game.Screens.Play; + +namespace osu.Game +{ + /// + /// Performs background updating of data stores at startup. + /// + public partial class BackgroundDataStoreProcessor : Component + { + [Resolved] + private RulesetStore rulesetStore { get; set; } = null!; + + [Resolved] + private BeatmapManager beatmapManager { get; set; } = null!; + + [Resolved] + private ScoreManager scoreManager { get; set; } = null!; + + [Resolved] + private RealmAccess realmAccess { get; set; } = null!; + + [Resolved] + private BeatmapUpdater beatmapUpdater { get; set; } = null!; + + [Resolved] + private IBindable gameBeatmap { get; set; } = null!; + + [Resolved] + private ILocalUserPlayInfo? localUserPlayInfo { get; set; } + + [Resolved] + private INotificationOverlay? notificationOverlay { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + protected virtual int TimeToSleepDuringGameplay => 30000; + + protected override void LoadComplete() + { + base.LoadComplete(); + + Task.Factory.StartNew(() => + { + Logger.Log("Beginning background data store processing.."); + + checkForOutdatedStarRatings(); + processBeatmapSetsWithMissingMetrics(); + processScoresWithMissingStatistics(); + convertLegacyTotalScoreToStandardised(); + }, TaskCreationOptions.LongRunning).ContinueWith(t => + { + if (t.Exception?.InnerException is ObjectDisposedException) + { + Logger.Log("Finished background aborted during shutdown"); + return; + } + + Logger.Log("Finished background data store processing!"); + }); + } + + /// + /// Check whether the databased difficulty calculation version matches the latest ruleset provided version. + /// If it doesn't, clear out any existing difficulties so they can be incrementally recalculated. + /// + private void checkForOutdatedStarRatings() + { + foreach (var ruleset in rulesetStore.AvailableRulesets) + { + // beatmap being passed in is arbitrary here. just needs to be non-null. + int currentVersion = ruleset.CreateInstance().CreateDifficultyCalculator(gameBeatmap.Value).Version; + + if (ruleset.LastAppliedDifficultyVersion < currentVersion) + { + Logger.Log($"Resetting star ratings for {ruleset.Name} (difficulty calculation version updated from {ruleset.LastAppliedDifficultyVersion} to {currentVersion})"); + + int countReset = 0; + + realmAccess.Write(r => + { + foreach (var b in r.All()) + { + if (b.Ruleset.ShortName == ruleset.ShortName) + { + b.StarRating = -1; + countReset++; + } + } + + r.Find(ruleset.ShortName)!.LastAppliedDifficultyVersion = currentVersion; + }); + + Logger.Log($"Finished resetting {countReset} beatmap sets for {ruleset.Name}"); + } + } + } + + private void processBeatmapSetsWithMissingMetrics() + { + HashSet beatmapSetIds = new HashSet(); + + Logger.Log("Querying for beatmap sets to reprocess..."); + + realmAccess.Run(r => + { + // BeatmapProcessor is responsible for both online and local processing. + // In the case a user isn't logged in, it won't update LastOnlineUpdate and therefore re-queue, + // causing overhead from the non-online processing to redundantly run every startup. + // + // We may eventually consider making the Process call more specific (or avoid this in any number + // of other possible ways), but for now avoid queueing if the user isn't logged in at startup. + if (api.IsLoggedIn) + { + foreach (var b in r.All().Where(b => b.StarRating < 0 || (b.OnlineID > 0 && b.LastOnlineUpdate == null))) + { + Debug.Assert(b.BeatmapSet != null); + beatmapSetIds.Add(b.BeatmapSet.ID); + } + } + else + { + foreach (var b in r.All().Where(b => b.StarRating < 0)) + { + Debug.Assert(b.BeatmapSet != null); + beatmapSetIds.Add(b.BeatmapSet.ID); + } + } + }); + + Logger.Log($"Found {beatmapSetIds.Count} beatmap sets which require reprocessing."); + + int i = 0; + + foreach (var id in beatmapSetIds) + { + sleepIfRequired(); + + realmAccess.Run(r => + { + var set = r.Find(id); + + if (set != null) + { + try + { + Logger.Log($"Background processing {set} ({++i} / {beatmapSetIds.Count})"); + beatmapUpdater.Process(set); + } + catch (Exception e) + { + Logger.Log($"Background processing failed on {set}: {e}"); + } + } + }); + } + } + + private void processScoresWithMissingStatistics() + { + HashSet scoreIds = new HashSet(); + + Logger.Log("Querying for scores to reprocess..."); + + realmAccess.Run(r => + { + foreach (var score in r.All().Where(s => !s.BackgroundReprocessingFailed)) + { + if (score.BeatmapInfo != null + && score.Statistics.Sum(kvp => kvp.Value) > 0 + && score.MaximumStatistics.Sum(kvp => kvp.Value) == 0) + { + scoreIds.Add(score.ID); + } + } + }); + + Logger.Log($"Found {scoreIds.Count} scores which require reprocessing."); + + foreach (var id in scoreIds) + { + sleepIfRequired(); + + try + { + var score = scoreManager.Query(s => s.ID == id); + + scoreManager.PopulateMaximumStatistics(score); + + // Can't use async overload because we're not on the update thread. + // ReSharper disable once MethodHasAsyncOverload + realmAccess.Write(r => + { + r.Find(id)!.MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics); + }); + + Logger.Log($"Populated maximum statistics for score {id}"); + } + catch (ObjectDisposedException) + { + throw; + } + catch (Exception e) + { + Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}"); + realmAccess.Write(r => r.Find(id)!.BackgroundReprocessingFailed = true); + } + } + } + + private void convertLegacyTotalScoreToStandardised() + { + Logger.Log("Querying for scores that need total score conversion..."); + + HashSet scoreIds = realmAccess.Run(r => new HashSet(r.All() + .Where(s => !s.BackgroundReprocessingFailed && s.BeatmapInfo != null + && (s.TotalScoreVersion == 30000002 + || s.TotalScoreVersion == 30000003)) + .AsEnumerable().Select(s => s.ID))); + + Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); + + if (scoreIds.Count == 0) + return; + + ProgressNotification notification = new ProgressNotification { State = ProgressNotificationState.Active }; + + notificationOverlay?.Post(notification); + + int processedCount = 0; + int failedCount = 0; + + foreach (var id in scoreIds) + { + if (notification.State == ProgressNotificationState.Cancelled) + break; + + notification.Text = $"Upgrading scores to new scoring algorithm ({processedCount} of {scoreIds.Count})"; + notification.Progress = (float)processedCount / scoreIds.Count; + + sleepIfRequired(); + + try + { + var score = scoreManager.Query(s => s.ID == id); + long newTotalScore = StandardisedScoreMigrationTools.ConvertFromLegacyTotalScore(score, beatmapManager); + + // Can't use async overload because we're not on the update thread. + // ReSharper disable once MethodHasAsyncOverload + realmAccess.Write(r => + { + ScoreInfo s = r.Find(id)!; + s.TotalScore = newTotalScore; + s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION; + }); + + Logger.Log($"Converted total score for score {id}"); + ++processedCount; + } + catch (ObjectDisposedException) + { + throw; + } + catch (Exception e) + { + Logger.Log($"Failed to convert total score for {id}: {e}"); + realmAccess.Write(r => r.Find(id)!.BackgroundReprocessingFailed = true); + ++failedCount; + } + } + + if (processedCount == scoreIds.Count) + { + notification.CompletionText = $"{processedCount} score(s) have been upgraded to the new scoring algorithm"; + notification.Progress = 1; + notification.State = ProgressNotificationState.Completed; + } + else + { + notification.Text = $"{processedCount} of {scoreIds.Count} score(s) have been upgraded to the new scoring algorithm."; + + // We may have arrived here due to user cancellation or completion with failures. + if (failedCount > 0) + notification.Text += $" Check logs for issues with {failedCount} failed upgrades."; + + notification.State = ProgressNotificationState.Cancelled; + } + } + + private void sleepIfRequired() + { + while (localUserPlayInfo?.IsPlaying.Value == true) + { + Logger.Log("Background processing sleeping due to active gameplay..."); + Thread.Sleep(TimeToSleepDuringGameplay); + } + } + } +} diff --git a/osu.Game/Beatmaps/APIFailTimes.cs b/osu.Game/Beatmaps/APIFailTimes.cs index 441d30d06b..09ab16598d 100644 --- a/osu.Game/Beatmaps/APIFailTimes.cs +++ b/osu.Game/Beatmaps/APIFailTimes.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 Newtonsoft.Json; @@ -17,12 +15,12 @@ namespace osu.Game.Beatmaps /// Points of failure on a relative time scale (usually 0..100). /// [JsonProperty(@"fail")] - public int[] Fails { get; set; } = Array.Empty(); + public int[]? Fails { get; set; } = Array.Empty(); /// /// Points of retry on a relative time scale (usually 0..100). /// [JsonProperty(@"exit")] - public int[] Retries { get; set; } = Array.Empty(); + public int[]? Retries { get; set; } = Array.Empty(); } } diff --git a/osu.Game/Beatmaps/BeatSyncProviderExtensions.cs b/osu.Game/Beatmaps/BeatSyncProviderExtensions.cs index 767aa5df73..e2b805bf0d 100644 --- a/osu.Game/Beatmaps/BeatSyncProviderExtensions.cs +++ b/osu.Game/Beatmaps/BeatSyncProviderExtensions.cs @@ -5,14 +5,9 @@ namespace osu.Game.Beatmaps { public static class BeatSyncProviderExtensions { - /// - /// Check whether beat sync is currently available. - /// - public static bool CheckBeatSyncAvailable(this IBeatSyncProvider provider) => provider.Clock != null; - /// /// Whether the beat sync provider is currently in a kiai section. Should make everything more epic. /// - public static bool CheckIsKiaiTime(this IBeatSyncProvider provider) => provider.Clock != null && provider.ControlPoints?.EffectPointAt(provider.Clock.CurrentTime).KiaiMode == true; + public static bool CheckIsKiaiTime(this IBeatSyncProvider provider) => provider.ControlPoints?.EffectPointAt(provider.Clock.CurrentTime).KiaiMode == true; } } diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 7d367ef77d..e89e5339e1 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps /// public class BeatmapImporter : RealmArchiveModelImporter { - public override IEnumerable HandledExtensions => new[] { ".osz" }; + public override IEnumerable HandledExtensions => new[] { ".osz", ".olz" }; protected override string[] HashableFileTypes => new[] { ".osu" }; @@ -68,7 +68,7 @@ namespace osu.Game.Beatmaps Logger.Log($"Beatmap \"{updated}\" update completed successfully", LoggingTarget.Database); - original = realm.Find(original.ID); + original = realm!.Find(original.ID)!; // Generally the import process will do this for us if the OnlineIDs match, // but that isn't a guarantee (ie. if the .osu file doesn't have OnlineIDs populated). @@ -145,13 +145,15 @@ namespace osu.Game.Beatmaps } } - protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz"; + protected override bool ShouldDeleteArchive(string path) => HandledExtensions.Contains(Path.GetExtension(path).ToLowerInvariant()); protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) { if (archive != null) beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet, realm)); + beatmapSet.DateAdded = getDateAdded(archive); + foreach (BeatmapInfo b in beatmapSet.Beatmaps) { b.BeatmapSet = beatmapSet; @@ -204,6 +206,14 @@ namespace osu.Game.Beatmaps protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters parameters) { base.PostImport(model, realm, parameters); + + // Scores are stored separately from beatmaps, and persist even when a beatmap is modified or deleted. + // Let's reattach any matching scores that exist in the database, based on hash. + foreach (BeatmapInfo beatmap in model.Beatmaps) + { + beatmap.UpdateLocalScores(realm); + } + ProcessBeatmap?.Invoke(model, parameters.Batch ? MetadataLookupScope.LocalCacheFirst : MetadataLookupScope.OnlineFirst); } @@ -270,7 +280,7 @@ namespace osu.Game.Beatmaps public override string HumanisedModelName => "beatmap"; - protected override BeatmapSetInfo? CreateModel(ArchiveReader reader) + protected override BeatmapSetInfo? CreateModel(ArchiveReader reader, ImportParameters parameters) { // let's make sure there are actually .osu files to import. string? mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)); @@ -297,11 +307,36 @@ namespace osu.Game.Beatmaps return new BeatmapSetInfo { OnlineID = beatmap.BeatmapInfo.BeatmapSet?.OnlineID ?? -1, - // Metadata = beatmap.Metadata, - DateAdded = DateTimeOffset.UtcNow }; } + /// + /// Determine the date a given beatmapset has been added to the game. + /// For legacy imports, we can use the oldest file write time for any `.osu` file in the directory. + /// For any other import types, use "now". + /// + private DateTimeOffset getDateAdded(ArchiveReader? reader) + { + DateTimeOffset dateAdded = DateTimeOffset.UtcNow; + + if (reader is DirectoryArchiveReader legacyReader) + { + var beatmaps = reader.Filenames.Where(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)); + + dateAdded = File.GetLastWriteTimeUtc(legacyReader.GetFullPath(beatmaps.First())); + + foreach (string beatmapName in beatmaps) + { + var currentDateAdded = File.GetLastWriteTimeUtc(legacyReader.GetFullPath(beatmapName)); + + if (currentDateAdded < dateAdded) + dateAdded = currentDateAdded; + } + } + + return dateAdded; + } + /// /// Create all required s for the provided archive. /// diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 5019d64276..c1aeec1f71 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -234,6 +234,22 @@ namespace osu.Game.Beatmaps } } + /// + /// Local scores are retained separate from a beatmap's lifetime, matched via . + /// Therefore we need to detach / reattach scores when a beatmap is edited or imported. + /// + /// A realm instance in an active write transaction. + public void UpdateLocalScores(Realm realm) + { + // first disassociate any scores which are already attached and no longer valid. + foreach (var score in Scores) + score.BeatmapInfo = null; + + // then attach any scores which match the new hash. + foreach (var score in realm.All().Where(s => s.BeatmapHash == Hash)) + score.BeatmapInfo = this; + } + IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata; IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet; IRulesetInfo IBeatmapInfo.Ruleset => Ruleset; diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 305dc01844..1f551f1218 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -26,6 +26,7 @@ using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Skinning; using osu.Game.Utils; +using Realms; namespace osu.Game.Beatmaps { @@ -40,7 +41,9 @@ namespace osu.Game.Beatmaps private readonly WorkingBeatmapCache workingBeatmapCache; - private readonly LegacyBeatmapExporter beatmapExporter; + private readonly BeatmapExporter beatmapExporter; + + private readonly LegacyBeatmapExporter legacyBeatmapExporter; public ProcessBeatmapDelegate? ProcessBeatmap { private get; set; } @@ -77,7 +80,12 @@ namespace osu.Game.Beatmaps workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host); - beatmapExporter = new LegacyBeatmapExporter(storage) + beatmapExporter = new BeatmapExporter(storage) + { + PostNotification = obj => PostNotification?.Invoke(obj) + }; + + legacyBeatmapExporter = new LegacyBeatmapExporter(storage) { PostNotification = obj => PostNotification?.Invoke(obj) }; @@ -208,7 +216,7 @@ namespace osu.Game.Beatmaps using (var transaction = r.BeginWrite()) { if (!beatmapInfo.IsManaged) - beatmapInfo = r.Find(beatmapInfo.ID); + beatmapInfo = r.Find(beatmapInfo.ID)!; beatmapInfo.Hidden = true; transaction.Commit(); @@ -227,7 +235,7 @@ namespace osu.Game.Beatmaps using (var transaction = r.BeginWrite()) { if (!beatmapInfo.IsManaged) - beatmapInfo = r.Find(beatmapInfo.ID); + beatmapInfo = r.Find(beatmapInfo.ID)!; beatmapInfo.Hidden = false; transaction.Commit(); @@ -277,7 +285,7 @@ namespace osu.Game.Beatmaps /// /// The query. /// The first result for the provided query, or null if no results were found. - public BeatmapInfo? QueryBeatmap(Expression> query) => Realm.Run(r => r.All().FirstOrDefault(query)?.Detach()); + public BeatmapInfo? QueryBeatmap(Expression> query) => Realm.Run(r => r.All().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach()); /// /// A default representation of a WorkingBeatmap to use when no beatmap is available. @@ -330,7 +338,7 @@ namespace osu.Game.Beatmaps Realm.Write(r => { if (!beatmapInfo.IsManaged) - beatmapInfo = r.Find(beatmapInfo.ID); + beatmapInfo = r.Find(beatmapInfo.ID)!; Debug.Assert(beatmapInfo.BeatmapSet != null); Debug.Assert(beatmapInfo.File != null); @@ -339,6 +347,8 @@ namespace osu.Game.Beatmaps DeleteFile(setInfo, beatmapInfo.File); setInfo.Beatmaps.Remove(beatmapInfo); + r.Remove(beatmapInfo.Metadata); + r.Remove(beatmapInfo); updateHashAndMarkDirty(setInfo); workingBeatmapCache.Invalidate(setInfo); @@ -400,6 +410,8 @@ namespace osu.Game.Beatmaps public Task Export(BeatmapSetInfo beatmap) => beatmapExporter.ExportAsync(beatmap.ToLive(Realm)); + public Task ExportLegacy(BeatmapSetInfo beatmap) => legacyBeatmapExporter.ExportAsync(beatmap.ToLive(Realm)); + private void updateHashAndMarkDirty(BeatmapSetInfo setInfo) { setInfo.Hash = beatmapImporter.ComputeHash(setInfo); @@ -429,8 +441,9 @@ namespace osu.Game.Beatmaps beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; beatmapInfo.ResetOnlineInfo(); - using (var stream = new MemoryStream()) + Realm.Write(r => { + using var stream = new MemoryStream(); using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); @@ -456,20 +469,20 @@ namespace osu.Game.Beatmaps updateHashAndMarkDirty(setInfo); - Realm.Write(r => - { - var liveBeatmapSet = r.Find(setInfo.ID); + var liveBeatmapSet = r.Find(setInfo.ID)!; - setInfo.CopyChangesToRealm(liveBeatmapSet); + setInfo.CopyChangesToRealm(liveBeatmapSet); - if (transferCollections) - beatmapInfo.TransferCollectionReferences(r, oldMd5Hash); + if (transferCollections) + beatmapInfo.TransferCollectionReferences(r, oldMd5Hash); - // do not look up metadata. - // this is a locally-modified set now, so looking up metadata is busy work at best and harmful at worst. - ProcessBeatmap?.Invoke(liveBeatmapSet, MetadataLookupScope.None); - }); - } + liveBeatmapSet.Beatmaps.Single(b => b.ID == beatmapInfo.ID) + .UpdateLocalScores(r); + + // do not look up metadata. + // this is a locally-modified set now, so looking up metadata is busy work at best and harmful at worst. + ProcessBeatmap?.Invoke(liveBeatmapSet, MetadataLookupScope.None); + }); Debug.Assert(beatmapInfo.BeatmapSet != null); diff --git a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs index fe4e815e62..be96a66614 100644 --- a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Localisation; diff --git a/osu.Game/Beatmaps/BeatmapOnlineStatus.cs b/osu.Game/Beatmaps/BeatmapOnlineStatus.cs index cdd99d4432..41393a8a39 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineStatus.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineStatus.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.ComponentModel; using osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs new file mode 100644 index 0000000000..acd60b664d --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs @@ -0,0 +1,95 @@ +// 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.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +namespace osu.Game.Beatmaps +{ + // Implementation of this class is based off of `MaxDimensionLimitedTextureLoaderStore`. + // If issues are found it's worth checking to make sure similar issues exist there. + public class BeatmapPanelBackgroundTextureLoaderStore : IResourceStore + { + // The aspect ratio of SetPanelBackground at its maximum size (very tall window). + private const float minimum_display_ratio = 512 / 80f; + + private readonly IResourceStore? textureStore; + + public BeatmapPanelBackgroundTextureLoaderStore(IResourceStore? textureStore) + { + this.textureStore = textureStore; + } + + public void Dispose() + { + textureStore?.Dispose(); + } + + public TextureUpload Get(string name) + { + var textureUpload = textureStore?.Get(name); + + // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp. + if (textureUpload == null) + return null!; + + return limitTextureUploadSize(textureUpload); + } + + public async Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) + { + // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp. + if (textureStore == null) + return null!; + + var textureUpload = await textureStore.GetAsync(name, cancellationToken).ConfigureAwait(false); + + if (textureUpload == null) + return null!; + + return await Task.Run(() => limitTextureUploadSize(textureUpload), cancellationToken).ConfigureAwait(false); + } + + private TextureUpload limitTextureUploadSize(TextureUpload textureUpload) + { + var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height); + + // The original texture upload will no longer be returned or used. + textureUpload.Dispose(); + + Size size = image.Size(); + + // Assume that panel backgrounds are always displayed using `FillMode.Fill`. + // Also assume that all backgrounds are wider than they are tall, so the + // fill is always going to be based on width. + // + // We need to include enough height to make this work for all ratio panels are displayed at. + int usableHeight = (int)Math.Ceiling(size.Width * 1 / minimum_display_ratio); + + usableHeight = Math.Min(size.Height, usableHeight); + + // Crop the centre region of the background for now. + Rectangle cropRectangle = new Rectangle( + 0, + (size.Height - usableHeight) / 2, + size.Width, + usableHeight + ); + + image.Mutate(i => i.Crop(cropRectangle)); + + return new TextureUpload(image); + } + + public Stream? GetStream(string name) => textureStore?.GetStream(name); + + public IEnumerable GetAvailableResources() => textureStore?.GetAvailableResources() ?? Array.Empty(); + } +} diff --git a/osu.Game/Beatmaps/BeatmapSetHypeStatus.cs b/osu.Game/Beatmaps/BeatmapSetHypeStatus.cs index b2d4cac210..4dd08203fc 100644 --- a/osu.Game/Beatmaps/BeatmapSetHypeStatus.cs +++ b/osu.Game/Beatmaps/BeatmapSetHypeStatus.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Newtonsoft.Json; namespace osu.Game.Beatmaps diff --git a/osu.Game/Beatmaps/BeatmapSetNominationStatus.cs b/osu.Game/Beatmaps/BeatmapSetNominationStatus.cs index cea0063814..8cf43ab320 100644 --- a/osu.Game/Beatmaps/BeatmapSetNominationStatus.cs +++ b/osu.Game/Beatmaps/BeatmapSetNominationStatus.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Newtonsoft.Json; namespace osu.Game.Beatmaps diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineAvailability.cs b/osu.Game/Beatmaps/BeatmapSetOnlineAvailability.cs index 12424b797c..8d519158b6 100644 --- a/osu.Game/Beatmaps/BeatmapSetOnlineAvailability.cs +++ b/osu.Game/Beatmaps/BeatmapSetOnlineAvailability.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Newtonsoft.Json; namespace osu.Game.Beatmaps diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs b/osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs index ad2e994d3e..e727e2c37f 100644 --- a/osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs +++ b/osu.Game/Beatmaps/BeatmapSetOnlineGenre.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 - namespace osu.Game.Beatmaps { public struct BeatmapSetOnlineGenre diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs b/osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs index c71c279086..5656fab721 100644 --- a/osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs +++ b/osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Beatmaps { public struct BeatmapSetOnlineLanguage diff --git a/osu.Game/Beatmaps/BeatmapStatisticIcon.cs b/osu.Game/Beatmaps/BeatmapStatisticIcon.cs index ca07e5f365..b2d59646ae 100644 --- a/osu.Game/Beatmaps/BeatmapStatisticIcon.cs +++ b/osu.Game/Beatmaps/BeatmapStatisticIcon.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; diff --git a/osu.Game/Beatmaps/BeatmapUpdater.cs b/osu.Game/Beatmaps/BeatmapUpdater.cs index 046adb8327..56bfdc5001 100644 --- a/osu.Game/Beatmaps/BeatmapUpdater.cs +++ b/osu.Game/Beatmaps/BeatmapUpdater.cs @@ -52,7 +52,7 @@ namespace osu.Game.Beatmaps /// /// 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(r => + public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapSet.Realm!.Write(_ => { // Before we use below, we want to invalidate. workingBeatmapCache.Invalidate(beatmapSet); diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index e6f1609d7f..05230c85f4 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -23,11 +23,16 @@ namespace osu.Game.Beatmaps.ControlPoints /// public readonly BindableDouble SliderVelocityBindable = new BindableDouble(1) { - Precision = 0.01, MinValue = 0.1, MaxValue = 10 }; + /// + /// Whether or not slider ticks should be generated at this control point. + /// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991). + /// + public bool GenerateTicks { get; set; } = true; + public override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1; /// @@ -41,11 +46,13 @@ namespace osu.Game.Beatmaps.ControlPoints public override bool IsRedundant(ControlPoint? existing) => existing is DifficultyControlPoint existingDifficulty + && GenerateTicks == existingDifficulty.GenerateTicks && SliderVelocity == existingDifficulty.SliderVelocity; public override void CopyFrom(ControlPoint other) { SliderVelocity = ((DifficultyControlPoint)other).SliderVelocity; + GenerateTicks = ((DifficultyControlPoint)other).GenerateTicks; base.CopyFrom(other); } @@ -56,8 +63,10 @@ namespace osu.Game.Beatmaps.ControlPoints public bool Equals(DifficultyControlPoint? other) => base.Equals(other) + && GenerateTicks == other.GenerateTicks && SliderVelocity == other.SliderVelocity; - public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SliderVelocity); + // ReSharper disable once NonReadonlyMemberInGetHashCode + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SliderVelocity, GenerateTicks); } } diff --git a/osu.Game/Beatmaps/CountdownType.cs b/osu.Game/Beatmaps/CountdownType.cs index 7fb3de74fb..bbe9b648f7 100644 --- a/osu.Game/Beatmaps/CountdownType.cs +++ b/osu.Game/Beatmaps/CountdownType.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.ComponentModel; namespace osu.Game.Beatmaps diff --git a/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs index 5b9cf6846c..052fae160f 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.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 osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs index c353b9e904..f18355505a 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs @@ -19,6 +19,8 @@ namespace osu.Game.Beatmaps.Drawables { public partial class BeatmapSetOnlineStatusPill : CircularContainer, IHasTooltip { + private const double animation_duration = 400; + private BeatmapOnlineStatus status; public BeatmapOnlineStatus Status @@ -32,7 +34,12 @@ namespace osu.Game.Beatmaps.Drawables status = value; if (IsLoaded) + { + AutoSizeDuration = (float)animation_duration; + AutoSizeEasing = Easing.OutQuint; + updateState(); + } } } @@ -61,6 +68,8 @@ namespace osu.Game.Beatmaps.Drawables { Masking = true; + Alpha = 0; + Children = new Drawable[] { background = new Box @@ -83,21 +92,32 @@ namespace osu.Game.Beatmaps.Drawables protected override void LoadComplete() { base.LoadComplete(); + updateState(); + FinishTransforms(true); } private void updateState() { - Alpha = Status == BeatmapOnlineStatus.None ? 0 : 1; + if (Status == BeatmapOnlineStatus.None) + { + this.FadeOut(animation_duration, Easing.OutQuint); + return; + } - statusText.Text = Status.GetLocalisableDescription().ToUpper(); + this.FadeIn(animation_duration, Easing.OutQuint); + + Color4 statusTextColour; if (colourProvider != null) - statusText.Colour = status == BeatmapOnlineStatus.Graveyard ? colourProvider.Background1 : colourProvider.Background3; + statusTextColour = status == BeatmapOnlineStatus.Graveyard ? colourProvider.Background1 : colourProvider.Background3; else - statusText.Colour = status == BeatmapOnlineStatus.Graveyard ? colours.GreySeaFoamLight : Color4.Black; + statusTextColour = status == BeatmapOnlineStatus.Graveyard ? colours.GreySeaFoamLight : Color4.Black; - background.Colour = OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter; + statusText.FadeColour(statusTextColour, animation_duration, Easing.OutQuint); + background.FadeColour(OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter, animation_duration, Easing.OutQuint); + + statusText.Text = Status.GetLocalisableDescription().ToUpper(); } public LocalisableString TooltipText diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 94b2956b4e..25e42bcbf7 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -19,7 +19,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { public abstract partial class BeatmapCard : OsuClickableContainer, IHasContextMenu { - public const float TRANSITION_DURATION = 400; + public const float TRANSITION_DURATION = 340; public const float CORNER_RADIUS = 10; protected const float WIDTH = 430; @@ -89,6 +89,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards { switch (size) { + case BeatmapCardSize.Nano: + return new BeatmapCardNano(beatmapSet); + case BeatmapCardSize.Normal: return new BeatmapCardNormal(beatmapSet, allowExpansion); diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs index 84445dc14c..5d0e3891c9 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Graphics; diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDownloadProgressBar.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDownloadProgressBar.cs index 3737715a7d..5ea42fe4b1 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDownloadProgressBar.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDownloadProgressBar.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -30,10 +28,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards private readonly Box foregroundFill; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; [Resolved] - private OverlayColourProvider colourProvider { get; set; } + private OverlayColourProvider colourProvider { get; set; } = null!; public BeatmapCardDownloadProgressBar() { diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index 5c6f0c4ee1..175c15ea7b 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -106,12 +106,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards { new Drawable[] { - new OsuSpriteText + new TruncatingSpriteText { Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, - Truncate = true }, titleBadgeArea = new FillFlowContainer { @@ -140,21 +139,19 @@ namespace osu.Game.Beatmaps.Drawables.Cards { new[] { - new OsuSpriteText + new TruncatingSpriteText { Text = createArtistText(), Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, - Truncate = true }, Empty() }, } }, - new OsuSpriteText + new TruncatingSpriteText { RelativeSizeAxes = Axes.X, - Truncate = true, Text = BeatmapSet.Source, Shadow = false, Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs new file mode 100644 index 0000000000..4ab2b0c973 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs @@ -0,0 +1,169 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Beatmaps.Drawables.Cards +{ + public partial class BeatmapCardNano : BeatmapCard + { + protected override Drawable IdleContent => idleBottomContent; + protected override Drawable DownloadInProgressContent => downloadProgressBar; + + public override float Width + { + get => base.Width; + set + { + base.Width = value; + + if (LoadState >= LoadState.Ready) + buttonContainer.Width = value; + } + } + + private const float height = 60; + private const float width = 300; + + [Cached] + private readonly BeatmapCardContent content; + + private CollapsibleButtonContainer buttonContainer = null!; + + private FillFlowContainer idleBottomContent = null!; + private BeatmapCardDownloadProgressBar downloadProgressBar = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public BeatmapCardNano(APIBeatmapSet beatmapSet) + : base(beatmapSet, false) + { + content = new BeatmapCardContent(height); + } + + [BackgroundDependencyLoader] + private void load() + { + Width = width; + Height = height; + + Child = content.With(c => + { + c.MainContent = new Container + { + RelativeSizeAxes = Axes.X, + Height = height, + Children = new Drawable[] + { + buttonContainer = new CollapsibleButtonContainer(BeatmapSet) + { + Width = Width, + FavouriteState = { BindTarget = FavouriteState }, + ButtonsCollapsedWidth = 5, + ButtonsExpandedWidth = 30, + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new TruncatingSpriteText + { + Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), + Font = OsuFont.Default.With(size: 19, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + }, + new TruncatingSpriteText + { + Text = createArtistText(), + Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + }, + } + }, + new Container + { + Name = @"Bottom content", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Children = new Drawable[] + { + idleBottomContent = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 3), + AlwaysPresent = true, + Children = new Drawable[] + { + new LinkFlowContainer(s => + { + s.Shadow = false; + s.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold); + }).With(d => + { + d.AutoSizeAxes = Axes.Both; + d.Margin = new MarginPadding { Top = 2 }; + d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); + d.AddUserLink(BeatmapSet.Author); + }), + } + }, + downloadProgressBar = new BeatmapCardDownloadProgressBar + { + RelativeSizeAxes = Axes.X, + Height = 6, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = { BindTarget = DownloadTracker.State }, + Progress = { BindTarget = DownloadTracker.Progress } + } + } + } + } + } + } + }; + c.ExpandedContent = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 10, Vertical = 13 }, + Child = new BeatmapCardDifficultyList(BeatmapSet) + }; + c.Expanded.BindTarget = Expanded; + }); + } + + private LocalisableString createArtistText() + { + var romanisableArtist = new RomanisableString(BeatmapSet.ArtistUnicode, BeatmapSet.Artist); + return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist); + } + + protected override void UpdateState() + { + base.UpdateState(); + + bool showDetails = IsHovered; + + buttonContainer.ShowDetails.Value = showDetails; + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs index 720d892495..18e1584a98 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs @@ -107,12 +107,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards { new Drawable[] { - new OsuSpriteText + new TruncatingSpriteText { Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, - Truncate = true }, titleBadgeArea = new FillFlowContainer { @@ -141,12 +140,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards { new[] { - new OsuSpriteText + new TruncatingSpriteText { Text = createArtistText(), Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, - Truncate = true }, Empty() }, diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardSize.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardSize.cs index 098265506d..0b5acc4a05 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardSize.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardSize.cs @@ -8,6 +8,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards /// public enum BeatmapCardSize { + Nano, Normal, Extra } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs index c99d1f0c76..5a26a988fb 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -1,19 +1,17 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.Drawables.Cards.Buttons; -using osu.Game.Graphics; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Framework.Graphics.UserInterface; using osuTK; -using osuTK.Graphics; namespace osu.Game.Beatmaps.Drawables.Cards { @@ -27,7 +25,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards set => foreground.Padding = value; } - private readonly UpdateableOnlineBeatmapSetCover cover; + private readonly Box background; private readonly Container foreground; private readonly PlayButton playButton; private readonly CircularProgress progress; @@ -35,15 +33,22 @@ namespace osu.Game.Beatmaps.Drawables.Cards protected override Container Content => content; + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + public BeatmapCardThumbnail(APIBeatmapSet beatmapSetInfo) { InternalChildren = new Drawable[] { - cover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) + new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) { RelativeSizeAxes = Axes.Both, OnlineInfo = beatmapSetInfo }, + background = new Box + { + RelativeSizeAxes = Axes.Both + }, foreground = new Container { RelativeSizeAxes = Axes.Both, @@ -70,7 +75,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load() { progress.Colour = colourProvider.Highlight1; } @@ -91,7 +96,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards bool shouldDim = Dimmed.Value || playButton.Playing.Value; playButton.FadeTo(shouldDim ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - cover.FadeColour(shouldDim ? OsuColour.Gray(0.2f) : Color4.White, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + background.FadeColour(colourProvider.Background6.Opacity(shouldDim ? 0.8f : 0f), BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapSetFavouriteState.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapSetFavouriteState.cs index 8f8a47c199..4645ca822c 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapSetFavouriteState.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapSetFavouriteState.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Beatmaps.Drawables.Cards.Buttons; using osu.Game.Beatmaps.Drawables.Cards.Statistics; diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs index ee45d56b6e..e78fd651fe 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; diff --git a/osu.Game/Beatmaps/Drawables/Cards/ExpandedContentScrollContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/ExpandedContentScrollContainer.cs index 9a2a37a09a..b4298e493a 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/ExpandedContentScrollContainer.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/ExpandedContentScrollContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Input.Events; diff --git a/osu.Game/Beatmaps/Drawables/Cards/IconPill.cs b/osu.Game/Beatmaps/Drawables/Cards/IconPill.cs index 3cabbba98d..16be57ac95 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/IconPill.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/IconPill.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; diff --git a/osu.Game/Beatmaps/Drawables/Cards/Statistics/FavouritesStatistic.cs b/osu.Game/Beatmaps/Drawables/Cards/Statistics/FavouritesStatistic.cs index 439e6acd22..6a329011f3 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Statistics/FavouritesStatistic.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Statistics/FavouritesStatistic.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Humanizer; using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; diff --git a/osu.Game/Beatmaps/Drawables/Cards/Statistics/PlayCountStatistic.cs b/osu.Game/Beatmaps/Drawables/Cards/Statistics/PlayCountStatistic.cs index 45ab6ddb40..4ce37b8659 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Statistics/PlayCountStatistic.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Statistics/PlayCountStatistic.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Humanizer; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Beatmaps/Drawables/Cards/StoryboardIconPill.cs b/osu.Game/Beatmaps/Drawables/Cards/StoryboardIconPill.cs index 6de16da2b1..6cb19696f8 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/StoryboardIconPill.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/StoryboardIconPill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Beatmaps/Drawables/Cards/VideoIconPill.cs b/osu.Game/Beatmaps/Drawables/Cards/VideoIconPill.cs index 63b5e95b12..b96ed23826 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/VideoIconPill.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/VideoIconPill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs index efce0f80f1..2fb3a8eee4 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; using osu.Framework.Allocation; diff --git a/osu.Game/Beatmaps/Drawables/DownloadProgressBar.cs b/osu.Game/Beatmaps/Drawables/DownloadProgressBar.cs index 9877b628db..55af57a45f 100644 --- a/osu.Game/Beatmaps/Drawables/DownloadProgressBar.cs +++ b/osu.Game/Beatmaps/Drawables/DownloadProgressBar.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; diff --git a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs index 36fff1dc3c..df8953d57c 100644 --- a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -47,10 +45,10 @@ namespace osu.Game.Beatmaps.Drawables public IBindable DisplayedStars => displayedStars; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; - [Resolved(canBeNull: true)] - private OverlayColourProvider colourProvider { get; set; } + [Resolved] + private OverlayColourProvider? colourProvider { get; set; } /// /// Creates a new using an already computed . diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs index 2cd9785048..0bb60847e5 100644 --- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; @@ -21,7 +19,7 @@ namespace osu.Game.Beatmaps.Drawables protected override double LoadDelay => 500; [Resolved] - private BeatmapManager beatmaps { get; set; } + private BeatmapManager beatmaps { get; set; } = null!; private readonly BeatmapSetCoverType beatmapSetCoverType; @@ -41,7 +39,7 @@ namespace osu.Game.Beatmaps.Drawables protected override double TransformDuration => 400; - protected override Drawable CreateDrawable(IBeatmapInfo model) + protected override Drawable CreateDrawable(IBeatmapInfo? model) { var drawable = getDrawableForModel(model); drawable.RelativeSizeAxes = Axes.Both; @@ -52,7 +50,7 @@ namespace osu.Game.Beatmaps.Drawables return drawable; } - private Drawable getDrawableForModel(IBeatmapInfo model) + private Drawable getDrawableForModel(IBeatmapInfo? model) { // prefer online cover where available. if (model?.BeatmapSet is IBeatmapSetOnlineInfo online) diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index 8089d789c1..d254945a51 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -36,9 +36,10 @@ namespace osu.Game.Beatmaps BeatmapSet = new BeatmapSetInfo(), Difficulty = new BeatmapDifficulty { - DrainRate = 0, CircleSize = 0, + DrainRate = 0, OverallDifficulty = 0, + ApproachRate = 0, }, Ruleset = new DummyRuleset().RulesetInfo }, audio) @@ -69,9 +70,9 @@ namespace osu.Game.Beatmaps throw new NotImplementedException(); } - public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new DummyBeatmapConverter { Beatmap = beatmap }; + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new DummyBeatmapConverter(beatmap); - public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null; + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException(); public override string Description => "dummy"; @@ -79,9 +80,15 @@ namespace osu.Game.Beatmaps private class DummyBeatmapConverter : IBeatmapConverter { - public event Action> ObjectConverted; + public IBeatmap Beatmap { get; } - public IBeatmap Beatmap { get; set; } + public DummyBeatmapConverter(IBeatmap beatmap) + { + Beatmap = beatmap; + } + + [CanBeNull] + public event Action> ObjectConverted; public bool CanConvert() => true; diff --git a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs b/osu.Game/Beatmaps/FlatWorkingBeatmap.cs similarity index 72% rename from osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs rename to osu.Game/Beatmaps/FlatWorkingBeatmap.cs index 0b53278ab3..c2505ec109 100644 --- a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/FlatWorkingBeatmap.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.IO; using osu.Framework.Audio.Track; @@ -14,25 +12,26 @@ using osu.Game.Skinning; namespace osu.Game.Beatmaps { /// - /// A which can be constructed directly from a .osu file, providing an implementation for + /// A which can be constructed directly from an .osu file (via ) + /// or an instance (via , + /// providing an implementation for /// . /// - public class FlatFileWorkingBeatmap : WorkingBeatmap + public class FlatWorkingBeatmap : WorkingBeatmap { - private readonly Beatmap beatmap; + private readonly IBeatmap beatmap; - public FlatFileWorkingBeatmap(string file, int? beatmapId = null) - : this(readFromFile(file), beatmapId) + public FlatWorkingBeatmap(string file, int? beatmapId = null) + : this(readFromFile(file)) { + if (beatmapId.HasValue) + beatmap.BeatmapInfo.OnlineID = beatmapId.Value; } - private FlatFileWorkingBeatmap(Beatmap beatmap, int? beatmapId = null) + public FlatWorkingBeatmap(IBeatmap beatmap) : base(beatmap.BeatmapInfo, null) { this.beatmap = beatmap; - - if (beatmapId.HasValue) - beatmap.BeatmapInfo.OnlineID = beatmapId.Value; } private static Beatmap readFromFile(string filename) diff --git a/osu.Game/Beatmaps/Formats/Decoder.cs b/osu.Game/Beatmaps/Formats/Decoder.cs index 4f0f11d053..c007f5dcdc 100644 --- a/osu.Game/Beatmaps/Formats/Decoder.cs +++ b/osu.Game/Beatmaps/Formats/Decoder.cs @@ -1,13 +1,10 @@ // 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.Collections.Generic; using System.IO; using System.Linq; -using JetBrains.Annotations; using osu.Game.IO; using osu.Game.Rulesets; @@ -45,7 +42,7 @@ namespace osu.Game.Beatmaps.Formats /// Register dependencies for use with static decoder classes. /// /// A store containing all available rulesets (used by ). - public static void RegisterDependencies([NotNull] RulesetStore rulesets) + public static void RegisterDependencies(RulesetStore rulesets) { LegacyBeatmapDecoder.RulesetStore = rulesets ?? throw new ArgumentNullException(nameof(rulesets)); } @@ -63,7 +60,7 @@ namespace osu.Game.Beatmaps.Formats throw new IOException(@"Unknown decoder type"); // start off with the first line of the file - string line = stream.PeekLine()?.Trim(); + string? line = stream.PeekLine()?.Trim(); while (line != null && line.Length == 0) { diff --git a/osu.Game/Beatmaps/Formats/IHasComboColours.cs b/osu.Game/Beatmaps/Formats/IHasComboColours.cs index 1d9cc0be65..1608adee7d 100644 --- a/osu.Game/Beatmaps/Formats/IHasComboColours.cs +++ b/osu.Game/Beatmaps/Formats/IHasComboColours.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.Collections.Generic; using osuTK.Graphics; @@ -13,7 +11,7 @@ namespace osu.Game.Beatmaps.Formats /// /// Retrieves the list of combo colours for presentation only. /// - IReadOnlyList ComboColours { get; } + IReadOnlyList? ComboColours { get; } /// /// The list of custom combo colours. diff --git a/osu.Game/Beatmaps/Formats/IHasCustomColours.cs b/osu.Game/Beatmaps/Formats/IHasCustomColours.cs index b651ef9515..9f2eeff253 100644 --- a/osu.Game/Beatmaps/Formats/IHasCustomColours.cs +++ b/osu.Game/Beatmaps/Formats/IHasCustomColours.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osuTK.Graphics; diff --git a/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs index 4f292a9a1f..a758e10f7c 100644 --- a/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.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 osu.Game.IO; using osu.Game.IO.Serialization; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 65a01befb4..8c5e4971d5 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -1,10 +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 - -#pragma warning disable 618 - using System; using System.Collections.Generic; using System.IO; @@ -32,15 +28,18 @@ namespace osu.Game.Beatmaps.Formats public const int EARLY_VERSION_TIMING_OFFSET = 24; /// - /// A small adjustment to the start time of control points to account for rounding/precision errors. + /// A small adjustment to the start time of sample control points to account for rounding/precision errors. /// - private const double control_point_leniency = 1; + /// + /// Compare: https://github.com/peppy/osu-stable-reference/blob/master/osu!/GameplayElements/HitObjects/HitObject.cs#L319 + /// + private const double control_point_leniency = 5; - internal static RulesetStore RulesetStore; + internal static RulesetStore? RulesetStore; - private Beatmap beatmap; + private Beatmap beatmap = null!; - private ConvertHitObjectParser parser; + private ConvertHitObjectParser? parser; private LegacySampleBank defaultSampleBank; private int defaultSampleVolume = 100; @@ -105,15 +104,11 @@ namespace osu.Game.Beatmaps.Formats { DifficultyControlPoint difficultyControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.DifficultyPointAt(hitObject.StartTime) ?? DifficultyControlPoint.DEFAULT; - if (difficultyControlPoint is LegacyDifficultyControlPoint legacyDifficultyControlPoint) - { - hitObject.LegacyBpmMultiplier = legacyDifficultyControlPoint.BpmMultiplier; - if (hitObject is IHasGenerateTicks hasGenerateTicks) - hasGenerateTicks.GenerateTicks = legacyDifficultyControlPoint.GenerateTicks; - } + if (hitObject is IHasGenerateTicks hasGenerateTicks) + hasGenerateTicks.GenerateTicks = difficultyControlPoint.GenerateTicks; if (hitObject is IHasSliderVelocity hasSliderVelocity) - hasSliderVelocity.SliderVelocity = difficultyControlPoint.SliderVelocity; + hasSliderVelocity.SliderVelocityMultiplier = difficultyControlPoint.SliderVelocity; hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); } @@ -204,7 +199,8 @@ namespace osu.Game.Beatmaps.Formats break; case @"PreviewTime": - metadata.PreviewTime = getOffsetTime(Parsing.ParseInt(pair.Value)); + int time = Parsing.ParseInt(pair.Value); + metadata.PreviewTime = time == -1 ? time : getOffsetTime(time); break; case @"SampleSet": @@ -222,7 +218,7 @@ namespace osu.Game.Beatmaps.Formats case @"Mode": int rulesetID = Parsing.ParseInt(pair.Value); - beatmap.BeatmapInfo.Ruleset = RulesetStore.GetRuleset(rulesetID) ?? throw new ArgumentException("Ruleset is not available locally."); + beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(rulesetID) ?? throw new ArgumentException("Ruleset is not available locally."); switch (rulesetID) { @@ -499,8 +495,9 @@ namespace osu.Game.Beatmaps.Formats int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID; - addControlPoint(time, new LegacyDifficultyControlPoint(onlineRulesetID, beatLength) + addControlPoint(time, new DifficultyControlPoint { + GenerateTicks = !double.IsNaN(beatLength), SliderVelocity = speedMultiplier, }, timingChange); diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index a5fc815a5e..4f8e935ee4 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -1,15 +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.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; -using JetBrains.Annotations; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; @@ -34,8 +31,7 @@ namespace osu.Game.Beatmaps.Formats private readonly IBeatmap beatmap; - [CanBeNull] - private readonly ISkin skin; + private readonly ISkin? skin; private readonly int onlineRulesetID; @@ -44,7 +40,7 @@ namespace osu.Game.Beatmaps.Formats /// /// The beatmap to encode. /// The beatmap's skin, used for encoding combo colours. - public LegacyBeatmapEncoder(IBeatmap beatmap, [CanBeNull] ISkin skin) + public LegacyBeatmapEncoder(IBeatmap beatmap, ISkin? skin) { this.beatmap = beatmap; this.skin = skin; @@ -93,7 +89,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}")); writer.WriteLine(FormattableString.Invariant($"Countdown: {(int)beatmap.BeatmapInfo.Countdown}")); writer.WriteLine(FormattableString.Invariant( - $"SampleSet: {toLegacySampleBank(((beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePoints?.FirstOrDefault() ?? SampleControlPoint.DEFAULT).SampleBank)}")); + $"SampleSet: {toLegacySampleBank(((beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePoints.FirstOrDefault() ?? SampleControlPoint.DEFAULT).SampleBank)}")); writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.BeatmapInfo.StackLeniency}")); writer.WriteLine(FormattableString.Invariant($"Mode: {onlineRulesetID}")); writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? '1' : '0')}")); @@ -180,8 +176,8 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine("[TimingPoints]"); - SampleControlPoint lastRelevantSamplePoint = null; - DifficultyControlPoint lastRelevantDifficultyPoint = null; + SampleControlPoint? lastRelevantSamplePoint = null; + DifficultyControlPoint? lastRelevantDifficultyPoint = null; // In osu!taiko and osu!mania, a scroll speed is stored as "slider velocity" in legacy formats. // In that case, a scrolling speed change is a global effect and per-hit object difficulty control points are ignored. @@ -273,7 +269,7 @@ namespace osu.Game.Beatmaps.Formats foreach (var hitObject in hitObjects) { if (hitObject is IHasSliderVelocity hasSliderVelocity) - yield return new DifficultyControlPoint { Time = hitObject.StartTime, SliderVelocity = hasSliderVelocity.SliderVelocity }; + yield return new DifficultyControlPoint { Time = hitObject.StartTime, SliderVelocity = hasSliderVelocity.SliderVelocityMultiplier }; } } @@ -585,7 +581,7 @@ namespace osu.Game.Beatmaps.Formats return type; } - private LegacySampleBank toLegacySampleBank(string sampleBank) + private LegacySampleBank toLegacySampleBank(string? sampleBank) { switch (sampleBank?.ToLowerInvariant()) { @@ -603,7 +599,7 @@ namespace osu.Game.Beatmaps.Formats } } - private int toLegacyCustomSampleBank(HitSampleInfo hitSampleInfo) + private int toLegacyCustomSampleBank(HitSampleInfo? hitSampleInfo) { if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy) return legacy.CustomSampleBank; diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 23440b8a1d..93af9cf41c 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -163,63 +163,6 @@ namespace osu.Game.Beatmaps.Formats Mania, } - [Obsolete("Do not use unless you're a legacy ruleset and 100% sure.")] - public class LegacyDifficultyControlPoint : DifficultyControlPoint, IEquatable - { - /// - /// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it. - /// DO NOT USE THIS UNLESS 100% SURE. - /// - public double BpmMultiplier { get; private set; } - - /// - /// Whether or not slider ticks should be generated at this control point. - /// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991). - /// - public bool GenerateTicks { get; private set; } = true; - - public LegacyDifficultyControlPoint(int rulesetId, double beatLength) - : this() - { - // Note: In stable, the division occurs on floats, but with compiler optimisations turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?). - if (rulesetId == 1 || rulesetId == 3) - BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1; - else - BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 1000) / 100.0 : 1; - - GenerateTicks = !double.IsNaN(beatLength); - } - - public LegacyDifficultyControlPoint() - { - SliderVelocityBindable.Precision = double.Epsilon; - } - - public override bool IsRedundant(ControlPoint? existing) - => base.IsRedundant(existing) - && GenerateTicks == ((existing as LegacyDifficultyControlPoint)?.GenerateTicks ?? true); - - public override void CopyFrom(ControlPoint other) - { - base.CopyFrom(other); - - BpmMultiplier = ((LegacyDifficultyControlPoint)other).BpmMultiplier; - GenerateTicks = ((LegacyDifficultyControlPoint)other).GenerateTicks; - } - - public override bool Equals(ControlPoint? other) - => other is LegacyDifficultyControlPoint otherLegacyDifficultyControlPoint - && Equals(otherLegacyDifficultyControlPoint); - - public bool Equals(LegacyDifficultyControlPoint? other) - => base.Equals(other) - && BpmMultiplier == other.BpmMultiplier - && GenerateTicks == other.GenerateTicks; - - // ReSharper disable twice NonReadonlyMemberInGetHashCode - public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), BpmMultiplier, GenerateTicks); - } - internal class LegacySampleControlPoint : SampleControlPoint, IEquatable { public int CustomSampleBank; diff --git a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs index bf69100361..b3815569ec 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; namespace osu.Game.Beatmaps.Formats diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index df5d3edb55..cf4700bf85 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.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.Collections.Generic; using System.IO; @@ -19,10 +17,10 @@ namespace osu.Game.Beatmaps.Formats { public class LegacyStoryboardDecoder : LegacyDecoder { - private StoryboardSprite storyboardSprite; - private CommandTimelineGroup timelineGroup; + private StoryboardSprite? storyboardSprite; + private CommandTimelineGroup? timelineGroup; - private Storyboard storyboard; + private Storyboard storyboard = null!; private readonly Dictionary variables = new Dictionary(); diff --git a/osu.Game/Beatmaps/Formats/Parsing.cs b/osu.Game/Beatmaps/Formats/Parsing.cs index 9b0d200077..a1683ced0d 100644 --- a/osu.Game/Beatmaps/Formats/Parsing.cs +++ b/osu.Game/Beatmaps/Formats/Parsing.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Globalization; diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 080b0ce7ec..587e6bbeed 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -5,7 +5,6 @@ using System; using System.Diagnostics; using osu.Framework; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Timing; @@ -28,16 +27,6 @@ namespace osu.Game.Beatmaps { private readonly bool applyOffsets; - /// - /// The length of the underlying beatmap track. Will default to 60 seconds if unavailable. - /// - public double TrackLength => Track.Length; - - /// - /// The underlying beatmap track, if available. - /// - public Track Track { get; private set; } = new TrackVirtual(60000); - /// /// The total frequency adjustment from pause transforms. Should eventually be handled in a better way. /// @@ -53,7 +42,7 @@ namespace osu.Game.Beatmaps private IDisposable? beatmapOffsetSubscription; - private readonly DecoupleableInterpolatingFramedClock decoupledClock; + private readonly DecouplingFramedClock decoupledTrack; [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -64,25 +53,23 @@ namespace osu.Game.Beatmaps [Resolved] private IBindable beatmap { get; set; } = null!; - public bool IsCoupled - { - get => decoupledClock.IsCoupled; - set => decoupledClock.IsCoupled = value; - } + public bool IsRewinding { get; private set; } - public FramedBeatmapClock(bool applyOffsets = false) + public FramedBeatmapClock(bool applyOffsets, bool requireDecoupling, IClock? source = null) { this.applyOffsets = applyOffsets; - // A decoupled clock is used to ensure precise time values even when the host audio subsystem is not reporting + decoupledTrack = new DecouplingFramedClock(source) { AllowDecoupling = requireDecoupling }; + + // An interpolating clock is used to ensure precise time values even when the host audio subsystem is not reporting // high precision times (on windows there's generally only 5-10ms reporting intervals, as an example). - decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; + var interpolatedTrack = new InterpolatingFramedClock(decoupledTrack); if (applyOffsets) { // Audio timings in general with newer BASS versions don't match stable. // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. - platformOffsetClock = new OffsetCorrectionClock(decoupledClock, ExternalPauseFrequencyAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; + platformOffsetClock = new OffsetCorrectionClock(interpolatedTrack, ExternalPauseFrequencyAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; // User global offset (set in settings) should also be applied. userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock, ExternalPauseFrequencyAdjust); @@ -92,7 +79,7 @@ namespace osu.Game.Beatmaps } else { - finalClockSource = decoupledClock; + finalClockSource = interpolatedTrack; } } @@ -108,6 +95,7 @@ namespace osu.Game.Beatmaps userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true); + // TODO: this doesn't update when using ChangeSource() to change beatmap. beatmapOffsetSubscription = realm.SubscribeToPropertyChanged( r => r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings, settings => settings.Offset, @@ -122,17 +110,10 @@ namespace osu.Game.Beatmaps { base.Update(); - if (Source != null && Source is not IAdjustableClock && Source.CurrentTime < decoupledClock.CurrentTime) - { - // InterpolatingFramedClock won't interpolate backwards unless its source has an ElapsedFrameTime. - // See https://github.com/ppy/osu-framework/blob/ba1385330cc501f34937e08257e586c84e35d772/osu.Framework/Timing/InterpolatingFramedClock.cs#L91-L93 - // This is not always the case here when doing large seeks. - // (Of note, this is not an issue if the source is adjustable, as the source is seeked to be in time by DecoupleableInterpolatingFramedClock). - // Rather than trying to get around this by fixing the framework clock stack, let's work around it for now. - Seek(Source.CurrentTime); - } - else - finalClockSource.ProcessFrame(); + finalClockSource.ProcessFrame(); + + if (Clock.ElapsedFrameTime != 0) + IsRewinding = Clock.ElapsedFrameTime < 0; } public double TotalAppliedOffset @@ -152,46 +133,42 @@ namespace osu.Game.Beatmaps #region Delegation of IAdjustableClock / ISourceChangeableClock to decoupled clock. - public void ChangeSource(IClock? source) - { - Track = source as Track ?? new TrackVirtual(60000); - decoupledClock.ChangeSource(source); - } + public void ChangeSource(IClock? source) => decoupledTrack.ChangeSource(source); - public IClock? Source => decoupledClock.Source; + public IClock Source => decoupledTrack.Source; public void Reset() { - decoupledClock.Reset(); + decoupledTrack.Reset(); finalClockSource.ProcessFrame(); } public void Start() { - decoupledClock.Start(); + decoupledTrack.Start(); finalClockSource.ProcessFrame(); } public void Stop() { - decoupledClock.Stop(); + decoupledTrack.Stop(); finalClockSource.ProcessFrame(); } public bool Seek(double position) { - bool success = decoupledClock.Seek(position - TotalAppliedOffset); + bool success = decoupledTrack.Seek(position - TotalAppliedOffset); finalClockSource.ProcessFrame(); return success; } - public void ResetSpeedAdjustments() => decoupledClock.ResetSpeedAdjustments(); + public void ResetSpeedAdjustments() => decoupledTrack.ResetSpeedAdjustments(); public double Rate { - get => decoupledClock.Rate; - set => decoupledClock.Rate = value; + get => decoupledTrack.Rate; + set => decoupledTrack.Rate = value; } #endregion @@ -211,8 +188,6 @@ namespace osu.Game.Beatmaps public double FramesPerSecond => finalClockSource.FramesPerSecond; - public FrameTimeInfo TimeInfo => finalClockSource.TimeInfo; - #endregion protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Beatmaps/IBeatSyncProvider.cs b/osu.Game/Beatmaps/IBeatSyncProvider.cs index 9ee19e720d..61fcf7f8e2 100644 --- a/osu.Game/Beatmaps/IBeatSyncProvider.cs +++ b/osu.Game/Beatmaps/IBeatSyncProvider.cs @@ -22,8 +22,8 @@ namespace osu.Game.Beatmaps ControlPointInfo? ControlPoints { get; } /// - /// Access a clock currently responsible for providing beat sync. If null, no current provider is available. + /// Access a clock currently responsible for providing beat sync. /// - IClock? Clock { get; } + IClock Clock { get; } } } diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 9dc3084cb5..d97eb00d7e 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; using osu.Game.Beatmaps.ControlPoints; @@ -112,6 +110,11 @@ namespace osu.Game.Beatmaps /// public static double CalculatePlayableLength(this IBeatmap beatmap) => CalculatePlayableLength(beatmap.HitObjects); + /// + /// Find the total milliseconds between the first and last hittable objects, excluding any break time. + /// + public static double CalculateDrainLength(this IBeatmap beatmap) => CalculatePlayableLength(beatmap.HitObjects) - beatmap.TotalBreakTime; + /// /// Find the timestamps in milliseconds of the start and end of the playable region. /// diff --git a/osu.Game/Beatmaps/IBeatmapConverter.cs b/osu.Game/Beatmaps/IBeatmapConverter.cs index f84188c5e2..2833af8ca2 100644 --- a/osu.Game/Beatmaps/IBeatmapConverter.cs +++ b/osu.Game/Beatmaps/IBeatmapConverter.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.Collections.Generic; using System.Threading; diff --git a/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs index 78234a9dd9..e7a3d87d0a 100644 --- a/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs @@ -55,13 +55,20 @@ namespace osu.Game.Beatmaps static double DifficultyRange(double difficulty, double min, double mid, double max) { if (difficulty > 5) - return mid + (max - mid) * (difficulty - 5) / 5; + return mid + (max - mid) * DifficultyRange(difficulty); if (difficulty < 5) - return mid - (mid - min) * (5 - difficulty) / 5; + return mid + (mid - min) * DifficultyRange(difficulty); return mid; } + /// + /// Maps a difficulty value [0, 10] to a linear range of [-1, 1]. + /// + /// The difficulty value to be mapped. + /// Value to which the difficulty value maps in the specified range. + static double DifficultyRange(double difficulty) => (difficulty - 5) / 5; + /// /// Maps a difficulty value [0, 10] to a two-piece linear range of values. /// diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs index 4f2c08f63d..b8c69cc525 100644 --- a/osu.Game/Beatmaps/IBeatmapInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapInfo.cs @@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps IBeatmapSetInfo? BeatmapSet { get; } /// - /// The playable length in milliseconds of this beatmap. + /// The total length in milliseconds of this beatmap. /// double Length { get; } diff --git a/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs b/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs index e1634e7d24..707a0696ba 100644 --- a/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs @@ -59,5 +59,10 @@ namespace osu.Game.Beatmaps int PassCount { get; } APIFailTimes? FailTimes { get; } + + /// + /// The playable length in milliseconds of this beatmap. + /// + double HitLength { get; } } } diff --git a/osu.Game/Beatmaps/IBeatmapProcessor.cs b/osu.Game/Beatmaps/IBeatmapProcessor.cs index 0a4a98c606..014dccf5e3 100644 --- a/osu.Game/Beatmaps/IBeatmapProcessor.cs +++ b/osu.Game/Beatmaps/IBeatmapProcessor.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 osu.Game.Rulesets; using osu.Game.Rulesets.Objects; diff --git a/osu.Game/Beatmaps/IBeatmapResourceProvider.cs b/osu.Game/Beatmaps/IBeatmapResourceProvider.cs index 22ff7ce8c8..fe9dada9d5 100644 --- a/osu.Game/Beatmaps/IBeatmapResourceProvider.cs +++ b/osu.Game/Beatmaps/IBeatmapResourceProvider.cs @@ -1,21 +1,24 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Game.IO; namespace osu.Game.Beatmaps { - public interface IBeatmapResourceProvider : IStorageResourceProvider + internal interface IBeatmapResourceProvider : IStorageResourceProvider { /// /// Retrieve a global large texture store, used for loading beatmap backgrounds. /// TextureStore LargeTextureStore { get; } + /// + /// Retrieve a global large texture store, used specifically for retrieving cropped beatmap panel backgrounds. + /// + TextureStore BeatmapPanelTextureStore { get; } + /// /// Access a global track store for retrieving beatmap tracks from. /// diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index 4b0a498a56..bdfa6bdf6d 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -32,12 +32,12 @@ namespace osu.Game.Beatmaps /// /// Whether the Beatmap has finished loading. /// - public bool BeatmapLoaded { get; } + bool BeatmapLoaded { get; } /// /// Whether the Track has finished loading. /// - public bool TrackLoaded { get; } + bool TrackLoaded { get; } /// /// Retrieves the which this represents. @@ -49,6 +49,11 @@ namespace osu.Game.Beatmaps /// Texture GetBackground(); + /// + /// Retrieves a cropped background for this used for display on panels. + /// + Texture GetPanelBackground(); + /// /// Retrieves the for the of this . /// @@ -124,12 +129,12 @@ namespace osu.Game.Beatmaps /// /// Beings loading the contents of this asynchronously. /// - public void BeginAsyncLoad(); + void BeginAsyncLoad(); /// /// Cancels the asynchronous loading of the contents of this . /// - public void CancelAsyncLoad(); + void CancelAsyncLoad(); /// /// Reads the correct track restart point from beatmap metadata and sets looping to enabled. diff --git a/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs b/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs index 5c3c72c9e4..6dda18bc4d 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs @@ -1,10 +1,7 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Lists; using osu.Game.Beatmaps.ControlPoints; @@ -26,7 +23,6 @@ namespace osu.Game.Beatmaps.Legacy /// /// The time to find the sound control point at. /// The sound control point. - [NotNull] public SampleControlPoint SamplePointAt(double time) => BinarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT); /// @@ -42,7 +38,6 @@ namespace osu.Game.Beatmaps.Legacy /// /// The time to find the difficulty control point at. /// The difficulty control point. - [NotNull] public DifficultyControlPoint DifficultyPointAt(double time) => BinarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT); public override void Clear() diff --git a/osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs b/osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs index b3717c81dc..78368daf09 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; namespace osu.Game.Beatmaps.Legacy diff --git a/osu.Game/Beatmaps/Legacy/LegacyEventType.cs b/osu.Game/Beatmaps/Legacy/LegacyEventType.cs index 42d21d14f6..c4a9566110 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyEventType.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyEventType.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Beatmaps.Legacy { internal enum LegacyEventType diff --git a/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs b/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs index ec947c6dc2..07f170f996 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; namespace osu.Game.Beatmaps.Legacy diff --git a/osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs b/osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs index d1782ec862..808a85b621 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; namespace osu.Game.Beatmaps.Legacy diff --git a/osu.Game/Beatmaps/Legacy/LegacyMods.cs b/osu.Game/Beatmaps/Legacy/LegacyMods.cs index 27b2e313ec..747015d90a 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyMods.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyMods.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; namespace osu.Game.Beatmaps.Legacy @@ -40,6 +38,7 @@ namespace osu.Game.Beatmaps.Legacy Key1 = 1 << 26, Key3 = 1 << 27, Key2 = 1 << 28, + ScoreV2 = 1 << 29, Mirror = 1 << 30, } } diff --git a/osu.Game/Beatmaps/Legacy/LegacySampleBank.cs b/osu.Game/Beatmaps/Legacy/LegacySampleBank.cs index f8a57c3ac9..70eb941a73 100644 --- a/osu.Game/Beatmaps/Legacy/LegacySampleBank.cs +++ b/osu.Game/Beatmaps/Legacy/LegacySampleBank.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Beatmaps.Legacy { internal enum LegacySampleBank diff --git a/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs b/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs index 69d0c96b57..5352fb65ed 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Beatmaps.Legacy { internal enum LegacyStoryLayer diff --git a/osu.Game/Beatmaps/Timing/BreakPeriod.cs b/osu.Game/Beatmaps/Timing/BreakPeriod.cs index 274b56a862..4c90b16745 100644 --- a/osu.Game/Beatmaps/Timing/BreakPeriod.cs +++ b/osu.Game/Beatmaps/Timing/BreakPeriod.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 osu.Game.Screens.Play; namespace osu.Game.Beatmaps.Timing diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index a69859f724..25159996f3 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -66,6 +66,7 @@ namespace osu.Game.Beatmaps protected abstract IBeatmap GetBeatmap(); public abstract Texture GetBackground(); + public virtual Texture GetPanelBackground() => GetBackground(); protected abstract Track GetBeatmapTrack(); /// diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 94865ed8d0..2c500146c5 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -42,6 +42,7 @@ namespace osu.Game.Beatmaps private readonly AudioManager audioManager; private readonly IResourceStore resources; private readonly LargeTextureStore largeTextureStore; + private readonly LargeTextureStore beatmapPanelTextureStore; private readonly ITrackStore trackStore; private readonly IResourceStore files; @@ -58,6 +59,7 @@ namespace osu.Game.Beatmaps this.host = host; this.files = files; largeTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), host?.CreateTextureLoaderStore(files)); + beatmapPanelTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), new BeatmapPanelBackgroundTextureLoaderStore(host?.CreateTextureLoaderStore(files))); this.trackStore = trackStore; } @@ -84,7 +86,7 @@ namespace osu.Game.Beatmaps public event Action OnInvalidated; - public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) + public virtual WorkingBeatmap GetWorkingBeatmap([CanBeNull] BeatmapInfo beatmapInfo) { if (beatmapInfo?.BeatmapSet == null) return DefaultBeatmap; @@ -110,6 +112,7 @@ namespace osu.Game.Beatmaps #region IResourceStorageProvider TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; + TextureStore IBeatmapResourceProvider.BeatmapPanelTextureStore => beatmapPanelTextureStore; ITrackStore IBeatmapResourceProvider.Tracks => trackStore; IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer(); AudioManager IStorageResourceProvider.AudioManager => audioManager; @@ -160,7 +163,11 @@ namespace osu.Game.Beatmaps } } - public override Texture GetBackground() + public override Texture GetPanelBackground() => getBackgroundFromStore(resources.BeatmapPanelTextureStore); + + public override Texture GetBackground() => getBackgroundFromStore(resources.LargeTextureStore); + + private Texture getBackgroundFromStore(TextureStore store) { if (string.IsNullOrEmpty(Metadata?.BackgroundFile)) return null; @@ -168,7 +175,7 @@ namespace osu.Game.Beatmaps try { string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile); - var texture = resources.LargeTextureStore.Get(fileStorePath); + var texture = store.Get(fileStorePath); if (texture == null) { @@ -257,7 +264,7 @@ namespace osu.Game.Beatmaps if (beatmapFileStream == null) { Logger.Log($"Beatmap failed to load (file {BeatmapInfo.Path} not found on disk at expected location {fileStorePath})", level: LogLevel.Error); - return null; + return new Storyboard(); } using (var reader = new LineBufferedReader(beatmapFileStream)) diff --git a/osu.Game/Collections/CollectionDropdown.cs b/osu.Game/Collections/CollectionDropdown.cs index 19fa3a3d66..e435992381 100644 --- a/osu.Game/Collections/CollectionDropdown.cs +++ b/osu.Game/Collections/CollectionDropdown.cs @@ -59,7 +59,7 @@ namespace osu.Game.Collections Current.BindValueChanged(selectionChanged); } - private void collectionsChanged(IRealmCollection collections, ChangeSet? changes, Exception error) + private void collectionsChanged(IRealmCollection collections, ChangeSet? changes) { var selectedItem = SelectedItem?.Value?.Collection; @@ -188,7 +188,7 @@ namespace osu.Game.Collections { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - X = -OsuScrollContainer.SCROLL_BAR_HEIGHT, + X = -OsuScrollContainer.SCROLL_BAR_WIDTH, Scale = new Vector2(0.65f), Action = addOrRemove, }); diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index 0fdf196c4a..6fe38a3229 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -41,7 +41,7 @@ namespace osu.Game.Collections realmSubscription = realm.RegisterForNotifications(r => r.All().OrderBy(c => c.Name), collectionsChanged); } - private void collectionsChanged(IRealmCollection collections, ChangeSet? changes, Exception error) + private void collectionsChanged(IRealmCollection collections, ChangeSet? changes) { Items.Clear(); Items.AddRange(collections.AsEnumerable().Select(c => c.ToLive(realm))); diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 0ab0ff520d..596bb5d673 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -36,7 +36,13 @@ namespace osu.Game.Collections public DrawableCollectionListItem(Live item, bool isCreated) : base(item) { - ShowDragHandle.Value = item.IsManaged; + // For now we don't support rearranging and always use alphabetical sort. + // Change this to: + // + // ShowDragHandle.Value = item.IsManaged; + // + // if we want to support user sorting (but changes will need to be made to realm to persist). + ShowDragHandle.Value = false; } protected override Drawable CreateContent() => new ItemContent(Model); @@ -197,7 +203,7 @@ namespace osu.Game.Collections return true; } - private void deleteCollection() => collection.PerformWrite(c => c.Realm.Remove(c)); + private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c)); } } } diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index 36142cf26f..cc0f23d030 100644 --- a/osu.Game/Collections/ManageCollectionsDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -23,6 +23,9 @@ namespace osu.Game.Collections private AudioFilter lowPassFilter = null!; + protected override string PopInSampleName => @"UI/overlay-big-pop-in"; + protected override string PopOutSampleName => @"UI/overlay-big-pop-out"; + public ManageCollectionsDialog() { Anchor = Anchor.Centre; @@ -114,8 +117,6 @@ namespace osu.Game.Collections protected override void PopIn() { - base.PopIn(); - lowPassFilter.CutoffTo(300, 100, Easing.OutCubic); this.FadeIn(enter_duration, Easing.OutQuint); this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutQuint); diff --git a/osu.Game/Configuration/BackgroundSource.cs b/osu.Game/Configuration/BackgroundSource.cs index f08b29cffe..2d9ed6df2c 100644 --- a/osu.Game/Configuration/BackgroundSource.cs +++ b/osu.Game/Configuration/BackgroundSource.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Configuration/DevelopmentOsuConfigManager.cs b/osu.Game/Configuration/DevelopmentOsuConfigManager.cs index 33329002a9..c979ebf453 100644 --- a/osu.Game/Configuration/DevelopmentOsuConfigManager.cs +++ b/osu.Game/Configuration/DevelopmentOsuConfigManager.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Platform; namespace osu.Game.Configuration diff --git a/osu.Game/Configuration/DiscordRichPresenceMode.cs b/osu.Game/Configuration/DiscordRichPresenceMode.cs index 150d23447e..6bec4fcc74 100644 --- a/osu.Game/Configuration/DiscordRichPresenceMode.cs +++ b/osu.Game/Configuration/DiscordRichPresenceMode.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Configuration/HUDVisibilityMode.cs b/osu.Game/Configuration/HUDVisibilityMode.cs index 9c69f33220..5ba7cbbe99 100644 --- a/osu.Game/Configuration/HUDVisibilityMode.cs +++ b/osu.Game/Configuration/HUDVisibilityMode.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Configuration/InMemoryConfigManager.cs b/osu.Game/Configuration/InMemoryConfigManager.cs index d8879daa3f..ccf697f680 100644 --- a/osu.Game/Configuration/InMemoryConfigManager.cs +++ b/osu.Game/Configuration/InMemoryConfigManager.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 osu.Framework.Configuration; diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 365ad37f4c..5d2d782063 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; @@ -66,7 +64,13 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.Username, string.Empty); SetDefault(OsuSetting.Token, string.Empty); +#pragma warning disable CS0618 // Type or member is obsolete + // this default set MUST remain despite the setting being deprecated, because `SetDefault()` calls are implicitly used to declare the type returned for the lookup. + // if this is removed, the setting will be interpreted as a string, and `Migrate()` will fail due to cast failure. + // can be removed 20240618 SetDefault(OsuSetting.AutomaticallyDownloadWhenSpectating, false); +#pragma warning restore CS0618 // Type or member is obsolete + SetDefault(OsuSetting.AutomaticallyDownloadMissingBeatmaps, false); SetDefault(OsuSetting.SavePassword, false).ValueChanged += enabled => { @@ -131,6 +135,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true); SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true); SetDefault(OsuSetting.KeyOverlay, false); + SetDefault(OsuSetting.ReplaySettingsOverlay, true); SetDefault(OsuSetting.GameplayLeaderboard, true); SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true); @@ -178,6 +183,8 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f, 0f, 1f, 0.25f); SetDefault(OsuSetting.EditorShowHitMarkers, true); SetDefault(OsuSetting.EditorAutoSeekOnPlacement, true); + SetDefault(OsuSetting.EditorLimitedDistanceSnap, false); + SetDefault(OsuSetting.EditorShowSpeedChanges, false); SetDefault(OsuSetting.LastProcessedMetadataId, -1); @@ -215,6 +222,12 @@ namespace osu.Game.Configuration // migrations can be added here using a condition like: // if (combined < 20220103) { performMigration() } + if (combined < 20230918) + { +#pragma warning disable CS0618 // Type or member is obsolete + SetValue(OsuSetting.AutomaticallyDownloadMissingBeatmaps, Get(OsuSetting.AutomaticallyDownloadWhenSpectating)); // can be removed 20240618 +#pragma warning restore CS0618 // Type or member is obsolete + } } public override TrackedSettings CreateTrackedSettings() @@ -237,6 +250,12 @@ namespace osu.Game.Configuration value: disabledState ? CommonStrings.Disabled.ToLower() : CommonStrings.Enabled.ToLower(), shortcut: LookupKeyBindings(GlobalAction.ToggleGameplayMouseButtons)) ), + new TrackedSetting(OsuSetting.GameplayLeaderboard, state => new SettingDescription( + rawValue: state, + name: GlobalActionKeyBindingStrings.ToggleInGameLeaderboard, + value: state ? CommonStrings.Enabled.ToLower() : CommonStrings.Disabled.ToLower(), + shortcut: LookupKeyBindings(GlobalAction.ToggleInGameLeaderboard)) + ), new TrackedSetting(OsuSetting.HUDVisibilityMode, visibilityMode => new SettingDescription( rawValue: visibilityMode, name: GameplaySettingsStrings.HUDVisibilityMode, @@ -258,7 +277,7 @@ namespace osu.Game.Configuration string skinName = string.Empty; if (Guid.TryParse(skin, out var id)) - skinName = LookupSkinName(id) ?? string.Empty; + skinName = LookupSkinName(id); return new SettingDescription( rawValue: skinName, @@ -377,11 +396,18 @@ namespace osu.Game.Configuration EditorShowHitMarkers, EditorAutoSeekOnPlacement, DiscordRichPresence, + + [Obsolete($"Use {nameof(AutomaticallyDownloadMissingBeatmaps)} instead.")] // can be removed 20240318 AutomaticallyDownloadWhenSpectating, + ShowOnlineExplicitContent, LastProcessedMetadataId, SafeAreaConsiderations, ComboColourNormalisationAmount, ProfileCoverExpanded, + EditorLimitedDistanceSnap, + ReplaySettingsOverlay, + AutomaticallyDownloadMissingBeatmaps, + EditorShowSpeedChanges } } diff --git a/osu.Game/Configuration/RandomSelectAlgorithm.cs b/osu.Game/Configuration/RandomSelectAlgorithm.cs index 052c6b4c55..985b4a8c55 100644 --- a/osu.Game/Configuration/RandomSelectAlgorithm.cs +++ b/osu.Game/Configuration/RandomSelectAlgorithm.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 osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Configuration/ScalingMode.cs b/osu.Game/Configuration/ScalingMode.cs index e0ad59746b..3ad46d771c 100644 --- a/osu.Game/Configuration/ScalingMode.cs +++ b/osu.Game/Configuration/ScalingMode.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Configuration/ScreenshotFormat.cs b/osu.Game/Configuration/ScreenshotFormat.cs index 13d0b64fd2..f043781c45 100644 --- a/osu.Game/Configuration/ScreenshotFormat.cs +++ b/osu.Game/Configuration/ScreenshotFormat.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 osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Configuration/ScrollVisualisationMethod.cs b/osu.Game/Configuration/ScrollVisualisationMethod.cs index 111bb95e67..5f48fe8bfd 100644 --- a/osu.Game/Configuration/ScrollVisualisationMethod.cs +++ b/osu.Game/Configuration/ScrollVisualisationMethod.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.ComponentModel; namespace osu.Game.Configuration diff --git a/osu.Game/Configuration/SeasonalBackgroundMode.cs b/osu.Game/Configuration/SeasonalBackgroundMode.cs index 3e6d9e42aa..4ef71ef09c 100644 --- a/osu.Game/Configuration/SeasonalBackgroundMode.cs +++ b/osu.Game/Configuration/SeasonalBackgroundMode.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 276563e163..5e2f0c2128 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -6,6 +6,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; +using osu.Game.Overlays.Mods; namespace osu.Game.Configuration { @@ -21,6 +22,7 @@ namespace osu.Game.Configuration SetDefault(Static.LowBatteryNotificationShownOnce, false); SetDefault(Static.FeaturedArtistDisclaimerShownOnce, false); SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null); + SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null); SetDefault(Static.SeasonalBackgrounds, null); } @@ -56,5 +58,11 @@ namespace osu.Game.Configuration /// Used to debounce hover sounds game-wide to avoid volume saturation, especially in scrolling views with many UI controls like . /// LastHoverSoundPlaybackTime, + + /// + /// The last playback time in milliseconds of an on/off sample (from ). + /// Used to debounce on/off sounds game-wide to avoid volume saturation, especially in activating mod presets with many mods. + /// + LastModSelectPanelSamplePlaybackTime } } diff --git a/osu.Game/Configuration/SettingsStore.cs b/osu.Game/Configuration/SettingsStore.cs index fda7193fea..e5d2d572c8 100644 --- a/osu.Game/Configuration/SettingsStore.cs +++ b/osu.Game/Configuration/SettingsStore.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 osu.Game.Database; namespace osu.Game.Configuration diff --git a/osu.Game/Configuration/StorageConfigManager.cs b/osu.Game/Configuration/StorageConfigManager.cs index c8781918e1..40c0e70488 100644 --- a/osu.Game/Configuration/StorageConfigManager.cs +++ b/osu.Game/Configuration/StorageConfigManager.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Configuration; using osu.Framework.Platform; diff --git a/osu.Game/Database/BeatmapExporter.cs b/osu.Game/Database/BeatmapExporter.cs new file mode 100644 index 0000000000..f37c57dea5 --- /dev/null +++ b/osu.Game/Database/BeatmapExporter.cs @@ -0,0 +1,22 @@ +// 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.Platform; +using osu.Game.Beatmaps; + +namespace osu.Game.Database +{ + /// + /// Exporter for beatmap archives. + /// This is not for legacy purposes and works for lazer only. + /// + public class BeatmapExporter : LegacyArchiveExporter + { + public BeatmapExporter(Storage storage) + : base(storage) + { + } + + protected override string FileExtension => @".olz"; + } +} diff --git a/osu.Game/Database/BeatmapLookupCache.cs b/osu.Game/Database/BeatmapLookupCache.cs index d9bf0138dc..973c25ec4f 100644 --- a/osu.Game/Database/BeatmapLookupCache.cs +++ b/osu.Game/Database/BeatmapLookupCache.cs @@ -1,13 +1,10 @@ // 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 System.Linq; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -21,8 +18,7 @@ namespace osu.Game.Database /// The beatmap to lookup. /// An optional cancellation token. /// The populated beatmap, or null if the beatmap does not exist or the request could not be satisfied. - [ItemCanBeNull] - public Task GetBeatmapAsync(int beatmapId, CancellationToken token = default) => LookupAsync(beatmapId, token); + public Task GetBeatmapAsync(int beatmapId, CancellationToken token = default) => LookupAsync(beatmapId, token); /// /// Perform an API lookup on the specified beatmaps, populating a model. @@ -30,10 +26,10 @@ namespace osu.Game.Database /// The beatmaps to lookup. /// An optional cancellation token. /// The populated beatmaps. May include null results for failed retrievals. - public Task GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) => LookupAsync(beatmapIds, token); + public Task GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) => LookupAsync(beatmapIds, token); protected override GetBeatmapsRequest CreateRequest(IEnumerable ids) => new GetBeatmapsRequest(ids.ToArray()); - protected override IEnumerable RetrieveResults(GetBeatmapsRequest request) => request.Response?.Beatmaps; + protected override IEnumerable? RetrieveResults(GetBeatmapsRequest request) => request.Response?.Beatmaps; } } diff --git a/osu.Game/Database/EmptyRealmSet.cs b/osu.Game/Database/EmptyRealmSet.cs index 7db946d79f..02dfa50fe5 100644 --- a/osu.Game/Database/EmptyRealmSet.cs +++ b/osu.Game/Database/EmptyRealmSet.cs @@ -19,8 +19,8 @@ namespace osu.Game.Database IEnumerator IEnumerable.GetEnumerator() => emptySet.GetEnumerator(); public int Count => emptySet.Count; public T this[int index] => emptySet[index]; - public int IndexOf(object item) => emptySet.IndexOf((T)item); - public bool Contains(object item) => emptySet.Contains((T)item); + public int IndexOf(object? item) => item == null ? -1 : emptySet.IndexOf((T)item); + public bool Contains(object? item) => item != null && emptySet.Contains((T)item); public event NotifyCollectionChangedEventHandler? CollectionChanged { diff --git a/osu.Game/Database/ICanAcceptFiles.cs b/osu.Game/Database/ICanAcceptFiles.cs index da970a29d4..bdbcebeaf5 100644 --- a/osu.Game/Database/ICanAcceptFiles.cs +++ b/osu.Game/Database/ICanAcceptFiles.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.Collections.Generic; using System.Threading.Tasks; diff --git a/osu.Game/Database/IHasFiles.cs b/osu.Game/Database/IHasFiles.cs index 9f8ce05218..d64ac9b662 100644 --- a/osu.Game/Database/IHasFiles.cs +++ b/osu.Game/Database/IHasFiles.cs @@ -1,10 +1,7 @@ // 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 JetBrains.Annotations; namespace osu.Game.Database { @@ -15,7 +12,6 @@ namespace osu.Game.Database public interface IHasFiles where TFile : INamedFileInfo { - [NotNull] List Files { get; } string Hash { get; set; } diff --git a/osu.Game/Database/IHasGuidPrimaryKey.cs b/osu.Game/Database/IHasGuidPrimaryKey.cs index 9cf7cf0683..8520707bd2 100644 --- a/osu.Game/Database/IHasGuidPrimaryKey.cs +++ b/osu.Game/Database/IHasGuidPrimaryKey.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Newtonsoft.Json; using Realms; diff --git a/osu.Game/Database/IHasNamedFiles.cs b/osu.Game/Database/IHasNamedFiles.cs index 3524eb4c99..34f6560f75 100644 --- a/osu.Game/Database/IHasNamedFiles.cs +++ b/osu.Game/Database/IHasNamedFiles.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; namespace osu.Game.Database diff --git a/osu.Game/Database/IHasPrimaryKey.cs b/osu.Game/Database/IHasPrimaryKey.cs index 84709ccd26..51a49948fe 100644 --- a/osu.Game/Database/IHasPrimaryKey.cs +++ b/osu.Game/Database/IHasPrimaryKey.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.ComponentModel.DataAnnotations.Schema; using Newtonsoft.Json; diff --git a/osu.Game/Database/IHasRealmFiles.cs b/osu.Game/Database/IHasRealmFiles.cs index 79ea719583..b301bb04de 100644 --- a/osu.Game/Database/IHasRealmFiles.cs +++ b/osu.Game/Database/IHasRealmFiles.cs @@ -10,13 +10,15 @@ namespace osu.Game.Database /// /// A model that contains a list of files it is responsible for. /// - public interface IHasRealmFiles + public interface IHasRealmFiles : IHasNamedFiles { /// /// Available files in this model, with locally filenames. /// When performing lookups, consider using or to do case-insensitive lookups. /// - IList Files { get; } + new IList Files { get; } + + IEnumerable IHasNamedFiles.Files => Files; /// /// A combined hash representing the model, based on the files it contains. diff --git a/osu.Game/Database/IModelFileManager.cs b/osu.Game/Database/IModelFileManager.cs index c40b57f663..390be4a69d 100644 --- a/osu.Game/Database/IModelFileManager.cs +++ b/osu.Game/Database/IModelFileManager.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.IO; namespace osu.Game.Database diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs index 988178818d..ce79aac966 100644 --- a/osu.Game/Database/IModelManager.cs +++ b/osu.Game/Database/IModelManager.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; namespace osu.Game.Database diff --git a/osu.Game/Database/INamedFileInfo.cs b/osu.Game/Database/INamedFileInfo.cs index 9df4a0869c..d95f228440 100644 --- a/osu.Game/Database/INamedFileInfo.cs +++ b/osu.Game/Database/INamedFileInfo.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 osu.Game.IO; namespace osu.Game.Database diff --git a/osu.Game/Database/IPostNotifications.cs b/osu.Game/Database/IPostNotifications.cs index 8bb2c54945..205350b80c 100644 --- a/osu.Game/Database/IPostNotifications.cs +++ b/osu.Game/Database/IPostNotifications.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Overlays.Notifications; diff --git a/osu.Game/Database/ImportParameters.cs b/osu.Game/Database/ImportParameters.cs index 83ca0ac694..8d37597afc 100644 --- a/osu.Game/Database/ImportParameters.cs +++ b/osu.Game/Database/ImportParameters.cs @@ -21,5 +21,11 @@ namespace osu.Game.Database /// Whether this import should use hard links rather than file copy operations if available. /// public bool PreferHardLinks { get; set; } + + /// + /// If set to , this import will not respect . + /// This is useful for cases where an import must complete even if gameplay is in progress. + /// + public bool ImportImmediately { get; set; } } } diff --git a/osu.Game/Database/ImportTask.cs b/osu.Game/Database/ImportTask.cs index def20bc1fb..8f2752020b 100644 --- a/osu.Game/Database/ImportTask.cs +++ b/osu.Game/Database/ImportTask.cs @@ -46,9 +46,29 @@ namespace osu.Game.Database /// public ArchiveReader GetReader() { - return Stream != null - ? getReaderFrom(Stream) - : getReaderFrom(Path); + if (Stream == null) + { + if (ZipUtils.IsZipArchive(Path)) + return new ZipArchiveReader(File.Open(Path, FileMode.Open, FileAccess.Read, FileShare.Read), System.IO.Path.GetFileName(Path)); + if (Directory.Exists(Path)) + return new DirectoryArchiveReader(Path); + if (File.Exists(Path)) + return new SingleFileArchiveReader(Path); + + throw new InvalidFormatException($"{Path} is not a valid archive"); + } + + if (Stream is not MemoryStream memoryStream) + { + // Path used primarily in tests (converting `ManifestResourceStream`s to `MemoryStream`s). + memoryStream = new MemoryStream(Stream.ReadAllBytesToArray()); + Stream.Dispose(); + } + + if (ZipUtils.IsZipArchive(memoryStream)) + return new ZipArchiveReader(memoryStream, Path); + + return new MemoryStreamArchiveReader(memoryStream, Path); } /// @@ -60,43 +80,6 @@ namespace osu.Game.Database File.Delete(Path); } - /// - /// Creates an from a stream. - /// - /// A seekable stream containing the archive content. - /// A reader giving access to the archive's content. - private ArchiveReader getReaderFrom(Stream stream) - { - if (!(stream is MemoryStream memoryStream)) - { - // This isn't used in any current path. May need to reconsider for performance reasons (ie. if we don't expect the incoming stream to be copied out). - memoryStream = new MemoryStream(stream.ReadAllBytesToArray()); - stream.Dispose(); - } - - if (ZipUtils.IsZipArchive(memoryStream)) - return new ZipArchiveReader(memoryStream, Path); - - return new LegacyByteArrayReader(memoryStream.ToArray(), Path); - } - - /// - /// Creates an from a valid storage path. - /// - /// A file or folder path resolving the archive content. - /// A reader giving access to the archive's content. - private ArchiveReader getReaderFrom(string path) - { - if (ZipUtils.IsZipArchive(path)) - return new ZipArchiveReader(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read), System.IO.Path.GetFileName(path)); - if (Directory.Exists(path)) - return new LegacyDirectoryArchiveReader(path); - if (File.Exists(path)) - return new LegacyFileArchiveReader(path); - - throw new InvalidFormatException($"{path} is not a valid archive"); - } - public override string ToString() => System.IO.Path.GetFileName(Path); } } diff --git a/osu.Game/Database/LegacyArchiveExporter.cs b/osu.Game/Database/LegacyArchiveExporter.cs index 7689ffc13d..9805207591 100644 --- a/osu.Game/Database/LegacyArchiveExporter.cs +++ b/osu.Game/Database/LegacyArchiveExporter.cs @@ -39,7 +39,7 @@ namespace osu.Game.Database { cancellationToken.ThrowIfCancellationRequested(); - using (var stream = UserFileStorage.GetStream(file.File.GetStoragePath())) + using (var stream = GetFileContents(model, file)) { if (stream == null) { @@ -65,5 +65,7 @@ namespace osu.Game.Database } } } + + protected virtual Stream? GetFileContents(TModel model, INamedFileUsage file) => UserFileStorage.GetStream(file.File.GetStoragePath()); } } diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 4ee8c0636e..ece705f685 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -1,11 +1,25 @@ // 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.IO; +using System.Linq; +using System.Text; using osu.Framework.Platform; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Skinning; +using osuTK; namespace osu.Game.Database { + /// + /// Exporter for osu!stable legacy beatmap archives. + /// Converts all beatmaps in the set to legacy format and exports it as a legacy package. + /// public class LegacyBeatmapExporter : LegacyArchiveExporter { public LegacyBeatmapExporter(Storage storage) @@ -13,6 +27,90 @@ namespace osu.Game.Database { } + protected override Stream? GetFileContents(BeatmapSetInfo model, INamedFileUsage file) + { + var beatmapInfo = model.Beatmaps.SingleOrDefault(o => o.Hash == file.File.Hash); + + if (beatmapInfo == null) + return base.GetFileContents(model, file); + + // Read the beatmap contents and skin + using var contentStream = base.GetFileContents(model, file); + + if (contentStream == null) + return null; + + using var contentStreamReader = new LineBufferedReader(contentStream); + var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader); + + var workingBeatmap = new FlatWorkingBeatmap(beatmapContent); + var playableBeatmap = workingBeatmap.GetPlayableBeatmap(beatmapInfo.Ruleset); + + using var skinStream = base.GetFileContents(model, file); + + if (skinStream == null) + return null; + + using var skinStreamReader = new LineBufferedReader(skinStream); + var beatmapSkin = new LegacySkin(new SkinInfo(), null!) + { + Configuration = new LegacySkinDecoder().Decode(skinStreamReader) + }; + + // Convert beatmap elements to be compatible with legacy format + // So we truncate time and position values to integers, and convert paths with multiple segments to bezier curves + foreach (var controlPoint in playableBeatmap.ControlPointInfo.AllControlPoints) + controlPoint.Time = Math.Floor(controlPoint.Time); + + foreach (var hitObject in playableBeatmap.HitObjects) + { + // Truncate end time before truncating start time because end time is dependent on start time + if (hitObject is IHasDuration hasDuration && hitObject is not IHasPath) + hasDuration.Duration = Math.Floor(hasDuration.EndTime) - Math.Floor(hitObject.StartTime); + + hitObject.StartTime = Math.Floor(hitObject.StartTime); + + if (hitObject is not IHasPath hasPath) continue; + + // stable's hit object parsing expects the entire slider to use only one type of curve, + // and happens to use the last non-empty curve type read for the entire slider. + // this clear of the last control point type handles an edge case + // wherein the last control point of an otherwise-single-segment slider path has a different type than previous, + // which would lead to sliders being mangled when exported back to stable. + // normally, that would be handled by the `BezierConverter.ConvertToModernBezier()` call below, + // which outputs a slider path containing only Bezier control points, + // but a non-inherited last control point is (rightly) not considered to be starting a new segment, + // therefore it would fail to clear the `CountSegments() <= 1` check. + // by clearing explicitly we both fix the issue and avoid unnecessary conversions to Bezier. + if (hasPath.Path.ControlPoints.Count > 1) + hasPath.Path.ControlPoints[^1].Type = null; + + if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue; + + var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); + + // Truncate control points to integer positions + foreach (var pathControlPoint in newControlPoints) + { + pathControlPoint.Position = new Vector2( + (float)Math.Floor(pathControlPoint.Position.X), + (float)Math.Floor(pathControlPoint.Position.Y)); + } + + hasPath.Path.ControlPoints.Clear(); + hasPath.Path.ControlPoints.AddRange(newControlPoints); + } + + // Encode to legacy format + var stream = new MemoryStream(); + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin).Encode(sw); + + stream.Seek(0, SeekOrigin.Begin); + + return stream; + } + protected override string FileExtension => @".osz"; } } diff --git a/osu.Game/Database/LegacyBeatmapImporter.cs b/osu.Game/Database/LegacyBeatmapImporter.cs index 20add54949..a090698a68 100644 --- a/osu.Game/Database/LegacyBeatmapImporter.cs +++ b/osu.Game/Database/LegacyBeatmapImporter.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; @@ -34,9 +33,9 @@ namespace osu.Game.Database try { - if (!directoryStorage.GetFiles(string.Empty).ExcludeSystemFileNames().Any()) + if (!directoryStorage.GetFiles(string.Empty, "*.osu").Any()) { - // if a directory doesn't contain files, attempt looking for beatmaps inside of that directory. + // if a directory doesn't contain any beatmap files, look for further nested beatmap directories. // this is a special behaviour in stable for beatmaps only, see https://github.com/ppy/osu/issues/18615. foreach (string subDirectory in GetStableImportPaths(directoryStorage)) paths.Add(subDirectory); diff --git a/osu.Game/Database/MemoryCachingComponent.cs b/osu.Game/Database/MemoryCachingComponent.cs index 5d1a381f09..e98475efae 100644 --- a/osu.Game/Database/MemoryCachingComponent.cs +++ b/osu.Game/Database/MemoryCachingComponent.cs @@ -1,13 +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; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Statistics; @@ -19,8 +17,9 @@ namespace osu.Game.Database /// Currently not persisted between game sessions. /// public abstract partial class MemoryCachingComponent : Component + where TLookup : notnull { - private readonly ConcurrentDictionary cache = new ConcurrentDictionary(); + private readonly ConcurrentDictionary cache = new ConcurrentDictionary(); private readonly GlobalStatistic statistics; @@ -37,12 +36,12 @@ namespace osu.Game.Database /// /// The lookup to retrieve. /// An optional to cancel the operation. - protected async Task GetAsync([NotNull] TLookup lookup, CancellationToken token = default) + protected async Task GetAsync(TLookup lookup, CancellationToken token = default) { - if (CheckExists(lookup, out TValue performance)) + if (CheckExists(lookup, out TValue? existing)) { statistics.Value.HitCount++; - return performance; + return existing; } var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false); @@ -73,7 +72,7 @@ namespace osu.Game.Database statistics.Value.Usage = cache.Count; } - protected bool CheckExists([NotNull] TLookup lookup, out TValue value) => + protected bool CheckExists(TLookup lookup, [MaybeNullWhen(false)] out TValue value) => cache.TryGetValue(lookup, out value); /// @@ -82,7 +81,7 @@ namespace osu.Game.Database /// The lookup to retrieve. /// An optional to cancel the operation. /// The computed value. - protected abstract Task ComputeValueAsync(TLookup lookup, CancellationToken token = default); + protected abstract Task ComputeValueAsync(TLookup lookup, CancellationToken token = default); private class MemoryCachingStatistics { diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs new file mode 100644 index 0000000000..584b2675f3 --- /dev/null +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -0,0 +1,103 @@ +// 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.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Configuration; +using osu.Game.IO.Archives; +using osu.Game.Localisation; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays.Notifications; +using osu.Game.Scoring; +using Realms; + +namespace osu.Game.Database +{ + public partial class MissingBeatmapNotification : SimpleNotification + { + [Resolved] + private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; + + [Resolved] + private ScoreManager scoreManager { get; set; } = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + private readonly ArchiveReader scoreArchive; + private readonly APIBeatmapSet beatmapSetInfo; + private readonly string beatmapHash; + + private Bindable autoDownloadConfig = null!; + private Bindable noVideoSetting = null!; + private BeatmapCardNano card = null!; + + private IDisposable? realmSubscription; + + public MissingBeatmapNotification(APIBeatmap beatmap, ArchiveReader scoreArchive, string beatmapHash) + { + beatmapSetInfo = beatmap.BeatmapSet!; + + this.beatmapHash = beatmapHash; + this.scoreArchive = scoreArchive; + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + realmSubscription = realm.RegisterForNotifications( + realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); + + autoDownloadConfig = config.GetBindable(OsuSetting.AutomaticallyDownloadMissingBeatmaps); + noVideoSetting = config.GetBindable(OsuSetting.PreferNoVideo); + + Content.Add(card = new BeatmapCardNano(beatmapSetInfo)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (autoDownloadConfig.Value) + { + Text = NotificationsStrings.DownloadingBeatmapForReplay; + beatmapDownloader.Download(beatmapSetInfo, noVideoSetting.Value); + } + else + { + bool missingSetMatchesExistingOnlineId = realm.Run(r => r.All().Any(s => !s.DeletePending && s.OnlineID == beatmapSetInfo.OnlineID)); + Text = missingSetMatchesExistingOnlineId ? NotificationsStrings.MismatchingBeatmapForReplay : NotificationsStrings.MissingBeatmapForReplay; + } + } + + protected override void Update() + { + base.Update(); + card.Width = Content.DrawWidth; + } + + private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) + { + if (changes?.InsertedIndices == null) return; + + if (sender.Any(s => s.Beatmaps.Any(b => b.MD5Hash == beatmapHash))) + { + string name = scoreArchive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase)); + var importTask = new ImportTask(scoreArchive.GetStream(name), name); + scoreManager.Import(new[] { importTask }); + realmSubscription?.Dispose(); + Close(false); + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + realmSubscription?.Dispose(); + } + } +} diff --git a/osu.Game/Database/ModelManager.cs b/osu.Game/Database/ModelManager.cs index 7d1dc5239a..39dae61d36 100644 --- a/osu.Game/Database/ModelManager.cs +++ b/osu.Game/Database/ModelManager.cs @@ -34,13 +34,13 @@ namespace osu.Game.Database } public void DeleteFile(TModel item, RealmNamedFileUsage file) => - performFileOperation(item, managed => DeleteFile(managed, managed.Files.First(f => f.Filename == file.Filename), managed.Realm)); + performFileOperation(item, managed => DeleteFile(managed, managed.Files.First(f => f.Filename == file.Filename), managed.Realm!)); public void ReplaceFile(TModel item, RealmNamedFileUsage file, Stream contents) => - performFileOperation(item, managed => ReplaceFile(file, contents, managed.Realm)); + performFileOperation(item, managed => ReplaceFile(file, contents, managed.Realm!)); public void AddFile(TModel item, Stream contents, string filename) => - performFileOperation(item, managed => AddFile(managed, contents, filename, managed.Realm)); + performFileOperation(item, managed => AddFile(managed, contents, filename, managed.Realm!)); private void performFileOperation(TModel item, Action operation) { @@ -52,7 +52,7 @@ namespace osu.Game.Database // (ie. if an async import finished very recently). Realm.Realm.Write(realm => { - var managed = realm.Find(item.ID); + var managed = realm.FindWithRefresh(item.ID); Debug.Assert(managed != null); operation(managed); @@ -178,13 +178,14 @@ namespace osu.Game.Database // (ie. if an async import finished very recently). return Realm.Write(realm => { - if (!item.IsManaged) - item = realm.Find(item.ID); + TModel? processableItem = item; + if (!processableItem.IsManaged) + processableItem = realm.Find(item.ID); - if (item?.DeletePending != false) + if (processableItem?.DeletePending != false) return false; - item.DeletePending = true; + processableItem.DeletePending = true; return true; }); } @@ -195,13 +196,14 @@ namespace osu.Game.Database // (ie. if an async import finished very recently). Realm.Write(realm => { - if (!item.IsManaged) - item = realm.Find(item.ID); + TModel? processableItem = item; + if (!processableItem.IsManaged) + processableItem = realm.Find(item.ID); - if (item?.DeletePending != true) + if (processableItem?.DeletePending != true) return; - item.DeletePending = false; + processableItem.DeletePending = false; }); } diff --git a/osu.Game/Database/OnlineLookupCache.cs b/osu.Game/Database/OnlineLookupCache.cs index d9b37e2f29..3b54804fec 100644 --- a/osu.Game/Database/OnlineLookupCache.cs +++ b/osu.Game/Database/OnlineLookupCache.cs @@ -1,14 +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; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Game.Online.API; @@ -21,7 +18,7 @@ namespace osu.Game.Database where TRequest : APIRequest { [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; /// /// Creates an to retrieve the values for a given collection of s. @@ -32,8 +29,7 @@ namespace osu.Game.Database /// /// Retrieves a list of s from a successful created by . /// - [CanBeNull] - protected abstract IEnumerable RetrieveResults(TRequest request); + protected abstract IEnumerable? RetrieveResults(TRequest request); /// /// Perform a lookup using the specified , populating a . @@ -41,8 +37,7 @@ namespace osu.Game.Database /// The ID to lookup. /// An optional cancellation token. /// The populated , or null if the value does not exist or the request could not be satisfied. - [ItemCanBeNull] - protected Task LookupAsync(TLookup id, CancellationToken token = default) => GetAsync(id, token); + protected Task LookupAsync(TLookup id, CancellationToken token = default) => GetAsync(id, token); /// /// Perform an API lookup on the specified , populating a . @@ -50,9 +45,9 @@ namespace osu.Game.Database /// The IDs to lookup. /// An optional cancellation token. /// The populated values. May include null results for failed retrievals. - protected Task LookupAsync(TLookup[] ids, CancellationToken token = default) + protected Task LookupAsync(TLookup[] ids, CancellationToken token = default) { - var lookupTasks = new List>(); + var lookupTasks = new List>(); foreach (var id in ids) { @@ -69,18 +64,18 @@ namespace osu.Game.Database } // cannot be sealed due to test usages (see TestUserLookupCache). - protected override async Task ComputeValueAsync(TLookup lookup, CancellationToken token = default) + protected override async Task ComputeValueAsync(TLookup lookup, CancellationToken token = default) => await queryValue(lookup).ConfigureAwait(false); - private readonly Queue<(TLookup id, TaskCompletionSource)> pendingTasks = new Queue<(TLookup, TaskCompletionSource)>(); - private Task pendingRequestTask; + private readonly Queue<(TLookup id, TaskCompletionSource)> pendingTasks = new Queue<(TLookup, TaskCompletionSource)>(); + private Task? pendingRequestTask; private readonly object taskAssignmentLock = new object(); - private Task queryValue(TLookup id) + private Task queryValue(TLookup id) { lock (taskAssignmentLock) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); // Add to the queue. pendingTasks.Enqueue((id, tcs)); @@ -96,14 +91,14 @@ namespace osu.Game.Database private async Task performLookup() { // contains at most 50 unique IDs from tasks, which is used to perform the lookup. - var nextTaskBatch = new Dictionary>>(); + var nextTaskBatch = new Dictionary>>(); // Grab at most 50 unique IDs from the queue. lock (taskAssignmentLock) { while (pendingTasks.Count > 0 && nextTaskBatch.Count < 50) { - (TLookup id, TaskCompletionSource task) next = pendingTasks.Dequeue(); + (TLookup id, TaskCompletionSource task) next = pendingTasks.Dequeue(); // Perform a secondary check for existence, in case the value was queried in a previous batch. if (CheckExists(next.id, out var existing)) @@ -113,7 +108,7 @@ namespace osu.Game.Database if (nextTaskBatch.TryGetValue(next.id, out var tasks)) tasks.Add(next.task); else - nextTaskBatch[next.id] = new List> { next.task }; + nextTaskBatch[next.id] = new List> { next.task }; } } } diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index f4c6c802f1..e9f49ec662 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -15,20 +15,27 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Extensions; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Input.Bindings; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; using osu.Framework.Threading; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; +using osu.Game.Extensions; +using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Models; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; using osu.Game.Skinning; +using osuTK.Input; using Realms; using Realms.Exceptions; @@ -72,8 +79,17 @@ namespace osu.Game.Database /// 25 2022-09-18 Remove skins to add with new naming. /// 26 2023-02-05 Added BeatmapHash to ScoreInfo. /// 27 2023-06-06 Added EditorTimestamp to BeatmapInfo. + /// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files. + /// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes. + /// 30 2023-06-16 Run migration of old lazer scores again. This time with more correct rounding considerations. + /// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores. + /// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files. + /// 33 2023-08-16 Reset default chat toggle key binding to avoid conflict with newly added leaderboard toggle key binding. + /// 34 2023-08-21 Add BackgroundReprocessingFailed flag to ScoreInfo to track upgrade failures. + /// 35 2023-10-16 Clear key combinations of keybindings that are assigned to more than one action in a given settings section. + /// 36 2023-10-26 Add LegacyOnlineID to ScoreInfo. Move osu_scores_*_high IDs stored in OnlineID to LegacyOnlineID. Reset anomalous OnlineIDs. /// - private const int schema_version = 27; + private const int schema_version = 36; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -528,7 +544,7 @@ namespace osu.Game.Database lock (notificationsResetMap) { // Store an action which is used when blocking to ensure consumers don't use results of a stale changeset firing. - notificationsResetMap.Add(action, () => callback(new EmptyRealmSet(), null, null)); + notificationsResetMap.Add(action, () => callback(new EmptyRealmSet(), null)); } return RegisterCustomSubscription(action); @@ -720,6 +736,13 @@ namespace osu.Game.Database private void applyMigrationsForVersion(Migration migration, ulong targetVersion) { + Logger.Log($"Running realm migration to version {targetVersion}..."); + Stopwatch stopwatch = new Stopwatch(); + + var files = new RealmFileStore(this, storage); + + stopwatch.Start(); + switch (targetVersion) { case 7: @@ -743,10 +766,10 @@ namespace osu.Game.Database for (int i = 0; i < itemCount; i++) { - dynamic? oldItem = oldItems.ElementAt(i); - dynamic? newItem = newItems.ElementAt(i); + dynamic oldItem = oldItems.ElementAt(i); + dynamic newItem = newItems.ElementAt(i); - long? nullableOnlineID = oldItem?.OnlineID; + long? nullableOnlineID = oldItem.OnlineID; newItem.OnlineID = (int)(nullableOnlineID ?? -1); } } @@ -754,6 +777,7 @@ namespace osu.Game.Database break; case 8: + { // Ctrl -/+ now adjusts UI scale so let's clear any bindings which overlap these combinations. // New defaults will be populated by the key store afterwards. var keyBindings = migration.NewRealm.All(); @@ -767,6 +791,7 @@ namespace osu.Game.Database migration.NewRealm.Remove(decreaseSpeedBinding); break; + } case 9: // Pretty pointless to do this as beatmaps aren't really loaded via realm yet, but oh well. @@ -783,7 +808,7 @@ namespace osu.Game.Database for (int i = 0; i < metadataCount; i++) { - dynamic? oldItem = oldMetadata.ElementAt(i); + dynamic oldItem = oldMetadata.ElementAt(i); var newItem = newMetadata.ElementAt(i); string username = oldItem.Author; @@ -806,7 +831,7 @@ namespace osu.Game.Database for (int i = 0; i < newSettings.Count; i++) { - dynamic? oldItem = oldSettings.ElementAt(i); + dynamic oldItem = oldSettings.ElementAt(i); var newItem = newSettings.ElementAt(i); long rulesetId = oldItem.RulesetID; @@ -821,6 +846,7 @@ namespace osu.Game.Database break; case 11: + { string keyBindingClassName = getMappedOrOriginalName(typeof(RealmKeyBinding)); if (!migration.OldRealm.Schema.TryFindObjectSchema(keyBindingClassName, out _)) @@ -831,7 +857,7 @@ namespace osu.Game.Database for (int i = 0; i < newKeyBindings.Count; i++) { - dynamic? oldItem = oldKeyBindings.ElementAt(i); + dynamic oldItem = oldKeyBindings.ElementAt(i); var newItem = newKeyBindings.ElementAt(i); if (oldItem.RulesetID == null) @@ -847,6 +873,7 @@ namespace osu.Game.Database } break; + } case 14: foreach (var beatmap in migration.NewRealm.All()) @@ -880,14 +907,196 @@ namespace osu.Game.Database break; case 26: + { // Add ScoreInfo.BeatmapHash property to ensure scores correspond to the correct version of beatmap. var scores = migration.NewRealm.All(); foreach (var score in scores) - score.BeatmapHash = score.BeatmapInfo.Hash; + score.BeatmapHash = score.BeatmapInfo?.Hash ?? string.Empty; break; + } + + case 28: + { + var scores = migration.NewRealm.All(); + + foreach (var score in scores) + { + score.PopulateFromReplay(files, sr => + { + sr.ReadByte(); // Ruleset. + int version = sr.ReadInt32(); + if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION) + score.IsLegacyScore = true; + }); + } + + break; + } + + case 29: + case 30: + { + var scores = migration.NewRealm + .All() + .Where(s => !s.IsLegacyScore); + + foreach (var score in scores) + { + try + { + if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(score)) + { + try + { + long calculatedNew = StandardisedScoreMigrationTools.GetNewStandardised(score); + score.TotalScore = calculatedNew; + } + catch + { + } + } + } + catch { } + } + + break; + } + + case 31: + { + foreach (var score in migration.NewRealm.All()) + { + if (score.IsLegacyScore && score.Ruleset.IsLegacyRuleset()) + { + // Scores with this version will trigger the score upgrade process in BackgroundBeatmapProcessor. + score.TotalScoreVersion = 30000002; + + // Transfer known legacy scores to a permanent storage field for preservation. + score.LegacyTotalScore = score.TotalScore; + } + else + score.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION; + } + + break; + } + + case 32: + { + foreach (var score in migration.NewRealm.All()) + { + if (!score.IsLegacyScore || !score.Ruleset.IsLegacyRuleset()) + continue; + + score.PopulateFromReplay(files, sr => + { + sr.ReadByte(); // Ruleset. + sr.ReadInt32(); // Version. + sr.ReadString(); // Beatmap hash. + sr.ReadString(); // Username. + sr.ReadString(); // MD5Hash. + sr.ReadUInt16(); // Count300. + sr.ReadUInt16(); // Count100. + sr.ReadUInt16(); // Count50. + sr.ReadUInt16(); // CountGeki. + sr.ReadUInt16(); // CountKatu. + sr.ReadUInt16(); // CountMiss. + + // we should have this in LegacyTotalScore already, but if we're reading through this anyways... + int totalScore = sr.ReadInt32(); + + sr.ReadUInt16(); // Max combo. + sr.ReadBoolean(); // Perfect. + + var legacyMods = (LegacyMods)sr.ReadInt32(); + + if (!legacyMods.HasFlagFast(LegacyMods.ScoreV2) || score.APIMods.Any(mod => mod.Acronym == @"SV2")) + return; + + score.APIMods = score.APIMods.Append(new APIMod(new ModScoreV2())).ToArray(); + score.LegacyTotalScore = score.TotalScore = totalScore; + }); + } + + break; + } + + case 33: + { + // Clear default bindings for the chat focus toggle, + // as they would conflict with the newly-added leaderboard toggle. + var keyBindings = migration.NewRealm.All(); + + var toggleChatBind = keyBindings.FirstOrDefault(bind => bind.ActionInt == (int)GlobalAction.ToggleChatFocus); + if (toggleChatBind != null && toggleChatBind.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Tab })) + migration.NewRealm.Remove(toggleChatBind); + + break; + } + + case 35: + { + // catch used `Shift` twice as a default key combination for dash, which generally was bothersome and causes issues elsewhere. + // the duplicate binding logic below had to account for it, it could also break keybinding conflict resolution on revert-to-default. + // as such, detect this situation and fix it before proceeding further. + var catchDashBindings = migration.NewRealm.All() + .Where(kb => kb.RulesetName == @"fruits" && kb.ActionInt == 2) + .ToList(); + + if (catchDashBindings.All(kb => kb.KeyCombination.Equals(new KeyCombination(InputKey.Shift)))) + { + Debug.Assert(catchDashBindings.Count == 2); + catchDashBindings.Last().KeyCombination = KeyCombination.FromMouseButton(MouseButton.Left); + } + + // with the catch case dealt with, de-duplicate the remaining bindings. + int countCleared = 0; + + var globalBindings = migration.NewRealm.All().Where(kb => kb.RulesetName == null).ToList(); + + foreach (var category in Enum.GetValues()) + { + var categoryActions = GlobalActionContainer.GetGlobalActionsFor(category).Cast().ToHashSet(); + var categoryBindings = globalBindings.Where(kb => categoryActions.Contains(kb.ActionInt)); + countCleared += RealmKeyBindingStore.ClearDuplicateBindings(categoryBindings); + } + + var rulesetBindings = migration.NewRealm.All().Where(kb => kb.RulesetName != null).ToList(); + + foreach (var variantGroup in rulesetBindings.GroupBy(kb => (kb.RulesetName, kb.Variant))) + countCleared += RealmKeyBindingStore.ClearDuplicateBindings(variantGroup); + + if (countCleared > 0) + { + Logger.Log($"{countCleared} of your keybinding(s) have been cleared due to being bound to multiple actions. " + + "Please choose new unique ones in the settings panel.", level: LogLevel.Important); + } + + break; + } + + case 36: + { + foreach (var score in migration.NewRealm.All()) + { + if (score.OnlineID > 0) + { + score.LegacyOnlineID = score.OnlineID; + score.OnlineID = -1; + } + else + { + score.LegacyOnlineID = score.OnlineID = -1; + } + } + + break; + } } + + Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); } private string? getRulesetShortNameFromLegacyID(long rulesetId) diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs index 9d06c14b4b..5383040eb4 100644 --- a/osu.Game/Database/RealmArchiveModelImporter.cs +++ b/osu.Game/Database/RealmArchiveModelImporter.cs @@ -149,7 +149,7 @@ namespace osu.Game.Database return imported; } - notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import failed!"; + notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import failed! Check logs for more information."; notification.State = ProgressNotificationState.Cancelled; } else @@ -229,7 +229,7 @@ namespace osu.Game.Database try { - model = CreateModel(archive); + model = CreateModel(archive, parameters); if (model == null) return null; @@ -261,7 +261,7 @@ namespace osu.Game.Database /// An optional cancellation token. public virtual Live? ImportModel(TModel item, ArchiveReader? archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) => Realm.Run(realm => { - pauseIfNecessary(cancellationToken); + pauseIfNecessary(parameters, cancellationToken); TModel? existing; @@ -474,8 +474,9 @@ namespace osu.Game.Database /// Actual expensive population should be done in ; this should just prepare for duplicate checking. /// /// The archive to create the model for. + /// Parameters to further configure the import process. /// A model populated with minimal information. Returning a null will abort importing silently. - protected abstract TModel? CreateModel(ArchiveReader archive); + protected abstract TModel? CreateModel(ArchiveReader archive, ImportParameters parameters); /// /// Populate the provided model completely from the given archive. @@ -559,9 +560,9 @@ namespace osu.Game.Database /// Whether to perform deletion. protected virtual bool ShouldDeleteArchive(string path) => false; - private void pauseIfNecessary(CancellationToken cancellationToken) + private void pauseIfNecessary(ImportParameters importParameters, CancellationToken cancellationToken) { - if (!PauseImports) + if (!PauseImports || importParameters.ImportImmediately) return; Logger.Log($@"{GetType().Name} is being paused."); diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index 13c4defb83..c84e1e35b8 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.cs @@ -8,6 +8,34 @@ namespace osu.Game.Database { public static class RealmExtensions { + /// + /// Performs a . + /// If a match was not found, a is performed before trying a second time. + /// This ensures that an instance is found even if the realm requested against was not in a consistent state. + /// + /// The realm to operate on. + /// The ID of the entity to find in the realm. + /// The type of the entity to find in the realm. + /// + /// The retrieved entity of type . + /// Can be if the entity is still not found by even after a refresh. + /// + public static T? FindWithRefresh(this Realm realm, Guid id) where T : IRealmObject + { + var found = realm.Find(id); + + if (found == null) + { + // It may be that we access this from the update thread before a refresh has taken place. + // To ensure that behaviour matches what we'd expect (the object generally *should be* available), force + // a refresh to bring in any off-thread changes immediately. + realm.Refresh(); + found = realm.Find(id); + } + + return found; + } + /// /// Perform a write operation against the provided realm instance. /// diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 9c871a3929..9e99cba45c 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using osu.Framework.Development; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Statistics; using Realms; @@ -29,7 +30,7 @@ namespace osu.Game.Database /// /// Construct a new instance of live realm data. /// - /// The realm data. + /// The realm data. Must be managed (see ). /// The realm factory the data was sourced from. May be null for an unmanaged object. public RealmLive(T data, RealmAccess realm) : base(data.ID) @@ -61,7 +62,7 @@ namespace osu.Game.Database return; } - perform(retrieveFromID(r)); + perform(r.FindWithRefresh(ID)!); RealmLiveStatistics.USAGE_ASYNC.Value++; }); } @@ -83,7 +84,7 @@ namespace osu.Game.Database return realm.Run(r => { - var returnData = perform(retrieveFromID(r)); + var returnData = perform(r.FindWithRefresh(ID)!); RealmLiveStatistics.USAGE_ASYNC.Value++; if (returnData is RealmObjectBase realmObject && realmObject.IsManaged) @@ -104,7 +105,7 @@ namespace osu.Game.Database PerformRead(t => { - using (var transaction = t.Realm.BeginWrite()) + using (var transaction = t.Realm!.BeginWrite()) { perform(t); transaction.Commit(); @@ -133,32 +134,17 @@ namespace osu.Game.Database { Debug.Assert(ThreadSafety.IsUpdateThread); - if (dataIsFromUpdateThread && !data.Realm.IsClosed) + if (dataIsFromUpdateThread && !data.Realm.AsNonNull().IsClosed) { RealmLiveStatistics.USAGE_UPDATE_IMMEDIATE.Value++; return; } dataIsFromUpdateThread = true; - data = retrieveFromID(realm.Realm); + data = realm.Realm.FindWithRefresh(ID)!; + RealmLiveStatistics.USAGE_UPDATE_REFETCH.Value++; } - - private T retrieveFromID(Realm realm) - { - var found = realm.Find(ID); - - if (found == null) - { - // It may be that we access this from the update thread before a refresh has taken place. - // To ensure that behaviour matches what we'd expect (the object *is* available), force - // a refresh to bring in any off-thread changes immediately. - realm.Refresh(); - found = realm.Find(ID); - } - - return found; - } } internal static class RealmLiveStatistics diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index a771aa04df..72529ed9ff 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.Serialization; using AutoMapper; using AutoMapper.Internal; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Input.Bindings; using osu.Game.Models; @@ -41,7 +43,7 @@ namespace osu.Game.Database .ForMember(s => s.BeatmapSet, cc => cc.Ignore()) .AfterMap((s, d) => { - d.Ruleset = d.Realm.Find(s.Ruleset.ShortName); + d.Ruleset = d.Realm!.Find(s.Ruleset.ShortName)!; copyChangesToRealm(s.Difficulty, d.Difficulty); copyChangesToRealm(s.Metadata, d.Metadata); }); @@ -52,18 +54,32 @@ namespace osu.Game.Database { foreach (var beatmap in s.Beatmaps) { - var existing = d.Beatmaps.FirstOrDefault(b => b.ID == beatmap.ID); + // Importantly, search all of realm for the beatmap (not just the set's beatmaps). + // It may have gotten detached, and if that's the case let's use this opportunity to fix + // things up. + var existingBeatmap = d.Realm!.Find(beatmap.ID); - if (existing != null) - copyChangesToRealm(beatmap, existing); + if (existingBeatmap != null) + { + // As above, reattach if it happens to not be in the set's beatmaps. + if (!d.Beatmaps.Contains(existingBeatmap)) + { + Debug.Fail("Beatmaps should never become detached under normal circumstances. If this ever triggers, it should be investigated further."); + Logger.Log("WARNING: One of the difficulties in a beatmap was detached from its set. Please save a copy of logs and report this to devs.", LoggingTarget.Database, LogLevel.Important); + d.Beatmaps.Add(existingBeatmap); + } + + copyChangesToRealm(beatmap, existingBeatmap); + } else { var newBeatmap = new BeatmapInfo { ID = beatmap.ID, BeatmapSet = d, - Ruleset = d.Realm.Find(beatmap.Ruleset.ShortName) + Ruleset = d.Realm.Find(beatmap.Ruleset.ShortName)! }; + d.Beatmaps.Add(newBeatmap); copyChangesToRealm(beatmap, newBeatmap); } @@ -266,12 +282,10 @@ namespace osu.Game.Database /// /// A subscription token. It must be kept alive for as long as you want to receive change notifications. /// To stop receiving notifications, call . - /// - /// May be null in the case the provided collection is not managed. /// /// /// - public static IDisposable? QueryAsyncWithNotifications(this IRealmCollection collection, NotificationCallbackDelegate callback) + public static IDisposable QueryAsyncWithNotifications(this IRealmCollection collection, NotificationCallbackDelegate callback) where T : RealmObjectBase { if (!RealmAccess.CurrentThreadSubscriptionsAllowed) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs new file mode 100644 index 0000000000..3d48a5d233 --- /dev/null +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -0,0 +1,349 @@ +// 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.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Logging; +using osu.Game.Beatmaps; +using osu.Game.Extensions; +using osu.Game.IO.Legacy; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; +using osu.Game.Scoring; + +namespace osu.Game.Database +{ + public static class StandardisedScoreMigrationTools + { + public static bool ShouldMigrateToNewStandardised(ScoreInfo score) + { + if (score.IsLegacyScore) + return false; + + if (score.TotalScoreVersion > 30000002) + return false; + + // Recalculate the old-style standardised score to see if this was an old lazer score. + bool oldScoreMatchesExpectations = GetOldStandardised(score) == score.TotalScore; + // Some older scores don't have correct statistics populated, so let's give them benefit of doubt. + bool scoreIsVeryOld = score.Date < new DateTime(2023, 1, 1, 0, 0, 0); + + return oldScoreMatchesExpectations || scoreIsVeryOld; + } + + public static long GetNewStandardised(ScoreInfo score) + { + int maxJudgementIndex = 0; + + // Avoid retrieving from realm inside loops. + int maxCombo = score.MaxCombo; + + var ruleset = score.Ruleset.CreateInstance(); + var processor = ruleset.CreateScoreProcessor(); + + processor.TrackHitEvents = false; + + var beatmap = new Beatmap(); + + HitResult maxRulesetJudgement = ruleset.GetHitResults().First().result; + + // This is a list of all results, ordered from best to worst. + // We are constructing a "best possible" score from the statistics provided because it's the best we can do. + List sortedHits = score.Statistics + .Where(kvp => kvp.Key.AffectsCombo()) + .OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key)) + .SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value)) + .ToList(); + + // Attempt to use maximum statistics from the database. + var maximumJudgements = score.MaximumStatistics + .Where(kvp => kvp.Key.AffectsCombo()) + .OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key)) + .SelectMany(kvp => Enumerable.Repeat(new FakeJudgement(kvp.Key), kvp.Value)) + .ToList(); + + // Some older scores may not have maximum statistics populated correctly. + // In this case we need to fill them with best-known-defaults. + if (maximumJudgements.Count != sortedHits.Count) + { + maximumJudgements = sortedHits + .Select(r => new FakeJudgement(getMaxJudgementFor(r, maxRulesetJudgement))) + .ToList(); + } + + // This is required to get the correct maximum combo portion. + foreach (var judgement in maximumJudgements) + beatmap.HitObjects.Add(new FakeHit(judgement)); + processor.ApplyBeatmap(beatmap); + processor.Mods.Value = score.Mods; + + // Insert all misses into a queue. + // These will be nibbled at whenever we need to reset the combo. + Queue misses = new Queue(score.Statistics + .Where(kvp => kvp.Key == HitResult.Miss || kvp.Key == HitResult.LargeTickMiss) + .SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value))); + + foreach (var result in sortedHits) + { + // For the main part of this loop, ignore all misses, as they will be inserted from the queue. + if (result == HitResult.Miss || result == HitResult.LargeTickMiss) + continue; + + // Reset combo if required. + if (processor.Combo.Value == maxCombo) + insertMiss(); + + processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++]) + { + Type = result + }); + } + + // Ensure we haven't forgotten any misses. + while (misses.Count > 0) + insertMiss(); + + var bonusHits = score.Statistics + .Where(kvp => kvp.Key.IsBonus()) + .SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value)); + + foreach (var result in bonusHits) + processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(result)) { Type = result }); + + // Not true for all scores for whatever reason. Oh well. + // Debug.Assert(processor.HighestCombo.Value == score.MaxCombo); + + return processor.TotalScore.Value; + + void insertMiss() + { + if (misses.Count > 0) + { + processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++]) + { + Type = misses.Dequeue(), + }); + } + else + { + // We ran out of misses. But we can't let max combo increase beyond the known value, + // so let's forge a miss. + processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(getMaxJudgementFor(HitResult.Miss, maxRulesetJudgement))) + { + Type = HitResult.Miss, + }); + } + } + } + + private static HitResult getMaxJudgementFor(HitResult hitResult, HitResult max) + { + switch (hitResult) + { + case HitResult.Miss: + case HitResult.Meh: + case HitResult.Ok: + case HitResult.Good: + case HitResult.Great: + case HitResult.Perfect: + return max; + + case HitResult.SmallTickMiss: + case HitResult.SmallTickHit: + return HitResult.SmallTickHit; + + case HitResult.LargeTickMiss: + case HitResult.LargeTickHit: + return HitResult.LargeTickHit; + } + + return HitResult.IgnoreHit; + } + + public static long GetOldStandardised(ScoreInfo score) + { + double accuracyScore = + (double)score.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value) + / score.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value); + double comboScore = (double)score.MaxCombo / score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value); + double bonusScore = score.Statistics.Where(kvp => kvp.Key.IsBonus()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value); + + double accuracyPortion = 0.3; + + switch (score.RulesetID) + { + case 1: + accuracyPortion = 0.75; + break; + + case 3: + accuracyPortion = 0.99; + break; + } + + double modMultiplier = 1; + + foreach (var mod in score.Mods) + modMultiplier *= mod.ScoreMultiplier; + + return (long)Math.Round((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier); + } + + /// + /// Converts from to the new standardised scoring of . + /// + /// The score to convert the total score of. + /// A used for lookups. + /// The standardised total score. + public static long ConvertFromLegacyTotalScore(ScoreInfo score, BeatmapManager beatmaps) + { + if (!score.IsLegacyScore) + return score.TotalScore; + + WorkingBeatmap beatmap = beatmaps.GetWorkingBeatmap(score.BeatmapInfo); + Ruleset ruleset = score.Ruleset.CreateInstance(); + + if (ruleset is not ILegacyRuleset legacyRuleset) + return score.TotalScore; + + var mods = score.Mods; + if (mods.Any(mod => mod is ModScoreV2)) + return score.TotalScore; + + var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods); + + if (playableBeatmap.HitObjects.Count == 0) + throw new InvalidOperationException("Beatmap contains no hit objects!"); + + ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator(); + LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap); + + return ConvertFromLegacyTotalScore(score, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes); + } + + /// + /// Converts from to the new standardised scoring of . + /// + /// The score to convert the total score of. + /// The beatmap difficulty. + /// The legacy scoring attributes for the beatmap which the score was set on. + /// The standardised total score. + public static long ConvertFromLegacyTotalScore(ScoreInfo score, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) + { + if (!score.IsLegacyScore) + return score.TotalScore; + + Debug.Assert(score.LegacyTotalScore != null); + + Ruleset ruleset = score.Ruleset.CreateInstance(); + if (ruleset is not ILegacyRuleset legacyRuleset) + return score.TotalScore; + + double legacyModMultiplier = legacyRuleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(score.Mods, difficulty); + int maximumLegacyAccuracyScore = attributes.AccuracyScore; + long maximumLegacyComboScore = (long)Math.Round(attributes.ComboScore * legacyModMultiplier); + double maximumLegacyBonusRatio = attributes.BonusScoreRatio; + + // The part of total score that doesn't include bonus. + long maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore; + + // The combo proportion is calculated as a proportion of maximumLegacyBaseScore. + double comboProportion = Math.Min(1, (double)score.LegacyTotalScore / maximumLegacyBaseScore); + + // The bonus proportion makes up the rest of the score that exceeds maximumLegacyBaseScore. + double bonusProportion = Math.Max(0, ((long)score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio); + + double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n); + + switch (score.Ruleset.OnlineID) + { + case 0: + return (long)Math.Round(( + 700000 * comboProportion + + 300000 * Math.Pow(score.Accuracy, 10) + + bonusProportion) * modMultiplier); + + case 1: + return (long)Math.Round(( + 250000 * comboProportion + + 750000 * Math.Pow(score.Accuracy, 3.6) + + bonusProportion) * modMultiplier); + + case 2: + return (long)Math.Round(( + 600000 * comboProportion + + 400000 * score.Accuracy + + bonusProportion) * modMultiplier); + + case 3: + return (long)Math.Round(( + 990000 * comboProportion + + 10000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy) + + bonusProportion) * modMultiplier); + + default: + return score.TotalScore; + } + } + + /// + /// Used to populate the model using data parsed from its corresponding replay file. + /// + /// The score to run population from replay for. + /// A instance to use for fetching replay. + /// + /// Delegate describing the population to execute. + /// The delegate's argument is a instance which permits to read data from the replay stream. + /// + public static void PopulateFromReplay(this ScoreInfo score, RealmFileStore files, Action populationFunc) + { + string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath(); + if (replayFilename == null) + return; + + try + { + using (var stream = files.Store.GetStream(replayFilename)) + { + if (stream == null) + return; + + using (SerializationReader sr = new SerializationReader(stream)) + populationFunc.Invoke(sr); + } + } + catch (Exception e) + { + Logger.Error(e, $"Failed to read replay {replayFilename} during score migration", LoggingTarget.Database); + } + } + + private class FakeHit : HitObject + { + private readonly Judgement judgement; + + public override Judgement CreateJudgement() => judgement; + + public FakeHit(Judgement judgement) + { + this.judgement = judgement; + } + } + + private class FakeJudgement : Judgement + { + public override HitResult MaxResult { get; } + + public FakeJudgement(HitResult maxResult) + { + MaxResult = maxResult; + } + } + } +} diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index b1609fbf7b..e581d5ce82 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -1,13 +1,10 @@ // 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 System.Linq; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -21,8 +18,7 @@ namespace osu.Game.Database /// The user to lookup. /// An optional cancellation token. /// The populated user, or null if the user does not exist or the request could not be satisfied. - [ItemCanBeNull] - public Task GetUserAsync(int userId, CancellationToken token = default) => LookupAsync(userId, token); + public Task GetUserAsync(int userId, CancellationToken token = default) => LookupAsync(userId, token); /// /// Perform an API lookup on the specified users, populating a model. @@ -30,10 +26,10 @@ namespace osu.Game.Database /// The users to lookup. /// An optional cancellation token. /// The populated users. May include null results for failed retrievals. - public Task GetUsersAsync(int[] userIds, CancellationToken token = default) => LookupAsync(userIds, token); + public Task GetUsersAsync(int[] userIds, CancellationToken token = default) => LookupAsync(userIds, token); protected override GetUsersRequest CreateRequest(IEnumerable ids) => new GetUsersRequest(ids.ToArray()); - protected override IEnumerable RetrieveResults(GetUsersRequest request) => request.Response?.Users; + protected override IEnumerable? RetrieveResults(GetUsersRequest request) => request.Response?.Users; } } diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs index 6553ad3886..99d748b267 100644 --- a/osu.Game/Extensions/DrawableExtensions.cs +++ b/osu.Game/Extensions/DrawableExtensions.cs @@ -43,7 +43,7 @@ namespace osu.Game.Extensions /// A delta in screen-space coordinates. /// The delta vector in Parent's coordinates. public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 delta) => - drawable.Parent.ToLocalSpace(drawable.Parent.ToScreenSpace(Vector2.Zero) + delta); + drawable.Parent!.ToLocalSpace(drawable.Parent!.ToScreenSpace(Vector2.Zero) + delta); /// /// Some elements don't handle rewind correctly and fixing them is non-trivial. diff --git a/osu.Game/Extensions/ModelExtensions.cs b/osu.Game/Extensions/ModelExtensions.cs index efb3c4d633..eef9b63b62 100644 --- a/osu.Game/Extensions/ModelExtensions.cs +++ b/osu.Game/Extensions/ModelExtensions.cs @@ -114,8 +114,24 @@ namespace osu.Game.Extensions /// /// The instance to compare. /// The other instance to compare against. - /// Whether online IDs match. If either instance is missing an online ID, this will return false. - public static bool MatchesOnlineID(this IScoreInfo? instance, IScoreInfo? other) => matchesOnlineID(instance, other); + /// + /// Whether online IDs match. + /// Both and are checked, in that order. + /// If either instance is missing an online ID, this will return false. + /// + public static bool MatchesOnlineID(this IScoreInfo? instance, IScoreInfo? other) + { + if (matchesOnlineID(instance, other)) + return true; + + if (instance == null || other == null) + return false; + + if (instance.LegacyOnlineID < 0 || other.LegacyOnlineID < 0) + return false; + + return instance.LegacyOnlineID.Equals(other.LegacyOnlineID); + } private static bool matchesOnlineID(this IHasOnlineID? instance, IHasOnlineID? other) { diff --git a/osu.Game/FodyWeavers.xml b/osu.Game/FodyWeavers.xml new file mode 100644 index 0000000000..7ff486f40c --- /dev/null +++ b/osu.Game/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs index 3ace67f410..685f03ae56 100644 --- a/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable diff --git a/osu.Game/Graphics/Backgrounds/SkinBackground.cs b/osu.Game/Graphics/Backgrounds/SkinBackground.cs index e30bb961a0..23806ae1d4 100644 --- a/osu.Game/Graphics/Backgrounds/SkinBackground.cs +++ b/osu.Game/Graphics/Backgrounds/SkinBackground.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Game.Skinning; @@ -24,7 +22,7 @@ namespace osu.Game.Graphics.Backgrounds Sprite.Texture = skin.GetTexture("menu-background") ?? Sprite.Texture; } - public override bool Equals(Background other) + public override bool Equals(Background? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 42b30f9d18..f911311a09 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Containers; @@ -86,14 +85,12 @@ namespace osu.Game.Graphics.Containers TimingControlPoint timingPoint; EffectControlPoint effectPoint; - IsBeatSyncedWithTrack = BeatSyncSource.CheckBeatSyncAvailable() && BeatSyncSource.Clock?.IsRunning == true; + IsBeatSyncedWithTrack = BeatSyncSource.Clock.IsRunning; double currentTrackTime; if (IsBeatSyncedWithTrack) { - Debug.Assert(BeatSyncSource.Clock != null); - currentTrackTime = BeatSyncSource.Clock.CurrentTime + EarlyActivationMilliseconds; timingPoint = BeatSyncSource.ControlPoints?.TimingPointAt(currentTrackTime) ?? TimingControlPoint.DEFAULT; diff --git a/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs b/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs index 55160e14af..7722374c69 100644 --- a/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs +++ b/osu.Game/Graphics/Containers/ConstrainedIconContainer.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs b/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs index a06af61125..5abb4096ac 100644 --- a/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs +++ b/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Graphics.Containers { /// diff --git a/osu.Game/Graphics/Containers/IExpandable.cs b/osu.Game/Graphics/Containers/IExpandable.cs index 05d569775c..8549d3bf2c 100644 --- a/osu.Game/Graphics/Containers/IExpandable.cs +++ b/osu.Game/Graphics/Containers/IExpandable.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/Containers/IExpandingContainer.cs b/osu.Game/Graphics/Containers/IExpandingContainer.cs index dbd9274ae7..6a36372e88 100644 --- a/osu.Game/Graphics/Containers/IExpandingContainer.cs +++ b/osu.Game/Graphics/Containers/IExpandingContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index 2d27ce906b..40e883f8ac 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -15,6 +15,7 @@ using osu.Framework.Localisation; using osu.Framework.Platform; using osu.Game.Online; using osu.Game.Users; +using osu.Game.Localisation; namespace osu.Game.Graphics.Containers { @@ -74,7 +75,7 @@ namespace osu.Game.Graphics.Containers } public void AddUserLink(IUser user, Action creationParameters = null) - => createLink(CreateChunkFor(user.Username, true, CreateSpriteText, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user), "view profile"); + => createLink(CreateChunkFor(user.Username, true, CreateSpriteText, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user), ContextMenuStrings.ViewProfile); private void createLink(ITextPart textPart, LinkDetails link, LocalisableString tooltipText, Action action = null) { diff --git a/osu.Game/Graphics/Containers/LogoTrackingContainer.cs b/osu.Game/Graphics/Containers/LogoTrackingContainer.cs index 984d60d35e..08eae25951 100644 --- a/osu.Game/Graphics/Containers/LogoTrackingContainer.cs +++ b/osu.Game/Graphics/Containers/LogoTrackingContainer.cs @@ -76,10 +76,10 @@ namespace osu.Game.Graphics.Containers /// Will only be correct if the logo's are set to Axes.Both protected Vector2 ComputeLogoTrackingPosition() { - var absolutePos = Logo.Parent.ToLocalSpace(LogoFacade.ScreenSpaceDrawQuad.Centre); + var absolutePos = Logo.Parent!.ToLocalSpace(LogoFacade.ScreenSpaceDrawQuad.Centre); - return new Vector2(absolutePos.X / Logo.Parent.RelativeToAbsoluteFactor.X, - absolutePos.Y / Logo.Parent.RelativeToAbsoluteFactor.Y); + return new Vector2(absolutePos.X / Logo.Parent!.RelativeToAbsoluteFactor.X, + absolutePos.Y / Logo.Parent!.RelativeToAbsoluteFactor.Y); } protected override void Update() diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs index 5b1780a068..5da785603a 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Markdig; using Markdig.Extensions.Footnotes; using Markdig.Extensions.Tables; @@ -37,6 +35,13 @@ namespace osu.Game.Graphics.Containers.Markdown break; case ListItemBlock listItemBlock: + // `ListBlock.Parent` is annotated as null-returning in xmldoc. + // Unfortunately code analysis sees that the type doesn't have NRT enabled and complains. + // This is fixed upstream in 0.24.0 (https://github.com/xoofx/markdig/commit/6684c8257cbbcba2d34457020876be289d3cd8b9), + // but markdig is a transitive dependency from framework, wherein we are locked to 0.23.0 + // (https://github.com/ppy/osu-framework/blob/9746d7d06f48910c05a24687a25f435f30d12f8b/osu.Framework/osu.Framework.csproj#L52C1-L54) + // Therefore... + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract bool isOrdered = ((ListBlock)listItemBlock.Parent)?.IsOrdered == true; OsuMarkdownListItem childContainer = CreateListItem(listItemBlock, level, isOrdered); diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownFencedCodeBlock.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownFencedCodeBlock.cs index b5bbe3e2cc..7d84d368ad 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownFencedCodeBlock.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownFencedCodeBlock.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Markdig.Syntax; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs index 800a0e1fc3..da4273c9b9 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Markdig.Syntax; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers.Markdown; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs index 8ccac158eb..10207dd389 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Markdig.Syntax.Inlines; using osu.Framework.Graphics.Containers.Markdown; using osu.Framework.Graphics.Cursor; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownOrderedListItem.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownOrderedListItem.cs index 6eac9378ae..1812a9529c 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownOrderedListItem.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownOrderedListItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownQuoteBlock.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownQuoteBlock.cs index 447085a48c..d2e6f4d370 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownQuoteBlock.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownQuoteBlock.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Markdig.Syntax; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownSeparator.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownSeparator.cs index 343a1d1015..b785b748a7 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownSeparator.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownSeparator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers.Markdown; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTable.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTable.cs index c9c1098e05..652d3d9e3e 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTable.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTable.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Markdig.Extensions.Tables; using osu.Framework.Graphics.Containers.Markdown; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTableCell.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTableCell.cs index dbf15a2546..4ffdeb179c 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTableCell.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTableCell.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Markdig.Extensions.Tables; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownUnorderedListItem.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownUnorderedListItem.cs index 64e98511c2..cdd6bc6ed5 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownUnorderedListItem.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownUnorderedListItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 07b5b53e0e..162c4b6a59 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -24,6 +24,7 @@ namespace osu.Game.Graphics.Containers private Sample samplePopOut; protected virtual string PopInSampleName => "UI/overlay-pop-in"; protected virtual string PopOutSampleName => "UI/overlay-pop-out"; + protected virtual double PopInOutSampleBalance => 0; protected override bool BlockNonPositionalInput => true; @@ -133,15 +134,21 @@ namespace osu.Game.Graphics.Containers return; } - if (didChange) - samplePopIn?.Play(); + if (didChange && samplePopIn != null) + { + samplePopIn.Balance.Value = PopInOutSampleBalance; + samplePopIn.Play(); + } if (BlockScreenWideMouse && DimMainContent) overlayManager?.ShowBlockingOverlay(this); break; case Visibility.Hidden: - if (didChange) - samplePopOut?.Play(); + if (didChange && samplePopOut != null) + { + samplePopOut.Balance.Value = PopInOutSampleBalance; + samplePopOut.Play(); + } if (BlockScreenWideMouse) overlayManager?.HideBlockingOverlay(this); break; @@ -152,7 +159,6 @@ namespace osu.Game.Graphics.Containers protected override void PopOut() { - base.PopOut(); previewTrackManager.StopAnyPlaying(this); } diff --git a/osu.Game/Graphics/Containers/OsuHoverContainer.cs b/osu.Game/Graphics/Containers/OsuHoverContainer.cs index b4b80f7574..3b5e48d23e 100644 --- a/osu.Game/Graphics/Containers/OsuHoverContainer.cs +++ b/osu.Game/Graphics/Containers/OsuHoverContainer.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 osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index e39fd45a16..da6996c170 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -28,7 +28,7 @@ namespace osu.Game.Graphics.Containers public partial class OsuScrollContainer : ScrollContainer where T : Drawable { - public const float SCROLL_BAR_HEIGHT = 10; + public const float SCROLL_BAR_WIDTH = 10; public const float SCROLL_BAR_PADDING = 3; /// @@ -139,6 +139,8 @@ namespace osu.Game.Graphics.Containers private readonly Box box; + protected override float MinimumDimSize => SCROLL_BAR_WIDTH * 3; + public OsuScrollbar(Direction scrollDir) : base(scrollDir) { @@ -147,7 +149,7 @@ namespace osu.Game.Graphics.Containers CornerRadius = 5; // needs to be set initially for the ResizeTo to respect minimum size - Size = new Vector2(SCROLL_BAR_HEIGHT); + Size = new Vector2(SCROLL_BAR_WIDTH); const float margin = 3; @@ -173,11 +175,10 @@ namespace osu.Game.Graphics.Containers public override void ResizeTo(float val, int duration = 0, Easing easing = Easing.None) { - Vector2 size = new Vector2(SCROLL_BAR_HEIGHT) + this.ResizeTo(new Vector2(SCROLL_BAR_WIDTH) { [(int)ScrollDirection] = val - }; - this.ResizeTo(size, duration, easing); + }, duration, easing); } protected override bool OnHover(HoverEvent e) diff --git a/osu.Game/Graphics/Containers/ReverseChildIDFillFlowContainer.cs b/osu.Game/Graphics/Containers/ReverseChildIDFillFlowContainer.cs index e37d23fe97..892edf8551 100644 --- a/osu.Game/Graphics/Containers/ReverseChildIDFillFlowContainer.cs +++ b/osu.Game/Graphics/Containers/ReverseChildIDFillFlowContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 27ff6b851d..9f41c4eff2 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -119,11 +119,11 @@ namespace osu.Game.Graphics.Containers headerBackgroundContainer.Clear(); headerBackground = value; - if (value == null) return; - - headerBackgroundContainer.Add(headerBackground); - - lastKnownScroll = null; + if (headerBackground != null) + { + headerBackgroundContainer.Add(headerBackground); + lastKnownScroll = null; + } } } diff --git a/osu.Game/Graphics/Containers/ShakeContainer.cs b/osu.Game/Graphics/Containers/ShakeContainer.cs index 9a1ddac40d..bb9be2b939 100644 --- a/osu.Game/Graphics/Containers/ShakeContainer.cs +++ b/osu.Game/Graphics/Containers/ShakeContainer.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 osu.Framework.Graphics.Containers; using osu.Game.Extensions; diff --git a/osu.Game/Graphics/Containers/UprightAspectMaintainingContainer.cs b/osu.Game/Graphics/Containers/UprightAspectMaintainingContainer.cs index 38ab6deb97..f263ae36cb 100644 --- a/osu.Game/Graphics/Containers/UprightAspectMaintainingContainer.cs +++ b/osu.Game/Graphics/Containers/UprightAspectMaintainingContainer.cs @@ -48,7 +48,7 @@ namespace osu.Game.Graphics.Containers private void keepUprightAndUnstretched() { // Decomposes the inverse of the parent DrawInfo.Matrix into rotation, shear and scale. - var parentMatrix = Parent.DrawInfo.Matrix; + var parentMatrix = Parent!.DrawInfo.Matrix; // Remove Translation.> parentMatrix.M31 = 0.0f; diff --git a/osu.Game/Graphics/Containers/UserDimContainer.cs b/osu.Game/Graphics/Containers/UserDimContainer.cs index 6f6292c3b2..af5e7692b3 100644 --- a/osu.Game/Graphics/Containers/UserDimContainer.cs +++ b/osu.Game/Graphics/Containers/UserDimContainer.cs @@ -24,15 +24,13 @@ namespace osu.Game.Graphics.Containers public const double BACKGROUND_FADE_DURATION = 800; /// - /// Whether or not user-configured settings relating to brightness of elements should be ignored + /// Whether or not user-configured settings relating to brightness of elements should be ignored. /// + /// + /// For best or worst, this also bypasses storyboard disable. Not sure this is correct but leaving it as to not break anything. + /// public readonly Bindable IgnoreUserSettings = new Bindable(); - /// - /// Whether or not the storyboard loaded should completely hide the background behind it. - /// - public readonly Bindable StoryboardReplacesBackground = new Bindable(); - /// /// Whether player is in break time. /// Must be bound to to allow for dim adjustments in gameplay. @@ -49,7 +47,7 @@ namespace osu.Game.Graphics.Containers /// /// The amount of dim to be used when is true. /// - public Bindable DimWhenUserSettingsIgnored { get; set; } = new Bindable(); + public Bindable DimWhenUserSettingsIgnored { get; } = new Bindable(); protected Bindable LightenDuringBreaks { get; private set; } = null!; @@ -57,7 +55,7 @@ namespace osu.Game.Graphics.Containers private float breakLightening => LightenDuringBreaks.Value && IsBreakTime.Value ? BREAK_LIGHTEN_AMOUNT : 0; - protected float DimLevel => Math.Max(!IgnoreUserSettings.Value ? (float)UserDimLevel.Value - breakLightening : DimWhenUserSettingsIgnored.Value, 0); + protected virtual float DimLevel => Math.Max(!IgnoreUserSettings.Value ? (float)UserDimLevel.Value - breakLightening : DimWhenUserSettingsIgnored.Value, 0); protected override Container Content => dimContent; @@ -83,7 +81,6 @@ namespace osu.Game.Graphics.Containers LightenDuringBreaks.ValueChanged += _ => UpdateVisuals(); IsBreakTime.ValueChanged += _ => UpdateVisuals(); ShowStoryboard.ValueChanged += _ => UpdateVisuals(); - StoryboardReplacesBackground.ValueChanged += _ => UpdateVisuals(); IgnoreUserSettings.ValueChanged += _ => UpdateVisuals(); } diff --git a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs index 715677aec1..6934c95385 100644 --- a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs +++ b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; namespace osu.Game.Graphics.Containers diff --git a/osu.Game/Graphics/Containers/WaveContainer.cs b/osu.Game/Graphics/Containers/WaveContainer.cs index 05a666721a..5abc66d2ac 100644 --- a/osu.Game/Graphics/Containers/WaveContainer.cs +++ b/osu.Game/Graphics/Containers/WaveContainer.cs @@ -1,9 +1,10 @@ // 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 osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -34,6 +35,12 @@ namespace osu.Game.Graphics.Containers protected override bool StartHidden => true; + private Sample? samplePopIn; + private Sample? samplePopOut; + + // required due to LoadAsyncComplete() in `VisibilityContainer` calling PopOut() during load - similar workaround to `OsuDropdownMenu` + private bool wasShown; + public Color4 FirstWaveColour { get => firstWave.Colour; @@ -58,6 +65,13 @@ namespace osu.Game.Graphics.Containers set => fourthWave.Colour = value; } + [BackgroundDependencyLoader(true)] + private void load(AudioManager audio) + { + samplePopIn = audio.Samples.Get("UI/wave-pop-in"); + samplePopOut = audio.Samples.Get("UI/overlay-big-pop-out"); + } + public WaveContainer() { Masking = true; @@ -112,6 +126,8 @@ namespace osu.Game.Graphics.Containers w.Show(); contentContainer.MoveToY(0, APPEAR_DURATION, Easing.OutQuint); + samplePopIn?.Play(); + wasShown = true; } protected override void PopOut() @@ -120,6 +136,9 @@ namespace osu.Game.Graphics.Containers w.Hide(); contentContainer.MoveToY(2, DISAPPEAR_DURATION, Easing.In); + + if (wasShown) + samplePopOut?.Play(); } protected override void UpdateAfterChildren() @@ -159,7 +178,7 @@ namespace osu.Game.Graphics.Containers // We can not use RelativeSizeAxes for Height, because the height // of our parent diminishes as the content moves up. - Height = Parent.Parent.DrawSize.Y * 1.5f; + Height = Parent!.Parent!.DrawSize.Y * 1.5f; } protected override void PopIn() => Schedule(() => this.MoveToY(FinalPosition, APPEAR_DURATION, easing_show)); @@ -169,7 +188,7 @@ namespace osu.Game.Graphics.Containers double duration = IsLoaded ? DISAPPEAR_DURATION : 0; // scheduling is required as parent may not be present at the time this is called. - Schedule(() => this.MoveToY(Parent.Parent.DrawSize.Y, duration, easing_hide)); + Schedule(() => this.MoveToY(Parent!.Parent!.DrawSize.Y, duration, easing_hide)); } } } diff --git a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs index 27700e71d9..c5bcfcd2df 100644 --- a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.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 osu.Framework.Allocation; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs index dc75d626b9..aab5b3ee36 100644 --- a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuTooltipContainer.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 osuTK; using osuTK.Graphics; using osu.Framework.Allocation; diff --git a/osu.Game/Graphics/DateTooltip.cs b/osu.Game/Graphics/DateTooltip.cs index c62f53f1d4..c084f498fe 100644 --- a/osu.Game/Graphics/DateTooltip.cs +++ b/osu.Game/Graphics/DateTooltip.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 osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs index 553b27acb1..0e5bcc8019 100644 --- a/osu.Game/Graphics/DrawableDate.cs +++ b/osu.Game/Graphics/DrawableDate.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 osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/ErrorTextFlowContainer.cs b/osu.Game/Graphics/ErrorTextFlowContainer.cs index 65a90534e5..40c7580647 100644 --- a/osu.Game/Graphics/ErrorTextFlowContainer.cs +++ b/osu.Game/Graphics/ErrorTextFlowContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Graphics.Containers; using osu.Game.Graphics.Containers; @@ -25,7 +23,7 @@ namespace osu.Game.Graphics RemovePart(textPart); } - public void AddErrors(string[] errors) + public void AddErrors(string[]? errors) { ClearErrors(); diff --git a/osu.Game/Graphics/IHasAccentColour.cs b/osu.Game/Graphics/IHasAccentColour.cs index fc722375ce..af497da70f 100644 --- a/osu.Game/Graphics/IHasAccentColour.cs +++ b/osu.Game/Graphics/IHasAccentColour.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 osuTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; diff --git a/osu.Game/Graphics/OpenGL/Vertices/PositionAndColourVertex.cs b/osu.Game/Graphics/OpenGL/Vertices/PositionAndColourVertex.cs index 78c8cbb79e..2428eebbe5 100644 --- a/osu.Game/Graphics/OpenGL/Vertices/PositionAndColourVertex.cs +++ b/osu.Game/Graphics/OpenGL/Vertices/PositionAndColourVertex.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Runtime.InteropServices; using osu.Framework.Graphics.Rendering.Vertices; diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index e06f6b3fd0..75d313d98c 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.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 osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Colour; @@ -80,6 +78,7 @@ namespace osu.Game.Graphics case HitResult.SmallTickMiss: case HitResult.LargeTickMiss: case HitResult.Miss: + case HitResult.ComboBreak: return Red; case HitResult.Meh: @@ -398,5 +397,7 @@ namespace osu.Game.Graphics public Color4 SpotlightColour => Green2; public Color4 FeaturedArtistColour => Blue2; + + public Color4 DangerousButtonColour => Pink3; } } diff --git a/osu.Game/Graphics/OsuIcon.cs b/osu.Game/Graphics/OsuIcon.cs index 0a099f1fcc..15af8f000b 100644 --- a/osu.Game/Graphics/OsuIcon.cs +++ b/osu.Game/Graphics/OsuIcon.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics.Sprites; namespace osu.Game.Graphics @@ -91,6 +89,8 @@ namespace osu.Game.Graphics public static IconUsage ModSpunOut => Get(0xe046); public static IconUsage ModSuddenDeath => Get(0xe047); public static IconUsage ModTarget => Get(0xe048); - public static IconUsage ModBg => Get(0xe04a); + + // Use "Icons/BeatmapDetails/mod-icon" instead + // public static IconUsage ModBg => Get(0xe04a); } } diff --git a/osu.Game/Graphics/ParticleExplosion.cs b/osu.Game/Graphics/ParticleExplosion.cs index 56e1568441..e16913c6c9 100644 --- a/osu.Game/Graphics/ParticleExplosion.cs +++ b/osu.Game/Graphics/ParticleExplosion.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/ParticleSpewer.cs b/osu.Game/Graphics/ParticleSpewer.cs index 8519cf0c59..64c70095bf 100644 --- a/osu.Game/Graphics/ParticleSpewer.cs +++ b/osu.Game/Graphics/ParticleSpewer.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Diagnostics; using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; @@ -20,9 +21,9 @@ namespace osu.Game.Graphics { private readonly FallingParticle[] particles; private int currentIndex; - private double lastParticleAdded; + private double? lastParticleAdded; - private readonly double cooldown; + private readonly double timeBetweenSpawns; private readonly double maxDuration; /// @@ -44,26 +45,68 @@ namespace osu.Game.Graphics particles = new FallingParticle[perSecond * (int)Math.Ceiling(maxDuration / 1000)]; - cooldown = 1000f / perSecond; + timeBetweenSpawns = 1000f / perSecond; this.maxDuration = maxDuration; } + protected override void LoadComplete() + { + base.LoadComplete(); + + Active.BindValueChanged(active => + { + // ensure that particles can be spawned immediately after the spewer becomes active. + if (active.NewValue) + lastParticleAdded = null; + }); + } + protected override void Update() { base.Update(); - if (Active.Value && CanSpawnParticles && Math.Abs(Time.Current - lastParticleAdded) > cooldown) + Invalidate(Invalidation.DrawNode); + + if (!Active.Value || !CanSpawnParticles) + return; + + if (lastParticleAdded == null) { - var newParticle = CreateParticle(); - newParticle.StartTime = (float)Time.Current; - - particles[currentIndex] = newParticle; - - currentIndex = (currentIndex + 1) % particles.Length; lastParticleAdded = Time.Current; + spawnParticle(); + return; } - Invalidate(Invalidation.DrawNode); + double timeElapsed = Time.Current - lastParticleAdded.Value; + + // Avoid spawning too many particles if a long amount of time has passed. + if (Math.Abs(timeElapsed) > maxDuration) + { + lastParticleAdded = Time.Current; + spawnParticle(); + return; + } + + Debug.Assert(lastParticleAdded != null); + + for (int i = 0; i < timeElapsed / timeBetweenSpawns; i++) + { + lastParticleAdded += timeBetweenSpawns; + spawnParticle(); + } + } + + private void spawnParticle() + { + Debug.Assert(lastParticleAdded != null); + + var newParticle = CreateParticle(); + + newParticle.StartTime = (float)lastParticleAdded.Value; + + particles[currentIndex] = newParticle; + + currentIndex = (currentIndex + 1) % particles.Length; } /// @@ -73,7 +116,7 @@ namespace osu.Game.Graphics protected override DrawNode CreateDrawNode() => new ParticleSpewerDrawNode(this); - # region DrawNode + #region DrawNode private class ParticleSpewerDrawNode : SpriteDrawNode { diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index 82f89d6889..26e499ae9a 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -43,6 +43,9 @@ namespace osu.Game.Graphics [Resolved] private GameHost host { get; set; } + [Resolved] + private Clipboard clipboard { get; set; } + private Storage storage; [Resolved] @@ -116,7 +119,7 @@ namespace osu.Game.Graphics using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false)) { - host.GetClipboard()?.SetImage(image); + clipboard.SetImage(image); (string filename, var stream) = getWritableStream(); diff --git a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs index ae594ddfe2..1355bfc272 100644 --- a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs +++ b/osu.Game/Graphics/Sprites/GlowingSpriteText.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 osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; diff --git a/osu.Game/Graphics/Sprites/OsuSpriteText.cs b/osu.Game/Graphics/Sprites/OsuSpriteText.cs index e149e0abfb..6ce64a9052 100644 --- a/osu.Game/Graphics/Sprites/OsuSpriteText.cs +++ b/osu.Game/Graphics/Sprites/OsuSpriteText.cs @@ -1,14 +1,19 @@ // 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 osu.Framework.Graphics.Sprites; namespace osu.Game.Graphics.Sprites { public partial class OsuSpriteText : SpriteText { + [Obsolete("Use TruncatingSpriteText instead.")] + public new bool Truncate + { + set => throw new InvalidOperationException($"Use {nameof(TruncatingSpriteText)} instead."); + } + public OsuSpriteText() { Shadow = true; diff --git a/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs b/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs new file mode 100644 index 0000000000..46abdbf09e --- /dev/null +++ b/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs @@ -0,0 +1,29 @@ +// 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.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; + +namespace osu.Game.Graphics.Sprites +{ + /// + /// A derived version of which automatically shows non-truncated text in tooltip when required. + /// + public sealed partial class TruncatingSpriteText : OsuSpriteText, IHasTooltip + { + /// + /// Whether a tooltip should be shown with non-truncated text on hover. + /// + public bool ShowTooltip { get; init; } = true; + + public LocalisableString TooltipText => Text; + + public override bool HandlePositionalInput => IsTruncated && ShowTooltip; + + public TruncatingSpriteText() + { + ((SpriteText)this).Truncate = true; + } + } +} diff --git a/osu.Game/Graphics/UserInterface/Bar.cs b/osu.Game/Graphics/UserInterface/Bar.cs index 53217e2120..9b63cabeda 100644 --- a/osu.Game/Graphics/UserInterface/Bar.cs +++ b/osu.Game/Graphics/UserInterface/Bar.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 osuTK; using osuTK.Graphics; diff --git a/osu.Game/Graphics/UserInterface/BarGraph.cs b/osu.Game/Graphics/UserInterface/BarGraph.cs index c394e58d87..27a41eb7e3 100644 --- a/osu.Game/Graphics/UserInterface/BarGraph.cs +++ b/osu.Game/Graphics/UserInterface/BarGraph.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osuTK; using osu.Framework.Graphics; using System.Collections.Generic; diff --git a/osu.Game/Graphics/UserInterface/BasicSearchTextBox.cs b/osu.Game/Graphics/UserInterface/BasicSearchTextBox.cs index c4e03133dc..2e76951e7b 100644 --- a/osu.Game/Graphics/UserInterface/BasicSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/BasicSearchTextBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osuTK; diff --git a/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs b/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs index ba76a17fc6..e0a5409683 100644 --- a/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs +++ b/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs b/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs index 5855a66ae1..39ef7924b9 100644 --- a/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs +++ b/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Game.Graphics.UserInterfaceV2; @@ -13,7 +11,7 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(OsuColour colours) { - BackgroundColour = colours.PinkDark; + BackgroundColour = colours.DangerousButtonColour; } } } diff --git a/osu.Game/Graphics/UserInterface/DialogButton.cs b/osu.Game/Graphics/UserInterface/DialogButton.cs index 670778b07b..db81bc991d 100644 --- a/osu.Game/Graphics/UserInterface/DialogButton.cs +++ b/osu.Game/Graphics/UserInterface/DialogButton.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 osu.Framework; using osu.Framework.Extensions.Color4Extensions; @@ -30,7 +28,7 @@ namespace osu.Game.Graphics.UserInterface private const float hover_duration = 500; private const float click_duration = 200; - public event Action StateChanged; + public event Action? StateChanged; private SelectionState state; diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs index eb046932e6..2f2cb7e5f8 100644 --- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs @@ -40,8 +40,14 @@ namespace osu.Game.Graphics.UserInterface AddInternal(hoverClickSounds = new HoverClickSounds()); updateTextColour(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); Item.Action.BindDisabledChanged(_ => updateState(), true); + FinishTransforms(); } private void updateTextColour() diff --git a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs index ec3a5744f8..5af275c9e7 100644 --- a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Graphics/UserInterface/ExpandableSlider.cs b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs index 5bc17303d8..121a1eef49 100644 --- a/osu.Game/Graphics/UserInterface/ExpandableSlider.cs +++ b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; @@ -106,8 +104,8 @@ namespace osu.Game.Graphics.UserInterface }; } - [Resolved(canBeNull: true)] - private IExpandingContainer expandingContainer { get; set; } + [Resolved] + private IExpandingContainer? expandingContainer { get; set; } protected override void LoadComplete() { diff --git a/osu.Game/Graphics/UserInterface/ExpandingBar.cs b/osu.Game/Graphics/UserInterface/ExpandingBar.cs index 6d7c41ee7c..bec4e6e49c 100644 --- a/osu.Game/Graphics/UserInterface/ExpandingBar.cs +++ b/osu.Game/Graphics/UserInterface/ExpandingBar.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 osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osuTK; diff --git a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs index 4eccb37613..7ba3d55162 100644 --- a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs +++ b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs @@ -27,6 +27,9 @@ namespace osu.Game.Graphics.UserInterface [Resolved] private GameHost host { get; set; } = null!; + [Resolved] + private Clipboard clipboard { get; set; } = null!; + [Resolved] private OnScreenDisplay? onScreenDisplay { get; set; } @@ -92,8 +95,11 @@ namespace osu.Game.Graphics.UserInterface private void copyUrl() { - host.GetClipboard()?.SetText(Link); - onScreenDisplay?.Display(new CopyUrlToast()); + if (Link != null) + { + clipboard.SetText(Link); + onScreenDisplay?.Display(new CopyUrlToast()); + } } } } diff --git a/osu.Game/Graphics/UserInterface/FPSCounter.cs b/osu.Game/Graphics/UserInterface/FPSCounter.cs index 9dbeba6449..000b85b900 100644 --- a/osu.Game/Graphics/UserInterface/FPSCounter.cs +++ b/osu.Game/Graphics/UserInterface/FPSCounter.cs @@ -167,9 +167,12 @@ namespace osu.Game.Graphics.UserInterface { base.Update(); + double elapsedDrawFrameTime = drawClock.ElapsedFrameTime; + double elapsedUpdateFrameTime = updateClock.ElapsedFrameTime; + // If the game goes into a suspended state (ie. debugger attached or backgrounded on a mobile device) // we want to ignore really long periods of no processing. - if (updateClock.ElapsedFrameTime > 10000) + if (elapsedUpdateFrameTime > 10000) return; mainContent.Width = Math.Max(mainContent.Width, counters.DrawWidth); @@ -178,17 +181,17 @@ namespace osu.Game.Graphics.UserInterface // frame limiter (we want to show the FPS as it's changing, even if it isn't an outlier). bool aimRatesChanged = updateAimFPS(); - bool hasUpdateSpike = displayedFrameTime < spike_time_ms && updateClock.ElapsedFrameTime > spike_time_ms; + bool hasUpdateSpike = displayedFrameTime < spike_time_ms && elapsedUpdateFrameTime > spike_time_ms; // use elapsed frame time rather then FramesPerSecond to better catch stutter frames. - bool hasDrawSpike = displayedFpsCount > (1000 / spike_time_ms) && drawClock.ElapsedFrameTime > spike_time_ms; + bool hasDrawSpike = displayedFpsCount > (1000 / spike_time_ms) && elapsedDrawFrameTime > spike_time_ms; const float damp_time = 100; - displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, updateClock.ElapsedFrameTime, hasUpdateSpike ? 0 : damp_time, updateClock.ElapsedFrameTime); + displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, elapsedUpdateFrameTime, hasUpdateSpike ? 0 : damp_time, elapsedUpdateFrameTime); if (hasDrawSpike) // show spike time using raw elapsed value, to account for `FramesPerSecond` being so averaged spike frames don't show. - displayedFpsCount = 1000 / drawClock.ElapsedFrameTime; + displayedFpsCount = 1000 / elapsedDrawFrameTime; else displayedFpsCount = Interpolation.DampContinuously(displayedFpsCount, drawClock.FramesPerSecond, damp_time, Time.Elapsed); @@ -210,7 +213,7 @@ namespace osu.Game.Graphics.UserInterface requestDisplay(); else if (isDisplayed && Time.Current - lastDisplayRequiredTime > 2000 && !IsHovered) { - mainContent.FadeTo(0, 300, Easing.OutQuint); + mainContent.FadeTo(0.7f, 300, Easing.OutQuint); isDisplayed = false; } } diff --git a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs index f0ff76b35d..72d50eb042 100644 --- a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs +++ b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.ComponentModel; namespace osu.Game.Graphics.UserInterface diff --git a/osu.Game/Graphics/UserInterface/IconButton.cs b/osu.Game/Graphics/UserInterface/IconButton.cs index 47f06715b5..0268fa59c2 100644 --- a/osu.Game/Graphics/UserInterface/IconButton.cs +++ b/osu.Game/Graphics/UserInterface/IconButton.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 osuTK; using osuTK.Graphics; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/UserInterface/LoadingButton.cs b/osu.Game/Graphics/UserInterface/LoadingButton.cs index 8a841ffc94..4a37811114 100644 --- a/osu.Game/Graphics/UserInterface/LoadingButton.cs +++ b/osu.Game/Graphics/UserInterface/LoadingButton.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs index 0ea44dfe49..df921c5c81 100644 --- a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs +++ b/osu.Game/Graphics/UserInterface/LoadingSpinner.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game/Graphics/UserInterface/Nub.cs b/osu.Game/Graphics/UserInterface/Nub.cs index 28a2eb40c3..4b953718bc 100644 --- a/osu.Game/Graphics/UserInterface/Nub.cs +++ b/osu.Game/Graphics/UserInterface/Nub.cs @@ -20,16 +20,16 @@ namespace osu.Game.Graphics.UserInterface { public const float HEIGHT = 15; - public const float EXPANDED_SIZE = 50; + public const float DEFAULT_EXPANDED_SIZE = 50; private const float border_width = 3; private readonly Box fill; private readonly Container main; - public Nub() + public Nub(float expandedSize = DEFAULT_EXPANDED_SIZE) { - Size = new Vector2(EXPANDED_SIZE, HEIGHT); + Size = new Vector2(expandedSize, HEIGHT); InternalChildren = new[] { diff --git a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs index 5ef590d253..0eec04541c 100644 --- a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.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 osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -41,15 +39,15 @@ namespace osu.Game.Graphics.UserInterface } [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; protected override Container Content => content; private readonly Container content; private readonly Box hover; - public OsuAnimatedButton() - : base(HoverSampleSet.Button) + public OsuAnimatedButton(HoverSampleSet sampleSet = HoverSampleSet.Button) + : base(sampleSet) { base.Content.Add(content = new Container { @@ -111,6 +109,10 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnClick(ClickEvent e) { + // Handle case where a click is triggered via TriggerClick(). + if (!IsHovered) + hover.FadeOutFromOne(1600); + hover.FlashColour(FlashColour, 800, Easing.OutQuint); return base.OnClick(e); } diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index 160105af1a..b7b405a7e8 100644 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -47,7 +47,7 @@ namespace osu.Game.Graphics.UserInterface private Sample sampleChecked; private Sample sampleUnchecked; - public OsuCheckbox(bool nubOnRight = true) + public OsuCheckbox(bool nubOnRight = true, float nubSize = Nub.DEFAULT_EXPANDED_SIZE) { AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; @@ -61,7 +61,7 @@ namespace osu.Game.Graphics.UserInterface AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, }, - Nub = new Nub(), + Nub = new Nub(nubSize), new HoverSounds() }; @@ -70,14 +70,14 @@ namespace osu.Game.Graphics.UserInterface Nub.Anchor = Anchor.CentreRight; Nub.Origin = Anchor.CentreRight; Nub.Margin = new MarginPadding { Right = nub_padding }; - LabelTextFlowContainer.Padding = new MarginPadding { Right = Nub.EXPANDED_SIZE + nub_padding * 2 }; + LabelTextFlowContainer.Padding = new MarginPadding { Right = Nub.DEFAULT_EXPANDED_SIZE + nub_padding * 2 }; } else { Nub.Anchor = Anchor.CentreLeft; Nub.Origin = Anchor.CentreLeft; Nub.Margin = new MarginPadding { Left = nub_padding }; - LabelTextFlowContainer.Padding = new MarginPadding { Left = Nub.EXPANDED_SIZE + nub_padding * 2 }; + LabelTextFlowContainer.Padding = new MarginPadding { Left = Nub.DEFAULT_EXPANDED_SIZE + nub_padding * 2 }; } Nub.Current.BindTo(Current); diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs index 1b5f7cc4b5..96797e5d01 100644 --- a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuContextMenu.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 osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -17,7 +15,7 @@ namespace osu.Game.Graphics.UserInterface private const int fade_duration = 250; [Resolved] - private OsuContextMenuSamples samples { get; set; } + private OsuContextMenuSamples samples { get; set; } = null!; // todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed. private bool wasOpened; diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 3230bb0569..b530172f3e 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -335,12 +335,11 @@ namespace osu.Game.Graphics.UserInterface { new Drawable[] { - Text = new OsuSpriteText + Text = new TruncatingSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.X, - Truncate = true, }, Icon = new SpriteIcon { diff --git a/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs b/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs index dc089e3410..14f8f6f3c5 100644 --- a/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuEnumDropdown.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; namespace osu.Game.Graphics.UserInterface diff --git a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs index f6a3abdaae..df92863797 100644 --- a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Extensions; namespace osu.Game.Graphics.UserInterface diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs index 63c98d7838..123854a2dd 100644 --- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.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 osuTK; using osuTK.Graphics; using osuTK.Input; @@ -16,6 +14,7 @@ using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Platform; +using osu.Game.Localisation; namespace osu.Game.Graphics.UserInterface { @@ -38,7 +37,7 @@ namespace osu.Game.Graphics.UserInterface private readonly CapsWarning warning; [Resolved] - private GameHost host { get; set; } + private GameHost host { get; set; } = null!; public OsuPasswordTextBox() { @@ -112,7 +111,7 @@ namespace osu.Game.Graphics.UserInterface private partial class CapsWarning : SpriteIcon, IHasTooltip { - public LocalisableString TooltipText => "caps lock is active"; + public LocalisableString TooltipText => CommonStrings.CapsLockIsActive; public CapsWarning() { diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index e5f5f97eb7..191a7ca246 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -62,7 +62,7 @@ namespace osu.Game.Graphics.UserInterface if (!PlaySamplesOnAdjust) return; - if (Clock == null || Clock.CurrentTime - lastSampleTime <= 30) + if (Clock.CurrentTime - lastSampleTime <= 30) return; if (value.Equals(lastSampleValue)) diff --git a/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs b/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs index 01d072b6d7..6272f95510 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabDropdown.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 osuTK; using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 99803e2956..04ecfa7e9a 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -86,6 +86,7 @@ namespace osu.Game.Graphics.UserInterface Placeholder.Colour = colourProvider?.Foreground1 ?? new Color4(180, 180, 180, 255); + // Note that `KeyBindingRow` uses similar logic for input feedback, so remember to update there if changing here. var textAddedSamples = new Sample?[4]; for (int i = 0; i < textAddedSamples.Length; i++) textAddedSamples[i] = audio.Samples.Get($@"Keyboard/key-press-{1 + i}"); diff --git a/osu.Game/Graphics/UserInterface/PageSelector/PageEllipsis.cs b/osu.Game/Graphics/UserInterface/PageSelector/PageEllipsis.cs index 068e477d79..0f3b09f2dc 100644 --- a/osu.Game/Graphics/UserInterface/PageSelector/PageEllipsis.cs +++ b/osu.Game/Graphics/UserInterface/PageSelector/PageEllipsis.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Graphics/UserInterface/PageSelector/PageSelector.cs b/osu.Game/Graphics/UserInterface/PageSelector/PageSelector.cs index 63f35d5f89..416cd4b8ff 100644 --- a/osu.Game/Graphics/UserInterface/PageSelector/PageSelector.cs +++ b/osu.Game/Graphics/UserInterface/PageSelector/PageSelector.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 osu.Framework.Graphics.Containers; using osu.Framework.Graphics; diff --git a/osu.Game/Graphics/UserInterface/PercentageCounter.cs b/osu.Game/Graphics/UserInterface/PercentageCounter.cs index de93d9b2b4..ed06211e8f 100644 --- a/osu.Game/Graphics/UserInterface/PercentageCounter.cs +++ b/osu.Game/Graphics/UserInterface/PercentageCounter.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 osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs index a666b83c05..0981881ead 100644 --- a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs @@ -51,7 +51,7 @@ namespace osu.Game.Graphics.UserInterface public RoundedSliderBar() { Height = Nub.HEIGHT; - RangePadding = Nub.EXPANDED_SIZE / 2; + RangePadding = Nub.DEFAULT_EXPANDED_SIZE / 2; Children = new Drawable[] { new Container @@ -93,11 +93,16 @@ namespace osu.Game.Graphics.UserInterface nubContainer = new Container { RelativeSizeAxes = Axes.Both, - Child = Nub = new Nub + Child = Nub = new SliderNub { Origin = Anchor.TopCentre, RelativePositionAxes = Axes.X, - Current = { Value = true } + Current = { Value = true }, + OnDoubleClicked = () => + { + if (!Current.Disabled) + Current.SetDefault(); + }, }, }, hoverClickSounds = new HoverClickSounds() @@ -166,5 +171,18 @@ namespace osu.Game.Graphics.UserInterface { Nub.MoveToX(value, 250, Easing.OutQuint); } + + public partial class SliderNub : Nub + { + public Action? OnDoubleClicked { get; init; } + + protected override bool OnClick(ClickEvent e) => true; + + protected override bool OnDoubleClick(DoubleClickEvent e) + { + OnDoubleClicked?.Invoke(); + return true; + } + } } } diff --git a/osu.Game/Graphics/UserInterface/SearchTextBox.cs b/osu.Game/Graphics/UserInterface/SearchTextBox.cs index 2d09a239bb..a2e0ab6482 100644 --- a/osu.Game/Graphics/UserInterface/SearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SearchTextBox.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 osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Graphics/UserInterface/SeekLimitedSearchTextBox.cs b/osu.Game/Graphics/UserInterface/SeekLimitedSearchTextBox.cs index a85cd36808..11cf88c20e 100644 --- a/osu.Game/Graphics/UserInterface/SeekLimitedSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SeekLimitedSearchTextBox.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 - namespace osu.Game.Graphics.UserInterface { /// diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs index f1afacb2f4..b1e7066a01 100644 --- a/osu.Game/Graphics/UserInterface/ShearedButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs @@ -17,6 +17,10 @@ namespace osu.Game.Graphics.UserInterface { public partial class ShearedButton : OsuClickableContainer { + public const float HEIGHT = 50; + public const float CORNER_RADIUS = 7; + public const float BORDER_THICKNESS = 2; + public LocalisableString Text { get => text.Text; @@ -83,12 +87,10 @@ namespace osu.Game.Graphics.UserInterface /// public ShearedButton(float? width = null) { - Height = 50; + Height = HEIGHT; Padding = new MarginPadding { Horizontal = shear * 50 }; - const float corner_radius = 7; - - Content.CornerRadius = corner_radius; + Content.CornerRadius = CORNER_RADIUS; Content.Shear = new Vector2(shear, 0); Content.Masking = true; Content.Anchor = Content.Origin = Anchor.Centre; @@ -98,9 +100,9 @@ namespace osu.Game.Graphics.UserInterface backgroundLayer = new Container { RelativeSizeAxes = Axes.Y, - CornerRadius = corner_radius, + CornerRadius = CORNER_RADIUS, Masking = true, - BorderThickness = 2, + BorderThickness = BORDER_THICKNESS, Children = new Drawable[] { background = new Box diff --git a/osu.Game/Graphics/UserInterface/ShearedNub.cs b/osu.Game/Graphics/UserInterface/ShearedNub.cs index 3a09fd7445..7485f68525 100644 --- a/osu.Game/Graphics/UserInterface/ShearedNub.cs +++ b/osu.Game/Graphics/UserInterface/ShearedNub.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -18,6 +19,8 @@ namespace osu.Game.Graphics.UserInterface { public partial class ShearedNub : Container, IHasCurrentValue, IHasAccentColour { + public Action? OnDoubleClicked { get; init; } + protected const float BORDER_WIDTH = 3; public const int HEIGHT = 30; @@ -179,5 +182,13 @@ namespace osu.Game.Graphics.UserInterface main.TransformTo(nameof(BorderThickness), BORDER_WIDTH, duration, Easing.OutQuint); } } + + protected override bool OnClick(ClickEvent e) => true; + + protected override bool OnDoubleClick(DoubleClickEvent e) + { + OnDoubleClicked?.Invoke(); + return true; + } } } diff --git a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs index 7bd083f9d5..3940bf8bca 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -10,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -37,6 +36,14 @@ namespace osu.Game.Graphics.UserInterface set => textBox.HoldFocus = value; } + public LocalisableString PlaceholderText + { + get => textBox.PlaceholderText; + set => textBox.PlaceholderText = value; + } + + public new bool HasFocus => textBox.HasFocus; + public void TakeFocus() => textBox.TakeFocus(); public void KillFocus() => textBox.KillFocus(); diff --git a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs index a18a6a259c..60a6670492 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs @@ -100,7 +100,12 @@ namespace osu.Game.Graphics.UserInterface X = -SHEAR.X * HEIGHT / 2f, Origin = Anchor.TopCentre, RelativePositionAxes = Axes.X, - Current = { Value = true } + Current = { Value = true }, + OnDoubleClicked = () => + { + if (!Current.Disabled) + Current.SetDefault(); + }, }, }, hoverClickSounds = new HoverClickSounds() diff --git a/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs index d5e0abe9d8..05ed531d02 100644 --- a/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs @@ -14,6 +14,12 @@ namespace osu.Game.Graphics.UserInterface private Sample? sampleOff; private Sample? sampleOn; + /// + /// Sheared toggle buttons by default play two samples when toggled: a click and a toggle (on/off). + /// Sometimes this might be too much. Setting this to false will silence the toggle sound. + /// + protected virtual bool PlayToggleSamples => true; + /// /// Whether this button is currently toggled to an active state. /// @@ -68,10 +74,13 @@ namespace osu.Game.Graphics.UserInterface { sampleClick?.Play(); - if (Active.Value) - sampleOn?.Play(); - else - sampleOff?.Play(); + if (PlayToggleSamples) + { + if (Active.Value) + sampleOn?.Play(); + else + sampleOff?.Play(); + } } } } diff --git a/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs b/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs index e3f5bc65e6..33382305cf 100644 --- a/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs +++ b/osu.Game/Graphics/UserInterface/SlimEnumDropdown.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 osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Graphics/UserInterface/StarCounter.cs b/osu.Game/Graphics/UserInterface/StarCounter.cs index d7d088d798..fe986b275e 100644 --- a/osu.Game/Graphics/UserInterface/StarCounter.cs +++ b/osu.Game/Graphics/UserInterface/StarCounter.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 osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -32,6 +30,11 @@ namespace osu.Game.Graphics.UserInterface private const float star_spacing = 4; + public virtual FillDirection Direction + { + set => stars.Direction = value; + } + private float current; /// @@ -66,7 +69,6 @@ namespace osu.Game.Graphics.UserInterface stars = new FillFlowContainer { AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, Spacing = new Vector2(star_spacing), ChildrenEnumerable = Enumerable.Range(0, StarCount).Select(_ => CreateStar()) } diff --git a/osu.Game/Graphics/UserInterface/TimeSlider.cs b/osu.Game/Graphics/UserInterface/TimeSlider.cs index e6e7ae9305..7652eda7be 100644 --- a/osu.Game/Graphics/UserInterface/TimeSlider.cs +++ b/osu.Game/Graphics/UserInterface/TimeSlider.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Localisation; namespace osu.Game.Graphics.UserInterface diff --git a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs index aa542b8f49..5532e5c6a7 100644 --- a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs +++ b/osu.Game/Graphics/UserInterface/TwoLayerButton.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledColourPalette.cs index 721d8990ba..163d1fb2b9 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledColourPalette.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledColourPalette.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs index 8fd9a62ad7..59a1812e5d 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledDropdown.cs index 0e2ea362da..dbbae390a7 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledDropdown.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledDropdown.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Graphics; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledEnumDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledEnumDropdown.cs index 3ca460be90..9c2c8397ed 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledEnumDropdown.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledEnumDropdown.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledNumberBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledNumberBox.cs index 2643db0547..4926afd7a3 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledNumberBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledNumberBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Graphics.UserInterface; namespace osu.Game.Graphics.UserInterfaceV2 diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs index 00f4ef1a30..4585d3a4c9 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Game.Overlays.Settings; diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledSwitchButton.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledSwitchButton.cs index 3c27829de3..d25bcbfbe9 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledSwitchButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledSwitchButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Graphics.UserInterfaceV2 { public partial class LabelledSwitchButton : LabelledComponent diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index 454be02d0b..8b9d35e343 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -35,6 +35,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 public string Text { + get => Component.Text; set => Component.Text = value; } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuColourPicker.cs b/osu.Game/Graphics/UserInterfaceV2/OsuColourPicker.cs index fed17eaf20..cf86206f5c 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuColourPicker.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuColourPicker.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics.UserInterface; namespace osu.Game.Graphics.UserInterfaceV2 diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs index beaeb86243..c0ac9f21ca 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.IO; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs b/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs index ff51f3aa92..63bad283a8 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs @@ -1,9 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Framework.Bindables; using osu.Framework.Graphics; @@ -26,7 +23,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected override SaturationValueSelector CreateSaturationValueSelector() => new OsuSaturationValueSelector(); [BackgroundDependencyLoader(true)] - private void load([CanBeNull] OverlayColourProvider colourProvider, OsuColour osuColour) + private void load(OverlayColourProvider? colourProvider, OsuColour osuColour) { Background.Colour = colourProvider?.Dark5 ?? osuColour.GreySeaFoamDark; diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs b/osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs index 9aa650d88d..3621ca165f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs @@ -1,9 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -23,7 +20,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 } [BackgroundDependencyLoader(true)] - private void load([CanBeNull] OverlayColourProvider overlayColourProvider, OsuColour osuColour) + private void load(OverlayColourProvider? overlayColourProvider, OsuColour osuColour) { Background.Colour = overlayColourProvider?.Dark6 ?? osuColour.GreySeaFoamDarker; } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs index d89322cecd..9b4689958c 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs @@ -1,10 +1,9 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -24,6 +23,14 @@ namespace osu.Game.Graphics.UserInterfaceV2 private const float fade_duration = 250; private const double scale_duration = 500; + private Sample? samplePopIn; + private Sample? samplePopOut; + protected virtual string PopInSampleName => "UI/overlay-pop-in"; + protected virtual string PopOutSampleName => "UI/overlay-pop-out"; + + // required due to LoadAsyncComplete() in `VisibilityContainer` calling PopOut() during load - similar workaround to `OsuDropdownMenu` + private bool wasOpened; + public OsuPopover(bool withPadding = true) { Content.Padding = withPadding ? new MarginPadding(20) : new MarginPadding(); @@ -41,9 +48,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 } [BackgroundDependencyLoader(true)] - private void load([CanBeNull] OverlayColourProvider colourProvider, OsuColour colours) + private void load(OverlayColourProvider? colourProvider, OsuColour colours, AudioManager audio) { Background.Colour = Arrow.Colour = colourProvider?.Background4 ?? colours.GreySeaFoamDarker; + samplePopIn = audio.Samples.Get(PopInSampleName); + samplePopOut = audio.Samples.Get(PopOutSampleName); } protected override Drawable CreateArrow() => Empty(); @@ -52,12 +61,18 @@ namespace osu.Game.Graphics.UserInterfaceV2 { this.ScaleTo(1, scale_duration, Easing.OutElasticHalf); this.FadeIn(fade_duration, Easing.OutQuint); + + samplePopIn?.Play(); + wasOpened = true; } protected override void PopOut() { this.ScaleTo(0.7f, scale_duration, Easing.OutQuint); this.FadeOut(fade_duration, Easing.OutQuint); + + if (wasOpened) + samplePopOut?.Play(); } protected override bool OnKeyDown(KeyDownEvent e) @@ -68,7 +83,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 return base.OnKeyDown(e); } - public bool OnPressed(KeyBindingPressEvent e) + public virtual bool OnPressed(KeyBindingPressEvent e) { if (e.Repeat) return false; diff --git a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs new file mode 100644 index 0000000000..37ea2a3f96 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs @@ -0,0 +1,145 @@ +// 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.Globalization; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; +using osu.Game.Overlays.Settings; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class SliderWithTextBoxInput : CompositeDrawable, IHasCurrentValue + where T : struct, IEquatable, IComparable, IConvertible + { + /// + /// A custom step value for each key press which actuates a change on this control. + /// + public float KeyboardStep + { + get => slider.KeyboardStep; + set => slider.KeyboardStep = value; + } + + public Bindable Current + { + get => slider.Current; + set => slider.Current = value; + } + + private bool instantaneous; + + /// + /// Whether changes to the slider should instantaneously transfer to the text box (and vice versa). + /// If , the transfer will happen on text box commit (explicit, or implicit via focus loss), or on slider drag end. + /// + public bool Instantaneous + { + get => instantaneous; + set + { + instantaneous = value; + slider.TransferValueOnCommit = !instantaneous; + } + } + + private readonly SettingsSlider slider; + private readonly LabelledTextBox textBox; + + public SliderWithTextBoxInput(LocalisableString labelText) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(20), + Children = new Drawable[] + { + textBox = new LabelledTextBox + { + Label = labelText, + }, + slider = new SettingsSlider + { + TransferValueOnCommit = true, + RelativeSizeAxes = Axes.X, + } + } + }, + }; + + textBox.OnCommit += textCommitted; + textBox.Current.BindValueChanged(textChanged); + + Current.BindValueChanged(updateTextBoxFromSlider, true); + } + + public bool TakeFocus() => GetContainingInputManager().ChangeFocus(textBox); + + private bool updatingFromTextBox; + + private void textChanged(ValueChangedEvent change) + { + if (!instantaneous) return; + + tryUpdateSliderFromTextBox(); + } + + private void textCommitted(TextBox t, bool isNew) + { + tryUpdateSliderFromTextBox(); + + // If the attempted update above failed, restore text box to match the slider. + Current.TriggerChange(); + } + + private void tryUpdateSliderFromTextBox() + { + updatingFromTextBox = true; + + try + { + switch (slider.Current) + { + case Bindable bindableInt: + bindableInt.Value = int.Parse(textBox.Current.Value); + break; + + case Bindable bindableDouble: + bindableDouble.Value = double.Parse(textBox.Current.Value); + break; + + default: + slider.Current.Parse(textBox.Current.Value); + break; + } + } + catch + { + // ignore parsing failures. + // sane state will eventually be restored by a commit (either explicit, or implicit via focus loss). + } + + updatingFromTextBox = false; + } + + private void updateTextBoxFromSlider(ValueChangedEvent _) + { + if (updatingFromTextBox) return; + + decimal decimalValue = slider.Current.Value.ToDecimal(NumberFormatInfo.InvariantInfo); + textBox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}"); + } + } +} diff --git a/osu.Game/IO/Archives/LegacyByteArrayReader.cs b/osu.Game/IO/Archives/ByteArrayArchiveReader.cs similarity index 73% rename from osu.Game/IO/Archives/LegacyByteArrayReader.cs rename to osu.Game/IO/Archives/ByteArrayArchiveReader.cs index e58dbb48ce..0e2dee3456 100644 --- a/osu.Game/IO/Archives/LegacyByteArrayReader.cs +++ b/osu.Game/IO/Archives/ByteArrayArchiveReader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.IO; @@ -11,11 +9,11 @@ namespace osu.Game.IO.Archives /// /// Allows reading a single file from the provided byte array. /// - public class LegacyByteArrayReader : ArchiveReader + public class ByteArrayArchiveReader : ArchiveReader { private readonly byte[] content; - public LegacyByteArrayReader(byte[] content, string filename) + public ByteArrayArchiveReader(byte[] content, string filename) : base(filename) { this.content = content; diff --git a/osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs b/osu.Game/IO/Archives/DirectoryArchiveReader.cs similarity index 76% rename from osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs rename to osu.Game/IO/Archives/DirectoryArchiveReader.cs index e26f6af081..f2012b7b49 100644 --- a/osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs +++ b/osu.Game/IO/Archives/DirectoryArchiveReader.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.Collections.Generic; using System.IO; using System.Linq; @@ -10,20 +8,22 @@ using System.Linq; namespace osu.Game.IO.Archives { /// - /// Reads an archive from a directory on disk. + /// Reads an archive directly from a directory on disk. /// - public class LegacyDirectoryArchiveReader : ArchiveReader + public class DirectoryArchiveReader : ArchiveReader { private readonly string path; - public LegacyDirectoryArchiveReader(string path) + public DirectoryArchiveReader(string path) : base(Path.GetFileName(path)) { // re-get full path to standardise with Directory.GetFiles return values below. this.path = Path.GetFullPath(path); } - public override Stream GetStream(string name) => File.OpenRead(Path.Combine(path, name)); + public override Stream GetStream(string name) => File.OpenRead(GetFullPath(name)); + + public string GetFullPath(string filename) => Path.Combine(path, filename); public override void Dispose() { diff --git a/osu.Game/IO/Archives/MemoryStreamArchiveReader.cs b/osu.Game/IO/Archives/MemoryStreamArchiveReader.cs new file mode 100644 index 0000000000..d8e1199e93 --- /dev/null +++ b/osu.Game/IO/Archives/MemoryStreamArchiveReader.cs @@ -0,0 +1,30 @@ +// 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.IO; + +namespace osu.Game.IO.Archives +{ + /// + /// Allows reading a single file from the provided memory stream. + /// + public class MemoryStreamArchiveReader : ArchiveReader + { + private readonly MemoryStream stream; + + public MemoryStreamArchiveReader(MemoryStream stream, string filename) + : base(filename) + { + this.stream = stream; + } + + public override Stream GetStream(string name) => new MemoryStream(stream.ToArray(), 0, (int)stream.Length); + + public override void Dispose() + { + } + + public override IEnumerable Filenames => new[] { Name }; + } +} diff --git a/osu.Game/IO/Archives/LegacyFileArchiveReader.cs b/osu.Game/IO/Archives/SingleFileArchiveReader.cs similarity index 74% rename from osu.Game/IO/Archives/LegacyFileArchiveReader.cs rename to osu.Game/IO/Archives/SingleFileArchiveReader.cs index aee1add2f6..79d9c5de71 100644 --- a/osu.Game/IO/Archives/LegacyFileArchiveReader.cs +++ b/osu.Game/IO/Archives/SingleFileArchiveReader.cs @@ -1,22 +1,20 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.IO; namespace osu.Game.IO.Archives { /// - /// Reads a file on disk as an archive. + /// Reads a single file on disk as an archive. /// Note: In this case, the file is not an extractable archive, use instead. /// - public class LegacyFileArchiveReader : ArchiveReader + public class SingleFileArchiveReader : ArchiveReader { private readonly string path; - public LegacyFileArchiveReader(string path) + public SingleFileArchiveReader(string path) : base(Path.GetFileName(path)) { // re-get full path to standardise diff --git a/osu.Game/IO/Archives/ZipArchiveReader.cs b/osu.Game/IO/Archives/ZipArchiveReader.cs index 1fca8aa055..5ef03b3641 100644 --- a/osu.Game/IO/Archives/ZipArchiveReader.cs +++ b/osu.Game/IO/Archives/ZipArchiveReader.cs @@ -31,7 +31,7 @@ namespace osu.Game.IO.Archives { ZipArchiveEntry entry = archive.Entries.SingleOrDefault(e => e.Key == name); if (entry == null) - throw new FileNotFoundException(); + return null; var owner = MemoryAllocator.Default.Allocate((int)entry.Size); diff --git a/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs b/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs index d47f936eb3..8d14385707 100644 --- a/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs +++ b/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.IO; diff --git a/osu.Game/IO/FileInfo.cs b/osu.Game/IO/FileInfo.cs index 3d32e7fb6d..b8a7f363e0 100644 --- a/osu.Game/IO/FileInfo.cs +++ b/osu.Game/IO/FileInfo.cs @@ -13,7 +13,7 @@ namespace osu.Game.IO public bool IsManaged => ID > 0; - public string Hash { get; set; } + public string Hash { get; set; } = string.Empty; public int ReferenceCount { get; set; } } diff --git a/osu.Game/IO/Legacy/ILegacySerializable.cs b/osu.Game/IO/Legacy/ILegacySerializable.cs index f21e67a34b..e5518a0a30 100644 --- a/osu.Game/IO/Legacy/ILegacySerializable.cs +++ b/osu.Game/IO/Legacy/ILegacySerializable.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.IO.Legacy { public interface ILegacySerializable diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index f4c55e4b0e..a936fa74da 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -1,11 +1,8 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Diagnostics; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Configuration; @@ -22,13 +19,11 @@ namespace osu.Game.IO /// /// The custom storage path as selected by the user. /// - [CanBeNull] - public string CustomStoragePath => storageConfig.Get(StorageConfig.FullPath); + public string? CustomStoragePath => storageConfig.Get(StorageConfig.FullPath); /// /// The default storage path to be used if a custom storage path hasn't been selected or is not accessible. /// - [NotNull] public string DefaultStoragePath => defaultStorage.GetFullPath("."); private readonly GameHost host; diff --git a/osu.Game/IO/Serialization/Converters/SnakeCaseStringEnumConverter.cs b/osu.Game/IO/Serialization/Converters/SnakeCaseStringEnumConverter.cs index 65283d0d82..450eebd736 100644 --- a/osu.Game/IO/Serialization/Converters/SnakeCaseStringEnumConverter.cs +++ b/osu.Game/IO/Serialization/Converters/SnakeCaseStringEnumConverter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; diff --git a/osu.Game/IO/Serialization/SnakeCaseKeyContractResolver.cs b/osu.Game/IO/Serialization/SnakeCaseKeyContractResolver.cs index b51a8473ca..d01e8de26d 100644 --- a/osu.Game/IO/Serialization/SnakeCaseKeyContractResolver.cs +++ b/osu.Game/IO/Serialization/SnakeCaseKeyContractResolver.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 Newtonsoft.Json.Serialization; using osu.Game.Extensions; diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index fab0be6cf0..be025e3aa2 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -51,7 +51,7 @@ namespace osu.Game.Input.Bindings protected override void LoadComplete() { - realmSubscription = realm.RegisterForNotifications(queryRealmKeyBindings, (sender, _, _) => + realmSubscription = realm.RegisterForNotifications(queryRealmKeyBindings, (sender, _) => { // The first fire of this is a bit redundant as this is being called in base.LoadComplete, // but this is safest in case the subscription is restored after a context recycle. diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index fdd96d3890..947cd5f54f 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -1,50 +1,86 @@ // 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.Collections.Generic; using System.Linq; -using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Localisation; namespace osu.Game.Input.Bindings { - public partial class GlobalActionContainer : DatabasedKeyBindingContainer, IHandleGlobalKeyboardInput + public partial class GlobalActionContainer : DatabasedKeyBindingContainer, IHandleGlobalKeyboardInput, IKeyBindingHandler { - private readonly Drawable? handler; + protected override bool Prioritised => true; - private InputManager? parentInputManager; + private readonly IKeyBindingHandler? handler; public GlobalActionContainer(OsuGameBase? game) : base(matchingMode: KeyCombinationMatchingMode.Modifiers) { - if (game is IKeyBindingHandler) - handler = game; + if (game is IKeyBindingHandler h) + handler = h; } - protected override void LoadComplete() - { - base.LoadComplete(); - - parentInputManager = GetContainingInputManager(); - } - - // IMPORTANT: Take care when changing order of the items in the enumerable. - // It is used to decide the order of precedence, with the earlier items having higher precedence. - public override IEnumerable DefaultKeyBindings => GlobalKeyBindings - .Concat(EditorKeyBindings) - .Concat(InGameKeyBindings) - .Concat(ReplayKeyBindings) - .Concat(SongSelectKeyBindings) - .Concat(AudioControlKeyBindings) + /// + /// All default key bindings across all categories, ordered with highest priority first. + /// + /// + /// IMPORTANT: Take care when changing order of the items in the enumerable. + /// It is used to decide the order of precedence, with the earlier items having higher precedence. + /// + public override IEnumerable DefaultKeyBindings => globalKeyBindings + .Concat(editorKeyBindings) + .Concat(inGameKeyBindings) + .Concat(replayKeyBindings) + .Concat(songSelectKeyBindings) + .Concat(audioControlKeyBindings) // Overlay bindings may conflict with more local cases like the editor so they are checked last. // It has generally been agreed on that local screens like the editor should have priority, // based on such usages potentially requiring a lot more key bindings that may be "shared" with global ones. - .Concat(OverlayKeyBindings); + .Concat(overlayKeyBindings); - public IEnumerable GlobalKeyBindings => new[] + public static IEnumerable GetDefaultBindingsFor(GlobalActionCategory category) + { + switch (category) + { + case GlobalActionCategory.General: + return globalKeyBindings; + + case GlobalActionCategory.Editor: + return editorKeyBindings; + + case GlobalActionCategory.InGame: + return inGameKeyBindings; + + case GlobalActionCategory.Replay: + return replayKeyBindings; + + case GlobalActionCategory.SongSelect: + return songSelectKeyBindings; + + case GlobalActionCategory.AudioControl: + return audioControlKeyBindings; + + case GlobalActionCategory.Overlays: + return overlayKeyBindings; + + default: + throw new ArgumentOutOfRangeException(nameof(category), category, $"Unexpected {nameof(GlobalActionCategory)}"); + } + } + + public static IEnumerable GetGlobalActionsFor(GlobalActionCategory category) + => GetDefaultBindingsFor(category).Select(binding => binding.Action).Cast().Distinct(); + + public bool OnPressed(KeyBindingPressEvent e) => handler?.OnPressed(e) == true; + + public void OnReleased(KeyBindingReleaseEvent e) => handler?.OnReleased(e); + + private static IEnumerable globalKeyBindings => new[] { new KeyBinding(InputKey.Up, GlobalAction.SelectPrevious), new KeyBinding(InputKey.Down, GlobalAction.SelectNext), @@ -74,7 +110,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.F12, GlobalAction.TakeScreenshot), }; - public IEnumerable OverlayKeyBindings => new[] + private static IEnumerable overlayKeyBindings => new[] { new KeyBinding(InputKey.F8, GlobalAction.ToggleChat), new KeyBinding(InputKey.F6, GlobalAction.ToggleNowPlaying), @@ -84,7 +120,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications), }; - public IEnumerable EditorKeyBindings => new[] + private static IEnumerable editorKeyBindings => new[] { new KeyBinding(new[] { InputKey.F1 }, GlobalAction.EditorComposeMode), new KeyBinding(new[] { InputKey.F2 }, GlobalAction.EditorDesignMode), @@ -105,31 +141,37 @@ namespace osu.Game.Input.Bindings // See https://github.com/ppy/osu-framework/blob/master/osu.Framework/Input/StateChanges/MouseScrollRelativeInput.cs#L37-L38. new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), + new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), }; - public IEnumerable InGameKeyBindings => new[] + private static IEnumerable inGameKeyBindings => new[] { new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene), new KeyBinding(InputKey.ExtraMouseButton2, GlobalAction.SkipCutscene), new KeyBinding(InputKey.Tilde, GlobalAction.QuickRetry), + new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.QuickRetry), new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit), new KeyBinding(new[] { InputKey.F3 }, GlobalAction.DecreaseScrollSpeed), new KeyBinding(new[] { InputKey.F4 }, GlobalAction.IncreaseScrollSpeed), new KeyBinding(new[] { InputKey.Shift, InputKey.Tab }, GlobalAction.ToggleInGameInterface), + new KeyBinding(InputKey.Tab, GlobalAction.ToggleInGameLeaderboard), new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), - new KeyBinding(InputKey.Tab, GlobalAction.ToggleChatFocus), + new KeyBinding(InputKey.Enter, GlobalAction.ToggleChatFocus), + new KeyBinding(InputKey.F1, GlobalAction.SaveReplay), + new KeyBinding(InputKey.F2, GlobalAction.ExportReplay), }; - public IEnumerable ReplayKeyBindings => new[] + private static IEnumerable replayKeyBindings => new[] { new KeyBinding(InputKey.Space, GlobalAction.TogglePauseReplay), new KeyBinding(InputKey.MouseMiddle, GlobalAction.TogglePauseReplay), new KeyBinding(InputKey.Left, GlobalAction.SeekReplayBackward), new KeyBinding(InputKey.Right, GlobalAction.SeekReplayForward), + new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.ToggleReplaySettings), }; - public IEnumerable SongSelectKeyBindings => new[] + private static IEnumerable songSelectKeyBindings => new[] { new KeyBinding(InputKey.F1, GlobalAction.ToggleModSelection), new KeyBinding(InputKey.F2, GlobalAction.SelectNextRandom), @@ -138,7 +180,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods), }; - public IEnumerable AudioControlKeyBindings => new[] + private static IEnumerable audioControlKeyBindings => new[] { new KeyBinding(new[] { InputKey.Alt, InputKey.Up }, GlobalAction.IncreaseVolume), new KeyBinding(new[] { InputKey.Alt, InputKey.Down }, GlobalAction.DecreaseVolume), @@ -155,21 +197,6 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.PlayPause, GlobalAction.MusicPlay), new KeyBinding(InputKey.F3, GlobalAction.MusicPlay) }; - - protected override IEnumerable KeyBindingInputQueue - { - get - { - // To ensure the global actions are handled with priority, this GlobalActionContainer is actually placed after game content. - // It does not contain children as expected, so we need to forward the NonPositionalInputQueue from the parent input manager to correctly - // allow the whole game to handle these actions. - - // An eventual solution to this hack is to create localised action containers for individual components like SongSelect, but this will take some rearranging. - var inputQueue = parentInputManager?.NonPositionalInputQueue ?? base.KeyBindingInputQueue; - - return handler != null ? inputQueue.Prepend(handler) : inputQueue; - } - } } public enum GlobalAction @@ -201,7 +228,6 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleMute))] ToggleMute, - // In-Game Keybindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.SkipCutscene))] SkipCutscene, @@ -229,7 +255,6 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.QuickExit))] QuickExit, - // Game-wide beatmap music controller keybindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.MusicNext))] MusicNext, @@ -257,7 +282,6 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.PauseGameplay))] PauseGameplay, - // Editor [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSetupMode))] EditorSetupMode, @@ -282,7 +306,6 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleInGameInterface))] ToggleInGameInterface, - // Song select keybindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleModSelection))] ToggleModSelection, @@ -366,5 +389,31 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleNextBeatSnapDivisor))] EditorCycleNextBeatSnapDivisor, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.SaveReplay))] + SaveReplay, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ExportReplay))] + ExportReplay, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleReplaySettings))] + ToggleReplaySettings, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleInGameLeaderboard))] + ToggleInGameLeaderboard, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))] + EditorToggleRotateControl, + } + + public enum GlobalActionCategory + { + General, + Editor, + InGame, + Replay, + SongSelect, + AudioControl, + Overlays } } diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index 4af0357535..28b142978b 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using JetBrains.Annotations; using osu.Framework.Input.Bindings; using osu.Game.Database; +using osu.Game.Rulesets; using Realms; namespace osu.Game.Input.Bindings @@ -26,6 +28,13 @@ namespace osu.Game.Input.Bindings set => KeyCombinationString = value.ToString(); } + /// + /// The resultant action which is triggered by this binding. + /// + /// + /// This implementation always returns an integer. + /// If wanting to get the actual enum-typed value, use . + /// [Ignored] public object Action { @@ -53,5 +62,20 @@ namespace osu.Game.Input.Bindings private RealmKeyBinding() { } + + public object GetAction(RulesetStore rulesets) + { + if (string.IsNullOrEmpty(RulesetName)) + return (GlobalAction)ActionInt; + + var ruleset = rulesets.GetRuleset(RulesetName); + var actionType = ruleset!.CreateInstance() + .GetDefaultKeyBindings(Variant ?? 0) + .First() // let's just assume nobody does something stupid like mix multiple types... + .Action + .GetType(); + + return Enum.ToObject(actionType, ActionInt); + } } } diff --git a/osu.Game/Input/IdleTracker.cs b/osu.Game/Input/IdleTracker.cs index 54157c9e3d..40f270c234 100644 --- a/osu.Game/Input/IdleTracker.cs +++ b/osu.Game/Input/IdleTracker.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -68,7 +66,7 @@ namespace osu.Game.Input public void OnReleased(KeyBindingReleaseEvent e) => updateLastInteractionTime(); [Resolved] - private GameHost host { get; set; } + private GameHost host { get; set; } = null!; protected override bool Handle(UIEvent e) { diff --git a/osu.Game/Input/OsuConfineMouseMode.cs b/osu.Game/Input/OsuConfineMouseMode.cs index 2d914ac6e0..e875ceebd9 100644 --- a/osu.Game/Input/OsuConfineMouseMode.cs +++ b/osu.Game/Input/OsuConfineMouseMode.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Input; using osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Input/OsuUserInputManager.cs b/osu.Game/Input/OsuUserInputManager.cs index c205636ab9..b8fd0bb11c 100644 --- a/osu.Game/Input/OsuUserInputManager.cs +++ b/osu.Game/Input/OsuUserInputManager.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Framework.Input; using osuTK.Input; diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 10ad731037..48ace58235 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -130,5 +130,31 @@ namespace osu.Game.Input return true; } + + /// + /// Clears all s from the provided + /// which are assigned to more than one binding. + /// + /// The s to de-duplicate. + /// Number of bindings cleared. + public static int ClearDuplicateBindings(IEnumerable keyBindings) + { + int countRemoved = 0; + + var lookup = keyBindings.ToLookup(kb => kb.KeyCombination); + + foreach (var group in lookup) + { + if (group.Select(kb => kb.Action).Distinct().Count() <= 1) + continue; + + foreach (var binding in group) + binding.KeyCombination = new KeyCombination(InputKey.None); + + countRemoved += group.Count(); + } + + return countRemoved; + } } } diff --git a/osu.Game/Localisation/AccountCreationStrings.cs b/osu.Game/Localisation/AccountCreationStrings.cs new file mode 100644 index 0000000000..2183df9b52 --- /dev/null +++ b/osu.Game/Localisation/AccountCreationStrings.cs @@ -0,0 +1,54 @@ +// 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.Localisation; + +namespace osu.Game.Localisation +{ + public static class AccountCreationStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.AccountCreation"; + + /// + /// "New player registration" + /// + public static LocalisableString NewPlayerRegistration => new TranslatableString(getKey(@"new_player_registration"), @"New player registration"); + + /// + /// "Let's get you started" + /// + public static LocalisableString LetsGetYouStarted => new TranslatableString(getKey(@"lets_get_you_started"), @"Let's get you started"); + + /// + /// "Let's create an account!" + /// + public static LocalisableString LetsCreateAnAccount => new TranslatableString(getKey(@"lets_create_an_account"), @"Let's create an account!"); + + /// + /// "Help, I can't access my account!" + /// + public static LocalisableString MultiAccountWarningHelp => new TranslatableString(getKey(@"multi_account_warning_help"), @"Help, I can't access my account!"); + + /// + /// "I understand. This account isn't for me." + /// + public static LocalisableString MultiAccountWarningAccept => new TranslatableString(getKey(@"multi_account_warning_accept"), @"I understand. This account isn't for me."); + + /// + /// "This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!" + /// + public static LocalisableString UsernameDescription => new TranslatableString(getKey(@"username_description"), @"This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!"); + + /// + /// "Will be used for notifications, account verification and in the case you forget your password. No spam, ever." + /// + public static LocalisableString EmailDescription1 => new TranslatableString(getKey(@"email_description_1"), @"Will be used for notifications, account verification and in the case you forget your password. No spam, ever."); + + /// + /// " Make sure to get it right!" + /// + public static LocalisableString EmailDescription2 => new TranslatableString(getKey(@"email_description_2"), @" Make sure to get it right!"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index fed7b6cab7..2c377a81d9 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -39,6 +39,11 @@ namespace osu.Game.Localisation /// public static LocalisableString Default => new TranslatableString(getKey(@"default"), @"Default"); + /// + /// "Export" + /// + public static LocalisableString Export => new TranslatableString(getKey(@"export"), @"Export"); + /// /// "Width" /// @@ -154,11 +159,21 @@ namespace osu.Game.Localisation /// public static LocalisableString Exit => new TranslatableString(getKey(@"exit"), @"Exit"); + /// + /// "Caps lock is active" + /// + public static LocalisableString CapsLockIsActive => new TranslatableString(getKey(@"caps_lock_is_active"), @"Caps lock is active"); + /// /// "Revert to default" /// public static LocalisableString RevertToDefault => new TranslatableString(getKey(@"revert_to_default"), @"Revert to default"); + /// + /// "General" + /// + public static LocalisableString General => new TranslatableString(getKey(@"general"), @"General"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/ContextMenuStrings.cs b/osu.Game/Localisation/ContextMenuStrings.cs index 8bc213016b..029fba67d8 100644 --- a/osu.Game/Localisation/ContextMenuStrings.cs +++ b/osu.Game/Localisation/ContextMenuStrings.cs @@ -19,6 +19,11 @@ namespace osu.Game.Localisation /// public static LocalisableString ViewBeatmap => new TranslatableString(getKey(@"view_beatmap"), @"View beatmap"); + /// + /// "Invite player" + /// + public static LocalisableString InvitePlayer => new TranslatableString(getKey(@"invite_player"), @"Invite player"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/DebugSettingsStrings.cs b/osu.Game/Localisation/DebugSettingsStrings.cs index dd21739096..18fd3e83da 100644 --- a/osu.Game/Localisation/DebugSettingsStrings.cs +++ b/osu.Game/Localisation/DebugSettingsStrings.cs @@ -14,11 +14,6 @@ namespace osu.Game.Localisation /// public static LocalisableString DebugSectionHeader => new TranslatableString(getKey(@"debug_section_header"), @"Debug"); - /// - /// "General" - /// - public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General"); - /// /// "Show log overlay" /// diff --git a/osu.Game/Localisation/DifficultyMultiplierDisplayStrings.cs b/osu.Game/Localisation/DifficultyMultiplierDisplayStrings.cs deleted file mode 100644 index 952ca22678..0000000000 --- a/osu.Game/Localisation/DifficultyMultiplierDisplayStrings.cs +++ /dev/null @@ -1,19 +0,0 @@ -// 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.Localisation; - -namespace osu.Game.Localisation -{ - public static class DifficultyMultiplierDisplayStrings - { - private const string prefix = @"osu.Game.Resources.Localisation.DifficultyMultiplierDisplay"; - - /// - /// "Difficulty Multiplier" - /// - public static LocalisableString DifficultyMultiplier => new TranslatableString(getKey(@"difficulty_multiplier"), @"Difficulty Multiplier"); - - private static string getKey(string key) => $@"{prefix}:{key}"; - } -} diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 20258b9c35..93e52746c5 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -35,9 +35,14 @@ namespace osu.Game.Localisation public static LocalisableString SetPreviewPointToCurrent => new TranslatableString(getKey(@"set_preview_point_to_current"), @"Set preview point to current time"); /// - /// "Export package" + /// "For editing (.olz)" /// - public static LocalisableString ExportPackage => new TranslatableString(getKey(@"export_package"), @"Export package"); + public static LocalisableString ExportForEditing => new TranslatableString(getKey(@"export_for_editing"), @"For editing (.olz)"); + + /// + /// "For compatibility (.osz)" + /// + public static LocalisableString ExportForCompatibility => new TranslatableString(getKey(@"export_for_compatibility"), @"For compatibility (.osz)"); /// /// "Create new difficulty" @@ -109,6 +114,11 @@ namespace osu.Game.Localisation /// public static LocalisableString RotationSnapped(float newRotation) => new TranslatableString(getKey(@"rotation_snapped"), @"{0:0}° (snapped)", newRotation); + /// + /// "Limit distance snap placement to current time" + /// + public static LocalisableString LimitedDistanceSnap => new TranslatableString(getKey(@"limited_distance_snap_grid"), @"Limit distance snap placement to current time"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs index 40f39d927d..8ee76fdd55 100644 --- a/osu.Game/Localisation/GameplaySettingsStrings.cs +++ b/osu.Game/Localisation/GameplaySettingsStrings.cs @@ -19,11 +19,6 @@ namespace osu.Game.Localisation /// public static LocalisableString BeatmapHeader => new TranslatableString(getKey(@"beatmap_header"), @"Beatmap"); - /// - /// "General" - /// - public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General"); - /// /// "Audio" /// @@ -65,10 +60,15 @@ namespace osu.Game.Localisation public static LocalisableString HUDVisibilityMode => new TranslatableString(getKey(@"hud_visibility_mode"), @"HUD overlay visibility mode"); /// - /// "Show health display even when you can't fail" + /// "Show health display even when you can't fail" /// public static LocalisableString ShowHealthDisplayWhenCantFail => new TranslatableString(getKey(@"show_health_display_when_cant_fail"), @"Show health display even when you can't fail"); + /// + /// "Show replay settings overlay" + /// + public static LocalisableString ShowReplaySettingsOverlay => new TranslatableString(getKey(@"show_replay_settings_overlay"), @"Show replay settings overlay"); + /// /// "Fade playfield to red when health is low" /// @@ -134,6 +134,6 @@ namespace osu.Game.Localisation /// public static LocalisableString ClassicScoreDisplay => new TranslatableString(getKey(@"classic_score_display"), @"Classic"); - private static string getKey(string key) => $"{prefix}:{key}"; + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/GeneralSettingsStrings.cs b/osu.Game/Localisation/GeneralSettingsStrings.cs index a525af508b..ebf57d8109 100644 --- a/osu.Game/Localisation/GeneralSettingsStrings.cs +++ b/osu.Game/Localisation/GeneralSettingsStrings.cs @@ -9,11 +9,6 @@ namespace osu.Game.Localisation { private const string prefix = @"osu.Game.Resources.Localisation.GeneralSettings"; - /// - /// "General" - /// - public static LocalisableString GeneralSectionHeader => new TranslatableString(getKey(@"general_section_header"), @"General"); - /// /// "Language" /// diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index aa608a603b..8356c480dd 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -219,6 +219,11 @@ namespace osu.Game.Localisation /// public static LocalisableString ToggleInGameInterface => new TranslatableString(getKey(@"toggle_in_game_interface"), @"Toggle in-game interface"); + /// + /// "Toggle in-game leaderboard" + /// + public static LocalisableString ToggleInGameLeaderboard => new TranslatableString(getKey(@"toggle_in_game_leaderboard"), @"Toggle in-game leaderboard"); + /// /// "Toggle mod select" /// @@ -324,6 +329,26 @@ namespace osu.Game.Localisation /// public static LocalisableString ToggleChatFocus => new TranslatableString(getKey(@"toggle_chat_focus"), @"Toggle chat focus"); + /// + /// "Toggle replay settings" + /// + public static LocalisableString ToggleReplaySettings => new TranslatableString(getKey(@"toggle_replay_settings"), @"Toggle replay settings"); + + /// + /// "Save replay" + /// + public static LocalisableString SaveReplay => new TranslatableString(getKey(@"save_replay"), @"Save replay"); + + /// + /// "Export replay" + /// + public static LocalisableString ExportReplay => new TranslatableString(getKey(@"export_replay"), @"Export replay"); + + /// + /// "Toggle rotate control" + /// + public static LocalisableString EditorToggleRotateControl => new TranslatableString(getKey(@"editor_toggle_rotate_control"), @"Toggle rotate control"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/InputSettingsStrings.cs b/osu.Game/Localisation/InputSettingsStrings.cs index 2c9b175dfb..fcfe48bedb 100644 --- a/osu.Game/Localisation/InputSettingsStrings.cs +++ b/osu.Game/Localisation/InputSettingsStrings.cs @@ -64,6 +64,26 @@ namespace osu.Game.Localisation /// public static LocalisableString KeyBindingPanelDescription => new TranslatableString(getKey(@"key_binding_panel_description"), @"Customise your keys!"); + /// + /// "The binding you've selected conflicts with another existing binding." + /// + public static LocalisableString KeyBindingConflictDetected => new TranslatableString(getKey(@"key_binding_conflict_detected"), @"The binding you've selected conflicts with another existing binding."); + + /// + /// "Keep existing" + /// + public static LocalisableString KeepExistingBinding => new TranslatableString(getKey(@"keep_existing_binding"), @"Keep existing"); + + /// + /// "Apply new" + /// + public static LocalisableString ApplyNewBinding => new TranslatableString(getKey(@"apply_new_binding"), @"Apply new"); + + /// + /// "(none)" + /// + public static LocalisableString ActionHasNoKeyBinding => new TranslatableString(getKey(@"action_has_no_key_binding"), @"(none)"); + private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/LoginPanelStrings.cs b/osu.Game/Localisation/LoginPanelStrings.cs new file mode 100644 index 0000000000..925c2b9146 --- /dev/null +++ b/osu.Game/Localisation/LoginPanelStrings.cs @@ -0,0 +1,54 @@ +// 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.Localisation; + +namespace osu.Game.Localisation +{ + public static class LoginPanelStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.LoginPanel"; + + /// + /// "Do not disturb" + /// + public static LocalisableString DoNotDisturb => new TranslatableString(getKey(@"do_not_disturb"), @"Do not disturb"); + + /// + /// "Appear offline" + /// + public static LocalisableString AppearOffline => new TranslatableString(getKey(@"appear_offline"), @"Appear offline"); + + /// + /// "Signed in" + /// + public static LocalisableString SignedIn => new TranslatableString(getKey(@"signed_in"), @"Signed in"); + + /// + /// "Sign out" + /// + public static LocalisableString SignOut => new TranslatableString(getKey(@"sign_out"), @"Sign out"); + + /// + /// "Account" + /// + public static LocalisableString Account => new TranslatableString(getKey(@"account"), @"Account"); + + /// + /// "Remember username" + /// + public static LocalisableString RememberUsername => new TranslatableString(getKey(@"remember_username"), @"Remember username"); + + /// + /// "Stay signed in" + /// + public static LocalisableString StaySignedIn => new TranslatableString(getKey(@"stay_signed_in"), @"Stay signed in"); + + /// + /// "Register" + /// + public static LocalisableString Register => new TranslatableString(getKey(@"register"), @"Register"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/ModSelectOverlayStrings.cs b/osu.Game/Localisation/ModSelectOverlayStrings.cs index f11c52ee20..86ebebd293 100644 --- a/osu.Game/Localisation/ModSelectOverlayStrings.cs +++ b/osu.Game/Localisation/ModSelectOverlayStrings.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Localisation; @@ -39,6 +39,16 @@ namespace osu.Game.Localisation /// public static LocalisableString UseCurrentMods => new TranslatableString(getKey(@"use_current_mods"), @"Use current mods"); + /// + /// "tab to search..." + /// + public static LocalisableString TabToSearch => new TranslatableString(getKey(@"tab_to_search"), @"tab to search..."); + + /// + /// "Score Multiplier" + /// + public static LocalisableString ScoreMultiplier => new TranslatableString(getKey(@"score_multiplier"), @"Score Multiplier"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/MultiplayerMatchStrings.cs b/osu.Game/Localisation/MultiplayerMatchStrings.cs new file mode 100644 index 0000000000..95c7168a09 --- /dev/null +++ b/osu.Game/Localisation/MultiplayerMatchStrings.cs @@ -0,0 +1,29 @@ +// 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.Localisation; + +namespace osu.Game.Localisation +{ + public static class MultiplayerMatchStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.MultiplayerMatchStrings"; + + /// + /// "Stop countdown" + /// + public static LocalisableString StopCountdown => new TranslatableString(getKey(@"stop_countdown"), @"Stop countdown"); + + /// + /// "Countdown settings" + /// + public static LocalisableString CountdownSettings => new TranslatableString(getKey(@"countdown_settings"), @"Countdown settings"); + + /// + /// "Start match in {0}" + /// + public static LocalisableString StartMatchWithCountdown(string humanReadableTime) => new TranslatableString(getKey(@"start_match_width_countdown"), @"Start match in {0}", humanReadableTime); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index 5e2600bc50..fb3dab032d 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -29,11 +29,6 @@ namespace osu.Game.Localisation /// public static LocalisableString ClearAll => new TranslatableString(getKey(@"clear_all"), @"Clear All"); - /// - /// "Cancel All" - /// - public static LocalisableString CancelAll => new TranslatableString(getKey(@"cancel_all"), @"Cancel All"); - /// /// "Your battery level is low! Charge your device to prevent interruptions during gameplay." /// @@ -63,6 +58,61 @@ Please try changing your audio device to a working setting."); /// public static LocalisableString ScoreOverlayDisabled(LocalisableString arg0) => new TranslatableString(getKey(@"score_overlay_disabled"), @"The score overlay is currently disabled. You can toggle this by pressing {0}.", arg0); + /// + /// "The URL {0} has an unsupported or dangerous protocol and will not be opened." + /// + public static LocalisableString UnsupportedOrDangerousUrlProtocol(string url) => new TranslatableString(getKey(@"unsupported_or_dangerous_url_protocol"), @"The URL {0} has an unsupported or dangerous protocol and will not be opened.", url); + + /// + /// "Subsequent messages have been logged. Click to view log files." + /// + public static LocalisableString SubsequentMessagesLogged => new TranslatableString(getKey(@"subsequent_messages_logged"), @"Subsequent messages have been logged. Click to view log files."); + + /// + /// "Disabling tablet support due to error: "{0}"" + /// + public static LocalisableString TabletSupportDisabledDueToError(string message) => new TranslatableString(getKey(@"tablet_support_disabled_due_to_error"), @"Disabling tablet support due to error: ""{0}""", message); + + /// + /// "Encountered tablet warning, your tablet may not function correctly. Click here for a list of all tablets supported." + /// + public static LocalisableString EncounteredTabletWarning => new TranslatableString(getKey(@"encountered_tablet_warning"), @"Encountered tablet warning, your tablet may not function correctly. Click here for a list of all tablets supported."); + + /// + /// "This link type is not yet supported!" + /// + public static LocalisableString LinkTypeNotSupported => new TranslatableString(getKey(@"unsupported_link_type"), @"This link type is not yet supported!"); + + /// + /// "You received a private message from '{0}'. Click to read it!" + /// + public static LocalisableString PrivateMessageReceived(string username) => new TranslatableString(getKey(@"private_message_received"), @"You received a private message from '{0}'. Click to read it!", username); + + /// + /// "Your name was mentioned in chat by '{0}'. Click to find out why!" + /// + public static LocalisableString YourNameWasMentioned(string username) => new TranslatableString(getKey(@"your_name_was_mentioned"), @"Your name was mentioned in chat by '{0}'. Click to find out why!", username); + + /// + /// "{0} invited you to the multiplayer match "{1}"! Click to join." + /// + public static LocalisableString InvitedYouToTheMultiplayer(string username, string roomName) => new TranslatableString(getKey(@"invited_you_to_the_multiplayer"), @"{0} invited you to the multiplayer match ""{1}""! Click to join.", username, roomName); + + /// + /// "You do not have the beatmap for this replay." + /// + public static LocalisableString MissingBeatmapForReplay => new TranslatableString(getKey(@"missing_beatmap_for_replay"), @"You do not have the beatmap for this replay."); + + /// + /// "Downloading missing beatmap for this replay..." + /// + public static LocalisableString DownloadingBeatmapForReplay => new TranslatableString(getKey(@"downloading_beatmap_for_replay"), @"Downloading missing beatmap for this replay..."); + + /// + /// "Your local copy of the beatmap for this replay appears to be different than expected. You may need to update or re-download it." + /// + public static LocalisableString MismatchingBeatmapForReplay => new TranslatableString(getKey(@"mismatching_beatmap_for_replay"), @"Your local copy of the beatmap for this replay appears to be different than expected. You may need to update or re-download it."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/OnlinePlayStrings.cs b/osu.Game/Localisation/OnlinePlayStrings.cs new file mode 100644 index 0000000000..1918519d36 --- /dev/null +++ b/osu.Game/Localisation/OnlinePlayStrings.cs @@ -0,0 +1,29 @@ +// 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.Localisation; + +namespace osu.Game.Localisation +{ + public static class OnlinePlayStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.OnlinePlay"; + + /// + /// "Playlist durations longer than 2 weeks require an active osu!supporter tag." + /// + public static LocalisableString SupporterOnlyDurationNotice => new TranslatableString(getKey(@"supporter_only_duration_notice"), @"Playlist durations longer than 2 weeks require an active osu!supporter tag."); + + /// + /// "Can't invite this user as you have blocked them or they have blocked you." + /// + public static LocalisableString InviteFailedUserBlocked => new TranslatableString(getKey(@"cant_invite_this_user_as"), @"Can't invite this user as you have blocked them or they have blocked you."); + + /// + /// "Can't invite this user as they have opted out of non-friend communications." + /// + public static LocalisableString InviteFailedUserOptOut => new TranslatableString(getKey(@"cant_invite_this_user_as1"), @"Can't invite this user as they have opted out of non-friend communications."); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/OnlineSettingsStrings.cs b/osu.Game/Localisation/OnlineSettingsStrings.cs index 3200b1c75c..0660bac172 100644 --- a/osu.Game/Localisation/OnlineSettingsStrings.cs +++ b/osu.Game/Localisation/OnlineSettingsStrings.cs @@ -55,9 +55,9 @@ namespace osu.Game.Localisation public static LocalisableString PreferNoVideo => new TranslatableString(getKey(@"prefer_no_video"), @"Prefer downloads without video"); /// - /// "Automatically download beatmaps when spectating" + /// "Automatically download missing beatmaps" /// - public static LocalisableString AutomaticallyDownloadWhenSpectating => new TranslatableString(getKey(@"automatically_download_when_spectating"), @"Automatically download beatmaps when spectating"); + public static LocalisableString AutomaticallyDownloadMissingBeatmaps => new TranslatableString(getKey(@"automatically_download_missing_beatmaps"), @"Automatically download missing beatmaps"); /// /// "Show explicit content in search results" diff --git a/osu.Game/Localisation/SongSelectStrings.cs b/osu.Game/Localisation/SongSelectStrings.cs index e1ac328420..e715ba8880 100644 --- a/osu.Game/Localisation/SongSelectStrings.cs +++ b/osu.Game/Localisation/SongSelectStrings.cs @@ -19,6 +19,41 @@ namespace osu.Game.Localisation /// public static LocalisableString LocallyModifiedTooltip => new TranslatableString(getKey(@"locally_modified_tooltip"), @"Has been locally modified"); + /// + /// "Manage collections" + /// + public static LocalisableString ManageCollections => new TranslatableString(getKey(@"manage_collections"), @"Manage collections"); + + /// + /// "For all difficulties" + /// + public static LocalisableString ForAllDifficulties => new TranslatableString(getKey(@"for_all_difficulties"), @"For all difficulties"); + + /// + /// "Delete beatmap" + /// + public static LocalisableString DeleteBeatmap => new TranslatableString(getKey(@"delete_beatmap"), @"Delete beatmap"); + + /// + /// "For selected difficulty" + /// + public static LocalisableString ForSelectedDifficulty => new TranslatableString(getKey(@"for_selected_difficulty"), @"For selected difficulty"); + + /// + /// "Mark as played" + /// + public static LocalisableString MarkAsPlayed => new TranslatableString(getKey(@"mark_as_played"), @"Mark as played"); + + /// + /// "Clear all local scores" + /// + public static LocalisableString ClearAllLocalScores => new TranslatableString(getKey(@"clear_all_local_scores"), @"Clear all local scores"); + + /// + /// "Edit beatmap" + /// + public static LocalisableString EditBeatmap => new TranslatableString(getKey(@"edit_beatmap"), @"Edit beatmap"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/UserInterfaceStrings.cs b/osu.Game/Localisation/UserInterfaceStrings.cs index ea664d7b50..612668171c 100644 --- a/osu.Game/Localisation/UserInterfaceStrings.cs +++ b/osu.Game/Localisation/UserInterfaceStrings.cs @@ -14,11 +14,6 @@ namespace osu.Game.Localisation ///
public static LocalisableString UserInterfaceSectionHeader => new TranslatableString(getKey(@"user_interface_section_header"), @"User Interface"); - /// - /// "General" - /// - public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General"); - /// /// "Rotate cursor when dragging" /// diff --git a/osu.Game/Online/API/APIException.cs b/osu.Game/Online/API/APIException.cs index 21a9c761c7..4327600e13 100644 --- a/osu.Game/Online/API/APIException.cs +++ b/osu.Game/Online/API/APIException.cs @@ -1,15 +1,13 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; namespace osu.Game.Online.API { public class APIException : InvalidOperationException { - public APIException(string message, Exception innerException) + public APIException(string message, Exception? innerException) : base(message, innerException) { } diff --git a/osu.Game/Online/API/APIMessagesRequest.cs b/osu.Game/Online/API/APIMessagesRequest.cs index 5bb3e29621..3ad6b1d7c8 100644 --- a/osu.Game/Online/API/APIMessagesRequest.cs +++ b/osu.Game/Online/API/APIMessagesRequest.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.Collections.Generic; using osu.Framework.IO.Network; using osu.Game.Online.Chat; diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index cd6e8df754..6b6b222043 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -7,6 +7,7 @@ using System; using System.Globalization; using JetBrains.Annotations; using Newtonsoft.Json; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.IO.Network; using osu.Framework.Logging; using osu.Game.Extensions; @@ -46,7 +47,7 @@ namespace osu.Game.Online.API if (WebRequest != null) { Response = ((OsuJsonWebRequest)WebRequest).ResponseObject; - Logger.Log($"{GetType()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes", LoggingTarget.Network); + Logger.Log($"{GetType().ReadableName()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes", LoggingTarget.Network); } } diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 16afef8e30..d585124db6 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.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.Threading; using System.Threading.Tasks; @@ -22,7 +20,7 @@ namespace osu.Game.Online.API public Bindable LocalUser { get; } = new Bindable(new APIUser { - Username = @"Dummy", + Username = @"Local user", Id = DUMMY_USER_ID, }); @@ -34,7 +32,8 @@ namespace osu.Game.Online.API public string AccessToken => "token"; - public bool IsLoggedIn => State.Value == APIState.Online; + /// + public bool IsLoggedIn => State.Value > APIState.Offline; public string ProvidedUsername => LocalUser.Value.Username; @@ -44,17 +43,18 @@ namespace osu.Game.Online.API public int APIVersion => int.Parse(DateTime.Now.ToString("yyyyMMdd")); - public Exception LastLoginError { get; private set; } + public Exception? LastLoginError { get; private set; } /// /// Provide handling logic for an arbitrary API request. /// Should return true is a request was handled. If null or false return, the request will be failed with a . /// - public Func HandleRequest; + public Func? HandleRequest; private readonly Bindable state = new Bindable(APIState.Online); private bool shouldFailNextLogin; + private bool stayConnectingNextLogin; /// /// The current connectivity state of the API. @@ -93,6 +93,12 @@ namespace osu.Game.Online.API { state.Value = APIState.Connecting; + if (stayConnectingNextLogin) + { + stayConnectingNextLogin = false; + return; + } + if (shouldFailNextLogin) { LastLoginError = new APIException("Not powerful enough to login.", new ArgumentException(nameof(shouldFailNextLogin))); @@ -106,7 +112,7 @@ namespace osu.Game.Online.API LocalUser.Value = new APIUser { Username = username, - Id = 1001, + Id = DUMMY_USER_ID, }; state.Value = APIState.Online; @@ -114,15 +120,17 @@ namespace osu.Game.Online.API public void Logout() { - LocalUser.Value = new GuestUser(); state.Value = APIState.Offline; + // must happen after `state.Value` is changed such that subscribers to that bindable's value changes see the correct user. + // compare: `APIAccess.Logout()`. + LocalUser.Value = new GuestUser(); } - public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null; + public IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null; public NotificationsClientConnector GetNotificationsConnector() => new PollingNotificationsClientConnector(this); - public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) + public RegistrationRequest.RegistrationRequestErrors? CreateAccount(string email, string username, string password) { Thread.Sleep(200); return null; @@ -134,8 +142,16 @@ namespace osu.Game.Online.API IBindableList IAPIProvider.Friends => Friends; IBindable IAPIProvider.Activity => Activity; + /// + /// During the next simulated login, the process will fail immediately. + /// public void FailNextLogin() => shouldFailNextLogin = true; + /// + /// During the next simulated login, the process will pause indefinitely at "connecting". + /// + public void PauseOnConnectingNextLogin() => stayConnectingNextLogin = true; + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs index df64984c7a..3fad032531 100644 --- a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs +++ b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Buffers; using System.Collections.Generic; using System.Text; @@ -37,8 +35,8 @@ namespace osu.Game.Online.API for (int i = 0; i < itemCount; i++) { - output[reader.ReadString()] = - PrimitiveObjectFormatter.Instance.Deserialize(ref reader, options); + output[reader.ReadString()!] = + PrimitiveObjectFormatter.Instance.Deserialize(ref reader, options)!; } return output; diff --git a/osu.Game/Online/API/OAuth.cs b/osu.Game/Online/API/OAuth.cs index 58306c1938..485274f349 100644 --- a/osu.Game/Online/API/OAuth.cs +++ b/osu.Game/Online/API/OAuth.cs @@ -6,6 +6,7 @@ using System; using System.Diagnostics; using System.Net.Http; +using System.Net.Sockets; using Newtonsoft.Json; using osu.Framework.Bindables; @@ -99,9 +100,19 @@ namespace osu.Game.Online.API return true; } } + catch (SocketException) + { + // Network failure. + return false; + } + catch (HttpRequestException) + { + // Network failure. + return false; + } catch { - //todo: potentially only kill the refresh token on certain exception types. + // Force a full re-authentication. Token.Value = null; return false; } diff --git a/osu.Game/Online/API/OsuJsonWebRequest.cs b/osu.Game/Online/API/OsuJsonWebRequest.cs index 2d402edd3f..eb0be57d9f 100644 --- a/osu.Game/Online/API/OsuJsonWebRequest.cs +++ b/osu.Game/Online/API/OsuJsonWebRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.IO.Network; namespace osu.Game.Online.API diff --git a/osu.Game/Online/API/OsuWebRequest.cs b/osu.Game/Online/API/OsuWebRequest.cs index 9a7cf45a2f..ee7115fa96 100644 --- a/osu.Game/Online/API/OsuWebRequest.cs +++ b/osu.Game/Online/API/OsuWebRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.IO.Network; namespace osu.Game.Online.API diff --git a/osu.Game/Online/API/Requests/CommentVoteRequest.cs b/osu.Game/Online/API/Requests/CommentVoteRequest.cs index a835b0365c..06a3b1126e 100644 --- a/osu.Game/Online/API/Requests/CommentVoteRequest.cs +++ b/osu.Game/Online/API/Requests/CommentVoteRequest.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 osu.Framework.IO.Network; using osu.Game.Online.API.Requests.Responses; using System.Net.Http; diff --git a/osu.Game/Online/API/Requests/CreateChannelRequest.cs b/osu.Game/Online/API/Requests/CreateChannelRequest.cs index 130210b1c3..e660c4a883 100644 --- a/osu.Game/Online/API/Requests/CreateChannelRequest.cs +++ b/osu.Game/Online/API/Requests/CreateChannelRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Net.Http; using osu.Framework.IO.Network; diff --git a/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs b/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs index 6b7192dbf4..0429a30b0b 100644 --- a/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs +++ b/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.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.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Online/API/Requests/Cursor.cs b/osu.Game/Online/API/Requests/Cursor.cs index c7bb119bd8..8ddbce39ca 100644 --- a/osu.Game/Online/API/Requests/Cursor.cs +++ b/osu.Game/Online/API/Requests/Cursor.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 JetBrains.Annotations; using Newtonsoft.Json; diff --git a/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs index f190b6e821..5254dc3cf8 100644 --- a/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs +++ b/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.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 osu.Framework.IO.Network; using osu.Game.Beatmaps; diff --git a/osu.Game/Online/API/Requests/DownloadReplayRequest.cs b/osu.Game/Online/API/Requests/DownloadReplayRequest.cs index 5635c4728e..3ea57cf637 100644 --- a/osu.Game/Online/API/Requests/DownloadReplayRequest.cs +++ b/osu.Game/Online/API/Requests/DownloadReplayRequest.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 osu.Game.Scoring; namespace osu.Game.Online.API.Requests @@ -16,6 +14,6 @@ namespace osu.Game.Online.API.Requests protected override string FileExtension => ".osr"; - protected override string Target => $@"scores/{Model.Ruleset.ShortName}/{Model.OnlineID}/download"; + protected override string Target => $@"scores/{Model.OnlineID}/download"; } } diff --git a/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs index f3690c934d..158ae03b8d 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapSetRequest.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 osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests diff --git a/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs index e118d1bddc..6cb9e6dd44 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; diff --git a/osu.Game/Online/API/Requests/GetChangelogBuildRequest.cs b/osu.Game/Online/API/Requests/GetChangelogBuildRequest.cs index 2d2d241b86..baa15c70c4 100644 --- a/osu.Game/Online/API/Requests/GetChangelogBuildRequest.cs +++ b/osu.Game/Online/API/Requests/GetChangelogBuildRequest.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 osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests diff --git a/osu.Game/Online/API/Requests/GetChangelogRequest.cs b/osu.Game/Online/API/Requests/GetChangelogRequest.cs index 82ed42615f..97799ff66a 100644 --- a/osu.Game/Online/API/Requests/GetChangelogRequest.cs +++ b/osu.Game/Online/API/Requests/GetChangelogRequest.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 osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests diff --git a/osu.Game/Online/API/Requests/GetCommentsRequest.cs b/osu.Game/Online/API/Requests/GetCommentsRequest.cs index 1aa08f2ed8..1e033a58a7 100644 --- a/osu.Game/Online/API/Requests/GetCommentsRequest.cs +++ b/osu.Game/Online/API/Requests/GetCommentsRequest.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 osu.Framework.IO.Network; using osu.Game.Extensions; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Online/API/Requests/GetCountryRankingsRequest.cs b/osu.Game/Online/API/Requests/GetCountryRankingsRequest.cs index 9d037ab116..d8a1198627 100644 --- a/osu.Game/Online/API/Requests/GetCountryRankingsRequest.cs +++ b/osu.Game/Online/API/Requests/GetCountryRankingsRequest.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 osu.Game.Rulesets; namespace osu.Game.Online.API.Requests diff --git a/osu.Game/Online/API/Requests/GetFriendsRequest.cs b/osu.Game/Online/API/Requests/GetFriendsRequest.cs index 640ddcbb9e..63a221d91a 100644 --- a/osu.Game/Online/API/Requests/GetFriendsRequest.cs +++ b/osu.Game/Online/API/Requests/GetFriendsRequest.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.Collections.Generic; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Online/API/Requests/GetMessagesRequest.cs b/osu.Game/Online/API/Requests/GetMessagesRequest.cs index 2f9879c63f..651f8a06c5 100644 --- a/osu.Game/Online/API/Requests/GetMessagesRequest.cs +++ b/osu.Game/Online/API/Requests/GetMessagesRequest.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.Collections.Generic; using osu.Game.Online.Chat; diff --git a/osu.Game/Online/API/Requests/GetRankingsRequest.cs b/osu.Game/Online/API/Requests/GetRankingsRequest.cs index f42da69dcc..ddc3298ca7 100644 --- a/osu.Game/Online/API/Requests/GetRankingsRequest.cs +++ b/osu.Game/Online/API/Requests/GetRankingsRequest.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 osu.Framework.IO.Network; using osu.Game.Rulesets; diff --git a/osu.Game/Online/API/Requests/GetSeasonalBackgroundsRequest.cs b/osu.Game/Online/API/Requests/GetSeasonalBackgroundsRequest.cs index 0ecce90749..941b47244a 100644 --- a/osu.Game/Online/API/Requests/GetSeasonalBackgroundsRequest.cs +++ b/osu.Game/Online/API/Requests/GetSeasonalBackgroundsRequest.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 osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests diff --git a/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.cs b/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.cs index 2d8a8b3b61..59b2928a2a 100644 --- a/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.cs +++ b/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.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 osu.Framework.IO.Network; using osu.Game.Overlays.Rankings; using osu.Game.Rulesets; diff --git a/osu.Game/Online/API/Requests/GetTopUsersRequest.cs b/osu.Game/Online/API/Requests/GetTopUsersRequest.cs index 7f05cd5eab..dbbd2119db 100644 --- a/osu.Game/Online/API/Requests/GetTopUsersRequest.cs +++ b/osu.Game/Online/API/Requests/GetTopUsersRequest.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 - namespace osu.Game.Online.API.Requests { public class GetTopUsersRequest : APIRequest diff --git a/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs index e4134980b1..226bc6bf1e 100644 --- a/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.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.Collections.Generic; using osu.Game.Extensions; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs b/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs index 3d0ee23080..67d3ad26b0 100644 --- a/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.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.Collections.Generic; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Online/API/Requests/GetUserMostPlayedBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetUserMostPlayedBeatmapsRequest.cs index e5e65415f7..bef3df42fb 100644 --- a/osu.Game/Online/API/Requests/GetUserMostPlayedBeatmapsRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserMostPlayedBeatmapsRequest.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.Collections.Generic; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Online/API/Requests/GetUserRankingsRequest.cs b/osu.Game/Online/API/Requests/GetUserRankingsRequest.cs index c27a83b695..628fc1be96 100644 --- a/osu.Game/Online/API/Requests/GetUserRankingsRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserRankingsRequest.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 osu.Framework.IO.Network; using osu.Game.Rulesets; using osu.Game.Users; diff --git a/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs b/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs index 82cf0a508a..79f0549d4a 100644 --- a/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.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.Collections.Generic; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Online/API/Requests/GetUsersRequest.cs b/osu.Game/Online/API/Requests/GetUsersRequest.cs index b57bb215aa..6f7e9c07d2 100644 --- a/osu.Game/Online/API/Requests/GetUsersRequest.cs +++ b/osu.Game/Online/API/Requests/GetUsersRequest.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; namespace osu.Game.Online.API.Requests diff --git a/osu.Game/Online/API/Requests/GetWikiRequest.cs b/osu.Game/Online/API/Requests/GetWikiRequest.cs index 7c84e1f790..f6bd80e210 100644 --- a/osu.Game/Online/API/Requests/GetWikiRequest.cs +++ b/osu.Game/Online/API/Requests/GetWikiRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Extensions; using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Online/API/Requests/JoinChannelRequest.cs b/osu.Game/Online/API/Requests/JoinChannelRequest.cs index 30b8fafd57..33eab7e355 100644 --- a/osu.Game/Online/API/Requests/JoinChannelRequest.cs +++ b/osu.Game/Online/API/Requests/JoinChannelRequest.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.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.Chat; diff --git a/osu.Game/Online/API/Requests/LeaveChannelRequest.cs b/osu.Game/Online/API/Requests/LeaveChannelRequest.cs index 4e77055e67..7dfc9a0aed 100644 --- a/osu.Game/Online/API/Requests/LeaveChannelRequest.cs +++ b/osu.Game/Online/API/Requests/LeaveChannelRequest.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.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.Chat; diff --git a/osu.Game/Online/API/Requests/ListChannelsRequest.cs b/osu.Game/Online/API/Requests/ListChannelsRequest.cs index 6f8fb427dc..9660695c14 100644 --- a/osu.Game/Online/API/Requests/ListChannelsRequest.cs +++ b/osu.Game/Online/API/Requests/ListChannelsRequest.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.Collections.Generic; using osu.Game.Online.Chat; diff --git a/osu.Game/Online/API/Requests/MarkChannelAsReadRequest.cs b/osu.Game/Online/API/Requests/MarkChannelAsReadRequest.cs index afdc8a47f4..b24669e6d5 100644 --- a/osu.Game/Online/API/Requests/MarkChannelAsReadRequest.cs +++ b/osu.Game/Online/API/Requests/MarkChannelAsReadRequest.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.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.Chat; diff --git a/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs b/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs index 4af1f58180..44b2b17d66 100644 --- a/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs +++ b/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Globalization; using osu.Framework.IO.Network; diff --git a/osu.Game/Online/API/Requests/PaginationParameters.cs b/osu.Game/Online/API/Requests/PaginationParameters.cs index 6dacb009bd..cab56bcf2e 100644 --- a/osu.Game/Online/API/Requests/PaginationParameters.cs +++ b/osu.Game/Online/API/Requests/PaginationParameters.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Online.API.Requests { /// diff --git a/osu.Game/Online/API/Requests/PostBeatmapFavouriteRequest.cs b/osu.Game/Online/API/Requests/PostBeatmapFavouriteRequest.cs index 1438c9c436..9fdc3382aa 100644 --- a/osu.Game/Online/API/Requests/PostBeatmapFavouriteRequest.cs +++ b/osu.Game/Online/API/Requests/PostBeatmapFavouriteRequest.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 osu.Framework.IO.Network; using System.Net.Http; diff --git a/osu.Game/Online/API/Requests/PostMessageRequest.cs b/osu.Game/Online/API/Requests/PostMessageRequest.cs index e3709d8f13..64b88f0340 100644 --- a/osu.Game/Online/API/Requests/PostMessageRequest.cs +++ b/osu.Game/Online/API/Requests/PostMessageRequest.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.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.Chat; diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index 7d6740ee46..902b651be9 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -63,6 +63,16 @@ namespace osu.Game.Online.API.Requests.Responses set => Length = TimeSpan.FromSeconds(value).TotalMilliseconds; } + [JsonIgnore] + public double HitLength { get; set; } + + [JsonProperty(@"hit_length")] + private double hitLengthInSeconds + { + get => TimeSpan.FromMilliseconds(HitLength).TotalSeconds; + set => HitLength = TimeSpan.FromSeconds(value).TotalMilliseconds; + } + [JsonProperty(@"convert")] public bool Convert { get; set; } diff --git a/osu.Game/Online/API/Requests/Responses/APIPlayStyle.cs b/osu.Game/Online/API/Requests/Responses/APIPlayStyle.cs index 4a877f392a..be43fc8ae7 100644 --- a/osu.Game/Online/API/Requests/Responses/APIPlayStyle.cs +++ b/osu.Game/Online/API/Requests/Responses/APIPlayStyle.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index e63395fe26..7c4093006d 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -29,7 +29,7 @@ namespace osu.Game.Online.API.Requests.Responses public DateTimeOffset JoinDate; [JsonProperty(@"username")] - public string Username { get; set; } + public string Username { get; set; } = string.Empty; [JsonProperty(@"previous_usernames")] public string[] PreviousUsernames; @@ -234,9 +234,8 @@ namespace osu.Game.Online.API.Requests.Responses set => Statistics.RankHistory = value; } - [JsonProperty(@"active_tournament_banner")] - [CanBeNull] - public TournamentBanner TournamentBanner; + [JsonProperty(@"active_tournament_banners")] + public TournamentBanner[] TournamentBanners; [JsonProperty("badges")] public Badge[] Badges; diff --git a/osu.Game/Online/API/Requests/Responses/APIUserAchievement.cs b/osu.Game/Online/API/Requests/Responses/APIUserAchievement.cs index 65001dd6cf..836a5bc485 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUserAchievement.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUserAchievement.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Newtonsoft.Json; diff --git a/osu.Game/Online/API/Requests/Responses/APIUserHistoryCount.cs b/osu.Game/Online/API/Requests/Responses/APIUserHistoryCount.cs index 6eb3c8b8a4..9a34699a1b 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUserHistoryCount.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUserHistoryCount.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Newtonsoft.Json; diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs index 15f4bace96..ac2d8152b1 100644 --- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs @@ -7,16 +7,16 @@ using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Users; namespace osu.Game.Online.API.Requests.Responses { [Serializable] - public class SoloScoreInfo : IHasOnlineID + public class SoloScoreInfo : IScoreInfo { [JsonProperty("beatmap_id")] public int BeatmapID { get; set; } @@ -138,6 +138,18 @@ namespace osu.Game.Online.API.Requests.Responses #endregion + #region IScoreInfo + + public long OnlineID => (long?)ID ?? -1; + + IUser IScoreInfo.User => User!; + DateTimeOffset IScoreInfo.Date => EndedAt; + long IScoreInfo.LegacyOnlineID => (long?)LegacyScoreId ?? -1; + IBeatmapInfo IScoreInfo.Beatmap => Beatmap!; + IRulesetInfo IScoreInfo.Ruleset => Beatmap!.Ruleset; + + #endregion + public override string ToString() => $"score_id: {ID} user_id: {UserID}"; /// @@ -178,6 +190,7 @@ namespace osu.Game.Online.API.Requests.Responses var score = new ScoreInfo { OnlineID = OnlineID, + LegacyOnlineID = (long?)LegacyScoreId ?? -1, User = User ?? new APIUser { Id = UserID }, BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID }, Ruleset = new RulesetInfo { OnlineID = RulesetID }, @@ -189,7 +202,7 @@ namespace osu.Game.Online.API.Requests.Responses Statistics = Statistics, MaximumStatistics = MaximumStatistics, Date = EndedAt, - Hash = HasReplay ? "online" : string.Empty, // TODO: temporary? + HasOnlineReplay = HasReplay, Mods = mods, PP = PP, }; @@ -223,7 +236,5 @@ namespace osu.Game.Online.API.Requests.Responses Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value), MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value), }; - - public long OnlineID => (long?)ID ?? -1; } } diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs index 144c4445a3..3db602c353 100644 --- a/osu.Game/Online/BeatmapDownloadTracker.cs +++ b/osu.Game/Online/BeatmapDownloadTracker.cs @@ -40,7 +40,7 @@ namespace osu.Game.Online // Used to interact with manager classes that don't support interface types. Will eventually be replaced. var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID }; - realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, _, _) => + realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, _) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index 761e8aba8d..15ce926039 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -12,6 +12,7 @@ using osu.Framework.Bindables; using osu.Framework.Lists; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Chat; +using osu.Game.Overlays.Chat.Listing; namespace osu.Game.Online.Chat { @@ -86,6 +87,12 @@ namespace osu.Game.Online.Chat [JsonProperty(@"last_read_id")] public long? LastReadId; + /// + /// Purposefully nullable for the sake of . + /// + [JsonProperty(@"message_length_limit")] + public int? MessageLengthLimit; + /// /// Signals if the current user joined this channel or not. Defaults to false. /// Note that this does not guarantee a join has completed. Check Id > 0 for confirmation. diff --git a/osu.Game/Online/Chat/DrawableLinkCompiler.cs b/osu.Game/Online/Chat/DrawableLinkCompiler.cs index ee53c00668..883a2496f7 100644 --- a/osu.Game/Online/Chat/DrawableLinkCompiler.cs +++ b/osu.Game/Online/Chat/DrawableLinkCompiler.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.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -27,8 +25,8 @@ namespace osu.Game.Online.Chat /// public readonly List Parts; - [Resolved(CanBeNull = true)] - private OverlayColourProvider overlayColourProvider { get; set; } + [Resolved] + private OverlayColourProvider? overlayColourProvider { get; set; } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parts.Any(d => d.ReceivePositionalInputAt(screenSpacePos)); diff --git a/osu.Game/Online/Chat/ErrorMessage.cs b/osu.Game/Online/Chat/ErrorMessage.cs index 9cd91a0927..ccfc0e29b4 100644 --- a/osu.Game/Online/Chat/ErrorMessage.cs +++ b/osu.Game/Online/Chat/ErrorMessage.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Online.Chat { public class ErrorMessage : InfoMessage diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 201212c648..56d24e35bb 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -18,6 +18,9 @@ namespace osu.Game.Online.Chat [Resolved] private GameHost host { get; set; } = null!; + [Resolved] + private Clipboard clipboard { get; set; } = null!; + [Resolved(CanBeNull = true)] private IDialogOverlay? dialogOverlay { get; set; } @@ -32,7 +35,7 @@ namespace osu.Game.Online.Chat public void OpenUrlExternally(string url, bool bypassWarning = false) { if (!bypassWarning && externalLinkWarning.Value && dialogOverlay != null) - dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => host.GetClipboard()?.SetText(url))); + dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => clipboard.SetText(url))); else host.OpenUrlExternally(url); } diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 523185a7cb..667175117f 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.Chat { @@ -27,7 +28,7 @@ namespace osu.Game.Online.Chat // http[s]://.[:port][/path][?query][#fragment] private static readonly Regex advanced_link_regex = new Regex( // protocol - @"(?[a-z]*?:\/\/" + + @"(?(https?|osu(mp)?):\/\/" + // domain + tld @"(?(?:[a-z0-9]\.|[a-z0-9][a-z0-9-]*[a-z0-9]\.)*[a-z0-9-]*[a-z0-9]" + // port (optional) @@ -172,7 +173,7 @@ namespace osu.Game.Online.Chat case "u": case "users": - return new LinkDetails(LinkAction.OpenUserProfile, mainArg); + return getUserLink(mainArg); case "wiki": return new LinkDetails(LinkAction.OpenWiki, string.Join('/', args.Skip(3))); @@ -230,8 +231,7 @@ namespace osu.Game.Online.Chat break; case "u": - linkType = LinkAction.OpenUserProfile; - break; + return getUserLink(args[2]); default: return new LinkDetails(LinkAction.External, url); @@ -246,6 +246,14 @@ namespace osu.Game.Online.Chat return new LinkDetails(LinkAction.External, url); } + private static LinkDetails getUserLink(string argument) + { + if (int.TryParse(argument, out int userId)) + return new LinkDetails(LinkAction.OpenUserProfile, new APIUser { Id = userId }); + + return new LinkDetails(LinkAction.OpenUserProfile, new APIUser { Username = argument }); + } + private static MessageFormatterResult format(string toFormat, int startIndex = 0, int space = 3) { var result = new MessageFormatterResult(toFormat); @@ -271,11 +279,8 @@ namespace osu.Game.Online.Chat // handle channels handleMatches(channel_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}chan/{{0}}", result, startIndex, LinkAction.OpenChannel); - string empty = ""; - while (space-- > 0) - empty += "\0"; - - handleMatches(emoji_regex, empty, "{0}", result, startIndex); + // see: https://github.com/ppy/osu/pull/24190 + result.Text = Regex.Replace(result.Text, emoji_regex.ToString(), "[emoji]"); return result; } diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 52bdd36169..56f490cb21 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; @@ -154,7 +155,7 @@ namespace osu.Game.Online.Chat : base(message, channel) { Icon = FontAwesome.Solid.Envelope; - Text = $"You received a private message from '{message.Sender.Username}'. Click to read it!"; + Text = NotificationsStrings.PrivateMessageReceived(message.Sender.Username); } } @@ -164,12 +165,14 @@ namespace osu.Game.Online.Chat : base(message, channel) { Icon = FontAwesome.Solid.At; - Text = $"Your name was mentioned in chat by '{message.Sender.Username}'. Click to find out why!"; + Text = NotificationsStrings.YourNameWasMentioned(message.Sender.Username); } } public abstract partial class HighlightMessageNotification : SimpleNotification { + public override string PopInSampleName => "UI/notification-mention"; + protected HighlightMessageNotification(Message message, Channel channel) { this.message = message; @@ -179,8 +182,6 @@ namespace osu.Game.Online.Chat private readonly Message message; private readonly Channel channel; - public override bool IsImportant => false; - [BackgroundDependencyLoader] private void load(OsuColour colours, ChatOverlay chatOverlay, INotificationOverlay notificationOverlay) { diff --git a/osu.Game/Online/DevelopmentEndpointConfiguration.cs b/osu.Game/Online/DevelopmentEndpointConfiguration.cs index 3171d15fc2..5f3c353f4d 100644 --- a/osu.Game/Online/DevelopmentEndpointConfiguration.cs +++ b/osu.Game/Online/DevelopmentEndpointConfiguration.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Online { public class DevelopmentEndpointConfiguration : EndpointConfiguration @@ -12,9 +10,9 @@ namespace osu.Game.Online WebsiteRootUrl = APIEndpointUrl = @"https://dev.ppy.sh"; APIClientSecret = @"3LP2mhUrV89xxzD1YKNndXHEhWWCRLPNKioZ9ymT"; APIClientID = "5"; - SpectatorEndpointUrl = $"{APIEndpointUrl}/spectator"; - MultiplayerEndpointUrl = $"{APIEndpointUrl}/multiplayer"; - MetadataEndpointUrl = $"{APIEndpointUrl}/metadata"; + SpectatorEndpointUrl = $@"{APIEndpointUrl}/signalr/spectator"; + MultiplayerEndpointUrl = $@"{APIEndpointUrl}/signalr/multiplayer"; + MetadataEndpointUrl = $@"{APIEndpointUrl}/signalr/metadata"; } } } diff --git a/osu.Game/Online/Leaderboards/DrawableRank.cs b/osu.Game/Online/Leaderboards/DrawableRank.cs index 5a65c15444..5177f35478 100644 --- a/osu.Game/Online/Leaderboards/DrawableRank.cs +++ b/osu.Game/Online/Leaderboards/DrawableRank.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 osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index e4ea277756..136c9cc8e7 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -31,6 +31,7 @@ using osuTK; using osuTK.Graphics; using osu.Game.Online.API; using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets.Mods; using osu.Game.Utils; namespace osu.Game.Online.Leaderboards @@ -242,7 +243,7 @@ namespace osu.Game.Online.Leaderboards Origin = Anchor.BottomRight, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - ChildrenEnumerable = Score.Mods.Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) }) + ChildrenEnumerable = Score.Mods.AsOrdered().Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) }) }, }, }, @@ -425,7 +426,7 @@ namespace osu.Game.Online.Leaderboards if (Score.Files.Count > 0) { - items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(Score))); + items.Add(new OsuMenuItem(Localisation.CommonStrings.Export, MenuItemType.Standard, () => scoreManager.Export(Score))); items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score)))); } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs index 0b2e401f57..ed3ee4d45e 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs @@ -118,7 +118,7 @@ namespace osu.Game.Online.Leaderboards topScoreStatistics.Clear(); bottomScoreStatistics.Clear(); - foreach (var mod in score.Mods) + foreach (var mod in score.Mods.AsOrdered()) { modStatistics.Add(new ModCell(mod)); } @@ -210,7 +210,7 @@ namespace osu.Game.Online.Leaderboards Spacing = new Vector2(2f, 0f), Children = new Drawable[] { - new ModIcon(mod, showTooltip: false).With(icon => + new ModIcon(mod, showTooltip: false, showExtendedInformation: false).With(icon => { icon.Origin = Anchor.CentreLeft; icon.Anchor = Anchor.CentreLeft; diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index 995bac1af5..327fb0d76a 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -42,6 +42,14 @@ namespace osu.Game.Online.Multiplayer /// The user. Task UserKicked(MultiplayerRoomUser user); + /// + /// Signals that the local user has been invited into a multiplayer room. + /// + /// Id of user that invited the player. + /// Id of the room the user got invited to. + /// Password to join the room. + Task Invited(int invitedBy, long roomID, string password); + /// /// Signal that the host of the room has changed. /// diff --git a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs index 68bf3cfaec..f266c38b8b 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Threading.Tasks; namespace osu.Game.Online.Multiplayer diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index a2608f1564..b7a608581c 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Threading.Tasks; using osu.Game.Online.API; @@ -101,5 +99,13 @@ namespace osu.Game.Online.Multiplayer /// /// The item to remove. Task RemovePlaylistItem(long playlistItemId); + + /// + /// Invites a player to the current room. + /// + /// The user to invite. + /// The user has blocked or has been blocked by the invited user. + /// The invited user does not accept private messages. + Task InvitePlayer(int userId); } } diff --git a/osu.Game/Online/Multiplayer/InvalidPasswordException.cs b/osu.Game/Online/Multiplayer/InvalidPasswordException.cs index 305c41e69b..d3da8f491b 100644 --- a/osu.Game/Online/Multiplayer/InvalidPasswordException.cs +++ b/osu.Game/Online/Multiplayer/InvalidPasswordException.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; diff --git a/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs b/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs index ab513e71ee..4c793dba68 100644 --- a/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs +++ b/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; diff --git a/osu.Game/Online/Multiplayer/InvalidStateException.cs b/osu.Game/Online/Multiplayer/InvalidStateException.cs index ba3b84ffe4..27b111a781 100644 --- a/osu.Game/Online/Multiplayer/InvalidStateException.cs +++ b/osu.Game/Online/Multiplayer/InvalidStateException.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; diff --git a/osu.Game/Online/Multiplayer/MatchUserRequest.cs b/osu.Game/Online/Multiplayer/MatchUserRequest.cs index 7fc1790434..8515256581 100644 --- a/osu.Game/Online/Multiplayer/MatchUserRequest.cs +++ b/osu.Game/Online/Multiplayer/MatchUserRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 MessagePack; using osu.Game.Online.Multiplayer.Countdown; diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 5716b7ad3b..515a0dda08 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -23,6 +23,7 @@ using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Utils; +using osu.Game.Localisation; namespace osu.Game.Online.Multiplayer { @@ -30,6 +31,8 @@ namespace osu.Game.Online.Multiplayer { public Action? PostNotification { protected get; set; } + public Action? PresentMatch { protected get; set; } + /// /// Invoked when any change occurs to the multiplayer room. /// @@ -260,6 +263,8 @@ namespace osu.Game.Online.Multiplayer protected abstract Task LeaveRoomInternal(); + public abstract Task InvitePlayer(int userId); + /// /// Change the current settings. /// @@ -440,6 +445,38 @@ namespace osu.Game.Online.Multiplayer return handleUserLeft(user, UserKicked); } + async Task IMultiplayerClient.Invited(int invitedBy, long roomID, string password) + { + APIUser? apiUser = await userLookupCache.GetUserAsync(invitedBy).ConfigureAwait(false); + Room? apiRoom = await getRoomAsync(roomID).ConfigureAwait(false); + + if (apiUser == null || apiRoom == null) return; + + PostNotification?.Invoke( + new UserAvatarNotification(apiUser, NotificationsStrings.InvitedYouToTheMultiplayer(apiUser.Username, apiRoom.Name.Value)) + { + Activated = () => + { + PresentMatch?.Invoke(apiRoom, password); + return true; + } + } + ); + + Task getRoomAsync(long id) + { + TaskCompletionSource taskCompletionSource = new TaskCompletionSource(); + + var request = new GetRoomRequest(id); + request.Success += room => taskCompletionSource.TrySetResult(room); + request.Failure += _ => taskCompletionSource.TrySetResult(null); + + API.Queue(request); + + return taskCompletionSource.Task; + } + } + private void addUserToAPIRoom(MultiplayerRoomUser user) { Debug.Assert(APIRoom != null); diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index d70a2797c4..f769b4c805 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -29,7 +29,7 @@ namespace osu.Game.Online.Multiplayer /// The availability state of the current beatmap. /// [Key(2)] - public BeatmapAvailability BeatmapAvailability { get; set; } = BeatmapAvailability.LocallyAvailable(); + public BeatmapAvailability BeatmapAvailability { get; set; } = BeatmapAvailability.Unknown(); /// /// Any mods applicable only to the local user. diff --git a/osu.Game/Online/Multiplayer/NotHostException.cs b/osu.Game/Online/Multiplayer/NotHostException.cs index 9f789f1e81..cd43b13e52 100644 --- a/osu.Game/Online/Multiplayer/NotHostException.cs +++ b/osu.Game/Online/Multiplayer/NotHostException.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; diff --git a/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs b/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs index c749e4615a..0a96406c16 100644 --- a/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs +++ b/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 8ff0ce4065..20ec030eac 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -12,6 +12,8 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.API; using osu.Game.Online.Rooms; +using osu.Game.Overlays.Notifications; +using osu.Game.Localisation; namespace osu.Game.Online.Multiplayer { @@ -50,6 +52,7 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined); connection.On(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft); connection.On(nameof(IMultiplayerClient.UserKicked), ((IMultiplayerClient)this).UserKicked); + connection.On(nameof(IMultiplayerClient.Invited), ((IMultiplayerClient)this).Invited); connection.On(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged); connection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); connection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); @@ -106,6 +109,32 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom)); } + public override async Task InvitePlayer(int userId) + { + if (!IsConnected.Value) + return; + + Debug.Assert(connection != null); + + try + { + await connection.InvokeAsync(nameof(IMultiplayerServer.InvitePlayer), userId).ConfigureAwait(false); + } + catch (HubException exception) + { + switch (exception.GetHubExceptionMessage()) + { + case UserBlockedException.MESSAGE: + PostNotification?.Invoke(new SimpleErrorNotification { Text = OnlinePlayStrings.InviteFailedUserBlocked }); + break; + + case UserBlocksPMsException.MESSAGE: + PostNotification?.Invoke(new SimpleErrorNotification { Text = OnlinePlayStrings.InviteFailedUserOptOut }); + break; + } + } + } + public override Task TransferHost(int userId) { if (!IsConnected.Value) diff --git a/osu.Game/Online/Multiplayer/QueueMode.cs b/osu.Game/Online/Multiplayer/QueueMode.cs index a7bc4ae00a..adc975539f 100644 --- a/osu.Game/Online/Multiplayer/QueueMode.cs +++ b/osu.Game/Online/Multiplayer/QueueMode.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.ComponentModel; namespace osu.Game.Online.Multiplayer diff --git a/osu.Game/Online/Multiplayer/UserBlockedException.cs b/osu.Game/Online/Multiplayer/UserBlockedException.cs new file mode 100644 index 0000000000..e964b13c75 --- /dev/null +++ b/osu.Game/Online/Multiplayer/UserBlockedException.cs @@ -0,0 +1,25 @@ +// 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.Runtime.Serialization; +using Microsoft.AspNetCore.SignalR; + +namespace osu.Game.Online.Multiplayer +{ + [Serializable] + public class UserBlockedException : HubException + { + public const string MESSAGE = @"Cannot perform action due to user being blocked."; + + public UserBlockedException() + : base(MESSAGE) + { + } + + protected UserBlockedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs b/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs new file mode 100644 index 0000000000..14ed6fc212 --- /dev/null +++ b/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs @@ -0,0 +1,25 @@ +// 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.Runtime.Serialization; +using Microsoft.AspNetCore.SignalR; + +namespace osu.Game.Online.Multiplayer +{ + [Serializable] + public class UserBlocksPMsException : HubException + { + public const string MESSAGE = "Cannot perform action because user has disabled non-friend communications."; + + public UserBlocksPMsException() + : base(MESSAGE) + { + } + + protected UserBlocksPMsException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/osu.Game/Online/Placeholders/LoginPlaceholder.cs b/osu.Game/Online/Placeholders/LoginPlaceholder.cs index de7ac6e936..60c9ecbcdc 100644 --- a/osu.Game/Online/Placeholders/LoginPlaceholder.cs +++ b/osu.Game/Online/Placeholders/LoginPlaceholder.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 osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; @@ -12,8 +10,8 @@ namespace osu.Game.Online.Placeholders { public sealed partial class LoginPlaceholder : ClickablePlaceholder { - [Resolved(CanBeNull = true)] - private LoginOverlay login { get; set; } + [Resolved] + private LoginOverlay? login { get; set; } public LoginPlaceholder(LocalisableString actionMessage) : base(actionMessage, FontAwesome.Solid.UserLock) diff --git a/osu.Game/Online/Placeholders/MessagePlaceholder.cs b/osu.Game/Online/Placeholders/MessagePlaceholder.cs index 07a111a10f..b4a9c2086b 100644 --- a/osu.Game/Online/Placeholders/MessagePlaceholder.cs +++ b/osu.Game/Online/Placeholders/MessagePlaceholder.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 osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; @@ -24,6 +22,6 @@ namespace osu.Game.Online.Placeholders AddText(this.message = message); } - public override bool Equals(Placeholder other) => (other as MessagePlaceholder)?.message == message; + public override bool Equals(Placeholder? other) => (other as MessagePlaceholder)?.message == message; } } diff --git a/osu.Game/Online/Rooms/APIScoreToken.cs b/osu.Game/Online/Rooms/APIScoreToken.cs index 58a633f3cf..542f972d3f 100644 --- a/osu.Game/Online/Rooms/APIScoreToken.cs +++ b/osu.Game/Online/Rooms/APIScoreToken.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Newtonsoft.Json; namespace osu.Game.Online.Rooms diff --git a/osu.Game/Online/Rooms/BeatmapAvailability.cs b/osu.Game/Online/Rooms/BeatmapAvailability.cs index f2b981c075..a907ee0d3b 100644 --- a/osu.Game/Online/Rooms/BeatmapAvailability.cs +++ b/osu.Game/Online/Rooms/BeatmapAvailability.cs @@ -34,6 +34,7 @@ namespace osu.Game.Online.Rooms DownloadProgress = downloadProgress; } + public static BeatmapAvailability Unknown() => new BeatmapAvailability(DownloadState.Unknown); public static BeatmapAvailability NotDownloaded() => new BeatmapAvailability(DownloadState.NotDownloaded); public static BeatmapAvailability Downloading(float progress) => new BeatmapAvailability(DownloadState.Downloading, progress); public static BeatmapAvailability Importing() => new BeatmapAvailability(DownloadState.Importing); diff --git a/osu.Game/Online/Rooms/CreateRoomRequest.cs b/osu.Game/Online/Rooms/CreateRoomRequest.cs index b22780490b..63a3b7bfa8 100644 --- a/osu.Game/Online/Rooms/CreateRoomRequest.cs +++ b/osu.Game/Online/Rooms/CreateRoomRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Net.Http; using Newtonsoft.Json; using osu.Framework.IO.Network; diff --git a/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs b/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs index 65731b2b68..c31c6a929a 100644 --- a/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs +++ b/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.API; diff --git a/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs b/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs index 6b5ed2d024..e016074f5d 100644 --- a/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs +++ b/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Online.API; namespace osu.Game.Online.Rooms diff --git a/osu.Game/Online/Rooms/GetRoomRequest.cs b/osu.Game/Online/Rooms/GetRoomRequest.cs index 237d427509..b968f4e864 100644 --- a/osu.Game/Online/Rooms/GetRoomRequest.cs +++ b/osu.Game/Online/Rooms/GetRoomRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Online.API; namespace osu.Game.Online.Rooms diff --git a/osu.Game/Online/Rooms/GetRoomsRequest.cs b/osu.Game/Online/Rooms/GetRoomsRequest.cs index afab83b5be..7feb709acb 100644 --- a/osu.Game/Online/Rooms/GetRoomsRequest.cs +++ b/osu.Game/Online/Rooms/GetRoomsRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.IO.Network; using osu.Game.Extensions; diff --git a/osu.Game/Online/Rooms/IndexScoresParams.cs b/osu.Game/Online/Rooms/IndexScoresParams.cs index 253caa13a1..b69af27e8b 100644 --- a/osu.Game/Online/Rooms/IndexScoresParams.cs +++ b/osu.Game/Online/Rooms/IndexScoresParams.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 JetBrains.Annotations; using Newtonsoft.Json; diff --git a/osu.Game/Online/Rooms/ItemAttemptsCount.cs b/osu.Game/Online/Rooms/ItemAttemptsCount.cs index 71f50b9898..dc86897660 100644 --- a/osu.Game/Online/Rooms/ItemAttemptsCount.cs +++ b/osu.Game/Online/Rooms/ItemAttemptsCount.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Newtonsoft.Json; namespace osu.Game.Online.Rooms diff --git a/osu.Game/Online/Rooms/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs index 0a687312e7..8645f2a2c0 100644 --- a/osu.Game/Online/Rooms/JoinRoomRequest.cs +++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.API; @@ -12,9 +10,9 @@ namespace osu.Game.Online.Rooms public class JoinRoomRequest : APIRequest { public readonly Room Room; - public readonly string Password; + public readonly string? Password; - public JoinRoomRequest(Room room, string password) + public JoinRoomRequest(Room room, string? password) { Room = room; Password = password; diff --git a/osu.Game/Online/Rooms/MatchType.cs b/osu.Game/Online/Rooms/MatchType.cs index fd2f583f83..28f2da897a 100644 --- a/osu.Game/Online/Rooms/MatchType.cs +++ b/osu.Game/Online/Rooms/MatchType.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index d5e0c7a970..f1b9584d57 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -58,6 +58,9 @@ namespace osu.Game.Online.Rooms [JsonProperty("position")] public int? Position { get; set; } + [JsonProperty("has_replay")] + public bool HasReplay { get; set; } + /// /// Any scores in the room around this score. /// @@ -84,7 +87,7 @@ namespace osu.Game.Online.Rooms User = User, Accuracy = Accuracy, Date = EndedAt, - Hash = string.Empty, // todo: temporary? + HasOnlineReplay = HasReplay, Rank = Rank, Mods = Mods?.Select(m => m.ToMod(rulesetInstance)).ToArray() ?? Array.Empty(), Position = Position, diff --git a/osu.Game/Online/Rooms/MultiplayerScores.cs b/osu.Game/Online/Rooms/MultiplayerScores.cs index 4c13579b3e..3331a2155c 100644 --- a/osu.Game/Online/Rooms/MultiplayerScores.cs +++ b/osu.Game/Online/Rooms/MultiplayerScores.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Newtonsoft.Json; using osu.Game.Online.API.Requests; diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 1d496cc636..ceb8e53778 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -60,6 +60,15 @@ namespace osu.Game.Online.Rooms if (item.NewValue == null) return; + // Initially set to unknown until we have attained a good state. + // This has the wanted side effect of forcing a state change when the current playlist + // item changes at the server but our local availability doesn't necessarily change + // (ie. we have both the previous and next item LocallyAvailable). + // + // Note that even without this, the server will trigger a state change and things will work. + // This is just for safety. + availability.Value = BeatmapAvailability.Unknown(); + downloadTracker?.RemoveAndDisposeImmediately(); selectedBeatmap = null; @@ -67,7 +76,7 @@ namespace osu.Game.Online.Rooms { var beatmap = task.GetResultSafely(); - if (SelectedItem.Value?.Beatmap.OnlineID == beatmap.OnlineID) + if (beatmap != null && SelectedItem.Value?.Beatmap.OnlineID == beatmap.OnlineID) { selectedBeatmap = beatmap; beginTracking(); @@ -98,7 +107,7 @@ namespace osu.Game.Online.Rooms // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). realmSubscription?.Dispose(); - realmSubscription = realm.RegisterForNotifications(_ => filteredBeatmaps(), (_, changes, _) => + realmSubscription = realm.RegisterForNotifications(_ => filteredBeatmaps(), (_, changes) => { if (changes == null) return; @@ -115,6 +124,9 @@ namespace osu.Game.Online.Rooms switch (downloadTracker.State.Value) { case DownloadState.Unknown: + availability.Value = BeatmapAvailability.Unknown(); + break; + case DownloadState.NotDownloaded: availability.Value = BeatmapAvailability.NotDownloaded(); break; diff --git a/osu.Game/Online/Rooms/PartRoomRequest.cs b/osu.Game/Online/Rooms/PartRoomRequest.cs index da4e9a44c5..09ba6f65c3 100644 --- a/osu.Game/Online/Rooms/PartRoomRequest.cs +++ b/osu.Game/Online/Rooms/PartRoomRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.API; diff --git a/osu.Game/Online/Rooms/RoomAvailability.cs b/osu.Game/Online/Rooms/RoomAvailability.cs index fada111826..3aea0e5948 100644 --- a/osu.Game/Online/Rooms/RoomAvailability.cs +++ b/osu.Game/Online/Rooms/RoomAvailability.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.ComponentModel; namespace osu.Game.Online.Rooms diff --git a/osu.Game/Online/Rooms/RoomCategory.cs b/osu.Game/Online/Rooms/RoomCategory.cs index ba17fb2121..17afb0dc7f 100644 --- a/osu.Game/Online/Rooms/RoomCategory.cs +++ b/osu.Game/Online/Rooms/RoomCategory.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.ComponentModel; namespace osu.Game.Online.Rooms diff --git a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs index 9aa6424592..0fc27d26b8 100644 --- a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs +++ b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Graphics; using osuTK.Graphics; diff --git a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs index c37b93ea1b..5cc664cf36 100644 --- a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs +++ b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Graphics; using osuTK.Graphics; diff --git a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs index 9eb61a82ec..4d0c93b8ab 100644 --- a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs +++ b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Graphics; using osuTK.Graphics; diff --git a/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs b/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs index affb2846a2..8e6a1ac7c7 100644 --- a/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs +++ b/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Online.API; namespace osu.Game.Online.Rooms diff --git a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs index f4cadc3fde..3f404386c3 100644 --- a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs +++ b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Scoring; namespace osu.Game.Online.Rooms diff --git a/osu.Game/Online/Rooms/SubmitScoreRequest.cs b/osu.Game/Online/Rooms/SubmitScoreRequest.cs index 48a7780a03..b4a91a5892 100644 --- a/osu.Game/Online/Rooms/SubmitScoreRequest.cs +++ b/osu.Game/Online/Rooms/SubmitScoreRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Net.Http; using Newtonsoft.Json; using osu.Framework.IO.Network; diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index 4ddcb40368..dfdac24d19 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -39,7 +39,8 @@ namespace osu.Game.Online var scoreInfo = new ScoreInfo { ID = TrackedItem.ID, - OnlineID = TrackedItem.OnlineID + OnlineID = TrackedItem.OnlineID, + LegacyOnlineID = TrackedItem.LegacyOnlineID }; Downloader.DownloadBegan += downloadBegan; @@ -47,8 +48,9 @@ namespace osu.Game.Online realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) + || (s.LegacyOnlineID > 0 && s.LegacyOnlineID == TrackedItem.LegacyOnlineID) || (!string.IsNullOrEmpty(s.Hash) && s.Hash == TrackedItem.Hash)) - && !s.DeletePending), (items, _, _) => + && !s.DeletePending), (items, _) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/Online/SignalRWorkaroundTypes.cs b/osu.Game/Online/SignalRWorkaroundTypes.cs index 0b545821ee..0e3eb0aab0 100644 --- a/osu.Game/Online/SignalRWorkaroundTypes.cs +++ b/osu.Game/Online/SignalRWorkaroundTypes.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using osu.Game.Online.Multiplayer; diff --git a/osu.Game/Online/Solo/CreateSoloScoreRequest.cs b/osu.Game/Online/Solo/CreateSoloScoreRequest.cs index 8c92b32915..2f462b2610 100644 --- a/osu.Game/Online/Solo/CreateSoloScoreRequest.cs +++ b/osu.Game/Online/Solo/CreateSoloScoreRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Globalization; using System.Net.Http; using osu.Framework.IO.Network; diff --git a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs index f387e61901..3260ba8e7c 100644 --- a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs +++ b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Online.Rooms; using osu.Game.Scoring; diff --git a/osu.Game/Online/Spectator/ISpectatorClient.cs b/osu.Game/Online/Spectator/ISpectatorClient.cs index 605ebc4ef0..9605604966 100644 --- a/osu.Game/Online/Spectator/ISpectatorClient.cs +++ b/osu.Game/Online/Spectator/ISpectatorClient.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Threading.Tasks; namespace osu.Game.Online.Spectator diff --git a/osu.Game/Online/Spectator/ISpectatorServer.cs b/osu.Game/Online/Spectator/ISpectatorServer.cs index fa9d04792a..848983009b 100644 --- a/osu.Game/Online/Spectator/ISpectatorServer.cs +++ b/osu.Game/Online/Spectator/ISpectatorServer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Threading.Tasks; namespace osu.Game.Online.Spectator diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 89da8b9d32..14e137caf1 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -185,7 +185,7 @@ namespace osu.Game.Online.Spectator IsPlaying = true; // transfer state at point of beginning play - currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID; + currentState.BeatmapID = score.ScoreInfo.BeatmapInfo!.OnlineID; currentState.RulesetID = score.ScoreInfo.RulesetID; currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); currentState.State = SpectatedUserState.Playing; diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 3768dad370..2f11964f6a 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -28,6 +29,7 @@ using osu.Framework.Input.Events; using osu.Framework.Input.Handlers.Tablet; using osu.Framework.Localisation; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; @@ -43,8 +45,8 @@ using osu.Game.Input.Bindings; using osu.Game.IO; using osu.Game.Localisation; using osu.Game.Online; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; +using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.Music; @@ -57,6 +59,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Menu; +using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; @@ -81,7 +84,7 @@ namespace osu.Game /// protected const float SIDE_OVERLAY_OFFSET_RATIO = 0.05f; - public Toolbar Toolbar; + public Toolbar Toolbar { get; private set; } private ChatOverlay chatOverlay; @@ -281,6 +284,52 @@ namespace osu.Game protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + private readonly List dragDropFiles = new List(); + private ScheduledDelegate dragDropImportSchedule; + + public override void SetHost(GameHost host) + { + base.SetHost(host); + + if (host.Window != null) + { + host.Window.DragDrop += path => + { + // on macOS/iOS, URL associations are handled via SDL_DROPFILE events. + if (path.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) + { + HandleLink(path); + return; + } + + lock (dragDropFiles) + { + dragDropFiles.Add(path); + + Logger.Log($@"Adding ""{Path.GetFileName(path)}"" for import"); + + // File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms. + // In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch. + dragDropImportSchedule?.Cancel(); + dragDropImportSchedule = Scheduler.AddDelayed(handlePendingDragDropImports, 100); + } + }; + } + } + + private void handlePendingDragDropImports() + { + lock (dragDropFiles) + { + Logger.Log($"Handling batch import of {dragDropFiles.Count} files"); + + string[] paths = dragDropFiles.ToArray(); + dragDropFiles.Clear(); + + Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning); + } + } + [BackgroundDependencyLoader] private void load() { @@ -388,7 +437,7 @@ namespace osu.Game case LinkAction.Spectate: waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification { - Text = @"This link type is not yet supported!", + Text = NotificationsStrings.LinkTypeNotSupported, Icon = FontAwesome.Solid.LifeRing, })); break; @@ -398,15 +447,7 @@ namespace osu.Game break; case LinkAction.OpenUserProfile: - if (!(link.Argument is IUser user)) - { - user = int.TryParse(argString, out int userId) - ? new APIUser { Id = userId } - : new APIUser { Username = argString }; - } - - ShowUser(user); - + ShowUser((IUser)link.Argument); break; case LinkAction.OpenWiki: @@ -438,7 +479,7 @@ namespace osu.Game { Notifications.Post(new SimpleErrorNotification { - Text = $"The URL {url} has an unsupported or dangerous protocol and will not be opened.", + Text = NotificationsStrings.UnsupportedOrDangerousUrlProtocol(url), }); return; @@ -604,6 +645,24 @@ namespace osu.Game }); } + /// + /// Join a multiplayer match immediately. + /// + /// The room to join. + /// The password to join the room, if any is given. + public void PresentMultiplayerMatch(Room room, string password) + { + PerformFromScreen(screen => + { + if (!(screen is Multiplayer multiplayer)) + screen.Push(multiplayer = new Multiplayer()); + + multiplayer.Join(room, password); + }); + // TODO: We should really be able to use `validScreens: new[] { typeof(Multiplayer) }` here + // but `PerformFromScreen` doesn't understand nested stacks. + } + /// /// Present a score's replay immediately. /// The user should have already requested this interactively. @@ -619,6 +678,9 @@ namespace osu.Game if (score.OnlineID > 0) databasedScoreInfo = ScoreManager.Query(s => s.OnlineID == score.OnlineID); + if (score.LegacyOnlineID > 0) + databasedScoreInfo ??= ScoreManager.Query(s => s.LegacyOnlineID == score.LegacyOnlineID); + if (score is ScoreInfo scoreInfo) databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == scoreInfo.Hash); @@ -730,8 +792,8 @@ namespace osu.Game public override void AttemptExit() { - // Using PerformFromScreen gives the user a chance to interrupt the exit process if needed. - PerformFromScreen(menu => menu.Exit()); + // The main menu exit implementation gives the user a chance to interrupt the exit process if needed. + PerformFromScreen(menu => menu.Exit(), new[] { typeof(MainMenu) }); } /// @@ -814,6 +876,7 @@ namespace osu.Game ScoreManager.PresentImport = items => PresentScore(items.First().Value); MultiplayerClient.PostNotification = n => Notifications.Post(n); + MultiplayerClient.PresentMatch = PresentMultiplayerMatch; // make config aware of how to lookup skins for on-screen display purposes. // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. @@ -911,9 +974,9 @@ namespace osu.Game if (!args?.Any(a => a == @"--no-version-overlay") ?? true) loadComponentSingleFile(versionManager = new VersionManager { Depth = int.MinValue }, ScreenContainer.Add); - loadComponentSingleFile(osuLogo, logo => + loadComponentSingleFile(osuLogo, _ => { - logoContainer.Add(logo); + osuLogo.SetupDefaultContainer(logoContainer); // Loader has to be created after the logo has finished loading as Loader performs logo transformations on entering. ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both)); @@ -986,7 +1049,7 @@ namespace osu.Game loadComponentSingleFile(CreateHighPerformanceSession(), Add); - loadComponentSingleFile(new BackgroundBeatmapProcessor(), Add); + loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add); Add(difficultyRecommender); Add(externalLinkOpener = new ExternalLinkOpener()); @@ -1108,7 +1171,7 @@ namespace osu.Game Schedule(() => Notifications.Post(new SimpleNotification { Icon = FontAwesome.Solid.EllipsisH, - Text = "Subsequent messages have been logged. Click to view log files.", + Text = NotificationsStrings.SubsequentMessagesLogged, Activated = () => { Storage.GetStorageForDirectory(@"logs").PresentFileExternally(logFile); @@ -1125,7 +1188,9 @@ namespace osu.Game private void forwardTabletLogsToNotifications() { const string tablet_prefix = @"[Tablet] "; + bool notifyOnWarning = true; + bool notifyOnError = true; Logger.NewEntry += entry => { @@ -1136,11 +1201,16 @@ namespace osu.Game if (entry.Level == LogLevel.Error) { + if (!notifyOnError) + return; + + notifyOnError = false; + Schedule(() => { Notifications.Post(new SimpleNotification { - Text = $"Disabling tablet support due to error: \"{message}\"", + Text = NotificationsStrings.TabletSupportDisabledDueToError(message), Icon = FontAwesome.Solid.PenSquare, IconColour = Colours.RedDark, }); @@ -1157,7 +1227,7 @@ namespace osu.Game { Schedule(() => Notifications.Post(new SimpleNotification { - Text = @"Encountered tablet warning, your tablet may not function correctly. Click here for a list of all tablets supported.", + Text = NotificationsStrings.EncounteredTabletWarning, Icon = FontAwesome.Solid.PenSquare, IconColour = Colours.YellowDark, Activated = () => @@ -1174,7 +1244,11 @@ namespace osu.Game Schedule(() => { ITabletHandler tablet = Host.AvailableInputHandlers.OfType().SingleOrDefault(); - tablet?.Tablet.BindValueChanged(_ => notifyOnWarning = true, true); + tablet?.Tablet.BindValueChanged(_ => + { + notifyOnWarning = true; + notifyOnError = true; + }, true); }); } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 63efe0e2c8..1f46eb0c0d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -215,7 +215,7 @@ namespace osu.Game /// For now, this is used as a source specifically for beat synced components. /// Going forward, it could potentially be used as the single source-of-truth for beatmap timing. /// - private readonly FramedBeatmapClock beatmapClock = new FramedBeatmapClock(true); + private readonly FramedBeatmapClock beatmapClock = new FramedBeatmapClock(applyOffsets: true, requireDecoupling: false); protected override Container Content => content; @@ -392,17 +392,18 @@ namespace osu.Game { SafeAreaOverrideEdges = SafeAreaOverrideEdges, RelativeSizeAxes = Axes.Both, - Child = CreateScalingContainer().WithChildren(new Drawable[] + Child = CreateScalingContainer().WithChild(globalBindings = new GlobalActionContainer(this) { - (GlobalCursorDisplay = new GlobalCursorDisplay + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both - }).WithChild(content = new OsuTooltipContainer(GlobalCursorDisplay.MenuCursor) - { - RelativeSizeAxes = Axes.Both - }), - // to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything. - globalBindings = new GlobalActionContainer(this) + (GlobalCursorDisplay = new GlobalCursorDisplay + { + RelativeSizeAxes = Axes.Both + }).WithChild(content = new OsuTooltipContainer(GlobalCursorDisplay.MenuCursor) + { + RelativeSizeAxes = Axes.Both + }), + } }) }); @@ -440,16 +441,7 @@ namespace osu.Game } } - private void onTrackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction) - { - // FramedBeatmapClock uses a decoupled clock internally which will mutate the source if it is an `IAdjustableClock`. - // We don't want this for now, as the intention of beatmapClock is to be a read-only source for beat sync components. - // - // Encapsulating in a FramedClock will avoid any mutations. - var framedClock = new FramedClock(beatmap.Track); - - beatmapClock.ChangeSource(framedClock); - } + private void onTrackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction) => beatmapClock.ChangeSource(beatmap.Track); protected virtual void InitialiseFonts() { @@ -515,6 +507,12 @@ namespace osu.Game Scheduler.AddDelayed(AttemptExit, 2000); } + /// + /// If supported by the platform, the game will automatically restart after the next exit. + /// + /// Whether a restart operation was queued. + public virtual bool RestartAppWhenExited() => false; + public bool Migrate(string path) { Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""..."); diff --git a/osu.Game/OsuGameBase_Importing.cs b/osu.Game/OsuGameBase_Importing.cs index cf65460bab..601d17bea9 100644 --- a/osu.Game/OsuGameBase_Importing.cs +++ b/osu.Game/OsuGameBase_Importing.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.IO; using System.Linq; diff --git a/osu.Game/Overlays/AccountCreation/AccountCreationBackground.cs b/osu.Game/Overlays/AccountCreation/AccountCreationBackground.cs index 0042b9f8f6..8d015709aa 100644 --- a/osu.Game/Overlays/AccountCreation/AccountCreationBackground.cs +++ b/osu.Game/Overlays/AccountCreation/AccountCreationBackground.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Overlays/AccountCreation/AccountCreationScreen.cs b/osu.Game/Overlays/AccountCreation/AccountCreationScreen.cs index a7dd53f511..32e5ca471c 100644 --- a/osu.Game/Overlays/AccountCreation/AccountCreationScreen.cs +++ b/osu.Game/Overlays/AccountCreation/AccountCreationScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Screens; diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index 219cbe7eef..9ad507d82a 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -17,6 +17,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Overlays.Settings; using osu.Game.Resources.Localisation.Web; @@ -71,7 +72,7 @@ namespace osu.Game.Overlays.AccountCreation Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Font = OsuFont.GetFont(size: 20), - Text = "Let's create an account!", + Text = AccountCreationStrings.LetsCreateAnAccount }, usernameTextBox = new OsuTextBox { @@ -86,7 +87,7 @@ namespace osu.Game.Overlays.AccountCreation }, emailTextBox = new OsuTextBox { - PlaceholderText = "email address", + PlaceholderText = ModelValidationStrings.UserAttributesUserEmail.ToLower(), RelativeSizeAxes = Axes.X, TabbableContentContainer = this }, @@ -118,7 +119,7 @@ namespace osu.Game.Overlays.AccountCreation AutoSizeAxes = Axes.Y, Child = new SettingsButton { - Text = "Register", + Text = LoginPanelStrings.Register, Margin = new MarginPadding { Vertical = 20 }, Action = performRegistration } @@ -132,10 +133,10 @@ namespace osu.Game.Overlays.AccountCreation textboxes = new[] { usernameTextBox, emailTextBox, passwordTextBox }; - usernameDescription.AddText("This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!"); + usernameDescription.AddText(AccountCreationStrings.UsernameDescription); - emailAddressDescription.AddText("Will be used for notifications, account verification and in the case you forget your password. No spam, ever."); - emailAddressDescription.AddText(" Make sure to get it right!", cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold)); + emailAddressDescription.AddText(AccountCreationStrings.EmailDescription1); + emailAddressDescription.AddText(AccountCreationStrings.EmailDescription2, cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold)); passwordDescription.AddText("At least "); characterCheckText = passwordDescription.AddText("8 characters long"); diff --git a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs b/osu.Game/Overlays/AccountCreation/ScreenWarning.cs index a833a871f9..0fbf6ba59e 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenWarning.cs @@ -17,6 +17,7 @@ using osu.Game.Overlays.Settings; using osu.Game.Screens.Menu; using osuTK; using osuTK.Graphics; +using osu.Game.Localisation; namespace osu.Game.Overlays.AccountCreation { @@ -101,13 +102,13 @@ namespace osu.Game.Overlays.AccountCreation }, new SettingsButton { - Text = "Help, I can't access my account!", + Text = AccountCreationStrings.MultiAccountWarningHelp, Margin = new MarginPadding { Top = 50 }, Action = () => game?.OpenUrlExternally(help_centre_url) }, new DangerousSettingsButton { - Text = "I understand. This account isn't for me.", + Text = AccountCreationStrings.MultiAccountWarningAccept, Action = () => this.Push(new ScreenEntry()) }, furtherAssistance = new LinkFlowContainer(cp => cp.Font = cp.Font.With(size: 12)) diff --git a/osu.Game/Overlays/AccountCreation/ScreenWelcome.cs b/osu.Game/Overlays/AccountCreation/ScreenWelcome.cs index 4becb225f8..610b9ee282 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenWelcome.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenWelcome.cs @@ -1,9 +1,8 @@ // 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 osu.Framework.Allocation; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; @@ -12,6 +11,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Overlays.Settings; using osu.Game.Screens.Menu; using osuTK; +using osu.Game.Localisation; namespace osu.Game.Overlays.AccountCreation { @@ -46,18 +46,18 @@ namespace osu.Game.Overlays.AccountCreation Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light), - Text = "New Player Registration", + Text = AccountCreationStrings.NewPlayerRegistration.ToTitle(), }, new OsuSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Font = OsuFont.GetFont(size: 12), - Text = "let's get you started", + Text = AccountCreationStrings.LetsGetYouStarted.ToLower(), }, new SettingsButton { - Text = "Let's create an account!", + Text = AccountCreationStrings.LetsCreateAnAccount, Margin = new MarginPadding { Vertical = 120 }, Action = () => this.Push(new ScreenWarning()) } diff --git a/osu.Game/Overlays/AccountCreationOverlay.cs b/osu.Game/Overlays/AccountCreationOverlay.cs index 6f79316670..ef2e055eae 100644 --- a/osu.Game/Overlays/AccountCreationOverlay.cs +++ b/osu.Game/Overlays/AccountCreationOverlay.cs @@ -90,7 +90,6 @@ namespace osu.Game.Overlays protected override void PopIn() { - base.PopIn(); this.FadeIn(transition_time, Easing.OutQuint); if (welcomeScreen.GetChildScreen() != null) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs index feb0c27ee7..9cd0031e3d 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs @@ -22,8 +22,12 @@ namespace osu.Game.Overlays.BeatmapListing public BeatmapListingCardSizeTabControl() { AutoSizeAxes = Axes.Both; + + Items = new[] { BeatmapCardSize.Normal, BeatmapCardSize.Extra }; } + protected override bool AddEnumEntriesAutomatically => false; + protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer { AutoSizeAxes = Axes.Both, diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs index 2f290d05e9..e3e2bcaf9a 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.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 osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs index 3ab0e47a6c..6d75521cb0 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs @@ -1,9 +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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -66,7 +63,6 @@ namespace osu.Game.Overlays.BeatmapListing Current = filterWithValue.Current; } - [NotNull] protected virtual Drawable CreateFilter() => new BeatmapSearchFilter(); protected partial class BeatmapSearchFilter : TabControl diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs index 96626d0ac6..2efa4b455e 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs index 031833a107..195ac09c3e 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.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.Collections.Generic; using System.Linq; using osu.Framework.Extensions; diff --git a/osu.Game/Overlays/BeatmapListing/SearchCategory.cs b/osu.Game/Overlays/BeatmapListing/SearchCategory.cs index b3e12d00a6..2c77177541 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchCategory.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchCategory.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/BeatmapListing/SearchExplicit.cs b/osu.Game/Overlays/BeatmapListing/SearchExplicit.cs index 10fea6d5b2..4df5cd4b0a 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchExplicit.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchExplicit.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/BeatmapListing/SearchExtra.cs b/osu.Game/Overlays/BeatmapListing/SearchExtra.cs index d307cd09eb..e54632acd8 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchExtra.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchExtra.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.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs b/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs index ebcbef1ad9..28d8473a6e 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/BeatmapListing/SearchGenre.cs b/osu.Game/Overlays/BeatmapListing/SearchGenre.cs index 7746eb50d7..f928f8a7bb 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchGenre.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchGenre.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs index d52f923158..af8a298919 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs b/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs index 0c379b3825..3b04ac01ca 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchPlayed.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 osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/BeatmapListing/SortCriteria.cs b/osu.Game/Overlays/BeatmapListing/SortCriteria.cs index 6c010c7504..5e3cbbfeea 100644 --- a/osu.Game/Overlays/BeatmapListing/SortCriteria.cs +++ b/osu.Game/Overlays/BeatmapListing/SortCriteria.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs index 4a9a3d8089..0b1befe7b9 100644 --- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs +++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs @@ -58,23 +58,25 @@ namespace osu.Game.Overlays.BeatmapSet private void updateDisplay() { - bpm.Value = BeatmapSet?.BPM.ToLocalisableString(@"0.##") ?? (LocalisableString)"-"; - if (beatmapInfo == null) { + bpm.Value = "-"; + length.Value = string.Empty; circleCount.Value = string.Empty; sliderCount.Value = string.Empty; } else { - length.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(TimeSpan.FromMilliseconds(beatmapInfo.Length).ToFormattedDuration()); + bpm.Value = beatmapInfo.BPM.ToLocalisableString(@"0.##"); + length.Value = TimeSpan.FromMilliseconds(beatmapInfo.Length).ToFormattedDuration(); - var onlineInfo = beatmapInfo as IBeatmapOnlineInfo; + if (beatmapInfo is not IBeatmapOnlineInfo onlineInfo) return; - circleCount.Value = (onlineInfo?.CircleCount ?? 0).ToLocalisableString(@"N0"); - sliderCount.Value = (onlineInfo?.SliderCount ?? 0).ToLocalisableString(@"N0"); + circleCount.Value = onlineInfo.CircleCount.ToLocalisableString(@"N0"); + sliderCount.Value = onlineInfo.SliderCount.ToLocalisableString(@"N0"); + length.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(TimeSpan.FromMilliseconds(onlineInfo.HitLength).ToFormattedDuration()); } } diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs index 104f861df7..1f38e2ed6c 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs @@ -185,7 +185,7 @@ namespace osu.Game.Overlays.BeatmapSet OnHovered = beatmap => { showBeatmap(beatmap); - starRating.Text = beatmap.StarRating.ToLocalisableString(@"0.##"); + starRating.Text = beatmap.StarRating.ToLocalisableString(@"0.00"); starRatingContainer.FadeIn(100); }, OnClicked = beatmap => { Beatmap.Value = beatmap; }, diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs b/osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs index 9291988367..426fbcdb8d 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.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 osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; using osu.Game.Rulesets; @@ -13,9 +11,9 @@ namespace osu.Game.Overlays.BeatmapSet { public partial class BeatmapRulesetSelector : OverlayRulesetSelector { - private readonly Bindable beatmapSet = new Bindable(); + private readonly Bindable beatmapSet = new Bindable(); - public APIBeatmapSet BeatmapSet + public APIBeatmapSet? BeatmapSet { get => beatmapSet.Value; set diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetLayoutSection.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetLayoutSection.cs index 305a3661a7..4dd257c6ea 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetLayoutSection.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetLayoutSection.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 osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs index c43be33290..5f9cdf5065 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs @@ -3,6 +3,7 @@ #nullable disable +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -24,6 +25,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons private readonly BindableBool playing = new BindableBool(); + [CanBeNull] public PreviewTrack Preview { get; private set; } private APIBeatmapSet beatmapSet; diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs index b3b8b80a0d..2254514a44 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -22,7 +20,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons private readonly Box background, progress; private readonly PlayButton playButton; - private PreviewTrack preview => playButton.Preview; + private PreviewTrack? preview => playButton.Preview; public IBindable Playing => playButton.Playing; diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs index 8758b9c5cf..d21b2546b9 100644 --- a/osu.Game/Overlays/BeatmapSet/Info.cs +++ b/osu.Game/Overlays/BeatmapSet/Info.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 osu.Framework.Allocation; using osu.Framework.Bindables; @@ -29,7 +27,7 @@ namespace osu.Game.Overlays.BeatmapSet public readonly Bindable BeatmapSet = new Bindable(); - public APIBeatmap BeatmapInfo + public APIBeatmap? BeatmapInfo { get => successRate.Beatmap; set => successRate.Beatmap = value; diff --git a/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs b/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs index 476a252c7b..5cfe4a35b3 100644 --- a/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs +++ b/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.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 osu.Game.Screens.Select.Leaderboards; using osu.Game.Graphics.UserInterface; using osu.Framework.Allocation; diff --git a/osu.Game/Overlays/BeatmapSet/MetadataType.cs b/osu.Game/Overlays/BeatmapSet/MetadataType.cs index dc96ce99e9..c92cecc17e 100644 --- a/osu.Game/Overlays/BeatmapSet/MetadataType.cs +++ b/osu.Game/Overlays/BeatmapSet/MetadataType.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/NoScoresPlaceholder.cs b/osu.Game/Overlays/BeatmapSet/Scores/NoScoresPlaceholder.cs index f7703af27d..29a696593d 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/NoScoresPlaceholder.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/NoScoresPlaceholder.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Screens.Select.Leaderboards; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/NotSupporterPlaceholder.cs b/osu.Game/Overlays/BeatmapSet/Scores/NotSupporterPlaceholder.cs index 04ab3ec72f..7cb119bf32 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/NotSupporterPlaceholder.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/NotSupporterPlaceholder.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Sprites; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 425f40258e..1fc997fdad 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -24,6 +24,7 @@ using osu.Framework.Localisation; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics.Cursor; using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring.Drawables; namespace osu.Game.Overlays.BeatmapSet.Scores @@ -163,7 +164,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }, username, #pragma warning disable 618 - new StatisticText(score.MaxCombo, score.BeatmapInfo.MaxCombo, @"0\x"), + new StatisticText(score.MaxCombo, score.BeatmapInfo!.MaxCombo, @"0\x"), #pragma warning restore 618 }; @@ -195,7 +196,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores Direction = FillDirection.Horizontal, AutoSizeAxes = Axes.Both, Spacing = new Vector2(1), - ChildrenEnumerable = score.Mods.Select(m => new ModIcon(m) + ChildrenEnumerable = score.Mods.AsOrdered().Select(m => new ModIcon(m) { AutoSizeAxes = Axes.Both, Scale = new Vector2(0.3f) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTableRowBackground.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTableRowBackground.cs index 130dfd45e7..8a6545a97b 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTableRowBackground.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTableRowBackground.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs index 04cbf171f6..59ba9cd449 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Extensions; using osu.Game.Graphics; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index 6d89313979..b53b7826f3 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -47,9 +47,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores [Resolved] private RulesetStore rulesets { get; set; } - [Resolved] - private ScoreManager scoreManager { get; set; } - private GetScoresRequest getScoresRequest; private CancellationTokenSource loadCancellationSource; @@ -85,7 +82,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores MD5Hash = apiBeatmap.MD5Hash }; - var scores = scoreManager.OrderByTotalScore(value.Scores.Select(s => s.ToScoreInfo(rulesets, beatmapInfo))).ToArray(); + var scores = value.Scores.Select(s => s.ToScoreInfo(rulesets, beatmapInfo)).OrderByTotalScore().ToArray(); var topScore = scores.First(); scoreTable.DisplayScores(scores, apiBeatmap.Status.GrantsPerformancePoints()); diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index e030b1e34f..72e590b009 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -123,7 +123,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores accuracyColumn.Text = value.DisplayAccuracy; maxComboColumn.Text = value.MaxCombo.ToLocalisableString(@"0\x"); - ppColumn.Alpha = value.BeatmapInfo.Status.GrantsPerformancePoints() ? 1 : 0; + ppColumn.Alpha = value.BeatmapInfo!.Status.GrantsPerformancePoints() ? 1 : 0; if (value.PP is double pp) ppColumn.Text = pp.ToLocalisableString(@"N0"); @@ -275,7 +275,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores set { modsContainer.Clear(); - modsContainer.Children = value.Select(mod => new ModIcon(mod) + modsContainer.Children = value.AsOrdered().Select(mod => new ModIcon(mod) { AutoSizeAxes = Axes.Both, Scale = new Vector2(0.25f), diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs index afaed85250..9dc2ce204f 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; diff --git a/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs b/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs index e730496b5c..67b11af0c1 100644 --- a/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs +++ b/osu.Game/Overlays/BreadcrumbControlOverlayHeader.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Overlays/Changelog/ChangelogEntry.cs b/osu.Game/Overlays/Changelog/ChangelogEntry.cs index ab671d9c86..9c40440778 100644 --- a/osu.Game/Overlays/Changelog/ChangelogEntry.cs +++ b/osu.Game/Overlays/Changelog/ChangelogEntry.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Net; @@ -14,6 +12,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osuTK; using osuTK.Graphics; @@ -26,10 +25,13 @@ namespace osu.Game.Overlays.Changelog private readonly APIChangelogEntry entry; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; [Resolved] - private OverlayColourProvider colourProvider { get; set; } + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private ILinkHandler? linkHandler { get; set; } private FontUsage fontLarge; private FontUsage fontMedium; @@ -88,11 +90,21 @@ namespace osu.Game.Overlays.Changelog } }; - title.AddText(entry.Title, t => + if (string.IsNullOrEmpty(entry.Url)) { - t.Font = fontLarge; - t.Colour = entryColour; - }); + title.AddText(entry.Title, t => + { + t.Font = fontLarge; + t.Colour = entryColour; + }); + } + else + { + title.AddLink(entry.Title, () => linkHandler?.HandleLink(entry.Url), entry.Url, t => + { + t.Font = fontLarge; + }); + } if (!string.IsNullOrEmpty(entry.Repository) && !string.IsNullOrEmpty(entry.GithubUrl)) addRepositoryReference(title, entryColour); diff --git a/osu.Game/Overlays/Changelog/ChangelogListing.cs b/osu.Game/Overlays/Changelog/ChangelogListing.cs index 4b784c7a28..5f1ae5b6fa 100644 --- a/osu.Game/Overlays/Changelog/ChangelogListing.cs +++ b/osu.Game/Overlays/Changelog/ChangelogListing.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using osu.Framework.Allocation; @@ -18,9 +16,9 @@ namespace osu.Game.Overlays.Changelog { public partial class ChangelogListing : ChangelogContent { - private readonly List entries; + private readonly List? entries; - public ChangelogListing(List entries) + public ChangelogListing(List? entries) { this.entries = entries; } diff --git a/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs b/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs index 4aded1dd59..012ccf8f64 100644 --- a/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs +++ b/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs index 155cbc7d65..e3dd989e2d 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.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 osu.Game.Online.API.Requests.Responses; namespace osu.Game.Overlays.Changelog diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs index 57b6f6268c..21b6147113 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Chat.ChannelList new Drawable?[] { createIcon(), - text = new OsuSpriteText + text = new TruncatingSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -94,7 +94,6 @@ namespace osu.Game.Overlays.Chat.ChannelList Colour = colourProvider.Light3, Margin = new MarginPadding { Bottom = 2 }, RelativeSizeAxes = Axes.X, - Truncate = true, }, createMentionPill(), close = createCloseButton(), diff --git a/osu.Game/Overlays/Chat/ChannelScrollContainer.cs b/osu.Game/Overlays/Chat/ChannelScrollContainer.cs index 090f7835ae..6d8b21a7c5 100644 --- a/osu.Game/Overlays/Chat/ChannelScrollContainer.cs +++ b/osu.Game/Overlays/Chat/ChannelScrollContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Game.Graphics.Containers; diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index c85206d5f7..bbc3ee5bf4 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -18,7 +18,10 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; +using osuTK.Graphics; +using Message = osu.Game.Online.Chat.Message; namespace osu.Game.Overlays.Chat { @@ -66,12 +69,31 @@ namespace osu.Game.Overlays.Chat private Container? highlight; + /// + /// The colour used to paint the author's username. + /// + /// + /// The colour can be set explicitly by consumers via the property initialiser. + /// If unspecified, the colour is by default initialised to: + /// + /// message.Sender.Colour, if non-empty, + /// a random colour from if the above is empty. + /// + /// + public Color4 UsernameColour { get; init; } + public ChatLine(Message message) { Message = message; RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + + // initialise using sane defaults. + // consumers can use the initialiser of `UsernameColour` to override this if they wish to. + UsernameColour = !string.IsNullOrEmpty(message.Sender.Colour) + ? Color4Extensions.FromHex(message.Sender.Colour) + : default_username_colours[message.SenderId % default_username_colours.Length]; } [BackgroundDependencyLoader] @@ -111,6 +133,8 @@ namespace osu.Game.Overlays.Chat Origin = Anchor.TopRight, Anchor = Anchor.TopRight, Margin = new MarginPadding { Horizontal = Spacing }, + AccentColour = UsernameColour, + Inverted = !string.IsNullOrEmpty(message.Sender.Colour), }, drawableContentFlow = new LinkFlowContainer(styleMessageContent) { @@ -195,5 +219,44 @@ namespace osu.Game.Overlays.Chat { drawableTimestamp.Text = message.Timestamp.LocalDateTime.ToLocalisableString(prefer24HourTime.Value ? @"HH:mm:ss" : @"hh:mm:ss tt"); } + + private static readonly Color4[] default_username_colours = + { + Color4Extensions.FromHex("588c7e"), + Color4Extensions.FromHex("b2a367"), + Color4Extensions.FromHex("c98f65"), + Color4Extensions.FromHex("bc5151"), + Color4Extensions.FromHex("5c8bd6"), + Color4Extensions.FromHex("7f6ab7"), + Color4Extensions.FromHex("a368ad"), + Color4Extensions.FromHex("aa6880"), + + Color4Extensions.FromHex("6fad9b"), + Color4Extensions.FromHex("f2e394"), + Color4Extensions.FromHex("f2ae72"), + Color4Extensions.FromHex("f98f8a"), + Color4Extensions.FromHex("7daef4"), + Color4Extensions.FromHex("a691f2"), + Color4Extensions.FromHex("c894d3"), + Color4Extensions.FromHex("d895b0"), + + Color4Extensions.FromHex("53c4a1"), + Color4Extensions.FromHex("eace5c"), + Color4Extensions.FromHex("ea8c47"), + Color4Extensions.FromHex("fc4f4f"), + Color4Extensions.FromHex("3d94ea"), + Color4Extensions.FromHex("7760ea"), + Color4Extensions.FromHex("af52c6"), + Color4Extensions.FromHex("e25696"), + + Color4Extensions.FromHex("677c66"), + Color4Extensions.FromHex("9b8732"), + Color4Extensions.FromHex("8c5129"), + Color4Extensions.FromHex("8c3030"), + Color4Extensions.FromHex("1f5d91"), + Color4Extensions.FromHex("4335a5"), + Color4Extensions.FromHex("812a96"), + Color4Extensions.FromHex("992861"), + }; } } diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index fd5e0e9836..16a8d14b10 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -73,14 +73,13 @@ namespace osu.Game.Overlays.Chat Width = chatting_text_width, Masking = true, Padding = new MarginPadding { Horizontal = padding }, - Child = chattingText = new OsuSpriteText + Child = chattingText = new TruncatingSpriteText { MaxWidth = chatting_text_width - padding * 2, Font = OsuFont.Torus.With(size: 20), Colour = colourProvider.Background1, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Truncate = true, }, }, searchIconContainer = new Container @@ -157,7 +156,11 @@ namespace osu.Game.Overlays.Chat chatTextBox.Current.UnbindFrom(change.OldValue.TextBoxMessage); if (newChannel != null) + { + // change length limit first before binding to avoid accidentally truncating pending message from new channel. + chatTextBox.LengthLimit = newChannel.MessageLengthLimit; chatTextBox.Current.BindTo(newChannel.TextBoxMessage); + } }, true); } diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index 4b4afc204c..67191f6836 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -33,7 +33,16 @@ namespace osu.Game.Overlays.Chat { public Action? ReportRequested; - public Color4 AccentColour { get; } + /// + /// The primary colour to use for the username. + /// + public Color4 AccentColour { get; init; } + + /// + /// If set to , the username will be drawn as plain text in . + /// If set to , the username will be drawn as black text inside a rounded rectangle in . + /// + public bool Inverted { get; init; } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => colouredDrawable.ReceivePositionalInputAt(screenSpacePos); @@ -75,7 +84,7 @@ namespace osu.Game.Overlays.Chat private readonly APIUser user; private readonly OsuSpriteText drawableText; - private readonly Drawable colouredDrawable; + private Drawable colouredDrawable = null!; public DrawableChatUsername(APIUser user) { @@ -83,25 +92,23 @@ namespace osu.Game.Overlays.Chat Action = openUserProfile; - drawableText = new OsuSpriteText + drawableText = new TruncatingSpriteText { Shadow = false, - Truncate = true, - EllipsisString = "…", Anchor = Anchor.TopRight, Origin = Anchor.TopRight, }; + } - if (string.IsNullOrWhiteSpace(user.Colour)) + [BackgroundDependencyLoader] + private void load() + { + if (!Inverted) { - AccentColour = default_colours[user.Id % default_colours.Length]; - Add(colouredDrawable = drawableText); } else { - AccentColour = Color4Extensions.FromHex(user.Colour); - Add(new Container { Anchor = Anchor.TopRight, @@ -143,7 +150,6 @@ namespace osu.Game.Overlays.Chat protected override void LoadComplete() { base.LoadComplete(); - drawableText.Colour = colours.ChatBlue; colouredDrawable.Colour = AccentColour; } @@ -202,44 +208,5 @@ namespace osu.Game.Overlays.Chat colouredDrawable.FadeColour(AccentColour, 800, Easing.OutQuint); } - - private static readonly Color4[] default_colours = - { - Color4Extensions.FromHex("588c7e"), - Color4Extensions.FromHex("b2a367"), - Color4Extensions.FromHex("c98f65"), - Color4Extensions.FromHex("bc5151"), - Color4Extensions.FromHex("5c8bd6"), - Color4Extensions.FromHex("7f6ab7"), - Color4Extensions.FromHex("a368ad"), - Color4Extensions.FromHex("aa6880"), - - Color4Extensions.FromHex("6fad9b"), - Color4Extensions.FromHex("f2e394"), - Color4Extensions.FromHex("f2ae72"), - Color4Extensions.FromHex("f98f8a"), - Color4Extensions.FromHex("7daef4"), - Color4Extensions.FromHex("a691f2"), - Color4Extensions.FromHex("c894d3"), - Color4Extensions.FromHex("d895b0"), - - Color4Extensions.FromHex("53c4a1"), - Color4Extensions.FromHex("eace5c"), - Color4Extensions.FromHex("ea8c47"), - Color4Extensions.FromHex("fc4f4f"), - Color4Extensions.FromHex("3d94ea"), - Color4Extensions.FromHex("7760ea"), - Color4Extensions.FromHex("af52c6"), - Color4Extensions.FromHex("e25696"), - - Color4Extensions.FromHex("677c66"), - Color4Extensions.FromHex("9b8732"), - Color4Extensions.FromHex("8c5129"), - Color4Extensions.FromHex("8c3030"), - Color4Extensions.FromHex("1f5d91"), - Color4Extensions.FromHex("4335a5"), - Color4Extensions.FromHex("812a96"), - Color4Extensions.FromHex("992861"), - }; } } diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 96dbfe31f3..724f77ad71 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -55,6 +55,9 @@ namespace osu.Game.Overlays private const float side_bar_width = 190; private const float chat_bar_height = 60; + protected override string PopInSampleName => @"UI/overlay-big-pop-in"; + protected override string PopOutSampleName => @"UI/overlay-big-pop-out"; + [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -264,7 +267,7 @@ namespace osu.Game.Overlays if (!isDraggingTopBar) return; - float targetChatHeight = dragStartChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y; + float targetChatHeight = dragStartChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent!.DrawSize.Y; chatHeight.Value = targetChatHeight; } @@ -276,8 +279,6 @@ namespace osu.Game.Overlays protected override void PopIn() { - base.PopIn(); - this.MoveToY(0, transition_length, Easing.OutQuint); this.FadeIn(transition_length, Easing.OutQuint); } diff --git a/osu.Game/Overlays/Comments/Buttons/ChevronButton.cs b/osu.Game/Overlays/Comments/Buttons/ChevronButton.cs index 88e7d00476..45024f25db 100644 --- a/osu.Game/Overlays/Comments/Buttons/ChevronButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/ChevronButton.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 osu.Framework.Graphics; using osu.Game.Graphics.Containers; using osu.Framework.Bindables; diff --git a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs index d9576f5b72..400820ddd9 100644 --- a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -26,7 +24,7 @@ namespace osu.Game.Overlays.Comments.Buttons } [Resolved] - private OverlayColourProvider colourProvider { get; set; } + private OverlayColourProvider colourProvider { get; set; } = null!; private readonly ChevronIcon icon; private readonly Box background; diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs index 2af7dd3093..02bcbb9d05 100644 --- a/osu.Game/Overlays/Comments/CommentEditor.cs +++ b/osu.Game/Overlays/Comments/CommentEditor.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online.API; using osuTK; using osuTK.Graphics; @@ -24,19 +25,37 @@ namespace osu.Game.Overlays.Comments protected abstract LocalisableString FooterText { get; } - protected abstract LocalisableString CommitButtonText { get; } - - protected abstract LocalisableString TextBoxPlaceholder { get; } - protected FillFlowContainer ButtonsContainer { get; private set; } = null!; protected readonly Bindable Current = new Bindable(string.Empty); private RoundedButton commitButton = null!; + private RoundedButton logInButton = null!; private LoadingSpinner loadingSpinner = null!; protected TextBox TextBox { get; private set; } = null!; + [Resolved] + protected IAPIProvider API { get; private set; } = null!; + + [Resolved] + private LoginOverlay? loginOverlay { get; set; } + + private readonly IBindable apiState = new Bindable(); + + /// + /// Returns the text content of the main action button. + /// When is , the text will apply to a button that posts a comment. + /// When is , the text will apply to a button that directs the user to the login overlay. + /// + protected abstract LocalisableString GetButtonText(bool isLoggedIn); + + /// + /// Returns the placeholder text for the comment box. + /// + /// Whether the current user is logged in. + protected abstract LocalisableString GetPlaceholderText(bool isLoggedIn); + protected bool ShowLoadingSpinner { set @@ -78,7 +97,6 @@ namespace osu.Game.Overlays.Comments { Height = 40, RelativeSizeAxes = Axes.X, - PlaceholderText = TextBoxPlaceholder, Current = Current }, new Container @@ -113,10 +131,19 @@ namespace osu.Game.Overlays.Comments AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(5, 0), - Child = commitButton = new EditorButton + Children = new Drawable[] { - Text = CommitButtonText, - Action = () => OnCommit(Current.Value) + commitButton = new EditorButton + { + Action = () => OnCommit(Current.Value), + Text = GetButtonText(true) + }, + logInButton = new EditorButton + { + Width = 100, + Action = () => loginOverlay?.Show(), + Text = GetButtonText(false) + } } }, loadingSpinner = new LoadingSpinner @@ -134,12 +161,14 @@ namespace osu.Game.Overlays.Comments }); TextBox.OnCommit += (_, _) => commitButton.TriggerClick(); + apiState.BindTo(API.State); } protected override void LoadComplete() { base.LoadComplete(); Current.BindValueChanged(_ => updateCommitButtonState(), true); + apiState.BindValueChanged(updateStateForLoggedIn, true); } protected abstract void OnCommit(string text); @@ -147,6 +176,25 @@ namespace osu.Game.Overlays.Comments private void updateCommitButtonState() => commitButton.Enabled.Value = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value); + private void updateStateForLoggedIn(ValueChangedEvent state) => Schedule(() => + { + bool isAvailable = state.NewValue > APIState.Offline; + + TextBox.PlaceholderText = GetPlaceholderText(isAvailable); + TextBox.ReadOnly = !isAvailable; + + if (isAvailable) + { + commitButton.Show(); + logInButton.Hide(); + } + else + { + commitButton.Hide(); + logInButton.Show(); + } + }); + private partial class EditorTextBox : OsuTextBox { protected override float LeftRightPadding => side_padding; diff --git a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs index 9cc20caa05..e48a52c787 100644 --- a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs +++ b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs @@ -1,11 +1,15 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Markdig.Syntax; +using Markdig.Syntax.Inlines; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers.Markdown; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Game.Graphics.Containers.Markdown; +using osuTK; namespace osu.Game.Overlays.Comments { @@ -18,6 +22,8 @@ namespace osu.Game.Overlays.Comments protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new CommentMarkdownHeading(headingBlock); + public override MarkdownTextFlowContainer CreateTextFlow() => new CommentMarkdownTextFlowContainer(); + private partial class CommentMarkdownHeading : OsuMarkdownHeading { public CommentMarkdownHeading(HeadingBlock headingBlock) @@ -42,5 +48,64 @@ namespace osu.Game.Overlays.Comments } } } + + private partial class CommentMarkdownTextFlowContainer : MarkdownTextFlowContainer + { + protected override void AddImage(LinkInline linkInline) => AddDrawable(new CommentMarkdownImage(linkInline.Url)); + + private partial class CommentMarkdownImage : MarkdownImage + { + public CommentMarkdownImage(string url) + : base(url) + { + } + + private DelayedLoadWrapper wrapper = null!; + + protected override Drawable CreateContent(string url) => wrapper = new DelayedLoadWrapper(CreateImageContainer(url)); + + protected override ImageContainer CreateImageContainer(string url) + { + var container = new CommentImageContainer(url); + container.OnLoadComplete += d => + { + // The size of DelayedLoadWrapper depends on AutoSizeAxes of it's content. + // But since it's set to None, we need to specify the size here manually. + wrapper.Size = container.Size; + d.FadeInFromZero(300, Easing.OutQuint); + }; + return container; + } + + private partial class CommentImageContainer : ImageContainer + { + // https://github.com/ppy/osu-web/blob/3bd0f406dc78d60b356d955cd4201f8c3e1cca09/resources/css/bem/osu-md.less#L36 + // Web version defines max height in em units (6 em), which assuming default font size as 14 results in 84 px, + // which also seems to match observations upon inspecting the web element. + private const float max_height = 84f; + + public CommentImageContainer(string url) + : base(url) + { + AutoSizeAxes = Axes.None; + } + + protected override Sprite CreateImageSprite() => new Sprite + { + RelativeSizeAxes = Axes.Both + }; + + protected override Texture GetImageTexture(TextureStore textures, string url) + { + Texture t = base.GetImageTexture(textures, url); + + if (t != null) + Size = t.Height > max_height ? new Vector2(max_height / t.Height * t.Width, max_height) : t.Size; + + return t!; + } + } + } + } } } diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs index 24536fe460..af5f4dd280 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -405,17 +405,16 @@ namespace osu.Game.Overlays.Comments [Resolved] private CommentsContainer commentsContainer { get; set; } - [Resolved] - private IAPIProvider api { get; set; } - public Action OnPost; //TODO should match web, left empty due to no multiline support protected override LocalisableString FooterText => default; - protected override LocalisableString CommitButtonText => CommonStrings.ButtonsPost; + protected override LocalisableString GetButtonText(bool isLoggedIn) => + isLoggedIn ? CommonStrings.ButtonsPost : CommentsStrings.GuestButtonNew; - protected override LocalisableString TextBoxPlaceholder => CommentsStrings.PlaceholderNew; + protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => + isLoggedIn ? CommentsStrings.PlaceholderNew : AuthorizationStrings.RequireLogin; protected override void OnCommit(string text) { @@ -432,7 +431,7 @@ namespace osu.Game.Overlays.Comments Current.Value = string.Empty; OnPost?.Invoke(cb); }); - api.Queue(req); + API.Queue(req); } } } diff --git a/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs b/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs index 1770fcb269..50bd08b66b 100644 --- a/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs +++ b/osu.Game/Overlays/Comments/CommentsShowMoreButton.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 osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs b/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs index 6adb388185..fa366f38c3 100644 --- a/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs +++ b/osu.Game/Overlays/Comments/DeletedCommentsCounter.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 osu.Framework.Graphics.Containers; using osu.Framework.Graphics; using osu.Game.Graphics; diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index a710406548..ba1c7ca8b2 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Comments private IAPIProvider api { get; set; } = null!; [Resolved] - private GameHost host { get; set; } = null!; + private Clipboard clipboard { get; set; } = null!; [Resolved] private OnScreenDisplay? onScreenDisplay { get; set; } @@ -444,7 +444,7 @@ namespace osu.Game.Overlays.Comments private void copyUrl() { - host.GetClipboard()?.SetText($@"{api.APIEndpointUrl}/comments/{Comment.Id}"); + clipboard.SetText($@"{api.APIEndpointUrl}/comments/{Comment.Id}"); onScreenDisplay?.Display(new CopyUrlToast()); } diff --git a/osu.Game/Overlays/Comments/HeaderButton.cs b/osu.Game/Overlays/Comments/HeaderButton.cs index de99cd6cc8..1a26148e49 100644 --- a/osu.Game/Overlays/Comments/HeaderButton.cs +++ b/osu.Game/Overlays/Comments/HeaderButton.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 osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs index 8aca183dee..dd4c35ef20 100644 --- a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs +++ b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs @@ -6,7 +6,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Localisation; using osu.Framework.Logging; -using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; @@ -18,16 +17,17 @@ namespace osu.Game.Overlays.Comments [Resolved] private CommentsContainer commentsContainer { get; set; } = null!; - [Resolved] - private IAPIProvider api { get; set; } = null!; - private readonly Comment parentComment; public Action? OnPost; protected override LocalisableString FooterText => default; - protected override LocalisableString CommitButtonText => CommonStrings.ButtonsReply; - protected override LocalisableString TextBoxPlaceholder => CommentsStrings.PlaceholderReply; + + protected override LocalisableString GetButtonText(bool isLoggedIn) => + isLoggedIn ? CommonStrings.ButtonsReply : CommentsStrings.GuestButtonReply; + + protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => + isLoggedIn ? CommentsStrings.PlaceholderReply : AuthorizationStrings.RequireLogin; public ReplyCommentEditor(Comment parent) { @@ -38,7 +38,8 @@ namespace osu.Game.Overlays.Comments { base.LoadComplete(); - GetContainingInputManager().ChangeFocus(TextBox); + if (!TextBox.ReadOnly) + GetContainingInputManager().ChangeFocus(TextBox); } protected override void OnCommit(string text) @@ -51,7 +52,7 @@ namespace osu.Game.Overlays.Comments Logger.Error(e, "Posting reply comment failed."); }); req.Success += cb => Schedule(processPostedComments, cb); - api.Queue(req); + API.Queue(req); } private void processPostedComments(CommentBundle cb) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 5047992c8b..02f0a6e80d 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -119,7 +119,7 @@ namespace osu.Game.Overlays.Dashboard { users.GetUserAsync(userId).ContinueWith(task => { - var user = task.GetResultSafely(); + APIUser user = task.GetResultSafely(); if (user == null) return; @@ -130,6 +130,9 @@ namespace osu.Game.Overlays.Dashboard if (!playingUsers.Contains(user.Id)) return; + // TODO: remove this once online state is being updated more correctly. + user.IsOnline = true; + userFlow.Add(createUserPanel(user)); }); }); diff --git a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs index 5cbeb8f306..0f4697e33c 100644 --- a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs +++ b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.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.ComponentModel; using osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendStream.cs b/osu.Game/Overlays/Dashboard/Friends/FriendStream.cs index 3bb42ec953..4abece9a8d 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendStream.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendStream.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 - namespace osu.Game.Overlays.Dashboard.Friends { public class FriendStream diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs index 785eef38ad..2aea631b7c 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.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 osu.Framework.Extensions; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs b/osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs index 21bc5b8203..dc291f1a44 100644 --- a/osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs +++ b/osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/Dashboard/Friends/UserListToolbar.cs b/osu.Game/Overlays/Dashboard/Friends/UserListToolbar.cs index db8510325c..3f31ceee1a 100644 --- a/osu.Game/Overlays/Dashboard/Friends/UserListToolbar.cs +++ b/osu.Game/Overlays/Dashboard/Friends/UserListToolbar.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osuTK; diff --git a/osu.Game/Overlays/Dashboard/Friends/UserSortTabControl.cs b/osu.Game/Overlays/Dashboard/Friends/UserSortTabControl.cs index 886ed08af2..db01e1f266 100644 --- a/osu.Game/Overlays/Dashboard/Friends/UserSortTabControl.cs +++ b/osu.Game/Overlays/Dashboard/Friends/UserSortTabControl.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.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapListing.cs b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapListing.cs index 0282ba8785..4eac1a1d29 100644 --- a/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapListing.cs +++ b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapListing.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.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs index 792d6cc785..f36e6b49bb 100644 --- a/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs @@ -100,17 +100,15 @@ namespace osu.Game.Overlays.Dashboard.Home Direction = FillDirection.Vertical, Children = new Drawable[] { - new OsuSpriteText + new TruncatingSpriteText { RelativeSizeAxes = Axes.X, - Truncate = true, Font = OsuFont.GetFont(weight: FontWeight.Regular), Text = BeatmapSet.Title }, - new OsuSpriteText + new TruncatingSpriteText { RelativeSizeAxes = Axes.X, - Truncate = true, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular), Text = BeatmapSet.Artist }, diff --git a/osu.Game/Overlays/Dashboard/Home/DashboardNewBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Home/DashboardNewBeatmapPanel.cs index fef33bdf5a..9e9c22fea2 100644 --- a/osu.Game/Overlays/Dashboard/Home/DashboardNewBeatmapPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/DashboardNewBeatmapPanel.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 osu.Framework.Graphics; using osu.Game.Graphics; diff --git a/osu.Game/Overlays/Dashboard/Home/DashboardPopularBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Home/DashboardPopularBeatmapPanel.cs index 54d95c994b..a08a1fef6f 100644 --- a/osu.Game/Overlays/Dashboard/Home/DashboardPopularBeatmapPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/DashboardPopularBeatmapPanel.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Overlays/Dashboard/Home/DrawableBeatmapList.cs b/osu.Game/Overlays/Dashboard/Home/DrawableBeatmapList.cs index af36f71dd2..a0e22a0faf 100644 --- a/osu.Game/Overlays/Dashboard/Home/DrawableBeatmapList.cs +++ b/osu.Game/Overlays/Dashboard/Home/DrawableBeatmapList.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.Collections.Generic; using System.Linq; using osu.Framework.Allocation; diff --git a/osu.Game/Overlays/Dashboard/Home/DrawableNewBeatmapList.cs b/osu.Game/Overlays/Dashboard/Home/DrawableNewBeatmapList.cs index 8a60d8568c..a22ce8acb0 100644 --- a/osu.Game/Overlays/Dashboard/Home/DrawableNewBeatmapList.cs +++ b/osu.Game/Overlays/Dashboard/Home/DrawableNewBeatmapList.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.Collections.Generic; using osu.Framework.Localisation; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Overlays/Dashboard/Home/DrawablePopularBeatmapList.cs b/osu.Game/Overlays/Dashboard/Home/DrawablePopularBeatmapList.cs index aab99d0ed3..b321057ef2 100644 --- a/osu.Game/Overlays/Dashboard/Home/DrawablePopularBeatmapList.cs +++ b/osu.Game/Overlays/Dashboard/Home/DrawablePopularBeatmapList.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.Collections.Generic; using osu.Framework.Localisation; using osu.Game.Online.API.Requests.Responses; diff --git a/osu.Game/Overlays/Dashboard/Home/HomePanel.cs b/osu.Game/Overlays/Dashboard/Home/HomePanel.cs index 8023c093aa..93db9978a7 100644 --- a/osu.Game/Overlays/Dashboard/Home/HomePanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/HomePanel.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 osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs index dabe65964a..86babf82b5 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.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 osu.Framework.Allocation; using osu.Framework.Extensions.LocalisationExtensions; diff --git a/osu.Game/Overlays/Dashboard/Home/News/NewsGroupItem.cs b/osu.Game/Overlays/Dashboard/Home/News/NewsGroupItem.cs index 9b27d1a193..9319d0dabd 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/NewsGroupItem.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/NewsGroupItem.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 osu.Framework.Allocation; using osu.Framework.Extensions.LocalisationExtensions; diff --git a/osu.Game/Overlays/Dashboard/Home/News/NewsItemGroupPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/NewsItemGroupPanel.cs index fa59f38690..f0d51be52a 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/NewsItemGroupPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/NewsItemGroupPanel.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.Collections.Generic; using System.Linq; using osu.Framework.Allocation; diff --git a/osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.cs b/osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.cs index 1960e0372e..d6f8499c85 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 527ac1689b..2f96421531 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.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 osu.Game.Overlays.Dashboard; using osu.Game.Overlays.Dashboard.Friends; diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index f5a7e9e43d..36a9baac67 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -1,10 +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.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -30,6 +31,9 @@ namespace osu.Game.Overlays.Dialog private readonly Vector2 ringMinifiedSize = new Vector2(20f); private readonly Vector2 buttonsEnterSpacing = new Vector2(0f, 50f); + private readonly Box flashLayer; + private Sample flashSample = null!; + private readonly Container content; private readonly Container ring; private readonly FillFlowContainer buttonsContainer; @@ -210,6 +214,13 @@ namespace osu.Game.Overlays.Dialog AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, }, + flashLayer = new Box + { + Alpha = 0, + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Colour = Color4Extensions.FromHex(@"221a21"), + }, }, }, }; @@ -219,6 +230,12 @@ namespace osu.Game.Overlays.Dialog Show(); } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + flashSample = audio.Samples.Get(@"UI/default-select-disabled"); + } + /// /// Programmatically clicks the first . /// @@ -227,7 +244,20 @@ namespace osu.Game.Overlays.Dialog /// /// Programmatically clicks the first button of the provided type. /// - public void PerformAction() where T : PopupDialogButton => Buttons.OfType().First().TriggerClick(); + public void PerformAction() where T : PopupDialogButton + { + // Buttons are regularly added in BDL or LoadComplete, so let's schedule to ensure + // they are ready to be pressed. + Scheduler.AddOnce(() => Buttons.OfType().FirstOrDefault()?.TriggerClick()); + } + + public void Flash() + { + flashLayer.FadeInFromZero(80, Easing.OutQuint) + .Then() + .FadeOutFromOne(1500, Easing.OutQuint); + flashSample.Play(); + } protected override bool OnKeyDown(KeyDownEvent e) { diff --git a/osu.Game/Overlays/Dialog/PopupDialogButton.cs b/osu.Game/Overlays/Dialog/PopupDialogButton.cs index 91a19add21..499dab3a1d 100644 --- a/osu.Game/Overlays/Dialog/PopupDialogButton.cs +++ b/osu.Game/Overlays/Dialog/PopupDialogButton.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 osu.Framework.Extensions.Color4Extensions; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Overlays/Dialog/PopupDialogCancelButton.cs b/osu.Game/Overlays/Dialog/PopupDialogCancelButton.cs index f4289c66f1..c55226b147 100644 --- a/osu.Game/Overlays/Dialog/PopupDialogCancelButton.cs +++ b/osu.Game/Overlays/Dialog/PopupDialogCancelButton.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 osu.Framework.Allocation; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Overlays/Dialog/PopupDialogOkButton.cs b/osu.Game/Overlays/Dialog/PopupDialogOkButton.cs index eb4a0f0709..968657755f 100644 --- a/osu.Game/Overlays/Dialog/PopupDialogOkButton.cs +++ b/osu.Game/Overlays/Dialog/PopupDialogOkButton.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 osu.Framework.Allocation; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index 098a5d0a33..005162bbcc 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -99,7 +99,6 @@ namespace osu.Game.Overlays protected override void PopIn() { - base.PopIn(); lowPassFilter.CutoffTo(300, 100, Easing.OutCubic); } diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs index 75bc8fd3a8..385695f669 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs @@ -123,7 +123,7 @@ namespace osu.Game.Overlays.FirstRunSetup beatmapSubscription?.Dispose(); } - private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes, Exception error) => Schedule(() => + private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) => Schedule(() => { currentlyLoadedBeatmaps.Text = FirstRunSetupBeatmapScreenStrings.CurrentlyLoadedBeatmaps(sender.Count); diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs index 95af8ec0f3..31a56c9748 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs @@ -65,7 +65,7 @@ namespace osu.Game.Overlays.FirstRunSetup { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - BackgroundColour = colours.Pink3, + BackgroundColour = colours.DangerousButtonColour, Text = FirstRunSetupOverlayStrings.ClassicDefaults, RelativeSizeAxes = Axes.X, Action = applyClassic diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs index e3cd2ae36c..02f0ad9506 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs @@ -104,7 +104,7 @@ namespace osu.Game.Overlays.FirstRunSetup { protected override bool ControlGlobalMusic => false; - public override bool? AllowTrackAdjustments => false; + public override bool? ApplyModTrackAdjustments => false; } private partial class UIScaleSlider : RoundedSliderBar diff --git a/osu.Game/Overlays/FullscreenOverlay.cs b/osu.Game/Overlays/FullscreenOverlay.cs index 032821f215..6ee045c492 100644 --- a/osu.Game/Overlays/FullscreenOverlay.cs +++ b/osu.Game/Overlays/FullscreenOverlay.cs @@ -1,9 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -20,7 +17,7 @@ namespace osu.Game.Overlays public abstract partial class FullscreenOverlay : WaveOverlayContainer, INamedOverlayComponent where T : OverlayHeader { - public virtual string IconTexture => Header.Title.IconTexture ?? string.Empty; + public virtual string IconTexture => Header.Title.IconTexture; public virtual LocalisableString Title => Header.Title.Title; public virtual LocalisableString Description => Header.Title.Description; @@ -29,7 +26,7 @@ namespace osu.Game.Overlays protected virtual Color4 BackgroundColour => ColourProvider.Background5; [Resolved] - protected IAPIProvider API { get; private set; } + protected IAPIProvider API { get; private set; } = null!; [Cached] protected readonly OverlayColourProvider ColourProvider; @@ -83,7 +80,6 @@ namespace osu.Game.Overlays Waves.FourthWaveColour = ColourProvider.Dark3; } - [NotNull] protected abstract T CreateHeader(); public override void Show() diff --git a/osu.Game/Overlays/INamedOverlayComponent.cs b/osu.Game/Overlays/INamedOverlayComponent.cs index e9d01a55e3..65664b12e7 100644 --- a/osu.Game/Overlays/INamedOverlayComponent.cs +++ b/osu.Game/Overlays/INamedOverlayComponent.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Localisation; namespace osu.Game.Overlays diff --git a/osu.Game/Overlays/INotificationOverlay.cs b/osu.Game/Overlays/INotificationOverlay.cs index b9ac466229..19c646a714 100644 --- a/osu.Game/Overlays/INotificationOverlay.cs +++ b/osu.Game/Overlays/INotificationOverlay.cs @@ -1,8 +1,8 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Overlays.Notifications; @@ -30,5 +30,20 @@ namespace osu.Game.Overlays /// Current number of unread notifications. /// IBindable UnreadCount { get; } + + /// + /// Whether there are any ongoing operations, such as imports or downloads. + /// + public bool HasOngoingOperations => OngoingOperations.Any(); + + /// + /// All current displayed notifications, whether in the toast tray or a section. + /// + IEnumerable AllNotifications { get; } + + /// + /// All ongoing operations (ie. any not in a completed or cancelled state). + /// + public IEnumerable OngoingOperations => AllNotifications.OfType().Where(p => p.Ongoing); } } diff --git a/osu.Game/Overlays/IOverlayManager.cs b/osu.Game/Overlays/IOverlayManager.cs index 0318b2b3a0..d771308e34 100644 --- a/osu.Game/Overlays/IOverlayManager.cs +++ b/osu.Game/Overlays/IOverlayManager.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index af145c418c..0eef55162f 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -11,11 +11,13 @@ using osu.Framework.Input.Events; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Overlays.Settings; using osu.Game.Resources.Localisation.Web; using osuTK; +using osu.Game.Localisation; namespace osu.Game.Overlays.Login { @@ -41,47 +43,64 @@ namespace osu.Game.Overlays.Login [BackgroundDependencyLoader(permitNulls: true)] private void load(OsuConfigManager config, AccountCreationOverlay accountCreation) { - Direction = FillDirection.Vertical; - Spacing = new Vector2(0, 5); - AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; + Spacing = new Vector2(0, SettingsSection.ITEM_SPACING); ErrorTextFlowContainer errorText; - LinkFlowContainer forgottenPaswordLink; + LinkFlowContainer forgottenPasswordLink; Children = new Drawable[] { - username = new OsuTextBox - { - PlaceholderText = UsersStrings.LoginUsername.ToLower(), - RelativeSizeAxes = Axes.X, - Text = api.ProvidedUsername, - TabbableContentContainer = this - }, - password = new OsuPasswordTextBox - { - PlaceholderText = UsersStrings.LoginPassword.ToLower(), - RelativeSizeAxes = Axes.X, - TabbableContentContainer = this, - }, - errorText = new ErrorTextFlowContainer + new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, SettingsSection.ITEM_SPACING), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = LoginPanelStrings.Account.ToUpper(), + Font = OsuFont.GetFont(weight: FontWeight.Bold), + }, + username = new OsuTextBox + { + PlaceholderText = UsersStrings.LoginUsername.ToLower(), + RelativeSizeAxes = Axes.X, + Text = api.ProvidedUsername, + TabbableContentContainer = this + }, + password = new OsuPasswordTextBox + { + PlaceholderText = UsersStrings.LoginPassword.ToLower(), + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + }, + errorText = new ErrorTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + }, + }, }, new SettingsCheckbox { - LabelText = "Remember username", + LabelText = LoginPanelStrings.RememberUsername, Current = config.GetBindable(OsuSetting.SaveUsername), }, new SettingsCheckbox { - LabelText = "Stay signed in", + LabelText = LoginPanelStrings.StaySignedIn, Current = config.GetBindable(OsuSetting.SavePassword), }, - forgottenPaswordLink = new LinkFlowContainer + forgottenPasswordLink = new LinkFlowContainer { - Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS }, + Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, }, @@ -105,7 +124,7 @@ namespace osu.Game.Overlays.Login }, new SettingsButton { - Text = "Register", + Text = LoginPanelStrings.Register, Action = () => { RequestHide?.Invoke(); @@ -114,12 +133,15 @@ namespace osu.Game.Overlays.Login } }; - forgottenPaswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.WebsiteRootUrl}/home/password-reset"); + forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.WebsiteRootUrl}/home/password-reset"); password.OnCommit += (_, _) => performLogin(); if (api.LastLoginError?.Message is string error) + { + errorText.Alpha = 1; errorText.AddErrors(new[] { error }); + } } public override bool AcceptsFocus => true; diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 44f2f3273a..71ecf2e75a 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -1,13 +1,13 @@ // 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 osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -15,33 +15,33 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Online.API; +using osu.Game.Overlays.Settings; using osu.Game.Users; using osuTK; -using RectangleF = osu.Framework.Graphics.Primitives.RectangleF; -using Container = osu.Framework.Graphics.Containers.Container; namespace osu.Game.Overlays.Login { - public partial class LoginPanel : FillFlowContainer + public partial class LoginPanel : Container { private bool bounding = true; - private LoginForm form; + + private LoginForm? form; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; - private UserGridPanel panel; - private UserDropdown dropdown; + private UserGridPanel panel = null!; + private UserDropdown dropdown = null!; /// /// Called to request a hide of a parent displaying this container. /// - public Action RequestHide; + public Action? RequestHide; private readonly IBindable apiState = new Bindable(); [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; public override RectangleF BoundingBox => bounding ? base.BoundingBox : RectangleF.Empty; @@ -59,8 +59,6 @@ namespace osu.Game.Overlays.Login { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Direction = FillDirection.Vertical; - Spacing = new Vector2(0f, 5f); } [BackgroundDependencyLoader] @@ -77,18 +75,9 @@ namespace osu.Game.Overlays.Login switch (state.NewValue) { case APIState.Offline: - Children = new Drawable[] + Child = form = new LoginForm { - new OsuSpriteText - { - Text = "ACCOUNT", - Margin = new MarginPadding { Bottom = 5 }, - Font = OsuFont.GetFont(weight: FontWeight.Bold), - }, - form = new LoginForm - { - RequestHide = RequestHide - } + RequestHide = RequestHide }; break; @@ -96,63 +85,58 @@ namespace osu.Game.Overlays.Login case APIState.Connecting: LinkFlowContainer linkFlow; - Children = new Drawable[] + Child = new FillFlowContainer { - new LoadingSpinner + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, SettingsSection.ITEM_SPACING), + Children = new Drawable[] { - State = { Value = Visibility.Visible }, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }, - linkFlow = new LinkFlowContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - TextAnchor = Anchor.TopCentre, - AutoSizeAxes = Axes.Both, - Text = state.NewValue == APIState.Failing ? ToolbarStrings.AttemptingToReconnect : ToolbarStrings.Connecting, - Margin = new MarginPadding { Top = 10, Bottom = 10 }, + new LoadingSpinner + { + State = { Value = Visibility.Visible }, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + linkFlow = new LinkFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + TextAnchor = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Text = state.NewValue == APIState.Failing ? ToolbarStrings.AttemptingToReconnect : ToolbarStrings.Connecting, + }, }, }; - linkFlow.AddLink("cancel", api.Logout, string.Empty); + linkFlow.AddLink(Resources.Localisation.Web.CommonStrings.ButtonsCancel.ToLower(), api.Logout, string.Empty); break; case APIState.Online: - Children = new Drawable[] + Child = new FillFlowContainer { - new FillFlowContainer + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, SettingsSection.ITEM_SPACING), + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = 20, Right = 20 }, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0f, 10f), - Children = new Drawable[] + new OsuSpriteText { - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new[] - { - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "Signed in", - Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold), - Margin = new MarginPadding { Top = 5, Bottom = 5 }, - }, - }, - }, - panel = new UserGridPanel(api.LocalUser.Value) - { - RelativeSizeAxes = Axes.X, - Action = RequestHide - }, - dropdown = new UserDropdown { RelativeSizeAxes = Axes.X }, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = LoginPanelStrings.SignedIn, + Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold), }, + panel = new UserGridPanel(api.LocalUser.Value) + { + RelativeSizeAxes = Axes.X, + Action = RequestHide + }, + dropdown = new UserDropdown { RelativeSizeAxes = Axes.X }, }, }; diff --git a/osu.Game/Overlays/Login/UserAction.cs b/osu.Game/Overlays/Login/UserAction.cs index 7a18e38109..813968a053 100644 --- a/osu.Game/Overlays/Login/UserAction.cs +++ b/osu.Game/Overlays/Login/UserAction.cs @@ -1,11 +1,9 @@ // 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.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; +using osu.Game.Localisation; namespace osu.Game.Overlays.Login { @@ -14,13 +12,13 @@ namespace osu.Game.Overlays.Login [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.StatusOnline))] Online, - [Description(@"Do not disturb")] + [LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.DoNotDisturb))] DoNotDisturb, - [Description(@"Appear offline")] + [LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.AppearOffline))] AppearOffline, - [Description(@"Sign out")] + [LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.SignOut))] SignOut, } } diff --git a/osu.Game/Overlays/Login/UserDropdown.cs b/osu.Game/Overlays/Login/UserDropdown.cs index 0bdfa82517..f2a12f9a1e 100644 --- a/osu.Game/Overlays/Login/UserDropdown.cs +++ b/osu.Game/Overlays/Login/UserDropdown.cs @@ -1,14 +1,8 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Users.Drawables; using osuTK; @@ -33,29 +27,6 @@ namespace osu.Game.Overlays.Login protected partial class UserDropdownMenu : OsuDropdownMenu { - public UserDropdownMenu() - { - Masking = true; - CornerRadius = 5; - - Margin = new MarginPadding { Bottom = 5 }; - - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(0.25f), - Radius = 4, - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - BackgroundColour = colours.Gray3; - SelectionColour = colours.Gray4; - HoverColour = colours.Gray5; - } - protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableUserDropdownMenuItem(item); private partial class DrawableUserDropdownMenuItem : DrawableOsuDropdownMenuItem @@ -64,20 +35,12 @@ namespace osu.Game.Overlays.Login : base(item) { Foreground.Padding = new MarginPadding { Top = 5, Bottom = 5, Left = 10, Right = 5 }; - CornerRadius = 5; } - - protected override Drawable CreateContent() => new Content - { - Label = { Margin = new MarginPadding { Left = UserDropdownHeader.LABEL_LEFT_MARGIN - 11 } } - }; } } private partial class UserDropdownHeader : OsuDropdownHeader { - public const float LABEL_LEFT_MARGIN = 20; - private readonly StatusIcon statusIcon; public Color4 StatusColour @@ -87,20 +50,6 @@ namespace osu.Game.Overlays.Login public UserDropdownHeader() { - Foreground.Padding = new MarginPadding { Left = 10, Right = 10 }; - Margin = new MarginPadding { Bottom = 5 }; - Masking = true; - CornerRadius = 5; - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(0.25f), - Radius = 4, - }; - - Icon.Size = new Vector2(14); - Icon.Margin = new MarginPadding(0); - Foreground.Add(statusIcon = new StatusIcon { Anchor = Anchor.CentreLeft, @@ -108,14 +57,7 @@ namespace osu.Game.Overlays.Login Size = new Vector2(14), }); - Text.Margin = new MarginPadding { Left = LABEL_LEFT_MARGIN }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - BackgroundColour = colours.Gray3; - BackgroundColourHover = colours.Gray5; + Text.Margin = new MarginPadding { Left = 20 }; } } } diff --git a/osu.Game/Overlays/LoginOverlay.cs b/osu.Game/Overlays/LoginOverlay.cs index 536811dfcf..c0aff6aae9 100644 --- a/osu.Game/Overlays/LoginOverlay.cs +++ b/osu.Game/Overlays/LoginOverlay.cs @@ -1,33 +1,45 @@ // 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; +using osu.Framework.Graphics.Effects; using osuTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Overlays.Login; +using osu.Game.Overlays.Settings; namespace osu.Game.Overlays { public partial class LoginOverlay : OsuFocusedOverlayContainer { - private LoginPanel panel; + private LoginPanel panel = null!; private const float transition_time = 400; + protected override double PopInOutSampleBalance => OsuGameBase.SFX_STEREO_STRENGTH * 0.75f; + + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + public LoginOverlay() { AutoSizeAxes = Axes.Both; + Masking = true; + EdgeEffect = new EdgeEffectParameters + { + Colour = Color4.Black, + Type = EdgeEffectType.Shadow, + Radius = 10, + Hollow = true, + }; } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { Children = new Drawable[] { @@ -40,8 +52,7 @@ namespace osu.Game.Overlays new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.6f, + Colour = colourProvider.Background4, }, new Container { @@ -50,23 +61,11 @@ namespace osu.Game.Overlays Masking = true, AutoSizeDuration = transition_time, AutoSizeEasing = Easing.OutQuint, - Children = new Drawable[] + Child = panel = new LoginPanel { - panel = new LoginPanel - { - Padding = new MarginPadding(10), - RequestHide = Hide, - }, - new Box - { - RelativeSizeAxes = Axes.X, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Height = 3, - Colour = colours.Yellow, - Alpha = 1, - }, - } + Padding = new MarginPadding { Vertical = SettingsSection.ITEM_SPACING }, + RequestHide = Hide, + }, } } } @@ -75,10 +74,9 @@ namespace osu.Game.Overlays protected override void PopIn() { - base.PopIn(); - panel.Bounding = true; this.FadeIn(transition_time, Easing.OutQuint); + FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out); ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(panel)); } @@ -89,6 +87,7 @@ namespace osu.Game.Overlays panel.Bounding = false; this.FadeOut(transition_time); + FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.In); } } } diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index bd895fe6bf..eba35ec6f9 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -246,9 +246,13 @@ namespace osu.Game.Overlays } } + protected override void PopIn() + { + this.FadeIn(200); + } + protected override void PopOut() { - base.PopOut(); this.FadeOut(200); } diff --git a/osu.Game/Overlays/Mods/AddPresetButton.cs b/osu.Game/Overlays/Mods/AddPresetButton.cs index 731079d1d9..276afd9bec 100644 --- a/osu.Game/Overlays/Mods/AddPresetButton.cs +++ b/osu.Game/Overlays/Mods/AddPresetButton.cs @@ -18,6 +18,8 @@ namespace osu.Game.Overlays.Mods { public partial class AddPresetButton : ShearedToggleButton, IHasPopover { + protected override bool PlayToggleSamples => false; + [Resolved] private OsuColour colours { get; set; } = null!; diff --git a/osu.Game/Overlays/Mods/AddPresetPopover.cs b/osu.Game/Overlays/Mods/AddPresetPopover.cs index d9e350e560..638592a9b5 100644 --- a/osu.Game/Overlays/Mods/AddPresetPopover.cs +++ b/osu.Game/Overlays/Mods/AddPresetPopover.cs @@ -8,10 +8,12 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -95,6 +97,18 @@ namespace osu.Game.Overlays.Mods }, true); } + public override bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.Select: + createButton.TriggerClick(); + return true; + } + + return base.OnPressed(e); + } + private void createPreset() { realm.Write(r => r.Add(new ModPreset @@ -102,7 +116,7 @@ namespace osu.Game.Overlays.Mods Name = nameTextBox.Current.Value, Description = descriptionTextBox.Current.Value, Mods = selectedMods.Value.ToArray(), - Ruleset = r.Find(ruleset.Value.ShortName) + Ruleset = r.Find(ruleset.Value.ShortName)! })); this.HidePopover(); diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs new file mode 100644 index 0000000000..44c29e313b --- /dev/null +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -0,0 +1,188 @@ +// 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.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Mods; +using osuTK; +using System.Threading; +using osu.Framework.Input.Events; +using osu.Game.Configuration; + +namespace osu.Game.Overlays.Mods +{ + /// + /// On the mod select overlay, this provides a local updating view of BPM, star rating and other + /// difficulty attributes so the user can have a better insight into what mods are changing. + /// + public partial class BeatmapAttributesDisplay : ModFooterInformationDisplay + { + private StarRatingDisplay starRatingDisplay = null!; + private BPMDisplay bpmDisplay = null!; + + private VerticalAttributeDisplay circleSizeDisplay = null!; + private VerticalAttributeDisplay drainRateDisplay = null!; + private VerticalAttributeDisplay approachRateDisplay = null!; + private VerticalAttributeDisplay overallDifficultyDisplay = null!; + + public Bindable BeatmapInfo { get; } = new Bindable(); + + [Resolved] + private Bindable> mods { get; set; } = null!; + + public BindableBool Collapsed { get; } = new BindableBool(true); + + private ModSettingChangeTracker? modSettingChangeTracker; + + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } = null!; + + private CancellationTokenSource? cancellationSource; + private IBindable starDifficulty = null!; + + private const float transition_duration = 250; + + [BackgroundDependencyLoader] + private void load() + { + const float shear = ShearedOverlayContainer.SHEAR; + + LeftContent.AddRange(new Drawable[] + { + starRatingDisplay = new StarRatingDisplay(default, animated: true) + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Shear = new Vector2(-shear, 0), + }, + bpmDisplay = new BPMDisplay + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Shear = new Vector2(-shear, 0), + AutoSizeAxes = Axes.Y, + Width = 75, + } + }); + + RightContent.Alpha = 0; + RightContent.AddRange(new Drawable[] + { + circleSizeDisplay = new VerticalAttributeDisplay("CS") { Shear = new Vector2(-shear, 0), }, + drainRateDisplay = new VerticalAttributeDisplay("HP") { Shear = new Vector2(-shear, 0), }, + approachRateDisplay = new VerticalAttributeDisplay("AR") { Shear = new Vector2(-shear, 0), }, + overallDifficultyDisplay = new VerticalAttributeDisplay("OD") { Shear = new Vector2(-shear, 0), }, + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + mods.BindValueChanged(_ => + { + modSettingChangeTracker?.Dispose(); + + modSettingChangeTracker = new ModSettingChangeTracker(mods.Value); + modSettingChangeTracker.SettingChanged += _ => updateValues(); + updateValues(); + }, true); + + BeatmapInfo.BindValueChanged(_ => updateValues(), true); + + Collapsed.BindValueChanged(_ => + { + // Only start autosize animations on first collapse toggle. This avoids an ugly initial presentation. + startAnimating(); + updateCollapsedState(); + }); + + updateCollapsedState(); + } + + protected override bool OnHover(HoverEvent e) + { + startAnimating(); + updateCollapsedState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateCollapsedState(); + base.OnHoverLost(e); + } + + protected override bool OnMouseDown(MouseDownEvent e) => true; + + protected override bool OnClick(ClickEvent e) => true; + + private void startAnimating() + { + Content.AutoSizeEasing = Easing.OutQuint; + Content.AutoSizeDuration = transition_duration; + } + + private void updateCollapsedState() + { + RightContent.FadeTo(Collapsed.Value && !IsHovered ? 0 : 1, transition_duration, Easing.OutQuint); + } + + private void updateValues() => Scheduler.AddOnce(() => + { + if (BeatmapInfo.Value == null) + return; + + cancellationSource?.Cancel(); + + starDifficulty = difficultyCache.GetBindableDifficulty(BeatmapInfo.Value, (cancellationSource = new CancellationTokenSource()).Token); + starDifficulty.BindValueChanged(s => + { + starRatingDisplay.Current.Value = s.NewValue ?? default; + + if (!starRatingDisplay.IsPresent) + starRatingDisplay.FinishTransforms(true); + }); + + double rate = 1; + foreach (var mod in mods.Value.OfType()) + rate = mod.ApplyToRate(0, rate); + + bpmDisplay.Current.Value = BeatmapInfo.Value.BPM * rate; + + BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty); + foreach (var mod in mods.Value.OfType()) + mod.ApplyToDifficulty(adjustedDifficulty); + + circleSizeDisplay.Current.Value = adjustedDifficulty.CircleSize; + drainRateDisplay.Current.Value = adjustedDifficulty.DrainRate; + approachRateDisplay.Current.Value = adjustedDifficulty.ApproachRate; + overallDifficultyDisplay.Current.Value = adjustedDifficulty.OverallDifficulty; + }); + + private partial class BPMDisplay : RollingCounter + { + protected override double RollingDuration => 500; + + protected override LocalisableString FormatCount(double count) => count.ToLocalisableString("0 BPM"); + + protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Font = OsuFont.Default.With(size: 20, weight: FontWeight.SemiBold), + UseFullGlyphHeight = false, + }; + } + } +} diff --git a/osu.Game/Overlays/Mods/DeselectAllModsButton.cs b/osu.Game/Overlays/Mods/DeselectAllModsButton.cs index 3e5a3b12d1..0e60fc3414 100644 --- a/osu.Game/Overlays/Mods/DeselectAllModsButton.cs +++ b/osu.Game/Overlays/Mods/DeselectAllModsButton.cs @@ -1,21 +1,16 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; using osu.Framework.Bindables; -using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; -using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; namespace osu.Game.Overlays.Mods { - public partial class DeselectAllModsButton : ShearedButton, IKeyBindingHandler + public partial class DeselectAllModsButton : ShearedButton { private readonly Bindable> selectedMods = new Bindable>(); @@ -39,18 +34,5 @@ namespace osu.Game.Overlays.Mods { Enabled.Value = selectedMods.Value.Any(); } - - public bool OnPressed(KeyBindingPressEvent e) - { - if (e.Repeat || e.Action != GlobalAction.DeselectAllMods) - return false; - - TriggerClick(); - return true; - } - - public void OnReleased(KeyBindingReleaseEvent e) - { - } } } diff --git a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs deleted file mode 100644 index ee4f932326..0000000000 --- a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs +++ /dev/null @@ -1,43 +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 osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Localisation; -using osuTK; -using osu.Game.Localisation; - -namespace osu.Game.Overlays.Mods -{ - public sealed partial class DifficultyMultiplierDisplay : ModsEffectDisplay - { - protected override LocalisableString Label => DifficultyMultiplierDisplayStrings.DifficultyMultiplier; - - protected override string CounterFormat => @"N2"; - - public DifficultyMultiplierDisplay() - { - Current.Default = 1d; - Current.Value = 1d; - Add(new SpriteIcon - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Icon = FontAwesome.Solid.Times, - Size = new Vector2(7), - Margin = new MarginPadding { Top = 1 } - }); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - // required to prevent the counter initially rolling up from 0 to 1 - // due to `Current.Value` having a nonstandard default value of 1. - Counter.SetCountWithoutRolling(Current.Value); - } - } -} diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 5220f6a391..571021b0f8 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -8,11 +8,13 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osuTK; @@ -130,6 +132,25 @@ namespace osu.Game.Overlays.Mods }, true); } + protected override void LoadComplete() + { + base.LoadComplete(); + + ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox)); + } + + public override bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.Select: + saveButton.TriggerClick(); + return true; + } + + return base.OnPressed(e); + } + private void useCurrentMods() { saveableMods = selectedMods.Value.ToHashSet(); @@ -138,7 +159,7 @@ namespace osu.Game.Overlays.Mods private void updateState() { - scrollContent.ChildrenEnumerable = saveableMods.Select(mod => new ModPresetRow(mod)); + scrollContent.ChildrenEnumerable = saveableMods.AsOrdered().Select(mod => new ModPresetRow(mod)); useCurrentModsButton.Enabled.Value = checkSelectedModsDiffersFromSaved(); } @@ -150,13 +171,6 @@ namespace osu.Game.Overlays.Mods return !saveableMods.SetEquals(selectedMods.Value); } - protected override void LoadComplete() - { - base.LoadComplete(); - - ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox)); - } - private void save() { preset.PerformWrite(s => diff --git a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs index 93279b6e1c..26c5b2ac49 100644 --- a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs +++ b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; using osu.Framework.Allocation; @@ -19,7 +17,7 @@ namespace osu.Game.Overlays.Mods private readonly BindableBool incompatible = new BindableBool(); [Resolved] - private Bindable> selectedMods { get; set; } + private Bindable> selectedMods { get; set; } = null!; public IncompatibilityDisplayingModPanel(ModState modState) : base(modState) diff --git a/osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.cs b/osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.cs index 1723634774..2f82711162 100644 --- a/osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.cs +++ b/osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; using osu.Framework.Allocation; @@ -24,7 +22,7 @@ namespace osu.Game.Overlays.Mods private readonly Bindable> incompatibleMods = new Bindable>(); [Resolved] - private Bindable ruleset { get; set; } + private Bindable ruleset { get; set; } = null!; public IncompatibilityDisplayingTooltip() { diff --git a/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs index 4f3c18fc43..59a631a7b5 100644 --- a/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs +++ b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs @@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Mods.Input if (!mod_type_lookup.TryGetValue(e.Key, out var typesToMatch)) return false; - var matchingMods = availableMods.Where(modState => matches(modState, typesToMatch) && !modState.Filtered.Value).ToArray(); + var matchingMods = availableMods.Where(modState => matches(modState, typesToMatch) && modState.Visible).ToArray(); if (matchingMods.Length == 0) return false; diff --git a/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs b/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs index dedb556304..e638063438 100644 --- a/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs +++ b/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Mods.Input if (index < 0) return false; - var modState = availableMods.Where(modState => !modState.Filtered.Value).ElementAtOrDefault(index); + var modState = availableMods.Where(modState => modState.Visible).ElementAtOrDefault(index); if (modState == null) return false; diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 5d9f616e5f..d65c94d14d 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -46,7 +46,8 @@ namespace osu.Game.Overlays.Mods foreach (var mod in availableMods) { mod.Active.BindValueChanged(_ => updateState()); - mod.Filtered.BindValueChanged(_ => updateState()); + mod.MatchingTextFilter.BindValueChanged(_ => updateState()); + mod.ValidForSelection.BindValueChanged(_ => updateState()); } updateState(); @@ -145,12 +146,17 @@ namespace osu.Game.Overlays.Mods private void updateState() { - Alpha = availableMods.All(mod => mod.Filtered.Value) ? 0 : 1; + Alpha = availableMods.All(mod => !mod.Visible) ? 0 : 1; if (toggleAllCheckbox != null && !SelectionAnimationRunning) { - toggleAllCheckbox.Alpha = availableMods.Any(panel => !panel.Filtered.Value) ? 1 : 0; - toggleAllCheckbox.Current.Value = availableMods.Where(panel => !panel.Filtered.Value).All(panel => panel.Active.Value); + bool anyPanelsVisible = availableMods.Any(panel => panel.Visible); + + toggleAllCheckbox.Alpha = anyPanelsVisible ? 1 : 0; + + // checking `anyPanelsVisible` is important since `.All()` returns `true` for empty enumerables. + if (anyPanelsVisible) + toggleAllCheckbox.Current.Value = availableMods.Where(panel => panel.Visible).All(panel => panel.Active.Value); } } @@ -176,7 +182,7 @@ namespace osu.Game.Overlays.Mods dequeuedAction(); // each time we play an animation, we decrease the time until the next animation (to ramp the visual and audible elements). - selectionDelay = Math.Max(30, selectionDelay * 0.8f); + selectionDelay = Math.Max(ModSelectPanel.SAMPLE_PLAYBACK_DELAY, selectionDelay * 0.8f); lastSelection = Time.Current; } else @@ -195,7 +201,7 @@ namespace osu.Game.Overlays.Mods { pendingSelectionOperations.Clear(); - foreach (var button in availableMods.Where(b => !b.Active.Value && !b.Filtered.Value)) + foreach (var button in availableMods.Where(b => !b.Active.Value && b.Visible)) pendingSelectionOperations.Enqueue(() => button.Active.Value = true); } @@ -206,8 +212,13 @@ namespace osu.Game.Overlays.Mods { pendingSelectionOperations.Clear(); - foreach (var button in availableMods.Where(b => b.Active.Value && !b.Filtered.Value)) - pendingSelectionOperations.Enqueue(() => button.Active.Value = false); + foreach (var button in availableMods.Where(b => b.Active.Value)) + { + if (!button.Visible) + button.Active.Value = false; + else + pendingSelectionOperations.Enqueue(() => button.Active.Value = false); + } } /// diff --git a/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs b/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs new file mode 100644 index 0000000000..7fccf0cc13 --- /dev/null +++ b/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs @@ -0,0 +1,109 @@ +// 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.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.UserInterface; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Mods +{ + public abstract partial class ModFooterInformationDisplay : CompositeDrawable + { + protected FillFlowContainer LeftContent { get; private set; } = null!; + protected FillFlowContainer RightContent { get; private set; } = null!; + protected Container Content { get; private set; } = null!; + + private Container innerContent = null!; + + protected Box MainBackground { get; private set; } = null!; + protected Box FrontBackground { get; private set; } = null!; + + [Resolved] + protected OverlayColourProvider ColourProvider { get; private set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + + InternalChild = Content = new Container + { + Origin = Anchor.BottomRight, + Anchor = Anchor.BottomRight, + AutoSizeAxes = Axes.X, + Height = ShearedButton.HEIGHT, + Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0), + CornerRadius = ShearedButton.CORNER_RADIUS, + BorderThickness = ShearedButton.BORDER_THICKNESS, + Masking = true, + Children = new Drawable[] + { + MainBackground = new Box + { + RelativeSizeAxes = Axes.Both + }, + new FillFlowContainer // divide inner and outer content + { + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + innerContent = new Container + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + BorderThickness = ShearedButton.BORDER_THICKNESS, + CornerRadius = ShearedButton.CORNER_RADIUS, + Masking = true, + Children = new Drawable[] + { + FrontBackground = new Box + { + RelativeSizeAxes = Axes.Both + }, + LeftContent = new FillFlowContainer // actual inner content + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Margin = new MarginPadding { Horizontal = 15 }, + Spacing = new Vector2(10), + } + } + }, + RightContent = new FillFlowContainer + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + } + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + MainBackground.Colour = ColourProvider.Background4; + FrontBackground.Colour = ColourProvider.Background3; + Color4 glowColour = ColourProvider.Background1; + + Content.BorderColour = ColourInfo.GradientVertical(MainBackground.Colour, glowColour); + innerContent.BorderColour = ColourInfo.GradientVertical(FrontBackground.Colour, glowColour); + } + } +} diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index b5fee9d116..f294b1892d 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -1,9 +1,12 @@ // 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -11,11 +14,10 @@ using osuTK; namespace osu.Game.Overlays.Mods { - public partial class ModPanel : ModSelectPanel + public partial class ModPanel : ModSelectPanel, IFilterable { public Mod Mod => modState.Mod; public override BindableBool Active => modState.Active; - public BindableBool Filtered => modState.Filtered; protected override float IdleSwitchWidth => 54; protected override float ExpandedSwitchWidth => 70; @@ -54,7 +56,8 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); - Filtered.BindValueChanged(_ => updateFilterState(), true); + modState.ValidForSelection.BindValueChanged(_ => updateFilterState()); + modState.MatchingTextFilter.BindValueChanged(_ => updateFilterState(), true); } protected override void Select() @@ -71,9 +74,25 @@ namespace osu.Game.Overlays.Mods #region Filtering support + /// + public bool Visible => modState.Visible; + + public override IEnumerable FilterTerms => new[] + { + Mod.Name, + Mod.Acronym, + Mod.Description + }; + + public override bool MatchingFilter + { + get => modState.MatchingTextFilter.Value; + set => modState.MatchingTextFilter.Value = value; + } + private void updateFilterState() { - this.FadeTo(Filtered.Value ? 0 : 1); + this.FadeTo(Visible ? 1 : 0); } #endregion diff --git a/osu.Game/Overlays/Mods/ModPresetColumn.cs b/osu.Game/Overlays/Mods/ModPresetColumn.cs index bf5e576277..0803389f45 100644 --- a/osu.Game/Overlays/Mods/ModPresetColumn.cs +++ b/osu.Game/Overlays/Mods/ModPresetColumn.cs @@ -26,6 +26,8 @@ namespace osu.Game.Overlays.Mods [Resolved] private IBindable ruleset { get; set; } = null!; + private const float contracted_width = WIDTH - 120; + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -42,6 +44,8 @@ namespace osu.Game.Overlays.Mods base.LoadComplete(); ruleset.BindValueChanged(_ => rulesetChanged(), true); + + Width = contracted_width; } private IDisposable? presetSubscription; @@ -61,11 +65,15 @@ namespace osu.Game.Overlays.Mods private Task? latestLoadTask; internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true; - private void asyncLoadPanels(IRealmCollection presets, ChangeSet changes, Exception error) + private void asyncLoadPanels(IRealmCollection presets, ChangeSet? changes) { cancellationTokenSource?.Cancel(); - if (!presets.Any()) + bool hasPresets = presets.Any(); + + this.ResizeWidthTo(hasPresets ? WIDTH : contracted_width, 200, Easing.OutQuint); + + if (!hasPresets) { removeAndDisposePresetPanels(); return; diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index 8bcb5e4e4e..00f6e36972 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics; @@ -81,6 +82,27 @@ namespace osu.Game.Overlays.Mods Active.Value = new HashSet(Preset.Value.Mods).SetEquals(selectedMods.Value); } + #region Filtering support + + public override IEnumerable FilterTerms => getFilterTerms(); + + private IEnumerable getFilterTerms() + { + var preset = Preset.Value; + + yield return preset.Name; + yield return preset.Description; + + foreach (Mod mod in preset.Mods) + { + yield return mod.Name; + yield return mod.Acronym; + yield return mod.Description; + } + } + + #endregion + #region IHasCustomTooltip public ModPreset TooltipContent => Preset.Value; diff --git a/osu.Game/Overlays/Mods/ModPresetTooltip.cs b/osu.Game/Overlays/Mods/ModPresetTooltip.cs index 8e8259de45..077bd14751 100644 --- a/osu.Game/Overlays/Mods/ModPresetTooltip.cs +++ b/osu.Game/Overlays/Mods/ModPresetTooltip.cs @@ -50,7 +50,7 @@ namespace osu.Game.Overlays.Mods return; lastPreset = preset; - Content.ChildrenEnumerable = preset.Mods.Select(mod => new ModPresetRow(mod)); + Content.ChildrenEnumerable = preset.Mods.AsOrdered().Select(mod => new ModPresetRow(mod)); } protected override void PopIn() => this.FadeIn(transition_duration, Easing.OutQuint); diff --git a/osu.Game/Overlays/Mods/ModSearchContainer.cs b/osu.Game/Overlays/Mods/ModSearchContainer.cs new file mode 100644 index 0000000000..8787530d5c --- /dev/null +++ b/osu.Game/Overlays/Mods/ModSearchContainer.cs @@ -0,0 +1,27 @@ +// 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.Graphics.Containers; + +namespace osu.Game.Overlays.Mods +{ + public partial class ModSearchContainer : SearchContainer + { + public new string SearchTerm + { + get => base.SearchTerm; + set + { + if (value == SearchTerm) + return; + + base.SearchTerm = value; + + // Manual filtering here is required because ModColumn can be hidden when search term applied, + // causing the whole SearchContainer to become non-present and never actually perform a subsequent + // filter. + Filter(); + } + } + } +} diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index e6d7bcd97d..1c56763bd9 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs @@ -43,10 +43,15 @@ namespace osu.Game.Overlays.Mods /// public readonly Bindable Active = new BindableBool(true); + public string SearchTerm + { + set => ItemsFlow.SearchTerm = value; + } + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value; protected readonly Container ControlContainer; - protected readonly FillFlowContainer ItemsFlow; + protected readonly ModSearchContainer ItemsFlow; private readonly TextFlowContainer headerText; private readonly Box headerBackground; @@ -56,9 +61,11 @@ namespace osu.Game.Overlays.Mods private const float header_height = 42; + protected const float WIDTH = 320; + protected ModSelectColumn() { - Width = 320; + Width = WIDTH; RelativeSizeAxes = Axes.Y; Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0); @@ -150,7 +157,7 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.Both, ClampExtension = 100, ScrollbarOverlapsContent = false, - Child = ItemsFlow = new FillFlowContainer + Child = ItemsFlow = new ModSearchContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 38ae8c68cb..f2b3264a84 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -12,9 +12,12 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Audio; +using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -25,10 +28,11 @@ using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Utils; using osuTK; +using osuTK.Input; namespace osu.Game.Overlays.Mods { - public abstract partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler + public abstract partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler, IKeyBindingHandler { public const int BUTTON_WIDTH = 200; @@ -45,7 +49,8 @@ namespace osu.Game.Overlays.Mods /// Contrary to and , the instances /// inside the objects are owned solely by this instance. /// - public Bindable>> AvailableMods { get; } = new Bindable>>(new Dictionary>()); + public Bindable>> AvailableMods { get; } = + new Bindable>>(new Dictionary>()); private Func isValidMod = _ => true; @@ -64,10 +69,18 @@ namespace osu.Game.Overlays.Mods } } + public string SearchTerm + { + get => SearchTextBox.Current.Value; + set => SearchTextBox.Current.Value = value; + } + + public ShearedSearchTextBox SearchTextBox { get; private set; } = null!; + /// - /// Whether the total score multiplier calculated from the current selected set of mods should be shown. + /// Whether the effects (on score multiplier, on or beatmap difficulty) of the current selected set of mods should be shown. /// - protected virtual bool ShowTotalMultiplier => true; + protected virtual bool ShowModEffects => true; /// /// Whether per-mod customisation controls are visible. @@ -94,12 +107,12 @@ namespace osu.Game.Overlays.Mods }; } - yield return new DeselectAllModsButton(this); + yield return deselectAllModsButton = new DeselectAllModsButton(this); } private readonly Bindable>> globalAvailableMods = new Bindable>>(); - private IEnumerable allAvailableMods => AvailableMods.Value.SelectMany(pair => pair.Value); + public IEnumerable AllAvailableMods => AvailableMods.Value.SelectMany(pair => pair.Value); private readonly BindableBool customisationVisible = new BindableBool(); @@ -107,14 +120,34 @@ namespace osu.Game.Overlays.Mods private ColumnScrollContainer columnScroll = null!; private ColumnFlowContainer columnFlow = null!; private FillFlowContainer footerButtonFlow = null!; + private FillFlowContainer footerContentFlow = null!; + private DeselectAllModsButton deselectAllModsButton = null!; - private DifficultyMultiplierDisplay? multiplierDisplay; + private Container aboveColumnsContent = null!; + private ScoreMultiplierDisplay? multiplierDisplay; + private BeatmapAttributesDisplay? beatmapAttributesDisplay; protected ShearedButton BackButton { get; private set; } = null!; protected ShearedToggleButton? CustomisationButton { get; private set; } + protected SelectAllModsButton? SelectAllModsButton { get; set; } private Sample? columnAppearSample; + private WorkingBeatmap? beatmap; + + public WorkingBeatmap? Beatmap + { + get => beatmap; + set + { + if (beatmap == value) return; + + beatmap = value; + if (IsLoaded && beatmapAttributesDisplay != null) + beatmapAttributesDisplay.BeatmapInfo.Value = beatmap?.BeatmapInfo; + } + } + protected ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green) : base(colourScheme) { @@ -146,6 +179,17 @@ namespace osu.Game.Overlays.Mods MainAreaContent.AddRange(new Drawable[] { + aboveColumnsContent = new Container + { + RelativeSizeAxes = Axes.X, + Height = ScoreMultiplierDisplay.HEIGHT, + Padding = new MarginPadding { Horizontal = 100 }, + Child = SearchTextBox = new ShearedSearchTextBox + { + HoldFocus = false, + Width = 300 + } + }, new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, @@ -153,7 +197,7 @@ namespace osu.Game.Overlays.Mods { Padding = new MarginPadding { - Top = (ShowTotalMultiplier ? ModsEffectDisplay.HEIGHT : 0) + PADDING, + Top = ScoreMultiplierDisplay.HEIGHT + PADDING, Bottom = PADDING }, RelativeSizeAxes = Axes.Both, @@ -184,24 +228,7 @@ namespace osu.Game.Overlays.Mods } }); - if (ShowTotalMultiplier) - { - MainAreaContent.Add(new Container - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.X, - Height = ModsEffectDisplay.HEIGHT, - Margin = new MarginPadding { Horizontal = 100 }, - Child = multiplierDisplay = new DifficultyMultiplierDisplay - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }, - }); - } - - FooterContent.Child = footerButtonFlow = new FillFlowContainer + FooterContent.Add(footerButtonFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -221,11 +248,50 @@ namespace osu.Game.Overlays.Mods DarkerColour = colours.Pink2, LighterColour = colours.Pink1 }) - }; + }); + + if (ShowModEffects) + { + FooterContent.Add(footerContentFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(30, 10), + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding + { + Vertical = PADDING, + Horizontal = 20 + }, + Children = new Drawable[] + { + multiplierDisplay = new ScoreMultiplierDisplay + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight + }, + beatmapAttributesDisplay = new BeatmapAttributesDisplay + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + BeatmapInfo = { Value = beatmap?.BeatmapInfo } + }, + } + }); + } globalAvailableMods.BindTo(game.AvailableMods); } + public override void Hide() + { + base.Hide(); + + // clear search for next user interaction with mod overlay + SearchTextBox.Current.Value = string.Empty; + } + private ModSettingChangeTracker? modSettingChangeTracker; protected override void LoadComplete() @@ -263,6 +329,12 @@ namespace osu.Game.Overlays.Mods customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true); + SearchTextBox.Current.BindValueChanged(query => + { + foreach (var column in columnFlow.Columns) + column.SearchTerm = query.NewValue; + }, true); + // Start scrolled slightly to the right to give the user a sense that // there is more horizontal content available. ScheduleAfterChildren(() => @@ -272,6 +344,32 @@ namespace osu.Game.Overlays.Mods }); } + protected override void Update() + { + base.Update(); + + SearchTextBox.PlaceholderText = SearchTextBox.HasFocus ? Resources.Localisation.Web.CommonStrings.InputSearch : ModSelectOverlayStrings.TabToSearch; + + if (beatmapAttributesDisplay != null) + { + float rightEdgeOfLastButton = footerButtonFlow.Last().ScreenSpaceDrawQuad.TopRight.X; + + // this is cheating a bit; the 640 value is hardcoded based on how wide the expanded panel _generally_ is. + // due to the transition applied, the raw screenspace quad of the panel cannot be used, as it will trigger an ugly feedback cycle of expanding and collapsing. + float projectedLeftEdgeOfExpandedBeatmapAttributesDisplay = footerButtonFlow.ToScreenSpace(footerButtonFlow.DrawSize - new Vector2(640, 0)).X; + + bool screenIsntWideEnough = rightEdgeOfLastButton > projectedLeftEdgeOfExpandedBeatmapAttributesDisplay; + + // only update preview panel's collapsed state after we are fully visible, to ensure all the buttons are where we expect them to be. + if (Alpha == 1) + beatmapAttributesDisplay.Collapsed.Value = screenIsntWideEnough; + + footerContentFlow.LayoutDuration = 200; + footerContentFlow.LayoutEasing = Easing.OutQuint; + footerContentFlow.Direction = screenIsntWideEnough ? FillDirection.Vertical : FillDirection.Horizontal; + } + } + /// /// Select all visible mods in all columns. /// @@ -343,8 +441,8 @@ namespace osu.Game.Overlays.Mods private void filterMods() { - foreach (var modState in allAvailableMods) - modState.Filtered.Value = !modState.Mod.HasImplementation || !IsValidMod.Invoke(modState.Mod); + foreach (var modState in AllAvailableMods) + modState.ValidForSelection.Value = modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod); } private void updateMultiplier() @@ -368,7 +466,7 @@ namespace osu.Game.Overlays.Mods bool anyCustomisableModActive = false; bool anyModPendingConfiguration = false; - foreach (var modState in allAvailableMods) + foreach (var modState in AllAvailableMods) { anyCustomisableModActive |= modState.Active.Value && modState.Mod.GetSettingsSourceProperties().Any(); anyModPendingConfiguration |= modState.PendingConfiguration; @@ -425,7 +523,7 @@ namespace osu.Game.Overlays.Mods var newSelection = new List(); - foreach (var modState in allAvailableMods) + foreach (var modState in AllAvailableMods) { var matchingSelectedMod = SelectedMods.Value.SingleOrDefault(selected => selected.GetType() == modState.Mod.GetType()); @@ -452,7 +550,7 @@ namespace osu.Game.Overlays.Mods if (externalSelectionUpdateInProgress) return; - var candidateSelection = allAvailableMods.Where(modState => modState.Active.Value) + var candidateSelection = AllAvailableMods.Where(modState => modState.Active.Value) .Select(modState => modState.Mod) .ToArray(); @@ -469,7 +567,7 @@ namespace osu.Game.Overlays.Mods base.PopIn(); - multiplierDisplay? + aboveColumnsContent .FadeIn(fade_in_duration, Easing.OutQuint) .MoveToY(0, fade_in_duration, Easing.OutQuint); @@ -479,7 +577,7 @@ namespace osu.Game.Overlays.Mods { var column = columnFlow[i].Column; - bool allFiltered = column is ModColumn modColumn && modColumn.AvailableMods.All(modState => modState.Filtered.Value); + bool allFiltered = column is ModColumn modColumn && modColumn.AvailableMods.All(modState => !modState.Visible); double delay = allFiltered ? 0 : nonFilteredColumnCount * 30; double duration = allFiltered ? 0 : fade_in_duration; @@ -506,7 +604,7 @@ namespace osu.Game.Overlays.Mods if (columnNumber > 5 && !column.Active.Value) return; // use X position of the column on screen as a basis for panning the sample - float balance = column.Parent.BoundingBox.Centre.X / RelativeToAbsoluteFactor.X; + float balance = column.Parent!.BoundingBox.Centre.X / RelativeToAbsoluteFactor.X; // dip frequency and ramp volume of sample over the first 5 displayed columns float progress = Math.Min(1, columnNumber / 5f); @@ -527,7 +625,7 @@ namespace osu.Game.Overlays.Mods base.PopOut(); - multiplierDisplay? + aboveColumnsContent .FadeOut(fade_out_duration / 2, Easing.OutQuint) .MoveToY(-distance, fade_out_duration / 2, Easing.OutQuint); @@ -541,7 +639,7 @@ namespace osu.Game.Overlays.Mods if (column is ModColumn modColumn) { - allFiltered = modColumn.AvailableMods.All(modState => modState.Filtered.Value); + allFiltered = modColumn.AvailableMods.All(modState => !modState.Visible); modColumn.FlushPendingSelections(); } @@ -578,10 +676,39 @@ namespace osu.Game.Overlays.Mods // This is handled locally here because this overlay is being registered at the game level // and therefore takes away keyboard focus from the screen stack. case GlobalAction.ToggleModSelection: + // Pressing toggle should completely hide the overlay in one shot. + hideOverlay(true); + return true; + + // This is handled locally here due to conflicts in input handling between the search text box and the deselect all mods button. + // Attempting to handle this action locally in both places leads to a possible scenario + // wherein activating the binding will both change the contents of the search text box and deselect all mods. + case GlobalAction.DeselectAllMods: + { + if (!SearchTextBox.HasFocus) + { + deselectAllModsButton.TriggerClick(); + return true; + } + + break; + } + case GlobalAction.Select: { - // Pressing toggle or select should completely hide the overlay in one shot. - hideOverlay(true); + // Pressing select should select first filtered mod if a search is in progress. + // If there is no search in progress, it should exit the dialog (a bit weird, but this is the expectation from stable). + if (string.IsNullOrEmpty(SearchTerm)) + { + hideOverlay(true); + return true; + } + + ModState? firstMod = columnFlow.Columns.OfType().FirstOrDefault(m => m.IsPresent)?.AvailableMods.FirstOrDefault(x => x.Visible); + + if (firstMod is not null) + firstMod.Active.Value = !firstMod.Active.Value; + return true; } } @@ -603,6 +730,39 @@ namespace osu.Game.Overlays.Mods } } + /// + /// + /// This is handled locally here due to conflicts in input handling between the search text box and the select all mods button. + /// Attempting to handle this action locally in both places leads to a possible scenario + /// wherein activating the "select all" platform binding will both select all text in the search box and select all mods. + /// > + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Repeat || e.Action != PlatformAction.SelectAll || SelectAllModsButton is null) + return false; + + SelectAllModsButton.TriggerClick(); + return true; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Repeat || e.Key != Key.Tab) + return false; + + // TODO: should probably eventually support typical platform search shortcuts (`Ctrl-F`, `/`) + if (SearchTextBox.HasFocus) + SearchTextBox.KillFocus(); + else + SearchTextBox.TakeFocus(); + + return true; + } + #endregion #region Sample playback control @@ -743,6 +903,9 @@ namespace osu.Game.Overlays.Mods if (!Active.Value) RequestScroll?.Invoke(this); + // Killing focus is done here because it's the only feasible place on ModSelectOverlay you can click on without triggering any action. + Scheduler.Add(() => GetContainingInputManager().ChangeFocus(null)); + return true; } @@ -782,6 +945,9 @@ namespace osu.Game.Overlays.Mods OnClicked?.Invoke(); return true; + case HoverEvent: + return false; + case MouseEvent: return true; } diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index 81285833bd..29f4c93e88 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs @@ -1,6 +1,7 @@ // 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 osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -14,6 +15,7 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Audio; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -24,7 +26,7 @@ using osuTK.Input; namespace osu.Game.Overlays.Mods { - public abstract partial class ModSelectPanel : OsuClickableContainer, IHasAccentColour + public abstract partial class ModSelectPanel : OsuClickableContainer, IHasAccentColour, IFilterable { public abstract BindableBool Active { get; } @@ -45,6 +47,8 @@ namespace osu.Game.Overlays.Mods public const float CORNER_RADIUS = 7; public const float HEIGHT = 42; + public const double SAMPLE_PLAYBACK_DELAY = 30; + protected virtual float IdleSwitchWidth => 14; protected virtual float ExpandedSwitchWidth => 30; protected virtual Colour4 BackgroundColour => Active.Value ? AccentColour.Darken(0.3f) : ColourProvider.Background3; @@ -69,6 +73,8 @@ namespace osu.Game.Overlays.Mods private Sample? sampleOff; private Sample? sampleOn; + private Bindable lastPlaybackTime = null!; + protected ModSelectPanel() { RelativeSizeAxes = Axes.X; @@ -118,23 +124,23 @@ namespace osu.Game.Overlays.Mods Direction = FillDirection.Vertical, Children = new[] { - titleText = new OsuSpriteText + titleText = new TruncatingSpriteText { Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, - Truncate = true, Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Margin = new MarginPadding { Left = -18 * ShearedOverlayContainer.SHEAR - } + }, + ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`. }, - descriptionText = new OsuSpriteText + descriptionText = new TruncatingSpriteText { Font = OsuFont.Default.With(size: 12), RelativeSizeAxes = Axes.X, - Truncate = true, - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0) + Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`. } } } @@ -163,13 +169,15 @@ namespace osu.Game.Overlays.Mods protected abstract void Deselect(); [BackgroundDependencyLoader] - private void load(AudioManager audio, ISamplePlaybackDisabler? samplePlaybackDisabler) + private void load(AudioManager audio, SessionStatics statics, ISamplePlaybackDisabler? samplePlaybackDisabler) { sampleOn = audio.Samples.Get(@"UI/check-on"); sampleOff = audio.Samples.Get(@"UI/check-off"); if (samplePlaybackDisabler != null) ((IBindable)samplePlaybackDisabled).BindTo(samplePlaybackDisabler.SamplePlaybackDisabled); + + lastPlaybackTime = statics.GetBindable(Static.LastHoverSoundPlaybackTime); } protected sealed override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet); @@ -192,10 +200,20 @@ namespace osu.Game.Overlays.Mods if (samplePlaybackDisabled.Value) return; - if (Active.Value) - sampleOn?.Play(); - else - sampleOff?.Play(); + if (!IsPresent) + return; + + bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= SAMPLE_PLAYBACK_DELAY; + + if (enoughTimePassedSinceLastPlayback) + { + if (Active.Value) + sampleOn?.Play(); + else + sampleOff?.Play(); + + lastPlaybackTime.Value = Time.Current; + } } protected override bool OnHover(HoverEvent e) @@ -263,5 +281,28 @@ namespace osu.Game.Overlays.Mods TextBackground.FadeColour(foregroundColour, transitionDuration, Easing.OutQuint); TextFlow.FadeColour(textColour, transitionDuration, Easing.OutQuint); } + + #region IFilterable + + public abstract IEnumerable FilterTerms { get; } + + private bool matchingFilter = true; + + public virtual bool MatchingFilter + { + get => matchingFilter; + set + { + if (matchingFilter == value) + return; + + matchingFilter = value; + this.FadeTo(value ? 1 : 0); + } + } + + public bool FilteringActive { set { } } + + #endregion } } diff --git a/osu.Game/Overlays/Mods/ModSettingsArea.cs b/osu.Game/Overlays/Mods/ModSettingsArea.cs index f11fef1299..6158c2c70f 100644 --- a/osu.Game/Overlays/Mods/ModSettingsArea.cs +++ b/osu.Game/Overlays/Mods/ModSettingsArea.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; @@ -32,7 +30,7 @@ namespace osu.Game.Overlays.Mods private readonly FillFlowContainer modSettingsFlow; [Resolved] - private OverlayColourProvider colourProvider { get; set; } + private OverlayColourProvider colourProvider { get; set; } = null!; public ModSettingsArea() { @@ -86,7 +84,7 @@ namespace osu.Game.Overlays.Mods { modSettingsFlow.Clear(); - foreach (var mod in SelectedMods.Value.OrderBy(mod => mod.Type).ThenBy(mod => mod.Acronym)) + foreach (var mod in SelectedMods.Value.AsOrdered()) { var settings = mod.CreateSettingsControls().ToList(); diff --git a/osu.Game/Overlays/Mods/ModState.cs b/osu.Game/Overlays/Mods/ModState.cs index 3ee890e876..7a5bc0f3ae 100644 --- a/osu.Game/Overlays/Mods/ModState.cs +++ b/osu.Game/Overlays/Mods/ModState.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 osu.Framework.Bindables; using osu.Game.Rulesets.Mods; @@ -32,9 +30,21 @@ namespace osu.Game.Overlays.Mods public bool PendingConfiguration { get; set; } /// - /// Whether the mod is currently filtered out due to not matching imposed criteria. + /// Whether the mod is currently valid for selection. + /// This can be in scenarios such as the free mod select overlay, where not all mods are selectable + /// regardless of search criteria imposed by the user selecting. /// - public BindableBool Filtered { get; } = new BindableBool(); + public BindableBool ValidForSelection { get; } = new BindableBool(true); + + /// + /// Whether the mod is matching the current textual filter. + /// + public BindableBool MatchingTextFilter { get; } = new BindableBool(true); + + /// + /// Whether the matches all applicable filters and visible for the user to select. + /// + public bool Visible => MatchingTextFilter.Value && ValidForSelection.Value; public ModState(Mod mod) { diff --git a/osu.Game/Overlays/Mods/ModsEffectDisplay.cs b/osu.Game/Overlays/Mods/ModsEffectDisplay.cs deleted file mode 100644 index 3f31736ee1..0000000000 --- a/osu.Game/Overlays/Mods/ModsEffectDisplay.cs +++ /dev/null @@ -1,223 +0,0 @@ -// 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.Extensions.LocalisationExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Localisation; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets.Mods; -using osuTK; - -namespace osu.Game.Overlays.Mods -{ - /// - /// Base class for displays of mods effects. - /// - public abstract partial class ModsEffectDisplay : Container, IHasCurrentValue - { - public const float HEIGHT = 42; - private const float transition_duration = 200; - - private readonly Box contentBackground; - private readonly Box labelBackground; - private readonly FillFlowContainer content; - - public Bindable Current - { - get => current.Current; - set => current.Current = value; - } - - private readonly BindableWithCurrent current = new BindableWithCurrent(); - - [Resolved] - private OsuColour colours { get; set; } = null!; - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; - - /// - /// Text to display in the left area of the display. - /// - protected abstract LocalisableString Label { get; } - - protected virtual float ValueAreaWidth => 56; - - protected virtual string CounterFormat => @"N0"; - - protected override Container Content => content; - - protected readonly RollingCounter Counter; - - protected ModsEffectDisplay() - { - Height = HEIGHT; - AutoSizeAxes = Axes.X; - - InternalChild = new InputBlockingContainer - { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Masking = true, - CornerRadius = ModSelectPanel.CORNER_RADIUS, - Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0), - Children = new Drawable[] - { - contentBackground = new Box - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.Y, - Width = ValueAreaWidth + ModSelectPanel.CORNER_RADIUS - }, - new GridContainer - { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, ValueAreaWidth) - }, - Content = new[] - { - new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Masking = true, - CornerRadius = ModSelectPanel.CORNER_RADIUS, - Children = new Drawable[] - { - labelBackground = new Box - { - RelativeSizeAxes = Axes.Both - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Margin = new MarginPadding { Horizontal = 18 }, - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), - Text = Label, - Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold) - } - } - }, - content = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Direction = FillDirection.Horizontal, - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), - Spacing = new Vector2(2, 0), - Child = Counter = new EffectCounter(CounterFormat) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = { BindTarget = Current } - } - } - } - } - } - } - }; - } - - [BackgroundDependencyLoader] - private void load() - { - labelBackground.Colour = colourProvider.Background4; - } - - protected override void LoadComplete() - { - Current.BindValueChanged(e => - { - var effect = CalculateEffectForComparison(e.NewValue.CompareTo(Current.Default)); - setColours(effect); - }, true); - } - - /// - /// Fades colours of text and its background according to displayed value. - /// - /// Effect of the value. - private void setColours(ModEffect effect) - { - switch (effect) - { - case ModEffect.NotChanged: - contentBackground.FadeColour(colourProvider.Background3, transition_duration, Easing.OutQuint); - content.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); - break; - - case ModEffect.DifficultyReduction: - contentBackground.FadeColour(colours.ForModType(ModType.DifficultyReduction), transition_duration, Easing.OutQuint); - content.FadeColour(colourProvider.Background5, transition_duration, Easing.OutQuint); - break; - - case ModEffect.DifficultyIncrease: - contentBackground.FadeColour(colours.ForModType(ModType.DifficultyIncrease), transition_duration, Easing.OutQuint); - content.FadeColour(colourProvider.Background5, transition_duration, Easing.OutQuint); - break; - - default: - throw new ArgumentOutOfRangeException(nameof(effect)); - } - } - - /// - /// Converts signed integer into . Negative values are counted as difficulty reduction, positive as increase. - /// - /// Value to convert. Will arrive from comparison between bindable once it changes and it's . - /// Effect of the value. - protected virtual ModEffect CalculateEffectForComparison(int comparison) - { - if (comparison == 0) - return ModEffect.NotChanged; - if (comparison < 0) - return ModEffect.DifficultyReduction; - - return ModEffect.DifficultyIncrease; - } - - protected enum ModEffect - { - NotChanged, - DifficultyReduction, - DifficultyIncrease - } - - private partial class EffectCounter : RollingCounter - { - private readonly string? format; - - public EffectCounter(string? format) - { - this.format = format; - } - - protected override double RollingDuration => 500; - - protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(format); - - protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText - { - Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold) - }; - } - } -} diff --git a/osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs b/osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs new file mode 100644 index 0000000000..c758632392 --- /dev/null +++ b/osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs @@ -0,0 +1,160 @@ +// 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.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; +using osu.Game.Rulesets.Mods; +using osuTK; + +namespace osu.Game.Overlays.Mods +{ + /// + /// On the mod select overlay, this provides a local updating view of the aggregate score multiplier coming from mods. + /// + public partial class ScoreMultiplierDisplay : ModFooterInformationDisplay, IHasCurrentValue + { + public const float HEIGHT = 42; + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + private const float transition_duration = 200; + + private RollingCounter counter = null!; + + private Box flashLayer = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public ScoreMultiplierDisplay() + { + Current.Default = 1d; + Current.Value = 1d; + } + + [BackgroundDependencyLoader] + private void load() + { + // You would think that we could add this to `Content`, but borders don't mix well + // with additive blending children elements. + AddInternal(new Container + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + RelativeSizeAxes = Axes.Both, + Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0), + CornerRadius = ShearedButton.CORNER_RADIUS, + Masking = true, + Children = new Drawable[] + { + flashLayer = new Box + { + Alpha = 0, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + } + } + }); + + LeftContent.AddRange(new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + Text = ModSelectOverlayStrings.ScoreMultiplier, + Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold) + } + }); + + RightContent.Add(new Container + { + Width = 40, + RelativeSizeAxes = Axes.Y, + Margin = new MarginPadding(10), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = counter = new EffectCounter + { + Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = { BindTarget = Current } + } + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(e => + { + if (e.NewValue > Current.Default) + { + MainBackground + .FadeColour(colours.ForModType(ModType.DifficultyIncrease), transition_duration, Easing.OutQuint); + counter.FadeColour(ColourProvider.Background5, transition_duration, Easing.OutQuint); + } + else if (e.NewValue < Current.Default) + { + MainBackground + .FadeColour(colours.ForModType(ModType.DifficultyReduction), transition_duration, Easing.OutQuint); + counter.FadeColour(ColourProvider.Background5, transition_duration, Easing.OutQuint); + } + else + { + MainBackground.FadeColour(ColourProvider.Background4, transition_duration, Easing.OutQuint); + counter.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); + } + + flashLayer + .FadeOutFromOne() + .FadeTo(0.15f, 60, Easing.OutQuint) + .Then().FadeOut(500, Easing.OutQuint); + + const float move_amount = 4; + if (e.NewValue > e.OldValue) + counter.MoveToY(Math.Max(-move_amount * 2, counter.Y - move_amount)).Then().MoveToY(0, transition_duration * 2, Easing.OutQuint); + else + counter.MoveToY(Math.Min(move_amount * 2, counter.Y + move_amount)).Then().MoveToY(0, transition_duration * 2, Easing.OutQuint); + }, true); + + // required to prevent the counter initially rolling up from 0 to 1 + // due to `Current.Value` having a nonstandard default value of 1. + counter.SetCountWithoutRolling(Current.Value); + } + + private partial class EffectCounter : RollingCounter + { + protected override double RollingDuration => 500; + + protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(@"0.00x"); + + protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold) + }; + } + } +} diff --git a/osu.Game/Overlays/Mods/SelectAllModsButton.cs b/osu.Game/Overlays/Mods/SelectAllModsButton.cs index f4b8025227..bb61cdc35d 100644 --- a/osu.Game/Overlays/Mods/SelectAllModsButton.cs +++ b/osu.Game/Overlays/Mods/SelectAllModsButton.cs @@ -1,14 +1,9 @@ // 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 System.Linq; using osu.Framework.Bindables; -using osu.Framework.Input; -using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; @@ -16,10 +11,11 @@ using osu.Game.Screens.OnlinePlay; namespace osu.Game.Overlays.Mods { - public partial class SelectAllModsButton : ShearedButton, IKeyBindingHandler + public partial class SelectAllModsButton : ShearedButton { private readonly Bindable> selectedMods = new Bindable>(); private readonly Bindable>> availableMods = new Bindable>>(); + private readonly Bindable searchTerm = new Bindable(); public SelectAllModsButton(FreeModSelectOverlay modSelectOverlay) : base(ModSelectOverlay.BUTTON_WIDTH) @@ -29,6 +25,7 @@ namespace osu.Game.Overlays.Mods selectedMods.BindTo(modSelectOverlay.SelectedMods); availableMods.BindTo(modSelectOverlay.AvailableMods); + searchTerm.BindTo(modSelectOverlay.SearchTextBox.Current); } protected override void LoadComplete() @@ -37,6 +34,7 @@ namespace osu.Game.Overlays.Mods selectedMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState)); availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState)); + searchTerm.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState)); updateEnabledState(); } @@ -44,20 +42,7 @@ namespace osu.Game.Overlays.Mods { Enabled.Value = availableMods.Value .SelectMany(pair => pair.Value) - .Any(modState => !modState.Active.Value && !modState.Filtered.Value); - } - - public bool OnPressed(KeyBindingPressEvent e) - { - if (e.Repeat || e.Action != PlatformAction.SelectAll) - return false; - - TriggerClick(); - return true; - } - - public void OnReleased(KeyBindingReleaseEvent e) - { + .Any(modState => !modState.Active.Value && modState.Visible); } } } diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index 7f7b09a62c..a372ec70db 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -130,7 +130,6 @@ namespace osu.Game.Overlays.Mods { const double fade_in_duration = 400; - base.PopIn(); this.FadeIn(fade_in_duration, Easing.OutQuint); Header.MoveToY(0, fade_in_duration, Easing.OutQuint); diff --git a/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs b/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs new file mode 100644 index 0000000000..60cc875dbb --- /dev/null +++ b/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs @@ -0,0 +1,78 @@ +// 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.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays.Mods +{ + public partial class VerticalAttributeDisplay : Container, IHasCurrentValue + { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + /// + /// Text to display in the top area of the display. + /// + public LocalisableString Label { get; protected set; } + + public VerticalAttributeDisplay(LocalisableString label) + { + Label = label; + + AutoSizeAxes = Axes.X; + + Origin = Anchor.CentreLeft; + Anchor = Anchor.CentreLeft; + + InternalChild = new FillFlowContainer + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Text = Label, + Margin = new MarginPadding { Horizontal = 15 }, // to reserve space for 0.XX value + Font = OsuFont.Default.With(size: 20, weight: FontWeight.Bold) + }, + new EffectCounter + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Current = { BindTarget = Current }, + } + } + }; + } + + private partial class EffectCounter : RollingCounter + { + protected override double RollingDuration => 500; + + protected override LocalisableString FormatCount(double count) => count.ToLocalisableString("0.0"); + + protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText + { + Font = OsuFont.Default.With(size: 18, weight: FontWeight.SemiBold) + }; + } + } +} diff --git a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs index 827caf0467..78de76b981 100644 --- a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs +++ b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; @@ -24,28 +22,22 @@ namespace osu.Game.Overlays.Music public partial class MusicKeyBindingHandler : Component, IKeyBindingHandler { [Resolved] - private IBindable beatmap { get; set; } + private IBindable beatmap { get; set; } = null!; [Resolved] - private MusicController musicController { get; set; } - - [Resolved(canBeNull: true)] - private OnScreenDisplay onScreenDisplay { get; set; } + private MusicController musicController { get; set; } = null!; [Resolved] - private OsuGame game { get; set; } + private OnScreenDisplay? onScreenDisplay { get; set; } public bool OnPressed(KeyBindingPressEvent e) { - if (e.Repeat) + if (e.Repeat || !musicController.AllowTrackControl.Value) return false; switch (e.Action) { case GlobalAction.MusicPlay: - if (game.LocalUserPlaying.Value) - return false; - // use previous state as TogglePause may not update the track's state immediately (state update is run on the audio thread see https://github.com/ppy/osu/issues/9880#issuecomment-674668842) bool wasPlaying = musicController.IsPlaying; diff --git a/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs b/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs index ae59fbb35e..fa9a2e3972 100644 --- a/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs +++ b/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.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 osuTK; using osuTK.Graphics; using osu.Framework.Allocation; diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 43b9024303..7784643163 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -109,7 +109,7 @@ namespace osu.Game.Overlays.Music beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo.ToLive(realm), true); } - private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) + private void beatmapsChanged(IRealmCollection sender, ChangeSet changes) { if (changes == null) { diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 1ad5a8c08b..0986c0513c 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -40,6 +40,11 @@ namespace osu.Game.Overlays /// public bool UserPauseRequested { get; private set; } + /// + /// Whether user control of the global track should be allowed. + /// + public readonly BindableBool AllowTrackControl = new BindableBool(true); + /// /// Fired when the global has changed. /// Includes direction information for display purposes. @@ -92,8 +97,10 @@ namespace osu.Game.Overlays seekDelegate?.Cancel(); seekDelegate = Schedule(() => { - if (!beatmap.Disabled) - CurrentTrack.Seek(position); + if (beatmap.Disabled || !AllowTrackControl.Value) + return; + + CurrentTrack.Seek(position); }); } @@ -107,7 +114,7 @@ namespace osu.Game.Overlays if (CurrentTrack.IsDummyDevice || beatmap.Value.BeatmapSetInfo.DeletePending) { - if (beatmap.Disabled) + if (beatmap.Disabled || !AllowTrackControl.Value) return; Logger.Log($"{nameof(MusicController)} skipping next track to {nameof(EnsurePlayingSomething)}"); @@ -132,6 +139,9 @@ namespace osu.Game.Overlays /// Whether the operation was successful. public bool Play(bool restart = false, bool requestedByUser = false) { + if (requestedByUser && !AllowTrackControl.Value) + return false; + if (requestedByUser) UserPauseRequested = false; @@ -153,6 +163,9 @@ namespace osu.Game.Overlays /// public void Stop(bool requestedByUser = false) { + if (requestedByUser && !AllowTrackControl.Value) + return; + UserPauseRequested |= requestedByUser; if (CurrentTrack.IsRunning) CurrentTrack.StopAsync(); @@ -164,6 +177,9 @@ namespace osu.Game.Overlays /// Whether the operation was successful. public bool TogglePause() { + if (!AllowTrackControl.Value) + return false; + if (CurrentTrack.IsRunning) Stop(true); else @@ -189,7 +205,7 @@ namespace osu.Game.Overlays /// The that indicate the decided action. private PreviousTrackResult prev() { - if (beatmap.Disabled) + if (beatmap.Disabled || !AllowTrackControl.Value) return PreviousTrackResult.None; double currentTrackPosition = CurrentTrack.CurrentTime; @@ -229,7 +245,7 @@ namespace osu.Game.Overlays private bool next() { - if (beatmap.Disabled) + if (beatmap.Disabled || !AllowTrackControl.Value) return false; queuedDirection = TrackChangeDirection.Next; @@ -316,6 +332,8 @@ namespace osu.Game.Overlays var queuedTrack = getQueuedTrack(); var lastTrack = CurrentTrack; + lastTrack.Completed -= onTrackCompleted; + CurrentTrack = queuedTrack; // At this point we may potentially be in an async context from tests. This is extremely dangerous but we have to make do for now. @@ -344,34 +362,30 @@ namespace osu.Game.Overlays // Important to keep this in its own method to avoid inadvertently capturing unnecessary variables in the callback. // Can lead to leaks. var queuedTrack = new DrawableTrack(current.LoadTrack()); - queuedTrack.Completed += () => onTrackCompleted(current); + queuedTrack.Completed += onTrackCompleted; return queuedTrack; } - private void onTrackCompleted(WorkingBeatmap workingBeatmap) + private void onTrackCompleted() { - // the source of track completion is the audio thread, so the beatmap may have changed before firing. - if (current != workingBeatmap) - return; - - if (!CurrentTrack.Looping && !beatmap.Disabled) + if (!CurrentTrack.Looping && !beatmap.Disabled && AllowTrackControl.Value) NextTrack(); } - private bool allowTrackAdjustments; + private bool applyModTrackAdjustments; /// /// Whether mod track adjustments are allowed to be applied. /// - public bool AllowTrackAdjustments + public bool ApplyModTrackAdjustments { - get => allowTrackAdjustments; + get => applyModTrackAdjustments; set { - if (allowTrackAdjustments == value) + if (applyModTrackAdjustments == value) return; - allowTrackAdjustments = value; + applyModTrackAdjustments = value; ResetTrackAdjustments(); } } @@ -379,7 +393,7 @@ namespace osu.Game.Overlays private AudioAdjustments modTrackAdjustments; /// - /// Resets the adjustments currently applied on and applies the mod adjustments if is true. + /// Resets the adjustments currently applied on and applies the mod adjustments if is true. /// /// /// Does not reset any adjustments applied directly to the beatmap track. @@ -392,7 +406,7 @@ namespace osu.Game.Overlays CurrentTrack.RemoveAllAdjustments(AdjustableProperty.Tempo); CurrentTrack.RemoveAllAdjustments(AdjustableProperty.Volume); - if (allowTrackAdjustments) + if (applyModTrackAdjustments) { CurrentTrack.BindAdjustments(modTrackAdjustments = new AudioAdjustments()); diff --git a/osu.Game/Overlays/News/NewsCard.cs b/osu.Game/Overlays/News/NewsCard.cs index b12aa4509e..8a579a5ccc 100644 --- a/osu.Game/Overlays/News/NewsCard.cs +++ b/osu.Game/Overlays/News/NewsCard.cs @@ -14,6 +14,7 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Overlays.News @@ -28,6 +29,7 @@ namespace osu.Game.Overlays.News private TextFlowContainer main = null!; public NewsCard(APINewsPost post) + : base(HoverSampleSet.Button) { this.post = post; diff --git a/osu.Game/Overlays/News/NewsPostBackground.cs b/osu.Game/Overlays/News/NewsPostBackground.cs index 05f8a639fa..663747255a 100644 --- a/osu.Game/Overlays/News/NewsPostBackground.cs +++ b/osu.Game/Overlays/News/NewsPostBackground.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index f2eefb6e4b..81233b4343 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -1,6 +1,8 @@ // 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.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -29,15 +31,23 @@ namespace osu.Game.Overlays public LocalisableString Title => NotificationsStrings.HeaderTitle; public LocalisableString Description => NotificationsStrings.HeaderDescription; + protected override double PopInOutSampleBalance => OsuGameBase.SFX_STEREO_STRENGTH; + public const float WIDTH = 320; public const float TRANSITION_LENGTH = 600; + public IEnumerable AllNotifications => + IsLoaded ? toastTray.Notifications.Concat(sections.SelectMany(s => s.Notifications)) : Array.Empty(); + private FlowContainer sections = null!; [Resolved] private AudioManager audio { get; set; } = null!; + [Resolved] + private OsuGame? game { get; set; } + [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); @@ -103,8 +113,9 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.X, Children = new[] { - new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }, NotificationsStrings.ClearAll), - new NotificationSection(NotificationsStrings.RunningTasks, new[] { typeof(ProgressNotification) }, NotificationsStrings.CancelAll), + // The main section adds as a catch-all for notifications which don't group into other sections. + new NotificationSection(AccountsStrings.NotificationsTitle), + new NotificationSection(NotificationsStrings.RunningTasks, new[] { typeof(ProgressNotification) }), } } } @@ -118,7 +129,7 @@ namespace osu.Game.Overlays private void updateProcessingMode() { - bool enabled = OverlayActivationMode.Value == OverlayActivation.All || State.Value == Visibility.Visible; + bool enabled = OverlayActivationMode.Value != OverlayActivation.Disabled || State.Value == Visibility.Visible; notificationsEnabler?.Cancel(); @@ -164,13 +175,19 @@ namespace osu.Game.Overlays Logger.Log($"⚠️ {notification.Text}"); - notification.Closed += notificationClosed; + notification.Closed += () => notificationClosed(notification); if (notification is IHasCompletionTarget hasCompletionTarget) hasCompletionTarget.CompletionTarget = Post; playDebouncedSample(notification.PopInSampleName); + if (notification.IsImportant) + { + game?.Window?.Flash(); + notification.Closed += () => game?.Window?.CancelFlash(); + } + if (State.Value == Visibility.Hidden) { notification.IsInToastTray = true; @@ -189,7 +206,8 @@ namespace osu.Game.Overlays var ourType = notification.GetType(); int depth = notification.DisplayOnTop ? -runningDepth : runningDepth; - var section = sections.Children.First(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType))); + var section = sections.Children.FirstOrDefault(s => s.AcceptedNotificationTypes?.Any(accept => accept.IsAssignableFrom(ourType)) == true) + ?? sections.First(); section.Add(notification, depth); @@ -206,8 +224,6 @@ namespace osu.Game.Overlays protected override void PopIn() { - base.PopIn(); - this.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint); mainContent.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint); mainContent.FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out); @@ -226,17 +242,20 @@ namespace osu.Game.Overlays mainContent.FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.In); } - private void notificationClosed() => Schedule(() => + private void notificationClosed(Notification notification) => Schedule(() => { updateCounts(); // this debounce is currently shared between popin/popout sounds, which means one could potentially not play when the user is expecting it. // popout is constant across all notification types, and should therefore be handled using playback concurrency instead, but seems broken at the moment. - playDebouncedSample("UI/overlay-pop-out"); + playDebouncedSample(notification.PopOutSampleName); }); private void playDebouncedSample(string sampleName) { + if (string.IsNullOrEmpty(sampleName)) + return; + if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) { audio.Samples.Get(sampleName)?.Play(); diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index 7a793ee092..0ebaff9437 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -28,6 +28,11 @@ namespace osu.Game.Overlays public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => toastFlow.ReceivePositionalInputAt(screenSpacePos); + /// + /// All notifications currently being displayed by the toast tray. + /// + public IEnumerable Notifications => toastFlow; + public bool IsDisplayingToasts => toastFlow.Count > 0; private FillFlowContainer toastFlow = null!; diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 77d3317b1f..d48524d8b0 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -50,7 +50,10 @@ namespace osu.Game.Overlays.Notifications /// public virtual bool DisplayOnTop => true; - public virtual string PopInSampleName => "UI/notification-pop-in"; + public virtual string PopInSampleName => "UI/notification-default"; + public virtual string PopOutSampleName => "UI/overlay-pop-out"; + + protected const float CORNER_RADIUS = 6; protected NotificationLight Light; @@ -58,7 +61,7 @@ namespace osu.Game.Overlays.Notifications public bool WasClosed { get; private set; } - private readonly Container content; + private readonly FillFlowContainer content; protected override Container Content => content; @@ -127,7 +130,7 @@ namespace osu.Game.Overlays.Notifications AutoSizeAxes = Axes.Y, }.WithChild(MainContent = new Container { - CornerRadius = 6, + CornerRadius = CORNER_RADIUS, Masking = true, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -165,11 +168,13 @@ namespace osu.Game.Overlays.Notifications Padding = new MarginPadding(10), Children = new Drawable[] { - content = new Container + content = new FillFlowContainer { Masking = true, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(15) }, } }, @@ -470,10 +475,9 @@ namespace osu.Game.Overlays.Notifications base.Colour = value; pulsateLayer.EdgeEffect = new EdgeEffectParameters { - Colour = ((Color4)value).Opacity(0.5f), //todo: avoid cast + Colour = ((Color4)value).Opacity(0.18f), Type = EdgeEffectType.Glow, - Radius = 12, - Roundness = 12, + Radius = 14, }; } } diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index de4c72e473..895f13ee9c 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -13,12 +13,18 @@ using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; using osuTK; namespace osu.Game.Overlays.Notifications { public partial class NotificationSection : AlwaysUpdateFillFlowContainer { + /// + /// All notifications currently being displayed in this section. + /// + public IEnumerable Notifications => notifications; + private OsuSpriteText countDrawable = null!; private FlowContainer notifications = null!; @@ -31,17 +37,18 @@ namespace osu.Game.Overlays.Notifications notifications.Insert((int)position, notification); } - public IEnumerable AcceptedNotificationTypes { get; } - - private readonly LocalisableString clearButtonText; + /// + /// Enumerable of notification types accepted in this section. + /// If , the section accepts any and all notifications. + /// + public IEnumerable? AcceptedNotificationTypes { get; } private readonly LocalisableString titleText; - public NotificationSection(LocalisableString title, IEnumerable acceptedNotificationTypes, LocalisableString clearButtonText) + public NotificationSection(LocalisableString title, IEnumerable? acceptedNotificationTypes = null) { - AcceptedNotificationTypes = acceptedNotificationTypes.ToArray(); + AcceptedNotificationTypes = acceptedNotificationTypes?.ToArray(); - this.clearButtonText = clearButtonText.ToUpper(); titleText = title; } @@ -70,7 +77,7 @@ namespace osu.Game.Overlays.Notifications { new ClearAllButton { - Text = clearButtonText, + Text = NotificationsStrings.ClearAll.ToUpper(), Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Action = clearAll @@ -110,10 +117,11 @@ namespace osu.Game.Overlays.Notifications }); } - private void clearAll() + private void clearAll() => notifications.Children.ForEach(c => { - notifications.Children.ForEach(c => c.Close(true)); - } + if (c is not ProgressNotification p || !p.Ongoing) + c.Close(true); + }); protected override void Update() { diff --git a/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs b/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs index 46972d4b5e..93286d9d36 100644 --- a/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs @@ -10,6 +10,8 @@ namespace osu.Game.Overlays.Notifications { public partial class ProgressCompletionNotification : SimpleNotification { + public override string PopInSampleName => "UI/notification-done"; + public ProgressCompletionNotification() { Icon = FontAwesome.Solid.Check; diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index e6662e2179..6ea032213e 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -4,6 +4,8 @@ using System; using System.Threading; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -25,8 +27,15 @@ namespace osu.Game.Overlays.Notifications public Func? CancelRequested { get; set; } + /// + /// Whether the operation represented by the is still ongoing. + /// + public bool Ongoing => State != ProgressNotificationState.Completed && State != ProgressNotificationState.Cancelled; + protected override bool AllowFlingDismiss => false; + public override string PopOutSampleName => State is ProgressNotificationState.Cancelled ? base.PopOutSampleName : ""; + /// /// The function to post completion notifications back to. /// @@ -122,6 +131,7 @@ namespace osu.Game.Overlays.Notifications cancellationTokenSource.Cancel(); IconContent.FadeColour(ColourInfo.GradientVertical(Color4.Gray, Color4.Gray.Lighten(0.5f)), colour_fade_duration); + cancelSample?.Play(); loadingSpinner.Hide(); var icon = new SpriteIcon @@ -190,6 +200,8 @@ namespace osu.Game.Overlays.Notifications private LoadingSpinner loadingSpinner = null!; + private Sample? cancelSample; + private readonly TextFlowContainer textDrawable; public ProgressNotification() @@ -217,7 +229,7 @@ namespace osu.Game.Overlays.Notifications } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, AudioManager audioManager) { colourQueued = colours.YellowDark; colourActive = colours.Blue; @@ -236,6 +248,8 @@ namespace osu.Game.Overlays.Notifications Size = new Vector2(loading_spinner_size), } }); + + cancelSample = audioManager.Samples.Get(@"UI/notification-cancel"); } public override void Close(bool runFlingAnimation) diff --git a/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs b/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs index 7d0d07fc1b..81e3b40ffc 100644 --- a/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs +++ b/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs @@ -1,15 +1,13 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics.Sprites; namespace osu.Game.Overlays.Notifications { public partial class SimpleErrorNotification : SimpleNotification { - public override string PopInSampleName => "UI/error-notification-pop-in"; + public override string PopInSampleName => "UI/notification-error"; public SimpleErrorNotification() { diff --git a/osu.Game/Overlays/Notifications/UserAvatarNotification.cs b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs new file mode 100644 index 0000000000..5a9241a2a1 --- /dev/null +++ b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs @@ -0,0 +1,74 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users.Drawables; + +namespace osu.Game.Overlays.Notifications +{ + public partial class UserAvatarNotification : Notification + { + private LocalisableString text; + + public override LocalisableString Text + { + get => text; + set + { + text = value; + if (textDrawable != null) + textDrawable.Text = text; + } + } + + private TextFlowContainer? textDrawable; + + private readonly APIUser user; + + public UserAvatarNotification(APIUser user, LocalisableString text) + { + this.user = user; + Text = text; + } + + protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times; + + [BackgroundDependencyLoader] + private void load(OsuColour colours, OverlayColourProvider colourProvider) + { + Light.Colour = colours.Orange2; + + Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14, weight: FontWeight.Medium)) + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Text = text + }); + + IconContent.Masking = true; + IconContent.CornerRadius = CORNER_RADIUS; + + IconContent.AddRange(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + }); + + LoadComponentAsync(new DrawableAvatar(user) + { + FillMode = FillMode.Fill, + }, IconContent.Add); + } + } +} diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index e3e3b4bd80..5bbf18a959 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.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.Threading.Tasks; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; @@ -40,33 +39,34 @@ namespace osu.Game.Overlays private const float bottom_black_area_height = 55; private const float margin = 10; - private Drawable background; - private ProgressBar progressBar; + private Drawable background = null!; + private ProgressBar progressBar = null!; - private IconButton prevButton; - private IconButton playButton; - private IconButton nextButton; - private IconButton playlistButton; + private IconButton prevButton = null!; + private IconButton playButton = null!; + private IconButton nextButton = null!; + private IconButton playlistButton = null!; - private SpriteText title, artist; + private SpriteText title = null!, artist = null!; - private PlaylistOverlay playlist; + private PlaylistOverlay? playlist; - private Container dragContainer; - private Container playerContainer; - private Container playlistContainer; + private Container dragContainer = null!; + private Container playerContainer = null!; + private Container playlistContainer = null!; - protected override string PopInSampleName => "UI/now-playing-pop-in"; - protected override string PopOutSampleName => "UI/now-playing-pop-out"; + protected override double PopInOutSampleBalance => OsuGameBase.SFX_STEREO_STRENGTH * 0.75f; [Resolved] - private MusicController musicController { get; set; } + private MusicController musicController { get; set; } = null!; [Resolved] - private Bindable beatmap { get; set; } + private Bindable beatmap { get; set; } = null!; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; + + private Bindable allowTrackControl = null!; public NowPlayingOverlay() { @@ -220,8 +220,10 @@ namespace osu.Game.Overlays { base.LoadComplete(); - beatmap.BindDisabledChanged(_ => Scheduler.AddOnce(beatmapDisabledChanged)); - beatmapDisabledChanged(); + beatmap.BindDisabledChanged(_ => Scheduler.AddOnce(updateEnabledStates)); + + allowTrackControl = musicController.AllowTrackControl.GetBoundCopy(); + allowTrackControl.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledStates), true); musicController.TrackChanged += trackChanged; trackChanged(beatmap.Value); @@ -229,8 +231,6 @@ namespace osu.Game.Overlays protected override void PopIn() { - base.PopIn(); - this.FadeIn(transition_length, Easing.OutQuint); dragContainer.ScaleTo(1, transition_length, Easing.OutElastic); } @@ -247,7 +247,7 @@ namespace osu.Game.Overlays { base.UpdateAfterChildren(); - playlistContainer.Height = MathF.Min(Parent.DrawHeight - margin * 3 - player_height, PlaylistOverlay.PLAYLIST_HEIGHT); + playlistContainer.Height = MathF.Min(Parent!.DrawHeight - margin * 3 - player_height, PlaylistOverlay.PLAYLIST_HEIGHT); float height = player_height; @@ -288,31 +288,34 @@ namespace osu.Game.Overlays } } - private Action pendingBeatmapSwitch; + private Action? pendingBeatmapSwitch; + + private CancellationTokenSource? backgroundLoadCancellation; + + private WorkingBeatmap? currentBeatmap; private void trackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction = TrackChangeDirection.None) { + currentBeatmap = beatmap; + // avoid using scheduler as our scheduler may not be run for a long time, holding references to beatmaps. pendingBeatmapSwitch = delegate { - // todo: this can likely be replaced with WorkingBeatmap.GetBeatmapAsync() - Task.Run(() => - { - if (beatmap?.Beatmap == null) // this is not needed if a placeholder exists - { - title.Text = @"Nothing to play"; - artist.Text = @"Nothing to play"; - } - else - { - BeatmapMetadata metadata = beatmap.Metadata; - title.Text = new RomanisableString(metadata.TitleUnicode, metadata.Title); - artist.Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); - } - }); + BeatmapMetadata metadata = beatmap.Metadata; + + title.Text = new RomanisableString(metadata.TitleUnicode, metadata.Title); + artist.Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); + + backgroundLoadCancellation?.Cancel(); LoadComponentAsync(new Background(beatmap) { Depth = float.MaxValue }, newBackground => { + if (beatmap != currentBeatmap) + { + newBackground.Dispose(); + return; + } + switch (direction) { case TrackChangeDirection.Next: @@ -332,27 +335,29 @@ namespace osu.Game.Overlays background = newBackground; playerContainer.Add(newBackground); - }); + }, (backgroundLoadCancellation = new CancellationTokenSource()).Token); }; } - private void beatmapDisabledChanged() + private void updateEnabledStates() { - bool disabled = beatmap.Disabled; + bool beatmapDisabled = beatmap.Disabled; + bool trackControlDisabled = !musicController.AllowTrackControl.Value; - if (disabled) + if (beatmapDisabled || trackControlDisabled) playlist?.Hide(); - prevButton.Enabled.Value = !disabled; - nextButton.Enabled.Value = !disabled; - playlistButton.Enabled.Value = !disabled; + prevButton.Enabled.Value = !beatmapDisabled && !trackControlDisabled; + nextButton.Enabled.Value = !beatmapDisabled && !trackControlDisabled; + playlistButton.Enabled.Value = !beatmapDisabled && !trackControlDisabled; + playButton.Enabled.Value = !trackControlDisabled; } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - if (musicController != null) + if (musicController.IsNotNull()) musicController.TrackChanged -= trackChanged; } @@ -385,7 +390,7 @@ namespace osu.Game.Overlays private readonly Sprite sprite; private readonly WorkingBeatmap beatmap; - public Background(WorkingBeatmap beatmap = null) + public Background(WorkingBeatmap beatmap) : base(cachedFrameBuffer: true) { this.beatmap = beatmap; @@ -415,7 +420,7 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load(LargeTextureStore textures) { - sprite.Texture = beatmap?.GetBackground() ?? textures.Get(@"Backgrounds/bg4"); + sprite.Texture = beatmap.GetBackground() ?? textures.Get(@"Backgrounds/bg4"); } } diff --git a/osu.Game/Overlays/OSD/CopyUrlToast.cs b/osu.Game/Overlays/OSD/CopyUrlToast.cs index ce5a5f56c4..2c5a9179f2 100644 --- a/osu.Game/Overlays/OSD/CopyUrlToast.cs +++ b/osu.Game/Overlays/OSD/CopyUrlToast.cs @@ -8,7 +8,7 @@ namespace osu.Game.Overlays.OSD public partial class CopyUrlToast : Toast { public CopyUrlToast() - : base(UserInterfaceStrings.GeneralHeader, ToastStrings.UrlCopied, "") + : base(CommonStrings.General, ToastStrings.UrlCopied, "") { } } diff --git a/osu.Game/Overlays/OSD/Toast.cs b/osu.Game/Overlays/OSD/Toast.cs index ff8696c04f..7df534d90d 100644 --- a/osu.Game/Overlays/OSD/Toast.cs +++ b/osu.Game/Overlays/OSD/Toast.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 osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Overlays/OnlineOverlay.cs b/osu.Game/Overlays/OnlineOverlay.cs index 8b7a82f899..051873b394 100644 --- a/osu.Game/Overlays/OnlineOverlay.cs +++ b/osu.Game/Overlays/OnlineOverlay.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 osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/OverlayColourProvider.cs b/osu.Game/Overlays/OverlayColourProvider.cs index d7581960f4..a4f6527024 100644 --- a/osu.Game/Overlays/OverlayColourProvider.cs +++ b/osu.Game/Overlays/OverlayColourProvider.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 osuTK; using osuTK.Graphics; diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs index 93de463204..3d71b7d5ae 100644 --- a/osu.Game/Overlays/OverlayHeader.cs +++ b/osu.Game/Overlays/OverlayHeader.cs @@ -1,9 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -98,10 +95,8 @@ namespace osu.Game.Overlays titleBackground.Colour = colourProvider.Dark5; } - [NotNull] protected virtual Drawable CreateContent() => Empty(); - [NotNull] protected virtual Drawable CreateBackground() => Empty(); protected abstract OverlayTitle CreateTitle(); diff --git a/osu.Game/Overlays/OverlayHeaderBackground.cs b/osu.Game/Overlays/OverlayHeaderBackground.cs index a089001385..7e264ee196 100644 --- a/osu.Game/Overlays/OverlayHeaderBackground.cs +++ b/osu.Game/Overlays/OverlayHeaderBackground.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Overlays/OverlayRulesetSelector.cs b/osu.Game/Overlays/OverlayRulesetSelector.cs index 9205a14d9f..e10bcda734 100644 --- a/osu.Game/Overlays/OverlayRulesetSelector.cs +++ b/osu.Game/Overlays/OverlayRulesetSelector.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Overlays/OverlayRulesetTabItem.cs b/osu.Game/Overlays/OverlayRulesetTabItem.cs index d5c70a46d0..6d318820b3 100644 --- a/osu.Game/Overlays/OverlayRulesetTabItem.cs +++ b/osu.Game/Overlays/OverlayRulesetTabItem.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; @@ -35,7 +33,7 @@ namespace osu.Game.Overlays protected override Container Content { get; } [Resolved] - private OverlayColourProvider colourProvider { get; set; } + private OverlayColourProvider colourProvider { get; set; } = null!; private readonly Drawable icon; diff --git a/osu.Game/Overlays/OverlaySidebar.cs b/osu.Game/Overlays/OverlaySidebar.cs index 93e5e83ffc..070f1f0c37 100644 --- a/osu.Game/Overlays/OverlaySidebar.cs +++ b/osu.Game/Overlays/OverlaySidebar.cs @@ -1,9 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -31,7 +28,7 @@ namespace osu.Game.Overlays scrollbarBackground = new Box { RelativeSizeAxes = Axes.Y, - Width = OsuScrollContainer.SCROLL_BAR_HEIGHT, + Width = OsuScrollContainer.SCROLL_BAR_WIDTH, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Alpha = 0.5f @@ -73,7 +70,6 @@ namespace osu.Game.Overlays scrollbarBackground.Colour = colourProvider.Background3; } - [NotNull] protected virtual Drawable CreateContent() => Empty(); private partial class SidebarScrollContainer : OsuScrollContainer diff --git a/osu.Game/Overlays/Profile/Header/BadgeHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BadgeHeaderContainer.cs index 24be6ce2f5..af9ec32421 100644 --- a/osu.Game/Overlays/Profile/Header/BadgeHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BadgeHeaderContainer.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Profile.Header new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background5, + Colour = colourProvider.Background4, }, new Container // artificial shadow { @@ -50,7 +50,7 @@ namespace osu.Game.Overlays.Profile.Header RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(10, 10), - Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Top = 10 }, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 10 }, } }; } diff --git a/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs index 8e6648dc4b..424ab4a529 100644 --- a/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs @@ -11,7 +11,7 @@ using osu.Game.Overlays.Profile.Header.Components; namespace osu.Game.Overlays.Profile.Header { - public partial class BannerHeaderContainer : CompositeDrawable + public partial class BannerHeaderContainer : FillFlowContainer { public readonly Bindable User = new Bindable(); @@ -19,9 +19,9 @@ namespace osu.Game.Overlays.Profile.Header private void load() { Alpha = 0; - RelativeSizeAxes = Axes.Both; - FillMode = FillMode.Fit; - FillAspectRatio = 1000 / 60f; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; } protected override void LoadComplete() @@ -40,13 +40,21 @@ namespace osu.Game.Overlays.Profile.Header ClearInternal(); - var banner = user?.TournamentBanner; + var banners = user?.TournamentBanners; - if (banner != null) + if (banners?.Length > 0) { Show(); - LoadComponentAsync(new DrawableTournamentBanner(banner), AddInternal, cancellationTokenSource.Token); + for (int index = 0; index < banners.Length; index++) + { + int displayIndex = index; + LoadComponentAsync(new DrawableTournamentBanner(banners[index]), asyncBanner => + { + // load in stable order regardless of async load order. + Insert(displayIndex, asyncBanner); + }, cancellationTokenSource.Token); + } } else { diff --git a/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs index 26d333ff95..c099009ca4 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs @@ -15,12 +15,13 @@ namespace osu.Game.Overlays.Profile.Header.Components [LongRunningLoad] public partial class DrawableTournamentBanner : OsuClickableContainer { + private const float banner_aspect_ratio = 60 / 1000f; private readonly TournamentBanner banner; public DrawableTournamentBanner(TournamentBanner banner) { this.banner = banner; - RelativeSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.X; } [BackgroundDependencyLoader] @@ -41,6 +42,12 @@ namespace osu.Game.Overlays.Profile.Header.Components this.FadeInFromZero(200); } + protected override void Update() + { + base.Update(); + Height = DrawWidth * banner_aspect_ratio; + } + public override LocalisableString TooltipText => "view in browser"; } } diff --git a/osu.Game/Overlays/Profile/Header/Components/PreviousUsernames.cs b/osu.Game/Overlays/Profile/Header/Components/PreviousUsernamesDisplay.cs similarity index 85% rename from osu.Game/Overlays/Profile/Header/Components/PreviousUsernames.cs rename to osu.Game/Overlays/Profile/Header/Components/PreviousUsernamesDisplay.cs index b722fe92e0..dce5c84d12 100644 --- a/osu.Game/Overlays/Profile/Header/Components/PreviousUsernames.cs +++ b/osu.Game/Overlays/Profile/Header/Components/PreviousUsernamesDisplay.cs @@ -18,12 +18,13 @@ using osuTK; namespace osu.Game.Overlays.Profile.Header.Components { - public partial class PreviousUsernames : CompositeDrawable + public partial class PreviousUsernamesDisplay : CompositeDrawable { private const int duration = 200; private const int margin = 10; - private const int width = 310; + private const int width = 300; private const int move_offset = 15; + private const int base_y_offset = -3; // eye balled to make it look good public readonly Bindable User = new Bindable(); @@ -31,14 +32,15 @@ namespace osu.Game.Overlays.Profile.Header.Components private readonly Box background; private readonly SpriteText header; - public PreviousUsernames() + public PreviousUsernamesDisplay() { HoverIconContainer hoverIcon; AutoSizeAxes = Axes.Y; Width = width; Masking = true; - CornerRadius = 5; + CornerRadius = 6; + Y = base_y_offset; AddRangeInternal(new Drawable[] { @@ -84,6 +86,9 @@ namespace osu.Game.Overlays.Profile.Header.Components RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Full, + // Prevents the tooltip of having a sudden size reduction and flickering when the text is being faded out. + // Also prevents a potential OnHover/HoverLost feedback loop. + AlwaysPresent = true, Margin = new MarginPadding { Bottom = margin, Top = margin / 2f } } } @@ -96,9 +101,9 @@ namespace osu.Game.Overlays.Profile.Header.Components } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OverlayColourProvider colours) { - background.Colour = colours.GreySeaFoamDarker; + background.Colour = colours.Background6; } protected override void LoadComplete() @@ -134,7 +139,7 @@ namespace osu.Game.Overlays.Profile.Header.Components text.FadeIn(duration, Easing.OutQuint); header.FadeIn(duration, Easing.OutQuint); background.FadeIn(duration, Easing.OutQuint); - this.MoveToY(-move_offset, duration, Easing.OutQuint); + this.MoveToY(base_y_offset - move_offset, duration, Easing.OutQuint); } private void hideContent() @@ -142,7 +147,7 @@ namespace osu.Game.Overlays.Profile.Header.Components text.FadeOut(duration, Easing.OutQuint); header.FadeOut(duration, Easing.OutQuint); background.FadeOut(duration, Easing.OutQuint); - this.MoveToY(0, duration, Easing.OutQuint); + this.MoveToY(base_y_offset, duration, Easing.OutQuint); } private partial class HoverIconContainer : Container @@ -156,7 +161,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { Margin = new MarginPadding { Top = 6, Left = margin, Right = margin * 2 }, Size = new Vector2(15), - Icon = FontAwesome.Solid.IdCard, + Icon = FontAwesome.Solid.AddressCard, }; } diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index de678cb5d1..36bd8a5af5 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -46,6 +46,7 @@ namespace osu.Game.Overlays.Profile.Header private OsuSpriteText userCountryText = null!; private GroupBadgeFlow groupBadgeFlow = null!; private ToggleCoverButton coverToggle = null!; + private PreviousUsernamesDisplay previousUsernamesDisplay = null!; private Bindable coverExpanded = null!; @@ -64,7 +65,7 @@ namespace osu.Game.Overlays.Profile.Header new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4, + Colour = colourProvider.Background3, }, new FillFlowContainer { @@ -143,6 +144,11 @@ namespace osu.Game.Overlays.Profile.Header Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, + new Container + { + // Intentionally use a zero-size container, else the fill flow will adjust to (and cancel) the upwards animation. + Child = previousUsernamesDisplay = new PreviousUsernamesDisplay(), + } } }, titleText = new OsuSpriteText @@ -216,6 +222,7 @@ namespace osu.Game.Overlays.Profile.Header titleText.Text = user?.Title ?? string.Empty; titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff"); groupBadgeFlow.User.Value = user; + previousUsernamesDisplay.User.Value = user; } private void updateCoverState() diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs index 529e78a7cf..c26f2f19ba 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs @@ -15,6 +15,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; using osu.Game.Scoring.Drawables; using osu.Game.Utils; @@ -48,6 +49,8 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks [BackgroundDependencyLoader] private void load(RulesetStore rulesets) { + var ruleset = rulesets.GetRuleset(Score.RulesetID)?.CreateInstance() ?? throw new InvalidOperationException($"Ruleset with ID of {Score.RulesetID} not found locally"); + AddInternal(new ProfileItemContainer { Children = new Drawable[] @@ -132,14 +135,9 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks Origin = Anchor.CentreRight, Direction = FillDirection.Horizontal, Spacing = new Vector2(2), - Children = Score.Mods.Select(mod => + Children = Score.Mods.Select(m => m.ToMod(ruleset)).AsOrdered().Select(mod => new ModIcon(mod) { - var ruleset = rulesets.GetRuleset(Score.RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {Score.RulesetID} not found locally"); - - return new ModIcon(mod.ToMod(ruleset.CreateInstance())) - { - Scale = new Vector2(0.35f) - }; + Scale = new Vector2(0.35f) }).ToList(), } } diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs index 0479ab7c16..8a0003b4ea 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs @@ -223,7 +223,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent private void addBeatmapsetLink() => content.AddLink(activity.Beatmapset.AsNonNull().Title, LinkAction.OpenBeatmapSet, getLinkArgument(activity.Beatmapset.AsNonNull().Url), creationParameters: t => t.Font = getLinkFont()); - private string getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.WebsiteRootUrl}{url}").Argument.ToString().AsNonNull(); + private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.WebsiteRootUrl}{url}").Argument.AsNonNull(); private FontUsage getLinkFont(FontWeight fontWeight = FontWeight.Regular) => OsuFont.GetFont(size: font_size, weight: fontWeight, italics: true); diff --git a/osu.Game/Overlays/Rankings/CountryFilter.cs b/osu.Game/Overlays/Rankings/CountryFilter.cs index 525816f8fd..c4e281402b 100644 --- a/osu.Game/Overlays/Rankings/CountryFilter.cs +++ b/osu.Game/Overlays/Rankings/CountryFilter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Rankings/CountryPill.cs b/osu.Game/Overlays/Rankings/CountryPill.cs index 5efa9d12f0..294b6df34d 100644 --- a/osu.Game/Overlays/Rankings/CountryPill.cs +++ b/osu.Game/Overlays/Rankings/CountryPill.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.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Overlays/Rankings/RankingsScope.cs b/osu.Game/Overlays/Rankings/RankingsScope.cs index 2644fee58b..3392db9360 100644 --- a/osu.Game/Overlays/Rankings/RankingsScope.cs +++ b/osu.Game/Overlays/Rankings/RankingsScope.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/Rankings/RankingsSortTabControl.cs b/osu.Game/Overlays/Rankings/RankingsSortTabControl.cs index 9e73c3adb0..b13ecc190e 100644 --- a/osu.Game/Overlays/Rankings/RankingsSortTabControl.cs +++ b/osu.Game/Overlays/Rankings/RankingsSortTabControl.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 osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs b/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs index 3be5cc994c..fb3e58d2ac 100644 --- a/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using System; @@ -68,8 +66,8 @@ namespace osu.Game.Overlays.Rankings.Tables private partial class CountryName : LinkFlowContainer { - [Resolved(canBeNull: true)] - private RankingsOverlay rankings { get; set; } + [Resolved] + private RankingsOverlay? rankings { get; set; } public CountryName(CountryCode countryCode) : base(t => t.Font = OsuFont.GetFont(size: 12)) diff --git a/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs b/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs index 19ed3afdca..f13fbd66ec 100644 --- a/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/PerformanceTable.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.Collections.Generic; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Rankings/Tables/ScoresTable.cs b/osu.Game/Overlays/Rankings/Tables/ScoresTable.cs index 0da3fba8cc..9005334dda 100644 --- a/osu.Game/Overlays/Rankings/Tables/ScoresTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/ScoresTable.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.Collections.Generic; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Rankings/Tables/TableRowBackground.cs b/osu.Game/Overlays/Rankings/Tables/TableRowBackground.cs index 54ec45f4ff..8c50e72207 100644 --- a/osu.Game/Overlays/Rankings/Tables/TableRowBackground.cs +++ b/osu.Game/Overlays/Rankings/Tables/TableRowBackground.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Overlays/RestoreDefaultValueButton.cs b/osu.Game/Overlays/RevertToDefaultButton.cs similarity index 56% rename from osu.Game/Overlays/RestoreDefaultValueButton.cs rename to osu.Game/Overlays/RevertToDefaultButton.cs index 97c66fdf02..582138b0b4 100644 --- a/osu.Game/Overlays/RestoreDefaultValueButton.cs +++ b/osu.Game/Overlays/RevertToDefaultButton.cs @@ -1,28 +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 osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterface; -using osuTK; using osu.Game.Localisation; +using osuTK; namespace osu.Game.Overlays { - public partial class RestoreDefaultValueButton : OsuClickableContainer, IHasCurrentValue + public partial class RevertToDefaultButton : OsuClickableContainer, IHasCurrentValue { public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; @@ -31,11 +27,20 @@ namespace osu.Game.Overlays // this is intentionally not using BindableWithCurrent, as it can use the wrong IsDefault implementation when passed a BindableNumber. // using GetBoundCopy() ensures that the received bindable is of the exact same type as the source bindable and uses the proper IsDefault implementation. - private Bindable current; + private Bindable? current; + + private SpriteIcon icon = null!; + private Circle circle = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider? colourProvider { get; set; } public Bindable Current { - get => current; + get => current.AsNonNull(); set { current?.UnbindAll(); @@ -50,43 +55,31 @@ namespace osu.Game.Overlays } } - [Resolved] - private OsuColour colours { get; set; } - - private const float size = 4; - - private CircularContainer circle = null!; - private Box background = null!; - - public RestoreDefaultValueButton() - : base(HoverSampleSet.Button) - { - } - [BackgroundDependencyLoader] - private void load(OsuColour colour) + private void load() { - // size intentionally much larger than actual drawn content, so that the button is easier to click. - Size = new Vector2(3 * size); + Size = new Vector2(14); - Add(circle = new CircularContainer + AddRange(new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(size), - Masking = true, - Child = background = new Box + circle = new Circle { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Colour = colour.Lime1 + }, + icon = new SpriteIcon + { + Icon = FontAwesome.Solid.Undo, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(8), } }); - Alpha = 0f; - Action += () => { - if (!current.Disabled) + if (current?.Disabled == false) current.SetDefault(); }; } @@ -120,28 +113,25 @@ namespace osu.Game.Overlays if (current == null) return; - Enabled.Value = !Current.Disabled; + Enabled.Value = !current.Disabled; - if (!Current.Disabled) + this.FadeTo(current.Disabled ? 0.2f : (current.IsDefault ? 0 : 1), fade_duration, Easing.OutQuint); + + if (IsHovered && Enabled.Value) { - this.FadeTo(Current.IsDefault ? 0 : 1, fade_duration, Easing.OutQuint); - background.FadeColour(IsHovered ? colours.Lime0 : colours.Lime1, fade_duration, Easing.OutQuint); - circle.TweenEdgeEffectTo(new EdgeEffectParameters - { - Colour = (IsHovered ? colours.Lime1 : colours.Lime3).Opacity(0.4f), - Radius = IsHovered ? 8 : 4, - Type = EdgeEffectType.Glow - }, fade_duration, Easing.OutQuint); + icon.RotateTo(-40, 500, Easing.OutQuint); + + icon.FadeColour(colourProvider?.Light1 ?? colours.YellowLight, 300, Easing.OutQuint); + circle.FadeColour(colourProvider?.Background2 ?? colours.Gray6, 300, Easing.OutQuint); + this.ScaleTo(1.2f, 300, Easing.OutQuint); } else { - background.FadeColour(colours.Lime3, fade_duration, Easing.OutQuint); - circle.TweenEdgeEffectTo(new EdgeEffectParameters - { - Colour = colours.Lime3.Opacity(0.1f), - Radius = 2, - Type = EdgeEffectType.Glow - }, fade_duration, Easing.OutQuint); + icon.RotateTo(0, 100, Easing.OutQuint); + + icon.FadeColour(colourProvider?.Colour0 ?? colours.Yellow, 100, Easing.OutQuint); + circle.FadeColour(colourProvider?.Background3 ?? colours.Gray3, 100, Easing.OutQuint); + this.ScaleTo(1f, 100, Easing.OutQuint); } } } diff --git a/osu.Game/Overlays/Settings/DangerousSettingsButton.cs b/osu.Game/Overlays/Settings/DangerousSettingsButton.cs index 248b4d339a..9a9d40cada 100644 --- a/osu.Game/Overlays/Settings/DangerousSettingsButton.cs +++ b/osu.Game/Overlays/Settings/DangerousSettingsButton.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 osu.Framework.Allocation; using osu.Game.Graphics; @@ -16,7 +14,7 @@ namespace osu.Game.Overlays.Settings [BackgroundDependencyLoader] private void load(OsuColour colours) { - BackgroundColour = colours.Pink3; + BackgroundColour = colours.DangerousButtonColour; } } } diff --git a/osu.Game/Overlays/Settings/ISettingsItem.cs b/osu.Game/Overlays/Settings/ISettingsItem.cs index 509fc1ab0d..b77a8d9268 100644 --- a/osu.Game/Overlays/Settings/ISettingsItem.cs +++ b/osu.Game/Overlays/Settings/ISettingsItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Settings/MultiplierSettingsSlider.cs b/osu.Game/Overlays/Settings/MultiplierSettingsSlider.cs new file mode 100644 index 0000000000..bd5b8f92b6 --- /dev/null +++ b/osu.Game/Overlays/Settings/MultiplierSettingsSlider.cs @@ -0,0 +1,24 @@ +// 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.Localisation; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays.Settings +{ + public partial class MultiplierSettingsSlider : SettingsSlider + { + public MultiplierSettingsSlider() + { + KeyboardStep = 0.01f; + } + + /// + /// A slider bar which adds a "x" to the end of the tooltip string. + /// + public partial class MultiplierRoundedSliderBar : RoundedSliderBar + { + public override LocalisableString TooltipText => $"{base.TooltipText}x"; + } + } +} diff --git a/osu.Game/Overlays/Settings/OutlinedTextBox.cs b/osu.Game/Overlays/Settings/OutlinedTextBox.cs index 56b662ecf0..ef2039f8bf 100644 --- a/osu.Game/Overlays/Settings/OutlinedTextBox.cs +++ b/osu.Game/Overlays/Settings/OutlinedTextBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Input.Events; diff --git a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs index 1755c12f94..6b5c769853 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.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.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -18,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio { protected override LocalisableString Header => AudioSettingsStrings.OffsetHeader; - public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "universal", "uo", "timing" }); + public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "universal", "uo", "timing", "delay", "latency" }); [BackgroundDependencyLoader] private void load(OsuConfigManager config) diff --git a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs index 7066be4f92..2bb5fa983f 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.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 osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Settings/Sections/AudioSection.cs b/osu.Game/Overlays/Settings/Sections/AudioSection.cs index 542d5bc8fd..fb3d486776 100644 --- a/osu.Game/Overlays/Settings/Sections/AudioSection.cs +++ b/osu.Game/Overlays/Settings/Sections/AudioSection.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.Collections.Generic; using System.Linq; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/DebugSection.cs b/osu.Game/Overlays/Settings/Sections/DebugSection.cs index 509410fbb1..33a6f4c673 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSection.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSection.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 osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs index 6c2bfedba0..049ccedf37 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.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 osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; @@ -17,10 +15,10 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings { public partial class GeneralSettings : SettingsSubsection { - protected override LocalisableString Header => DebugSettingsStrings.GeneralHeader; + protected override LocalisableString Header => CommonStrings.General; - [BackgroundDependencyLoader(true)] - private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig, IPerformFromScreenRunner performer) + [BackgroundDependencyLoader] + private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig, IPerformFromScreenRunner? performer) { Children = new Drawable[] { diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs index bf0a48d2c2..d5de7ae2db 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.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; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Framework.Logging; @@ -58,7 +57,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings { try { - var token = realm.BlockAllOperations("maintenance"); + IDisposable? token = realm.BlockAllOperations("maintenance"); blockAction.Enabled.Value = false; @@ -75,10 +74,10 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings void unblock() { - if (token == null) + if (token.IsNull()) return; - token?.Dispose(); + token.Dispose(); token = null; Scheduler.Add(() => diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs index 00eb6fa62c..467c988020 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs index 09e5f3e163..048351b4cb 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs index da5fc519e6..69566d85f4 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -32,7 +30,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay }, new SettingsCheckbox { - Keywords = new[] { "combo", "override" }, + Keywords = new[] { "combo", "override", "color" }, LabelText = SkinSettingsStrings.BeatmapColours, Current = config.GetBindable(OsuSetting.BeatmapColours) }, @@ -49,6 +47,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay }, new SettingsSlider { + Keywords = new[] { "color" }, LabelText = GraphicsSettingsStrings.ComboColourNormalisation, Current = comboColourNormalisation, DisplayAsPercentage = true, diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 96d458a942..83e9140b33 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -14,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { public partial class GeneralSettings : SettingsSubsection { - protected override LocalisableString Header => GameplaySettingsStrings.GeneralHeader; + protected override LocalisableString Header => CommonStrings.General; [BackgroundDependencyLoader] private void load(OsuConfigManager config) diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs index c67c14bb43..3e67b2f103 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -27,10 +25,9 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay }, new SettingsCheckbox { - ClassicDefault = false, - LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail, - Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail), - Keywords = new[] { "hp", "bar" } + LabelText = GameplaySettingsStrings.ShowReplaySettingsOverlay, + Current = config.GetBindable(OsuSetting.ReplaySettingsOverlay), + Keywords = new[] { "hide" }, }, new SettingsCheckbox { @@ -43,6 +40,13 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay LabelText = GameplaySettingsStrings.AlwaysShowGameplayLeaderboard, Current = config.GetBindable(OsuSetting.GameplayLeaderboard), }, + new SettingsCheckbox + { + ClassicDefault = false, + LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail, + Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail), + Keywords = new[] { "hp", "bar" } + }, }; } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs index 9291dfe923..c245a1a9ea 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs index f6b3c12487..79a971510f 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.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.Collections.Generic; using System.Linq; using osu.Framework.Allocation; diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index ae6145752b..b60689b611 100644 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.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 osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs index f4a79d65e6..2b043d40bc 100644 --- a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Settings.Sections [Resolved(CanBeNull = true)] private OsuGame? game { get; set; } - public override LocalisableString Header => GeneralSettingsStrings.GeneralSectionHeader; + public override LocalisableString Header => CommonStrings.General; public override Drawable CreateIcon() => new SpriteIcon { diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs index a1f728ca87..d4cef3f4d1 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs @@ -67,10 +67,17 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics if (r.NewValue == RendererType.Automatic && automaticRendererInUse) return; - dialogOverlay?.Push(new ConfirmDialog(GraphicsSettingsStrings.ChangeRendererConfirmation, () => game?.AttemptExit(), () => + if (game?.RestartAppWhenExited() == true) { - renderer.Value = automaticRendererInUse ? RendererType.Automatic : host.ResolvedRenderer; - })); + game.AttemptExit(); + } + else + { + dialogOverlay?.Push(new ConfirmDialog(GraphicsSettingsStrings.ChangeRendererConfirmation, () => game?.AttemptExit(), () => + { + renderer.Value = automaticRendererInUse ? RendererType.Automatic : host.ResolvedRenderer; + })); + } }); // TODO: remove this once we support SDL+android. diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs index 8054b27de5..c7180ec51b 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs index 323cdaf14d..98f6908512 100644 --- a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GraphicsSection.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 osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs index 2b478f6af3..a93e6c37af 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.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.Collections.Generic; using System.Linq; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs b/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs index 291e9a93cf..5a05d78905 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs @@ -1,6 +1,7 @@ // 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.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; @@ -18,92 +19,19 @@ namespace osu.Game.Overlays.Settings.Sections.Input public override LocalisableString Header => InputSettingsStrings.GlobalKeyBindingHeader; - public GlobalKeyBindingsSection(GlobalActionContainer manager) + [BackgroundDependencyLoader] + private void load() { - Add(new DefaultBindingsSubsection(manager)); - Add(new OverlayBindingsSubsection(manager)); - Add(new AudioControlKeyBindingsSubsection(manager)); - Add(new SongSelectKeyBindingSubsection(manager)); - Add(new InGameKeyBindingsSubsection(manager)); - Add(new ReplayKeyBindingsSubsection(manager)); - Add(new EditorKeyBindingsSubsection(manager)); - } - - private partial class DefaultBindingsSubsection : KeyBindingsSubsection - { - protected override LocalisableString Header => string.Empty; - - public DefaultBindingsSubsection(GlobalActionContainer manager) - : base(null) + AddRange(new[] { - Defaults = manager.GlobalKeyBindings; - } - } - - private partial class OverlayBindingsSubsection : KeyBindingsSubsection - { - protected override LocalisableString Header => InputSettingsStrings.OverlaysSection; - - public OverlayBindingsSubsection(GlobalActionContainer manager) - : base(null) - { - Defaults = manager.OverlayKeyBindings; - } - } - - private partial class SongSelectKeyBindingSubsection : KeyBindingsSubsection - { - protected override LocalisableString Header => InputSettingsStrings.SongSelectSection; - - public SongSelectKeyBindingSubsection(GlobalActionContainer manager) - : base(null) - { - Defaults = manager.SongSelectKeyBindings; - } - } - - private partial class InGameKeyBindingsSubsection : KeyBindingsSubsection - { - protected override LocalisableString Header => InputSettingsStrings.InGameSection; - - public InGameKeyBindingsSubsection(GlobalActionContainer manager) - : base(null) - { - Defaults = manager.InGameKeyBindings; - } - } - - private partial class ReplayKeyBindingsSubsection : KeyBindingsSubsection - { - protected override LocalisableString Header => InputSettingsStrings.ReplaySection; - - public ReplayKeyBindingsSubsection(GlobalActionContainer manager) - : base(null) - { - Defaults = manager.ReplayKeyBindings; - } - } - - private partial class AudioControlKeyBindingsSubsection : KeyBindingsSubsection - { - protected override LocalisableString Header => InputSettingsStrings.AudioSection; - - public AudioControlKeyBindingsSubsection(GlobalActionContainer manager) - : base(null) - { - Defaults = manager.AudioControlKeyBindings; - } - } - - private partial class EditorKeyBindingsSubsection : KeyBindingsSubsection - { - protected override LocalisableString Header => InputSettingsStrings.EditorSection; - - public EditorKeyBindingsSubsection(GlobalActionContainer manager) - : base(null) - { - Defaults = manager.EditorKeyBindings; - } + new GlobalKeyBindingsSubsection(string.Empty, GlobalActionCategory.General), + new GlobalKeyBindingsSubsection(InputSettingsStrings.OverlaysSection, GlobalActionCategory.Overlays), + new GlobalKeyBindingsSubsection(InputSettingsStrings.AudioSection, GlobalActionCategory.AudioControl), + new GlobalKeyBindingsSubsection(InputSettingsStrings.SongSelectSection, GlobalActionCategory.SongSelect), + new GlobalKeyBindingsSubsection(InputSettingsStrings.InGameSection, GlobalActionCategory.InGame), + new GlobalKeyBindingsSubsection(InputSettingsStrings.ReplaySection, GlobalActionCategory.Replay), + new GlobalKeyBindingsSubsection(InputSettingsStrings.EditorSection, GlobalActionCategory.Editor), + }); } } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSubsection.cs new file mode 100644 index 0000000000..2e42e46330 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSubsection.cs @@ -0,0 +1,36 @@ +// 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.Localisation; +using osu.Game.Database; +using osu.Game.Input.Bindings; +using Realms; + +namespace osu.Game.Overlays.Settings.Sections.Input +{ + public partial class GlobalKeyBindingsSubsection : KeyBindingsSubsection + { + protected override LocalisableString Header { get; } + + private readonly GlobalActionCategory category; + + public GlobalKeyBindingsSubsection(LocalisableString header, GlobalActionCategory category) + { + Header = header; + this.category = category; + Defaults = GlobalActionContainer.GetDefaultBindingsFor(category); + } + + protected override IEnumerable GetKeyBindings(Realm realm) + { + var bindings = realm.All() + .Where(b => b.RulesetName == null && b.Variant == null) + .Detach(); + + var actionsInSection = GlobalActionContainer.GetGlobalActionsFor(category).Cast().ToHashSet(); + return bindings.Where(kb => actionsInSection.Contains(kb.ActionInt)); + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs new file mode 100644 index 0000000000..60d1bd31be --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs @@ -0,0 +1,301 @@ +// 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.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Input.Bindings; +using osu.Game.Localisation; +using osuTK; + +namespace osu.Game.Overlays.Settings.Sections.Input +{ + public partial class KeyBindingConflictPopover : OsuPopover + { + public Action? BindingConflictResolved { get; init; } + + private ConflictingKeyBindingPreview newPreview = null!; + private ConflictingKeyBindingPreview existingPreview = null!; + private HoverableRoundedButton keepExistingButton = null!; + private HoverableRoundedButton applyNewButton = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + private readonly KeyBindingRow.KeyBindingConflictInfo conflictInfo; + + protected override string PopInSampleName => @"UI/generic-error"; + + public KeyBindingConflictPopover(KeyBindingRow.KeyBindingConflictInfo conflictInfo) + { + this.conflictInfo = conflictInfo; + } + + [BackgroundDependencyLoader] + private void load() + { + Child = new FillFlowContainer + { + Width = 250, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + Children = new Drawable[] + { + new OsuTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = InputSettingsStrings.KeyBindingConflictDetected, + Margin = new MarginPadding { Bottom = 10 } + }, + existingPreview = new ConflictingKeyBindingPreview( + conflictInfo.Existing.Action, + conflictInfo.Existing.CombinationWhenChosen, + conflictInfo.Existing.CombinationWhenNotChosen), + newPreview = new ConflictingKeyBindingPreview( + conflictInfo.New.Action, + conflictInfo.New.CombinationWhenChosen, + conflictInfo.New.CombinationWhenNotChosen), + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 10 }, + Children = new[] + { + keepExistingButton = new HoverableRoundedButton + { + Text = InputSettingsStrings.KeepExistingBinding, + RelativeSizeAxes = Axes.X, + Width = 0.48f, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Action = Hide + }, + applyNewButton = new HoverableRoundedButton + { + Text = InputSettingsStrings.ApplyNewBinding, + BackgroundColour = colours.DangerousButtonColour, + RelativeSizeAxes = Axes.X, + Width = 0.48f, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Action = applyNew + } + } + } + } + }; + } + + private void applyNew() + { + // only "apply new" needs to cause actual realm changes, since the flow in `KeyBindingsSubsection` does not actually make db changes + // if it detects a binding conflict. + // the temporary visual changes will be reverted by calling `Hide()` / `BindingConflictResolved`. + realm.Write(r => + { + var existingBinding = r.Find(conflictInfo.Existing.ID); + existingBinding!.KeyCombinationString = conflictInfo.Existing.CombinationWhenNotChosen.ToString(); + + var newBinding = r.Find(conflictInfo.New.ID); + newBinding!.KeyCombinationString = conflictInfo.Existing.CombinationWhenChosen.ToString(); + }); + + Hide(); + } + + protected override void PopOut() + { + base.PopOut(); + + // workaround for `VisibilityContainer.PopOut()` being called in `LoadAsyncComplete()` + if (IsLoaded) + BindingConflictResolved?.Invoke(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + keepExistingButton.IsHoveredBindable.BindValueChanged(_ => updatePreviews()); + applyNewButton.IsHoveredBindable.BindValueChanged(_ => updatePreviews()); + updatePreviews(); + } + + private void updatePreviews() + { + if (!keepExistingButton.IsHovered && !applyNewButton.IsHovered) + { + existingPreview.IsChosen.Value = newPreview.IsChosen.Value = null; + return; + } + + existingPreview.IsChosen.Value = keepExistingButton.IsHovered; + newPreview.IsChosen.Value = applyNewButton.IsHovered; + } + + private partial class ConflictingKeyBindingPreview : CompositeDrawable + { + private readonly object action; + private readonly KeyCombination combinationWhenChosen; + private readonly KeyCombination combinationWhenNotChosen; + + private OsuSpriteText newBindingText = null!; + + public Bindable IsChosen { get; } = new Bindable(); + + [Resolved] + private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public ConflictingKeyBindingPreview(object action, KeyCombination combinationWhenChosen, KeyCombination combinationWhenNotChosen) + { + this.action = action; + this.combinationWhenChosen = combinationWhenChosen; + this.combinationWhenNotChosen = combinationWhenNotChosen; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + CornerRadius = 5, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5 + }, + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + new OsuSpriteText + { + Text = action.GetLocalisableDescription(), + Margin = new MarginPadding(7.5f), + }, + new Container + { + AutoSizeAxes = Axes.Both, + CornerRadius = 5, + Masking = true, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + X = -5, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6 + }, + Empty().With(d => d.Width = 80), // poor man's min-width + newBindingText = new OsuSpriteText + { + Font = OsuFont.Numeric.With(size: 10), + Margin = new MarginPadding(5), + Anchor = Anchor.Centre, + Origin = Anchor.Centre + } + } + }, + } + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + IsChosen.BindValueChanged(_ => updateState(), true); + } + + private void updateState() + { + LocalisableString keyCombinationText; + + switch (IsChosen.Value) + { + case true: + keyCombinationText = keyCombinationProvider.GetReadableString(combinationWhenChosen); + newBindingText.Colour = colours.Green1; + break; + + case false: + keyCombinationText = keyCombinationProvider.GetReadableString(combinationWhenNotChosen); + newBindingText.Colour = colours.Red1; + break; + + case null: + keyCombinationText = keyCombinationProvider.GetReadableString(combinationWhenChosen); + newBindingText.Colour = Colour4.White; + break; + } + + if (LocalisableString.IsNullOrEmpty(keyCombinationText)) + keyCombinationText = InputSettingsStrings.ActionHasNoKeyBinding; + + newBindingText.Text = keyCombinationText; + } + } + + private partial class HoverableRoundedButton : RoundedButton + { + public BindableBool IsHoveredBindable { get; set; } = new BindableBool(); + + protected override bool OnHover(HoverEvent e) + { + IsHoveredBindable.Value = IsHovered; + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + IsHoveredBindable.Value = IsHovered; + base.OnHoverLost(e); + } + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingPanel.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingPanel.cs index 30429c84f0..4c5610a15e 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingPanel.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingPanel.cs @@ -1,11 +1,8 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Rulesets; @@ -16,9 +13,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input protected override Drawable CreateHeader() => new SettingsHeader(InputSettingsStrings.KeyBindingPanelHeader, InputSettingsStrings.KeyBindingPanelDescription); [BackgroundDependencyLoader(permitNulls: true)] - private void load(RulesetStore rulesets, GlobalActionContainer global) + private void load(RulesetStore rulesets) { - AddSection(new GlobalKeyBindingsSection(global)); + AddSection(new GlobalKeyBindingsSection()); foreach (var ruleset in rulesets.AvailableRulesets) AddSection(new RulesetBindingsSection(ruleset)); diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index b04e514ec2..c85fe4727a 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -1,12 +1,13 @@ // 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.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; @@ -18,16 +19,15 @@ using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Framework.Utils; using osu.Game.Database; -using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; using osuTK; -using osuTK.Graphics; using osuTK.Input; namespace osu.Game.Overlays.Settings.Sections.Input @@ -37,16 +37,28 @@ namespace osu.Game.Overlays.Settings.Sections.Input /// /// Invoked when the binding of this row is updated with a change being written. /// - public Action BindingUpdated { get; set; } + public KeyBindingUpdated? BindingUpdated { get; set; } - private readonly object action; - private readonly IEnumerable bindings; + public delegate void KeyBindingUpdated(KeyBindingRow sender, KeyBindingUpdatedEventArgs args); - private const float transition_time = 150; + public Func> GetAllSectionBindings { get; set; } = null!; - private const float height = 20; + /// + /// Whether left and right mouse button clicks should be included in the edited bindings. + /// + public bool AllowMainMouseButtons { get; init; } - private const float padding = 5; + /// + /// The bindings to display in this row. + /// + public BindableList KeyBindings { get; } = new BindableList(); + + /// + /// The default key bindings for this row. + /// + public IEnumerable Defaults { get; init; } = Array.Empty(); + + #region IFilterable private bool matchingFilter; @@ -60,38 +72,58 @@ namespace osu.Game.Overlays.Settings.Sections.Input } } - private Container content; + public bool FilteringActive { get; set; } + + public IEnumerable FilterTerms => KeyBindings.Select(b => (LocalisableString)keyCombinationProvider.GetReadableString(b.KeyCombination)).Prepend(text.Text); + + #endregion + + public readonly object Action; + + private Bindable isDefault { get; } = new BindableBool(true); + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + [Resolved] + private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!; + + private Container content = null!; + + private OsuSpriteText text = null!; + private FillFlowContainer cancelAndClearButtons = null!; + private FillFlowContainer buttons = null!; + + private KeyButton? bindTarget; + + private Sample?[]? keypressSamples; + + private const float transition_time = 150; + private const float height = 20; + private const float padding = 5; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => content.ReceivePositionalInputAt(screenSpacePos); - public bool FilteringActive { get; set; } + public override bool AcceptsFocus => bindTarget == null; - [Resolved] - private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } - - private OsuSpriteText text; - private FillFlowContainer cancelAndClearButtons; - private FillFlowContainer buttons; - - private Bindable isDefault { get; } = new BindableBool(true); - - public IEnumerable FilterTerms => bindings.Select(b => (LocalisableString)keyCombinationProvider.GetReadableString(b.KeyCombination)).Prepend(text.Text); - - public KeyBindingRow(object action, List bindings) + /// + /// Creates a new . + /// + /// The action that this row contains bindings for. + public KeyBindingRow(object action) { - this.action = action; - this.bindings = bindings; + Action = action; RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; } - [Resolved] - private RealmAccess realm { get; set; } - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider, AudioManager audioManager) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -103,7 +135,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { RelativeSizeAxes = Axes.Y, Width = SettingsPanel.CONTENT_MARGINS, - Child = new RestoreDefaultValueButton + Child = new RevertToDefaultButton { Current = isDefault, Action = RestoreDefaults, @@ -140,7 +172,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input }, text = new OsuSpriteText { - Text = action.GetLocalisableDescription(), + Text = Action.GetLocalisableDescription(), Margin = new MarginPadding(1.5f * padding), }, buttons = new FillFlowContainer @@ -170,10 +202,15 @@ namespace osu.Game.Overlays.Settings.Sections.Input } }; - foreach (var b in bindings) - buttons.Add(new KeyButton(b)); + KeyBindings.BindCollectionChanged((_, _) => + { + Scheduler.AddOnce(updateButtons); + updateIsDefaultValue(); + }, true); - updateIsDefaultValue(); + keypressSamples = new Sample[4]; + for (int i = 0; i < keypressSamples.Length; i++) + keypressSamples[i] = audioManager.Samples.Get($@"Keyboard/key-press-{1 + i}"); } public void RestoreDefaults() @@ -185,7 +222,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input var button = buttons[i++]; button.UpdateKeyCombination(d); - updateStoreFromButton(button); + tryPersistKeyBinding(button.KeyBinding.Value, advanceToNextBinding: false); } isDefault.Value = true; @@ -205,21 +242,16 @@ namespace osu.Game.Overlays.Settings.Sections.Input base.OnHoverLost(e); } - public override bool AcceptsFocus => bindTarget == null; - - private KeyButton bindTarget; - - public bool AllowMainMouseButtons; - - public IEnumerable Defaults; - - private bool isModifier(Key k) => k < Key.F1; - protected override bool OnClick(ClickEvent e) => true; protected override bool OnMouseDown(MouseDownEvent e) { - if (!HasFocus || !bindTarget.IsHovered) + if (!HasFocus) + return base.OnMouseDown(e); + + Debug.Assert(bindTarget != null); + + if (!bindTarget.IsHovered) return base.OnMouseDown(e); if (!AllowMainMouseButtons) @@ -245,6 +277,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input return; } + Debug.Assert(bindTarget != null); + if (bindTarget.IsHovered) finalise(false); // prevent updating bind target before clear button's action @@ -256,6 +290,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input { if (HasFocus) { + Debug.Assert(bindTarget != null); + if (bindTarget.IsHovered) { bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState, e.ScrollDelta), KeyCombination.FromScrollDelta(e.ScrollDelta).First()); @@ -272,10 +308,16 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (!HasFocus || e.Repeat) return false; + Debug.Assert(bindTarget != null); + + keypressSamples?[RNG.Next(0, keypressSamples.Length)]?.Play(); + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromKey(e.Key)); if (!isModifier(e.Key)) finalise(); return true; + + bool isModifier(Key k) => k < Key.F1; } protected override void OnKeyUp(KeyUpEvent e) @@ -294,6 +336,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (!HasFocus) return false; + Debug.Assert(bindTarget != null); + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromJoystickButton(e.Button)); finalise(); @@ -316,6 +360,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (!HasFocus) return false; + Debug.Assert(bindTarget != null); + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromMidiKey(e.Key)); finalise(); @@ -338,6 +384,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (!HasFocus) return false; + Debug.Assert(bindTarget != null); + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromTabletAuxiliaryButton(e.Button)); finalise(); @@ -360,6 +408,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (!HasFocus) return false; + Debug.Assert(bindTarget != null); + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromTabletPenButton(e.Button)); finalise(); @@ -377,6 +427,18 @@ namespace osu.Game.Overlays.Settings.Sections.Input finalise(); } + private void updateButtons() + { + if (buttons.Count > KeyBindings.Count) + buttons.RemoveRange(buttons.Skip(KeyBindings.Count).ToArray(), true); + + while (buttons.Count < KeyBindings.Count) + buttons.Add(new KeyButton()); + + foreach (var (button, binding) in buttons.Zip(KeyBindings)) + button.KeyBinding.Value = binding; + } + private void clear() { if (bindTarget == null) @@ -386,21 +448,19 @@ namespace osu.Game.Overlays.Settings.Sections.Input finalise(false); } - private void finalise(bool hasChanged = true) + private void finalise(bool advanceToNextBinding = true) { if (bindTarget != null) { - updateStoreFromButton(bindTarget); - updateIsDefaultValue(); bindTarget.IsBinding = false; + var bindingToPersist = bindTarget.KeyBinding.Value; Schedule(() => { // schedule to ensure we don't instantly get focus back on next OnMouseClick (see AcceptFocus impl.) bindTarget = null; - if (hasChanged) - BindingUpdated?.Invoke(this); + tryPersistKeyBinding(bindingToPersist, advanceToNextBinding); }); } @@ -429,6 +489,28 @@ namespace osu.Game.Overlays.Settings.Sections.Input base.OnFocusLost(e); } + private void tryPersistKeyBinding(RealmKeyBinding keyBinding, bool advanceToNextBinding) + { + List bindings = GetAllSectionBindings(); + RealmKeyBinding? existingBinding = keyBinding.KeyCombination.Equals(new KeyCombination(InputKey.None)) + ? null + : bindings.FirstOrDefault(other => other.ID != keyBinding.ID && other.KeyCombination.Equals(keyBinding.KeyCombination)); + + if (existingBinding == null) + { + realm.WriteAsync(r => r.Find(keyBinding.ID)!.KeyCombinationString = keyBinding.KeyCombination.ToString()); + BindingUpdated?.Invoke(this, new KeyBindingUpdatedEventArgs(bindingConflictResolved: false, advanceToNextBinding)); + return; + } + + var keyBindingBeforeUpdate = bindings.Single(other => other.ID == keyBinding.ID); + + showBindingConflictPopover( + new KeyBindingConflictInfo( + new ConflictingKeyBinding(existingBinding.ID, existingBinding.GetAction(rulesets), existingBinding.KeyCombination, new KeyCombination(InputKey.None)), + new ConflictingKeyBinding(keyBindingBeforeUpdate.ID, Action, keyBinding.KeyCombination, keyBindingBeforeUpdate.KeyCombination))); + } + /// /// Updates the bind target to the currently hovered key button or the first if clicked anywhere else. /// @@ -439,12 +521,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (bindTarget != null) bindTarget.IsBinding = true; } - private void updateStoreFromButton(KeyButton button) => - realm.WriteAsync(r => r.Find(button.KeyBinding.ID).KeyCombinationString = button.KeyBinding.KeyCombinationString); - private void updateIsDefaultValue() { - isDefault.Value = bindings.Select(b => b.KeyCombination).SequenceEqual(Defaults); + isDefault.Value = KeyBindings.Select(b => b.KeyCombination).SequenceEqual(Defaults); } private partial class CancelButton : RoundedButton @@ -464,144 +543,5 @@ namespace osu.Game.Overlays.Settings.Sections.Input Size = new Vector2(80, 20); } } - - public partial class KeyButton : Container - { - public readonly RealmKeyBinding KeyBinding; - - private readonly Box box; - public readonly OsuSpriteText Text; - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } - - [Resolved] - private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } - - private bool isBinding; - - public bool IsBinding - { - get => isBinding; - set - { - if (value == isBinding) return; - - isBinding = value; - - updateHoverState(); - } - } - - public KeyButton(RealmKeyBinding keyBinding) - { - if (keyBinding.IsManaged) - throw new ArgumentException("Key binding should not be attached as we make temporary changes", nameof(keyBinding)); - - KeyBinding = keyBinding; - - Margin = new MarginPadding(padding); - - Masking = true; - CornerRadius = padding; - - Height = height; - AutoSizeAxes = Axes.X; - - Children = new Drawable[] - { - new Container - { - AlwaysPresent = true, - Width = 80, - Height = height, - }, - box = new Box - { - RelativeSizeAxes = Axes.Both, - }, - Text = new OsuSpriteText - { - Font = OsuFont.Numeric.With(size: 10), - Margin = new MarginPadding(5), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new HoverSounds() - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - keyCombinationProvider.KeymapChanged += updateKeyCombinationText; - updateKeyCombinationText(); - } - - [BackgroundDependencyLoader] - private void load() - { - updateHoverState(); - } - - protected override bool OnHover(HoverEvent e) - { - updateHoverState(); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateHoverState(); - base.OnHoverLost(e); - } - - private void updateHoverState() - { - if (isBinding) - { - box.FadeColour(colourProvider.Light2, transition_time, Easing.OutQuint); - Text.FadeColour(Color4.Black, transition_time, Easing.OutQuint); - } - else - { - box.FadeColour(IsHovered ? colourProvider.Light4 : colourProvider.Background6, transition_time, Easing.OutQuint); - Text.FadeColour(IsHovered ? Color4.Black : Color4.White, transition_time, Easing.OutQuint); - } - } - - /// - /// Update from a key combination, only allowing a single non-modifier key to be specified. - /// - /// A generated from the full input state. - /// The key which triggered this update, and should be used as the binding. - public void UpdateKeyCombination(KeyCombination fullState, InputKey triggerKey) => - UpdateKeyCombination(new KeyCombination(fullState.Keys.Where(KeyCombination.IsModifierKey).Append(triggerKey))); - - public void UpdateKeyCombination(KeyCombination newCombination) - { - if (KeyBinding.RulesetName != null && !RealmKeyBindingStore.CheckValidForGameplay(newCombination)) - return; - - KeyBinding.KeyCombination = newCombination; - updateKeyCombinationText(); - } - - private void updateKeyCombinationText() - { - Scheduler.AddOnce(updateText); - - void updateText() => Text.Text = keyCombinationProvider.GetReadableString(KeyBinding.KeyCombination); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (keyCombinationProvider != null) - keyCombinationProvider.KeymapChanged -= updateKeyCombinationText; - } - } } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_ConflictResolution.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_ConflictResolution.cs new file mode 100644 index 0000000000..b5d482b6b4 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_ConflictResolution.cs @@ -0,0 +1,78 @@ +// 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 osu.Framework.Extensions; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; + +namespace osu.Game.Overlays.Settings.Sections.Input +{ + public partial class KeyBindingRow : IHasPopover + { + private KeyBindingConflictInfo? pendingKeyBindingConflict; + + public Popover GetPopover() + { + Debug.Assert(pendingKeyBindingConflict != null); + return new KeyBindingConflictPopover(pendingKeyBindingConflict) + { + BindingConflictResolved = () => BindingUpdated?.Invoke(this, new KeyBindingUpdatedEventArgs(bindingConflictResolved: true, canAdvanceToNextBinding: false)) + }; + } + + private void showBindingConflictPopover(KeyBindingConflictInfo conflictInfo) + { + pendingKeyBindingConflict = conflictInfo; + this.ShowPopover(); + } + + /// + /// Contains information about the key binding conflict to be resolved. + /// + public class KeyBindingConflictInfo + { + public ConflictingKeyBinding Existing { get; } + public ConflictingKeyBinding New { get; } + + /// + /// Contains information about the key binding conflict to be resolved. + /// + public KeyBindingConflictInfo(ConflictingKeyBinding existingBinding, ConflictingKeyBinding newBinding) + { + Existing = existingBinding; + New = newBinding; + } + } + + public class ConflictingKeyBinding + { + public Guid ID { get; } + public object Action { get; } + public KeyCombination CombinationWhenChosen { get; } + public KeyCombination CombinationWhenNotChosen { get; } + + public ConflictingKeyBinding(Guid id, object action, KeyCombination combinationWhenChosen, KeyCombination combinationWhenNotChosen) + { + ID = id; + Action = action; + CombinationWhenChosen = combinationWhenChosen; + CombinationWhenNotChosen = combinationWhenNotChosen; + } + } + + public class KeyBindingUpdatedEventArgs + { + public bool BindingConflictResolved { get; } + public bool CanAdvanceToNextBinding { get; } + + public KeyBindingUpdatedEventArgs(bool bindingConflictResolved, bool canAdvanceToNextBinding) + { + BindingConflictResolved = bindingConflictResolved; + CanAdvanceToNextBinding = canAdvanceToNextBinding; + } + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_KeyButton.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_KeyButton.cs new file mode 100644 index 0000000000..53d0f50605 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_KeyButton.cs @@ -0,0 +1,183 @@ +// 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.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input; +using osu.Game.Input.Bindings; +using osu.Game.Localisation; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Settings.Sections.Input +{ + public partial class KeyBindingRow + { + public partial class KeyButton : Container + { + public Bindable KeyBinding { get; } = new Bindable(); + + private readonly Box box; + public readonly OsuSpriteText Text; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!; + + private bool isBinding; + + public bool IsBinding + { + get => isBinding; + set + { + if (value == isBinding) return; + + isBinding = value; + + updateHoverState(); + } + } + + public KeyButton() + { + Margin = new MarginPadding(padding); + + Masking = true; + CornerRadius = padding; + + Height = height; + AutoSizeAxes = Axes.X; + + Children = new Drawable[] + { + new Container + { + AlwaysPresent = true, + Width = 80, + Height = height, + }, + box = new Box + { + RelativeSizeAxes = Axes.Both, + }, + Text = new OsuSpriteText + { + Font = OsuFont.Numeric.With(size: 10), + Margin = new MarginPadding(5), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new HoverSounds() + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + KeyBinding.BindValueChanged(_ => + { + if (KeyBinding.Value.IsManaged) + throw new ArgumentException("Key binding should not be attached as we make temporary changes", nameof(KeyBinding)); + + updateKeyCombinationText(); + }); + keyCombinationProvider.KeymapChanged += updateKeyCombinationText; + updateKeyCombinationText(); + } + + [BackgroundDependencyLoader] + private void load() + { + updateHoverState(); + FinishTransforms(true); + } + + protected override bool OnHover(HoverEvent e) + { + updateHoverState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateHoverState(); + base.OnHoverLost(e); + } + + private void updateHoverState() + { + if (isBinding) + { + box.FadeColour(colourProvider.Light2, transition_time, Easing.OutQuint); + Text.FadeColour(Color4.Black, transition_time, Easing.OutQuint); + } + else + { + box.FadeColour(IsHovered ? colourProvider.Light4 : colourProvider.Background6, transition_time, Easing.OutQuint); + Text.FadeColour(IsHovered ? Color4.Black : Color4.White, transition_time, Easing.OutQuint); + } + } + + /// + /// Update from a key combination, only allowing a single non-modifier key to be specified. + /// + /// A generated from the full input state. + /// The key which triggered this update, and should be used as the binding. + public void UpdateKeyCombination(KeyCombination fullState, InputKey triggerKey) => + UpdateKeyCombination(new KeyCombination(fullState.Keys.Where(KeyCombination.IsModifierKey).Append(triggerKey))); + + public void UpdateKeyCombination(KeyCombination newCombination) + { + if (KeyBinding.Value.RulesetName != null && !RealmKeyBindingStore.CheckValidForGameplay(newCombination)) + return; + + KeyBinding.Value.KeyCombination = newCombination; + updateKeyCombinationText(); + } + + private void updateKeyCombinationText() + { + Scheduler.AddOnce(updateText); + + void updateText() + { + LocalisableString keyCombinationString = keyCombinationProvider.GetReadableString(KeyBinding.Value.KeyCombination); + float alpha = 1; + + if (LocalisableString.IsNullOrEmpty(keyCombinationString)) + { + keyCombinationString = InputSettingsStrings.ActionHasNoKeyBinding; + alpha = 0.4f; + } + + Text.Text = keyCombinationString; + Text.Alpha = alpha; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (keyCombinationProvider.IsNotNull()) + keyCombinationProvider.KeymapChanged -= updateKeyCombinationText; + } + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index d6d4abfa92..dd0a88bfb1 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -1,19 +1,18 @@ // 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.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Framework.Input.Bindings; using osu.Framework.Localisation; using osu.Game.Database; using osu.Game.Input.Bindings; -using osu.Game.Rulesets; using osu.Game.Localisation; using osuTK; +using Realms; namespace osu.Game.Overlays.Settings.Sections.Input { @@ -25,50 +24,85 @@ namespace osu.Game.Overlays.Settings.Sections.Input /// protected virtual bool AutoAdvanceTarget => false; - protected IEnumerable Defaults; + protected IEnumerable Defaults { get; init; } = Array.Empty(); - public RulesetInfo Ruleset { get; protected set; } + [Resolved] + private RealmAccess realm { get; set; } = null!; - private readonly int? variant; - - protected KeyBindingsSubsection(int? variant) + protected KeyBindingsSubsection() { - this.variant = variant; - FlowContent.Spacing = new Vector2(0, 3); } [BackgroundDependencyLoader] - private void load(RealmAccess realm) + private void load() { - string rulesetName = Ruleset?.ShortName; - - var bindings = realm.Run(r => r.All() - .Where(b => b.RulesetName == rulesetName && b.Variant == variant) - .Detach()); + var bindings = getAllBindings(); foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { int intKey = (int)defaultGroup.Key; - // one row per valid action. - Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.ActionInt.Equals(intKey)).ToList()) - { - AllowMainMouseButtons = Ruleset != null, - Defaults = defaultGroup.Select(d => d.KeyCombination), - BindingUpdated = onBindingUpdated - }); + var row = CreateKeyBindingRow(defaultGroup.Key, defaultGroup) + .With(row => + { + row.BindingUpdated = onBindingUpdated; + row.GetAllSectionBindings = getAllBindings; + }); + row.KeyBindings.AddRange(bindings.Where(b => b.ActionInt.Equals(intKey))); + Add(row); } Add(new ResetButton { - Action = () => Children.OfType().ForEach(k => k.RestoreDefaults()) + Action = () => + { + realm.Write(r => + { + // can't use `RestoreDefaults()` for each key binding row here as it might trigger binding conflicts along the way. + foreach (var row in Children.OfType()) + { + foreach (var (currentBinding, defaultBinding) in row.KeyBindings.Zip(row.Defaults)) + r.Find(currentBinding.ID)!.KeyCombinationString = defaultBinding.ToString(); + } + }); + reloadAllBindings(); + } }); } - private void onBindingUpdated(KeyBindingRow sender) + protected abstract IEnumerable GetKeyBindings(Realm realm); + + private List getAllBindings() => realm.Run(r => { - if (AutoAdvanceTarget) + r.Refresh(); + return GetKeyBindings(r).Detach(); + }); + + protected virtual KeyBindingRow CreateKeyBindingRow(object action, IEnumerable defaults) + => new KeyBindingRow(action) + { + AllowMainMouseButtons = false, + Defaults = defaults.Select(d => d.KeyCombination), + }; + + private void reloadAllBindings() + { + var bindings = getAllBindings(); + + foreach (var row in Children.OfType()) + { + row.KeyBindings.Clear(); + row.KeyBindings.AddRange(bindings.Where(b => b.ActionInt.Equals((int)row.Action))); + } + } + + private void onBindingUpdated(KeyBindingRow sender, KeyBindingRow.KeyBindingUpdatedEventArgs args) + { + if (args.BindingConflictResolved) + reloadAllBindings(); + + if (AutoAdvanceTarget && args.CanAdvanceToNextBinding) { var next = Children.SkipWhile(c => c != sender).Skip(1).FirstOrDefault(); if (next != null) diff --git a/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs b/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs index 19f0d0f7d1..f326592016 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs @@ -1,22 +1,16 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; -using osu.Game.Graphics; using osu.Game.Rulesets; namespace osu.Game.Overlays.Settings.Sections.Input { public partial class RulesetBindingsSection : SettingsSection { - public override Drawable CreateIcon() => ruleset?.CreateInstance().CreateIcon() ?? new SpriteIcon - { - Icon = OsuIcon.Hot - }; + public override Drawable CreateIcon() => ruleset.CreateInstance().CreateIcon(); public override LocalisableString Header => ruleset.Name; @@ -25,7 +19,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input public RulesetBindingsSection(RulesetInfo ruleset) { this.ruleset = ruleset; + } + [BackgroundDependencyLoader] + private void load() + { var r = ruleset.CreateInstance(); foreach (int variant in r.AvailableVariants) diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs index 8b15bc8f72..686002fe71 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs @@ -241,7 +241,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input protected override void OnDrag(DragEvent e) { var newPos = Position + e.Delta; - this.MoveTo(Vector2.Clamp(newPos, Vector2.Zero, Parent.Size)); + this.MoveTo(Vector2.Clamp(newPos, Vector2.Zero, Parent!.Size)); } protected override void OnDragEnd(DragEndEvent e) diff --git a/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs index d00de7f549..6db8aa7259 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs @@ -1,8 +1,13 @@ // 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.Input.Bindings; using osu.Framework.Localisation; +using osu.Game.Input.Bindings; using osu.Game.Rulesets; +using Realms; namespace osu.Game.Overlays.Settings.Sections.Input { @@ -12,15 +17,33 @@ namespace osu.Game.Overlays.Settings.Sections.Input protected override LocalisableString Header { get; } + public RulesetInfo Ruleset { get; } + private readonly int variant; + public VariantBindingsSubsection(RulesetInfo ruleset, int variant) - : base(variant) { Ruleset = ruleset; + this.variant = variant; var rulesetInstance = ruleset.CreateInstance(); Header = rulesetInstance.GetVariantName(variant); Defaults = rulesetInstance.GetDefaultKeyBindings(variant); } + + protected override IEnumerable GetKeyBindings(Realm realm) + { + string rulesetName = Ruleset.ShortName; + + return realm.All() + .Where(b => b.RulesetName == rulesetName && b.Variant == variant); + } + + protected override KeyBindingRow CreateKeyBindingRow(object action, IEnumerable defaults) + => new KeyBindingRow(action) + { + AllowMainMouseButtons = true, + Defaults = defaults.Select(d => d.KeyCombination), + }; } } diff --git a/osu.Game/Overlays/Settings/Sections/InputSection.cs b/osu.Game/Overlays/Settings/Sections/InputSection.cs index 0647068da7..a8f19cc91d 100644 --- a/osu.Game/Overlays/Settings/Sections/InputSection.cs +++ b/osu.Game/Overlays/Settings/Sections/InputSection.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectoryLocationDialog.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectoryLocationDialog.cs index 633bf8c5a5..fcbc603c83 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectoryLocationDialog.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectoryLocationDialog.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.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; @@ -15,7 +13,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance public partial class StableDirectoryLocationDialog : PopupDialog { [Resolved] - private IPerformFromScreenRunner performer { get; set; } + private IPerformFromScreenRunner performer { get; set; } = null!; public StableDirectoryLocationDialog(TaskCompletionSource taskCompletionSource) { diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectorySelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectorySelectScreen.cs index 048f3ee683..1b935b0cec 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectorySelectScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectorySelectScreen.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.IO; using System.Linq; using System.Threading.Tasks; @@ -17,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.Disabled; - protected override bool IsValidDirectory(DirectoryInfo info) => info?.GetFiles("osu!.*.cfg").Any() ?? false; + protected override bool IsValidDirectory(DirectoryInfo? info) => info?.GetFiles("osu!.*.cfg").Any() ?? false; public override LocalisableString HeaderText => "Please select your osu!stable install location"; diff --git a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs index dc6743c042..e7b6aa56a8 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/Online/IntegrationSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/IntegrationSettings.cs index 33748d0f5e..3d0fac32cf 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/IntegrationSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/IntegrationSettings.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs index d0707a434a..ce5c85bed0 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -33,9 +31,9 @@ namespace osu.Game.Overlays.Settings.Sections.Online }, new SettingsCheckbox { - LabelText = OnlineSettingsStrings.AutomaticallyDownloadWhenSpectating, - Keywords = new[] { "spectator" }, - Current = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating), + LabelText = OnlineSettingsStrings.AutomaticallyDownloadMissingBeatmaps, + Keywords = new[] { "spectator", "replay" }, + Current = config.GetBindable(OsuSetting.AutomaticallyDownloadMissingBeatmaps), }, new SettingsCheckbox { diff --git a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs index 775c6f9839..c8faa3b697 100644 --- a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs +++ b/osu.Game/Overlays/Settings/Sections/OnlineSection.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 osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/SizeSlider.cs b/osu.Game/Overlays/Settings/Sections/SizeSlider.cs index 20d77bef0d..c73831d8d1 100644 --- a/osu.Game/Overlays/Settings/Sections/SizeSlider.cs +++ b/osu.Game/Overlays/Settings/Sections/SizeSlider.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Globalization; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 5382eac675..e997e70157 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Settings.Sections }); } - private void skinsChanged(IRealmCollection sender, ChangeSet changes, Exception error) + private void skinsChanged(IRealmCollection sender, ChangeSet changes) { // This can only mean that realm is recycling, else we would see the protected skins. // Because we are using `Live<>` in this class, we don't need to worry about this scenario too much. diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs index 2e8d005401..3f39980b43 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -14,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface { public partial class GeneralSettings : SettingsSubsection { - protected override LocalisableString Header => UserInterfaceStrings.GeneralHeader; + protected override LocalisableString Header => CommonStrings.General; [BackgroundDependencyLoader] private void load(OsuConfigManager config) diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs index d3303e409c..addf5ce163 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs b/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs index 0926574a54..2ec9e32ea9 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; diff --git a/osu.Game/Overlays/Settings/SettingsCheckbox.cs b/osu.Game/Overlays/Settings/SettingsCheckbox.cs index a413bcf220..f8edcaf53d 100644 --- a/osu.Game/Overlays/Settings/SettingsCheckbox.cs +++ b/osu.Game/Overlays/Settings/SettingsCheckbox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs index 62dd4f2905..cf6bc30f85 100644 --- a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsEnumDropdown.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 osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Overlays/Settings/SettingsFooter.cs b/osu.Game/Overlays/Settings/SettingsFooter.cs index 9ab0fa7ad8..4e9d4c0d28 100644 --- a/osu.Game/Overlays/Settings/SettingsFooter.cs +++ b/osu.Game/Overlays/Settings/SettingsFooter.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 osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Graphics; @@ -83,7 +81,7 @@ namespace osu.Game.Overlays.Settings private readonly string version; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; public BuildDisplay(string version) { @@ -94,8 +92,8 @@ namespace osu.Game.Overlays.Settings Height = 20; } - [BackgroundDependencyLoader(true)] - private void load(ChangelogOverlay changelog) + [BackgroundDependencyLoader] + private void load(ChangelogOverlay? changelog) { Action = () => changelog?.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version); diff --git a/osu.Game/Overlays/Settings/SettingsHeader.cs b/osu.Game/Overlays/Settings/SettingsHeader.cs index f2b84c4ba9..8d155fd01e 100644 --- a/osu.Game/Overlays/Settings/SettingsHeader.cs +++ b/osu.Game/Overlays/Settings/SettingsHeader.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 5f4bb9d57f..9c6bb5ae60 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -196,7 +196,7 @@ namespace osu.Game.Overlays.Settings { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0, 10), + Spacing = new Vector2(0, 5), Child = Control = CreateControl(), } } @@ -217,7 +217,7 @@ namespace osu.Game.Overlays.Settings // intentionally done before LoadComplete to avoid overhead. if (ShowsDefaultIndicator) { - defaultValueIndicatorContainer.Add(new RestoreDefaultValueButton + defaultValueIndicatorContainer.Add(new RevertToDefaultButton { Current = controlWithCurrent.Current, Anchor = Anchor.Centre, diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index 97b8f6de60..a0f85eda31 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; @@ -38,7 +36,6 @@ namespace osu.Game.Overlays.Settings { numberBox = new OutlinedNumberBox { - Margin = new MarginPadding { Top = 5 }, RelativeSizeAxes = Axes.X, CommitOnFocusLost = true } diff --git a/osu.Game/Overlays/Settings/SettingsSidebar.cs b/osu.Game/Overlays/Settings/SettingsSidebar.cs index 36411e01cc..06bc2fd788 100644 --- a/osu.Game/Overlays/Settings/SettingsSidebar.cs +++ b/osu.Game/Overlays/Settings/SettingsSidebar.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game/Overlays/Settings/SettingsSlider.cs b/osu.Game/Overlays/Settings/SettingsSlider.cs index e1483d4202..6c81fece13 100644 --- a/osu.Game/Overlays/Settings/SettingsSlider.cs +++ b/osu.Game/Overlays/Settings/SettingsSlider.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 osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Overlays/Settings/SettingsSubsection.cs b/osu.Game/Overlays/Settings/SettingsSubsection.cs index eda18abaef..87772eb18c 100644 --- a/osu.Game/Overlays/Settings/SettingsSubsection.cs +++ b/osu.Game/Overlays/Settings/SettingsSubsection.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 osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Overlays/Settings/SettingsTextBox.cs b/osu.Game/Overlays/Settings/SettingsTextBox.cs index 3f9fa06384..7fc7e1a97b 100644 --- a/osu.Game/Overlays/Settings/SettingsTextBox.cs +++ b/osu.Game/Overlays/Settings/SettingsTextBox.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 osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs index aec0509394..a63688762d 100644 --- a/osu.Game/Overlays/Settings/SidebarButton.cs +++ b/osu.Game/Overlays/Settings/SidebarButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; @@ -14,10 +12,10 @@ namespace osu.Game.Overlays.Settings protected const double FADE_DURATION = 500; [Resolved] - protected OverlayColourProvider ColourProvider { get; private set; } + protected OverlayColourProvider ColourProvider { get; private set; } = null!; - protected SidebarButton() - : base(HoverSampleSet.ButtonSidebar) + protected SidebarButton(HoverSampleSet? hoverSounds = HoverSampleSet.ButtonSidebar) + : base(hoverSounds) { } diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index 1681187f82..3bac6c400f 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -14,6 +14,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; @@ -56,6 +57,7 @@ namespace osu.Game.Overlays private SeekLimitedSearchTextBox searchTextBox; protected override string PopInSampleName => "UI/settings-pop-in"; + protected override double PopInOutSampleBalance => -OsuGameBase.SFX_STEREO_STRENGTH; private readonly bool showSidebar; @@ -105,39 +107,43 @@ namespace osu.Game.Overlays } }; - Add(SectionsContainer = new SettingsSectionsContainer + Add(new PopoverContainer { - Masking = true, - EdgeEffect = new EdgeEffectParameters - { - Colour = Color4.Black.Opacity(0), - Type = EdgeEffectType.Shadow, - Hollow = true, - Radius = 10 - }, - MaskingSmoothness = 0, RelativeSizeAxes = Axes.Both, - ExpandableHeader = CreateHeader(), - SelectedSection = { BindTarget = CurrentSection }, - FixedHeader = new Container + Child = SectionsContainer = new SettingsSectionsContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding + Masking = true, + EdgeEffect = new EdgeEffectParameters { - Vertical = 20, - Horizontal = CONTENT_MARGINS + Colour = Color4.Black.Opacity(0), + Type = EdgeEffectType.Shadow, + Hollow = true, + Radius = 10 }, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Child = searchTextBox = new SeekLimitedSearchTextBox + MaskingSmoothness = 0, + RelativeSizeAxes = Axes.Both, + ExpandableHeader = CreateHeader(), + SelectedSection = { BindTarget = CurrentSection }, + FixedHeader = new Container { RelativeSizeAxes = Axes.X, - Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Vertical = 20, + Horizontal = CONTENT_MARGINS + }, Anchor = Anchor.TopCentre, - } - }, - Footer = CreateFooter().With(f => f.Alpha = 0) + Origin = Anchor.TopCentre, + Child = searchTextBox = new SettingsSearchTextBox + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + } + }, + Footer = CreateFooter().With(f => f.Alpha = 0) + } }); if (showSidebar) @@ -163,8 +169,6 @@ namespace osu.Game.Overlays protected override void PopIn() { - base.PopIn(); - ContentContainer.MoveToX(ExpandedPosition, TRANSITION_LENGTH, Easing.OutQuint); SectionsContainer.FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out); diff --git a/osu.Game/Overlays/SettingsSearchTextBox.cs b/osu.Game/Overlays/SettingsSearchTextBox.cs new file mode 100644 index 0000000000..84cff1b508 --- /dev/null +++ b/osu.Game/Overlays/SettingsSearchTextBox.cs @@ -0,0 +1,22 @@ +// 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 osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays +{ + public partial class SettingsSearchTextBox : SeekLimitedSearchTextBox + { + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + + // on mobile platforms, focus is not held by the search text box, and the select all feature + // will not make sense on it, and might annoy the user when they try to focus manually. + if (HoldFocus) + SelectAll(); + } + } +} diff --git a/osu.Game/Overlays/SettingsSubPanel.cs b/osu.Game/Overlays/SettingsSubPanel.cs index 5890d1c8fa..1651975a74 100644 --- a/osu.Game/Overlays/SettingsSubPanel.cs +++ b/osu.Game/Overlays/SettingsSubPanel.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; using osuTK; @@ -38,6 +39,11 @@ namespace osu.Game.Overlays { private Container content; + public BackButton() + : base(HoverSampleSet.Default) + { + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs index c090878899..01cd3d97e0 100644 --- a/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs +++ b/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs @@ -202,7 +202,7 @@ namespace osu.Game.Overlays.SkinEditor if (drawable.Parent == null) return; - var newAnchor = drawable.Parent.ToSpaceOfOtherDrawable(drawable.AnchorPosition, this); + var newAnchor = drawable.Parent!.ToSpaceOfOtherDrawable(drawable.AnchorPosition, this); anchorPosition = tweenPosition(anchorPosition ?? newAnchor, newAnchor); anchorBox.Position = anchorPosition.Value; diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs index db27e20010..3f8d9f80d4 100644 --- a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs +++ b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs @@ -25,8 +25,6 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private SkinEditor editor { get; set; } = null!; - protected override bool AllowCyclicSelection => true; - public SkinBlueprintContainer(ISerialisableDrawableContainer targetContainer) { this.targetContainer = targetContainer; diff --git a/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs b/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs index 1ce253d67c..a476fc1a6d 100644 --- a/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs +++ b/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs @@ -13,6 +13,7 @@ using osu.Framework.Threading; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; +using osu.Game.Rulesets; using osu.Game.Screens.Edit.Components; using osu.Game.Skinning; using osuTK; @@ -23,14 +24,22 @@ namespace osu.Game.Overlays.SkinEditor { public Action? RequestPlacement; - private readonly SkinComponentsContainer? target; + private readonly SkinComponentsContainer target; + + private readonly RulesetInfo? ruleset; private FillFlowContainer fill = null!; - public SkinComponentToolbox(SkinComponentsContainer? target = null) - : base(target?.Lookup.Ruleset == null ? SkinEditorStrings.Components : LocalisableString.Interpolate($"{SkinEditorStrings.Components} ({target.Lookup.Ruleset.Name})")) + /// + /// Create a new component toolbox for the specified taget. + /// + /// The target. This is mainly used as a dependency source to find candidate components. + /// A ruleset to filter components by. If null, only components which are not ruleset-specific will be included. + public SkinComponentToolbox(SkinComponentsContainer target, RulesetInfo? ruleset) + : base(ruleset == null ? SkinEditorStrings.Components : LocalisableString.Interpolate($"{SkinEditorStrings.Components} ({ruleset.Name})")) { this.target = target; + this.ruleset = ruleset; } [BackgroundDependencyLoader] @@ -51,7 +60,7 @@ namespace osu.Game.Overlays.SkinEditor { fill.Clear(); - var skinnableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables(target?.Lookup.Ruleset); + var skinnableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables(ruleset); foreach (var type in skinnableTypes) attemptAddComponent(type); } diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 2b23ce290f..5affaedf1d 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -356,7 +356,7 @@ namespace osu.Game.Overlays.SkinEditor { new SettingsDropdown { - Items = availableTargets.Select(t => t.Lookup), + Items = availableTargets.Select(t => t.Lookup).Distinct(), Current = selectedTarget, } } @@ -366,14 +366,14 @@ namespace osu.Game.Overlays.SkinEditor // If the new target has a ruleset, let's show ruleset-specific items at the top, and the rest below. if (target.NewValue.Ruleset != null) { - componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer) + componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer, target.NewValue.Ruleset) { RequestPlacement = requestPlacement }); } // Remove the ruleset from the lookup to get base components. - componentsSidebar.Add(new SkinComponentToolbox(getTarget(new SkinComponentsContainerLookup(target.NewValue.Target))) + componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer, null) { RequestPlacement = requestPlacement }); diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 1c0ece28fe..68d6b7ced5 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -3,11 +3,13 @@ using System.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using osu.Game.Screens; @@ -45,6 +47,12 @@ namespace osu.Game.Overlays.SkinEditor RelativeSizeAxes = Axes.Both; } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins); + } + public bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) @@ -62,6 +70,8 @@ namespace osu.Game.Overlays.SkinEditor protected override void PopIn() { + globallyDisableBeatmapSkinSetting(); + if (skinEditor != null) { skinEditor.Show(); @@ -87,7 +97,13 @@ namespace osu.Game.Overlays.SkinEditor }); } - protected override void PopOut() => skinEditor?.Hide(); + protected override void PopOut() + { + skinEditor?.Save(false); + skinEditor?.Hide(); + + globallyReenableBeatmapSkinSetting(); + } protected override void Update() { @@ -151,8 +167,6 @@ namespace osu.Game.Overlays.SkinEditor if (skinEditor == null) return; - skinEditor.Save(userTriggered: false); - // ensure the toolbar is re-hidden even if a new screen decides to try and show it. updateComponentVisibility(); @@ -182,5 +196,25 @@ namespace osu.Game.Overlays.SkinEditor skinEditor = null; } } + + private readonly Bindable beatmapSkins = new Bindable(); + private LeasedBindable? leasedBeatmapSkins; + + private void globallyDisableBeatmapSkinSetting() + { + if (beatmapSkins.Disabled) + return; + + // The skin editor doesn't work well if beatmap skins are being applied to the player screen. + // To keep things simple, disable the setting game-wide while using the skin editor. + leasedBeatmapSkins = beatmapSkins.BeginLease(true); + leasedBeatmapSkins.Value = false; + } + + private void globallyReenableBeatmapSkinSetting() + { + leasedBeatmapSkins?.Return(); + leasedBeatmapSkins = null; + } } } diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index b43f4eeb00..c4e2c4c6bd 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Skinning; +using osu.Game.Utils; using osuTK; namespace osu.Game.Overlays.SkinEditor @@ -25,31 +26,10 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private SkinEditor skinEditor { get; set; } = null!; - public override bool HandleRotation(float angle) + public override SelectionRotationHandler CreateRotationHandler() => new SkinSelectionRotationHandler { - if (SelectedBlueprints.Count == 1) - { - // for single items, rotate around the origin rather than the selection centre. - ((Drawable)SelectedBlueprints.First().Item).Rotation += angle; - } - else - { - var selectionQuad = getSelectionQuad(); - - foreach (var b in SelectedBlueprints) - { - var drawableItem = (Drawable)b.Item; - - var rotatedPosition = RotatePointAroundOrigin(b.ScreenSpaceSelectionPoint, selectionQuad.Centre, angle); - updateDrawablePosition(drawableItem, rotatedPosition); - - drawableItem.Rotation += angle; - } - } - - // this isn't always the case but let's be lenient for now. - return true; - } + UpdatePosition = updateDrawablePosition + }; public override bool HandleScale(Vector2 scale, Anchor anchor) { @@ -67,10 +47,7 @@ namespace osu.Game.Overlays.SkinEditor // copy to mutate, as we will need to compare to the original later on. var adjustedRect = selectionRect; - - // first, remove any scale axis we are not interested in. - if (anchor.HasFlagFast(Anchor.x1)) scale.X = 0; - if (anchor.HasFlagFast(Anchor.y1)) scale.Y = 0; + bool isRotated = false; // for now aspect lock scale adjustments that occur at corners.. if (!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1)) @@ -81,8 +58,9 @@ namespace osu.Game.Overlays.SkinEditor } // ..or if any of the selection have been rotated. // this is to avoid requiring skew logic (which would likely not be the user's expected transform anyway). - else if (SelectedBlueprints.Any(b => !Precision.AlmostEquals(((Drawable)b.Item).Rotation, 0))) + else if (SelectedBlueprints.Any(b => !Precision.AlmostEquals(((Drawable)b.Item).Rotation % 90, 0))) { + isRotated = true; if (anchor.HasFlagFast(Anchor.x1)) // if dragging from the horizontal centre, only a vertical component is available. scale.X = scale.Y / selectionRect.Height * selectionRect.Width; @@ -94,13 +72,28 @@ namespace osu.Game.Overlays.SkinEditor if (anchor.HasFlagFast(Anchor.x0)) adjustedRect.X -= scale.X; if (anchor.HasFlagFast(Anchor.y0)) adjustedRect.Y -= scale.Y; + // Maintain the selection's centre position if dragging from the centre anchors and selection is rotated. + if (isRotated && anchor.HasFlagFast(Anchor.x1)) adjustedRect.X -= scale.X / 2; + if (isRotated && anchor.HasFlagFast(Anchor.y1)) adjustedRect.Y -= scale.Y / 2; + adjustedRect.Width += scale.X; adjustedRect.Height += scale.Y; + if (adjustedRect.Width <= 0 || adjustedRect.Height <= 0) + { + Axes toFlip = Axes.None; + + if (adjustedRect.Width <= 0) toFlip |= Axes.X; + if (adjustedRect.Height <= 0) toFlip |= Axes.Y; + + SelectionBox.PerformFlipFromScaleHandles(toFlip); + return true; + } + // scale adjust applied to each individual item should match that of the quad itself. var scaledDelta = new Vector2( - MathF.Max(adjustedRect.Width / selectionRect.Width, 0), - MathF.Max(adjustedRect.Height / selectionRect.Height, 0) + adjustedRect.Width / selectionRect.Width, + adjustedRect.Height / selectionRect.Height ); foreach (var b in SelectedBlueprints) @@ -122,7 +115,12 @@ namespace osu.Game.Overlays.SkinEditor ); updateDrawablePosition(drawableItem, newPositionInAdjusted); - drawableItem.Scale *= scaledDelta; + + var currentScaledDelta = scaledDelta; + if (Precision.AlmostEquals(MathF.Abs(drawableItem.Rotation) % 180, 90)) + currentScaledDelta = new Vector2(scaledDelta.Y, scaledDelta.X); + + drawableItem.Scale *= currentScaledDelta; } return true; @@ -137,7 +135,7 @@ namespace osu.Game.Overlays.SkinEditor { var drawableItem = (Drawable)b.Item; - var flippedPosition = GetFlippedPosition(direction, flipOverOrigin ? drawableItem.Parent.ScreenSpaceDrawQuad : selectionQuad, b.ScreenSpaceSelectionPoint); + var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipOverOrigin ? drawableItem.Parent!.ScreenSpaceDrawQuad : selectionQuad, b.ScreenSpaceSelectionPoint); updateDrawablePosition(drawableItem, flippedPosition); @@ -171,7 +169,6 @@ namespace osu.Game.Overlays.SkinEditor { base.OnSelectionChanged(); - SelectionBox.CanRotate = true; SelectionBox.CanScaleX = true; SelectionBox.CanScaleY = true; SelectionBox.CanFlipX = true; @@ -245,7 +242,7 @@ namespace osu.Game.Overlays.SkinEditor private static void updateDrawablePosition(Drawable drawable, Vector2 screenSpacePosition) { drawable.Position = - drawable.Parent.ToLocalSpace(screenSpacePosition) - drawable.AnchorPosition; + drawable.Parent!.ToLocalSpace(screenSpacePosition) - drawable.AnchorPosition; } private void applyOrigins(Anchor origin) @@ -275,7 +272,7 @@ namespace osu.Game.Overlays.SkinEditor /// /// private Quad getSelectionQuad() => - GetSurroundingQuad(SelectedBlueprints.SelectMany(b => b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray())); + GeometryUtils.GetSurroundingQuad(SelectedBlueprints.SelectMany(b => b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray())); private void applyFixedAnchors(Anchor anchor) { @@ -312,7 +309,7 @@ namespace osu.Game.Overlays.SkinEditor if (parent == null) return drawable.Anchor; - var screenPosition = getScreenPosition(); + var screenPosition = drawable.ToScreenSpace(drawable.OriginPosition); var absolutePosition = parent.ToLocalSpace(screenPosition); var factor = parent.RelativeToAbsoluteFactor; @@ -334,26 +331,6 @@ namespace osu.Game.Overlays.SkinEditor result |= getAnchorFromPosition(absolutePosition.Y / factor.Y, Anchor.y0, Anchor.y1, Anchor.y2); return result; - - Vector2 getScreenPosition() - { - var quad = drawable.ScreenSpaceDrawQuad; - var origin = drawable.Origin; - - var pos = quad.TopLeft; - - if (origin.HasFlagFast(Anchor.x2)) - pos.X += quad.Width; - else if (origin.HasFlagFast(Anchor.x1)) - pos.X += quad.Width / 2f; - - if (origin.HasFlagFast(Anchor.y2)) - pos.Y += quad.Height; - else if (origin.HasFlagFast(Anchor.y1)) - pos.Y += quad.Height / 2f; - - return pos; - } } private static void applyAnchor(Drawable drawable, Anchor anchor) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs new file mode 100644 index 0000000000..60f69000a2 --- /dev/null +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs @@ -0,0 +1,104 @@ +// 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.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Skinning; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Overlays.SkinEditor +{ + public partial class SkinSelectionRotationHandler : SelectionRotationHandler + { + public Action UpdatePosition { get; init; } = null!; + + [Resolved] + private IEditorChangeHandler? changeHandler { get; set; } + + private BindableList selectedItems { get; } = new BindableList(); + + [BackgroundDependencyLoader] + private void load(SkinEditor skinEditor) + { + selectedItems.BindTo(skinEditor.SelectedComponents); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedItems.CollectionChanged += (_, __) => updateState(); + updateState(); + } + + private void updateState() + { + CanRotate.Value = selectedItems.Count > 0; + } + + private Drawable[]? objectsInRotation; + + private Vector2? defaultOrigin; + private Dictionary? originalRotations; + private Dictionary? originalPositions; + + public override void Begin() + { + if (objectsInRotation != null) + throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!"); + + changeHandler?.BeginChange(); + + objectsInRotation = selectedItems.Cast().ToArray(); + originalRotations = objectsInRotation.ToDictionary(d => d, d => d.Rotation); + originalPositions = objectsInRotation.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition)); + defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())).Centre; + } + + public override void Update(float rotation, Vector2? origin = null) + { + if (objectsInRotation == null) + throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!"); + + Debug.Assert(originalRotations != null && originalPositions != null && defaultOrigin != null); + + if (objectsInRotation.Length == 1 && origin == null) + { + // for single items, rotate around the origin rather than the selection centre by default. + objectsInRotation[0].Rotation = originalRotations.Single().Value + rotation; + return; + } + + var actualOrigin = origin ?? defaultOrigin.Value; + + foreach (var drawableItem in objectsInRotation) + { + var rotatedPosition = GeometryUtils.RotatePointAroundOrigin(originalPositions[drawableItem], actualOrigin, rotation); + UpdatePosition(drawableItem, rotatedPosition); + + drawableItem.Rotation = originalRotations[drawableItem] + rotation; + } + } + + public override void Commit() + { + if (objectsInRotation == null) + throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!"); + + changeHandler?.EndChange(); + + objectsInRotation = null; + originalPositions = null; + originalRotations = null; + defaultOrigin = null; + } + } +} diff --git a/osu.Game/Overlays/TabControlOverlayHeader.cs b/osu.Game/Overlays/TabControlOverlayHeader.cs index 2b87535708..3875f18152 100644 --- a/osu.Game/Overlays/TabControlOverlayHeader.cs +++ b/osu.Game/Overlays/TabControlOverlayHeader.cs @@ -1,9 +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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -85,13 +82,11 @@ namespace osu.Game.Overlays controlBackground.Colour = colourProvider.Dark4; } - [NotNull] protected virtual OsuTabControl CreateTabControl() => new OverlayHeaderTabControl(); /// /// Creates a on the opposite side of the . Used mostly to create . /// - [NotNull] protected virtual Drawable CreateTabControlContent() => Empty(); public partial class OverlayHeaderTabControl : OverlayTabControl diff --git a/osu.Game/Overlays/Toolbar/ClockDisplay.cs b/osu.Game/Overlays/Toolbar/ClockDisplay.cs index 088631f8d6..c72c92b61b 100644 --- a/osu.Game/Overlays/Toolbar/ClockDisplay.cs +++ b/osu.Game/Overlays/Toolbar/ClockDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics.Containers; diff --git a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs index efcb011293..247be553e1 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Input.Bindings; diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 4193e52584..e181322dda 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.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 osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -39,10 +37,10 @@ namespace osu.Game.Overlays.Toolbar } [Resolved] - private TextureStore textures { get; set; } + private TextureStore textures { get; set; } = null!; [Resolved] - private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } + private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!; public void SetIcon(string texture) => SetIcon(new Sprite @@ -81,7 +79,7 @@ namespace osu.Game.Overlays.Toolbar protected FillFlowContainer Flow; [Resolved] - private RealmAccess realm { get; set; } + private RealmAccess realm { get; set; } = null!; protected ToolbarButton() { diff --git a/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs index 30e32d831c..06f171b1f2 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs index 7bb94067ab..126f8383ce 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Input.Bindings; diff --git a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs index dba4e8feb6..ba2c8282c5 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.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 osu.Framework.Allocation; using osu.Game.Input.Bindings; using osu.Game.Localisation; diff --git a/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs index bdcf6c3fec..13900dffa9 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarNewsButton.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 osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs index 3dfec2cba0..9971871229 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -43,8 +41,7 @@ namespace osu.Game.Overlays.Toolbar { StateContainer = notificationOverlay as NotificationOverlay; - if (notificationOverlay != null) - NotificationCount.BindTo(notificationOverlay.UnreadCount); + NotificationCount.BindTo(notificationOverlay.UnreadCount); NotificationCount.ValueChanged += count => { diff --git a/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs index ddbf4889b6..3e94ff90c5 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.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 osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs index 07f7d52545..74f76c7c89 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.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 osu.Framework.Graphics; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Rulesets; using osuTK.Graphics; @@ -40,6 +39,8 @@ namespace osu.Game.Overlays.Toolbar private partial class RulesetButton : ToolbarButton { + protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(); + public bool Active { set @@ -65,7 +66,7 @@ namespace osu.Game.Overlays.Toolbar protected override bool OnClick(ClickEvent e) { - Parent.TriggerClick(); + Parent!.TriggerClick(); return base.OnClick(e); } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs index 6ebf2a4c02..78df060252 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.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 osu.Framework.Allocation; using osu.Game.Input.Bindings; diff --git a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs index a8a88813d2..8e6a5fdb78 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Input.Bindings; diff --git a/osu.Game/Overlays/Toolbar/ToolbarWikiButton.cs b/osu.Game/Overlays/Toolbar/ToolbarWikiButton.cs index 49e6be7978..e60aea53c3 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarWikiButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarWikiButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/VersionManager.cs b/osu.Game/Overlays/VersionManager.cs index 0e74cada29..71f8fc05aa 100644 --- a/osu.Game/Overlays/VersionManager.cs +++ b/osu.Game/Overlays/VersionManager.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 osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Volume/MuteButton.cs b/osu.Game/Overlays/Volume/MuteButton.cs index c83ad4ac0d..1dc8d754b7 100644 --- a/osu.Game/Overlays/Volume/MuteButton.cs +++ b/osu.Game/Overlays/Volume/MuteButton.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 osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs index 00474cc0d8..0295ff467a 100644 --- a/osu.Game/Overlays/WaveOverlayContainer.cs +++ b/osu.Game/Overlays/WaveOverlayContainer.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 osu.Framework.Graphics.Containers; using osu.Framework.Graphics; using osu.Game.Graphics.Containers; @@ -20,7 +18,9 @@ namespace osu.Game.Overlays protected override bool StartHidden => true; - protected override string PopInSampleName => "UI/wave-pop-in"; + // `WaveContainer` plays PopIn/PopOut samples, so we disable the overlay-level one as to not double-up sample playback. + protected override string PopInSampleName => string.Empty; + protected override string PopOutSampleName => string.Empty; public const float HORIZONTAL_PADDING = 50; @@ -34,8 +34,6 @@ namespace osu.Game.Overlays protected override void PopIn() { - base.PopIn(); - Waves.Show(); this.FadeIn(100, Easing.OutQuint); } diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs index 7c36caa62f..9107ad342b 100644 --- a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs +++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Markdig.Extensions.CustomContainers; using Markdig.Extensions.Yaml; diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImage.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImage.cs index 71c2df538d..cfeb4de19c 100644 --- a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImage.cs +++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImage.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Markdig.Syntax.Inlines; using osu.Game.Graphics.Containers.Markdown; diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs index 641c6242b6..cae2d0167a 100644 --- a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs +++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Markdig.Syntax.Inlines; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -16,7 +14,7 @@ namespace osu.Game.Overlays.Wiki.Markdown public partial class WikiMarkdownImageBlock : FillFlowContainer { [Resolved] - private IMarkdownTextFlowComponent parentFlowComponent { get; set; } + private IMarkdownTextFlowComponent parentFlowComponent { get; set; } = null!; private readonly LinkInline linkInline; @@ -83,10 +81,10 @@ namespace osu.Game.Overlays.Wiki.Markdown { base.Update(); - if (Width > Parent.DrawWidth) + if (Width > Parent!.DrawWidth) { float ratio = Height / Width; - Width = Parent.DrawWidth; + Width = Parent!.DrawWidth; Height = ratio * Width; } } diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiNoticeContainer.cs b/osu.Game/Overlays/Wiki/Markdown/WikiNoticeContainer.cs index a40bd14878..1ab35b1972 100644 --- a/osu.Game/Overlays/Wiki/Markdown/WikiNoticeContainer.cs +++ b/osu.Game/Overlays/Wiki/Markdown/WikiNoticeContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Markdig.Extensions.Yaml; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -80,7 +78,7 @@ namespace osu.Game.Overlays.Wiki.Markdown private partial class NoticeBox : Container { [Resolved] - private IMarkdownTextFlowComponent parentFlowComponent { get; set; } + private IMarkdownTextFlowComponent parentFlowComponent { get; set; } = null!; public LocalisableString Text { get; set; } diff --git a/osu.Game/Overlays/Wiki/WikiPanelContainer.cs b/osu.Game/Overlays/Wiki/WikiPanelContainer.cs index ef31e9cfdd..c5b71cfeb6 100644 --- a/osu.Game/Overlays/Wiki/WikiPanelContainer.cs +++ b/osu.Game/Overlays/Wiki/WikiPanelContainer.cs @@ -75,7 +75,7 @@ namespace osu.Game.Overlays.Wiki protected override void Update() { base.Update(); - Height = Math.Max(panelContainer.Height, Parent.DrawHeight); + Height = Math.Max(panelContainer.Height, Parent!.DrawHeight); } private partial class WikiPanelMarkdownContainer : WikiMarkdownContainer diff --git a/osu.Game/Performance/HighPerformanceSession.cs b/osu.Game/Performance/HighPerformanceSession.cs index c113e7a342..07b5e7da98 100644 --- a/osu.Game/Performance/HighPerformanceSession.cs +++ b/osu.Game/Performance/HighPerformanceSession.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game/Properties/AssemblyInfo.cs b/osu.Game/Properties/AssemblyInfo.cs index dde1af6461..1b77e45891 100644 --- a/osu.Game/Properties/AssemblyInfo.cs +++ b/osu.Game/Properties/AssemblyInfo.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.Runtime.CompilerServices; // We publish our internal attributes to other sub-projects of the framework. diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index bd45482235..9690924b1c 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; @@ -69,7 +68,10 @@ namespace osu.Game.Rulesets.Difficulty /// /// See: osu_difficulty_attribs table. /// - public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() => Enumerable.Empty<(int, object)>(); + public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() + { + yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); + } /// /// Reads osu-web database attribute mappings into this object. @@ -78,6 +80,7 @@ namespace osu.Game.Rulesets.Difficulty /// The where more information about the beatmap may be extracted from (such as AR/CS/OD/etc). public virtual void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { + MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs index 15b90e5147..e8c4c71913 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.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.Collections.Generic; using Newtonsoft.Json; diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs index bd971db476..6e41855ca3 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.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 - namespace osu.Game.Rulesets.Difficulty { /// @@ -19,5 +17,16 @@ namespace osu.Game.Rulesets.Difficulty /// Performance of a perfect play for comparison. /// public PerformanceAttributes PerfectPerformance { get; set; } + + /// + /// Create a new performance breakdown. + /// + /// Actual gameplay performance. + /// Performance of a perfect play for comparison. + public PerformanceBreakdown(PerformanceAttributes performance, PerformanceAttributes perfectPerformance) + { + Performance = performance; + PerfectPerformance = perfectPerformance; + } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 64a04f896f..ad9257d4f3 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; +using osu.Framework.Extensions.ObjectExtensions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -39,7 +40,7 @@ namespace osu.Game.Rulesets.Difficulty getPerfectPerformance(score, cancellationToken) ).ConfigureAwait(false); - return new PerformanceBreakdown { Performance = performanceArray[0], PerfectPerformance = performanceArray[1] }; + return new PerformanceBreakdown(performanceArray[0] ?? new PerformanceAttributes(), performanceArray[1] ?? new PerformanceAttributes()); } [ItemCanBeNull] @@ -88,7 +89,7 @@ namespace osu.Game.Rulesets.Difficulty ).ConfigureAwait(false); // ScorePerformanceCache is not used to avoid caching multiple copies of essentially identical perfect performance attributes - return difficulty == null ? null : ruleset.CreatePerformanceCalculator()?.Calculate(perfectPlay, difficulty.Value.Attributes); + return difficulty == null ? null : ruleset.CreatePerformanceCalculator()?.Calculate(perfectPlay, difficulty.Value.Attributes.AsNonNull()); }, cancellationToken); } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs index 38a35ddb3b..f5e826f8c7 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Beatmaps; using osu.Game.Scoring; diff --git a/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs index 76dfca3db7..a654652ef8 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Rulesets.Difficulty { /// diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs index 44abbaaf41..8b8892113b 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs index 6abde64eb7..8fab61ed62 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index 4beba22e05..b43a272324 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index 5f5aba26bb..6782c4324a 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; using osu.Game.Rulesets.Edit.Checks; @@ -36,9 +34,13 @@ namespace osu.Game.Rulesets.Edit new CheckUnsnappedObjects(), new CheckConcurrentObjects(), new CheckZeroLengthObjects(), + new CheckDrainLength(), // Timing new CheckPreviewTime(), + + // Events + new CheckBreaks() }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioPresence.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioPresence.cs index e922ddf023..416a0d5897 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAudioPresence.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioPresence.cs @@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Edit.Checks { protected override CheckCategory Category => CheckCategory.Audio; protected override string TypeOfFile => "audio"; - protected override string? GetFilename(IBeatmap beatmap) => beatmap.Metadata?.AudioFile; + protected override string GetFilename(IBeatmap beatmap) => beatmap.Metadata.AudioFile; } } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs index daa33fb0da..440d4e8e62 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioQuality.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Edit.Checks public IEnumerable Run(BeatmapVerifierContext context) { - string? audioFile = context.Beatmap.Metadata?.AudioFile; + string audioFile = context.Beatmap.Metadata.AudioFile; if (string.IsNullOrEmpty(audioFile)) yield break; diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundPresence.cs b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundPresence.cs index 4ca93a9807..04cbba1e8c 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundPresence.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundPresence.cs @@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Edit.Checks { protected override CheckCategory Category => CheckCategory.Resources; protected override string TypeOfFile => "background"; - protected override string? GetFilename(IBeatmap beatmap) => beatmap.Metadata?.BackgroundFile; + protected override string GetFilename(IBeatmap beatmap) => beatmap.Metadata.BackgroundFile; } } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs index 8c3a5c026d..5008c13d9a 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs @@ -33,8 +33,8 @@ namespace osu.Game.Rulesets.Edit.Checks public IEnumerable Run(BeatmapVerifierContext context) { - string? backgroundFile = context.Beatmap.Metadata?.BackgroundFile; - if (backgroundFile == null) + string backgroundFile = context.Beatmap.Metadata.BackgroundFile; + if (string.IsNullOrEmpty(backgroundFile)) yield break; var texture = context.WorkingBeatmap.GetBackground(); diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs b/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs new file mode 100644 index 0000000000..94369443c2 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs @@ -0,0 +1,94 @@ +// 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.Beatmaps.Timing; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckBreaks : ICheck + { + // Breaks may be off by 1 ms. + private const int leniency_threshold = 1; + private const double minimum_gap_before_break = 200; + + // Break end time depends on the upcoming object's pre-empt time. + // As things stand, "pre-empt time" is only defined for osu! standard + // This is a generic value representing AR=10 + // Relevant: https://github.com/ppy/osu/issues/14330#issuecomment-1002158551 + private const double min_end_threshold = 450; + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Events, "Breaks not achievable using the editor"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateEarlyStart(this), + new IssueTemplateLateEnd(this), + new IssueTemplateTooShort(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var startTimes = context.Beatmap.HitObjects.Select(ho => ho.StartTime).OrderBy(x => x).ToList(); + var endTimes = context.Beatmap.HitObjects.Select(ho => ho.GetEndTime()).OrderBy(x => x).ToList(); + + foreach (var breakPeriod in context.Beatmap.Breaks) + { + if (breakPeriod.Duration < BreakPeriod.MIN_BREAK_DURATION) + yield return new IssueTemplateTooShort(this).Create(breakPeriod.StartTime); + + int previousObjectEndTimeIndex = endTimes.BinarySearch(breakPeriod.StartTime); + if (previousObjectEndTimeIndex < 0) previousObjectEndTimeIndex = ~previousObjectEndTimeIndex - 1; + + if (previousObjectEndTimeIndex >= 0) + { + double gapBeforeBreak = breakPeriod.StartTime - endTimes[previousObjectEndTimeIndex]; + if (gapBeforeBreak < minimum_gap_before_break - leniency_threshold) + yield return new IssueTemplateEarlyStart(this).Create(breakPeriod.StartTime, minimum_gap_before_break - gapBeforeBreak); + } + + int nextObjectStartTimeIndex = startTimes.BinarySearch(breakPeriod.EndTime); + if (nextObjectStartTimeIndex < 0) nextObjectStartTimeIndex = ~nextObjectStartTimeIndex; + + if (nextObjectStartTimeIndex < startTimes.Count) + { + double gapAfterBreak = startTimes[nextObjectStartTimeIndex] - breakPeriod.EndTime; + if (gapAfterBreak < min_end_threshold - leniency_threshold) + yield return new IssueTemplateLateEnd(this).Create(breakPeriod.StartTime, min_end_threshold - gapAfterBreak); + } + } + } + + public class IssueTemplateEarlyStart : IssueTemplate + { + public IssueTemplateEarlyStart(ICheck check) + : base(check, IssueType.Problem, "Break starts {0} ms early.") + { + } + + public Issue Create(double startTime, double diff) => new Issue(startTime, this, (int)diff); + } + + public class IssueTemplateLateEnd : IssueTemplate + { + public IssueTemplateLateEnd(ICheck check) + : base(check, IssueType.Problem, "Break ends {0} ms late.") + { + } + + public Issue Create(double startTime, double diff) => new Issue(startTime, this, (int)diff); + } + + public class IssueTemplateTooShort : IssueTemplate + { + public IssueTemplateTooShort(ICheck check) + : base(check, IssueType.Warning, "Break is non-functional due to being less than {0} ms.") + { + } + + public Issue Create(double startTime) => new Issue(startTime, this, BreakPeriod.MIN_BREAK_DURATION); + } + } +} diff --git a/osu.Game/Rulesets/Edit/Checks/CheckDrainLength.cs b/osu.Game/Rulesets/Edit/Checks/CheckDrainLength.cs new file mode 100644 index 0000000000..ac65dfadff --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckDrainLength.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 osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckDrainLength : ICheck + { + private const int min_drain_threshold = 30 * 1000; + + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Compose, "Drain length is too short"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateTooShort(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + double drainTime = context.Beatmap.CalculateDrainLength(); + + if (drainTime < min_drain_threshold) + yield return new IssueTemplateTooShort(this).Create((int)(drainTime / 1000)); + } + + public class IssueTemplateTooShort : IssueTemplate + { + public IssueTemplateTooShort(ICheck check) + : base(check, IssueType.Problem, "Less than 30 seconds of drain time, currently {0}.") + { + } + + public Issue Create(int drainTimeSeconds) => new Issue(this, drainTimeSeconds); + } + } +} diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs similarity index 71% rename from osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs rename to osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index a8972775de..ddf539771d 100644 --- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -1,8 +1,7 @@ // 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.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -24,16 +23,13 @@ using osu.Game.Overlays.OSD; using osu.Game.Overlays.Settings.Sections; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.TernaryButtons; namespace osu.Game.Rulesets.Edit { - /// - /// Represents a for rulesets with the concept of distances between objects. - /// - /// The base type of supported objects. - public abstract partial class DistancedHitObjectComposer : HitObjectComposer, IDistanceSnapProvider, IScrollBindingHandler - where TObject : HitObject + public abstract partial class ComposerDistanceSnapProvider : Component, IDistanceSnapProvider, IScrollBindingHandler { private const float adjust_step = 0.1f; @@ -44,27 +40,38 @@ namespace osu.Game.Rulesets.Edit Precision = 0.01, }; - IBindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier; + Bindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier; - private ExpandableSlider> distanceSpacingSlider; - private ExpandableButton currentDistanceSpacingButton; + private ExpandableSlider> distanceSpacingSlider = null!; + private ExpandableButton currentDistanceSpacingButton = null!; - [Resolved(canBeNull: true)] - private OnScreenDisplay onScreenDisplay { get; set; } + [Resolved] + private Playfield playfield { get; set; } = null!; - protected readonly Bindable DistanceSnapToggle = new Bindable(); + [Resolved] + private EditorClock editorClock { get; set; } = null!; + + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; + + [Resolved] + private IBeatSnapProvider beatSnapProvider { get; set; } = null!; + + [Resolved] + private OnScreenDisplay? onScreenDisplay { get; set; } + + public readonly Bindable DistanceSnapToggle = new Bindable(); private bool distanceSnapMomentary; - protected DistancedHitObjectComposer(Ruleset ruleset) - : base(ruleset) - { - } + private EditorToolboxGroup? toolboxGroup; - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + public void AttachToToolbox(ExpandingToolboxContainer toolboxContainer) { - RightToolbox.Add(new EditorToolboxGroup("snapping") + if (toolboxGroup != null) + throw new InvalidOperationException($"{nameof(AttachToToolbox)} may be called only once for a single {nameof(ComposerDistanceSnapProvider)} instance."); + + toolboxContainer.Add(toolboxGroup = new EditorToolboxGroup("snapping") { Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1, Children = new Drawable[] @@ -90,16 +97,38 @@ namespace osu.Game.Rulesets.Edit } } }); + + DistanceSpacingMultiplier.Value = editorBeatmap.BeatmapInfo.DistanceSpacing; + DistanceSpacingMultiplier.BindValueChanged(multiplier => + { + distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})"; + distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({multiplier.NewValue:0.##x})"; + + if (multiplier.NewValue != multiplier.OldValue) + onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier)); + + editorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue; + }, true); + + DistanceSpacingMultiplier.BindDisabledChanged(disabled => distanceSpacingSlider.Alpha = disabled ? 0 : 1, true); + + // Manual binding to handle enabling distance spacing when the slider is interacted with. + distanceSpacingSlider.Current.BindValueChanged(spacing => + { + DistanceSpacingMultiplier.Value = spacing.NewValue; + DistanceSnapToggle.Value = TernaryState.True; + }); + DistanceSpacingMultiplier.BindValueChanged(spacing => distanceSpacingSlider.Current.Value = spacing.NewValue); } private (HitObject before, HitObject after)? getObjectsOnEitherSideOfCurrentTime() { - HitObject lastBefore = Playfield.HitObjectContainer.AliveObjects.LastOrDefault(h => h.HitObject.StartTime < EditorClock.CurrentTime)?.HitObject; + HitObject? lastBefore = playfield.HitObjectContainer.AliveObjects.LastOrDefault(h => h.HitObject.StartTime < editorClock.CurrentTime)?.HitObject; if (lastBefore == null) return null; - HitObject firstAfter = Playfield.HitObjectContainer.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime >= EditorClock.CurrentTime)?.HitObject; + HitObject? firstAfter = playfield.HitObjectContainer.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime >= editorClock.CurrentTime)?.HitObject; if (firstAfter == null) return null; @@ -125,6 +154,7 @@ namespace osu.Game.Rulesets.Edit if (currentSnap > DistanceSpacingMultiplier.MinValue) { currentDistanceSpacingButton.Enabled.Value = currentDistanceSpacingButton.Expanded.Value + && !DistanceSpacingMultiplier.Disabled && !Precision.AlmostEquals(currentSnap, DistanceSpacingMultiplier.Value, DistanceSpacingMultiplier.Precision / 2); currentDistanceSpacingButton.ContractedLabelText = $"current {currentSnap:N2}x"; currentDistanceSpacingButton.ExpandedLabelText = $"Use current ({currentSnap:N2}x)"; @@ -137,38 +167,10 @@ namespace osu.Game.Rulesets.Edit } } - protected override void LoadComplete() - { - base.LoadComplete(); - - if (!DistanceSpacingMultiplier.Disabled) - { - DistanceSpacingMultiplier.Value = EditorBeatmap.BeatmapInfo.DistanceSpacing; - DistanceSpacingMultiplier.BindValueChanged(multiplier => - { - distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})"; - distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({multiplier.NewValue:0.##x})"; - - if (multiplier.NewValue != multiplier.OldValue) - onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier)); - - EditorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue; - }, true); - - // Manual binding to handle enabling distance spacing when the slider is interacted with. - distanceSpacingSlider.Current.BindValueChanged(spacing => - { - DistanceSpacingMultiplier.Value = spacing.NewValue; - DistanceSnapToggle.Value = TernaryState.True; - }); - DistanceSpacingMultiplier.BindValueChanged(spacing => distanceSpacingSlider.Current.Value = spacing.NewValue); - } - } - - protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[] + public IEnumerable CreateTernaryButtons() => new[] { new TernaryButton(DistanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }) - }); + }; protected override bool OnKeyDown(KeyDownEvent e) { @@ -238,26 +240,28 @@ namespace osu.Game.Rulesets.Edit return true; } + #region IDistanceSnapProvider + public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true) { - return (float)(100 * (useReferenceSliderVelocity && referenceObject is IHasSliderVelocity hasSliderVelocity ? hasSliderVelocity.SliderVelocity : 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1 - / BeatSnapProvider.BeatDivisor); + return (float)(100 * (useReferenceSliderVelocity && referenceObject is IHasSliderVelocity hasSliderVelocity ? hasSliderVelocity.SliderVelocityMultiplier : 1) * editorBeatmap.Difficulty.SliderMultiplier * 1 + / beatSnapProvider.BeatDivisor); } public virtual float DurationToDistance(HitObject referenceObject, double duration) { - double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); + double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceObject)); } public virtual double DistanceToDuration(HitObject referenceObject, float distance) { - double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); + double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength; } public virtual double FindSnappedDuration(HitObject referenceObject, float distance) - => BeatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime; + => beatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime; public virtual float FindSnappedDistance(HitObject referenceObject, float distance) { @@ -265,9 +269,9 @@ namespace osu.Game.Rulesets.Edit double actualDuration = startTime + DistanceToDuration(referenceObject, distance); - double snappedEndTime = BeatSnapProvider.SnapTime(actualDuration, startTime); + double snappedEndTime = beatSnapProvider.SnapTime(actualDuration, startTime); - double beatLength = BeatSnapProvider.GetBeatLengthAtTime(startTime); + double beatLength = beatSnapProvider.GetBeatLengthAtTime(startTime); // we don't want to exceed the actual duration and snap to a point in the future. // as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it. @@ -277,6 +281,8 @@ namespace osu.Game.Rulesets.Edit return DurationToDistance(referenceObject, snappedEndTime - startTime); } + #endregion + private partial class DistanceSpacingToast : Toast { private readonly ValueChangedEvent change; diff --git a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs index 20ee409937..174b278d89 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs @@ -1,10 +1,9 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Game.Rulesets.Mods; @@ -25,7 +24,7 @@ namespace osu.Game.Rulesets.Edit private readonly DrawableRuleset drawableRuleset; [Resolved] - private EditorBeatmap beatmap { get; set; } + private EditorBeatmap beatmap { get; set; } = null!; public DrawableEditorRulesetWrapper(DrawableRuleset drawableRuleset) { @@ -43,8 +42,8 @@ namespace osu.Game.Rulesets.Edit Playfield.DisplayJudgements.Value = false; } - [Resolved(canBeNull: true)] - private IEditorChangeHandler changeHandler { get; set; } + [Resolved] + private IEditorChangeHandler? changeHandler { get; set; } protected override void LoadComplete() { @@ -94,7 +93,7 @@ namespace osu.Game.Rulesets.Edit { base.Dispose(isDisposing); - if (beatmap != null) + if (beatmap.IsNotNull()) { beatmap.HitObjectAdded -= addHitObject; beatmap.HitObjectRemoved -= removeHitObject; diff --git a/osu.Game/Rulesets/Edit/EditorToolboxGroup.cs b/osu.Game/Rulesets/Edit/EditorToolboxGroup.cs index 312ba62b61..f30f5148fe 100644 --- a/osu.Game/Rulesets/Edit/EditorToolboxGroup.cs +++ b/osu.Game/Rulesets/Edit/EditorToolboxGroup.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Game.Overlays; diff --git a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs index 7bf10f6beb..36cbf49885 100644 --- a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs +++ b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 6fcb8f62ee..07e5869e28 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -44,6 +44,11 @@ namespace osu.Game.Rulesets.Edit public abstract partial class HitObjectComposer : HitObjectComposer, IPlacementHandler where TObject : HitObject { + /// + /// Whether the playfield should be centered horizontally. Should be disabled for playfields which span the full horizontal width. + /// + protected virtual bool ApplyHorizontalCentering => true; + protected IRulesetConfigManager Config { get; private set; } // Provides `Playfield` @@ -71,7 +76,7 @@ namespace osu.Game.Rulesets.Edit protected readonly Container LayerBelowRuleset = new Container { RelativeSizeAxes = Axes.Both }; - private InputManager inputManager; + protected InputManager InputManager { get; private set; } private EditorRadioButtonCollection toolboxCollection; @@ -82,6 +87,8 @@ namespace osu.Game.Rulesets.Edit private IBindable hasTiming; private Bindable autoSeekOnPlacement; + protected DrawableRuleset DrawableRuleset { get; private set; } + protected HitObjectComposer(Ruleset ruleset) : base(ruleset) { @@ -99,7 +106,8 @@ namespace osu.Game.Rulesets.Edit try { - drawableRulesetWrapper = new DrawableEditorRulesetWrapper(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap, new[] { Ruleset.GetAutoplayMod() })) + DrawableRuleset = CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap, new[] { Ruleset.GetAutoplayMod() }); + drawableRulesetWrapper = new DrawableEditorRulesetWrapper(DrawableRuleset) { Clock = EditorClock, ProcessCustomClock = false @@ -111,19 +119,17 @@ namespace osu.Game.Rulesets.Edit return; } + if (DrawableRuleset is IDrawableScrollingRuleset scrollingRuleset) + dependencies.CacheAs(scrollingRuleset.ScrollingInfo); + dependencies.CacheAs(Playfield); - InternalChildren = new Drawable[] + InternalChildren = new[] { PlayfieldContentContainer = new Container { - Name = "Content", - Padding = new MarginPadding - { - Left = TOOLBOX_CONTRACTED_SIZE_LEFT, - Right = TOOLBOX_CONTRACTED_SIZE_RIGHT, - }, - RelativeSizeAxes = Axes.Both, + Name = "Playfield content", + RelativeSizeAxes = Axes.Y, Children = new Drawable[] { // layers below playfield @@ -198,7 +204,7 @@ namespace osu.Game.Rulesets.Edit }, } } - } + }, }; toolboxCollection.Items = CompositionTools @@ -229,7 +235,7 @@ namespace osu.Game.Rulesets.Edit { base.LoadComplete(); - inputManager = GetContainingInputManager(); + InputManager = GetContainingInputManager(); hasTiming = EditorBeatmap.HasTiming.GetBoundCopy(); hasTiming.BindValueChanged(timing => @@ -240,11 +246,34 @@ namespace osu.Game.Rulesets.Edit }); } + protected override void Update() + { + base.Update(); + + if (ApplyHorizontalCentering) + { + PlayfieldContentContainer.Anchor = Anchor.Centre; + PlayfieldContentContainer.Origin = Anchor.Centre; + + // Ensure that the playfield is always centered but also doesn't get cut off by toolboxes. + PlayfieldContentContainer.Width = Math.Max(1024, DrawWidth) - TOOLBOX_CONTRACTED_SIZE_RIGHT * 2; + PlayfieldContentContainer.X = 0; + } + else + { + PlayfieldContentContainer.Anchor = Anchor.CentreLeft; + PlayfieldContentContainer.Origin = Anchor.CentreLeft; + + PlayfieldContentContainer.Width = Math.Max(1024, DrawWidth) - (TOOLBOX_CONTRACTED_SIZE_LEFT + TOOLBOX_CONTRACTED_SIZE_RIGHT); + PlayfieldContentContainer.X = TOOLBOX_CONTRACTED_SIZE_LEFT; + } + } + public override Playfield Playfield => drawableRulesetWrapper.Playfield; public override IEnumerable HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects; - public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position); + public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(InputManager.CurrentState.Mouse.Position); /// /// Defines all available composition tools, listed on the left side of the editor screen as button controls. @@ -281,7 +310,7 @@ namespace osu.Game.Rulesets.Edit /// The loaded beatmap. /// The mods to be applied. /// An editor-relevant . - protected virtual DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + protected virtual DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) => (DrawableRuleset)ruleset.CreateDrawableRulesetWith(beatmap, mods); #region Tool selection logic @@ -472,7 +501,7 @@ namespace osu.Game.Rulesets.Edit public abstract partial class HitObjectComposer : CompositeDrawable, IPositionSnapProvider { public const float TOOLBOX_CONTRACTED_SIZE_LEFT = 60; - public const float TOOLBOX_CONTRACTED_SIZE_RIGHT = 130; + public const float TOOLBOX_CONTRACTED_SIZE_RIGHT = 120; public readonly Ruleset Ruleset; diff --git a/osu.Game/Rulesets/Edit/IBeatmapVerifier.cs b/osu.Game/Rulesets/Edit/IBeatmapVerifier.cs index 826bffef5f..7fc9772598 100644 --- a/osu.Game/Rulesets/Edit/IBeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/IBeatmapVerifier.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Game.Rulesets.Edit.Checks.Components; diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index 6fbd994e23..380038eadf 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; @@ -14,14 +12,14 @@ namespace osu.Game.Rulesets.Edit /// A snap provider which given a reference hit object and proposed distance from it, offers a more correct duration or distance value. /// [Cached] - public interface IDistanceSnapProvider : IPositionSnapProvider + public interface IDistanceSnapProvider { /// /// A multiplier which changes the ratio of distance travelled per time unit. /// Importantly, this is provided for manual usage, and not multiplied into any of the methods exposed by this interface. /// /// - IBindable DistanceSpacingMultiplier { get; } + Bindable DistanceSpacingMultiplier { get; } /// /// Retrieves the distance between two points within a timing point that are one beat length apart. diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs index ad129e068d..002a0aafe6 100644 --- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osuTK; diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 717c026ded..5cb9adfd72 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Edit /// protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent?.ReceivePositionalInputAt(screenSpacePos) == true; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; protected override bool Handle(UIEvent e) { diff --git a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs new file mode 100644 index 0000000000..eb73cef01a --- /dev/null +++ b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs @@ -0,0 +1,108 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Edit.Components.TernaryButtons; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; + +namespace osu.Game.Rulesets.Edit +{ + public abstract partial class ScrollingHitObjectComposer : HitObjectComposer + where TObject : HitObject + { + private readonly Bindable showSpeedChanges = new Bindable(); + private Bindable configShowSpeedChanges = null!; + + private BeatSnapGrid? beatSnapGrid; + + /// + /// Construct an optional beat snap grid. + /// + protected virtual BeatSnapGrid? CreateBeatSnapGrid() => null; + + protected ScrollingHitObjectComposer(Ruleset ruleset) + : base(ruleset) + { + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + if (DrawableRuleset is ISupportConstantAlgorithmToggle toggleRuleset) + { + LeftToolbox.Add(new EditorToolboxGroup("playfield") + { + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new[] + { + new DrawableTernaryButton(new TernaryButton(showSpeedChanges, "Show speed changes", () => new SpriteIcon { Icon = FontAwesome.Solid.TachometerAlt })) + } + }, + }); + + configShowSpeedChanges = config.GetBindable(OsuSetting.EditorShowSpeedChanges); + configShowSpeedChanges.BindValueChanged(enabled => showSpeedChanges.Value = enabled.NewValue ? TernaryState.True : TernaryState.False, true); + + showSpeedChanges.BindValueChanged(state => + { + bool enabled = state.NewValue == TernaryState.True; + + toggleRuleset.ShowSpeedChanges.Value = enabled; + configShowSpeedChanges.Value = enabled; + }, true); + } + + beatSnapGrid = CreateBeatSnapGrid(); + + if (beatSnapGrid != null) + AddInternal(beatSnapGrid); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + updateBeatSnapGrid(); + } + + private void updateBeatSnapGrid() + { + if (beatSnapGrid == null) + return; + + if (BlueprintContainer.CurrentTool is SelectTool) + { + if (EditorBeatmap.SelectedHitObjects.Any()) + { + beatSnapGrid.SelectionTimeRange = (EditorBeatmap.SelectedHitObjects.Min(h => h.StartTime), EditorBeatmap.SelectedHitObjects.Max(h => h.GetEndTime())); + } + else + beatSnapGrid.SelectionTimeRange = null; + } + else + { + var result = FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position); + if (result.Time is double time) + beatSnapGrid.SelectionTimeRange = (time, time); + else + beatSnapGrid.SelectionTimeRange = null; + } + } + } +} diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs index 3c878ffd33..3ed7558bcb 100644 --- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs +++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs @@ -5,6 +5,7 @@ using System; using System.Linq; +using JetBrains.Annotations; using osu.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -32,7 +33,7 @@ namespace osu.Game.Rulesets.Edit /// public event Action> Deselected; - public override bool HandlePositionalInput => ShouldBeAlive; + public override bool HandlePositionalInput => IsSelectable; public override bool RemoveWhenNotAlive => false; protected SelectionBlueprint(T item) @@ -51,6 +52,7 @@ namespace osu.Game.Rulesets.Edit private SelectionState state; + [CanBeNull] public event Action StateChanged; public SelectionState State @@ -125,6 +127,11 @@ namespace osu.Game.Rulesets.Edit /// public virtual MenuItem[] ContextMenuItems => Array.Empty(); + /// + /// Whether the can be currently selected via a click or a drag box. + /// + public virtual bool IsSelectable => ShouldBeAlive && IsPresent; + /// /// The screen-space main point that causes this to be selected via a drag. /// diff --git a/osu.Game/Rulesets/Edit/SnapType.cs b/osu.Game/Rulesets/Edit/SnapType.cs index f5f9ab0437..cf743f6ace 100644 --- a/osu.Game/Rulesets/Edit/SnapType.cs +++ b/osu.Game/Rulesets/Edit/SnapType.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; namespace osu.Game.Rulesets.Edit diff --git a/osu.Game/Rulesets/ILegacyRuleset.cs b/osu.Game/Rulesets/ILegacyRuleset.cs index f4b03baccd..6900afa243 100644 --- a/osu.Game/Rulesets/ILegacyRuleset.cs +++ b/osu.Game/Rulesets/ILegacyRuleset.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Scoring.Legacy; + namespace osu.Game.Rulesets { public interface ILegacyRuleset @@ -11,5 +13,7 @@ namespace osu.Game.Rulesets /// Identifies the server-side ID of a legacy ruleset. /// int LegacyID { get; } + + ILegacyScoreSimulator CreateLegacyScoreSimulator(); } } diff --git a/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs b/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs index f08b43e72a..2c78561d31 100644 --- a/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs +++ b/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Judgements diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index 770f656e8f..cd1e81046d 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.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 osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; @@ -37,7 +35,40 @@ namespace osu.Game.Rulesets.Judgements /// /// The minimum that can be achieved - the inverse of . /// - public HitResult MinResult + /// + /// Defaults to a sane value for the given . May be overridden to provide a supported custom value: + /// + /// + /// s + /// Valid s + /// + /// + /// , , , , + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// , + /// + /// + /// + public virtual HitResult MinResult { get { @@ -159,10 +190,9 @@ namespace osu.Game.Rulesets.Judgements return 200; case HitResult.Great: - return 300; - + // Perfect doesn't actually give more score / accuracy directly. case HitResult.Perfect: - return 315; + return 300; case HitResult.SmallBonus: return SMALL_BONUS_SCORE; diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs index 34d1f1f6e9..c67f8b9fd5 100644 --- a/osu.Game/Rulesets/Judgements/JudgementResult.cs +++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs @@ -1,10 +1,7 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 JetBrains.Annotations; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; @@ -24,13 +21,11 @@ namespace osu.Game.Rulesets.Judgements /// /// The which was judged. /// - [NotNull] public readonly HitObject HitObject; /// /// The which this applies for. /// - [NotNull] public readonly Judgement Judgement; /// @@ -99,7 +94,7 @@ namespace osu.Game.Rulesets.Judgements /// /// The which was judged. /// The to refer to for scoring information. - public JudgementResult([NotNull] HitObject hitObject, [NotNull] Judgement judgement) + public JudgementResult(HitObject hitObject, Judgement judgement) { HitObject = hitObject; Judgement = judgement; diff --git a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs index a941c0a1db..d04d7636ec 100644 --- a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs +++ b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs @@ -29,7 +29,14 @@ namespace osu.Game.Rulesets.Mods /// private readonly BindableNumber sliderDisplayCurrent = new BindableNumber(); - protected override Drawable CreateControl() => new SliderControl(sliderDisplayCurrent); + protected sealed override Drawable CreateControl() => new SliderControl(sliderDisplayCurrent, CreateSlider); + + protected virtual RoundedSliderBar CreateSlider(BindableNumber current) => new RoundedSliderBar + { + RelativeSizeAxes = Axes.X, + Current = current, + KeyboardStep = 0.1f, + }; /// /// Guards against beatmap values displayed on slider bars being transferred to user override. @@ -100,16 +107,11 @@ namespace osu.Game.Rulesets.Mods set => current.Current = value; } - public SliderControl(BindableNumber currentNumber) + public SliderControl(BindableNumber currentNumber, Func, RoundedSliderBar> createSlider) { InternalChildren = new Drawable[] { - new RoundedSliderBar - { - RelativeSizeAxes = Axes.X, - Current = currentNumber, - KeyboardStep = 0.1f, - } + createSlider(currentNumber) }; AutoSizeAxes = Axes.Y; diff --git a/osu.Game/Rulesets/Mods/DifficultyBindable.cs b/osu.Game/Rulesets/Mods/DifficultyBindable.cs index c21ce756c9..a207048882 100644 --- a/osu.Game/Rulesets/Mods/DifficultyBindable.cs +++ b/osu.Game/Rulesets/Mods/DifficultyBindable.cs @@ -34,9 +34,18 @@ namespace osu.Game.Rulesets.Mods set => CurrentNumber.Precision = value; } + private float minValue; + public float MinValue { - set => CurrentNumber.MinValue = value; + set + { + if (value == minValue) + return; + + minValue = value; + updateExtents(); + } } private float maxValue; @@ -49,7 +58,24 @@ namespace osu.Game.Rulesets.Mods return; maxValue = value; - updateMaxValue(); + updateExtents(); + } + } + + private float? extendedMinValue; + + /// + /// The minimum value to be used when extended limits are applied. + /// + public float? ExtendedMinValue + { + set + { + if (value == extendedMinValue) + return; + + extendedMinValue = value; + updateExtents(); } } @@ -66,7 +92,7 @@ namespace osu.Game.Rulesets.Mods return; extendedMaxValue = value; - updateMaxValue(); + updateExtents(); } } @@ -78,7 +104,7 @@ namespace osu.Game.Rulesets.Mods public DifficultyBindable(float? defaultValue = null) : base(defaultValue) { - ExtendedLimits.BindValueChanged(_ => updateMaxValue()); + ExtendedLimits.BindValueChanged(_ => updateExtents()); } public override float? Value @@ -94,8 +120,9 @@ namespace osu.Game.Rulesets.Mods } } - private void updateMaxValue() + private void updateExtents() { + CurrentNumber.MinValue = ExtendedLimits.Value && extendedMinValue != null ? extendedMinValue.Value : minValue; CurrentNumber.MaxValue = ExtendedLimits.Value && extendedMaxValue != null ? extendedMaxValue.Value : maxValue; } diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index 05b2510e53..ce2d123884 100644 --- a/osu.Game/Rulesets/Mods/IMod.cs +++ b/osu.Game/Rulesets/Mods/IMod.cs @@ -19,6 +19,13 @@ namespace osu.Game.Rulesets.Mods /// string Name { get; } + /// + /// Short important information to display on the mod icon. For example, a rate adjust mod's rate + /// or similarly important setting. + /// Use if the icon should not display any additional info. + /// + string ExtendedIconInformation { get; } + /// /// The user readable description of this mod. /// diff --git a/osu.Game/Rulesets/Mods/MetronomeBeat.cs b/osu.Game/Rulesets/Mods/MetronomeBeat.cs index 265970ea46..5615362d1a 100644 --- a/osu.Game/Rulesets/Mods/MetronomeBeat.cs +++ b/osu.Game/Rulesets/Mods/MetronomeBeat.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mods int timeSignature = timingPoint.TimeSignature.Numerator; // play metronome from one measure before the first object. - if (BeatSyncSource.Clock?.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature) + if (BeatSyncSource.Clock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature) return; sample.Frequency.Value = beatIndex % timeSignature == 0 ? 1 : 0.5f; diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index f9812d6c00..a0bdc9ff51 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -27,6 +27,9 @@ namespace osu.Game.Rulesets.Mods public abstract string Acronym { get; } + [JsonIgnore] + public virtual string ExtendedIconInformation => string.Empty; + [JsonIgnore] public virtual IconUsage? Icon => null; @@ -71,8 +74,21 @@ namespace osu.Game.Rulesets.Mods { var bindable = (IBindable)property.GetValue(this)!; + string valueText; + + switch (bindable) + { + case Bindable b: + valueText = b.Value ? "on" : "off"; + break; + + default: + valueText = bindable.ToString() ?? string.Empty; + break; + } + if (!bindable.IsDefault) - tooltipTexts.Add($"{attr.Label} {bindable}"); + tooltipTexts.Add($"{attr.Label}: {valueText}"); } return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s))); diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index e7127abcf0..607e6b8399 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -10,6 +10,7 @@ using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp), typeof(ModAutoplay) }; - [SettingSource("Initial rate", "The starting speed of the track")] + [SettingSource("Initial rate", "The starting speed of the track", SettingControlType = typeof(MultiplierSettingsSlider))] public BindableNumber InitialRate { get; } = new BindableDouble(1) { MinValue = 0.5, @@ -70,7 +71,6 @@ namespace osu.Game.Rulesets.Mods // Apply a fixed rate change when missing, allowing the player to catch up when the rate is too fast. private const double rate_change_on_miss = 0.95d; - private IAdjustableAudioComponent? track; private double targetRate = 1d; /// @@ -122,24 +122,27 @@ namespace osu.Game.Rulesets.Mods /// private readonly Dictionary ratesForRewinding = new Dictionary(); + private readonly RateAdjustModHelper rateAdjustHelper; + public ModAdaptiveSpeed() { + rateAdjustHelper = new RateAdjustModHelper(SpeedChange); + rateAdjustHelper.HandleAudioAdjustments(AdjustPitch); + InitialRate.BindValueChanged(val => { SpeedChange.Value = val.NewValue; targetRate = val.NewValue; }); - AdjustPitch.BindValueChanged(adjustPitchChanged); } public void ApplyToTrack(IAdjustableAudioComponent track) { - this.track = track; - InitialRate.TriggerChange(); - AdjustPitch.TriggerChange(); recentRates.Clear(); recentRates.AddRange(Enumerable.Repeat(InitialRate.Value, recent_rate_count)); + + rateAdjustHelper.ApplyToTrack(track); } public void ApplyToSample(IAdjustableAudioComponent sample) @@ -198,15 +201,6 @@ namespace osu.Game.Rulesets.Mods } } - private void adjustPitchChanged(ValueChangedEvent adjustPitchSetting) - { - track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); - track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); - } - - private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue) - => adjustPitchSettingValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo; - private IEnumerable getAllApplicableHitObjects(IEnumerable hitObjects) { foreach (var hitObject in hitObjects) diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 83afda3a28..a3a4adc53d 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -11,7 +11,7 @@ using osu.Game.Replays; namespace osu.Game.Rulesets.Mods { - public abstract class ModAutoplay : Mod, IApplicableFailOverride, ICreateReplayData + public abstract class ModAutoplay : Mod, ICreateReplayData { public override string Name => "Autoplay"; public override string Acronym => "AT"; @@ -20,15 +20,11 @@ namespace osu.Game.Rulesets.Mods public override LocalisableString Description => "Watch a perfect automated play through the song."; public override double ScoreMultiplier => 1; - public bool PerformFail() => false; - - public bool RestartOnFail => false; - public override bool UserPlayable => false; public override bool ValidForMultiplayer => false; public override bool ValidForMultiplayerAsFreeMod => false; - public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAdaptiveSpeed) }; + public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModAdaptiveSpeed) }; public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; diff --git a/osu.Game/Rulesets/Mods/ModDaycore.cs b/osu.Game/Rulesets/Mods/ModDaycore.cs index de1a5ab56c..39ebd1fe4c 100644 --- a/osu.Game/Rulesets/Mods/ModDaycore.cs +++ b/osu.Game/Rulesets/Mods/ModDaycore.cs @@ -5,21 +5,36 @@ using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Configuration; namespace osu.Game.Rulesets.Mods { - public abstract class ModDaycore : ModHalfTime + public abstract class ModDaycore : ModRateAdjust { public override string Name => "Daycore"; public override string Acronym => "DC"; public override IconUsage? Icon => null; + public override ModType Type => ModType.DifficultyReduction; public override LocalisableString Description => "Whoaaaaa..."; + [SettingSource("Speed decrease", "The actual decrease to apply")] + public override BindableNumber SpeedChange { get; } = new BindableDouble(0.75) + { + MinValue = 0.5, + MaxValue = 0.99, + Precision = 0.01, + }; + private readonly BindableNumber tempoAdjust = new BindableDouble(1); private readonly BindableNumber freqAdjust = new BindableDouble(1); + private readonly RateAdjustModHelper rateAdjustHelper; protected ModDaycore() { + rateAdjustHelper = new RateAdjustModHelper(SpeedChange); + + // intentionally not deferring the speed change handling to `RateAdjustModHelper` + // as the expected result of operation is not the same (daycore should preserve constant pitch). SpeedChange.BindValueChanged(val => { freqAdjust.Value = SpeedChange.Default; @@ -29,9 +44,10 @@ namespace osu.Game.Rulesets.Mods public override void ApplyToTrack(IAdjustableAudioComponent track) { - // base.ApplyToTrack() intentionally not called (different tempo adjustment is applied) track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); } + + public override double ScoreMultiplier => rateAdjustHelper.ScoreMultiplier; } } diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 733610c040..789291772d 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -1,11 +1,13 @@ // 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.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Overlays.Settings; namespace osu.Game.Rulesets.Mods { @@ -17,7 +19,7 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.DifficultyIncrease; public override LocalisableString Description => "Zoooooooooom..."; - [SettingSource("Speed increase", "The actual increase to apply")] + [SettingSource("Speed increase", "The actual increase to apply", SettingControlType = typeof(MultiplierSettingsSlider))] public override BindableNumber SpeedChange { get; } = new BindableDouble(1.5) { MinValue = 1.01, @@ -25,21 +27,22 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override double ScoreMultiplier + [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] + public virtual BindableBool AdjustPitch { get; } = new BindableBool(); + + private readonly RateAdjustModHelper rateAdjustHelper; + + protected ModDoubleTime() { - get - { - // Round to the nearest multiple of 0.1. - double value = (int)(SpeedChange.Value * 10) / 10.0; - - // Offset back to 0. - value -= 1; - - // Each 0.1 multiple changes score multiplier by 0.02. - value /= 5; - - return 1 + value; - } + rateAdjustHelper = new RateAdjustModHelper(SpeedChange); + rateAdjustHelper.HandleAudioAdjustments(AdjustPitch); } + + public override void ApplyToTrack(IAdjustableAudioComponent track) + { + rateAdjustHelper.ApplyToTrack(track); + } + + public override double ScoreMultiplier => rateAdjustHelper.ScoreMultiplier; } } diff --git a/osu.Game/Rulesets/Mods/ModExtensions.cs b/osu.Game/Rulesets/Mods/ModExtensions.cs index b22030414b..bd2d42f3eb 100644 --- a/osu.Game/Rulesets/Mods/ModExtensions.cs +++ b/osu.Game/Rulesets/Mods/ModExtensions.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; using osu.Game.Online.API.Requests.Responses; using osu.Game.Scoring; @@ -21,11 +22,16 @@ namespace osu.Game.Rulesets.Mods { User = new APIUser { - Id = APIUser.SYSTEM_USER_ID, + Id = replayData.User.OnlineID, Username = replayData.User.Username, + IsBot = replayData.User.IsBot, } } }; } + + public static IEnumerable AsOrdered(this IEnumerable mods) => mods + .OrderBy(m => m.Type) + .ThenBy(m => m.Acronym); } } diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs index 97789b7f5a..e671c065cf 100644 --- a/osu.Game/Rulesets/Mods/ModFailCondition.cs +++ b/osu.Game/Rulesets/Mods/ModFailCondition.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Mods { public abstract class ModFailCondition : Mod, IApplicableToHealthProcessor, IApplicableFailOverride { - public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) }; + public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax) }; [SettingSource("Restart on fail", "Automatically restarts when failed.")] public BindableBool Restart { get; } = new BindableBool(); diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 06c7750035..8b5dd39584 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -1,11 +1,13 @@ // 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.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Overlays.Settings; namespace osu.Game.Rulesets.Mods { @@ -17,7 +19,7 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.DifficultyReduction; public override LocalisableString Description => "Less zoom..."; - [SettingSource("Speed decrease", "The actual decrease to apply")] + [SettingSource("Speed decrease", "The actual decrease to apply", SettingControlType = typeof(MultiplierSettingsSlider))] public override BindableNumber SpeedChange { get; } = new BindableDouble(0.75) { MinValue = 0.5, @@ -25,18 +27,22 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override double ScoreMultiplier + [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] + public virtual BindableBool AdjustPitch { get; } = new BindableBool(); + + private readonly RateAdjustModHelper rateAdjustHelper; + + protected ModHalfTime() { - get - { - // Round to the nearest multiple of 0.1. - double value = (int)(SpeedChange.Value * 10) / 10.0; - - // Offset back to 0. - value -= 1; - - return 1 + value; - } + rateAdjustHelper = new RateAdjustModHelper(SpeedChange); + rateAdjustHelper.HandleAudioAdjustments(AdjustPitch); } + + public override void ApplyToTrack(IAdjustableAudioComponent track) + { + rateAdjustHelper.ApplyToTrack(track); + } + + public override double ScoreMultiplier => rateAdjustHelper.ScoreMultiplier; } } diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs index 2886e59c54..4b2d1d050e 100644 --- a/osu.Game/Rulesets/Mods/ModHardRock.cs +++ b/osu.Game/Rulesets/Mods/ModHardRock.cs @@ -18,17 +18,16 @@ namespace osu.Game.Rulesets.Mods public override LocalisableString Description => "Everything just got a bit harder..."; public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModDifficultyAdjust) }; + protected const float ADJUST_RATIO = 1.4f; + public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty) { } public virtual void ApplyToDifficulty(BeatmapDifficulty difficulty) { - const float ratio = 1.4f; - difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio. - difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ratio, 10.0f); - difficulty.DrainRate = Math.Min(difficulty.DrainRate * ratio, 10.0f); - difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ratio, 10.0f); + difficulty.DrainRate = Math.Min(difficulty.DrainRate * ADJUST_RATIO, 10.0f); + difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f); } } } diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 9b1f7d5cf7..b519ab4db7 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -11,6 +11,7 @@ using osu.Framework.Localisation; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Objects; @@ -19,22 +20,33 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Mods { - public abstract class ModNightcore : ModDoubleTime + public abstract class ModNightcore : ModRateAdjust { public override string Name => "Nightcore"; public override string Acronym => "NC"; public override IconUsage? Icon => OsuIcon.ModNightcore; + public override ModType Type => ModType.DifficultyIncrease; public override LocalisableString Description => "Uguuuuuuuu..."; - } - public abstract partial class ModNightcore : ModNightcore, IApplicableToDrawableRuleset - where TObject : HitObject - { + [SettingSource("Speed increase", "The actual increase to apply")] + public override BindableNumber SpeedChange { get; } = new BindableDouble(1.5) + { + MinValue = 1.01, + MaxValue = 2, + Precision = 0.01, + }; + private readonly BindableNumber tempoAdjust = new BindableDouble(1); private readonly BindableNumber freqAdjust = new BindableDouble(1); + private readonly RateAdjustModHelper rateAdjustHelper; + protected ModNightcore() { + rateAdjustHelper = new RateAdjustModHelper(SpeedChange); + + // intentionally not deferring the speed change handling to `RateAdjustModHelper` + // as the expected result of operation is not the same (nightcore should preserve constant pitch). SpeedChange.BindValueChanged(val => { freqAdjust.Value = SpeedChange.Default; @@ -44,11 +56,16 @@ namespace osu.Game.Rulesets.Mods public override void ApplyToTrack(IAdjustableAudioComponent track) { - // base.ApplyToTrack() intentionally not called (different tempo adjustment is applied) track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); } + public override double ScoreMultiplier => rateAdjustHelper.ScoreMultiplier; + } + + public abstract partial class ModNightcore : ModNightcore, IApplicableToDrawableRuleset + where TObject : HitObject + { public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { drawableRuleset.Overlays.Add(new NightcoreBeatContainer()); diff --git a/osu.Game/Rulesets/Mods/ModNoFail.cs b/osu.Game/Rulesets/Mods/ModNoFail.cs index 31bb4338b3..8c61d948a4 100644 --- a/osu.Game/Rulesets/Mods/ModNoFail.cs +++ b/osu.Game/Rulesets/Mods/ModNoFail.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.DifficultyReduction; public override LocalisableString Description => "You can't fail, no matter what."; public override double ScoreMultiplier => 0.5; - public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModAutoplay) }; + public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition) }; } } diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 7b55ba4ad0..fa1c143585 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -13,10 +13,7 @@ namespace osu.Game.Rulesets.Mods public abstract BindableNumber SpeedChange { get; } - public virtual void ApplyToTrack(IAdjustableAudioComponent track) - { - track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); - } + public abstract void ApplyToTrack(IAdjustableAudioComponent track); public virtual void ApplyToSample(IAdjustableAudioComponent sample) { @@ -28,5 +25,7 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp), typeof(ModAdaptiveSpeed), typeof(ModRateAdjust) }; public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x"; + + public override string ExtendedIconInformation => SettingDescription; } } diff --git a/osu.Game/Rulesets/Mods/ModScoreV2.cs b/osu.Game/Rulesets/Mods/ModScoreV2.cs new file mode 100644 index 0000000000..df83d96769 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModScoreV2.cs @@ -0,0 +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.Localisation; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// This mod is used strictly to mark osu!stable scores set with the "Score V2" mod active. + /// It should not be used in any real capacity going forward. + /// + public class ModScoreV2 : Mod + { + public override string Name => "Score V2"; + public override string Acronym => @"SV2"; + public override ModType Type => ModType.System; + public override LocalisableString Description => "Score set on earlier osu! versions with the V2 scoring algorithm active."; + public override double ScoreMultiplier => 1; + public override bool UserPlayable => false; + } +} diff --git a/osu.Game/Rulesets/Mods/ModSynesthesia.cs b/osu.Game/Rulesets/Mods/ModSynesthesia.cs new file mode 100644 index 0000000000..23cb135c50 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModSynesthesia.cs @@ -0,0 +1,19 @@ +// 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.Localisation; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// Mod that colours hitobjects based on the musical division they are on + /// + public class ModSynesthesia : Mod + { + public override string Name => "Synesthesia"; + public override string Acronym => "SY"; + public override LocalisableString Description => "Colours hit objects based on the rhythm."; + public override double ScoreMultiplier => 1; + public override ModType Type => ModType.Fun; + } +} diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 7285315c3b..d2772417db 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -7,6 +7,7 @@ using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Overlays.Settings; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mods @@ -20,10 +21,10 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 0.5; - [SettingSource("Initial rate", "The starting speed of the track")] + [SettingSource("Initial rate", "The starting speed of the track", SettingControlType = typeof(MultiplierSettingsSlider))] public abstract BindableNumber InitialRate { get; } - [SettingSource("Final rate", "The final speed to ramp to")] + [SettingSource("Final rate", "The final speed to ramp to", SettingControlType = typeof(MultiplierSettingsSlider))] public abstract BindableNumber FinalRate { get; } [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] @@ -43,21 +44,21 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - private IAdjustableAudioComponent? track; + private readonly RateAdjustModHelper rateAdjustHelper; protected ModTimeRamp() { + rateAdjustHelper = new RateAdjustModHelper(SpeedChange); + rateAdjustHelper.HandleAudioAdjustments(AdjustPitch); + // for preview purpose at song select. eventually we'll want to be able to update every frame. FinalRate.BindValueChanged(_ => applyRateAdjustment(double.PositiveInfinity), true); - AdjustPitch.BindValueChanged(applyPitchAdjustment); } public void ApplyToTrack(IAdjustableAudioComponent track) { - this.track = track; - + rateAdjustHelper.ApplyToTrack(track); FinalRate.TriggerChange(); - AdjustPitch.TriggerChange(); } public void ApplyToSample(IAdjustableAudioComponent sample) @@ -94,16 +95,5 @@ namespace osu.Game.Rulesets.Mods /// Adjust the rate along the specified ramp. /// private void applyRateAdjustment(double time) => SpeedChange.Value = ApplyToRate(time); - - private void applyPitchAdjustment(ValueChangedEvent adjustPitchSetting) - { - // remove existing old adjustment - track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); - - track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); - } - - private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue) - => adjustPitchSettingValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo; } } diff --git a/osu.Game/Rulesets/Mods/RateAdjustModHelper.cs b/osu.Game/Rulesets/Mods/RateAdjustModHelper.cs new file mode 100644 index 0000000000..ffd4de0e90 --- /dev/null +++ b/osu.Game/Rulesets/Mods/RateAdjustModHelper.cs @@ -0,0 +1,84 @@ +// 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.Audio; +using osu.Framework.Bindables; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// Provides common functionality shared across various rate adjust mods. + /// + public class RateAdjustModHelper : IApplicableToTrack + { + public readonly IBindableNumber SpeedChange; + + private IAdjustableAudioComponent? track; + + private BindableBool? adjustPitch; + + /// + /// The score multiplier for the current . + /// + public double ScoreMultiplier + { + get + { + // Round to the nearest multiple of 0.1. + double value = (int)(SpeedChange.Value * 10) / 10.0; + + // Offset back to 0. + value -= 1; + + if (SpeedChange.Value >= 1) + value /= 5; + + return 1 + value; + } + } + + /// + /// Construct a new . + /// + /// The main speed adjust parameter which is exposed to the user. + public RateAdjustModHelper(IBindableNumber speedChange) + { + SpeedChange = speedChange; + } + + /// + /// Setup audio track adjustments for a rate adjust mod. + /// Importantly, must be called when a track is obtained/changed for this to work. + /// + /// The "adjust pitch" setting as exposed to the user. + public void HandleAudioAdjustments(BindableBool adjustPitch) + { + this.adjustPitch = adjustPitch; + + // When switching between pitch adjust, we need to update adjustments to time-shift or frequency-scale. + adjustPitch.BindValueChanged(adjustPitchSetting => + { + track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); + track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); + + AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue) + => adjustPitchSettingValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo; + }); + } + + /// + /// Should be invoked when a track is obtained / changed. + /// + /// The new track. + /// If this method is called before . + public void ApplyToTrack(IAdjustableAudioComponent track) + { + if (adjustPitch == null) + throw new InvalidOperationException($"Must call {nameof(HandleAudioAdjustments)} first"); + + this.track = track; + adjustPitch.TriggerChange(); + } + } +} diff --git a/osu.Game/Rulesets/Objects/BezierConverter.cs b/osu.Game/Rulesets/Objects/BezierConverter.cs index ebee36a7db..0c878fa1fd 100644 --- a/osu.Game/Rulesets/Objects/BezierConverter.cs +++ b/osu.Game/Rulesets/Objects/BezierConverter.cs @@ -39,6 +39,13 @@ namespace osu.Game.Rulesets.Objects new[] { new Vector2d(1, 0), new Vector2d(1, 1.2447058f), new Vector2d(-0.8526471f, 2.118367f), new Vector2d(-2.6211002f, 7.854936e-06f), new Vector2d(-0.8526448f, -2.118357f), new Vector2d(1, -1.2447058f), new Vector2d(1, 0) }) }; + /// + /// Counts the number of segments in a slider path. + /// + /// The control points of the path. + /// The number of segments in a slider path. + public static int CountSegments(IList controlPoints) => controlPoints.Where((t, i) => t.Type != null && i < controlPoints.Count - 1).Count(); + /// /// Converts a slider path to bezier control point positions compatible with the legacy osu! client. /// diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 07c0d1f8a1..ce6475d3ce 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -24,6 +24,7 @@ using osu.Game.Rulesets.Objects.Pooling; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK.Graphics; @@ -98,9 +99,9 @@ namespace osu.Game.Rulesets.Objects.Drawables public virtual bool DisplayResult => true; /// - /// Whether this and all of its nested s have been judged. + /// The scoring result of this . /// - public bool AllJudged => Judged && NestedHitObjects.All(h => h.AllJudged); + public JudgementResult Result => Entry?.Result; /// /// Whether this has been hit. This occurs if is hit. @@ -112,12 +113,12 @@ namespace osu.Game.Rulesets.Objects.Drawables /// Whether this has been judged. /// Note: This does NOT include nested hitobjects. /// - public bool Judged => Result?.HasResult ?? true; + public bool Judged => Entry?.Judged ?? false; /// - /// The scoring result of this . + /// Whether this and all of its nested s have been judged. /// - public JudgementResult Result => Entry?.Result; + public bool AllJudged => Entry?.AllJudged ?? false; /// /// The relative X position of this hit object for sample playback balance adjustment. @@ -158,6 +159,26 @@ namespace osu.Game.Rulesets.Objects.Drawables /// internal bool IsInitialized; + /// + /// The minimum allowable volume for sample playback. + /// quieter than that will be forcibly played at this volume instead. + /// + /// + /// + /// Drawable hitobjects adding their own custom samples, or other sample playback sources + /// (i.e. ) must enforce this themselves. + /// + /// + /// This sample volume floor is present in stable, although it is set at 8% rather than 5%. + /// See: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Audio/AudioEngine.cs#L1070, + /// https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Audio/AudioEngine.cs#L1404-L1405. + /// The reason why it is 5% here is that the 8% cap was enforced in a silent manner + /// (i.e. the minimum selectable volume in the editor was 5%, but it would be played at 8% anyways), + /// which is confusing and arbitrary, so we're just doing 5% here at the cost of sacrificing strict parity. + /// + /// + public const int MINIMUM_SAMPLE_VOLUME = 5; + /// /// Creates a new . /// @@ -180,7 +201,10 @@ namespace osu.Game.Rulesets.Objects.Drawables comboColourBrightness.BindTo(gameplaySettings.ComboColourNormalisationAmount); // Explicit non-virtual function call in case a DrawableHitObject overrides AddInternal. - base.AddInternal(Samples = new PausableSkinnableSound()); + base.AddInternal(Samples = new PausableSkinnableSound + { + MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME + }); CurrentSkin = skinSource; CurrentSkin.SourceChanged += skinSourceChanged; @@ -218,6 +242,8 @@ namespace osu.Game.Rulesets.Objects.Drawables protected sealed override void OnApply(HitObjectLifetimeEntry entry) { + Debug.Assert(Entry != null); + // LifetimeStart is already computed using HitObjectLifetimeEntry's InitialLifetimeOffset. // We override this with DHO's InitialLifetimeOffset for a non-pooled DHO. if (entry is SyntheticHitObjectEntry) @@ -247,11 +273,16 @@ namespace osu.Game.Rulesets.Objects.Drawables drawableNested.ParentHitObject = this; nestedHitObjects.Add(drawableNested); + + // assume that synthetic entries are not pooled and therefore need to be managed from within the DHO. + // this is important for the correctness of value of flags such as `AllJudged`. + if (drawableNested.Entry is SyntheticHitObjectEntry syntheticNestedEntry) + Entry.NestedEntries.Add(syntheticNestedEntry); + AddNestedHitObject(drawableNested); } StartTimeBindable.BindTo(HitObject.StartTimeBindable); - StartTimeBindable.BindValueChanged(onStartTimeChanged); if (HitObject is IHasComboInformation combo) { @@ -290,6 +321,8 @@ namespace osu.Game.Rulesets.Objects.Drawables protected sealed override void OnFree(HitObjectLifetimeEntry entry) { + Debug.Assert(Entry != null); + StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable); if (HitObject is IHasComboInformation combo) @@ -300,9 +333,6 @@ namespace osu.Game.Rulesets.Objects.Drawables samplesBindable.UnbindFrom(HitObject.SamplesBindable); - // Changes in start time trigger state updates. When a new hitobject is applied, OnApply() automatically performs a state update anyway. - StartTimeBindable.ValueChanged -= onStartTimeChanged; - // When a new hitobject is applied, the samples will be cleared before re-populating. // In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply(). samplesBindable.CollectionChanged -= onSamplesChanged; @@ -318,8 +348,12 @@ namespace osu.Game.Rulesets.Objects.Drawables } nestedHitObjects.Clear(); + // clean up synthetic entries manually added in `Apply()`. + Entry.NestedEntries.RemoveAll(nestedEntry => nestedEntry is SyntheticHitObjectEntry); ClearNestedHitObjects(); + // Changes to `HitObject` properties trigger default application, which triggers `State` updates. + // When a new hitobject is applied, `OnApply()` automatically performs a state update. HitObject.DefaultsApplied -= onDefaultsApplied; entry.RevertResult -= onRevertResult; @@ -362,8 +396,6 @@ namespace osu.Game.Rulesets.Objects.Drawables private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples(); - private void onStartTimeChanged(ValueChangedEvent startTime) => updateState(State.Value, true); - private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result); private void onRevertResult() @@ -381,6 +413,15 @@ namespace osu.Game.Rulesets.Objects.Drawables Debug.Assert(Entry != null); Apply(Entry); + // Applied defaults indicate a change in hit object state. + // We need to update the judgement result time to the new end time + // and update state to ensure the hit object fades out at the correct time. + if (Result is not null) + { + Result.TimeOffset = 0; + updateState(State.Value, true); + } + DefaultsApplied?.Invoke(this); } @@ -654,6 +695,8 @@ namespace osu.Game.Rulesets.Objects.Drawables if (!Result.HasResult) throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}."); + HitResultExtensions.ValidateHitResultPair(Result.Judgement.MaxResult, Result.Judgement.MinResult); + if (!Result.Type.IsValidHitResult(Result.Judgement.MinResult, Result.Judgement.MaxResult)) { throw new InvalidOperationException( @@ -676,7 +719,7 @@ namespace osu.Game.Rulesets.Objects.Drawables protected bool UpdateResult(bool userTriggered) { // It's possible for input to get into a bad state when rewinding gameplay, so results should not be processed - if (Time.Elapsed < 0) + if ((Clock as IGameplayClock)?.IsRewinding == true) return false; if (Judged) diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index ed3d3a6eb2..ec2a4a31f6 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -76,12 +76,6 @@ namespace osu.Game.Rulesets.Objects /// public virtual IList AuxiliarySamples => ImmutableList.Empty; - /// - /// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it. - /// DO NOT USE THIS UNLESS 100% SURE. - /// - public double? LegacyBpmMultiplier { get; set; } - /// /// Whether this is in Kiai time. /// diff --git a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs index b517f6b9e6..4450f026b4 100644 --- a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs +++ b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics.Performance; using osu.Game.Rulesets.Judgements; @@ -19,12 +21,28 @@ namespace osu.Game.Rulesets.Objects /// public readonly HitObject HitObject; + /// + /// The list of for the 's nested objects (if any). + /// + public List NestedEntries { get; internal set; } = new List(); + /// /// The result that was judged with. /// This is set by the accompanying , and reused when required for rewinding. /// internal JudgementResult? Result; + /// + /// Whether has been judged. + /// Note: This does NOT include nested hitobjects. + /// + public bool Judged => Result?.HasResult ?? false; + + /// + /// Whether and all of its nested objects have been judged. + /// + public bool AllJudged => Judged && NestedEntries.All(h => h.AllJudged); + private readonly IBindable startTimeBindable = new BindableDouble(); internal event Action? RevertResult; diff --git a/osu.Game/Rulesets/Objects/HitObjectParser.cs b/osu.Game/Rulesets/Objects/HitObjectParser.cs index 9728a4393b..c6e250bd74 100644 --- a/osu.Game/Rulesets/Objects/HitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/HitObjectParser.cs @@ -1,12 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Rulesets.Objects { public abstract class HitObjectParser { - public abstract HitObject Parse(string text); + public abstract HitObject? Parse(string text); } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs index 9facfec96f..12b4812824 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.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 osu.Game.Rulesets.Objects.Types; using osuTK; diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs index 62726019bb..fb1afed3b4 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.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 osu.Game.Rulesets.Objects.Types; using osuTK; diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs index cccb66d92b..014494ec54 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.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 osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Objects.Legacy.Catch diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs index d95f97624d..54dbd28c76 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 8eda2a8f61..d20f2d31bb 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -44,7 +44,6 @@ namespace osu.Game.Rulesets.Objects.Legacy FormatVersion = formatVersion; } - [CanBeNull] public override HitObject Parse(string text) { string[] split = text.Split(','); @@ -191,7 +190,12 @@ namespace osu.Game.Rulesets.Objects.Legacy string[] split = str.Split(':'); var bank = (LegacySampleBank)Parsing.ParseInt(split[0]); + if (!Enum.IsDefined(bank)) + bank = LegacySampleBank.Normal; + var addBank = (LegacySampleBank)Parsing.ParseInt(split[1]); + if (!Enum.IsDefined(addBank)) + addBank = LegacySampleBank.Normal; string stringBank = bank.ToString().ToLowerInvariant(); if (stringBank == @"none") diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index 7ddd372dc9..683eefa8f4 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -13,7 +13,7 @@ using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Objects.Legacy { - internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset, IHasSliderVelocity + internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasSliderVelocity { /// /// Scoring distance with a speed-adjusted beat length of 1 second. @@ -23,11 +23,12 @@ namespace osu.Game.Rulesets.Objects.Legacy /// /// s don't need a curve since they're converted to ruleset-specific hitobjects. /// - public SliderPath Path { get; set; } + public SliderPath Path { get; set; } = null!; public double Distance => Path.Distance; - public IList> NodeSamples { get; set; } + public IList> NodeSamples { get; set; } = null!; + public int RepeatCount { get; set; } [JsonIgnore] @@ -41,12 +42,12 @@ namespace osu.Game.Rulesets.Objects.Legacy public double Velocity = 1; - public BindableNumber SliderVelocityBindable { get; } = new BindableDouble(1); + public BindableNumber SliderVelocityMultiplierBindable { get; } = new BindableDouble(1); - public double SliderVelocity + public double SliderVelocityMultiplier { - get => SliderVelocityBindable.Value; - set => SliderVelocityBindable.Value = value; + get => SliderVelocityMultiplierBindable.Value; + set => SliderVelocityMultiplierBindable.Value = value; } protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) @@ -55,11 +56,9 @@ namespace osu.Game.Rulesets.Objects.Legacy TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * SliderVelocity; + double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * SliderVelocityMultiplier; Velocity = scoringDistance / timingPoint.BeatLength; } - - public double LegacyLastTickOffset => 36; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs b/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs new file mode 100644 index 0000000000..53cf835248 --- /dev/null +++ b/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs @@ -0,0 +1,61 @@ +// 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.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects.Types; + +namespace osu.Game.Rulesets.Objects.Legacy +{ + public static class LegacyRulesetExtensions + { + /// + /// Introduces floating-point errors to post-multiplied beat length for legacy rulesets that depend on it. + /// You should definitely not use this unless you know exactly what you're doing. + /// + public static double GetPrecisionAdjustedBeatLength(IHasSliderVelocity hasSliderVelocity, TimingControlPoint timingControlPoint, string rulesetShortName) + { + double sliderVelocityAsBeatLength = -100 / hasSliderVelocity.SliderVelocityMultiplier; + + // Note: In stable, the division occurs on floats, but with compiler optimisations turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?). + double bpmMultiplier; + + switch (rulesetShortName) + { + case "taiko": + case "mania": + bpmMultiplier = sliderVelocityAsBeatLength < 0 ? Math.Clamp((float)-sliderVelocityAsBeatLength, 10, 10000) / 100.0 : 1; + break; + + case "osu": + case "fruits": + bpmMultiplier = sliderVelocityAsBeatLength < 0 ? Math.Clamp((float)-sliderVelocityAsBeatLength, 10, 1000) / 100.0 : 1; + break; + + default: + throw new ArgumentException("Must be a legacy ruleset", nameof(rulesetShortName)); + } + + return timingControlPoint.BeatLength * bpmMultiplier; + } + + /// + /// Calculates scale from a CS value, with an optional fudge that was historically applied to the osu! ruleset. + /// + public static float CalculateScaleFromCircleSize(float circleSize, bool applyFudge = false) + { + // The following comment is copied verbatim from osu-stable: + // + // Builds of osu! up to 2013-05-04 had the gamefield being rounded down, which caused incorrect radius calculations + // in widescreen cases. This ratio adjusts to allow for old replays to work post-fix, which in turn increases the lenience + // for all plays, but by an amount so small it should only be effective in replays. + // + // To match expectations of gameplay we need to apply this multiplier to circle scale. It's weird but is what it is. + // It works out to under 1 game pixel and is generally not meaningful to gameplay, but is to replay playback accuracy. + const float broken_gamefield_rounding_allowance = 1.00041f; + + return (float)(1.0f - 0.7f * IBeatmapDifficultyInfo.DifficultyRange(circleSize)) / 2 * (applyFudge ? broken_gamefield_rounding_allowance : 1); + } + } +} diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs index 6f1968b41d..386eb8d3ee 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.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 osuTK; using osu.Game.Audio; using System.Collections.Generic; diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs index b6594d0206..2fa4766c1d 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.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 osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Objects.Legacy.Mania diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs index dcbaf22c51..c05aaceb9c 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.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 osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Objects.Legacy.Mania diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs index 33b390e3ba..069366bad3 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.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 osu.Game.Rulesets.Objects.Types; using osuTK; diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs index 2f8e9dd352..790af6cfc1 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.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 osu.Game.Rulesets.Objects.Types; using osuTK; @@ -11,7 +9,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu /// /// Legacy osu! Slider-type, used for parsing Beatmaps. /// - internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition, IHasCombo + internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition, IHasCombo, IHasGenerateTicks { public Vector2 Position { get; set; } @@ -22,5 +20,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public bool NewCombo { get; set; } public int ComboOffset { get; set; } + + public bool GenerateTicks { get; set; } = true; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs index d49e9fe9db..e9e5ca8c94 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.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 osu.Game.Rulesets.Objects.Types; using osuTK; diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs index ec8d7971ec..1d5ecb1ef3 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.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 osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Objects.Legacy.Taiko diff --git a/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs b/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs index 6c39ea44da..fabf4fc444 100644 --- a/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs +++ b/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs @@ -43,11 +43,6 @@ namespace osu.Game.Rulesets.Objects.Pooling /// private readonly Dictionary parentMap = new Dictionary(); - /// - /// Stores the list of child entries for each hit object managed by this . - /// - private readonly Dictionary> childrenMap = new Dictionary>(); - public void Add(HitObjectLifetimeEntry entry, HitObject? parent) { HitObject hitObject = entry.HitObject; @@ -57,22 +52,24 @@ namespace osu.Game.Rulesets.Objects.Pooling // Add the entry. entryMap[hitObject] = entry; - childrenMap[hitObject] = new List(); // If the entry has a parent, set it and add the entry to the parent's children. if (parent != null) { parentMap[entry] = parent; - if (childrenMap.TryGetValue(parent, out var parentChildEntries)) - parentChildEntries.Add(entry); + if (entryMap.TryGetValue(parent, out var parentEntry)) + parentEntry.NestedEntries.Add(entry); } hitObject.DefaultsApplied += onDefaultsApplied; OnEntryAdded?.Invoke(entry, parent); } - public void Remove(HitObjectLifetimeEntry entry) + public bool Remove(HitObjectLifetimeEntry entry) { + if (entry is SyntheticHitObjectEntry) + return false; + HitObject hitObject = entry.HitObject; if (!entryMap.ContainsKey(hitObject)) @@ -81,18 +78,16 @@ namespace osu.Game.Rulesets.Objects.Pooling entryMap.Remove(hitObject); // If the entry has a parent, unset it and remove the entry from the parents' children. - if (parentMap.Remove(entry, out var parent) && childrenMap.TryGetValue(parent, out var parentChildEntries)) - parentChildEntries.Remove(entry); + if (parentMap.Remove(entry, out var parent) && entryMap.TryGetValue(parent, out var parentEntry)) + parentEntry.NestedEntries.Remove(entry); // Remove all the entries' children. - if (childrenMap.Remove(hitObject, out var childEntries)) - { - foreach (var childEntry in childEntries) - Remove(childEntry); - } + foreach (var childEntry in entry.NestedEntries) + Remove(childEntry); hitObject.DefaultsApplied -= onDefaultsApplied; OnEntryRemoved?.Invoke(entry, parent); + return true; } public bool TryGet(HitObject hitObject, [MaybeNullWhen(false)] out HitObjectLifetimeEntry entry) @@ -105,16 +100,16 @@ namespace osu.Game.Rulesets.Objects.Pooling /// private void onDefaultsApplied(HitObject hitObject) { - if (!childrenMap.Remove(hitObject, out var childEntries)) + if (!entryMap.TryGetValue(hitObject, out var entry)) return; - // Remove all the entries' children. At this point the parents' (this entries') children list has been removed from the map, so this does not cause upwards traversal. - foreach (var entry in childEntries) - Remove(entry); + // Replace the entire list rather than clearing to prevent circular traversal later. + var previousEntries = entry.NestedEntries; + entry.NestedEntries = new List(); - // The removed children list needs to be added back to the map for the entry to potentially receive children. - childEntries.Clear(); - childrenMap[hitObject] = childEntries; + // Remove all the entries' children. At this point the parents' (this entries') children list has been reconstructed, so this does not cause upwards traversal. + foreach (var nested in previousEntries) + Remove(nested); } } } diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index d32a7cb16d..9b8375f208 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; @@ -12,9 +10,21 @@ namespace osu.Game.Rulesets.Objects { public static class SliderEventGenerator { - // ReSharper disable once MethodOverloadWithOptionalParameter + /// + /// Historically, slider's final tick (aka the place where the slider would receive a final judgement) was offset by -36 ms. Originally this was + /// done to workaround a technical detail (unimportant), but over the years it has become an expectation of players that you don't need to hold + /// until the true end of the slider. This very small amount of leniency makes it easier to jump away from fast sliders to the next hit object. + /// + /// After discussion on how this should be handled going forward, players have unanimously stated that this lenience should remain in some way. + /// These days, this is implemented in the drawable implementation of Slider in the osu! ruleset. + /// + /// We need to keep the *only* for osu!catch conversion, which relies on it to generate tiny ticks + /// correctly. + /// + public const double TAIL_LENIENCY = -36; + public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, - double? legacyLastTickOffset, CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default) { // A very lenient maximum length of a slider for ticks to be generated. // This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage. @@ -78,18 +88,27 @@ namespace osu.Game.Rulesets.Objects int finalSpanIndex = spanCount - 1; double finalSpanStartTime = startTime + finalSpanIndex * spanDuration; - double finalSpanEndTime = Math.Max(startTime + totalDuration / 2, (finalSpanStartTime + spanDuration) - (legacyLastTickOffset ?? 0)); - double finalProgress = (finalSpanEndTime - finalSpanStartTime) / spanDuration; - if (spanCount % 2 == 0) finalProgress = 1 - finalProgress; + // Note that `finalSpanStartTime + spanDuration ≈ startTime + totalDuration`, but we write it like this to match floating point precision + // of stable. + // + // So thinking about this in a saner way, the time of the LegacyLastTick is + // + // `slider.StartTime + max(slider.Duration / 2, slider.Duration - 36)` + // + // As a slider gets shorter than 72 ms, the leniency offered falls below the 36 ms `TAIL_LENIENCY` constant. + double legacyLastTickTime = Math.Max(startTime + totalDuration / 2, (finalSpanStartTime + spanDuration) + TAIL_LENIENCY); + double legacyLastTickProgress = (legacyLastTickTime - finalSpanStartTime) / spanDuration; + + if (spanCount % 2 == 0) legacyLastTickProgress = 1 - legacyLastTickProgress; yield return new SliderEventDescriptor { Type = SliderEventType.LegacyLastTick, SpanIndex = finalSpanIndex, SpanStartTime = finalSpanStartTime, - Time = finalSpanEndTime, - PathProgress = finalProgress, + Time = legacyLastTickTime, + PathProgress = legacyLastTickProgress, }; yield return new SliderEventDescriptor @@ -175,6 +194,11 @@ namespace osu.Game.Rulesets.Objects public enum SliderEventType { Tick, + + /// + /// Occurs just before the tail. See . + /// Should generally be ignored. + /// LegacyLastTick, Head, Tail, diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 13cc6361cf..0ac057578b 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Collections.Specialized; @@ -42,11 +40,13 @@ namespace osu.Game.Rulesets.Objects private readonly List calculatedPath = new List(); private readonly List cumulativeLength = new List(); - private readonly List segmentEnds = new List(); private readonly Cached pathCache = new Cached(); private double calculatedLength; + private readonly List segmentEnds = new List(); + private double[] segmentEndDistances = Array.Empty(); + /// /// Creates a new . /// @@ -198,13 +198,28 @@ namespace osu.Game.Rulesets.Objects } /// - /// Returns the progress values at which segments of the path end. + /// Returns the progress values at which (control point) segments of the path end. + /// Ranges from 0 (beginning of the path) to 1 (end of the path) to infinity (beyond the end of the path). /// + /// + /// truncates the progression values to [0,1], + /// so you can't use this method in conjunction with that one to retrieve the positions of segment ends beyond the end of the path. + /// + /// + /// + /// In case is less than , + /// the last segment ends after the end of the path, hence it returns a value greater than 1. + /// + /// + /// In case is greater than , + /// the last segment ends before the end of the path, hence it returns a value less than 1. + /// + /// public IEnumerable GetSegmentEnds() { ensureValid(); - return segmentEnds.Select(i => cumulativeLength[i] / calculatedLength); + return segmentEndDistances.Select(d => d / Distance); } private void invalidate() @@ -247,14 +262,24 @@ namespace osu.Game.Rulesets.Objects var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); var segmentType = ControlPoints[start].Type ?? PathType.Linear; - foreach (Vector2 t in calculateSubPath(segmentVertices, segmentType)) + // No need to calculate path when there is only 1 vertex + if (segmentVertices.Length == 1) + calculatedPath.Add(segmentVertices[0]); + else if (segmentVertices.Length > 1) { - if (calculatedPath.Count == 0 || calculatedPath.Last() != t) + List subPath = calculateSubPath(segmentVertices, segmentType); + // Skip the first vertex if it is the same as the last vertex from the previous segment + int skipFirst = calculatedPath.Count > 0 && subPath.Count > 0 && calculatedPath.Last() == subPath[0] ? 1 : 0; + + foreach (Vector2 t in subPath.Skip(skipFirst)) calculatedPath.Add(t); } - // Remember the index of the segment end - segmentEnds.Add(calculatedPath.Count - 1); + if (i > 0) + { + // Remember the index of the segment end + segmentEnds.Add(calculatedPath.Count - 1); + } // Start the new segment at the current vertex start = i; @@ -300,10 +325,18 @@ namespace osu.Game.Rulesets.Objects cumulativeLength.Add(calculatedLength); } + // Store the distances of the segment ends now, because after shortening the indices may be out of range + segmentEndDistances = new double[segmentEnds.Count]; + + for (int i = 0; i < segmentEnds.Count; i++) + { + segmentEndDistances[i] = cumulativeLength[segmentEnds[i]]; + } + if (ExpectedDistance.Value is double expectedDistance && calculatedLength != expectedDistance) { - // In osu-stable, if the last two control points of a slider are equal, extension is not performed. - if (ControlPoints.Count >= 2 && ControlPoints[^1].Position == ControlPoints[^2].Position && expectedDistance > calculatedLength) + // In osu-stable, if the last two path points of a slider are equal, extension is not performed. + if (calculatedPath.Count >= 2 && calculatedPath[^1] == calculatedPath[^2] && expectedDistance > calculatedLength) { cumulativeLength.Add(calculatedLength); return; @@ -321,10 +354,6 @@ namespace osu.Game.Rulesets.Objects { cumulativeLength.RemoveAt(cumulativeLength.Count - 1); calculatedPath.RemoveAt(pathEndIndex--); - - // Shorten the last segment to the expected distance - if (segmentEnds.Count > 0) - segmentEnds[^1]--; } } diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs index 92a3b570fb..6c88f01249 100644 --- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs +++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs @@ -1,6 +1,7 @@ // 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.Rulesets.Edit; using osu.Game.Rulesets.Objects.Types; @@ -25,6 +26,53 @@ namespace osu.Game.Rulesets.Objects /// The . /// The positional offset of the resulting path. It should be added to the start position of this path. public static void Reverse(this SliderPath sliderPath, out Vector2 positionalOffset) + { + var controlPoints = sliderPath.ControlPoints; + + var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.Linear && p.Type is null).ToList(); + + // Inherited points after a linear point, as well as the first control point if it inherited, + // should be treated as linear points, so their types are temporarily changed to linear. + inheritedLinearPoints.ForEach(p => p.Type = PathType.Linear); + + double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray(); + + // Remove segments after the end of the slider. + for (int numSegmentsToRemove = segmentEnds.Count(se => se >= 1) - 1; numSegmentsToRemove > 0 && controlPoints.Count > 0;) + { + if (controlPoints.Last().Type is not null) + { + numSegmentsToRemove--; + segmentEnds = segmentEnds[..^1]; + } + + controlPoints.RemoveAt(controlPoints.Count - 1); + } + + // Restore original control point types. + inheritedLinearPoints.ForEach(p => p.Type = null); + + // Recalculate middle perfect curve control points at the end of the slider path. + if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PerfectCurve && controlPoints[^2].Type is null && segmentEnds.Any()) + { + double lastSegmentStart = segmentEnds.Length > 1 ? segmentEnds[^2] : 0; + double lastSegmentEnd = segmentEnds[^1]; + + var circleArcPath = new List(); + sliderPath.GetPathToProgress(circleArcPath, lastSegmentStart / lastSegmentEnd, 1); + + controlPoints[^2].Position = circleArcPath[circleArcPath.Count / 2]; + } + + sliderPath.reverseControlPoints(out positionalOffset); + } + + /// + /// Reverses the order of the provided 's s. + /// + /// The . + /// The positional offset of the resulting path. It should be added to the start position of this path. + private static void reverseControlPoints(this SliderPath sliderPath, out Vector2 positionalOffset) { var points = sliderPath.ControlPoints.ToArray(); positionalOffset = sliderPath.PositionAt(1); diff --git a/osu.Game/Rulesets/Objects/SyntheticHitObjectEntry.cs b/osu.Game/Rulesets/Objects/SyntheticHitObjectEntry.cs index ee860e82e2..7a9f6948b0 100644 --- a/osu.Game/Rulesets/Objects/SyntheticHitObjectEntry.cs +++ b/osu.Game/Rulesets/Objects/SyntheticHitObjectEntry.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Objects diff --git a/osu.Game/Rulesets/Objects/Types/IHasDisplayColour.cs b/osu.Game/Rulesets/Objects/Types/IHasDisplayColour.cs index 89ee5022bf..691418ec48 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasDisplayColour.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasDisplayColour.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osuTK.Graphics; diff --git a/osu.Game/Rulesets/Objects/Types/IHasLegacyLastTickOffset.cs b/osu.Game/Rulesets/Objects/Types/IHasLegacyLastTickOffset.cs deleted file mode 100644 index caf22c3023..0000000000 --- a/osu.Game/Rulesets/Objects/Types/IHasLegacyLastTickOffset.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Rulesets.Objects.Types -{ - /// - /// A type of which may require the last tick to be offset. - /// This is specific to osu!stable conversion, and should not be used elsewhere. - /// - public interface IHasLegacyLastTickOffset - { - double LegacyLastTickOffset { get; } - } -} diff --git a/osu.Game/Rulesets/Objects/Types/IHasPath.cs b/osu.Game/Rulesets/Objects/Types/IHasPath.cs index 46834a55dd..5a3f270f54 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasPath.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasPath.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Rulesets.Objects.Types { public interface IHasPath : IHasDistance diff --git a/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs b/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs index 536707e95f..279946b44e 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.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 osuTK; namespace osu.Game.Rulesets.Objects.Types diff --git a/osu.Game/Rulesets/Objects/Types/IHasPosition.cs b/osu.Game/Rulesets/Objects/Types/IHasPosition.cs index 281f619ba5..8948fe59a9 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasPosition.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasPosition.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 osuTK; namespace osu.Game.Rulesets.Objects.Types diff --git a/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs b/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs index 821a6de520..2a4215b960 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasRepeats.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 osu.Game.Audio; using System.Collections.Generic; diff --git a/osu.Game/Rulesets/Objects/Types/IHasSliderVelocity.cs b/osu.Game/Rulesets/Objects/Types/IHasSliderVelocity.cs index 80fd8dd8dc..e665dcc752 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasSliderVelocity.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasSliderVelocity.cs @@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Objects.Types /// /// The slider velocity multiplier. /// - double SliderVelocity { get; set; } + double SliderVelocityMultiplier { get; set; } - BindableNumber SliderVelocityBindable { get; } + BindableNumber SliderVelocityMultiplierBindable { get; } } } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 490ec1475c..be0d757e06 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -192,6 +192,10 @@ namespace osu.Game.Rulesets case ModAutoplay: value |= LegacyMods.Autoplay; break; + + case ModScoreV2: + value |= LegacyMods.ScoreV2; + break; } } @@ -380,5 +384,10 @@ namespace osu.Game.Rulesets /// Can be overridden to add a ruleset-specific section to the editor beatmap setup screen. /// public virtual RulesetSetupSection? CreateEditorSetupSection() => null; + + /// + /// Can be overridden to alter the difficulty section to the editor beatmap setup screen. + /// + public virtual DifficultySection? CreateEditorDifficultySection() => null; } } diff --git a/osu.Game/Rulesets/RulesetLoadException.cs b/osu.Game/Rulesets/RulesetLoadException.cs index 6fee8f446b..803c756b41 100644 --- a/osu.Game/Rulesets/RulesetLoadException.cs +++ b/osu.Game/Rulesets/RulesetLoadException.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; namespace osu.Game.Rulesets diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 881b09bd1b..ac36ee6494 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Reflection; using osu.Framework; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Logging; using osu.Framework.Platform; @@ -32,7 +33,7 @@ namespace osu.Game.Rulesets // This null check prevents Android from attempting to load the rulesets from disk, // as the underlying path "AppContext.BaseDirectory", despite being non-nullable, it returns null on android. // See https://github.com/xamarin/xamarin-android/issues/3489. - if (RuntimeInfo.StartupDirectory != null) + if (RuntimeInfo.StartupDirectory.IsNotNull()) loadFromDisk(); // the event handler contains code for resolving dependency on the game assembly for rulesets located outside the base game directory. diff --git a/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs index af6e825b06..422bf8ea79 100644 --- a/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Rulesets.Scoring { /// diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index 2fde73d5a2..b4bdd8a1ea 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 0013a9f20d..fed338b012 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -55,6 +55,13 @@ namespace osu.Game.Rulesets.Scoring [Order(1)] Great, + /// + /// This is an optional timing window tighter than . + /// + /// + /// By default, this does not give any bonus accuracy or score. + /// To have it affect scoring, consider adding a nested bonus object. + /// [Description(@"Perfect")] [EnumMember(Value = "perfect")] [Order(0)] @@ -120,6 +127,16 @@ namespace osu.Game.Rulesets.Scoring [Order(12)] IgnoreHit, + /// + /// Indicates that a combo break should occur, but does not otherwise affect score. + /// + /// + /// May be paired with . + /// + [EnumMember(Value = "combo_break")] + [Order(15)] + ComboBreak, + /// /// A special result used as a padding value for legacy rulesets. It is a hit type and affects combo, but does not affect the base score (does not affect accuracy). /// @@ -165,6 +182,7 @@ namespace osu.Game.Rulesets.Scoring case HitResult.LargeTickHit: case HitResult.LargeTickMiss: case HitResult.LegacyComboIncrease: + case HitResult.ComboBreak: return true; default: @@ -177,11 +195,19 @@ namespace osu.Game.Rulesets.Scoring /// public static bool AffectsAccuracy(this HitResult result) { - // LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result. - if (result == HitResult.LegacyComboIncrease) - return false; + switch (result) + { + // LegacyComboIncrease is a special non-gameplay type which is neither a basic, tick, bonus, or accuracy-affecting result. + case HitResult.LegacyComboIncrease: + return false; - return IsScorable(result) && !IsBonus(result); + // ComboBreak is a special type that only affects combo. It cannot be considered as basic, tick, bonus, or accuracy-affecting. + case HitResult.ComboBreak: + return false; + + default: + return IsScorable(result) && !IsBonus(result); + } } /// @@ -189,11 +215,19 @@ namespace osu.Game.Rulesets.Scoring /// public static bool IsBasic(this HitResult result) { - // LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result. - if (result == HitResult.LegacyComboIncrease) - return false; + switch (result) + { + // LegacyComboIncrease is a special non-gameplay type which is neither a basic, tick, bonus, or accuracy-affecting result. + case HitResult.LegacyComboIncrease: + return false; - return IsScorable(result) && !IsTick(result) && !IsBonus(result); + // ComboBreak is a special type that only affects combo. It cannot be considered as basic, tick, bonus, or accuracy-affecting. + case HitResult.ComboBreak: + return false; + + default: + return IsScorable(result) && !IsTick(result) && !IsBonus(result); + } } /// @@ -242,6 +276,7 @@ namespace osu.Game.Rulesets.Scoring case HitResult.Miss: case HitResult.SmallTickMiss: case HitResult.LargeTickMiss: + case HitResult.ComboBreak: return false; default: @@ -254,11 +289,20 @@ namespace osu.Game.Rulesets.Scoring /// public static bool IsScorable(this HitResult result) { - // LegacyComboIncrease is not actually scorable (in terms of usable by rulesets for that purpose), but needs to be defined as such to be correctly included in statistics output. - if (result == HitResult.LegacyComboIncrease) - return true; + switch (result) + { + // LegacyComboIncrease is not actually scorable (in terms of usable by rulesets for that purpose), but needs to be defined as such to be correctly included in statistics output. + case HitResult.LegacyComboIncrease: + return true; - return result >= HitResult.Miss && result < HitResult.IgnoreMiss; + // ComboBreak is its own type that affects score via combo. + case HitResult.ComboBreak: + return true; + + default: + // Note that IgnoreHit and IgnoreMiss are excluded as they do not affect score. + return result >= HitResult.Miss && result < HitResult.IgnoreMiss; + } } /// @@ -291,6 +335,30 @@ namespace osu.Game.Rulesets.Scoring /// The to get the index of. /// The index of . public static int GetIndexForOrderedDisplay(this HitResult result) => order.IndexOf(result); + + public static void ValidateHitResultPair(HitResult maxResult, HitResult minResult) + { + if (maxResult == HitResult.None || !IsHit(maxResult)) + throw new ArgumentOutOfRangeException(nameof(maxResult), $"{maxResult} is not a valid maximum judgement result."); + + if (minResult == HitResult.None || IsHit(minResult)) + throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum judgement result."); + + if (maxResult == HitResult.IgnoreHit && minResult is not (HitResult.IgnoreMiss or HitResult.ComboBreak)) + throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement."); + + if (maxResult.IsBonus() && minResult != HitResult.IgnoreMiss) + throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.IgnoreMiss} is the only valid minimum result for a {maxResult} judgement."); + + if (maxResult == HitResult.LargeTickHit && minResult != HitResult.LargeTickMiss) + throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.LargeTickMiss} is the only valid minimum result for a {maxResult} judgement."); + + if (maxResult == HitResult.SmallTickHit && minResult != HitResult.SmallTickMiss) + throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.SmallTickMiss} is the only valid minimum result for a {maxResult} judgement."); + + if (maxResult.IsBasic() && minResult != HitResult.Miss) + throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.Miss} is the only valid minimum result for a {maxResult} judgement."); + } } #pragma warning restore CS0618 } diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 99129fcf96..2d008b58ba 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.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.Collections.Generic; using System.Diagnostics; diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 09b5f0a6bc..e9f3bcb949 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.TypeExtensions; @@ -30,6 +31,11 @@ namespace osu.Game.Rulesets.Scoring /// protected int MaxHits { get; private set; } + /// + /// Whether is currently running. + /// + protected bool IsSimulating { get; private set; } + /// /// The total number of judged s at the current point in time. /// @@ -132,28 +138,17 @@ namespace osu.Game.Rulesets.Scoring JudgedHits += count; } - /// - /// Creates the that represents the scoring result for a . - /// - /// The which was judged. - /// The that provides the scoring information. - protected virtual JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new JudgementResult(hitObject, judgement); - /// /// Simulates an autoplay of the to determine scoring values. /// /// This provided temporarily. DO NOT USE. /// The to simulate. - protected virtual void SimulateAutoplay(IBeatmap beatmap) + protected void SimulateAutoplay(IBeatmap beatmap) { - foreach (var obj in beatmap.HitObjects) - simulate(obj); + IsSimulating = true; - void simulate(HitObject obj) + foreach (var obj in EnumerateHitObjects(beatmap)) { - foreach (var nested in obj.NestedHitObjects) - simulate(nested); - var judgement = obj.CreateJudgement(); var result = CreateResult(obj, judgement); @@ -163,8 +158,47 @@ namespace osu.Game.Rulesets.Scoring result.Type = GetSimulatedHitResult(judgement); ApplyResult(result); } + + IsSimulating = false; } + /// + /// Enumerates all s in the given in the order in which they are to be judged. + /// Used in . + /// + /// + /// In Score V2, the score awarded for each object includes a component based on the combo value after the judgement of that object. + /// This means that the score is dependent on the order of evaluation of judgements. + /// This method is provided so that rulesets can specify custom ordering that is correct for them and matches processing order during actual gameplay. + /// + protected virtual IEnumerable EnumerateHitObjects(IBeatmap beatmap) + => enumerateRecursively(beatmap.HitObjects); + + private IEnumerable enumerateRecursively(IEnumerable hitObjects) + { + foreach (var hitObject in hitObjects) + { + foreach (var nested in enumerateRecursively(hitObject.NestedHitObjects)) + yield return nested; + + yield return hitObject; + } + } + + /// + /// Creates the that represents the scoring result for a . + /// + /// The which was judged. + /// The that provides the scoring information. + protected virtual JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new JudgementResult(hitObject, judgement); + + /// + /// Gets a simulated for a judgement. Used during to simulate a "perfect" play. + /// + /// The judgement to simulate a for. + /// The simulated for the judgement. + protected virtual HitResult GetSimulatedHitResult(Judgement judgement) => judgement.MaxResult; + protected override void Update() { base.Update(); @@ -175,12 +209,5 @@ namespace osu.Game.Rulesets.Scoring // Last applied result is guaranteed to be non-null when JudgedHits > 0. || lastAppliedResult.AsNonNull().TimeAbsolute < Clock.CurrentTime); } - - /// - /// Gets a simulated for a judgement. Used during to simulate a "perfect" play. - /// - /// The judgement to simulate a for. - /// The simulated for the judgement. - protected virtual HitResult GetSimulatedHitResult(Judgement judgement) => judgement.MaxResult; } } diff --git a/osu.Game/Rulesets/Scoring/Legacy/ILegacyScoreSimulator.cs b/osu.Game/Rulesets/Scoring/Legacy/ILegacyScoreSimulator.cs new file mode 100644 index 0000000000..824f38fe2c --- /dev/null +++ b/osu.Game/Rulesets/Scoring/Legacy/ILegacyScoreSimulator.cs @@ -0,0 +1,30 @@ +// 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 osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Scoring.Legacy +{ + /// + /// Generates attributes which are required to calculate old-style Score V1 scores. + /// + public interface ILegacyScoreSimulator + { + /// + /// Performs the simulation, computing the maximum scoring values achievable for the given beatmap. + /// + /// The working beatmap. + /// A playable version of the beatmap for the ruleset. + LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap); + + /// + /// Returns the legacy score multiplier for the mods. This is only used during legacy score conversion. + /// + /// The mods. + /// Extra difficulty parameters. + /// The legacy multiplier. + double GetLegacyScoreMultiplier(IReadOnlyList mods, LegacyBeatmapConversionDifficultyInfo difficulty); + } +} diff --git a/osu.Game/Rulesets/Scoring/Legacy/LegacyBeatmapConversionDifficultyInfo.cs b/osu.Game/Rulesets/Scoring/Legacy/LegacyBeatmapConversionDifficultyInfo.cs new file mode 100644 index 0000000000..97ccf787af --- /dev/null +++ b/osu.Game/Rulesets/Scoring/Legacy/LegacyBeatmapConversionDifficultyInfo.cs @@ -0,0 +1,67 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets.Objects.Types; + +namespace osu.Game.Rulesets.Scoring.Legacy +{ + /// + /// A set of properties that are required to facilitate beatmap conversion between legacy rulesets. + /// + public class LegacyBeatmapConversionDifficultyInfo : IBeatmapDifficultyInfo + { + /// + /// The beatmap's ruleset. + /// + public IRulesetInfo SourceRuleset { get; set; } = new RulesetInfo(); + + /// + /// The beatmap circle size. + /// + public float CircleSize { get; set; } + + /// + /// The beatmap overall difficulty. + /// + public float OverallDifficulty { get; set; } + + /// + /// The count of hitcircles in the beatmap. + /// + /// + /// When converting from osu! ruleset beatmaps, this is equivalent to the sum of sliders and spinners in the beatmap. + /// + public int CircleCount { get; set; } + + /// + /// The total count of hitobjects in the beatmap. + /// + public int TotalObjectCount { get; set; } + + float IBeatmapDifficultyInfo.DrainRate => 0; + float IBeatmapDifficultyInfo.ApproachRate => 0; + double IBeatmapDifficultyInfo.SliderMultiplier => 0; + double IBeatmapDifficultyInfo.SliderTickRate => 0; + + public static LegacyBeatmapConversionDifficultyInfo FromAPIBeatmap(APIBeatmap apiBeatmap) => new LegacyBeatmapConversionDifficultyInfo + { + SourceRuleset = apiBeatmap.Ruleset, + CircleSize = apiBeatmap.CircleSize, + OverallDifficulty = apiBeatmap.OverallDifficulty, + CircleCount = apiBeatmap.CircleCount, + TotalObjectCount = apiBeatmap.SliderCount + apiBeatmap.SpinnerCount + apiBeatmap.CircleCount + }; + + public static LegacyBeatmapConversionDifficultyInfo FromBeatmap(IBeatmap beatmap) => new LegacyBeatmapConversionDifficultyInfo + { + SourceRuleset = beatmap.BeatmapInfo.Ruleset, + CircleSize = beatmap.Difficulty.CircleSize, + OverallDifficulty = beatmap.Difficulty.OverallDifficulty, + CircleCount = beatmap.HitObjects.Count(h => h is not IHasDuration), + TotalObjectCount = beatmap.HitObjects.Count + }; + } +} diff --git a/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreAttributes.cs b/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreAttributes.cs new file mode 100644 index 0000000000..47ab68bf88 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreAttributes.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Scoring.Legacy +{ + public struct LegacyScoreAttributes + { + /// + /// The accuracy portion of the legacy (ScoreV1) total score. + /// + public int AccuracyScore; + + /// + /// The combo-multiplied portion of the legacy (ScoreV1) total score. + /// + public long ComboScore; + + /// + /// A ratio of standardised score to legacy score for the bonus part of total score. + /// + public double BonusScoreRatio; + } +} diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index ac17de32d8..35a7dfe369 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -30,6 +30,14 @@ namespace osu.Game.Rulesets.Scoring private const double accuracy_cutoff_c = 0.7; private const double accuracy_cutoff_d = 0; + /// + /// Whether should be populated during application of results. + /// + /// + /// Should only be disabled for special cases. + /// When disabled, cannot be used. + internal bool TrackHitEvents = true; + /// /// Invoked when this was reset from a replay frame. /// @@ -226,10 +234,16 @@ namespace osu.Game.Rulesets.Scoring ApplyScoreChange(result); - hitEvents.Add(CreateHitEvent(result)); - lastHitObject = result.HitObject; + if (!IsSimulating) + { + if (TrackHitEvents) + { + hitEvents.Add(CreateHitEvent(result)); + lastHitObject = result.HitObject; + } - updateScore(); + updateScore(); + } } /// @@ -242,6 +256,9 @@ namespace osu.Game.Rulesets.Scoring protected sealed override void RevertResultInternal(JudgementResult result) { + if (!TrackHitEvents) + throw new InvalidOperationException(@$"Rewind is not supported when {nameof(TrackHitEvents)} is disabled."); + Combo.Value = result.ComboAtJudgement; HighestCombo.Value = result.HighestComboAtJudgement; @@ -311,6 +328,9 @@ namespace osu.Game.Rulesets.Scoring /// Whether to store the current state of the for future use. protected override void Reset(bool storeResults) { + // Run one last time to store max values. + updateScore(); + base.Reset(storeResults); hitEvents.Clear(); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 4f22c0c617..4aeb3d4862 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -71,6 +71,12 @@ namespace osu.Game.Rulesets.UI private readonly AudioContainer audioContainer = new AudioContainer { RelativeSizeAxes = Axes.Both }; + /// + /// A container which encapsulates the and provides any adjustments to + /// ensure correct scale and position. + /// + public virtual PlayfieldAdjustmentContainer PlayfieldAdjustmentContainer { get; private set; } + public override Container FrameStableComponents { get; } = new Container { RelativeSizeAxes = Axes.Both }; public override IFrameStableClock FrameStableClock => frameStabilityContainer; @@ -178,7 +184,7 @@ namespace osu.Game.Rulesets.UI audioContainer.WithChild(KeyBindingInputManager .WithChildren(new Drawable[] { - CreatePlayfieldAdjustmentContainer() + PlayfieldAdjustmentContainer = CreatePlayfieldAdjustmentContainer() .WithChild(Playfield), Overlays })), @@ -329,11 +335,11 @@ namespace osu.Game.Rulesets.UI /// The representing . public abstract DrawableHitObject CreateDrawableRepresentation(TObject h); - public void Attach(KeyCounterDisplay keyCounter) => - (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(keyCounter); + public void Attach(InputCountController inputCountController) => + (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(inputCountController); - public void Attach(ClicksPerSecondCalculator calculator) => - (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(calculator); + public void Attach(ClicksPerSecondController controller) => + (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(controller); /// /// Creates a key conversion input manager. An exception will be thrown if a valid is not returned. diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index e34289c968..02c5bbb27e 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -186,7 +186,7 @@ namespace osu.Game.Rulesets.UI this.fallback = fallback; } - public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) + public override Texture? Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => primary.Get(name, wrapModeS, wrapModeT) ?? fallback.Get(name, wrapModeS, wrapModeT); protected override void Dispose(bool disposing) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 4bb145973d..2af9916a6b 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -171,6 +171,9 @@ namespace osu.Game.Rulesets.UI // The manual clock time has changed in the above code. The framed clock now needs to be updated // to ensure that the its time is valid for our children before input is processed framedClock.ProcessFrame(); + + if (framedClock.ElapsedFrameTime != 0) + IsRewinding = framedClock.ElapsedFrameTime < 0; } /// @@ -247,6 +250,8 @@ namespace osu.Game.Rulesets.UI public IBindable IsPaused { get; } = new BindableBool(); + public bool IsRewinding { get; private set; } + public double CurrentTime => framedClock.CurrentTime; public double Rate => framedClock.Rate; @@ -259,8 +264,6 @@ namespace osu.Game.Rulesets.UI public double FramesPerSecond => framedClock.FramesPerSecond; - public FrameTimeInfo TimeInfo => framedClock.TimeInfo; - public double StartTime => parentGameplayClock?.StartTime ?? 0; private readonly AudioAdjustments gameplayAdjustments = new AudioAdjustments(); diff --git a/osu.Game/Rulesets/UI/GameplayCursorContainer.cs b/osu.Game/Rulesets/UI/GameplayCursorContainer.cs index cbce397d1e..0ce3f76384 100644 --- a/osu.Game/Rulesets/UI/GameplayCursorContainer.cs +++ b/osu.Game/Rulesets/UI/GameplayCursorContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index fbb7a20a5d..b61e8d9674 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -1,13 +1,15 @@ // 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 System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Game.Audio; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play; using osu.Game.Skinning; namespace osu.Game.Rulesets.UI @@ -28,25 +30,36 @@ namespace osu.Game.Rulesets.UI private readonly Container hitSounds; + private HitObjectLifetimeEntry? mostValidObject; + + [Resolved] + private IGameplayClock? gameplayClock { get; set; } + + protected readonly AudioContainer AudioContainer; + public GameplaySampleTriggerSource(HitObjectContainer hitObjectContainer) { this.hitObjectContainer = hitObjectContainer; - InternalChild = hitSounds = new Container + InternalChild = AudioContainer = new AudioContainer { - Name = "concurrent sample pool", - ChildrenEnumerable = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new PausableSkinnableSound()) + Child = hitSounds = new Container + { + Name = "concurrent sample pool", + ChildrenEnumerable = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new PausableSkinnableSound + { + MinimumSampleVolume = DrawableHitObject.MINIMUM_SAMPLE_VOLUME + }) + } }; } - private HitObjectLifetimeEntry fallbackObject; - /// /// Play the most appropriate hit sound for the current point in time. /// public virtual void Play() { - var nextObject = GetMostValidObject(); + HitObject? nextObject = GetMostValidObject(); if (nextObject == null) return; @@ -60,72 +73,88 @@ namespace osu.Game.Rulesets.UI protected virtual void PlaySamples(ISampleInfo[] samples) => Schedule(() => { - var hitSound = getNextSample(); - hitSound.Samples = samples; + var hitSound = GetNextSample(); + ApplySampleInfo(hitSound, samples); hitSound.Play(); }); - protected HitObject GetMostValidObject() + protected virtual void ApplySampleInfo(SkinnableSound hitSound, ISampleInfo[] samples) { - // The most optimal lookup case we have is when an object is alive. There are usually very few alive objects so there's no drawbacks in attempting this lookup each time. - var drawableHitObject = hitObjectContainer.AliveObjects.FirstOrDefault(h => h.Result?.HasResult != true); + hitSound.Samples = samples; + } - if (drawableHitObject != null) - { - // A hit object may have a more valid nested object. - drawableHitObject = getMostValidNestedDrawable(drawableHitObject); + public void StopAllPlayback() => Schedule(() => + { + foreach (var sound in hitSounds) + sound.Stop(); + }); - return drawableHitObject.HitObject; - } + protected override void Update() + { + base.Update(); - // In the case a next object isn't available in drawable form, we need to do a somewhat expensive traversal to get a valid sound to play. - // This lookup can be skipped if the last entry is still valid (in the future and not yet hit). - if (fallbackObject == null || fallbackObject.Result?.HasResult == true) + if (gameplayClock?.IsRewinding == true) + mostValidObject = null; + } + + protected HitObject? GetMostValidObject() + { + if (mostValidObject == null || isAlreadyHit(mostValidObject)) { // We need to use lifetime entries to find the next object (we can't just use `hitObjectContainer.Objects` due to pooling - it may even be empty). // If required, we can make this lookup more efficient by adding support to get next-future-entry in LifetimeEntryManager. - fallbackObject = hitObjectContainer.Entries - .Where(e => e.Result?.HasResult != true).MinBy(e => e.HitObject.StartTime); - - if (fallbackObject != null) - return getEarliestNestedObject(fallbackObject.HitObject); + var candidate = + // Use alive entries first as an optimisation. + hitObjectContainer.AliveEntries.Select(tuple => tuple.Entry).Where(e => !isAlreadyHit(e)).MinBy(e => e.HitObject.StartTime) + ?? hitObjectContainer.Entries.Where(e => !isAlreadyHit(e)).MinBy(e => e.HitObject.StartTime); // In the case there are no non-judged objects, the last hit object should be used instead. - fallbackObject ??= hitObjectContainer.Entries.LastOrDefault(); + if (candidate == null) + { + mostValidObject = hitObjectContainer.Entries.LastOrDefault(); + } + else + { + if (isCloseEnoughToCurrentTime(candidate.HitObject)) + { + mostValidObject = candidate; + } + else + { + mostValidObject ??= hitObjectContainer.Entries.FirstOrDefault(); + } + } } - if (fallbackObject == null) + if (mostValidObject == null) return null; - bool fallbackHasResult = fallbackObject.Result?.HasResult == true; - // If the fallback has been judged then we want the sample from the object itself. - if (fallbackHasResult) - return fallbackObject.HitObject; + if (isAlreadyHit(mostValidObject)) + return mostValidObject.HitObject; - // Else we want the earliest (including nested). + // Else we want the earliest valid nested. // In cases of nested objects, they will always have earlier sample data than their parent object. - return getEarliestNestedObject(fallbackObject.HitObject); + return getAllNested(mostValidObject.HitObject).OrderBy(h => h.GetEndTime()).SkipWhile(h => h.GetEndTime() <= getReferenceTime()).FirstOrDefault() ?? mostValidObject.HitObject; } - private DrawableHitObject getMostValidNestedDrawable(DrawableHitObject o) + private bool isAlreadyHit(HitObjectLifetimeEntry h) => h.AllJudged; + private bool isCloseEnoughToCurrentTime(HitObject h) => getReferenceTime() >= h.StartTime - h.HitWindows.WindowFor(HitResult.Miss) * 2; + + private double getReferenceTime() => gameplayClock?.CurrentTime ?? Clock.CurrentTime; + + private IEnumerable getAllNested(HitObject hitObject) { - var nestedWithoutResult = o.NestedHitObjects.FirstOrDefault(n => n.Result?.HasResult != true); + foreach (var h in hitObject.NestedHitObjects) + { + yield return h; - if (nestedWithoutResult == null) - return o; - - return getMostValidNestedDrawable(nestedWithoutResult); + foreach (var n in getAllNested(h)) + yield return n; + } } - private HitObject getEarliestNestedObject(HitObject hitObject) - { - var nested = hitObject.NestedHitObjects.FirstOrDefault(); - - return nested != null ? getEarliestNestedObject(nested) : hitObject; - } - - private SkinnableSound getNextSample() + protected SkinnableSound GetNextSample() { SkinnableSound hitSound = hitSounds[nextHitSoundIndex]; diff --git a/osu.Game/Rulesets/UI/ICanAttachHUDPieces.cs b/osu.Game/Rulesets/UI/ICanAttachHUDPieces.cs new file mode 100644 index 0000000000..276881d17a --- /dev/null +++ b/osu.Game/Rulesets/UI/ICanAttachHUDPieces.cs @@ -0,0 +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.Game.Screens.Play.HUD; +using osu.Game.Screens.Play.HUD.ClicksPerSecond; + +namespace osu.Game.Rulesets.UI +{ + /// + /// A target (generally always ) which can attach various skinnable components. + /// + /// + /// Attach methods will give the target permission to prepare the component into a usable state, usually via + /// calling methods on the component (attaching various gameplay devices). + /// + public interface ICanAttachHUDPieces + { + void Attach(InputCountController inputCountController); + void Attach(ClicksPerSecondController controller); + } +} diff --git a/osu.Game/Rulesets/UI/IHasRecordingHandler.cs b/osu.Game/Rulesets/UI/IHasRecordingHandler.cs new file mode 100644 index 0000000000..f73398dd98 --- /dev/null +++ b/osu.Game/Rulesets/UI/IHasRecordingHandler.cs @@ -0,0 +1,15 @@ +// 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.Input; + +namespace osu.Game.Rulesets.UI +{ + /// + /// Expose the in a capable . + /// + public interface IHasRecordingHandler + { + public ReplayRecorder? Recorder { set; } + } +} diff --git a/osu.Game/Rulesets/UI/IHasReplayHandler.cs b/osu.Game/Rulesets/UI/IHasReplayHandler.cs new file mode 100644 index 0000000000..561c582b71 --- /dev/null +++ b/osu.Game/Rulesets/UI/IHasReplayHandler.cs @@ -0,0 +1,16 @@ +// 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.Input; +using osu.Game.Input.Handlers; + +namespace osu.Game.Rulesets.UI +{ + /// + /// Expose the in a capable . + /// + public interface IHasReplayHandler + { + ReplayInputHandler? ReplayInputHandler { get; set; } + } +} diff --git a/osu.Game/Rulesets/UI/IHitObjectContainer.cs b/osu.Game/Rulesets/UI/IHitObjectContainer.cs index 74fd7dee81..6dcb0944be 100644 --- a/osu.Game/Rulesets/UI/IHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/IHitObjectContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Game.Rulesets.Objects.Drawables; diff --git a/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs b/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs index b842e708b0..01c8e6d1da 100644 --- a/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs +++ b/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs @@ -1,9 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -17,7 +14,6 @@ namespace osu.Game.Rulesets.UI /// The to retrieve the representation of. /// The parenting , if any. /// The representing , or null if no poolable representation exists. - [CanBeNull] - DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject, [CanBeNull] DrawableHitObject parent); + DrawableHitObject? GetPooledDrawableRepresentation(HitObject hitObject, DrawableHitObject? parent); } } diff --git a/osu.Game/Rulesets/UI/JudgementContainer.cs b/osu.Game/Rulesets/UI/JudgementContainer.cs index 7181e80206..886dd34fc7 100644 --- a/osu.Game/Rulesets/UI/JudgementContainer.cs +++ b/osu.Game/Rulesets/UI/JudgementContainer.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 osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Judgements; diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index bf212ad72f..5fd1507039 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -1,22 +1,22 @@ // 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 osuTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Localisation; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; using osuTK; -using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Localisation; +using osuTK.Graphics; namespace osu.Game.Rulesets.UI { @@ -27,22 +27,27 @@ namespace osu.Game.Rulesets.UI { public readonly BindableBool Selected = new BindableBool(); - private readonly SpriteIcon modIcon; - private readonly SpriteText modAcronym; - private readonly SpriteIcon background; + private SpriteIcon modIcon = null!; + private SpriteText modAcronym = null!; + private Sprite background = null!; - private const float size = 80; + public static readonly Vector2 MOD_ICON_SIZE = new Vector2(80); - public virtual LocalisableString TooltipText => showTooltip ? ((mod as Mod)?.IconTooltip ?? mod.Name) : null; + public virtual LocalisableString TooltipText => showTooltip ? ((mod as Mod)?.IconTooltip ?? mod.Name) : string.Empty; private IMod mod; + private readonly bool showTooltip; + private readonly bool showExtendedInformation; public IMod Mod { get => mod; set { + if (mod == value) + return; + mod = value; if (IsLoaded) @@ -51,49 +56,103 @@ namespace osu.Game.Rulesets.UI } [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; private Color4 backgroundColour; + private Sprite extendedBackground = null!; + + private OsuSpriteText extendedText = null!; + + private Container extendedContent = null!; + + private ModSettingChangeTracker? modSettingsChangeTracker; + /// /// Construct a new instance. /// /// The mod to be displayed /// Whether a tooltip describing the mod should display on hover. - public ModIcon(IMod mod, bool showTooltip = true) + /// Whether to display a mod's extended information, if available. + public ModIcon(IMod mod, bool showTooltip = true, bool showExtendedInformation = true) { + // May expand due to expanded content, so autosize here. + AutoSizeAxes = Axes.X; + Height = MOD_ICON_SIZE.Y; + this.mod = mod ?? throw new ArgumentNullException(nameof(mod)); this.showTooltip = showTooltip; + this.showExtendedInformation = showExtendedInformation; + } - Size = new Vector2(size); - + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { Children = new Drawable[] { - background = new SpriteIcon + extendedContent = new Container { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Size = new Vector2(size), - Icon = OsuIcon.ModBg, - Shadow = true, + Name = "extended content", + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(116, MOD_ICON_SIZE.Y), + X = MOD_ICON_SIZE.X - 22, + Children = new Drawable[] + { + extendedBackground = new Sprite + { + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Texture = textures.Get("Icons/BeatmapDetails/mod-icon-extender"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + extendedText = new OsuSpriteText + { + Font = OsuFont.Default.With(size: 34f, weight: FontWeight.Bold), + UseFullGlyphHeight = false, + Text = mod.ExtendedIconInformation, + X = 6, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } }, - modAcronym = new OsuSpriteText + new Container { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Colour = OsuColour.Gray(84), - Alpha = 0, - Font = OsuFont.Numeric.With(null, 22f), - UseFullGlyphHeight = false, - Text = mod.Acronym - }, - modIcon = new SpriteIcon - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Colour = OsuColour.Gray(84), - Size = new Vector2(45), - Icon = FontAwesome.Solid.Question + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Name = "main content", + Size = MOD_ICON_SIZE, + Children = new Drawable[] + { + background = new Sprite + { + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Texture = textures.Get("Icons/BeatmapDetails/mod-icon"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + modAcronym = new OsuSpriteText + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Colour = OsuColour.Gray(84), + Alpha = 0, + Font = OsuFont.Numeric.With(null, 22f), + UseFullGlyphHeight = false, + Text = mod.Acronym + }, + modIcon = new SpriteIcon + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Colour = OsuColour.Gray(84), + Size = new Vector2(45), + Icon = FontAwesome.Solid.Question + }, + } }, }; } @@ -109,6 +168,14 @@ namespace osu.Game.Rulesets.UI private void updateMod(IMod value) { + modSettingsChangeTracker?.Dispose(); + + if (value is Mod actualMod) + { + modSettingsChangeTracker = new ModSettingChangeTracker(new[] { actualMod }); + modSettingsChangeTracker.SettingChanged = _ => updateExtendedInformation(); + } + modAcronym.Text = value.Acronym; modIcon.Icon = value.Icon ?? FontAwesome.Solid.Question; @@ -125,11 +192,28 @@ namespace osu.Game.Rulesets.UI backgroundColour = colours.ForModType(value.Type); updateColour(); + + updateExtendedInformation(); + } + + private void updateExtendedInformation() + { + bool showExtended = showExtendedInformation && !string.IsNullOrEmpty(mod.ExtendedIconInformation); + + extendedContent.Alpha = showExtended ? 1 : 0; + extendedText.Text = mod.ExtendedIconInformation; } private void updateColour() { - background.Colour = Selected.Value ? backgroundColour.Lighten(0.2f) : backgroundColour; + extendedText.Colour = background.Colour = Selected.Value ? backgroundColour.Lighten(0.2f) : backgroundColour; + extendedBackground.Colour = Selected.Value ? backgroundColour.Darken(2.4f) : backgroundColour.Darken(2.8f); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + modSettingsChangeTracker?.Dispose(); } } } diff --git a/osu.Game/Rulesets/UI/ModSwitchSmall.cs b/osu.Game/Rulesets/UI/ModSwitchSmall.cs index b6058c16ce..927379c684 100644 --- a/osu.Game/Rulesets/UI/ModSwitchSmall.cs +++ b/osu.Game/Rulesets/UI/ModSwitchSmall.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Overlays; @@ -23,8 +24,8 @@ namespace osu.Game.Rulesets.UI private readonly IMod mod; - private readonly SpriteIcon background; - private readonly SpriteIcon? modIcon; + private Drawable background = null!; + private SpriteIcon? modIcon; private Color4 activeForegroundColour; private Color4 inactiveForegroundColour; @@ -36,19 +37,24 @@ namespace osu.Game.Rulesets.UI { this.mod = mod; - AutoSizeAxes = Axes.Both; + Size = new Vector2(DEFAULT_SIZE); + } + [BackgroundDependencyLoader] + private void load(TextureStore textures, OsuColour colours, OverlayColourProvider? colourProvider) + { FillFlowContainer contentFlow; ModSwitchTiny tinySwitch; - InternalChildren = new Drawable[] + InternalChildren = new[] { - background = new SpriteIcon + background = new Sprite { + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Texture = textures.Get("Icons/BeatmapDetails/mod-icon"), Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(DEFAULT_SIZE), - Icon = OsuIcon.ModBg }, contentFlow = new FillFlowContainer { @@ -78,11 +84,7 @@ namespace osu.Game.Rulesets.UI }); tinySwitch.Scale = new Vector2(0.3f); } - } - [BackgroundDependencyLoader(true)] - private void load(OsuColour colours, OverlayColourProvider? colourProvider) - { inactiveForegroundColour = colourProvider?.Background5 ?? colours.Gray3; activeForegroundColour = colours.ForModType(mod.Type); diff --git a/osu.Game/Rulesets/UI/ModSwitchTiny.cs b/osu.Game/Rulesets/UI/ModSwitchTiny.cs index a5cf75bd07..a3e325ace8 100644 --- a/osu.Game/Rulesets/UI/ModSwitchTiny.cs +++ b/osu.Game/Rulesets/UI/ModSwitchTiny.cs @@ -3,15 +3,16 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Rulesets.Mods; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.UI @@ -21,8 +22,10 @@ namespace osu.Game.Rulesets.UI public BindableBool Active { get; } = new BindableBool(); public const float DEFAULT_HEIGHT = 30; + private const float width = 73; - private readonly IMod mod; + protected readonly IMod Mod; + private readonly bool showExtendedInformation; private readonly Box background; private readonly OsuSpriteText acronymText; @@ -33,33 +36,69 @@ namespace osu.Game.Rulesets.UI private Color4 activeBackgroundColour; private Color4 inactiveBackgroundColour; - public ModSwitchTiny(IMod mod) - { - this.mod = mod; - Size = new Vector2(73, DEFAULT_HEIGHT); + private readonly CircularContainer extendedContent; + private readonly Box extendedBackground; + private readonly OsuSpriteText extendedText; + private ModSettingChangeTracker? modSettingsChangeTracker; - InternalChild = new CircularContainer + public ModSwitchTiny(IMod mod, bool showExtendedInformation = false) + { + Mod = mod; + this.showExtendedInformation = showExtendedInformation; + AutoSizeAxes = Axes.X; + Height = DEFAULT_HEIGHT; + + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] + extendedContent = new CircularContainer { - background = new Box + Name = "extended content", + Width = 100 + DEFAULT_HEIGHT / 2, + RelativeSizeAxes = Axes.Y, + Masking = true, + X = width, + Margin = new MarginPadding { Left = -DEFAULT_HEIGHT }, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both - }, - acronymText = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Shadow = false, - Font = OsuFont.Numeric.With(size: 24, weight: FontWeight.Black), - Text = mod.Acronym, - Margin = new MarginPadding + extendedBackground = new Box { - Top = 4 - } + RelativeSizeAxes = Axes.Both, + }, + extendedText = new OsuSpriteText + { + Margin = new MarginPadding { Left = 3 * DEFAULT_HEIGHT / 4 }, + Font = OsuFont.Default.With(size: 30f, weight: FontWeight.Bold), + UseFullGlyphHeight = false, + Text = mod.ExtendedIconInformation, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, } + }, + new CircularContainer + { + Width = width, + RelativeSizeAxes = Axes.Y, + Masking = true, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + acronymText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shadow = false, + Font = OsuFont.Numeric.With(size: 24, weight: FontWeight.Black), + Text = mod.Acronym, + Margin = new MarginPadding + { + Top = 4 + } + }, + }, } }; } @@ -68,7 +107,7 @@ namespace osu.Game.Rulesets.UI private void load(OsuColour colours, OverlayColourProvider? colourProvider) { inactiveBackgroundColour = colourProvider?.Background5 ?? colours.Gray3; - activeBackgroundColour = colours.ForModType(mod.Type); + activeBackgroundColour = colours.ForModType(Mod.Type); inactiveForegroundColour = colourProvider?.Background2 ?? colours.Gray5; activeForegroundColour = Interpolation.ValueAt(0.1f, Colour4.Black, activeForegroundColour, 0, 1); @@ -80,12 +119,37 @@ namespace osu.Game.Rulesets.UI Active.BindValueChanged(_ => updateState(), true); FinishTransforms(true); + + if (Mod is Mod actualMod) + { + modSettingsChangeTracker = new ModSettingChangeTracker(new[] { actualMod }); + modSettingsChangeTracker.SettingChanged = _ => updateExtendedInformation(); + } + + updateExtendedInformation(); + } + + private void updateExtendedInformation() + { + bool showExtended = showExtendedInformation && !string.IsNullOrEmpty(Mod.ExtendedIconInformation); + + extendedContent.Alpha = showExtended ? 1 : 0; + extendedText.Text = Mod.ExtendedIconInformation; } private void updateState() { acronymText.FadeColour(Active.Value ? activeForegroundColour : inactiveForegroundColour, 200, Easing.OutQuint); background.FadeColour(Active.Value ? activeBackgroundColour : inactiveBackgroundColour, 200, Easing.OutQuint); + + extendedText.Colour = Active.Value ? activeBackgroundColour.Lighten(0.2f) : inactiveBackgroundColour; + extendedBackground.Colour = Active.Value ? activeBackgroundColour.Darken(2.4f) : inactiveBackgroundColour.Darken(2.8f); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + modSettingsChangeTracker?.Dispose(); } } } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 6016a53918..e9c35555c8 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -23,11 +23,13 @@ using osu.Game.Skinning; using osuTK; using osu.Game.Rulesets.Objects.Pooling; using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics.Primitives; namespace osu.Game.Rulesets.UI { [Cached(typeof(IPooledHitObjectProvider))] [Cached(typeof(IPooledSampleProvider))] + [Cached] public abstract partial class Playfield : CompositeDrawable, IPooledHitObjectProvider, IPooledSampleProvider { /// @@ -93,6 +95,16 @@ namespace osu.Game.Rulesets.UI /// public readonly BindableBool DisplayJudgements = new BindableBool(true); + /// + /// A screen space draw quad which resembles the edges of the playfield for skinning purposes. + /// This will allow users / components to snap objects to the "edge" of the playfield. + /// + /// + /// Rulesets which reduce the visible area further than the full relative playfield space itself + /// should retarget this to the ScreenSpaceDrawQuad of the appropriate container. + /// + public virtual Quad SkinnableComponentScreenSpaceDrawQuad => ScreenSpaceDrawQuad; + [Resolved(CanBeNull = true)] [CanBeNull] protected IReadOnlyList Mods { get; private set; } diff --git a/osu.Game/Rulesets/UI/PlayfieldAdjustmentContainer.cs b/osu.Game/Rulesets/UI/PlayfieldAdjustmentContainer.cs index 0f440adef8..9339602ac6 100644 --- a/osu.Game/Rulesets/UI/PlayfieldAdjustmentContainer.cs +++ b/osu.Game/Rulesets/UI/PlayfieldAdjustmentContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Rulesets/UI/PlayfieldBorder.cs b/osu.Game/Rulesets/UI/PlayfieldBorder.cs index 18bd5b9b93..e87421fc88 100644 --- a/osu.Game/Rulesets/UI/PlayfieldBorder.cs +++ b/osu.Game/Rulesets/UI/PlayfieldBorder.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 osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Rulesets/UI/PlayfieldBorderStyle.cs b/osu.Game/Rulesets/UI/PlayfieldBorderStyle.cs index 79f3a2ca84..503bc8fd99 100644 --- a/osu.Game/Rulesets/UI/PlayfieldBorderStyle.cs +++ b/osu.Game/Rulesets/UI/PlayfieldBorderStyle.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Localisation; using osu.Game.Localisation; diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index a24e22f22b..39b83ecca1 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -160,62 +160,37 @@ namespace osu.Game.Rulesets.UI #region Key Counter Attachment - public void Attach(KeyCounterDisplay keyCounter) + public void Attach(InputCountController inputCountController) { - var receptor = new ActionReceptor(keyCounter); + var triggers = KeyBindingContainer.DefaultKeyBindings + .Select(b => b.GetAction()) + .Distinct() + .OrderBy(action => action) + .Select(action => new KeyCounterActionTrigger(action)) + .ToArray(); - KeyBindingContainer.Add(receptor); - - keyCounter.SetReceptor(receptor); - keyCounter.AddRange(KeyBindingContainer.DefaultKeyBindings - .Select(b => b.GetAction()) - .Distinct() - .OrderBy(action => action) - .Select(action => new KeyCounterActionTrigger(action))); - } - - private partial class ActionReceptor : KeyCounterDisplay.Receptor, IKeyBindingHandler - { - public ActionReceptor(KeyCounterDisplay target) - : base(target) - { - } - - public bool OnPressed(KeyBindingPressEvent e) => Target.Counters.Where(c => c.Trigger is KeyCounterActionTrigger) - .Select(c => (KeyCounterActionTrigger)c.Trigger) - .Any(c => c.OnPressed(e.Action, Clock.Rate >= 0)); - - public void OnReleased(KeyBindingReleaseEvent e) - { - foreach (var c - in Target.Counters.Where(c => c.Trigger is KeyCounterActionTrigger).Select(c => (KeyCounterActionTrigger)c.Trigger)) - c.OnReleased(e.Action, Clock.Rate >= 0); - } + KeyBindingContainer.AddRange(triggers); + inputCountController.AddRange(triggers); } #endregion #region Keys per second Counter Attachment - public void Attach(ClicksPerSecondCalculator calculator) - { - var listener = new ActionListener(calculator); - - KeyBindingContainer.Add(listener); - } + public void Attach(ClicksPerSecondController controller) => KeyBindingContainer.Add(new ActionListener(controller)); private partial class ActionListener : Component, IKeyBindingHandler { - private readonly ClicksPerSecondCalculator calculator; + private readonly ClicksPerSecondController controller; - public ActionListener(ClicksPerSecondCalculator calculator) + public ActionListener(ClicksPerSecondController controller) { - this.calculator = calculator; + this.controller = controller; } public bool OnPressed(KeyBindingPressEvent e) { - calculator.AddInputTimestamp(); + controller.AddInputTimestamp(); return false; } @@ -243,33 +218,11 @@ namespace osu.Game.Rulesets.UI base.ReloadMappings(realmKeyBindings); KeyBindings = KeyBindings.Where(b => RealmKeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList(); + RealmKeyBindingStore.ClearDuplicateBindings(KeyBindings); } } } - /// - /// Expose the in a capable . - /// - public interface IHasReplayHandler - { - ReplayInputHandler ReplayInputHandler { get; set; } - } - - public interface IHasRecordingHandler - { - public ReplayRecorder Recorder { set; } - } - - /// - /// Supports attaching various HUD pieces. - /// Keys will be populated automatically and a receptor will be injected inside. - /// - public interface ICanAttachHUDPieces - { - void Attach(KeyCounterDisplay keyCounter); - void Attach(ClicksPerSecondCalculator calculator); - } - public class RulesetInputManagerInputState : InputState where T : struct { diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs index c957a84eb1..b0bde50cae 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.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 - namespace osu.Game.Rulesets.UI.Scrolling.Algorithms { public class ConstantScrollAlgorithm : IScrollAlgorithm diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs index 4c7564b791..d23658ac33 100644 --- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs +++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs @@ -64,8 +64,6 @@ namespace osu.Game.Rulesets.UI.Scrolling MaxValue = time_span_max }; - protected virtual ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Sequential; - ScrollVisualisationMethod IDrawableScrollingRuleset.VisualisationMethod => VisualisationMethod; /// @@ -83,7 +81,7 @@ namespace osu.Game.Rulesets.UI.Scrolling /// protected readonly SortedList ControlPoints = new SortedList(Comparer.Default); - protected IScrollingInfo ScrollingInfo => scrollingInfo; + public IScrollingInfo ScrollingInfo => scrollingInfo; [Cached(Type = typeof(IScrollingInfo))] private readonly LocalScrollingInfo scrollingInfo; @@ -99,20 +97,7 @@ namespace osu.Game.Rulesets.UI.Scrolling [BackgroundDependencyLoader] private void load() { - switch (VisualisationMethod) - { - case ScrollVisualisationMethod.Sequential: - scrollingInfo.Algorithm = new SequentialScrollAlgorithm(ControlPoints); - break; - - case ScrollVisualisationMethod.Overlapping: - scrollingInfo.Algorithm = new OverlappingScrollAlgorithm(ControlPoints); - break; - - case ScrollVisualisationMethod.Constant: - scrollingInfo.Algorithm = new ConstantScrollAlgorithm(); - break; - } + updateScrollAlgorithm(); double lastObjectTime = Beatmap.HitObjects.Any() ? Beatmap.GetLastObjectTime() : double.MaxValue; double baseBeatLength = TimingControlPoint.DEFAULT_BEAT_LENGTH; @@ -178,6 +163,36 @@ namespace osu.Game.Rulesets.UI.Scrolling throw new ArgumentException($"{nameof(Playfield)} must be a {nameof(ScrollingPlayfield)} when using {nameof(DrawableScrollingRuleset)}."); } + private ScrollVisualisationMethod visualisationMethod = ScrollVisualisationMethod.Sequential; + + public ScrollVisualisationMethod VisualisationMethod + { + get => visualisationMethod; + set + { + visualisationMethod = value; + updateScrollAlgorithm(); + } + } + + private void updateScrollAlgorithm() + { + switch (VisualisationMethod) + { + case ScrollVisualisationMethod.Sequential: + scrollingInfo.Algorithm.Value = new SequentialScrollAlgorithm(ControlPoints); + break; + + case ScrollVisualisationMethod.Overlapping: + scrollingInfo.Algorithm.Value = new OverlappingScrollAlgorithm(ControlPoints); + break; + + case ScrollVisualisationMethod.Constant: + scrollingInfo.Algorithm.Value = new ConstantScrollAlgorithm(); + break; + } + } + /// /// Adjusts the scroll speed of s. /// @@ -217,7 +232,9 @@ namespace osu.Game.Rulesets.UI.Scrolling public IBindable TimeRange { get; } = new BindableDouble(); - public IScrollAlgorithm Algorithm { get; set; } + public readonly Bindable Algorithm = new Bindable(new ConstantScrollAlgorithm()); + + IBindable IScrollingInfo.Algorithm => Algorithm; } } } diff --git a/osu.Game/Rulesets/UI/Scrolling/IDrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/IDrawableScrollingRuleset.cs index f3a3bb18bd..b348a22009 100644 --- a/osu.Game/Rulesets/UI/Scrolling/IDrawableScrollingRuleset.cs +++ b/osu.Game/Rulesets/UI/Scrolling/IDrawableScrollingRuleset.cs @@ -11,5 +11,7 @@ namespace osu.Game.Rulesets.UI.Scrolling public interface IDrawableScrollingRuleset { ScrollVisualisationMethod VisualisationMethod { get; } + + IScrollingInfo ScrollingInfo { get; } } } diff --git a/osu.Game/Rulesets/UI/Scrolling/IScrollingInfo.cs b/osu.Game/Rulesets/UI/Scrolling/IScrollingInfo.cs index e00f0ffe5d..4a79c1a447 100644 --- a/osu.Game/Rulesets/UI/Scrolling/IScrollingInfo.cs +++ b/osu.Game/Rulesets/UI/Scrolling/IScrollingInfo.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 osu.Framework.Bindables; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI.Scrolling.Algorithms; @@ -17,13 +15,13 @@ namespace osu.Game.Rulesets.UI.Scrolling IBindable Direction { get; } /// - /// + /// The span of time that is visible by the length of the scrolling axes. /// IBindable TimeRange { get; } /// /// The algorithm which controls positions and sizes. /// - IScrollAlgorithm Algorithm { get; } + IBindable Algorithm { get; } } } diff --git a/osu.Game/Rulesets/UI/Scrolling/ISupportConstantAlgorithmToggle.cs b/osu.Game/Rulesets/UI/Scrolling/ISupportConstantAlgorithmToggle.cs new file mode 100644 index 0000000000..aaa635350e --- /dev/null +++ b/osu.Game/Rulesets/UI/Scrolling/ISupportConstantAlgorithmToggle.cs @@ -0,0 +1,15 @@ +// 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.Bindables; + +namespace osu.Game.Rulesets.UI.Scrolling +{ + /// + /// Denotes a which supports toggling constant algorithm for better display in the editor. + /// + public interface ISupportConstantAlgorithmToggle : IDrawableScrollingRuleset + { + public BindableBool ShowSpeedChanges { get; } + } +} diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index b93a427196..129918da14 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -13,6 +13,7 @@ using osu.Framework.Layout; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.UI.Scrolling.Algorithms; using osuTK; namespace osu.Game.Rulesets.UI.Scrolling @@ -21,6 +22,7 @@ namespace osu.Game.Rulesets.UI.Scrolling { private readonly IBindable timeRange = new BindableDouble(); private readonly IBindable direction = new Bindable(); + private readonly IBindable algorithm = new Bindable(); /// /// Whether the scrolling direction is horizontal or vertical. @@ -59,9 +61,11 @@ namespace osu.Game.Rulesets.UI.Scrolling { direction.BindTo(scrollingInfo.Direction); timeRange.BindTo(scrollingInfo.TimeRange); + algorithm.BindTo(scrollingInfo.Algorithm); direction.ValueChanged += _ => layoutCache.Invalidate(); timeRange.ValueChanged += _ => layoutCache.Invalidate(); + algorithm.ValueChanged += _ => layoutCache.Invalidate(); } /// @@ -73,7 +77,7 @@ namespace osu.Game.Rulesets.UI.Scrolling public double TimeAtPosition(float localPosition, double currentTime) { float scrollPosition = axisInverted ? -localPosition : localPosition; - return scrollingInfo.Algorithm.TimeAt(scrollPosition, currentTime, timeRange.Value, scrollLength); + return algorithm.Value.TimeAt(scrollPosition, currentTime, timeRange.Value, scrollLength); } /// @@ -95,7 +99,7 @@ namespace osu.Game.Rulesets.UI.Scrolling /// public float PositionAtTime(double time, double currentTime, double? originTime = null) { - float scrollPosition = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength, originTime); + float scrollPosition = algorithm.Value.PositionAt(time, currentTime, timeRange.Value, scrollLength, originTime); return axisInverted ? -scrollPosition : scrollPosition; } @@ -122,7 +126,7 @@ namespace osu.Game.Rulesets.UI.Scrolling /// public float LengthAtTime(double startTime, double endTime) { - return scrollingInfo.Algorithm.GetLength(startTime, endTime, timeRange.Value, scrollLength); + return algorithm.Value.GetLength(startTime, endTime, timeRange.Value, scrollLength); } private float scrollLength => scrollingAxis == Direction.Horizontal ? DrawWidth : DrawHeight; @@ -169,7 +173,7 @@ namespace osu.Game.Rulesets.UI.Scrolling foreach (var entry in Entries) setComputedLifetimeStart(entry); - scrollingInfo.Algorithm.Reset(); + algorithm.Value.Reset(); layoutCache.Validate(); } @@ -224,7 +228,7 @@ namespace osu.Game.Rulesets.UI.Scrolling break; } - return scrollingInfo.Algorithm.GetDisplayStartTime(entry.HitObject.StartTime, startOffset, timeRange.Value, scrollLength); + return algorithm.Value.GetDisplayStartTime(entry.HitObject.StartTime, startOffset, timeRange.Value, scrollLength); } private void setComputedLifetimeStart(HitObjectLifetimeEntry entry) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index 7d141113df..1a17349d12 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Rulesets.Objects.Drawables; @@ -20,7 +18,7 @@ namespace osu.Game.Rulesets.UI.Scrolling public new ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)base.HitObjectContainer; [Resolved] - public IScrollingInfo ScrollingInfo { get; private set; } + public IScrollingInfo ScrollingInfo { get; private set; } = null!; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Scoring/HitResultDisplayStatistic.cs b/osu.Game/Scoring/HitResultDisplayStatistic.cs index 20deff4875..59e074fb5f 100644 --- a/osu.Game/Scoring/HitResultDisplayStatistic.cs +++ b/osu.Game/Scoring/HitResultDisplayStatistic.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Localisation; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game/Scoring/IScoreInfo.cs b/osu.Game/Scoring/IScoreInfo.cs index ffc30384d2..a1d076b8c2 100644 --- a/osu.Game/Scoring/IScoreInfo.cs +++ b/osu.Game/Scoring/IScoreInfo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Beatmaps; using osu.Game.Database; @@ -11,7 +9,7 @@ using osu.Game.Users; namespace osu.Game.Scoring { - public interface IScoreInfo : IHasOnlineID, IHasNamedFiles + public interface IScoreInfo : IHasOnlineID { IUser User { get; } @@ -24,13 +22,13 @@ namespace osu.Game.Scoring double Accuracy { get; } - bool HasReplay { get; } + long LegacyOnlineID { get; } DateTimeOffset Date { get; } double? PP { get; } - IBeatmapInfo Beatmap { get; } + IBeatmapInfo? Beatmap { get; } IRulesetInfo Ruleset { get; } diff --git a/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs b/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs index f2e8cf141b..d34edf7bdf 100644 --- a/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs +++ b/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs @@ -19,6 +19,13 @@ namespace osu.Game.Scoring.Legacy [JsonObject(MemberSerialization.OptIn)] public class LegacyReplaySoloScoreInfo { + /// + /// The value of this property should correspond to + /// (i.e. come from the `solo_scores` ID scheme). + /// + [JsonProperty("online_id")] + public long OnlineID { get; set; } = -1; + [JsonProperty("mods")] public APIMod[] Mods { get; set; } = Array.Empty(); @@ -30,6 +37,7 @@ namespace osu.Game.Scoring.Legacy public static LegacyReplaySoloScoreInfo FromScore(ScoreInfo score) => new LegacyReplaySoloScoreInfo { + OnlineID = score.OnlineID, Mods = score.APIMods, Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value), MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value), diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 9b145ad56e..c5e6e3bcce 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -46,6 +46,16 @@ namespace osu.Game.Scoring.Legacy score.ScoreInfo = scoreInfo; int version = sr.ReadInt32(); + + scoreInfo.IsLegacyScore = version < LegacyScoreEncoder.FIRST_LAZER_VERSION; + + // TotalScoreVersion gets initialised to LATEST_VERSION. + // In the case where the incoming score has either an osu!stable or old lazer version, we need + // to mark it with the correct version increment to trigger reprocessing to new standardised scoring. + // + // See StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(). + scoreInfo.TotalScoreVersion = version < 30000002 ? 30000001 : LegacyScoreEncoder.LATEST_VERSION; + string beatmapHash = sr.ReadString(); workingBeatmap = GetBeatmap(beatmapHash); @@ -91,9 +101,9 @@ namespace osu.Game.Scoring.Legacy byte[] compressedReplay = sr.ReadByteArray(); if (version >= 20140721) - scoreInfo.OnlineID = sr.ReadInt64(); + scoreInfo.LegacyOnlineID = sr.ReadInt64(); else if (version >= 20121008) - scoreInfo.OnlineID = sr.ReadInt32(); + scoreInfo.LegacyOnlineID = sr.ReadInt32(); byte[] compressedScoreInfo = null; @@ -111,6 +121,7 @@ namespace osu.Game.Scoring.Legacy Debug.Assert(readScore != null); + score.ScoreInfo.OnlineID = readScore.OnlineID; score.ScoreInfo.Statistics = readScore.Statistics; score.ScoreInfo.MaximumStatistics = readScore.MaximumStatistics; score.ScoreInfo.Mods = readScore.Mods.Select(m => m.ToMod(currentRuleset)).ToArray(); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index a78ae24da2..872f09dda6 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -28,9 +28,12 @@ namespace osu.Game.Scoring.Legacy /// /// /// 30000001: Appends to the end of scores. + /// 30000002: Score stored to replay calculated using the Score V2 algorithm. Legacy scores on this version are candidate to Score V1 -> V2 conversion. + /// 30000003: First version after converting legacy total score to standardised. + /// 30000004: Fixed mod multipliers during legacy score conversion. Reconvert all scores. /// /// - public const int LATEST_VERSION = 30000001; + public const int LATEST_VERSION = 30000004; /// /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays. @@ -64,7 +67,7 @@ namespace osu.Game.Scoring.Legacy { sw.Write((byte)(score.ScoreInfo.Ruleset.OnlineID)); sw.Write(LATEST_VERSION); - sw.Write(score.ScoreInfo.BeatmapInfo.MD5Hash); + sw.Write(score.ScoreInfo.BeatmapInfo!.MD5Hash); sw.Write(score.ScoreInfo.User.Username); sw.Write(FormattableString.Invariant($"lazer-{score.ScoreInfo.User.Username}-{score.ScoreInfo.Date}").ComputeMD5Hash()); sw.Write((ushort)(score.ScoreInfo.GetCount300() ?? 0)); @@ -81,7 +84,7 @@ namespace osu.Game.Scoring.Legacy sw.Write(getHpGraphFormatted()); sw.Write(score.ScoreInfo.Date.DateTime); sw.WriteByteArray(createReplayData()); - sw.Write((long)0); + sw.Write(score.ScoreInfo.LegacyOnlineID); writeModSpecificData(score.ScoreInfo, sw); sw.WriteByteArray(createScoreInfoData()); } @@ -125,10 +128,10 @@ namespace osu.Game.Scoring.Legacy // As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing. double offset = beatmap?.BeatmapInfo.BeatmapVersion < 5 ? -LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0; + int lastTime = 0; + if (score.Replay != null) { - int lastTime = 0; - foreach (var f in score.Replay.Frames) { var legacyFrame = getLegacyFrame(f); diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs index 84bf6d15f6..f6ea5aa455 100644 --- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs @@ -1,11 +1,10 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Scoring; namespace osu.Game.Scoring.Legacy @@ -18,6 +17,9 @@ namespace osu.Game.Scoring.Legacy public static long GetDisplayScore(this ScoreInfo scoreInfo, ScoringMode mode) => getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics); + public static long GetDisplayScore(this SoloScoreInfo soloScoreInfo, ScoringMode mode) + => getDisplayScore(soloScoreInfo.RulesetID, soloScoreInfo.TotalScore, mode, soloScoreInfo.MaximumStatistics); + private static long getDisplayScore(int rulesetId, long score, ScoringMode mode, IReadOnlyDictionary maximumStatistics) { if (mode == ScoringMode.Standardised) @@ -29,44 +31,37 @@ namespace osu.Game.Scoring.Legacy .DefaultIfEmpty(0) .Sum(); - // This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring. - // The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes. - double scaledRawScore = score / ScoreProcessor.MAX_SCORE; - - return (long)Math.Round(Math.Pow(scaledRawScore * Math.Max(1, maxBasicJudgements), 2) * getStandardisedToClassicMultiplier(rulesetId)); + return convertStandardisedToClassic(rulesetId, score, maxBasicJudgements); } /// - /// Returns a ballpark multiplier which gives a similar "feel" for how large scores should get when displayed in "classic" mode. + /// Returns a ballpark "classic" score which gives a similar "feel" to stable. /// This is different per ruleset to match the different algorithms used in the scoring implementation. /// - private static double getStandardisedToClassicMultiplier(int rulesetId) + /// + /// The coefficients chosen here were determined by a least-squares fit performed over all beatmaps + /// with the goal of minimising the relative error of maximum possible base score (without bonus). + /// The constant coefficients (100000, 1 / 10d) - while being detrimental to the least-squares fit - are forced, + /// so that every 10 points in standardised mode converts to at least 1 point in classic mode. + /// This is done to account for bonus judgements in a way that does not reorder scores. + /// + private static long convertStandardisedToClassic(int rulesetId, long standardisedTotalScore, int objectCount) { - double multiplier; - switch (rulesetId) { - // For non-legacy rulesets, just go with the same as the osu! ruleset. - // This is arbitrary, but at least allows the setting to do something to the score. - default: case 0: - multiplier = 36; - break; + return (long)Math.Round((objectCount * objectCount * 32.57 + 100000) * standardisedTotalScore / ScoreProcessor.MAX_SCORE); case 1: - multiplier = 22; - break; + return (long)Math.Round((objectCount * 1109 + 100000) * standardisedTotalScore / ScoreProcessor.MAX_SCORE); case 2: - multiplier = 28; - break; + return (long)Math.Round(Math.Pow(standardisedTotalScore / ScoreProcessor.MAX_SCORE * objectCount, 2) * 21.62 + standardisedTotalScore / 10d); case 3: - multiplier = 16; - break; + default: + return standardisedTotalScore; } - - return multiplier; } public static int? GetCountGeki(this ScoreInfo scoreInfo) diff --git a/osu.Game/Scoring/Score.cs b/osu.Game/Scoring/Score.cs index 06bc3edd37..7152f93f94 100644 --- a/osu.Game/Scoring/Score.cs +++ b/osu.Game/Scoring/Score.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index f69c1b9385..b216c0897e 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -42,7 +42,7 @@ namespace osu.Game.Scoring this.api = api; } - protected override ScoreInfo? CreateModel(ArchiveReader archive) + protected override ScoreInfo? CreateModel(ArchiveReader archive, ImportParameters parameters) { string name = archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase)); @@ -52,9 +52,23 @@ namespace osu.Game.Scoring { return new DatabasedLegacyScoreDecoder(rulesets, beatmaps()).Parse(stream).ScoreInfo; } - catch (LegacyScoreDecoder.BeatmapNotFoundException e) + catch (LegacyScoreDecoder.BeatmapNotFoundException notFound) { - Logger.Log($@"Score '{name}' failed to import: no corresponding beatmap with the hash '{e.Hash}' could be found.", LoggingTarget.Database); + Logger.Log($@"Score '{archive.Name}' failed to import: no corresponding beatmap with the hash '{notFound.Hash}' could be found.", LoggingTarget.Database); + + if (!parameters.Batch) + { + // In the case of a missing beatmap, let's attempt to resolve it and show a prompt to the user to download the required beatmap. + var req = new GetBeatmapRequest(new BeatmapInfo { MD5Hash = notFound.Hash }); + req.Success += res => PostNotification?.Invoke(new MissingBeatmapNotification(res, archive, notFound.Hash)); + api.Queue(req); + } + + return null; + } + catch (Exception e) + { + Logger.Log($@"Failed to parse headers of score '{archive.Name}': {e}.", LoggingTarget.Database); return null; } } @@ -64,12 +78,14 @@ namespace osu.Game.Scoring protected override void Populate(ScoreInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) { + Debug.Assert(model.BeatmapInfo != null); + // Ensure the beatmap is not detached. if (!model.BeatmapInfo.IsManaged) - model.BeatmapInfo = realm.Find(model.BeatmapInfo.ID); + model.BeatmapInfo = realm.Find(model.BeatmapInfo.ID)!; if (!model.Ruleset.IsManaged) - model.Ruleset = realm.Find(model.Ruleset.ShortName); + model.Ruleset = realm.Find(model.Ruleset.ShortName)!; // These properties are known to be non-null, but these final checks ensure a null hasn't come from somewhere (or the refetch has failed). // Under no circumstance do we want these to be written to realm as null. @@ -83,6 +99,16 @@ namespace osu.Game.Scoring if (string.IsNullOrEmpty(model.MaximumStatisticsJson)) model.MaximumStatisticsJson = JsonConvert.SerializeObject(model.MaximumStatistics); + + // for pre-ScoreV2 lazer scores, apply a best-effort conversion of total score to ScoreV2. + // this requires: max combo, statistics, max statistics (where available), and mods to already be populated on the score. + if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(model)) + model.TotalScore = StandardisedScoreMigrationTools.GetNewStandardised(model); + else if (model.IsLegacyScore) + { + model.LegacyTotalScore = model.TotalScore; + model.TotalScore = StandardisedScoreMigrationTools.ConvertFromLegacyTotalScore(model, beatmaps()); + } } /// @@ -91,10 +117,12 @@ namespace osu.Game.Scoring /// The score to populate the statistics of. public void PopulateMaximumStatistics(ScoreInfo score) { + Debug.Assert(score.BeatmapInfo != null); + if (score.MaximumStatistics.Select(kvp => kvp.Value).Sum() > 0) return; - var beatmap = score.BeatmapInfo.Detach(); + var beatmap = score.BeatmapInfo!.Detach(); var ruleset = score.Ruleset.Detach(); var rulesetInstance = ruleset.CreateInstance(); @@ -146,16 +174,57 @@ namespace osu.Game.Scoring #pragma warning restore CS0618 } + // Very naive local caching to improve performance of large score imports (where the username is usually the same for most or all scores). + private readonly Dictionary usernameLookupCache = new Dictionary(); + protected override void PostImport(ScoreInfo model, Realm realm, ImportParameters parameters) { base.PostImport(model, realm, parameters); - var userRequest = new GetUserRequest(model.RealmUser.Username); + populateUserDetails(model); + + Debug.Assert(model.BeatmapInfo != null); + + // This needs to be run after user detail population to ensure we have a valid user id. + if (api.IsLoggedIn && api.LocalUser.Value.OnlineID == model.UserID && (model.BeatmapInfo.LastPlayed == null || model.Date > model.BeatmapInfo.LastPlayed)) + model.BeatmapInfo.LastPlayed = model.Date; + } + + /// + /// Legacy replays only store a username. + /// This will populate a user ID during import. + /// + private void populateUserDetails(ScoreInfo model) + { + if (model.RealmUser.OnlineID == APIUser.SYSTEM_USER_ID) + return; + + string username = model.RealmUser.Username; + + if (usernameLookupCache.TryGetValue(username, out var existing)) + { + model.User = existing; + return; + } + + var userRequest = new GetUserRequest(username); api.Perform(userRequest); if (userRequest.Response is APIUser user) + { + usernameLookupCache.TryAdd(username, new APIUser + { + // Because this is a permanent cache, let's only store the pieces we're interested in, + // rather than the full API response. If we start to store more than these three fields + // in realm, this should be undone. + Id = user.Id, + Username = user.Username, + CountryCode = user.CountryCode, + }); + model.User = user; + } } } } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index e084c45de0..d712702331 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -15,6 +15,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring.Legacy; using osu.Game.Users; using osu.Game.Utils; using Realms; @@ -34,9 +35,16 @@ namespace osu.Game.Scoring /// The this score was made against. /// /// - /// When setting this, make sure to also set to allow relational consistency when a beatmap is potentially changed. + /// + /// This property may be if the score was set on a beatmap (or a version of the beatmap) that is not available locally + /// e.g. due to online updates, or local modifications to the beatmap. + /// The property will only link to a if its matches . + /// + /// + /// Due to the above, whenever setting this, make sure to also set to allow relational consistency when a beatmap is potentially changed. + /// /// - public BeatmapInfo BeatmapInfo { get; set; } = null!; + public BeatmapInfo? BeatmapInfo { get; set; } /// /// The at the point in time when the score was set. @@ -53,19 +61,65 @@ namespace osu.Game.Scoring public long TotalScore { get; set; } + /// + /// The version of processing applied to calculate total score as stored in the database. + /// If this does not match , + /// the total score has not yet been updated to reflect the current scoring values. + /// + /// See 's conversion logic. + /// + /// + /// This may not match the version stored in the replay files. + /// + public int TotalScoreVersion { get; set; } = LegacyScoreEncoder.LATEST_VERSION; + + /// + /// Used to preserve the total score for legacy scores. + /// + /// + /// Not populated if is false. + /// + public long? LegacyTotalScore { get; set; } + + /// + /// If background processing of this beatmap failed in some way, this flag will become true. + /// Should be used to ensure we don't repeatedly attempt to reprocess the same scores each startup even though we already know they will fail. + /// + /// + /// See https://github.com/ppy/osu/issues/24301 for one example of how this can occur (missing beatmap file on disk). + /// + public bool BackgroundReprocessingFailed { get; set; } + public int MaxCombo { get; set; } public double Accuracy { get; set; } - public bool HasReplay => !string.IsNullOrEmpty(Hash); + [Ignored] + public bool HasOnlineReplay { get; set; } public DateTimeOffset Date { get; set; } public double? PP { get; set; } + /// + /// The online ID of this score. + /// + /// + /// In the osu-web database, this ID (if present) comes from the new solo_scores table. + /// [Indexed] public long OnlineID { get; set; } = -1; + /// + /// The legacy online ID of this score. + /// + /// + /// In the osu-web database, this ID (if present) comes from the legacy osu_scores_*_high tables. + /// This ID is also stored to replays set on osu!stable. + /// + [Indexed] + public long LegacyOnlineID { get; set; } = -1; + [MapTo("User")] public RealmUser RealmUser { get; set; } = null!; @@ -82,6 +136,7 @@ namespace osu.Game.Scoring { Ruleset = ruleset ?? new RulesetInfo(); BeatmapInfo = beatmap ?? new BeatmapInfo(); + BeatmapHash = BeatmapInfo.Hash; RealmUser = realmUser ?? new RealmUser(); ID = Guid.NewGuid(); } @@ -128,14 +183,11 @@ namespace osu.Game.Scoring public int RankInt { get; set; } IRulesetInfo IScoreInfo.Ruleset => Ruleset; - IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo; + IBeatmapInfo? IScoreInfo.Beatmap => BeatmapInfo; IUser IScoreInfo.User => User; - IEnumerable IHasNamedFiles.Files => Files; #region Properties required to make things work with existing usages - public Guid BeatmapInfoID => BeatmapInfo.ID; - public int UserID => RealmUser.OnlineID; public int RulesetID => Ruleset.OnlineID; @@ -181,8 +233,7 @@ namespace osu.Game.Scoring /// /// Whether this represents a legacy (osu!stable) score. /// - [Ignored] - public bool IsLegacyScore => Mods.OfType().Any(); + public bool IsLegacyScore { get; set; } private Dictionary? statistics; @@ -311,7 +362,7 @@ namespace osu.Game.Scoring case HitResult.LargeBonus: case HitResult.SmallBonus: if (MaximumStatistics.TryGetValue(r.result, out int count) && count > 0) - yield return new HitResultDisplayStatistic(r.result, value, null, r.displayName); + yield return new HitResultDisplayStatistic(r.result, value, count, r.displayName); break; diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs index 7979ca8aaa..6e57a9fd0b 100644 --- a/osu.Game/Scoring/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/ScoreInfoExtensions.cs @@ -1,9 +1,10 @@ // 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 System.Linq; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Scoring { @@ -12,6 +13,24 @@ namespace osu.Game.Scoring /// /// A user-presentable display title representing this score. /// - public static string GetDisplayTitle(this IScoreInfo scoreInfo) => $"{scoreInfo.User.Username} playing {scoreInfo.Beatmap.GetDisplayTitle()}"; + public static string GetDisplayTitle(this IScoreInfo scoreInfo) => $"{scoreInfo.User.Username} playing {scoreInfo.Beatmap?.GetDisplayTitle() ?? "unknown"}"; + + /// + /// Orders an array of s by total score. + /// + /// The array of s to reorder. + /// The given ordered by decreasing total score. + public static IEnumerable OrderByTotalScore(this IEnumerable scores) + => scores.OrderByDescending(s => s.TotalScore) + .ThenBy(s => s.OnlineID) + // Local scores may not have an online ID. Fall back to date in these cases. + .ThenBy(s => s.Date); + + /// + /// Retrieves the maximum achievable combo for the provided score. + /// + /// The to compute the maximum achievable combo for. + /// The maximum achievable combo. + public static int GetMaximumAchievableCombo(this ScoreInfo score) => score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value); } } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index d5509538fd..02d9e0a280 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -69,17 +69,6 @@ namespace osu.Game.Scoring return Realm.Run(r => r.All().FirstOrDefault(query)?.Detach()); } - /// - /// Orders an array of s by total score. - /// - /// The array of s to reorder. - /// The given ordered by decreasing total score. - public IEnumerable OrderByTotalScore(IEnumerable scores) - => scores.OrderByDescending(s => s.TotalScore) - .ThenBy(s => s.OnlineID) - // Local scores may not have an online ID. Fall back to date in these cases. - .ThenBy(s => s.Date); - /// /// Retrieves a bindable that represents the total score of a . /// @@ -100,13 +89,6 @@ namespace osu.Game.Scoring /// The bindable containing the formatted total score string. public Bindable GetBindableTotalScoreString([NotNull] ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score)); - /// - /// Retrieves the maximum achievable combo for the provided score. - /// - /// The to compute the maximum achievable combo for. - /// The maximum achievable combo. - public int GetMaximumAchievableCombo([NotNull] ScoreInfo score) => score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value); - /// /// Provides the total score of a . Responds to changes in the currently-selected . /// @@ -159,7 +141,7 @@ namespace osu.Game.Scoring { Realm.Run(r => { - var beatmapScores = r.Find(beatmap.ID).Scores.ToList(); + var beatmapScores = r.Find(beatmap.ID)!.Scores.ToList(); Delete(beatmapScores, silent); }); } @@ -168,7 +150,11 @@ namespace osu.Game.Scoring public Task Import(ImportTask[] imports, ImportParameters parameters = default) => scoreImporter.Import(imports, parameters); - public override bool IsAvailableLocally(ScoreInfo model) => Realm.Run(realm => realm.All().Any(s => s.OnlineID == model.OnlineID)); + public override bool IsAvailableLocally(ScoreInfo model) + => Realm.Run(realm => realm.All() + // this basically inlines `ModelExtension.MatchesOnlineID(IScoreInfo, IScoreInfo)`, + // because that method can't be used here, as realm can't translate it to its query language. + .Any(s => s.OnlineID == model.OnlineID || s.LegacyOnlineID == model.LegacyOnlineID)); public IEnumerable HandledExtensions => scoreImporter.HandledExtensions; diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs index 17a0c0ea6a..1f2b1aeb95 100644 --- a/osu.Game/Scoring/ScorePerformanceCache.cs +++ b/osu.Game/Scoring/ScorePerformanceCache.cs @@ -1,12 +1,9 @@ // 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.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Database; @@ -21,7 +18,7 @@ namespace osu.Game.Scoring public partial class ScorePerformanceCache : MemoryCachingComponent { [Resolved] - private BeatmapDifficultyCache difficultyCache { get; set; } + private BeatmapDifficultyCache difficultyCache { get; set; } = null!; protected override bool CacheNullValues => false; @@ -30,14 +27,14 @@ namespace osu.Game.Scoring /// /// The score to do the calculation on. /// An optional to cancel the operation. - public Task CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) => + public Task CalculatePerformanceAsync(ScoreInfo score, CancellationToken token = default) => GetAsync(new PerformanceCacheLookup(score), token); - protected override async Task ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default) + protected override async Task ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default) { var score = lookup.ScoreInfo; - var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, token).ConfigureAwait(false); + var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, token).ConfigureAwait(false); // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. if (attributes?.Attributes == null) diff --git a/osu.Game/Scoring/ScoreRank.cs b/osu.Game/Scoring/ScoreRank.cs index a1916953c4..327e4191d7 100644 --- a/osu.Game/Scoring/ScoreRank.cs +++ b/osu.Game/Scoring/ScoreRank.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.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Screens/BackgroundScreenStack.cs b/osu.Game/Screens/BackgroundScreenStack.cs index ca0dad83c8..99ca383b9f 100644 --- a/osu.Game/Screens/BackgroundScreenStack.cs +++ b/osu.Game/Screens/BackgroundScreenStack.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Graphics; using osu.Framework.Screens; @@ -24,7 +22,7 @@ namespace osu.Game.Screens /// /// The screen to attempt to push. /// Whether the push succeeded. For example, if the existing screen was already of the correct type this will return false. - public bool Push(BackgroundScreen screen) + public bool Push(BackgroundScreen? screen) { if (screen == null) return false; diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs index 312fd496a1..85ea881006 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs @@ -36,6 +36,9 @@ namespace osu.Game.Screens.Backgrounds /// public readonly Bindable IgnoreUserSettings = new Bindable(true); + /// + /// Whether or not the storyboard loaded should completely hide the background behind it. + /// public readonly Bindable StoryboardReplacesBackground = new Bindable(); /// @@ -60,12 +63,11 @@ namespace osu.Game.Screens.Backgrounds InternalChild = dimmable = CreateFadeContainer(); + dimmable.StoryboardReplacesBackground.BindTo(StoryboardReplacesBackground); dimmable.IgnoreUserSettings.BindTo(IgnoreUserSettings); dimmable.IsBreakTime.BindTo(IsBreakTime); dimmable.BlurAmount.BindTo(BlurAmount); dimmable.DimWhenUserSettingsIgnored.BindTo(DimWhenUserSettingsIgnored); - - StoryboardReplacesBackground.BindTo(dimmable.StoryboardReplacesBackground); } [BackgroundDependencyLoader] @@ -144,6 +146,8 @@ namespace osu.Game.Screens.Backgrounds /// public readonly Bindable BlurAmount = new BindableFloat(); + public readonly Bindable StoryboardReplacesBackground = new Bindable(); + public Background Background { get => background; @@ -162,6 +166,8 @@ namespace osu.Game.Screens.Backgrounds public override void Add(Drawable drawable) { + ArgumentNullException.ThrowIfNull(drawable); + if (drawable is Background) throw new InvalidOperationException($"Use {nameof(Background)} to set a background."); @@ -187,11 +193,19 @@ namespace osu.Game.Screens.Backgrounds userBlurLevel.ValueChanged += _ => UpdateVisuals(); BlurAmount.ValueChanged += _ => UpdateVisuals(); + StoryboardReplacesBackground.ValueChanged += _ => UpdateVisuals(); } - protected override bool ShowDimContent - // The background needs to be hidden in the case of it being replaced by the storyboard - => (!ShowStoryboard.Value && !IgnoreUserSettings.Value) || !StoryboardReplacesBackground.Value; + protected override float DimLevel + { + get + { + if ((IgnoreUserSettings.Value || ShowStoryboard.Value) && StoryboardReplacesBackground.Value) + return 1; + + return base.DimLevel; + } + } protected override void UpdateVisuals() { diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenBlack.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenBlack.cs index 09778c5cdf..742d149580 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenBlack.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenBlack.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 osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenCustom.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenCustom.cs index 3c8ed6fe76..3862e2a7e0 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenCustom.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenCustom.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 osu.Game.Graphics.Backgrounds; namespace osu.Game.Screens.Backgrounds @@ -17,7 +15,7 @@ namespace osu.Game.Screens.Backgrounds AddInternal(new Background(textureName)); } - public override bool Equals(BackgroundScreen other) + public override bool Equals(BackgroundScreen? other) { if (other is BackgroundScreenCustom backgroundScreenCustom) return base.Equals(other) && textureName == backgroundScreenCustom.textureName; diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 0d9b39f099..d9554c10e2 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Backgrounds if (nextBackground == background) return false; - Logger.Log("🌅 Background change queued"); + Logger.Log(@"🌅 Global background change queued"); cancellationTokenSource?.Cancel(); cancellationTokenSource = new CancellationTokenSource(); @@ -94,6 +94,7 @@ namespace osu.Game.Screens.Backgrounds nextTask?.Cancel(); nextTask = Scheduler.AddDelayed(() => { + Logger.Log(@"🌅 Global background loading"); LoadComponentAsync(nextBackground, displayNext, cancellationTokenSource.Token); }, 500); diff --git a/osu.Game/Screens/Edit/BottomBar.cs b/osu.Game/Screens/Edit/BottomBar.cs index b8fed4b935..aa3c4ba0d0 100644 --- a/osu.Game/Screens/Edit/BottomBar.cs +++ b/osu.Game/Screens/Edit/BottomBar.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Game.Overlays; +using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Timelines.Summary; using osuTK; @@ -57,7 +58,7 @@ namespace osu.Game.Screens.Edit new Dimension(GridSizeMode.Absolute, 170), new Dimension(), new Dimension(GridSizeMode.Absolute, 220), - new Dimension(GridSizeMode.Absolute, 120), + new Dimension(GridSizeMode.Absolute, HitObjectComposer.TOOLBOX_CONTRACTED_SIZE_RIGHT), }, Content = new[] { @@ -69,7 +70,6 @@ namespace osu.Game.Screens.Edit TestGameplayButton = new TestGameplayButton { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = 10 }, Size = new Vector2(1), Action = editor.TestGameplay, } diff --git a/osu.Game/Screens/Edit/Components/EditorToolButton.cs b/osu.Game/Screens/Edit/Components/EditorToolButton.cs new file mode 100644 index 0000000000..6550362687 --- /dev/null +++ b/osu.Game/Screens/Edit/Components/EditorToolButton.cs @@ -0,0 +1,107 @@ +// 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.Extensions; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Components +{ + public partial class EditorToolButton : OsuButton, IHasPopover + { + public BindableBool Selected { get; } = new BindableBool(); + + private readonly Func createIcon; + private readonly Func createPopover; + + private Color4 defaultBackgroundColour; + private Color4 defaultIconColour; + private Color4 selectedBackgroundColour; + private Color4 selectedIconColour; + + private Drawable icon = null!; + + public EditorToolButton(LocalisableString text, Func createIcon, Func createPopover) + { + Text = text; + this.createIcon = createIcon; + this.createPopover = createPopover; + + RelativeSizeAxes = Axes.X; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + defaultBackgroundColour = colourProvider.Background3; + selectedBackgroundColour = colourProvider.Background1; + + defaultIconColour = defaultBackgroundColour.Darken(0.5f); + selectedIconColour = selectedBackgroundColour.Lighten(0.5f); + + Add(icon = createIcon().With(b => + { + b.Blending = BlendingParameters.Additive; + b.Anchor = Anchor.CentreLeft; + b.Origin = Anchor.CentreLeft; + b.Size = new Vector2(20); + b.X = 10; + })); + + Action = Selected.Toggle; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Selected.BindValueChanged(_ => updateSelectionState(), true); + } + + private void updateSelectionState() + { + if (!IsLoaded) + return; + + BackgroundColour = Selected.Value ? selectedBackgroundColour : defaultBackgroundColour; + icon.Colour = Selected.Value ? selectedIconColour : defaultIconColour; + + if (Selected.Value) + this.ShowPopover(); + else + this.HidePopover(); + } + + protected override SpriteText CreateText() => new OsuSpriteText + { + Depth = -1, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + X = 40f + }; + + public Popover? GetPopover() => Enabled.Value + ? createPopover()?.With(p => + { + p.State.BindValueChanged(state => + { + if (state.NewValue == Visibility.Hidden) + Selected.Value = false; + }); + }) + : null; + } +} diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index a911b4e1d8..fb0ae2df73 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -3,6 +3,10 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics; @@ -14,19 +18,59 @@ namespace osu.Game.Screens.Edit.Components.Menus { public partial class EditorMenuBar : OsuMenu { + private const float heading_area = 114; + public EditorMenuBar() : base(Direction.Horizontal, true) { RelativeSizeAxes = Axes.X; MaskingContainer.CornerRadius = 0; - ItemsContainer.Padding = new MarginPadding { Left = 100 }; + ItemsContainer.Padding = new MarginPadding(); + + ContentContainer.Margin = new MarginPadding { Left = heading_area }; + ContentContainer.Masking = true; } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider, TextureStore textures) { BackgroundColour = colourProvider.Background3; + + TextFlowContainer text; + + AddRangeInternal(new[] + { + new Container + { + RelativeSizeAxes = Axes.Y, + Width = heading_area, + Padding = new MarginPadding(8), + Children = new Drawable[] + { + new Sprite + { + Size = new Vector2(26), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Texture = textures.Get("Icons/Hexacons/editor"), + }, + text = new TextFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + } + } + }, + }); + + text.AddText(@"osu!", t => t.Font = OsuFont.TorusAlternate); + text.AddText(@"editor", t => + { + t.Font = OsuFont.TorusAlternate; + t.Colour = colourProvider.Highlight1; + }); } protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu(); @@ -157,7 +201,17 @@ namespace osu.Game.Screens.Edit.Components.Menus public DrawableSpacer(MenuItem item) : base(item) { - Scale = new Vector2(1, 0.3f); + Scale = new Vector2(1, 0.6f); + + AddInternal(new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = BackgroundColourHover, + RelativeSizeAxes = Axes.X, + Height = 2f, + Width = 0.8f, + }); } protected override bool OnHover(HoverEvent e) => true; diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index 72c299f443..431336aa60 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -76,6 +76,9 @@ namespace osu.Game.Screens.Edit.Components protected override bool OnKeyDown(KeyDownEvent e) { + if (e.Repeat) + return false; + switch (e.Key) { case Key.Space: diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs index 873551db77..95d5dd36d8 100644 --- a/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs @@ -14,7 +14,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.Edit.Components.TernaryButtons { - internal partial class DrawableTernaryButton : OsuButton + public partial class DrawableTernaryButton : OsuButton { private Color4 defaultBackgroundColour; private Color4 defaultIconColour; diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs index d42c02e03d..ff707407dd 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs @@ -73,6 +73,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { public MarkerVisualisation() { + const float box_height = 4; + Anchor = Anchor.CentreLeft; Origin = Anchor.Centre; RelativePositionAxes = Axes.X; @@ -80,32 +82,46 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts AutoSizeAxes = Axes.X; InternalChildren = new Drawable[] { + new Box + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(14, box_height), + }, new Triangle { Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, Scale = new Vector2(1, -1), Size = new Vector2(10, 5), + Y = box_height, }, new Triangle { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Size = new Vector2(10, 5) + Size = new Vector2(10, 5), + Y = -box_height, + }, + new Box + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Size = new Vector2(14, box_height), }, new Box { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, - Width = 2, + Width = 1.4f, EdgeSmoothness = new Vector2(1, 0) } }; } [BackgroundDependencyLoader] - private void load(OsuColour colours) => Colour = colours.Red; + private void load(OsuColour colours) => Colour = colours.Red1; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 62925ff708..7eba1fe1cd 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.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.Diagnostics; using System.Linq; @@ -33,12 +31,10 @@ namespace osu.Game.Screens.Edit.Compose.Components { public partial class BeatDivisorControl : CompositeDrawable, IKeyBindingHandler { - private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); + private int? lastCustomDivisor; - public BeatDivisorControl(BindableBeatDivisor beatDivisor) - { - this.beatDivisor.BindTo(beatDivisor); - } + [Resolved] + private BindableBeatDivisor beatDivisor { get; set; } = null!; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) @@ -186,29 +182,46 @@ namespace osu.Game.Screens.Edit.Compose.Components }; } + protected override void LoadComplete() + { + base.LoadComplete(); + + beatDivisor.ValidDivisors.BindValueChanged(valid => + { + if (valid.NewValue.Type == BeatDivisorType.Custom) + lastCustomDivisor = valid.NewValue.Presets.Last(); + }, true); + } + private void cycleDivisorType(int direction) { - Debug.Assert(Math.Abs(direction) == 1); - int nextDivisorType = (int)beatDivisor.ValidDivisors.Value.Type + direction; - if (nextDivisorType > (int)BeatDivisorType.Triplets) - nextDivisorType = (int)BeatDivisorType.Common; - else if (nextDivisorType < (int)BeatDivisorType.Common) - nextDivisorType = (int)BeatDivisorType.Triplets; + int totalTypes = Enum.GetValues().Length; + BeatDivisorType currentType = beatDivisor.ValidDivisors.Value.Type; - switch ((BeatDivisorType)nextDivisorType) + Debug.Assert(Math.Abs(direction) == 1); + + cycleOnce(); + + if (lastCustomDivisor == null && currentType == BeatDivisorType.Custom) + cycleOnce(); + + switch (currentType) { case BeatDivisorType.Common: - beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.COMMON; + beatDivisor.SetArbitraryDivisor(4); break; case BeatDivisorType.Triplets: - beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.TRIPLETS; + beatDivisor.SetArbitraryDivisor(6); break; case BeatDivisorType.Custom: - beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.Custom(beatDivisor.ValidDivisors.Value.Presets.Max()); + Debug.Assert(lastCustomDivisor != null); + beatDivisor.SetArbitraryDivisor(lastCustomDivisor.Value); break; } + + void cycleOnce() => currentType = (BeatDivisorType)(((int)currentType + totalTypes + direction) % totalTypes); } protected override bool OnKeyDown(KeyDownEvent e) @@ -249,6 +262,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly OsuSpriteText divisorText; public DivisorDisplay() + : base(HoverSampleSet.Default) { Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -326,12 +340,12 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.LoadComplete(); BeatDivisor.BindValueChanged(_ => updateState(), true); - divisorTextBox.OnCommit += (_, _) => setPresets(); + divisorTextBox.OnCommit += (_, _) => setPresetsFromTextBoxEntry(); divisorTextBox.SelectAll(); } - private void setPresets() + private void setPresetsFromTextBoxEntry() { if (!int.TryParse(divisorTextBox.Text, out int divisor) || divisor < 1 || divisor > 64) { @@ -394,10 +408,10 @@ namespace osu.Game.Screens.Edit.Compose.Components private partial class TickSliderBar : SliderBar { - private Marker marker; + private Marker marker = null!; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; private readonly BindableBeatDivisor beatDivisor; @@ -539,7 +553,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private partial class Marker : CompositeDrawable { [Resolved] - private OverlayColourProvider colourProvider { get; set; } + private OverlayColourProvider colourProvider { get; set; } = null!; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorPresetCollection.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorPresetCollection.cs index 67b346fb64..56df0552cc 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorPresetCollection.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorPresetCollection.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs new file mode 100644 index 0000000000..766d5b5601 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs @@ -0,0 +1,213 @@ +// 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.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Caching; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + /// + /// A grid which displays coloured beat divisor lines in proximity to the selection or placement cursor. + /// + public abstract partial class BeatSnapGrid : CompositeComponent + { + private const double visible_range = 750; + + /// + /// The range of time values of the current selection. + /// + public (double start, double end)? SelectionTimeRange + { + set + { + if (value == selectionTimeRange) + return; + + selectionTimeRange = value; + lineCache.Invalidate(); + } + } + + [Resolved] + private EditorBeatmap beatmap { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private BindableBeatDivisor beatDivisor { get; set; } = null!; + + private readonly List grids = new List(); + + private readonly DrawablePool linesPool = new DrawablePool(50); + + private readonly Cached lineCache = new Cached(); + + private (double start, double end)? selectionTimeRange; + + [BackgroundDependencyLoader] + private void load(HitObjectComposer composer) + { + AddInternal(linesPool); + + foreach (var target in GetTargetContainers(composer)) + { + var lineContainer = new ScrollingHitObjectContainer(); + + grids.Add(lineContainer); + target.Add(lineContainer); + } + + beatDivisor.BindValueChanged(_ => createLines(), true); + } + + protected abstract IEnumerable GetTargetContainers(HitObjectComposer composer); + + protected override void Update() + { + base.Update(); + + if (!lineCache.IsValid) + { + lineCache.Validate(); + createLines(); + } + } + + private void createLines() + { + foreach (var grid in grids) + grid.Clear(); + + if (selectionTimeRange == null) + return; + + var range = selectionTimeRange.Value; + + var timingPoint = beatmap.ControlPointInfo.TimingPointAt(range.start - visible_range); + + double time = timingPoint.Time; + int beat = 0; + + // progress time until in the visible range. + while (time < range.start - visible_range) + { + time += timingPoint.BeatLength / beatDivisor.Value; + beat++; + } + + while (time < range.end + visible_range) + { + var nextTimingPoint = beatmap.ControlPointInfo.TimingPointAt(time); + + // switch to the next timing point if we have reached it. + if (nextTimingPoint.Time > timingPoint.Time) + { + beat = 0; + time = nextTimingPoint.Time; + timingPoint = nextTimingPoint; + } + + Color4 colour = BindableBeatDivisor.GetColourFor( + BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value), colours); + + foreach (var grid in grids) + { + var line = linesPool.Get(); + + line.Apply(new HitObject + { + StartTime = time + }); + + line.Colour = colour; + + grid.Add(line); + } + + beat++; + time += timingPoint.BeatLength / beatDivisor.Value; + } + + foreach (var grid in grids) + { + // required to update ScrollingHitObjectContainer's cache. + grid.UpdateSubTree(); + + foreach (var line in grid.Objects.OfType()) + { + time = line.HitObject.StartTime; + + if (time >= range.start && time <= range.end) + line.Alpha = 1; + else + { + double timeSeparation = time < range.start ? range.start - time : time - range.end; + line.Alpha = (float)Math.Max(0, 1 - timeSeparation / visible_range); + } + } + } + } + + private partial class DrawableGridLine : DrawableHitObject + { + [Resolved] + private IScrollingInfo scrollingInfo { get; set; } = null!; + + private readonly IBindable direction = new Bindable(); + + public DrawableGridLine() + : base(new HitObject()) + { + AddInternal(new Box { RelativeSizeAxes = Axes.Both }); + } + + [BackgroundDependencyLoader] + private void load() + { + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + Origin = Anchor = direction.NewValue == ScrollingDirection.Up + ? Anchor.TopLeft + : Anchor.BottomLeft; + + bool isHorizontal = direction.NewValue == ScrollingDirection.Left || direction.NewValue == ScrollingDirection.Right; + + if (isHorizontal) + { + RelativeSizeAxes = Axes.Y; + Width = 2; + } + else + { + RelativeSizeAxes = Axes.X; + Height = 2; + } + } + + protected override void UpdateInitialTransforms() + { + // don't perform any fading – we are handling that ourselves. + LifetimeEnd = HitObject.StartTime + visible_range; + } + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 56a6b18433..110beb0fa6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Edit.Compose.Components public Container> SelectionBlueprints { get; private set; } - protected SelectionHandler SelectionHandler { get; private set; } + public SelectionHandler SelectionHandler { get; private set; } private readonly Dictionary> blueprintMap = new Dictionary>(); @@ -46,15 +46,6 @@ namespace osu.Game.Screens.Edit.Compose.Components protected readonly BindableList SelectedItems = new BindableList(); - /// - /// Whether to allow cyclic selection on clicking multiple times. - /// - /// - /// Disabled by default as it does not work well with editors that support double-clicking or other advanced interactions. - /// Can probably be made to work with more thought. - /// - protected virtual bool AllowCyclicSelection => false; - protected BlueprintContainer() { RelativeSizeAxes = Axes.Both; @@ -167,6 +158,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (ClickedBlueprint == null || SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != ClickedBlueprint) return false; + doubleClickHandled = true; return true; } @@ -177,6 +169,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { endClickSelection(e); clickSelectionHandled = false; + doubleClickHandled = false; isDraggingBlueprint = false; wasDragStarted = false; }); @@ -376,11 +369,22 @@ namespace osu.Game.Screens.Edit.Compose.Components /// private bool clickSelectionHandled; + /// + /// Whether a blueprint was double-clicked since last mouse down. + /// + private bool doubleClickHandled; + /// /// Whether the selected blueprint(s) were already selected on mouse down. Generally used to perform selection cycling on mouse up in such a case. /// private bool selectedBlueprintAlreadySelectedOnMouseDown; + /// + /// Sorts the supplied by the order of preference when making a selection. + /// Blueprints at the start of the list will be prioritised over later items if the selection requested is ambiguous due to spatial overlap. + /// + protected virtual IEnumerable> ApplySelectionOrder(IEnumerable> blueprints) => blueprints.Reverse(); + /// /// Attempts to select any hovered blueprints. /// @@ -390,15 +394,28 @@ namespace osu.Game.Screens.Edit.Compose.Components { // Iterate from the top of the input stack (blueprints closest to the front of the screen first). // Priority is given to already-selected blueprints. - foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected)) + foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Where(b => b.IsSelected)) { - if (!blueprint.IsHovered) continue; + if (runForBlueprint(blueprint)) + return true; + } - selectedBlueprintAlreadySelectedOnMouseDown = blueprint.State == SelectionState.Selected; - return clickSelectionHandled = SelectionHandler.MouseDownSelectionRequested(blueprint, e); + foreach (SelectionBlueprint blueprint in ApplySelectionOrder(SelectionBlueprints.AliveChildren)) + { + if (runForBlueprint(blueprint)) + return true; } return false; + + bool runForBlueprint(SelectionBlueprint blueprint) + { + if (!blueprint.IsHovered) return false; + + selectedBlueprintAlreadySelectedOnMouseDown = blueprint.State == SelectionState.Selected; + clickSelectionHandled = SelectionHandler.MouseDownSelectionRequested(blueprint, e); + return true; + } } /// @@ -408,8 +425,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a click selection was active. private bool endClickSelection(MouseButtonEvent e) { - // If already handled a selection or drag, we don't want to perform a mouse up / click action. - if (clickSelectionHandled || isDraggingBlueprint) return true; + // If already handled a selection, double-click, or drag, we don't want to perform a mouse up / click action. + if (clickSelectionHandled || doubleClickHandled || isDraggingBlueprint) return true; if (e.Button != MouseButton.Left) return false; @@ -425,20 +442,20 @@ namespace osu.Game.Screens.Edit.Compose.Components return false; } - if (!wasDragStarted && selectedBlueprintAlreadySelectedOnMouseDown && SelectedItems.Count == 1 && AllowCyclicSelection) + if (!wasDragStarted && selectedBlueprintAlreadySelectedOnMouseDown && SelectedItems.Count == 1) { // If a click occurred and was handled by the currently selected blueprint but didn't result in a drag, // cycle between other blueprints which are also under the cursor. // The depth of blueprints is constantly changing (see above where selected blueprints are brought to the front). // For this logic, we want a stable sort order so we can correctly cycle, thus using the blueprintMap instead. - IEnumerable> cyclingSelectionBlueprints = blueprintMap.Values; + IEnumerable> cyclingSelectionBlueprints = ApplySelectionOrder(blueprintMap.Values); // If there's already a selection, let's start from the blueprint after the selection. cyclingSelectionBlueprints = cyclingSelectionBlueprints.SkipWhile(b => !b.IsSelected).Skip(1); // Add the blueprints from before the selection to the end of the enumerable to allow for cyclic selection. - cyclingSelectionBlueprints = cyclingSelectionBlueprints.Concat(blueprintMap.Values.TakeWhile(b => !b.IsSelected)); + cyclingSelectionBlueprints = cyclingSelectionBlueprints.Concat(ApplySelectionOrder(blueprintMap.Values).TakeWhile(b => !b.IsSelected)); foreach (SelectionBlueprint blueprint in cyclingSelectionBlueprints) { @@ -470,7 +487,7 @@ namespace osu.Game.Screens.Edit.Compose.Components break; case SelectionState.NotSelected: - if (blueprint.IsAlive && blueprint.IsPresent && quad.Contains(blueprint.ScreenSpaceSelectionPoint)) + if (blueprint.IsSelectable && quad.Contains(blueprint.ScreenSpaceSelectionPoint)) blueprint.Select(); break; } diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index d6e4e1f030..e33ef66007 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -18,6 +16,9 @@ namespace osu.Game.Screens.Edit.Compose.Components { public abstract partial class CircularDistanceSnapGrid : DistanceSnapGrid { + [Resolved] + private EditorClock editorClock { get; set; } = null!; + protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null) : base(referenceObject, startPosition, startTime, endTime) { @@ -62,14 +63,15 @@ namespace osu.Game.Screens.Edit.Compose.Components for (int i = 0; i < requiredCircles; i++) { - float diameter = (offset + (i + 1) * DistanceBetweenTicks) * 2; + const float thickness = 4; + float diameter = (offset + (i + 1) * DistanceBetweenTicks + thickness / 2) * 2; AddInternal(new Ring(ReferenceObject, GetColourForIndexFromPlacement(i)) { Position = StartPosition, Origin = Anchor.Centre, Size = new Vector2(diameter), - InnerRadius = 4 * 1f / diameter, + InnerRadius = thickness * 1f / diameter, }); } } @@ -98,9 +100,12 @@ namespace osu.Game.Screens.Edit.Compose.Components if (travelLength < DistanceBetweenTicks) travelLength = DistanceBetweenTicks; - // When interacting with the resolved snap provider, the distance spacing multiplier should first be removed - // to allow for snapping at a non-multiplied ratio. - float snappedDistance = SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier); + float snappedDistance = LimitedDistanceSnap.Value + ? SnapProvider.DurationToDistance(ReferenceObject, editorClock.CurrentTime - ReferenceObject.GetEndTime()) + // When interacting with the resolved snap provider, the distance spacing multiplier should first be removed + // to allow for snapping at a non-multiplied ratio. + : SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier); + double snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance); if (snappedTime > LatestEndTime) @@ -120,10 +125,10 @@ namespace osu.Game.Screens.Edit.Compose.Components private partial class Ring : CircularProgress { [Resolved] - private IDistanceSnapProvider snapProvider { get; set; } + private IDistanceSnapProvider snapProvider { get; set; } = null!; - [Resolved(canBeNull: true)] - private EditorClock editorClock { get; set; } + [Resolved] + private EditorClock? editorClock { get; set; } private readonly HitObject referenceObject; diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index c8cfac454a..c7c7c4aa83 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -40,11 +40,14 @@ namespace osu.Game.Screens.Edit.Compose.Components public PlacementBlueprint CurrentPlacement { get; private set; } + [Resolved(canBeNull: true)] + private EditorScreenWithTimeline editorScreen { get; set; } + /// /// Positional input must be received outside the container's bounds, /// in order to handle composer blueprints which are partially offscreen. /// - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => editorScreen?.MainContent.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos); public ComposeBlueprintContainer(HitObjectComposer composer) : base(composer) diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 6092ebc08f..8aa2fa9f45 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -10,6 +10,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Layout; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -60,6 +61,18 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved] private BindableBeatDivisor beatDivisor { get; set; } + /// + /// When enabled, distance snap should only snap to the current time (as per the editor clock). + /// This is to emulate stable behaviour. + /// + protected Bindable LimitedDistanceSnap { get; private set; } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + LimitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); + } + private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); protected readonly HitObject ReferenceObject; diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index 65797a968d..ad0e8b124b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -129,6 +130,10 @@ namespace osu.Game.Screens.Edit.Compose.Components return true; } + protected override IEnumerable> ApplySelectionOrder(IEnumerable> blueprints) => + base.ApplySelectionOrder(blueprints) + .OrderBy(b => Math.Min(Math.Abs(EditorClock.CurrentTime - b.Item.GetEndTime()), Math.Abs(EditorClock.CurrentTime - b.Item.StartTime))); + protected override Container> CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }; protected override SelectionHandler CreateSelectionHandler() => new EditorSelectionHandler(); diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index d618541685..a73278a61e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; @@ -28,7 +26,7 @@ namespace osu.Game.Screens.Edit.Compose.Components public const string HIT_BANK_AUTO = "auto"; [Resolved] - protected EditorBeatmap EditorBeatmap { get; private set; } + protected EditorBeatmap EditorBeatmap { get; private set; } = null!; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs index 7beaf7d086..ac339dc9d9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs @@ -73,7 +73,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (selected is IHasSliderVelocity sliderVelocity) { AddHeader("Slider Velocity"); - AddValue($"{sliderVelocity.SliderVelocity:#,0.00}x ({sliderVelocity.SliderVelocity * EditorBeatmap.Difficulty.SliderMultiplier:#,0.00}x)"); + AddValue($"{sliderVelocity.SliderVelocityMultiplier:#,0.00}x ({sliderVelocity.SliderVelocityMultiplier * EditorBeatmap.Difficulty.SliderMultiplier:#,0.00}x)"); } if (selected is IHasRepeats repeats) diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs index 849a526556..8f54d55d5d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs @@ -1,9 +1,8 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Edit; @@ -18,7 +17,7 @@ namespace osu.Game.Screens.Edit.Compose.Components public sealed partial class HitObjectOrderedSelectionContainer : Container> { [Resolved] - private EditorBeatmap editorBeatmap { get; set; } + private EditorBeatmap editorBeatmap { get; set; } = null!; protected override void LoadComplete() { @@ -69,7 +68,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.Dispose(isDisposing); - if (editorBeatmap != null) + if (editorBeatmap.IsNotNull()) editorBeatmap.BeatmapReprocessed -= SortInternal; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/MoveSelectionEvent.cs b/osu.Game/Screens/Edit/Compose/Components/MoveSelectionEvent.cs index 46d948f8b6..4515e4d7be 100644 --- a/osu.Game/Screens/Edit/Compose/Components/MoveSelectionEvent.cs +++ b/osu.Game/Screens/Edit/Compose/Components/MoveSelectionEvent.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Rulesets.Edit; using osuTK; diff --git a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs index 06b73c8af4..cfc01fe17b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; @@ -56,7 +54,10 @@ namespace osu.Game.Screens.Edit.Compose.Components if (!gridCache.IsValid) { ClearInternal(); - createContent(); + + if (DrawWidth > 0 && DrawHeight > 0) + createContent(); + gridCache.Validate(); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 1c5faed0e5..72d96213ee 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -1,10 +1,10 @@ // 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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -24,13 +24,19 @@ namespace osu.Game.Screens.Edit.Compose.Components private const float button_padding = 5; - public Func OnRotation; - public Func OnScale; - public Func OnFlip; - public Func OnReverse; + [Resolved] + private SelectionRotationHandler? rotationHandler { get; set; } - public Action OperationStarted; - public Action OperationEnded; + public Func? OnScale; + public Func? OnFlip; + public Func? OnReverse; + + public Action? OperationStarted; + public Action? OperationEnded; + + private SelectionBoxButton? reverseButton; + private SelectionBoxButton? rotateClockwiseButton; + private SelectionBoxButton? rotateCounterClockwiseButton; private bool canReverse; @@ -49,22 +55,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - private bool canRotate; - - /// - /// Whether rotation support should be enabled. - /// - public bool CanRotate - { - get => canRotate; - set - { - if (canRotate == value) return; - - canRotate = value; - recreate(); - } - } + private readonly IBindable canRotate = new BindableBool(); private bool canScaleX; @@ -134,7 +125,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - private string text; + private string text = string.Empty; public string Text { @@ -150,35 +141,38 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - private SelectionBoxDragHandleContainer dragHandles; - private FillFlowContainer buttons; + private SelectionBoxDragHandleContainer dragHandles = null!; + private FillFlowContainer buttons = null!; - private OsuSpriteText selectionDetailsText; + private OsuSpriteText? selectionDetailsText; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; [BackgroundDependencyLoader] - private void load() => recreate(); + private void load() + { + if (rotationHandler != null) + canRotate.BindTo(rotationHandler.CanRotate); + + canRotate.BindValueChanged(_ => recreate(), true); + } protected override bool OnKeyDown(KeyDownEvent e) { if (e.Repeat || !e.ControlPressed) return false; - bool runOperationFromHotkey(Func operation) - { - operationStarted(); - bool result = operation?.Invoke() ?? false; - operationEnded(); - - return result; - } - switch (e.Key) { case Key.G: - return CanReverse && runOperationFromHotkey(OnReverse); + return CanReverse && reverseButton?.TriggerClick() == true; + + case Key.Comma: + return canRotate.Value && rotateCounterClockwiseButton?.TriggerClick() == true; + + case Key.Period: + return canRotate.Value && rotateClockwiseButton?.TriggerClick() == true; } return base.OnKeyDown(e); @@ -255,14 +249,14 @@ namespace osu.Game.Screens.Edit.Compose.Components if (CanScaleY) addYScaleComponents(); if (CanFlipX) addXFlipComponents(); if (CanFlipY) addYFlipComponents(); - if (CanRotate) addRotationComponents(); - if (CanReverse) addButton(FontAwesome.Solid.Backward, "Reverse pattern (Ctrl-G)", () => OnReverse?.Invoke()); + if (canRotate.Value) addRotationComponents(); + if (CanReverse) reverseButton = addButton(FontAwesome.Solid.Backward, "Reverse pattern (Ctrl-G)", () => OnReverse?.Invoke()); } private void addRotationComponents() { - addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise", () => OnRotation?.Invoke(-90)); - addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise", () => OnRotation?.Invoke(90)); + rotateCounterClockwiseButton = addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise (Ctrl-<)", () => rotationHandler?.Rotate(-90)); + rotateClockwiseButton = addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise (Ctrl->)", () => rotationHandler?.Rotate(90)); addRotateHandle(Anchor.TopLeft); addRotateHandle(Anchor.TopRight); @@ -300,7 +294,7 @@ namespace osu.Game.Screens.Edit.Compose.Components addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically", () => OnFlip?.Invoke(Direction.Vertical, false)); } - private void addButton(IconUsage icon, string tooltip, Action action) + private SelectionBoxButton addButton(IconUsage icon, string tooltip, Action action) { var button = new SelectionBoxButton(icon, tooltip) { @@ -310,6 +304,27 @@ namespace osu.Game.Screens.Edit.Compose.Components button.OperationStarted += operationStarted; button.OperationEnded += operationEnded; buttons.Add(button); + + return button; + } + + /// + /// This method should be called when a selection needs to be flipped + /// because of an ongoing scale handle drag that would otherwise cause width or height to go negative. + /// + public void PerformFlipFromScaleHandles(Axes axes) + { + if (axes.HasFlagFast(Axes.X)) + { + dragHandles.FlipScaleHandles(Direction.Horizontal); + OnFlip?.Invoke(Direction.Horizontal, false); + } + + if (axes.HasFlagFast(Axes.Y)) + { + dragHandles.FlipScaleHandles(Direction.Vertical); + OnFlip?.Invoke(Direction.Vertical, false); + } } private void addScaleHandle(Anchor anchor) @@ -330,7 +345,6 @@ namespace osu.Game.Screens.Edit.Compose.Components var handle = new SelectionBoxRotationHandle { Anchor = anchor, - HandleRotate = angle => OnRotation?.Invoke(angle) }; handle.OperationStarted += operationStarted; @@ -369,17 +383,20 @@ namespace osu.Game.Screens.Edit.Compose.Components // Shrink the parent quad to give a bit of padding so the buttons don't stick *right* on the border. // AABBFloat assumes no rotation. one would hope the whole editor is not being rotated. - var parentQuad = Parent.ScreenSpaceDrawQuad.AABBFloat.Shrink(ToLocalSpace(thisQuad.TopLeft + new Vector2(button_padding * 2))); + var parentQuad = Parent!.ScreenSpaceDrawQuad.AABBFloat.Shrink(ToLocalSpace(thisQuad.TopLeft + new Vector2(button_padding * 2))); float topExcess = thisQuad.TopLeft.Y - parentQuad.TopLeft.Y; float bottomExcess = parentQuad.BottomLeft.Y - thisQuad.BottomLeft.Y; float leftExcess = thisQuad.TopLeft.X - parentQuad.TopLeft.X; float rightExcess = parentQuad.TopRight.X - thisQuad.TopRight.X; - if (topExcess + bottomExcess < buttons.Height + button_padding) + float minHeight = buttons.ScreenSpaceDrawQuad.Height; + + if (topExcess < minHeight && bottomExcess < minHeight) { buttons.Anchor = Anchor.BottomCentre; buttons.Origin = Anchor.BottomCentre; + buttons.Y = Math.Min(0, ToLocalSpace(Parent!.ScreenSpaceDrawQuad.BottomLeft).Y - DrawHeight); } else if (topExcess > bottomExcess) { diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs index 832d8b65e5..6108d44c81 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.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 osu.Framework.Allocation; using osu.Framework.Graphics; @@ -17,11 +15,11 @@ namespace osu.Game.Screens.Edit.Compose.Components { public sealed partial class SelectionBoxButton : SelectionBoxControl, IHasTooltip { - private SpriteIcon icon; + private SpriteIcon icon = null!; private readonly IconUsage iconUsage; - public Action Action; + public Action? Action; public SelectionBoxButton(IconUsage iconUsage, string tooltip) { @@ -49,6 +47,8 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnClick(ClickEvent e) { + Circle.FlashColour(Colours.GrayF, 300); + TriggerOperationStarted(); Action?.Invoke(); TriggerOperationEnded(); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxControl.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxControl.cs index 35c67a1c67..3746c9652e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxControl.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Edit.Compose.Components public event Action OperationStarted; public event Action OperationEnded; - private Circle circle; + protected Circle Circle { get; private set; } /// /// Whether the user is currently holding the control with mouse. @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Edit.Compose.Components InternalChildren = new Drawable[] { - circle = new Circle + Circle = new Circle { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -85,9 +85,9 @@ namespace osu.Game.Screens.Edit.Compose.Components protected virtual void UpdateHoverState() { if (IsHeld) - circle.FadeColour(Colours.GrayF, TRANSFORM_DURATION, Easing.OutQuint); + Circle.FadeColour(Colours.GrayF, TRANSFORM_DURATION, Easing.OutQuint); else - circle.FadeColour(IsHovered ? Colours.Red : Colours.YellowDark, TRANSFORM_DURATION, Easing.OutQuint); + Circle.FadeColour(IsHovered ? Colours.Red : Colours.YellowDark, TRANSFORM_DURATION, Easing.OutQuint); this.ScaleTo(IsHeld || IsHovered ? 1.5f : 1, TRANSFORM_DURATION, Easing.OutQuint); } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleContainer.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleContainer.cs index 5c87271493..e7f69b7b37 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleContainer.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -69,6 +70,17 @@ namespace osu.Game.Screens.Edit.Compose.Components allDragHandles.Add(handle); } + public void FlipScaleHandles(Direction direction) + { + foreach (var handle in scaleHandles) + { + if (direction == Direction.Horizontal && !handle.Anchor.HasFlagFast(Anchor.x1)) + handle.Anchor ^= Anchor.x0 | Anchor.x2; + if (direction == Direction.Vertical && !handle.Anchor.HasFlagFast(Anchor.y1)) + handle.Anchor ^= Anchor.y0 | Anchor.y2; + } + } + private SelectionBoxRotationHandle displayedRotationHandle; private SelectionBoxDragHandle activeHandle; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index c2a3f12efd..024749a701 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.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 osu.Framework.Allocation; using osu.Framework.Bindables; @@ -15,24 +13,25 @@ using osu.Framework.Localisation; using osu.Game.Localisation; using osuTK; using osuTK.Graphics; -using Key = osuTK.Input.Key; +using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components { public partial class SelectionBoxRotationHandle : SelectionBoxDragHandle, IHasTooltip { - public Action HandleRotate { get; set; } - public LocalisableString TooltipText { get; private set; } - private SpriteIcon icon; + private SpriteIcon icon = null!; private const float snap_step = 15; private readonly Bindable cumulativeRotation = new Bindable(); [Resolved] - private SelectionBox selectionBox { get; set; } + private SelectionBox selectionBox { get; set; } = null!; + + [Resolved] + private SelectionRotationHandler? rotationHandler { get; set; } [BackgroundDependencyLoader] private void load() @@ -63,10 +62,10 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnDragStart(DragStartEvent e) { - bool handle = base.OnDragStart(e); - if (handle) - cumulativeRotation.Value = 0; - return handle; + if (rotationHandler == null) return false; + + rotationHandler.Begin(); + return true; } protected override void OnDrag(DragEvent e) @@ -99,7 +98,9 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override void OnDragEnd(DragEndEvent e) { - base.OnDragEnd(e); + rotationHandler?.Commit(); + UpdateHoverState(); + cumulativeRotation.Value = null; rawCumulativeRotation = 0; TooltipText = default; @@ -116,14 +117,12 @@ namespace osu.Game.Screens.Edit.Compose.Components private void applyRotation(bool shouldSnap) { - float oldRotation = cumulativeRotation.Value ?? 0; - float newRotation = shouldSnap ? snap(rawCumulativeRotation, snap_step) : MathF.Round(rawCumulativeRotation); newRotation = (newRotation - 180) % 360 + 180; cumulativeRotation.Value = newRotation; - HandleRotate?.Invoke(newRotation - oldRotation); + rotationHandler?.Update(newRotation); TooltipText = shouldSnap ? EditorStrings.RotationSnapped(newRotation) : EditorStrings.RotationUnsnapped(newRotation); } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 5cedf1ca42..3c859c65ff 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -16,7 +16,6 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Framework.Utils; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Resources.Localisation.Web; @@ -56,6 +55,8 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved(CanBeNull = true)] protected IEditorChangeHandler ChangeHandler { get; private set; } + public SelectionRotationHandler RotationHandler { get; private set; } + protected SelectionHandler() { selectedBlueprints = new List>(); @@ -64,10 +65,21 @@ namespace osu.Game.Screens.Edit.Compose.Components AlwaysPresent = true; } + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies.CacheAs(RotationHandler = CreateRotationHandler()); + return dependencies; + } + [BackgroundDependencyLoader] private void load() { - InternalChild = SelectionBox = CreateSelectionBox(); + AddRangeInternal(new Drawable[] + { + RotationHandler, + SelectionBox = CreateSelectionBox(), + }); SelectedItems.CollectionChanged += (_, _) => { @@ -81,7 +93,6 @@ namespace osu.Game.Screens.Edit.Compose.Components OperationStarted = OnOperationBegan, OperationEnded = OnOperationEnded, - OnRotation = HandleRotation, OnScale = HandleScale, OnFlip = HandleFlip, OnReverse = HandleReverse, @@ -133,6 +144,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether any items could be rotated. public virtual bool HandleRotation(float angle) => false; + /// + /// Creates the handler to use for rotation operations. + /// + public virtual SelectionRotationHandler CreateRotationHandler() => new SelectionRotationHandler(); + /// /// Handles the selected items being scaled. /// @@ -160,13 +176,23 @@ namespace osu.Game.Screens.Edit.Compose.Components if (e.Repeat) return false; + bool handled; + switch (e.Action) { case GlobalAction.EditorFlipHorizontally: - return HandleFlip(Direction.Horizontal, true); + ChangeHandler?.BeginChange(); + handled = HandleFlip(Direction.Horizontal, true); + ChangeHandler?.EndChange(); + + return handled; case GlobalAction.EditorFlipVertically: - return HandleFlip(Direction.Vertical, true); + ChangeHandler?.BeginChange(); + handled = HandleFlip(Direction.Vertical, true); + ChangeHandler?.EndChange(); + + return handled; } return false; @@ -391,98 +417,5 @@ namespace osu.Game.Screens.Edit.Compose.Components => Enumerable.Empty(); #endregion - - #region Helper Methods - - /// - /// Rotate a point around an arbitrary origin. - /// - /// The point. - /// The centre origin to rotate around. - /// The angle to rotate (in degrees). - protected static Vector2 RotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle) - { - angle = -angle; - - point.X -= origin.X; - point.Y -= origin.Y; - - Vector2 ret; - ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle)); - ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle)); - - ret.X += origin.X; - ret.Y += origin.Y; - - return ret; - } - - /// - /// Given a flip direction, a surrounding quad for all selected objects, and a position, - /// will return the flipped position in screen space coordinates. - /// - protected static Vector2 GetFlippedPosition(Direction direction, Quad quad, Vector2 position) - { - var centre = quad.Centre; - - switch (direction) - { - case Direction.Horizontal: - position.X = centre.X - (position.X - centre.X); - break; - - case Direction.Vertical: - position.Y = centre.Y - (position.Y - centre.Y); - break; - } - - return position; - } - - /// - /// Given a scale vector, a surrounding quad for all selected objects, and a position, - /// will return the scaled position in screen space coordinates. - /// - protected static Vector2 GetScaledPosition(Anchor reference, Vector2 scale, Quad selectionQuad, Vector2 position) - { - // adjust the direction of scale depending on which side the user is dragging. - float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0; - float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0; - - // guard against no-ops and NaN. - if (scale.X != 0 && selectionQuad.Width > 0) - position.X = selectionQuad.TopLeft.X + xOffset + (position.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X); - - if (scale.Y != 0 && selectionQuad.Height > 0) - position.Y = selectionQuad.TopLeft.Y + yOffset + (position.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y); - - return position; - } - - /// - /// Returns a quad surrounding the provided points. - /// - /// The points to calculate a quad for. - protected static Quad GetSurroundingQuad(IEnumerable points) - { - if (!points.Any()) - return new Quad(); - - Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue); - Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue); - - // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted - foreach (var p in points) - { - minPosition = Vector2.ComponentMin(minPosition, p); - maxPosition = Vector2.ComponentMax(maxPosition, p); - } - - Vector2 size = maxPosition - minPosition; - - return new Quad(minPosition.X, minPosition.Y, size.X, size.Y); - } - - #endregion } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs new file mode 100644 index 0000000000..5faa4a108d --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -0,0 +1,85 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osuTK; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + /// + /// Base handler for editor rotation operations. + /// + public partial class SelectionRotationHandler : Component + { + /// + /// Whether the rotation can currently be performed. + /// + public Bindable CanRotate { get; private set; } = new BindableBool(); + + /// + /// Performs a single, instant, atomic rotation operation. + /// + /// + /// This method is intended to be used in atomic contexts (such as when pressing a single button). + /// For continuous operations, see the -- flow. + /// + /// Rotation to apply in degrees. + /// + /// The origin point to rotate around. + /// If the default value is supplied, a sane implementation-defined default will be used. + /// + public void Rotate(float rotation, Vector2? origin = null) + { + Begin(); + Update(rotation, origin); + Commit(); + } + + /// + /// Begins a continuous rotation operation. + /// + /// + /// This flow is intended to be used when a rotation operation is made incrementally (such as when dragging a rotation handle or slider). + /// For instantaneous, atomic operations, use the convenience method. + /// + public virtual void Begin() + { + } + + /// + /// Updates a continuous rotation operation. + /// Must be preceded by a call. + /// + /// + /// + /// This flow is intended to be used when a rotation operation is made incrementally (such as when dragging a rotation handle or slider). + /// As such, the values of and supplied should be relative to the state of the objects being rotated + /// when was called, rather than instantaneous deltas. + /// + /// + /// For instantaneous, atomic operations, use the convenience method. + /// + /// + /// Rotation to apply in degrees. + /// + /// The origin point to rotate around. + /// If the default value is supplied, a sane implementation-defined default will be used. + /// + public virtual void Update(float rotation, Vector2? origin = null) + { + } + + /// + /// Ends a continuous rotation operation. + /// Must be preceded by a call. + /// + /// + /// This flow is intended to be used when a rotation operation is made incrementally (such as when dragging a rotation handle or slider). + /// For instantaneous, atomic operations, use the convenience method. + /// + public virtual void Commit() + { + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs index 44daf70577..74786cc0c9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -14,17 +12,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public partial class CentreMarker : CompositeDrawable { - private const float triangle_width = 15; - private const float triangle_height = 10; - private const float bar_width = 2; + private const float triangle_width = 8; + + private const float bar_width = 1.6f; public CentreMarker() { RelativeSizeAxes = Axes.Y; Size = new Vector2(triangle_width, 1); - Anchor = Anchor.Centre; - Origin = Anchor.Centre; + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; InternalChildren = new Drawable[] { @@ -39,22 +37,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, - Size = new Vector2(triangle_width, triangle_height), + Size = new Vector2(triangle_width, triangle_width * 0.8f), Scale = new Vector2(1, -1) }, - new Triangle - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Size = new Vector2(triangle_width, triangle_height), - } }; } [BackgroundDependencyLoader] private void load(OsuColour colours) { - Colour = colours.RedDark; + Colour = colours.Red1; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs index 173a665d5c..ca1e9a5d9b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { HitObject = hitObject; - speedMultiplier = (hitObject as IHasSliderVelocity)?.SliderVelocityBindable.GetBoundCopy(); + speedMultiplier = (hitObject as IHasSliderVelocity)?.SliderVelocityMultiplierBindable.GetBoundCopy(); } protected override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1; @@ -106,8 +106,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).Where(o => o is IHasSliderVelocity).ToArray(); // even if there are multiple objects selected, we can still display a value if they all have the same value. - var selectedPointBindable = relevantObjects.Select(point => ((IHasSliderVelocity)point).SliderVelocity).Distinct().Count() == 1 - ? ((IHasSliderVelocity)relevantObjects.First()).SliderVelocityBindable + var selectedPointBindable = relevantObjects.Select(point => ((IHasSliderVelocity)point).SliderVelocityMultiplier).Distinct().Count() == 1 + ? ((IHasSliderVelocity)relevantObjects.First()).SliderVelocityMultiplierBindable : null; if (selectedPointBindable != null) @@ -127,7 +127,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline foreach (var h in relevantObjects) { - ((IHasSliderVelocity)h).SliderVelocity = val.NewValue.Value; + ((IHasSliderVelocity)h).SliderVelocityMultiplier = val.NewValue.Value; beatmap.Update(h); } @@ -169,7 +169,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline InspectorText.Clear(); - double[] sliderVelocities = EditorBeatmap.HitObjects.OfType().Select(sv => sv.SliderVelocity).OrderBy(v => v).ToArray(); + double[] sliderVelocities = EditorBeatmap.HitObjects.OfType().Select(sv => sv.SliderVelocityMultiplier).OrderBy(v => v).ToArray(); AddHeader("Base velocity (from beatmap setup)"); AddValue($"{beatmapVelocity:#,0.00}x"); @@ -177,6 +177,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline AddHeader("Final velocity"); AddValue($"{beatmapVelocity * current.Value:#,0.00}x"); + if (sliderVelocities.Length == 0) + { + return; + } + if (sliderVelocities.First() != sliderVelocities.Last()) { AddHeader("Beatmap velocity range"); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index b02cfb505e..28841fc9e5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -16,6 +16,7 @@ using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Screens.Edit.Timing; using osuTK; using osuTK.Graphics; @@ -101,7 +102,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, volume = new IndeterminateSliderWithTextBoxInput("Volume", new BindableInt(100) { - MinValue = 0, + MinValue = DrawableHitObject.MINIMUM_SAMPLE_VOLUME, MaxValue = 100, }) } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index 0b83258f8b..b973ac3731 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -1,63 +1,68 @@ // 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets.Edit; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public partial class TimelineArea : CompositeDrawable { - public Timeline Timeline; + public Timeline Timeline = null!; private readonly Drawable userContent; - public TimelineArea(Drawable content = null) + public TimelineArea(Drawable? content = null) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - userContent = content ?? Drawable.Empty(); + userContent = content ?? Empty(); } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider, OsuColour colours) { - Masking = true; - OsuCheckbox waveformCheckbox; OsuCheckbox controlPointsCheckbox; OsuCheckbox ticksCheckbox; + const float padding = 10; + InternalChildren = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background5 - }, new GridContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 135), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 35), + new Dimension(GridSizeMode.Absolute, HitObjectComposer.TOOLBOX_CONTRACTED_SIZE_RIGHT - padding * 2), + }, Content = new[] { new Drawable[] { new Container { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, Name = @"Toggle controls", Children = new Drawable[] { @@ -68,24 +73,23 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, new FillFlowContainer { - AutoSizeAxes = Axes.Y, - Width = 160, - Padding = new MarginPadding(10), + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(padding), Direction = FillDirection.Vertical, Spacing = new Vector2(0, 4), Children = new[] { - waveformCheckbox = new OsuCheckbox + waveformCheckbox = new OsuCheckbox(nubSize: 30f) { LabelText = EditorStrings.TimelineWaveform, Current = { Value = true }, }, - ticksCheckbox = new OsuCheckbox + ticksCheckbox = new OsuCheckbox(nubSize: 30f) { LabelText = EditorStrings.TimelineTicks, Current = { Value = true }, }, - controlPointsCheckbox = new OsuCheckbox + controlPointsCheckbox = new OsuCheckbox(nubSize: 30f) { LabelText = BeatmapsetsStrings.ShowStatsBpm, Current = { Value = true }, @@ -96,29 +100,52 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, new Container { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + // the out-of-bounds portion of the centre marker. + new Box + { + Width = 24, + Height = EditorScreenWithTimeline.PADDING, + Depth = float.MaxValue, + Colour = colours.Red1, + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue, + Colour = colourProvider.Background5 + }, + Timeline = new Timeline(userContent), + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, Name = @"Zoom controls", + Padding = new MarginPadding { Right = padding }, Children = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background3, + Colour = colourProvider.Background2, }, new Container { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Masking = true, + RelativeSizeAxes = Axes.Both, Children = new[] { new TimelineButton { - RelativeSizeAxes = Axes.Y, - Height = 0.5f, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1, 0.5f), Icon = FontAwesome.Solid.SearchPlus, Action = () => Timeline.AdjustZoomRelatively(1) }, @@ -126,8 +153,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.Y, - Height = 0.5f, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1, 0.5f), Icon = FontAwesome.Solid.SearchMinus, Action = () => Timeline.AdjustZoomRelatively(-1) }, @@ -135,19 +162,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } }, - Timeline = new Timeline(userContent), + new BeatDivisorControl { RelativeSizeAxes = Axes.Both } }, }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - }, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } } }; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineButton.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineButton.cs index c94de0fe67..767854252e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineButton.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineButton.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 osu.Framework.Allocation; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs index 29983c9cbf..cd97b293ba 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Specialized; using System.Diagnostics; using System.Linq; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs index 257cc9e635..c1b6069523 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -24,7 +22,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline RelativeSizeAxes = Axes.Y; AutoSizeAxes = Axes.X; - Origin = Anchor.TopCentre; + Origin = Anchor.TopLeft; X = (float)group.Time; } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs index a1dfd0718b..c16a948822 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; @@ -20,7 +18,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private double? startTime; [Resolved] - private Timeline timeline { get; set; } + private Timeline timeline { get; set; } = null!; protected override Drawable CreateBox() => new Box { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 55f122669d..77afad2d4f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -395,12 +395,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (e.CurrentState.Keyboard.ShiftPressed && hitObject is IHasSliderVelocity hasSliderVelocity) { - double newVelocity = hasSliderVelocity.SliderVelocity * (repeatHitObject.Duration / proposedDuration); + double newVelocity = hasSliderVelocity.SliderVelocityMultiplier * (repeatHitObject.Duration / proposedDuration); - if (Precision.AlmostEquals(newVelocity, hasSliderVelocity.SliderVelocity)) + if (Precision.AlmostEquals(newVelocity, hasSliderVelocity.SliderVelocityMultiplier)) return; - hasSliderVelocity.SliderVelocity = newVelocity; + hasSliderVelocity.SliderVelocityMultiplier = newVelocity; beatmap.Update(hitObject); } else diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 6a0688e19c..7e7bef8cf2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; @@ -21,19 +19,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public partial class TimelineTickDisplay : TimelinePart { [Resolved] - private EditorBeatmap beatmap { get; set; } + private EditorBeatmap beatmap { get; set; } = null!; [Resolved] - private Bindable working { get; set; } + private Bindable working { get; set; } = null!; [Resolved] - private BindableBeatDivisor beatDivisor { get; set; } - - [Resolved(CanBeNull = true)] - private IEditorChangeHandler changeHandler { get; set; } + private BindableBeatDivisor beatDivisor { get; set; } = null!; [Resolved] - private OsuColour colours { get; set; } + private IEditorChangeHandler? changeHandler { get; set; } + + [Resolved] + private OsuColour colours { get; set; } = null!; public TimelineTickDisplay() { @@ -72,8 +70,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// private float? nextMaxTick; - [Resolved(canBeNull: true)] - private Timeline timeline { get; set; } + [Resolved] + private Timeline? timeline { get; set; } protected override void Update() { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs index 4191864e5c..2a4ad66918 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps.ControlPoints; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs index 69fb001a66..243cdc6ddd 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -10,24 +8,25 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public partial class TopPointPiece : CompositeDrawable { - private readonly ControlPoint point; + protected readonly ControlPoint Point; - protected OsuSpriteText Label { get; private set; } + protected OsuSpriteText Label { get; private set; } = null!; + + private const float width = 80; public TopPointPiece(ControlPoint point) { - this.point = point; - AutoSizeAxes = Axes.X; + Point = point; + Width = width; Height = 16; - Margin = new MarginPadding(4); - - Masking = true; - CornerRadius = Height / 2; + Margin = new MarginPadding { Vertical = 4 }; Origin = Anchor.TopCentre; Anchor = Anchor.TopCentre; @@ -36,17 +35,52 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [BackgroundDependencyLoader] private void load(OsuColour colours) { + const float corner_radius = 4; + const float arrow_extension = 3; + const float triangle_portion = 15; + InternalChildren = new Drawable[] { - new Box + // This is a triangle, trust me. + // Doing it this way looks okay. Doing it using Triangle primitive is basically impossible. + new Container { - Colour = point.GetRepresentingColour(colours), - RelativeSizeAxes = Axes.Both, + Colour = Point.GetRepresentingColour(colours), + X = -corner_radius, + Size = new Vector2(triangle_portion * arrow_extension, Height), + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Masking = true, + CornerRadius = Height, + CornerExponent = 1.4f, + Children = new Drawable[] + { + new Box + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.Y, + Width = width - triangle_portion, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = Point.GetRepresentingColour(colours), + Masking = true, + CornerRadius = corner_radius, + Child = new Box + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + }, }, Label = new OsuSpriteText { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Padding = new MarginPadding(3), Font = OsuFont.Default.With(size: 14, weight: FontWeight.SemiBold), Colour = colours.B5, diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index 951f4129d4..848c8f9a0f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.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 osu.Framework.Allocation; using osu.Framework.Graphics; @@ -41,8 +39,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private bool isZoomSetUp; - [Resolved(canBeNull: true)] - private IFrameBasedClock editorClock { get; set; } + [Resolved] + private IFrameBasedClock? editorClock { get; set; } private readonly LayoutValue zoomedContentWidthCache = new LayoutValue(Invalidation.DrawSize); diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index dc026f7eac..0a58b34da6 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Edit.Compose public partial class ComposeScreen : EditorScreenWithTimeline, IGameplaySettings { [Resolved] - private GameHost host { get; set; } + private Clipboard hostClipboard { get; set; } = null!; [Resolved] private EditorClock clock { get; set; } @@ -101,26 +101,31 @@ namespace osu.Game.Screens.Edit.Compose #region Clipboard operations - protected override void PerformCut() + public override void Cut() { - base.PerformCut(); + if (!CanCut.Value) + return; Copy(); EditorBeatmap.RemoveRange(EditorBeatmap.SelectedHitObjects.ToArray()); } - protected override void PerformCopy() + public override void Copy() { - base.PerformCopy(); + // on stable, pressing Ctrl-C would copy the current timestamp to system clipboard + // regardless of whether anything was even selected at all. + // UX-wise this is generally strange and unexpected, but make it work anyways to preserve muscle memory. + // note that this means that `getTimestamp()` must handle no-selection case, too. + hostClipboard.SetText(getTimestamp()); - clipboard.Value = new ClipboardContent(EditorBeatmap).Serialize(); - - host.GetClipboard()?.SetText(formatSelectionAsString()); + if (CanCopy.Value) + clipboard.Value = new ClipboardContent(EditorBeatmap).Serialize(); } - protected override void PerformPaste() + public override void Paste() { - base.PerformPaste(); + if (!CanPaste.Value) + return; var objects = clipboard.Value.Deserialize().HitObjects; @@ -147,7 +152,7 @@ namespace osu.Game.Screens.Edit.Compose CanPaste.Value = composer.IsLoaded && !string.IsNullOrEmpty(clipboard.Value); } - private string formatSelectionAsString() + private string getTimestamp() { if (composer == null) return string.Empty; diff --git a/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs b/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs index 46d9555e0c..57960a76a1 100644 --- a/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs +++ b/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Game.Rulesets.Objects; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index bb052b1d22..91c3c98f01 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Edit public override bool DisallowExternalBeatmapRulesetChanges => true; - public override bool? AllowTrackAdjustments => false; + public override bool? ApplyModTrackAdjustments => false; protected override bool PlayExitSound => !ExitConfirmed && !switchingDifficulty; @@ -185,6 +185,7 @@ namespace osu.Game.Screens.Edit private Bindable editorBackgroundDim; private Bindable editorHitMarkers; private Bindable editorAutoSeekOnPlacement; + private Bindable editorLimitedDistanceSnap; public Editor(EditorLoader loader = null) { @@ -198,6 +199,8 @@ namespace osu.Game.Screens.Edit if (loadableBeatmap is DummyWorkingBeatmap) { + Logger.Log("Editor was loaded without a valid beatmap; creating a new beatmap."); + isNewBeatmap = true; loadableBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value); @@ -276,6 +279,7 @@ namespace osu.Game.Screens.Edit editorBackgroundDim = config.GetBindable(OsuSetting.EditorDim); editorHitMarkers = config.GetBindable(OsuSetting.EditorShowHitMarkers); editorAutoSeekOnPlacement = config.GetBindable(OsuSetting.EditorAutoSeekOnPlacement); + editorLimitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); AddInternal(new OsuContextMenuContainer { @@ -337,6 +341,10 @@ namespace osu.Game.Screens.Edit new ToggleMenuItem(EditorStrings.AutoSeekOnPlacement) { State = { BindTarget = editorAutoSeekOnPlacement }, + }, + new ToggleMenuItem(EditorStrings.LimitedDistanceSnap) + { + State = { BindTarget = editorLimitedDistanceSnap }, } } }, @@ -353,7 +361,7 @@ namespace osu.Game.Screens.Edit { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - X = -15, + X = -10, Current = Mode, }, }, @@ -417,9 +425,10 @@ namespace osu.Game.Screens.Edit { if (HasUnsavedChanges) { - dialogOverlay.Push(new SaveBeforeGameplayTestDialog(() => + dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to test it.", () => { - Save(); + if (!Save()) return; + pushEditorPlayer(); })); } @@ -533,6 +542,9 @@ namespace osu.Game.Screens.Edit // Track traversal keys. // Matching osu-stable implementations. case Key.Z: + if (e.Repeat) + return false; + // Seek to first object time, or track start if already there. double? firstObjectTime = editorBeatmap.HitObjects.FirstOrDefault()?.StartTime; @@ -543,12 +555,18 @@ namespace osu.Game.Screens.Edit return true; case Key.X: + if (e.Repeat) + return false; + // Restart playback from beginning of track. clock.Seek(0); clock.Start(); return true; case Key.C: + if (e.Repeat) + return false; + // Pause or resume. if (clock.IsRunning) clock.Stop(); @@ -557,6 +575,9 @@ namespace osu.Game.Screens.Edit return true; case Key.V: + if (e.Repeat) + return false; + // Seek to last object time, or track end if already there. // Note that in osu-stable subsequent presses when at track end won't return to last object. // This has intentionally been changed to make it more useful. @@ -693,8 +714,11 @@ namespace osu.Game.Screens.Edit } // if the dialog is already displayed, block exiting until the user explicitly makes a decision. - if (dialogOverlay.CurrentDialog is PromptForSaveDialog) + if (dialogOverlay.CurrentDialog is PromptForSaveDialog saveDialog) + { + saveDialog.Flash(); return true; + } if (isNewBeatmap || HasUnsavedChanges) { @@ -746,7 +770,7 @@ namespace osu.Game.Screens.Edit private void confirmExitWithSave() { - Save(); + if (!Save()) return; ExitConfirmed = true; this.Exit(); @@ -838,7 +862,7 @@ namespace osu.Game.Screens.Edit private void resetTrack(bool seekToStart = false) { - Beatmap.Value.Track.Stop(); + clock.Stop(); if (seekToStart) { @@ -979,21 +1003,51 @@ namespace osu.Game.Screens.Edit private List createFileMenuItems() => new List { - new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), - new EditorMenuItem(EditorStrings.ExportPackage, MenuItemType.Standard, exportBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, - new EditorMenuItemSpacer(), createDifficultyCreationMenu(), createDifficultySwitchMenu(), new EditorMenuItemSpacer(), new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } }, new EditorMenuItemSpacer(), + new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), + createExportMenu(), + new EditorMenuItemSpacer(), new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit) }; - private void exportBeatmap() + private EditorMenuItem createExportMenu() { - Save(); - beatmapManager.Export(Beatmap.Value.BeatmapSetInfo); + var exportItems = new List + { + new EditorMenuItem(EditorStrings.ExportForEditing, MenuItemType.Standard, () => exportBeatmap(false)) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + new EditorMenuItem(EditorStrings.ExportForCompatibility, MenuItemType.Standard, () => exportBeatmap(true)) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + }; + + return new EditorMenuItem(CommonStrings.Export) { Items = exportItems }; + } + + private void exportBeatmap(bool legacy) + { + if (HasUnsavedChanges) + { + dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to export it.", () => + { + if (!Save()) return; + + runExport(); + })); + } + else + { + runExport(); + } + + void runExport() + { + if (legacy) + beatmapManager.ExportLegacy(Beatmap.Value.BeatmapSetInfo); + else + beatmapManager.Export(Beatmap.Value.BeatmapSetInfo); + } } /// diff --git a/osu.Game/Screens/Edit/EditorClipboard.cs b/osu.Game/Screens/Edit/EditorClipboard.cs index f749f4bad6..af303618fb 100644 --- a/osu.Game/Screens/Edit/EditorClipboard.cs +++ b/osu.Game/Screens/Edit/EditorClipboard.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; namespace osu.Game.Screens.Edit diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index e5e88a04d9..d5ca6fc35e 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.Edit this.beatDivisor = beatDivisor ?? new BindableBeatDivisor(); - underlyingClock = new FramedBeatmapClock(applyOffsets: true) { IsCoupled = false }; + underlyingClock = new FramedBeatmapClock(applyOffsets: true, requireDecoupling: true); AddInternal(underlyingClock); } @@ -158,8 +158,6 @@ namespace osu.Game.Screens.Edit public double CurrentTime => underlyingClock.CurrentTime; - public double TotalAppliedOffset => underlyingClock.TotalAppliedOffset; - public void Reset() { ClearTransforms(); @@ -231,8 +229,6 @@ namespace osu.Game.Screens.Edit public double FramesPerSecond => underlyingClock.FramesPerSecond; - public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo; - public void ChangeSource(IClock source) { track.Value = source as Track; diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index f665b7c511..8bcfa7b9f0 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -42,6 +42,8 @@ namespace osu.Game.Screens.Edit public override bool DisallowExternalBeatmapRulesetChanges => true; + public override bool? AllowGlobalTrackControl => false; + [Resolved] private BeatmapManager beatmapManager { get; set; } diff --git a/osu.Game/Screens/Edit/EditorRoundedScreenSettings.cs b/osu.Game/Screens/Edit/EditorRoundedScreenSettings.cs index 1c083b4fab..510c27e8c6 100644 --- a/osu.Game/Screens/Edit/EditorRoundedScreenSettings.cs +++ b/osu.Game/Screens/Edit/EditorRoundedScreenSettings.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Graphics; diff --git a/osu.Game/Screens/Edit/EditorScreen.cs b/osu.Game/Screens/Edit/EditorScreen.cs index 069a5490bb..3bc870b898 100644 --- a/osu.Game/Screens/Edit/EditorScreen.cs +++ b/osu.Game/Screens/Edit/EditorScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -17,7 +15,7 @@ namespace osu.Game.Screens.Edit public abstract partial class EditorScreen : VisibilityContainer { [Resolved] - protected EditorBeatmap EditorBeatmap { get; private set; } + protected EditorBeatmap EditorBeatmap { get; private set; } = null!; protected override Container Content => content; private readonly Container content; @@ -46,29 +44,23 @@ namespace osu.Game.Screens.Edit /// /// Performs a "cut to clipboard" operation appropriate for the given screen. /// - protected virtual void PerformCut() + /// + /// Implementors are responsible for checking themselves. + /// + public virtual void Cut() { } - public void Cut() - { - if (CanCut.Value) - PerformCut(); - } - public BindableBool CanCopy { get; } = new BindableBool(); /// /// Performs a "copy to clipboard" operation appropriate for the given screen. /// - protected virtual void PerformCopy() - { - } - + /// + /// Implementors are responsible for checking themselves. + /// public virtual void Copy() { - if (CanCopy.Value) - PerformCopy(); } public BindableBool CanPaste { get; } = new BindableBool(); @@ -76,14 +68,11 @@ namespace osu.Game.Screens.Edit /// /// Performs a "paste from clipboard" operation appropriate for the given screen. /// - protected virtual void PerformPaste() - { - } - + /// + /// Implementors are responsible for checking themselves. + /// public virtual void Paste() { - if (CanPaste.Value) - PerformPaste(); } #endregion diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index 84cfac8f65..575a66d421 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -1,43 +1,38 @@ // 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.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; -using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components.Timeline; namespace osu.Game.Screens.Edit { + [Cached] public abstract partial class EditorScreenWithTimeline : EditorScreen { - private const float padding = 10; + public const float PADDING = 10; - private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); + public Container TimelineContent { get; private set; } = null!; - private Container timelineContainer; + public Container MainContent { get; private set; } = null!; + + private LoadingSpinner spinner = null!; protected EditorScreenWithTimeline(EditorScreenMode type) : base(type) { } - private Container mainContent; - - private LoadingSpinner spinner; - [BackgroundDependencyLoader(true)] - private void load(OverlayColourProvider colourProvider, [CanBeNull] BindableBeatDivisor beatDivisor) + private void load(OverlayColourProvider colourProvider) { - if (beatDivisor != null) - this.beatDivisor.BindTo(beatDivisor); - + // Grid with only two rows. + // First is the timeline area, which should be allowed to expand as required. + // Second is the main editor content, including the playfield and side toolbars (but not the bottom). Child = new GridContainer { RelativeSizeAxes = Axes.Both, @@ -67,7 +62,7 @@ namespace osu.Game.Screens.Edit Name = "Timeline content", RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = padding, Top = padding }, + Padding = new MarginPadding { Horizontal = PADDING, Top = PADDING }, Child = new GridContainer { RelativeSizeAxes = Axes.X, @@ -76,13 +71,11 @@ namespace osu.Game.Screens.Edit { new Drawable[] { - timelineContainer = new Container + TimelineContent = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Right = 5 }, }, - new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both } }, }, RowDimensions = new[] @@ -101,7 +94,7 @@ namespace osu.Game.Screens.Edit }, new Drawable[] { - mainContent = new Container + MainContent = new Container { Name = "Main content", RelativeSizeAxes = Axes.Both, @@ -124,10 +117,10 @@ namespace osu.Game.Screens.Edit { spinner.State.Value = Visibility.Hidden; - mainContent.Add(content); + MainContent.Add(content); content.FadeInFromZero(300, Easing.OutQuint); - LoadComponentAsync(new TimelineArea(CreateTimelineContent()), timelineContainer.Add); + LoadComponentAsync(new TimelineArea(CreateTimelineContent()), TimelineContent.Add); }); } diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs index a74d97cdc7..bb151e4a45 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; @@ -14,7 +12,7 @@ namespace osu.Game.Screens.Edit.GameplayTest public partial class EditorPlayerLoader : PlayerLoader { [Resolved] - private OsuLogo osuLogo { get; set; } + private OsuLogo osuLogo { get; set; } = null!; public EditorPlayerLoader(Editor editor) : base(() => new EditorPlayer(editor)) diff --git a/osu.Game/Screens/Edit/HitAnimationsMenuItem.cs b/osu.Game/Screens/Edit/HitAnimationsMenuItem.cs index 3e1e0c4cfe..ee64a53301 100644 --- a/osu.Game/Screens/Edit/HitAnimationsMenuItem.cs +++ b/osu.Game/Screens/Edit/HitAnimationsMenuItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Bindables; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs index 565379f391..bb9f702cb5 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs @@ -114,7 +114,7 @@ namespace osu.Game.Screens.Edit continue; if (oldObject is IHasSliderVelocity oldWithVelocity && newObject is IHasSliderVelocity newWithVelocity) - oldWithVelocity.SliderVelocity = newWithVelocity.SliderVelocity; + oldWithVelocity.SliderVelocityMultiplier = newWithVelocity.SliderVelocityMultiplier; oldObject.Samples = newObject.Samples; @@ -123,6 +123,8 @@ namespace osu.Game.Screens.Edit oldWithRepeats.NodeSamples.Clear(); oldWithRepeats.NodeSamples.AddRange(newWithRepeats.NodeSamples); } + + editorBeatmap.Update(oldObject); } } diff --git a/osu.Game/Screens/Edit/GameplayTest/SaveBeforeGameplayTestDialog.cs b/osu.Game/Screens/Edit/SaveRequiredPopupDialog.cs similarity index 67% rename from osu.Game/Screens/Edit/GameplayTest/SaveBeforeGameplayTestDialog.cs rename to osu.Game/Screens/Edit/SaveRequiredPopupDialog.cs index 5a5572b508..3ca92876f1 100644 --- a/osu.Game/Screens/Edit/GameplayTest/SaveBeforeGameplayTestDialog.cs +++ b/osu.Game/Screens/Edit/SaveRequiredPopupDialog.cs @@ -1,19 +1,17 @@ // 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 osu.Framework.Graphics.Sprites; using osu.Game.Overlays.Dialog; -namespace osu.Game.Screens.Edit.GameplayTest +namespace osu.Game.Screens.Edit { - public partial class SaveBeforeGameplayTestDialog : PopupDialog + public partial class SaveRequiredPopupDialog : PopupDialog { - public SaveBeforeGameplayTestDialog(Action saveAndPreview) + public SaveRequiredPopupDialog(string headerText, Action saveAndAction) { - HeaderText = "The beatmap will be saved in order to test it."; + HeaderText = headerText; Icon = FontAwesome.Regular.Save; @@ -22,7 +20,7 @@ namespace osu.Game.Screens.Edit.GameplayTest new PopupDialogOkButton { Text = "Sounds good, let's go!", - Action = saveAndPreview + Action = saveAndAction }, new PopupDialogCancelButton { diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index 4c062b0cb7..1915b0cfd1 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -13,14 +13,14 @@ using osu.Game.Localisation; namespace osu.Game.Screens.Edit.Setup { - internal partial class DifficultySection : SetupSection + public partial class DifficultySection : SetupSection { - private LabelledSliderBar circleSizeSlider = null!; - private LabelledSliderBar healthDrainSlider = null!; - private LabelledSliderBar approachRateSlider = null!; - private LabelledSliderBar overallDifficultySlider = null!; - private LabelledSliderBar baseVelocitySlider = null!; - private LabelledSliderBar tickRateSlider = null!; + protected LabelledSliderBar CircleSizeSlider { get; private set; } = null!; + protected LabelledSliderBar HealthDrainSlider { get; private set; } = null!; + protected LabelledSliderBar ApproachRateSlider { get; private set; } = null!; + protected LabelledSliderBar OverallDifficultySlider { get; private set; } = null!; + protected LabelledSliderBar BaseVelocitySlider { get; private set; } = null!; + protected LabelledSliderBar TickRateSlider { get; private set; } = null!; public override LocalisableString Title => EditorSetupStrings.DifficultyHeader; @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Edit.Setup { Children = new Drawable[] { - circleSizeSlider = new LabelledSliderBar + CircleSizeSlider = new LabelledSliderBar { Label = BeatmapsetsStrings.ShowStatsCs, FixedLabelWidth = LABEL_WIDTH, @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Edit.Setup Precision = 0.1f, } }, - healthDrainSlider = new LabelledSliderBar + HealthDrainSlider = new LabelledSliderBar { Label = BeatmapsetsStrings.ShowStatsDrain, FixedLabelWidth = LABEL_WIDTH, @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Edit.Setup Precision = 0.1f, } }, - approachRateSlider = new LabelledSliderBar + ApproachRateSlider = new LabelledSliderBar { Label = BeatmapsetsStrings.ShowStatsAr, FixedLabelWidth = LABEL_WIDTH, @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Edit.Setup Precision = 0.1f, } }, - overallDifficultySlider = new LabelledSliderBar + OverallDifficultySlider = new LabelledSliderBar { Label = BeatmapsetsStrings.ShowStatsAccuracy, FixedLabelWidth = LABEL_WIDTH, @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Edit.Setup Precision = 0.1f, } }, - baseVelocitySlider = new LabelledSliderBar + BaseVelocitySlider = new LabelledSliderBar { Label = EditorSetupStrings.BaseVelocity, FixedLabelWidth = LABEL_WIDTH, @@ -94,7 +94,7 @@ namespace osu.Game.Screens.Edit.Setup Precision = 0.01f, } }, - tickRateSlider = new LabelledSliderBar + TickRateSlider = new LabelledSliderBar { Label = EditorSetupStrings.TickRate, FixedLabelWidth = LABEL_WIDTH, @@ -120,12 +120,12 @@ namespace osu.Game.Screens.Edit.Setup { // for now, update these on commit rather than making BeatmapMetadata bindables. // after switching database engines we can reconsider if switching to bindables is a good direction. - Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value; - Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value; - Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value; - Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value; - Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value; - Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value; + Beatmap.Difficulty.CircleSize = CircleSizeSlider.Current.Value; + Beatmap.Difficulty.DrainRate = HealthDrainSlider.Current.Value; + Beatmap.Difficulty.ApproachRate = ApproachRateSlider.Current.Value; + Beatmap.Difficulty.OverallDifficulty = OverallDifficultySlider.Current.Value; + Beatmap.Difficulty.SliderMultiplier = BaseVelocitySlider.Current.Value; + Beatmap.Difficulty.SliderTickRate = TickRateSlider.Current.Value; Beatmap.UpdateAllHitObjects(); Beatmap.SaveState(); diff --git a/osu.Game/Screens/Edit/Setup/LabelledFileChooser.cs b/osu.Game/Screens/Edit/Setup/LabelledFileChooser.cs index d14357e875..61f33c4bdc 100644 --- a/osu.Game/Screens/Edit/Setup/LabelledFileChooser.cs +++ b/osu.Game/Screens/Edit/Setup/LabelledFileChooser.cs @@ -114,6 +114,9 @@ namespace osu.Game.Screens.Edit.Setup private partial class FileChooserPopover : OsuPopover { + protected override string PopInSampleName => "UI/overlay-big-pop-in"; + protected override string PopOutSampleName => "UI/overlay-big-pop-out"; + public FileChooserPopover(string[] handledExtensions, Bindable currentFile, string? chooserPath) { Child = new Container diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index ab4299a2f0..266ea1f929 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -26,16 +26,18 @@ namespace osu.Game.Screens.Edit.Setup [BackgroundDependencyLoader] private void load(EditorBeatmap beatmap, OverlayColourProvider colourProvider) { + var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance(); + var sectionsEnumerable = new List { new ResourcesSection(), new MetadataSection(), - new DifficultySection(), + ruleset.CreateEditorDifficultySection() ?? new DifficultySection(), new ColoursSection(), new DesignSection(), }; - var rulesetSpecificSection = beatmap.BeatmapInfo.Ruleset.CreateInstance().CreateEditorSetupSection(); + var rulesetSpecificSection = ruleset.CreateEditorSetupSection(); if (rulesetSpecificSection != null) sectionsEnumerable.Add(rulesetSpecificSection); diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index 555c36aac0..22e37b9efb 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -147,13 +147,25 @@ namespace osu.Game.Screens.Edit.Timing trackedType = null; else { - // If the selected group only has one control point, update the tracking type. - if (selectedGroup.Value.ControlPoints.Count == 1) - trackedType = selectedGroup.Value?.ControlPoints.Single().GetType(); - // If the selected group has more than one control point, choose the first as the tracking type - // if we don't already have a singular tracked type. - else if (trackedType == null) - trackedType = selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType(); + switch (selectedGroup.Value.ControlPoints.Count) + { + // If the selected group has no control points, clear the tracked type. + // Otherwise the user will be unable to select a group with no control points. + case 0: + trackedType = null; + break; + + // If the selected group only has one control point, update the tracking type. + case 1: + trackedType = selectedGroup.Value?.ControlPoints.Single().GetType(); + break; + + // If the selected group has more than one control point, choose the first as the tracking type + // if we don't already have a singular tracked type. + default: + trackedType ??= selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType(); + break; + } } if (trackedType != null) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index f4a39405a1..9f03281d0c 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -240,7 +240,7 @@ namespace osu.Game.Screens.Edit.Timing { base.Update(); - if (BeatSyncSource.ControlPoints == null || BeatSyncSource.Clock == null) + if (BeatSyncSource.ControlPoints == null) return; metronomeClock.Rate = IsBeatSyncedWithTrack ? BeatSyncSource.Clock.Rate : 1; @@ -259,7 +259,7 @@ namespace osu.Game.Screens.Edit.Timing this.TransformBindableTo(interpolatedBpm, (int)Math.Round(timingPoint.BPM), 600, Easing.OutQuint); } - if (BeatSyncSource.Clock?.IsRunning != true && isSwinging) + if (!BeatSyncSource.Clock.IsRunning && isSwinging) { swing.ClearTransforms(true); diff --git a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs deleted file mode 100644 index 1bf0e5299d..0000000000 --- a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs +++ /dev/null @@ -1,106 +0,0 @@ -// 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.Globalization; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Localisation; -using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Overlays.Settings; -using osu.Game.Utils; -using osuTK; - -namespace osu.Game.Screens.Edit.Timing -{ - public partial class SliderWithTextBoxInput : CompositeDrawable, IHasCurrentValue - where T : struct, IEquatable, IComparable, IConvertible - { - private readonly SettingsSlider slider; - - public SliderWithTextBoxInput(LocalisableString labelText) - { - LabelledTextBox textBox; - - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - - InternalChildren = new Drawable[] - { - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(20), - Children = new Drawable[] - { - textBox = new LabelledTextBox - { - Label = labelText, - }, - slider = new SettingsSlider - { - TransferValueOnCommit = true, - RelativeSizeAxes = Axes.X, - } - } - }, - }; - - textBox.OnCommit += (t, isNew) => - { - if (!isNew) return; - - try - { - switch (slider.Current) - { - case Bindable bindableInt: - bindableInt.Value = int.Parse(t.Text); - break; - - case Bindable bindableDouble: - bindableDouble.Value = double.Parse(t.Text); - break; - - default: - slider.Current.Parse(t.Text); - break; - } - } - catch - { - // TriggerChange below will restore the previous text value on failure. - } - - // 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. - Current.TriggerChange(); - }; - - Current.BindValueChanged(_ => - { - decimal decimalValue = slider.Current.Value.ToDecimal(NumberFormatInfo.InvariantInfo); - textBox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}"); - }, true); - } - - /// - /// A custom step value for each key press which actuates a change on this control. - /// - public float KeyboardStep - { - get => slider.KeyboardStep; - set => slider.KeyboardStep = value; - } - - public Bindable Current - { - get => slider.Current; - set => slider.Current = value; - } - } -} diff --git a/osu.Game/Screens/Edit/Timing/TapButton.cs b/osu.Game/Screens/Edit/Timing/TapButton.cs index f28c6ccf0a..fd60fb1b5b 100644 --- a/osu.Game/Screens/Edit/Timing/TapButton.cs +++ b/osu.Game/Screens/Edit/Timing/TapButton.cs @@ -310,7 +310,7 @@ namespace osu.Game.Screens.Edit.Timing } double averageBeatLength = (tapTimings.Last() - tapTimings.Skip(initial_taps_to_ignore).First()) / (tapTimings.Count - initial_taps_to_ignore - 1); - double clockRate = beatSyncSource?.Clock?.Rate ?? 1; + double clockRate = beatSyncSource?.Clock.Rate ?? 1; double bpm = Math.Round(60000 / averageBeatLength / clockRate); diff --git a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs index 3b3acea935..856bc7c303 100644 --- a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs @@ -94,7 +94,7 @@ namespace osu.Game.Screens.Edit.Timing controlPointGroups.BindTo(editorBeatmap.ControlPointInfo.Groups); controlPointGroups.BindCollectionChanged((_, _) => updateTimingGroup()); - beatLength.BindValueChanged(_ => regenerateDisplay(true), true); + beatLength.BindValueChanged(_ => Scheduler.AddOnce(regenerateDisplay, true), true); displayLocked.BindValueChanged(locked => { @@ -186,11 +186,18 @@ namespace osu.Game.Screens.Edit.Timing return; displayedTime = time; - regenerateDisplay(animated); + Scheduler.AddOnce(regenerateDisplay, animated); } private void regenerateDisplay(bool animated) { + // Before a track is loaded, it won't have a valid length, which will break things. + if (!beatmap.Value.Track.IsLoaded) + { + Scheduler.AddOnce(regenerateDisplay, animated); + return; + } + double index = (displayedTime - selectedGroupStartTime) / timingPoint.BeatLength; // Chosen as a pretty usable number across all BPMs. diff --git a/osu.Game/Screens/Edit/Verify/InterpretationSection.cs b/osu.Game/Screens/Edit/Verify/InterpretationSection.cs index 5b6eea098c..b16e3750bf 100644 --- a/osu.Game/Screens/Edit/Verify/InterpretationSection.cs +++ b/osu.Game/Screens/Edit/Verify/InterpretationSection.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; diff --git a/osu.Game/Screens/Edit/Verify/IssueSettings.cs b/osu.Game/Screens/Edit/Verify/IssueSettings.cs index e8275c3684..6d3c0520a2 100644 --- a/osu.Game/Screens/Edit/Verify/IssueSettings.cs +++ b/osu.Game/Screens/Edit/Verify/IssueSettings.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Graphics; diff --git a/osu.Game/Screens/Edit/WaveformOpacityMenuItem.cs b/osu.Game/Screens/Edit/WaveformOpacityMenuItem.cs index 5b1d7142e4..9dc0ea0d07 100644 --- a/osu.Game/Screens/Edit/WaveformOpacityMenuItem.cs +++ b/osu.Game/Screens/Edit/WaveformOpacityMenuItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Bindables; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Screens/IHandlePresentBeatmap.cs b/osu.Game/Screens/IHandlePresentBeatmap.cs index 62cd2c3d3e..323e3b1c0c 100644 --- a/osu.Game/Screens/IHandlePresentBeatmap.cs +++ b/osu.Game/Screens/IHandlePresentBeatmap.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Beatmaps; using osu.Game.Rulesets; diff --git a/osu.Game/Screens/IHasSubScreenStack.cs b/osu.Game/Screens/IHasSubScreenStack.cs index 325702313b..0fcf21ef2b 100644 --- a/osu.Game/Screens/IHasSubScreenStack.cs +++ b/osu.Game/Screens/IHasSubScreenStack.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Screens; namespace osu.Game.Screens diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index a5739a41b1..5b4e2d75f4 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Framework.Screens; using osu.Game.Beatmaps; @@ -69,7 +67,13 @@ namespace osu.Game.Screens /// Whether mod track adjustments should be applied on entering this screen. /// A value means that the parent screen's value of this setting will be used. /// - bool? AllowTrackAdjustments { get; } + bool? ApplyModTrackAdjustments { get; } + + /// + /// Whether control of the global track should be allowed via the music controller / now playing overlay. + /// A value means that the parent screen's value of this setting will be used. + /// + bool? AllowGlobalTrackControl { get; } /// /// Invoked when the back button has been pressed to close any overlays before exiting this . diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 2ead18c3d6..bf2eba43c0 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -132,11 +132,9 @@ namespace osu.Game.Screens.Menu buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, - Key.P)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-edit-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E)); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, - Key.D)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.B, Key.D)); if (host.CanExit) buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q)); diff --git a/osu.Game/Screens/Menu/ConfirmExitDialog.cs b/osu.Game/Screens/Menu/ConfirmExitDialog.cs index 4906232d21..0041d047bd 100644 --- a/osu.Game/Screens/Menu/ConfirmExitDialog.cs +++ b/osu.Game/Screens/Menu/ConfirmExitDialog.cs @@ -2,38 +2,80 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; +using osu.Game.Localisation; +using osu.Game.Overlays; using osu.Game.Overlays.Dialog; namespace osu.Game.Screens.Menu { public partial class ConfirmExitDialog : PopupDialog { + private readonly Action onConfirm; + private readonly Action? onCancel; + /// /// Construct a new exit confirmation dialog. /// /// An action to perform on confirmation. /// An optional action to perform on cancel. public ConfirmExitDialog(Action onConfirm, Action? onCancel = null) + { + this.onConfirm = onConfirm; + this.onCancel = onCancel; + } + + [BackgroundDependencyLoader] + private void load(INotificationOverlay notifications) { HeaderText = "Are you sure you want to exit osu!?"; - BodyText = "Last chance to turn back"; Icon = FontAwesome.Solid.ExclamationTriangle; - Buttons = new PopupDialogButton[] + if (notifications.HasOngoingOperations) { - new PopupDialogOkButton + string text = "There are currently some background operations which will be aborted if you continue:\n\n"; + + foreach (var n in notifications.OngoingOperations) + text += $"{n.Text} ({n.Progress:0%})\n"; + + text += "\nLast chance to turn back"; + + BodyText = text; + + Buttons = new PopupDialogButton[] { - Text = @"Let me out!", - Action = onConfirm - }, - new PopupDialogCancelButton + new PopupDialogDangerousButton + { + Text = @"Let me out!", + Action = onConfirm + }, + new PopupDialogCancelButton + { + Text = CommonStrings.Back, + Action = onCancel + }, + }; + } + else + { + BodyText = "Last chance to turn back"; + + Buttons = new PopupDialogButton[] { - Text = @"Just a little more...", - Action = onCancel - }, - }; + new PopupDialogOkButton + { + Text = @"Let me out!", + Action = onConfirm + }, + new PopupDialogCancelButton + { + Text = @"Just a little more...", + Action = onCancel + }, + }; + } } } } diff --git a/osu.Game/Screens/Menu/ExitConfirmOverlay.cs b/osu.Game/Screens/Menu/HoldToExitGameOverlay.cs similarity index 80% rename from osu.Game/Screens/Menu/ExitConfirmOverlay.cs rename to osu.Game/Screens/Menu/HoldToExitGameOverlay.cs index bc2f6ea00f..f3642f2175 100644 --- a/osu.Game/Screens/Menu/ExitConfirmOverlay.cs +++ b/osu.Game/Screens/Menu/HoldToExitGameOverlay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Input.Bindings; @@ -10,13 +8,13 @@ using osu.Game.Overlays; namespace osu.Game.Screens.Menu { - public partial class ExitConfirmOverlay : HoldToConfirmOverlay, IKeyBindingHandler + public partial class HoldToExitGameOverlay : HoldToConfirmOverlay, IKeyBindingHandler { protected override bool AllowMultipleFires => true; public void Abort() => AbortConfirm(); - public ExitConfirmOverlay() + public HoldToExitGameOverlay() : base(0.7f) { } diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index a9c86b10c4..aab3afcd24 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -36,7 +36,6 @@ namespace osu.Game.Screens.Menu private Sample welcome; - private DecoupleableInterpolatingFramedClock decoupledClock; private TrianglesIntroSequence intro; public IntroTriangles([CanBeNull] Func createNextScreen = null) @@ -59,18 +58,12 @@ namespace osu.Game.Screens.Menu { PrepareMenuLoad(); - decoupledClock = new DecoupleableInterpolatingFramedClock - { - IsCoupled = false - }; - - if (UsingThemedIntro) - decoupledClock.ChangeSource(Track); + var decouplingClock = new DecouplingFramedClock(UsingThemedIntro ? Track : null); LoadComponentAsync(intro = new TrianglesIntroSequence(logo, () => FadeInBackground()) { RelativeSizeAxes = Axes.Both, - Clock = decoupledClock, + Clock = new InterpolatingFramedClock(decouplingClock), LoadMenu = LoadMenu }, _ => { @@ -94,7 +87,7 @@ namespace osu.Game.Screens.Menu StartTrack(); // no-op for the case of themed intro, no harm in calling for both scenarios as a safety measure. - decoupledClock.Start(); + decouplingClock.Start(); }); } } diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs new file mode 100644 index 0000000000..07c06dcdb9 --- /dev/null +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -0,0 +1,87 @@ +// 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.Audio.Track; +using osu.Framework.Graphics; +using osu.Framework.Utils; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Screens.Menu +{ + public partial class KiaiMenuFountains : BeatSyncedContainer + { + private StarFountain leftFountain = null!; + private StarFountain rightFountain = null!; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + + Children = new[] + { + leftFountain = new StarFountain + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + X = 250, + }, + rightFountain = new StarFountain + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + X = -250, + }, + }; + } + + private bool isTriggered; + + private double? lastTrigger; + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + if (effectPoint.KiaiMode && !isTriggered) + { + bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - effectPoint.Time) < 500; + if (isNearEffectPoint) + Shoot(); + } + + isTriggered = effectPoint.KiaiMode; + } + + public void Shoot() + { + if (lastTrigger != null && Clock.CurrentTime - lastTrigger < 500) + return; + + int direction = RNG.Next(-1, 2); + + switch (direction) + { + case -1: + leftFountain.Shoot(1); + rightFountain.Shoot(-1); + break; + + case 0: + leftFountain.Shoot(0); + rightFountain.Shoot(0); + break; + + case 1: + leftFountain.Shoot(-1); + rightFountain.Shoot(1); + break; + } + + lastTrigger = Clock.CurrentTime; + } + } +} diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 5000a97b3d..fa26cfab46 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -102,8 +102,7 @@ namespace osu.Game.Screens.Menu for (int i = 0; i < temporalAmplitudes.Length; i++) temporalAmplitudes[i] = 0; - if (beatSyncProvider.Clock != null) - addAmplitudesFromSource(beatSyncProvider); + addAmplitudesFromSource(beatSyncProvider); foreach (var source in amplitudeSources) addAmplitudesFromSource(source); diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 69b8596474..22040b4f0b 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -5,9 +5,11 @@ using System; using System.Diagnostics; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Logging; @@ -54,14 +56,20 @@ namespace osu.Game.Screens.Menu private GameHost host { get; set; } [Resolved] - private MusicController musicController { get; set; } + private INotificationOverlay notifications { get; set; } - [Resolved(canBeNull: true)] - private LoginOverlay login { get; set; } + [Resolved] + private MusicController musicController { get; set; } [Resolved] private IAPIProvider api { get; set; } + [Resolved] + private Storage storage { get; set; } + + [Resolved(canBeNull: true)] + private LoginOverlay login { get; set; } + [Resolved(canBeNull: true)] private IDialogOverlay dialogOverlay { get; set; } @@ -72,10 +80,14 @@ namespace osu.Game.Screens.Menu private Bindable holdDelay; private Bindable loginDisplayed; - private ExitConfirmOverlay exitConfirmOverlay; + private HoldToExitGameOverlay holdToExitGameOverlay; + + private bool exitConfirmedViaDialog; + private bool exitConfirmedViaHoldOrClick; private ParallaxContainer buttonsContainer; private SongTicker songTicker; + private Container logoTarget; [BackgroundDependencyLoader(true)] private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics) @@ -85,14 +97,12 @@ namespace osu.Game.Screens.Menu if (host.CanExit) { - AddInternal(exitConfirmOverlay = new ExitConfirmOverlay + AddInternal(holdToExitGameOverlay = new HoldToExitGameOverlay { Action = () => { - if (holdDelay.Value > 0) - confirmAndExit(); - else - this.Exit(); + exitConfirmedViaHoldOrClick = holdDelay.Value > 0; + this.Exit(); } }); } @@ -114,10 +124,15 @@ namespace osu.Game.Screens.Menu OnSolo = loadSoloSongSelect, OnMultiplayer = () => this.Push(new Multiplayer()), OnPlaylists = () => this.Push(new Playlists()), - OnExit = confirmAndExit, + OnExit = () => + { + exitConfirmedViaHoldOrClick = true; + this.Exit(); + } } } }, + logoTarget = new Container { RelativeSizeAxes = Axes.Both, }, sideFlashes = new MenuSideFlashes(), songTicker = new SongTicker { @@ -125,7 +140,8 @@ namespace osu.Game.Screens.Menu Origin = Anchor.TopRight, Margin = new MarginPadding { Right = 15, Top = 5 } }, - exitConfirmOverlay?.CreateProxy() ?? Empty() + new KiaiMenuFountains(), + holdToExitGameOverlay?.CreateProxy() ?? Empty() }); Buttons.StateChanged += state => @@ -149,19 +165,8 @@ namespace osu.Game.Screens.Menu preloadSongSelect(); } - [Resolved(canBeNull: true)] - private IPerformFromScreenRunner performer { get; set; } - public void ReturnToOsuLogo() => Buttons.State = ButtonSystemState.Initial; - private void confirmAndExit() - { - if (exitConfirmed) return; - - exitConfirmed = true; - performer?.PerformFromScreen(menu => menu.Exit()); - } - private void preloadSongSelect() { if (songSelect == null) @@ -177,9 +182,6 @@ namespace osu.Game.Screens.Menu return s; } - [Resolved] - private Storage storage { get; set; } - public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); @@ -201,7 +203,8 @@ namespace osu.Game.Screens.Menu dialogOverlay?.Push(new StorageErrorDialog(osuStorage, osuStorage.Error)); } - private bool exitConfirmed; + [CanBeNull] + private Drawable proxiedLogo; protected override void LogoArriving(OsuLogo logo, bool resuming) { @@ -212,6 +215,8 @@ namespace osu.Game.Screens.Menu logo.FadeColour(Color4.White, 100, Easing.OutQuint); logo.FadeIn(100, Easing.OutQuint); + proxiedLogo = logo.ProxyToContainer(logoTarget); + if (resuming) { Buttons.State = ButtonSystemState.TopLevel; @@ -249,10 +254,27 @@ namespace osu.Game.Screens.Menu var seq = logo.FadeOut(300, Easing.InSine) .ScaleTo(0.2f, 300, Easing.InSine); + if (proxiedLogo != null) + { + logo.ReturnProxy(); + proxiedLogo = null; + } + seq.OnComplete(_ => Buttons.SetOsuLogo(null)); seq.OnAbort(_ => Buttons.SetOsuLogo(null)); } + protected override void LogoExiting(OsuLogo logo) + { + base.LogoExiting(logo); + + if (proxiedLogo != null) + { + logo.ReturnProxy(); + proxiedLogo = null; + } + } + public override void OnSuspending(ScreenTransitionEvent e) { base.OnSuspending(e); @@ -279,12 +301,29 @@ namespace osu.Game.Screens.Menu public override bool OnExiting(ScreenExitEvent e) { - if (!exitConfirmed && dialogOverlay != null) + bool requiresConfirmation = + // we need to have a dialog overlay to confirm in the first place. + dialogOverlay != null + // if the dialog has already displayed and been accepted by the user, we are good. + && !exitConfirmedViaDialog + // Only require confirmation if there is either an ongoing operation or the user exited via a non-hold escape press. + && (notifications.HasOngoingOperations || !exitConfirmedViaHoldOrClick); + + if (requiresConfirmation) { if (dialogOverlay.CurrentDialog is ConfirmExitDialog exitDialog) exitDialog.PerformOkAction(); else - dialogOverlay.Push(new ConfirmExitDialog(confirmAndExit, () => exitConfirmOverlay.Abort())); + { + dialogOverlay.Push(new ConfirmExitDialog(() => + { + exitConfirmedViaDialog = true; + this.Exit(); + }, () => + { + holdToExitGameOverlay.Abort(); + })); + } return true; } diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index cd3795711e..c3a96e36a1 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Linq; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -34,7 +35,7 @@ namespace osu.Game.Screens.Menu { public event Action StateChanged; - public readonly Key TriggerKey; + public readonly Key[] TriggerKeys; private readonly Container iconText; private readonly Container box; @@ -53,11 +54,11 @@ namespace osu.Game.Screens.Menu public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos); - public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, Key triggerKey = Key.Unknown) + public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, params Key[] triggerKeys) { this.sampleName = sampleName; this.clickAction = clickAction; - TriggerKey = triggerKey; + TriggerKeys = triggerKeys; AutoSizeAxes = Axes.Both; Alpha = 0; @@ -213,7 +214,7 @@ namespace osu.Game.Screens.Menu if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed || e.SuperPressed) return false; - if (TriggerKey == e.Key && TriggerKey != Key.Unknown) + if (TriggerKeys.Contains(e.Key)) { trigger(); return true; diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 277b8bf888..8867ecfb2a 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -435,5 +435,46 @@ namespace osu.Game.Screens.Menu logoBounceContainer.MoveTo(Vector2.Zero, 800, Easing.OutElastic); base.OnDragEnd(e); } + + private Container defaultProxyTarget; + private Container currentProxyTarget; + private Drawable proxy; + + public Drawable ProxyToContainer(Container c) + { + if (currentProxyTarget != null) + throw new InvalidOperationException("Previous proxy usage was not returned"); + + if (defaultProxyTarget == null) + throw new InvalidOperationException($"{nameof(SetupDefaultContainer)} must be called first"); + + currentProxyTarget = c; + + defaultProxyTarget.Remove(proxy, false); + currentProxyTarget.Add(proxy); + return proxy; + } + + public void ReturnProxy() + { + if (currentProxyTarget == null) + throw new InvalidOperationException("No usage to return"); + + if (defaultProxyTarget == null) + throw new InvalidOperationException($"{nameof(SetupDefaultContainer)} must be called first"); + + currentProxyTarget.Remove(proxy, false); + currentProxyTarget = null; + + defaultProxyTarget.Add(proxy); + } + + public void SetupDefaultContainer(Container container) + { + defaultProxyTarget = container; + + defaultProxyTarget.Add(this); + defaultProxyTarget.Add(proxy = CreateProxy()); + } } } diff --git a/osu.Game/Screens/Menu/SongTicker.cs b/osu.Game/Screens/Menu/SongTicker.cs index bac7e15461..3bdc0efe19 100644 --- a/osu.Game/Screens/Menu/SongTicker.cs +++ b/osu.Game/Screens/Menu/SongTicker.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -20,7 +18,7 @@ namespace osu.Game.Screens.Menu private const int fade_duration = 800; [Resolved] - private Bindable beatmap { get; set; } + private Bindable beatmap { get; set; } = null!; private readonly OsuSpriteText title, artist; diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs new file mode 100644 index 0000000000..fd59ec3573 --- /dev/null +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -0,0 +1,93 @@ +// 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.Graphics.Textures; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Screens.Menu +{ + public partial class StarFountain : SkinReloadableDrawable + { + private StarFountainSpewer spewer = null!; + + [Resolved] + private TextureStore textures { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = spewer = new StarFountainSpewer(); + } + + public void Shoot(int direction) => spewer.Shoot(direction); + + protected override void SkinChanged(ISkinSource skin) + { + base.SkinChanged(skin); + spewer.Texture = skin.GetTexture("Menu/fountain-star") ?? textures.Get("Menu/fountain-star"); + } + + public partial class StarFountainSpewer : ParticleSpewer + { + private const int particle_duration_min = 300; + private const int particle_duration_max = 1000; + + private double? lastShootTime; + private int lastShootDirection; + + protected override float ParticleGravity => 800; + + private const double shoot_duration = 800; + + protected override bool CanSpawnParticles => lastShootTime != null && Time.Current - lastShootTime < shoot_duration; + + [Resolved] + private ISkinSource skin { get; set; } = null!; + + public StarFountainSpewer() + : base(null, 240, particle_duration_max) + { + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + Texture = skin.GetTexture("Menu/fountain-star") ?? textures.Get("Menu/fountain-star"); + Active.Value = true; + } + + protected override FallingParticle CreateParticle() + { + return new FallingParticle + { + StartPosition = new Vector2(0, 50), + Duration = RNG.NextSingle(particle_duration_min, particle_duration_max), + StartAngle = getRandomVariance(4), + EndAngle = getRandomVariance(2), + EndScale = 2.2f + getRandomVariance(0.4f), + Velocity = new Vector2(getCurrentAngle(), -1400 + getRandomVariance(100)), + }; + } + + private float getCurrentAngle() + { + const float x_velocity_from_direction = 500; + const float x_velocity_random_variance = 60; + + return lastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / shoot_duration) + getRandomVariance(x_velocity_random_variance); + } + + public void Shoot(int direction) + { + lastShootTime = Clock.CurrentTime; + lastShootDirection = direction; + } + + private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance); + } + } +} diff --git a/osu.Game/Screens/Menu/StorageErrorDialog.cs b/osu.Game/Screens/Menu/StorageErrorDialog.cs index ba05ad8b76..dd43289873 100644 --- a/osu.Game/Screens/Menu/StorageErrorDialog.cs +++ b/osu.Game/Screens/Menu/StorageErrorDialog.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Graphics.Sprites; @@ -15,7 +13,7 @@ namespace osu.Game.Screens.Menu public partial class StorageErrorDialog : PopupDialog { [Resolved] - private IDialogOverlay dialogOverlay { get; set; } + private IDialogOverlay dialogOverlay { get; set; } = null!; public StorageErrorDialog(OsuStorage storage, OsuStorageError error) { diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapDetailAreaPlaylistTabItem.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapDetailAreaPlaylistTabItem.cs index 7c48fc0871..41b994ea32 100644 --- a/osu.Game/Screens/OnlinePlay/Components/BeatmapDetailAreaPlaylistTabItem.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapDetailAreaPlaylistTabItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Screens.Select; namespace osu.Game.Screens.OnlinePlay.Components diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs index ebcc08360e..7c57f5b4f5 100644 --- a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.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 osu.Framework.Allocation; using osu.Framework.Graphics; @@ -49,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Components } [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; private void updateText() { diff --git a/osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs b/osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs index 3f7f38f3bc..97716759c3 100644 --- a/osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs +++ b/osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; diff --git a/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs index 77e461ce41..d24ad74a68 100644 --- a/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs +++ b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; @@ -41,7 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Components } [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs b/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs index 0e2ce6703f..09a3602cdd 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs b/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs index f8dcd7b75d..fc86cbbbdd 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Components diff --git a/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs index 4fdf41d0f7..5128bc4c14 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -31,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Components RelativeSizeAxes = Axes.X; scroll.RelativeSizeAxes = Axes.X; - scroll.Height = ParticipantsList.TILE_SIZE + OsuScrollContainer.SCROLL_BAR_HEIGHT + OsuScrollContainer.SCROLL_BAR_PADDING * 2; + scroll.Height = ParticipantsList.TILE_SIZE + OsuScrollContainer.SCROLL_BAR_WIDTH + OsuScrollContainer.SCROLL_BAR_PADDING * 2; list.RelativeSizeAxes = Axes.Y; list.AutoSizeAxes = Axes.X; diff --git a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs index 772c8c4278..813e243449 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Cursor; diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs index 395a77b9e6..0ba7f20f1c 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Game.Online; using osu.Game.Online.API; @@ -12,9 +10,9 @@ namespace osu.Game.Screens.OnlinePlay.Components public abstract partial class RoomPollingComponent : PollingComponent { [Resolved] - protected IAPIProvider API { get; private set; } + protected IAPIProvider API { get; private set; } = null!; [Resolved] - protected IRoomManager RoomManager { get; private set; } + protected IRoomManager RoomManager { get; private set; } = null!; } } diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 3fab0fc180..8f405399a7 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -456,6 +456,7 @@ namespace osu.Game.Screens.OnlinePlay private IEnumerable createButtons() => new[] { + beatmap == null ? Empty() : new PlaylistDownloadButton(beatmap), showResultsButton = new GrayButton(FontAwesome.Solid.ChartPie) { Size = new Vector2(30, 30), @@ -463,7 +464,6 @@ namespace osu.Game.Screens.OnlinePlay Alpha = AllowShowingResults ? 1 : 0, TooltipText = "View results" }, - beatmap == null ? Empty() : new PlaylistDownloadButton(beatmap), editButton = new PlaylistEditButton { Size = new Vector2(30, 30), @@ -500,7 +500,11 @@ namespace osu.Game.Screens.OnlinePlay { if (beatmaps.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID) is BeatmapInfo local && !local.BeatmapSet.AsNonNull().DeletePending) { - var collectionItems = realm.Realm.All().AsEnumerable().Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmap)).Cast().ToList(); + var collectionItems = realm.Realm.All() + .OrderBy(c => c.Name) + .AsEnumerable() + .Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmap)).Cast().ToList(); + if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 98f3df525d..dd6536cf26 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -1,17 +1,21 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Mods; -using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Select; using osuTK; @@ -19,28 +23,71 @@ namespace osu.Game.Screens.OnlinePlay { public partial class FooterButtonFreeMods : FooterButton, IHasCurrentValue> { + private readonly BindableWithCurrent> current = new BindableWithCurrent>(Array.Empty()); + public Bindable> Current { - get => modDisplay.Current; - set => modDisplay.Current = value; - } - - private readonly ModDisplay modDisplay; - - public FooterButtonFreeMods() - { - ButtonContentContainer.Add(modDisplay = new ModDisplay + get => current.Current; + set { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(0.8f), - ExpansionMode = ExpansionMode.AlwaysContracted, - }); + ArgumentNullException.ThrowIfNull(value); + + current.Current = value; + } } + private OsuSpriteText count = null!; + + private Circle circle = null!; + + private readonly FreeModSelectOverlay freeModSelectOverlay; + + public FooterButtonFreeMods(FreeModSelectOverlay freeModSelectOverlay) + { + this.freeModSelectOverlay = freeModSelectOverlay; + } + + [Resolved] + private OsuColour colours { get; set; } = null!; + [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { + ButtonContentContainer.AddRange(new[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + circle = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colours.YellowDark, + RelativeSizeAxes = Axes.Both, + }, + count = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding(5), + UseFullGlyphHeight = false, + } + } + }, + new IconButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.8f), + Icon = FontAwesome.Solid.Bars, + Action = () => freeModSelectOverlay.ToggleVisibility() + } + }); + SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); Text = @"freemods"; @@ -51,14 +98,49 @@ namespace osu.Game.Screens.OnlinePlay base.LoadComplete(); Current.BindValueChanged(_ => updateModDisplay(), true); + + // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. + Action = toggleAllFreeMods; + } + + /// + /// Immediately toggle all free mods on/off. + /// + private void toggleAllFreeMods() + { + var availableMods = allAvailableAndValidMods.ToArray(); + + Current.Value = Current.Value.Count == availableMods.Length + ? Array.Empty() + : availableMods; } private void updateModDisplay() { - if (Current.Value?.Count > 0) - modDisplay.FadeIn(); + int currentCount = Current.Value.Count; + + if (currentCount == allAvailableAndValidMods.Count()) + { + count.Text = "all"; + count.FadeColour(colours.Gray2, 200, Easing.OutQuint); + circle.FadeColour(colours.Yellow, 200, Easing.OutQuint); + } + else if (currentCount > 0) + { + count.Text = $"{currentCount} mods"; + count.FadeColour(colours.Gray2, 200, Easing.OutQuint); + circle.FadeColour(colours.YellowDark, 200, Easing.OutQuint); + } else - modDisplay.FadeOut(); + { + count.Text = "off"; + count.FadeColour(colours.GrayF, 200, Easing.OutQuint); + circle.FadeColour(colours.Gray4, 200, Easing.OutQuint); + } } + + private IEnumerable allAvailableAndValidMods => freeModSelectOverlay.AllAvailableMods + .Where(state => state.ValidForSelection.Value) + .Select(state => state.Mod); } } diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 6313d907a5..7f090aca57 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.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 osu.Game.Overlays; using System.Collections.Generic; @@ -16,7 +14,7 @@ namespace osu.Game.Screens.OnlinePlay { public partial class FreeModSelectOverlay : ModSelectOverlay { - protected override bool ShowTotalMultiplier => false; + protected override bool ShowModEffects => false; protected override bool AllowCustomisation => false; @@ -34,11 +32,12 @@ namespace osu.Game.Screens.OnlinePlay protected override ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, true); - protected override IEnumerable CreateFooterButtons() => base.CreateFooterButtons().Prepend( - new SelectAllModsButton(this) - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - }); + protected override IEnumerable CreateFooterButtons() + => base.CreateFooterButtons() + .Prepend(SelectAllModsButton = new SelectAllModsButton(this) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }); } } diff --git a/osu.Game/Screens/OnlinePlay/IOnlinePlaySubScreen.cs b/osu.Game/Screens/OnlinePlay/IOnlinePlaySubScreen.cs index f32ead5a11..c528e3952e 100644 --- a/osu.Game/Screens/OnlinePlay/IOnlinePlaySubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/IOnlinePlaySubScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Screens.OnlinePlay { public interface IOnlinePlaySubScreen : IOsuScreen diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 8c85a8235c..ef06d21655 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -103,118 +103,129 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components CornerRadius = CORNER_RADIUS, Children = new Drawable[] { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Background5, + Width = 0.2f, + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f)), + Width = 0.8f, + }, new GridContainer { RelativeSizeAxes = Axes.Both, ColumnDimensions = new[] { - new Dimension(GridSizeMode.Relative, 0.2f) + new Dimension(), + new Dimension(GridSizeMode.AutoSize), }, Content = new[] { new Drawable[] { - new Box + new Container { + Name = @"Left details", RelativeSizeAxes = Axes.Both, - Colour = colours.Background5, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f)) - }, - } - } - }, - new Container - { - Name = @"Left details", - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Left = 20, - Vertical = 5 - }, - Children = new Drawable[] - { - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new FillFlowContainer + Padding = new MarginPadding { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - Children = new Drawable[] - { - new RoomStatusPill - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft - }, - specialCategoryPill = new RoomSpecialCategoryPill - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft - }, - endDateInfo = new EndDateInfo - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - } + Left = 20, + Right = DrawableRoomParticipantsList.SHEAR_WIDTH, + Vertical = 5 }, - new FillFlowContainer + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Top = 3 }, - Direction = FillDirection.Vertical, - Children = new Drawable[] + new FillFlowContainer { - new RoomNameText(), - new RoomStatusText() + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new RoomStatusPill + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + specialCategoryPill = new RoomSpecialCategoryPill + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + endDateInfo = new EndDateInfo + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + } + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Top = 3 }, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new TruncatingSpriteText + { + RelativeSizeAxes = Axes.X, + Font = OsuFont.GetFont(size: 28), + Current = { BindTarget = Room.Name } + }, + new RoomStatusText() + } + } + }, + }, + new FillFlowContainer + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + ChildrenEnumerable = CreateBottomDetails() + } + } + }, + new FillFlowContainer + { + Name = "Right content", + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Spacing = new Vector2(5), + Padding = new MarginPadding + { + Right = 10, + Vertical = 20, + }, + Children = new Drawable[] + { + ButtonsContainer, + drawableRoomParticipantsList = new DrawableRoomParticipantsList + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + NumberOfCircles = NumberOfAvatars } } }, - }, - new FillFlowContainer - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - ChildrenEnumerable = CreateBottomDetails() - } - } - }, - new FillFlowContainer - { - Name = "Right content", - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Spacing = new Vector2(5), - Padding = new MarginPadding - { - Right = 10, - Vertical = 20, - }, - Children = new Drawable[] - { - ButtonsContainer, - drawableRoomParticipantsList = new DrawableRoomParticipantsList - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - NumberOfCircles = NumberOfAvatars } } }, @@ -311,23 +322,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components return pills; } - private partial class RoomNameText : OsuSpriteText - { - [Resolved(typeof(Room), nameof(Online.Rooms.Room.Name))] - private Bindable name { get; set; } - - public RoomNameText() - { - Font = OsuFont.GetFont(size: 28); - } - - [BackgroundDependencyLoader] - private void load() - { - Current = name; - } - } - private partial class RoomStatusText : OnlinePlayComposite { [Resolved] @@ -343,7 +337,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Width = 0.5f; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index c31633eefc..06f9f35479 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -24,8 +24,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public partial class DrawableRoomParticipantsList : OnlinePlayComposite { + public const float SHEAR_WIDTH = 12f; + private const float avatar_size = 36; + private const float height = 60f; + + private static readonly Vector2 shear = new Vector2(SHEAR_WIDTH / height, 0); + private FillFlowContainer avatarFlow; private CircularAvatar hostAvatar; @@ -36,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public DrawableRoomParticipantsList() { AutoSizeAxes = Axes.X; - Height = 60; + Height = height; } [BackgroundDependencyLoader] @@ -49,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.Both, Masking = true, CornerRadius = 10, - Shear = new Vector2(0.2f, 0), + Shear = shear, Child = new Box { RelativeSizeAxes = Axes.Both, @@ -98,7 +104,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.Both, Masking = true, CornerRadius = 10, - Shear = new Vector2(0.2f, 0), + Shear = shear, Child = new Box { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs index c25dd6f158..844991095e 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/EndDateInfo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/MatchTypePill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/MatchTypePill.cs index 35e0482f2b..e30d673b26 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/MatchTypePill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/MatchTypePill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Game.Online.Rooms; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/PillContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/PillContainer.cs index 263261143d..b473ea82c6 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/PillContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/PillContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs index d1365c02f3..fe5ccb4f09 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 Humanizer; using osu.Framework.Extensions.LocalisationExtensions; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/QueueModePill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/QueueModePill.cs index 208c11c155..23f4ecf8db 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/QueueModePill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/QueueModePill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Game.Online.Multiplayer; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomSpecialCategoryPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomSpecialCategoryPill.cs index 10f6e59260..9b8954bb33 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomSpecialCategoryPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomSpecialCategoryPill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics.Sprites; @@ -14,7 +12,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public partial class RoomSpecialCategoryPill : OnlinePlayPill { [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; protected override FontUsage Font => base.Font.With(weight: FontWeight.SemiBold); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs index 463b883f11..53fbf670e1 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.ComponentModel; namespace osu.Game.Screens.OnlinePlay.Lounge.Components diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs index ca9917ad00..aae82b6721 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; @@ -19,7 +17,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public partial class RoomStatusPill : OnlinePlayPill { [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; protected override FontUsage Font => base.Font.With(weight: FontWeight.SemiBold); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 70e4b2a589..66bbf92e58 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -170,7 +170,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (Room.HasPassword.Value) { - sampleJoin?.Play(); this.ShowPopover(); return true; } @@ -240,7 +239,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge } }; - sampleJoinFail = audio.Samples.Get(@"UI/password-fail"); + sampleJoinFail = audio.Samples.Get(@"UI/generic-error"); joinButton.Action = performJoin; } diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/CreateRoomButton.cs b/osu.Game/Screens/OnlinePlay/Match/Components/CreateRoomButton.cs index 0251dba6ce..3788f4c0b2 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/CreateRoomButton.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/CreateRoomButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Input; using osu.Framework.Input.Bindings; diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs index 55d39407b0..8dc1704fcd 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.Chat; @@ -14,8 +12,8 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components { private readonly IBindable channelId = new Bindable(); - [Resolved(CanBeNull = true)] - private ChannelManager channelManager { get; set; } + [Resolved] + private ChannelManager? channelManager { get; set; } private readonly Room room; private readonly bool leaveChannelOnDispose; diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs index 4d4fe4ea56..916b799d50 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs @@ -54,14 +54,12 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components protected override void PopIn() { - base.PopIn(); Settings.MoveToY(0, TRANSITION_DURATION, Easing.OutQuint); Settings.FadeIn(TRANSITION_DURATION / 2); } protected override void PopOut() { - base.PopOut(); Settings.MoveToY(-1, TRANSITION_DURATION, Easing.InSine); Settings.Delay(TRANSITION_DURATION / 2).FadeOut(TRANSITION_DURATION / 2); } @@ -115,7 +113,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components protected partial class Section : Container { - private readonly Container content; + private readonly ReverseChildIDFillFlowContainer content; protected override Container Content => content; @@ -137,10 +135,11 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12), Text = title.ToUpperInvariant(), }, - content = new Container + content = new ReverseChildIDFillFlowContainer { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical }, }, }; diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomBackgroundScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomBackgroundScreen.cs index c9e51d376c..53a52a8cb8 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomBackgroundScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomBackgroundScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; @@ -11,9 +9,9 @@ namespace osu.Game.Screens.OnlinePlay.Match { public partial class RoomBackgroundScreen : OnlinePlayBackgroundScreen { - public readonly Bindable SelectedItem = new Bindable(); + public readonly Bindable SelectedItem = new Bindable(); - public RoomBackgroundScreen(PlaylistItem initialPlaylistItem) + public RoomBackgroundScreen(PlaylistItem? initialPlaylistItem) { PlaylistItem = initialPlaylistItem; SelectedItem.BindValueChanged(item => PlaylistItem = item.NewValue); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 6b68024393..2cd8e45d28 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -23,6 +23,7 @@ using osu.Framework.Screens; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Input.Bindings; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -40,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Match [Cached(typeof(IBindable))] public readonly Bindable SelectedItem = new Bindable(); - public override bool? AllowTrackAdjustments => true; + public override bool? ApplyModTrackAdjustments => true; protected override BackgroundScreen CreateBackground() => new RoomBackgroundScreen(Room.Playlist.FirstOrDefault()) { @@ -76,6 +77,9 @@ namespace osu.Game.Screens.OnlinePlay.Match [Resolved] private RulesetStore rulesets { get; set; } + [Resolved] + private IAPIProvider api { get; set; } = null!; + [Resolved(canBeNull: true)] protected OnlinePlayScreen ParentScreen { get; private set; } @@ -284,6 +288,8 @@ namespace osu.Game.Screens.OnlinePlay.Match [Resolved(canBeNull: true)] private IDialogOverlay dialogOverlay { get; set; } + protected virtual bool IsConnected => api.State.Value == APIState.Online; + public override bool OnBackButton() { if (Room.RoomID.Value == null) @@ -356,12 +362,20 @@ namespace osu.Game.Screens.OnlinePlay.Match if (ExitConfirmed) return true; - if (dialogOverlay == null || Room.RoomID.Value != null || Room.Playlist.Count == 0) + if (!IsConnected) + return true; + + bool hasUnsavedChanges = Room.RoomID.Value == null && Room.Playlist.Count > 0; + + if (dialogOverlay == null || !hasUnsavedChanges) return true; // if the dialog is already displayed, block exiting until the user explicitly makes a decision. - if (dialogOverlay.CurrentDialog is ConfirmDiscardChangesDialog) + if (dialogOverlay.CurrentDialog is ConfirmDiscardChangesDialog discardChangesDialog) + { + discardChangesDialog.Flash(); return false; + } dialogOverlay.Push(new ConfirmDiscardChangesDialog(() => { @@ -441,7 +455,7 @@ namespace osu.Game.Screens.OnlinePlay.Match // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID); - Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); + UserModsSelectOverlay.Beatmap = Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); } protected virtual void UpdateMods() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayMatchScoreDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayMatchScoreDisplay.cs index 8c08390c73..b4373d728f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayMatchScoreDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayMatchScoreDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Screens.Play.HUD; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs index 6dc343f00a..e1543eaceb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; using osu.Game.Online.Multiplayer; using osuTK; @@ -56,7 +57,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match base.Action = this.ShowPopover; - TooltipText = "Countdown settings"; + TooltipText = MultiplayerMatchStrings.CountdownSettings; } [BackgroundDependencyLoader] @@ -112,7 +113,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match flow.Add(new RoundedButton { RelativeSizeAxes = Axes.X, - Text = $"Start match in {duration.Humanize()}", + Text = MultiplayerMatchStrings.StartMatchWithCountdown(duration.Humanize()), BackgroundColour = colours.Green, Action = () => { @@ -127,7 +128,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match flow.Add(new RoundedButton { RelativeSizeAxes = Axes.X, - Text = "Stop countdown", + Text = MultiplayerMatchStrings.StopCountdown, BackgroundColour = colours.Red, Action = () => { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs index a19f61787b..d18bb011f0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; using osu.Framework.Graphics; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs index a5589c48b9..e5d94c5358 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.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 osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 164d1c9a4b..edf5ce276a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -1,13 +1,13 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; @@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer public partial class Multiplayer : OnlinePlayScreen { [Resolved] - private MultiplayerClient client { get; set; } + private MultiplayerClient client { get; set; } = null!; protected override void LoadComplete() { @@ -91,11 +91,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override LoungeSubScreen CreateLounge() => new MultiplayerLoungeSubScreen(); + public void Join(Room room, string? password) => Schedule(() => Lounge.Join(room, password)); + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - if (client != null) + if (client.IsNotNull()) client.RoomUpdated -= onRoomUpdated; } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index dd4f35cdd4..4478179726 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer // To work around this, temporarily remove the room and trigger an immediate listing poll. if (e.Last is MultiplayerMatchSubScreen match) { - RoomManager.RemoveRoom(match.Room); + RoomManager?.RemoveRoom(match.Room); ListingPollingComponent.PollImmediately(); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index a36c7e801e..7c12e6eab5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -49,6 +49,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient client { get; set; } + [Resolved(canBeNull: true)] + private OsuGame game { get; set; } + private AddItemButton addItemButton; public MultiplayerMatchSubScreen(Room room) @@ -72,6 +75,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer handleRoomLost(); } + protected override bool IsConnected => base.IsConnected && client.IsConnected.Value; + protected override Drawable CreateMainContent() => new Container { RelativeSizeAxes = Axes.Both, @@ -247,13 +252,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer public override bool OnExiting(ScreenExitEvent e) { - // the room may not be left immediately after a disconnection due to async flow, - // so checking the IsConnected status is also required. - if (client.Room == null || !client.IsConnected.Value) - { - // room has not been created yet; exit immediately. + // room has not been created yet or we're offline; exit immediately. + if (client.Room == null || !IsConnected) return base.OnExiting(e); - } if (!exitConfirmed && dialogOverlay != null) { @@ -264,7 +265,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () => { exitConfirmed = true; - this.Exit(); + if (this.IsCurrentScreen()) + this.Exit(); })); } @@ -310,16 +312,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.ChangeBeatmapAvailability(availability.NewValue).FireAndForget(); - if (availability.NewValue.State != DownloadState.LocallyAvailable) + switch (availability.NewValue.State) { - // while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap. - if (client.LocalUser?.State == MultiplayerUserState.Ready) - client.ChangeState(MultiplayerUserState.Idle); - } - else if (client.LocalUser?.State == MultiplayerUserState.Spectating - && (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing)) - { - onLoadRequested(); + case DownloadState.LocallyAvailable: + if (client.LocalUser?.State == MultiplayerUserState.Spectating + && (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing)) + { + onLoadRequested(); + } + + break; + + case DownloadState.Unknown: + // Don't do anything rash in an unknown state. + break; + + default: + // while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap. + if (client.LocalUser?.State == MultiplayerUserState.Ready) + client.ChangeState(MultiplayerUserState.Idle); + break; } } @@ -334,11 +346,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer updateCurrentItem(); - addItemButton.Alpha = client.IsHost || Room.QueueMode.Value != QueueMode.HostOnly ? 1 : 0; + addItemButton.Alpha = localUserCanAddItem ? 1 : 0; Scheduler.AddOnce(UpdateMods); } + private bool localUserCanAddItem => client.IsHost || Room.QueueMode.Value != QueueMode.HostOnly; + private void updateCurrentItem() { Debug.Assert(client.Room != null); @@ -357,9 +371,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onLoadRequested() { - if (BeatmapAvailability.Value.State != DownloadState.LocallyAvailable) - return; - // In the case of spectating, IMultiplayerClient.LoadRequested can be fired while the game is still spectating a previous session. // For now, we want to game to switch to the new game so need to request exiting from the play screen. if (!ParentScreen.IsCurrentScreen()) @@ -377,6 +388,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client.LocalUser?.State == MultiplayerUserState.Spectating && (SelectedItem.Value == null || Beatmap.IsDefault)) return; + if (BeatmapAvailability.Value.State != DownloadState.LocallyAvailable) + return; + StartPlay(); } @@ -403,18 +417,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (!this.IsCurrentScreen()) return; - if (client.Room == null) + if (!localUserCanAddItem) return; - if (!client.IsHost) - { - // todo: should handle this when the request queue is implemented. - // if we decide that the presentation should exit the user from the multiplayer game, the PresentBeatmap - // flow may need to change to support an "unable to present" return value. - return; - } + // If there's only one playlist item and we are the host, assume we want to change it. Else add a new one. + PlaylistItem itemToEdit = client.IsHost && Room.Playlist.Count == 1 ? Room.Playlist.Single() : null; - this.Push(new MultiplayerMatchSongSelect(Room, Room.Playlist.Single(item => item.ID == client.Room.Settings.PlaylistItemId))); + OpenSongSelection(itemToEdit); + + // Re-run PresentBeatmap now that we've pushed a song select that can handle it. + game?.PresentBeatmap(beatmap.BeatmapSetInfo, b => b.ID == beatmap.BeatmapInfo.ID); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 7b448e4b5c..e6d9dd4cd0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -148,6 +148,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer loadingDisplay.Show(); client.ChangeState(MultiplayerUserState.ReadyForGameplay); } + + // This will pause the clock, pending the gameplay started callback from the server. + GameplayClockContainer.Reset(); } private void failAndBail(string message = null) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs index de19d3a0e9..f665ed2d41 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Online.Rooms; using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Playlists; @@ -12,7 +10,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer public partial class MultiplayerResultsScreen : PlaylistsResultsScreen { public MultiplayerResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem) - : base(score, roomId, playlistItem, false, false) + : base(score, roomId, playlistItem, false) { } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsListHeader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsListHeader.cs index 7f4e3360e4..79c6fb33cd 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsListHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsListHeader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Game.Online.Multiplayer; using osu.Game.Resources.Localisation.Web; @@ -13,7 +11,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants public partial class ParticipantsListHeader : OverlinedHeader { [Resolved] - private MultiplayerClient client { get; set; } + private MultiplayerClient client { get; set; } = null!; public ParticipantsListHeader() : base(RankingsStrings.SpotlightParticipants) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs index bfdc0c02ac..b0cc13d645 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs @@ -154,6 +154,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants this.FadeOut(fade_time); break; + case DownloadState.Unknown: + text.Text = "checking availability"; + icon.Icon = FontAwesome.Solid.Question; + icon.Colour = colours.Orange0; + break; + case DownloadState.NotDownloaded: text.Text = "no map"; icon.Icon = FontAwesome.Solid.MinusCircle; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs index 930bea4497..8526e11e12 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs @@ -49,7 +49,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void Update() { // The player clock's running state is controlled externally, but the local pausing state needs to be updated to start/stop gameplay. - if (GameplayClockContainer.SourceClock.IsRunning) + if (GameplayClockContainer.IsRunning) GameplayClockContainer.Start(); else GameplayClockContainer.Stop(); @@ -67,7 +67,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) { - var gameplayClockContainer = new GameplayClockContainer(spectatorPlayerClock); + // Importantly, we don't want to apply decoupling because SpectatorPlayerClock updates its IsRunning directly. + // If we applied decoupling, this state change wouldn't actually cause the clock to stop. + // TODO: Can we just use Start/Stop rather than this workaround, now that DecouplingClock is more sane? + var gameplayClockContainer = new GameplayClockContainer(spectatorPlayerClock, applyOffsets: false, requireDecoupling: false); clockAdjustmentsFromMods.BindAdjustments(gameplayClockContainer.AdjustmentsFromMods); return gameplayClockContainer; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs index eb55b0d18a..737f301f4d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayerLoader.cs @@ -1,10 +1,7 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Game.Scoring; using osu.Game.Screens.Menu; @@ -17,7 +14,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public partial class MultiSpectatorPlayerLoader : SpectatorPlayerLoader { - public MultiSpectatorPlayerLoader([NotNull] Score score, [NotNull] Func createPlayer) + public MultiSpectatorPlayerLoader(Score score, Func createPlayer) : base(score, createPlayer) { } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs index fe3f02466d..2afc187e40 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs @@ -18,6 +18,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { } + protected override void LoadComplete() + { + base.LoadComplete(); + + Scheduler.AddDelayed(() => StatisticsPanel.ToggleVisibility(), 1000); + } + protected override APIRequest FetchScores(Action> scoresCallback) => null; protected override APIRequest FetchNextPage(int direction, Action> scoresCallback) => null; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 2d2aa0f1d5..c1b1127542 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -2,12 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -29,7 +30,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public override bool DisallowExternalBeatmapRulesetChanges => true; // We are managing our own adjustments. For now, this happens inside the Player instances themselves. - public override bool? AllowTrackAdjustments => false; + public override bool? ApplyModTrackAdjustments => false; + + public override bool HideOverlaysOnEnter => true; /// /// Whether all spectating players have finished loading. @@ -196,15 +199,29 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private void performInitialSeek() { - // Seek the master clock to the gameplay time. - // This is chosen as the first available frame in the players' replays, which matches the seek by each individual SpectatorPlayer. - double startTime = instances.Where(i => i.Score != null) - .SelectMany(i => i.Score.AsNonNull().Replay.Frames) - .Select(f => f.Time) - .DefaultIfEmpty(0) - .Min(); + // We want to start showing gameplay as soon as possible. + // Each client may be in a different place in the beatmap, so we need to do our best to find a common + // starting point. + // + // Preferring a lower value ensures that we don't have some clients stuttering to keep up. + List minFrameTimes = new List(); + + foreach (var instance in instances) + { + if (instance.Score == null) + continue; + + minFrameTimes.Add(instance.Score.Replay.Frames.MinBy(f => f.Time)?.Time ?? 0); + } + + // Remove any outliers (only need to worry about removing those lower than the mean since we will take a Min() after). + double mean = minFrameTimes.Average(); + minFrameTimes.RemoveAll(t => mean - t > 1000); + + double startTime = minFrameTimes.Min(); masterClockContainer.Reset(startTime, true); + Logger.Log($"Multiplayer spectator seeking to initial time of {startTime}"); } protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState) @@ -212,7 +229,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) - => instances.Single(i => i.UserId == userId).LoadScore(spectatorGameplayState.Score); + { + var playerArea = instances.Single(i => i.UserId == userId); + + // The multiplayer spectator flow requires the client to return to a higher level screen + // (ie. StartGameplay should only be called once per player). + // + // Meanwhile, the solo spectator flow supports multiple `StartGameplay` calls. + // To ensure we don't crash out in an edge case where this is called more than once in multiplayer, + // guard against re-entry for the same player. + if (playerArea.Score != null) + return; + + playerArea.LoadScore(spectatorGameplayState.Score); + } protected override void QuitGameplay(int userId) { @@ -230,6 +260,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate return base.OnBackButton(); // On a manual exit, set the player back to idle unless gameplay has finished. + // Of note, this doesn't cover exiting using alt-f4 or menu home option. if (multiplayerClient.Room.State != MultiplayerRoomState.Open) multiplayerClient.ChangeState(MultiplayerUserState.Idle); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index dc4a2df9d8..1b03452df7 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -67,7 +67,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate SpectatorPlayerClock = clock; RelativeSizeAxes = Axes.Both; - Masking = true; AudioContainer audioContainer; InternalChildren = new Drawable[] diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs index 82d4cf5caf..6e71c010e5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs @@ -1,14 +1,13 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { @@ -17,20 +16,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public partial class PlayerGrid : CompositeDrawable { + public const float ANIMATION_DELAY = 400; + /// /// A temporary limitation on the number of players, because only layouts up to 16 players are supported for a single screen. /// Todo: Can be removed in the future with scrolling support + performance improvements. /// public const int MAX_PLAYERS = 16; - private const float player_spacing = 5; + private const float player_spacing = 6; /// /// The currently-maximised facade. /// - public Drawable MaximisedFacade => maximisedFacade; + public Facade MaximisedFacade { get; } - private readonly Facade maximisedFacade; private readonly Container paddingContainer; private readonly FillFlowContainer facadeContainer; private readonly Container cellContainer; @@ -50,12 +50,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate RelativeSizeAxes = Axes.Both, Child = facadeContainer = new FillFlowContainer { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(player_spacing), } }, - maximisedFacade = new Facade { RelativeSizeAxes = Axes.Both } + MaximisedFacade = new Facade + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.8f), + } } }, cellContainer = new Container { RelativeSizeAxes = Axes.Both } @@ -77,8 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate var facade = new Facade(); facadeContainer.Add(facade); - var cell = new Cell(index, content) { ToggleMaximisationState = toggleMaximisationState }; - cell.SetFacade(facade); + var cell = new Cell(index, content, facade) { ToggleMaximisationState = toggleMaximisationState }; cellContainer.Add(cell); } @@ -93,26 +98,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private void toggleMaximisationState(Cell target) { - // Iterate through all cells to ensure only one is maximised at any time. - foreach (var i in cellContainer.ToList()) - { - if (i == target) - i.IsMaximised = !i.IsMaximised; - else - i.IsMaximised = false; + // in the case the target is the already maximised cell (or there is only one cell), no cell should be maximised. + bool hasMaximised = !target.IsMaximised && cellContainer.Count > 1; - if (i.IsMaximised) + // Iterate through all cells to ensure only one is maximised at any time. + foreach (var cell in cellContainer.ToList()) + { + if (hasMaximised && cell == target) { // Transfer cell to the maximised facade. - i.SetFacade(maximisedFacade); - cellContainer.ChangeChildDepth(i, maximisedInstanceDepth -= 0.001f); + cell.SetFacade(MaximisedFacade, true); + cellContainer.ChangeChildDepth(cell, maximisedInstanceDepth -= 0.001f); } else { // Transfer cell back to its original facade. - i.SetFacade(facadeContainer[i.FacadeIndex]); + cell.SetFacade(facadeContainer[cell.FacadeIndex], false); } + + cell.FadeColour(hasMaximised && cell != target ? Color4.Gray : Color4.White, ANIMATION_DELAY, Easing.OutQuint); } + + facadeContainer.ScaleTo(hasMaximised ? 0.95f : 1, ANIMATION_DELAY, Easing.OutQuint); } protected override void Update() @@ -171,5 +178,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate foreach (var cell in facadeContainer) cell.Size = cellSize; } + + /// + /// A facade of the grid which is used as a dummy object to store the required position/size of cells. + /// + public partial class Facade : Drawable + { + public Facade() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs index 4a8b8f49e1..bc31299615 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.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 JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate @@ -32,68 +31,79 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// An action that toggles the maximisation state of this cell. /// - public Action ToggleMaximisationState; + public Action? ToggleMaximisationState; /// /// Whether this cell is currently maximised. /// - public bool IsMaximised; + public bool IsMaximised { get; private set; } private Facade facade; - private bool isTracking = true; - public Cell(int facadeIndex, Drawable content) + private bool isAnimating; + + public Cell(int facadeIndex, Drawable content, Facade facade) { FacadeIndex = facadeIndex; + this.facade = facade; Origin = Anchor.Centre; InternalChild = Content = content; + + Masking = true; + CornerRadius = 5; } protected override void Update() { base.Update(); - if (isTracking) - { - Position = getFinalPosition(); - Size = getFinalSize(); - } + var targetPos = getFinalPosition(); + var targetSize = getFinalSize(); + + double duration = isAnimating ? 60 : 0; + + Position = new Vector2( + (float)Interpolation.DampContinuously(Position.X, targetPos.X, duration, Time.Elapsed), + (float)Interpolation.DampContinuously(Position.Y, targetPos.Y, duration, Time.Elapsed) + ); + + Size = new Vector2( + (float)Interpolation.DampContinuously(Size.X, targetSize.X, duration, Time.Elapsed), + (float)Interpolation.DampContinuously(Size.Y, targetSize.Y, duration, Time.Elapsed) + ); + + // If we don't track the animating state, the animation will also occur when resizing the window. + isAnimating &= !Precision.AlmostEquals(Size, targetSize, 0.5f); } /// /// Makes this cell track a new facade. /// - public void SetFacade([NotNull] Facade newFacade) + public void SetFacade(Facade newFacade, bool isMaximised) { - Facade lastFacade = facade; facade = newFacade; + IsMaximised = isMaximised; + isAnimating = true; - if (lastFacade == null || lastFacade == newFacade) - return; - - isTracking = false; - - this.MoveTo(getFinalPosition(), 400, Easing.OutQuint).ResizeTo(getFinalSize(), 400, Easing.OutQuint) - .Then() - .OnComplete(_ => - { - if (facade == newFacade) - isTracking = true; - }); + TweenEdgeEffectTo(new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = isMaximised ? 30 : 10, + Colour = Colour4.Black.Opacity(isMaximised ? 0.5f : 0.2f), + }, ANIMATION_DELAY, Easing.OutQuint); } - private Vector2 getFinalPosition() - { - var topLeft = Parent.ToLocalSpace(facade.ToScreenSpace(Vector2.Zero)); - return topLeft + facade.DrawSize / 2; - } + private Vector2 getFinalPosition() => + Parent!.ToLocalSpace(facade.ScreenSpaceDrawQuad.Centre); - private Vector2 getFinalSize() => facade.DrawSize; + private Vector2 getFinalSize() => + Parent!.ToLocalSpace(facade.ScreenSpaceDrawQuad.BottomRight) + - Parent!.ToLocalSpace(facade.ScreenSpaceDrawQuad.TopLeft); protected override bool OnClick(ClickEvent e) { - ToggleMaximisationState(this); + ToggleMaximisationState?.Invoke(this); return true; } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs deleted file mode 100644 index 2f4ed35392..0000000000 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs +++ /dev/null @@ -1,24 +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 osu.Framework.Graphics; - -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate -{ - public partial class PlayerGrid - { - /// - /// A facade of the grid which is used as a dummy object to store the required position/size of cells. - /// - private partial class Facade : Drawable - { - public Facade() - { - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - } - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs index 45615d4e19..2ce78818a0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Logging; using osu.Framework.Timing; using osu.Game.Screens.Play; @@ -59,6 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public bool Seek(double position) { + Logger.Log($"{nameof(SpectatorPlayerClock)} seeked to {position}"); CurrentTime = position; return true; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs index 615c0d7c2b..fd61b60fe4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs @@ -182,7 +182,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate return; masterState = newState; - Logger.Log($"{nameof(SpectatorSyncManager)}'s master clock become {masterState}"); + Logger.Log($"{nameof(SpectatorSyncManager)}'s master clock became {masterState}"); switch (masterState) { diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 37b50b4863..f652e88f5a 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -26,14 +26,17 @@ namespace osu.Game.Screens.OnlinePlay [Cached] protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + public IScreen CurrentSubScreen => screenStack.CurrentScreen; + public override bool CursorVisible => (screenStack?.CurrentScreen as IOnlinePlaySubScreen)?.CursorVisible ?? true; // this is required due to PlayerLoader eventually being pushed to the main stack // while leases may be taken out by a subscreen. public override bool DisallowExternalBeatmapRulesetChanges => true; + protected LoungeSubScreen Lounge { get; private set; } + private MultiplayerWaveContainer waves; - private LoungeSubScreen loungeSubScreen; private ScreenStack screenStack; [Cached(Type = typeof(IRoomManager))] @@ -89,7 +92,7 @@ namespace osu.Game.Screens.OnlinePlay screenStack.ScreenPushed += screenPushed; screenStack.ScreenExited += screenExited; - screenStack.Push(loungeSubScreen = CreateLounge()); + screenStack.Push(Lounge = CreateLounge()); apiState.BindTo(API.State); apiState.BindValueChanged(onlineStateChanged, true); @@ -120,10 +123,10 @@ namespace osu.Game.Screens.OnlinePlay Mods.SetDefault(); - if (loungeSubScreen.IsCurrentScreen()) - loungeSubScreen.OnEntering(e); + if (Lounge.IsCurrentScreen()) + Lounge.OnEntering(e); else - loungeSubScreen.MakeCurrent(); + Lounge.MakeCurrent(); } public override void OnResuming(ScreenTransitionEvent e) @@ -224,8 +227,6 @@ namespace osu.Game.Screens.OnlinePlay ((IBindable)Activity).BindTo(newOsuScreen.Activity); } - public IScreen CurrentSubScreen => screenStack.CurrentScreen; - protected abstract string ScreenTitle { get; } protected virtual RoomManager CreateRoomManager() => new RoomManager(); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index e0ae437d49..622ffddba6 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -175,9 +175,12 @@ namespace osu.Game.Screens.OnlinePlay protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateFooterButtons() { - var buttons = base.CreateFooterButtons().ToList(); - buttons.Insert(buttons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (new FooterButtonFreeMods { Current = FreeMods }, freeModSelectOverlay)); - return buttons; + var baseButtons = base.CreateFooterButtons().ToList(); + var freeModsButton = new FooterButtonFreeMods(freeModSelectOverlay) { Current = FreeMods }; + + baseButtons.Insert(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (freeModsButton, freeModSelectOverlay)); + + return baseButtons; } /// diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs index c7b32131cf..b527bf98a2 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; @@ -15,8 +13,8 @@ namespace osu.Game.Screens.OnlinePlay public virtual string ShortTitle => Title; - [Resolved(CanBeNull = true)] - protected IRoomManager RoomManager { get; private set; } + [Resolved] + protected IRoomManager? RoomManager { get; private set; } protected OnlinePlaySubScreen() { diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs index 7ecb7d954e..6695c97508 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs @@ -1,20 +1,21 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Diagnostics; using osu.Framework.Screens; namespace osu.Game.Screens.OnlinePlay { public partial class OnlinePlaySubScreenStack : OsuScreenStack { - protected override void ScreenChanged(IScreen prev, IScreen next) + protected override void ScreenChanged(IScreen prev, IScreen? next) { base.ScreenChanged(prev, next); // because this is a screen stack within a screen stack, let's manually handle disabled changes to simplify things. - var osuScreen = ((OsuScreen)next); + var osuScreen = next as OsuScreen; + + Debug.Assert(osuScreen != null); bool disallowChanges = osuScreen.DisallowExternalBeatmapRulesetChanges; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs index 9507169e0f..d56ef9ef0c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Game.Screens.OnlinePlay.Match.Components; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs b/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs index f9324840dc..f1d2384c2f 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Screens.OnlinePlay.Lounge; namespace osu.Game.Screens.OnlinePlay.Playlists diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs index d40d43cd54..aa72394ac9 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs @@ -182,7 +182,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// An optional pivot around which the scores were retrieved. private void performSuccessCallback([NotNull] Action> callback, [NotNull] List scores, [CanBeNull] MultiplayerScores pivot = null) => Schedule(() => { - var scoreInfos = scoreManager.OrderByTotalScore(scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, Beatmap.Value.BeatmapInfo))).ToArray(); + var scoreInfos = scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); // Select a score if we don't already have one selected. // Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll). diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index e93f56c2e2..84e419d67a 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -23,6 +23,7 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Match.Components; using osuTK; +using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay.Playlists { @@ -80,6 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private IBindable localUser = null!; private readonly Room room; + private OsuSpriteText durationNoticeText = null!; public MatchSettings(Room room) { @@ -141,14 +143,22 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }, new Section("Duration") { - Child = new Container + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - Height = 40, - Child = DurationField = new DurationDropdown + new Container { - RelativeSizeAxes = Axes.X - } + RelativeSizeAxes = Axes.X, + Height = 40, + Child = DurationField = new DurationDropdown + { + RelativeSizeAxes = Axes.X + }, + }, + durationNoticeText = new OsuSpriteText + { + Alpha = 0, + Colour = colours.Yellow, + }, } }, new Section("Allowed attempts (across all playlist items)") @@ -305,6 +315,17 @@ namespace osu.Game.Screens.OnlinePlay.Playlists MaxAttempts.BindValueChanged(count => MaxAttemptsField.Text = count.NewValue?.ToString(), true); Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true); + DurationField.Current.BindValueChanged(duration => + { + if (hasValidDuration) + durationNoticeText.Hide(); + else + { + durationNoticeText.Show(); + durationNoticeText.Text = OnlinePlayStrings.SupporterOnlyDurationNotice; + } + }); + localUser = api.LocalUser.GetBoundCopy(); localUser.BindValueChanged(populateDurations, true); @@ -314,6 +335,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private void populateDurations(ValueChangedEvent user) { + // roughly correct (see https://github.com/Humanizr/Humanizer/blob/18167e56c082449cc4fe805b8429e3127a7b7f93/readme.md?plain=1#L427) + // if we want this to be more accurate we might consider sending an actual end time, not a time span. probably not required though. + const int days_in_month = 31; + DurationField.Items = new[] { TimeSpan.FromMinutes(30), @@ -326,18 +351,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists TimeSpan.FromDays(3), TimeSpan.FromDays(7), TimeSpan.FromDays(14), + TimeSpan.FromDays(days_in_month), + TimeSpan.FromDays(days_in_month * 3), }; - - // TODO: show these in the interface at all times. - if (user.NewValue.IsSupporter) - { - // roughly correct (see https://github.com/Humanizr/Humanizer/blob/18167e56c082449cc4fe805b8429e3127a7b7f93/readme.md?plain=1#L427) - // if we want this to be more accurate we might consider sending an actual end time, not a time span. probably not required though. - const int days_in_month = 31; - - DurationField.AddDropdownItem(TimeSpan.FromDays(days_in_month)); - DurationField.AddDropdownItem(TimeSpan.FromDays(days_in_month * 3)); - } } protected override void Update() @@ -352,7 +368,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private void onPlaylistChanged(object? sender, NotifyCollectionChangedEventArgs e) => playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}"; - private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0; + private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0 + && hasValidDuration; + + private bool hasValidDuration => DurationField.Current.Value <= TimeSpan.FromDays(14) || localUser.Value.IsSupporter; private void apply() { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsPlaylist.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsPlaylist.cs index 736f09584b..2ca1f4cd1f 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsPlaylist.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Extensions.IEnumerableExtensions; diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 9c098794a6..490a1ae6b8 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -85,17 +85,21 @@ namespace osu.Game.Screens [Resolved] private MusicController musicController { get; set; } - public virtual bool? AllowTrackAdjustments => null; + public virtual bool? ApplyModTrackAdjustments => null; - public Bindable Beatmap { get; private set; } + public virtual bool? AllowGlobalTrackControl => null; - public Bindable Ruleset { get; private set; } + public Bindable Beatmap { get; private set; } = null!; + + public Bindable Ruleset { get; private set; } = null!; public Bindable> Mods { get; private set; } private OsuScreenDependencies screenDependencies; - private bool? trackAdjustmentStateAtSuspend; + private bool? globalMusicControlStateAtSuspend; + + private bool? modTrackAdjustmentStateAtSuspend; internal void CreateLeasedDependencies(IReadOnlyDependencyContainer dependencies) => createDependencies(dependencies); @@ -178,8 +182,10 @@ namespace osu.Game.Screens // it's feasible to resume to a screen if the target screen never loaded successfully. // in such a case there's no need to restore this value. - if (trackAdjustmentStateAtSuspend != null) - musicController.AllowTrackAdjustments = trackAdjustmentStateAtSuspend.Value; + if (modTrackAdjustmentStateAtSuspend != null) + musicController.ApplyModTrackAdjustments = modTrackAdjustmentStateAtSuspend.Value; + if (globalMusicControlStateAtSuspend != null) + musicController.AllowTrackControl.Value = globalMusicControlStateAtSuspend.Value; base.OnResuming(e); } @@ -188,7 +194,8 @@ namespace osu.Game.Screens { base.OnSuspending(e); - trackAdjustmentStateAtSuspend = musicController.AllowTrackAdjustments; + modTrackAdjustmentStateAtSuspend = musicController.ApplyModTrackAdjustments; + globalMusicControlStateAtSuspend = musicController.AllowTrackControl.Value; onSuspendingLogo(); } @@ -197,8 +204,11 @@ namespace osu.Game.Screens { applyArrivingDefaults(false); - if (AllowTrackAdjustments != null) - musicController.AllowTrackAdjustments = AllowTrackAdjustments.Value; + if (ApplyModTrackAdjustments != null) + musicController.ApplyModTrackAdjustments = ApplyModTrackAdjustments.Value; + + if (AllowGlobalTrackControl != null) + musicController.AllowTrackControl.Value = AllowGlobalTrackControl.Value; if (backgroundStack?.Push(ownedBackground = CreateBackground()) != true) { diff --git a/osu.Game/Screens/OsuScreenStack.cs b/osu.Game/Screens/OsuScreenStack.cs index dffbbdbc55..7d1f6419ad 100644 --- a/osu.Game/Screens/OsuScreenStack.cs +++ b/osu.Game/Screens/OsuScreenStack.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; @@ -54,12 +52,12 @@ namespace osu.Game.Screens ScreenChanged(prev, next); } - protected virtual void ScreenChanged(IScreen prev, IScreen next) + protected virtual void ScreenChanged(IScreen prev, IScreen? next) { setParallax(next); } - private void setParallax(IScreen next) => - parallaxContainer.ParallaxAmount = ParallaxContainer.DEFAULT_PARALLAX_AMOUNT * (((IOsuScreen)next)?.BackgroundParallaxAmount ?? 1.0f); + private void setParallax(IScreen? next) => + parallaxContainer.ParallaxAmount = ParallaxContainer.DEFAULT_PARALLAX_AMOUNT * ((next as IOsuScreen)?.BackgroundParallaxAmount ?? 1.0f); } } diff --git a/osu.Game/Screens/Play/Break/BlurredIcon.cs b/osu.Game/Screens/Play/Break/BlurredIcon.cs index 6ce1c2e686..2bf59ea63b 100644 --- a/osu.Game/Screens/Play/Break/BlurredIcon.cs +++ b/osu.Game/Screens/Play/Break/BlurredIcon.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Screens/Play/Break/BreakArrows.cs b/osu.Game/Screens/Play/Break/BreakArrows.cs index f0f1e8cc3d..41277c7557 100644 --- a/osu.Game/Screens/Play/Break/BreakArrows.cs +++ b/osu.Game/Screens/Play/Break/BreakArrows.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Screens/Play/Break/BreakInfo.cs b/osu.Game/Screens/Play/Break/BreakInfo.cs index f99c1d1817..ef453405b5 100644 --- a/osu.Game/Screens/Play/Break/BreakInfo.cs +++ b/osu.Game/Screens/Play/Break/BreakInfo.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; diff --git a/osu.Game/Screens/Play/Break/BreakInfoLine.cs b/osu.Game/Screens/Play/Break/BreakInfoLine.cs index 7261155c94..df71767f82 100644 --- a/osu.Game/Screens/Play/Break/BreakInfoLine.cs +++ b/osu.Game/Screens/Play/Break/BreakInfoLine.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 osu.Framework.Allocation; using osu.Framework.Bindables; @@ -23,47 +21,43 @@ namespace osu.Game.Screens.Play.Break public Bindable Current = new Bindable(); - private readonly OsuSpriteText text; - private readonly OsuSpriteText valueText; + private readonly LocalisableString name; - private readonly string prefix; + private OsuSpriteText valueText = null!; - public BreakInfoLine(LocalisableString name, string prefix = @"") + public BreakInfoLine(LocalisableString name) { - this.prefix = prefix; + this.name = name; AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { Children = new Drawable[] { - text = new OsuSpriteText + new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.CentreRight, Text = name, Font = OsuFont.GetFont(size: 17), - Margin = new MarginPadding { Right = margin } + Margin = new MarginPadding { Right = margin }, + Colour = colours.Yellow, }, valueText = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.CentreLeft, - Text = prefix + @"-", + Text = @"-", Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 17), - Margin = new MarginPadding { Left = margin } + Margin = new MarginPadding { Left = margin }, + Colour = colours.YellowLight, } }; - Current.ValueChanged += currentValueChanged; - } - - private void currentValueChanged(ValueChangedEvent e) - { - string newText = prefix + Format(e.NewValue); - - if (valueText.Text == newText) - return; - - valueText.Text = newText; + Current.BindValueChanged(text => valueText.Text = Format(text.NewValue)); } protected virtual LocalisableString Format(T count) @@ -71,21 +65,14 @@ namespace osu.Game.Screens.Play.Break if (count is Enum countEnum) return countEnum.GetDescription(); - return count.ToString(); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - text.Colour = colours.Yellow; - valueText.Colour = colours.YellowLight; + return count.ToString() ?? string.Empty; } } public partial class PercentageBreakInfoLine : BreakInfoLine { - public PercentageBreakInfoLine(LocalisableString name, string prefix = "") - : base(name, prefix) + public PercentageBreakInfoLine(LocalisableString name) + : base(name) { } diff --git a/osu.Game/Screens/Play/Break/GlowIcon.cs b/osu.Game/Screens/Play/Break/GlowIcon.cs index 595c4dd494..8e2b9da0ad 100644 --- a/osu.Game/Screens/Play/Break/GlowIcon.cs +++ b/osu.Game/Screens/Play/Break/GlowIcon.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Screens/Play/Break/RemainingTimeCounter.cs b/osu.Game/Screens/Play/Break/RemainingTimeCounter.cs index da83f8c29f..3ac0a493da 100644 --- a/osu.Game/Screens/Play/Break/RemainingTimeCounter.cs +++ b/osu.Game/Screens/Play/Break/RemainingTimeCounter.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 osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 4927800059..3ca82ec00b 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -46,13 +46,14 @@ namespace osu.Game.Screens.Play private readonly Container remainingTimeBox; private readonly RemainingTimeCounter remainingTimeCounter; private readonly BreakArrows breakArrows; + private readonly ScoreProcessor scoreProcessor; + private readonly BreakInfo info; public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor) { + this.scoreProcessor = scoreProcessor; RelativeSizeAxes = Axes.Both; - BreakInfo info; - Child = fadeContainer = new Container { Alpha = 0, @@ -102,18 +103,18 @@ namespace osu.Game.Screens.Play } } }; - - if (scoreProcessor != null) - { - info.AccuracyDisplay.Current.BindTo(scoreProcessor.Accuracy); - info.GradeDisplay.Current.BindTo(scoreProcessor.Rank); - } } protected override void LoadComplete() { base.LoadComplete(); initializeBreaks(); + + if (scoreProcessor != null) + { + info.AccuracyDisplay.Current.BindTo(scoreProcessor.Accuracy); + info.GradeDisplay.Current.BindTo(scoreProcessor.Rank); + } } private void initializeBreaks() diff --git a/osu.Game/Screens/Play/ComboEffects.cs b/osu.Game/Screens/Play/ComboEffects.cs index 09c94a8f1d..6f12cfde64 100644 --- a/osu.Game/Screens/Play/ComboEffects.cs +++ b/osu.Game/Screens/Play/ComboEffects.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Play if (gameplayClock.CurrentTime < firstBreakTime) firstBreakTime = null; - if (gameplayClock.ElapsedFrameTime < 0) + if (gameplayClock.IsRewinding) return; if (combo.NewValue == 0 && (combo.OldValue > 20 || (alwaysPlayFirst.Value && firstBreakTime == null))) diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimationContainer.cs similarity index 97% rename from osu.Game/Screens/Play/FailAnimation.cs rename to osu.Game/Screens/Play/FailAnimationContainer.cs index 57bdad079e..821c67e3cb 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimationContainer.cs @@ -27,10 +27,10 @@ using osuTK.Graphics; namespace osu.Game.Screens.Play { /// - /// Manage the animation to be applied when a player fails. + /// Manage the animation to be applied when a player fails. Applies the animation to children. /// Single use and automatically disposed after use. /// - public partial class FailAnimation : Container + public partial class FailAnimationContainer : Container { public Action? OnComplete; @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Play /// public BackgroundScreen? Background { private get; set; } - public FailAnimation(DrawableRuleset drawableRuleset) + public FailAnimationContainer(DrawableRuleset drawableRuleset) { this.drawableRuleset = drawableRuleset; diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index c42f607908..5a713fdae7 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -19,15 +19,9 @@ namespace osu.Game.Screens.Play [Cached(typeof(IGameplayClock))] public partial class GameplayClockContainer : Container, IAdjustableClock, IGameplayClock { - /// - /// Whether gameplay is paused. - /// public IBindable IsPaused => isPaused; - /// - /// The source clock. Should generally not be used for any timekeeping purposes. - /// - public IClock SourceClock { get; private set; } + public bool IsRewinding => GameplayClock.IsRewinding; /// /// Invoked when a seek has been performed via @@ -61,15 +55,14 @@ namespace osu.Game.Screens.Play /// /// The source used for timing. /// Whether to apply platform, user and beatmap offsets to the mix. - public GameplayClockContainer(IClock sourceClock, bool applyOffsets = false) + /// Whether decoupling logic should be applied on the source clock. + public GameplayClockContainer(IClock sourceClock, bool applyOffsets, bool requireDecoupling) { - SourceClock = sourceClock; - RelativeSizeAxes = Axes.Both; InternalChildren = new Drawable[] { - GameplayClock = new FramedBeatmapClock(applyOffsets) { IsCoupled = false }, + GameplayClock = new FramedBeatmapClock(applyOffsets, requireDecoupling, sourceClock), Content }; } @@ -84,8 +77,6 @@ namespace osu.Game.Screens.Play isPaused.Value = false; - ensureSourceClockSet(); - PrepareStart(); // The case which caused this to be added is FrameStabilityContainer, which manages its own current and elapsed time. @@ -147,14 +138,16 @@ namespace osu.Game.Screens.Play /// Resets this and the source to an initial state ready for gameplay. /// /// The time to seek to on resetting. If null, the existing will be used. - /// Whether to start the clock immediately, if not already started. + /// Whether to start the clock immediately. If false and the clock was already paused, the clock will remain paused after this call. + /// public void Reset(double? time = null, bool startClock = false) { bool wasPaused = isPaused.Value; - Stop(); - - ensureSourceClockSet(); + // The intention of the Reset method is to get things into a known sane state. + // As such, we intentionally stop the underlying clock directly here, bypassing Stop/StopGameplayClock. + // This is to avoid any kind of isPaused state checks and frequency ramping (as provided by MasterGameplayClockContainer). + GameplayClock.Stop(); if (time != null) StartTime = time.Value; @@ -169,20 +162,7 @@ namespace osu.Game.Screens.Play /// Changes the source clock. /// /// The new source. - protected void ChangeSource(IClock sourceClock) => GameplayClock.ChangeSource(SourceClock = sourceClock); - - /// - /// Ensures that the is set to , if it hasn't been given a source yet. - /// This is usually done before a seek to avoid accidentally seeking only the adjustable source in decoupled mode, - /// but not the actual source clock. - /// That will pretty much only happen on the very first call of this method, as the source clock is passed in the constructor, - /// but it is not yet set on the adjustable source there. - /// - private void ensureSourceClockSet() - { - if (GameplayClock.Source == null) - ChangeSource(SourceClock); - } + protected void ChangeSource(IClock sourceClock) => GameplayClock.ChangeSource(sourceClock); #region IAdjustableClock @@ -220,7 +200,5 @@ namespace osu.Game.Screens.Play public double ElapsedFrameTime => GameplayClock.ElapsedFrameTime; public double FramesPerSecond => GameplayClock.FramesPerSecond; - - public FrameTimeInfo TimeInfo => GameplayClock.TimeInfo; } } diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs new file mode 100644 index 0000000000..793d43f7ef --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -0,0 +1,344 @@ +// 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.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Lines; +using osu.Framework.Layout; +using osu.Framework.Threading; +using osu.Framework.Utils; +using osu.Game.Configuration; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Skinning; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class ArgonHealthDisplay : HealthDisplay, ISerialisableDrawable + { + public bool UsesFixedAnchor { get; set; } + + [SettingSource("Bar height")] + public BindableFloat BarHeight { get; } = new BindableFloat(20) + { + MinValue = 0, + MaxValue = 64, + Precision = 1 + }; + + [SettingSource("Bar length")] + public BindableFloat BarLength { get; } = new BindableFloat(0.98f) + { + MinValue = 0.2f, + MaxValue = 1, + Precision = 0.01f, + }; + + private BarPath mainBar = null!; + + /// + /// Used to show a glow at the end of the main bar, or red "damage" area when missing. + /// + private BarPath glowBar = null!; + + private BackgroundPath background = null!; + + private SliderPath barPath = null!; + + private static readonly Colour4 main_bar_colour = Colour4.White; + private static readonly Colour4 main_bar_glow_colour = Color4Extensions.FromHex("#7ED7FD").Opacity(0.5f); + + private ScheduledDelegate? resetMissBarDelegate; + + private readonly List missBarVertices = new List(); + private readonly List healthBarVertices = new List(); + + private double glowBarValue; + + public double GlowBarValue + { + get => glowBarValue; + set + { + if (glowBarValue == value) + return; + + glowBarValue = value; + Scheduler.AddOnce(updatePathVertices); + } + } + + private double healthBarValue; + + public double HealthBarValue + { + get => healthBarValue; + set + { + if (healthBarValue == value) + return; + + healthBarValue = value; + Scheduler.AddOnce(updatePathVertices); + } + } + + private const float main_path_radius = 10f; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new Container + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + background = new BackgroundPath + { + PathRadius = main_path_radius, + }, + glowBar = new BarPath + { + BarColour = Color4.White, + GlowColour = main_bar_glow_colour, + Blending = BlendingParameters.Additive, + Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.8f), Color4.White), + PathRadius = 40f, + // Kinda hacky, but results in correct positioning with increased path radius. + Margin = new MarginPadding(-30f), + GlowPortion = 0.9f, + }, + mainBar = new BarPath + { + AutoSizeAxes = Axes.None, + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + BarColour = main_bar_colour, + GlowColour = main_bar_glow_colour, + PathRadius = main_path_radius, + GlowPortion = 0.6f, + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(_ => Scheduler.AddOnce(updateCurrent), true); + + BarLength.BindValueChanged(l => Width = l.NewValue, true); + BarHeight.BindValueChanged(_ => updatePath()); + updatePath(); + } + + protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) + { + if ((invalidation & Invalidation.DrawSize) > 0) + updatePath(); + + return base.OnInvalidate(invalidation, source); + } + + private void updateCurrent() + { + if (Current.Value >= GlowBarValue) finishMissDisplay(); + + double time = Current.Value > GlowBarValue ? 500 : 250; + + // TODO: this should probably use interpolation in update. + this.TransformTo(nameof(HealthBarValue), Current.Value, time, Easing.OutQuint); + if (resetMissBarDelegate == null) this.TransformTo(nameof(GlowBarValue), Current.Value, time, Easing.OutQuint); + } + + protected override void Update() + { + base.Update(); + + mainBar.Alpha = (float)Interpolation.DampContinuously(mainBar.Alpha, Current.Value > 0 ? 1 : 0, 40, Time.Elapsed); + glowBar.Alpha = (float)Interpolation.DampContinuously(glowBar.Alpha, GlowBarValue > 0 ? 1 : 0, 40, Time.Elapsed); + } + + protected override void Flash() + { + base.Flash(); + + mainBar.TransformTo(nameof(BarPath.GlowColour), main_bar_glow_colour.Opacity(0.8f)) + .TransformTo(nameof(BarPath.GlowColour), main_bar_glow_colour, 300, Easing.OutQuint); + + if (resetMissBarDelegate == null) + { + glowBar.TransformTo(nameof(BarPath.BarColour), Colour4.White, 30, Easing.OutQuint) + .Then() + .TransformTo(nameof(BarPath.BarColour), main_bar_colour, 1000, Easing.OutQuint); + + glowBar.TransformTo(nameof(BarPath.GlowColour), Colour4.White, 30, Easing.OutQuint) + .Then() + .TransformTo(nameof(BarPath.GlowColour), main_bar_glow_colour, 300, Easing.OutQuint); + } + } + + protected override void Miss() + { + base.Miss(); + + if (resetMissBarDelegate != null) + { + resetMissBarDelegate.Cancel(); + resetMissBarDelegate = null; + } + else + { + // Reset any ongoing animation immediately, else things get weird. + this.TransformTo(nameof(GlowBarValue), HealthBarValue); + } + + this.Delay(500).Schedule(() => + { + this.TransformTo(nameof(GlowBarValue), Current.Value, 300, Easing.OutQuint); + finishMissDisplay(); + }, out resetMissBarDelegate); + + glowBar.TransformTo(nameof(BarPath.BarColour), new Colour4(255, 147, 147, 255), 100, Easing.OutQuint).Then() + .TransformTo(nameof(BarPath.BarColour), new Colour4(255, 93, 93, 255), 800, Easing.OutQuint); + + glowBar.TransformTo(nameof(BarPath.GlowColour), new Colour4(253, 0, 0, 255).Lighten(0.2f)) + .TransformTo(nameof(BarPath.GlowColour), new Colour4(253, 0, 0, 255), 800, Easing.OutQuint); + } + + private void finishMissDisplay() + { + if (resetMissBarDelegate == null) + return; + + if (Current.Value > 0) + { + glowBar.TransformTo(nameof(BarPath.BarColour), main_bar_colour, 300, Easing.In); + glowBar.TransformTo(nameof(BarPath.GlowColour), main_bar_glow_colour, 300, Easing.In); + } + + resetMissBarDelegate?.Cancel(); + resetMissBarDelegate = null; + } + + private void updatePath() + { + float barLength = DrawWidth - main_path_radius * 2; + float curveStart = barLength - 70; + float curveEnd = barLength - 40; + + const float curve_smoothness = 10; + + Vector2 diagonalDir = (new Vector2(curveEnd, BarHeight.Value) - new Vector2(curveStart, 0)).Normalized(); + + barPath = new SliderPath(new[] + { + new PathControlPoint(new Vector2(0, 0), PathType.Linear), + new PathControlPoint(new Vector2(curveStart - curve_smoothness, 0), PathType.Bezier), + new PathControlPoint(new Vector2(curveStart, 0)), + new PathControlPoint(new Vector2(curveStart, 0) + diagonalDir * curve_smoothness, PathType.Linear), + new PathControlPoint(new Vector2(curveEnd, BarHeight.Value) - diagonalDir * curve_smoothness, PathType.Bezier), + new PathControlPoint(new Vector2(curveEnd, BarHeight.Value)), + new PathControlPoint(new Vector2(curveEnd + curve_smoothness, BarHeight.Value), PathType.Linear), + new PathControlPoint(new Vector2(barLength, BarHeight.Value)), + }); + + List vertices = new List(); + barPath.GetPathToProgress(vertices, 0.0, 1.0); + + background.Vertices = vertices; + mainBar.Vertices = vertices; + glowBar.Vertices = vertices; + + updatePathVertices(); + } + + private void updatePathVertices() + { + barPath.GetPathToProgress(healthBarVertices, 0.0, healthBarValue); + barPath.GetPathToProgress(missBarVertices, healthBarValue, Math.Max(glowBarValue, healthBarValue)); + + if (healthBarVertices.Count == 0) + healthBarVertices.Add(Vector2.Zero); + + if (missBarVertices.Count == 0) + missBarVertices.Add(Vector2.Zero); + + glowBar.Vertices = missBarVertices.Select(v => v - missBarVertices[0]).ToList(); + glowBar.Position = missBarVertices[0]; + + mainBar.Vertices = healthBarVertices.Select(v => v - healthBarVertices[0]).ToList(); + mainBar.Position = healthBarVertices[0]; + } + + private partial class BackgroundPath : SmoothPath + { + protected override Color4 ColourAt(float position) + { + if (position <= 0.16f) + return Color4.White.Opacity(0.8f); + + return Interpolation.ValueAt(position, + Color4.White.Opacity(0.8f), + Color4.Black.Opacity(0.2f), + -0.5f, 1f, Easing.OutQuint); + } + } + + private partial class BarPath : SmoothPath + { + private Colour4 barColour; + + public Colour4 BarColour + { + get => barColour; + set + { + if (barColour == value) + return; + + barColour = value; + InvalidateTexture(); + } + } + + private Colour4 glowColour; + + public Colour4 GlowColour + { + get => glowColour; + set + { + if (glowColour == value) + return; + + glowColour = value; + InvalidateTexture(); + } + } + + public float GlowPortion { get; init; } + + protected override Color4 ColourAt(float position) + { + if (position >= GlowPortion) + return BarColour; + + return Interpolation.ValueAt(position, Colour4.Black.Opacity(0.0f), GlowColour, 0.0, GlowPortion, Easing.InQuint); + } + } + } +} diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs index 9dce8996c3..be2ce3b272 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs @@ -95,7 +95,6 @@ namespace osu.Game.Screens.Play.HUD private void updateGraphVisibility() { graph.FadeTo(ShowGraph.Value ? 1 : 0, 200, Easing.In); - bar.ShowBackground = !ShowGraph.Value; } protected override void Update() diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs index dd6e10ba5d..beaee0e9ee 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs @@ -14,7 +14,6 @@ using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Graphics; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { @@ -32,18 +31,8 @@ namespace osu.Game.Screens.Play.HUD private readonly Box background; - private readonly BindableBool showBackground = new BindableBool(); - private readonly ColourInfo mainColour; - private readonly ColourInfo mainColourDarkened; private ColourInfo catchUpColour; - private ColourInfo catchUpColourDarkened; - - public bool ShowBackground - { - get => showBackground.Value; - set => showBackground.Value = value; - } public double StartTime { @@ -95,7 +84,7 @@ namespace osu.Game.Screens.Play.HUD { RelativeSizeAxes = Axes.Both, Alpha = 0, - Colour = Colour4.White.Darken(1 + 1 / 4f) + Colour = OsuColour.Gray(0.2f), }, catchupBar = new RoundedBar { @@ -112,12 +101,10 @@ namespace osu.Game.Screens.Play.HUD Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, CornerRadius = 5, - AccentColour = mainColour = Color4.White, + AccentColour = mainColour = OsuColour.Gray(0.9f), RelativeSizeAxes = Axes.Both }, }; - - mainColourDarkened = Colour4.White.Darken(1 / 3f); } private void setupAlternateValue() @@ -141,16 +128,15 @@ namespace osu.Game.Screens.Play.HUD [BackgroundDependencyLoader] private void load(OsuColour colours) { - catchUpColour = colours.BlueLight; - catchUpColourDarkened = colours.BlueDark; - - showBackground.BindValueChanged(_ => updateBackground(), true); + catchUpColour = colours.BlueDark; } - private void updateBackground() + protected override void LoadComplete() { - background.FadeTo(showBackground.Value ? 1 / 4f : 0, 200, Easing.In); - playfieldBar.TransformTo(nameof(playfieldBar.AccentColour), ShowBackground ? mainColour : mainColourDarkened, 200, Easing.In); + base.LoadComplete(); + + background.FadeTo(0.3f, 200, Easing.In); + playfieldBar.TransformTo(nameof(playfieldBar.AccentColour), mainColour, 200, Easing.In); } protected override bool OnHover(HoverEvent e) @@ -190,8 +176,8 @@ namespace osu.Game.Screens.Play.HUD catchupBar.AccentColour = Interpolation.ValueAt( Math.Min(timeDelta, colour_transition_threshold), - ShowBackground ? mainColour : mainColourDarkened, - ShowBackground ? catchUpColour : catchUpColourDarkened, + mainColour, + catchUpColour, 0, colour_transition_threshold, Easing.OutQuint); diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs index 63ab9d15e0..be570c1578 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs @@ -4,8 +4,10 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Rulesets.Objects; using osu.Game.Graphics.UserInterface; @@ -13,6 +15,10 @@ namespace osu.Game.Screens.Play.HUD { public partial class ArgonSongProgressGraph : SegmentedGraph { + private const int tier_count = 5; + + private const int display_granularity = 200; + private IEnumerable? objects; public IEnumerable Objects @@ -21,8 +27,7 @@ namespace osu.Game.Screens.Play.HUD { objects = value; - const int granularity = 200; - int[] values = new int[granularity]; + int[] values = new int[display_granularity]; if (!objects.Any()) return; @@ -32,7 +37,7 @@ namespace osu.Game.Screens.Play.HUD if (lastHit == 0) lastHit = objects.Last().StartTime; - double interval = (lastHit - firstHit + 1) / granularity; + double interval = (lastHit - firstHit + 1) / display_granularity; foreach (var h in objects) { @@ -51,12 +56,12 @@ namespace osu.Game.Screens.Play.HUD } public ArgonSongProgressGraph() - : base(5) + : base(tier_count) { var colours = new List(); - for (int i = 0; i < 5; i++) - colours.Add(Colour4.White.Darken(1 + 1 / 5f).Opacity(1 / 5f)); + for (int i = 0; i < tier_count; i++) + colours.Add(OsuColour.Gray(0.2f).Opacity(0.1f)); TierColours = colours; } diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondController.cs similarity index 93% rename from osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs rename to osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondController.cs index ba0c47dc8b..f2dd20cc8e 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondController.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Play.HUD.ClicksPerSecond { - public partial class ClicksPerSecondCalculator : Component + public partial class ClicksPerSecondController : Component { private readonly List timestamps = new List(); @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Play.HUD.ClicksPerSecond private IGameplayClock clock => frameStableClock ?? gameplayClock; - public ClicksPerSecondCalculator() + public ClicksPerSecondController() { RelativeSizeAxes = Axes.Both; } diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs index 1aa7c5e091..9b5ea309b0 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Play.HUD.ClicksPerSecond public partial class ClicksPerSecondCounter : RollingCounter, ISerialisableDrawable { [Resolved] - private ClicksPerSecondCalculator calculator { get; set; } = null!; + private ClicksPerSecondController controller { get; set; } = null!; protected override double RollingDuration => 350; @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Play.HUD.ClicksPerSecond { base.Update(); - Current.Value = calculator.Value; + Current.Value = controller.Value; } protected override IHasText CreateText() => new TextComponent(); diff --git a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs index eb3c71afbb..4a0e8b4f39 100644 --- a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Game.Graphics; using osu.Game.Skinning; diff --git a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs index 2c43905a46..f531b1c609 100644 --- a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.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 osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -10,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Game.Graphics; -using osu.Game.Rulesets.Judgements; using osuTK; using osuTK.Graphics; using osu.Framework.Graphics.Shapes; @@ -114,6 +111,13 @@ namespace osu.Game.Screens.Play.HUD }; } + protected override void Flash() + { + fill.FadeEdgeEffectTo(Math.Min(1, fill.EdgeEffect.Colour.Linear.A + (1f - base_glow_opacity) / glow_max_hits), 50, Easing.OutQuint) + .Delay(glow_fade_delay) + .FadeEdgeEffectTo(base_glow_opacity, glow_fade_time, Easing.OutQuint); + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -121,15 +125,6 @@ namespace osu.Game.Screens.Play.HUD GlowColour = colours.BlueDarker; } - protected override void Flash(JudgementResult result) => Scheduler.AddOnce(flash); - - private void flash() - { - fill.FadeEdgeEffectTo(Math.Min(1, fill.EdgeEffect.Colour.Linear.A + (1f - base_glow_opacity) / glow_max_hits), 50, Easing.OutQuint) - .Delay(glow_fade_delay) - .FadeEdgeEffectTo(base_glow_opacity, glow_fade_time, Easing.OutQuint); - } - protected override void Update() { base.Update(); diff --git a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs index 7cc2dc1751..2a17559503 100644 --- a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index 67e7ae8f3f..3954e23cbe 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -29,6 +29,8 @@ namespace osu.Game.Screens.Play.HUD /// public readonly Bindable ShowHealth = new Bindable(); + protected override bool PlayInitialIncreaseAnimation => false; + private const float max_alpha = 0.4f; private const int fade_time = 400; private const float gradient_size = 0.2f; diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 4ac2f1afda..7471955493 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -11,6 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; using osu.Game.Rulesets.Scoring; using osu.Game.Users; using osu.Game.Users.Drawables; @@ -107,6 +109,8 @@ namespace osu.Game.Screens.Play.HUD private IBindable scoreDisplayMode = null!; + private bool isFriend; + /// /// Creates a new . /// @@ -124,7 +128,7 @@ namespace osu.Game.Screens.Play.HUD } [BackgroundDependencyLoader] - private void load(OsuColour colours, OsuConfigManager osuConfigManager) + private void load(OsuColour colours, OsuConfigManager osuConfigManager, IAPIProvider api) { Container avatarContainer; @@ -235,7 +239,7 @@ namespace osu.Game.Screens.Play.HUD } } }, - usernameText = new OsuSpriteText + usernameText = new TruncatingSpriteText { RelativeSizeAxes = Axes.X, Width = 0.6f, @@ -244,7 +248,6 @@ namespace osu.Game.Screens.Play.HUD Colour = Color4.White, Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), Text = User?.Username ?? string.Empty, - Truncate = true, Shadow = false, } } @@ -312,6 +315,8 @@ namespace osu.Game.Screens.Play.HUD }, true); HasQuit.BindValueChanged(_ => updateState()); + + isFriend = User != null && api.Friends.Any(u => User.OnlineID == u.Id); } protected override void LoadComplete() @@ -390,6 +395,11 @@ namespace osu.Game.Screens.Play.HUD panelColour = BackgroundColour ?? Color4Extensions.FromHex("ffd966"); textColour = TextColour ?? Color4Extensions.FromHex("2e576b"); } + else if (isFriend) + { + panelColour = BackgroundColour ?? Color4Extensions.FromHex("ff549a"); + textColour = TextColour ?? Color4.White; + } else { panelColour = BackgroundColour ?? Color4Extensions.FromHex("3399cc"); diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index 7a73eb1657..fdbce15b40 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -1,12 +1,13 @@ // 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 osu.Framework.Allocation; 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.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -22,46 +23,114 @@ namespace osu.Game.Screens.Play.HUD private readonly Bindable showHealthBar = new Bindable(true); [Resolved] - protected HealthProcessor HealthProcessor { get; private set; } + protected HealthProcessor HealthProcessor { get; private set; } = null!; - public Bindable Current { get; } = new BindableDouble(1) + protected virtual bool PlayInitialIncreaseAnimation => true; + + public Bindable Current { get; } = new BindableDouble { MinValue = 0, MaxValue = 1 }; - protected virtual void Flash(JudgementResult result) + private BindableNumber health = null!; + + private ScheduledDelegate? initialIncrease; + + /// + /// Triggered when a is a successful hit, signaling the health display to perform a flash animation (if designed to do so). + /// Calls to this method are debounced. + /// + protected virtual void Flash() { } - [Resolved(canBeNull: true)] - private HUDOverlay hudOverlay { get; set; } + /// + /// Triggered when a resulted in the player losing health. + /// Calls to this method are debounced. + /// + protected virtual void Miss() + { + } + + [Resolved] + private HUDOverlay? hudOverlay { get; set; } protected override void LoadComplete() { base.LoadComplete(); - Current.BindTo(HealthProcessor.Health); HealthProcessor.NewJudgement += onNewJudgement; + // Don't bind directly so we can animate the startup procedure. + health = HealthProcessor.Health.GetBoundCopy(); + health.BindValueChanged(h => + { + finishInitialAnimation(); + Current.Value = h.NewValue; + }); + if (hudOverlay != null) showHealthBar.BindTo(hudOverlay.ShowHealthBar); // this probably shouldn't be operating on `this.` showHealthBar.BindValueChanged(healthBar => this.FadeTo(healthBar.NewValue ? 1 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING), true); + + if (PlayInitialIncreaseAnimation) + startInitialAnimation(); + else + Current.Value = health.Value; + } + + private void startInitialAnimation() + { + if (Current.Value >= health.Value) + return; + + // TODO: this should run in gameplay time, including showing a larger increase when skipping. + // TODO: it should also start increasing relative to the first hitobject. + const double increase_delay = 150; + + initialIncrease = Scheduler.AddDelayed(() => + { + double newValue = Math.Min(Current.Value + 0.05f, health.Value); + this.TransformBindableTo(Current, newValue, increase_delay); + Scheduler.AddOnce(Flash); + + if (newValue >= health.Value) + finishInitialAnimation(); + }, increase_delay, true); + } + + private void finishInitialAnimation() + { + if (initialIncrease == null) + return; + + initialIncrease?.Cancel(); + initialIncrease = null; + + // aside from the repeating `initialIncrease` scheduled task, + // there may also be a `Current` transform in progress from that schedule. + // ensure it plays out fully, to prevent changes to `Current.Value` being discarded by the ongoing transform. + // and yes, this funky `targetMember` spec is seemingly the only way to do this + // (see: https://github.com/ppy/osu-framework/blob/fe2769171c6e26d1b6fdd6eb7ea8353162fe9065/osu.Framework/Graphics/Transforms/TransformBindable.cs#L21) + FinishTransforms(targetMember: $"{Current.GetHashCode()}.{nameof(Current.Value)}"); } private void onNewJudgement(JudgementResult judgement) { if (judgement.IsHit && judgement.Type != HitResult.IgnoreHit) - Flash(judgement); + Scheduler.AddOnce(Flash); + else if (judgement.Judgement.HealthIncreaseFor(judgement) < 0) + Scheduler.AddOnce(Miss); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - if (HealthProcessor != null) + if (HealthProcessor.IsNotNull()) HealthProcessor.NewJudgement -= onNewJudgement; } } diff --git a/osu.Game/Screens/Play/HUD/InputCountController.cs b/osu.Game/Screens/Play/HUD/InputCountController.cs new file mode 100644 index 0000000000..cfe17d8ce0 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/InputCountController.cs @@ -0,0 +1,33 @@ +// 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 osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; + +namespace osu.Game.Screens.Play.HUD +{ + /// + /// Keeps track of key press counts for a current play session, exposing bindable counts which can + /// be used for display purposes. + /// + public partial class InputCountController : Component + { + public readonly Bindable IsCounting = new BindableBool(true); + + private readonly BindableList triggers = new BindableList(); + + public IBindableList Triggers => triggers; + + public void AddRange(IEnumerable triggers) => triggers.ForEach(Add); + + public void Add(InputTrigger trigger) + { + // Note that these triggers are not added to the hierarchy here. It is presumed they are added externally at a + // more correct location (ie. inside a RulesetInputManager). + triggers.Add(trigger); + trigger.IsCounting.BindTo(IsCounting); + } + } +} diff --git a/osu.Game/Screens/Play/HUD/InputTrigger.cs b/osu.Game/Screens/Play/HUD/InputTrigger.cs index b57f2cdf91..edc61ec142 100644 --- a/osu.Game/Screens/Play/HUD/InputTrigger.cs +++ b/osu.Game/Screens/Play/HUD/InputTrigger.cs @@ -1,6 +1,7 @@ // 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.Bindables; using osu.Framework.Graphics; namespace osu.Game.Screens.Play.HUD @@ -25,13 +26,38 @@ namespace osu.Game.Screens.Play.HUD public event OnActivateCallback? OnActivate; public event OnDeactivateCallback? OnDeactivate; + private readonly Bindable activationCount = new BindableInt(); + private readonly Bindable isCounting = new BindableBool(true); + + /// + /// Number of times this has been activated. + /// + public IBindable ActivationCount => activationCount; + + /// + /// Whether any activation or deactivation of this impacts its + /// + public IBindable IsCounting => isCounting; + protected InputTrigger(string name) { Name = name; } - protected void Activate(bool forwardPlayback = true) => OnActivate?.Invoke(forwardPlayback); + protected void Activate(bool forwardPlayback = true) + { + if (forwardPlayback && isCounting.Value) + activationCount.Value++; - protected void Deactivate(bool forwardPlayback = true) => OnDeactivate?.Invoke(forwardPlayback); + OnActivate?.Invoke(forwardPlayback); + } + + protected void Deactivate(bool forwardPlayback = true) + { + if (!forwardPlayback && isCounting.Value) + activationCount.Value--; + + OnDeactivate?.Invoke(forwardPlayback); + } } } diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementTally.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs similarity index 95% rename from osu.Game/Screens/Play/HUD/JudgementCounter/JudgementTally.cs rename to osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs index e9e3fde92a..43c2ae442a 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementTally.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter /// Keeps track of judgements for a current play session, exposing bindable counts which can /// be used for display purposes. /// - public partial class JudgementTally : CompositeDrawable + public partial class JudgementCountController : Component { [Resolved] private ScoreProcessor scoreProcessor { get; set; } = null!; diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs index 7675d0cc4f..6c417faac2 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs @@ -18,9 +18,9 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter public BindableBool ShowName = new BindableBool(); public Bindable Direction = new Bindable(); - public readonly JudgementTally.JudgementCount Result; + public readonly JudgementCountController.JudgementCount Result; - public JudgementCounter(JudgementTally.JudgementCount result) => Result = result; + public JudgementCounter(JudgementCountController.JudgementCount result) => Result = result; public OsuSpriteText ResultName = null!; private FillFlowContainer flowContainer = null!; diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs index a9b59a02b5..128897ddde 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs @@ -22,19 +22,19 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter public bool UsesFixedAnchor { get; set; } [SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.JudgementDisplayMode))] - public Bindable Mode { get; set; } = new Bindable(); + public Bindable Mode { get; } = new Bindable(); [SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.FlowDirection))] - public Bindable FlowDirection { get; set; } = new Bindable(); + public Bindable FlowDirection { get; } = new Bindable(); [SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.ShowJudgementNames))] - public BindableBool ShowJudgementNames { get; set; } = new BindableBool(true); + public BindableBool ShowJudgementNames { get; } = new BindableBool(true); [SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.ShowMaxJudgement))] - public BindableBool ShowMaxJudgement { get; set; } = new BindableBool(true); + public BindableBool ShowMaxJudgement { get; } = new BindableBool(true); [Resolved] - private JudgementTally tally { get; set; } = null!; + private JudgementCountController judgementCountController { get; set; } = null!; protected FillFlowContainer CounterFlow = null!; @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter AutoSizeAxes = Axes.Both }; - foreach (var result in tally.Results) + foreach (var result in judgementCountController.Results) CounterFlow.Add(createCounter(result)); } @@ -123,7 +123,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter } } - private JudgementCounter createCounter(JudgementTally.JudgementCount info) => + private JudgementCounter createCounter(JudgementCountController.JudgementCount info) => new JudgementCounter(info) { State = { Value = Visibility.Hidden }, diff --git a/osu.Game/Screens/Play/HUD/KeyCounter.cs b/osu.Game/Screens/Play/HUD/KeyCounter.cs index 7cdd6b025f..f12d2166fc 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounter.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounter.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; namespace osu.Game.Screens.Play.HUD @@ -17,24 +16,10 @@ namespace osu.Game.Screens.Play.HUD /// public readonly InputTrigger Trigger; - /// - /// Whether the actions reported by should be counted. - /// - public Bindable IsCounting { get; } = new BindableBool(true); - - private readonly Bindable countPresses = new BindableInt - { - MinValue = 0 - }; - /// /// The current count of registered key presses. /// - public IBindable CountPresses => countPresses; - - private readonly Container content; - - protected override Container Content => content; + public IBindable CountPresses => Trigger.ActivationCount; /// /// Whether this is currently in the "activated" state because the associated key is currently pressed. @@ -43,52 +28,26 @@ namespace osu.Game.Screens.Play.HUD protected KeyCounter(InputTrigger trigger) { - InternalChildren = new Drawable[] - { - content = new Container - { - RelativeSizeAxes = Axes.Both - }, - Trigger = trigger, - }; + Trigger = trigger; Trigger.OnActivate += Activate; Trigger.OnDeactivate += Deactivate; } - private void increment() - { - if (!IsCounting.Value) - return; - - countPresses.Value++; - } - - private void decrement() - { - if (!IsCounting.Value) - return; - - countPresses.Value--; - } - protected virtual void Activate(bool forwardPlayback = true) { IsActive.Value = true; - if (forwardPlayback) - increment(); } protected virtual void Deactivate(bool forwardPlayback = true) { IsActive.Value = false; - if (!forwardPlayback) - decrement(); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); + Trigger.OnActivate -= Activate; Trigger.OnDeactivate -= Deactivate; } diff --git a/osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs b/osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs index e5951a8bf4..f2c4487854 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs @@ -2,10 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; namespace osu.Game.Screens.Play.HUD { - public partial class KeyCounterActionTrigger : InputTrigger + public partial class KeyCounterActionTrigger : InputTrigger, IKeyBindingHandler where T : struct { public T Action { get; } @@ -16,21 +18,21 @@ namespace osu.Game.Screens.Play.HUD Action = action; } - public bool OnPressed(T action, bool forwards) + public bool OnPressed(KeyBindingPressEvent e) { - if (!EqualityComparer.Default.Equals(action, Action)) + if (!EqualityComparer.Default.Equals(e.Action, Action)) return false; - Activate(forwards); + Activate(Clock.Rate >= 0); return false; } - public void OnReleased(T action, bool forwards) + public void OnReleased(KeyBindingReleaseEvent e) { - if (!EqualityComparer.Default.Equals(action, Action)) + if (!EqualityComparer.Default.Equals(e.Action, Action)) return; - Deactivate(forwards); + Deactivate(Clock.Rate >= 0); } } } diff --git a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs index 05427d3a32..e7e866932e 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs @@ -1,24 +1,20 @@ // 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.Collections.Generic; -using System.Linq; +using System.Collections.Specialized; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; using osu.Game.Configuration; -using osuTK; +using osu.Game.Rulesets.UI; +using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD { /// /// A flowing display of all gameplay keys. Individual keys can be added using implementations. /// - public abstract partial class KeyCounterDisplay : CompositeDrawable + public abstract partial class KeyCounterDisplay : CompositeDrawable, ISerialisableDrawable { /// /// Whether the key counter should be visible regardless of the configuration value. @@ -26,95 +22,46 @@ namespace osu.Game.Screens.Play.HUD /// public Bindable AlwaysVisible { get; } = new Bindable(true); - /// - /// The s contained in this . - /// - public IEnumerable Counters => KeyFlow; - protected abstract FillFlowContainer KeyFlow { get; } - /// - /// Whether the actions reported by all s within this should be counted. - /// - public Bindable IsCounting { get; } = new BindableBool(true); - protected readonly Bindable ConfigVisibility = new Bindable(); + private readonly IBindableList triggers = new BindableList(); + + [Resolved] + private InputCountController controller { get; set; } = null!; + protected abstract void UpdateVisibility(); - private Receptor? receptor; - - public void SetReceptor(Receptor receptor) - { - if (this.receptor != null) - throw new InvalidOperationException("Cannot set a new receptor when one is already active"); - - this.receptor = receptor; - } - - /// - /// Add a to this display. - /// - public void Add(InputTrigger trigger) - { - var keyCounter = CreateCounter(trigger); - - KeyFlow.Add(keyCounter); - - IsCounting.BindTo(keyCounter.IsCounting); - } - - /// - /// Add a range of to this display. - /// - public void AddRange(IEnumerable triggers) => triggers.ForEach(Add); - protected abstract KeyCounter CreateCounter(InputTrigger trigger); [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, DrawableRuleset? drawableRuleset) { config.BindWith(OsuSetting.KeyOverlay, ConfigVisibility); + + if (drawableRuleset != null) + AlwaysVisible.BindTo(drawableRuleset.HasReplayLoaded); } protected override void LoadComplete() { base.LoadComplete(); + triggers.BindTo(controller.Triggers); + triggers.BindCollectionChanged(triggersChanged, true); + AlwaysVisible.BindValueChanged(_ => UpdateVisibility()); ConfigVisibility.BindValueChanged(_ => UpdateVisibility(), true); } - public override bool HandleNonPositionalInput => receptor == null; - - public override bool HandlePositionalInput => receptor == null; - - public partial class Receptor : Drawable + private void triggersChanged(object? sender, NotifyCollectionChangedEventArgs e) { - protected readonly KeyCounterDisplay Target; - - public Receptor(KeyCounterDisplay target) - { - RelativeSizeAxes = Axes.Both; - Depth = float.MinValue; - Target = target; - } - - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - - protected override bool Handle(UIEvent e) - { - switch (e) - { - case KeyDownEvent: - case KeyUpEvent: - case MouseDownEvent: - case MouseUpEvent: - return Target.InternalChildren.Any(c => c.TriggerEvent(e)); - } - - return base.Handle(e); - } + KeyFlow.Clear(); + foreach (var trigger in controller.Triggers) + KeyFlow.Add(CreateCounter(trigger)); } + + public bool UsesFixedAnchor { get; set; } } } diff --git a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs index 58bf4eea4b..4a61c7fd1b 100644 --- a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs +++ b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.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 osu.Framework.Allocation; using osu.Framework.Bindables; @@ -24,11 +22,13 @@ namespace osu.Game.Screens.Play.HUD public BindableLong Team1Score = new BindableLong(); public BindableLong Team2Score = new BindableLong(); - protected MatchScoreCounter Score1Text; - protected MatchScoreCounter Score2Text; + protected MatchScoreCounter Score1Text = null!; + protected MatchScoreCounter Score2Text = null!; - private Drawable score1Bar; - private Drawable score2Bar; + private Drawable score1Bar = null!; + private Drawable score2Bar = null!; + + private MatchScoreDiffCounter scoreDiffText = null!; [BackgroundDependencyLoader] private void load(OsuColour colours) @@ -98,6 +98,16 @@ namespace osu.Game.Screens.Play.HUD }, } }, + scoreDiffText = new MatchScoreDiffCounter + { + Anchor = Anchor.TopCentre, + Margin = new MarginPadding + { + Top = bar_height / 4, + Horizontal = 8 + }, + Alpha = 0 + } }; } @@ -139,6 +149,10 @@ namespace osu.Game.Screens.Play.HUD losingBar.ResizeWidthTo(0, 400, Easing.OutQuint); winningBar.ResizeWidthTo(Math.Min(0.4f, MathF.Pow(diff / 1500000f, 0.5f) / 2), 400, Easing.OutQuint); + + scoreDiffText.Alpha = diff != 0 ? 1 : 0; + scoreDiffText.Current.Value = -diff; + scoreDiffText.Origin = Team1Score.Value > Team2Score.Value ? Anchor.TopLeft : Anchor.TopRight; } protected override void UpdateAfterChildren() @@ -150,7 +164,7 @@ namespace osu.Game.Screens.Play.HUD protected partial class MatchScoreCounter : CommaSeparatedScoreCounter { - private OsuSpriteText displayedSpriteText; + private OsuSpriteText displayedSpriteText = null!; public MatchScoreCounter() { @@ -174,5 +188,14 @@ namespace osu.Game.Screens.Play.HUD ? OsuFont.Torus.With(weight: FontWeight.Bold, size: font_size, fixedWidth: true) : OsuFont.Torus.With(weight: FontWeight.Regular, size: font_size * 0.8f, fixedWidth: true); } + + private partial class MatchScoreDiffCounter : CommaSeparatedScoreCounter + { + protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => + { + s.Spacing = new Vector2(-2); + s.Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: bar_height, fixedWidth: true); + }); + } } } diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 8b2b8f9464..ba948b516e 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.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.Collections.Generic; using osu.Framework.Bindables; @@ -26,7 +24,7 @@ namespace osu.Game.Screens.Play.HUD public ExpansionMode ExpansionMode = ExpansionMode.ExpandOnHover; - private readonly BindableWithCurrent> current = new BindableWithCurrent>(); + private readonly BindableWithCurrent> current = new BindableWithCurrent>(Array.Empty()); public Bindable> Current { @@ -65,9 +63,7 @@ namespace osu.Game.Screens.Play.HUD { iconsContainer.Clear(); - if (mods.NewValue == null) return; - - foreach (Mod mod in mods.NewValue) + foreach (Mod mod in mods.NewValue.AsOrdered()) iconsContainer.Add(new ModIcon(mod) { Scale = new Vector2(0.6f) }); appearTransform(); diff --git a/osu.Game/Screens/Play/HUD/ModFlowDisplay.cs b/osu.Game/Screens/Play/HUD/ModFlowDisplay.cs index 38027c64ac..f9cf025b47 100644 --- a/osu.Game/Screens/Play/HUD/ModFlowDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModFlowDisplay.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using osu.Framework.Bindables; @@ -70,7 +68,7 @@ namespace osu.Game.Screens.Play.HUD Spacing = new Vector2(0, -12 * iconScale); - foreach (Mod mod in current.Value) + foreach (Mod mod in current.Value.AsOrdered()) { Add(new ModIcon(mod) { diff --git a/osu.Game/Screens/Play/HUD/PlayerAvatar.cs b/osu.Game/Screens/Play/HUD/PlayerAvatar.cs index 1d0331593a..1341a10d60 100644 --- a/osu.Game/Screens/Play/HUD/PlayerAvatar.cs +++ b/osu.Game/Screens/Play/HUD/PlayerAvatar.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Play.HUD { [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.CornerRadius), nameof(SkinnableComponentStrings.CornerRadiusDescription), SettingControlType = typeof(SettingsPercentageSlider))] - public new BindableFloat CornerRadius { get; set; } = new BindableFloat(0.25f) + public new BindableFloat CornerRadius { get; } = new BindableFloat(0.25f) { MinValue = 0, MaxValue = 0.5f, diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index 45b2c1b13c..dbb0456cd0 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -1,14 +1,10 @@ // 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; using osuTK; using osu.Game.Screens.Play.PlayerSettings; -using osuTK.Input; namespace osu.Game.Screens.Play.HUD { @@ -16,16 +12,12 @@ namespace osu.Game.Screens.Play.HUD { private const int fade_duration = 200; - public bool ReplayLoaded; - public readonly PlaybackSettings PlaybackSettings; public readonly VisualSettings VisualSettings; public PlayerSettingsOverlay() { - AlwaysPresent = true; - Anchor = Anchor.TopRight; Origin = Anchor.TopRight; AutoSizeAxes = Axes.Both; @@ -39,34 +31,14 @@ namespace osu.Game.Screens.Play.HUD Spacing = new Vector2(0, 20), Children = new PlayerSettingsGroup[] { - //CollectionSettings = new CollectionSettings(), - //DiscussionSettings = new DiscussionSettings(), PlaybackSettings = new PlaybackSettings { Expanded = { Value = false } }, - VisualSettings = new VisualSettings { Expanded = { Value = false } } + VisualSettings = new VisualSettings { Expanded = { Value = false } }, + new AudioSettings { Expanded = { Value = false } } } }; } protected override void PopIn() => this.FadeIn(fade_duration); protected override void PopOut() => this.FadeOut(fade_duration); - - // We want to handle keyboard inputs all the time in order to trigger ToggleVisibility() when not visible - public override bool PropagateNonPositionalInputSubTree => true; - - protected override bool OnKeyDown(KeyDownEvent e) - { - if (e.Repeat) return false; - - if (e.ControlPressed) - { - if (e.Key == Key.H && ReplayLoaded) - { - ToggleVisibility(); - return true; - } - } - - return base.OnKeyDown(e); - } } } diff --git a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs index 4ceca817e2..701b8a8732 100644 --- a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs +++ b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs @@ -1,11 +1,10 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -30,7 +29,7 @@ namespace osu.Game.Screens.Play.HUD private readonly Bindable valid = new Bindable(); [Resolved] - private ScoreProcessor scoreProcessor { get; set; } + private ScoreProcessor scoreProcessor { get; set; } = null!; public UnstableRateCounter() { @@ -77,10 +76,11 @@ namespace osu.Game.Screens.Play.HUD { base.Dispose(isDisposing); - if (scoreProcessor == null) return; - - scoreProcessor.NewJudgement -= updateDisplay; - scoreProcessor.JudgementReverted -= updateDisplay; + if (scoreProcessor.IsNotNull()) + { + scoreProcessor.NewJudgement -= updateDisplay; + scoreProcessor.JudgementReverted -= updateDisplay; + } } private partial class TextComponent : CompositeDrawable, IHasText diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 9f050a07bd..128f8d5ffd 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -12,12 +12,15 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Configuration; using osu.Game.Input.Bindings; +using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -26,8 +29,6 @@ using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osu.Game.Screens.Play.HUD.JudgementCounter; using osu.Game.Skinning; using osuTK; -using osu.Game.Localisation; -using osu.Game.Rulesets; namespace osu.Game.Screens.Play { @@ -54,20 +55,24 @@ namespace osu.Game.Screens.Play return child == bottomRightElements; } - public readonly KeyCounterDisplay KeyCounter; public readonly ModDisplay ModDisplay; public readonly HoldForMenuButton HoldToQuit; public readonly PlayerSettingsOverlay PlayerSettingsOverlay; [Cached] - private readonly ClicksPerSecondCalculator clicksPerSecondCalculator; + private readonly ClicksPerSecondController clicksPerSecondController; [Cached] - private readonly JudgementTally tally; + public readonly InputCountController InputCountController; + + [Cached] + private readonly JudgementCountController judgementCountController; public Bindable ShowHealthBar = new Bindable(true); + [CanBeNull] private readonly DrawableRuleset drawableRuleset; + private readonly IReadOnlyList mods; /// @@ -76,6 +81,8 @@ namespace osu.Game.Screens.Play public Bindable ShowHud { get; } = new BindableBool(); private Bindable configVisibilityMode; + private Bindable configLeaderboardVisibility; + private Bindable configSettingsOverlay; private readonly BindableBool replayLoaded = new BindableBool(); @@ -100,10 +107,11 @@ namespace osu.Game.Screens.Play private readonly List hideTargets; - public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList mods, bool alwaysShowLeaderboard = true) + private readonly Drawable playfieldComponents; + + public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList mods, bool alwaysShowLeaderboard = true) { Drawable rulesetComponents; - this.drawableRuleset = drawableRuleset; this.mods = mods; @@ -113,11 +121,16 @@ namespace osu.Game.Screens.Play { CreateFailingLayer(), //Needs to be initialized before skinnable drawables. - tally = new JudgementTally(), + judgementCountController = new JudgementCountController(), + clicksPerSecondController = new ClicksPerSecondController(), + InputCountController = new InputCountController(), mainComponents = new HUDComponentsContainer { AlwaysPresent = true, }, rulesetComponents = drawableRuleset != null ? new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, } : Empty(), + playfieldComponents = drawableRuleset != null + ? new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, } + : Empty(), topRightElements = new FillFlowContainer { Anchor = Anchor.TopRight, @@ -145,7 +158,6 @@ namespace osu.Game.Screens.Play Direction = FillDirection.Vertical, Children = new Drawable[] { - KeyCounter = CreateKeyCounter(), HoldToQuit = CreateHoldForMenuButton(), } }, @@ -156,10 +168,9 @@ namespace osu.Game.Screens.Play Padding = new MarginPadding(44), // enough margin to avoid the hit error display Spacing = new Vector2(5) }, - clicksPerSecondCalculator = new ClicksPerSecondCalculator(), }; - hideTargets = new List { mainComponents, rulesetComponents, KeyCounter, topRightElements }; + hideTargets = new List { mainComponents, rulesetComponents, playfieldComponents, topRightElements }; if (!alwaysShowLeaderboard) hideTargets.Add(LeaderboardFlow); @@ -176,6 +187,8 @@ namespace osu.Game.Screens.Play ModDisplay.Current.Value = mods; configVisibilityMode = config.GetBindable(OsuSetting.HUDVisibilityMode); + configLeaderboardVisibility = config.GetBindable(OsuSetting.GameplayLeaderboard); + configSettingsOverlay = config.GetBindable(OsuSetting.ReplaySettingsOverlay); if (configVisibilityMode.Value == HUDVisibilityMode.Never && !hasShownNotificationOnce) { @@ -202,15 +215,40 @@ namespace osu.Game.Screens.Play holdingForHUD.BindValueChanged(_ => updateVisibility()); IsPlaying.BindValueChanged(_ => updateVisibility()); - configVisibilityMode.BindValueChanged(_ => updateVisibility(), true); + configVisibilityMode.BindValueChanged(_ => updateVisibility()); + configSettingsOverlay.BindValueChanged(_ => updateVisibility()); - replayLoaded.BindValueChanged(replayLoadedValueChanged, true); + replayLoaded.BindValueChanged(e => + { + if (e.NewValue) + { + ModDisplay.FadeIn(200); + InputCountController.Margin = new MarginPadding(10) { Bottom = 30 }; + } + else + { + ModDisplay.Delay(2000).FadeOut(200); + InputCountController.Margin = new MarginPadding(10); + } + + updateVisibility(); + }, true); } protected override void Update() { base.Update(); + if (drawableRuleset != null) + { + Quad playfieldScreenSpaceDrawQuad = drawableRuleset.Playfield.SkinnableComponentScreenSpaceDrawQuad; + + playfieldComponents.Position = ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft); + playfieldComponents.Width = (ToLocalSpace(playfieldScreenSpaceDrawQuad.TopRight) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length; + playfieldComponents.Height = (ToLocalSpace(playfieldScreenSpaceDrawQuad.BottomLeft) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length; + playfieldComponents.Rotation = drawableRuleset.Playfield.Rotation; + } + float? lowestTopScreenSpaceLeft = null; float? lowestTopScreenSpaceRight = null; @@ -278,6 +316,11 @@ namespace osu.Game.Screens.Play return; } + if (configSettingsOverlay.Value && replayLoaded.Value) + PlayerSettingsOverlay.Show(); + else + PlayerSettingsOverlay.Hide(); + switch (configVisibilityMode.Value) { case HUDVisibilityMode.Never: @@ -295,32 +338,12 @@ namespace osu.Game.Screens.Play } } - private void replayLoadedValueChanged(ValueChangedEvent e) - { - PlayerSettingsOverlay.ReplayLoaded = e.NewValue; - - if (e.NewValue) - { - PlayerSettingsOverlay.Show(); - ModDisplay.FadeIn(200); - KeyCounter.Margin = new MarginPadding(10) { Bottom = 30 }; - } - else - { - PlayerSettingsOverlay.Hide(); - ModDisplay.Delay(2000).FadeOut(200); - KeyCounter.Margin = new MarginPadding(10); - } - - updateVisibility(); - } - protected virtual void BindDrawableRuleset(DrawableRuleset drawableRuleset) { if (drawableRuleset is ICanAttachHUDPieces attachTarget) { - attachTarget.Attach(KeyCounter); - attachTarget.Attach(clicksPerSecondCalculator); + attachTarget.Attach(InputCountController); + attachTarget.Attach(clicksPerSecondController); } replayLoaded.BindTo(drawableRuleset.HasReplayLoaded); @@ -331,12 +354,6 @@ namespace osu.Game.Screens.Play ShowHealth = { BindTarget = ShowHealthBar } }; - protected KeyCounterDisplay CreateKeyCounter() => new DefaultKeyCounterDisplay - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - }; - protected HoldForMenuButton CreateHoldForMenuButton() => new HoldForMenuButton { Anchor = Anchor.BottomRight, @@ -358,6 +375,10 @@ namespace osu.Game.Screens.Play switch (e.Action) { + case GlobalAction.ToggleReplaySettings: + configSettingsOverlay.Value = !configSettingsOverlay.Value; + return true; + case GlobalAction.HoldForHUD: holdingForHUD.Value = true; return true; @@ -379,6 +400,10 @@ namespace osu.Game.Screens.Play } return true; + + case GlobalAction.ToggleInGameLeaderboard: + configLeaderboardVisibility.Value = !configLeaderboardVisibility.Value; + return true; } return false; diff --git a/osu.Game/Screens/Play/HotkeyExitOverlay.cs b/osu.Game/Screens/Play/HotkeyExitOverlay.cs index 4c1265c699..bcd9bd7cd6 100644 --- a/osu.Game/Screens/Play/HotkeyExitOverlay.cs +++ b/osu.Game/Screens/Play/HotkeyExitOverlay.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 osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Input.Bindings; diff --git a/osu.Game/Screens/Play/HotkeyRetryOverlay.cs b/osu.Game/Screens/Play/HotkeyRetryOverlay.cs index 582b5a1691..11d0b4f84f 100644 --- a/osu.Game/Screens/Play/HotkeyRetryOverlay.cs +++ b/osu.Game/Screens/Play/HotkeyRetryOverlay.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 osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Input.Bindings; diff --git a/osu.Game/Screens/Play/IGameplayClock.cs b/osu.Game/Screens/Play/IGameplayClock.cs index 83ba5f3474..ad28e343ff 100644 --- a/osu.Game/Screens/Play/IGameplayClock.cs +++ b/osu.Game/Screens/Play/IGameplayClock.cs @@ -23,6 +23,14 @@ namespace osu.Game.Screens.Play /// IAdjustableAudioComponent AdjustmentsFromMods { get; } + /// + /// Whether gameplay is paused. + /// IBindable IsPaused { get; } + + /// + /// Whether the clock is currently rewinding. + /// + bool IsRewinding { get; } } } diff --git a/osu.Game/Screens/Play/ILocalUserPlayInfo.cs b/osu.Game/Screens/Play/ILocalUserPlayInfo.cs index e4328b2c78..2d181a09d4 100644 --- a/osu.Game/Screens/Play/ILocalUserPlayInfo.cs +++ b/osu.Game/Screens/Play/ILocalUserPlayInfo.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 489a4ef8b3..54ed7ba626 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play private readonly WorkingBeatmap beatmap; - private readonly Track track; + private Track track; private readonly double skipTargetTime; @@ -54,7 +54,7 @@ namespace osu.Game.Screens.Play /// /// In the future I want to change this. /// - private double? actualStopTime; + internal double? LastStopTime; [Resolved] private MusicController musicController { get; set; } = null!; @@ -65,7 +65,7 @@ namespace osu.Game.Screens.Play /// The beatmap to be used for time and metadata references. /// The latest time which should be used when introducing gameplay. Will be used when skipping forward. public MasterGameplayClockContainer(WorkingBeatmap beatmap, double skipTargetTime) - : base(beatmap.Track, true) + : base(beatmap.Track, applyOffsets: true, requireDecoupling: true) { this.beatmap = beatmap; this.skipTargetTime = skipTargetTime; @@ -100,7 +100,7 @@ namespace osu.Game.Screens.Play protected override void StopGameplayClock() { - actualStopTime = GameplayClock.CurrentTime; + LastStopTime = GameplayClock.CurrentTime; if (IsLoaded) { @@ -127,17 +127,17 @@ namespace osu.Game.Screens.Play public override void Seek(double time) { // Safety in case the clock is seeked while stopped. - actualStopTime = null; + LastStopTime = null; base.Seek(time); } protected override void PrepareStart() { - if (actualStopTime != null) + if (LastStopTime != null) { - Seek(actualStopTime.Value); - actualStopTime = null; + Seek(LastStopTime.Value); + LastStopTime = null; } else base.PrepareStart(); @@ -145,7 +145,7 @@ namespace osu.Game.Screens.Play protected override void StartGameplayClock() { - addSourceClockAdjustments(); + addAdjustmentsToTrack(); base.StartGameplayClock(); @@ -186,14 +186,20 @@ namespace osu.Game.Screens.Play /// public void StopUsingBeatmapClock() { - removeSourceClockAdjustments(); - ChangeSource(new TrackVirtual(beatmap.Track.Length)); - addSourceClockAdjustments(); + removeAdjustmentsFromTrack(); + + track = new TrackVirtual(beatmap.Track.Length); + track.Seek(CurrentTime); + if (IsRunning) + track.Start(); + ChangeSource(track); + + addAdjustmentsToTrack(); } private bool speedAdjustmentsApplied; - private void addSourceClockAdjustments() + private void addAdjustmentsToTrack() { if (speedAdjustmentsApplied) return; @@ -207,7 +213,7 @@ namespace osu.Game.Screens.Play speedAdjustmentsApplied = true; } - private void removeSourceClockAdjustments() + private void removeAdjustmentsFromTrack() { if (!speedAdjustmentsApplied) return; @@ -222,7 +228,7 @@ namespace osu.Game.Screens.Play protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - removeSourceClockAdjustments(); + removeAdjustmentsFromTrack(); } ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.Beatmap.ControlPointInfo; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 18ea9d0acb..8c7fc551ba 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -11,8 +11,6 @@ using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; @@ -24,6 +22,7 @@ using osu.Framework.Threading; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Graphics.Containers; using osu.Game.IO.Archives; @@ -71,7 +70,7 @@ namespace osu.Game.Screens.Play protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; // We are managing our own adjustments (see OnEntering/OnExiting). - public override bool? AllowTrackAdjustments => false; + public override bool? ApplyModTrackAdjustments => false; private readonly IBindable gameActive = new Bindable(true); @@ -114,8 +113,6 @@ namespace osu.Game.Screens.Play private Ruleset ruleset; - private Sample sampleRestart; - public BreakOverlay BreakOverlay; /// @@ -195,7 +192,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader(true)] - private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game, CancellationToken cancellationToken) + private void load(OsuConfigManager config, OsuGameBase game, CancellationToken cancellationToken) { var gameplayMods = Mods.Value.Select(m => m.DeepClone()).ToArray(); @@ -213,8 +210,6 @@ namespace osu.Game.Screens.Play if (playableBeatmap == null) return; - sampleRestart = audio.Samples.Get(@"Gameplay/restart"); - mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); if (game != null) @@ -262,7 +257,7 @@ namespace osu.Game.Screens.Play rulesetSkinProvider.AddRange(new Drawable[] { - failAnimationLayer = new FailAnimation(DrawableRuleset) + failAnimationContainer = new FailAnimationContainer(DrawableRuleset) { OnComplete = onFailComplete, Children = new[] @@ -284,8 +279,10 @@ namespace osu.Game.Screens.Play { if (!this.IsCurrentScreen()) return; - fadeOut(true); - PerformExit(false); + if (PerformExit(false)) + // The hotkey overlay dims the screen. + // If the operation succeeds, we want to make sure we stay dimmed to keep continuity. + fadeOut(true); }, }, }); @@ -295,14 +292,19 @@ namespace osu.Game.Screens.Play if (Configuration.AllowRestart) { - rulesetSkinProvider.Add(new HotkeyRetryOverlay + rulesetSkinProvider.AddRange(new Drawable[] { - Action = () => + new HotkeyRetryOverlay { - if (!this.IsCurrentScreen()) return; + Action = () => + { + if (!this.IsCurrentScreen()) return; - fadeOut(true); - Restart(true); + if (Restart(true)) + // The hotkey overlay dims the screen. + // If the operation succeeds, we want to make sure we stay dimmed to keep continuity. + fadeOut(true); + }, }, }); } @@ -312,7 +314,7 @@ namespace osu.Game.Screens.Play // add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components. // also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.) // we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there. - failAnimationLayer.Add(createOverlayComponents(Beatmap.Value)); + failAnimationContainer.Add(createOverlayComponents(Beatmap.Value)); if (!DrawableRuleset.AllowGameplayOverlays) { @@ -378,9 +380,6 @@ namespace osu.Game.Screens.Play IsBreakTime.BindTo(breakTracker.IsBreakTime); IsBreakTime.BindValueChanged(onBreakTimeChanged, true); - if (Configuration.AutomaticallySkipIntro) - skipIntroOverlay.SkipWhenReady(); - loadLeaderboard(); } @@ -432,13 +431,12 @@ namespace osu.Game.Screens.Play IsPaused = { BindTarget = GameplayClockContainer.IsPaused }, ReplayLoaded = { BindTarget = DrawableRuleset.HasReplayLoaded }, }, - KeyCounter = + InputCountController = { IsCounting = { Value = false }, - AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, }, Anchor = Anchor.Centre, Origin = Anchor.Centre @@ -478,7 +476,7 @@ namespace osu.Game.Screens.Play { updateGameplayState(); updatePauseOnFocusLostState(); - HUDOverlay.KeyCounter.IsCounting.Value = !isBreakTime.NewValue; + HUDOverlay.InputCountController.IsCounting.Value = !isBreakTime.NewValue; } private void updateGameplayState() @@ -571,20 +569,9 @@ namespace osu.Game.Screens.Play /// Whether the pause or fail dialog should be shown before performing an exit. /// If and a dialog is not yet displayed, the exit will be blocked and the relevant dialog will display instead. /// - protected void PerformExit(bool showDialogFirst) + /// Whether this call resulted in a final exit. + protected bool PerformExit(bool showDialogFirst) { - // there is a chance that an exit request occurs after the transition to results has already started. - // even in such a case, the user has shown intent, so forcefully return to this screen (to proceed with the upwards exit process). - if (!this.IsCurrentScreen()) - { - ValidForResume = false; - - // in the potential case that this instance has already been exited, this is required to avoid a crash. - if (this.GetChildScreen() != null) - this.MakeCurrent(); - return; - } - bool pauseOrFailDialogVisible = PauseOverlay.State.Value == Visibility.Visible || FailOverlay.State.Value == Visibility.Visible; @@ -593,8 +580,8 @@ namespace osu.Game.Screens.Play // if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion). if (ValidForResume && GameplayState.HasFailed) { - failAnimationLayer.FinishTransforms(true); - return; + failAnimationContainer.FinishTransforms(true); + return false; } // even if this call has requested a dialog, there is a chance the current player mode doesn't support pausing. @@ -603,21 +590,32 @@ namespace osu.Game.Screens.Play // in the case a dialog needs to be shown, attempt to pause and show it. // this may fail (see internal checks in Pause()) but the fail cases are temporary, so don't fall through to Exit(). Pause(); - return; + return false; } } - // if an exit has been requested, cancel any pending completion (the user has shown intention to exit). - resultsDisplayDelegate?.Cancel(); + // Matching osu!stable behaviour, if the results screen is pending and the user requests an exit, + // show the results instead. + if (GameplayState.HasPassed && !isRestarting) + { + progressToResults(false); + return false; + } // import current score if possible. prepareAndImportScoreAsync(); - // The actual exit is performed if - // - the pause / fail dialog was not requested - // - the pause / fail dialog was requested but is already displayed (user showing intention to exit). - // - the pause / fail dialog was requested but couldn't be displayed due to the type or state of this Player instance. - this.Exit(); + // Screen may not be current if a restart has been performed. + if (this.IsCurrentScreen()) + { + // The actual exit is performed if + // - the pause / fail dialog was not requested + // - the pause / fail dialog was requested but is already displayed (user showing intention to exit). + // - the pause / fail dialog was requested but couldn't be displayed due to the type or state of this Player instance. + this.Exit(); + } + + return true; } private void performUserRequestedSkip() @@ -666,10 +664,11 @@ namespace osu.Game.Screens.Play /// This can be called from a child screen in order to trigger the restart process. /// /// Whether a quick restart was requested (skipping intro etc.). - public void Restart(bool quickRestart = false) + /// Whether this call resulted in a restart. + public bool Restart(bool quickRestart = false) { if (!Configuration.AllowRestart) - return; + return false; isRestarting = true; @@ -677,10 +676,9 @@ namespace osu.Game.Screens.Play // stopping here is to ensure music doesn't become audible after exiting back to PlayerLoader. musicController.Stop(); - sampleRestart?.Play(); RestartRequested?.Invoke(quickRestart); - PerformExit(false); + return PerformExit(false); } /// @@ -736,9 +734,6 @@ namespace osu.Game.Screens.Play // is no chance that a user could return to the (already completed) Player instance from a child screen. ValidForResume = false; - if (!Configuration.ShowResults) - return; - bool storyboardStillRunning = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value; // If the current beatmap has a storyboard, this method will be called again on storyboard completion. @@ -761,10 +756,16 @@ namespace osu.Game.Screens.Play /// Whether a minimum delay () should be added before the screen is displayed. private void progressToResults(bool withDelay) { - resultsDisplayDelegate?.Cancel(); + if (!Configuration.ShowResults) + return; + + // Setting this early in the process means that even if something were to go wrong in the order of events following, there + // is no chance that a user could return to the (already completed) Player instance from a child screen. + ValidForResume = false; double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0; + resultsDisplayDelegate?.Cancel(); resultsDisplayDelegate = new ScheduledDelegate(() => { if (prepareScoreForDisplayTask == null) @@ -817,10 +818,13 @@ namespace osu.Game.Screens.Play if (!canShowResults && !forceImport) return Task.FromResult(null); + // Clone score before beginning any async processing. + // - Must be run synchronously as the score may potentially be mutated in the background. + // - Must be cloned for the same reason. + Score scoreCopy = Score.DeepClone(); + return prepareScoreForDisplayTask = Task.Run(async () => { - var scoreCopy = Score.DeepClone(); - try { await PrepareScoreForResultsAsync(scoreCopy).ConfigureAwait(false); @@ -892,7 +896,7 @@ namespace osu.Game.Screens.Play protected FailOverlay FailOverlay { get; private set; } - private FailAnimation failAnimationLayer; + private FailAnimationContainer failAnimationContainer; private bool onFail() { @@ -917,7 +921,7 @@ namespace osu.Game.Screens.Play if (PauseOverlay.State.Value == Visibility.Visible) PauseOverlay.Hide(); - failAnimationLayer.Start(); + failAnimationContainer.Start(); if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) Restart(true); @@ -1048,7 +1052,7 @@ namespace osu.Game.Screens.Play b.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); - failAnimationLayer.Background = b; + failAnimationContainer.Background = b; }); HUDOverlay.IsPlaying.BindTo(localUserPlaying); @@ -1056,8 +1060,6 @@ namespace osu.Game.Screens.Play DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime); - DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); - storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable; foreach (var mod in GameplayState.Mods.OfType()) @@ -1084,9 +1086,12 @@ namespace osu.Game.Screens.Play protected virtual void StartGameplay() { if (GameplayClockContainer.IsRunning) - throw new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running"); + Logger.Error(new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running"), "Clock failure"); GameplayClockContainer.Reset(startClock: true); + + if (Configuration.AutomaticallySkipIntro) + skipIntroOverlay.SkipWhenReady(); } public override void OnSuspending(ScreenTransitionEvent e) @@ -1102,16 +1107,17 @@ namespace osu.Game.Screens.Play screenSuspension?.RemoveAndDisposeImmediately(); // Eagerly clean these up as disposal of child components is asynchronous and may leave sounds playing beyond user expectations. - failAnimationLayer?.Stop(); + failAnimationContainer?.Stop(); PauseOverlay?.StopAllSamples(); - if (LoadedBeatmapSuccessfully) + if (LoadedBeatmapSuccessfully && !GameplayState.HasPassed) { - if (!GameplayState.HasPassed && !GameplayState.HasFailed) + Debug.Assert(resultsDisplayDelegate == null); + + if (!GameplayState.HasFailed) GameplayState.HasQuit = true; - // if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap. - if (prepareScoreForDisplayTask == null && DrawableRuleset.ReplayScore == null) + if (DrawableRuleset.ReplayScore == null) ScoreProcessor.FailScore(Score.ScoreInfo); } @@ -1147,27 +1153,20 @@ namespace osu.Game.Screens.Play if (DrawableRuleset.ReplayScore != null) return Task.CompletedTask; - LegacyByteArrayReader replayReader = null; + ByteArrayArchiveReader replayReader = null; if (score.ScoreInfo.Ruleset.IsLegacyRuleset()) { using (var stream = new MemoryStream()) { new LegacyScoreEncoder(score, GameplayState.Beatmap).Encode(stream); - replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); + replayReader = new ByteArrayArchiveReader(stream.ToArray(), "replay.osr"); } } // the import process will re-attach managed beatmap/rulesets to this score. we don't want this for now, so create a temporary copy to import. var importableScore = score.ScoreInfo.DeepClone(); - // For the time being, online ID responses are not really useful for anything. - // In addition, the IDs provided via new (lazer) endpoints are based on a different autoincrement from legacy (stable) scores. - // - // Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint - // conflicts across various systems (ie. solo and multiplayer). - importableScore.OnlineID = -1; - var imported = scoreManager.Import(importableScore, replayReader); imported.PerformRead(s => @@ -1175,6 +1174,7 @@ namespace osu.Game.Screens.Play // because of the clone above, it's required that we copy back the post-import hash/ID to use for availability matching. score.ScoreInfo.Hash = s.Hash; score.ScoreInfo.ID = s.ID; + score.ScoreInfo.Files.AddRange(s.Files.Detach()); }); return Task.CompletedTask; @@ -1202,8 +1202,11 @@ namespace osu.Game.Screens.Play float fadeOutDuration = instant ? 0 : 250; this.FadeOut(fadeOutDuration); - ApplyToBackground(b => b.IgnoreUserSettings.Value = true); - storyboardReplacesBackground.Value = false; + if (this.IsCurrentScreen()) + { + ApplyToBackground(b => b.IgnoreUserSettings.Value = true); + storyboardReplacesBackground.Value = false; + } } #endregion diff --git a/osu.Game/Screens/Play/PlayerConfiguration.cs b/osu.Game/Screens/Play/PlayerConfiguration.cs index b82925ccb8..122e25f406 100644 --- a/osu.Game/Screens/Play/PlayerConfiguration.cs +++ b/osu.Game/Screens/Play/PlayerConfiguration.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Screens.Play { public class PlayerConfiguration diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 30ae5ee5aa..681189d184 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.Transforms; using osu.Framework.Input; using osu.Framework.Screens; using osu.Framework.Threading; +using osu.Game.Audio; using osu.Game.Audio.Effects; using osu.Game.Configuration; using osu.Game.Graphics; @@ -25,6 +26,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Menu; using osu.Game.Screens.Play.PlayerSettings; +using osu.Game.Skinning; using osu.Game.Users; using osu.Game.Utils; using osuTK; @@ -44,6 +46,8 @@ namespace osu.Game.Screens.Play public override bool DisallowExternalBeatmapRulesetChanges => true; + public override bool? AllowGlobalTrackControl => false; + // Here because IsHovered will not update unless we do so. public override bool HandlePositionalInput => true; @@ -76,6 +80,8 @@ namespace osu.Game.Screens.Play private AudioFilter lowPassFilter = null!; private AudioFilter highPassFilter = null!; + private SkinnableSound sampleRestart = null!; + [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); @@ -199,7 +205,8 @@ namespace osu.Game.Screens.Play }, idleTracker = new IdleTracker(750), lowPassFilter = new AudioFilter(audio.TrackMixer), - highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass) + highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass), + sampleRestart = new SkinnableSound(new SampleInfo(@"Gameplay/restart", @"pause-retry-click")) }; if (Beatmap.Value.BeatmapInfo.EpilepsyWarning) @@ -240,7 +247,7 @@ namespace osu.Game.Screens.Play contentIn(); - MetadataInfo.Delay(750).FadeIn(500); + MetadataInfo.Delay(750).FadeIn(500, Easing.OutQuint); // after an initial delay, start the debounced load check. // this will continue to execute even after resuming back on restart. @@ -265,6 +272,8 @@ namespace osu.Game.Screens.Play playerConsumed = false; cancelLoad(); + sampleRestart.Play(); + contentIn(); } @@ -405,13 +414,15 @@ namespace osu.Game.Screens.Play quickRestart = quickRestartRequested; hideOverlays = true; ValidForResume = true; + + this.MakeCurrent(); } private void contentIn() { MetadataInfo.Loading = true; - content.FadeInFromZero(400); + content.FadeInFromZero(500, Easing.OutQuint); content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer); settingsScroll.FadeInFromZero(500, Easing.Out) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index b542707185..840077eb7f 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -162,17 +162,20 @@ namespace osu.Game.Screens.Play.PlayerSettings realmWriteTask = realm.WriteAsync(r => { - var settings = r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings; + var setInfo = r.Find(beatmap.Value.BeatmapSetInfo.ID); - if (settings == null) // only the case for tests. + if (setInfo == null) // only the case for tests. return; - double val = Current.Value; + // Apply to all difficulties in a beatmap set for now (they generally always share timing). + foreach (var b in setInfo.Beatmaps) + { + BeatmapUserSettings settings = b.UserSettings; + double val = Current.Value; - if (settings.Offset == val) - return; - - settings.Offset = val; + if (settings.Offset != val) + settings.Offset = val; + } }); } } diff --git a/osu.Game/Screens/Play/PlayerSettings/DiscussionSettings.cs b/osu.Game/Screens/Play/PlayerSettings/DiscussionSettings.cs index 7c76936621..f64861cfa5 100644 --- a/osu.Game/Screens/Play/PlayerSettings/DiscussionSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/DiscussionSettings.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; diff --git a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs index 13e5b66a70..cf261ba49b 100644 --- a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/InputSettings.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index cb6fcb2413..4753effdb0 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.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 osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs index 45009684a6..88b778fafb 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 8a4e63d21c..ca71a89b48 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using osu.Framework.Bindables; using osu.Framework.Input.Bindings; @@ -30,7 +31,7 @@ namespace osu.Game.Screens.Play // Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108) protected override bool CheckModsAllowFailure() { - if (!replayIsFailedScore) + if (!replayIsFailedScore && !GameplayState.Mods.OfType().Any()) return false; return base.CheckModsAllowFailure(); diff --git a/osu.Game/Screens/Play/ReplayPlayerLoader.cs b/osu.Game/Screens/Play/ReplayPlayerLoader.cs index 1c9d694325..7da06fe506 100644 --- a/osu.Game/Screens/Play/ReplayPlayerLoader.cs +++ b/osu.Game/Screens/Play/ReplayPlayerLoader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Screens; using osu.Game.Scoring; diff --git a/osu.Game/Screens/Play/SaveFailedScoreButton.cs b/osu.Game/Screens/Play/SaveFailedScoreButton.cs index 20d2130e76..0a2696339c 100644 --- a/osu.Game/Screens/Play/SaveFailedScoreButton.cs +++ b/osu.Game/Screens/Play/SaveFailedScoreButton.cs @@ -8,16 +8,26 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Game.Database; using osu.Game.Scoring; using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; using osu.Game.Online; +using osu.Game.Online.Multiplayer; using osuTK; namespace osu.Game.Screens.Play { - public partial class SaveFailedScoreButton : CompositeDrawable + public partial class SaveFailedScoreButton : CompositeDrawable, IKeyBindingHandler { + [Resolved] + private RealmAccess realm { get; set; } = null!; + + [Resolved] + private ScoreManager scoreManager { get; set; } = null!; + private readonly Bindable state = new Bindable(); private readonly Func> importFailedScore; @@ -34,7 +44,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load(OsuGame? game, Player? player, RealmAccess realm) + private void load(OsuGame? game, Player? player) { InternalChild = button = new DownloadButton { @@ -54,7 +64,7 @@ namespace osu.Game.Screens.Play { importedScore = realm.Run(r => r.Find(t.GetResultSafely().ID)?.Detach()); Schedule(() => state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded); - }); + }).FireAndForget(); break; } } @@ -87,5 +97,43 @@ namespace osu.Game.Screens.Play } }, true); } + + #region Export via hotkey logic (also in ReplayDownloadButton) + + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.SaveReplay: + button.TriggerClick(); + return true; + + case GlobalAction.ExportReplay: + state.BindValueChanged(exportWhenReady, true); + + // start the import via button + if (state.Value != DownloadState.LocallyAvailable) + button.TriggerClick(); + + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + + private void exportWhenReady(ValueChangedEvent state) + { + if (state.NewValue != DownloadState.LocallyAvailable) return; + + scoreManager.Export(importedScore); + + this.state.ValueChanged -= exportWhenReady; + } + + #endregion } } diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 7d69f0ca18..29b2e5229b 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -211,6 +212,7 @@ namespace osu.Game.Screens.Play public partial class FadeContainer : Container, IStateful { + [CanBeNull] public event Action StateChanged; private Visibility state; diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index dafdf00136..f7ae3eb62b 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Play { IBeatmapInfo beatmap = score.ScoreInfo.BeatmapInfo; - Debug.Assert(beatmap.OnlineID > 0); + Debug.Assert(beatmap!.OnlineID > 0); return new SubmitSoloScoreRequest(score.ScoreInfo, token, beatmap.OnlineID); } diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index a5c84e97ab..f5af2684d3 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -143,7 +143,7 @@ namespace osu.Game.Screens.Play automaticDownload = new SettingsCheckbox { LabelText = "Automatically download beatmaps", - Current = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating), + Current = config.GetBindable(OsuSetting.AutomaticallyDownloadMissingBeatmaps), Anchor = Anchor.Centre, Origin = Anchor.Centre, }, diff --git a/osu.Game/Screens/Play/SpectatorPlayerLoader.cs b/osu.Game/Screens/Play/SpectatorPlayerLoader.cs index 3830443ce8..8f2bcfe046 100644 --- a/osu.Game/Screens/Play/SpectatorPlayerLoader.cs +++ b/osu.Game/Screens/Play/SpectatorPlayerLoader.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Screens; using osu.Game.Scoring; diff --git a/osu.Game/Screens/Play/SpectatorResultsScreen.cs b/osu.Game/Screens/Play/SpectatorResultsScreen.cs index b54dbb387a..001d3b4bbc 100644 --- a/osu.Game/Screens/Play/SpectatorResultsScreen.cs +++ b/osu.Game/Screens/Play/SpectatorResultsScreen.cs @@ -1,9 +1,8 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Screens; using osu.Game.Online.Spectator; using osu.Game.Scoring; @@ -19,7 +18,7 @@ namespace osu.Game.Screens.Play } [Resolved] - private SpectatorClient spectatorClient { get; set; } + private SpectatorClient spectatorClient { get; set; } = null!; [BackgroundDependencyLoader] private void load() @@ -42,7 +41,7 @@ namespace osu.Game.Screens.Play { base.Dispose(isDisposing); - if (spectatorClient != null) + if (spectatorClient.IsNotNull()) spectatorClient.OnUserBeganPlaying -= userBeganPlaying; } } diff --git a/osu.Game/Screens/Play/SquareGraph.cs b/osu.Game/Screens/Play/SquareGraph.cs index b53e86a41b..0c7b485755 100644 --- a/osu.Game/Screens/Play/SquareGraph.cs +++ b/osu.Game/Screens/Play/SquareGraph.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using JetBrains.Annotations; using osu.Framework; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -190,6 +191,7 @@ namespace osu.Game.Screens.Play private const float padding = 2; public const float WIDTH = cube_size + padding; + [CanBeNull] public event Action StateChanged; private readonly List drawableRows = new List(); diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 5fa6508a31..a75546f835 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -188,7 +188,10 @@ namespace osu.Game.Screens.Play { // token may be null if the request failed but gameplay was still allowed (see HandleTokenRetrievalFailure). if (token == null) + { + Logger.Log("No token, skipping score submission"); return Task.CompletedTask; + } if (scoreSubmissionSource != null) return scoreSubmissionSource.Task; @@ -197,6 +200,8 @@ namespace osu.Game.Screens.Play if (!score.ScoreInfo.Statistics.Any(s => s.Key.IsHit() && s.Value > 0)) return Task.CompletedTask; + Logger.Log($"Beginning score submission (token:{token.Value})..."); + scoreSubmissionSource = new TaskCompletionSource(); var request = CreateSubmissionRequest(score, token.Value); @@ -206,11 +211,12 @@ namespace osu.Game.Screens.Play score.ScoreInfo.Position = s.Position; scoreSubmissionSource.SetResult(true); + Logger.Log($"Score submission completed! (token:{token.Value} id:{s.ID})"); }; request.Failure += e => { - Logger.Error(e, $"Failed to submit score ({e.Message})"); + Logger.Error(e, $"Failed to submit score (token:{token.Value}): {e.Message}"); scoreSubmissionSource.SetResult(false); }; diff --git a/osu.Game/Screens/Ranking/AspectContainer.cs b/osu.Game/Screens/Ranking/AspectContainer.cs index 9ec2a15044..a26bb8fe43 100644 --- a/osu.Game/Screens/Ranking/AspectContainer.cs +++ b/osu.Game/Screens/Ranking/AspectContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index 402322c611..195cd03e9b 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Color4Extensions; @@ -36,7 +34,7 @@ namespace osu.Game.Screens.Ranking.Contracted private readonly ScoreInfo score; [Resolved] - private ScoreManager scoreManager { get; set; } + private ScoreManager scoreManager { get; set; } = null!; /// /// Creates a new . diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs index 32f2eb2fa5..244acbe8b1 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index f23b469f5c..d1dc1a81db 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -66,14 +66,14 @@ namespace osu.Game.Screens.Ranking.Expanded [BackgroundDependencyLoader] private void load(BeatmapDifficultyCache beatmapDifficultyCache) { - var beatmap = score.BeatmapInfo; + var beatmap = score.BeatmapInfo!; var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata; string creator = metadata.Author.Username; var topStatistics = new List { new AccuracyStatistic(score.Accuracy), - new ComboStatistic(score.MaxCombo, scoreManager.GetMaximumAchievableCombo(score)), + new ComboStatistic(score.MaxCombo, score.GetMaximumAchievableCombo()), new PerformanceStatistic(score), }; @@ -101,23 +101,21 @@ namespace osu.Game.Screens.Ranking.Expanded Direction = FillDirection.Vertical, Children = new Drawable[] { - new OsuSpriteText + new TruncatingSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = new RomanisableString(metadata.TitleUnicode, metadata.Title), Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, - Truncate = true, }, - new OsuSpriteText + new TruncatingSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist), Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, - Truncate = true, }, new Container { @@ -156,14 +154,13 @@ namespace osu.Game.Screens.Ranking.Expanded AutoSizeAxes = Axes.Both, Children = new Drawable[] { - new OsuSpriteText + new TruncatingSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = beatmap.DifficultyName, Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold), MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, - Truncate = true, }, new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12)) { diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs index 863c450617..384e5661b4 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Game.Graphics; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticCounter.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticCounter.cs index ecadc9eed6..b279c8107c 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticCounter.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticCounter.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 osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index 5c5cb61b79..df5f9c7a8a 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -1,30 +1,34 @@ // 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; using osu.Game.Online; using osu.Game.Scoring; using osuTK; namespace osu.Game.Screens.Ranking { - public partial class ReplayDownloadButton : CompositeDrawable + public partial class ReplayDownloadButton : CompositeDrawable, IKeyBindingHandler { public readonly Bindable Score = new Bindable(); protected readonly Bindable State = new Bindable(); - private DownloadButton button; - private ShakeContainer shakeContainer; + private DownloadButton button = null!; + private ShakeContainer shakeContainer = null!; - private ScoreDownloadTracker downloadTracker; + private ScoreDownloadTracker? downloadTracker; + + [Resolved] + private ScoreManager scoreManager { get; set; } = null!; private ReplayAvailability replayAvailability { @@ -33,7 +37,7 @@ namespace osu.Game.Screens.Ranking if (State.Value == DownloadState.LocallyAvailable) return ReplayAvailability.Local; - if (Score.Value?.HasReplay == true) + if (Score.Value?.HasOnlineReplay == true) return ReplayAvailability.Online; return ReplayAvailability.NotAvailable; @@ -46,8 +50,8 @@ namespace osu.Game.Screens.Ranking Size = new Vector2(50, 30); } - [BackgroundDependencyLoader(true)] - private void load(OsuGame game, ScoreModelDownloader scores) + [BackgroundDependencyLoader] + private void load(OsuGame? game, ScoreModelDownloader scoreDownloader) { InternalChild = shakeContainer = new ShakeContainer { @@ -67,7 +71,7 @@ namespace osu.Game.Screens.Ranking break; case DownloadState.NotDownloaded: - scores.Download(Score.Value); + scoreDownloader.Download(Score.Value); break; case DownloadState.Importing: @@ -79,6 +83,10 @@ namespace osu.Game.Screens.Ranking Score.BindValueChanged(score => { + // An export may be pending from the last score. + // Reset this to meet user expectations (a new score which has just been switched to shouldn't export) + State.ValueChanged -= exportWhenReady; + downloadTracker?.RemoveAndDisposeImmediately(); if (score.NewValue != null) @@ -99,6 +107,53 @@ namespace osu.Game.Screens.Ranking }, true); } + #region Export via hotkey logic (also in SaveFailedScoreButton) + + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Repeat) + return false; + + switch (e.Action) + { + case GlobalAction.SaveReplay: + button.TriggerClick(); + return true; + + case GlobalAction.ExportReplay: + if (State.Value == DownloadState.LocallyAvailable) + { + State.BindValueChanged(exportWhenReady, true); + } + else + { + // A download needs to be performed before we can export this replay. + button.TriggerClick(); + if (button.Enabled.Value) + State.BindValueChanged(exportWhenReady, true); + } + + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + + private void exportWhenReady(ValueChangedEvent state) + { + if (state.NewValue != DownloadState.LocallyAvailable) return; + + scoreManager.Export(Score.Value); + + State.ValueChanged -= exportWhenReady; + } + + #endregion + private void updateState() { switch (replayAvailability) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 78239e0dbe..e3d19725da 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -36,6 +36,8 @@ namespace osu.Game.Screens.Ranking public override bool DisallowExternalBeatmapRulesetChanges => true; + public override bool? AllowGlobalTrackControl => true; + // Temporary for now to stop dual transitions. Should respect the current toolbar mode, but there's no way to do so currently. public override bool HideOverlaysOnEnter => true; @@ -53,7 +55,8 @@ namespace osu.Game.Screens.Ranking [Resolved] private IAPIProvider api { get; set; } - private StatisticsPanel statisticsPanel; + protected StatisticsPanel StatisticsPanel { get; private set; } + private Drawable bottomPanel; private Container detachedPanelContainer; @@ -96,7 +99,7 @@ namespace osu.Game.Screens.Ranking RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - statisticsPanel = CreateStatisticsPanel().With(panel => + StatisticsPanel = CreateStatisticsPanel().With(panel => { panel.RelativeSizeAxes = Axes.Both; panel.Score.BindTarget = SelectedScore; @@ -105,7 +108,7 @@ namespace osu.Game.Screens.Ranking { RelativeSizeAxes = Axes.Both, SelectedScore = { BindTarget = SelectedScore }, - PostExpandAction = () => statisticsPanel.ToggleVisibility() + PostExpandAction = () => StatisticsPanel.ToggleVisibility() }, detachedPanelContainer = new Container { @@ -153,14 +156,14 @@ namespace osu.Game.Screens.Ranking if (Score != null) { // only show flair / animation when arriving after watching a play that isn't autoplay. - bool shouldFlair = player != null && Score.Mods.All(m => m.UserPlayable); + bool shouldFlair = player != null && !Score.User.IsBot; ScorePanelList.AddScore(Score, shouldFlair); } if (allowWatchingReplay) { - buttons.Add(new ReplayDownloadButton(null) + buttons.Add(new ReplayDownloadButton(SelectedScore.Value) { Score = { BindTarget = SelectedScore }, Width = 300 @@ -192,7 +195,7 @@ namespace osu.Game.Screens.Ranking if (req != null) api.Queue(req); - statisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true); + StatisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true); } protected override void Update() @@ -232,7 +235,7 @@ namespace osu.Game.Screens.Ranking protected virtual APIRequest FetchNextPage(int direction, Action> scoresCallback) => null; /// - /// Creates the to be used to display extended information about scores. + /// Creates the to be used to display extended information about scores. /// protected virtual StatisticsPanel CreateStatisticsPanel() => new StatisticsPanel(); @@ -270,9 +273,9 @@ namespace osu.Game.Screens.Ranking public override bool OnBackButton() { - if (statisticsPanel.State.Value == Visibility.Visible) + if (StatisticsPanel.State.Value == Visibility.Visible) { - statisticsPanel.Hide(); + StatisticsPanel.Hide(); return true; } @@ -305,7 +308,7 @@ namespace osu.Game.Screens.Ranking float origLocation = detachedPanelContainer.ToLocalSpace(screenSpacePos).X; expandedPanel.MoveToX(origLocation) .Then() - .MoveToX(StatisticsPanel.SIDE_PADDING, 150, Easing.OutQuint); + .MoveToX(StatisticsPanel.SIDE_PADDING, 400, Easing.OutElasticQuarter); // Hide contracted panels. foreach (var contracted in ScorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted)) @@ -313,7 +316,7 @@ namespace osu.Game.Screens.Ranking ScorePanelList.HandleInput = false; // Dim background. - ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.1f), 150)); + ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.4f), 400, Easing.OutQuint)); detachedPanel = expandedPanel; } @@ -326,10 +329,10 @@ namespace osu.Game.Screens.Ranking ScorePanelList.Attach(detachedPanel); // Move into its original location in the attached container first, then to the final location. - float origLocation = detachedPanel.Parent.ToLocalSpace(screenSpacePos).X; + float origLocation = detachedPanel.Parent!.ToLocalSpace(screenSpacePos).X; detachedPanel.MoveToX(origLocation) .Then() - .MoveToX(0, 150, Easing.OutQuint); + .MoveToX(0, 250, Easing.OutElasticQuarter); // Show contracted panels. foreach (var contracted in ScorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted)) @@ -337,7 +340,7 @@ namespace osu.Game.Screens.Ranking ScorePanelList.HandleInput = true; // Un-dim background. - ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.5f), 150)); + ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.5f), 250, Easing.OutQuint)); detachedPanel = null; } @@ -351,7 +354,7 @@ namespace osu.Game.Screens.Ranking switch (e.Action) { case GlobalAction.Select: - statisticsPanel.ToggleVisibility(); + StatisticsPanel.ToggleVisibility(); return true; } diff --git a/osu.Game/Screens/Ranking/RetryButton.cs b/osu.Game/Screens/Ranking/RetryButton.cs index c7d2416e29..d977f25323 100644 --- a/osu.Game/Screens/Ranking/RetryButton.cs +++ b/osu.Game/Screens/Ranking/RetryButton.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; @@ -18,8 +16,8 @@ namespace osu.Game.Screens.Ranking { private readonly Box background; - [Resolved(canBeNull: true)] - private Player player { get; set; } + [Resolved] + private Player? player { get; set; } public RetryButton() { diff --git a/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs b/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs index ec153cbd63..f5a26ef754 100644 --- a/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs +++ b/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics.Containers; diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index c8920a734d..da08a26a58 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Solo; @@ -63,11 +64,11 @@ namespace osu.Game.Screens.Ranking protected override APIRequest? FetchScores(Action>? scoresCallback) { - if (Score.BeatmapInfo.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending) + if (Score.BeatmapInfo!.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending) return null; getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); - getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineID != Score.OnlineID).Select(s => s.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo))); + getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => !s.MatchesOnlineID(Score)).Select(s => s.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo))); return getScoreRequest; } diff --git a/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs b/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs index bb9905d29c..fb7107cc88 100644 --- a/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs +++ b/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 6b1850002d..1260ec2339 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -5,9 +5,11 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Layout; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Scoring; @@ -113,94 +115,92 @@ namespace osu.Game.Screens.Ranking.Statistics } } - if (barDrawables != null) - { - for (int i = 0; i < barDrawables.Length; i++) - { - barDrawables[i].UpdateOffset(bins[i].Sum(b => b.Value)); - } - } + if (barDrawables == null) + createBarDrawables(); else { - int maxCount = bins.Max(b => b.Values.Sum()); - barDrawables = bins.Select((bin, i) => new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index)).ToArray(); + for (int i = 0; i < barDrawables.Length; i++) + barDrawables[i].UpdateOffset(bins[i].Sum(b => b.Value)); + } + } - Container axisFlow; + private void createBarDrawables() + { + int maxCount = bins.Max(b => b.Values.Sum()); + barDrawables = bins.Select((_, i) => new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index)).ToArray(); - const float axis_font_size = 12; + Container axisFlow; - InternalChild = new GridContainer + Padding = new MarginPadding { Horizontal = 5 }; + + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Width = 0.8f, - Content = new[] + new Drawable[] { - new Drawable[] + new GridContainer { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] { barDrawables } - } - }, - new Drawable[] - { - axisFlow = new Container - { - RelativeSizeAxes = Axes.X, - Height = axis_font_size, - } - }, + RelativeSizeAxes = Axes.Both, + Content = new[] { barDrawables } + } }, - RowDimensions = new[] + new Drawable[] { - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - } - }; + axisFlow = new Container + { + RelativeSizeAxes = Axes.X, + Height = StatisticItem.FONT_SIZE, + } + }, + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + } + }; - // Our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. - double maxValue = timing_distribution_bins * binSize; - double axisValueStep = maxValue / axis_points; + // Our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. + double maxValue = timing_distribution_bins * binSize; + double axisValueStep = maxValue / axis_points; + + axisFlow.Add(new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "0", + Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold) + }); + + for (int i = 1; i <= axis_points; i++) + { + double axisValue = i * axisValueStep; + float position = (float)(axisValue / maxValue); + float alpha = 1f - position * 0.8f; axisFlow.Add(new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = "0", - Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) + RelativePositionAxes = Axes.X, + X = -position / 2, + Alpha = alpha, + Text = axisValue.ToString("-0"), + Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold) }); - for (int i = 1; i <= axis_points; i++) + axisFlow.Add(new OsuSpriteText { - double axisValue = i * axisValueStep; - float position = (float)(axisValue / maxValue); - float alpha = 1f - position * 0.8f; - - axisFlow.Add(new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - X = -position / 2, - Alpha = alpha, - Text = axisValue.ToString("-0"), - Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) - }); - - axisFlow.Add(new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - X = position / 2, - Alpha = alpha, - Text = axisValue.ToString("+0"), - Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) - }); - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + X = position / 2, + Alpha = alpha, + Text = axisValue.ToString("+0"), + Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold) + }); } } @@ -211,13 +211,16 @@ namespace osu.Game.Screens.Ranking.Statistics private readonly bool isCentre; private readonly float totalValue; - private float basalHeight; + private const float minimum_height = 0.02f; + private float offsetAdjustment; private Circle[] boxOriginals = null!; private Circle? boxAdjustment; + private float? lastDrawHeight; + [Resolved] private OsuColour colours { get; set; } = null!; @@ -256,15 +259,17 @@ namespace osu.Game.Screens.Ranking.Statistics else { // A bin with no value draws a grey dot instead. - Circle dot = new Circle + InternalChildren = boxOriginals = new[] { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Colour = isCentre ? Color4.White : Color4.Gray, - Height = 0, + new Circle + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Colour = isCentre ? Color4.White : Color4.Gray, + Height = 0, + } }; - InternalChildren = boxOriginals = new[] { dot }; } } @@ -272,31 +277,18 @@ namespace osu.Game.Screens.Ranking.Statistics { base.LoadComplete(); - if (!values.Any()) - return; - - updateBasalHeight(); - - foreach (var boxOriginal in boxOriginals) - { - boxOriginal.Y = 0; - boxOriginal.Height = basalHeight; - } - - float offsetValue = 0; - - for (int i = 0; i < values.Count; i++) - { - boxOriginals[i].MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint); - boxOriginals[i].ResizeHeightTo(heightForValue(values[i].Value), duration, Easing.OutQuint); - offsetValue -= values[i].Value; - } + Scheduler.AddOnce(updateMetrics, true); } - protected override void Update() + protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) { - base.Update(); - updateBasalHeight(); + if (invalidation.HasFlagFast(Invalidation.DrawSize)) + { + if (lastDrawHeight != null && lastDrawHeight != DrawHeight) + Scheduler.AddOnce(updateMetrics, false); + } + + return base.OnInvalidate(invalidation, source); } public void UpdateOffset(float adjustment) @@ -321,45 +313,32 @@ namespace osu.Game.Screens.Ranking.Statistics } offsetAdjustment = adjustment; - drawAdjustmentBar(); + + Scheduler.AddOnce(updateMetrics, true); } - private void updateBasalHeight() - { - float newBasalHeight = DrawHeight > DrawWidth ? DrawWidth / DrawHeight : 1; - - if (newBasalHeight == basalHeight) - return; - - basalHeight = newBasalHeight; - foreach (var dot in boxOriginals) - dot.Height = basalHeight; - - draw(); - } - - private float offsetForValue(float value) => (1 - basalHeight) * value / maxValue; - - private float heightForValue(float value) => MathF.Max(basalHeight + offsetForValue(value), 0); - - private void draw() - { - resizeBars(); - - if (boxAdjustment != null) - drawAdjustmentBar(); - } - - private void resizeBars() + private void updateMetrics(bool animate = true) { float offsetValue = 0; - for (int i = 0; i < values.Count; i++) + for (int i = 0; i < boxOriginals.Length; i++) { - boxOriginals[i].Y = offsetForValue(offsetValue) * DrawHeight; - boxOriginals[i].Height = heightForValue(values[i].Value); - offsetValue -= values[i].Value; + int value = i < values.Count ? values[i].Value : 0; + + var box = boxOriginals[i]; + + box.MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint); + box.ResizeHeightTo(heightForValue(value), duration, Easing.OutQuint); + offsetValue -= value; } + + if (boxAdjustment != null) + drawAdjustmentBar(); + + if (!animate) + FinishTransforms(true); + + lastDrawHeight = DrawHeight; } private void drawAdjustmentBar() @@ -369,6 +348,10 @@ namespace osu.Game.Screens.Ranking.Statistics boxAdjustment.ResizeHeightTo(heightForValue(offsetAdjustment), duration, Easing.OutQuint); boxAdjustment.FadeTo(!hasAdjustment ? 0 : 1, duration, Easing.OutQuint); } + + private float offsetForValue(float value) => (1 - minimum_height) * value / maxValue; + + private float heightForValue(float value) => minimum_height + offsetForValue(value); } } } diff --git a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs index 10cb77fa91..ee0ce6183d 100644 --- a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs +++ b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs @@ -97,7 +97,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: StatisticItem.FONT_SIZE), Text = "Achieved PP", Colour = Color4Extensions.FromHex("#66FFCC") }, @@ -105,7 +105,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreRight, Anchor = Anchor.CentreRight, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 18), + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: StatisticItem.FONT_SIZE), Colour = Color4Extensions.FromHex("#66FFCC") } }, @@ -115,7 +115,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: StatisticItem.FONT_SIZE), Text = "Maximum", Colour = OsuColour.Gray(0.7f) }, @@ -123,7 +123,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: StatisticItem.FONT_SIZE), Colour = OsuColour.Gray(0.7f) } } @@ -208,7 +208,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: StatisticItem.FONT_SIZE), Text = attribute.DisplayName, Colour = Colour4.White }, @@ -233,7 +233,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Origin = Anchor.CentreRight, Anchor = Anchor.CentreRight, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: StatisticItem.FONT_SIZE), Text = percentage.ToLocalisableString("0%"), Colour = Colour4.White } diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs index 99f4e1e342..23ccc3d0b7 100644 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs @@ -44,13 +44,13 @@ namespace osu.Game.Screens.Ranking.Statistics Text = Name, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 14) + Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE) }, value = new OsuSpriteText { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold) + Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.Bold) } }); } diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs index d68df4558a..ed31bc8643 100644 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs @@ -98,12 +98,11 @@ namespace osu.Game.Screens.Ranking.Statistics Direction = FillDirection.Vertical }; - private partial class Spacer : CompositeDrawable + public partial class Spacer : CompositeDrawable { public Spacer() { RelativeSizeAxes = Axes.Both; - Padding = new MarginPadding { Vertical = 4 }; InternalChild = new CircularContainer { diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index c5bdc6f6f5..cf95886905 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -1,10 +1,7 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -15,6 +12,11 @@ namespace osu.Game.Screens.Ranking.Statistics /// public class StatisticItem { + /// + /// The recommended font size to use in statistic items to make sure they match others. + /// + public const float FONT_SIZE = 13; + /// /// The name of this item. /// @@ -36,7 +38,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// The name of the item. Can be to hide the item header. /// A function returning the content to be displayed. /// Whether this item requires hit events. If true, will not be called if no hit events are available. - public StatisticItem(LocalisableString name, [NotNull] Func createContent, bool requiresHitEvents = false) + public StatisticItem(LocalisableString name, Func createContent, bool requiresHitEvents = false) { Name = name; RequiresHitEvents = requiresHitEvents; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItemContainer.cs similarity index 56% rename from osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs rename to osu.Game/Screens/Ranking/Statistics/StatisticItemContainer.cs index d3327224dc..6e18ae1fe4 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItemContainer.cs @@ -1,11 +1,9 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Diagnostics.CodeAnalysis; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; @@ -18,42 +16,53 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// Wraps a to add a header and suitable layout for use in . /// - internal partial class StatisticContainer : CompositeDrawable + internal partial class StatisticItemContainer : CompositeDrawable { /// - /// Creates a new . + /// Creates a new . /// /// The to display. - public StatisticContainer([NotNull] StatisticItem item) + public StatisticItemContainer(StatisticItem item) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - InternalChild = new GridContainer + Padding = new MarginPadding(5); + + InternalChild = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Content = new[] + Masking = true, + CornerRadius = 6, + Children = new Drawable[] { - new[] + new Box { - createHeader(item) + Colour = ColourInfo.GradientVertical( + OsuColour.Gray(0.25f), + OsuColour.Gray(0.18f) + ), + Alpha = 0.95f, + RelativeSizeAxes = Axes.Both, }, - new Drawable[] + new Container { - new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(5), + Children = new[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Top = 15 }, - Child = item.CreateContent() + createHeader(item), + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(10) { Top = 30 }, + Child = item.CreateContent() + } } }, - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), } }; } @@ -66,7 +75,7 @@ namespace osu.Game.Screens.Ranking.Statistics return new FillFlowContainer { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + Height = 20, Direction = FillDirection.Horizontal, Spacing = new Vector2(5, 0), Children = new Drawable[] @@ -84,7 +93,7 @@ namespace osu.Game.Screens.Ranking.Statistics Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Text = item.Name, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), + Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold), } } }; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index c36d7726dc..19bd0c4393 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -124,20 +124,23 @@ namespace osu.Game.Screens.Ranking.Statistics } else { - FillFlowContainer rows; + FillFlowContainer flow; container = new OsuScrollContainer(Direction.Vertical) { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, + Masking = false, + ScrollbarOverlapsContent = false, Alpha = 0, Children = new[] { - rows = new FillFlowContainer + flow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(30, 15) + Spacing = new Vector2(30, 15), + Direction = FillDirection.Full, } } }; @@ -146,35 +149,22 @@ namespace osu.Game.Screens.Ranking.Statistics foreach (var item in statisticItems) { - var columnContent = new List(); - if (!hitEventsAvailable && item.RequiresHitEvents) { anyRequiredHitEvents = true; continue; } - columnContent.Add(new StatisticContainer(item) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); - - rows.Add(new GridContainer + flow.Add(new StatisticItemContainer(item) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Content = new[] { columnContent.ToArray() }, - ColumnDimensions = new[] { new Dimension() }, - RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } }); } if (anyRequiredHitEvents) { - rows.Add(new FillFlowContainer + flow.Add(new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -223,7 +213,7 @@ namespace osu.Game.Screens.Ranking.Statistics protected override void PopIn() { - this.FadeIn(150, Easing.OutQuint); + this.FadeIn(350, Easing.OutQuint); popInSample?.Play(); wasOpened = true; @@ -231,7 +221,7 @@ namespace osu.Game.Screens.Ranking.Statistics protected override void PopOut() { - this.FadeOut(150, Easing.OutQuint); + this.FadeOut(250, Easing.OutQuint); if (wasOpened) popOutSample?.Play(); diff --git a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs index de01668029..cc3535a426 100644 --- a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs +++ b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Game.Rulesets.Scoring; diff --git a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs index 447f206128..d08a654e99 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Solo; -using osuTK; namespace osu.Game.Screens.Ranking.Statistics.User { @@ -18,7 +17,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User public Bindable StatisticsUpdate { get; } = new Bindable(); private LoadingLayer loadingLayer = null!; - private FillFlowContainer content = null!; + private GridContainer content = null!; [BackgroundDependencyLoader] private void load() @@ -33,21 +32,47 @@ namespace osu.Game.Screens.Ranking.Statistics.User { RelativeSizeAxes = Axes.Both, }, - content = new FillFlowContainer + content = new GridContainer { AlwaysPresent = true, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(10), - Children = new Drawable[] + ColumnDimensions = new[] { - new GlobalRankChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, - new AccuracyChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, - new MaximumComboChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, - new RankedScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, - new TotalScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, - new PerformancePointsChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } } + new Dimension(), + new Dimension(GridSizeMode.Absolute, 30), + new Dimension(), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + new GlobalRankChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new SimpleStatisticTable.Spacer(), + new PerformancePointsChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + }, + new Drawable[] { }, + new Drawable[] + { + new MaximumComboChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new SimpleStatisticTable.Spacer(), + new AccuracyChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + }, + new Drawable[] { }, + new Drawable[] + { + new RankedScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + new SimpleStatisticTable.Spacer(), + new TotalScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, + } } } }; diff --git a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs index 5348b4a522..906bf8d5ca 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics; @@ -46,14 +47,15 @@ namespace osu.Game.Screens.Ranking.Statistics.User new OsuSpriteText { Text = Label, - Font = OsuFont.Default.With(size: 18) + Font = OsuFont.Default.With(size: StatisticItem.FONT_SIZE) }, new FillFlowContainer { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, + AutoSizeAxes = Axes.X, + Height = StatisticItem.FONT_SIZE * 2, Children = new Drawable[] { new FillFlowContainer @@ -65,17 +67,31 @@ namespace osu.Game.Screens.Ranking.Statistics.User Spacing = new Vector2(5), Children = new Drawable[] { - changeIcon = new SpriteIcon + new Container { + Size = new Vector2(14), Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Size = new Vector2(18) + Children = new Drawable[] + { + new Circle + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray1 + }, + changeIcon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(10), + }, + } }, currentValueText = new OsuSpriteText { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Font = OsuFont.Default.With(size: 18, weight: FontWeight.Bold) + Font = OsuFont.Default.With(size: StatisticItem.FONT_SIZE, weight: FontWeight.Bold) }, } }, @@ -83,7 +99,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Font = OsuFont.Default.With(weight: FontWeight.Bold) + Font = OsuFont.Default.With(size: StatisticItem.FONT_SIZE, weight: FontWeight.Bold) } } } @@ -123,7 +139,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User } else { - comparisonColour = colours.Orange1; + comparisonColour = colours.Gray4; icon = FontAwesome.Solid.Minus; } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 6ba9843f7b..eb47a7201a 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -78,6 +78,8 @@ namespace osu.Game.Screens.Select private CarouselBeatmapSet? selectedBeatmapSet; + private List originalBeatmapSetsDetached = new List(); + /// /// Raised when the is changed. /// @@ -127,15 +129,38 @@ namespace osu.Game.Screens.Select private void loadBeatmapSets(IEnumerable beatmapSets) { + originalBeatmapSetsDetached = beatmapSets.Detach(); + + if (selectedBeatmapSet != null && !originalBeatmapSetsDetached.Contains(selectedBeatmapSet.BeatmapSet)) + selectedBeatmapSet = null; + + var selectedBeatmapBefore = selectedBeatmap?.BeatmapInfo; + CarouselRoot newRoot = new CarouselRoot(this); - newRoot.AddItems(beatmapSets.Select(s => createCarouselSet(s.Detach())).OfType()); + if (beatmapsSplitOut) + { + var carouselBeatmapSets = originalBeatmapSetsDetached.SelectMany(s => s.Beatmaps).Select(b => + { + return createCarouselSet(new BeatmapSetInfo(new[] { b }) + { + ID = b.BeatmapSet!.ID, + OnlineID = b.BeatmapSet!.OnlineID, + Status = b.BeatmapSet!.Status, + }); + }).OfType(); + + newRoot.AddItems(carouselBeatmapSets); + } + else + { + var carouselBeatmapSets = originalBeatmapSetsDetached.Select(createCarouselSet).OfType(); + + newRoot.AddItems(carouselBeatmapSets); + } root = newRoot; - if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet)) - selectedBeatmapSet = null; - Scroll.Clear(false); itemsCache.Invalidate(); ScrollToSelected(); @@ -144,6 +169,15 @@ namespace osu.Game.Screens.Select if (loadedTestBeatmaps) signalBeatmapsLoaded(); + + // Restore selection + if (selectedBeatmapBefore != null && newRoot.BeatmapSetsByID.TryGetValue(selectedBeatmapBefore.BeatmapSet!.ID, out var newSelectionCandidates)) + { + CarouselBeatmap? found = newSelectionCandidates.SelectMany(s => s.Beatmaps).SingleOrDefault(b => b.BeatmapInfo.ID == selectedBeatmapBefore.ID); + + if (found != null) + found.State.Value = CarouselItemState.Selected; + } } private readonly List visibleItems = new List(); @@ -155,7 +189,7 @@ namespace osu.Game.Screens.Select public Bindable RandomAlgorithm = new Bindable(); private readonly List previouslyVisitedRandomSets = new List(); - private readonly Stack randomSelectedBeatmaps = new Stack(); + private readonly List randomSelectedBeatmaps = new List(); private CarouselRoot root; @@ -223,7 +257,7 @@ namespace osu.Game.Screens.Select subscriptionHiddenBeatmaps = realm.RegisterForNotifications(r => r.All().Where(b => b.Hidden), beatmapsChanged); } - private void deletedBeatmapSetsChanged(IRealmCollection sender, ChangeSet? changes, Exception? error) + private void deletedBeatmapSetsChanged(IRealmCollection sender, ChangeSet? changes) { // If loading test beatmaps, avoid overwriting with realm subscription callbacks. if (loadedTestBeatmaps) @@ -236,7 +270,7 @@ namespace osu.Game.Screens.Select removeBeatmapSet(sender[i].ID); } - private void beatmapSetsChanged(IRealmCollection sender, ChangeSet? changes, Exception? error) + private void beatmapSetsChanged(IRealmCollection sender, ChangeSet? changes) { // If loading test beatmaps, avoid overwriting with realm subscription callbacks. if (loadedTestBeatmaps) @@ -255,7 +289,7 @@ namespace osu.Game.Screens.Select foreach (var id in realmSets) { if (!root.BeatmapSetsByID.ContainsKey(id)) - UpdateBeatmapSet(realm.Realm.Find(id).Detach()); + UpdateBeatmapSet(realm.Realm.Find(id)!.Detach()); } foreach (var id in root.BeatmapSetsByID.Keys) @@ -315,7 +349,7 @@ namespace osu.Game.Screens.Select } } - private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes, Exception? error) + private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) { // we only care about actual changes in hidden status. if (changes == null) @@ -330,8 +364,8 @@ namespace osu.Game.Screens.Select // Only require to action here if the beatmap is missing. // This avoids processing these events unnecessarily when new beatmaps are imported, for example. - if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSet) - && existingSet.BeatmapSet.Beatmaps.All(b => b.ID != beatmapInfo.ID)) + if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSets) + && existingSets.SelectMany(s => s.Beatmaps).All(b => b.BeatmapInfo.ID != beatmapInfo.ID)) { UpdateBeatmapSet(beatmapSet.Detach()); } @@ -345,10 +379,20 @@ namespace osu.Game.Screens.Select private void removeBeatmapSet(Guid beatmapSetID) => Schedule(() => { - if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSet)) + if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSets)) return; - root.RemoveItem(existingSet); + originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSetID); + + foreach (var set in existingSets) + { + foreach (var beatmap in set.Beatmaps) + randomSelectedBeatmaps.Remove(beatmap); + previouslyVisitedRandomSets.Remove(set); + + root.RemoveItem(set); + } + itemsCache.Invalidate(); if (!Scroll.UserScrolling) @@ -361,26 +405,64 @@ namespace osu.Game.Screens.Select { Guid? previouslySelectedID = null; + originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSet.ID); + originalBeatmapSetsDetached.Add(beatmapSet.Detach()); + // If the selected beatmap is about to be removed, store its ID so it can be re-selected if required if (selectedBeatmapSet?.BeatmapSet.ID == beatmapSet.ID) previouslySelectedID = selectedBeatmap?.BeatmapInfo.ID; - var newSet = createCarouselSet(beatmapSet); - var removedSet = root.RemoveChild(beatmapSet.ID); + var removedSets = root.RemoveItemsByID(beatmapSet.ID); - // If we don't remove this here, it may remain in a hidden state until scrolled off screen. - // Doesn't really affect anything during actual user interaction, but makes testing annoying. - var removedDrawable = Scroll.FirstOrDefault(c => c.Item == removedSet); - if (removedDrawable != null) - expirePanelImmediately(removedDrawable); - - if (newSet != null) + foreach (var removedSet in removedSets) { - root.AddItem(newSet); + // If we don't remove this here, it may remain in a hidden state until scrolled off screen. + // Doesn't really affect anything during actual user interaction, but makes testing annoying. + var removedDrawable = Scroll.FirstOrDefault(c => c.Item == removedSet); + if (removedDrawable != null) + expirePanelImmediately(removedDrawable); + } + + if (beatmapsSplitOut) + { + var newSets = new List(); + + foreach (var beatmap in beatmapSet.Beatmaps) + { + var newSet = createCarouselSet(new BeatmapSetInfo(new[] { beatmap }) + { + ID = beatmapSet.ID, + OnlineID = beatmapSet.OnlineID, + Status = beatmapSet.Status, + }); + + if (newSet != null) + { + newSets.Add(newSet); + root.AddItem(newSet); + } + } // check if we can/need to maintain our current selection. if (previouslySelectedID != null) - select((CarouselItem?)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet); + { + var toSelect = newSets.FirstOrDefault(s => s.Beatmaps.Any(b => b.BeatmapInfo.ID == previouslySelectedID)) + ?? newSets.FirstOrDefault(); + select(toSelect); + } + } + else + { + var newSet = createCarouselSet(beatmapSet); + + if (newSet != null) + { + root.AddItem(newSet); + + // check if we can/need to maintain our current selection. + if (previouslySelectedID != null) + select((CarouselItem?)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet); + } } itemsCache.Invalidate(); @@ -501,7 +583,7 @@ namespace osu.Game.Screens.Select if (selectedBeatmap != null && selectedBeatmapSet != null) { - randomSelectedBeatmaps.Push(selectedBeatmap); + randomSelectedBeatmaps.Add(selectedBeatmap); // when performing a random, we want to add the current set to the previously visited list // else the user may be "randomised" to the existing selection. @@ -538,9 +620,10 @@ namespace osu.Game.Screens.Select { while (randomSelectedBeatmaps.Any()) { - var beatmap = randomSelectedBeatmaps.Pop(); + var beatmap = randomSelectedBeatmaps[^1]; + randomSelectedBeatmaps.Remove(beatmap); - if (!beatmap.Filtered.Value) + if (!beatmap.Filtered.Value && beatmap.BeatmapInfo.BeatmapSet?.DeletePending != true) { if (selectedBeatmapSet != null) { @@ -626,6 +709,8 @@ namespace osu.Game.Screens.Select applyActiveCriteria(debounce); } + private bool beatmapsSplitOut; + private void applyActiveCriteria(bool debounce, bool alwaysResetScrollPosition = true) { PendingFilter?.Cancel(); @@ -646,6 +731,13 @@ namespace osu.Game.Screens.Select { PendingFilter = null; + if (activeCriteria.SplitOutDifficulties != beatmapsSplitOut) + { + beatmapsSplitOut = activeCriteria.SplitOutDifficulties; + loadBeatmapSets(originalBeatmapSetsDetached); + return; + } + root.Filter(activeCriteria); itemsCache.Invalidate(); @@ -1049,7 +1141,7 @@ namespace osu.Game.Screens.Select // May only be null during construction (State.Value set causes PerformSelection to be triggered). private readonly BeatmapCarousel? carousel; - public readonly Dictionary BeatmapSetsByID = new Dictionary(); + public readonly Dictionary> BeatmapSetsByID = new Dictionary>(); public CarouselRoot(BeatmapCarousel carousel) { @@ -1063,20 +1155,25 @@ namespace osu.Game.Screens.Select public override void AddItem(CarouselItem i) { CarouselBeatmapSet set = (CarouselBeatmapSet)i; - BeatmapSetsByID.Add(set.BeatmapSet.ID, set); + if (BeatmapSetsByID.TryGetValue(set.BeatmapSet.ID, out var sets)) + sets.Add(set); + else + BeatmapSetsByID.Add(set.BeatmapSet.ID, new List { set }); base.AddItem(i); } - public CarouselBeatmapSet? RemoveChild(Guid beatmapSetID) + public IEnumerable RemoveItemsByID(Guid beatmapSetID) { - if (BeatmapSetsByID.TryGetValue(beatmapSetID, out var carouselBeatmapSet)) + if (BeatmapSetsByID.TryGetValue(beatmapSetID, out var carouselBeatmapSets)) { - RemoveItem(carouselBeatmapSet); - return carouselBeatmapSet; + foreach (var set in carouselBeatmapSets) + RemoveItem(set); + + return carouselBeatmapSets; } - return null; + return Enumerable.Empty(); } public override void RemoveItem(CarouselItem i) @@ -1100,6 +1197,8 @@ namespace osu.Game.Screens.Select { private bool rightMouseScrollBlocked; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + public CarouselScrollContainer() { // size is determined by the carousel itself, due to not all content necessarily being loaded. diff --git a/osu.Game/Screens/Select/BeatmapDetailAreaDetailTabItem.cs b/osu.Game/Screens/Select/BeatmapDetailAreaDetailTabItem.cs index d6b076f30b..4ff2600a72 100644 --- a/osu.Game/Screens/Select/BeatmapDetailAreaDetailTabItem.cs +++ b/osu.Game/Screens/Select/BeatmapDetailAreaDetailTabItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Screens.Select { public class BeatmapDetailAreaDetailTabItem : BeatmapDetailAreaTabItem diff --git a/osu.Game/Screens/Select/BeatmapDetailAreaLeaderboardTabItem.cs b/osu.Game/Screens/Select/BeatmapDetailAreaLeaderboardTabItem.cs index 6efadc77b3..8dbe5b8bea 100644 --- a/osu.Game/Screens/Select/BeatmapDetailAreaLeaderboardTabItem.cs +++ b/osu.Game/Screens/Select/BeatmapDetailAreaLeaderboardTabItem.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; namespace osu.Game.Screens.Select diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index 712b610515..179323176a 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -141,9 +141,9 @@ namespace osu.Game.Screens.Select LayoutEasing = Easing.OutQuad, Children = new[] { - description = new MetadataSectionDescription(searchOnSongSelect), - source = new MetadataSectionSource(searchOnSongSelect), - tags = new MetadataSectionTags(searchOnSongSelect), + description = new MetadataSectionDescription(query => songSelect?.Search(query)), + source = new MetadataSectionSource(query => songSelect?.Search(query)), + tags = new MetadataSectionTags(query => songSelect?.Search(query)), }, }, }, @@ -176,12 +176,6 @@ namespace osu.Game.Screens.Select }, loading = new LoadingLayer(true) }; - - void searchOnSongSelect(string text) - { - if (songSelect != null) - songSelect.FilterControl.CurrentTextSearch.Value = text; - } } private void updateStatistics() diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 2102df1022..8bbf569566 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -30,6 +30,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; using osu.Game.Graphics.Containers; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Screens.Select { @@ -76,14 +77,12 @@ namespace osu.Game.Screens.Select protected override void PopIn() { this.MoveToX(0, animation_duration, Easing.OutQuint); - this.RotateTo(0, animation_duration, Easing.OutQuint); this.FadeIn(transition_duration); } protected override void PopOut() { this.MoveToX(-100, animation_duration, Easing.In); - this.RotateTo(10, animation_duration, Easing.In); this.FadeOut(transition_duration * 2, Easing.In); } @@ -233,12 +232,11 @@ namespace osu.Game.Screens.Select RelativeSizeAxes = Axes.X, Children = new Drawable[] { - VersionLabel = new OsuSpriteText + VersionLabel = new TruncatingSpriteText { Text = beatmapInfo.DifficultyName, Font = OsuFont.GetFont(size: 24, italics: true), RelativeSizeAxes = Axes.X, - Truncate = true, }, } }, @@ -286,19 +284,17 @@ namespace osu.Game.Screens.Select RelativeSizeAxes = Axes.X, Children = new Drawable[] { - TitleLabel = new OsuSpriteText + TitleLabel = new TruncatingSpriteText { Current = { BindTarget = titleBinding }, Font = OsuFont.GetFont(size: 28, italics: true), RelativeSizeAxes = Axes.X, - Truncate = true, }, - ArtistLabel = new OsuSpriteText + ArtistLabel = new TruncatingSpriteText { Current = { BindTarget = artistBinding }, Font = OsuFont.GetFont(size: 17, italics: true), RelativeSizeAxes = Axes.X, - Truncate = true, }, MapperContainer = new FillFlowContainer { @@ -354,32 +350,9 @@ namespace osu.Game.Screens.Select private void addInfoLabels() { - if (working.Beatmap?.HitObjects?.Any() != true) + if (working.Beatmap?.HitObjects.Any() != true) return; - infoLabelContainer.Children = new Drawable[] - { - new InfoLabel(new BeatmapStatistic - { - Name = "Length", - CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length), - Content = working.BeatmapInfo.Length.ToFormattedDuration().ToString(), - }), - bpmLabelContainer = new Container - { - AutoSizeAxes = Axes.Both, - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(20, 0), - Children = getRulesetInfoLabels() - } - }; - } - - private InfoLabel[] getRulesetInfoLabels() - { try { IBeatmap playableBeatmap; @@ -395,14 +368,30 @@ namespace osu.Game.Screens.Select playableBeatmap = working.GetPlayableBeatmap(working.BeatmapInfo.Ruleset, Array.Empty()); } - return playableBeatmap.GetStatistics().Select(s => new InfoLabel(s)).ToArray(); + infoLabelContainer.Children = new Drawable[] + { + new InfoLabel(new BeatmapStatistic + { + Name = BeatmapsetsStrings.ShowStatsTotalLength(playableBeatmap.CalculateDrainLength().ToFormattedDuration()), + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length), + Content = working.BeatmapInfo.Length.ToFormattedDuration().ToString(), + }), + bpmLabelContainer = new Container + { + AutoSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(20, 0), + Children = playableBeatmap.GetStatistics().Select(s => new InfoLabel(s)).ToArray() + } + }; } catch (Exception e) { Logger.Error(e, "Could not load beatmap successfully!"); } - - return Array.Empty(); } private void refreshBPMLabel() @@ -427,7 +416,7 @@ namespace osu.Game.Screens.Select bpmLabelContainer.Child = new InfoLabel(new BeatmapStatistic { - Name = "BPM", + Name = BeatmapsetsStrings.ShowStatsBpm, CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm), Content = labelText }); diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeBackground.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeBackground.cs index d5d258704b..50ec446c4f 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeBackground.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeBackground.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 osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs new file mode 100644 index 0000000000..3c76ae1f08 --- /dev/null +++ b/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs @@ -0,0 +1,329 @@ +// 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.Threading; +using osuTK; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; + +namespace osu.Game.Screens.Select +{ + public partial class BeatmapInfoWedgeV2 : VisibilityContainer + { + public const float WEDGE_HEIGHT = 120; + private const float shear_width = 21; + private const float transition_duration = 250; + private const float corner_radius = 10; + private const float colour_bar_width = 30; + + /// Todo: move this const out to song select when more new design elements are implemented for the beatmap details area, since it applies to text alignment of various elements + private const float text_margin = 62; + + private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / WEDGE_HEIGHT, 0); + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } = null!; + + protected Container? DisplayedContent { get; private set; } + + protected WedgeInfoText? Info { get; private set; } + + private Container difficultyColourBar = null!; + private StarCounter starCounter = null!; + private StarRatingDisplay starRatingDisplay = null!; + private BeatmapSetOnlineStatusPill statusPill = null!; + private Container content = null!; + + private IBindable? starDifficulty; + private CancellationTokenSource? cancellationSource; + + public BeatmapInfoWedgeV2() + { + Height = WEDGE_HEIGHT; + Shear = wedged_container_shear; + Masking = true; + Margin = new MarginPadding { Left = -corner_radius }; + EdgeEffect = new EdgeEffectParameters + { + Colour = Colour4.Black.Opacity(0.2f), + Type = EdgeEffectType.Shadow, + Radius = 3, + }; + CornerRadius = corner_radius; + } + + [BackgroundDependencyLoader] + private void load() + { + Child = content = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + // These elements can't be grouped with the rest of the content, due to being present either outside or under the backgrounds area + difficultyColourBar = new Container + { + Colour = Colour4.Transparent, + Depth = float.MaxValue, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Y, + + // By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it. + Width = colour_bar_width + corner_radius, + Child = new Box { RelativeSizeAxes = Axes.Both } + }, + new Container + { + // Applying the shear to this container and nesting the starCounter inside avoids + // the deformation that occurs if the shear is applied to the starCounter whilst rotated + Shear = -wedged_container_shear, + X = -colour_bar_width / 2, + Anchor = Anchor.CentreRight, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Width = colour_bar_width, + Child = starCounter = new StarCounter + { + Rotation = (float)(Math.Atan(shear_width / WEDGE_HEIGHT) * (180 / Math.PI)), + Colour = Colour4.Transparent, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.35f), + Direction = FillDirection.Vertical + } + }, + new FillFlowContainer + { + Name = "Topright-aligned metadata", + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Top = 3, Right = colour_bar_width + 8 }, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(0, 5), + Depth = float.MinValue, + Children = new Drawable[] + { + starRatingDisplay = new StarRatingDisplay(default, animated: true) + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Shear = -wedged_container_shear, + Alpha = 0, + }, + statusPill = new BeatmapSetOnlineStatusPill + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Shear = -wedged_container_shear, + TextSize = 11, + TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + Alpha = 0, + } + } + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ruleset.BindValueChanged(_ => updateDisplay()); + + starRatingDisplay.Current.BindValueChanged(s => + { + // use actual stars as star counter has its own animation + starCounter.Current = (float)s.NewValue.Stars; + }, true); + + starRatingDisplay.DisplayedStars.BindValueChanged(s => + { + // sync color with star rating display + starCounter.Colour = s.NewValue >= 6.5 ? colours.Orange1 : Colour4.Black.Opacity(0.75f); + difficultyColourBar.FadeColour(colours.ForStarDifficulty(s.NewValue)); + }, true); + } + + private const double animation_duration = 600; + + protected override void PopIn() + { + this.MoveToX(0, animation_duration, Easing.OutQuint); + this.FadeIn(200, Easing.In); + } + + protected override void PopOut() + { + this.MoveToX(-150, animation_duration, Easing.OutQuint); + this.FadeOut(200, Easing.OutQuint); + } + + private WorkingBeatmap beatmap = null!; + + public WorkingBeatmap Beatmap + { + get => beatmap; + set + { + if (beatmap == value) return; + + beatmap = value; + + updateDisplay(); + } + } + + private Container? loadingInfo; + + private void updateDisplay() + { + statusPill.Status = beatmap.BeatmapInfo.Status; + + starDifficulty = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo, (cancellationSource = new CancellationTokenSource()).Token); + + starDifficulty.BindValueChanged(s => + { + starRatingDisplay.Current.Value = s.NewValue ?? default; + + starRatingDisplay.FadeIn(transition_duration); + }); + + Scheduler.AddOnce(() => + { + LoadComponentAsync(loadingInfo = new Container + { + Padding = new MarginPadding { Right = colour_bar_width }, + RelativeSizeAxes = Axes.Both, + Depth = DisplayedContent?.Depth + 1 ?? 0, + Child = new Container + { + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + // TODO: New wedge design uses a coloured horizontal gradient for its background, however this lacks implementation information in the figma draft. + // pending https://www.figma.com/file/DXKwqZhD5yyb1igc3mKo1P?node-id=2980:3361#340801912 being answered. + new BeatmapInfoWedgeBackground(beatmap) { Shear = -Shear }, + Info = new WedgeInfoText(beatmap) { Shear = -Shear } + } + } + }, d => + { + // Ensure we are the most recent loaded wedge. + if (d != loadingInfo) return; + + removeOldInfo(); + content.Add(DisplayedContent = d); + }); + }); + + void removeOldInfo() + { + DisplayedContent?.FadeOut(transition_duration); + DisplayedContent?.Expire(); + DisplayedContent = null; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + cancellationSource?.Cancel(); + } + + public partial class WedgeInfoText : Container + { + public OsuSpriteText TitleLabel { get; private set; } = null!; + public OsuSpriteText ArtistLabel { get; private set; } = null!; + + private readonly WorkingBeatmap working; + + public WedgeInfoText(WorkingBeatmap working) + { + this.working = working; + + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(SongSelect? songSelect, LocalisationManager localisation) + { + var metadata = working.Metadata; + + var titleText = new RomanisableString(metadata.TitleUnicode, metadata.Title); + var artistText = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); + + Child = new FillFlowContainer + { + Name = "Top-left aligned metadata", + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Left = text_margin, Top = 12 }, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Children = new Drawable[] + { + new OsuHoverContainer + { + AutoSizeAxes = Axes.Both, + Action = () => songSelect?.Search(titleText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript)), + Child = TitleLabel = new TruncatingSpriteText + { + Shadow = true, + Text = titleText, + Font = OsuFont.TorusAlternate.With(size: 40, weight: FontWeight.SemiBold), + }, + }, + new OsuHoverContainer + { + AutoSizeAxes = Axes.Both, + Action = () => songSelect?.Search(artistText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript)), + Child = ArtistLabel = new TruncatingSpriteText + { + // TODO : figma design has a diffused shadow, instead of the solid one present here, not possible currently as far as i'm aware. + Shadow = true, + Text = artistText, + // Not sure if this should be semi bold or medium + Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), + }, + }, + } + }; + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + // best effort to confine the auto-sized text to wedge bounds + // the artist label doesn't have an extra text_margin as it doesn't touch the right metadata + TitleLabel.MaxWidth = DrawWidth - text_margin * 2 - shear_width; + ArtistLabel.MaxWidth = DrawWidth - text_margin - shear_width; + } + } + } +} diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 7e48bc5cdd..1d40862df7 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.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.Linq; using osu.Game.Beatmaps; using osu.Game.Screens.Select.Filter; @@ -58,23 +57,25 @@ namespace osu.Game.Screens.Select.Carousel match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(BeatmapInfo.Metadata.Author.Username); match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) || criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode); - + match &= !criteria.Title.HasFilter || criteria.Title.Matches(BeatmapInfo.Metadata.Title) || + criteria.Title.Matches(BeatmapInfo.Metadata.TitleUnicode); + match &= !criteria.DifficultyName.HasFilter || criteria.DifficultyName.Matches(BeatmapInfo.DifficultyName); match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating); if (!match) return false; if (criteria.SearchTerms.Length > 0) { - var terms = BeatmapInfo.GetSearchableTerms(); + var searchableTerms = BeatmapInfo.GetSearchableTerms(); - foreach (string criteriaTerm in criteria.SearchTerms) + foreach (FilterCriteria.OptionalTextFilter criteriaTerm in criteria.SearchTerms) { bool any = false; // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator - foreach (string term in terms) + foreach (string searchTerm in searchableTerms) { - if (!term.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase)) continue; + if (!criteriaTerm.Matches(searchTerm)) continue; any = true; break; @@ -98,7 +99,6 @@ namespace osu.Game.Screens.Select.Carousel if (!match) return false; match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true; - if (match && criteria.RulesetCriteria != null) match &= criteria.RulesetCriteria.Matches(BeatmapInfo); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index f08d14720b..3dfd801f02 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -233,7 +233,11 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapInfo.OnlineID > 0 && beatmapOverlay != null) items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmapInfo.OnlineID))); - var collectionItems = realm.Realm.All().AsEnumerable().Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmapInfo)).Cast().ToList(); + var collectionItems = realm.Realm.All() + .OrderBy(c => c.Name) + .AsEnumerable() + .Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmapInfo)).Cast().ToList(); + if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index b97d37c854..dd711b2513 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -112,11 +112,11 @@ namespace osu.Game.Screens.Select.Carousel background = new DelayedLoadWrapper(() => new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID))) { RelativeSizeAxes = Axes.Both, - }, 300) + }, 200) { RelativeSizeAxes = Axes.Both }, - mainFlow = new DelayedLoadWrapper(() => new SetPanelContent((CarouselBeatmapSet)Item), 100) + mainFlow = new DelayedLoadWrapper(() => new SetPanelContent((CarouselBeatmapSet)Item), 50) { RelativeSizeAxes = Axes.Both }, @@ -126,7 +126,7 @@ namespace osu.Game.Screens.Select.Carousel mainFlow.DelayedLoadComplete += fadeContentIn; } - private void fadeContentIn(Drawable d) => d.FadeInFromZero(750, Easing.OutQuint); + private void fadeContentIn(Drawable d) => d.FadeInFromZero(150); protected override void Deselected() { @@ -225,7 +225,12 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapSet.OnlineID > 0 && viewDetails != null) items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails(beatmapSet.OnlineID))); - var collectionItems = realm.Realm.All().AsEnumerable().Select(createCollectionMenuItem).ToList(); + var collectionItems = realm.Realm.All() + .OrderBy(c => c.Name) + .AsEnumerable() + .Select(createCollectionMenuItem) + .ToList(); + if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); diff --git a/osu.Game/Screens/Select/Carousel/SetPanelBackground.cs b/osu.Game/Screens/Select/Carousel/SetPanelBackground.cs index 6f13a34bfc..b8729b7174 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelBackground.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelBackground.cs @@ -1,12 +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.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; using osuTK; using osuTK.Graphics; @@ -21,7 +23,7 @@ namespace osu.Game.Screens.Select.Carousel Children = new Drawable[] { - new BeatmapBackgroundSprite(working) + new PanelBeatmapBackground(working) { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -68,5 +70,23 @@ namespace osu.Game.Screens.Select.Carousel }, }; } + + public partial class PanelBeatmapBackground : Sprite + { + private readonly IWorkingBeatmap working; + + public PanelBeatmapBackground(IWorkingBeatmap working) + { + ArgumentNullException.ThrowIfNull(working); + + this.working = working; + } + + [BackgroundDependencyLoader] + private void load() + { + Texture = working.GetPanelBackground(); + } + } } } diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index a57a8b0f27..da9661f702 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -29,9 +29,6 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private RealmAccess realm { get; set; } = null!; - [Resolved] - private ScoreManager scoreManager { get; set; } = null!; - [Resolved] private IAPIProvider api { get; set; } = null!; @@ -71,15 +68,14 @@ namespace osu.Game.Screens.Select.Carousel localScoresChanged); }, true); - void localScoresChanged(IRealmCollection sender, ChangeSet? changes, Exception _) + void localScoresChanged(IRealmCollection sender, ChangeSet? changes) { // This subscription may fire from changes to linked beatmaps, which we don't care about. // It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications. if (changes?.HasCollectionChanges() == false) return; - ScoreInfo? topScore = scoreManager.OrderByTotalScore(sender.Detach()).FirstOrDefault(); - + ScoreInfo? topScore = sender.MaxBy(info => (info.TotalScore, -info.Date.UtcDateTime.Ticks)); updateable.Rank = topScore?.Rank; updateable.Alpha = topScore != null ? 1 : 0; } diff --git a/osu.Game/Screens/Select/Filter/GroupMode.cs b/osu.Game/Screens/Select/Filter/GroupMode.cs index 8e2b9271b0..d794c215a3 100644 --- a/osu.Game/Screens/Select/Filter/GroupMode.cs +++ b/osu.Game/Screens/Select/Filter/GroupMode.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.ComponentModel; namespace osu.Game.Screens.Select.Filter diff --git a/osu.Game/Screens/Select/Filter/SortMode.cs b/osu.Game/Screens/Select/Filter/SortMode.cs index c77bdbfbc6..7f2b33adbe 100644 --- a/osu.Game/Screens/Select/Filter/SortMode.cs +++ b/osu.Game/Screens/Select/Filter/SortMode.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.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 38520a85b7..c15bd76ef8 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Collections; @@ -23,6 +24,7 @@ using osu.Game.Rulesets; using osu.Game.Screens.Select.Filter; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Screens.Select { @@ -274,6 +276,15 @@ namespace osu.Game.Screens.Select Colour = colours.Yellow }); } + + public override bool OnPressed(KeyBindingPressEvent e) + { + // the "cut" platform key binding (shift-delete) conflicts with the beatmap deletion action. + if (e.Action == PlatformAction.Cut && e.ShiftPressed && e.CurrentState.Keyboard.Keys.IsPressed(Key.Delete)) + return false; + + return base.OnPressed(e); + } } } } diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 320bfb1b45..812a16c484 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.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; using System.Collections.Generic; +using System.Globalization; using System.Linq; -using JetBrains.Annotations; +using System.Text.RegularExpressions; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Rulesets; @@ -20,7 +19,12 @@ namespace osu.Game.Screens.Select public GroupMode Group; public SortMode Sort; - public BeatmapSetInfo SelectedBeatmapSet; + /// + /// Whether the display of beatmap sets should be split apart per-difficulty for the current criteria. + /// + public bool SplitOutDifficulties => Sort == SortMode.Difficulty; + + public BeatmapSetInfo? SelectedBeatmapSet; public OptionalRange StarDifficulty; public OptionalRange ApproachRate; @@ -33,6 +37,8 @@ namespace osu.Game.Screens.Select public OptionalRange OnlineStatus; public OptionalTextFilter Creator; public OptionalTextFilter Artist; + public OptionalTextFilter Title; + public OptionalTextFilter DifficultyName; public OptionalRange UserStarDifficulty = new OptionalRange { @@ -40,12 +46,12 @@ namespace osu.Game.Screens.Select IsUpperInclusive = true }; - public string[] SearchTerms = Array.Empty(); + public OptionalTextFilter[] SearchTerms = Array.Empty(); - public RulesetInfo Ruleset; + public RulesetInfo? Ruleset; public bool AllowConvertedBeatmaps; - private string searchText; + private string searchText = string.Empty; /// /// as a number (if it can be parsed as one). @@ -58,11 +64,44 @@ namespace osu.Game.Screens.Select set { searchText = value; - SearchTerms = searchText.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray(); + + List terms = new List(); + + string remainingText = value; + + // Match either an open difficulty tag to the end of string, + // or match a closed one with a whitespace after it. + // + // To keep things simple, the closing ']' may be included in the match group, + // and is trimmed post-match. + foreach (Match quotedSegment in Regex.Matches(value, "(^|\\s)\\[(.*)(\\]\\s|$)")) + { + DifficultyName = new OptionalTextFilter + { + SearchTerm = quotedSegment.Groups[2].Value.Trim(']') + }; + + remainingText = remainingText.Replace(quotedSegment.Value, string.Empty); + } + + // First handle quoted segments to ensure we keep inline spaces in exact matches. + foreach (Match quotedSegment in Regex.Matches(value, "(\"[^\"]+\"[!]?)")) + { + terms.Add(new OptionalTextFilter { SearchTerm = quotedSegment.Value }); + remainingText = remainingText.Replace(quotedSegment.Value, string.Empty); + } + + // Then handle the rest splitting on any spaces. + terms.AddRange(remainingText.Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(s => new OptionalTextFilter + { + SearchTerm = s + })); + + SearchTerms = terms.ToArray(); SearchNumber = null; - if (SearchTerms.Length == 1 && int.TryParse(SearchTerms[0], out int parsed)) + if (SearchTerms.Length == 1 && int.TryParse(SearchTerms[0].SearchTerm, out int parsed)) SearchNumber = parsed; } } @@ -70,11 +109,9 @@ namespace osu.Game.Screens.Select /// /// Hashes from the to filter to. /// - [CanBeNull] - public IEnumerable CollectionBeatmapMD5Hashes { get; set; } + public IEnumerable? CollectionBeatmapMD5Hashes { get; set; } - [CanBeNull] - public IRulesetFilterCriteria RulesetCriteria { get; set; } + public IRulesetFilterCriteria? RulesetCriteria { get; set; } public struct OptionalRange : IEquatable> where T : struct @@ -124,6 +161,8 @@ namespace osu.Game.Screens.Select { public bool HasFilter => !string.IsNullOrEmpty(SearchTerm); + public MatchMode MatchMode { get; private set; } + public bool Matches(string value) { if (!HasFilter) @@ -133,12 +172,67 @@ namespace osu.Game.Screens.Select if (string.IsNullOrEmpty(value)) return false; - return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase); + switch (MatchMode) + { + default: + case MatchMode.Substring: + return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase); + + case MatchMode.IsolatedPhrase: + return Regex.IsMatch(value, $@"(^|\s){Regex.Escape(searchTerm)}($|\s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + case MatchMode.FullPhrase: + return CultureInfo.InvariantCulture.CompareInfo.Compare(value, searchTerm, CompareOptions.IgnoreCase) == 0; + } } - public string SearchTerm; + private string searchTerm; + + public string SearchTerm + { + get => searchTerm; + set + { + searchTerm = value; + + if (searchTerm.StartsWith('\"')) + { + // length check ensures that the quote character in the `StartsWith()` check above and the `EndsWith()` check below is not the same character. + if (searchTerm.EndsWith("\"!", StringComparison.Ordinal) && searchTerm.Length >= 3) + { + searchTerm = searchTerm.TrimEnd('!').Trim('\"'); + MatchMode = MatchMode.FullPhrase; + } + else + { + searchTerm = searchTerm.Trim('\"'); + MatchMode = MatchMode.IsolatedPhrase; + } + } + else + MatchMode = MatchMode.Substring; + } + } public bool Equals(OptionalTextFilter other) => SearchTerm == other.SearchTerm; } + + public enum MatchMode + { + /// + /// Match using a simple "contains" substring match. + /// + Substring, + + /// + /// Match for the search phrase being isolated by spaces, or at the start or end of the text. + /// + IsolatedPhrase, + + /// + /// Match for the search phrase matching the full text in completion. + /// + FullPhrase, + } } } diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index c86554ddbc..0d8905347b 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Select public static class FilterQueryParser { private static readonly Regex query_syntax_regex = new Regex( - @"\b(?\w+)(?(:|=|(>|<)(:|=)?))(?("".*"")|(\S*))", + @"\b(?\w+)(?(:|=|(>|<)(:|=)?))(?("".*""[!]?)|(\S*))", RegexOptions.Compiled | RegexOptions.IgnoreCase); internal static void ApplyQueries(FilterCriteria criteria, string query) @@ -73,6 +73,12 @@ namespace osu.Game.Screens.Select case "artist": return TryUpdateCriteriaText(ref criteria.Artist, op, value); + case "title": + return TryUpdateCriteriaText(ref criteria.Title, op, value); + + case "diff": + return TryUpdateCriteriaText(ref criteria.DifficultyName, op, value); + default: return criteria.RulesetCriteria?.TryParseCustomKeywordCriteria(key, op, value) ?? false; } @@ -161,7 +167,7 @@ namespace osu.Game.Screens.Select switch (op) { case Operator.Equal: - textFilter.SearchTerm = value.Trim('"'); + textFilter.SearchTerm = value; return true; default: diff --git a/osu.Game/Screens/Select/FooterButtonOptions.cs b/osu.Game/Screens/Select/FooterButtonOptions.cs index e56efcb458..532051369b 100644 --- a/osu.Game/Screens/Select/FooterButtonOptions.cs +++ b/osu.Game/Screens/Select/FooterButtonOptions.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Game.Graphics; diff --git a/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs b/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs new file mode 100644 index 0000000000..f81036f745 --- /dev/null +++ b/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs @@ -0,0 +1,196 @@ +// 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.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Collections; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; +using osuTK.Input; +using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; + +namespace osu.Game.Screens.Select.FooterV2 +{ + public partial class BeatmapOptionsPopover : OsuPopover + { + private FillFlowContainer buttonFlow = null!; + private readonly FooterButtonOptionsV2 footerButton; + + private WorkingBeatmap beatmapWhenOpening = null!; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + public BeatmapOptionsPopover(FooterButtonOptionsV2 footerButton) + { + this.footerButton = footerButton; + } + + [BackgroundDependencyLoader] + private void load(ManageCollectionsDialog? manageCollectionsDialog, SongSelect? songSelect, OsuColour colours, BeatmapManager? beatmapManager) + { + Content.Padding = new MarginPadding(5); + + Child = buttonFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(3), + }; + + beatmapWhenOpening = beatmap.Value; + + addHeader(CommonStrings.General); + addButton(SongSelectStrings.ManageCollections, FontAwesome.Solid.Book, () => manageCollectionsDialog?.Show()); + + addHeader(SongSelectStrings.ForAllDifficulties, beatmapWhenOpening.BeatmapSetInfo.ToString()); + addButton(SongSelectStrings.DeleteBeatmap, FontAwesome.Solid.Trash, () => songSelect?.DeleteBeatmap(beatmapWhenOpening.BeatmapSetInfo), colours.Red1); + + addHeader(SongSelectStrings.ForSelectedDifficulty, beatmapWhenOpening.BeatmapInfo.DifficultyName); + // TODO: make work, and make show "unplayed" or "played" based on status. + addButton(SongSelectStrings.MarkAsPlayed, FontAwesome.Regular.TimesCircle, null); + addButton(SongSelectStrings.ClearAllLocalScores, FontAwesome.Solid.Eraser, () => songSelect?.ClearScores(beatmapWhenOpening.BeatmapInfo), colours.Red1); + + if (songSelect != null && songSelect.AllowEditing) + addButton(SongSelectStrings.EditBeatmap, FontAwesome.Solid.PencilAlt, () => songSelect.Edit(beatmapWhenOpening.BeatmapInfo)); + + addButton(WebCommonStrings.ButtonsHide.ToSentence(), FontAwesome.Solid.Magic, () => beatmapManager?.Hide(beatmapWhenOpening.BeatmapInfo)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(this)); + + beatmap.BindValueChanged(_ => Hide()); + } + + [Resolved] + private OverlayColourProvider overlayColourProvider { get; set; } = null!; + + private void addHeader(LocalisableString text, string? context = null) + { + var textFlow = new OsuTextFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Padding = new MarginPadding(10), + }; + + textFlow.AddText(text, t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold)); + + if (context != null) + { + textFlow.NewLine(); + textFlow.AddText(context, t => + { + t.Colour = overlayColourProvider.Content2; + t.Font = t.Font.With(size: 13); + }); + } + + buttonFlow.Add(textFlow); + } + + private void addButton(LocalisableString text, IconUsage icon, Action? action, Color4? colour = null) + { + var button = new OptionButton + { + Text = text, + Icon = icon, + TextColour = colour, + Action = () => + { + Scheduler.AddDelayed(Hide, 50); + action?.Invoke(); + }, + }; + + buttonFlow.Add(button); + } + + private partial class OptionButton : OsuButton + { + public IconUsage Icon { get; init; } + public Color4? TextColour { get; init; } + + public OptionButton() + { + Size = new Vector2(265, 50); + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + BackgroundColour = colourProvider.Background3; + + SpriteText.Colour = TextColour ?? Color4.White; + Content.CornerRadius = 10; + + Add(new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(17), + X = 15, + Icon = Icon, + Colour = TextColour ?? Color4.White, + }); + } + + protected override SpriteText CreateText() => new OsuSpriteText + { + Depth = -1, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + X = 40 + }; + } + + protected override bool OnKeyDown(KeyDownEvent e) + { + // don't absorb control as ToolbarRulesetSelector uses control + number to navigate + if (e.ControlPressed) return false; + + if (!e.Repeat && e.Key >= Key.Number1 && e.Key <= Key.Number9) + { + int requested = e.Key - Key.Number1; + + OptionButton? found = buttonFlow.Children.OfType().ElementAtOrDefault(requested); + + if (found != null) + { + found.TriggerClick(); + return true; + } + } + + return base.OnKeyDown(e); + } + + protected override void UpdateState(ValueChangedEvent state) + { + base.UpdateState(state); + + if (state.NewValue == Visibility.Hidden) + footerButton.IsActive.Value = false; + } + } +} diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs index 87cca0042a..a1559d32dc 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs @@ -2,14 +2,21 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Input.Bindings; namespace osu.Game.Screens.Select.FooterV2 { - public partial class FooterButtonOptionsV2 : FooterButtonV2 + public partial class FooterButtonOptionsV2 : FooterButtonV2, IHasPopover { + public readonly BindableBool IsActive = new BindableBool(); + [BackgroundDependencyLoader] private void load(OsuColour colour) { @@ -17,6 +24,34 @@ namespace osu.Game.Screens.Select.FooterV2 Icon = FontAwesome.Solid.Cog; AccentColour = colour.Purple1; Hotkey = GlobalAction.ToggleBeatmapOptions; + + Action = () => IsActive.Toggle(); } + + protected override void LoadComplete() + { + base.LoadComplete(); + + IsActive.BindValueChanged(active => + { + OverlayState.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden; + }); + + OverlayState.BindValueChanged(state => + { + switch (state.NewValue) + { + case Visibility.Hidden: + this.HidePopover(); + break; + + case Visibility.Visible: + this.ShowPopover(); + break; + } + }); + } + + public Popover GetPopover() => new BeatmapOptionsPopover(this); } } diff --git a/osu.Game/Screens/Select/FooterV2/FooterV2.cs b/osu.Game/Screens/Select/FooterV2/FooterV2.cs index cd95f3eb6c..0529f0d082 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterV2.cs @@ -48,11 +48,17 @@ namespace osu.Game.Screens.Select.FooterV2 private FillFlowContainer buttons = null!; - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + public FooterV2() { RelativeSizeAxes = Axes.X; Height = height; + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomLeft; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { InternalChildren = new Drawable[] { new Box diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 2b40b9faf8..58c14b15b9 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -67,9 +67,6 @@ namespace osu.Game.Screens.Select.Leaderboards } } - [Resolved] - private ScoreManager scoreManager { get; set; } = null!; - [Resolved] private IBindable ruleset { get; set; } = null!; @@ -164,7 +161,7 @@ namespace osu.Game.Screens.Select.Leaderboards return; SetScores( - scoreManager.OrderByTotalScore(response.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo))), + response.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo)).OrderByTotalScore(), response.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo) ); }); @@ -196,7 +193,7 @@ namespace osu.Game.Screens.Select.Leaderboards + $" AND {nameof(ScoreInfo.DeletePending)} == false" , beatmapInfo.ID, ruleset.Value.ShortName), localScoresChanged); - void localScoresChanged(IRealmCollection sender, ChangeSet? changes, Exception exception) + void localScoresChanged(IRealmCollection sender, ChangeSet? changes) { if (cancellationToken.IsCancellationRequested) return; @@ -222,7 +219,7 @@ namespace osu.Game.Screens.Select.Leaderboards scores = scores.Where(s => selectedMods.SetEquals(s.Mods.Select(m => m.Acronym))); } - scores = scoreManager.OrderByTotalScore(scores.Detach()); + scores = scores.Detach().OrderByTotalScore(); SetScores(scores); } diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs index b8840b124a..5bcb4c27a7 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.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.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; diff --git a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs index c4add31a4f..cd98872b65 100644 --- a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs +++ b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs @@ -4,9 +4,7 @@ using osu.Framework.Allocation; using osu.Game.Overlays.Dialog; using osu.Game.Scoring; -using System.Diagnostics; using osu.Framework.Graphics.Sprites; -using osu.Game.Beatmaps; namespace osu.Game.Screens.Select { @@ -20,11 +18,8 @@ namespace osu.Game.Screens.Select } [BackgroundDependencyLoader] - private void load(BeatmapManager beatmapManager, ScoreManager scoreManager) + private void load(ScoreManager scoreManager) { - BeatmapInfo? beatmapInfo = beatmapManager.QueryBeatmap(b => b.ID == score.BeatmapInfoID); - Debug.Assert(beatmapInfo != null); - BodyText = $"{score.User} ({score.DisplayAccuracy}, {score.Rank})"; Icon = FontAwesome.Regular.TrashAlt; diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs index 0d3e1238f3..045a518525 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.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 osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs index c92dc2e343..7b631ebfea 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs @@ -32,6 +32,9 @@ namespace osu.Game.Screens.Select.Options public override bool BlockScreenWideMouse => false; + protected override string PopInSampleName => "SongSelect/options-pop-in"; + protected override string PopOutSampleName => "SongSelect/options-pop-out"; + public BeatmapOptionsOverlay() { AutoSizeAxes = Axes.Y; @@ -86,8 +89,6 @@ namespace osu.Game.Screens.Select.Options protected override void PopIn() { - base.PopIn(); - this.FadeIn(transition_duration, Easing.OutQuint); if (buttonsContainer.Position.X == 1 || Alpha == 0) diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index b99d949b43..fe13d6d5a8 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -146,12 +146,24 @@ namespace osu.Game.Screens.Select public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); + revertMods(); + } - if (playerLoader != null) - { - Mods.Value = modsAtGameplayStart; - playerLoader = null; - } + public override bool OnExiting(ScreenExitEvent e) + { + if (base.OnExiting(e)) + return true; + + revertMods(); + return false; + } + + private void revertMods() + { + if (playerLoader == null) return; + + Mods.Value = modsAtGameplayStart; + playerLoader = null; } } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 4d6a5398c5..dfea4e3794 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using Humanizer; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -61,7 +60,7 @@ namespace osu.Game.Screens.Select protected virtual bool ShowFooter => true; - public override bool? AllowTrackAdjustments => true; + public override bool? ApplyModTrackAdjustments => true; /// /// Can be null if is false. @@ -139,7 +138,7 @@ namespace osu.Game.Screens.Select [Resolved] internal IOverlayManager? OverlayManager { get; private set; } - private Bindable configBackgroundBlur { get; set; } = new BindableBool(); + private Bindable configBackgroundBlur = null!; [BackgroundDependencyLoader(true)] private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog? manageCollectionsDialog, DifficultyRecommender? recommender, OsuConfigManager config) @@ -172,11 +171,6 @@ namespace osu.Game.Screens.Select AddRangeInternal(new Drawable[] { - new ResetScrollContainer(() => Carousel.ScrollToSelected()) - { - RelativeSizeAxes = Axes.Y, - Width = 250, - }, new VerticalMaskingContainer { Children = new Drawable[] @@ -244,6 +238,10 @@ namespace osu.Game.Screens.Select Padding = new MarginPadding { Top = left_area_padding }, Children = new Drawable[] { + new LeftSideInteractionContainer(() => Carousel.ScrollToSelected()) + { + RelativeSizeAxes = Axes.Both, + }, beatmapInfoWedge = new BeatmapInfoWedge { Height = WEDGE_HEIGHT, @@ -312,9 +310,9 @@ namespace osu.Game.Screens.Select Footer.AddButton(button, overlay); BeatmapOptions.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, () => manageCollectionsDialog?.Show()); - BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo)); + BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => DeleteBeatmap(Beatmap.Value.BeatmapSetInfo)); BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null); - BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo)); + BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => ClearScores(Beatmap.Value.BeatmapInfo)); } sampleChangeDifficulty = audio.Samples.Get(@"SongSelect/select-difficulty"); @@ -391,6 +389,15 @@ namespace osu.Game.Screens.Select this.Push(new EditorLoader()); } + /// + /// Set the query to the search text box. + /// + /// The string to search. + public void Search(string query) + { + FilterControl.CurrentTextSearch.Value = query; + } + /// /// Call to make a selection and perform the default action for this SongSelect. /// @@ -517,7 +524,11 @@ namespace osu.Game.Screens.Select if (beatmapInfoNoDebounce == null) run(); else - selectionChangedDebounce = Scheduler.AddDelayed(run, 200); + { + // Intentionally slightly higher than repeat_tick_rate to avoid loading songs when holding left / right arrows. + // See https://github.com/ppy/osu-framework/blob/master/osu.Framework/Input/InputManager.cs#L44 + selectionChangedDebounce = Scheduler.AddDelayed(run, 80); + } if (beatmap?.Equals(beatmapInfoPrevious) != true) { @@ -635,7 +646,7 @@ namespace osu.Game.Screens.Select beginLooping(); - if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending) + if (!Beatmap.Value.BeatmapSetInfo.DeletePending) { updateCarouselSelection(); @@ -784,6 +795,8 @@ namespace osu.Game.Screens.Select BeatmapDetails.Beatmap = beatmap; + ModSelect.Beatmap = beatmap; + bool beatmapSelected = beatmap is not DummyWorkingBeatmap; if (beatmapSelected) @@ -864,7 +877,7 @@ namespace osu.Game.Screens.Select { // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 // but also in this case we want support for formatting a number within a string). - FilterControl.InformationalText = $"{"match".ToQuantity(Carousel.CountDisplayed, "#,0")}"; + FilterControl.InformationalText = Carousel.CountDisplayed != 1 ? $"{Carousel.CountDisplayed:#,0} matches" : $"{Carousel.CountDisplayed:#,0} match"; } private bool boundLocalBindables; @@ -917,14 +930,20 @@ namespace osu.Game.Screens.Select return true; } - private void delete(BeatmapSetInfo? beatmap) + /// + /// Request to delete a specific beatmap. + /// + public void DeleteBeatmap(BeatmapSetInfo? beatmap) { if (beatmap == null) return; dialogOverlay?.Push(new BeatmapDeleteDialog(beatmap)); } - private void clearScores(BeatmapInfo? beatmapInfo) + /// + /// Request to clear the scores of a specific beatmap. + /// + public void ClearScores(BeatmapInfo? beatmapInfo) { if (beatmapInfo == null) return; @@ -964,7 +983,7 @@ namespace osu.Game.Screens.Select if (e.ShiftPressed) { if (!Beatmap.IsDefault) - delete(Beatmap.Value.BeatmapSetInfo); + DeleteBeatmap(Beatmap.Value.BeatmapSetInfo); return true; } @@ -997,18 +1016,28 @@ namespace osu.Game.Screens.Select } } - private partial class ResetScrollContainer : Container + /// + /// Handles mouse interactions required when moving away from the carousel. + /// + internal partial class LeftSideInteractionContainer : Container { - private readonly Action? onHoverAction; + private readonly Action? resetCarouselPosition; - public ResetScrollContainer(Action onHoverAction) + public LeftSideInteractionContainer(Action resetCarouselPosition) { - this.onHoverAction = onHoverAction; + this.resetCarouselPosition = resetCarouselPosition; } + // we want to block plain scrolls on the left side so that they don't scroll the carousel, + // but also we *don't* want to handle scrolls when they're combined with keyboard modifiers + // as those will usually correspond to other interactions like adjusting volume. + protected override bool OnScroll(ScrollEvent e) => !e.ControlPressed && !e.AltPressed && !e.ShiftPressed && !e.SuperPressed; + + protected override bool OnMouseDown(MouseDownEvent e) => true; + protected override bool OnHover(HoverEvent e) { - onHoverAction?.Invoke(); + resetCarouselPosition?.Invoke(); return base.OnHover(e); } } diff --git a/osu.Game/Screens/Spectate/SpectatorGameplayState.cs b/osu.Game/Screens/Spectate/SpectatorGameplayState.cs index 498363adef..1ee328a307 100644 --- a/osu.Game/Screens/Spectate/SpectatorGameplayState.cs +++ b/osu.Game/Screens/Spectate/SpectatorGameplayState.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Scoring; diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 2b56767bd0..48b5c210b8 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -90,7 +90,7 @@ namespace osu.Game.Screens.Spectate })); } - private void beatmapsChanged(IRealmCollection items, ChangeSet changes, Exception ___) + private void beatmapsChanged(IRealmCollection items, ChangeSet changes) { if (changes?.InsertedIndices == null) return; diff --git a/osu.Game/Screens/StartupScreen.cs b/osu.Game/Screens/StartupScreen.cs index 84ef3eac78..9e04a238eb 100644 --- a/osu.Game/Screens/StartupScreen.cs +++ b/osu.Game/Screens/StartupScreen.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Overlays; namespace osu.Game.Screens diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index a9b26f13e8..d530efbfdd 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -12,6 +12,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps.Formats; using osu.Game.Extensions; using osu.Game.IO; +using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; using osuTK; @@ -108,11 +109,13 @@ namespace osu.Game.Skinning case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container => { + var health = container.OfType().FirstOrDefault(); var score = container.OfType().FirstOrDefault(); var accuracy = container.OfType().FirstOrDefault(); var combo = container.OfType().FirstOrDefault(); var ppCounter = container.OfType().FirstOrDefault(); var songProgress = container.OfType().FirstOrDefault(); + var keyCounter = container.OfType().FirstOrDefault(); if (score != null) { @@ -126,6 +129,13 @@ namespace osu.Game.Skinning score.Position = new Vector2(0, vertical_offset); + if (health != null) + { + health.Origin = Anchor.TopCentre; + health.Anchor = Anchor.TopCentre; + health.Y = 5; + } + if (ppCounter != null) { ppCounter.Y = score.Position.Y + ppCounter.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).Y - 4; @@ -166,8 +176,20 @@ namespace osu.Game.Skinning if (songProgress != null) { - songProgress.Position = new Vector2(0, -10); + const float padding = 10; + + songProgress.Position = new Vector2(0, -padding); songProgress.Scale = new Vector2(0.9f, 1); + + if (keyCounter != null && hitError != null) + { + // Hard to find this at runtime, so taken from the most expanded state during replay. + const float song_progress_offset_height = 36 + padding; + + keyCounter.Anchor = Anchor.BottomRight; + keyCounter.Origin = Anchor.BottomRight; + keyCounter.Position = new Vector2(-(hitError.Width + padding), -(padding * 2 + song_progress_offset_height)); + } } } }) @@ -177,8 +199,9 @@ namespace osu.Game.Skinning new DefaultComboCounter(), new DefaultScoreCounter(), new DefaultAccuracyCounter(), - new DefaultHealthDisplay(), + new ArgonHealthDisplay(), new ArgonSongProgress(), + new ArgonKeyCounterDisplay(), new BarHitErrorMeter(), new BarHitErrorMeter(), new PerformancePointsCounter() @@ -204,19 +227,24 @@ namespace osu.Game.Skinning switch (global) { case GlobalSkinColours.ComboColours: + { + LogLookupDebug(this, lookup, LookupDebugType.Hit); return SkinUtils.As(new Bindable?>(Configuration.ComboColours)); + } } break; case SkinComboColourLookup comboColour: + LogLookupDebug(this, lookup, LookupDebugType.Hit); return SkinUtils.As(new Bindable(getComboColour(Configuration, comboColour.ColourIndex))); } + LogLookupDebug(this, lookup, LookupDebugType.Miss); return null; } private static Color4 getComboColour(IHasComboColours source, int colourIndex) - => source.ComboColours[colourIndex % source.ComboColours.Count]; + => source.ComboColours![colourIndex % source.ComboColours.Count]; } } diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index 6523039a3f..52c439a624 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -30,7 +30,7 @@ namespace osu.Game.Skinning.Components public Bindable Attribute { get; } = new Bindable(BeatmapAttribute.StarRating); [SettingSource(typeof(BeatmapAttributeTextStrings), nameof(BeatmapAttributeTextStrings.Template), nameof(BeatmapAttributeTextStrings.TemplateDescription))] - public Bindable Template { get; set; } = new Bindable("{Label}: {Value}"); + public Bindable Template { get; } = new Bindable("{Label}: {Value}"); [Resolved] private IBindable beatmap { get; set; } = null!; diff --git a/osu.Game/Skinning/IAnimationTimeReference.cs b/osu.Game/Skinning/IAnimationTimeReference.cs index b6a944ddf8..91f1171a72 100644 --- a/osu.Game/Skinning/IAnimationTimeReference.cs +++ b/osu.Game/Skinning/IAnimationTimeReference.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Textures; using osu.Framework.Timing; +using osuTK; namespace osu.Game.Skinning { @@ -13,7 +14,7 @@ namespace osu.Game.Skinning /// /// /// This should not be used to start an animation immediately at the current time. - /// To do so, use with startAtCurrentTime = true instead. + /// To do so, use with startAtCurrentTime = true instead. /// [Cached] public interface IAnimationTimeReference diff --git a/osu.Game/Skinning/IPooledSampleProvider.cs b/osu.Game/Skinning/IPooledSampleProvider.cs index 3ea299f5e2..0e57050c4d 100644 --- a/osu.Game/Skinning/IPooledSampleProvider.cs +++ b/osu.Game/Skinning/IPooledSampleProvider.cs @@ -8,7 +8,7 @@ namespace osu.Game.Skinning /// /// Provides pooled samples to be used by s. /// - internal interface IPooledSampleProvider + public interface IPooledSampleProvider { /// /// Retrieves a from a pool. diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index c99cdba91c..ed12292eb3 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -17,14 +17,15 @@ namespace osu.Game.Skinning Anchor = Anchor.TopRight; Origin = Anchor.TopRight; - Scale = new Vector2(0.6f); - Margin = new MarginPadding(10); + Scale = new Vector2(0.6f * 0.96f); + Margin = new MarginPadding { Vertical = 9, Horizontal = 17 }; } protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(LegacyFont.Score) { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, + FixedWidth = true, }; } } diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 1cbfda16cf..90eb5fa013 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -19,7 +19,7 @@ namespace osu.Game.Skinning { public class LegacyBeatmapSkin : LegacySkin { - protected override bool AllowManiaSkin => false; + protected override bool AllowManiaConfigLookups => false; protected override bool UseCustomSampleBanks => true; /// @@ -72,6 +72,8 @@ namespace osu.Game.Skinning // If it is decided that we need this due to beatmaps somehow using it, the default (1.0 specified in LegacySkinDecoder.CreateTemplateObject) // needs to be removed else it will cause incorrect skin behaviours. This is due to the config lookup having no context of which skin // it should be returning the version for. + + Skin.LogLookupDebug(this, lookup, Skin.LookupDebugType.Miss); return null; } diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index f785022f84..845fc77394 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Utils; -using osu.Game.Rulesets.Judgements; using osu.Game.Screens.Play.HUD; using osu.Game.Utils; using osuTK; @@ -66,6 +65,7 @@ namespace osu.Game.Skinning marker.Current.BindTo(Current); maxFillWidth = fill.Width; + fill.Width = 0; } protected override void Update() @@ -79,7 +79,7 @@ namespace osu.Game.Skinning marker.Position = fill.Position + new Vector2(fill.DrawWidth, isNewStyle ? fill.DrawHeight / 2 : 0); } - protected override void Flash(JudgementResult result) => marker.Flash(result); + protected override void Flash() => marker.Flash(); private static Texture getTexture(ISkin skin, string name) => skin?.GetTexture($"scorebar-{name}"); @@ -237,7 +237,7 @@ namespace osu.Game.Skinning }); } - public override void Flash(JudgementResult result) + public override void Flash() { bulgeMain(); @@ -256,7 +256,7 @@ namespace osu.Game.Skinning { public Bindable Current { get; } = new Bindable(); - public virtual void Flash(JudgementResult result) + public virtual void Flash() { } } diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index f460a3d31a..9acb29a793 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; @@ -42,6 +40,7 @@ namespace osu.Game.Skinning public float ScorePosition = 300 * POSITION_SCALE_FACTOR; public bool ShowJudgementLine = true; public bool KeysUnderNotes; + public int LightFramePerSecond = 60; public LegacyNoteBodyStyle? NoteBodyStyle; diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index a2408a92bb..cacca0de23 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -30,6 +30,8 @@ namespace osu.Game.Skinning Lookup = lookup; ColumnIndex = columnIndex; } + + public override string ToString() => $"[{nameof(LegacyManiaSkinConfigurationLookup)} lookup:{Lookup} col:{ColumnIndex} totalcols:{TotalColumns}]"; } public enum LegacyManiaSkinConfigurationLookups @@ -72,6 +74,7 @@ namespace osu.Game.Skinning Hit50, Hit0, KeysUnderNotes, - NoteBodyStyle + NoteBodyStyle, + LightFramePerSecond } } diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index e880e3c1ed..b472afb74f 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -123,6 +123,11 @@ namespace osu.Game.Skinning currentConfig.WidthForNoteHeightScale = (float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; break; + case "LightFramePerSecond": + int lightFramePerSecond = int.Parse(pair.Value, CultureInfo.InvariantCulture); + currentConfig.LightFramePerSecond = lightFramePerSecond > 0 ? lightFramePerSecond : 24; + break; + case string when pair.Key.StartsWith("Colour", StringComparison.Ordinal): HandleColours(currentConfig, line, true); break; diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index d8ee6b21de..a86f122836 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -21,13 +21,14 @@ namespace osu.Game.Skinning Origin = Anchor.TopRight; Scale = new Vector2(0.96f); - Margin = new MarginPadding(10); + Margin = new MarginPadding { Horizontal = 10 }; } protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(LegacyFont.Score) { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, + FixedWidth = true, }; } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index e46eaf90c1..dc683f1dae 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using JetBrains.Annotations; @@ -22,19 +23,14 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; +using osuTK; using osuTK.Graphics; namespace osu.Game.Skinning { public class LegacySkin : Skin { - /// - /// Whether texture for the keys exists. - /// Used to determine if the mania ruleset is skinned. - /// - private readonly Lazy hasKeyTexture; - - protected virtual bool AllowManiaSkin => hasKeyTexture.Value; + protected virtual bool AllowManiaConfigLookups => true; /// /// Whether this skin can use samples with a custom bank (custom sample set in stable terminology). @@ -60,10 +56,6 @@ namespace osu.Game.Skinning protected LegacySkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? storage, string configurationFilename = @"skin.ini") : base(skin, resources, storage, configurationFilename) { - // todo: this shouldn't really be duplicated here (from ManiaLegacySkinTransformer). we need to come up with a better solution. - hasKeyTexture = new Lazy(() => this.GetAnimation( - lookupForMania(new LegacyManiaSkinConfigurationLookup(4, LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value ?? "mania-key1", true, - true) != null); } protected override void ParseConfigurationStream(Stream stream) @@ -81,50 +73,61 @@ namespace osu.Game.Skinning } } + [SuppressMessage("ReSharper", "RedundantAssignment")] // for `wasHit` assignments used in `finally` debug logic public override IBindable? GetConfig(TLookup lookup) { - switch (lookup) + bool wasHit = true; + + try { - case GlobalSkinColours colour: - switch (colour) - { - case GlobalSkinColours.ComboColours: - var comboColours = Configuration.ComboColours; - if (comboColours != null) - return SkinUtils.As(new Bindable>(comboColours)); + switch (lookup) + { + case GlobalSkinColours colour: + switch (colour) + { + case GlobalSkinColours.ComboColours: + var comboColours = Configuration.ComboColours; + if (comboColours != null) + return SkinUtils.As(new Bindable>(comboColours)); - break; + break; - default: - return SkinUtils.As(getCustomColour(Configuration, colour.ToString())); - } + default: + return SkinUtils.As(getCustomColour(Configuration, colour.ToString())); + } - break; - - case SkinComboColourLookup comboColour: - return SkinUtils.As(GetComboColour(Configuration, comboColour.ColourIndex, comboColour.Combo)); - - case SkinCustomColourLookup customColour: - return SkinUtils.As(getCustomColour(Configuration, customColour.Lookup.ToString() ?? string.Empty)); - - case LegacyManiaSkinConfigurationLookup maniaLookup: - if (!AllowManiaSkin) break; - var result = lookupForMania(maniaLookup); - if (result != null) - return result; + case SkinComboColourLookup comboColour: + return SkinUtils.As(GetComboColour(Configuration, comboColour.ColourIndex, comboColour.Combo)); - break; + case SkinCustomColourLookup customColour: + return SkinUtils.As(getCustomColour(Configuration, customColour.Lookup.ToString() ?? string.Empty)); - case SkinConfiguration.LegacySetting legacy: - return legacySettingLookup(legacy); + case LegacyManiaSkinConfigurationLookup maniaLookup: + if (!AllowManiaConfigLookups) + break; - default: - return genericLookup(lookup); + var result = lookupForMania(maniaLookup); + if (result != null) + return result; + + break; + + case SkinConfiguration.LegacySetting legacy: + return legacySettingLookup(legacy); + + default: + return genericLookup(lookup); + } + + wasHit = false; + return null; + } + finally + { + LogLookupDebug(this, lookup, wasHit ? LookupDebugType.Hit : LookupDebugType.Miss); } - - return null; } private IBindable? lookupForMania(LegacyManiaSkinConfigurationLookup maniaLookup) @@ -270,6 +273,9 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.KeysUnderNotes: return SkinUtils.As(new Bindable(existing.KeysUnderNotes)); + + case LegacyManiaSkinConfigurationLookups.LightFramePerSecond: + return SkinUtils.As(new Bindable(existing.LightFramePerSecond)); } return null; @@ -367,17 +373,27 @@ namespace osu.Game.Skinning { songProgress.Anchor = Anchor.TopRight; songProgress.Origin = Anchor.CentreRight; - songProgress.X = -accuracy.ScreenSpaceDeltaToParentSpace(accuracy.ScreenSpaceDrawQuad.Size).X - 10; + songProgress.X = -accuracy.ScreenSpaceDeltaToParentSpace(accuracy.ScreenSpaceDrawQuad.Size).X - 18; songProgress.Y = container.ToLocalSpace(accuracy.ScreenSpaceDrawQuad.TopLeft).Y + (accuracy.ScreenSpaceDeltaToParentSpace(accuracy.ScreenSpaceDrawQuad.Size).Y / 2); } var hitError = container.OfType().FirstOrDefault(); + var keyCounter = container.OfType().FirstOrDefault(); if (hitError != null) { hitError.Anchor = Anchor.BottomCentre; hitError.Origin = Anchor.CentreLeft; hitError.Rotation = -90; + + if (keyCounter != null) + { + const float padding = 10; + + keyCounter.Anchor = Anchor.BottomRight; + keyCounter.Origin = Anchor.BottomRight; + keyCounter.Position = new Vector2(-padding, -(padding + hitError.Width)); + } } }) { @@ -386,9 +402,10 @@ namespace osu.Game.Skinning new LegacyComboCounter(), new LegacyScoreCounter(), new LegacyAccuracyCounter(), - new LegacyHealthDisplay(), new LegacySongProgress(), + new LegacyHealthDisplay(), new BarHitErrorMeter(), + new DefaultKeyCounterDisplay() } }; } @@ -456,6 +473,13 @@ namespace osu.Game.Skinning public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) { + switch (componentName) + { + case "Menu/fountain-star": + componentName = "star2"; + break; + } + foreach (string name in getFallbackNames(componentName)) { // some component names (especially user-controlled ones, like `HitX` in mania) diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 0d2461567f..62197fa8a7 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -9,8 +9,10 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osuTK; using static osu.Game.Skinning.SkinConfiguration; namespace osu.Game.Skinning @@ -18,16 +20,16 @@ namespace osu.Game.Skinning public static partial class LegacySkinExtensions { public static Drawable? GetAnimation(this ISkin? source, string componentName, bool animatable, bool looping, bool applyConfigFrameRate = false, string animationSeparator = "-", - bool startAtCurrentTime = true, double? frameLength = null) - => source.GetAnimation(componentName, default, default, animatable, looping, applyConfigFrameRate, animationSeparator, startAtCurrentTime, frameLength); + bool startAtCurrentTime = true, double? frameLength = null, Vector2? maxSize = null) + => source.GetAnimation(componentName, default, default, animatable, looping, applyConfigFrameRate, animationSeparator, startAtCurrentTime, frameLength, maxSize); public static Drawable? GetAnimation(this ISkin? source, string componentName, WrapMode wrapModeS, WrapMode wrapModeT, bool animatable, bool looping, bool applyConfigFrameRate = false, - string animationSeparator = "-", bool startAtCurrentTime = true, double? frameLength = null) + string animationSeparator = "-", bool startAtCurrentTime = true, double? frameLength = null, Vector2? maxSize = null) { if (source == null) return null; - var textures = GetTextures(source, componentName, wrapModeS, wrapModeT, animatable, animationSeparator, out var retrievalSource); + var textures = GetTextures(source, componentName, wrapModeS, wrapModeT, animatable, animationSeparator, maxSize, out var retrievalSource); switch (textures.Length) { @@ -53,7 +55,7 @@ namespace osu.Game.Skinning } } - public static Texture[] GetTextures(this ISkin? source, string componentName, WrapMode wrapModeS, WrapMode wrapModeT, bool animatable, string animationSeparator, out ISkin? retrievalSource) + public static Texture[] GetTextures(this ISkin? source, string componentName, WrapMode wrapModeS, WrapMode wrapModeT, bool animatable, string animationSeparator, Vector2? maxSize, out ISkin? retrievalSource) { retrievalSource = null; @@ -80,6 +82,9 @@ namespace osu.Game.Skinning // if an animation was not allowed or not found, fall back to a sprite retrieval. var singleTexture = retrievalSource.GetTexture(componentName, wrapModeS, wrapModeT); + if (singleTexture != null && maxSize != null) + singleTexture = singleTexture.WithMaximumSize(maxSize.Value); + return singleTexture != null ? new[] { singleTexture } : Array.Empty(); @@ -88,11 +93,14 @@ namespace osu.Game.Skinning { for (int i = 0; true; i++) { - Texture? texture; + var texture = skin.GetTexture(getFrameName(i), wrapModeS, wrapModeT); - if ((texture = skin.GetTexture(getFrameName(i), wrapModeS, wrapModeT)) == null) + if (texture == null) break; + if (maxSize != null) + texture = texture.WithMaximumSize(maxSize.Value); + yield return texture; } } @@ -100,6 +108,18 @@ namespace osu.Game.Skinning string getFrameName(int frameIndex) => $"{componentName}{animationSeparator}{frameIndex}"; } + public static Texture WithMaximumSize(this Texture texture, Vector2 maxSize) + { + if (texture.DisplayWidth <= maxSize.X && texture.DisplayHeight <= maxSize.Y) + return texture; + + maxSize *= texture.ScaleAdjust; + + var croppedTexture = texture.Crop(new RectangleF(texture.Width / 2f - maxSize.X / 2f, texture.Height / 2f - maxSize.Y / 2f, maxSize.X, maxSize.Y)); + croppedTexture.ScaleAdjust = texture.ScaleAdjust; + return croppedTexture; + } + public static bool HasFont(this ISkin source, LegacyFont font) { return source.GetTexture($"{source.GetFontPrefix(font)}-0") != null; @@ -180,7 +200,11 @@ namespace osu.Game.Skinning } } - private const double default_frame_time = 1000 / 60d; + /// + /// The frame length of each frame at a 60 FPS rate. + /// Default frame rate for legacy skin animations. + /// + public const double SIXTY_FRAME_TIME = 1000 / 60d; private static double getFrameLength(ISkin source, bool applyConfigFrameRate, Texture[] textures) { @@ -194,7 +218,7 @@ namespace osu.Game.Skinning return 1000f / textures.Length; } - return default_frame_time; + return SIXTY_FRAME_TIME; } } } diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index d6af52855b..041a32e8de 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -1,6 +1,7 @@ // 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.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; @@ -12,6 +13,9 @@ namespace osu.Game.Skinning { public sealed partial class LegacySpriteText : OsuSpriteText { + public Vector2? MaxSizePerGlyph { get; init; } + public bool FixedWidth { get; init; } + private readonly LegacyFont font; private LegacyGlyphStore glyphStore = null!; @@ -20,9 +24,19 @@ namespace osu.Game.Skinning protected override char[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x' }; + // ReSharper disable once UnusedMember.Global + // being unused is the point here + public new FontUsage Font + { + get => base.Font; + set => throw new InvalidOperationException(@"Attempting to use this setter will not work correctly. " + + $@"Use specific init-only properties exposed by {nameof(LegacySpriteText)} instead."); + } + public LegacySpriteText(LegacyFont font) { this.font = font; + Shadow = false; UseFullGlyphHeight = false; } @@ -30,10 +44,10 @@ namespace osu.Game.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin) { - Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: true); + base.Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: FixedWidth); Spacing = new Vector2(-skin.GetFontOverlap(font), 0); - glyphStore = new LegacyGlyphStore(skin); + glyphStore = new LegacyGlyphStore(skin, MaxSizePerGlyph); } protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore); @@ -41,10 +55,12 @@ namespace osu.Game.Skinning private class LegacyGlyphStore : ITexturedGlyphLookupStore { private readonly ISkin skin; + private readonly Vector2? maxSize; - public LegacyGlyphStore(ISkin skin) + public LegacyGlyphStore(ISkin skin, Vector2? maxSize) { this.skin = skin; + this.maxSize = maxSize; } public ITexturedCharacterGlyph? Get(string fontName, char character) @@ -56,6 +72,9 @@ namespace osu.Game.Skinning if (texture == null) return null; + if (maxSize != null) + texture = texture.WithMaximumSize(maxSize.Value); + return new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null), texture, 1f / texture.ScaleAdjust); } diff --git a/osu.Game/Skinning/RealmBackedResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs index cc887a7a61..cce099a268 100644 --- a/osu.Game/Skinning/RealmBackedResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -38,7 +38,7 @@ namespace osu.Game.Skinning realmSubscription?.Dispose(); } - private void skinChanged(IRealmCollection sender, ChangeSet changes, Exception error) => invalidateCache(); + private void skinChanged(IRealmCollection sender, ChangeSet? changes) => invalidateCache(); protected override IEnumerable GetFilenames(string name) { diff --git a/osu.Game/Skinning/ResourceStoreBackedSkin.cs b/osu.Game/Skinning/ResourceStoreBackedSkin.cs index f5c6192ba5..206c400a88 100644 --- a/osu.Game/Skinning/ResourceStoreBackedSkin.cs +++ b/osu.Game/Skinning/ResourceStoreBackedSkin.cs @@ -46,7 +46,10 @@ namespace osu.Game.Skinning public IBindable? GetConfig(TLookup lookup) where TLookup : notnull where TValue : notnull - => null; + { + Skin.LogLookupDebug(this, lookup, Skin.LookupDebugType.Miss); + return null; + } public void Dispose() { diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index a6250d7488..1e312142d7 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -6,10 +6,13 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; +using System.Threading; using Newtonsoft.Json; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; @@ -54,6 +57,8 @@ namespace osu.Game.Skinning private readonly RealmBackedResourceStore? realmBackedStorage; + public string Name { get; } + /// /// Construct a new skin. /// @@ -63,6 +68,8 @@ namespace osu.Game.Skinning /// An optional filename to read the skin configuration from. If not provided, the configuration will be retrieved from the storage using "skin.ini". protected Skin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? storage = null, string configurationFilename = @"skin.ini") { + Name = skin.Name; + if (resources != null) { SkinInfo = skin.ToLive(resources.RealmAccess); @@ -81,7 +88,7 @@ namespace osu.Game.Skinning } Samples = samples; - Textures = new TextureStore(resources.Renderer, new MaxDimensionLimitedTextureLoaderStore(resources.CreateTextureLoaderStore(storage))); + Textures = new TextureStore(resources.Renderer, CreateTextureLoaderStore(resources, storage)); } else { @@ -98,7 +105,14 @@ namespace osu.Game.Skinning Debug.Assert(Configuration != null); } else - Configuration = new SkinConfiguration(); + { + Configuration = new SkinConfiguration + { + // generally won't be hit as we always write a `skin.ini` on import, but best be safe than sorry. + // see https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/Graphics/Skinning/SkinManager.cs#L297-L298 + LegacyVersion = SkinConfiguration.LATEST_VERSION, + }; + } // skininfo files may be null for default skin. foreach (SkinComponentsContainerLookup.TargetArea skinnableTarget in Enum.GetValues()) @@ -157,6 +171,9 @@ namespace osu.Game.Skinning } } + protected virtual IResourceStore CreateTextureLoaderStore(IStorageResourceProvider resources, IResourceStore storage) + => new MaxDimensionLimitedTextureLoaderStore(resources.CreateTextureLoaderStore(storage)); + protected virtual void ParseConfigurationStream(Stream stream) { using (LineBufferedReader reader = new LineBufferedReader(stream, true)) @@ -190,7 +207,7 @@ namespace osu.Game.Skinning { // This fallback is important for user skins which use SkinnableSprites. case SkinnableSprite.SpriteComponentLookup sprite: - return this.GetAnimation(sprite.LookupName, false, false); + return this.GetAnimation(sprite.LookupName, false, false, maxSize: sprite.MaxSize); case SkinComponentsContainerLookup containerLookup: @@ -239,5 +256,50 @@ namespace osu.Game.Skinning } #endregion + + public override string ToString() => $"{GetType().ReadableName()} {{ Name: {Name} }}"; + + private static readonly ThreadLocal nested_level = new ThreadLocal(() => 0); + + [Conditional("SKIN_LOOKUP_DEBUG")] + internal static void LogLookupDebug(object callingClass, object lookup, LookupDebugType type, [CallerMemberName] string callerMethod = "") + { + string icon = string.Empty; + int level = nested_level.Value; + + switch (type) + { + case LookupDebugType.Hit: + icon = "🟢 hit"; + break; + + case LookupDebugType.Miss: + icon = "🔴 miss"; + break; + + case LookupDebugType.Enter: + nested_level.Value++; + break; + + case LookupDebugType.Exit: + nested_level.Value--; + if (nested_level.Value == 0) + Logger.Log(string.Empty); + return; + } + + string lookupString = lookup.ToString() ?? string.Empty; + string callingClassString = callingClass.ToString() ?? string.Empty; + + Logger.Log($"{string.Join(null, Enumerable.Repeat("|-", level))}{callingClassString}.{callerMethod}(lookup: {lookupString}) {icon}"); + } + + internal enum LookupDebugType + { + Hit, + Miss, + Enter, + Exit + } } } diff --git a/osu.Game/Skinning/SkinComponentsContainerLookup.cs b/osu.Game/Skinning/SkinComponentsContainerLookup.cs index fbc0ab58ad..34358c3f06 100644 --- a/osu.Game/Skinning/SkinComponentsContainerLookup.cs +++ b/osu.Game/Skinning/SkinComponentsContainerLookup.cs @@ -68,7 +68,10 @@ namespace osu.Game.Skinning MainHUDComponents, [Description("Song select")] - SongSelect + SongSelect, + + [Description("Playfield")] + Playfield } } } diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index 43760c4a19..3e948a8afb 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -39,7 +39,7 @@ namespace osu.Game.Skinning protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == @".osk"; - protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name ?? @"No name" }; + protected override SkinInfo CreateModel(ArchiveReader archive, ImportParameters parameters) => new SkinInfo { Name = archive.Name ?? @"No name" }; private const string unknown_creator_string = @"Unknown"; @@ -118,7 +118,7 @@ namespace osu.Game.Skinning string nameLine = @$"Name: {item.Name}"; string authorLine = @$"Author: {item.Creator}"; - string[] newLines = + List newLines = new List { @"// The following content was automatically added by osu! during import, based on filename / folder metadata.", @"[General]", @@ -130,6 +130,10 @@ namespace osu.Game.Skinning if (existingFile == null) { + // skins without a skin.ini are supposed to import using the "latest version" spec. + // see https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/Graphics/Skinning/SkinManager.cs#L297-L298 + newLines.Add($"Version: {SkinConfiguration.LATEST_VERSION}"); + // In the case a skin doesn't have a skin.ini yet, let's create one. writeNewSkinIni(); } @@ -198,7 +202,7 @@ namespace osu.Game.Skinning using (var streamContent = new MemoryStream(Encoding.UTF8.GetBytes(skinInfoJson))) { - modelManager.AddFile(s, streamContent, skin_info_file, s.Realm); + modelManager.AddFile(s, streamContent, skin_info_file, s.Realm!); } // Then serialise each of the drawable component groups into respective files. @@ -213,9 +217,9 @@ namespace osu.Game.Skinning var oldFile = s.GetFile(filename); if (oldFile != null) - modelManager.ReplaceFile(oldFile, streamContent, s.Realm); + modelManager.ReplaceFile(oldFile, streamContent, s.Realm!); else - modelManager.AddFile(s, streamContent, filename, s.Realm); + modelManager.AddFile(s, streamContent, filename, s.Realm!); } } diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index c2b80b7ead..9763d3b57e 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -14,7 +14,7 @@ namespace osu.Game.Skinning { [MapTo("Skin")] [JsonObject(MemberSerialization.OptIn)] - public class SkinInfo : RealmObject, IHasRealmFiles, IEquatable, IHasGuidPrimaryKey, ISoftDelete, IHasNamedFiles + public class SkinInfo : RealmObject, IHasRealmFiles, IEquatable, IHasGuidPrimaryKey, ISoftDelete { internal static readonly Guid TRIANGLES_SKIN = new Guid("2991CFD8-2140-469A-BCB9-2EC23FBCE4AD"); internal static readonly Guid ARGON_SKIN = new Guid("CFFA69DE-B3E3-4DEE-8563-3C4F425C05D0"); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 51605c6045..59c2a8bca0 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -182,7 +182,10 @@ namespace osu.Game.Skinning Name = NamingUtils.GetNextBestName(existingSkinNames, $@"{s.Name} (modified)") }; - var result = skinImporter.ImportModel(skinInfo); + var result = skinImporter.ImportModel(skinInfo, parameters: new ImportParameters + { + ImportImmediately = true // to avoid possible deadlocks when editing skin during gameplay. + }); if (result != null) { @@ -261,13 +264,22 @@ namespace osu.Game.Skinning private T lookupWithFallback(Func lookupFunction) where T : class { - foreach (var source in AllSources) + try { - if (lookupFunction(source) is T skinSourced) - return skinSourced; - } + Skin.LogLookupDebug(this, lookupFunction, Skin.LookupDebugType.Enter); - return null; + foreach (var source in AllSources) + { + if (lookupFunction(source) is T skinSourced) + return skinSourced; + } + + return null; + } + finally + { + Skin.LogLookupDebug(this, lookupFunction, Skin.LookupDebugType.Exit); + } } #region IResourceStorageProvider diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index 2612e0b47c..acb15da80e 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; @@ -161,17 +162,26 @@ namespace osu.Game.Skinning where TLookup : notnull where TValue : notnull { - foreach (var (_, lookupWrapper) in skinSources) + try { - IBindable? bindable; - if ((bindable = lookupWrapper.GetConfig(lookup)) != null) - return bindable; + Skin.LogLookupDebug(this, lookup, Skin.LookupDebugType.Enter); + + foreach (var (_, lookupWrapper) in skinSources) + { + IBindable? bindable; + if ((bindable = lookupWrapper.GetConfig(lookup)) != null) + return bindable; + } + + if (!AllowFallingBackToParent) + return null; + + return ParentSource?.GetConfig(lookup); + } + finally + { + Skin.LogLookupDebug(this, lookup, Skin.LookupDebugType.Exit); } - - if (!AllowFallingBackToParent) - return null; - - return ParentSource?.GetConfig(lookup); } /// @@ -271,25 +281,36 @@ namespace osu.Game.Skinning where TLookup : notnull where TValue : notnull { - switch (lookup) + try { - case GlobalSkinColours: - case SkinComboColourLookup: - case SkinCustomColourLookup: - if (provider.AllowColourLookup) - return skin.GetConfig(lookup); + Skin.LogLookupDebug(this, lookup, Skin.LookupDebugType.Enter); - break; + switch (lookup) + { + case GlobalSkinColours: + case SkinComboColourLookup: + case SkinCustomColourLookup: + if (provider.AllowColourLookup) + return skin.GetConfig(lookup); - default: - if (provider.AllowConfigurationLookup) - return skin.GetConfig(lookup); + break; - break; + default: + if (provider.AllowConfigurationLookup) + return skin.GetConfig(lookup); + + break; + } + + return null; + } + finally + { + Skin.LogLookupDebug(this, lookup, Skin.LookupDebugType.Exit); } - - return null; } + + public override string ToString() => $"{GetType().ReadableName()} {{ Skin: {skin} }}"; } } } diff --git a/osu.Game/Skinning/SkinTransformer.cs b/osu.Game/Skinning/SkinTransformer.cs index ed5b04da1e..95572aa7b1 100644 --- a/osu.Game/Skinning/SkinTransformer.cs +++ b/osu.Game/Skinning/SkinTransformer.cs @@ -34,6 +34,19 @@ namespace osu.Game.Skinning public virtual ISample? GetSample(ISampleInfo sampleInfo) => Skin.GetSample(sampleInfo); - public virtual IBindable? GetConfig(TLookup lookup) where TLookup : notnull where TValue : notnull => Skin.GetConfig(lookup); + public virtual IBindable? GetConfig(TLookup lookup) where TLookup : notnull where TValue : notnull + { + try + { + Skinning.Skin.LogLookupDebug(this, lookup, Skinning.Skin.LookupDebugType.Enter); + return Skin.GetConfig(lookup); + } + finally + { + Skinning.Skin.LogLookupDebug(this, lookup, Skinning.Skin.LookupDebugType.Exit); + } + } + + public override string ToString() => $"{nameof(SkinTransformer)} {{ Skin: {Skin} }}"; } } diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 59b3799e0a..f866a4f8ec 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -20,6 +20,12 @@ namespace osu.Game.Skinning /// public partial class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent { + /// + /// The minimum allowable volume for . + /// that specify a lower will be forcibly pulled up to this volume. + /// + public int MinimumSampleVolume { get; set; } + public override bool RemoveWhenNotAlive => false; public override bool RemoveCompletedTransforms => false; @@ -156,7 +162,7 @@ namespace osu.Game.Skinning { var sample = samplePool?.GetPooledSample(s) ?? new PoolableSkinnableSample(s); sample.Looping = Looping; - sample.Volume.Value = s.Volume / 100.0; + sample.Volume.Value = Math.Max(s.Volume, MinimumSampleVolume) / 100.0; samplesContainer.Add(sample); } diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index 1d97566470..9effb483c4 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -34,8 +34,8 @@ namespace osu.Game.Skinning [Resolved] private ISkinSource source { get; set; } = null!; - public SkinnableSprite(string textureName, ConfineMode confineMode = ConfineMode.NoScaling) - : base(new SpriteComponentLookup(textureName), confineMode) + public SkinnableSprite(string textureName, Vector2? maxSize = null, ConfineMode confineMode = ConfineMode.NoScaling) + : base(new SpriteComponentLookup(textureName, maxSize), confineMode) { SpriteName.Value = textureName; } @@ -56,10 +56,14 @@ namespace osu.Game.Skinning protected override Drawable CreateDefault(ISkinComponentLookup lookup) { - var texture = textures.Get(((SpriteComponentLookup)lookup).LookupName); + var spriteLookup = (SpriteComponentLookup)lookup; + var texture = textures.Get(spriteLookup.LookupName); if (texture == null) - return new SpriteNotFound(((SpriteComponentLookup)lookup).LookupName); + return new SpriteNotFound(spriteLookup.LookupName); + + if (spriteLookup.MaxSize != null) + texture = texture.WithMaximumSize(spriteLookup.MaxSize.Value); return new Sprite { Texture = texture }; } @@ -69,10 +73,12 @@ namespace osu.Game.Skinning internal class SpriteComponentLookup : ISkinComponentLookup { public string LookupName { get; set; } + public Vector2? MaxSize { get; set; } - public SpriteComponentLookup(string textureName) + public SpriteComponentLookup(string textureName, Vector2? maxSize = null) { LookupName = textureName; + MaxSize = maxSize; } } diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index e88b827807..a2dca5d333 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -90,6 +90,8 @@ namespace osu.Game.Skinning var accuracy = container.OfType().FirstOrDefault(); var combo = container.OfType().FirstOrDefault(); var ppCounter = container.OfType().FirstOrDefault(); + var songProgress = container.OfType().FirstOrDefault(); + var keyCounter = container.OfType().FirstOrDefault(); if (score != null) { @@ -141,6 +143,18 @@ namespace osu.Game.Skinning hitError2.Origin = Anchor.CentreLeft; } } + + if (songProgress != null && keyCounter != null) + { + const float padding = 10; + + // Hard to find this at runtime, so taken from the most expanded state during replay. + const float song_progress_offset_height = 73; + + keyCounter.Anchor = Anchor.BottomRight; + keyCounter.Origin = Anchor.BottomRight; + keyCounter.Position = new Vector2(-padding, -(song_progress_offset_height + padding)); + } }) { Children = new Drawable[] @@ -150,6 +164,7 @@ namespace osu.Game.Skinning new DefaultAccuracyCounter(), new DefaultHealthDisplay(), new DefaultSongProgress(), + new DefaultKeyCounterDisplay(), new BarHitErrorMeter(), new BarHitErrorMeter(), new PerformancePointsCounter() @@ -175,19 +190,24 @@ namespace osu.Game.Skinning switch (global) { case GlobalSkinColours.ComboColours: + { + LogLookupDebug(this, lookup, LookupDebugType.Hit); return SkinUtils.As(new Bindable?>(Configuration.ComboColours)); + } } break; case SkinComboColourLookup comboColour: + LogLookupDebug(this, lookup, LookupDebugType.Hit); return SkinUtils.As(new Bindable(getComboColour(Configuration, comboColour.ColourIndex))); } + LogLookupDebug(this, lookup, LookupDebugType.Miss); return null; } private static Color4 getComboColour(IHasComboColours source, int colourIndex) - => source.ComboColours[colourIndex % source.ComboColours.Count]; + => source.ComboColours![colourIndex % source.ComboColours.Count]; } } diff --git a/osu.Game/Storyboards/CommandLoop.cs b/osu.Game/Storyboards/CommandLoop.cs index 29e034d86c..480d69c12f 100644 --- a/osu.Game/Storyboards/CommandLoop.cs +++ b/osu.Game/Storyboards/CommandLoop.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.Collections.Generic; diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs index d198ed68bd..0b96db6861 100644 --- a/osu.Game/Storyboards/CommandTimelineGroup.cs +++ b/osu.Game/Storyboards/CommandTimelineGroup.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 osuTK; using osuTK.Graphics; diff --git a/osu.Game/Storyboards/CommandTrigger.cs b/osu.Game/Storyboards/CommandTrigger.cs index 50f3f0ef49..011f345df2 100644 --- a/osu.Game/Storyboards/CommandTrigger.cs +++ b/osu.Game/Storyboards/CommandTrigger.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 - namespace osu.Game.Storyboards { public class CommandTrigger : CommandTimelineGroup diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index e674e7512c..fc5ef12fb8 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -1,28 +1,29 @@ // 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.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; -using osuTK; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play; +using osuTK; namespace osu.Game.Storyboards.Drawables { public partial class DrawableStoryboard : Container { - [Cached] + [Cached(typeof(Storyboard))] public Storyboard Storyboard { get; } /// @@ -34,7 +35,7 @@ namespace osu.Game.Storyboards.Drawables protected override Container Content { get; } - protected override Vector2 DrawScale => new Vector2(Parent.DrawHeight / 480); + protected override Vector2 DrawScale => new Vector2(Parent!.DrawHeight / 480); private bool passing = true; @@ -57,12 +58,18 @@ namespace osu.Game.Storyboards.Drawables [Cached(typeof(IReadOnlyList))] public IReadOnlyList Mods { get; } - private DependencyContainer dependencies; + [Resolved] + private GameHost host { get; set; } = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + private DependencyContainer dependencies = null!; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - public DrawableStoryboard(Storyboard storyboard, IReadOnlyList mods = null) + public DrawableStoryboard(Storyboard storyboard, IReadOnlyList? mods = null) { Storyboard = storyboard; Mods = mods ?? Array.Empty(); @@ -85,12 +92,15 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader(true)] - private void load(IGameplayClock clock, CancellationToken? cancellationToken, GameHost host, RealmAccess realm) + private void load(IGameplayClock? clock, CancellationToken? cancellationToken) { if (clock != null) Clock = clock; - dependencies.Cache(new TextureStore(host.Renderer, host.CreateTextureLoaderStore(new RealmFileStore(realm, host.Storage).Store), false, scaleAdjust: 1)); + dependencies.CacheAs(typeof(TextureStore), + new TextureStore(host.Renderer, host.CreateTextureLoaderStore( + CreateResourceLookupStore() + ), false, scaleAdjust: 1)); foreach (var layer in Storyboard.Layers) { @@ -102,6 +112,8 @@ namespace osu.Game.Storyboards.Drawables lastEventEndTime = Storyboard.LatestEventTime; } + protected virtual IResourceStore CreateResourceLookupStore() => new StoryboardResourceLookupStore(Storyboard, realm, host); + protected override void Update() { base.Update(); @@ -115,5 +127,50 @@ namespace osu.Game.Storyboards.Drawables foreach (var layer in Children) layer.Enabled = passing ? layer.Layer.VisibleWhenPassing : layer.Layer.VisibleWhenFailing; } + + private class StoryboardResourceLookupStore : IResourceStore + { + private readonly IResourceStore realmFileStore; + private readonly Storyboard storyboard; + + public StoryboardResourceLookupStore(Storyboard storyboard, RealmAccess realm, GameHost host) + { + realmFileStore = new RealmFileStore(realm, host.Storage).Store; + this.storyboard = storyboard; + } + + public void Dispose() => + realmFileStore.Dispose(); + + public byte[] Get(string name) + { + string? storagePath = storyboard.GetStoragePathFromStoryboardPath(name); + + return string.IsNullOrEmpty(storagePath) + ? null! + : realmFileStore.Get(storagePath); + } + + public Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) + { + string? storagePath = storyboard.GetStoragePathFromStoryboardPath(name); + + return string.IsNullOrEmpty(storagePath) + ? Task.FromResult(null!) + : realmFileStore.GetAsync(storagePath, cancellationToken); + } + + public Stream? GetStream(string name) + { + string? storagePath = storyboard.GetStoragePathFromStoryboardPath(name); + + return string.IsNullOrEmpty(storagePath) + ? null + : realmFileStore.GetStream(storagePath); + } + + public IEnumerable GetAvailableResources() => + realmFileStore.GetAvailableResources(); + } } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index be77c9a98e..cefd51b2aa 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -94,27 +94,19 @@ namespace osu.Game.Storyboards.Drawables [Resolved] private IBeatSyncProvider beatSyncProvider { get; set; } + [Resolved] + private TextureStore textureStore { get; set; } + [BackgroundDependencyLoader] - private void load(TextureStore textureStore, Storyboard storyboard) + private void load(Storyboard storyboard) { - int frameIndex = 0; - - Texture frameTexture = storyboard.GetTextureFromPath(getFramePath(frameIndex), textureStore); - - if (frameTexture != null) + if (storyboard.UseSkinSprites) { - // sourcing from storyboard. - for (frameIndex = 0; frameIndex < Animation.FrameCount; frameIndex++) - { - AddFrame(storyboard.GetTextureFromPath(getFramePath(frameIndex), textureStore), Animation.FrameDelay); - } - } - else if (storyboard.UseSkinSprites) - { - // fallback to skin if required. skin.SourceChanged += skinSourceChanged; skinSourceChanged(); } + else + addFramesFromStoryboardSource(); Animation.ApplyTransforms(this); } @@ -128,7 +120,7 @@ namespace osu.Game.Storyboards.Drawables // // In the case of storyboard animations, we want to synchronise with game time perfectly // so let's get a correct time based on gameplay clock and earliest transform. - PlaybackPosition = (beatSyncProvider.Clock?.CurrentTime ?? Clock.CurrentTime) - Animation.EarliestTransformTime; + PlaybackPosition = beatSyncProvider.Clock.CurrentTime - Animation.EarliestTransformTime; } private void skinSourceChanged() @@ -137,11 +129,28 @@ namespace osu.Game.Storyboards.Drawables // When reading from a skin, we match stables weird behaviour where `FrameCount` is ignored // and resources are retrieved until the end of the animation. - foreach (var texture in skin.GetTextures(Path.GetFileNameWithoutExtension(Animation.Path)!, default, default, true, string.Empty, out _)) - AddFrame(texture, Animation.FrameDelay); + var skinTextures = skin.GetTextures(Path.GetFileNameWithoutExtension(Animation.Path)!, default, default, true, string.Empty, null, out _); + + if (skinTextures.Length > 0) + { + foreach (var texture in skinTextures) + AddFrame(texture, Animation.FrameDelay); + } + else + { + addFramesFromStoryboardSource(); + } } - private string getFramePath(int i) => Animation.Path.Replace(".", $"{i}."); + private void addFramesFromStoryboardSource() + { + int frameIndex; + // sourcing from storyboard. + for (frameIndex = 0; frameIndex < Animation.FrameCount; frameIndex++) + AddFrame(textureStore.Get(getFramePath(frameIndex)), Animation.FrameDelay); + + string getFramePath(int i) => Animation.Path.Replace(".", $"{i}."); + } protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs index 6fc8d124c7..40842fe7ed 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs @@ -1,8 +1,7 @@ // 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 System.Threading; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -32,10 +31,12 @@ namespace osu.Game.Storyboards.Drawables InternalChild = ElementContainer = new LayerElementContainer(layer); } - protected partial class LayerElementContainer : LifetimeManagementContainer + public partial class LayerElementContainer : LifetimeManagementContainer { private readonly StoryboardLayer storyboardLayer; + public IEnumerable Elements => InternalChildren; + public LayerElementContainer(StoryboardLayer layer) { storyboardLayer = layer; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index c281d23804..830b6a5caa 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.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.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -30,8 +28,8 @@ namespace osu.Game.Storyboards.Drawables LifetimeStart = sampleInfo.StartTime; } - [Resolved(CanBeNull = true)] - private IReadOnlyList mods { get; set; } + [Resolved] + private IReadOnlyList? mods { get; set; } protected override void SkinChanged(ISkinSource skin) { diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 400d33481c..ec875219b6 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -1,10 +1,9 @@ // 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 osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; @@ -75,6 +74,12 @@ namespace osu.Game.Storyboards.Drawables public override bool IsPresent => !float.IsNaN(DrawPosition.X) && !float.IsNaN(DrawPosition.Y) && base.IsPresent; + [Resolved] + private ISkinSource skin { get; set; } = null!; + + [Resolved] + private TextureStore textureStore { get; set; } = null!; + public DrawableStoryboardSprite(StoryboardSprite sprite) { Sprite = sprite; @@ -85,30 +90,34 @@ namespace osu.Game.Storyboards.Drawables LifetimeEnd = sprite.EndTimeForDisplay; } - [Resolved] - private ISkinSource skin { get; set; } - [BackgroundDependencyLoader] - private void load(TextureStore textureStore, Storyboard storyboard) + private void load(Storyboard storyboard) { - Texture = storyboard.GetTextureFromPath(Sprite.Path, textureStore); - - if (Texture == null && storyboard.UseSkinSprites) + if (storyboard.UseSkinSprites) { skin.SourceChanged += skinSourceChanged; skinSourceChanged(); } + else + Texture = textureStore.Get(Sprite.Path); Sprite.ApplyTransforms(this); } - private void skinSourceChanged() => Texture = skin.GetTexture(Sprite.Path); + private void skinSourceChanged() + { + Texture = skin.GetTexture(Sprite.Path) ?? textureStore.Get(Sprite.Path); + + // Setting texture will only update the size if it's zero. + // So let's force an explicit update. + Size = new Vector2(Texture?.DisplayWidth ?? 0, Texture?.DisplayHeight ?? 0); + } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - if (skin != null) + if (skin.IsNotNull()) skin.SourceChanged -= skinSourceChanged; } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index eec2cd6a60..9a5db4bb39 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -29,12 +29,7 @@ namespace osu.Game.Storyboards.Drawables [BackgroundDependencyLoader(true)] private void load(IBindable beatmap, TextureStore textureStore) { - string? path = beatmap.Value.BeatmapSetInfo?.GetPathForFile(Video.Path); - - if (path == null) - return; - - var stream = textureStore.GetStream(path); + var stream = textureStore.GetStream(Video.Path); if (stream == null) return; diff --git a/osu.Game/Storyboards/Drawables/DrawablesExtensions.cs b/osu.Game/Storyboards/Drawables/DrawablesExtensions.cs index 779c8384c5..bbc55a336d 100644 --- a/osu.Game/Storyboards/Drawables/DrawablesExtensions.cs +++ b/osu.Game/Storyboards/Drawables/DrawablesExtensions.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 osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; diff --git a/osu.Game/Storyboards/Drawables/IFlippable.cs b/osu.Game/Storyboards/Drawables/IFlippable.cs index aceb5c041c..165b3d97cc 100644 --- a/osu.Game/Storyboards/Drawables/IFlippable.cs +++ b/osu.Game/Storyboards/Drawables/IFlippable.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 osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; diff --git a/osu.Game/Storyboards/Drawables/IVectorScalable.cs b/osu.Game/Storyboards/Drawables/IVectorScalable.cs index 3b43a35a90..60a297e126 100644 --- a/osu.Game/Storyboards/Drawables/IVectorScalable.cs +++ b/osu.Game/Storyboards/Drawables/IVectorScalable.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; using osuTK; diff --git a/osu.Game/Storyboards/IStoryboardElement.cs b/osu.Game/Storyboards/IStoryboardElement.cs index 7e83f8b692..9a059991e6 100644 --- a/osu.Game/Storyboards/IStoryboardElement.cs +++ b/osu.Game/Storyboards/IStoryboardElement.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 osu.Framework.Graphics; namespace osu.Game.Storyboards diff --git a/osu.Game/Storyboards/IStoryboardElementWithDuration.cs b/osu.Game/Storyboards/IStoryboardElementWithDuration.cs index 9eed139ad4..3e0f7fb576 100644 --- a/osu.Game/Storyboards/IStoryboardElementWithDuration.cs +++ b/osu.Game/Storyboards/IStoryboardElementWithDuration.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Storyboards { /// diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 566e064aad..8c43b99702 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Storyboards.Drawables; @@ -19,7 +18,7 @@ namespace osu.Game.Storyboards public BeatmapInfo BeatmapInfo = new BeatmapInfo(); /// - /// Whether the storyboard can fall back to skin sprites in case no matching storyboard sprites are found. + /// Whether the storyboard should prefer textures from the current skin before using local storyboard textures. /// public bool UseSkinSprites { get; set; } @@ -31,8 +30,12 @@ namespace osu.Game.Storyboards /// /// /// This iterates all elements and as such should be used sparingly or stored locally. + /// Sample events use their start time as "end time" during this calculation. + /// Video and background events are not included to match stable. /// - public double? EarliestEventTime => Layers.SelectMany(l => l.Elements).MinBy(e => e.StartTime)?.StartTime; + public double? EarliestEventTime => Layers.SelectMany(l => l.Elements) + .Where(e => e is not StoryboardVideo) + .MinBy(e => e.StartTime)?.StartTime; /// /// Across all layers, find the latest point in time that a storyboard element ends at. @@ -40,9 +43,12 @@ namespace osu.Game.Storyboards /// /// /// This iterates all elements and as such should be used sparingly or stored locally. - /// Videos and samples return StartTime as their EndTIme. + /// Sample events use their start time as "end time" during this calculation. + /// Video and background events are not included to match stable. /// - public double? LatestEventTime => Layers.SelectMany(l => l.Elements).MaxBy(e => e.GetEndTime())?.GetEndTime(); + public double? LatestEventTime => Layers.SelectMany(l => l.Elements) + .Where(e => e is not StoryboardVideo) + .MaxBy(e => e.GetEndTime())?.GetEndTime(); /// /// Depth of the currently front-most storyboard layer, excluding the overlay layer. @@ -87,12 +93,12 @@ namespace osu.Game.Storyboards } } - public DrawableStoryboard CreateDrawable(IReadOnlyList? mods = null) => + public virtual DrawableStoryboard CreateDrawable(IReadOnlyList? mods = null) => new DrawableStoryboard(this, mods); private static readonly string[] image_extensions = { @".png", @".jpg" }; - public Texture? GetTextureFromPath(string path, TextureStore textureStore) + public virtual string? GetStoragePathFromStoryboardPath(string path) { string? resolvedPath = null; @@ -102,10 +108,7 @@ namespace osu.Game.Storyboards } else { - // Just doing this extension logic locally here for simplicity. - // - // A more "sane" path may be to use the ISkinSource.GetTexture path (which will use the extensions of the underlying TextureStore), - // but comes with potential complexity (what happens if the user has beatmap skins disabled?). + // Some old storyboards don't include a file extension, so let's best guess at one. foreach (string ext in image_extensions) { if ((resolvedPath = BeatmapInfo.BeatmapSet?.GetPathForFile($"{path}{ext}")) != null) @@ -113,10 +116,7 @@ namespace osu.Game.Storyboards } } - if (!string.IsNullOrEmpty(resolvedPath)) - return textureStore.Get(resolvedPath); - - return null; + return resolvedPath; } } } diff --git a/osu.Game/Storyboards/StoryboardExtensions.cs b/osu.Game/Storyboards/StoryboardExtensions.cs index e5cafc152b..04c7196315 100644 --- a/osu.Game/Storyboards/StoryboardExtensions.cs +++ b/osu.Game/Storyboards/StoryboardExtensions.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osuTK; diff --git a/osu.Game/Storyboards/StoryboardLayer.cs b/osu.Game/Storyboards/StoryboardLayer.cs index 2ab8d9fc2a..fa9d4ebfea 100644 --- a/osu.Game/Storyboards/StoryboardLayer.cs +++ b/osu.Game/Storyboards/StoryboardLayer.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 osu.Game.Storyboards.Drawables; using System.Collections.Generic; diff --git a/osu.Game/Storyboards/StoryboardSample.cs b/osu.Game/Storyboards/StoryboardSample.cs index 752d086993..5d6ce215f5 100644 --- a/osu.Game/Storyboards/StoryboardSample.cs +++ b/osu.Game/Storyboards/StoryboardSample.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.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Audio; diff --git a/osu.Game/Storyboards/StoryboardVideo.cs b/osu.Game/Storyboards/StoryboardVideo.cs index 04ff941397..8c11e19a06 100644 --- a/osu.Game/Storyboards/StoryboardVideo.cs +++ b/osu.Game/Storyboards/StoryboardVideo.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 osu.Framework.Graphics; using osu.Game.Storyboards.Drawables; @@ -16,7 +14,7 @@ namespace osu.Game.Storyboards public double StartTime { get; } - public StoryboardVideo(string path, int offset) + public StoryboardVideo(string path, double offset) { Path = path; StartTime = offset; diff --git a/osu.Game/Storyboards/StoryboardVideoLayer.cs b/osu.Game/Storyboards/StoryboardVideoLayer.cs index f08c02cfd2..f780604029 100644 --- a/osu.Game/Storyboards/StoryboardVideoLayer.cs +++ b/osu.Game/Storyboards/StoryboardVideoLayer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Graphics; using osu.Game.Storyboards.Drawables; using osuTK; diff --git a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs index 921a039065..b7803f3420 100644 --- a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.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 NUnit.Framework; diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index 1aa99ceed9..ff670e1232 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.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.Diagnostics; using System.IO; diff --git a/osu.Game/Tests/CleanRunHeadlessGameHost.cs b/osu.Game/Tests/CleanRunHeadlessGameHost.cs index 02d67de5a5..f3c69201e2 100644 --- a/osu.Game/Tests/CleanRunHeadlessGameHost.cs +++ b/osu.Game/Tests/CleanRunHeadlessGameHost.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.Runtime.CompilerServices; using osu.Framework; diff --git a/osu.Game/Tests/OsuTestBrowser.cs b/osu.Game/Tests/OsuTestBrowser.cs index 689eae336e..477ffb2908 100644 --- a/osu.Game/Tests/OsuTestBrowser.cs +++ b/osu.Game/Tests/OsuTestBrowser.cs @@ -1,14 +1,8 @@ // 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 osu.Framework.Graphics; using osu.Framework.Platform; -using osu.Framework.Screens; using osu.Framework.Testing; -using osu.Game.Graphics; -using osu.Game.Screens.Backgrounds; namespace osu.Game.Tests { @@ -18,12 +12,6 @@ namespace osu.Game.Tests { base.LoadComplete(); - LoadComponentAsync(new ScreenStack(new BackgroundScreenDefault { Colour = OsuColour.Gray(0.5f) }) - { - Depth = 10, - RelativeSizeAxes = Axes.Both, - }, Add); - // Have to construct this here, rather than in the constructor, because // we depend on some dependencies to be loaded within OsuGameBase.load(). Add(new TestBrowser()); diff --git a/osu.Game/Tests/Visual/DependencyProvidingContainer.cs b/osu.Game/Tests/Visual/DependencyProvidingContainer.cs index acfff4cefe..000509598d 100644 --- a/osu.Game/Tests/Visual/DependencyProvidingContainer.cs +++ b/osu.Game/Tests/Visual/DependencyProvidingContainer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Allocation; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs new file mode 100644 index 0000000000..de4688a6fe --- /dev/null +++ b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs @@ -0,0 +1,596 @@ +// 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.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring.Legacy; +using osuTK; +using osuTK.Graphics; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public abstract partial class ScoringTestScene : OsuTestScene + { + protected abstract IBeatmap CreateBeatmap(int maxCombo); + + protected abstract IScoringAlgorithm CreateScoreV1(); + protected abstract IScoringAlgorithm CreateScoreV2(int maxCombo); + protected abstract ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode); + + protected Bindable MaxCombo => sliderMaxCombo.Current; + protected BindableList NonPerfectLocations => graphs.NonPerfectLocations; + protected BindableList MissLocations => graphs.MissLocations; + + private readonly bool supportsNonPerfectJudgements; + + private GraphContainer graphs = null!; + private SettingsSlider sliderMaxCombo = null!; + private SettingsCheckbox scaleToMax = null!; + + private FillFlowContainer legend = null!; + + private readonly BindableBool standardisedVisible = new BindableBool(true); + private readonly BindableBool classicVisible = new BindableBool(true); + private readonly BindableBool scoreV1Visible = new BindableBool(true); + private readonly BindableBool scoreV2Visible = new BindableBool(true); + + [Resolved] + private OsuColour colours { get; set; } = null!; + + protected ScoringTestScene(bool supportsNonPerfectJudgements = true) + { + this.supportsNonPerfectJudgements = supportsNonPerfectJudgements; + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("setup tests", () => + { + OsuTextFlowContainer clickExplainer; + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.Black + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + graphs = new GraphContainer(supportsNonPerfectJudgements) + { + RelativeSizeAxes = Axes.Both, + }, + }, + new Drawable[] + { + legend = new FillFlowContainer + { + Padding = new MarginPadding(20), + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + }, + new Drawable[] + { + new FillFlowContainer + { + Padding = new MarginPadding(20), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + Children = new Drawable[] + { + sliderMaxCombo = new SettingsSlider + { + TransferValueOnCommit = true, + Current = new BindableInt(1024) + { + MinValue = 96, + MaxValue = 8192, + }, + LabelText = "Max combo", + }, + scaleToMax = new SettingsCheckbox + { + LabelText = "Rescale plots to 100%", + Current = { Value = true, Default = true } + }, + clickExplainer = new OsuTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 20 } + } + } + }, + }, + } + } + }; + + clickExplainer.AddParagraph("Left click to add miss"); + if (supportsNonPerfectJudgements) + clickExplainer.AddParagraph("Right click to add OK"); + + sliderMaxCombo.Current.BindValueChanged(_ => Rerun()); + scaleToMax.Current.BindValueChanged(_ => Rerun()); + + standardisedVisible.BindValueChanged(_ => rescalePlots()); + classicVisible.BindValueChanged(_ => rescalePlots()); + scoreV1Visible.BindValueChanged(_ => rescalePlots()); + scoreV2Visible.BindValueChanged(_ => rescalePlots()); + + graphs.MissLocations.BindCollectionChanged((_, __) => Rerun()); + graphs.NonPerfectLocations.BindCollectionChanged((_, __) => Rerun()); + + graphs.MaxCombo.BindTo(sliderMaxCombo.Current); + + Rerun(); + }); + } + + protected void Rerun() + { + graphs.Clear(); + legend.Clear(); + + runForProcessor("lazer-standardised", colours.Green1, ScoringMode.Standardised, standardisedVisible); + runForProcessor("lazer-classic", colours.Blue1, ScoringMode.Classic, classicVisible); + + runForAlgorithm(new ScoringAlgorithmInfo + { + Name = "ScoreV1 (classic)", + Colour = colours.Purple1, + Algorithm = CreateScoreV1(), + Visible = scoreV1Visible + }); + runForAlgorithm(new ScoringAlgorithmInfo + { + Name = "ScoreV2", + Colour = colours.Red1, + Algorithm = CreateScoreV2(sliderMaxCombo.Current.Value), + Visible = scoreV2Visible + }); + + rescalePlots(); + } + + private void rescalePlots() + { + if (!scaleToMax.Current.Value && legend.Any(entry => entry.Visible.Value)) + { + long maxScore = legend.Where(entry => entry.Visible.Value).Max(entry => entry.FinalScore); + + foreach (var graph in graphs) + graph.Height = graph.Values.Max() / maxScore; + } + else + { + foreach (var graph in graphs) + graph.Height = 1; + } + } + + private void runForProcessor(string name, Color4 colour, ScoringMode scoringMode, BindableBool visibility) + { + int maxCombo = sliderMaxCombo.Current.Value; + var beatmap = CreateBeatmap(maxCombo); + var algorithm = CreateScoreAlgorithm(beatmap, scoringMode); + + runForAlgorithm(new ScoringAlgorithmInfo + { + Name = name, + Colour = colour, + Algorithm = algorithm, + Visible = visibility + }); + } + + private void runForAlgorithm(ScoringAlgorithmInfo algorithmInfo) + { + int maxCombo = sliderMaxCombo.Current.Value; + + List results = new List(); + + for (int i = 0; i < maxCombo; i++) + { + if (graphs.MissLocations.Contains(i)) + algorithmInfo.Algorithm.ApplyMiss(); + else if (graphs.NonPerfectLocations.Contains(i)) + algorithmInfo.Algorithm.ApplyNonPerfect(); + else + algorithmInfo.Algorithm.ApplyHit(); + + results.Add(algorithmInfo.Algorithm.TotalScore); + } + + LineGraph graph; + graphs.Add(graph = new LineGraph + { + Name = algorithmInfo.Name, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.Both, + LineColour = algorithmInfo.Colour, + Values = results + }); + + legend.Add(new LegendEntry(algorithmInfo, graph) + { + AccentColour = algorithmInfo.Colour, + }); + } + + private class ScoringAlgorithmInfo + { + public string Name { get; init; } = null!; + public Color4 Colour { get; init; } + public IScoringAlgorithm Algorithm { get; init; } = null!; + public BindableBool Visible { get; init; } = null!; + } + + protected interface IScoringAlgorithm + { + void ApplyHit(); + void ApplyNonPerfect(); + void ApplyMiss(); + + long TotalScore { get; } + } + + protected abstract class ProcessorBasedScoringAlgorithm : IScoringAlgorithm + { + protected abstract ScoreProcessor CreateScoreProcessor(); + protected abstract JudgementResult CreatePerfectJudgementResult(); + protected abstract JudgementResult CreateNonPerfectJudgementResult(); + protected abstract JudgementResult CreateMissJudgementResult(); + + private readonly ScoreProcessor scoreProcessor; + private readonly ScoringMode mode; + + protected ProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode) + { + this.mode = mode; + scoreProcessor = CreateScoreProcessor(); + scoreProcessor.ApplyBeatmap(beatmap); + } + + public void ApplyHit() => scoreProcessor.ApplyResult(CreatePerfectJudgementResult()); + public void ApplyNonPerfect() => scoreProcessor.ApplyResult(CreateNonPerfectJudgementResult()); + public void ApplyMiss() => scoreProcessor.ApplyResult(CreateMissJudgementResult()); + + public long TotalScore => scoreProcessor.GetDisplayScore(mode); + } + + public partial class GraphContainer : Container, IHasCustomTooltip> + { + private readonly bool supportsNonPerfectJudgements; + + public readonly BindableList MissLocations = new BindableList(); + public readonly BindableList NonPerfectLocations = new BindableList(); + + public Bindable MaxCombo = new Bindable(); + + protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; + + private readonly Box hoverLine; + + private readonly Container missLines; + private readonly Container verticalGridLines; + + public int CurrentHoverCombo { get; private set; } + + public GraphContainer(bool supportsNonPerfectJudgements) + { + this.supportsNonPerfectJudgements = supportsNonPerfectJudgements; + InternalChild = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = OsuColour.Gray(0.1f), + RelativeSizeAxes = Axes.Both, + }, + verticalGridLines = new Container + { + RelativeSizeAxes = Axes.Both, + }, + hoverLine = new Box + { + Colour = Color4.Yellow, + RelativeSizeAxes = Axes.Y, + Origin = Anchor.TopCentre, + Alpha = 0, + Width = 1, + }, + missLines = new Container + { + Alpha = 0.6f, + RelativeSizeAxes = Axes.Both, + }, + Content, + } + }; + + MissLocations.BindCollectionChanged((_, _) => updateMissLocations()); + NonPerfectLocations.BindCollectionChanged((_, _) => updateMissLocations()); + + MaxCombo.BindValueChanged(_ => + { + updateMissLocations(); + updateVerticalGridLines(); + }, true); + } + + private void updateVerticalGridLines() + { + verticalGridLines.Clear(); + + for (int i = 0; i < MaxCombo.Value; i++) + { + if (i % 100 == 0) + { + verticalGridLines.AddRange(new Drawable[] + { + new Box + { + Colour = OsuColour.Gray(0.2f), + Origin = Anchor.TopCentre, + Width = 1, + RelativeSizeAxes = Axes.Y, + RelativePositionAxes = Axes.X, + X = (float)i / MaxCombo.Value, + }, + new OsuSpriteText + { + RelativePositionAxes = Axes.X, + X = (float)i / MaxCombo.Value, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = $"{i:#,0}", + Rotation = -30, + Y = -20, + } + }); + } + } + } + + private void updateMissLocations() + { + missLines.Clear(); + + foreach (int miss in MissLocations) + { + missLines.Add(new Box + { + Colour = Color4.Red, + Origin = Anchor.TopCentre, + Width = 1, + RelativeSizeAxes = Axes.Y, + RelativePositionAxes = Axes.X, + X = (float)miss / MaxCombo.Value, + }); + } + + foreach (int miss in NonPerfectLocations) + { + missLines.Add(new Box + { + Colour = Color4.Orange, + Origin = Anchor.TopCentre, + Width = 1, + RelativeSizeAxes = Axes.Y, + RelativePositionAxes = Axes.X, + X = (float)miss / MaxCombo.Value, + }); + } + } + + protected override bool OnHover(HoverEvent e) + { + hoverLine.Show(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + hoverLine.Hide(); + base.OnHoverLost(e); + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + CurrentHoverCombo = (int)(e.MousePosition.X / DrawWidth * MaxCombo.Value); + + hoverLine.X = e.MousePosition.X; + return base.OnMouseMove(e); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Left) + MissLocations.Add(CurrentHoverCombo); + else if (supportsNonPerfectJudgements) + NonPerfectLocations.Add(CurrentHoverCombo); + + return true; + } + + private GraphTooltip? tooltip; + + public ITooltip> GetCustomTooltip() => tooltip ??= new GraphTooltip(this); + + public IEnumerable TooltipContent => Content; + + public partial class GraphTooltip : CompositeDrawable, ITooltip> + { + private readonly GraphContainer graphContainer; + + private readonly OsuTextFlowContainer textFlow; + + public GraphTooltip(GraphContainer graphContainer) + { + this.graphContainer = graphContainer; + AutoSizeAxes = Axes.Both; + + Masking = true; + CornerRadius = 10; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = OsuColour.Gray(0.15f), + RelativeSizeAxes = Axes.Both, + }, + textFlow = new OsuTextFlowContainer + { + Colour = Color4.White, + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + } + }; + } + + private int? lastContentCombo; + + public void SetContent(IEnumerable content) + { + int relevantCombo = graphContainer.CurrentHoverCombo; + + if (lastContentCombo == relevantCombo) + return; + + lastContentCombo = relevantCombo; + textFlow.Clear(); + + textFlow.AddParagraph($"At combo {relevantCombo}:"); + + foreach (var graph in content) + { + if (graph.Alpha == 0) continue; + + float valueAtHover = graph.Values.ElementAt(relevantCombo); + float ofTotal = valueAtHover / graph.Values.Last(); + + textFlow.AddParagraph($"{graph.Name}: {valueAtHover:#,0} ({ofTotal * 100:N0}% of final)\n", st => st.Colour = graph.LineColour); + } + } + + public void Move(Vector2 pos) => this.MoveTo(pos); + } + } + + private partial class LegendEntry : OsuClickableContainer, IHasAccentColour + { + public Color4 AccentColour { get; set; } + + public BindableBool Visible { get; } = new BindableBool(true); + + public readonly long FinalScore; + + private readonly string description; + private readonly LineGraph lineGraph; + + private OsuSpriteText descriptionText = null!; + private OsuSpriteText finalScoreText = null!; + + public LegendEntry(ScoringAlgorithmInfo scoringAlgorithmInfo, LineGraph lineGraph) + { + description = scoringAlgorithmInfo.Name; + FinalScore = scoringAlgorithmInfo.Algorithm.TotalScore; + AccentColour = scoringAlgorithmInfo.Colour; + Visible.BindTo(scoringAlgorithmInfo.Visible); + + this.lineGraph = lineGraph; + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Content.RelativeSizeAxes = Axes.X; + AutoSizeAxes = Content.AutoSizeAxes = Axes.Y; + + Children = new Drawable[] + { + descriptionText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + finalScoreText = new OsuSpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Font = OsuFont.Default.With(fixedWidth: true) + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Visible.BindValueChanged(_ => updateState(), true); + Action = Visible.Toggle; + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + private void updateState() + { + Colour = IsHovered ? AccentColour.Lighten(0.2f) : AccentColour; + + descriptionText.Text = $"{(Visible.Value ? FontAwesome.Solid.CheckCircle.Icon : FontAwesome.Solid.Circle.Icon)} {description}"; + finalScoreText.Text = FinalScore.ToString("#,0"); + lineGraph.Alpha = Visible.Value ? 1 : 0; + } + } + } +} diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index 167d5450e9..164faa16aa 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; diff --git a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs index 0570c4e2f2..efd0b80ebf 100644 --- a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.Spectator; diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs index 0f286475bd..88202d4327 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Screens.OnlinePlay; diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index c27e30d5bb..6007c7c076 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -263,6 +263,11 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } + public override Task InvitePlayer(int userId) + { + return Task.CompletedTask; + } + public override Task TransferHost(int userId) { userId = clone(userId); @@ -635,7 +640,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private T clone(T incoming) { - byte[]? serialized = MessagePackSerializer.Serialize(typeof(T), incoming, SignalRUnionWorkaroundResolver.OPTIONS); + byte[] serialized = MessagePackSerializer.Serialize(typeof(T), incoming, SignalRUnionWorkaroundResolver.OPTIONS); return MessagePackSerializer.Deserialize(serialized, SignalRUnionWorkaroundResolver.OPTIONS); } diff --git a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs index 12d1846ece..3509519113 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Bindables; using osu.Game.Database; using osu.Game.Online.Rooms; diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs index a9acbdcd7e..975423d19b 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using osu.Framework.Allocation; diff --git a/osu.Game/Tests/Visual/OsuGridTestScene.cs b/osu.Game/Tests/Visual/OsuGridTestScene.cs index 9ef3b2a59d..6ee5593a69 100644 --- a/osu.Game/Tests/Visual/OsuGridTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGridTestScene.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs index a5e0bddc6b..ffe40243ab 100644 --- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -10,6 +8,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Framework.Testing.Input; +using osu.Game.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterfaceV2; @@ -27,8 +26,7 @@ namespace osu.Game.Tests.Visual protected readonly ManualInputManager InputManager; - private readonly RoundedButton buttonTest; - private readonly RoundedButton buttonLocal; + private readonly Container takeControlOverlay; /// /// Whether to create a nested container to handle s that result from local (manual) test input. @@ -59,7 +57,13 @@ namespace osu.Game.Tests.Visual } if (CreateNestedActionContainer) - mainContent.Add(new GlobalActionContainer(null)); + { + var globalActionContainer = new GlobalActionContainer(null) + { + Child = mainContent + }; + mainContent = globalActionContainer; + } base.Content.AddRange(new Drawable[] { @@ -68,12 +72,12 @@ namespace osu.Game.Tests.Visual UseParentInput = true, Child = mainContent }, - new Container + takeControlOverlay = new Container { AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Margin = new MarginPadding(5), + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Margin = new MarginPadding(40), CornerRadius = 5, Masking = true, Children = new Drawable[] @@ -82,44 +86,38 @@ namespace osu.Game.Tests.Visual { Colour = Color4.Black, RelativeSizeAxes = Axes.Both, - Alpha = 0.5f, + Alpha = 0.4f, }, new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Margin = new MarginPadding(5), - Spacing = new Vector2(5), + Margin = new MarginPadding(10), + Spacing = new Vector2(10), Children = new Drawable[] { new OsuSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = "Input Priority" + Font = OsuFont.Default.With(weight: FontWeight.Bold), + Text = "The test is currently overriding local input", }, new FillFlowContainer { AutoSizeAxes = Axes.Both, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Margin = new MarginPadding(5), Spacing = new Vector2(5), Direction = FillDirection.Horizontal, Children = new Drawable[] { - buttonLocal = new RoundedButton + new RoundedButton { - Text = "local", - Size = new Vector2(50, 30), - Action = returnUserInput - }, - buttonTest = new RoundedButton - { - Text = "test", - Size = new Vector2(50, 30), - Action = returnTestInput + Text = "Take control back", + Size = new Vector2(180, 30), + Action = () => InputManager.UseParentInput = true }, } }, @@ -130,6 +128,13 @@ namespace osu.Game.Tests.Visual }); } + protected override void Update() + { + base.Update(); + + takeControlOverlay.Alpha = InputManager.UseParentInput ? 0 : 1; + } + /// /// Wait for a button to become enabled, then click it. /// @@ -148,19 +153,5 @@ namespace osu.Game.Tests.Visual InputManager.Click(MouseButton.Left); }); } - - protected override void Update() - { - base.Update(); - - buttonTest.Enabled.Value = InputManager.UseParentInput; - buttonLocal.Enabled.Value = !InputManager.UseParentInput; - } - - private void returnUserInput() => - InputManager.UseParentInput = true; - - private void returnTestInput() => - InputManager.UseParentInput = false; } } diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 0ec5a4c5c2..2b4c64dca8 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -16,6 +16,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.IO.Stores; using osu.Framework.Platform; @@ -23,6 +24,7 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Graphics; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; @@ -158,19 +160,24 @@ namespace osu.Game.Tests.Visual return Dependencies; } + [Resolved] + private OsuColour colours { get; set; } + protected override void LoadComplete() { base.LoadComplete(); - var parentBeatmap = Parent.Dependencies.Get>(); + ChangeBackgroundColour(ColourInfo.GradientVertical(colours.GreyCarmine, colours.GreyCarmineDarker)); + + var parentBeatmap = Parent!.Dependencies.Get>(); parentBeatmap.Value = Beatmap.Value; Beatmap.BindTo(parentBeatmap); - var parentRuleset = Parent.Dependencies.Get>(); + var parentRuleset = Parent!.Dependencies.Get>(); parentRuleset.Value = Ruleset.Value; Ruleset.BindTo(parentRuleset); - var parentMods = Parent.Dependencies.Get>>(); + var parentMods = Parent!.Dependencies.Get>>(); parentMods.Value = SelectedMods.Value; SelectedMods.BindTo(parentMods); } @@ -265,7 +272,7 @@ namespace osu.Game.Tests.Visual { Debug.Assert(original.BeatmapSet != null); - return new APIBeatmapSet + var result = new APIBeatmapSet { OnlineID = original.BeatmapSet.OnlineID, Status = BeatmapOnlineStatus.Ranked, @@ -301,6 +308,11 @@ namespace osu.Game.Tests.Visual } } }; + + foreach (var beatmap in result.Beatmaps) + beatmap.BeatmapSet = result; + + return result; } protected WorkingBeatmap CreateWorkingBeatmap(RulesetInfo ruleset) => @@ -420,6 +432,11 @@ namespace osu.Game.Tests.Visual private bool running; + public override double Rate => base.Rate + // This is mainly to allow some tests to override the rate to zero + // and avoid interpolation. + * referenceClock.Rate; + public TrackVirtualManual(IFrameBasedClock referenceClock, string name = "virtual") : base(name) { diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 0392e3ae52..ee184c1f35 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -12,6 +12,7 @@ using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { @@ -79,6 +80,11 @@ namespace osu.Game.Tests.Visual protected void LoadPlayer(Mod[] mods) { + // if a player screen is present already, we must exit that before loading another one, + // otherwise it'll crash on SpectatorClient.BeginPlaying being called while client is in "playing" state already. + if (Stack.CurrentScreen is Player) + Stack.Exit(); + var ruleset = CreatePlayerRuleset(); Ruleset.Value = ruleset.RulesetInfo; diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index 7d382ca1bc..3cca1e59cc 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.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 osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Tests/Visual/ScrollingTestContainer.cs b/osu.Game/Tests/Visual/ScrollingTestContainer.cs index b8b39e16b5..bce4299688 100644 --- a/osu.Game/Tests/Visual/ScrollingTestContainer.cs +++ b/osu.Game/Tests/Visual/ScrollingTestContainer.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual IBindable IScrollingInfo.TimeRange => TimeRange; public readonly TestScrollAlgorithm Algorithm = new TestScrollAlgorithm(); - IScrollAlgorithm IScrollingInfo.Algorithm => Algorithm; + IBindable IScrollingInfo.Algorithm => new Bindable(Algorithm); } public class TestScrollAlgorithm : IScrollAlgorithm diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 305a615102..5db08810ca 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -124,7 +124,12 @@ namespace osu.Game.Tests.Visual.Spectator if (frames.Count == 0) return; - var bundle = new FrameDataBundle(new ScoreInfo { Combo = currentFrameIndex }, new ScoreProcessor(rulesetStore.GetRuleset(0)!.CreateInstance()), frames.ToArray()); + var bundle = new FrameDataBundle(new ScoreInfo + { + Combo = currentFrameIndex, + TotalScore = (long)(currentFrameIndex * 123478 * RNG.NextDouble(0.99, 1.01)), + Accuracy = RNG.NextDouble(0.98, 1), + }, new ScoreProcessor(rulesetStore.GetRuleset(0)!.CreateInstance()), frames.ToArray()); ((ISpectatorClient)this).UserSentFrames(userId, bundle); frames.Clear(); diff --git a/osu.Game/Tests/Visual/TestReplayPlayer.cs b/osu.Game/Tests/Visual/TestReplayPlayer.cs index bc6dc9bb27..0c9b466152 100644 --- a/osu.Game/Tests/Visual/TestReplayPlayer.cs +++ b/osu.Game/Tests/Visual/TestReplayPlayer.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 System.Linq; using osu.Framework.Bindables; diff --git a/osu.Game/Tests/Visual/TestUserLookupCache.cs b/osu.Game/Tests/Visual/TestUserLookupCache.cs index a3028f1a34..261e0fa75c 100644 --- a/osu.Game/Tests/Visual/TestUserLookupCache.cs +++ b/osu.Game/Tests/Visual/TestUserLookupCache.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.Threading; using System.Threading.Tasks; using osu.Game.Database; @@ -18,12 +16,12 @@ namespace osu.Game.Tests.Visual /// public const int UNRESOLVED_USER_ID = -1; - protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) + protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) { if (lookup == UNRESOLVED_USER_ID) - return Task.FromResult((APIUser)null); + return Task.FromResult(null); - return Task.FromResult(new APIUser + return Task.FromResult(new APIUser { Id = lookup, Username = $"User {lookup}" diff --git a/osu.Game/Tests/VisualTestRunner.cs b/osu.Game/Tests/VisualTestRunner.cs index c8279b9e3c..e04c71d193 100644 --- a/osu.Game/Tests/VisualTestRunner.cs +++ b/osu.Game/Tests/VisualTestRunner.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 osu.Framework; using osu.Framework.Platform; diff --git a/osu.Game/Updater/NoActionUpdateManager.cs b/osu.Game/Updater/NoActionUpdateManager.cs index 97d3275757..f776cd67be 100644 --- a/osu.Game/Updater/NoActionUpdateManager.cs +++ b/osu.Game/Updater/NoActionUpdateManager.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable @@ -47,7 +47,7 @@ namespace osu.Game.Updater { Text = $"A newer release of osu! has been found ({version} → {latestTagName}).\n\n" + "Check with your package manager / provider to bring osu! up-to-date!", - Icon = FontAwesome.Solid.Upload, + Icon = FontAwesome.Solid.Download, }); return true; diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index 1ecb73a154..bc1b0919b8 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/SimpleUpdateManager.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable @@ -54,7 +54,7 @@ namespace osu.Game.Updater { Text = $"A newer release of osu! has been found ({version} → {latestTagName}).\n\n" + "Click here to download the new version, which can be installed over the top of your existing installation", - Icon = FontAwesome.Solid.Upload, + Icon = FontAwesome.Solid.Download, Activated = () => { host.OpenUrlExternally(getBestUrl(latest)); diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 47c2a169ed..190748137a 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -134,7 +134,7 @@ namespace osu.Game.Updater { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Icon = FontAwesome.Solid.Upload, + Icon = FontAwesome.Solid.Download, Size = new Vector2(34), Colour = OsuColour.Gray(0.2f), Depth = float.MaxValue, diff --git a/osu.Game/Users/Badge.cs b/osu.Game/Users/Badge.cs index b87e2ddecd..42f2234744 100644 --- a/osu.Game/Users/Badge.cs +++ b/osu.Game/Users/Badge.cs @@ -16,9 +16,12 @@ namespace osu.Game.Users [JsonProperty("description")] public string Description; - [JsonProperty("image_url")] + [JsonProperty("image@2x_url")] public string ImageUrl; + [JsonProperty("image_url")] + public string ImageUrlLowRes; + [JsonProperty("url")] public string Url; } diff --git a/osu.Game/Users/CountryCode.cs b/osu.Game/Users/CountryCode.cs index 775de2bcf5..edaa1562c7 100644 --- a/osu.Game/Users/CountryCode.cs +++ b/osu.Game/Users/CountryCode.cs @@ -8,6 +8,9 @@ using Newtonsoft.Json.Converters; namespace osu.Game.Users { + /// + /// Matches `osu_countries` database table. + /// [JsonConverter(typeof(StringEnumConverter))] [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] public enum CountryCode @@ -15,6 +18,72 @@ namespace osu.Game.Users [Description("Unknown")] Unknown = 0, + [Description("Anonymous Proxy")] + A1, + + [Description("Satellite Provider")] + A2, + + [Description("Andorra")] + AD, + + [Description("United Arab Emirates")] + AE, + + [Description("Afghanistan")] + AF, + + [Description("Antigua and Barbuda")] + AG, + + [Description("Anguilla")] + AI, + + [Description("Albania")] + AL, + + [Description("Armenia")] + AM, + + [Description("Netherlands Antilles")] + AN, + + [Description("Angola")] + AO, + + [Description("Asia/Pacific Region")] + AP, + + [Description("Antarctica")] + AQ, + + [Description("Argentina")] + AR, + + [Description("American Samoa")] + AS, + + [Description("Austria")] + AT, + + [Description("Australia")] + AU, + + [Description("Aruba")] + AW, + + [Description("Aland Islands")] + AX, + + [Description("Azerbaijan")] + AZ, + + [Description("Bosnia and Herzegovina")] + BA, + + [Description("Barbados")] + BB, + [Description("Bangladesh")] BD, @@ -27,14 +96,14 @@ namespace osu.Game.Users [Description("Bulgaria")] BG, - [Description("Bosnia and Herzegovina")] - BA, + [Description("Bahrain")] + BH, - [Description("Barbados")] - BB, + [Description("Burundi")] + BI, - [Description("Wallis and Futuna")] - WF, + [Description("Benin")] + BJ, [Description("Saint Barthelemy")] BL, @@ -48,31 +117,7 @@ namespace osu.Game.Users [Description("Bolivia")] BO, - [Description("Bahrain")] - BH, - - [Description("Burundi")] - BI, - - [Description("Benin")] - BJ, - - [Description("Bhutan")] - BT, - - [Description("Jamaica")] - JM, - - [Description("Bouvet Island")] - BV, - - [Description("Botswana")] - BW, - - [Description("Samoa")] - WS, - - [Description("Bonaire, Saint Eustatius and Saba")] + [Description("Caribbean Netherlands")] BQ, [Description("Brazil")] @@ -81,8 +126,14 @@ namespace osu.Game.Users [Description("Bahamas")] BS, - [Description("Jersey")] - JE, + [Description("Bhutan")] + BT, + + [Description("Bouvet Island")] + BV, + + [Description("Botswana")] + BW, [Description("Belarus")] BY, @@ -90,104 +141,191 @@ namespace osu.Game.Users [Description("Belize")] BZ, - [Description("Russia")] - RU, + [Description("Canada")] + CA, - [Description("Rwanda")] - RW, + [Description("Cocos (Keeling) Islands")] + CC, - [Description("Serbia")] - RS, + [Description("The Democratic Republic of the Congo")] + CD, - [Description("East Timor")] - TL, + [Description("Central African Republic")] + CF, - [Description("Reunion")] - RE, + [Description("Congo")] + CG, - [Description("Turkmenistan")] - TM, + [Description("Switzerland")] + CH, - [Description("Tajikistan")] - TJ, + [Description("Cote D'Ivoire")] + CI, - [Description("Romania")] - RO, + [Description("Cook Islands")] + CK, - [Description("Tokelau")] - TK, + [Description("Chile")] + CL, - [Description("Guinea-Bissau")] - GW, + [Description("Cameroon")] + CM, - [Description("Guam")] - GU, + [Description("China")] + CN, - [Description("Guatemala")] - GT, + [Description("Colombia")] + CO, - [Description("South Georgia and the South Sandwich Islands")] - GS, + [Description("Costa Rica")] + CR, - [Description("Greece")] - GR, + [Description("Cuba")] + CU, - [Description("Equatorial Guinea")] - GQ, + [Description("Cabo Verde")] + CV, - [Description("Guadeloupe")] - GP, + [Description("Curaçao")] + CW, - [Description("Japan")] - JP, + [Description("Christmas Island")] + CX, - [Description("Guyana")] - GY, + [Description("Cyprus")] + CY, - [Description("Guernsey")] - GG, + [Description("Czechia")] + CZ, - [Description("French Guiana")] - GF, + [Description("Germany")] + DE, - [Description("Georgia")] - GE, + [Description("Djibouti")] + DJ, - [Description("Grenada")] - GD, + [Description("Denmark")] + DK, - [Description("United Kingdom")] - GB, + [Description("Dominica")] + DM, + + [Description("Dominican Republic")] + DO, + + [Description("Algeria")] + DZ, + + [Description("Ecuador")] + EC, + + [Description("Estonia")] + EE, + + [Description("Egypt")] + EG, + + [Description("Western Sahara")] + EH, + + [Description("Eritrea")] + ER, + + [Description("Spain")] + ES, + + [Description("Ethiopia")] + ET, + + [Description("Europe")] + EU, + + [Description("Finland")] + FI, + + [Description("Fiji")] + FJ, + + [Description("Falkland Islands (Malvinas)")] + FK, + + [Description("Federated States of Micronesia")] + FM, + + [Description("Faroe Islands")] + FO, + + [Description("France")] + FR, + + [Description("France, Metropolitan")] + FX, [Description("Gabon")] GA, - [Description("El Salvador")] - SV, + [Description("United Kingdom")] + GB, - [Description("Guinea")] - GN, + [Description("Grenada")] + GD, - [Description("Gambia")] - GM, + [Description("Georgia")] + GE, - [Description("Greenland")] - GL, + [Description("French Guiana")] + GF, - [Description("Gibraltar")] - GI, + [Description("Guernsey")] + GG, [Description("Ghana")] GH, - [Description("Oman")] - OM, + [Description("Gibraltar")] + GI, - [Description("Tunisia")] - TN, + [Description("Greenland")] + GL, - [Description("Jordan")] - JO, + [Description("Gambia")] + GM, + + [Description("Guinea")] + GN, + + [Description("Guadeloupe")] + GP, + + [Description("Equatorial Guinea")] + GQ, + + [Description("Greece")] + GR, + + [Description("South Georgia and the South Sandwich Islands")] + GS, + + [Description("Guatemala")] + GT, + + [Description("Guam")] + GU, + + [Description("Guinea-Bissau")] + GW, + + [Description("Guyana")] + GY, + + [Description("Hong Kong")] + HK, + + [Description("Heard Island and McDonald Islands")] + HM, + + [Description("Honduras")] + HN, [Description("Croatia")] HR, @@ -198,122 +336,113 @@ namespace osu.Game.Users [Description("Hungary")] HU, - [Description("Hong Kong")] - HK, + [Description("Indonesia")] + ID, - [Description("Honduras")] - HN, + [Description("Ireland")] + IE, - [Description("Heard Island and McDonald Islands")] - HM, + [Description("Israel")] + IL, - [Description("Venezuela")] - VE, + [Description("Isle of Man")] + IM, - [Description("Puerto Rico")] - PR, + [Description("India")] + IN, - [Description("Palestinian Territory")] - PS, - - [Description("Palau")] - PW, - - [Description("Portugal")] - PT, - - [Description("Svalbard and Jan Mayen")] - SJ, - - [Description("Paraguay")] - PY, + [Description("British Indian Ocean Territory")] + IO, [Description("Iraq")] IQ, - [Description("Panama")] - PA, + [Description("Islamic Republic of Iran")] + IR, - [Description("French Polynesia")] - PF, - - [Description("Papua New Guinea")] - PG, - - [Description("Peru")] - PE, - - [Description("Pakistan")] - PK, - - [Description("Philippines")] - PH, - - [Description("Pitcairn")] - PN, - - [Description("Poland")] - PL, - - [Description("Saint Pierre and Miquelon")] - PM, - - [Description("Zambia")] - ZM, - - [Description("Western Sahara")] - EH, - - [Description("Estonia")] - EE, - - [Description("Egypt")] - EG, - - [Description("South Africa")] - ZA, - - [Description("Ecuador")] - EC, + [Description("Iceland")] + IS, [Description("Italy")] IT, - [Description("Vietnam")] - VN, + [Description("Jersey")] + JE, - [Description("Solomon Islands")] - SB, + [Description("Jamaica")] + JM, - [Description("Ethiopia")] - ET, + [Description("Jordan")] + JO, - [Description("Somalia")] - SO, + [Description("Japan")] + JP, - [Description("Zimbabwe")] - ZW, + [Description("Kenya")] + KE, - [Description("Saudi Arabia")] - SA, + [Description("Kyrgyzstan")] + KG, - [Description("Spain")] - ES, + [Description("Cambodia")] + KH, - [Description("Eritrea")] - ER, + [Description("Kiribati")] + KI, - [Description("Montenegro")] - ME, + [Description("Comoros")] + KM, - [Description("Moldova")] - MD, + [Description("Saint Kitts and Nevis")] + KN, - [Description("Madagascar")] - MG, + [Description("Democratic People's Republic of Korea")] + KP, - [Description("Saint Martin")] - MF, + [Description("South Korea")] + KR, + + [Description("Kuwait")] + KW, + + [Description("Cayman Islands")] + KY, + + [Description("Kazakhstan")] + KZ, + + [Description("Lao People's Democratic Republic")] + LA, + + [Description("Lebanon")] + LB, + + [Description("Saint Lucia")] + LC, + + [Description("Liechtenstein")] + LI, + + [Description("Sri Lanka")] + LK, + + [Description("Liberia")] + LR, + + [Description("Lesotho")] + LS, + + [Description("Lithuania")] + LT, + + [Description("Luxembourg")] + LU, + + [Description("Latvia")] + LV, + + [Description("Libya")] + LY, [Description("Morocco")] MA, @@ -321,20 +450,17 @@ namespace osu.Game.Users [Description("Monaco")] MC, - [Description("Uzbekistan")] - UZ, + [Description("Moldova")] + MD, - [Description("Myanmar")] - MM, + [Description("Montenegro")] + ME, - [Description("Mali")] - ML, + [Description("Saint Martin")] + MF, - [Description("Macao")] - MO, - - [Description("Mongolia")] - MN, + [Description("Madagascar")] + MG, [Description("Marshall Islands")] MH, @@ -342,87 +468,54 @@ namespace osu.Game.Users [Description("North Macedonia")] MK, - [Description("Mauritius")] - MU, + [Description("Mali")] + ML, - [Description("Malta")] - MT, + [Description("Myanmar")] + MM, - [Description("Malawi")] - MW, + [Description("Mongolia")] + MN, - [Description("Maldives")] - MV, - - [Description("Martinique")] - MQ, + [Description("Macau")] + MO, [Description("Northern Mariana Islands")] MP, - [Description("Montserrat")] - MS, + [Description("Martinique")] + MQ, [Description("Mauritania")] MR, - [Description("Isle of Man")] - IM, + [Description("Montserrat")] + MS, - [Description("Uganda")] - UG, + [Description("Malta")] + MT, - [Description("Tanzania")] - TZ, + [Description("Mauritius")] + MU, - [Description("Malaysia")] - MY, + [Description("Maldives")] + MV, + + [Description("Malawi")] + MW, [Description("Mexico")] MX, - [Description("Israel")] - IL, + [Description("Malaysia")] + MY, - [Description("France")] - FR, - - [Description("British Indian Ocean Territory")] - IO, - - [Description("Saint Helena")] - SH, - - [Description("Finland")] - FI, - - [Description("Fiji")] - FJ, - - [Description("Falkland Islands")] - FK, - - [Description("Micronesia")] - FM, - - [Description("Faroe Islands")] - FO, - - [Description("Nicaragua")] - NI, - - [Description("Netherlands")] - NL, - - [Description("Norway")] - NO, + [Description("Mozambique")] + MZ, [Description("Namibia")] NA, - [Description("Vanuatu")] - VU, - [Description("New Caledonia")] NC, @@ -435,8 +528,14 @@ namespace osu.Game.Users [Description("Nigeria")] NG, - [Description("New Zealand")] - NZ, + [Description("Nicaragua")] + NI, + + [Description("Netherlands")] + NL, + + [Description("Norway")] + NO, [Description("Nepal")] NP, @@ -447,227 +546,140 @@ namespace osu.Game.Users [Description("Niue")] NU, - [Description("Cook Islands")] - CK, + [Description("New Zealand")] + NZ, - [Description("Kosovo")] - XK, + [Description("Other")] + O1, - [Description("Ivory Coast")] - CI, + [Description("Oman")] + OM, - [Description("Switzerland")] - CH, + [Description("Panama")] + PA, - [Description("Colombia")] - CO, + [Description("Peru")] + PE, - [Description("China")] - CN, + [Description("French Polynesia")] + PF, - [Description("Cameroon")] - CM, + [Description("Papua New Guinea")] + PG, - [Description("Chile")] - CL, + [Description("Philippines")] + PH, - [Description("Cocos Islands")] - CC, + [Description("Pakistan")] + PK, - [Description("Canada")] - CA, + [Description("Poland")] + PL, - [Description("Republic of the Congo")] - CG, + [Description("Saint Pierre and Miquelon")] + PM, - [Description("Central African Republic")] - CF, + [Description("Pitcairn")] + PN, - [Description("Democratic Republic of the Congo")] - CD, + [Description("Puerto Rico")] + PR, - [Description("Czech Republic")] - CZ, + [Description("State of Palestine")] + PS, - [Description("Cyprus")] - CY, + [Description("Portugal")] + PT, - [Description("Christmas Island")] - CX, + [Description("Palau")] + PW, - [Description("Costa Rica")] - CR, + [Description("Paraguay")] + PY, - [Description("Curacao")] - CW, + [Description("Qatar")] + QA, - [Description("Cabo Verde")] - CV, + [Description("Reunion")] + RE, - [Description("Cuba")] - CU, + [Description("Romania")] + RO, - [Description("Eswatini")] - SZ, + [Description("Serbia")] + RS, - [Description("Syria")] - SY, + [Description("Russian Federation")] + RU, - [Description("Sint Maarten")] - SX, + [Description("Rwanda")] + RW, - [Description("Kyrgyzstan")] - KG, + [Description("Saudi Arabia")] + SA, - [Description("Kenya")] - KE, - - [Description("South Sudan")] - SS, - - [Description("Suriname")] - SR, - - [Description("Kiribati")] - KI, - - [Description("Cambodia")] - KH, - - [Description("Saint Kitts and Nevis")] - KN, - - [Description("Comoros")] - KM, - - [Description("Sao Tome and Principe")] - ST, - - [Description("Slovakia")] - SK, - - [Description("South Korea")] - KR, - - [Description("Slovenia")] - SI, - - [Description("North Korea")] - KP, - - [Description("Kuwait")] - KW, - - [Description("Senegal")] - SN, - - [Description("San Marino")] - SM, - - [Description("Sierra Leone")] - SL, + [Description("Solomon Islands")] + SB, [Description("Seychelles")] SC, - [Description("Kazakhstan")] - KZ, - - [Description("Cayman Islands")] - KY, - - [Description("Singapore")] - SG, + [Description("Sudan")] + SD, [Description("Sweden")] SE, - [Description("Sudan")] - SD, + [Description("Singapore")] + SG, - [Description("Dominican Republic")] - DO, + [Description("Saint Helena")] + SH, - [Description("Dominica")] - DM, + [Description("Slovenia")] + SI, - [Description("Djibouti")] - DJ, + [Description("Svalbard and Jan Mayen")] + SJ, - [Description("Denmark")] - DK, + [Description("Slovakia")] + SK, - [Description("British Virgin Islands")] - VG, + [Description("Sierra Leone")] + SL, - [Description("Germany")] - DE, + [Description("San Marino")] + SM, - [Description("Yemen")] - YE, + [Description("Senegal")] + SN, - [Description("Algeria")] - DZ, + [Description("Somalia")] + SO, - [Description("United States")] - US, + [Description("Suriname")] + SR, - [Description("Uruguay")] - UY, + [Description("Sao Tome and Principe")] + ST, - [Description("Mayotte")] - YT, + [Description("El Salvador")] + SV, - [Description("United States Minor Outlying Islands")] - UM, + [Description("Sint Maarten")] + SX, - [Description("Lebanon")] - LB, + [Description("Syrian Arab Republic")] + SY, - [Description("Saint Lucia")] - LC, + [Description("Eswatini")] + SZ, - [Description("Laos")] - LA, + [Description("Turks and Caicos Islands")] + TC, - [Description("Tuvalu")] - TV, - - [Description("Taiwan")] - TW, - - [Description("Trinidad and Tobago")] - TT, - - [Description("Turkey")] - TR, - - [Description("Sri Lanka")] - LK, - - [Description("Liechtenstein")] - LI, - - [Description("Latvia")] - LV, - - [Description("Tonga")] - TO, - - [Description("Lithuania")] - LT, - - [Description("Luxembourg")] - LU, - - [Description("Liberia")] - LR, - - [Description("Lesotho")] - LS, - - [Description("Thailand")] - TH, + [Description("Chad")] + TD, [Description("French Southern Territories")] TF, @@ -675,94 +687,103 @@ namespace osu.Game.Users [Description("Togo")] TG, - [Description("Chad")] - TD, + [Description("Thailand")] + TH, - [Description("Turks and Caicos Islands")] - TC, + [Description("Tajikistan")] + TJ, - [Description("Libya")] - LY, + [Description("Tokelau")] + TK, - [Description("Vatican")] + [Description("Timor-Leste")] + TL, + + [Description("Turkmenistan")] + TM, + + [Description("Tunisia")] + TN, + + [Description("Tonga")] + TO, + + [Description("Türkiye")] + TR, + + [Description("Trinidad and Tobago")] + TT, + + [Description("Tuvalu")] + TV, + + [Description("Taiwan")] + TW, + + [Description("United Republic of Tanzania")] + TZ, + + [Description("Ukraine")] + UA, + + [Description("Uganda")] + UG, + + [Description("United States Minor Outlying Islands")] + UM, + + [Description("United States")] + US, + + [Description("Uruguay")] + UY, + + [Description("Uzbekistan")] + UZ, + + [Description("Holy See (Vatican City State)")] VA, [Description("Saint Vincent and the Grenadines")] VC, - [Description("United Arab Emirates")] - AE, + [Description("Venezuela")] + VE, - [Description("Andorra")] - AD, + [Description("Virgin Islands, British")] + VG, - [Description("Antigua and Barbuda")] - AG, - - [Description("Afghanistan")] - AF, - - [Description("Anguilla")] - AI, - - [Description("U.S. Virgin Islands")] + [Description("Virgin Islands, U.S.")] VI, - [Description("Iceland")] - IS, + [Description("Vietnam")] + VN, - [Description("Iran")] - IR, + [Description("Vanuatu")] + VU, - [Description("Armenia")] - AM, + [Description("Wallis and Futuna")] + WF, - [Description("Albania")] - AL, + [Description("Samoa")] + WS, - [Description("Angola")] - AO, + [Description("Kosovo")] + XK, - [Description("Antarctica")] - AQ, + [Description("Yemen")] + YE, - [Description("American Samoa")] - AS, + [Description("Mayotte")] + YT, - [Description("Argentina")] - AR, + [Description("South Africa")] + ZA, - [Description("Australia")] - AU, + [Description("Zambia")] + ZM, - [Description("Austria")] - AT, - - [Description("Aruba")] - AW, - - [Description("India")] - IN, - - [Description("Aland Islands")] - AX, - - [Description("Azerbaijan")] - AZ, - - [Description("Ireland")] - IE, - - [Description("Indonesia")] - ID, - - [Description("Ukraine")] - UA, - - [Description("Qatar")] - QA, - - [Description("Mozambique")] - MZ, + [Description("Zimbabwe")] + ZW, } } diff --git a/osu.Game/Users/CountryStatistics.cs b/osu.Game/Users/CountryStatistics.cs index 03d455bc04..921d60bb44 100644 --- a/osu.Game/Users/CountryStatistics.cs +++ b/osu.Game/Users/CountryStatistics.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 Newtonsoft.Json; namespace osu.Game.Users diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index e74ffc9d54..677a8fff36 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -6,14 +6,13 @@ using osu.Framework.Allocation; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics.Containers; +using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Users.Drawables { public partial class ClickableAvatar : OsuClickableContainer { - private const string default_tooltip_text = "view profile"; - public override LocalisableString TooltipText { get @@ -21,7 +20,7 @@ namespace osu.Game.Users.Drawables if (!Enabled.Value) return string.Empty; - return ShowUsernameTooltip ? (user?.Username ?? string.Empty) : default_tooltip_text; + return ShowUsernameTooltip ? (user?.Username ?? string.Empty) : ContextMenuStrings.ViewProfile; } set => throw new NotSupportedException(); } diff --git a/osu.Game/Users/Drawables/DrawableFlag.cs b/osu.Game/Users/Drawables/DrawableFlag.cs index 929a29251d..289f68ee7f 100644 --- a/osu.Game/Users/Drawables/DrawableFlag.cs +++ b/osu.Game/Users/Drawables/DrawableFlag.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 osu.Framework.Allocation; using osu.Framework.Extensions; diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index 0b11d12c46..c82f642fdc 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.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 osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Online.Rooms; @@ -113,7 +111,7 @@ namespace osu.Game.Users protected string Username => score.User.Username; - public BeatmapInfo BeatmapInfo => score.BeatmapInfo; + public BeatmapInfo? BeatmapInfo => score.BeatmapInfo; public WatchingReplay(ScoreInfo score) { diff --git a/osu.Game/Users/UserBrickPanel.cs b/osu.Game/Users/UserBrickPanel.cs index 69b390b36e..b92c9a9afd 100644 --- a/osu.Game/Users/UserBrickPanel.cs +++ b/osu.Game/Users/UserBrickPanel.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 osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; diff --git a/osu.Game/Users/UserGridPanel.cs b/osu.Game/Users/UserGridPanel.cs index 90b6c11f0e..f4ec1475b1 100644 --- a/osu.Game/Users/UserGridPanel.cs +++ b/osu.Game/Users/UserGridPanel.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Users/UserListPanel.cs b/osu.Game/Users/UserListPanel.cs index 3047e70a1a..4942cc7512 100644 --- a/osu.Game/Users/UserListPanel.cs +++ b/osu.Game/Users/UserListPanel.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 osu.Framework.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics.Colour; diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index e2dc511391..273faf9bd1 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; @@ -18,6 +19,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Resources.Localisation.Web; using osu.Game.Localisation; +using osu.Game.Online.Multiplayer; namespace osu.Game.Users { @@ -61,6 +63,9 @@ namespace osu.Game.Users [Resolved] protected OsuColour Colours { get; private set; } = null!; + [Resolved] + private MultiplayerClient? multiplayerClient { get; set; } + [BackgroundDependencyLoader] private void load() { @@ -117,6 +122,16 @@ namespace osu.Game.Users })); } + if ( + // TODO: uncomment this once lazer / osu-web is updating online states + // User.IsOnline && + multiplayerClient?.Room != null && + multiplayerClient.Room.Users.All(u => u.UserID != User.Id) + ) + { + items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () => multiplayerClient.InvitePlayer(User.Id))); + } + return items.ToArray(); } } diff --git a/osu.Game/Users/UserStatus.cs b/osu.Game/Users/UserStatus.cs index 075463c1e0..ffd86b78c7 100644 --- a/osu.Game/Users/UserStatus.cs +++ b/osu.Game/Users/UserStatus.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osu.Framework.Localisation; using osuTK.Graphics; using osu.Game.Graphics; diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs new file mode 100644 index 0000000000..725e93d098 --- /dev/null +++ b/osu.Game/Utils/GeometryUtils.cs @@ -0,0 +1,126 @@ +// 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.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Utils; +using osu.Game.Rulesets.Objects.Types; +using osuTK; + +namespace osu.Game.Utils +{ + public static class GeometryUtils + { + /// + /// Rotate a point around an arbitrary origin. + /// + /// The point. + /// The centre origin to rotate around. + /// The angle to rotate (in degrees). + public static Vector2 RotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle) + { + angle = -angle; + + point.X -= origin.X; + point.Y -= origin.Y; + + Vector2 ret; + ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle)); + ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle)); + + ret.X += origin.X; + ret.Y += origin.Y; + + return ret; + } + + /// + /// Given a flip direction, a surrounding quad for all selected objects, and a position, + /// will return the flipped position in screen space coordinates. + /// + public static Vector2 GetFlippedPosition(Direction direction, Quad quad, Vector2 position) + { + var centre = quad.Centre; + + switch (direction) + { + case Direction.Horizontal: + position.X = centre.X - (position.X - centre.X); + break; + + case Direction.Vertical: + position.Y = centre.Y - (position.Y - centre.Y); + break; + } + + return position; + } + + /// + /// Given a scale vector, a surrounding quad for all selected objects, and a position, + /// will return the scaled position in screen space coordinates. + /// + public static Vector2 GetScaledPosition(Anchor reference, Vector2 scale, Quad selectionQuad, Vector2 position) + { + // adjust the direction of scale depending on which side the user is dragging. + float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0; + float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0; + + // guard against no-ops and NaN. + if (scale.X != 0 && selectionQuad.Width > 0) + position.X = selectionQuad.TopLeft.X + xOffset + (position.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X); + + if (scale.Y != 0 && selectionQuad.Height > 0) + position.Y = selectionQuad.TopLeft.Y + yOffset + (position.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y); + + return position; + } + + /// + /// Returns a quad surrounding the provided points. + /// + /// The points to calculate a quad for. + public static Quad GetSurroundingQuad(IEnumerable points) + { + if (!points.Any()) + return new Quad(); + + Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue); + Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue); + + // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted + foreach (var p in points) + { + minPosition = Vector2.ComponentMin(minPosition, p); + maxPosition = Vector2.ComponentMax(maxPosition, p); + } + + Vector2 size = maxPosition - minPosition; + + return new Quad(minPosition.X, minPosition.Y, size.X, size.Y); + } + + /// + /// Returns a gamefield-space quad surrounding the provided hit objects. + /// + /// The hit objects to calculate a quad for. + public static Quad GetSurroundingQuad(IEnumerable hitObjects) => + GetSurroundingQuad(hitObjects.SelectMany(h => + { + if (h is IHasPath path) + { + return new[] + { + h.Position, + // can't use EndPosition for reverse slider cases. + h.Position + path.Path.PositionAt(1) + }; + } + + return new[] { h.Position }; + })); + } +} diff --git a/osu.Game/Utils/LimitedCapacityQueue.cs b/osu.Game/Utils/LimitedCapacityQueue.cs index 86a106a678..d36aa8af2c 100644 --- a/osu.Game/Utils/LimitedCapacityQueue.cs +++ b/osu.Game/Utils/LimitedCapacityQueue.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections; using System.Collections.Generic; diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index 8c39a2d15a..61622a7122 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -47,6 +47,7 @@ namespace osu.Game.Utils options.AutoSessionTracking = true; options.IsEnvironmentUser = false; + options.IsGlobalModeEnabled = true; // The reported release needs to match version as reported to Sentry in .github/workflows/sentry-release.yml options.Release = $"osu@{game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty)}"; }); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 8a941ca6c1..60c71a736d 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -1,4 +1,4 @@ - + net6.0 Library @@ -21,27 +21,28 @@ - + - - - - - + + + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + + - + diff --git a/osu.iOS.props b/osu.iOS.props index 1dcece7741..f1159f58b9 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -2,13 +2,20 @@ iPhone Developer true - - true $(NoWarn);MT7091 + + + true + + + + false + -all + ios-arm64 @@ -16,6 +23,6 @@ iossimulator-x64 - + diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 0ce1d952d0..cf51fe995b 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -34,9 +34,9 @@ CADisableMinimumFrameDurationOnPhone NSCameraUsageDescription - We don't really use the camera. + We don't really use the camera. NSMicrophoneUsageDescription - We don't really use the microphone. + We don't really use the microphone. UISupportedInterfaceOrientations UIInterfaceOrientationLandscapeRight @@ -130,5 +130,7 @@ Editor + LSApplicationCategoryType + public.app-category.music-games diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index c49e6907ff..502f302157 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.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 Foundation; using Microsoft.Maui.Devices; diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index b54794cd6d..c2778ca5b1 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -5,6 +5,7 @@ True ExplicitlyExcluded ExplicitlyExcluded + g_*.cs SOLUTION WARNING WARNING @@ -139,6 +140,8 @@ HINT HINT HINT + HINT + HINT DO_NOT_SHOW HINT HINT @@ -167,7 +170,7 @@ ERROR WARNING WARNING - HINT + DO_NOT_SHOW WARNING WARNING WARNING