diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 3cecb0d07c..b8dc201559 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -21,10 +21,10 @@ ] }, "ppy.localisationanalyser.tools": { - "version": "2023.712.0", + "version": "2023.1117.0", "commands": [ "localisation" ] } } -} \ No newline at end of file +} diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml index ff6d869e72..a8a5d5e64b 100644 --- a/.github/ISSUE_TEMPLATE/bug-issue.yml +++ b/.github/ISSUE_TEMPLATE/bug-issue.yml @@ -11,6 +11,10 @@ body: - Current open `priority:0` issues, filterable [here](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0). - And most importantly, search for your issue both in the [issue listing](https://github.com/ppy/osu/issues) and the [Q&A discussion listing](https://github.com/ppy/osu/discussions/categories/q-a). If you find that it already exists, respond with a reaction or add any further information that may be helpful. + # ATTENTION LINUX USERS + + If you are having an issue and it is hardware related, **please open a [q&a discussion](https://github.com/ppy/osu/discussions/categories/q-a)** instead of an issue. There's a high chance your issue is due to your system configuration, and not our software. + - type: dropdown attributes: label: Type @@ -38,7 +42,7 @@ body: - type: input attributes: label: Version - description: The version you encountered this bug on. This is shown at the bottom of the main menu and also at the end of the settings screen. + description: The version you encountered this bug on. This is shown at the end of the settings overlay. validations: required: true - type: markdown @@ -46,22 +50,16 @@ body: value: | ## Logs - Attaching log files is required for every reported bug. See instructions below on how to find them. - - **Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead. + Attaching log files is required for **every** issue, regardless of whether you deem them required or not. See instructions below on how to find them. ### Desktop platforms If the game has not yet been closed since you found the bug: - 1. Head on to game settings and click on "Open osu! folder" - 2. Then open the `logs` folder located there + 1. Head on to game settings and click on "Export logs" + 2. Click the notification to locate the file + 3. Drag the generated `.zip` files into the github issue window - The default places to find the logs on desktop platforms are as follows: - - `%AppData%/osu/logs` *on Windows* - - `~/.local/share/osu/logs` *on Linux* - - `~/Library/Application Support/osu/logs` *on macOS* - - If you have selected a custom location for the game files, you can find the `logs` folder there. + ![export logs button](https://github.com/ppy/osu/assets/191335/cbfa5550-b7ed-4c5c-8dd0-8b87cc90ad9b) ### Mobile platforms @@ -69,10 +67,6 @@ body: - *On Android*, navigate to `Android/data/sh.ppy.osulazer/files/logs` using a file browser app. - *On iOS*, connect your device to a PC and copy the `logs` directory from the app's document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer) - --- - - After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below. - - type: textarea attributes: label: Logs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8167ec4db..103e4dbc30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,6 +108,12 @@ jobs: - name: Checkout uses: actions/checkout@v3 + - name: Setup JDK 11 + uses: actions/setup-java@v3 + with: + distribution: microsoft + java-version: 11 + - name: Install .NET 6.0.x uses: actions/setup-dotnet@v3 with: @@ -121,24 +127,14 @@ jobs: build-only-ios: name: Build only (iOS) - # `macos-13` is required, because Xcode 14.3 is required (see below). - # TODO: can be changed to `macos-latest` once `macos-13` becomes latest (currently in beta) + # `macos-13` is required, because the newest Microsoft.iOS.Sdk versions require Xcode 14.3. + # TODO: can be changed to `macos-latest` once `macos-13` becomes latest (currently in beta: https://github.com/actions/runner-images/tree/main#available-images) runs-on: macos-13 timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@v3 - # newest Microsoft.iOS.Sdk versions require Xcode 14.3. - # 14.3 is currently not the default Xcode version (https://github.com/actions/runner-images/blob/main/images/macos/macos-13-Readme.md#xcode), - # so set it manually. - # TODO: remove when 14.3 becomes the default Xcode version. - - name: Set Xcode version - shell: bash - run: | - sudo xcode-select -s "/Applications/Xcode_14.3.app" - echo "MD_APPLE_SDK_ROOT=/Applications/Xcode_14.3.app" >> $GITHUB_ENV - - name: Install .NET 6.0.x uses: actions/setup-dotnet@v3 with: diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index 2c6ec17e18..5f16e09040 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -1,206 +1,382 @@ -# 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 + - name: Add comment environment + if: ${{ github.event_name == 'issue_comment' }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMENT_BODY: ${{ github.event.comment.body }} 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 "$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/CONTRIBUTING.md b/CONTRIBUTING.md index 9f7d88f5c7..4106641adb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,7 @@ The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive. -If you'd like to propose a subjective change to one of the visual aspects of the game, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort. This in particular applies if you want to work on [one of the available designs from the osu! public Figma library](https://www.figma.com/file/6m10GiGEncVFWmgOoSyakH/osu!-Figma-Library). +If you'd like to propose a subjective change to one of the visual aspects of the game, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort. This in particular applies if you want to work on [one of the available designs from the osu! Figma master library](https://www.figma.com/file/VIkXMYNPMtQem2RJg9k2iQ/Master-Library). Aside from the above, below is a brief checklist of things to watch out when you're preparing your code changes: @@ -85,4 +85,4 @@ If you're uncertain about some part of the codebase or some inner workings of th - [Development roadmap](https://github.com/orgs/ppy/projects/7/views/6): What the core team is currently working on - [`ppy/osu-framework` wiki](https://github.com/ppy/osu-framework/wiki): Contains introductory information about osu!framework, the bespoke 2D game framework we use for the game - [`ppy/osu` wiki](https://github.com/ppy/osu/wiki): Contains articles about various technical aspects of the game -- [Public Figma library](https://www.figma.com/file/6m10GiGEncVFWmgOoSyakH/osu!-Figma-Library): Contains finished and draft designs for osu! +- [Figma master library](https://www.figma.com/file/VIkXMYNPMtQem2RJg9k2iQ/Master-Library): Contains finished and draft designs for osu! 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/VisualTestRunner.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs index 03ee7c9204..63c481a623 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu")) { host.Run(new OsuTestBrowser()); return 0; 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..5babdb47ff 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/VisualTestRunner.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs index 55c0cf6a3b..c44cbb845b 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu")) { host.Run(new OsuTestBrowser()); return 0; 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..5d64ca832a 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/VisualTestRunner.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs index b45505678c..5beb6616a7 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu")) { host.Run(new OsuTestBrowser()); return 0; 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..6796a8962b 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/VisualTestRunner.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs index 55c0cf6a3b..c44cbb845b 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu")) { host.Run(new OsuTestBrowser()); return 0; 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..5d64ca832a 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/osu.Android.props b/osu.Android.props index 7f15d9fafd..a7376aa5a7 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + + + + + + diff --git a/osu.Android/AndroidMouseSettings.cs b/osu.Android/AndroidMouseSettings.cs index d6d7750448..fd01b11164 100644 --- a/osu.Android/AndroidMouseSettings.cs +++ b/osu.Android/AndroidMouseSettings.cs @@ -70,7 +70,7 @@ namespace osu.Android }, new SettingsCheckbox { - LabelText = MouseSettingsStrings.DisableMouseButtons, + LabelText = MouseSettingsStrings.DisableClicksDuringGameplay, Current = osuConfig.GetBindable(OsuSetting.MouseDisableButtons), }, }); diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index dea70e6b27..52cfb67f42 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -11,6 +11,7 @@ using osu.Framework.Input.Handlers; using osu.Framework.Platform; using osu.Game; using osu.Game.Overlays.Settings; +using osu.Game.Overlays.Settings.Sections.Input; using osu.Game.Updater; using osu.Game.Utils; @@ -97,6 +98,9 @@ namespace osu.Android case AndroidJoystickHandler jh: return new AndroidJoystickSettings(jh); + case AndroidTouchHandler th: + return new TouchSettings(th); + default: return base.CreateSettingsSubsectionFor(handler); } diff --git a/osu.Desktop.slnf b/osu.Desktop.slnf index 503e5935f5..606988ccdf 100644 --- a/osu.Desktop.slnf +++ b/osu.Desktop.slnf @@ -16,15 +16,14 @@ "osu.Game.Tournament.Tests\\osu.Game.Tournament.Tests.csproj", "osu.Game.Tournament\\osu.Game.Tournament.csproj", "osu.Game\\osu.Game.csproj", - - "Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform\\osu.Game.Rulesets.EmptyFreeform.csproj", "Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform.Tests\\osu.Game.Rulesets.EmptyFreeform.Tests.csproj", - "Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj", + "Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform\\osu.Game.Rulesets.EmptyFreeform.csproj", "Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj", - "Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj", + "Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj", "Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling.Tests\\osu.Game.Rulesets.EmptyScrolling.Tests.csproj", - "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj", - "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj" + "Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj", + "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj", + "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj" ] } -} +} \ No newline at end of file diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index caf0a1d9fd..f990fd55fc 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -9,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; -using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Online.API; @@ -34,7 +33,7 @@ namespace osu.Desktop [Resolved] private IAPIProvider api { get; set; } = null!; - private readonly IBindable status = new Bindable(); + private readonly IBindable status = new Bindable(); private readonly IBindable activity = new Bindable(); private readonly Bindable privacyMode = new Bindable(); @@ -87,25 +86,26 @@ namespace osu.Desktop if (!client.IsInitialized) return; - if (status.Value is UserStatusOffline || privacyMode.Value == DiscordRichPresenceMode.Off) + if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off) { client.ClearPresence(); return; } - if (status.Value is UserStatusOnline && activity.Value != null) + if (status.Value == UserStatus.Online && activity.Value != null) { - presence.State = truncate(activity.Value.GetStatus(privacyMode.Value == DiscordRichPresenceMode.Limited)); - presence.Details = truncate(getDetails(activity.Value)); + bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited; + presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation)); + presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); - if (getBeatmap(activity.Value) is IBeatmapInfo beatmap && beatmap.OnlineID > 0) + if (getBeatmapID(activity.Value) is int beatmapId && beatmapId > 0) { presence.Buttons = new[] { new Button { Label = "View beatmap", - Url = $@"{api.WebsiteRootUrl}/beatmapsets/{beatmap.BeatmapSet?.OnlineID}#{ruleset.Value.ShortName}/{beatmap.OnlineID}" + Url = $@"{api.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}" } }; } @@ -159,40 +159,20 @@ namespace osu.Desktop }); } - private IBeatmapInfo? getBeatmap(UserActivity activity) + private int? getBeatmapID(UserActivity activity) { switch (activity) { case UserActivity.InGame game: - return game.BeatmapInfo; + return game.BeatmapID; case UserActivity.EditingBeatmap edit: - return edit.BeatmapInfo; + return edit.BeatmapID; } return null; } - private string getDetails(UserActivity activity) - { - switch (activity) - { - case UserActivity.InGame game: - return game.BeatmapInfo.ToString() ?? string.Empty; - - case UserActivity.EditingBeatmap edit: - return edit.BeatmapInfo.ToString() ?? string.Empty; - - case UserActivity.WatchingReplay watching: - return watching.BeatmapInfo?.ToString() ?? string.Empty; - - case UserActivity.InLobby lobby: - return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value; - } - - return string.Empty; - } - protected override void Dispose(bool isDisposing) { client.Dispose(); 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/NVAPI.cs b/osu.Desktop/NVAPI.cs new file mode 100644 index 0000000000..bb3a59cc7f --- /dev/null +++ b/osu.Desktop/NVAPI.cs @@ -0,0 +1,739 @@ +// 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 IDE1006 // Naming rule violation + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using osu.Framework.Logging; + +namespace osu.Desktop +{ + [SuppressMessage("ReSharper", "InconsistentNaming")] + internal static class NVAPI + { + private const string osu_filename = "osu!.exe"; + + // This is a good reference: + // https://github.com/errollw/Warp-and-Blend-Quadros/blob/master/WarpBlend-Quadros/UnwarpAll-Quadros/include/nvapi.h + // Note our Stride == their VERSION (e.g. NVDRS_SETTING_VER) + + public const int MAX_PHYSICAL_GPUS = 64; + public const int UNICODE_STRING_MAX = 2048; + + public const string APPLICATION_NAME = @"osu!"; + public const string PROFILE_NAME = @"osu!"; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus EnumPhysicalGPUsDelegate([Out] IntPtr[] gpuHandles, out int gpuCount); + + public static readonly EnumPhysicalGPUsDelegate EnumPhysicalGPUs; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus EnumLogicalGPUsDelegate([Out] IntPtr[] gpuHandles, out int gpuCount); + + public static readonly EnumLogicalGPUsDelegate EnumLogicalGPUs; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus GetSystemTypeDelegate(IntPtr gpuHandle, out NvSystemType systemType); + + public static readonly GetSystemTypeDelegate GetSystemType; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus GetGPUTypeDelegate(IntPtr gpuHandle, out NvGpuType gpuType); + + public static readonly GetGPUTypeDelegate GetGPUType; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus CreateSessionDelegate(out IntPtr sessionHandle); + + public static CreateSessionDelegate CreateSession; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus LoadSettingsDelegate(IntPtr sessionHandle); + + public static LoadSettingsDelegate LoadSettings; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus FindApplicationByNameDelegate(IntPtr sessionHandle, [MarshalAs(UnmanagedType.BStr)] string appName, out IntPtr profileHandle, ref NvApplication application); + + public static FindApplicationByNameDelegate FindApplicationByName; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus GetCurrentGlobalProfileDelegate(IntPtr sessionHandle, out IntPtr profileHandle); + + public static GetCurrentGlobalProfileDelegate GetCurrentGlobalProfile; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus GetProfileInfoDelegate(IntPtr sessionHandle, IntPtr profileHandle, ref NvProfile profile); + + public static GetProfileInfoDelegate GetProfileInfo; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus GetSettingDelegate(IntPtr sessionHandle, IntPtr profileHandle, NvSettingID settingID, ref NvSetting setting); + + public static GetSettingDelegate GetSetting; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate NvStatus CreateProfileDelegate(IntPtr sessionHandle, ref NvProfile profile, out IntPtr profileHandle); + + private static readonly CreateProfileDelegate CreateProfile; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate NvStatus SetSettingDelegate(IntPtr sessionHandle, IntPtr profileHandle, ref NvSetting setting); + + private static readonly SetSettingDelegate SetSetting; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate NvStatus EnumApplicationsDelegate(IntPtr sessionHandle, IntPtr profileHandle, uint startIndex, ref uint appCount, [In, Out, MarshalAs(UnmanagedType.LPArray)] NvApplication[] applications); + + private static readonly EnumApplicationsDelegate EnumApplications; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate NvStatus CreateApplicationDelegate(IntPtr sessionHandle, IntPtr profileHandle, ref NvApplication application); + + private static readonly CreateApplicationDelegate CreateApplication; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate NvStatus SaveSettingsDelegate(IntPtr sessionHandle); + + private static readonly SaveSettingsDelegate SaveSettings; + + public static NvStatus Status { get; private set; } = NvStatus.OK; + public static bool Available { get; private set; } + + private static IntPtr sessionHandle; + + public static bool IsUsingOptimusDedicatedGpu + { + get + { + if (!Available) + return false; + + if (!IsLaptop) + return false; + + IntPtr profileHandle; + if (!getProfile(out profileHandle, out _, out bool _)) + return false; + + // Get the optimus setting + NvSetting setting; + if (!getSetting(NvSettingID.SHIM_RENDERING_MODE_ID, profileHandle, out setting)) + return false; + + return (setting.U32CurrentValue & (uint)NvShimSetting.SHIM_RENDERING_MODE_ENABLE) > 0; + } + } + + public static bool IsLaptop + { + get + { + if (!Available) + return false; + + // Make sure that this is a laptop. + var gpus = new IntPtr[64]; + if (checkError(EnumPhysicalGPUs(gpus, out int gpuCount))) + return false; + + for (int i = 0; i < gpuCount; i++) + { + if (checkError(GetSystemType(gpus[i], out var type))) + return false; + + if (type == NvSystemType.LAPTOP) + return true; + } + + return false; + } + } + + public static NvThreadControlSetting ThreadedOptimisations + { + get + { + if (!Available) + return NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT; + + IntPtr profileHandle; + if (!getProfile(out profileHandle, out _, out bool _)) + return NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT; + + // Get the threaded optimisations setting + NvSetting setting; + if (!getSetting(NvSettingID.OGL_THREAD_CONTROL_ID, profileHandle, out setting)) + return NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT; + + return (NvThreadControlSetting)setting.U32CurrentValue; + } + set + { + if (!Available) + return; + + bool success = setSetting(NvSettingID.OGL_THREAD_CONTROL_ID, (uint)value); + + Logger.Log(success ? $"Threaded optimizations set to \"{value}\"!" : "Threaded optimizations set failed!"); + } + } + + /// + /// Checks if the profile contains the current application. + /// + /// If the profile contains the current application. + private static bool containsApplication(IntPtr profileHandle, NvProfile profile, out NvApplication application) + { + application = new NvApplication + { + Version = NvApplication.Stride + }; + + if (profile.NumOfApps == 0) + return false; + + NvApplication[] applications = new NvApplication[profile.NumOfApps]; + applications[0].Version = NvApplication.Stride; + + uint numApps = profile.NumOfApps; + + if (checkError(EnumApplications(sessionHandle, profileHandle, 0, ref numApps, applications))) + return false; + + for (uint i = 0; i < numApps; i++) + { + if (applications[i].AppName == osu_filename) + { + application = applications[i]; + return true; + } + } + + return false; + } + + /// + /// Retrieves the profile of the current application. + /// + /// The profile handle. + /// The current application description. + /// If this profile is not a global (default) profile. + /// If the operation succeeded. + private static bool getProfile(out IntPtr profileHandle, out NvApplication application, out bool isApplicationSpecific) + { + application = new NvApplication + { + Version = NvApplication.Stride + }; + + isApplicationSpecific = true; + + if (checkError(FindApplicationByName(sessionHandle, osu_filename, out profileHandle, ref application))) + { + isApplicationSpecific = false; + if (checkError(GetCurrentGlobalProfile(sessionHandle, out profileHandle))) + return false; + } + + return true; + } + + /// + /// Creates a profile. + /// + /// The profile handle. + /// If the operation succeeded. + private static bool createProfile(out IntPtr profileHandle) + { + NvProfile newProfile = new NvProfile + { + Version = NvProfile.Stride, + IsPredefined = 0, + ProfileName = PROFILE_NAME, + GPUSupport = new uint[32] + }; + + newProfile.GPUSupport[0] = 1; + + if (checkError(CreateProfile(sessionHandle, ref newProfile, out profileHandle))) + return false; + + return true; + } + + /// + /// Retrieves a setting from the profile. + /// + /// The setting to retrieve. + /// The profile handle to retrieve the setting from. + /// The setting. + /// If the operation succeeded. + private static bool getSetting(NvSettingID settingId, IntPtr profileHandle, out NvSetting setting) + { + setting = new NvSetting + { + Version = NvSetting.Stride, + SettingID = settingId + }; + + if (checkError(GetSetting(sessionHandle, profileHandle, settingId, ref setting))) + return false; + + return true; + } + + private static bool setSetting(NvSettingID settingId, uint settingValue) + { + NvApplication application; + IntPtr profileHandle; + bool isApplicationSpecific; + if (!getProfile(out profileHandle, out application, out isApplicationSpecific)) + return false; + + if (!isApplicationSpecific) + { + // We don't want to interfere with the user's other settings, so let's create a separate config for osu! + if (!createProfile(out profileHandle)) + return false; + } + + NvSetting newSetting = new NvSetting + { + Version = NvSetting.Stride, + SettingID = settingId, + U32CurrentValue = settingValue + }; + + // Set the thread state + if (checkError(SetSetting(sessionHandle, profileHandle, ref newSetting))) + return false; + + // Get the profile (needed to check app count) + NvProfile profile = new NvProfile + { + Version = NvProfile.Stride + }; + if (checkError(GetProfileInfo(sessionHandle, profileHandle, ref profile))) + return false; + + if (!containsApplication(profileHandle, profile, out application)) + { + // Need to add the current application to the profile + application.IsPredefined = 0; + + application.AppName = osu_filename; + application.UserFriendlyName = APPLICATION_NAME; + + if (checkError(CreateApplication(sessionHandle, profileHandle, ref application))) + return false; + } + + // Save! + return !checkError(SaveSettings(sessionHandle)); + } + + /// + /// Creates a session to access the driver configuration. + /// + /// If the operation succeeded. + private static bool createSession() + { + if (checkError(CreateSession(out sessionHandle))) + return false; + + // Load settings into session + if (checkError(LoadSettings(sessionHandle))) + return false; + + return true; + } + + private static bool checkError(NvStatus status) + { + Status = status; + return status != NvStatus.OK; + } + + static NVAPI() + { + // TODO: check whether gpu vendor contains NVIDIA before attempting load? + + try + { + // Try to load NVAPI + if ((IntPtr.Size == 4 && loadLibrary(@"nvapi.dll") == IntPtr.Zero) + || (IntPtr.Size == 8 && loadLibrary(@"nvapi64.dll") == IntPtr.Zero)) + { + return; + } + + InitializeDelegate initialize; + getDelegate(0x0150E828, out initialize); + + if (initialize?.Invoke() == NvStatus.OK) + { + // IDs can be found here: https://github.com/jNizM/AHK_NVIDIA_NvAPI/blob/master/info/NvAPI_IDs.txt + + getDelegate(0xE5AC921F, out EnumPhysicalGPUs); + getDelegate(0x48B3EA59, out EnumLogicalGPUs); + getDelegate(0xBAAABFCC, out GetSystemType); + getDelegate(0xC33BAEB1, out GetGPUType); + getDelegate(0x0694D52E, out CreateSession); + getDelegate(0x375DBD6B, out LoadSettings); + getDelegate(0xEEE566B2, out FindApplicationByName); + getDelegate(0x617BFF9F, out GetCurrentGlobalProfile); + getDelegate(0x577DD202, out SetSetting); + getDelegate(0x61CD6FD6, out GetProfileInfo); + getDelegate(0x73BF8338, out GetSetting); + getDelegate(0xCC176068, out CreateProfile); + getDelegate(0x7FA2173A, out EnumApplications); + getDelegate(0x4347A9DE, out CreateApplication); + getDelegate(0xFCBC7E14, out SaveSettings); + } + + if (createSession()) + Available = true; + } + catch { } + } + + private static void getDelegate(uint id, out T newDelegate) where T : class + { + IntPtr ptr = IntPtr.Size == 4 ? queryInterface32(id) : queryInterface64(id); + newDelegate = ptr == IntPtr.Zero ? null : Marshal.GetDelegateForFunctionPointer(ptr, typeof(T)) as T; + } + + [DllImport("kernel32.dll", EntryPoint = "LoadLibrary")] + private static extern IntPtr loadLibrary(string dllToLoad); + + [DllImport(@"nvapi.dll", EntryPoint = "nvapi_QueryInterface", CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr queryInterface32(uint id); + + [DllImport(@"nvapi64.dll", EntryPoint = "nvapi_QueryInterface", CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr queryInterface64(uint id); + + private delegate NvStatus InitializeDelegate(); + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct NvSetting + { + public uint Version; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)] + public string SettingName; + + public NvSettingID SettingID; + public uint SettingType; + public uint SettingLocation; + public uint IsCurrentPredefined; + public uint IsPredefinedValid; + + public uint U32PredefinedValue; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)] + public string StringPredefinedValue; + + public uint U32CurrentValue; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)] + public string StringCurrentValue; + + public static uint Stride => (uint)Marshal.SizeOf(typeof(NvSetting)) | (1 << 16); + } + + [StructLayout(LayoutKind.Sequential, Pack = 8, CharSet = CharSet.Unicode)] + internal struct NvProfile + { + public uint Version; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)] + public string ProfileName; + + [MarshalAs(UnmanagedType.ByValArray)] + public uint[] GPUSupport; + + public uint IsPredefined; + public uint NumOfApps; + public uint NumOfSettings; + + public static uint Stride => (uint)Marshal.SizeOf(typeof(NvProfile)) | (1 << 16); + } + + [StructLayout(LayoutKind.Sequential, Pack = 8, CharSet = CharSet.Unicode)] + internal struct NvApplication + { + public uint Version; + public uint IsPredefined; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)] + public string AppName; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)] + public string UserFriendlyName; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)] + public string Launcher; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)] + public string FileInFolder; + + public static uint Stride => (uint)Marshal.SizeOf(typeof(NvApplication)) | (2 << 16); + } + + internal enum NvStatus + { + OK = 0, // Success. Request is completed. + ERROR = -1, // Generic error + LIBRARY_NOT_FOUND = -2, // NVAPI support library cannot be loaded. + NO_IMPLEMENTATION = -3, // not implemented in current driver installation + API_NOT_INITIALIZED = -4, // Initialize has not been called (successfully) + INVALID_ARGUMENT = -5, // The argument/parameter value is not valid or NULL. + NVIDIA_DEVICE_NOT_FOUND = -6, // No NVIDIA display driver, or NVIDIA GPU driving a display, was found. + END_ENUMERATION = -7, // No more items to enumerate + INVALID_HANDLE = -8, // Invalid handle + INCOMPATIBLE_STRUCT_VERSION = -9, // An argument's structure version is not supported + HANDLE_INVALIDATED = -10, // The handle is no longer valid (likely due to GPU or display re-configuration) + OPENGL_CONTEXT_NOT_CURRENT = -11, // No NVIDIA OpenGL context is current (but needs to be) + INVALID_POINTER = -14, // An invalid pointer, usually NULL, was passed as a parameter + NO_GL_EXPERT = -12, // OpenGL Expert is not supported by the current drivers + INSTRUMENTATION_DISABLED = -13, // OpenGL Expert is supported, but driver instrumentation is currently disabled + NO_GL_NSIGHT = -15, // OpenGL does not support Nsight + + EXPECTED_LOGICAL_GPU_HANDLE = -100, // Expected a logical GPU handle for one or more parameters + EXPECTED_PHYSICAL_GPU_HANDLE = -101, // Expected a physical GPU handle for one or more parameters + EXPECTED_DISPLAY_HANDLE = -102, // Expected an NV display handle for one or more parameters + INVALID_COMBINATION = -103, // The combination of parameters is not valid. + NOT_SUPPORTED = -104, // Requested feature is not supported in the selected GPU + PORTID_NOT_FOUND = -105, // No port ID was found for the I2C transaction + EXPECTED_UNATTACHED_DISPLAY_HANDLE = -106, // Expected an unattached display handle as one of the input parameters. + INVALID_PERF_LEVEL = -107, // Invalid perf level + DEVICE_BUSY = -108, // Device is busy; request not fulfilled + NV_PERSIST_FILE_NOT_FOUND = -109, // NV persist file is not found + PERSIST_DATA_NOT_FOUND = -110, // NV persist data is not found + EXPECTED_TV_DISPLAY = -111, // Expected a TV output display + EXPECTED_TV_DISPLAY_ON_DCONNECTOR = -112, // Expected a TV output on the D Connector - HDTV_EIAJ4120. + NO_ACTIVE_SLI_TOPOLOGY = -113, // SLI is not active on this device. + SLI_RENDERING_MODE_NOTALLOWED = -114, // Setup of SLI rendering mode is not possible right now. + EXPECTED_DIGITAL_FLAT_PANEL = -115, // Expected a digital flat panel. + ARGUMENT_EXCEED_MAX_SIZE = -116, // Argument exceeds the expected size. + DEVICE_SWITCHING_NOT_ALLOWED = -117, // Inhibit is ON due to one of the flags in NV_GPU_DISPLAY_CHANGE_INHIBIT or SLI active. + TESTING_CLOCKS_NOT_SUPPORTED = -118, // Testing of clocks is not supported. + UNKNOWN_UNDERSCAN_CONFIG = -119, // The specified underscan config is from an unknown source (e.g. INF) + TIMEOUT_RECONFIGURING_GPU_TOPO = -120, // Timeout while reconfiguring GPUs + DATA_NOT_FOUND = -121, // Requested data was not found + EXPECTED_ANALOG_DISPLAY = -122, // Expected an analog display + NO_VIDLINK = -123, // No SLI video bridge is present + REQUIRES_REBOOT = -124, // NVAPI requires a reboot for the settings to take effect + INVALID_HYBRID_MODE = -125, // The function is not supported with the current Hybrid mode. + MIXED_TARGET_TYPES = -126, // The target types are not all the same + SYSWOW64_NOT_SUPPORTED = -127, // The function is not supported from 32-bit on a 64-bit system. + IMPLICIT_SET_GPU_TOPOLOGY_CHANGE_NOT_ALLOWED = -128, // There is no implicit GPU topology active. Use SetHybridMode to change topology. + REQUEST_USER_TO_CLOSE_NON_MIGRATABLE_APPS = -129, // Prompt the user to close all non-migratable applications. + OUT_OF_MEMORY = -130, // Could not allocate sufficient memory to complete the call. + WAS_STILL_DRAWING = -131, // The previous operation that is transferring information to or from this surface is incomplete. + FILE_NOT_FOUND = -132, // The file was not found. + TOO_MANY_UNIQUE_STATE_OBJECTS = -133, // There are too many unique instances of a particular type of state object. + INVALID_CALL = -134, // The method call is invalid. For example, a method's parameter may not be a valid pointer. + D3D10_1_LIBRARY_NOT_FOUND = -135, // d3d10_1.dll cannot be loaded. + FUNCTION_NOT_FOUND = -136, // Couldn't find the function in the loaded DLL. + INVALID_USER_PRIVILEGE = -137, // Current User is not Admin. + EXPECTED_NON_PRIMARY_DISPLAY_HANDLE = -138, // The handle corresponds to GDIPrimary. + EXPECTED_COMPUTE_GPU_HANDLE = -139, // Setting Physx GPU requires that the GPU is compute-capable. + STEREO_NOT_INITIALIZED = -140, // The Stereo part of NVAPI failed to initialize completely. Check if the stereo driver is installed. + STEREO_REGISTRY_ACCESS_FAILED = -141, // Access to stereo-related registry keys or values has failed. + STEREO_REGISTRY_PROFILE_TYPE_NOT_SUPPORTED = -142, // The given registry profile type is not supported. + STEREO_REGISTRY_VALUE_NOT_SUPPORTED = -143, // The given registry value is not supported. + STEREO_NOT_ENABLED = -144, // Stereo is not enabled and the function needed it to execute completely. + STEREO_NOT_TURNED_ON = -145, // Stereo is not turned on and the function needed it to execute completely. + STEREO_INVALID_DEVICE_INTERFACE = -146, // Invalid device interface. + STEREO_PARAMETER_OUT_OF_RANGE = -147, // Separation percentage or JPEG image capture quality is out of [0-100] range. + STEREO_FRUSTUM_ADJUST_MODE_NOT_SUPPORTED = -148, // The given frustum adjust mode is not supported. + TOPO_NOT_POSSIBLE = -149, // The mosaic topology is not possible given the current state of the hardware. + MODE_CHANGE_FAILED = -150, // An attempt to do a display resolution mode change has failed. + D3D11_LIBRARY_NOT_FOUND = -151, // d3d11.dll/d3d11_beta.dll cannot be loaded. + INVALID_ADDRESS = -152, // Address is outside of valid range. + STRING_TOO_SMALL = -153, // The pre-allocated string is too small to hold the result. + MATCHING_DEVICE_NOT_FOUND = -154, // The input does not match any of the available devices. + DRIVER_RUNNING = -155, // Driver is running. + DRIVER_NOTRUNNING = -156, // Driver is not running. + ERROR_DRIVER_RELOAD_REQUIRED = -157, // A driver reload is required to apply these settings. + SET_NOT_ALLOWED = -158, // Intended setting is not allowed. + ADVANCED_DISPLAY_TOPOLOGY_REQUIRED = -159, // Information can't be returned due to "advanced display topology". + SETTING_NOT_FOUND = -160, // Setting is not found. + SETTING_SIZE_TOO_LARGE = -161, // Setting size is too large. + TOO_MANY_SETTINGS_IN_PROFILE = -162, // There are too many settings for a profile. + PROFILE_NOT_FOUND = -163, // Profile is not found. + PROFILE_NAME_IN_USE = -164, // Profile name is duplicated. + PROFILE_NAME_EMPTY = -165, // Profile name is empty. + EXECUTABLE_NOT_FOUND = -166, // Application not found in the Profile. + EXECUTABLE_ALREADY_IN_USE = -167, // Application already exists in the other profile. + DATATYPE_MISMATCH = -168, // Data Type mismatch + PROFILE_REMOVED = -169, // The profile passed as parameter has been removed and is no longer valid. + UNREGISTERED_RESOURCE = -170, // An unregistered resource was passed as a parameter. + ID_OUT_OF_RANGE = -171, // The DisplayId corresponds to a display which is not within the normal outputId range. + DISPLAYCONFIG_VALIDATION_FAILED = -172, // Display topology is not valid so the driver cannot do a mode set on this configuration. + DPMST_CHANGED = -173, // Display Port Multi-Stream topology has been changed. + INSUFFICIENT_BUFFER = -174, // Input buffer is insufficient to hold the contents. + ACCESS_DENIED = -175, // No access to the caller. + MOSAIC_NOT_ACTIVE = -176, // The requested action cannot be performed without Mosaic being enabled. + SHARE_RESOURCE_RELOCATED = -177, // The surface is relocated away from video memory. + REQUEST_USER_TO_DISABLE_DWM = -178, // The user should disable DWM before calling NvAPI. + D3D_DEVICE_LOST = -179, // D3D device status is D3DERR_DEVICELOST or D3DERR_DEVICENOTRESET - the user has to reset the device. + INVALID_CONFIGURATION = -180, // The requested action cannot be performed in the current state. + STEREO_HANDSHAKE_NOT_DONE = -181, // Call failed as stereo handshake not completed. + EXECUTABLE_PATH_IS_AMBIGUOUS = -182, // The path provided was too short to determine the correct NVDRS_APPLICATION + DEFAULT_STEREO_PROFILE_IS_NOT_DEFINED = -183, // Default stereo profile is not currently defined + DEFAULT_STEREO_PROFILE_DOES_NOT_EXIST = -184, // Default stereo profile does not exist + CLUSTER_ALREADY_EXISTS = -185, // A cluster is already defined with the given configuration. + DPMST_DISPLAY_ID_EXPECTED = -186, // The input display id is not that of a multi stream enabled connector or a display device in a multi stream topology + INVALID_DISPLAY_ID = -187, // The input display id is not valid or the monitor associated to it does not support the current operation + STREAM_IS_OUT_OF_SYNC = -188, // While playing secure audio stream, stream goes out of sync + INCOMPATIBLE_AUDIO_DRIVER = -189, // Older audio driver version than required + VALUE_ALREADY_SET = -190, // Value already set, setting again not allowed. + TIMEOUT = -191, // Requested operation timed out + GPU_WORKSTATION_FEATURE_INCOMPLETE = -192, // The requested workstation feature set has incomplete driver internal allocation resources + STEREO_INIT_ACTIVATION_NOT_DONE = -193, // Call failed because InitActivation was not called. + SYNC_NOT_ACTIVE = -194, // The requested action cannot be performed without Sync being enabled. + SYNC_MASTER_NOT_FOUND = -195, // The requested action cannot be performed without Sync Master being enabled. + INVALID_SYNC_TOPOLOGY = -196, // Invalid displays passed in the NV_GSYNC_DISPLAY pointer. + ECID_SIGN_ALGO_UNSUPPORTED = -197, // The specified signing algorithm is not supported. Either an incorrect value was entered or the current installed driver/hardware does not support the input value. + ECID_KEY_VERIFICATION_FAILED = -198, // The encrypted public key verification has failed. + FIRMWARE_OUT_OF_DATE = -199, // The device's firmware is out of date. + FIRMWARE_REVISION_NOT_SUPPORTED = -200, // The device's firmware is not supported. + } + + internal enum NvSystemType + { + UNKNOWN = 0, + LAPTOP = 1, + DESKTOP = 2 + } + + internal enum NvGpuType + { + UNKNOWN = 0, + IGPU = 1, // Integrated + DGPU = 2, // Discrete + } + + internal enum NvSettingID : uint + { + OGL_AA_LINE_GAMMA_ID = 0x2089BF6C, + OGL_DEEP_COLOR_SCANOUT_ID = 0x2097C2F6, + OGL_DEFAULT_SWAP_INTERVAL_ID = 0x206A6582, + OGL_DEFAULT_SWAP_INTERVAL_FRACTIONAL_ID = 0x206C4581, + OGL_DEFAULT_SWAP_INTERVAL_SIGN_ID = 0x20655CFA, + OGL_EVENT_LOG_SEVERITY_THRESHOLD_ID = 0x209DF23E, + OGL_EXTENSION_STRING_VERSION_ID = 0x20FF7493, + OGL_FORCE_BLIT_ID = 0x201F619F, + OGL_FORCE_STEREO_ID = 0x204D9A0C, + OGL_IMPLICIT_GPU_AFFINITY_ID = 0x20D0F3E6, + OGL_MAX_FRAMES_ALLOWED_ID = 0x208E55E3, + OGL_MULTIMON_ID = 0x200AEBFC, + OGL_OVERLAY_PIXEL_TYPE_ID = 0x209AE66F, + OGL_OVERLAY_SUPPORT_ID = 0x206C28C4, + OGL_QUALITY_ENHANCEMENTS_ID = 0x20797D6C, + OGL_SINGLE_BACKDEPTH_BUFFER_ID = 0x20A29055, + OGL_THREAD_CONTROL_ID = 0x20C1221E, + OGL_TRIPLE_BUFFER_ID = 0x20FDD1F9, + OGL_VIDEO_EDITING_MODE_ID = 0x20EE02B4, + AA_BEHAVIOR_FLAGS_ID = 0x10ECDB82, + AA_MODE_ALPHATOCOVERAGE_ID = 0x10FC2D9C, + AA_MODE_GAMMACORRECTION_ID = 0x107D639D, + AA_MODE_METHOD_ID = 0x10D773D2, + AA_MODE_REPLAY_ID = 0x10D48A85, + AA_MODE_SELECTOR_ID = 0x107EFC5B, + AA_MODE_SELECTOR_SLIAA_ID = 0x107AFC5B, + ANISO_MODE_LEVEL_ID = 0x101E61A9, + ANISO_MODE_SELECTOR_ID = 0x10D2BB16, + APPLICATION_PROFILE_NOTIFICATION_TIMEOUT_ID = 0x104554B6, + APPLICATION_STEAM_ID_ID = 0x107CDDBC, + CPL_HIDDEN_PROFILE_ID = 0x106D5CFF, + CUDA_EXCLUDED_GPUS_ID = 0x10354FF8, + D3DOGL_GPU_MAX_POWER_ID = 0x10D1EF29, + EXPORT_PERF_COUNTERS_ID = 0x108F0841, + FXAA_ALLOW_ID = 0x1034CB89, + FXAA_ENABLE_ID = 0x1074C972, + FXAA_INDICATOR_ENABLE_ID = 0x1068FB9C, + MCSFRSHOWSPLIT_ID = 0x10287051, + OPTIMUS_MAXAA_ID = 0x10F9DC83, + PHYSXINDICATOR_ID = 0x1094F16F, + PREFERRED_PSTATE_ID = 0x1057EB71, + PREVENT_UI_AF_OVERRIDE_ID = 0x103BCCB5, + PS_FRAMERATE_LIMITER_ID = 0x10834FEE, + PS_FRAMERATE_LIMITER_GPS_CTRL_ID = 0x10834F01, + SHIM_MAXRES_ID = 0x10F9DC82, + SHIM_MCCOMPAT_ID = 0x10F9DC80, + SHIM_RENDERING_MODE_ID = 0x10F9DC81, + SHIM_RENDERING_OPTIONS_ID = 0x10F9DC84, + SLI_GPU_COUNT_ID = 0x1033DCD1, + SLI_PREDEFINED_GPU_COUNT_ID = 0x1033DCD2, + SLI_PREDEFINED_GPU_COUNT_DX10_ID = 0x1033DCD3, + SLI_PREDEFINED_MODE_ID = 0x1033CEC1, + SLI_PREDEFINED_MODE_DX10_ID = 0x1033CEC2, + SLI_RENDERING_MODE_ID = 0x1033CED1, + VRRFEATUREINDICATOR_ID = 0x1094F157, + VRROVERLAYINDICATOR_ID = 0x1095F16F, + VRRREQUESTSTATE_ID = 0x1094F1F7, + VSYNCSMOOTHAFR_ID = 0x101AE763, + VSYNCVRRCONTROL_ID = 0x10A879CE, + VSYNC_BEHAVIOR_FLAGS_ID = 0x10FDEC23, + WKS_API_STEREO_EYES_EXCHANGE_ID = 0x11AE435C, + WKS_API_STEREO_MODE_ID = 0x11E91A61, + WKS_MEMORY_ALLOCATION_POLICY_ID = 0x11112233, + WKS_STEREO_DONGLE_SUPPORT_ID = 0x112493BD, + WKS_STEREO_SUPPORT_ID = 0x11AA9E99, + WKS_STEREO_SWAP_MODE_ID = 0x11333333, + AO_MODE_ID = 0x00667329, + AO_MODE_ACTIVE_ID = 0x00664339, + AUTO_LODBIASADJUST_ID = 0x00638E8F, + ICAFE_LOGO_CONFIG_ID = 0x00DB1337, + LODBIASADJUST_ID = 0x00738E8F, + PRERENDERLIMIT_ID = 0x007BA09E, + PS_DYNAMIC_TILING_ID = 0x00E5C6C0, + PS_SHADERDISKCACHE_ID = 0x00198FFF, + PS_TEXFILTER_ANISO_OPTS2_ID = 0x00E73211, + PS_TEXFILTER_BILINEAR_IN_ANISO_ID = 0x0084CD70, + PS_TEXFILTER_DISABLE_TRILIN_SLOPE_ID = 0x002ECAF2, + PS_TEXFILTER_NO_NEG_LODBIAS_ID = 0x0019BB68, + QUALITY_ENHANCEMENTS_ID = 0x00CE2691, + REFRESH_RATE_OVERRIDE_ID = 0x0064B541, + SET_POWER_THROTTLE_FOR_PCIe_COMPLIANCE_ID = 0x00AE785C, + SET_VAB_DATA_ID = 0x00AB8687, + VSYNCMODE_ID = 0x00A879CF, + VSYNCTEARCONTROL_ID = 0x005A375C, + TOTAL_DWORD_SETTING_NUM = 80, + TOTAL_WSTRING_SETTING_NUM = 4, + TOTAL_SETTING_NUM = 84, + INVALID_SETTING_ID = 0xFFFFFFFF + } + + internal enum NvShimSetting : uint + { + SHIM_RENDERING_MODE_INTEGRATED = 0x00000000, + SHIM_RENDERING_MODE_ENABLE = 0x00000001, + SHIM_RENDERING_MODE_USER_EDITABLE = 0x00000002, + SHIM_RENDERING_MODE_MASK = 0x00000003, + SHIM_RENDERING_MODE_VIDEO_MASK = 0x00000004, + SHIM_RENDERING_MODE_VARYING_BIT = 0x00000008, + SHIM_RENDERING_MODE_AUTO_SELECT = 0x00000010, + SHIM_RENDERING_MODE_OVERRIDE_BIT = 0x80000000, + SHIM_RENDERING_MODE_NUM_VALUES = 8, + SHIM_RENDERING_MODE_DEFAULT = SHIM_RENDERING_MODE_AUTO_SELECT + } + + internal enum NvThreadControlSetting : uint + { + OGL_THREAD_CONTROL_ENABLE = 0x00000001, + OGL_THREAD_CONTROL_DISABLE = 0x00000002, + OGL_THREAD_CONTROL_NUM_VALUES = 2, + OGL_THREAD_CONTROL_DEFAULT = 0 + } +} diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 5a1373e040..a7453dc0e0 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -30,7 +30,19 @@ namespace osu.Desktop [STAThread] public static void Main(string[] args) { - // run Squirrel first, as the app may exit after these run + /* + * WARNING: DO NOT PLACE **ANY** CODE ABOVE THE FOLLOWING BLOCK! + * + * Logic handling Squirrel MUST run before EVERYTHING if you do not want to break it. + * To be more precise: Squirrel is internally using a rather... crude method to determine whether it is running under NUnit, + * namely by checking loaded assemblies: + * https://github.com/clowd/Clowd.Squirrel/blob/24427217482deeeb9f2cacac555525edfc7bd9ac/src/Squirrel/SimpleSplat/PlatformModeDetector.cs#L17-L32 + * + * If it finds ANY assembly from the ones listed above - REGARDLESS of the reason why it is loaded - + * the app will then do completely broken things like: + * - not creating system shortcuts (as the logic is if'd out if "running tests") + * - not exiting after the install / first-update / uninstall hooks are ran (as the `Environment.Exit()` calls are if'd out if "running tests") + */ if (OperatingSystem.IsWindows()) { var windowsVersion = Environment.OSVersion.Version; @@ -54,6 +66,11 @@ namespace osu.Desktop setupSquirrel(); } + // NVIDIA profiles are based on the executable name of a process. + // Lazer and stable share the same executable name. + // Stable sets this setting to "Off", which may not be what we want, so let's force it back to the default "Auto" on startup. + NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT; + // Back up the cwd before DesktopGameHost changes it string cwd = Environment.CurrentDirectory; @@ -85,7 +102,7 @@ namespace osu.Desktop } } - using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null })) { if (!host.IsPrimaryInstance) { diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index 941ab335e8..dba157a6e9 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -10,8 +10,8 @@ using osu.Game; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Play; -using Squirrel; using Squirrel.SimpleSplat; +using Squirrel.Sources; using LogLevel = Squirrel.SimpleSplat.LogLevel; using UpdateManager = osu.Game.Updater.UpdateManager; @@ -63,7 +63,7 @@ namespace osu.Desktop.Updater if (localUserInfo?.IsPlaying.Value == true) return false; - updateManager ??= new GithubUpdateManager(@"https://github.com/ppy/osu", false, github_token, @"osulazer"); + updateManager ??= new Squirrel.UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false), @"osulazer"); var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false); diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 25f4cff00e..d6a11fa924 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -23,10 +23,10 @@ - + - - + + diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index 4719d54138..47c93fbd02 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/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs index baca8166d1..d0ecb828df 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.Tests [TestFixture] public class CatchBeatmapConversionTest : BeatmapConversionTest { - protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; + protected override string ResourceAssembly => "osu.Game.Rulesets.Catch.Tests"; [TestCase("basic")] [TestCase("spinner")] @@ -27,6 +27,32 @@ namespace osu.Game.Rulesets.Catch.Tests [TestCase("hardrock-spinner", new[] { typeof(CatchModHardRock) })] [TestCase("right-bound-hr-offset", new[] { typeof(CatchModHardRock) })] [TestCase("basic-hyperdash")] + [TestCase("pixel-jump")] + [TestCase("tiny-ticks")] + [TestCase("v8-tick-distance")] + [TestCase("spinner-precision")] + [TestCase("37902", new[] { typeof(CatchModDoubleTime), typeof(CatchModHardRock), typeof(CatchModHidden) })] + [TestCase("39206", new[] { typeof(CatchModDoubleTime), typeof(CatchModHidden) })] + [TestCase("42587")] + [TestCase("50859", new[] { typeof(CatchModDoubleTime), typeof(CatchModHidden) })] + [TestCase("75858", new[] { typeof(CatchModHardRock), typeof(CatchModHidden) })] + [TestCase("103019", new[] { typeof(CatchModHidden) })] + [TestCase("104973", new[] { typeof(CatchModHardRock), typeof(CatchModHidden) })] + [TestCase("871815", new[] { typeof(CatchModDoubleTime), typeof(CatchModHidden) })] + [TestCase("1284935", new[] { typeof(CatchModDoubleTime), typeof(CatchModHardRock) })] + [TestCase("1431386", new[] { typeof(CatchModDoubleTime), typeof(CatchModHardRock), typeof(CatchModHidden) })] + [TestCase("1597806", new[] { typeof(CatchModDoubleTime), typeof(CatchModHidden) })] + [TestCase("2190499", new[] { typeof(CatchModDoubleTime), typeof(CatchModHidden) })] + [TestCase("2571731", new[] { typeof(CatchModHardRock), typeof(CatchModHidden) })] + [TestCase("2768615", new[] { typeof(CatchModDoubleTime), typeof(CatchModHardRock) })] + [TestCase("2781126", new[] { typeof(CatchModHidden) })] + [TestCase("3152510", new[] { typeof(CatchModDoubleTime) })] + [TestCase("3227428", new[] { typeof(CatchModHardRock), typeof(CatchModHidden) })] + [TestCase("3524302", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })] + [TestCase("3644427", new[] { typeof(CatchModEasy), typeof(CatchModFlashlight) })] + [TestCase("3689906", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })] + [TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })] + [TestCase("112643")] public new void Test(string name, params Type[] mods) => base.Test(name, mods); protected override IEnumerable CreateConvertValue(HitObject hitObject) @@ -60,7 +86,7 @@ namespace osu.Game.Rulesets.Catch.Tests /// /// A sane value to account for osu!stable using ints everwhere. /// - private const float conversion_lenience = 2; + private const float conversion_lenience = 3; [JsonIgnore] public readonly CatchHitObject HitObject; diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs index 880316f177..6a70173c4a 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Catch.Tests { public class CatchDifficultyCalculatorTest : DifficultyCalculatorTest { - protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; + protected override string ResourceAssembly => "osu.Game.Rulesets.Catch.Tests"; [TestCase(4.0505463516206195d, 127, "diffcalc-test")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchRateAdjustedDisplayDifficultyTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchRateAdjustedDisplayDifficultyTest.cs new file mode 100644 index 0000000000..f77ec64df3 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/CatchRateAdjustedDisplayDifficultyTest.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 NUnit.Framework; +using osu.Game.Beatmaps; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [TestFixture] + public class CatchRateAdjustedDisplayDifficultyTest + { + private static IEnumerable difficultyValuesToTest() + { + for (float i = 0; i <= 10; i += 0.5f) + yield return i; + } + + [TestCaseSource(nameof(difficultyValuesToTest))] + public void TestApproachRateIsUnchangedWithRateEqualToOne(float originalApproachRate) + { + var ruleset = new CatchRuleset(); + var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate }; + + var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1); + + Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate)); + } + + [Test] + public void TestRateBelowOne() + { + var ruleset = new CatchRuleset(); + var difficulty = new BeatmapDifficulty(); + + var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75); + + Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01)); + } + + [Test] + public void TestRateAboveOne() + { + var ruleset = new CatchRuleset(); + var difficulty = new BeatmapDifficulty(); + + var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5); + + Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01)); + } + } +} diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs index 72011042bc..74b02bab9b 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs @@ -28,9 +28,9 @@ namespace osu.Game.Rulesets.Catch.Tests private class TestLegacySkin : LegacySkin { - public TestLegacySkin(SkinInfo skin, IResourceStore storage) + public TestLegacySkin(SkinInfo skin, IResourceStore fallbackStore) // Bypass LegacySkinResourceStore to avoid returning null for retrieving files due to bad skin info (SkinInfo.Files = null). - : base(skin, null, storage) + : base(skin, null, fallbackStore) { } } 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..c96f32d87c 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] @@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor AddStep("update hit object path", () => { - hitObject.Path = new SliderPath(PathType.PerfectCurve, new[] + hitObject.Path = new SliderPath(PathType.PERFECT_CURVE, new[] { Vector2.Zero, new Vector2(100, 100), @@ -190,16 +190,16 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor [Test] public void TestVertexResampling() { - addBlueprintStep(100, 100, new SliderPath(PathType.PerfectCurve, new[] + addBlueprintStep(100, 100, new SliderPath(PathType.PERFECT_CURVE, new[] { Vector2.Zero, new Vector2(100, 100), new Vector2(50, 200), }), 0.5); AddAssert("1 vertex per 1 nested HO", () => getVertices().Count == hitObject.NestedHitObjects.Count); - AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type == PathType.PerfectCurve); + AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE); addAddVertexSteps(150, 150); - AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type == PathType.Linear); + AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type == PathType.LINEAR); } private void addBlueprintStep(double time, float x, SliderPath sliderPath, double velocity) => AddStep("add selection blueprint", () => diff --git a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs index 95b4fdc07e..9fb55fc057 100644 --- a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs @@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Catch.Tests } while (rng.Next(2) != 0); int length = sliderPath.ControlPoints.Count - start + 1; - sliderPath.ControlPoints[start].Type = length <= 2 ? PathType.Linear : length == 3 ? PathType.PerfectCurve : PathType.Bezier; + sliderPath.ControlPoints[start].Type = length <= 2 ? PathType.LINEAR : length == 3 ? PathType.PERFECT_CURVE : PathType.BEZIER; } while (rng.Next(3) != 0); if (rng.Next(5) == 0) @@ -215,7 +215,7 @@ namespace osu.Game.Rulesets.Catch.Tests foreach (var point in sliderPath.ControlPoints) { - Assert.That(point.Type, Is.EqualTo(PathType.Linear).Or.Null); + Assert.That(point.Type, Is.EqualTo(PathType.LINEAR).Or.Null); Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT)); } 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/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs index 71df523951..7d539f91e4 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs @@ -11,7 +11,7 @@ using osuTK; namespace osu.Game.Rulesets.Catch.Tests.Mods { - public partial class TestSceneCatchModPerfect : ModPerfectTestScene + public partial class TestSceneCatchModPerfect : ModFailConditionTestScene { protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods var stream = new JuiceStream { StartTime = 1000, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs index 5835ccaf78..a161615579 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods { X = CatchPlayfield.CENTER_X, StartTime = 3000, - Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }) + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, Vector2.UnitY * 200 }) } } } diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/103019-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/103019-expected-conversion.json new file mode 100644 index 0000000000..f518db17a0 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/103019-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":571.0,"Objects":[{"StartTime":571.0,"Position":184.0,"HyperDash":false},{"StartTime":656.0,"Position":168.664017,"HyperDash":false},{"StartTime":742.0,"Position":196.577621,"HyperDash":false},{"StartTime":827.0,"Position":218.922379,"HyperDash":false},{"StartTime":913.0,"Position":255.565826,"HyperDash":false},{"StartTime":999.0,"Position":306.3156,"HyperDash":false},{"StartTime":1085.0,"Position":315.164,"HyperDash":false},{"StartTime":1152.0,"Position":325.552582,"HyperDash":false},{"StartTime":1256.0,"Position":328.091736,"HyperDash":false}]},{"StartTime":1599.0,"Objects":[{"StartTime":1599.0,"Position":256.0,"HyperDash":false},{"StartTime":1684.0,"Position":241.0,"HyperDash":false},{"StartTime":1770.0,"Position":256.0,"HyperDash":false},{"StartTime":1855.0,"Position":244.0,"HyperDash":false},{"StartTime":1941.0,"Position":256.0,"HyperDash":false},{"StartTime":2027.0,"Position":252.0,"HyperDash":false},{"StartTime":2113.0,"Position":256.0,"HyperDash":false},{"StartTime":2198.0,"Position":260.0,"HyperDash":false},{"StartTime":2284.0,"Position":256.0,"HyperDash":false},{"StartTime":2370.0,"Position":247.0,"HyperDash":false},{"StartTime":2456.0,"Position":256.0,"HyperDash":false},{"StartTime":2523.0,"Position":237.0,"HyperDash":false},{"StartTime":2627.0,"Position":256.0,"HyperDash":false}]},{"StartTime":2971.0,"Objects":[{"StartTime":2971.0,"Position":256.0,"HyperDash":false}]},{"StartTime":3313.0,"Objects":[{"StartTime":3313.0,"Position":128.0,"HyperDash":false}]},{"StartTime":3656.0,"Objects":[{"StartTime":3656.0,"Position":128.0,"HyperDash":false},{"StartTime":3741.0,"Position":119.0,"HyperDash":false},{"StartTime":3827.0,"Position":128.0,"HyperDash":false},{"StartTime":3894.0,"Position":146.0,"HyperDash":false},{"StartTime":3998.0,"Position":128.0,"HyperDash":false}]},{"StartTime":4342.0,"Objects":[{"StartTime":4342.0,"Position":384.0,"HyperDash":false},{"StartTime":4427.0,"Position":401.0,"HyperDash":false},{"StartTime":4513.0,"Position":384.0,"HyperDash":false},{"StartTime":4580.0,"Position":397.0,"HyperDash":false},{"StartTime":4684.0,"Position":384.0,"HyperDash":false}]},{"StartTime":4856.0,"Objects":[{"StartTime":4856.0,"Position":384.0,"HyperDash":false}]},{"StartTime":5028.0,"Objects":[{"StartTime":5028.0,"Position":384.0,"HyperDash":false}]},{"StartTime":5371.0,"Objects":[{"StartTime":5371.0,"Position":256.0,"HyperDash":false}]},{"StartTime":5713.0,"Objects":[{"StartTime":5713.0,"Position":256.0,"HyperDash":false}]},{"StartTime":6056.0,"Objects":[{"StartTime":6056.0,"Position":128.0,"HyperDash":false},{"StartTime":6141.0,"Position":88.01805,"HyperDash":false},{"StartTime":6227.0,"Position":72.0,"HyperDash":false},{"StartTime":6294.0,"Position":85.0,"HyperDash":false},{"StartTime":6398.0,"Position":72.0,"HyperDash":false}]},{"StartTime":6742.0,"Objects":[{"StartTime":6742.0,"Position":384.0,"HyperDash":false},{"StartTime":6827.0,"Position":410.981934,"HyperDash":false},{"StartTime":6913.0,"Position":440.0,"HyperDash":false},{"StartTime":6980.0,"Position":425.0,"HyperDash":false},{"StartTime":7084.0,"Position":440.0,"HyperDash":false}]},{"StartTime":7428.0,"Objects":[{"StartTime":7428.0,"Position":256.0,"HyperDash":false},{"StartTime":7513.0,"Position":243.6103,"HyperDash":false},{"StartTime":7599.0,"Position":259.546265,"HyperDash":false},{"StartTime":7684.0,"Position":282.3688,"HyperDash":false},{"StartTime":7770.0,"Position":257.824768,"HyperDash":false},{"StartTime":7856.0,"Position":253.344818,"HyperDash":false},{"StartTime":7942.0,"Position":259.546265,"HyperDash":false},{"StartTime":8009.0,"Position":232.678436,"HyperDash":false},{"StartTime":8113.0,"Position":256.0,"HyperDash":false}]},{"StartTime":8456.0,"Objects":[{"StartTime":8456.0,"Position":256.0,"HyperDash":false}]},{"StartTime":8799.0,"Objects":[{"StartTime":8799.0,"Position":428.0,"HyperDash":false},{"StartTime":8874.0,"Position":243.0,"HyperDash":false},{"StartTime":8949.0,"Position":422.0,"HyperDash":false},{"StartTime":9024.0,"Position":481.0,"HyperDash":false},{"StartTime":9099.0,"Position":104.0,"HyperDash":false},{"StartTime":9174.0,"Position":473.0,"HyperDash":false},{"StartTime":9249.0,"Position":135.0,"HyperDash":false},{"StartTime":9324.0,"Position":360.0,"HyperDash":false},{"StartTime":9399.0,"Position":123.0,"HyperDash":false},{"StartTime":9474.0,"Position":42.0,"HyperDash":false},{"StartTime":9549.0,"Position":393.0,"HyperDash":false},{"StartTime":9624.0,"Position":75.0,"HyperDash":false},{"StartTime":9699.0,"Position":377.0,"HyperDash":false},{"StartTime":9774.0,"Position":354.0,"HyperDash":false},{"StartTime":9849.0,"Position":287.0,"HyperDash":false},{"StartTime":9924.0,"Position":361.0,"HyperDash":false},{"StartTime":9999.0,"Position":479.0,"HyperDash":false},{"StartTime":10074.0,"Position":346.0,"HyperDash":false},{"StartTime":10149.0,"Position":266.0,"HyperDash":false},{"StartTime":10224.0,"Position":400.0,"HyperDash":false},{"StartTime":10299.0,"Position":202.0,"HyperDash":false},{"StartTime":10374.0,"Position":500.0,"HyperDash":false},{"StartTime":10449.0,"Position":80.0,"HyperDash":false},{"StartTime":10524.0,"Position":399.0,"HyperDash":false},{"StartTime":10599.0,"Position":455.0,"HyperDash":false},{"StartTime":10674.0,"Position":105.0,"HyperDash":false},{"StartTime":10749.0,"Position":100.0,"HyperDash":false},{"StartTime":10824.0,"Position":195.0,"HyperDash":false},{"StartTime":10899.0,"Position":106.0,"HyperDash":false},{"StartTime":10974.0,"Position":305.0,"HyperDash":false},{"StartTime":11049.0,"Position":225.0,"HyperDash":false},{"StartTime":11124.0,"Position":79.0,"HyperDash":false},{"StartTime":11199.0,"Position":38.0,"HyperDash":false}]},{"StartTime":11542.0,"Objects":[{"StartTime":11542.0,"Position":256.0,"HyperDash":false}]},{"StartTime":11885.0,"Objects":[{"StartTime":11885.0,"Position":60.0,"HyperDash":false},{"StartTime":11970.0,"Position":34.9856834,"HyperDash":false},{"StartTime":12056.0,"Position":54.15636,"HyperDash":false},{"StartTime":12141.0,"Position":60.52591,"HyperDash":false},{"StartTime":12227.0,"Position":114.312965,"HyperDash":false},{"StartTime":12313.0,"Position":82.90555,"HyperDash":false},{"StartTime":12399.0,"Position":54.15636,"HyperDash":false},{"StartTime":12466.0,"Position":53.6008873,"HyperDash":false},{"StartTime":12570.0,"Position":60.0,"HyperDash":false}]},{"StartTime":12913.0,"Objects":[{"StartTime":12913.0,"Position":256.0,"HyperDash":false}]},{"StartTime":13256.0,"Objects":[{"StartTime":13256.0,"Position":452.0,"HyperDash":false},{"StartTime":13341.0,"Position":477.0143,"HyperDash":false},{"StartTime":13427.0,"Position":457.843628,"HyperDash":false},{"StartTime":13512.0,"Position":425.4741,"HyperDash":false},{"StartTime":13598.0,"Position":397.687042,"HyperDash":false},{"StartTime":13684.0,"Position":442.094452,"HyperDash":false},{"StartTime":13770.0,"Position":457.843628,"HyperDash":false},{"StartTime":13837.0,"Position":471.3991,"HyperDash":false},{"StartTime":13941.0,"Position":452.0,"HyperDash":false}]},{"StartTime":14285.0,"Objects":[{"StartTime":14285.0,"Position":256.0,"HyperDash":false}]},{"StartTime":14799.0,"Objects":[{"StartTime":14799.0,"Position":88.0,"HyperDash":false},{"StartTime":14884.0,"Position":60.0,"HyperDash":false},{"StartTime":14970.0,"Position":88.0,"HyperDash":false},{"StartTime":15056.0,"Position":60.0,"HyperDash":false},{"StartTime":15141.0,"Position":88.0,"HyperDash":false},{"StartTime":15227.0,"Position":60.0,"HyperDash":false},{"StartTime":15313.0,"Position":88.0,"HyperDash":false},{"StartTime":15399.0,"Position":60.0,"HyperDash":false},{"StartTime":15484.0,"Position":88.0,"HyperDash":false},{"StartTime":15570.0,"Position":60.0,"HyperDash":false},{"StartTime":15656.0,"Position":88.0,"HyperDash":false}]},{"StartTime":15999.0,"Objects":[{"StartTime":15999.0,"Position":32.0,"HyperDash":false}]},{"StartTime":16171.0,"Objects":[{"StartTime":16171.0,"Position":96.0,"HyperDash":false}]},{"StartTime":16342.0,"Objects":[{"StartTime":16342.0,"Position":160.0,"HyperDash":false}]},{"StartTime":16685.0,"Objects":[{"StartTime":16685.0,"Position":224.0,"HyperDash":false}]},{"StartTime":17028.0,"Objects":[{"StartTime":17028.0,"Position":328.0,"HyperDash":false},{"StartTime":17095.0,"Position":334.2591,"HyperDash":false},{"StartTime":17199.0,"Position":349.0792,"HyperDash":false}]},{"StartTime":17371.0,"Objects":[{"StartTime":17371.0,"Position":412.0,"HyperDash":false},{"StartTime":17438.0,"Position":425.881073,"HyperDash":false},{"StartTime":17542.0,"Position":432.114349,"HyperDash":false}]},{"StartTime":17713.0,"Objects":[{"StartTime":17713.0,"Position":448.0,"HyperDash":false},{"StartTime":17780.0,"Position":467.063019,"HyperDash":false},{"StartTime":17884.0,"Position":511.9668,"HyperDash":false}]},{"StartTime":18056.0,"Objects":[{"StartTime":18056.0,"Position":472.0,"HyperDash":false},{"StartTime":18123.0,"Position":439.87265,"HyperDash":false},{"StartTime":18227.0,"Position":407.869,"HyperDash":false}]},{"StartTime":18399.0,"Objects":[{"StartTime":18399.0,"Position":388.0,"HyperDash":false},{"StartTime":18466.0,"Position":396.55722,"HyperDash":false},{"StartTime":18570.0,"Position":361.3475,"HyperDash":false}]},{"StartTime":18742.0,"Objects":[{"StartTime":18742.0,"Position":300.0,"HyperDash":false},{"StartTime":18809.0,"Position":305.44278,"HyperDash":false},{"StartTime":18913.0,"Position":326.6525,"HyperDash":false}]},{"StartTime":19085.0,"Objects":[{"StartTime":19085.0,"Position":344.0,"HyperDash":false}]},{"StartTime":19428.0,"Objects":[{"StartTime":19428.0,"Position":156.0,"HyperDash":false}]},{"StartTime":19771.0,"Objects":[{"StartTime":19771.0,"Position":256.0,"HyperDash":false}]},{"StartTime":20456.0,"Objects":[{"StartTime":20456.0,"Position":256.0,"HyperDash":false}]},{"StartTime":21142.0,"Objects":[{"StartTime":21142.0,"Position":124.0,"HyperDash":false}]},{"StartTime":21485.0,"Objects":[{"StartTime":21485.0,"Position":256.0,"HyperDash":false}]},{"StartTime":21828.0,"Objects":[{"StartTime":21828.0,"Position":388.0,"HyperDash":false}]},{"StartTime":22513.0,"Objects":[{"StartTime":22513.0,"Position":504.0,"HyperDash":false},{"StartTime":22580.0,"Position":476.5731,"HyperDash":false},{"StartTime":22684.0,"Position":434.0,"HyperDash":false}]},{"StartTime":22856.0,"Objects":[{"StartTime":22856.0,"Position":448.0,"HyperDash":false}]},{"StartTime":23028.0,"Objects":[{"StartTime":23028.0,"Position":376.0,"HyperDash":false}]},{"StartTime":23199.0,"Objects":[{"StartTime":23199.0,"Position":360.0,"HyperDash":false},{"StartTime":23266.0,"Position":347.5731,"HyperDash":false},{"StartTime":23370.0,"Position":290.0,"HyperDash":false}]},{"StartTime":23542.0,"Objects":[{"StartTime":23542.0,"Position":304.0,"HyperDash":false}]},{"StartTime":23713.0,"Objects":[{"StartTime":23713.0,"Position":232.0,"HyperDash":false}]},{"StartTime":23885.0,"Objects":[{"StartTime":23885.0,"Position":216.0,"HyperDash":false},{"StartTime":23952.0,"Position":172.5731,"HyperDash":false},{"StartTime":24056.0,"Position":146.0,"HyperDash":false}]},{"StartTime":24228.0,"Objects":[{"StartTime":24228.0,"Position":160.0,"HyperDash":false}]},{"StartTime":24399.0,"Objects":[{"StartTime":24399.0,"Position":88.0,"HyperDash":false}]},{"StartTime":24571.0,"Objects":[{"StartTime":24571.0,"Position":72.0,"HyperDash":false},{"StartTime":24656.0,"Position":54.2046776,"HyperDash":false},{"StartTime":24742.0,"Position":2.0,"HyperDash":false},{"StartTime":24809.0,"Position":32.4269028,"HyperDash":false},{"StartTime":24913.0,"Position":72.0,"HyperDash":false}]},{"StartTime":25256.0,"Objects":[{"StartTime":25256.0,"Position":8.0,"HyperDash":false},{"StartTime":25323.0,"Position":31.4269028,"HyperDash":false},{"StartTime":25427.0,"Position":78.0,"HyperDash":false}]},{"StartTime":25599.0,"Objects":[{"StartTime":25599.0,"Position":64.0,"HyperDash":false}]},{"StartTime":25771.0,"Objects":[{"StartTime":25771.0,"Position":136.0,"HyperDash":false}]},{"StartTime":25942.0,"Objects":[{"StartTime":25942.0,"Position":152.0,"HyperDash":false},{"StartTime":26009.0,"Position":187.4269,"HyperDash":false},{"StartTime":26113.0,"Position":222.0,"HyperDash":false}]},{"StartTime":26285.0,"Objects":[{"StartTime":26285.0,"Position":208.0,"HyperDash":false}]},{"StartTime":26456.0,"Objects":[{"StartTime":26456.0,"Position":280.0,"HyperDash":false}]},{"StartTime":26628.0,"Objects":[{"StartTime":26628.0,"Position":296.0,"HyperDash":false},{"StartTime":26695.0,"Position":322.4269,"HyperDash":false},{"StartTime":26799.0,"Position":366.0,"HyperDash":false}]},{"StartTime":26971.0,"Objects":[{"StartTime":26971.0,"Position":352.0,"HyperDash":false}]},{"StartTime":27142.0,"Objects":[{"StartTime":27142.0,"Position":424.0,"HyperDash":false}]},{"StartTime":27313.0,"Objects":[{"StartTime":27313.0,"Position":440.0,"HyperDash":false},{"StartTime":27398.0,"Position":489.795319,"HyperDash":false},{"StartTime":27484.0,"Position":510.0,"HyperDash":false},{"StartTime":27551.0,"Position":470.5731,"HyperDash":false},{"StartTime":27655.0,"Position":440.0,"HyperDash":false}]},{"StartTime":27999.0,"Objects":[{"StartTime":27999.0,"Position":40.0,"HyperDash":false},{"StartTime":28066.0,"Position":24.0,"HyperDash":false},{"StartTime":28170.0,"Position":40.0,"HyperDash":false}]},{"StartTime":28342.0,"Objects":[{"StartTime":28342.0,"Position":112.0,"HyperDash":false},{"StartTime":28427.0,"Position":112.0,"HyperDash":false},{"StartTime":28513.0,"Position":112.0,"HyperDash":false}]},{"StartTime":28685.0,"Objects":[{"StartTime":28685.0,"Position":184.0,"HyperDash":false},{"StartTime":28752.0,"Position":177.0,"HyperDash":false},{"StartTime":28856.0,"Position":184.0,"HyperDash":false}]},{"StartTime":29028.0,"Objects":[{"StartTime":29028.0,"Position":260.0,"HyperDash":false},{"StartTime":29113.0,"Position":260.0,"HyperDash":false},{"StartTime":29199.0,"Position":260.0,"HyperDash":false}]},{"StartTime":29371.0,"Objects":[{"StartTime":29371.0,"Position":336.0,"HyperDash":false},{"StartTime":29438.0,"Position":333.2137,"HyperDash":false},{"StartTime":29542.0,"Position":374.829,"HyperDash":false}]},{"StartTime":29713.0,"Objects":[{"StartTime":29713.0,"Position":440.0,"HyperDash":false},{"StartTime":29780.0,"Position":420.18338,"HyperDash":false},{"StartTime":29884.0,"Position":399.632172,"HyperDash":false}]},{"StartTime":30056.0,"Objects":[{"StartTime":30056.0,"Position":460.0,"HyperDash":false},{"StartTime":30141.0,"Position":479.41452,"HyperDash":false},{"StartTime":30227.0,"Position":460.0,"HyperDash":false},{"StartTime":30313.0,"Position":479.41452,"HyperDash":false},{"StartTime":30398.0,"Position":460.0,"HyperDash":false}]},{"StartTime":30742.0,"Objects":[{"StartTime":30742.0,"Position":328.0,"HyperDash":false},{"StartTime":30827.0,"Position":293.0,"HyperDash":false},{"StartTime":30913.0,"Position":328.0,"HyperDash":false},{"StartTime":30999.0,"Position":293.0,"HyperDash":false}]},{"StartTime":31085.0,"Objects":[{"StartTime":31085.0,"Position":256.0,"HyperDash":false},{"StartTime":31170.0,"Position":221.0,"HyperDash":false},{"StartTime":31256.0,"Position":256.0,"HyperDash":false},{"StartTime":31342.0,"Position":221.0,"HyperDash":false}]},{"StartTime":31428.0,"Objects":[{"StartTime":31428.0,"Position":184.0,"HyperDash":false},{"StartTime":31513.0,"Position":149.0,"HyperDash":false},{"StartTime":31599.0,"Position":184.0,"HyperDash":false},{"StartTime":31685.0,"Position":149.0,"HyperDash":false}]},{"StartTime":31771.0,"Objects":[{"StartTime":31771.0,"Position":112.0,"HyperDash":false},{"StartTime":31856.0,"Position":77.0,"HyperDash":false},{"StartTime":31942.0,"Position":112.0,"HyperDash":false},{"StartTime":32028.0,"Position":77.0,"HyperDash":false}]},{"StartTime":32113.0,"Objects":[{"StartTime":32113.0,"Position":40.0,"HyperDash":false}]},{"StartTime":32456.0,"Objects":[{"StartTime":32456.0,"Position":40.0,"HyperDash":false}]},{"StartTime":32799.0,"Objects":[{"StartTime":32799.0,"Position":184.0,"HyperDash":false}]},{"StartTime":33142.0,"Objects":[{"StartTime":33142.0,"Position":184.0,"HyperDash":false}]},{"StartTime":33485.0,"Objects":[{"StartTime":33485.0,"Position":304.0,"HyperDash":false},{"StartTime":33570.0,"Position":332.600983,"HyperDash":false},{"StartTime":33656.0,"Position":351.4796,"HyperDash":false},{"StartTime":33723.0,"Position":368.082733,"HyperDash":false},{"StartTime":33827.0,"Position":398.9592,"HyperDash":false}]},{"StartTime":34342.0,"Objects":[{"StartTime":34342.0,"Position":256.0,"HyperDash":false}]},{"StartTime":34513.0,"Objects":[{"StartTime":34513.0,"Position":256.0,"HyperDash":false}]},{"StartTime":34856.0,"Objects":[{"StartTime":34856.0,"Position":136.0,"HyperDash":false},{"StartTime":34941.0,"Position":152.0,"HyperDash":false},{"StartTime":35027.0,"Position":136.0,"HyperDash":false},{"StartTime":35094.0,"Position":150.0,"HyperDash":false},{"StartTime":35198.0,"Position":136.0,"HyperDash":false}]},{"StartTime":35371.0,"Objects":[{"StartTime":35371.0,"Position":104.0,"HyperDash":false},{"StartTime":35456.0,"Position":124.558014,"HyperDash":false},{"StartTime":35542.0,"Position":170.988922,"HyperDash":false},{"StartTime":35609.0,"Position":180.576416,"HyperDash":false},{"StartTime":35713.0,"Position":209.857956,"HyperDash":false}]},{"StartTime":35885.0,"Objects":[{"StartTime":35885.0,"Position":212.0,"HyperDash":false}]},{"StartTime":36228.0,"Objects":[{"StartTime":36228.0,"Position":408.0,"HyperDash":false},{"StartTime":36313.0,"Position":441.692383,"HyperDash":false},{"StartTime":36399.0,"Position":463.7653,"HyperDash":false},{"StartTime":36466.0,"Position":471.929932,"HyperDash":false},{"StartTime":36570.0,"Position":480.400452,"HyperDash":false}]},{"StartTime":37085.0,"Objects":[{"StartTime":37085.0,"Position":360.0,"HyperDash":false}]},{"StartTime":37256.0,"Objects":[{"StartTime":37256.0,"Position":360.0,"HyperDash":false}]},{"StartTime":37599.0,"Objects":[{"StartTime":37599.0,"Position":232.0,"HyperDash":false},{"StartTime":37684.0,"Position":186.367691,"HyperDash":false},{"StartTime":37770.0,"Position":175.2116,"HyperDash":false},{"StartTime":37837.0,"Position":153.710571,"HyperDash":false},{"StartTime":37941.0,"Position":106.279663,"HyperDash":false}]},{"StartTime":38113.0,"Objects":[{"StartTime":38113.0,"Position":56.0,"HyperDash":false},{"StartTime":38198.0,"Position":39.6659164,"HyperDash":false},{"StartTime":38284.0,"Position":38.9134,"HyperDash":false},{"StartTime":38351.0,"Position":31.39479,"HyperDash":false},{"StartTime":38455.0,"Position":85.0976944,"HyperDash":false}]},{"StartTime":38628.0,"Objects":[{"StartTime":38628.0,"Position":156.0,"HyperDash":false}]},{"StartTime":38971.0,"Objects":[{"StartTime":38971.0,"Position":256.0,"HyperDash":false},{"StartTime":39056.0,"Position":221.399033,"HyperDash":false},{"StartTime":39142.0,"Position":208.5204,"HyperDash":false},{"StartTime":39209.0,"Position":182.917267,"HyperDash":false},{"StartTime":39313.0,"Position":161.0408,"HyperDash":false}]},{"StartTime":39828.0,"Objects":[{"StartTime":39828.0,"Position":256.0,"HyperDash":false}]},{"StartTime":39999.0,"Objects":[{"StartTime":39999.0,"Position":256.0,"HyperDash":false}]},{"StartTime":40342.0,"Objects":[{"StartTime":40342.0,"Position":376.0,"HyperDash":false},{"StartTime":40427.0,"Position":392.0,"HyperDash":false},{"StartTime":40513.0,"Position":376.0,"HyperDash":false},{"StartTime":40580.0,"Position":369.0,"HyperDash":false},{"StartTime":40684.0,"Position":376.0,"HyperDash":false}]},{"StartTime":40856.0,"Objects":[{"StartTime":40856.0,"Position":408.0,"HyperDash":false},{"StartTime":40941.0,"Position":355.442,"HyperDash":false},{"StartTime":41027.0,"Position":341.011078,"HyperDash":false},{"StartTime":41094.0,"Position":333.423584,"HyperDash":false},{"StartTime":41198.0,"Position":302.142059,"HyperDash":false}]},{"StartTime":41371.0,"Objects":[{"StartTime":41371.0,"Position":300.0,"HyperDash":false}]},{"StartTime":41713.0,"Objects":[{"StartTime":41713.0,"Position":104.0,"HyperDash":false},{"StartTime":41798.0,"Position":74.30763,"HyperDash":false},{"StartTime":41884.0,"Position":48.23472,"HyperDash":false},{"StartTime":41951.0,"Position":33.07008,"HyperDash":false},{"StartTime":42055.0,"Position":31.59955,"HyperDash":false}]},{"StartTime":42571.0,"Objects":[{"StartTime":42571.0,"Position":152.0,"HyperDash":false}]},{"StartTime":42742.0,"Objects":[{"StartTime":42742.0,"Position":152.0,"HyperDash":false}]},{"StartTime":43085.0,"Objects":[{"StartTime":43085.0,"Position":256.0,"HyperDash":false},{"StartTime":43170.0,"Position":256.0,"HyperDash":false},{"StartTime":43256.0,"Position":256.0,"HyperDash":false},{"StartTime":43342.0,"Position":256.0,"HyperDash":false},{"StartTime":43427.0,"Position":256.0,"HyperDash":false},{"StartTime":43513.0,"Position":256.0,"HyperDash":false},{"StartTime":43599.0,"Position":256.0,"HyperDash":false},{"StartTime":43685.0,"Position":256.0,"HyperDash":false},{"StartTime":43770.0,"Position":256.0,"HyperDash":false}]},{"StartTime":44113.0,"Objects":[{"StartTime":44113.0,"Position":256.0,"HyperDash":false}]},{"StartTime":44456.0,"Objects":[{"StartTime":44456.0,"Position":124.0,"HyperDash":false}]},{"StartTime":44628.0,"Objects":[{"StartTime":44628.0,"Position":72.0,"HyperDash":false},{"StartTime":44713.0,"Position":40.92307,"HyperDash":false},{"StartTime":44799.0,"Position":52.98573,"HyperDash":false},{"StartTime":44884.0,"Position":77.93154,"HyperDash":false},{"StartTime":44970.0,"Position":95.82509,"HyperDash":false},{"StartTime":45038.0,"Position":118.951027,"HyperDash":false},{"StartTime":45142.0,"Position":163.988525,"HyperDash":false}]},{"StartTime":45485.0,"Objects":[{"StartTime":45485.0,"Position":256.0,"HyperDash":false}]},{"StartTime":45828.0,"Objects":[{"StartTime":45828.0,"Position":388.0,"HyperDash":false}]},{"StartTime":45999.0,"Objects":[{"StartTime":45999.0,"Position":440.0,"HyperDash":false},{"StartTime":46084.0,"Position":441.0769,"HyperDash":false},{"StartTime":46170.0,"Position":459.014282,"HyperDash":false},{"StartTime":46255.0,"Position":425.068451,"HyperDash":false},{"StartTime":46341.0,"Position":416.174927,"HyperDash":false},{"StartTime":46409.0,"Position":398.048981,"HyperDash":false},{"StartTime":46513.0,"Position":348.011475,"HyperDash":false}]},{"StartTime":46856.0,"Objects":[{"StartTime":46856.0,"Position":256.0,"HyperDash":false}]},{"StartTime":47199.0,"Objects":[{"StartTime":47199.0,"Position":256.0,"HyperDash":false},{"StartTime":47284.0,"Position":244.431641,"HyperDash":false},{"StartTime":47370.0,"Position":255.566513,"HyperDash":false},{"StartTime":47455.0,"Position":277.621033,"HyperDash":false},{"StartTime":47541.0,"Position":254.8021,"HyperDash":false},{"StartTime":47627.0,"Position":267.5996,"HyperDash":false},{"StartTime":47713.0,"Position":255.632889,"HyperDash":false},{"StartTime":47798.0,"Position":231.420425,"HyperDash":false},{"StartTime":47884.0,"Position":256.0,"HyperDash":false},{"StartTime":47970.0,"Position":247.424866,"HyperDash":false},{"StartTime":48056.0,"Position":255.699265,"HyperDash":false},{"StartTime":48123.0,"Position":258.327057,"HyperDash":false},{"StartTime":48227.0,"Position":254.8021,"HyperDash":false}]},{"StartTime":48571.0,"Objects":[{"StartTime":48571.0,"Position":392.0,"HyperDash":false},{"StartTime":48656.0,"Position":373.0,"HyperDash":false},{"StartTime":48742.0,"Position":392.0,"HyperDash":false},{"StartTime":48809.0,"Position":387.0,"HyperDash":false},{"StartTime":48913.0,"Position":392.0,"HyperDash":false}]},{"StartTime":49085.0,"Objects":[{"StartTime":49085.0,"Position":464.0,"HyperDash":false},{"StartTime":49170.0,"Position":434.350128,"HyperDash":false},{"StartTime":49256.0,"Position":431.4105,"HyperDash":false},{"StartTime":49341.0,"Position":405.503876,"HyperDash":false},{"StartTime":49427.0,"Position":365.203827,"HyperDash":false},{"StartTime":49495.0,"Position":336.536133,"HyperDash":false},{"StartTime":49599.0,"Position":324.364319,"HyperDash":false}]},{"StartTime":49942.0,"Objects":[{"StartTime":49942.0,"Position":187.0,"HyperDash":false},{"StartTime":50027.0,"Position":163.228943,"HyperDash":false},{"StartTime":50113.0,"Position":148.783264,"HyperDash":false},{"StartTime":50198.0,"Position":108.904266,"HyperDash":false},{"StartTime":50284.0,"Position":81.87666,"HyperDash":false},{"StartTime":50352.0,"Position":62.3181648,"HyperDash":false},{"StartTime":50456.0,"Position":47.9551849,"HyperDash":false}]},{"StartTime":50628.0,"Objects":[{"StartTime":50628.0,"Position":120.0,"HyperDash":false},{"StartTime":50713.0,"Position":106.0,"HyperDash":false},{"StartTime":50799.0,"Position":120.0,"HyperDash":false},{"StartTime":50866.0,"Position":135.0,"HyperDash":false},{"StartTime":50970.0,"Position":120.0,"HyperDash":false}]},{"StartTime":51313.0,"Objects":[{"StartTime":51313.0,"Position":257.0,"HyperDash":false},{"StartTime":51398.0,"Position":234.050232,"HyperDash":false},{"StartTime":51484.0,"Position":255.277374,"HyperDash":false},{"StartTime":51569.0,"Position":284.5524,"HyperDash":false},{"StartTime":51655.0,"Position":256.423248,"HyperDash":false},{"StartTime":51741.0,"Position":248.555389,"HyperDash":false},{"StartTime":51827.0,"Position":255.347473,"HyperDash":false},{"StartTime":51912.0,"Position":263.0151,"HyperDash":false},{"StartTime":51998.0,"Position":257.0,"HyperDash":false},{"StartTime":52084.0,"Position":228.030624,"HyperDash":false},{"StartTime":52170.0,"Position":255.417587,"HyperDash":false},{"StartTime":52237.0,"Position":278.820038,"HyperDash":false},{"StartTime":52341.0,"Position":256.423248,"HyperDash":false}]},{"StartTime":52685.0,"Objects":[{"StartTime":52685.0,"Position":256.0,"HyperDash":false}]},{"StartTime":53028.0,"Objects":[{"StartTime":53028.0,"Position":169.0,"HyperDash":false},{"StartTime":53113.0,"Position":148.767334,"HyperDash":false},{"StartTime":53199.0,"Position":102.039978,"HyperDash":false},{"StartTime":53284.0,"Position":65.15436,"HyperDash":false},{"StartTime":53370.0,"Position":56.49534,"HyperDash":false},{"StartTime":53438.0,"Position":50.6727638,"HyperDash":false},{"StartTime":53542.0,"Position":72.11841,"HyperDash":false}]},{"StartTime":53713.0,"Objects":[{"StartTime":53713.0,"Position":124.0,"HyperDash":false}]},{"StartTime":54056.0,"Objects":[{"StartTime":54056.0,"Position":68.0,"HyperDash":false},{"StartTime":54141.0,"Position":56.93203,"HyperDash":false},{"StartTime":54227.0,"Position":68.0,"HyperDash":false}]},{"StartTime":54399.0,"Objects":[{"StartTime":54399.0,"Position":156.0,"HyperDash":false}]},{"StartTime":54742.0,"Objects":[{"StartTime":54742.0,"Position":444.0,"HyperDash":false},{"StartTime":54827.0,"Position":455.067963,"HyperDash":false},{"StartTime":54913.0,"Position":444.0,"HyperDash":false}]},{"StartTime":55085.0,"Objects":[{"StartTime":55085.0,"Position":356.0,"HyperDash":false}]},{"StartTime":55428.0,"Objects":[{"StartTime":55428.0,"Position":356.0,"HyperDash":false},{"StartTime":55513.0,"Position":335.3816,"HyperDash":false},{"StartTime":55599.0,"Position":294.1601,"HyperDash":false},{"StartTime":55684.0,"Position":272.865723,"HyperDash":false},{"StartTime":55770.0,"Position":255.69072,"HyperDash":false},{"StartTime":55856.0,"Position":254.907425,"HyperDash":false},{"StartTime":55942.0,"Position":216.981689,"HyperDash":false},{"StartTime":56009.0,"Position":188.30954,"HyperDash":false},{"StartTime":56113.0,"Position":154.812271,"HyperDash":false}]},{"StartTime":56285.0,"Objects":[{"StartTime":56285.0,"Position":160.0,"HyperDash":false}]},{"StartTime":56456.0,"Objects":[{"StartTime":56456.0,"Position":92.0,"HyperDash":false}]},{"StartTime":56628.0,"Objects":[{"StartTime":56628.0,"Position":84.0,"HyperDash":false}]},{"StartTime":56799.0,"Objects":[{"StartTime":56799.0,"Position":156.0,"HyperDash":false},{"StartTime":56884.0,"Position":179.6867,"HyperDash":false},{"StartTime":56970.0,"Position":184.530014,"HyperDash":false},{"StartTime":57055.0,"Position":210.992,"HyperDash":false},{"StartTime":57141.0,"Position":239.917923,"HyperDash":false},{"StartTime":57227.0,"Position":197.399063,"HyperDash":false},{"StartTime":57313.0,"Position":182.462265,"HyperDash":false},{"StartTime":57380.0,"Position":158.21933,"HyperDash":false},{"StartTime":57484.0,"Position":155.038208,"HyperDash":false}]},{"StartTime":57656.0,"Objects":[{"StartTime":57656.0,"Position":92.0,"HyperDash":false}]},{"StartTime":57828.0,"Objects":[{"StartTime":57828.0,"Position":88.0,"HyperDash":false}]},{"StartTime":57999.0,"Objects":[{"StartTime":57999.0,"Position":148.0,"HyperDash":false}]},{"StartTime":58171.0,"Objects":[{"StartTime":58171.0,"Position":155.0,"HyperDash":false},{"StartTime":58256.0,"Position":190.6184,"HyperDash":false},{"StartTime":58342.0,"Position":216.83992,"HyperDash":false},{"StartTime":58427.0,"Position":255.134277,"HyperDash":false},{"StartTime":58513.0,"Position":255.3093,"HyperDash":false},{"StartTime":58599.0,"Position":262.09256,"HyperDash":false},{"StartTime":58685.0,"Position":294.0183,"HyperDash":false},{"StartTime":58752.0,"Position":306.69046,"HyperDash":false},{"StartTime":58856.0,"Position":356.187744,"HyperDash":false}]},{"StartTime":59028.0,"Objects":[{"StartTime":59028.0,"Position":356.0,"HyperDash":false}]},{"StartTime":59199.0,"Objects":[{"StartTime":59199.0,"Position":424.0,"HyperDash":false}]},{"StartTime":59371.0,"Objects":[{"StartTime":59371.0,"Position":428.0,"HyperDash":false}]},{"StartTime":59542.0,"Objects":[{"StartTime":59542.0,"Position":356.0,"HyperDash":false},{"StartTime":59627.0,"Position":337.313324,"HyperDash":false},{"StartTime":59713.0,"Position":327.469971,"HyperDash":false},{"StartTime":59798.0,"Position":290.008,"HyperDash":false},{"StartTime":59884.0,"Position":272.0821,"HyperDash":false},{"StartTime":59970.0,"Position":294.600952,"HyperDash":false},{"StartTime":60056.0,"Position":329.53775,"HyperDash":false},{"StartTime":60123.0,"Position":351.78067,"HyperDash":false},{"StartTime":60227.0,"Position":356.9618,"HyperDash":false}]},{"StartTime":60399.0,"Objects":[{"StartTime":60399.0,"Position":424.0,"HyperDash":false}]},{"StartTime":60571.0,"Objects":[{"StartTime":60571.0,"Position":428.0,"HyperDash":false}]},{"StartTime":60742.0,"Objects":[{"StartTime":60742.0,"Position":360.0,"HyperDash":false}]},{"StartTime":60913.0,"Objects":[{"StartTime":60913.0,"Position":284.0,"HyperDash":false},{"StartTime":60980.0,"Position":271.5731,"HyperDash":false},{"StartTime":61084.0,"Position":214.0,"HyperDash":false}]},{"StartTime":61256.0,"Objects":[{"StartTime":61256.0,"Position":136.0,"HyperDash":false}]},{"StartTime":61428.0,"Objects":[{"StartTime":61428.0,"Position":60.0,"HyperDash":false}]},{"StartTime":61513.0,"Objects":[{"StartTime":61513.0,"Position":60.0,"HyperDash":false}]},{"StartTime":61599.0,"Objects":[{"StartTime":61599.0,"Position":60.0,"HyperDash":false},{"StartTime":61666.0,"Position":65.0,"HyperDash":false},{"StartTime":61770.0,"Position":60.0,"HyperDash":false}]},{"StartTime":61942.0,"Objects":[{"StartTime":61942.0,"Position":60.0,"HyperDash":false}]},{"StartTime":62113.0,"Objects":[{"StartTime":62113.0,"Position":136.0,"HyperDash":false}]},{"StartTime":62199.0,"Objects":[{"StartTime":62199.0,"Position":136.0,"HyperDash":false}]},{"StartTime":62285.0,"Objects":[{"StartTime":62285.0,"Position":136.0,"HyperDash":false},{"StartTime":62352.0,"Position":120.0,"HyperDash":false},{"StartTime":62456.0,"Position":136.0,"HyperDash":false}]},{"StartTime":62628.0,"Objects":[{"StartTime":62628.0,"Position":212.0,"HyperDash":false}]},{"StartTime":62799.0,"Objects":[{"StartTime":62799.0,"Position":212.0,"HyperDash":false}]},{"StartTime":62885.0,"Objects":[{"StartTime":62885.0,"Position":212.0,"HyperDash":false}]},{"StartTime":62971.0,"Objects":[{"StartTime":62971.0,"Position":212.0,"HyperDash":false},{"StartTime":63038.0,"Position":195.0,"HyperDash":false},{"StartTime":63142.0,"Position":212.0,"HyperDash":false}]},{"StartTime":63313.0,"Objects":[{"StartTime":63313.0,"Position":136.0,"HyperDash":false},{"StartTime":63380.0,"Position":120.5731,"HyperDash":false},{"StartTime":63484.0,"Position":66.0,"HyperDash":false}]},{"StartTime":63656.0,"Objects":[{"StartTime":63656.0,"Position":356.0,"HyperDash":false},{"StartTime":63741.0,"Position":362.0,"HyperDash":false},{"StartTime":63827.0,"Position":347.0,"HyperDash":false},{"StartTime":63913.0,"Position":252.0,"HyperDash":false},{"StartTime":63999.0,"Position":477.0,"HyperDash":false},{"StartTime":64084.0,"Position":358.0,"HyperDash":false},{"StartTime":64170.0,"Position":17.0,"HyperDash":false},{"StartTime":64256.0,"Position":399.0,"HyperDash":false},{"StartTime":64342.0,"Position":280.0,"HyperDash":false},{"StartTime":64427.0,"Position":304.0,"HyperDash":false},{"StartTime":64513.0,"Position":221.0,"HyperDash":false},{"StartTime":64599.0,"Position":407.0,"HyperDash":false},{"StartTime":64685.0,"Position":287.0,"HyperDash":false},{"StartTime":64770.0,"Position":135.0,"HyperDash":false},{"StartTime":64856.0,"Position":437.0,"HyperDash":false},{"StartTime":64942.0,"Position":289.0,"HyperDash":false},{"StartTime":65028.0,"Position":464.0,"HyperDash":false}]},{"StartTime":65713.0,"Objects":[{"StartTime":65713.0,"Position":256.0,"HyperDash":false},{"StartTime":65798.0,"Position":256.0,"HyperDash":false},{"StartTime":65884.0,"Position":256.0,"HyperDash":false}]},{"StartTime":66056.0,"Objects":[{"StartTime":66056.0,"Position":288.0,"HyperDash":false}]},{"StartTime":66228.0,"Objects":[{"StartTime":66228.0,"Position":328.0,"HyperDash":false}]},{"StartTime":66399.0,"Objects":[{"StartTime":66399.0,"Position":400.0,"HyperDash":false},{"StartTime":66466.0,"Position":404.432526,"HyperDash":false},{"StartTime":66570.0,"Position":443.844757,"HyperDash":false}]},{"StartTime":66742.0,"Objects":[{"StartTime":66742.0,"Position":380.0,"HyperDash":false}]},{"StartTime":66913.0,"Objects":[{"StartTime":66913.0,"Position":444.0,"HyperDash":false},{"StartTime":66980.0,"Position":415.4034,"HyperDash":false},{"StartTime":67084.0,"Position":392.189362,"HyperDash":false}]},{"StartTime":67256.0,"Objects":[{"StartTime":67256.0,"Position":316.0,"HyperDash":false},{"StartTime":67323.0,"Position":306.150818,"HyperDash":false},{"StartTime":67427.0,"Position":300.033234,"HyperDash":false}]},{"StartTime":67599.0,"Objects":[{"StartTime":67599.0,"Position":224.0,"HyperDash":false},{"StartTime":67666.0,"Position":211.175949,"HyperDash":false},{"StartTime":67770.0,"Position":163.867111,"HyperDash":false}]},{"StartTime":67942.0,"Objects":[{"StartTime":67942.0,"Position":104.0,"HyperDash":false},{"StartTime":68009.0,"Position":130.849182,"HyperDash":false},{"StartTime":68113.0,"Position":119.966782,"HyperDash":false}]},{"StartTime":68285.0,"Objects":[{"StartTime":68285.0,"Position":80.0,"HyperDash":false},{"StartTime":68352.0,"Position":100.824059,"HyperDash":false},{"StartTime":68456.0,"Position":140.132889,"HyperDash":false}]},{"StartTime":68628.0,"Objects":[{"StartTime":68628.0,"Position":200.0,"HyperDash":false},{"StartTime":68713.0,"Position":188.823929,"HyperDash":false},{"StartTime":68799.0,"Position":213.728134,"HyperDash":false},{"StartTime":68866.0,"Position":223.349274,"HyperDash":false},{"StartTime":68970.0,"Position":200.0,"HyperDash":false}]},{"StartTime":69142.0,"Objects":[{"StartTime":69142.0,"Position":212.0,"HyperDash":false}]},{"StartTime":69313.0,"Objects":[{"StartTime":69313.0,"Position":256.0,"HyperDash":false}]},{"StartTime":69485.0,"Objects":[{"StartTime":69485.0,"Position":256.0,"HyperDash":false}]},{"StartTime":69656.0,"Objects":[{"StartTime":69656.0,"Position":292.0,"HyperDash":false}]},{"StartTime":69828.0,"Objects":[{"StartTime":69828.0,"Position":292.0,"HyperDash":false}]},{"StartTime":69999.0,"Objects":[{"StartTime":69999.0,"Position":368.0,"HyperDash":false}]},{"StartTime":70085.0,"Objects":[{"StartTime":70085.0,"Position":376.0,"HyperDash":false}]},{"StartTime":70171.0,"Objects":[{"StartTime":70171.0,"Position":384.0,"HyperDash":false}]},{"StartTime":70256.0,"Objects":[{"StartTime":70256.0,"Position":392.0,"HyperDash":false}]},{"StartTime":70342.0,"Objects":[{"StartTime":70342.0,"Position":400.0,"HyperDash":false}]},{"StartTime":70428.0,"Objects":[{"StartTime":70428.0,"Position":408.0,"HyperDash":false}]},{"StartTime":70513.0,"Objects":[{"StartTime":70513.0,"Position":416.0,"HyperDash":false},{"StartTime":70598.0,"Position":442.363953,"HyperDash":false},{"StartTime":70684.0,"Position":451.799652,"HyperDash":false},{"StartTime":70769.0,"Position":450.290955,"HyperDash":false},{"StartTime":70855.0,"Position":444.293518,"HyperDash":false},{"StartTime":70941.0,"Position":469.222717,"HyperDash":false},{"StartTime":71027.0,"Position":451.823273,"HyperDash":false},{"StartTime":71112.0,"Position":447.6526,"HyperDash":false},{"StartTime":71198.0,"Position":416.0,"HyperDash":false},{"StartTime":71284.0,"Position":452.508881,"HyperDash":false},{"StartTime":71370.0,"Position":451.846527,"HyperDash":false},{"StartTime":71437.0,"Position":457.989929,"HyperDash":false},{"StartTime":71541.0,"Position":444.293518,"HyperDash":false}]},{"StartTime":71885.0,"Objects":[{"StartTime":71885.0,"Position":312.0,"HyperDash":false}]},{"StartTime":72056.0,"Objects":[{"StartTime":72056.0,"Position":312.0,"HyperDash":false}]},{"StartTime":72228.0,"Objects":[{"StartTime":72228.0,"Position":224.0,"HyperDash":false}]},{"StartTime":72313.0,"Objects":[{"StartTime":72313.0,"Position":216.0,"HyperDash":false}]},{"StartTime":72399.0,"Objects":[{"StartTime":72399.0,"Position":208.0,"HyperDash":false}]},{"StartTime":72485.0,"Objects":[{"StartTime":72485.0,"Position":200.0,"HyperDash":false}]},{"StartTime":72571.0,"Objects":[{"StartTime":72571.0,"Position":192.0,"HyperDash":false}]},{"StartTime":72742.0,"Objects":[{"StartTime":72742.0,"Position":124.0,"HyperDash":false}]},{"StartTime":72913.0,"Objects":[{"StartTime":72913.0,"Position":48.0,"HyperDash":false},{"StartTime":72980.0,"Position":68.42183,"HyperDash":false},{"StartTime":73084.0,"Position":84.285,"HyperDash":false}]},{"StartTime":73256.0,"Objects":[{"StartTime":73256.0,"Position":44.0,"HyperDash":false},{"StartTime":73323.0,"Position":27.0,"HyperDash":false},{"StartTime":73427.0,"Position":44.0,"HyperDash":false}]},{"StartTime":73599.0,"Objects":[{"StartTime":73599.0,"Position":116.0,"HyperDash":false},{"StartTime":73666.0,"Position":134.0,"HyperDash":false},{"StartTime":73770.0,"Position":116.0,"HyperDash":false}]},{"StartTime":73942.0,"Objects":[{"StartTime":73942.0,"Position":188.0,"HyperDash":false},{"StartTime":74027.0,"Position":194.0,"HyperDash":false},{"StartTime":74113.0,"Position":188.0,"HyperDash":false},{"StartTime":74180.0,"Position":177.0,"HyperDash":false},{"StartTime":74284.0,"Position":188.0,"HyperDash":false}]},{"StartTime":74456.0,"Objects":[{"StartTime":74456.0,"Position":188.0,"HyperDash":false}]},{"StartTime":74628.0,"Objects":[{"StartTime":74628.0,"Position":260.0,"HyperDash":false},{"StartTime":74695.0,"Position":292.008942,"HyperDash":false},{"StartTime":74799.0,"Position":311.0676,"HyperDash":false}]},{"StartTime":74971.0,"Objects":[{"StartTime":74971.0,"Position":361.0,"HyperDash":false},{"StartTime":75038.0,"Position":333.214569,"HyperDash":false},{"StartTime":75142.0,"Position":310.502869,"HyperDash":false}]},{"StartTime":75313.0,"Objects":[{"StartTime":75313.0,"Position":260.0,"HyperDash":false},{"StartTime":75380.0,"Position":290.008942,"HyperDash":false},{"StartTime":75484.0,"Position":311.0676,"HyperDash":false}]},{"StartTime":75656.0,"Objects":[{"StartTime":75656.0,"Position":360.0,"HyperDash":false},{"StartTime":75723.0,"Position":337.803131,"HyperDash":false},{"StartTime":75827.0,"Position":311.005,"HyperDash":false}]},{"StartTime":75999.0,"Objects":[{"StartTime":75999.0,"Position":49.0,"HyperDash":false},{"StartTime":76063.0,"Position":21.0,"HyperDash":false},{"StartTime":76127.0,"Position":193.0,"HyperDash":false},{"StartTime":76191.0,"Position":52.0,"HyperDash":false},{"StartTime":76256.0,"Position":466.0,"HyperDash":false},{"StartTime":76320.0,"Position":135.0,"HyperDash":false},{"StartTime":76384.0,"Position":121.0,"HyperDash":false},{"StartTime":76449.0,"Position":427.0,"HyperDash":false},{"StartTime":76513.0,"Position":176.0,"HyperDash":false},{"StartTime":76577.0,"Position":96.0,"HyperDash":false},{"StartTime":76642.0,"Position":345.0,"HyperDash":false},{"StartTime":76706.0,"Position":11.0,"HyperDash":false},{"StartTime":76770.0,"Position":393.0,"HyperDash":false},{"StartTime":76835.0,"Position":440.0,"HyperDash":false},{"StartTime":76899.0,"Position":179.0,"HyperDash":false},{"StartTime":76963.0,"Position":470.0,"HyperDash":false},{"StartTime":77028.0,"Position":89.0,"HyperDash":false}]},{"StartTime":77371.0,"Objects":[{"StartTime":77371.0,"Position":48.0,"HyperDash":false},{"StartTime":77456.0,"Position":59.0,"HyperDash":false},{"StartTime":77542.0,"Position":48.0,"HyperDash":false},{"StartTime":77609.0,"Position":67.0,"HyperDash":false},{"StartTime":77713.0,"Position":48.0,"HyperDash":false}]},{"StartTime":78056.0,"Objects":[{"StartTime":78056.0,"Position":152.0,"HyperDash":false},{"StartTime":78141.0,"Position":162.0,"HyperDash":false},{"StartTime":78227.0,"Position":152.0,"HyperDash":false},{"StartTime":78294.0,"Position":135.0,"HyperDash":false},{"StartTime":78398.0,"Position":152.0,"HyperDash":false}]},{"StartTime":78742.0,"Objects":[{"StartTime":78742.0,"Position":152.0,"HyperDash":false},{"StartTime":78827.0,"Position":154.0,"HyperDash":false},{"StartTime":78913.0,"Position":152.0,"HyperDash":false},{"StartTime":78980.0,"Position":138.0,"HyperDash":false},{"StartTime":79084.0,"Position":152.0,"HyperDash":false}]},{"StartTime":79427.0,"Objects":[{"StartTime":79427.0,"Position":256.0,"HyperDash":false},{"StartTime":79512.0,"Position":248.0,"HyperDash":false},{"StartTime":79598.0,"Position":256.0,"HyperDash":false},{"StartTime":79665.0,"Position":270.0,"HyperDash":false},{"StartTime":79769.0,"Position":256.0,"HyperDash":false}]},{"StartTime":80113.0,"Objects":[{"StartTime":80113.0,"Position":256.0,"HyperDash":false},{"StartTime":80198.0,"Position":249.0,"HyperDash":false},{"StartTime":80284.0,"Position":256.0,"HyperDash":false},{"StartTime":80369.0,"Position":245.0,"HyperDash":false},{"StartTime":80455.0,"Position":256.0,"HyperDash":false},{"StartTime":80541.0,"Position":244.0,"HyperDash":false},{"StartTime":80627.0,"Position":256.0,"HyperDash":false},{"StartTime":80694.0,"Position":265.0,"HyperDash":false},{"StartTime":80798.0,"Position":256.0,"HyperDash":false}]},{"StartTime":81142.0,"Objects":[{"StartTime":81142.0,"Position":256.0,"HyperDash":false},{"StartTime":81227.0,"Position":292.744537,"HyperDash":false},{"StartTime":81313.0,"Position":325.897827,"HyperDash":false},{"StartTime":81398.0,"Position":358.642334,"HyperDash":false},{"StartTime":81484.0,"Position":396.0,"HyperDash":false},{"StartTime":81570.0,"Position":346.0511,"HyperDash":false},{"StartTime":81656.0,"Position":325.897827,"HyperDash":false},{"StartTime":81723.0,"Position":285.510956,"HyperDash":false},{"StartTime":81827.0,"Position":256.0,"HyperDash":false}]},{"StartTime":82171.0,"Objects":[{"StartTime":82171.0,"Position":468.0,"HyperDash":false}]},{"StartTime":82513.0,"Objects":[{"StartTime":82513.0,"Position":468.0,"HyperDash":false}]},{"StartTime":82856.0,"Objects":[{"StartTime":82856.0,"Position":352.0,"HyperDash":false},{"StartTime":82941.0,"Position":368.54422,"HyperDash":false},{"StartTime":83027.0,"Position":407.5205,"HyperDash":false},{"StartTime":83112.0,"Position":374.08432,"HyperDash":false},{"StartTime":83198.0,"Position":352.0,"HyperDash":false},{"StartTime":83266.0,"Position":371.819336,"HyperDash":false},{"StartTime":83370.0,"Position":407.5205,"HyperDash":false}]},{"StartTime":83542.0,"Objects":[{"StartTime":83542.0,"Position":448.0,"HyperDash":false}]},{"StartTime":83885.0,"Objects":[{"StartTime":83885.0,"Position":324.0,"HyperDash":false}]},{"StartTime":84228.0,"Objects":[{"StartTime":84228.0,"Position":160.0,"HyperDash":false},{"StartTime":84313.0,"Position":124.276367,"HyperDash":false},{"StartTime":84399.0,"Position":104.117874,"HyperDash":false},{"StartTime":84484.0,"Position":150.732773,"HyperDash":false},{"StartTime":84570.0,"Position":160.0,"HyperDash":false},{"StartTime":84638.0,"Position":132.038544,"HyperDash":false},{"StartTime":84742.0,"Position":104.117874,"HyperDash":false}]},{"StartTime":84913.0,"Objects":[{"StartTime":84913.0,"Position":64.0,"HyperDash":false}]},{"StartTime":85256.0,"Objects":[{"StartTime":85256.0,"Position":188.0,"HyperDash":false}]},{"StartTime":85599.0,"Objects":[{"StartTime":85599.0,"Position":352.0,"HyperDash":false},{"StartTime":85684.0,"Position":376.7821,"HyperDash":false},{"StartTime":85770.0,"Position":408.0,"HyperDash":false},{"StartTime":85855.0,"Position":395.326843,"HyperDash":false},{"StartTime":85941.0,"Position":352.0,"HyperDash":false},{"StartTime":86009.0,"Position":380.007782,"HyperDash":false},{"StartTime":86113.0,"Position":408.0,"HyperDash":false}]},{"StartTime":86285.0,"Objects":[{"StartTime":86285.0,"Position":356.0,"HyperDash":false}]},{"StartTime":86456.0,"Objects":[{"StartTime":86456.0,"Position":356.0,"HyperDash":false}]},{"StartTime":86628.0,"Objects":[{"StartTime":86628.0,"Position":356.0,"HyperDash":false}]},{"StartTime":86971.0,"Objects":[{"StartTime":86971.0,"Position":160.0,"HyperDash":false},{"StartTime":87056.0,"Position":162.926041,"HyperDash":false},{"StartTime":87142.0,"Position":133.659821,"HyperDash":false},{"StartTime":87227.0,"Position":161.695328,"HyperDash":false},{"StartTime":87313.0,"Position":160.0,"HyperDash":false},{"StartTime":87399.0,"Position":140.849136,"HyperDash":false},{"StartTime":87485.0,"Position":133.659821,"HyperDash":false},{"StartTime":87552.0,"Position":142.003632,"HyperDash":false},{"StartTime":87656.0,"Position":160.0,"HyperDash":false}]},{"StartTime":87999.0,"Objects":[{"StartTime":87999.0,"Position":256.0,"HyperDash":false}]},{"StartTime":88342.0,"Objects":[{"StartTime":88342.0,"Position":104.0,"HyperDash":false},{"StartTime":88427.0,"Position":100.104553,"HyperDash":false},{"StartTime":88513.0,"Position":76.88288,"HyperDash":false},{"StartTime":88598.0,"Position":61.4504166,"HyperDash":false},{"StartTime":88684.0,"Position":80.21796,"HyperDash":false},{"StartTime":88770.0,"Position":95.16115,"HyperDash":false},{"StartTime":88856.0,"Position":115.62986,"HyperDash":false},{"StartTime":88941.0,"Position":138.0912,"HyperDash":false},{"StartTime":89027.0,"Position":175.517044,"HyperDash":false},{"StartTime":89113.0,"Position":220.3342,"HyperDash":false},{"StartTime":89199.0,"Position":244.674866,"HyperDash":false},{"StartTime":89284.0,"Position":264.2282,"HyperDash":false},{"StartTime":89370.0,"Position":300.583649,"HyperDash":false},{"StartTime":89456.0,"Position":336.589539,"HyperDash":false},{"StartTime":89542.0,"Position":332.588257,"HyperDash":false},{"StartTime":89609.0,"Position":357.060455,"HyperDash":false},{"StartTime":89713.0,"Position":343.816345,"HyperDash":false}]},{"StartTime":89885.0,"Objects":[{"StartTime":89885.0,"Position":408.0,"HyperDash":false}]},{"StartTime":90056.0,"Objects":[{"StartTime":90056.0,"Position":416.0,"HyperDash":false}]},{"StartTime":90228.0,"Objects":[{"StartTime":90228.0,"Position":400.0,"HyperDash":false}]},{"StartTime":90399.0,"Objects":[{"StartTime":90399.0,"Position":360.0,"HyperDash":false},{"StartTime":90484.0,"Position":325.326477,"HyperDash":false},{"StartTime":90570.0,"Position":314.08017,"HyperDash":false},{"StartTime":90655.0,"Position":295.765259,"HyperDash":false},{"StartTime":90741.0,"Position":250.349167,"HyperDash":false},{"StartTime":90827.0,"Position":234.540588,"HyperDash":false},{"StartTime":90913.0,"Position":180.487732,"HyperDash":false},{"StartTime":90998.0,"Position":158.6242,"HyperDash":false},{"StartTime":91084.0,"Position":114.161362,"HyperDash":false},{"StartTime":91170.0,"Position":64.53248,"HyperDash":false},{"StartTime":91256.0,"Position":58.7642,"HyperDash":false},{"StartTime":91323.0,"Position":33.0224953,"HyperDash":false},{"StartTime":91427.0,"Position":23.1158314,"HyperDash":false}]},{"StartTime":91599.0,"Objects":[{"StartTime":91599.0,"Position":60.0,"HyperDash":false}]},{"StartTime":91771.0,"Objects":[{"StartTime":91771.0,"Position":24.0,"HyperDash":false},{"StartTime":91856.0,"Position":42.1049347,"HyperDash":false},{"StartTime":91942.0,"Position":82.55228,"HyperDash":false},{"StartTime":92009.0,"Position":124.493813,"HyperDash":false},{"StartTime":92113.0,"Position":141.104553,"HyperDash":false}]},{"StartTime":92285.0,"Objects":[{"StartTime":92285.0,"Position":339.0,"HyperDash":false},{"StartTime":92381.0,"Position":342.0,"HyperDash":false},{"StartTime":92477.0,"Position":249.0,"HyperDash":false},{"StartTime":92574.0,"Position":235.0,"HyperDash":false},{"StartTime":92670.0,"Position":323.0,"HyperDash":false},{"StartTime":92767.0,"Position":365.0,"HyperDash":false},{"StartTime":92863.0,"Position":74.0,"HyperDash":false},{"StartTime":92960.0,"Position":281.0,"HyperDash":false},{"StartTime":93056.0,"Position":398.0,"HyperDash":false},{"StartTime":93152.0,"Position":335.0,"HyperDash":false},{"StartTime":93249.0,"Position":388.0,"HyperDash":false},{"StartTime":93345.0,"Position":228.0,"HyperDash":false},{"StartTime":93442.0,"Position":323.0,"HyperDash":false},{"StartTime":93538.0,"Position":441.0,"HyperDash":false},{"StartTime":93635.0,"Position":442.0,"HyperDash":false},{"StartTime":93731.0,"Position":278.0,"HyperDash":false},{"StartTime":93828.0,"Position":90.0,"HyperDash":false}]},{"StartTime":94513.0,"Objects":[{"StartTime":94513.0,"Position":64.0,"HyperDash":false},{"StartTime":94598.0,"Position":68.14916,"HyperDash":false},{"StartTime":94684.0,"Position":62.2626343,"HyperDash":false},{"StartTime":94769.0,"Position":86.91272,"HyperDash":false},{"StartTime":94855.0,"Position":102.010681,"HyperDash":false},{"StartTime":94941.0,"Position":141.25354,"HyperDash":false},{"StartTime":95027.0,"Position":166.435471,"HyperDash":false},{"StartTime":95094.0,"Position":206.542572,"HyperDash":false},{"StartTime":95198.0,"Position":230.41568,"HyperDash":false}]},{"StartTime":95371.0,"Objects":[{"StartTime":95371.0,"Position":300.0,"HyperDash":false}]},{"StartTime":95542.0,"Objects":[{"StartTime":95542.0,"Position":340.0,"HyperDash":false}]},{"StartTime":95713.0,"Objects":[{"StartTime":95713.0,"Position":404.0,"HyperDash":false}]},{"StartTime":95885.0,"Objects":[{"StartTime":95885.0,"Position":448.0,"HyperDash":false},{"StartTime":95970.0,"Position":440.850861,"HyperDash":false},{"StartTime":96056.0,"Position":449.737366,"HyperDash":false},{"StartTime":96141.0,"Position":429.08728,"HyperDash":false},{"StartTime":96227.0,"Position":409.989319,"HyperDash":false},{"StartTime":96313.0,"Position":361.74646,"HyperDash":false},{"StartTime":96399.0,"Position":345.564545,"HyperDash":false},{"StartTime":96466.0,"Position":303.457428,"HyperDash":false},{"StartTime":96570.0,"Position":281.58432,"HyperDash":false}]},{"StartTime":96913.0,"Objects":[{"StartTime":96913.0,"Position":256.0,"HyperDash":false}]},{"StartTime":97256.0,"Objects":[{"StartTime":97256.0,"Position":464.0,"HyperDash":false},{"StartTime":97341.0,"Position":440.493042,"HyperDash":false},{"StartTime":97427.0,"Position":396.852631,"HyperDash":false},{"StartTime":97494.0,"Position":353.930542,"HyperDash":false},{"StartTime":97598.0,"Position":329.726563,"HyperDash":false}]},{"StartTime":97771.0,"Objects":[{"StartTime":97771.0,"Position":252.0,"HyperDash":false}]},{"StartTime":97942.0,"Objects":[{"StartTime":97942.0,"Position":176.0,"HyperDash":false},{"StartTime":98027.0,"Position":129.262726,"HyperDash":false},{"StartTime":98113.0,"Position":106.0,"HyperDash":false},{"StartTime":98198.0,"Position":154.620514,"HyperDash":false},{"StartTime":98284.0,"Position":176.0,"HyperDash":false},{"StartTime":98370.0,"Position":142.08757,"HyperDash":false},{"StartTime":98456.0,"Position":106.0,"HyperDash":false},{"StartTime":98541.0,"Position":132.795654,"HyperDash":false},{"StartTime":98627.0,"Position":176.0,"HyperDash":false},{"StartTime":98713.0,"Position":145.912415,"HyperDash":false},{"StartTime":98799.0,"Position":106.0,"HyperDash":false},{"StartTime":98884.0,"Position":134.9708,"HyperDash":false},{"StartTime":98970.0,"Position":176.0,"HyperDash":false},{"StartTime":99038.0,"Position":137.093414,"HyperDash":false},{"StartTime":99141.0,"Position":106.0,"HyperDash":false}]},{"StartTime":99313.0,"Objects":[{"StartTime":99313.0,"Position":28.0,"HyperDash":false},{"StartTime":99398.0,"Position":26.2349854,"HyperDash":false},{"StartTime":99484.0,"Position":17.7651138,"HyperDash":false},{"StartTime":99569.0,"Position":33.3757133,"HyperDash":false},{"StartTime":99655.0,"Position":31.6727753,"HyperDash":false},{"StartTime":99741.0,"Position":33.7641869,"HyperDash":false},{"StartTime":99827.0,"Position":72.2299042,"HyperDash":false},{"StartTime":99912.0,"Position":92.74443,"HyperDash":false},{"StartTime":99998.0,"Position":133.558716,"HyperDash":false},{"StartTime":100084.0,"Position":158.430649,"HyperDash":false},{"StartTime":100170.0,"Position":202.717,"HyperDash":false},{"StartTime":100237.0,"Position":217.776627,"HyperDash":false},{"StartTime":100341.0,"Position":255.047836,"HyperDash":false}]},{"StartTime":100685.0,"Objects":[{"StartTime":100685.0,"Position":484.0,"HyperDash":false},{"StartTime":100770.0,"Position":489.764954,"HyperDash":false},{"StartTime":100856.0,"Position":494.2329,"HyperDash":false},{"StartTime":100941.0,"Position":504.6108,"HyperDash":false},{"StartTime":101027.0,"Position":480.2734,"HyperDash":false},{"StartTime":101113.0,"Position":448.084351,"HyperDash":false},{"StartTime":101199.0,"Position":439.444244,"HyperDash":false},{"StartTime":101284.0,"Position":426.701172,"HyperDash":false},{"StartTime":101370.0,"Position":377.68396,"HyperDash":false},{"StartTime":101456.0,"Position":326.754578,"HyperDash":false},{"StartTime":101542.0,"Position":308.535339,"HyperDash":false},{"StartTime":101609.0,"Position":299.317078,"HyperDash":false},{"StartTime":101713.0,"Position":254.267319,"HyperDash":false}]},{"StartTime":102056.0,"Objects":[{"StartTime":102056.0,"Position":160.0,"HyperDash":false},{"StartTime":102141.0,"Position":171.288788,"HyperDash":false},{"StartTime":102227.0,"Position":190.8506,"HyperDash":false},{"StartTime":102312.0,"Position":206.4524,"HyperDash":false},{"StartTime":102398.0,"Position":256.016479,"HyperDash":false},{"StartTime":102484.0,"Position":285.578949,"HyperDash":false},{"StartTime":102570.0,"Position":321.497925,"HyperDash":false},{"StartTime":102655.0,"Position":361.7266,"HyperDash":false},{"StartTime":102741.0,"Position":352.033936,"HyperDash":false},{"StartTime":102827.0,"Position":339.916229,"HyperDash":false},{"StartTime":102913.0,"Position":321.497925,"HyperDash":false},{"StartTime":102998.0,"Position":307.967651,"HyperDash":false},{"StartTime":103084.0,"Position":256.424927,"HyperDash":false},{"StartTime":103170.0,"Position":225.84108,"HyperDash":false},{"StartTime":103256.0,"Position":190.8506,"HyperDash":false},{"StartTime":103323.0,"Position":189.241409,"HyperDash":false},{"StartTime":103427.0,"Position":160.0,"HyperDash":false}]},{"StartTime":103771.0,"Objects":[{"StartTime":103771.0,"Position":48.0,"HyperDash":false}]},{"StartTime":104113.0,"Objects":[{"StartTime":104113.0,"Position":256.0,"HyperDash":false}]},{"StartTime":104456.0,"Objects":[{"StartTime":104456.0,"Position":464.0,"HyperDash":false}]},{"StartTime":104799.0,"Objects":[{"StartTime":104799.0,"Position":352.0,"HyperDash":false}]},{"StartTime":104971.0,"Objects":[{"StartTime":104971.0,"Position":272.0,"HyperDash":false},{"StartTime":105056.0,"Position":237.0,"HyperDash":false},{"StartTime":105142.0,"Position":272.0,"HyperDash":false}]},{"StartTime":105313.0,"Objects":[{"StartTime":105313.0,"Position":240.0,"HyperDash":false},{"StartTime":105398.0,"Position":275.0,"HyperDash":false},{"StartTime":105484.0,"Position":240.0,"HyperDash":false}]},{"StartTime":105656.0,"Objects":[{"StartTime":105656.0,"Position":272.0,"HyperDash":false},{"StartTime":105741.0,"Position":237.0,"HyperDash":false},{"StartTime":105827.0,"Position":272.0,"HyperDash":false}]},{"StartTime":105999.0,"Objects":[{"StartTime":105999.0,"Position":240.0,"HyperDash":false},{"StartTime":106084.0,"Position":275.0,"HyperDash":false},{"StartTime":106170.0,"Position":240.0,"HyperDash":false}]},{"StartTime":106342.0,"Objects":[{"StartTime":106342.0,"Position":168.0,"HyperDash":false},{"StartTime":106409.0,"Position":144.031464,"HyperDash":false},{"StartTime":106513.0,"Position":104.274345,"HyperDash":false}]},{"StartTime":106685.0,"Objects":[{"StartTime":106685.0,"Position":56.0,"HyperDash":false},{"StartTime":106752.0,"Position":96.4269,"HyperDash":false},{"StartTime":106856.0,"Position":126.0,"HyperDash":false}]},{"StartTime":107028.0,"Objects":[{"StartTime":107028.0,"Position":104.0,"HyperDash":false},{"StartTime":107095.0,"Position":137.981888,"HyperDash":false},{"StartTime":107199.0,"Position":167.759735,"HyperDash":false}]},{"StartTime":107371.0,"Objects":[{"StartTime":107371.0,"Position":256.0,"HyperDash":false}]},{"StartTime":107542.0,"Objects":[{"StartTime":107542.0,"Position":344.0,"HyperDash":false},{"StartTime":107609.0,"Position":367.968536,"HyperDash":false},{"StartTime":107713.0,"Position":407.725647,"HyperDash":false}]},{"StartTime":107885.0,"Objects":[{"StartTime":107885.0,"Position":456.0,"HyperDash":false},{"StartTime":107952.0,"Position":441.5731,"HyperDash":false},{"StartTime":108056.0,"Position":386.0,"HyperDash":false}]},{"StartTime":108228.0,"Objects":[{"StartTime":108228.0,"Position":408.0,"HyperDash":false},{"StartTime":108295.0,"Position":368.018127,"HyperDash":false},{"StartTime":108399.0,"Position":344.240265,"HyperDash":false}]},{"StartTime":108571.0,"Objects":[{"StartTime":108571.0,"Position":256.0,"HyperDash":false},{"StartTime":108628.0,"Position":256.0,"HyperDash":false},{"StartTime":108685.0,"Position":256.0,"HyperDash":false},{"StartTime":108742.0,"Position":256.0,"HyperDash":false},{"StartTime":108799.0,"Position":256.0,"HyperDash":false},{"StartTime":108856.0,"Position":256.0,"HyperDash":false},{"StartTime":108913.0,"Position":256.0,"HyperDash":false}]},{"StartTime":108999.0,"Objects":[{"StartTime":108999.0,"Position":152.0,"HyperDash":false},{"StartTime":109057.0,"Position":321.0,"HyperDash":false},{"StartTime":109116.0,"Position":303.0,"HyperDash":false},{"StartTime":109175.0,"Position":259.0,"HyperDash":false},{"StartTime":109234.0,"Position":186.0,"HyperDash":false},{"StartTime":109293.0,"Position":140.0,"HyperDash":false},{"StartTime":109352.0,"Position":207.0,"HyperDash":false},{"StartTime":109411.0,"Position":278.0,"HyperDash":false},{"StartTime":109470.0,"Position":223.0,"HyperDash":false},{"StartTime":109529.0,"Position":389.0,"HyperDash":false},{"StartTime":109588.0,"Position":245.0,"HyperDash":false},{"StartTime":109647.0,"Position":400.0,"HyperDash":false},{"StartTime":109706.0,"Position":445.0,"HyperDash":false},{"StartTime":109765.0,"Position":443.0,"HyperDash":false},{"StartTime":109824.0,"Position":245.0,"HyperDash":false},{"StartTime":109883.0,"Position":259.0,"HyperDash":false},{"StartTime":109942.0,"Position":422.0,"HyperDash":false}]},{"StartTime":110285.0,"Objects":[{"StartTime":110285.0,"Position":168.0,"HyperDash":false},{"StartTime":110352.0,"Position":172.393753,"HyperDash":false},{"StartTime":110456.0,"Position":217.497467,"HyperDash":false}]},{"StartTime":110628.0,"Objects":[{"StartTime":110628.0,"Position":344.0,"HyperDash":false},{"StartTime":110695.0,"Position":329.606262,"HyperDash":false},{"StartTime":110799.0,"Position":294.502533,"HyperDash":false}]},{"StartTime":110971.0,"Objects":[{"StartTime":110971.0,"Position":207.0,"HyperDash":false},{"StartTime":111038.0,"Position":237.393753,"HyperDash":false},{"StartTime":111142.0,"Position":256.497467,"HyperDash":false}]},{"StartTime":111313.0,"Objects":[{"StartTime":111313.0,"Position":305.0,"HyperDash":false},{"StartTime":111380.0,"Position":285.606262,"HyperDash":false},{"StartTime":111484.0,"Position":255.502533,"HyperDash":false}]},{"StartTime":111656.0,"Objects":[{"StartTime":111656.0,"Position":216.0,"HyperDash":false},{"StartTime":111741.0,"Position":222.948441,"HyperDash":false},{"StartTime":111827.0,"Position":256.131561,"HyperDash":false},{"StartTime":111912.0,"Position":267.080017,"HyperDash":false},{"StartTime":111998.0,"Position":296.419617,"HyperDash":false},{"StartTime":112084.0,"Position":260.392944,"HyperDash":false},{"StartTime":112170.0,"Position":256.2098,"HyperDash":false},{"StartTime":112255.0,"Position":223.261353,"HyperDash":false},{"StartTime":112341.0,"Position":216.0,"HyperDash":false},{"StartTime":112427.0,"Position":243.1049,"HyperDash":false},{"StartTime":112513.0,"Position":256.288025,"HyperDash":false},{"StartTime":112580.0,"Position":290.012115,"HyperDash":false},{"StartTime":112684.0,"Position":296.419617,"HyperDash":false}]},{"StartTime":113028.0,"Objects":[{"StartTime":113028.0,"Position":352.0,"HyperDash":false}]},{"StartTime":113199.0,"Objects":[{"StartTime":113199.0,"Position":360.0,"HyperDash":false},{"StartTime":113284.0,"Position":364.341217,"HyperDash":false},{"StartTime":113370.0,"Position":360.0,"HyperDash":false}]},{"StartTime":113542.0,"Objects":[{"StartTime":113542.0,"Position":424.0,"HyperDash":false}]},{"StartTime":113713.0,"Objects":[{"StartTime":113713.0,"Position":352.0,"HyperDash":false}]},{"StartTime":113885.0,"Objects":[{"StartTime":113885.0,"Position":408.0,"HyperDash":false},{"StartTime":113970.0,"Position":441.203918,"HyperDash":false},{"StartTime":114056.0,"Position":408.0,"HyperDash":false}]},{"StartTime":114228.0,"Objects":[{"StartTime":114228.0,"Position":336.0,"HyperDash":false}]},{"StartTime":114399.0,"Objects":[{"StartTime":114399.0,"Position":352.0,"HyperDash":false}]},{"StartTime":114571.0,"Objects":[{"StartTime":114571.0,"Position":280.0,"HyperDash":false},{"StartTime":114656.0,"Position":248.695053,"HyperDash":false},{"StartTime":114742.0,"Position":280.0,"HyperDash":false}]},{"StartTime":114913.0,"Objects":[{"StartTime":114913.0,"Position":352.0,"HyperDash":false}]},{"StartTime":115085.0,"Objects":[{"StartTime":115085.0,"Position":296.0,"HyperDash":false}]},{"StartTime":115256.0,"Objects":[{"StartTime":115256.0,"Position":368.0,"HyperDash":false}]},{"StartTime":115428.0,"Objects":[{"StartTime":115428.0,"Position":424.0,"HyperDash":false}]},{"StartTime":115771.0,"Objects":[{"StartTime":115771.0,"Position":128.0,"HyperDash":false},{"StartTime":115838.0,"Position":111.239639,"HyperDash":false},{"StartTime":115942.0,"Position":60.5060654,"HyperDash":false}]},{"StartTime":116113.0,"Objects":[{"StartTime":116113.0,"Position":64.0,"HyperDash":false},{"StartTime":116180.0,"Position":98.76035,"HyperDash":false},{"StartTime":116284.0,"Position":131.493927,"HyperDash":false}]},{"StartTime":116456.0,"Objects":[{"StartTime":116456.0,"Position":136.0,"HyperDash":false},{"StartTime":116523.0,"Position":92.23965,"HyperDash":false},{"StartTime":116627.0,"Position":68.5060654,"HyperDash":false}]},{"StartTime":116798.0,"Objects":[{"StartTime":116798.0,"Position":72.0,"HyperDash":false},{"StartTime":116865.0,"Position":112.760361,"HyperDash":false},{"StartTime":116969.0,"Position":139.493927,"HyperDash":false}]},{"StartTime":117142.0,"Objects":[{"StartTime":117142.0,"Position":216.0,"HyperDash":false}]},{"StartTime":117313.0,"Objects":[{"StartTime":117313.0,"Position":216.0,"HyperDash":false}]},{"StartTime":117485.0,"Objects":[{"StartTime":117485.0,"Position":216.0,"HyperDash":false}]},{"StartTime":117828.0,"Objects":[{"StartTime":117828.0,"Position":296.0,"HyperDash":false}]},{"StartTime":117999.0,"Objects":[{"StartTime":117999.0,"Position":296.0,"HyperDash":false}]},{"StartTime":118171.0,"Objects":[{"StartTime":118171.0,"Position":296.0,"HyperDash":false}]},{"StartTime":118513.0,"Objects":[{"StartTime":118513.0,"Position":448.0,"HyperDash":false},{"StartTime":118580.0,"Position":418.681824,"HyperDash":false},{"StartTime":118684.0,"Position":391.038666,"HyperDash":false}]},{"StartTime":118856.0,"Objects":[{"StartTime":118856.0,"Position":392.0,"HyperDash":false}]},{"StartTime":118942.0,"Objects":[{"StartTime":118942.0,"Position":392.0,"HyperDash":false}]},{"StartTime":119028.0,"Objects":[{"StartTime":119028.0,"Position":392.0,"HyperDash":false}]},{"StartTime":119199.0,"Objects":[{"StartTime":119199.0,"Position":392.0,"HyperDash":false},{"StartTime":119284.0,"Position":392.0,"HyperDash":false},{"StartTime":119370.0,"Position":392.0,"HyperDash":false}]},{"StartTime":119542.0,"Objects":[{"StartTime":119542.0,"Position":464.0,"HyperDash":false}]},{"StartTime":119628.0,"Objects":[{"StartTime":119628.0,"Position":464.0,"HyperDash":false}]},{"StartTime":119713.0,"Objects":[{"StartTime":119713.0,"Position":464.0,"HyperDash":false}]},{"StartTime":119885.0,"Objects":[{"StartTime":119885.0,"Position":464.0,"HyperDash":false},{"StartTime":119970.0,"Position":443.501282,"HyperDash":false},{"StartTime":120056.0,"Position":394.59668,"HyperDash":false},{"StartTime":120141.0,"Position":351.097931,"HyperDash":false},{"StartTime":120227.0,"Position":325.193329,"HyperDash":false},{"StartTime":120313.0,"Position":287.288727,"HyperDash":false},{"StartTime":120399.0,"Position":255.384125,"HyperDash":false},{"StartTime":120484.0,"Position":213.8854,"HyperDash":false},{"StartTime":120570.0,"Position":185.980774,"HyperDash":false},{"StartTime":120656.0,"Position":160.0762,"HyperDash":false},{"StartTime":120742.0,"Position":116.1716,"HyperDash":false},{"StartTime":120809.0,"Position":79.9784546,"HyperDash":false},{"StartTime":120913.0,"Position":46.7682343,"HyperDash":false}]},{"StartTime":121256.0,"Objects":[{"StartTime":121256.0,"Position":441.0,"HyperDash":false},{"StartTime":121341.0,"Position":45.0,"HyperDash":false},{"StartTime":121427.0,"Position":92.0,"HyperDash":false},{"StartTime":121513.0,"Position":399.0,"HyperDash":false},{"StartTime":121598.0,"Position":494.0,"HyperDash":false},{"StartTime":121684.0,"Position":324.0,"HyperDash":false},{"StartTime":121770.0,"Position":31.0,"HyperDash":false},{"StartTime":121856.0,"Position":79.0,"HyperDash":false},{"StartTime":121941.0,"Position":163.0,"HyperDash":false},{"StartTime":122027.0,"Position":303.0,"HyperDash":false},{"StartTime":122113.0,"Position":462.0,"HyperDash":false},{"StartTime":122198.0,"Position":74.0,"HyperDash":false},{"StartTime":122284.0,"Position":4.0,"HyperDash":false},{"StartTime":122370.0,"Position":253.0,"HyperDash":false},{"StartTime":122456.0,"Position":159.0,"HyperDash":false},{"StartTime":122541.0,"Position":74.0,"HyperDash":false},{"StartTime":122627.0,"Position":242.0,"HyperDash":false},{"StartTime":122713.0,"Position":251.0,"HyperDash":false},{"StartTime":122798.0,"Position":146.0,"HyperDash":false},{"StartTime":122884.0,"Position":487.0,"HyperDash":false},{"StartTime":122970.0,"Position":294.0,"HyperDash":false},{"StartTime":123056.0,"Position":322.0,"HyperDash":false},{"StartTime":123141.0,"Position":208.0,"HyperDash":false},{"StartTime":123227.0,"Position":154.0,"HyperDash":false},{"StartTime":123313.0,"Position":476.0,"HyperDash":false},{"StartTime":123398.0,"Position":27.0,"HyperDash":false},{"StartTime":123484.0,"Position":377.0,"HyperDash":false},{"StartTime":123570.0,"Position":234.0,"HyperDash":false},{"StartTime":123656.0,"Position":459.0,"HyperDash":false},{"StartTime":123741.0,"Position":106.0,"HyperDash":false},{"StartTime":123827.0,"Position":321.0,"HyperDash":false},{"StartTime":123913.0,"Position":368.0,"HyperDash":false},{"StartTime":123999.0,"Position":488.0,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/103019.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/103019.osu new file mode 100644 index 0000000000..2f3814c57b --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/103019.osu @@ -0,0 +1,329 @@ +osu file format v9 + +[General] +StackLeniency: 0.3 +Mode: 0 + +[Difficulty] +HPDrainRate:5 +CircleSize:4 +OverallDifficulty:5 +ApproachRate:7 +SliderMultiplier:1.4 +SliderTickRate:2 + +[Events] +//Background and Video events +//Break Periods +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples +//Background Colour Transformations +3,100,163,162,255 + +[TimingPoints] +571.114342485549,342.857142857143,4,1,0,60,1,0 +11542,-100,4,1,0,90,0,0 +59542,-100,4,2,0,90,0,0 +60913,-100,4,1,0,90,0,0 +65028,-100,4,1,0,90,0,0 +66399,-100,4,1,0,90,0,1 +77371,-100,4,1,0,90,0,0 +86971,-200,4,1,0,90,0,0 +87999,-50,4,1,0,90,0,0 +88342,-100,4,1,0,90,0,0 +110285,-100,4,1,0,90,0,1 +119885,-100,4,1,0,100,0,0 +121574,-50,4,2,0,90,0,0 +121917,-50,4,2,0,80,0,0 +122260,-50,4,2,0,70,0,0 +122603,-50,4,2,0,60,0,0 +122946,-50,4,2,0,50,0,0 +123288,-50,4,2,0,40,0,0 +123631,-50,4,2,0,30,0,0 +123974,-50,4,2,0,5,0,0 + +[HitObjects] +184,192,571,6,0,B|168:104|256:45|344:104|328:192,1,280 +256,312,1599,2,0,B|256:160,3,140 +256,32,2971,5,0 +128,88,3313,1,0 +128,232,3656,2,0,B|128:328,2,70 +384,232,4342,2,0,B|384:328,2,70 +384,160,4856,1,0 +384,88,5028,1,0 +256,32,5371,37,2 +256,352,5713,1,2 +128,296,6056,22,0,B|72:272|72:272|72:192,1,140 +384,296,6742,2,0,B|440:272|440:272|440:192,1,140 +256,72,7428,2,0,B|224:136|296:136|256:208,2,140 +256,296,8456,1,2 +256,192,8799,12,0,11199 +256,192,11542,5,4 +60,192,11885,2,0,B|28:240|60:304|132:280,2,140,8|0|8 +256,192,12913,5,0 +452,192,13256,2,0,B|484:144|452:80|380:104,2,140,8|0|8 +256,192,14285,1,0 +88,64,14799,6,0,B|56:40,10,35 +32,192,15999,1,8 +96,220,16171,1,0 +160,248,16342,1,0 +224,120,16685,1,8 +328,24,17028,6,0,B|352:100,1,70 +412,56,17371,2,0,B|436:136,1,70,8|0 +448,192,17713,2,0,B|520:224,1,70 +472,280,18056,2,0,B|408:252,1,70,8|0 +388,320,18399,2,0,B|360:388,1,70 +300,348,18742,2,0,B|328:280,1,70,8|0 +344,216,19085,1,0 +156,192,19428,1,0 +256,76,19771,5,4 +256,76,20456,1,4 +124,324,21142,1,2 +256,372,21485,1,2 +388,324,21828,1,2 +504,32,22513,6,0,B|424:32,1,70,4|0 +448,104,22856,1,8 +376,104,23028,1,0 +360,32,23199,2,0,B|280:32,1,70 +304,104,23542,1,8 +232,104,23713,1,0 +216,32,23885,2,0,B|144:32,1,70 +160,104,24228,1,8 +88,104,24399,1,0 +72,32,24571,2,8,B|0:32,2,70,2|8|10 +8,352,25256,6,0,B|88:352,1,70 +64,280,25599,1,8 +136,280,25771,1,0 +152,352,25942,2,0,B|232:352,1,70 +208,280,26285,1,8 +280,280,26456,1,0 +296,352,26628,2,0,B|368:352,1,70 +352,280,26971,1,8 +424,280,27142,1,0 +440,352,27313,2,0,B|512:352,2,70,2|0|10 +40,228,27999,6,0,B|40:148,1,70,6|0 +112,196,28342,2,0,B|112:232,2,35,8|0|0 +184,156,28685,2,0,B|184:236,1,70,2|0 +260,188,29028,2,0,B|260:152,2,35,8|0|0 +336,188,29371,2,0,B|376:248,1,70,2|0 +440,216,29713,2,0,B|392:148,1,70,8|0 +460,124,30056,2,0,B|484:160,4,35,10|0|8|0|10 +328,72,30742,6,0,B|288:72,3,35 +256,72,31085,2,0,B|208:72,3,35,8|0|0|0 +184,72,31428,2,0,B|128:72,3,35,0|0|0|0 +112,72,31771,2,0,B|72:72,3,35,8|0|0|0 +40,72,32113,5,8 +40,216,32456,1,0 +184,216,32799,1,8 +184,360,33142,1,0 +304,288,33485,6,0,B|400:184,1,140,0|8 +256,32,34342,1,0 +256,32,34513,1,8 +136,112,34856,2,0,B|136:256,1,140,0|10 +104,320,35371,2,0,B|144:344|224:304|208:240,1,140,0|2 +212,192,35885,1,8 +408,336,36228,6,0,B|488:304|480:224,1,140,0|8 +360,56,37085,1,0 +360,56,37256,1,8 +232,120,37599,2,0,B|184:64|96:72,1,140,0|10 +56,120,38113,2,0,B|16:72|40:16|80:0|104:8,1,140,0|2 +156,4,38628,1,8 +256,100,38971,6,0,B|160:204,1,140,0|8 +256,352,39828,1,0 +256,352,39999,1,8 +376,272,40342,2,0,B|376:128,1,140,0|10 +408,64,40856,2,0,B|368:40|288:80|304:144,1,140,0|2 +300,192,41371,1,8 +104,48,41713,6,0,B|24:80|32:160,1,140,0|8 +152,328,42571,1,0 +152,328,42742,1,8 +256,232,43085,6,0,B|256:184,8,35,0|0|0|0|8|0|0|0|8 +256,92,44113,1,2 +124,140,44456,5,4 +72,92,44628,2,0,B|16:144|80:260|184:192,1,210 +256,92,45485,1,8 +388,140,45828,5,0 +440,92,45999,2,0,B|496:144|432:260|328:192,1,210 +256,92,46856,1,8 +256,232,47199,2,0,B|216:296|296:296|252:368,3,140,0|8|0|8 +392,320,48571,6,0,B|392:176,1,140,0|8 +464,184,49085,2,0,B|448:96|376:88|320:128|324:184,1,210,0|8 +187,170,49942,6,0,B|188:128|140:88|60:96|48:180,1,210,4|8 +120,180,50628,2,0,B|120:320,1,140,0|8 +257,363,51313,2,0,B|216:296|296:296|256:232,3,140,0|8|0|8 +256,92,52685,5,0 +169,196,53028,2,0,B|80:248|16:140|80:84,1,210,8|0 +124,140,53713,1,8 +68,268,54056,6,0,B|56:304,2,35 +156,296,54399,1,8 +444,268,54742,6,0,B|456:304,2,35 +356,296,55085,1,8 +356,96,55428,6,0,B|296:96|256:9|256:9|216:96|152:96,1,280 +160,20,56285,1,0 +92,56,56456,1,8 +84,132,56628,1,0 +156,96,56799,6,0,B|156:155|242:195|242:195|156:236|155:300,1,280 +92,252,57656,1,0 +88,328,57828,1,8 +148,376,57999,1,0 +155,299,58171,6,0,B|215:299|255:386|255:386|295:299|359:299,1,280 +356,376,59028,1,0 +424,336,59199,1,8 +428,260,59371,1,0 +356,298,59542,6,0,B|356:239|270:199|270:199|356:158|357:94,1,280 +424,140,60399,1,0 +428,64,60571,1,8 +360,24,60742,1,0 +284,24,60913,6,0,B|212:24,1,70 +136,24,61256,1,8 +60,24,61428,1,0 +60,36,61513,1,0 +60,48,61599,2,0,B|60:124,1,70 +60,196,61942,1,8 +136,196,62113,1,0 +136,184,62199,1,0 +136,172,62285,6,0,B|136:96,1,70 +212,104,62628,1,8 +212,180,62799,1,0 +212,192,62885,1,0 +212,204,62971,2,0,B|212:292,1,70,8|0 +136,272,63313,2,0,B|60:272,1,70,8|0 +256,192,63656,12,0,65028 +256,324,65713,6,0,B|256:360,2,35,0|0|0 +288,256,66056,1,0 +328,316,66228,1,8 +400,288,66399,6,0,B|448:264|448:204,1,70,6|0 +380,192,66742,1,8 +444,148,66913,2,0,B|420:100|360:100,1,70,2|0 +316,124,67256,2,0,B|292:96|292:96|300:64,1,70,0|10 +224,48,67599,2,0,B|196:72|196:72|164:64,1,70,0|2 +104,112,67942,2,0,B|128:140|128:140|120:172,1,70,0|10 +80,240,68285,2,0,B|108:264|108:264|140:256,1,70,0|2 +200,208,68628,2,0,B|216:128,2,70,0|10|0 +212,284,69142,5,2 +256,356,69313,1,0 +256,356,69485,1,0 +292,280,69656,1,2 +292,280,69828,1,0 +368,308,69999,1,0 +376,304,70085,1,0 +384,300,70171,1,10 +392,296,70256,1,0 +400,292,70342,1,0 +408,288,70428,1,0 +416,284,70513,2,8,B|472:240|444:156,3,140,10|10|10|10 +312,44,71885,5,2 +312,44,72056,1,0 +224,32,72228,1,8 +216,40,72313,1,0 +208,48,72399,1,2 +200,56,72485,1,0 +192,64,72571,1,0 +124,28,72742,1,0 +48,36,72913,2,0,B|60:84|100:104,1,70,10|0 +44,160,73256,6,0,B|44:228,1,70,2|0 +116,200,73599,2,0,B|116:272,1,70,10|0 +188,240,73942,2,0,B|188:312,2,70,2|0|10 +188,164,74456,1,0 +260,196,74628,6,0,B|324:256,1,70,2|0 +361,195,74971,2,0,B|311:243,1,70,8|2 +260,292,75313,2,0,B|324:232,1,70 +360,294,75656,2,0,B|311:244,1,70,10|0 +256,192,75999,12,0,77028 +48,192,77371,6,0,B|48:48,1,140 +152,192,78056,2,0,B|152:336,1,140 +152,192,78742,2,0,B|152:48,1,140 +256,192,79427,2,0,B|256:336,1,140 +256,192,80113,6,0,B|256:32,2,140 +256,304,81142,2,0,B|416:304,2,140 +468,304,82171,1,0 +468,304,82513,1,0 +352,112,82856,6,0,B|408:69,3,70 +448,128,83542,1,0 +324,192,83885,1,0 +160,112,84228,6,0,B|103:69,3,70 +64,128,84913,1,0 +188,192,85256,1,0 +352,272,85599,6,0,B|408:314,3,70 +356,364,86285,1,0 +356,364,86456,1,0 +356,364,86628,1,0 +160,272,86971,6,0,B|128:300,4,35 +256,64,87999,1,12 +104,80,88342,6,0,B|20:200|96:376|336:380|344:128,1,560,4|0 +408,100,89885,1,0 +416,168,90056,1,0 +400,236,90228,1,0 +360,296,90399,6,0,B|300:412|104:424|24:300|16:224,1,420 +60,196,91599,1,0 +24,136,91771,2,0,B|140:60,1,140 +256,192,92285,12,0,93828 +64,168,94513,6,0,B|24:272|168:376|244:280,1,280 +300,300,95371,1,0 +340,244,95542,1,0 +404,272,95713,1,0 +448,216,95885,6,0,B|488:112|344:8|268:104,1,280 +256,228,96913,1,0 +464,336,97256,6,0,B|388:296|388:364|320:324,1,140 +252,328,97771,1,0 +176,328,97942,2,0,B|104:328,7,70,0|0|0|0|0|0|8|0 +28,328,99313,6,0,B|-16:184|72:68|216:64|260:160,1,420,4|8 +484,328,100685,2,0,B|528:184|440:68|296:64|244:168,1,420,0|8 +160,264,102056,6,0,B|160:336|256:385|352:336|352:264,2,280,0|8|0 +48,152,103771,1,8 +256,72,104113,1,0 +464,152,104456,1,8 +352,264,104799,5,0 +272,264,104971,2,0,B|208:264,2,35,0|0|8 +240,184,105313,2,0,B|304:184,2,35 +272,104,105656,2,0,B|208:104,2,35,0|0|8 +240,28,105999,2,0,B|304:28,2,35 +168,64,106342,6,0,B|80:104,1,70,0|8 +56,192,106685,2,0,B|128:192,1,70 +104,291,107028,2,0,B|168:320,1,70,0|8 +256,192,107371,1,0 +344,64,107542,6,0,B|432:104,1,70,8|0 +456,192,107885,2,0,B|384:192,1,70,8|0 +408,291,108228,2,0,B|344:320,1,70,8|0 +256,192,108571,2,0,B|256:160,6,23.3333333333333,0|0|0|0|0|0|4 +256,192,108999,12,8,109942 +168,120,110285,6,0,B|232:56,1,70 +344,120,110628,2,0,B|280:56,1,70,8|0 +207,192,110971,2,0,B|271:128,1,70 +305,192,111313,2,0,B|241:128,1,70,8|0 +216,304,111656,2,0,B|256:247|256:247|296:304,3,140,0|8|0|8 +352,112,113028,5,0 +360,192,113199,2,0,B|368:256,2,35,0|0|8 +424,144,113542,1,0 +352,112,113713,1,0 +408,64,113885,2,0,B|456:48,2,35,0|0|8 +336,40,114228,1,0 +352,112,114399,1,0 +280,88,114571,2,0,B|248:72,2,35,0|0|8 +352,112,114913,1,0 +296,160,115085,1,8 +368,184,115256,1,8 +424,136,115428,1,8 +128,72,115771,6,0,B|88:56|88:88|56:72,1,70 +64,152,116113,2,0,B|104:168|104:136|136:152,1,70,8|0 +136,232,116456,2,0,B|96:216|96:248|64:232,1,70 +72,312,116798,2,0,B|112:328|112:296|144:312,1,70,8|0 +216,312,117142,5,0 +216,192,117313,1,0 +216,72,117485,1,8 +296,296,117828,5,0 +296,176,117999,1,0 +296,56,118171,1,8 +448,64,118513,6,0,B|392:104,1,70 +392,184,118856,1,8 +392,192,118942,1,0 +392,200,119028,1,0 +392,288,119199,2,0,B|392:328,2,35 +464,240,119542,1,8 +464,248,119628,1,0 +464,256,119713,1,0 +464,336,119885,6,2,B|256:360|256:360|48:336,1,420 +256,192,121256,12,0,123999 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/104973-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/104973-expected-conversion.json new file mode 100644 index 0000000000..8a5fa1ab79 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/104973-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":11980.0,"Objects":[{"StartTime":11980.0,"Position":152.0,"HyperDash":false}]},{"StartTime":12313.0,"Objects":[{"StartTime":12313.0,"Position":344.0,"HyperDash":false}]},{"StartTime":12647.0,"Objects":[{"StartTime":12647.0,"Position":132.0,"HyperDash":false},{"StartTime":12730.0,"Position":96.8423157,"HyperDash":false},{"StartTime":12813.0,"Position":80.68463,"HyperDash":false},{"StartTime":12896.0,"Position":68.52695,"HyperDash":false},{"StartTime":12980.0,"Position":51.1263962,"HyperDash":false},{"StartTime":13054.0,"Position":84.0983,"HyperDash":false},{"StartTime":13128.0,"Position":104.070213,"HyperDash":false},{"StartTime":13202.0,"Position":106.04213,"HyperDash":false},{"StartTime":13313.0,"Position":132.0,"HyperDash":false}]},{"StartTime":13646.0,"Objects":[{"StartTime":13646.0,"Position":220.0,"HyperDash":false}]},{"StartTime":13980.0,"Objects":[{"StartTime":13980.0,"Position":240.0,"HyperDash":false},{"StartTime":14063.0,"Position":219.934647,"HyperDash":false},{"StartTime":14146.0,"Position":186.8693,"HyperDash":false},{"StartTime":14229.0,"Position":174.80394,"HyperDash":false},{"StartTime":14313.0,"Position":163.508881,"HyperDash":false},{"StartTime":14387.0,"Position":168.5069,"HyperDash":false},{"StartTime":14461.0,"Position":193.504929,"HyperDash":false},{"StartTime":14535.0,"Position":228.50296,"HyperDash":false},{"StartTime":14646.0,"Position":240.0,"HyperDash":false}]},{"StartTime":14980.0,"Objects":[{"StartTime":14980.0,"Position":316.0,"HyperDash":false}]},{"StartTime":15313.0,"Objects":[{"StartTime":15313.0,"Position":304.0,"HyperDash":false},{"StartTime":15387.0,"Position":327.87616,"HyperDash":false},{"StartTime":15461.0,"Position":334.752319,"HyperDash":false},{"StartTime":15535.0,"Position":368.628479,"HyperDash":false},{"StartTime":15646.0,"Position":393.442719,"HyperDash":false}]},{"StartTime":15980.0,"Objects":[{"StartTime":15980.0,"Position":496.0,"HyperDash":false},{"StartTime":16054.0,"Position":463.669525,"HyperDash":false},{"StartTime":16128.0,"Position":457.4449,"HyperDash":false},{"StartTime":16202.0,"Position":466.1673,"HyperDash":false},{"StartTime":16313.0,"Position":418.33728,"HyperDash":false}]},{"StartTime":16647.0,"Objects":[{"StartTime":16647.0,"Position":296.0,"HyperDash":false},{"StartTime":16730.0,"Position":288.3361,"HyperDash":false},{"StartTime":16813.0,"Position":260.278259,"HyperDash":false},{"StartTime":16896.0,"Position":216.255356,"HyperDash":false},{"StartTime":16980.0,"Position":202.409332,"HyperDash":false},{"StartTime":17063.0,"Position":195.537857,"HyperDash":false},{"StartTime":17146.0,"Position":159.494614,"HyperDash":false},{"StartTime":17229.0,"Position":152.264984,"HyperDash":false},{"StartTime":17313.0,"Position":133.499115,"HyperDash":false},{"StartTime":17396.0,"Position":132.895508,"HyperDash":false},{"StartTime":17480.0,"Position":160.2883,"HyperDash":false},{"StartTime":17563.0,"Position":181.309479,"HyperDash":false},{"StartTime":17647.0,"Position":202.409348,"HyperDash":false},{"StartTime":17721.0,"Position":206.570633,"HyperDash":false},{"StartTime":17795.0,"Position":222.901245,"HyperDash":false},{"StartTime":17869.0,"Position":247.13147,"HyperDash":false},{"StartTime":17980.0,"Position":296.0,"HyperDash":false}]},{"StartTime":18312.0,"Objects":[{"StartTime":18312.0,"Position":296.0,"HyperDash":false}]},{"StartTime":18646.0,"Objects":[{"StartTime":18646.0,"Position":276.0,"HyperDash":false}]},{"StartTime":18980.0,"Objects":[{"StartTime":18980.0,"Position":416.0,"HyperDash":false},{"StartTime":19054.0,"Position":407.972717,"HyperDash":false},{"StartTime":19128.0,"Position":394.945435,"HyperDash":false},{"StartTime":19202.0,"Position":393.918152,"HyperDash":false},{"StartTime":19313.0,"Position":384.377228,"HyperDash":false}]},{"StartTime":19646.0,"Objects":[{"StartTime":19646.0,"Position":160.0,"HyperDash":false}]},{"StartTime":19980.0,"Objects":[{"StartTime":19980.0,"Position":376.0,"HyperDash":false}]},{"StartTime":20313.0,"Objects":[{"StartTime":20313.0,"Position":168.0,"HyperDash":false},{"StartTime":20396.0,"Position":166.842316,"HyperDash":false},{"StartTime":20479.0,"Position":121.684631,"HyperDash":false},{"StartTime":20562.0,"Position":112.526947,"HyperDash":false},{"StartTime":20646.0,"Position":87.1263962,"HyperDash":false},{"StartTime":20720.0,"Position":118.0983,"HyperDash":false},{"StartTime":20794.0,"Position":140.070221,"HyperDash":false},{"StartTime":20868.0,"Position":158.04213,"HyperDash":false},{"StartTime":20979.0,"Position":168.0,"HyperDash":false}]},{"StartTime":21313.0,"Objects":[{"StartTime":21313.0,"Position":232.0,"HyperDash":false},{"StartTime":21396.0,"Position":222.713379,"HyperDash":false},{"StartTime":21479.0,"Position":200.426743,"HyperDash":false},{"StartTime":21562.0,"Position":168.140121,"HyperDash":false},{"StartTime":21646.0,"Position":134.560883,"HyperDash":false},{"StartTime":21720.0,"Position":139.21402,"HyperDash":false},{"StartTime":21794.0,"Position":182.867157,"HyperDash":false},{"StartTime":21868.0,"Position":187.5203,"HyperDash":false},{"StartTime":21979.0,"Position":232.0,"HyperDash":false}]},{"StartTime":22647.0,"Objects":[{"StartTime":22647.0,"Position":453.0,"HyperDash":false}]},{"StartTime":22980.0,"Objects":[{"StartTime":22980.0,"Position":363.0,"HyperDash":false}]},{"StartTime":23313.0,"Objects":[{"StartTime":23313.0,"Position":309.0,"HyperDash":false}]},{"StartTime":23647.0,"Objects":[{"StartTime":23647.0,"Position":448.0,"HyperDash":false}]},{"StartTime":23980.0,"Objects":[{"StartTime":23980.0,"Position":336.0,"HyperDash":false},{"StartTime":24063.0,"Position":321.0,"HyperDash":false},{"StartTime":24146.0,"Position":354.0,"HyperDash":false},{"StartTime":24229.0,"Position":328.0,"HyperDash":false},{"StartTime":24313.0,"Position":336.0,"HyperDash":false},{"StartTime":24387.0,"Position":352.0,"HyperDash":false},{"StartTime":24461.0,"Position":339.0,"HyperDash":false},{"StartTime":24535.0,"Position":326.0,"HyperDash":false},{"StartTime":24646.0,"Position":336.0,"HyperDash":false}]},{"StartTime":24980.0,"Objects":[{"StartTime":24980.0,"Position":176.0,"HyperDash":false}]},{"StartTime":25313.0,"Objects":[{"StartTime":25313.0,"Position":48.0,"HyperDash":false}]},{"StartTime":25647.0,"Objects":[{"StartTime":25647.0,"Position":228.0,"HyperDash":false}]},{"StartTime":25979.0,"Objects":[{"StartTime":25979.0,"Position":36.0,"HyperDash":false}]},{"StartTime":26313.0,"Objects":[{"StartTime":26313.0,"Position":176.0,"HyperDash":false}]},{"StartTime":26646.0,"Objects":[{"StartTime":26646.0,"Position":132.0,"HyperDash":false},{"StartTime":26729.0,"Position":157.86972,"HyperDash":false},{"StartTime":26812.0,"Position":171.739441,"HyperDash":false},{"StartTime":26895.0,"Position":206.609161,"HyperDash":false},{"StartTime":26979.0,"Position":231.7785,"HyperDash":false},{"StartTime":27053.0,"Position":191.605515,"HyperDash":false},{"StartTime":27127.0,"Position":171.43251,"HyperDash":false},{"StartTime":27201.0,"Position":173.2595,"HyperDash":false},{"StartTime":27312.0,"Position":132.0,"HyperDash":false}]},{"StartTime":27647.0,"Objects":[{"StartTime":27647.0,"Position":256.0,"HyperDash":false}]},{"StartTime":27980.0,"Objects":[{"StartTime":27980.0,"Position":404.0,"HyperDash":false},{"StartTime":28054.0,"Position":410.368652,"HyperDash":false},{"StartTime":28128.0,"Position":420.008545,"HyperDash":false},{"StartTime":28202.0,"Position":448.395325,"HyperDash":false},{"StartTime":28313.0,"Position":467.645935,"HyperDash":false}]},{"StartTime":28646.0,"Objects":[{"StartTime":28646.0,"Position":220.0,"HyperDash":false}]},{"StartTime":28980.0,"Objects":[{"StartTime":28980.0,"Position":348.0,"HyperDash":false}]},{"StartTime":29313.0,"Objects":[{"StartTime":29313.0,"Position":336.0,"HyperDash":false},{"StartTime":29387.0,"Position":303.6809,"HyperDash":false},{"StartTime":29461.0,"Position":303.67157,"HyperDash":false},{"StartTime":29535.0,"Position":277.6959,"HyperDash":false},{"StartTime":29646.0,"Position":260.962,"HyperDash":false}]},{"StartTime":29979.0,"Objects":[{"StartTime":29979.0,"Position":360.0,"HyperDash":false}]},{"StartTime":30313.0,"Objects":[{"StartTime":30313.0,"Position":248.0,"HyperDash":false}]},{"StartTime":30646.0,"Objects":[{"StartTime":30646.0,"Position":360.0,"HyperDash":false}]},{"StartTime":31313.0,"Objects":[{"StartTime":31313.0,"Position":24.0,"HyperDash":false}]},{"StartTime":31646.0,"Objects":[{"StartTime":31646.0,"Position":96.0,"HyperDash":false}]},{"StartTime":31979.0,"Objects":[{"StartTime":31979.0,"Position":116.0,"HyperDash":false}]},{"StartTime":32313.0,"Objects":[{"StartTime":32313.0,"Position":168.0,"HyperDash":false}]},{"StartTime":32647.0,"Objects":[{"StartTime":32647.0,"Position":360.0,"HyperDash":false}]},{"StartTime":33313.0,"Objects":[{"StartTime":33313.0,"Position":488.0,"HyperDash":false}]},{"StartTime":33647.0,"Objects":[{"StartTime":33647.0,"Position":488.0,"HyperDash":false},{"StartTime":33721.0,"Position":462.092926,"HyperDash":false},{"StartTime":33795.0,"Position":475.185822,"HyperDash":false},{"StartTime":33869.0,"Position":480.278748,"HyperDash":false},{"StartTime":33980.0,"Position":447.918121,"HyperDash":false}]},{"StartTime":34313.0,"Objects":[{"StartTime":34313.0,"Position":380.0,"HyperDash":false},{"StartTime":34396.0,"Position":378.853241,"HyperDash":false},{"StartTime":34479.0,"Position":357.6393,"HyperDash":false},{"StartTime":34544.0,"Position":376.301575,"HyperDash":false},{"StartTime":34646.0,"Position":380.0,"HyperDash":false}]},{"StartTime":34980.0,"Objects":[{"StartTime":34980.0,"Position":312.0,"HyperDash":false},{"StartTime":35054.0,"Position":293.8341,"HyperDash":false},{"StartTime":35128.0,"Position":274.729065,"HyperDash":false},{"StartTime":35202.0,"Position":262.3615,"HyperDash":false},{"StartTime":35313.0,"Position":217.647247,"HyperDash":false}]},{"StartTime":35646.0,"Objects":[{"StartTime":35646.0,"Position":116.0,"HyperDash":false},{"StartTime":35729.0,"Position":76.07507,"HyperDash":false},{"StartTime":35812.0,"Position":66.0,"HyperDash":false},{"StartTime":35877.0,"Position":82.36937,"HyperDash":false},{"StartTime":35979.0,"Position":116.0,"HyperDash":false}]},{"StartTime":36313.0,"Objects":[{"StartTime":36313.0,"Position":232.0,"HyperDash":false},{"StartTime":36387.0,"Position":244.2069,"HyperDash":false},{"StartTime":36461.0,"Position":281.2214,"HyperDash":false},{"StartTime":36535.0,"Position":306.592651,"HyperDash":false},{"StartTime":36646.0,"Position":327.491272,"HyperDash":false}]},{"StartTime":36813.0,"Objects":[{"StartTime":36813.0,"Position":356.0,"HyperDash":false},{"StartTime":36896.0,"Position":384.924927,"HyperDash":false},{"StartTime":36979.0,"Position":406.0,"HyperDash":false},{"StartTime":37044.0,"Position":376.6306,"HyperDash":false},{"StartTime":37146.0,"Position":356.0,"HyperDash":false}]},{"StartTime":37313.0,"Objects":[{"StartTime":37313.0,"Position":172.0,"HyperDash":false}]},{"StartTime":37646.0,"Objects":[{"StartTime":37646.0,"Position":176.0,"HyperDash":false},{"StartTime":37729.0,"Position":141.075073,"HyperDash":false},{"StartTime":37812.0,"Position":126.0,"HyperDash":false},{"StartTime":37877.0,"Position":152.36937,"HyperDash":false},{"StartTime":37979.0,"Position":176.0,"HyperDash":false}]},{"StartTime":38313.0,"Objects":[{"StartTime":38313.0,"Position":232.0,"HyperDash":false}]},{"StartTime":38647.0,"Objects":[{"StartTime":38647.0,"Position":60.0,"HyperDash":false}]},{"StartTime":38980.0,"Objects":[{"StartTime":38980.0,"Position":276.0,"HyperDash":false}]},{"StartTime":39313.0,"Objects":[{"StartTime":39313.0,"Position":60.0,"HyperDash":false},{"StartTime":39396.0,"Position":79.53542,"HyperDash":false},{"StartTime":39479.0,"Position":80.3983,"HyperDash":false},{"StartTime":39562.0,"Position":95.7953949,"HyperDash":false},{"StartTime":39646.0,"Position":96.9988861,"HyperDash":false},{"StartTime":39711.0,"Position":90.0326843,"HyperDash":false},{"StartTime":39813.0,"Position":130.8265,"HyperDash":false}]},{"StartTime":39980.0,"Objects":[{"StartTime":39980.0,"Position":148.0,"HyperDash":false},{"StartTime":40063.0,"Position":155.921555,"HyperDash":false},{"StartTime":40146.0,"Position":200.495987,"HyperDash":false},{"StartTime":40229.0,"Position":229.243881,"HyperDash":false},{"StartTime":40313.0,"Position":244.105148,"HyperDash":false},{"StartTime":40378.0,"Position":263.884155,"HyperDash":false},{"StartTime":40479.0,"Position":285.356873,"HyperDash":false}]},{"StartTime":40647.0,"Objects":[{"StartTime":40647.0,"Position":274.0,"HyperDash":false}]},{"StartTime":40980.0,"Objects":[{"StartTime":40980.0,"Position":392.0,"HyperDash":false},{"StartTime":41063.0,"Position":414.371643,"HyperDash":false},{"StartTime":41146.0,"Position":440.8901,"HyperDash":false},{"StartTime":41211.0,"Position":438.9507,"HyperDash":false},{"StartTime":41313.0,"Position":392.0,"HyperDash":false}]},{"StartTime":41647.0,"Objects":[{"StartTime":41647.0,"Position":292.0,"HyperDash":false},{"StartTime":41721.0,"Position":255.813812,"HyperDash":false},{"StartTime":41795.0,"Position":263.524658,"HyperDash":false},{"StartTime":41869.0,"Position":212.4873,"HyperDash":false},{"StartTime":41980.0,"Position":199.067825,"HyperDash":false}]},{"StartTime":42147.0,"Objects":[{"StartTime":42147.0,"Position":176.0,"HyperDash":false},{"StartTime":42212.0,"Position":183.0,"HyperDash":false},{"StartTime":42313.0,"Position":176.0,"HyperDash":false}]},{"StartTime":42480.0,"Objects":[{"StartTime":42480.0,"Position":140.0,"HyperDash":false},{"StartTime":42545.0,"Position":131.421692,"HyperDash":false},{"StartTime":42646.0,"Position":90.0,"HyperDash":false}]},{"StartTime":42980.0,"Objects":[{"StartTime":42980.0,"Position":210.0,"HyperDash":false},{"StartTime":43063.0,"Position":225.924927,"HyperDash":false},{"StartTime":43146.0,"Position":260.0,"HyperDash":false},{"StartTime":43211.0,"Position":233.63063,"HyperDash":false},{"StartTime":43313.0,"Position":210.0,"HyperDash":false}]},{"StartTime":43647.0,"Objects":[{"StartTime":43647.0,"Position":248.0,"HyperDash":false}]},{"StartTime":43980.0,"Objects":[{"StartTime":43980.0,"Position":264.0,"HyperDash":false}]},{"StartTime":44313.0,"Objects":[{"StartTime":44313.0,"Position":248.0,"HyperDash":false}]},{"StartTime":44647.0,"Objects":[{"StartTime":44647.0,"Position":344.0,"HyperDash":false},{"StartTime":44721.0,"Position":364.6328,"HyperDash":false},{"StartTime":44795.0,"Position":391.265625,"HyperDash":false},{"StartTime":44869.0,"Position":390.898438,"HyperDash":false},{"StartTime":44980.0,"Position":436.847656,"HyperDash":false}]},{"StartTime":45313.0,"Objects":[{"StartTime":45313.0,"Position":340.0,"HyperDash":false},{"StartTime":45387.0,"Position":332.927124,"HyperDash":false},{"StartTime":45461.0,"Position":320.854218,"HyperDash":false},{"StartTime":45535.0,"Position":286.7813,"HyperDash":false},{"StartTime":45646.0,"Position":272.172,"HyperDash":false}]},{"StartTime":45980.0,"Objects":[{"StartTime":45980.0,"Position":236.0,"HyperDash":false},{"StartTime":46054.0,"Position":231.452988,"HyperDash":false},{"StartTime":46128.0,"Position":230.905975,"HyperDash":false},{"StartTime":46202.0,"Position":205.358963,"HyperDash":false},{"StartTime":46313.0,"Position":197.538452,"HyperDash":false}]},{"StartTime":46647.0,"Objects":[{"StartTime":46647.0,"Position":92.0,"HyperDash":false},{"StartTime":46721.0,"Position":83.9194641,"HyperDash":false},{"StartTime":46795.0,"Position":66.01362,"HyperDash":false},{"StartTime":46869.0,"Position":83.9567261,"HyperDash":false},{"StartTime":46980.0,"Position":93.07765,"HyperDash":false}]},{"StartTime":47313.0,"Objects":[{"StartTime":47313.0,"Position":312.0,"HyperDash":false}]},{"StartTime":47647.0,"Objects":[{"StartTime":47647.0,"Position":324.0,"HyperDash":false},{"StartTime":47730.0,"Position":367.924927,"HyperDash":false},{"StartTime":47813.0,"Position":374.0,"HyperDash":false},{"StartTime":47878.0,"Position":351.6306,"HyperDash":false},{"StartTime":47980.0,"Position":324.0,"HyperDash":false}]},{"StartTime":48313.0,"Objects":[{"StartTime":48313.0,"Position":212.0,"HyperDash":false},{"StartTime":48387.0,"Position":201.990753,"HyperDash":false},{"StartTime":48461.0,"Position":179.8291,"HyperDash":false},{"StartTime":48535.0,"Position":186.227524,"HyperDash":false},{"StartTime":48646.0,"Position":213.404251,"HyperDash":false}]},{"StartTime":48980.0,"Objects":[{"StartTime":48980.0,"Position":428.0,"HyperDash":false}]},{"StartTime":49313.0,"Objects":[{"StartTime":49313.0,"Position":220.0,"HyperDash":false}]},{"StartTime":49647.0,"Objects":[{"StartTime":49647.0,"Position":256.0,"HyperDash":false},{"StartTime":49730.0,"Position":255.0,"HyperDash":false},{"StartTime":49813.0,"Position":256.0,"HyperDash":false},{"StartTime":49878.0,"Position":273.0,"HyperDash":false},{"StartTime":49980.0,"Position":256.0,"HyperDash":false}]},{"StartTime":50313.0,"Objects":[{"StartTime":50313.0,"Position":392.0,"HyperDash":false}]},{"StartTime":50647.0,"Objects":[{"StartTime":50647.0,"Position":256.0,"HyperDash":false}]},{"StartTime":50980.0,"Objects":[{"StartTime":50980.0,"Position":256.0,"HyperDash":false},{"StartTime":51063.0,"Position":268.0,"HyperDash":false},{"StartTime":51146.0,"Position":256.0,"HyperDash":false},{"StartTime":51211.0,"Position":267.0,"HyperDash":false},{"StartTime":51313.0,"Position":256.0,"HyperDash":false}]},{"StartTime":51647.0,"Objects":[{"StartTime":51647.0,"Position":200.0,"HyperDash":false},{"StartTime":51721.0,"Position":220.222229,"HyperDash":false},{"StartTime":51795.0,"Position":261.444458,"HyperDash":false},{"StartTime":51869.0,"Position":257.6667,"HyperDash":false},{"StartTime":51980.0,"Position":300.0,"HyperDash":false}]},{"StartTime":52647.0,"Objects":[{"StartTime":52647.0,"Position":136.0,"HyperDash":false}]},{"StartTime":52980.0,"Objects":[{"StartTime":52980.0,"Position":256.0,"HyperDash":false},{"StartTime":53063.0,"Position":291.9663,"HyperDash":false},{"StartTime":53146.0,"Position":284.932617,"HyperDash":false},{"StartTime":53229.0,"Position":307.898926,"HyperDash":false},{"StartTime":53313.0,"Position":340.117859,"HyperDash":false},{"StartTime":53387.0,"Position":325.425,"HyperDash":false},{"StartTime":53461.0,"Position":290.732147,"HyperDash":false},{"StartTime":53535.0,"Position":300.039276,"HyperDash":false},{"StartTime":53646.0,"Position":256.0,"HyperDash":false}]},{"StartTime":53980.0,"Objects":[{"StartTime":53980.0,"Position":384.0,"HyperDash":false}]},{"StartTime":54313.0,"Objects":[{"StartTime":54313.0,"Position":256.0,"HyperDash":false},{"StartTime":54396.0,"Position":223.033691,"HyperDash":false},{"StartTime":54479.0,"Position":213.067383,"HyperDash":false},{"StartTime":54562.0,"Position":180.101074,"HyperDash":false},{"StartTime":54646.0,"Position":171.882141,"HyperDash":false},{"StartTime":54720.0,"Position":184.575012,"HyperDash":false},{"StartTime":54794.0,"Position":201.267853,"HyperDash":false},{"StartTime":54868.0,"Position":218.960709,"HyperDash":false},{"StartTime":54979.0,"Position":256.0,"HyperDash":false}]},{"StartTime":55313.0,"Objects":[{"StartTime":55313.0,"Position":368.0,"HyperDash":false}]},{"StartTime":55647.0,"Objects":[{"StartTime":55647.0,"Position":256.0,"HyperDash":false},{"StartTime":55730.0,"Position":251.0,"HyperDash":false},{"StartTime":55813.0,"Position":251.0,"HyperDash":false},{"StartTime":55896.0,"Position":270.0,"HyperDash":false},{"StartTime":55980.0,"Position":256.0,"HyperDash":false},{"StartTime":56054.0,"Position":259.0,"HyperDash":false},{"StartTime":56128.0,"Position":244.0,"HyperDash":false},{"StartTime":56202.0,"Position":244.0,"HyperDash":false},{"StartTime":56313.0,"Position":256.0,"HyperDash":false}]},{"StartTime":56647.0,"Objects":[{"StartTime":56647.0,"Position":276.0,"HyperDash":false}]},{"StartTime":57313.0,"Objects":[{"StartTime":57313.0,"Position":488.0,"HyperDash":false}]},{"StartTime":57647.0,"Objects":[{"StartTime":57647.0,"Position":488.0,"HyperDash":false},{"StartTime":57721.0,"Position":481.4433,"HyperDash":false},{"StartTime":57795.0,"Position":483.349152,"HyperDash":false},{"StartTime":57869.0,"Position":454.0119,"HyperDash":false},{"StartTime":57980.0,"Position":458.509216,"HyperDash":false}]},{"StartTime":58313.0,"Objects":[{"StartTime":58313.0,"Position":360.0,"HyperDash":false},{"StartTime":58387.0,"Position":344.2625,"HyperDash":false},{"StartTime":58461.0,"Position":344.958252,"HyperDash":false},{"StartTime":58535.0,"Position":319.941345,"HyperDash":false},{"StartTime":58646.0,"Position":314.506317,"HyperDash":false}]},{"StartTime":58980.0,"Objects":[{"StartTime":58980.0,"Position":428.0,"HyperDash":false}]},{"StartTime":59313.0,"Objects":[{"StartTime":59313.0,"Position":260.0,"HyperDash":false}]},{"StartTime":59647.0,"Objects":[{"StartTime":59647.0,"Position":224.0,"HyperDash":false},{"StartTime":59730.0,"Position":233.954819,"HyperDash":false},{"StartTime":59813.0,"Position":211.873215,"HyperDash":false},{"StartTime":59878.0,"Position":207.570984,"HyperDash":false},{"StartTime":59980.0,"Position":224.0,"HyperDash":false}]},{"StartTime":60313.0,"Objects":[{"StartTime":60313.0,"Position":304.0,"HyperDash":false}]},{"StartTime":60647.0,"Objects":[{"StartTime":60647.0,"Position":208.0,"HyperDash":false}]},{"StartTime":60980.0,"Objects":[{"StartTime":60980.0,"Position":136.0,"HyperDash":false},{"StartTime":61063.0,"Position":100.414207,"HyperDash":false},{"StartTime":61146.0,"Position":86.6803,"HyperDash":false},{"StartTime":61211.0,"Position":91.78613,"HyperDash":false},{"StartTime":61313.0,"Position":136.0,"HyperDash":false}]},{"StartTime":61647.0,"Objects":[{"StartTime":61647.0,"Position":448.0,"HyperDash":false}]},{"StartTime":61980.0,"Objects":[{"StartTime":61980.0,"Position":256.0,"HyperDash":false}]},{"StartTime":62313.0,"Objects":[{"StartTime":62313.0,"Position":420.0,"HyperDash":false}]},{"StartTime":62647.0,"Objects":[{"StartTime":62647.0,"Position":228.0,"HyperDash":false}]},{"StartTime":62980.0,"Objects":[{"StartTime":62980.0,"Position":204.0,"HyperDash":false},{"StartTime":63063.0,"Position":197.227524,"HyperDash":false},{"StartTime":63146.0,"Position":154.305817,"HyperDash":false},{"StartTime":63211.0,"Position":183.556717,"HyperDash":false},{"StartTime":63313.0,"Position":204.0,"HyperDash":false}]},{"StartTime":63647.0,"Objects":[{"StartTime":63647.0,"Position":324.0,"HyperDash":false},{"StartTime":63721.0,"Position":356.66507,"HyperDash":false},{"StartTime":63795.0,"Position":328.949554,"HyperDash":false},{"StartTime":63869.0,"Position":341.904877,"HyperDash":false},{"StartTime":63980.0,"Position":341.121216,"HyperDash":false}]},{"StartTime":64313.0,"Objects":[{"StartTime":64313.0,"Position":180.0,"HyperDash":false}]},{"StartTime":64647.0,"Objects":[{"StartTime":64647.0,"Position":116.0,"HyperDash":false}]},{"StartTime":64980.0,"Objects":[{"StartTime":64980.0,"Position":36.0,"HyperDash":false},{"StartTime":65063.0,"Position":48.422226,"HyperDash":false},{"StartTime":65146.0,"Position":60.8444481,"HyperDash":false},{"StartTime":65229.0,"Position":42.26667,"HyperDash":false},{"StartTime":65313.0,"Position":61.7662659,"HyperDash":false},{"StartTime":65387.0,"Position":42.0404358,"HyperDash":false},{"StartTime":65461.0,"Position":31.3145981,"HyperDash":false},{"StartTime":65535.0,"Position":47.5887566,"HyperDash":false},{"StartTime":65646.0,"Position":36.0,"HyperDash":false}]},{"StartTime":65980.0,"Objects":[{"StartTime":65980.0,"Position":24.0,"HyperDash":false},{"StartTime":66063.0,"Position":33.5504036,"HyperDash":false},{"StartTime":66146.0,"Position":78.42056,"HyperDash":false},{"StartTime":66229.0,"Position":110.084938,"HyperDash":false},{"StartTime":66313.0,"Position":121.840134,"HyperDash":false},{"StartTime":66387.0,"Position":136.73616,"HyperDash":false},{"StartTime":66461.0,"Position":175.859619,"HyperDash":false},{"StartTime":66535.0,"Position":197.00679,"HyperDash":false},{"StartTime":66646.0,"Position":219.1586,"HyperDash":false}]},{"StartTime":66980.0,"Objects":[{"StartTime":66980.0,"Position":340.0,"HyperDash":false},{"StartTime":67054.0,"Position":368.672729,"HyperDash":false},{"StartTime":67128.0,"Position":380.049255,"HyperDash":false},{"StartTime":67202.0,"Position":406.45874,"HyperDash":false},{"StartTime":67313.0,"Position":423.819183,"HyperDash":false}]},{"StartTime":67647.0,"Objects":[{"StartTime":67647.0,"Position":436.0,"HyperDash":false},{"StartTime":67730.0,"Position":414.429535,"HyperDash":false},{"StartTime":67813.0,"Position":404.765259,"HyperDash":false},{"StartTime":67878.0,"Position":409.865173,"HyperDash":false},{"StartTime":67980.0,"Position":436.0,"HyperDash":false}]},{"StartTime":68313.0,"Objects":[{"StartTime":68313.0,"Position":468.0,"HyperDash":false}]},{"StartTime":68646.0,"Objects":[{"StartTime":68646.0,"Position":332.0,"HyperDash":false},{"StartTime":68720.0,"Position":334.127625,"HyperDash":false},{"StartTime":68794.0,"Position":293.255249,"HyperDash":false},{"StartTime":68868.0,"Position":281.382874,"HyperDash":false},{"StartTime":68979.0,"Position":256.074341,"HyperDash":false}]},{"StartTime":69313.0,"Objects":[{"StartTime":69313.0,"Position":272.0,"HyperDash":false},{"StartTime":69387.0,"Position":268.51,"HyperDash":false},{"StartTime":69461.0,"Position":219.019989,"HyperDash":false},{"StartTime":69535.0,"Position":233.529968,"HyperDash":false},{"StartTime":69646.0,"Position":188.794968,"HyperDash":false}]},{"StartTime":69980.0,"Objects":[{"StartTime":69980.0,"Position":208.0,"HyperDash":false},{"StartTime":70054.0,"Position":193.222229,"HyperDash":false},{"StartTime":70128.0,"Position":168.444443,"HyperDash":false},{"StartTime":70202.0,"Position":162.666656,"HyperDash":false},{"StartTime":70313.0,"Position":127.999992,"HyperDash":false}]},{"StartTime":70647.0,"Objects":[{"StartTime":70647.0,"Position":128.0,"HyperDash":false},{"StartTime":70721.0,"Position":108.251968,"HyperDash":false},{"StartTime":70795.0,"Position":105.503937,"HyperDash":false},{"StartTime":70869.0,"Position":59.7558975,"HyperDash":false},{"StartTime":70980.0,"Position":43.63385,"HyperDash":false}]},{"StartTime":71313.0,"Objects":[{"StartTime":71313.0,"Position":20.0,"HyperDash":false}]},{"StartTime":71647.0,"Objects":[{"StartTime":71647.0,"Position":72.0,"HyperDash":false},{"StartTime":71730.0,"Position":42.17414,"HyperDash":false},{"StartTime":71813.0,"Position":44.26499,"HyperDash":false},{"StartTime":71878.0,"Position":48.0091858,"HyperDash":false},{"StartTime":71980.0,"Position":72.0,"HyperDash":false}]},{"StartTime":72313.0,"Objects":[{"StartTime":72313.0,"Position":344.0,"HyperDash":false}]},{"StartTime":72647.0,"Objects":[{"StartTime":72647.0,"Position":256.0,"HyperDash":false}]},{"StartTime":72980.0,"Objects":[{"StartTime":72980.0,"Position":344.0,"HyperDash":false}]},{"StartTime":73313.0,"Objects":[{"StartTime":73313.0,"Position":192.0,"HyperDash":false}]},{"StartTime":73647.0,"Objects":[{"StartTime":73647.0,"Position":72.0,"HyperDash":false},{"StartTime":73730.0,"Position":35.075592,"HyperDash":false},{"StartTime":73813.0,"Position":34.03717,"HyperDash":false},{"StartTime":73878.0,"Position":44.7434921,"HyperDash":false},{"StartTime":73980.0,"Position":72.0,"HyperDash":false}]},{"StartTime":74313.0,"Objects":[{"StartTime":74313.0,"Position":208.0,"HyperDash":false}]},{"StartTime":74647.0,"Objects":[{"StartTime":74647.0,"Position":112.0,"HyperDash":false},{"StartTime":74730.0,"Position":135.84227,"HyperDash":false},{"StartTime":74813.0,"Position":152.1162,"HyperDash":false},{"StartTime":74896.0,"Position":171.061676,"HyperDash":false},{"StartTime":74980.0,"Position":196.921387,"HyperDash":false},{"StartTime":75063.0,"Position":218.520676,"HyperDash":false},{"StartTime":75146.0,"Position":260.403442,"HyperDash":false},{"StartTime":75229.0,"Position":258.21,"HyperDash":false},{"StartTime":75313.0,"Position":295.594574,"HyperDash":false},{"StartTime":75387.0,"Position":303.6625,"HyperDash":false},{"StartTime":75462.0,"Position":337.3289,"HyperDash":false},{"StartTime":75536.0,"Position":361.249237,"HyperDash":false},{"StartTime":75646.0,"Position":374.243744,"HyperDash":false}]},{"StartTime":75980.0,"Objects":[{"StartTime":75980.0,"Position":492.0,"HyperDash":false},{"StartTime":76063.0,"Position":469.9186,"HyperDash":false},{"StartTime":76146.0,"Position":442.890717,"HyperDash":false},{"StartTime":76229.0,"Position":461.403656,"HyperDash":false},{"StartTime":76313.0,"Position":454.9664,"HyperDash":false},{"StartTime":76387.0,"Position":453.878967,"HyperDash":false},{"StartTime":76461.0,"Position":434.64566,"HyperDash":false},{"StartTime":76535.0,"Position":431.048553,"HyperDash":false},{"StartTime":76646.0,"Position":439.4531,"HyperDash":false}]},{"StartTime":76980.0,"Objects":[{"StartTime":76980.0,"Position":320.0,"HyperDash":false},{"StartTime":77054.0,"Position":316.474152,"HyperDash":false},{"StartTime":77128.0,"Position":343.948273,"HyperDash":false},{"StartTime":77202.0,"Position":335.422424,"HyperDash":false},{"StartTime":77313.0,"Position":353.633636,"HyperDash":false}]},{"StartTime":77646.0,"Objects":[{"StartTime":77646.0,"Position":256.0,"HyperDash":false},{"StartTime":77720.0,"Position":272.0,"HyperDash":false},{"StartTime":77794.0,"Position":270.0,"HyperDash":false},{"StartTime":77868.0,"Position":249.0,"HyperDash":false},{"StartTime":77979.0,"Position":256.0,"HyperDash":false}]},{"StartTime":78313.0,"Objects":[{"StartTime":78313.0,"Position":192.0,"HyperDash":false},{"StartTime":78387.0,"Position":165.525864,"HyperDash":false},{"StartTime":78461.0,"Position":159.051712,"HyperDash":false},{"StartTime":78535.0,"Position":183.577576,"HyperDash":false},{"StartTime":78646.0,"Position":158.366364,"HyperDash":false}]},{"StartTime":78980.0,"Objects":[{"StartTime":78980.0,"Position":280.0,"HyperDash":false}]},{"StartTime":79313.0,"Objects":[{"StartTime":79313.0,"Position":320.0,"HyperDash":false},{"StartTime":79396.0,"Position":342.939819,"HyperDash":false},{"StartTime":79479.0,"Position":365.9111,"HyperDash":false},{"StartTime":79562.0,"Position":376.53537,"HyperDash":false},{"StartTime":79646.0,"Position":394.836121,"HyperDash":false},{"StartTime":79711.0,"Position":403.9664,"HyperDash":false},{"StartTime":79813.0,"Position":418.107727,"HyperDash":false}]},{"StartTime":79980.0,"Objects":[{"StartTime":79980.0,"Position":408.0,"HyperDash":false},{"StartTime":80063.0,"Position":393.190674,"HyperDash":false},{"StartTime":80146.0,"Position":340.936066,"HyperDash":false},{"StartTime":80229.0,"Position":331.749939,"HyperDash":false},{"StartTime":80313.0,"Position":313.736053,"HyperDash":false},{"StartTime":80378.0,"Position":308.810822,"HyperDash":false},{"StartTime":80480.0,"Position":274.773529,"HyperDash":false}]},{"StartTime":80647.0,"Objects":[{"StartTime":80647.0,"Position":236.0,"HyperDash":false},{"StartTime":80730.0,"Position":199.526276,"HyperDash":false},{"StartTime":80813.0,"Position":215.925659,"HyperDash":false},{"StartTime":80896.0,"Position":186.386475,"HyperDash":false},{"StartTime":80980.0,"Position":154.006546,"HyperDash":false},{"StartTime":81045.0,"Position":134.148682,"HyperDash":false},{"StartTime":81147.0,"Position":104.824638,"HyperDash":false}]},{"StartTime":81313.0,"Objects":[{"StartTime":81313.0,"Position":88.0,"HyperDash":false},{"StartTime":81396.0,"Position":110.135536,"HyperDash":false},{"StartTime":81479.0,"Position":112.874176,"HyperDash":false},{"StartTime":81562.0,"Position":127.188362,"HyperDash":false},{"StartTime":81646.0,"Position":144.1023,"HyperDash":false},{"StartTime":81711.0,"Position":162.4082,"HyperDash":false},{"StartTime":81813.0,"Position":185.452866,"HyperDash":false}]},{"StartTime":81980.0,"Objects":[{"StartTime":81980.0,"Position":240.0,"HyperDash":false}]},{"StartTime":82313.0,"Objects":[{"StartTime":82313.0,"Position":344.0,"HyperDash":false},{"StartTime":82396.0,"Position":356.924927,"HyperDash":false},{"StartTime":82479.0,"Position":394.0,"HyperDash":false},{"StartTime":82544.0,"Position":367.6306,"HyperDash":false},{"StartTime":82646.0,"Position":344.0,"HyperDash":false}]},{"StartTime":82980.0,"Objects":[{"StartTime":82980.0,"Position":96.0,"HyperDash":false}]},{"StartTime":83313.0,"Objects":[{"StartTime":83313.0,"Position":344.0,"HyperDash":false}]},{"StartTime":83647.0,"Objects":[{"StartTime":83647.0,"Position":436.0,"HyperDash":false}]},{"StartTime":83980.0,"Objects":[{"StartTime":83980.0,"Position":252.0,"HyperDash":false}]},{"StartTime":84313.0,"Objects":[{"StartTime":84313.0,"Position":228.0,"HyperDash":false},{"StartTime":84396.0,"Position":209.0,"HyperDash":false},{"StartTime":84479.0,"Position":228.0,"HyperDash":false},{"StartTime":84544.0,"Position":230.0,"HyperDash":false},{"StartTime":84646.0,"Position":228.0,"HyperDash":false}]},{"StartTime":84980.0,"Objects":[{"StartTime":84980.0,"Position":12.0,"HyperDash":false}]},{"StartTime":85313.0,"Objects":[{"StartTime":85313.0,"Position":228.0,"HyperDash":false}]},{"StartTime":85647.0,"Objects":[{"StartTime":85647.0,"Position":12.0,"HyperDash":false}]},{"StartTime":85980.0,"Objects":[{"StartTime":85980.0,"Position":228.0,"HyperDash":false}]},{"StartTime":86313.0,"Objects":[{"StartTime":86313.0,"Position":220.0,"HyperDash":false}]},{"StartTime":86647.0,"Objects":[{"StartTime":86647.0,"Position":104.0,"HyperDash":false}]},{"StartTime":86980.0,"Objects":[{"StartTime":86980.0,"Position":124.0,"HyperDash":false}]},{"StartTime":87313.0,"Objects":[{"StartTime":87313.0,"Position":104.0,"HyperDash":false},{"StartTime":87396.0,"Position":109.906219,"HyperDash":false},{"StartTime":87479.0,"Position":138.812454,"HyperDash":false},{"StartTime":87562.0,"Position":184.718689,"HyperDash":false},{"StartTime":87646.0,"Position":203.924988,"HyperDash":false},{"StartTime":87729.0,"Position":222.8312,"HyperDash":false},{"StartTime":87812.0,"Position":240.737427,"HyperDash":false},{"StartTime":87895.0,"Position":269.643677,"HyperDash":false},{"StartTime":87979.0,"Position":304.0,"HyperDash":false},{"StartTime":88062.0,"Position":273.2438,"HyperDash":false},{"StartTime":88146.0,"Position":243.0375,"HyperDash":false},{"StartTime":88229.0,"Position":229.131271,"HyperDash":false},{"StartTime":88313.0,"Position":203.924973,"HyperDash":false},{"StartTime":88387.0,"Position":182.719421,"HyperDash":false},{"StartTime":88461.0,"Position":174.513885,"HyperDash":false},{"StartTime":88535.0,"Position":126.308319,"HyperDash":false},{"StartTime":88646.0,"Position":104.0,"HyperDash":false}]},{"StartTime":88980.0,"Objects":[{"StartTime":88980.0,"Position":12.0,"HyperDash":false}]},{"StartTime":89313.0,"Objects":[{"StartTime":89313.0,"Position":196.0,"HyperDash":false}]},{"StartTime":89647.0,"Objects":[{"StartTime":89647.0,"Position":52.0,"HyperDash":false}]},{"StartTime":89980.0,"Objects":[{"StartTime":89980.0,"Position":244.0,"HyperDash":false},{"StartTime":90063.0,"Position":262.898438,"HyperDash":false},{"StartTime":90146.0,"Position":310.591949,"HyperDash":false},{"StartTime":90229.0,"Position":298.8366,"HyperDash":false},{"StartTime":90313.0,"Position":341.672577,"HyperDash":false},{"StartTime":90387.0,"Position":379.917847,"HyperDash":false},{"StartTime":90461.0,"Position":364.344666,"HyperDash":false},{"StartTime":90535.0,"Position":383.885345,"HyperDash":false},{"StartTime":90646.0,"Position":425.976227,"HyperDash":false}]},{"StartTime":90980.0,"Objects":[{"StartTime":90980.0,"Position":388.0,"HyperDash":false}]},{"StartTime":91313.0,"Objects":[{"StartTime":91313.0,"Position":312.0,"HyperDash":false},{"StartTime":91396.0,"Position":299.122223,"HyperDash":false},{"StartTime":91479.0,"Position":274.510773,"HyperDash":false},{"StartTime":91562.0,"Position":253.377548,"HyperDash":false},{"StartTime":91646.0,"Position":214.587158,"HyperDash":false},{"StartTime":91720.0,"Position":180.224533,"HyperDash":false},{"StartTime":91794.0,"Position":170.445953,"HyperDash":false},{"StartTime":91868.0,"Position":168.25264,"HyperDash":false},{"StartTime":91979.0,"Position":127.528435,"HyperDash":false}]},{"StartTime":92313.0,"Objects":[{"StartTime":92313.0,"Position":88.0,"HyperDash":false},{"StartTime":92387.0,"Position":105.606987,"HyperDash":false},{"StartTime":92461.0,"Position":128.524353,"HyperDash":false},{"StartTime":92535.0,"Position":143.583023,"HyperDash":false},{"StartTime":92646.0,"Position":182.5748,"HyperDash":false}]},{"StartTime":92980.0,"Objects":[{"StartTime":92980.0,"Position":292.0,"HyperDash":false},{"StartTime":93063.0,"Position":310.7525,"HyperDash":false},{"StartTime":93146.0,"Position":297.521576,"HyperDash":false},{"StartTime":93211.0,"Position":304.3826,"HyperDash":false},{"StartTime":93313.0,"Position":292.0,"HyperDash":false}]},{"StartTime":93647.0,"Objects":[{"StartTime":93647.0,"Position":260.0,"HyperDash":false}]},{"StartTime":93980.0,"Objects":[{"StartTime":93980.0,"Position":392.0,"HyperDash":false}]},{"StartTime":94313.0,"Objects":[{"StartTime":94313.0,"Position":424.0,"HyperDash":false}]},{"StartTime":94647.0,"Objects":[{"StartTime":94647.0,"Position":216.0,"HyperDash":false}]},{"StartTime":94980.0,"Objects":[{"StartTime":94980.0,"Position":200.0,"HyperDash":false},{"StartTime":95063.0,"Position":195.7525,"HyperDash":false},{"StartTime":95146.0,"Position":205.521576,"HyperDash":false},{"StartTime":95211.0,"Position":219.382584,"HyperDash":false},{"StartTime":95313.0,"Position":200.0,"HyperDash":false}]},{"StartTime":95647.0,"Objects":[{"StartTime":95647.0,"Position":80.0,"HyperDash":false}]},{"StartTime":95980.0,"Objects":[{"StartTime":95980.0,"Position":20.0,"HyperDash":false},{"StartTime":96063.0,"Position":23.3388672,"HyperDash":false},{"StartTime":96146.0,"Position":59.53566,"HyperDash":false},{"StartTime":96229.0,"Position":66.5166855,"HyperDash":false},{"StartTime":96313.0,"Position":108.143875,"HyperDash":false},{"StartTime":96387.0,"Position":118.3307,"HyperDash":false},{"StartTime":96461.0,"Position":144.318436,"HyperDash":false},{"StartTime":96535.0,"Position":175.625229,"HyperDash":false},{"StartTime":96646.0,"Position":203.7997,"HyperDash":false}]},{"StartTime":96980.0,"Objects":[{"StartTime":96980.0,"Position":396.0,"HyperDash":false}]},{"StartTime":97313.0,"Objects":[{"StartTime":97313.0,"Position":416.0,"HyperDash":false},{"StartTime":97396.0,"Position":391.7448,"HyperDash":false},{"StartTime":97479.0,"Position":402.383942,"HyperDash":false},{"StartTime":97562.0,"Position":373.653778,"HyperDash":false},{"StartTime":97646.0,"Position":341.410828,"HyperDash":false},{"StartTime":97720.0,"Position":351.982941,"HyperDash":false},{"StartTime":97794.0,"Position":395.896729,"HyperDash":false},{"StartTime":97868.0,"Position":388.3252,"HyperDash":false},{"StartTime":97979.0,"Position":416.0,"HyperDash":false}]},{"StartTime":98146.0,"Objects":[{"StartTime":98146.0,"Position":127.0,"HyperDash":false},{"StartTime":98224.0,"Position":161.0,"HyperDash":false},{"StartTime":98302.0,"Position":332.0,"HyperDash":false},{"StartTime":98380.0,"Position":356.0,"HyperDash":false},{"StartTime":98458.0,"Position":362.0,"HyperDash":false},{"StartTime":98536.0,"Position":347.0,"HyperDash":false},{"StartTime":98614.0,"Position":252.0,"HyperDash":false},{"StartTime":98692.0,"Position":477.0,"HyperDash":false},{"StartTime":98771.0,"Position":358.0,"HyperDash":false},{"StartTime":98849.0,"Position":17.0,"HyperDash":false},{"StartTime":98927.0,"Position":399.0,"HyperDash":false},{"StartTime":99005.0,"Position":280.0,"HyperDash":false},{"StartTime":99083.0,"Position":304.0,"HyperDash":false},{"StartTime":99161.0,"Position":221.0,"HyperDash":false},{"StartTime":99239.0,"Position":407.0,"HyperDash":false},{"StartTime":99317.0,"Position":287.0,"HyperDash":false},{"StartTime":99396.0,"Position":135.0,"HyperDash":false},{"StartTime":99474.0,"Position":437.0,"HyperDash":false},{"StartTime":99552.0,"Position":289.0,"HyperDash":false},{"StartTime":99630.0,"Position":464.0,"HyperDash":false},{"StartTime":99708.0,"Position":36.0,"HyperDash":false},{"StartTime":99786.0,"Position":378.0,"HyperDash":false},{"StartTime":99864.0,"Position":297.0,"HyperDash":false},{"StartTime":99942.0,"Position":418.0,"HyperDash":false},{"StartTime":100021.0,"Position":329.0,"HyperDash":false},{"StartTime":100099.0,"Position":338.0,"HyperDash":false},{"StartTime":100177.0,"Position":394.0,"HyperDash":false},{"StartTime":100255.0,"Position":40.0,"HyperDash":false},{"StartTime":100333.0,"Position":13.0,"HyperDash":false},{"StartTime":100411.0,"Position":80.0,"HyperDash":false},{"StartTime":100489.0,"Position":138.0,"HyperDash":false},{"StartTime":100567.0,"Position":311.0,"HyperDash":false},{"StartTime":100646.0,"Position":216.0,"HyperDash":false}]},{"StartTime":121313.0,"Objects":[{"StartTime":121313.0,"Position":104.0,"HyperDash":false},{"StartTime":121387.0,"Position":130.222229,"HyperDash":false},{"StartTime":121461.0,"Position":155.444443,"HyperDash":false},{"StartTime":121535.0,"Position":183.666672,"HyperDash":false},{"StartTime":121646.0,"Position":204.0,"HyperDash":false}]},{"StartTime":121980.0,"Objects":[{"StartTime":121980.0,"Position":176.0,"HyperDash":false},{"StartTime":122063.0,"Position":189.658371,"HyperDash":false},{"StartTime":122146.0,"Position":232.316742,"HyperDash":false},{"StartTime":122229.0,"Position":235.975128,"HyperDash":false},{"StartTime":122313.0,"Position":266.9065,"HyperDash":false},{"StartTime":122387.0,"Position":295.1079,"HyperDash":false},{"StartTime":122461.0,"Position":303.3094,"HyperDash":false},{"StartTime":122535.0,"Position":343.5108,"HyperDash":false},{"StartTime":122646.0,"Position":357.813,"HyperDash":false}]},{"StartTime":122980.0,"Objects":[{"StartTime":122980.0,"Position":240.0,"HyperDash":false},{"StartTime":123063.0,"Position":249.293518,"HyperDash":false},{"StartTime":123146.0,"Position":284.721375,"HyperDash":false},{"StartTime":123211.0,"Position":269.396881,"HyperDash":false},{"StartTime":123313.0,"Position":240.0,"HyperDash":false}]},{"StartTime":123647.0,"Objects":[{"StartTime":123647.0,"Position":136.0,"HyperDash":false},{"StartTime":123721.0,"Position":175.807312,"HyperDash":false},{"StartTime":123795.0,"Position":177.614624,"HyperDash":false},{"StartTime":123869.0,"Position":204.421951,"HyperDash":false},{"StartTime":123980.0,"Position":229.632919,"HyperDash":false}]},{"StartTime":124313.0,"Objects":[{"StartTime":124313.0,"Position":348.0,"HyperDash":false},{"StartTime":124387.0,"Position":311.12384,"HyperDash":false},{"StartTime":124461.0,"Position":301.247681,"HyperDash":false},{"StartTime":124535.0,"Position":296.371521,"HyperDash":false},{"StartTime":124646.0,"Position":258.557281,"HyperDash":false}]},{"StartTime":124980.0,"Objects":[{"StartTime":124980.0,"Position":132.0,"HyperDash":false}]},{"StartTime":125313.0,"Objects":[{"StartTime":125313.0,"Position":308.0,"HyperDash":false}]},{"StartTime":125647.0,"Objects":[{"StartTime":125647.0,"Position":192.0,"HyperDash":false}]},{"StartTime":125980.0,"Objects":[{"StartTime":125980.0,"Position":256.0,"HyperDash":false},{"StartTime":126063.0,"Position":236.0,"HyperDash":false},{"StartTime":126146.0,"Position":241.0,"HyperDash":false},{"StartTime":126229.0,"Position":259.0,"HyperDash":false},{"StartTime":126313.0,"Position":256.0,"HyperDash":false},{"StartTime":126387.0,"Position":266.0,"HyperDash":false},{"StartTime":126461.0,"Position":262.0,"HyperDash":false},{"StartTime":126535.0,"Position":251.0,"HyperDash":false},{"StartTime":126646.0,"Position":256.0,"HyperDash":false}]},{"StartTime":126980.0,"Objects":[{"StartTime":126980.0,"Position":456.0,"HyperDash":false}]},{"StartTime":127313.0,"Objects":[{"StartTime":127313.0,"Position":240.0,"HyperDash":false},{"StartTime":127396.0,"Position":206.706223,"HyperDash":false},{"StartTime":127479.0,"Position":204.91954,"HyperDash":false},{"StartTime":127562.0,"Position":169.054108,"HyperDash":false},{"StartTime":127646.0,"Position":141.47023,"HyperDash":false},{"StartTime":127720.0,"Position":125.911591,"HyperDash":false},{"StartTime":127794.0,"Position":94.83778,"HyperDash":false},{"StartTime":127868.0,"Position":101.478622,"HyperDash":false},{"StartTime":127979.0,"Position":61.6785927,"HyperDash":false}]},{"StartTime":128313.0,"Objects":[{"StartTime":128313.0,"Position":24.0,"HyperDash":false},{"StartTime":128387.0,"Position":48.1436577,"HyperDash":false},{"StartTime":128461.0,"Position":55.9805756,"HyperDash":false},{"StartTime":128535.0,"Position":105.202553,"HyperDash":false},{"StartTime":128646.0,"Position":122.252655,"HyperDash":false}]},{"StartTime":128980.0,"Objects":[{"StartTime":128980.0,"Position":240.0,"HyperDash":false},{"StartTime":129063.0,"Position":255.475082,"HyperDash":false},{"StartTime":129146.0,"Position":232.928925,"HyperDash":false},{"StartTime":129211.0,"Position":224.668167,"HyperDash":false},{"StartTime":129313.0,"Position":240.0,"HyperDash":false}]},{"StartTime":129647.0,"Objects":[{"StartTime":129647.0,"Position":208.0,"HyperDash":false},{"StartTime":129721.0,"Position":242.2032,"HyperDash":false},{"StartTime":129795.0,"Position":238.131622,"HyperDash":false},{"StartTime":129869.0,"Position":289.174744,"HyperDash":false},{"StartTime":129980.0,"Position":301.803345,"HyperDash":false}]},{"StartTime":130313.0,"Objects":[{"StartTime":130313.0,"Position":464.0,"HyperDash":false}]},{"StartTime":130647.0,"Objects":[{"StartTime":130647.0,"Position":312.0,"HyperDash":false}]},{"StartTime":130980.0,"Objects":[{"StartTime":130980.0,"Position":360.0,"HyperDash":false}]},{"StartTime":131313.0,"Objects":[{"StartTime":131313.0,"Position":312.0,"HyperDash":false}]},{"StartTime":131980.0,"Objects":[{"StartTime":131980.0,"Position":128.0,"HyperDash":false}]},{"StartTime":132313.0,"Objects":[{"StartTime":132313.0,"Position":108.0,"HyperDash":false}]},{"StartTime":132647.0,"Objects":[{"StartTime":132647.0,"Position":128.0,"HyperDash":false},{"StartTime":132721.0,"Position":135.994476,"HyperDash":false},{"StartTime":132795.0,"Position":180.585373,"HyperDash":false},{"StartTime":132869.0,"Position":207.755859,"HyperDash":false},{"StartTime":132980.0,"Position":224.793228,"HyperDash":false}]},{"StartTime":133147.0,"Objects":[{"StartTime":133147.0,"Position":288.0,"HyperDash":false}]},{"StartTime":133313.0,"Objects":[{"StartTime":133313.0,"Position":272.0,"HyperDash":false},{"StartTime":133387.0,"Position":276.649445,"HyperDash":false},{"StartTime":133461.0,"Position":249.773849,"HyperDash":false},{"StartTime":133535.0,"Position":218.139557,"HyperDash":false},{"StartTime":133646.0,"Position":186.0562,"HyperDash":false}]},{"StartTime":133980.0,"Objects":[{"StartTime":133980.0,"Position":68.0,"HyperDash":false}]},{"StartTime":134313.0,"Objects":[{"StartTime":134313.0,"Position":61.0,"HyperDash":false}]},{"StartTime":134647.0,"Objects":[{"StartTime":134647.0,"Position":88.0,"HyperDash":false},{"StartTime":134721.0,"Position":102.674133,"HyperDash":false},{"StartTime":134795.0,"Position":111.358536,"HyperDash":false},{"StartTime":134869.0,"Position":120.496475,"HyperDash":false},{"StartTime":134980.0,"Position":164.774765,"HyperDash":false}]},{"StartTime":135147.0,"Objects":[{"StartTime":135147.0,"Position":232.0,"HyperDash":false}]},{"StartTime":135313.0,"Objects":[{"StartTime":135313.0,"Position":244.0,"HyperDash":false},{"StartTime":135387.0,"Position":257.8205,"HyperDash":false},{"StartTime":135461.0,"Position":293.698364,"HyperDash":false},{"StartTime":135535.0,"Position":319.993317,"HyperDash":false},{"StartTime":135646.0,"Position":330.966125,"HyperDash":false}]},{"StartTime":135980.0,"Objects":[{"StartTime":135980.0,"Position":400.0,"HyperDash":false},{"StartTime":136054.0,"Position":393.3103,"HyperDash":false},{"StartTime":136128.0,"Position":410.291168,"HyperDash":false},{"StartTime":136202.0,"Position":374.1771,"HyperDash":false},{"StartTime":136313.0,"Position":363.078583,"HyperDash":false}]},{"StartTime":136647.0,"Objects":[{"StartTime":136647.0,"Position":168.0,"HyperDash":false}]},{"StartTime":136980.0,"Objects":[{"StartTime":136980.0,"Position":336.0,"HyperDash":false}]},{"StartTime":137313.0,"Objects":[{"StartTime":137313.0,"Position":240.0,"HyperDash":false},{"StartTime":137387.0,"Position":248.065033,"HyperDash":false},{"StartTime":137461.0,"Position":292.435242,"HyperDash":false},{"StartTime":137535.0,"Position":300.6758,"HyperDash":false},{"StartTime":137646.0,"Position":307.714966,"HyperDash":false}]},{"StartTime":137813.0,"Objects":[{"StartTime":137813.0,"Position":288.0,"HyperDash":false}]},{"StartTime":137980.0,"Objects":[{"StartTime":137980.0,"Position":276.0,"HyperDash":false},{"StartTime":138054.0,"Position":257.487183,"HyperDash":false},{"StartTime":138128.0,"Position":243.974365,"HyperDash":false},{"StartTime":138202.0,"Position":212.461533,"HyperDash":false},{"StartTime":138313.0,"Position":183.692291,"HyperDash":false}]},{"StartTime":138647.0,"Objects":[{"StartTime":138647.0,"Position":144.0,"HyperDash":false},{"StartTime":138721.0,"Position":108.367188,"HyperDash":false},{"StartTime":138795.0,"Position":83.73437,"HyperDash":false},{"StartTime":138869.0,"Position":69.10155,"HyperDash":false},{"StartTime":138980.0,"Position":51.1523361,"HyperDash":false}]},{"StartTime":139313.0,"Objects":[{"StartTime":139313.0,"Position":176.0,"HyperDash":false},{"StartTime":139387.0,"Position":150.773682,"HyperDash":false},{"StartTime":139461.0,"Position":141.547363,"HyperDash":false},{"StartTime":139535.0,"Position":131.321045,"HyperDash":false},{"StartTime":139646.0,"Position":111.981567,"HyperDash":false}]},{"StartTime":139980.0,"Objects":[{"StartTime":139980.0,"Position":252.0,"HyperDash":false},{"StartTime":140054.0,"Position":258.226318,"HyperDash":false},{"StartTime":140128.0,"Position":299.452637,"HyperDash":false},{"StartTime":140202.0,"Position":288.678955,"HyperDash":false},{"StartTime":140313.0,"Position":316.018433,"HyperDash":false}]},{"StartTime":140647.0,"Objects":[{"StartTime":140647.0,"Position":436.0,"HyperDash":false},{"StartTime":140730.0,"Position":419.370178,"HyperDash":false},{"StartTime":140813.0,"Position":421.2818,"HyperDash":false},{"StartTime":140896.0,"Position":393.820648,"HyperDash":false},{"StartTime":140980.0,"Position":367.0077,"HyperDash":false},{"StartTime":141054.0,"Position":362.243469,"HyperDash":false},{"StartTime":141128.0,"Position":320.487732,"HyperDash":false},{"StartTime":141202.0,"Position":303.0496,"HyperDash":false},{"StartTime":141313.0,"Position":272.1492,"HyperDash":false}]},{"StartTime":141647.0,"Objects":[{"StartTime":141647.0,"Position":152.0,"HyperDash":false},{"StartTime":141730.0,"Position":140.075073,"HyperDash":false},{"StartTime":141813.0,"Position":102.0,"HyperDash":false},{"StartTime":141878.0,"Position":106.36937,"HyperDash":false},{"StartTime":141980.0,"Position":152.0,"HyperDash":false}]},{"StartTime":142647.0,"Objects":[{"StartTime":142647.0,"Position":388.0,"HyperDash":false},{"StartTime":142730.0,"Position":394.674561,"HyperDash":false},{"StartTime":142813.0,"Position":424.3491,"HyperDash":false},{"StartTime":142896.0,"Position":448.023621,"HyperDash":false},{"StartTime":142980.0,"Position":466.935242,"HyperDash":false},{"StartTime":143054.0,"Position":446.394073,"HyperDash":false},{"StartTime":143128.0,"Position":426.8529,"HyperDash":false},{"StartTime":143202.0,"Position":417.311737,"HyperDash":false},{"StartTime":143313.0,"Position":388.0,"HyperDash":false}]},{"StartTime":143647.0,"Objects":[{"StartTime":143647.0,"Position":272.0,"HyperDash":false},{"StartTime":143721.0,"Position":277.467682,"HyperDash":false},{"StartTime":143795.0,"Position":265.935364,"HyperDash":false},{"StartTime":143869.0,"Position":247.403046,"HyperDash":false},{"StartTime":143980.0,"Position":251.604568,"HyperDash":false}]},{"StartTime":144313.0,"Objects":[{"StartTime":144313.0,"Position":250.0,"HyperDash":false}]},{"StartTime":144647.0,"Objects":[{"StartTime":144647.0,"Position":130.0,"HyperDash":false},{"StartTime":144730.0,"Position":126.174141,"HyperDash":false},{"StartTime":144813.0,"Position":102.264992,"HyperDash":false},{"StartTime":144878.0,"Position":130.009186,"HyperDash":false},{"StartTime":144980.0,"Position":130.0,"HyperDash":false}]},{"StartTime":145313.0,"Objects":[{"StartTime":145313.0,"Position":302.0,"HyperDash":false}]},{"StartTime":145647.0,"Objects":[{"StartTime":145647.0,"Position":98.0,"HyperDash":false}]},{"StartTime":145980.0,"Objects":[{"StartTime":145980.0,"Position":304.0,"HyperDash":false},{"StartTime":146045.0,"Position":329.9953,"HyperDash":false},{"StartTime":146146.0,"Position":349.957245,"HyperDash":false}]},{"StartTime":146480.0,"Objects":[{"StartTime":146480.0,"Position":400.0,"HyperDash":false},{"StartTime":146545.0,"Position":412.621429,"HyperDash":false},{"StartTime":146646.0,"Position":386.263947,"HyperDash":false}]},{"StartTime":146980.0,"Objects":[{"StartTime":146980.0,"Position":160.0,"HyperDash":false}]},{"StartTime":147313.0,"Objects":[{"StartTime":147313.0,"Position":152.0,"HyperDash":false},{"StartTime":147396.0,"Position":112.075073,"HyperDash":false},{"StartTime":147479.0,"Position":102.0,"HyperDash":false},{"StartTime":147562.0,"Position":121.774773,"HyperDash":false},{"StartTime":147646.0,"Position":152.0,"HyperDash":false},{"StartTime":147729.0,"Position":139.075073,"HyperDash":false},{"StartTime":147813.0,"Position":102.0,"HyperDash":false},{"StartTime":147878.0,"Position":112.669662,"HyperDash":false},{"StartTime":147979.0,"Position":152.0,"HyperDash":false}]},{"StartTime":148313.0,"Objects":[{"StartTime":148313.0,"Position":384.0,"HyperDash":false}]},{"StartTime":148647.0,"Objects":[{"StartTime":148647.0,"Position":360.0,"HyperDash":false},{"StartTime":148730.0,"Position":399.623871,"HyperDash":false},{"StartTime":148813.0,"Position":408.1816,"HyperDash":false},{"StartTime":148896.0,"Position":430.2179,"HyperDash":false},{"StartTime":148980.0,"Position":434.200317,"HyperDash":false},{"StartTime":149045.0,"Position":424.324982,"HyperDash":false},{"StartTime":149146.0,"Position":454.563965,"HyperDash":false}]},{"StartTime":149313.0,"Objects":[{"StartTime":149313.0,"Position":396.0,"HyperDash":false},{"StartTime":149396.0,"Position":387.613281,"HyperDash":false},{"StartTime":149479.0,"Position":406.6472,"HyperDash":false},{"StartTime":149562.0,"Position":410.1058,"HyperDash":false},{"StartTime":149646.0,"Position":424.7098,"HyperDash":false},{"StartTime":149711.0,"Position":445.476379,"HyperDash":false},{"StartTime":149813.0,"Position":427.845062,"HyperDash":false}]},{"StartTime":149980.0,"Objects":[{"StartTime":149980.0,"Position":426.0,"HyperDash":false}]},{"StartTime":150313.0,"Objects":[{"StartTime":150313.0,"Position":316.0,"HyperDash":false},{"StartTime":150396.0,"Position":342.7388,"HyperDash":false},{"StartTime":150479.0,"Position":357.6025,"HyperDash":false},{"StartTime":150544.0,"Position":351.486237,"HyperDash":false},{"StartTime":150646.0,"Position":316.0,"HyperDash":false}]},{"StartTime":150980.0,"Objects":[{"StartTime":150980.0,"Position":436.0,"HyperDash":false},{"StartTime":151054.0,"Position":413.307129,"HyperDash":false},{"StartTime":151128.0,"Position":416.6143,"HyperDash":false},{"StartTime":151202.0,"Position":385.921417,"HyperDash":false},{"StartTime":151313.0,"Position":351.882141,"HyperDash":false}]},{"StartTime":151480.0,"Objects":[{"StartTime":151480.0,"Position":296.0,"HyperDash":false},{"StartTime":151545.0,"Position":293.135956,"HyperDash":false},{"StartTime":151646.0,"Position":247.8241,"HyperDash":false}]},{"StartTime":151813.0,"Objects":[{"StartTime":151813.0,"Position":292.0,"HyperDash":false},{"StartTime":151878.0,"Position":304.3741,"HyperDash":false},{"StartTime":151979.0,"Position":287.847717,"HyperDash":false}]},{"StartTime":152147.0,"Objects":[{"StartTime":152147.0,"Position":248.0,"HyperDash":false},{"StartTime":152212.0,"Position":247.426376,"HyperDash":false},{"StartTime":152313.0,"Position":200.565826,"HyperDash":false}]},{"StartTime":152480.0,"Objects":[{"StartTime":152480.0,"Position":244.0,"HyperDash":false},{"StartTime":152545.0,"Position":240.712448,"HyperDash":false},{"StartTime":152646.0,"Position":238.157944,"HyperDash":false}]},{"StartTime":153313.0,"Objects":[{"StartTime":153313.0,"Position":276.0,"HyperDash":false}]},{"StartTime":153647.0,"Objects":[{"StartTime":153647.0,"Position":236.0,"HyperDash":false}]},{"StartTime":153980.0,"Objects":[{"StartTime":153980.0,"Position":256.0,"HyperDash":false},{"StartTime":154063.0,"Position":218.410385,"HyperDash":false},{"StartTime":154146.0,"Position":217.82077,"HyperDash":false},{"StartTime":154229.0,"Position":187.231171,"HyperDash":false},{"StartTime":154313.0,"Position":169.381439,"HyperDash":false},{"StartTime":154387.0,"Position":156.132874,"HyperDash":false},{"StartTime":154461.0,"Position":124.884308,"HyperDash":false},{"StartTime":154535.0,"Position":111.635742,"HyperDash":false},{"StartTime":154646.0,"Position":82.76289,"HyperDash":false}]},{"StartTime":154980.0,"Objects":[{"StartTime":154980.0,"Position":464.0,"HyperDash":false}]},{"StartTime":155313.0,"Objects":[{"StartTime":155313.0,"Position":140.0,"HyperDash":false},{"StartTime":155396.0,"Position":157.959641,"HyperDash":false},{"StartTime":155479.0,"Position":183.919281,"HyperDash":false},{"StartTime":155562.0,"Position":179.8789,"HyperDash":false},{"StartTime":155646.0,"Position":191.99469,"HyperDash":false},{"StartTime":155720.0,"Position":211.549072,"HyperDash":false},{"StartTime":155794.0,"Position":199.103455,"HyperDash":false},{"StartTime":155868.0,"Position":218.6578,"HyperDash":false},{"StartTime":155979.0,"Position":243.9894,"HyperDash":false}]},{"StartTime":156313.0,"Objects":[{"StartTime":156313.0,"Position":28.0,"HyperDash":false}]},{"StartTime":156647.0,"Objects":[{"StartTime":156647.0,"Position":84.0,"HyperDash":false},{"StartTime":156721.0,"Position":99.0253143,"HyperDash":false},{"StartTime":156795.0,"Position":91.05062,"HyperDash":false},{"StartTime":156869.0,"Position":100.075928,"HyperDash":false},{"StartTime":156980.0,"Position":133.613892,"HyperDash":false}]},{"StartTime":157147.0,"Objects":[{"StartTime":157147.0,"Position":180.0,"HyperDash":false}]},{"StartTime":157313.0,"Objects":[{"StartTime":157313.0,"Position":228.0,"HyperDash":false}]},{"StartTime":157647.0,"Objects":[{"StartTime":157647.0,"Position":324.0,"HyperDash":false},{"StartTime":157721.0,"Position":364.239532,"HyperDash":false},{"StartTime":157795.0,"Position":364.479065,"HyperDash":false},{"StartTime":157869.0,"Position":389.7186,"HyperDash":false},{"StartTime":157980.0,"Position":419.577881,"HyperDash":false}]},{"StartTime":158313.0,"Objects":[{"StartTime":158313.0,"Position":336.0,"HyperDash":false},{"StartTime":158387.0,"Position":312.2865,"HyperDash":false},{"StartTime":158461.0,"Position":300.573029,"HyperDash":false},{"StartTime":158535.0,"Position":297.859528,"HyperDash":false},{"StartTime":158646.0,"Position":265.2893,"HyperDash":false}]},{"StartTime":158980.0,"Objects":[{"StartTime":158980.0,"Position":80.0,"HyperDash":false}]},{"StartTime":159313.0,"Objects":[{"StartTime":159313.0,"Position":248.0,"HyperDash":false}]},{"StartTime":159646.0,"Objects":[{"StartTime":159646.0,"Position":48.0,"HyperDash":false},{"StartTime":159729.0,"Position":51.11805,"HyperDash":false},{"StartTime":159812.0,"Position":32.1886139,"HyperDash":false},{"StartTime":159877.0,"Position":24.3137436,"HyperDash":false},{"StartTime":159979.0,"Position":48.0,"HyperDash":false}]},{"StartTime":160313.0,"Objects":[{"StartTime":160313.0,"Position":200.0,"HyperDash":false}]},{"StartTime":160647.0,"Objects":[{"StartTime":160647.0,"Position":248.0,"HyperDash":false}]},{"StartTime":160980.0,"Objects":[{"StartTime":160980.0,"Position":440.0,"HyperDash":false}]},{"StartTime":161313.0,"Objects":[{"StartTime":161313.0,"Position":392.0,"HyperDash":false}]},{"StartTime":161980.0,"Objects":[{"StartTime":161980.0,"Position":120.0,"HyperDash":false}]},{"StartTime":162313.0,"Objects":[{"StartTime":162313.0,"Position":360.0,"HyperDash":false},{"StartTime":162396.0,"Position":370.924927,"HyperDash":false},{"StartTime":162479.0,"Position":394.849854,"HyperDash":false},{"StartTime":162562.0,"Position":440.77478,"HyperDash":false},{"StartTime":162646.0,"Position":460.0,"HyperDash":false},{"StartTime":162720.0,"Position":455.777771,"HyperDash":false},{"StartTime":162794.0,"Position":421.555542,"HyperDash":false},{"StartTime":162868.0,"Position":408.333344,"HyperDash":false},{"StartTime":162979.0,"Position":360.0,"HyperDash":false}]},{"StartTime":163313.0,"Objects":[{"StartTime":163313.0,"Position":48.0,"HyperDash":false}]},{"StartTime":163646.0,"Objects":[{"StartTime":163646.0,"Position":152.0,"HyperDash":false},{"StartTime":163729.0,"Position":137.075073,"HyperDash":false},{"StartTime":163812.0,"Position":112.150146,"HyperDash":false},{"StartTime":163895.0,"Position":86.22523,"HyperDash":false},{"StartTime":163979.0,"Position":52.0,"HyperDash":false},{"StartTime":164053.0,"Position":75.22222,"HyperDash":false},{"StartTime":164127.0,"Position":93.44444,"HyperDash":false},{"StartTime":164201.0,"Position":131.666656,"HyperDash":false},{"StartTime":164312.0,"Position":152.0,"HyperDash":false}]},{"StartTime":164647.0,"Objects":[{"StartTime":164647.0,"Position":256.0,"HyperDash":false}]},{"StartTime":164980.0,"Objects":[{"StartTime":164980.0,"Position":360.0,"HyperDash":false},{"StartTime":165063.0,"Position":391.924927,"HyperDash":false},{"StartTime":165146.0,"Position":415.849854,"HyperDash":false},{"StartTime":165229.0,"Position":439.77478,"HyperDash":false},{"StartTime":165313.0,"Position":460.0,"HyperDash":false},{"StartTime":165387.0,"Position":421.777771,"HyperDash":false},{"StartTime":165461.0,"Position":412.555542,"HyperDash":false},{"StartTime":165535.0,"Position":400.333344,"HyperDash":false},{"StartTime":165646.0,"Position":360.0,"HyperDash":false}]},{"StartTime":165980.0,"Objects":[{"StartTime":165980.0,"Position":48.0,"HyperDash":false}]},{"StartTime":166646.0,"Objects":[{"StartTime":166646.0,"Position":16.0,"HyperDash":false},{"StartTime":166720.0,"Position":33.9701347,"HyperDash":false},{"StartTime":166794.0,"Position":24.45197,"HyperDash":false},{"StartTime":166868.0,"Position":40.2451935,"HyperDash":false},{"StartTime":166979.0,"Position":44.51446,"HyperDash":false}]},{"StartTime":167313.0,"Objects":[{"StartTime":167313.0,"Position":116.0,"HyperDash":false},{"StartTime":167387.0,"Position":129.7839,"HyperDash":false},{"StartTime":167461.0,"Position":169.077835,"HyperDash":false},{"StartTime":167535.0,"Position":179.400436,"HyperDash":false},{"StartTime":167646.0,"Position":209.385559,"HyperDash":false}]},{"StartTime":167814.0,"Objects":[{"StartTime":167814.0,"Position":276.0,"HyperDash":false}]},{"StartTime":167980.0,"Objects":[{"StartTime":167980.0,"Position":288.0,"HyperDash":false},{"StartTime":168054.0,"Position":297.026276,"HyperDash":false},{"StartTime":168128.0,"Position":311.4158,"HyperDash":false},{"StartTime":168202.0,"Position":352.7142,"HyperDash":false},{"StartTime":168313.0,"Position":379.425873,"HyperDash":false}]},{"StartTime":168480.0,"Objects":[{"StartTime":168480.0,"Position":440.0,"HyperDash":false}]},{"StartTime":168647.0,"Objects":[{"StartTime":168647.0,"Position":428.0,"HyperDash":false},{"StartTime":168721.0,"Position":416.346558,"HyperDash":false},{"StartTime":168795.0,"Position":376.215485,"HyperDash":false},{"StartTime":168869.0,"Position":354.074921,"HyperDash":false},{"StartTime":168980.0,"Position":333.4033,"HyperDash":false}]},{"StartTime":169147.0,"Objects":[{"StartTime":169147.0,"Position":292.0,"HyperDash":false}]},{"StartTime":169313.0,"Objects":[{"StartTime":169313.0,"Position":260.0,"HyperDash":false},{"StartTime":169387.0,"Position":226.354462,"HyperDash":false},{"StartTime":169461.0,"Position":218.650589,"HyperDash":false},{"StartTime":169535.0,"Position":188.49968,"HyperDash":false},{"StartTime":169646.0,"Position":162.278046,"HyperDash":false}]},{"StartTime":169814.0,"Objects":[{"StartTime":169814.0,"Position":108.0,"HyperDash":false}]},{"StartTime":169980.0,"Objects":[{"StartTime":169980.0,"Position":88.0,"HyperDash":false},{"StartTime":170054.0,"Position":102.962883,"HyperDash":false},{"StartTime":170128.0,"Position":119.505386,"HyperDash":false},{"StartTime":170202.0,"Position":134.055634,"HyperDash":false},{"StartTime":170313.0,"Position":155.916748,"HyperDash":false}]},{"StartTime":170480.0,"Objects":[{"StartTime":170480.0,"Position":184.0,"HyperDash":false}]},{"StartTime":170647.0,"Objects":[{"StartTime":170647.0,"Position":232.0,"HyperDash":false},{"StartTime":170721.0,"Position":263.15802,"HyperDash":false},{"StartTime":170795.0,"Position":293.183655,"HyperDash":false},{"StartTime":170869.0,"Position":306.346649,"HyperDash":false},{"StartTime":170980.0,"Position":326.30188,"HyperDash":false}]},{"StartTime":171314.0,"Objects":[{"StartTime":171314.0,"Position":424.0,"HyperDash":false}]},{"StartTime":171647.0,"Objects":[{"StartTime":171647.0,"Position":404.0,"HyperDash":false}]},{"StartTime":171980.0,"Objects":[{"StartTime":171980.0,"Position":424.0,"HyperDash":false},{"StartTime":172054.0,"Position":432.217773,"HyperDash":false},{"StartTime":172128.0,"Position":396.404236,"HyperDash":false},{"StartTime":172202.0,"Position":412.493378,"HyperDash":false},{"StartTime":172313.0,"Position":371.9598,"HyperDash":false}]},{"StartTime":172480.0,"Objects":[{"StartTime":172480.0,"Position":312.0,"HyperDash":false}]},{"StartTime":172646.0,"Objects":[{"StartTime":172646.0,"Position":296.0,"HyperDash":false},{"StartTime":172729.0,"Position":266.640961,"HyperDash":false},{"StartTime":172812.0,"Position":246.785126,"HyperDash":false},{"StartTime":172895.0,"Position":204.6299,"HyperDash":false},{"StartTime":172979.0,"Position":199.5078,"HyperDash":false},{"StartTime":173053.0,"Position":230.801788,"HyperDash":false},{"StartTime":173127.0,"Position":226.161774,"HyperDash":false},{"StartTime":173201.0,"Position":272.241882,"HyperDash":false},{"StartTime":173312.0,"Position":296.0,"HyperDash":false}]},{"StartTime":173647.0,"Objects":[{"StartTime":173647.0,"Position":256.0,"HyperDash":false}]},{"StartTime":173980.0,"Objects":[{"StartTime":173980.0,"Position":164.0,"HyperDash":false},{"StartTime":174063.0,"Position":132.238632,"HyperDash":false},{"StartTime":174146.0,"Position":97.919014,"HyperDash":false},{"StartTime":174229.0,"Position":81.1318741,"HyperDash":false},{"StartTime":174313.0,"Position":74.66674,"HyperDash":false},{"StartTime":174387.0,"Position":104.74202,"HyperDash":false},{"StartTime":174461.0,"Position":110.645523,"HyperDash":false},{"StartTime":174535.0,"Position":122.876343,"HyperDash":false},{"StartTime":174646.0,"Position":164.0,"HyperDash":false}]},{"StartTime":174980.0,"Objects":[{"StartTime":174980.0,"Position":132.0,"HyperDash":false},{"StartTime":175054.0,"Position":123.056931,"HyperDash":false},{"StartTime":175128.0,"Position":102.477112,"HyperDash":false},{"StartTime":175202.0,"Position":92.91614,"HyperDash":false},{"StartTime":175313.0,"Position":105.479126,"HyperDash":false}]},{"StartTime":175646.0,"Objects":[{"StartTime":175646.0,"Position":212.0,"HyperDash":false},{"StartTime":175729.0,"Position":240.889877,"HyperDash":false},{"StartTime":175812.0,"Position":250.558151,"HyperDash":false},{"StartTime":175895.0,"Position":278.151367,"HyperDash":false},{"StartTime":175979.0,"Position":273.195679,"HyperDash":false},{"StartTime":176053.0,"Position":272.262177,"HyperDash":false},{"StartTime":176127.0,"Position":241.994537,"HyperDash":false},{"StartTime":176201.0,"Position":248.795273,"HyperDash":false},{"StartTime":176312.0,"Position":212.0,"HyperDash":false}]},{"StartTime":176647.0,"Objects":[{"StartTime":176647.0,"Position":212.0,"HyperDash":false}]},{"StartTime":177313.0,"Objects":[{"StartTime":177313.0,"Position":8.0,"HyperDash":false},{"StartTime":177387.0,"Position":10.2332268,"HyperDash":false},{"StartTime":177461.0,"Position":20.5555458,"HyperDash":false},{"StartTime":177535.0,"Position":40.06486,"HyperDash":false},{"StartTime":177646.0,"Position":79.97232,"HyperDash":false}]},{"StartTime":177980.0,"Objects":[{"StartTime":177980.0,"Position":200.0,"HyperDash":false},{"StartTime":178063.0,"Position":241.128418,"HyperDash":false},{"StartTime":178146.0,"Position":239.256821,"HyperDash":false},{"StartTime":178229.0,"Position":270.385223,"HyperDash":false},{"StartTime":178313.0,"Position":296.804352,"HyperDash":false},{"StartTime":178378.0,"Position":329.7001,"HyperDash":false},{"StartTime":178479.0,"Position":345.061157,"HyperDash":false}]},{"StartTime":178647.0,"Objects":[{"StartTime":178647.0,"Position":344.0,"HyperDash":false},{"StartTime":178730.0,"Position":319.3755,"HyperDash":false},{"StartTime":178813.0,"Position":279.750977,"HyperDash":false},{"StartTime":178896.0,"Position":284.126465,"HyperDash":false},{"StartTime":178980.0,"Position":245.205261,"HyperDash":false},{"StartTime":179045.0,"Position":217.92099,"HyperDash":false},{"StartTime":179147.0,"Position":195.659546,"HyperDash":false}]},{"StartTime":179313.0,"Objects":[{"StartTime":179313.0,"Position":196.0,"HyperDash":false},{"StartTime":179396.0,"Position":204.644592,"HyperDash":false},{"StartTime":179479.0,"Position":247.289169,"HyperDash":false},{"StartTime":179562.0,"Position":284.933777,"HyperDash":false},{"StartTime":179646.0,"Position":294.875275,"HyperDash":false},{"StartTime":179711.0,"Position":321.175262,"HyperDash":false},{"StartTime":179812.0,"Position":344.164429,"HyperDash":false}]},{"StartTime":179980.0,"Objects":[{"StartTime":179980.0,"Position":344.0,"HyperDash":false},{"StartTime":180063.0,"Position":304.223572,"HyperDash":false},{"StartTime":180146.0,"Position":297.447144,"HyperDash":false},{"StartTime":180229.0,"Position":264.670715,"HyperDash":false},{"StartTime":180313.0,"Position":244.595779,"HyperDash":false},{"StartTime":180378.0,"Position":243.192551,"HyperDash":false},{"StartTime":180480.0,"Position":194.744415,"HyperDash":false}]},{"StartTime":180647.0,"Objects":[{"StartTime":180647.0,"Position":136.0,"HyperDash":false},{"StartTime":180730.0,"Position":111.127846,"HyperDash":false},{"StartTime":180813.0,"Position":94.761,"HyperDash":false},{"StartTime":180896.0,"Position":98.9445953,"HyperDash":false},{"StartTime":180980.0,"Position":71.38005,"HyperDash":false},{"StartTime":181063.0,"Position":69.46596,"HyperDash":false},{"StartTime":181147.0,"Position":63.5731277,"HyperDash":false},{"StartTime":181230.0,"Position":82.42001,"HyperDash":false},{"StartTime":181313.0,"Position":71.28203,"HyperDash":false},{"StartTime":181387.0,"Position":81.29693,"HyperDash":false},{"StartTime":181462.0,"Position":106.020226,"HyperDash":false},{"StartTime":181536.0,"Position":117.600555,"HyperDash":false},{"StartTime":181647.0,"Position":136.0,"HyperDash":false}]},{"StartTime":181980.0,"Objects":[{"StartTime":181980.0,"Position":188.0,"HyperDash":false}]},{"StartTime":182647.0,"Objects":[{"StartTime":182647.0,"Position":168.0,"HyperDash":false}]},{"StartTime":182980.0,"Objects":[{"StartTime":182980.0,"Position":76.0,"HyperDash":false},{"StartTime":183063.0,"Position":66.09038,"HyperDash":false},{"StartTime":183146.0,"Position":30.0427513,"HyperDash":false},{"StartTime":183211.0,"Position":32.84601,"HyperDash":false},{"StartTime":183313.0,"Position":76.0,"HyperDash":false}]},{"StartTime":183647.0,"Objects":[{"StartTime":183647.0,"Position":356.0,"HyperDash":false}]},{"StartTime":183980.0,"Objects":[{"StartTime":183980.0,"Position":300.0,"HyperDash":false},{"StartTime":184063.0,"Position":315.398315,"HyperDash":false},{"StartTime":184146.0,"Position":337.263855,"HyperDash":false},{"StartTime":184229.0,"Position":376.114166,"HyperDash":false},{"StartTime":184313.0,"Position":398.933929,"HyperDash":false},{"StartTime":184387.0,"Position":361.090729,"HyperDash":false},{"StartTime":184461.0,"Position":359.967743,"HyperDash":false},{"StartTime":184535.0,"Position":348.762024,"HyperDash":false},{"StartTime":184646.0,"Position":300.0,"HyperDash":false}]},{"StartTime":184980.0,"Objects":[{"StartTime":184980.0,"Position":256.0,"HyperDash":false},{"StartTime":185063.0,"Position":211.878,"HyperDash":false},{"StartTime":185146.0,"Position":210.733841,"HyperDash":false},{"StartTime":185229.0,"Position":193.655289,"HyperDash":false},{"StartTime":185313.0,"Position":174.843628,"HyperDash":false},{"StartTime":185387.0,"Position":205.589539,"HyperDash":false},{"StartTime":185461.0,"Position":212.06926,"HyperDash":false},{"StartTime":185535.0,"Position":226.121918,"HyperDash":false},{"StartTime":185646.0,"Position":256.0,"HyperDash":false}]},{"StartTime":185980.0,"Objects":[{"StartTime":185980.0,"Position":344.0,"HyperDash":false}]},{"StartTime":186647.0,"Objects":[{"StartTime":186647.0,"Position":168.0,"HyperDash":false}]},{"StartTime":186980.0,"Objects":[{"StartTime":186980.0,"Position":316.0,"HyperDash":false}]},{"StartTime":187313.0,"Objects":[{"StartTime":187313.0,"Position":196.0,"HyperDash":false}]},{"StartTime":187980.0,"Objects":[{"StartTime":187980.0,"Position":408.0,"HyperDash":false}]},{"StartTime":188313.0,"Objects":[{"StartTime":188313.0,"Position":456.0,"HyperDash":false}]},{"StartTime":188647.0,"Objects":[{"StartTime":188647.0,"Position":320.0,"HyperDash":false}]},{"StartTime":188980.0,"Objects":[{"StartTime":188980.0,"Position":224.0,"HyperDash":false},{"StartTime":189063.0,"Position":203.261215,"HyperDash":false},{"StartTime":189146.0,"Position":182.397491,"HyperDash":false},{"StartTime":189211.0,"Position":196.513779,"HyperDash":false},{"StartTime":189313.0,"Position":224.0,"HyperDash":false}]},{"StartTime":189647.0,"Objects":[{"StartTime":189647.0,"Position":120.0,"HyperDash":false},{"StartTime":189730.0,"Position":102.325584,"HyperDash":false},{"StartTime":189813.0,"Position":70.5025253,"HyperDash":false},{"StartTime":189878.0,"Position":77.67722,"HyperDash":false},{"StartTime":189980.0,"Position":120.0,"HyperDash":false}]},{"StartTime":190313.0,"Objects":[{"StartTime":190313.0,"Position":96.0,"HyperDash":false},{"StartTime":190396.0,"Position":67.70647,"HyperDash":false},{"StartTime":190479.0,"Position":51.27864,"HyperDash":false},{"StartTime":190544.0,"Position":85.6031342,"HyperDash":false},{"StartTime":190646.0,"Position":96.0,"HyperDash":false}]},{"StartTime":190980.0,"Objects":[{"StartTime":190980.0,"Position":188.0,"HyperDash":false},{"StartTime":191054.0,"Position":204.489685,"HyperDash":false},{"StartTime":191128.0,"Position":220.740356,"HyperDash":false},{"StartTime":191202.0,"Position":229.801239,"HyperDash":false},{"StartTime":191313.0,"Position":258.899475,"HyperDash":false}]},{"StartTime":191646.0,"Objects":[{"StartTime":191646.0,"Position":320.0,"HyperDash":false},{"StartTime":191729.0,"Position":322.9096,"HyperDash":false},{"StartTime":191812.0,"Position":365.957245,"HyperDash":false},{"StartTime":191877.0,"Position":363.154,"HyperDash":false},{"StartTime":191979.0,"Position":320.0,"HyperDash":false}]},{"StartTime":192313.0,"Objects":[{"StartTime":192313.0,"Position":256.0,"HyperDash":false}]},{"StartTime":192646.0,"Objects":[{"StartTime":192646.0,"Position":376.0,"HyperDash":false}]},{"StartTime":192980.0,"Objects":[{"StartTime":192980.0,"Position":264.0,"HyperDash":false}]},{"StartTime":193313.0,"Objects":[{"StartTime":193313.0,"Position":376.0,"HyperDash":false}]},{"StartTime":193647.0,"Objects":[{"StartTime":193647.0,"Position":404.0,"HyperDash":false},{"StartTime":193730.0,"Position":427.956543,"HyperDash":false},{"StartTime":193813.0,"Position":436.009216,"HyperDash":false},{"StartTime":193878.0,"Position":419.609253,"HyperDash":false},{"StartTime":193980.0,"Position":404.0,"HyperDash":false}]},{"StartTime":194313.0,"Objects":[{"StartTime":194313.0,"Position":404.0,"HyperDash":false},{"StartTime":194387.0,"Position":411.0,"HyperDash":false},{"StartTime":194461.0,"Position":401.0,"HyperDash":false},{"StartTime":194535.0,"Position":423.0,"HyperDash":false},{"StartTime":194646.0,"Position":404.0,"HyperDash":false}]},{"StartTime":194980.0,"Objects":[{"StartTime":194980.0,"Position":344.0,"HyperDash":false},{"StartTime":195054.0,"Position":333.802856,"HyperDash":false},{"StartTime":195128.0,"Position":315.605743,"HyperDash":false},{"StartTime":195202.0,"Position":294.4086,"HyperDash":false},{"StartTime":195313.0,"Position":293.612885,"HyperDash":false}]},{"StartTime":195647.0,"Objects":[{"StartTime":195647.0,"Position":300.0,"HyperDash":false},{"StartTime":195721.0,"Position":295.574554,"HyperDash":false},{"StartTime":195795.0,"Position":256.1491,"HyperDash":false},{"StartTime":195869.0,"Position":239.723663,"HyperDash":false},{"StartTime":195980.0,"Position":208.08551,"HyperDash":false}]},{"StartTime":196313.0,"Objects":[{"StartTime":196313.0,"Position":300.0,"HyperDash":false},{"StartTime":196396.0,"Position":270.293732,"HyperDash":false},{"StartTime":196479.0,"Position":253.587433,"HyperDash":false},{"StartTime":196562.0,"Position":235.881165,"HyperDash":false},{"StartTime":196646.0,"Position":200.877213,"HyperDash":false},{"StartTime":196720.0,"Position":215.90448,"HyperDash":false},{"StartTime":196794.0,"Position":240.931778,"HyperDash":false},{"StartTime":196868.0,"Position":248.959076,"HyperDash":false},{"StartTime":196979.0,"Position":300.0,"HyperDash":false}]},{"StartTime":197313.0,"Objects":[{"StartTime":197313.0,"Position":420.0,"HyperDash":false}]},{"StartTime":197647.0,"Objects":[{"StartTime":197647.0,"Position":400.0,"HyperDash":false}]},{"StartTime":197980.0,"Objects":[{"StartTime":197980.0,"Position":300.0,"HyperDash":false},{"StartTime":198054.0,"Position":293.0362,"HyperDash":false},{"StartTime":198128.0,"Position":249.072357,"HyperDash":false},{"StartTime":198202.0,"Position":251.108551,"HyperDash":false},{"StartTime":198313.0,"Position":201.162827,"HyperDash":false}]},{"StartTime":198647.0,"Objects":[{"StartTime":198647.0,"Position":80.0,"HyperDash":false}]},{"StartTime":198980.0,"Objects":[{"StartTime":198980.0,"Position":60.0,"HyperDash":false}]},{"StartTime":199313.0,"Objects":[{"StartTime":199313.0,"Position":200.0,"HyperDash":false},{"StartTime":199387.0,"Position":224.2925,"HyperDash":false},{"StartTime":199461.0,"Position":246.710052,"HyperDash":false},{"StartTime":199535.0,"Position":263.1878,"HyperDash":false},{"StartTime":199646.0,"Position":270.120148,"HyperDash":false}]},{"StartTime":199813.0,"Objects":[{"StartTime":199813.0,"Position":296.0,"HyperDash":false}]},{"StartTime":199980.0,"Objects":[{"StartTime":199980.0,"Position":272.0,"HyperDash":false}]},{"StartTime":200313.0,"Objects":[{"StartTime":200313.0,"Position":56.0,"HyperDash":false}]},{"StartTime":200647.0,"Objects":[{"StartTime":200647.0,"Position":284.0,"HyperDash":false},{"StartTime":200721.0,"Position":297.376343,"HyperDash":false},{"StartTime":200795.0,"Position":265.3053,"HyperDash":false},{"StartTime":200869.0,"Position":263.664337,"HyperDash":false},{"StartTime":200980.0,"Position":247.205276,"HyperDash":false}]},{"StartTime":201147.0,"Objects":[{"StartTime":201147.0,"Position":196.0,"HyperDash":false}]},{"StartTime":201314.0,"Objects":[{"StartTime":201314.0,"Position":156.0,"HyperDash":false}]},{"StartTime":201647.0,"Objects":[{"StartTime":201647.0,"Position":172.0,"HyperDash":false}]},{"StartTime":201980.0,"Objects":[{"StartTime":201980.0,"Position":176.0,"HyperDash":false},{"StartTime":202054.0,"Position":140.8467,"HyperDash":false},{"StartTime":202128.0,"Position":146.271057,"HyperDash":false},{"StartTime":202202.0,"Position":100.283012,"HyperDash":false},{"StartTime":202313.0,"Position":85.75247,"HyperDash":false}]},{"StartTime":202480.0,"Objects":[{"StartTime":202480.0,"Position":48.0,"HyperDash":false}]},{"StartTime":202647.0,"Objects":[{"StartTime":202647.0,"Position":40.0,"HyperDash":false}]},{"StartTime":202980.0,"Objects":[{"StartTime":202980.0,"Position":164.0,"HyperDash":false}]},{"StartTime":203313.0,"Objects":[{"StartTime":203313.0,"Position":44.0,"HyperDash":false},{"StartTime":203396.0,"Position":54.64748,"HyperDash":false},{"StartTime":203479.0,"Position":34.0083046,"HyperDash":false},{"StartTime":203562.0,"Position":34.9480324,"HyperDash":false},{"StartTime":203646.0,"Position":41.9094124,"HyperDash":false},{"StartTime":203720.0,"Position":47.4630432,"HyperDash":false},{"StartTime":203794.0,"Position":38.06579,"HyperDash":false},{"StartTime":203868.0,"Position":41.2230949,"HyperDash":false},{"StartTime":203979.0,"Position":44.0,"HyperDash":false}]},{"StartTime":204313.0,"Objects":[{"StartTime":204313.0,"Position":152.0,"HyperDash":false},{"StartTime":204396.0,"Position":127.075073,"HyperDash":false},{"StartTime":204479.0,"Position":102.0,"HyperDash":false},{"StartTime":204544.0,"Position":132.36937,"HyperDash":false},{"StartTime":204646.0,"Position":152.0,"HyperDash":false}]},{"StartTime":204980.0,"Objects":[{"StartTime":204980.0,"Position":464.0,"HyperDash":false}]},{"StartTime":205313.0,"Objects":[{"StartTime":205313.0,"Position":272.0,"HyperDash":false},{"StartTime":205396.0,"Position":258.4456,"HyperDash":false},{"StartTime":205479.0,"Position":269.674164,"HyperDash":false},{"StartTime":205562.0,"Position":250.012192,"HyperDash":false},{"StartTime":205646.0,"Position":212.531021,"HyperDash":false},{"StartTime":205720.0,"Position":201.934311,"HyperDash":false},{"StartTime":205794.0,"Position":166.713287,"HyperDash":false},{"StartTime":205868.0,"Position":145.157013,"HyperDash":false},{"StartTime":205979.0,"Position":152.274872,"HyperDash":false}]},{"StartTime":206313.0,"Objects":[{"StartTime":206313.0,"Position":152.0,"HyperDash":false},{"StartTime":206396.0,"Position":157.0,"HyperDash":false},{"StartTime":206479.0,"Position":152.0,"HyperDash":false},{"StartTime":206544.0,"Position":163.0,"HyperDash":false},{"StartTime":206646.0,"Position":152.0,"HyperDash":false}]},{"StartTime":206980.0,"Objects":[{"StartTime":206980.0,"Position":172.0,"HyperDash":false}]},{"StartTime":207313.0,"Objects":[{"StartTime":207313.0,"Position":172.0,"HyperDash":false}]},{"StartTime":207646.0,"Objects":[{"StartTime":207646.0,"Position":152.0,"HyperDash":false},{"StartTime":207729.0,"Position":138.0,"HyperDash":false},{"StartTime":207812.0,"Position":152.0,"HyperDash":false},{"StartTime":207877.0,"Position":143.0,"HyperDash":false},{"StartTime":207979.0,"Position":152.0,"HyperDash":false}]},{"StartTime":208313.0,"Objects":[{"StartTime":208313.0,"Position":248.0,"HyperDash":false},{"StartTime":208387.0,"Position":239.45256,"HyperDash":false},{"StartTime":208461.0,"Position":243.221558,"HyperDash":false},{"StartTime":208535.0,"Position":244.170654,"HyperDash":false},{"StartTime":208646.0,"Position":250.445511,"HyperDash":false}]},{"StartTime":208980.0,"Objects":[{"StartTime":208980.0,"Position":353.0,"HyperDash":false},{"StartTime":209042.0,"Position":358.0,"HyperDash":false},{"StartTime":209105.0,"Position":447.0,"HyperDash":false},{"StartTime":209167.0,"Position":222.0,"HyperDash":false},{"StartTime":209230.0,"Position":382.0,"HyperDash":false},{"StartTime":209292.0,"Position":433.0,"HyperDash":false},{"StartTime":209355.0,"Position":450.0,"HyperDash":false},{"StartTime":209417.0,"Position":326.0,"HyperDash":false},{"StartTime":209480.0,"Position":414.0,"HyperDash":false},{"StartTime":209542.0,"Position":285.0,"HyperDash":false},{"StartTime":209605.0,"Position":336.0,"HyperDash":false},{"StartTime":209667.0,"Position":509.0,"HyperDash":false},{"StartTime":209730.0,"Position":334.0,"HyperDash":false},{"StartTime":209792.0,"Position":72.0,"HyperDash":false},{"StartTime":209855.0,"Position":425.0,"HyperDash":false},{"StartTime":209917.0,"Position":451.0,"HyperDash":false},{"StartTime":209980.0,"Position":220.0,"HyperDash":false}]},{"StartTime":210313.0,"Objects":[{"StartTime":210313.0,"Position":25.0,"HyperDash":false},{"StartTime":210375.0,"Position":77.0,"HyperDash":false},{"StartTime":210438.0,"Position":509.0,"HyperDash":false},{"StartTime":210500.0,"Position":90.0,"HyperDash":false},{"StartTime":210563.0,"Position":118.0,"HyperDash":false},{"StartTime":210625.0,"Position":58.0,"HyperDash":false},{"StartTime":210688.0,"Position":12.0,"HyperDash":false},{"StartTime":210750.0,"Position":215.0,"HyperDash":false},{"StartTime":210813.0,"Position":487.0,"HyperDash":false},{"StartTime":210875.0,"Position":446.0,"HyperDash":false},{"StartTime":210938.0,"Position":491.0,"HyperDash":false},{"StartTime":211000.0,"Position":459.0,"HyperDash":false},{"StartTime":211063.0,"Position":37.0,"HyperDash":false},{"StartTime":211125.0,"Position":291.0,"HyperDash":false},{"StartTime":211188.0,"Position":315.0,"HyperDash":false},{"StartTime":211250.0,"Position":35.0,"HyperDash":false},{"StartTime":211313.0,"Position":208.0,"HyperDash":false}]},{"StartTime":211980.0,"Objects":[{"StartTime":211980.0,"Position":440.0,"HyperDash":false},{"StartTime":212054.0,"Position":437.20932,"HyperDash":false},{"StartTime":212128.0,"Position":384.41864,"HyperDash":false},{"StartTime":212202.0,"Position":361.62793,"HyperDash":false},{"StartTime":212313.0,"Position":341.941925,"HyperDash":false}]},{"StartTime":212647.0,"Objects":[{"StartTime":212647.0,"Position":324.0,"HyperDash":false},{"StartTime":212730.0,"Position":307.11853,"HyperDash":false},{"StartTime":212813.0,"Position":283.23703,"HyperDash":false},{"StartTime":212896.0,"Position":247.35556,"HyperDash":false},{"StartTime":212980.0,"Position":236.210449,"HyperDash":false},{"StartTime":213054.0,"Position":224.70166,"HyperDash":false},{"StartTime":213128.0,"Position":185.192871,"HyperDash":false},{"StartTime":213202.0,"Position":194.684082,"HyperDash":false},{"StartTime":213313.0,"Position":148.420883,"HyperDash":false}]},{"StartTime":213647.0,"Objects":[{"StartTime":213647.0,"Position":12.0,"HyperDash":false}]},{"StartTime":213980.0,"Objects":[{"StartTime":213980.0,"Position":192.0,"HyperDash":false}]},{"StartTime":214313.0,"Objects":[{"StartTime":214313.0,"Position":312.0,"HyperDash":false}]},{"StartTime":214647.0,"Objects":[{"StartTime":214647.0,"Position":424.0,"HyperDash":false}]},{"StartTime":214980.0,"Objects":[{"StartTime":214980.0,"Position":472.0,"HyperDash":false},{"StartTime":215063.0,"Position":469.524933,"HyperDash":false},{"StartTime":215146.0,"Position":479.071075,"HyperDash":false},{"StartTime":215211.0,"Position":494.331818,"HyperDash":false},{"StartTime":215313.0,"Position":472.0,"HyperDash":false}]},{"StartTime":215647.0,"Objects":[{"StartTime":215647.0,"Position":352.0,"HyperDash":false},{"StartTime":215730.0,"Position":363.954834,"HyperDash":false},{"StartTime":215813.0,"Position":339.87323,"HyperDash":false},{"StartTime":215878.0,"Position":351.570984,"HyperDash":false},{"StartTime":215980.0,"Position":352.0,"HyperDash":false}]},{"StartTime":216313.0,"Objects":[{"StartTime":216313.0,"Position":256.0,"HyperDash":false}]},{"StartTime":216647.0,"Objects":[{"StartTime":216647.0,"Position":96.0,"HyperDash":false}]},{"StartTime":216980.0,"Objects":[{"StartTime":216980.0,"Position":208.0,"HyperDash":false}]},{"StartTime":217313.0,"Objects":[{"StartTime":217313.0,"Position":336.0,"HyperDash":false}]},{"StartTime":217647.0,"Objects":[{"StartTime":217647.0,"Position":360.0,"HyperDash":false},{"StartTime":217730.0,"Position":379.256866,"HyperDash":false},{"StartTime":217813.0,"Position":378.569519,"HyperDash":false},{"StartTime":217878.0,"Position":356.375916,"HyperDash":false},{"StartTime":217980.0,"Position":360.0,"HyperDash":false}]},{"StartTime":218313.0,"Objects":[{"StartTime":218313.0,"Position":248.0,"HyperDash":false},{"StartTime":218387.0,"Position":227.656219,"HyperDash":false},{"StartTime":218461.0,"Position":211.892563,"HyperDash":false},{"StartTime":218535.0,"Position":191.882538,"HyperDash":false},{"StartTime":218646.0,"Position":190.6999,"HyperDash":false}]},{"StartTime":218980.0,"Objects":[{"StartTime":218980.0,"Position":232.0,"HyperDash":false}]},{"StartTime":219313.0,"Objects":[{"StartTime":219313.0,"Position":152.0,"HyperDash":false}]},{"StartTime":219647.0,"Objects":[{"StartTime":219647.0,"Position":192.0,"HyperDash":false},{"StartTime":219721.0,"Position":214.85907,"HyperDash":false},{"StartTime":219795.0,"Position":222.038834,"HyperDash":false},{"StartTime":219869.0,"Position":223.900543,"HyperDash":false},{"StartTime":219980.0,"Position":247.507462,"HyperDash":false}]},{"StartTime":220313.0,"Objects":[{"StartTime":220313.0,"Position":344.0,"HyperDash":false},{"StartTime":220396.0,"Position":373.282257,"HyperDash":false},{"StartTime":220479.0,"Position":384.686676,"HyperDash":false},{"StartTime":220544.0,"Position":349.925171,"HyperDash":false},{"StartTime":220646.0,"Position":344.0,"HyperDash":false}]},{"StartTime":220980.0,"Objects":[{"StartTime":220980.0,"Position":320.0,"HyperDash":false},{"StartTime":221054.0,"Position":307.766663,"HyperDash":false},{"StartTime":221128.0,"Position":306.876526,"HyperDash":false},{"StartTime":221202.0,"Position":287.838531,"HyperDash":false},{"StartTime":221313.0,"Position":256.301666,"HyperDash":false}]},{"StartTime":221647.0,"Objects":[{"StartTime":221647.0,"Position":140.0,"HyperDash":false},{"StartTime":221730.0,"Position":123.227524,"HyperDash":false},{"StartTime":221813.0,"Position":90.30582,"HyperDash":false},{"StartTime":221878.0,"Position":121.556717,"HyperDash":false},{"StartTime":221980.0,"Position":140.0,"HyperDash":false}]},{"StartTime":222313.0,"Objects":[{"StartTime":222313.0,"Position":436.0,"HyperDash":false}]},{"StartTime":222647.0,"Objects":[{"StartTime":222647.0,"Position":316.0,"HyperDash":false}]},{"StartTime":222980.0,"Objects":[{"StartTime":222980.0,"Position":428.0,"HyperDash":false}]},{"StartTime":223313.0,"Objects":[{"StartTime":223313.0,"Position":252.0,"HyperDash":false}]},{"StartTime":223646.0,"Objects":[{"StartTime":223646.0,"Position":272.0,"HyperDash":false}]},{"StartTime":223980.0,"Objects":[{"StartTime":223980.0,"Position":380.0,"HyperDash":false}]},{"StartTime":224313.0,"Objects":[{"StartTime":224313.0,"Position":212.0,"HyperDash":false}]},{"StartTime":224647.0,"Objects":[{"StartTime":224647.0,"Position":192.0,"HyperDash":false}]},{"StartTime":224980.0,"Objects":[{"StartTime":224980.0,"Position":232.0,"HyperDash":false}]},{"StartTime":225313.0,"Objects":[{"StartTime":225313.0,"Position":232.0,"HyperDash":false}]},{"StartTime":225647.0,"Objects":[{"StartTime":225647.0,"Position":212.0,"HyperDash":false}]},{"StartTime":225980.0,"Objects":[{"StartTime":225980.0,"Position":212.0,"HyperDash":false},{"StartTime":226054.0,"Position":247.605728,"HyperDash":false},{"StartTime":226128.0,"Position":273.6619,"HyperDash":false},{"StartTime":226202.0,"Position":283.86673,"HyperDash":false},{"StartTime":226313.0,"Position":310.620728,"HyperDash":false}]},{"StartTime":226480.0,"Objects":[{"StartTime":226480.0,"Position":380.0,"HyperDash":false}]},{"StartTime":226647.0,"Objects":[{"StartTime":226647.0,"Position":400.0,"HyperDash":false}]},{"StartTime":226980.0,"Objects":[{"StartTime":226980.0,"Position":180.0,"HyperDash":false}]},{"StartTime":227313.0,"Objects":[{"StartTime":227313.0,"Position":372.0,"HyperDash":false},{"StartTime":227387.0,"Position":339.487122,"HyperDash":false},{"StartTime":227461.0,"Position":345.4503,"HyperDash":false},{"StartTime":227535.0,"Position":299.24823,"HyperDash":false},{"StartTime":227646.0,"Position":273.555176,"HyperDash":false}]},{"StartTime":227813.0,"Objects":[{"StartTime":227813.0,"Position":204.0,"HyperDash":false}]},{"StartTime":227980.0,"Objects":[{"StartTime":227980.0,"Position":212.0,"HyperDash":false}]},{"StartTime":228313.0,"Objects":[{"StartTime":228313.0,"Position":300.0,"HyperDash":false}]},{"StartTime":228647.0,"Objects":[{"StartTime":228647.0,"Position":212.0,"HyperDash":false}]},{"StartTime":228980.0,"Objects":[{"StartTime":228980.0,"Position":60.0,"HyperDash":false}]},{"StartTime":229147.0,"Objects":[{"StartTime":229147.0,"Position":136.0,"HyperDash":false}]},{"StartTime":229313.0,"Objects":[{"StartTime":229313.0,"Position":136.0,"HyperDash":false},{"StartTime":229396.0,"Position":126.907516,"HyperDash":false},{"StartTime":229479.0,"Position":112.738968,"HyperDash":false},{"StartTime":229562.0,"Position":135.404449,"HyperDash":false},{"StartTime":229646.0,"Position":130.813385,"HyperDash":false},{"StartTime":229720.0,"Position":122.399216,"HyperDash":false},{"StartTime":229794.0,"Position":152.142029,"HyperDash":false},{"StartTime":229868.0,"Position":137.941391,"HyperDash":false},{"StartTime":229979.0,"Position":150.917847,"HyperDash":false}]},{"StartTime":230313.0,"Objects":[{"StartTime":230313.0,"Position":352.0,"HyperDash":false}]},{"StartTime":230647.0,"Objects":[{"StartTime":230647.0,"Position":352.0,"HyperDash":false},{"StartTime":230730.0,"Position":366.5288,"HyperDash":false},{"StartTime":230813.0,"Position":373.811279,"HyperDash":false},{"StartTime":230896.0,"Position":365.95578,"HyperDash":false},{"StartTime":230980.0,"Position":365.109131,"HyperDash":false},{"StartTime":231054.0,"Position":343.7144,"HyperDash":false},{"StartTime":231128.0,"Position":374.024841,"HyperDash":false},{"StartTime":231202.0,"Position":338.171265,"HyperDash":false},{"StartTime":231313.0,"Position":349.468353,"HyperDash":false}]},{"StartTime":231647.0,"Objects":[{"StartTime":231647.0,"Position":236.0,"HyperDash":false},{"StartTime":231730.0,"Position":222.198776,"HyperDash":false},{"StartTime":231813.0,"Position":186.248138,"HyperDash":false},{"StartTime":231878.0,"Position":214.5214,"HyperDash":false},{"StartTime":231980.0,"Position":236.0,"HyperDash":false}]},{"StartTime":232313.0,"Objects":[{"StartTime":232313.0,"Position":316.0,"HyperDash":false}]},{"StartTime":232647.0,"Objects":[{"StartTime":232647.0,"Position":156.0,"HyperDash":false}]},{"StartTime":233313.0,"Objects":[{"StartTime":233313.0,"Position":256.0,"HyperDash":false},{"StartTime":233387.0,"Position":231.421722,"HyperDash":false},{"StartTime":233461.0,"Position":222.304459,"HyperDash":false},{"StartTime":233535.0,"Position":195.48584,"HyperDash":false},{"StartTime":233646.0,"Position":174.843628,"HyperDash":false}]},{"StartTime":233980.0,"Objects":[{"StartTime":233980.0,"Position":192.0,"HyperDash":false},{"StartTime":234063.0,"Position":220.6892,"HyperDash":false},{"StartTime":234146.0,"Position":257.786133,"HyperDash":false},{"StartTime":234229.0,"Position":260.765076,"HyperDash":false},{"StartTime":234313.0,"Position":285.29007,"HyperDash":false},{"StartTime":234396.0,"Position":317.35672,"HyperDash":false},{"StartTime":234479.0,"Position":321.969574,"HyperDash":false},{"StartTime":234562.0,"Position":349.117,"HyperDash":false},{"StartTime":234646.0,"Position":347.1605,"HyperDash":false},{"StartTime":234729.0,"Position":345.428131,"HyperDash":false},{"StartTime":234813.0,"Position":305.1539,"HyperDash":false},{"StartTime":234896.0,"Position":317.5711,"HyperDash":false},{"StartTime":234980.0,"Position":285.290039,"HyperDash":false},{"StartTime":235054.0,"Position":254.43042,"HyperDash":false},{"StartTime":235128.0,"Position":258.165863,"HyperDash":false},{"StartTime":235202.0,"Position":239.908249,"HyperDash":false},{"StartTime":235313.0,"Position":192.0,"HyperDash":false}]},{"StartTime":235647.0,"Objects":[{"StartTime":235647.0,"Position":164.0,"HyperDash":false}]},{"StartTime":235980.0,"Objects":[{"StartTime":235980.0,"Position":348.0,"HyperDash":false}]},{"StartTime":236313.0,"Objects":[{"StartTime":236313.0,"Position":256.0,"HyperDash":false},{"StartTime":236396.0,"Position":252.0,"HyperDash":false},{"StartTime":236479.0,"Position":256.0,"HyperDash":false},{"StartTime":236544.0,"Position":263.0,"HyperDash":false},{"StartTime":236646.0,"Position":256.0,"HyperDash":false}]},{"StartTime":236980.0,"Objects":[{"StartTime":236980.0,"Position":256.0,"HyperDash":false},{"StartTime":237063.0,"Position":268.0,"HyperDash":false},{"StartTime":237146.0,"Position":256.0,"HyperDash":false},{"StartTime":237211.0,"Position":262.0,"HyperDash":false},{"StartTime":237313.0,"Position":256.0,"HyperDash":false}]},{"StartTime":237647.0,"Objects":[{"StartTime":237647.0,"Position":276.0,"HyperDash":false}]},{"StartTime":237980.0,"Objects":[{"StartTime":237980.0,"Position":276.0,"HyperDash":false}]},{"StartTime":238313.0,"Objects":[{"StartTime":238313.0,"Position":344.0,"HyperDash":false},{"StartTime":238387.0,"Position":349.4431,"HyperDash":false},{"StartTime":238461.0,"Position":367.88623,"HyperDash":false},{"StartTime":238535.0,"Position":402.329346,"HyperDash":false},{"StartTime":238646.0,"Position":417.994019,"HyperDash":false}]},{"StartTime":238980.0,"Objects":[{"StartTime":238980.0,"Position":224.0,"HyperDash":false}]},{"StartTime":239147.0,"Objects":[{"StartTime":239147.0,"Position":328.0,"HyperDash":false}]},{"StartTime":239313.0,"Objects":[{"StartTime":239313.0,"Position":328.0,"HyperDash":false},{"StartTime":239387.0,"Position":303.777771,"HyperDash":false},{"StartTime":239461.0,"Position":283.555542,"HyperDash":false},{"StartTime":239535.0,"Position":243.333313,"HyperDash":false},{"StartTime":239646.0,"Position":228.0,"HyperDash":false}]},{"StartTime":239980.0,"Objects":[{"StartTime":239980.0,"Position":288.0,"HyperDash":false},{"StartTime":240054.0,"Position":273.789337,"HyperDash":false},{"StartTime":240128.0,"Position":255.578659,"HyperDash":false},{"StartTime":240202.0,"Position":211.368,"HyperDash":false},{"StartTime":240313.0,"Position":192.552,"HyperDash":false}]},{"StartTime":240647.0,"Objects":[{"StartTime":240647.0,"Position":72.0,"HyperDash":false}]},{"StartTime":240980.0,"Objects":[{"StartTime":240980.0,"Position":92.0,"HyperDash":false}]},{"StartTime":241313.0,"Objects":[{"StartTime":241313.0,"Position":92.0,"HyperDash":false}]},{"StartTime":241647.0,"Objects":[{"StartTime":241647.0,"Position":52.0,"HyperDash":false}]},{"StartTime":241980.0,"Objects":[{"StartTime":241980.0,"Position":152.0,"HyperDash":false},{"StartTime":242063.0,"Position":152.083969,"HyperDash":false},{"StartTime":242146.0,"Position":194.167923,"HyperDash":false},{"StartTime":242229.0,"Position":202.251892,"HyperDash":false},{"StartTime":242313.0,"Position":216.594238,"HyperDash":false},{"StartTime":242396.0,"Position":191.57486,"HyperDash":false},{"StartTime":242479.0,"Position":179.4909,"HyperDash":false},{"StartTime":242562.0,"Position":169.406937,"HyperDash":false},{"StartTime":242646.0,"Position":152.0,"HyperDash":false},{"StartTime":242720.0,"Position":158.210739,"HyperDash":false},{"StartTime":242795.0,"Position":179.744431,"HyperDash":false},{"StartTime":242869.0,"Position":185.084351,"HyperDash":false},{"StartTime":242980.0,"Position":216.594238,"HyperDash":false}]},{"StartTime":243313.0,"Objects":[{"StartTime":243313.0,"Position":216.0,"HyperDash":false}]},{"StartTime":243980.0,"Objects":[{"StartTime":243980.0,"Position":444.0,"HyperDash":false}]},{"StartTime":244313.0,"Objects":[{"StartTime":244313.0,"Position":292.0,"HyperDash":false}]},{"StartTime":244647.0,"Objects":[{"StartTime":244647.0,"Position":204.0,"HyperDash":false}]},{"StartTime":244980.0,"Objects":[{"StartTime":244980.0,"Position":52.0,"HyperDash":false}]},{"StartTime":245147.0,"Objects":[{"StartTime":245147.0,"Position":128.0,"HyperDash":false}]},{"StartTime":245313.0,"Objects":[{"StartTime":245313.0,"Position":128.0,"HyperDash":false},{"StartTime":245387.0,"Position":95.02887,"HyperDash":false},{"StartTime":245461.0,"Position":102.54911,"HyperDash":false},{"StartTime":245535.0,"Position":83.8343353,"HyperDash":false},{"StartTime":245646.0,"Position":76.92937,"HyperDash":false}]},{"StartTime":245980.0,"Objects":[{"StartTime":245980.0,"Position":52.0,"HyperDash":false}]},{"StartTime":246313.0,"Objects":[{"StartTime":246313.0,"Position":312.0,"HyperDash":false}]},{"StartTime":246480.0,"Objects":[{"StartTime":246480.0,"Position":192.0,"HyperDash":false}]},{"StartTime":246647.0,"Objects":[{"StartTime":246647.0,"Position":192.0,"HyperDash":false},{"StartTime":246730.0,"Position":188.38472,"HyperDash":false},{"StartTime":246813.0,"Position":225.710083,"HyperDash":false},{"StartTime":246896.0,"Position":227.818253,"HyperDash":false},{"StartTime":246980.0,"Position":260.7363,"HyperDash":false},{"StartTime":247054.0,"Position":259.404358,"HyperDash":false},{"StartTime":247128.0,"Position":316.934875,"HyperDash":false},{"StartTime":247202.0,"Position":301.161316,"HyperDash":false},{"StartTime":247313.0,"Position":350.4887,"HyperDash":false}]},{"StartTime":247646.0,"Objects":[{"StartTime":247646.0,"Position":436.0,"HyperDash":false}]},{"StartTime":247813.0,"Objects":[{"StartTime":247813.0,"Position":368.0,"HyperDash":false}]},{"StartTime":247980.0,"Objects":[{"StartTime":247980.0,"Position":402.0,"HyperDash":false},{"StartTime":248054.0,"Position":427.9642,"HyperDash":false},{"StartTime":248128.0,"Position":455.292267,"HyperDash":false},{"StartTime":248202.0,"Position":467.624146,"HyperDash":false},{"StartTime":248313.0,"Position":467.800751,"HyperDash":false}]},{"StartTime":248647.0,"Objects":[{"StartTime":248647.0,"Position":230.0,"HyperDash":false}]},{"StartTime":248980.0,"Objects":[{"StartTime":248980.0,"Position":467.0,"HyperDash":false},{"StartTime":249054.0,"Position":448.114563,"HyperDash":false},{"StartTime":249128.0,"Position":449.648,"HyperDash":false},{"StartTime":249202.0,"Position":452.133575,"HyperDash":false},{"StartTime":249313.0,"Position":426.641052,"HyperDash":false}]},{"StartTime":249647.0,"Objects":[{"StartTime":249647.0,"Position":205.0,"HyperDash":false}]},{"StartTime":249813.0,"Objects":[{"StartTime":249813.0,"Position":307.0,"HyperDash":false}]},{"StartTime":249980.0,"Objects":[{"StartTime":249980.0,"Position":200.0,"HyperDash":false}]},{"StartTime":250313.0,"Objects":[{"StartTime":250313.0,"Position":360.0,"HyperDash":false}]},{"StartTime":250647.0,"Objects":[{"StartTime":250647.0,"Position":200.0,"HyperDash":false}]},{"StartTime":250980.0,"Objects":[{"StartTime":250980.0,"Position":320.0,"HyperDash":false}]},{"StartTime":251313.0,"Objects":[{"StartTime":251313.0,"Position":240.0,"HyperDash":false}]},{"StartTime":251647.0,"Objects":[{"StartTime":251647.0,"Position":152.0,"HyperDash":false}]},{"StartTime":251980.0,"Objects":[{"StartTime":251980.0,"Position":280.0,"HyperDash":false}]},{"StartTime":252647.0,"Objects":[{"StartTime":252647.0,"Position":232.0,"HyperDash":false}]},{"StartTime":253313.0,"Objects":[{"StartTime":253313.0,"Position":280.0,"HyperDash":false}]},{"StartTime":253980.0,"Objects":[{"StartTime":253980.0,"Position":120.0,"HyperDash":false}]},{"StartTime":254646.0,"Objects":[{"StartTime":254646.0,"Position":392.0,"HyperDash":false}]},{"StartTime":255313.0,"Objects":[{"StartTime":255313.0,"Position":120.0,"HyperDash":false}]},{"StartTime":255647.0,"Objects":[{"StartTime":255647.0,"Position":256.0,"HyperDash":false}]},{"StartTime":255813.0,"Objects":[{"StartTime":255813.0,"Position":236.0,"HyperDash":false}]},{"StartTime":255980.0,"Objects":[{"StartTime":255980.0,"Position":276.0,"HyperDash":false}]},{"StartTime":256146.0,"Objects":[{"StartTime":256146.0,"Position":496.0,"HyperDash":false},{"StartTime":256216.0,"Position":27.0,"HyperDash":false},{"StartTime":256286.0,"Position":477.0,"HyperDash":false},{"StartTime":256356.0,"Position":163.0,"HyperDash":false},{"StartTime":256427.0,"Position":260.0,"HyperDash":false},{"StartTime":256497.0,"Position":253.0,"HyperDash":false},{"StartTime":256567.0,"Position":423.0,"HyperDash":false},{"StartTime":256638.0,"Position":367.0,"HyperDash":false},{"StartTime":256708.0,"Position":146.0,"HyperDash":false},{"StartTime":256778.0,"Position":322.0,"HyperDash":false},{"StartTime":256849.0,"Position":169.0,"HyperDash":false},{"StartTime":256919.0,"Position":159.0,"HyperDash":false},{"StartTime":256989.0,"Position":388.0,"HyperDash":false},{"StartTime":257060.0,"Position":67.0,"HyperDash":false},{"StartTime":257130.0,"Position":176.0,"HyperDash":false},{"StartTime":257200.0,"Position":371.0,"HyperDash":false},{"StartTime":257271.0,"Position":365.0,"HyperDash":false},{"StartTime":257341.0,"Position":104.0,"HyperDash":false},{"StartTime":257411.0,"Position":363.0,"HyperDash":false},{"StartTime":257481.0,"Position":75.0,"HyperDash":false},{"StartTime":257552.0,"Position":158.0,"HyperDash":false},{"StartTime":257622.0,"Position":98.0,"HyperDash":false},{"StartTime":257692.0,"Position":30.0,"HyperDash":false},{"StartTime":257763.0,"Position":164.0,"HyperDash":false},{"StartTime":257833.0,"Position":341.0,"HyperDash":false},{"StartTime":257903.0,"Position":18.0,"HyperDash":false},{"StartTime":257974.0,"Position":210.0,"HyperDash":false},{"StartTime":258044.0,"Position":420.0,"HyperDash":false},{"StartTime":258114.0,"Position":447.0,"HyperDash":false},{"StartTime":258185.0,"Position":78.0,"HyperDash":false},{"StartTime":258255.0,"Position":177.0,"HyperDash":false},{"StartTime":258325.0,"Position":305.0,"HyperDash":false},{"StartTime":258396.0,"Position":400.0,"HyperDash":false},{"StartTime":258466.0,"Position":462.0,"HyperDash":false},{"StartTime":258536.0,"Position":64.0,"HyperDash":false},{"StartTime":258606.0,"Position":458.0,"HyperDash":false},{"StartTime":258677.0,"Position":380.0,"HyperDash":false},{"StartTime":258747.0,"Position":65.0,"HyperDash":false},{"StartTime":258817.0,"Position":23.0,"HyperDash":false},{"StartTime":258888.0,"Position":379.0,"HyperDash":false},{"StartTime":258958.0,"Position":44.0,"HyperDash":false},{"StartTime":259028.0,"Position":485.0,"HyperDash":false},{"StartTime":259099.0,"Position":269.0,"HyperDash":false},{"StartTime":259169.0,"Position":155.0,"HyperDash":false},{"StartTime":259239.0,"Position":324.0,"HyperDash":false},{"StartTime":259310.0,"Position":149.0,"HyperDash":false},{"StartTime":259380.0,"Position":351.0,"HyperDash":false},{"StartTime":259450.0,"Position":385.0,"HyperDash":false},{"StartTime":259521.0,"Position":338.0,"HyperDash":false},{"StartTime":259591.0,"Position":322.0,"HyperDash":false},{"StartTime":259661.0,"Position":84.0,"HyperDash":false},{"StartTime":259731.0,"Position":342.0,"HyperDash":false},{"StartTime":259802.0,"Position":395.0,"HyperDash":false},{"StartTime":259872.0,"Position":72.0,"HyperDash":false},{"StartTime":259942.0,"Position":324.0,"HyperDash":false},{"StartTime":260013.0,"Position":67.0,"HyperDash":false},{"StartTime":260083.0,"Position":371.0,"HyperDash":false},{"StartTime":260153.0,"Position":446.0,"HyperDash":false},{"StartTime":260224.0,"Position":29.0,"HyperDash":false},{"StartTime":260294.0,"Position":22.0,"HyperDash":false},{"StartTime":260364.0,"Position":432.0,"HyperDash":false},{"StartTime":260435.0,"Position":12.0,"HyperDash":false},{"StartTime":260505.0,"Position":330.0,"HyperDash":false},{"StartTime":260575.0,"Position":419.0,"HyperDash":false},{"StartTime":260646.0,"Position":278.0,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/104973.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/104973.osu new file mode 100644 index 0000000000..6edd8229a2 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/104973.osu @@ -0,0 +1,491 @@ +osu file format v9 + +[General] +StackLeniency: 0.7 +Mode: 0 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:6 +ApproachRate:6 +SliderMultiplier:2 +SliderTickRate:2 + +[Events] +//Background and Video events +//Break Periods +2,100846,120263 +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples +//Background Colour Transformations +3,100,163,162,255 + +[TimingPoints] +1980,666.666666666667,4,2,2,20,1,0 +12647,-100,4,2,2,42,0,0 +39646,-100,4,2,1,22,0,0 +39813,-100,4,2,2,42,0,0 +40313,-100,4,2,1,22,0,0 +40480,-100,4,2,2,42,0,0 +57980,-100,4,2,2,47,0,1 +75313,-100,4,2,1,22,0,1 +75646,-100,4,2,2,47,0,1 +79646,-100,4,2,1,22,0,1 +79813,-100,4,2,2,47,0,1 +80313,-100,4,2,1,22,0,1 +80480,-100,4,2,2,47,0,1 +80980,-100,4,2,1,22,0,1 +81146,-100,4,2,2,47,0,1 +81646,-100,4,2,1,22,0,1 +81813,-100,4,2,2,47,0,1 +100646,-100,4,2,2,42,0,0 +148980,-100,4,2,1,22,0,0 +149146,-100,4,2,2,42,0,0 +149646,-100,4,2,1,22,0,0 +149813,-100,4,2,2,42,0,0 +167313,-100,4,2,2,47,0,1 +178313,-100,4,2,1,22,0,1 +178480,-100,4,2,2,47,0,1 +178980,-100,4,2,1,22,0,1 +179146,-100,4,2,2,47,0,1 +179646,-100,4,2,1,22,0,1 +179813,-100,4,2,2,47,0,1 +180313,-100,4,2,1,22,0,1 +180480,-100,4,2,2,47,0,1 +187980,-100,4,2,2,42,0,0 +212646,-100,4,2,2,47,0,1 +260646,-100,4,2,2,42,0,0 + +[HitObjects] +152,72,11980,1,0 +248,144,12313,1,0 +132,176,12647,2,0,B|44:112,2,100,0|0|8 +132,176,13646,1,0 +240,232,13980,2,0,B|164:296,2,100,0|0|12 +240,232,14980,1,0 +304,128,15313,6,0,B|416:184,1,100,0|0 +496,240,15980,2,0,B|466:289|384:312,1,100,8|0 +296,304,16647,2,0,B|192:296|128:192,2,200,2|12|0 +296,184,18312,5,0 +296,184,18646,1,8 +416,184,18980,2,0,B|376:64,1,100,0|0 +268,116,19646,1,0 +268,116,19980,1,12 +168,184,20313,2,0,B|80:248,2,100,0|0|2 +232,80,21313,2,0,B|128:56,2,100,8|0|2 +453,174,22647,5,12 +408,284,22980,1,0 +336,188,23313,1,2 +448,236,23647,1,0 +336,188,23980,2,0,B|336:300,2,100,8|0|2 +256,60,24980,1,0 +112,104,25313,5,12 +228,136,25647,1,0 +132,208,25979,1,2 +176,96,26313,1,0 +132,208,26646,2,0,B|252:200,2,100,8|0|2 +256,292,27647,1,0 +404,280,27980,6,0,B|460:256|476:176,1,100,12|0 +348,184,28646,1,2 +348,184,28980,1,0 +336,64,29313,2,0,B|280:72|248:152,1,100,8|0 +304,236,29979,1,2 +304,236,30313,1,0 +304,236,30646,1,12 +24,120,31313,5,2 +60,264,31646,1,0 +96,120,31979,1,8 +132,264,32313,1,0 +264,192,32647,1,2 +488,108,33313,5,12 +488,108,33647,2,0,B|432:236,1,100,0|0 +380,300,34313,2,0,B|356:348,2,50,0|0|8 +312,200,34980,2,0,B|248:208|208:168,1,100,0|2 +116,112,35646,2,0,B|60:112,2,50,0|0|12 +232,80,36313,2,0,B|292:76|340:112,1,100,0|0 +356,156,36813,2,0,B|420:156,2,50,2|0|0 +296,156,37313,1,8 +176,156,37646,2,0,B|120:156,2,50,0|2|0 +176,156,38313,1,2 +60,128,38647,5,12 +168,88,38980,1,0 +60,128,39313,2,0,B|76:216|140:264,1,150,2|0 +148,312,39980,2,0,B|224:316|296:252,1,150,8|0 +285,261,40647,1,2 +392,204,40980,2,0,B|448:192,2,50,2|2|12 +292,140,41647,2,0,B|244:108|164:100,1,100 +176,160,42147,2,0,B|176:256,1,50,2|0 +140,258,42480,2,0,B|76:258,1,50,0|8 +210,258,42980,2,0,B|266:258,2,50,0|2|0 +257,147,43647,1,2 +256,28,43980,5,4 +256,28,44313,1,0 +344,108,44647,2,0,B|464:156,1,100,2|0 +340,216,45313,2,0,B|244:320,1,100,8|0 +236,176,45980,2,0,B|196:80,1,100,2|0 +92,144,46647,2,0,B|64:192|96:244,1,100,12|0 +204,192,47313,1,2 +324,192,47647,2,0,B|380:192,2,50,0|0|8 +212,144,48313,2,0,B|180:192|220:248,1,100,0|2 +324,192,48980,1,0 +324,192,49313,1,12 +256,292,49647,6,0,B|256:340,2,50,0|0|2 +324,192,50313,1,0 +324,192,50647,1,8 +256,92,50980,2,0,B|256:28,2,50,0|0|2 +200,200,51647,2,0,B|304:200,1,100,0|12 +136,24,52647,5,6 +256,112,52980,2,0,B|368:184,2,100,0|2|0 +376,24,53980,1,6 +256,112,54313,2,0,B|144:184,2,100,0|2|0 +256,264,55313,1,6 +256,112,55647,2,0,B|256:0,2,100,0|2|0 +256,112,56647,1,6 +488,48,57313,5,12 +488,48,57647,2,0,B|485:103|448:160,1,100,0|2 +360,72,58313,2,0,B|320:104|312:176,1,100,8|8 +428,200,58980,1,0 +344,288,59313,1,2 +224,288,59647,2,0,B|208:352,2,50,2|2|12 +256,172,60313,1,0 +256,172,60647,1,2 +136,192,60980,2,0,B|64:204,2,50,2|2|8 +256,172,61647,5,0 +352,244,61980,1,2 +420,144,62313,1,8 +324,72,62647,1,12 +204,72,62980,2,0,B|132:80,2,50,2|2|0 +324,72,63647,2,0,B|372:120|324:200,1,100,0|8 +252,244,64313,1,0 +148,184,64647,1,2 +36,224,64980,2,0,B|68:344,2,100,0|12|8 +24,104,65980,6,0,B|81:72|168:144|232:88,1,200,2|8 +340,84,66980,2,0,B|404:92|444:164,1,100,0|2 +436,252,67647,2,0,B|404:292|404:292,2,50,0|0|12 +436,252,68313,1,0 +332,192,68646,6,0,B|248:120,1,100,0|8 +272,248,69313,2,0,B|176:312,1,100,8|0 +208,184,69980,2,0,B|112:112,1,100,0|8 +128,244,70647,2,0,B|40:300,1,100,12|0 +20,180,71313,5,0 +72,72,71647,2,0,B|40:24,2,50,2|2|8 +192,80,72313,1,0 +300,132,72647,1,2 +300,252,72980,1,8 +192,304,73313,1,12 +72,320,73647,2,0,B|16:368,2,50,2|2|0 +112,208,74313,5,2 +112,208,74647,2,0,B|232:96|264:216|384:72,1,300,8|2 +492,104,75980,2,0,B|428:144|477:263|428:304,1,200,12|0 +320,268,76980,2,0,B|360:156,1,100,0|8 +256,76,77646,2,0,B|256:180,1,100,0|2 +192,268,78313,2,0,B|152:156,1,100,8|12 +216,68,78980,5,0 +320,128,79313,2,0,B|392:160|424:252,1,150,2|0 +408,276,79980,2,0,B|325:276|256:356,1,150,8|0 +236,336,80647,2,0,B|180:272|92:272,1,150,2|0 +88,236,81313,2,0,B|120:152|208:116,1,150,8|0 +224,112,81980,1,2 +344,116,82313,6,0,B|408:116,2,50,2|2|8 +252,192,82980,1,8 +344,268,83313,1,2 +436,192,83647,1,2 +344,116,83980,1,12 +228,80,84313,6,0,B|228:24,2,50,2|2|0 +120,132,84980,1,8 +120,252,85313,1,8 +120,132,85647,1,0 +120,252,85980,1,2 +224,192,86313,1,0 +104,192,86647,1,12 +104,192,86980,1,0 +104,192,87313,6,0,B|312:192,2,200,2|8|2 +12,112,88980,1,0 +104,192,89313,1,12 +124,72,89647,1,2 +244,56,89980,6,0,B|355:55|444:144,1,200,2|8 +416,248,90980,1,2 +312,308,91313,2,0,B|216:308|112:228,1,200,2|12 +88,124,92313,2,0,B|102:102|160:116|192:92,1,100,2|2 +292,144,92980,2,0,B|300:216,2,50,0|0|8 +280,24,93647,1,0 +392,68,93980,1,2 +408,188,94313,1,8 +320,272,94647,1,12 +200,284,94980,6,0,B|208:212,2,50,2|2|0 +80,260,95647,1,2 +20,156,95980,2,0,B|108:76|212:140,1,200,8|0 +304,204,96980,1,8 +416,252,97313,2,0,B|392:300|336:316,2,100,12|0|6 +256,192,98146,12,4,100646 +104,104,121313,6,0,B|216:104,1,100,12|0 +176,220,121980,2,0,B|368:132,1,200,2|8 +240,120,122980,2,0,B|320:80,2,50,2|2|0 +136,180,123647,2,0,B|264:228,1,100,0|12 +348,240,124313,2,0,B|252:288,1,100,0|2 +192,184,124980,1,2 +308,160,125313,1,8 +192,132,125647,1,0 +256,32,125980,6,0,B|256:240,1,200,2|12 +356,296,126980,1,0 +240,328,127313,2,0,B|128:360|56:264,1,200,2|8 +24,156,128313,2,0,B|76:148|80:176|128:164,1,100,2|0 +240,192,128980,2,0,B|232:248,2,50,2|2|12 +208,76,129647,2,0,B|268:72|312:112,1,100,2|0 +388,188,130313,1,0 +388,188,130647,1,8 +336,296,130980,1,0 +336,296,131313,1,2 +128,176,131980,5,12 +128,176,132313,1,2 +128,176,132647,2,0,B|171:149|240:168,1,100,2|0 +264,176,133147,1,0 +272,216,133313,2,0,B|239:264|176:256,1,100,8|0 +68,232,133980,1,2 +68,232,134313,1,0 +88,112,134647,6,0,B|115:65|176:48,1,100,12|2 +204,40,135147,1,0 +244,40,135313,2,0,B|316:48|356:120,1,100,2|0 +400,184,135980,2,0,B|408:248|336:292,1,100,8|0 +252,316,136647,1,2 +252,316,136980,1,0 +240,196,137313,6,0,B|288:180|312:116,1,100,12|2 +300,88,137813,1,0 +276,56,137980,2,0,B|180:16,1,100,2|0 +144,152,138647,2,0,B|24:200,1,100,8|0 +176,252,139313,2,0,B|96:348,1,100,2|0 +252,336,139980,2,0,B|332:240,1,100,12|0 +436,252,140647,2,0,B|382:158|258:151,1,200,2|8 +152,152,141647,2,0,B|104:152,2,50,2|2|0 +388,116,142647,6,0,B|496:32,2,100,12|0|2 +272,152,143647,2,0,B|252:248,1,100,2|8 +251,249,144313,1,2 +130,250,144647,2,0,B|98:298,2,50,2|2|0 +200,152,145313,1,12 +200,152,145647,1,2 +304,92,145980,6,0,B|360:68,1,50,0|2 +400,180,146480,2,0,B|384:236,1,50,0|8 +272,192,146980,1,0 +152,192,147313,2,0,B|96:192,4,50,2|0|2|0|12 +240,272,148313,5,0 +360,296,148647,2,0,B|448:240|456:176,1,150,2|0 +396,168,149313,2,0,B|428:120|428:8,1,150,8|0 +427,23,149980,1,2 +316,68,150313,2,0,B|364:36,2,50,2|2|12 +436,76,150980,2,0,B|324:148,1,100 +296,152,151480,6,0,B|224:172,1,50,2|2 +292,208,151813,2,0,B|288:256,1,50,0|8 +248,212,152147,2,0,B|176:236,1,50,2|2 +244,268,152480,2,0,B|236:336,1,50,0|0 +256,76,153313,5,12 +256,76,153647,1,0 +256,76,153980,2,0,B|48:196,1,200,2|8 +256,76,154980,1,0 +140,44,155313,2,0,B|252:228,1,200,2|12 +140,44,156313,1,0 +84,152,156647,6,0,B|148:264,1,100,2|2 +164,264,157147,1,0 +204,272,157313,1,8 +324,268,157647,2,0,B|428:236,1,100,2|2 +336,152,158313,2,0,B|248:64,1,100,0|12 +164,148,158980,5,0 +164,148,159313,1,2 +48,120,159646,2,0,B|24:48,2,50,2|0|8 +112,224,160313,1,0 +224,272,160647,1,2 +344,248,160980,1,0 +416,152,161313,1,12 +256,336,161980,5,6 +360,272,162313,2,0,B|464:272,2,100,0|8|0 +256,216,163313,1,6 +152,152,163646,2,0,B|48:152,2,100,0|8|0 +256,96,164647,1,6 +360,40,164980,2,0,B|464:40,2,100,0|8|0 +256,96,165980,1,6 +16,80,166646,6,0,B|24:136|56:200,1,100,12|0 +116,80,167313,2,0,B|158:111|220:112,1,100,2|2 +248,112,167814,1,0 +288,112,167980,2,0,B|341:115|384:152,1,100,12|8 +412,172,168480,1,0 +428,208,168647,2,0,B|380:248|300:208,1,100,2|2 +296,208,169147,1,0 +260,192,169313,6,0,B|212:168|140:184,1,100,12|2 +124,188,169814,1,0 +88,204,169980,2,0,B|96:260|200:284,1,100,2|2 +192,284,170480,1,0 +232,288,170647,2,0,B|288:296|336:256,1,100,8|8 +424,196,171314,1,2 +424,196,171647,1,2 +424,196,171980,6,0,B|416:136|360:108,1,100,12|0 +336,100,172480,1,2 +296,88,172646,2,0,B|248:72|192:104,2,100,2|0|8 +256,204,173647,1,8 +164,124,173980,2,0,B|108:112|68:164,2,100,0|0|12 +132,240,174980,2,0,B|92:280|108:344,1,100,2|0 +212,280,175646,2,0,B|272:264|276:184,2,100,2|8|2 +212,280,176647,1,2 +8,136,177313,6,0,B|29:82|104:64,1,100,12|0 +200,64,177980,2,0,B|352:104,1,150,2|0 +344,144,178647,2,0,B|184:168,1,150,8|0 +196,208,179313,2,0,B|348:232,1,150,2|0 +344,272,179980,2,0,B|184:288,1,150,12|0 +136,276,180647,2,0,B|58:233|64:140,2,150,2|2|2 +188,168,181980,1,2 +188,168,182647,5,12 +76,124,182980,2,0,B|20:100,2,50,2|2|0 +188,168,183647,1,8 +300,212,183980,2,0,B|356:228|428:204,2,100,8|0|2 +256,324,184980,2,0,B|200:316|168:260,2,100,0|12|0 +256,324,185980,1,2 +256,84,186647,5,8 +316,188,186980,1,0 +196,188,187313,1,2 +408,300,187980,5,12 +432,184,188313,1,0 +320,228,188647,1,2 +224,300,188980,2,0,B|176:332,2,50,0|0|8 +120,240,189647,2,0,B|64:248,2,50,0|0|2 +96,120,190313,2,0,B|48:96,2,50,0|0|12 +188,40,190980,2,0,B|236:60|272:132,1,100 +320,212,191646,2,0,B|376:236,2,50,0|0|8 +316,92,192313,1,0 +316,92,192646,1,2 +320,212,192980,1,2 +320,212,193313,1,12 +404,124,193647,6,0,B|444:76,2,50,0|0|2 +404,244,194313,2,0,B|404:356,1,100,0|8 +344,216,194980,2,0,B|288:312,1,100,0|2 +300,164,195647,2,0,B|188:212,1,100,0|12 +300,96,196313,2,0,B|180:80,2,100,0|2|0 +420,116,197313,1,8 +420,116,197647,1,0 +300,96,197980,2,0,B|196:80,1,100 +80,72,198647,1,12 +80,72,198980,1,0 +200,68,199313,6,0,B|256:88|272:140,1,100,2|0 +284,172,199813,1,0 +284,212,199980,1,8 +164,224,200313,1,8 +284,212,200647,6,0,B|288:276|228:316,1,100,2|0 +212,324,201147,1,0 +176,344,201314,1,12 +164,224,201647,1,8 +176,344,201980,6,0,B|124:352|72:296,1,100,2|0 +60,280,202480,1,0 +44,244,202647,1,8 +164,224,202980,1,8 +44,244,203313,6,0,B|24:196|44:140,2,100,2|0|12 +152,192,204313,2,0,B|80:192,2,50,2|2|0 +272,192,204980,1,2 +272,192,205313,2,0,B|272:104|153:100|152:200,1,200,8|0 +152,312,206313,6,0,B|152:360,2,50,2|2|12 +152,192,206980,1,0 +152,192,207313,1,14 +152,72,207646,2,0,B|152:16,2,50,0|0|2 +248,144,208313,2,0,B|272:192|240:272,1,100,0|12 +256,192,208980,12,12,209980 +256,192,210313,12,12,211313 +440,208,211980,6,0,B|320:184,1,100,12|0 +324,68,212647,2,0,B|148:164,1,200,2|8 +80,264,213647,1,8 +192,312,213980,1,2 +312,296,214313,1,2 +424,256,214647,1,12 +472,144,214980,6,0,B|480:88,2,50,2|2|0 +352,120,215647,2,0,B|336:56,2,50,2|2|8 +296,224,216313,1,0 +176,208,216647,1,0 +152,88,216980,1,8 +272,104,217313,1,12 +360,184,217647,6,0,B|392:264,2,50,2|2|0 +248,144,218313,2,0,B|200:176|184:248,1,100,0|8 +208,344,218980,1,8 +192,224,219313,1,2 +192,224,219647,2,0,B|200:176|248:144,1,100,0|12 +344,72,220313,2,0,B|400:32,2,50,2|0|2 +320,192,220980,2,0,B|296:248|224:288,1,100,0|8 +140,296,221647,2,0,B|68:304,2,50,2|0|2 +252,248,222313,5,0 +316,144,222647,1,12 +372,248,222980,1,0 +252,248,223313,1,2 +252,248,223646,5,8 +316,144,223980,1,2 +212,80,224313,1,0 +212,80,224647,5,2 +212,176,224980,1,8 +212,176,225313,1,12 +212,176,225647,1,0 +212,296,225980,6,0,B|266:312|316:296,1,100,2|0 +348,284,226480,1,2 +380,260,226647,1,8 +280,192,226980,1,8 +372,116,227313,2,0,B|319:99|268:116,1,100,2|0 +236,128,227813,1,2 +208,156,227980,1,12 +256,268,228313,1,0 +256,268,228647,1,2 +136,284,228980,5,2 +136,284,229147,1,0 +136,284,229313,2,0,B|115:183|160:60,1,200,8|0 +256,20,230313,1,0 +352,92,230647,2,0,B|385:194|336:332,1,200,12|0 +236,336,231647,2,0,B|156:344,2,50,0|0|8 +236,336,232313,1,2 +236,336,232647,1,2 +256,96,233313,6,0,B|200:104|168:160,1,100,12|0 +192,268,233980,2,0,B|304:260|352:148,2,200,2|8|2 +164,152,235647,1,0 +256,76,235980,1,12 +256,196,236313,2,0,B|256:260,2,50,2|2|0 +256,76,236980,2,0,B|256:20,2,50,2|2|8 +256,76,237647,1,8 +256,76,237980,1,2 +344,156,238313,2,0,B|432:236,1,100,0|12 +328,304,238980,5,2 +328,304,239147,1,0 +328,304,239313,2,0,B|192:304,1,100,2|0 +288,200,239980,2,0,B|160:160,1,100,8|8 +72,152,240647,1,2 +72,272,240980,1,0 +72,152,241313,1,12 +72,272,241647,1,0 +152,184,241980,2,0,B|240:80,3,100,2|0|8|0 +216,107,243313,1,2 +444,176,243980,5,12 +368,268,244313,1,0 +248,280,244647,1,2 +128,256,244980,1,2 +128,256,245147,1,0 +128,256,245313,2,0,B|80:216|72:144,1,100,8|8 +72,52,245980,5,2 +192,72,246313,1,2 +192,72,246480,1,0 +192,72,246647,2,0,B|248:160|368:192,1,200,12|8 +402,78,247646,5,2 +402,78,247813,1,0 +402,78,247980,2,0,B|453:111|474:166,1,100,8|8 +352,187,248647,1,2 +467,153,248980,2,0,B|459:217|419:249,1,100,0|12 +312,280,249647,5,2 +256,300,249813,1,0 +200,280,249980,1,2 +280,192,250313,1,0 +280,192,250647,1,8 +320,80,250980,1,0 +280,192,251313,1,2 +196,108,251647,1,0 +280,192,251980,1,12 +256,56,252647,5,2 +256,328,253313,1,2 +120,192,253980,1,2 +392,192,254646,1,2 +256,192,255313,1,2 +256,192,255647,1,2 +256,192,255813,1,0 +256,192,255980,1,12 +256,192,256146,12,4,260646 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/112643-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/112643-expected-conversion.json new file mode 100644 index 0000000000..7d6e29b6c1 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/112643-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":2375.0,"Objects":[{"StartTime":2375.0,"Position":64.0,"HyperDash":false}]},{"StartTime":2625.0,"Objects":[{"StartTime":2625.0,"Position":172.0,"HyperDash":false}]},{"StartTime":2875.0,"Objects":[{"StartTime":2875.0,"Position":152.0,"HyperDash":false}]},{"StartTime":3125.0,"Objects":[{"StartTime":3125.0,"Position":80.0,"HyperDash":false}]},{"StartTime":3375.0,"Objects":[{"StartTime":3375.0,"Position":224.0,"HyperDash":false}]},{"StartTime":3625.0,"Objects":[{"StartTime":3625.0,"Position":192.0,"HyperDash":false}]},{"StartTime":3875.0,"Objects":[{"StartTime":3875.0,"Position":136.0,"HyperDash":false}]},{"StartTime":4125.0,"Objects":[{"StartTime":4125.0,"Position":272.0,"HyperDash":false},{"StartTime":4187.0,"Position":295.965057,"HyperDash":false},{"StartTime":4250.0,"Position":339.30658,"HyperDash":false},{"StartTime":4312.0,"Position":372.55603,"HyperDash":false},{"StartTime":4375.0,"Position":372.509583,"HyperDash":false},{"StartTime":4437.0,"Position":372.203644,"HyperDash":false},{"StartTime":4500.0,"Position":340.885864,"HyperDash":false},{"StartTime":4562.0,"Position":348.843384,"HyperDash":false},{"StartTime":4625.0,"Position":384.566772,"HyperDash":false},{"StartTime":4749.0,"Position":462.643433,"HyperDash":false}]},{"StartTime":4875.0,"Objects":[{"StartTime":4875.0,"Position":504.0,"HyperDash":false},{"StartTime":4937.0,"Position":456.809235,"HyperDash":false},{"StartTime":5000.0,"Position":413.577362,"HyperDash":false},{"StartTime":5062.0,"Position":384.032623,"HyperDash":false},{"StartTime":5125.0,"Position":351.76297,"HyperDash":false},{"StartTime":5178.0,"Position":327.56488,"HyperDash":false},{"StartTime":5232.0,"Position":288.905457,"HyperDash":false},{"StartTime":5285.0,"Position":281.458923,"HyperDash":false},{"StartTime":5375.0,"Position":249.3499,"HyperDash":false}]},{"StartTime":5625.0,"Objects":[{"StartTime":5625.0,"Position":384.0,"HyperDash":false}]},{"StartTime":5875.0,"Objects":[{"StartTime":5875.0,"Position":272.0,"HyperDash":false}]},{"StartTime":6000.0,"Objects":[{"StartTime":6000.0,"Position":272.0,"HyperDash":false}]},{"StartTime":6125.0,"Objects":[{"StartTime":6125.0,"Position":272.0,"HyperDash":false}]},{"StartTime":6375.0,"Objects":[{"StartTime":6375.0,"Position":92.0,"HyperDash":false}]},{"StartTime":6625.0,"Objects":[{"StartTime":6625.0,"Position":124.0,"HyperDash":false}]},{"StartTime":6875.0,"Objects":[{"StartTime":6875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":7125.0,"Objects":[{"StartTime":7125.0,"Position":388.0,"HyperDash":false}]},{"StartTime":7375.0,"Objects":[{"StartTime":7375.0,"Position":420.0,"HyperDash":false}]},{"StartTime":7625.0,"Objects":[{"StartTime":7625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":7875.0,"Objects":[{"StartTime":7875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":8125.0,"Objects":[{"StartTime":8125.0,"Position":443.0,"HyperDash":false},{"StartTime":8187.0,"Position":392.598877,"HyperDash":false},{"StartTime":8250.0,"Position":365.1502,"HyperDash":false},{"StartTime":8312.0,"Position":352.954926,"HyperDash":false},{"StartTime":8375.0,"Position":294.614716,"HyperDash":false},{"StartTime":8437.0,"Position":268.171936,"HyperDash":false},{"StartTime":8500.0,"Position":207.09552,"HyperDash":false},{"StartTime":8562.0,"Position":158.395874,"HyperDash":false},{"StartTime":8625.0,"Position":135.590256,"HyperDash":false},{"StartTime":8749.0,"Position":67.66239,"HyperDash":false}]},{"StartTime":8875.0,"Objects":[{"StartTime":8875.0,"Position":24.0,"HyperDash":false},{"StartTime":8937.0,"Position":54.41505,"HyperDash":false},{"StartTime":9000.0,"Position":92.0854,"HyperDash":false},{"StartTime":9062.0,"Position":91.62684,"HyperDash":false},{"StartTime":9125.0,"Position":114.961037,"HyperDash":false},{"StartTime":9178.0,"Position":112.725426,"HyperDash":false},{"StartTime":9232.0,"Position":118.526962,"HyperDash":false},{"StartTime":9285.0,"Position":72.53759,"HyperDash":false},{"StartTime":9374.0,"Position":43.35332,"HyperDash":false}]},{"StartTime":9625.0,"Objects":[{"StartTime":9625.0,"Position":16.0,"HyperDash":false}]},{"StartTime":9875.0,"Objects":[{"StartTime":9875.0,"Position":136.0,"HyperDash":false}]},{"StartTime":10000.0,"Objects":[{"StartTime":10000.0,"Position":136.0,"HyperDash":false}]},{"StartTime":10125.0,"Objects":[{"StartTime":10125.0,"Position":136.0,"HyperDash":false}]},{"StartTime":10375.0,"Objects":[{"StartTime":10375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":10625.0,"Objects":[{"StartTime":10625.0,"Position":368.0,"HyperDash":false}]},{"StartTime":10875.0,"Objects":[{"StartTime":10875.0,"Position":196.0,"HyperDash":false}]},{"StartTime":11125.0,"Objects":[{"StartTime":11125.0,"Position":316.0,"HyperDash":false}]},{"StartTime":11375.0,"Objects":[{"StartTime":11375.0,"Position":144.0,"HyperDash":false}]},{"StartTime":11625.0,"Objects":[{"StartTime":11625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":11875.0,"Objects":[{"StartTime":11875.0,"Position":112.0,"HyperDash":false}]},{"StartTime":12125.0,"Objects":[{"StartTime":12125.0,"Position":164.0,"HyperDash":false},{"StartTime":12250.0,"Position":238.49942,"HyperDash":false}]},{"StartTime":12500.0,"Objects":[{"StartTime":12500.0,"Position":100.0,"HyperDash":false},{"StartTime":12625.0,"Position":25.50058,"HyperDash":false}]},{"StartTime":12875.0,"Objects":[{"StartTime":12875.0,"Position":144.0,"HyperDash":false},{"StartTime":13000.0,"Position":69.50058,"HyperDash":false}]},{"StartTime":13250.0,"Objects":[{"StartTime":13250.0,"Position":208.0,"HyperDash":false},{"StartTime":13375.0,"Position":282.49942,"HyperDash":false}]},{"StartTime":13625.0,"Objects":[{"StartTime":13625.0,"Position":332.0,"HyperDash":false}]},{"StartTime":13875.0,"Objects":[{"StartTime":13875.0,"Position":180.0,"HyperDash":false}]},{"StartTime":14125.0,"Objects":[{"StartTime":14125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":14250.0,"Objects":[{"StartTime":14250.0,"Position":256.0,"HyperDash":false}]},{"StartTime":14500.0,"Objects":[{"StartTime":14500.0,"Position":324.0,"HyperDash":false}]},{"StartTime":14625.0,"Objects":[{"StartTime":14625.0,"Position":324.0,"HyperDash":false}]},{"StartTime":14875.0,"Objects":[{"StartTime":14875.0,"Position":192.0,"HyperDash":false}]},{"StartTime":15000.0,"Objects":[{"StartTime":15000.0,"Position":192.0,"HyperDash":false}]},{"StartTime":15250.0,"Objects":[{"StartTime":15250.0,"Position":256.0,"HyperDash":false}]},{"StartTime":15375.0,"Objects":[{"StartTime":15375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":15625.0,"Objects":[{"StartTime":15625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":15875.0,"Objects":[{"StartTime":15875.0,"Position":120.0,"HyperDash":false}]},{"StartTime":16125.0,"Objects":[{"StartTime":16125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":18375.0,"Objects":[{"StartTime":18375.0,"Position":20.0,"HyperDash":false}]},{"StartTime":18625.0,"Objects":[{"StartTime":18625.0,"Position":180.0,"HyperDash":false}]},{"StartTime":18875.0,"Objects":[{"StartTime":18875.0,"Position":52.0,"HyperDash":false}]},{"StartTime":19125.0,"Objects":[{"StartTime":19125.0,"Position":120.0,"HyperDash":false}]},{"StartTime":19375.0,"Objects":[{"StartTime":19375.0,"Position":128.0,"HyperDash":false}]},{"StartTime":19625.0,"Objects":[{"StartTime":19625.0,"Position":48.0,"HyperDash":false}]},{"StartTime":19875.0,"Objects":[{"StartTime":19875.0,"Position":192.0,"HyperDash":false}]},{"StartTime":20125.0,"Objects":[{"StartTime":20125.0,"Position":300.0,"HyperDash":false},{"StartTime":20187.0,"Position":319.510284,"HyperDash":false},{"StartTime":20250.0,"Position":361.959717,"HyperDash":false},{"StartTime":20312.0,"Position":410.823639,"HyperDash":false},{"StartTime":20375.0,"Position":393.9937,"HyperDash":false},{"StartTime":20428.0,"Position":389.407,"HyperDash":false},{"StartTime":20482.0,"Position":394.563232,"HyperDash":false},{"StartTime":20535.0,"Position":430.098541,"HyperDash":false},{"StartTime":20624.0,"Position":486.9303,"HyperDash":false}]},{"StartTime":20875.0,"Objects":[{"StartTime":20875.0,"Position":472.0,"HyperDash":false},{"StartTime":20937.0,"Position":454.614349,"HyperDash":false},{"StartTime":21000.0,"Position":395.812744,"HyperDash":false},{"StartTime":21062.0,"Position":377.009979,"HyperDash":false},{"StartTime":21125.0,"Position":345.3677,"HyperDash":false},{"StartTime":21178.0,"Position":342.8652,"HyperDash":false},{"StartTime":21232.0,"Position":325.856567,"HyperDash":false},{"StartTime":21285.0,"Position":310.223846,"HyperDash":false},{"StartTime":21374.0,"Position":280.7244,"HyperDash":false}]},{"StartTime":21625.0,"Objects":[{"StartTime":21625.0,"Position":404.0,"HyperDash":false}]},{"StartTime":21875.0,"Objects":[{"StartTime":21875.0,"Position":432.0,"HyperDash":false}]},{"StartTime":22000.0,"Objects":[{"StartTime":22000.0,"Position":432.0,"HyperDash":false}]},{"StartTime":22125.0,"Objects":[{"StartTime":22125.0,"Position":432.0,"HyperDash":false}]},{"StartTime":22375.0,"Objects":[{"StartTime":22375.0,"Position":296.0,"HyperDash":false}]},{"StartTime":22625.0,"Objects":[{"StartTime":22625.0,"Position":168.0,"HyperDash":false},{"StartTime":22678.0,"Position":157.672318,"HyperDash":false},{"StartTime":22732.0,"Position":121.82901,"HyperDash":false},{"StartTime":22785.0,"Position":68.50134,"HyperDash":false},{"StartTime":22875.0,"Position":39.09584,"HyperDash":false}]},{"StartTime":23125.0,"Objects":[{"StartTime":23125.0,"Position":268.0,"HyperDash":false},{"StartTime":23178.0,"Position":252.906113,"HyperDash":false},{"StartTime":23232.0,"Position":215.4331,"HyperDash":false},{"StartTime":23285.0,"Position":192.339218,"HyperDash":false},{"StartTime":23375.0,"Position":173.217529,"HyperDash":false}]},{"StartTime":23625.0,"Objects":[{"StartTime":23625.0,"Position":252.0,"HyperDash":false},{"StartTime":23678.0,"Position":297.327667,"HyperDash":false},{"StartTime":23732.0,"Position":299.171,"HyperDash":false},{"StartTime":23785.0,"Position":350.498657,"HyperDash":false},{"StartTime":23875.0,"Position":380.904175,"HyperDash":false}]},{"StartTime":24125.0,"Objects":[{"StartTime":24125.0,"Position":484.0,"HyperDash":false},{"StartTime":24187.0,"Position":459.330444,"HyperDash":false},{"StartTime":24250.0,"Position":410.3108,"HyperDash":false},{"StartTime":24312.0,"Position":381.927948,"HyperDash":false},{"StartTime":24375.0,"Position":342.702942,"HyperDash":false},{"StartTime":24437.0,"Position":307.727,"HyperDash":false},{"StartTime":24500.0,"Position":254.618744,"HyperDash":false},{"StartTime":24562.0,"Position":219.823792,"HyperDash":false},{"StartTime":24625.0,"Position":195.842667,"HyperDash":false},{"StartTime":24750.0,"Position":124.114441,"HyperDash":false}]},{"StartTime":24875.0,"Objects":[{"StartTime":24875.0,"Position":72.0,"HyperDash":false},{"StartTime":24937.0,"Position":90.6446,"HyperDash":false},{"StartTime":25000.0,"Position":102.976662,"HyperDash":false},{"StartTime":25062.0,"Position":121.259918,"HyperDash":false},{"StartTime":25125.0,"Position":115.072632,"HyperDash":false},{"StartTime":25178.0,"Position":104.017952,"HyperDash":false},{"StartTime":25232.0,"Position":66.87554,"HyperDash":false},{"StartTime":25285.0,"Position":53.7148743,"HyperDash":false},{"StartTime":25374.0,"Position":0.0,"HyperDash":false}]},{"StartTime":25625.0,"Objects":[{"StartTime":25625.0,"Position":56.0,"HyperDash":false}]},{"StartTime":25875.0,"Objects":[{"StartTime":25875.0,"Position":176.0,"HyperDash":false}]},{"StartTime":26000.0,"Objects":[{"StartTime":26000.0,"Position":176.0,"HyperDash":false}]},{"StartTime":26125.0,"Objects":[{"StartTime":26125.0,"Position":176.0,"HyperDash":false}]},{"StartTime":26375.0,"Objects":[{"StartTime":26375.0,"Position":316.0,"HyperDash":false}]},{"StartTime":26625.0,"Objects":[{"StartTime":26625.0,"Position":464.0,"HyperDash":false},{"StartTime":26678.0,"Position":423.678864,"HyperDash":false},{"StartTime":26732.0,"Position":428.026764,"HyperDash":false},{"StartTime":26785.0,"Position":431.558746,"HyperDash":false},{"StartTime":26875.0,"Position":408.8022,"HyperDash":false}]},{"StartTime":27125.0,"Objects":[{"StartTime":27125.0,"Position":232.0,"HyperDash":false},{"StartTime":27178.0,"Position":266.0937,"HyperDash":false},{"StartTime":27232.0,"Position":284.472229,"HyperDash":false},{"StartTime":27285.0,"Position":289.223022,"HyperDash":false},{"StartTime":27374.0,"Position":288.2113,"HyperDash":false}]},{"StartTime":27625.0,"Objects":[{"StartTime":27625.0,"Position":136.0,"HyperDash":false}]},{"StartTime":27875.0,"Objects":[{"StartTime":27875.0,"Position":60.0,"HyperDash":false}]},{"StartTime":28125.0,"Objects":[{"StartTime":28125.0,"Position":212.0,"HyperDash":false},{"StartTime":28250.0,"Position":244.219086,"HyperDash":false}]},{"StartTime":28500.0,"Objects":[{"StartTime":28500.0,"Position":340.0,"HyperDash":false},{"StartTime":28625.0,"Position":372.2191,"HyperDash":false}]},{"StartTime":28875.0,"Objects":[{"StartTime":28875.0,"Position":256.0,"HyperDash":false},{"StartTime":29000.0,"Position":223.780914,"HyperDash":false}]},{"StartTime":29250.0,"Objects":[{"StartTime":29250.0,"Position":128.0,"HyperDash":false},{"StartTime":29375.0,"Position":95.7809143,"HyperDash":false}]},{"StartTime":29625.0,"Objects":[{"StartTime":29625.0,"Position":238.0,"HyperDash":false},{"StartTime":29678.0,"Position":279.04657,"HyperDash":false},{"StartTime":29731.0,"Position":322.09314,"HyperDash":false},{"StartTime":29784.0,"Position":325.1397,"HyperDash":false},{"StartTime":29874.0,"Position":397.954651,"HyperDash":false}]},{"StartTime":30125.0,"Objects":[{"StartTime":30125.0,"Position":512.0,"HyperDash":false}]},{"StartTime":30250.0,"Objects":[{"StartTime":30250.0,"Position":512.0,"HyperDash":false}]},{"StartTime":30500.0,"Objects":[{"StartTime":30500.0,"Position":416.0,"HyperDash":false}]},{"StartTime":30625.0,"Objects":[{"StartTime":30625.0,"Position":416.0,"HyperDash":false}]},{"StartTime":30875.0,"Objects":[{"StartTime":30875.0,"Position":300.0,"HyperDash":false}]},{"StartTime":31000.0,"Objects":[{"StartTime":31000.0,"Position":300.0,"HyperDash":false}]},{"StartTime":31250.0,"Objects":[{"StartTime":31250.0,"Position":236.0,"HyperDash":false}]},{"StartTime":31375.0,"Objects":[{"StartTime":31375.0,"Position":236.0,"HyperDash":false}]},{"StartTime":31625.0,"Objects":[{"StartTime":31625.0,"Position":152.0,"HyperDash":false}]},{"StartTime":31875.0,"Objects":[{"StartTime":31875.0,"Position":300.0,"HyperDash":false}]},{"StartTime":32125.0,"Objects":[{"StartTime":32125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":34625.0,"Objects":[{"StartTime":34625.0,"Position":52.0,"HyperDash":false}]},{"StartTime":34875.0,"Objects":[{"StartTime":34875.0,"Position":152.0,"HyperDash":false}]},{"StartTime":35125.0,"Objects":[{"StartTime":35125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":35625.0,"Objects":[{"StartTime":35625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":36125.0,"Objects":[{"StartTime":36125.0,"Position":256.0,"HyperDash":false},{"StartTime":36178.0,"Position":285.74295,"HyperDash":false},{"StartTime":36232.0,"Position":306.695557,"HyperDash":false},{"StartTime":36285.0,"Position":338.9461,"HyperDash":false},{"StartTime":36375.0,"Position":338.0262,"HyperDash":false}]},{"StartTime":36625.0,"Objects":[{"StartTime":36625.0,"Position":320.0,"HyperDash":false}]},{"StartTime":36875.0,"Objects":[{"StartTime":36875.0,"Position":204.0,"HyperDash":false}]},{"StartTime":37125.0,"Objects":[{"StartTime":37125.0,"Position":104.0,"HyperDash":false},{"StartTime":37178.0,"Position":84.88513,"HyperDash":false},{"StartTime":37232.0,"Position":58.02897,"HyperDash":false},{"StartTime":37285.0,"Position":32.3897247,"HyperDash":false},{"StartTime":37375.0,"Position":42.93435,"HyperDash":false}]},{"StartTime":37625.0,"Objects":[{"StartTime":37625.0,"Position":92.0,"HyperDash":false}]},{"StartTime":37875.0,"Objects":[{"StartTime":37875.0,"Position":212.0,"HyperDash":false}]},{"StartTime":38000.0,"Objects":[{"StartTime":38000.0,"Position":268.0,"HyperDash":false}]},{"StartTime":38125.0,"Objects":[{"StartTime":38125.0,"Position":324.0,"HyperDash":false},{"StartTime":38178.0,"Position":338.3627,"HyperDash":false},{"StartTime":38232.0,"Position":380.1851,"HyperDash":false},{"StartTime":38285.0,"Position":411.5478,"HyperDash":false},{"StartTime":38375.0,"Position":438.918457,"HyperDash":false}]},{"StartTime":38625.0,"Objects":[{"StartTime":38625.0,"Position":504.0,"HyperDash":false}]},{"StartTime":38875.0,"Objects":[{"StartTime":38875.0,"Position":364.0,"HyperDash":false}]},{"StartTime":39125.0,"Objects":[{"StartTime":39125.0,"Position":232.0,"HyperDash":false},{"StartTime":39187.0,"Position":199.986359,"HyperDash":false},{"StartTime":39250.0,"Position":169.811844,"HyperDash":false},{"StartTime":39312.0,"Position":133.274048,"HyperDash":false},{"StartTime":39375.0,"Position":115.502953,"HyperDash":false},{"StartTime":39437.0,"Position":95.79658,"HyperDash":false},{"StartTime":39500.0,"Position":126.272606,"HyperDash":false},{"StartTime":39562.0,"Position":153.43367,"HyperDash":false},{"StartTime":39625.0,"Position":177.594223,"HyperDash":false},{"StartTime":39687.0,"Position":138.43367,"HyperDash":false},{"StartTime":39750.0,"Position":126.007256,"HyperDash":false},{"StartTime":39812.0,"Position":110.796577,"HyperDash":false},{"StartTime":39875.0,"Position":115.652954,"HyperDash":false},{"StartTime":39928.0,"Position":111.270706,"HyperDash":false},{"StartTime":39982.0,"Position":160.599289,"HyperDash":false},{"StartTime":40035.0,"Position":158.120911,"HyperDash":false},{"StartTime":40124.0,"Position":232.0,"HyperDash":false}]},{"StartTime":40375.0,"Objects":[{"StartTime":40375.0,"Position":280.0,"HyperDash":false}]},{"StartTime":40625.0,"Objects":[{"StartTime":40625.0,"Position":400.0,"HyperDash":false},{"StartTime":40678.0,"Position":429.074829,"HyperDash":false},{"StartTime":40732.0,"Position":455.5662,"HyperDash":false},{"StartTime":40785.0,"Position":457.641022,"HyperDash":false},{"StartTime":40875.0,"Position":504.126617,"HyperDash":false}]},{"StartTime":41125.0,"Objects":[{"StartTime":41125.0,"Position":480.0,"HyperDash":false}]},{"StartTime":41375.0,"Objects":[{"StartTime":41375.0,"Position":324.0,"HyperDash":false}]},{"StartTime":41625.0,"Objects":[{"StartTime":41625.0,"Position":168.0,"HyperDash":false}]},{"StartTime":41875.0,"Objects":[{"StartTime":41875.0,"Position":72.0,"HyperDash":false}]},{"StartTime":42000.0,"Objects":[{"StartTime":42000.0,"Position":48.0,"HyperDash":false}]},{"StartTime":42125.0,"Objects":[{"StartTime":42125.0,"Position":96.0,"HyperDash":false},{"StartTime":42178.0,"Position":114.931221,"HyperDash":false},{"StartTime":42232.0,"Position":153.604843,"HyperDash":false},{"StartTime":42285.0,"Position":193.4396,"HyperDash":false},{"StartTime":42374.0,"Position":240.778946,"HyperDash":false}]},{"StartTime":42625.0,"Objects":[{"StartTime":42625.0,"Position":400.0,"HyperDash":false}]},{"StartTime":42875.0,"Objects":[{"StartTime":42875.0,"Position":440.0,"HyperDash":false}]},{"StartTime":43000.0,"Objects":[{"StartTime":43000.0,"Position":464.0,"HyperDash":false}]},{"StartTime":43125.0,"Objects":[{"StartTime":43125.0,"Position":416.0,"HyperDash":false},{"StartTime":43178.0,"Position":375.182983,"HyperDash":false},{"StartTime":43232.0,"Position":366.663025,"HyperDash":false},{"StartTime":43285.0,"Position":335.968475,"HyperDash":false},{"StartTime":43375.0,"Position":271.221039,"HyperDash":false}]},{"StartTime":43625.0,"Objects":[{"StartTime":43625.0,"Position":112.0,"HyperDash":false}]},{"StartTime":43875.0,"Objects":[{"StartTime":43875.0,"Position":140.0,"HyperDash":false}]},{"StartTime":44125.0,"Objects":[{"StartTime":44125.0,"Position":52.0,"HyperDash":false}]},{"StartTime":44375.0,"Objects":[{"StartTime":44375.0,"Position":208.0,"HyperDash":false}]},{"StartTime":44625.0,"Objects":[{"StartTime":44625.0,"Position":344.0,"HyperDash":false}]},{"StartTime":44875.0,"Objects":[{"StartTime":44875.0,"Position":448.0,"HyperDash":false},{"StartTime":44937.0,"Position":411.344635,"HyperDash":false},{"StartTime":45000.0,"Position":386.572845,"HyperDash":false},{"StartTime":45062.0,"Position":355.1799,"HyperDash":false},{"StartTime":45125.0,"Position":304.139374,"HyperDash":false},{"StartTime":45187.0,"Position":271.8332,"HyperDash":false},{"StartTime":45250.0,"Position":232.840988,"HyperDash":false},{"StartTime":45312.0,"Position":235.629944,"HyperDash":false},{"StartTime":45375.0,"Position":232.882874,"HyperDash":false},{"StartTime":45437.0,"Position":251.629944,"HyperDash":false},{"StartTime":45500.0,"Position":243.152222,"HyperDash":false},{"StartTime":45562.0,"Position":270.8332,"HyperDash":false},{"StartTime":45625.0,"Position":304.729126,"HyperDash":false},{"StartTime":45678.0,"Position":323.441345,"HyperDash":false},{"StartTime":45732.0,"Position":370.914246,"HyperDash":false},{"StartTime":45785.0,"Position":421.2586,"HyperDash":false},{"StartTime":45874.0,"Position":448.0,"HyperDash":false}]},{"StartTime":46125.0,"Objects":[{"StartTime":46125.0,"Position":326.0,"HyperDash":false},{"StartTime":46187.0,"Position":309.377716,"HyperDash":false},{"StartTime":46250.0,"Position":271.650543,"HyperDash":false},{"StartTime":46312.0,"Position":219.299332,"HyperDash":false},{"StartTime":46375.0,"Position":182.286819,"HyperDash":false},{"StartTime":46428.0,"Position":144.357529,"HyperDash":false},{"StartTime":46482.0,"Position":145.0256,"HyperDash":false},{"StartTime":46535.0,"Position":101.934631,"HyperDash":false},{"StartTime":46625.0,"Position":110.882874,"HyperDash":false}]},{"StartTime":46875.0,"Objects":[{"StartTime":46875.0,"Position":230.0,"HyperDash":false},{"StartTime":46937.0,"Position":247.622284,"HyperDash":false},{"StartTime":47000.0,"Position":299.3495,"HyperDash":false},{"StartTime":47062.0,"Position":322.700653,"HyperDash":false},{"StartTime":47125.0,"Position":373.7132,"HyperDash":false},{"StartTime":47178.0,"Position":390.642456,"HyperDash":false},{"StartTime":47232.0,"Position":424.974426,"HyperDash":false},{"StartTime":47285.0,"Position":428.065369,"HyperDash":false},{"StartTime":47375.0,"Position":445.1171,"HyperDash":false}]},{"StartTime":47625.0,"Objects":[{"StartTime":47625.0,"Position":376.0,"HyperDash":false}]},{"StartTime":48125.0,"Objects":[{"StartTime":48125.0,"Position":376.0,"HyperDash":false},{"StartTime":48178.0,"Position":340.223816,"HyperDash":false},{"StartTime":48232.0,"Position":305.204224,"HyperDash":false},{"StartTime":48285.0,"Position":270.449249,"HyperDash":false},{"StartTime":48375.0,"Position":222.9901,"HyperDash":false}]},{"StartTime":48625.0,"Objects":[{"StartTime":48625.0,"Position":84.0,"HyperDash":false}]},{"StartTime":48875.0,"Objects":[{"StartTime":48875.0,"Position":152.0,"HyperDash":false}]},{"StartTime":49125.0,"Objects":[{"StartTime":49125.0,"Position":44.0,"HyperDash":false},{"StartTime":49178.0,"Position":69.96314,"HyperDash":false},{"StartTime":49232.0,"Position":103.8065,"HyperDash":false},{"StartTime":49285.0,"Position":156.7781,"HyperDash":false},{"StartTime":49374.0,"Position":197.1017,"HyperDash":false}]},{"StartTime":49625.0,"Objects":[{"StartTime":49625.0,"Position":336.0,"HyperDash":false}]},{"StartTime":49875.0,"Objects":[{"StartTime":49875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":50125.0,"Objects":[{"StartTime":50125.0,"Position":176.0,"HyperDash":false}]},{"StartTime":50625.0,"Objects":[{"StartTime":50625.0,"Position":340.0,"HyperDash":false}]},{"StartTime":50875.0,"Objects":[{"StartTime":50875.0,"Position":420.0,"HyperDash":false}]},{"StartTime":51125.0,"Objects":[{"StartTime":51125.0,"Position":500.0,"HyperDash":false}]},{"StartTime":51625.0,"Objects":[{"StartTime":51625.0,"Position":172.0,"HyperDash":false}]},{"StartTime":51875.0,"Objects":[{"StartTime":51875.0,"Position":92.0,"HyperDash":false}]},{"StartTime":52125.0,"Objects":[{"StartTime":52125.0,"Position":12.0,"HyperDash":false},{"StartTime":52178.0,"Position":43.4575653,"HyperDash":false},{"StartTime":52232.0,"Position":57.4520721,"HyperDash":false},{"StartTime":52285.0,"Position":85.90964,"HyperDash":false},{"StartTime":52375.0,"Position":146.23381,"HyperDash":false}]},{"StartTime":52625.0,"Objects":[{"StartTime":52625.0,"Position":304.0,"HyperDash":false}]},{"StartTime":52875.0,"Objects":[{"StartTime":52875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":53125.0,"Objects":[{"StartTime":53125.0,"Position":216.0,"HyperDash":false},{"StartTime":53178.0,"Position":229.457565,"HyperDash":false},{"StartTime":53232.0,"Position":269.452057,"HyperDash":false},{"StartTime":53285.0,"Position":304.909637,"HyperDash":false},{"StartTime":53375.0,"Position":350.233826,"HyperDash":false}]},{"StartTime":53625.0,"Objects":[{"StartTime":53625.0,"Position":508.0,"HyperDash":false}]},{"StartTime":53875.0,"Objects":[{"StartTime":53875.0,"Position":460.0,"HyperDash":false}]},{"StartTime":54125.0,"Objects":[{"StartTime":54125.0,"Position":344.0,"HyperDash":false}]},{"StartTime":54375.0,"Objects":[{"StartTime":54375.0,"Position":228.0,"HyperDash":false}]},{"StartTime":54625.0,"Objects":[{"StartTime":54625.0,"Position":153.0,"HyperDash":false}]},{"StartTime":54875.0,"Objects":[{"StartTime":54875.0,"Position":72.0,"HyperDash":false}]},{"StartTime":55125.0,"Objects":[{"StartTime":55125.0,"Position":180.0,"HyperDash":false}]},{"StartTime":55375.0,"Objects":[{"StartTime":55375.0,"Position":284.0,"HyperDash":false}]},{"StartTime":55625.0,"Objects":[{"StartTime":55625.0,"Position":359.0,"HyperDash":false}]},{"StartTime":55875.0,"Objects":[{"StartTime":55875.0,"Position":440.0,"HyperDash":false}]},{"StartTime":56125.0,"Objects":[{"StartTime":56125.0,"Position":352.0,"HyperDash":false},{"StartTime":56178.0,"Position":355.0677,"HyperDash":false},{"StartTime":56231.0,"Position":396.135376,"HyperDash":false},{"StartTime":56284.0,"Position":431.2031,"HyperDash":false},{"StartTime":56374.0,"Position":455.6765,"HyperDash":false}]},{"StartTime":56625.0,"Objects":[{"StartTime":56625.0,"Position":312.0,"HyperDash":false}]},{"StartTime":56875.0,"Objects":[{"StartTime":56875.0,"Position":200.0,"HyperDash":false}]},{"StartTime":57125.0,"Objects":[{"StartTime":57125.0,"Position":160.0,"HyperDash":false},{"StartTime":57178.0,"Position":134.932312,"HyperDash":false},{"StartTime":57231.0,"Position":131.864609,"HyperDash":false},{"StartTime":57284.0,"Position":84.7969055,"HyperDash":false},{"StartTime":57374.0,"Position":56.32347,"HyperDash":false}]},{"StartTime":57625.0,"Objects":[{"StartTime":57625.0,"Position":200.0,"HyperDash":false}]},{"StartTime":57875.0,"Objects":[{"StartTime":57875.0,"Position":312.0,"HyperDash":false}]},{"StartTime":58125.0,"Objects":[{"StartTime":58125.0,"Position":444.0,"HyperDash":false},{"StartTime":58178.0,"Position":405.081421,"HyperDash":false},{"StartTime":58232.0,"Position":380.062256,"HyperDash":false},{"StartTime":58285.0,"Position":399.193085,"HyperDash":false},{"StartTime":58374.0,"Position":377.6735,"HyperDash":false}]},{"StartTime":58500.0,"Objects":[{"StartTime":58500.0,"Position":344.0,"HyperDash":false}]},{"StartTime":58625.0,"Objects":[{"StartTime":58625.0,"Position":272.0,"HyperDash":false},{"StartTime":58678.0,"Position":263.870544,"HyperDash":false},{"StartTime":58732.0,"Position":246.779541,"HyperDash":false},{"StartTime":58785.0,"Position":179.497513,"HyperDash":false},{"StartTime":58875.0,"Position":139.25528,"HyperDash":false}]},{"StartTime":59125.0,"Objects":[{"StartTime":59125.0,"Position":68.0,"HyperDash":false},{"StartTime":59178.0,"Position":89.57149,"HyperDash":false},{"StartTime":59232.0,"Position":123.207489,"HyperDash":false},{"StartTime":59285.0,"Position":141.936157,"HyperDash":false},{"StartTime":59375.0,"Position":133.961975,"HyperDash":false}]},{"StartTime":59500.0,"Objects":[{"StartTime":59500.0,"Position":168.0,"HyperDash":false}]},{"StartTime":59625.0,"Objects":[{"StartTime":59625.0,"Position":240.0,"HyperDash":false},{"StartTime":59678.0,"Position":245.129486,"HyperDash":false},{"StartTime":59732.0,"Position":270.220459,"HyperDash":false},{"StartTime":59785.0,"Position":296.5025,"HyperDash":false},{"StartTime":59875.0,"Position":372.74472,"HyperDash":false}]},{"StartTime":60125.0,"Objects":[{"StartTime":60125.0,"Position":456.0,"HyperDash":false}]},{"StartTime":60375.0,"Objects":[{"StartTime":60375.0,"Position":328.0,"HyperDash":false}]},{"StartTime":60625.0,"Objects":[{"StartTime":60625.0,"Position":216.0,"HyperDash":false}]},{"StartTime":60875.0,"Objects":[{"StartTime":60875.0,"Position":72.0,"HyperDash":false},{"StartTime":60937.0,"Position":71.25553,"HyperDash":false},{"StartTime":61000.0,"Position":61.5583878,"HyperDash":false},{"StartTime":61062.0,"Position":98.84126,"HyperDash":false},{"StartTime":61125.0,"Position":119.510284,"HyperDash":false},{"StartTime":61187.0,"Position":142.845825,"HyperDash":false},{"StartTime":61250.0,"Position":184.319992,"HyperDash":false},{"StartTime":61312.0,"Position":240.90744,"HyperDash":false},{"StartTime":61375.0,"Position":269.728363,"HyperDash":false},{"StartTime":61437.0,"Position":239.90744,"HyperDash":false},{"StartTime":61500.0,"Position":197.687851,"HyperDash":false},{"StartTime":61562.0,"Position":150.845825,"HyperDash":false},{"StartTime":61625.0,"Position":119.024872,"HyperDash":false},{"StartTime":61678.0,"Position":90.12531,"HyperDash":false},{"StartTime":61732.0,"Position":72.3374557,"HyperDash":false},{"StartTime":61785.0,"Position":89.06496,"HyperDash":false},{"StartTime":61874.0,"Position":72.0,"HyperDash":false}]},{"StartTime":62125.0,"Objects":[{"StartTime":62125.0,"Position":200.0,"HyperDash":false},{"StartTime":62187.0,"Position":191.234039,"HyperDash":false},{"StartTime":62250.0,"Position":203.319962,"HyperDash":false},{"StartTime":62312.0,"Position":235.3192,"HyperDash":false},{"StartTime":62375.0,"Position":246.7092,"HyperDash":false},{"StartTime":62428.0,"Position":291.675018,"HyperDash":false},{"StartTime":62482.0,"Position":309.9024,"HyperDash":false},{"StartTime":62535.0,"Position":336.449463,"HyperDash":false},{"StartTime":62625.0,"Position":396.8608,"HyperDash":false}]},{"StartTime":62875.0,"Objects":[{"StartTime":62875.0,"Position":480.0,"HyperDash":false},{"StartTime":62937.0,"Position":492.1737,"HyperDash":false},{"StartTime":63000.0,"Position":476.1641,"HyperDash":false},{"StartTime":63062.0,"Position":475.045135,"HyperDash":false},{"StartTime":63125.0,"Position":433.461975,"HyperDash":false},{"StartTime":63178.0,"Position":389.354034,"HyperDash":false},{"StartTime":63232.0,"Position":366.034546,"HyperDash":false},{"StartTime":63285.0,"Position":321.454956,"HyperDash":false},{"StartTime":63375.0,"Position":283.111176,"HyperDash":false}]},{"StartTime":63625.0,"Objects":[{"StartTime":63625.0,"Position":136.0,"HyperDash":false},{"StartTime":63678.0,"Position":111.887825,"HyperDash":false},{"StartTime":63732.0,"Position":108.904541,"HyperDash":false},{"StartTime":63785.0,"Position":105.234535,"HyperDash":false},{"StartTime":63874.0,"Position":128.127991,"HyperDash":false}]},{"StartTime":64125.0,"Objects":[{"StartTime":64125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":64375.0,"Objects":[{"StartTime":64375.0,"Position":284.0,"HyperDash":false}]},{"StartTime":64625.0,"Objects":[{"StartTime":64625.0,"Position":440.0,"HyperDash":false}]},{"StartTime":64875.0,"Objects":[{"StartTime":64875.0,"Position":420.0,"HyperDash":false}]},{"StartTime":65125.0,"Objects":[{"StartTime":65125.0,"Position":300.0,"HyperDash":false}]},{"StartTime":65375.0,"Objects":[{"StartTime":65375.0,"Position":272.0,"HyperDash":false}]},{"StartTime":65625.0,"Objects":[{"StartTime":65625.0,"Position":116.0,"HyperDash":false}]},{"StartTime":65875.0,"Objects":[{"StartTime":65875.0,"Position":136.0,"HyperDash":false}]},{"StartTime":66125.0,"Objects":[{"StartTime":66125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":68125.0,"Objects":[{"StartTime":68125.0,"Position":256.0,"HyperDash":false},{"StartTime":68187.0,"Position":266.157,"HyperDash":false},{"StartTime":68250.0,"Position":280.344269,"HyperDash":false},{"StartTime":68312.0,"Position":243.4508,"HyperDash":false},{"StartTime":68375.0,"Position":216.9601,"HyperDash":false},{"StartTime":68428.0,"Position":173.102234,"HyperDash":false},{"StartTime":68482.0,"Position":150.915558,"HyperDash":false},{"StartTime":68535.0,"Position":106.794662,"HyperDash":false},{"StartTime":68625.0,"Position":73.61266,"HyperDash":false}]},{"StartTime":68875.0,"Objects":[{"StartTime":68875.0,"Position":132.0,"HyperDash":false},{"StartTime":68937.0,"Position":160.783325,"HyperDash":false},{"StartTime":69000.0,"Position":193.9825,"HyperDash":false},{"StartTime":69062.0,"Position":205.765823,"HyperDash":false},{"StartTime":69125.0,"Position":235.965,"HyperDash":false},{"StartTime":69178.0,"Position":262.005585,"HyperDash":false},{"StartTime":69232.0,"Position":285.462,"HyperDash":false},{"StartTime":69285.0,"Position":302.5026,"HyperDash":false},{"StartTime":69375.0,"Position":339.93,"HyperDash":false}]},{"StartTime":69625.0,"Objects":[{"StartTime":69625.0,"Position":456.0,"HyperDash":false}]},{"StartTime":69875.0,"Objects":[{"StartTime":69875.0,"Position":340.0,"HyperDash":false}]},{"StartTime":70000.0,"Objects":[{"StartTime":70000.0,"Position":340.0,"HyperDash":false}]},{"StartTime":70125.0,"Objects":[{"StartTime":70125.0,"Position":340.0,"HyperDash":false}]},{"StartTime":70375.0,"Objects":[{"StartTime":70375.0,"Position":228.0,"HyperDash":false}]},{"StartTime":70625.0,"Objects":[{"StartTime":70625.0,"Position":256.0,"HyperDash":false},{"StartTime":70678.0,"Position":210.6065,"HyperDash":false},{"StartTime":70732.0,"Position":177.325424,"HyperDash":false},{"StartTime":70785.0,"Position":151.573288,"HyperDash":false},{"StartTime":70875.0,"Position":107.425896,"HyperDash":false}]},{"StartTime":71125.0,"Objects":[{"StartTime":71125.0,"Position":148.0,"HyperDash":false},{"StartTime":71178.0,"Position":184.328445,"HyperDash":false},{"StartTime":71232.0,"Position":200.780228,"HyperDash":false},{"StartTime":71285.0,"Position":257.6842,"HyperDash":false},{"StartTime":71374.0,"Position":296.433563,"HyperDash":false}]},{"StartTime":71625.0,"Objects":[{"StartTime":71625.0,"Position":424.0,"HyperDash":false}]},{"StartTime":71875.0,"Objects":[{"StartTime":71875.0,"Position":336.0,"HyperDash":false}]},{"StartTime":72000.0,"Objects":[{"StartTime":72000.0,"Position":336.0,"HyperDash":false}]},{"StartTime":72125.0,"Objects":[{"StartTime":72125.0,"Position":336.0,"HyperDash":false}]},{"StartTime":72375.0,"Objects":[{"StartTime":72375.0,"Position":228.0,"HyperDash":false},{"StartTime":72428.0,"Position":211.104858,"HyperDash":false},{"StartTime":72482.0,"Position":163.608932,"HyperDash":false},{"StartTime":72535.0,"Position":134.045914,"HyperDash":false},{"StartTime":72625.0,"Position":143.764755,"HyperDash":false}]},{"StartTime":72875.0,"Objects":[{"StartTime":72875.0,"Position":268.0,"HyperDash":false},{"StartTime":72937.0,"Position":248.6492,"HyperDash":false},{"StartTime":73000.0,"Position":273.503021,"HyperDash":false},{"StartTime":73062.0,"Position":247.768143,"HyperDash":false},{"StartTime":73125.0,"Position":228.062622,"HyperDash":false},{"StartTime":73178.0,"Position":204.959824,"HyperDash":false},{"StartTime":73232.0,"Position":170.633987,"HyperDash":false},{"StartTime":73285.0,"Position":155.368179,"HyperDash":false},{"StartTime":73375.0,"Position":103.8164,"HyperDash":false}]},{"StartTime":73625.0,"Objects":[{"StartTime":73625.0,"Position":24.0,"HyperDash":false}]},{"StartTime":73875.0,"Objects":[{"StartTime":73875.0,"Position":92.0,"HyperDash":false}]},{"StartTime":74000.0,"Objects":[{"StartTime":74000.0,"Position":92.0,"HyperDash":false}]},{"StartTime":74125.0,"Objects":[{"StartTime":74125.0,"Position":92.0,"HyperDash":false}]},{"StartTime":74375.0,"Objects":[{"StartTime":74375.0,"Position":224.0,"HyperDash":false}]},{"StartTime":74625.0,"Objects":[{"StartTime":74625.0,"Position":340.0,"HyperDash":false},{"StartTime":74678.0,"Position":381.308228,"HyperDash":false},{"StartTime":74732.0,"Position":376.477844,"HyperDash":false},{"StartTime":74785.0,"Position":399.771942,"HyperDash":false},{"StartTime":74875.0,"Position":387.2963,"HyperDash":false}]},{"StartTime":75125.0,"Objects":[{"StartTime":75125.0,"Position":268.0,"HyperDash":false},{"StartTime":75178.0,"Position":219.691772,"HyperDash":false},{"StartTime":75232.0,"Position":224.522156,"HyperDash":false},{"StartTime":75285.0,"Position":185.228043,"HyperDash":false},{"StartTime":75375.0,"Position":220.70369,"HyperDash":false}]},{"StartTime":75625.0,"Objects":[{"StartTime":75625.0,"Position":268.0,"HyperDash":false},{"StartTime":75678.0,"Position":251.437485,"HyperDash":false},{"StartTime":75732.0,"Position":209.2417,"HyperDash":false},{"StartTime":75785.0,"Position":166.6792,"HyperDash":false},{"StartTime":75875.0,"Position":109.686234,"HyperDash":false}]},{"StartTime":76125.0,"Objects":[{"StartTime":76125.0,"Position":24.0,"HyperDash":false},{"StartTime":76250.0,"Position":103.510704,"HyperDash":false}]},{"StartTime":76375.0,"Objects":[{"StartTime":76375.0,"Position":176.0,"HyperDash":false}]},{"StartTime":76625.0,"Objects":[{"StartTime":76625.0,"Position":348.0,"HyperDash":false}]},{"StartTime":76875.0,"Objects":[{"StartTime":76875.0,"Position":248.0,"HyperDash":false}]},{"StartTime":77125.0,"Objects":[{"StartTime":77125.0,"Position":264.0,"HyperDash":false}]},{"StartTime":77375.0,"Objects":[{"StartTime":77375.0,"Position":324.0,"HyperDash":false}]},{"StartTime":77625.0,"Objects":[{"StartTime":77625.0,"Position":180.0,"HyperDash":false}]},{"StartTime":77875.0,"Objects":[{"StartTime":77875.0,"Position":240.0,"HyperDash":false}]},{"StartTime":78125.0,"Objects":[{"StartTime":78125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":78375.0,"Objects":[{"StartTime":78375.0,"Position":100.0,"HyperDash":false}]},{"StartTime":78625.0,"Objects":[{"StartTime":78625.0,"Position":8.0,"HyperDash":false},{"StartTime":78678.0,"Position":30.0805969,"HyperDash":false},{"StartTime":78732.0,"Position":72.26928,"HyperDash":false},{"StartTime":78785.0,"Position":94.77067,"HyperDash":false},{"StartTime":78874.0,"Position":149.724487,"HyperDash":false}]},{"StartTime":79125.0,"Objects":[{"StartTime":79125.0,"Position":304.0,"HyperDash":false},{"StartTime":79178.0,"Position":282.0235,"HyperDash":false},{"StartTime":79232.0,"Position":238.981018,"HyperDash":false},{"StartTime":79285.0,"Position":222.634567,"HyperDash":false},{"StartTime":79375.0,"Position":162.2755,"HyperDash":false}]},{"StartTime":79625.0,"Objects":[{"StartTime":79625.0,"Position":304.0,"HyperDash":false}]},{"StartTime":79875.0,"Objects":[{"StartTime":79875.0,"Position":460.0,"HyperDash":false}]},{"StartTime":80125.0,"Objects":[{"StartTime":80125.0,"Position":420.0,"HyperDash":false},{"StartTime":80250.0,"Position":340.0,"HyperDash":false}]},{"StartTime":80375.0,"Objects":[{"StartTime":80375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":80625.0,"Objects":[{"StartTime":80625.0,"Position":344.0,"HyperDash":false}]},{"StartTime":80875.0,"Objects":[{"StartTime":80875.0,"Position":168.0,"HyperDash":false}]},{"StartTime":81125.0,"Objects":[{"StartTime":81125.0,"Position":384.0,"HyperDash":false}]},{"StartTime":81375.0,"Objects":[{"StartTime":81375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":81625.0,"Objects":[{"StartTime":81625.0,"Position":168.0,"HyperDash":false}]},{"StartTime":81875.0,"Objects":[{"StartTime":81875.0,"Position":344.0,"HyperDash":false}]},{"StartTime":82125.0,"Objects":[{"StartTime":82125.0,"Position":128.0,"HyperDash":false}]},{"StartTime":82250.0,"Objects":[{"StartTime":82250.0,"Position":48.0,"HyperDash":false},{"StartTime":82303.0,"Position":38.86482,"HyperDash":false},{"StartTime":82357.0,"Position":53.93512,"HyperDash":false},{"StartTime":82410.0,"Position":60.0134125,"HyperDash":false},{"StartTime":82500.0,"Position":124.821884,"HyperDash":false}]},{"StartTime":82625.0,"Objects":[{"StartTime":82625.0,"Position":204.0,"HyperDash":false},{"StartTime":82678.0,"Position":208.888657,"HyperDash":false},{"StartTime":82731.0,"Position":211.78508,"HyperDash":false},{"StartTime":82784.0,"Position":215.863892,"HyperDash":false},{"StartTime":82874.0,"Position":280.821869,"HyperDash":false}]},{"StartTime":83000.0,"Objects":[{"StartTime":83000.0,"Position":352.0,"HyperDash":false},{"StartTime":83053.0,"Position":303.246552,"HyperDash":false},{"StartTime":83107.0,"Position":291.2771,"HyperDash":false},{"StartTime":83160.0,"Position":254.710571,"HyperDash":false},{"StartTime":83250.0,"Position":222.496063,"HyperDash":false}]},{"StartTime":83375.0,"Objects":[{"StartTime":83375.0,"Position":192.0,"HyperDash":false},{"StartTime":83428.0,"Position":152.246567,"HyperDash":false},{"StartTime":83482.0,"Position":112.277092,"HyperDash":false},{"StartTime":83535.0,"Position":87.71058,"HyperDash":false},{"StartTime":83625.0,"Position":62.496067,"HyperDash":false}]},{"StartTime":83875.0,"Objects":[{"StartTime":83875.0,"Position":32.0,"HyperDash":false}]},{"StartTime":84125.0,"Objects":[{"StartTime":84125.0,"Position":172.0,"HyperDash":false}]},{"StartTime":84250.0,"Objects":[{"StartTime":84250.0,"Position":179.0,"HyperDash":false},{"StartTime":84308.0,"Position":278.0,"HyperDash":false},{"StartTime":84367.0,"Position":474.0,"HyperDash":false},{"StartTime":84425.0,"Position":50.0,"HyperDash":false},{"StartTime":84484.0,"Position":458.0,"HyperDash":false},{"StartTime":84542.0,"Position":425.0,"HyperDash":false},{"StartTime":84601.0,"Position":466.0,"HyperDash":false},{"StartTime":84660.0,"Position":56.0,"HyperDash":false},{"StartTime":84718.0,"Position":109.0,"HyperDash":false},{"StartTime":84777.0,"Position":482.0,"HyperDash":false},{"StartTime":84835.0,"Position":147.0,"HyperDash":false},{"StartTime":84894.0,"Position":285.0,"HyperDash":false},{"StartTime":84953.0,"Position":452.0,"HyperDash":false},{"StartTime":85011.0,"Position":419.0,"HyperDash":false},{"StartTime":85070.0,"Position":269.0,"HyperDash":false},{"StartTime":85128.0,"Position":249.0,"HyperDash":false},{"StartTime":85187.0,"Position":233.0,"HyperDash":false},{"StartTime":85246.0,"Position":449.0,"HyperDash":false},{"StartTime":85304.0,"Position":411.0,"HyperDash":false},{"StartTime":85363.0,"Position":75.0,"HyperDash":false},{"StartTime":85421.0,"Position":474.0,"HyperDash":false},{"StartTime":85480.0,"Position":176.0,"HyperDash":false},{"StartTime":85539.0,"Position":1.0,"HyperDash":false},{"StartTime":85597.0,"Position":37.0,"HyperDash":false},{"StartTime":85656.0,"Position":481.0,"HyperDash":false},{"StartTime":85714.0,"Position":375.0,"HyperDash":false},{"StartTime":85773.0,"Position":407.0,"HyperDash":false},{"StartTime":85832.0,"Position":231.0,"HyperDash":false},{"StartTime":85890.0,"Position":338.0,"HyperDash":false},{"StartTime":85949.0,"Position":322.0,"HyperDash":false},{"StartTime":86007.0,"Position":347.0,"HyperDash":false},{"StartTime":86066.0,"Position":365.0,"HyperDash":false},{"StartTime":86125.0,"Position":453.0,"HyperDash":false}]},{"StartTime":86250.0,"Objects":[{"StartTime":86250.0,"Position":486.0,"HyperDash":false},{"StartTime":86304.0,"Position":68.0,"HyperDash":false},{"StartTime":86359.0,"Position":498.0,"HyperDash":false},{"StartTime":86414.0,"Position":164.0,"HyperDash":false},{"StartTime":86468.0,"Position":1.0,"HyperDash":false},{"StartTime":86523.0,"Position":501.0,"HyperDash":false},{"StartTime":86578.0,"Position":82.0,"HyperDash":false},{"StartTime":86632.0,"Position":494.0,"HyperDash":false},{"StartTime":86687.0,"Position":479.0,"HyperDash":false},{"StartTime":86742.0,"Position":373.0,"HyperDash":false},{"StartTime":86796.0,"Position":450.0,"HyperDash":false},{"StartTime":86851.0,"Position":144.0,"HyperDash":false},{"StartTime":86906.0,"Position":365.0,"HyperDash":false},{"StartTime":86960.0,"Position":285.0,"HyperDash":false},{"StartTime":87015.0,"Position":45.0,"HyperDash":false},{"StartTime":87070.0,"Position":65.0,"HyperDash":false},{"StartTime":87125.0,"Position":337.0,"HyperDash":false}]},{"StartTime":88125.0,"Objects":[{"StartTime":88125.0,"Position":256.0,"HyperDash":false},{"StartTime":88178.0,"Position":292.30423,"HyperDash":false},{"StartTime":88232.0,"Position":341.450134,"HyperDash":false},{"StartTime":88285.0,"Position":358.591034,"HyperDash":false},{"StartTime":88375.0,"Position":390.822968,"HyperDash":false}]},{"StartTime":88625.0,"Objects":[{"StartTime":88625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":88875.0,"Objects":[{"StartTime":88875.0,"Position":136.0,"HyperDash":false}]},{"StartTime":89125.0,"Objects":[{"StartTime":89125.0,"Position":8.0,"HyperDash":false},{"StartTime":89178.0,"Position":0.0,"HyperDash":false},{"StartTime":89232.0,"Position":12.7492714,"HyperDash":false},{"StartTime":89285.0,"Position":7.342363,"HyperDash":false},{"StartTime":89375.0,"Position":41.059124,"HyperDash":false}]},{"StartTime":89625.0,"Objects":[{"StartTime":89625.0,"Position":164.0,"HyperDash":false}]},{"StartTime":89875.0,"Objects":[{"StartTime":89875.0,"Position":288.0,"HyperDash":false}]},{"StartTime":90000.0,"Objects":[{"StartTime":90000.0,"Position":288.0,"HyperDash":false}]},{"StartTime":90125.0,"Objects":[{"StartTime":90125.0,"Position":288.0,"HyperDash":false},{"StartTime":90178.0,"Position":307.058655,"HyperDash":false},{"StartTime":90232.0,"Position":366.7033,"HyperDash":false},{"StartTime":90285.0,"Position":400.761932,"HyperDash":false},{"StartTime":90375.0,"Position":434.503052,"HyperDash":false}]},{"StartTime":90625.0,"Objects":[{"StartTime":90625.0,"Position":476.0,"HyperDash":false}]},{"StartTime":90875.0,"Objects":[{"StartTime":90875.0,"Position":332.0,"HyperDash":false}]},{"StartTime":91125.0,"Objects":[{"StartTime":91125.0,"Position":180.0,"HyperDash":false}]},{"StartTime":91375.0,"Objects":[{"StartTime":91375.0,"Position":36.0,"HyperDash":false}]},{"StartTime":91625.0,"Objects":[{"StartTime":91625.0,"Position":56.0,"HyperDash":false}]},{"StartTime":92125.0,"Objects":[{"StartTime":92125.0,"Position":56.0,"HyperDash":false},{"StartTime":92178.0,"Position":78.15752,"HyperDash":false},{"StartTime":92232.0,"Position":134.940643,"HyperDash":false},{"StartTime":92285.0,"Position":142.098145,"HyperDash":false},{"StartTime":92375.0,"Position":212.403366,"HyperDash":false}]},{"StartTime":92625.0,"Objects":[{"StartTime":92625.0,"Position":84.0,"HyperDash":false}]},{"StartTime":92875.0,"Objects":[{"StartTime":92875.0,"Position":220.0,"HyperDash":false}]},{"StartTime":93125.0,"Objects":[{"StartTime":93125.0,"Position":320.0,"HyperDash":false},{"StartTime":93178.0,"Position":369.1575,"HyperDash":false},{"StartTime":93232.0,"Position":398.940643,"HyperDash":false},{"StartTime":93285.0,"Position":408.098145,"HyperDash":false},{"StartTime":93375.0,"Position":476.403381,"HyperDash":false}]},{"StartTime":93625.0,"Objects":[{"StartTime":93625.0,"Position":432.0,"HyperDash":false}]},{"StartTime":93875.0,"Objects":[{"StartTime":93875.0,"Position":296.0,"HyperDash":false}]},{"StartTime":94000.0,"Objects":[{"StartTime":94000.0,"Position":296.0,"HyperDash":false}]},{"StartTime":94125.0,"Objects":[{"StartTime":94125.0,"Position":296.0,"HyperDash":false},{"StartTime":94178.0,"Position":273.1039,"HyperDash":false},{"StartTime":94232.0,"Position":244.445969,"HyperDash":false},{"StartTime":94285.0,"Position":219.02182,"HyperDash":false},{"StartTime":94374.0,"Position":170.471848,"HyperDash":false}]},{"StartTime":94625.0,"Objects":[{"StartTime":94625.0,"Position":216.0,"HyperDash":false},{"StartTime":94678.0,"Position":259.7602,"HyperDash":false},{"StartTime":94732.0,"Position":282.299927,"HyperDash":false},{"StartTime":94785.0,"Position":294.678436,"HyperDash":false},{"StartTime":94875.0,"Position":341.528168,"HyperDash":false}]},{"StartTime":95000.0,"Objects":[{"StartTime":95000.0,"Position":341.0,"HyperDash":false}]},{"StartTime":95125.0,"Objects":[{"StartTime":95125.0,"Position":341.0,"HyperDash":false},{"StartTime":95178.0,"Position":347.282532,"HyperDash":false},{"StartTime":95232.0,"Position":344.6459,"HyperDash":false},{"StartTime":95285.0,"Position":339.928436,"HyperDash":false},{"StartTime":95375.0,"Position":361.200684,"HyperDash":false}]},{"StartTime":95625.0,"Objects":[{"StartTime":95625.0,"Position":171.0,"HyperDash":false},{"StartTime":95678.0,"Position":158.717453,"HyperDash":false},{"StartTime":95732.0,"Position":169.354111,"HyperDash":false},{"StartTime":95785.0,"Position":172.071564,"HyperDash":false},{"StartTime":95875.0,"Position":150.799316,"HyperDash":false}]},{"StartTime":96125.0,"Objects":[{"StartTime":96125.0,"Position":43.0,"HyperDash":false}]},{"StartTime":96375.0,"Objects":[{"StartTime":96375.0,"Position":81.0,"HyperDash":false}]},{"StartTime":96625.0,"Objects":[{"StartTime":96625.0,"Position":169.0,"HyperDash":false}]},{"StartTime":96875.0,"Objects":[{"StartTime":96875.0,"Position":304.0,"HyperDash":false},{"StartTime":96937.0,"Position":333.433136,"HyperDash":false},{"StartTime":97000.0,"Position":385.325043,"HyperDash":false},{"StartTime":97062.0,"Position":379.667,"HyperDash":false},{"StartTime":97125.0,"Position":401.778076,"HyperDash":false},{"StartTime":97187.0,"Position":418.125366,"HyperDash":false},{"StartTime":97250.0,"Position":403.005768,"HyperDash":false},{"StartTime":97312.0,"Position":375.9013,"HyperDash":false},{"StartTime":97375.0,"Position":343.426239,"HyperDash":false},{"StartTime":97437.0,"Position":382.9013,"HyperDash":false},{"StartTime":97499.0,"Position":392.005768,"HyperDash":false},{"StartTime":97561.0,"Position":388.066345,"HyperDash":false},{"StartTime":97624.0,"Position":401.778076,"HyperDash":false},{"StartTime":97677.0,"Position":380.074066,"HyperDash":false},{"StartTime":97731.0,"Position":366.190063,"HyperDash":false},{"StartTime":97785.0,"Position":348.305481,"HyperDash":false},{"StartTime":97874.0,"Position":304.0,"HyperDash":false}]},{"StartTime":98125.0,"Objects":[{"StartTime":98125.0,"Position":240.0,"HyperDash":false},{"StartTime":98187.0,"Position":220.193451,"HyperDash":false},{"StartTime":98250.0,"Position":179.67662,"HyperDash":false},{"StartTime":98312.0,"Position":167.455551,"HyperDash":false},{"StartTime":98375.0,"Position":115.407051,"HyperDash":false},{"StartTime":98428.0,"Position":97.24337,"HyperDash":false},{"StartTime":98482.0,"Position":115.416969,"HyperDash":false},{"StartTime":98535.0,"Position":122.237556,"HyperDash":false},{"StartTime":98624.0,"Position":166.963364,"HyperDash":false}]},{"StartTime":98875.0,"Objects":[{"StartTime":98875.0,"Position":240.0,"HyperDash":false},{"StartTime":98937.0,"Position":273.329651,"HyperDash":false},{"StartTime":99000.0,"Position":306.601349,"HyperDash":false},{"StartTime":99062.0,"Position":324.816467,"HyperDash":false},{"StartTime":99125.0,"Position":363.818481,"HyperDash":false},{"StartTime":99178.0,"Position":391.8492,"HyperDash":false},{"StartTime":99232.0,"Position":363.507568,"HyperDash":false},{"StartTime":99285.0,"Position":349.543182,"HyperDash":false},{"StartTime":99374.0,"Position":311.711731,"HyperDash":false}]},{"StartTime":99625.0,"Objects":[{"StartTime":99625.0,"Position":180.0,"HyperDash":false},{"StartTime":99678.0,"Position":143.011124,"HyperDash":false},{"StartTime":99732.0,"Position":113.192444,"HyperDash":false},{"StartTime":99785.0,"Position":79.4256439,"HyperDash":false},{"StartTime":99874.0,"Position":45.3982735,"HyperDash":false}]},{"StartTime":100125.0,"Objects":[{"StartTime":100125.0,"Position":48.0,"HyperDash":false},{"StartTime":100178.0,"Position":75.85622,"HyperDash":false},{"StartTime":100231.0,"Position":116.712425,"HyperDash":false},{"StartTime":100284.0,"Position":156.568634,"HyperDash":false},{"StartTime":100374.0,"Position":202.3622,"HyperDash":false}]},{"StartTime":100625.0,"Objects":[{"StartTime":100625.0,"Position":348.0,"HyperDash":false},{"StartTime":100678.0,"Position":383.8562,"HyperDash":false},{"StartTime":100731.0,"Position":402.712433,"HyperDash":false},{"StartTime":100784.0,"Position":456.568634,"HyperDash":false},{"StartTime":100874.0,"Position":502.362183,"HyperDash":false}]},{"StartTime":101125.0,"Objects":[{"StartTime":101125.0,"Position":504.0,"HyperDash":false},{"StartTime":101178.0,"Position":488.1438,"HyperDash":false},{"StartTime":101231.0,"Position":446.287567,"HyperDash":false},{"StartTime":101284.0,"Position":423.431366,"HyperDash":false},{"StartTime":101374.0,"Position":349.637817,"HyperDash":false}]},{"StartTime":101625.0,"Objects":[{"StartTime":101625.0,"Position":204.0,"HyperDash":false},{"StartTime":101678.0,"Position":156.143784,"HyperDash":false},{"StartTime":101731.0,"Position":133.287567,"HyperDash":false},{"StartTime":101784.0,"Position":117.431358,"HyperDash":false},{"StartTime":101874.0,"Position":49.6378021,"HyperDash":false}]},{"StartTime":102000.0,"Objects":[{"StartTime":102000.0,"Position":49.0,"HyperDash":false}]},{"StartTime":102125.0,"Objects":[{"StartTime":102125.0,"Position":49.0,"HyperDash":false}]},{"StartTime":102625.0,"Objects":[{"StartTime":102625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":102875.0,"Objects":[{"StartTime":102875.0,"Position":384.0,"HyperDash":false}]},{"StartTime":103125.0,"Objects":[{"StartTime":103125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":103625.0,"Objects":[{"StartTime":103625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":103875.0,"Objects":[{"StartTime":103875.0,"Position":128.0,"HyperDash":false}]},{"StartTime":104125.0,"Objects":[{"StartTime":104125.0,"Position":256.0,"HyperDash":false},{"StartTime":104178.0,"Position":272.994537,"HyperDash":false},{"StartTime":104232.0,"Position":332.6524,"HyperDash":false},{"StartTime":104285.0,"Position":355.331024,"HyperDash":false},{"StartTime":104375.0,"Position":402.7333,"HyperDash":false}]},{"StartTime":104625.0,"Objects":[{"StartTime":104625.0,"Position":492.0,"HyperDash":false}]},{"StartTime":104875.0,"Objects":[{"StartTime":104875.0,"Position":332.0,"HyperDash":false}]},{"StartTime":105125.0,"Objects":[{"StartTime":105125.0,"Position":256.0,"HyperDash":false},{"StartTime":105178.0,"Position":241.889923,"HyperDash":false},{"StartTime":105232.0,"Position":211.080078,"HyperDash":false},{"StartTime":105285.0,"Position":144.258759,"HyperDash":false},{"StartTime":105374.0,"Position":109.266708,"HyperDash":false}]},{"StartTime":105625.0,"Objects":[{"StartTime":105625.0,"Position":20.0,"HyperDash":false}]},{"StartTime":105875.0,"Objects":[{"StartTime":105875.0,"Position":180.0,"HyperDash":false}]},{"StartTime":106125.0,"Objects":[{"StartTime":106125.0,"Position":368.0,"HyperDash":false},{"StartTime":106178.0,"Position":376.172974,"HyperDash":false},{"StartTime":106231.0,"Position":410.418945,"HyperDash":false},{"StartTime":106284.0,"Position":421.151764,"HyperDash":false},{"StartTime":106374.0,"Position":416.485779,"HyperDash":false}]},{"StartTime":106625.0,"Objects":[{"StartTime":106625.0,"Position":220.0,"HyperDash":false},{"StartTime":106678.0,"Position":241.054184,"HyperDash":false},{"StartTime":106731.0,"Position":282.792328,"HyperDash":false},{"StartTime":106784.0,"Position":301.047577,"HyperDash":false},{"StartTime":106874.0,"Position":349.247284,"HyperDash":false}]},{"StartTime":107125.0,"Objects":[{"StartTime":107125.0,"Position":144.0,"HyperDash":false},{"StartTime":107178.0,"Position":125.932152,"HyperDash":false},{"StartTime":107232.0,"Position":88.3730545,"HyperDash":false},{"StartTime":107285.0,"Position":97.8173752,"HyperDash":false},{"StartTime":107375.0,"Position":95.51424,"HyperDash":false}]},{"StartTime":107625.0,"Objects":[{"StartTime":107625.0,"Position":292.0,"HyperDash":false},{"StartTime":107678.0,"Position":279.032471,"HyperDash":false},{"StartTime":107732.0,"Position":263.904663,"HyperDash":false},{"StartTime":107785.0,"Position":230.72316,"HyperDash":false},{"StartTime":107875.0,"Position":162.752716,"HyperDash":false}]},{"StartTime":108125.0,"Objects":[{"StartTime":108125.0,"Position":44.0,"HyperDash":false},{"StartTime":108178.0,"Position":95.98508,"HyperDash":false},{"StartTime":108232.0,"Position":110.3604,"HyperDash":false},{"StartTime":108285.0,"Position":123.706589,"HyperDash":false},{"StartTime":108374.0,"Position":169.6919,"HyperDash":false}]},{"StartTime":108625.0,"Objects":[{"StartTime":108625.0,"Position":304.0,"HyperDash":false}]},{"StartTime":108875.0,"Objects":[{"StartTime":108875.0,"Position":408.0,"HyperDash":false}]},{"StartTime":109125.0,"Objects":[{"StartTime":109125.0,"Position":468.0,"HyperDash":false},{"StartTime":109178.0,"Position":439.149963,"HyperDash":false},{"StartTime":109232.0,"Position":396.891418,"HyperDash":false},{"StartTime":109285.0,"Position":370.5935,"HyperDash":false},{"StartTime":109375.0,"Position":342.308075,"HyperDash":false}]},{"StartTime":109625.0,"Objects":[{"StartTime":109625.0,"Position":208.0,"HyperDash":false}]},{"StartTime":109875.0,"Objects":[{"StartTime":109875.0,"Position":104.0,"HyperDash":false}]},{"StartTime":110125.0,"Objects":[{"StartTime":110125.0,"Position":256.0,"HyperDash":false},{"StartTime":110178.0,"Position":239.263885,"HyperDash":false},{"StartTime":110232.0,"Position":204.098785,"HyperDash":false},{"StartTime":110285.0,"Position":187.362686,"HyperDash":false},{"StartTime":110375.0,"Position":148.7542,"HyperDash":false}]},{"StartTime":110625.0,"Objects":[{"StartTime":110625.0,"Position":256.0,"HyperDash":false},{"StartTime":110678.0,"Position":283.827423,"HyperDash":false},{"StartTime":110731.0,"Position":319.654846,"HyperDash":false},{"StartTime":110784.0,"Position":325.482239,"HyperDash":false},{"StartTime":110874.0,"Position":363.2458,"HyperDash":false}]},{"StartTime":111125.0,"Objects":[{"StartTime":111125.0,"Position":208.0,"HyperDash":false},{"StartTime":111178.0,"Position":185.263885,"HyperDash":false},{"StartTime":111232.0,"Position":170.098785,"HyperDash":false},{"StartTime":111285.0,"Position":123.362686,"HyperDash":false},{"StartTime":111375.0,"Position":100.754196,"HyperDash":false}]},{"StartTime":111625.0,"Objects":[{"StartTime":111625.0,"Position":304.0,"HyperDash":false},{"StartTime":111678.0,"Position":318.7361,"HyperDash":false},{"StartTime":111732.0,"Position":353.901184,"HyperDash":false},{"StartTime":111785.0,"Position":357.6373,"HyperDash":false},{"StartTime":111875.0,"Position":411.2458,"HyperDash":false}]},{"StartTime":112125.0,"Objects":[{"StartTime":112125.0,"Position":252.0,"HyperDash":false}]},{"StartTime":112375.0,"Objects":[{"StartTime":112375.0,"Position":112.0,"HyperDash":false}]},{"StartTime":112625.0,"Objects":[{"StartTime":112625.0,"Position":72.0,"HyperDash":false}]},{"StartTime":112875.0,"Objects":[{"StartTime":112875.0,"Position":158.0,"HyperDash":false},{"StartTime":112937.0,"Position":180.39856,"HyperDash":false},{"StartTime":113000.0,"Position":253.684036,"HyperDash":false},{"StartTime":113062.0,"Position":263.862976,"HyperDash":false},{"StartTime":113125.0,"Position":289.459473,"HyperDash":false},{"StartTime":113187.0,"Position":294.857574,"HyperDash":false},{"StartTime":113250.0,"Position":301.491974,"HyperDash":false},{"StartTime":113312.0,"Position":306.150818,"HyperDash":false},{"StartTime":113375.0,"Position":278.112,"HyperDash":false},{"StartTime":113437.0,"Position":308.150818,"HyperDash":false},{"StartTime":113500.0,"Position":291.538177,"HyperDash":false},{"StartTime":113562.0,"Position":288.857574,"HyperDash":false},{"StartTime":113625.0,"Position":289.160065,"HyperDash":false},{"StartTime":113678.0,"Position":275.785217,"HyperDash":false},{"StartTime":113732.0,"Position":261.88623,"HyperDash":false},{"StartTime":113785.0,"Position":219.895935,"HyperDash":false},{"StartTime":113874.0,"Position":158.0,"HyperDash":false}]},{"StartTime":114125.0,"Objects":[{"StartTime":114125.0,"Position":176.0,"HyperDash":false},{"StartTime":114187.0,"Position":215.46962,"HyperDash":false},{"StartTime":114250.0,"Position":243.459351,"HyperDash":false},{"StartTime":114312.0,"Position":280.9655,"HyperDash":false},{"StartTime":114375.0,"Position":311.184082,"HyperDash":false},{"StartTime":114428.0,"Position":345.321442,"HyperDash":false},{"StartTime":114482.0,"Position":372.3753,"HyperDash":false},{"StartTime":114535.0,"Position":414.472534,"HyperDash":false},{"StartTime":114624.0,"Position":431.115143,"HyperDash":false}]},{"StartTime":114875.0,"Objects":[{"StartTime":114875.0,"Position":328.0,"HyperDash":false},{"StartTime":114937.0,"Position":303.669556,"HyperDash":false},{"StartTime":115000.0,"Position":279.312225,"HyperDash":false},{"StartTime":115062.0,"Position":265.2286,"HyperDash":false},{"StartTime":115125.0,"Position":258.051422,"HyperDash":false},{"StartTime":115178.0,"Position":262.0706,"HyperDash":false},{"StartTime":115231.0,"Position":286.7301,"HyperDash":false},{"StartTime":115284.0,"Position":315.1607,"HyperDash":false},{"StartTime":115374.0,"Position":349.780029,"HyperDash":false}]},{"StartTime":115625.0,"Objects":[{"StartTime":115625.0,"Position":488.0,"HyperDash":false},{"StartTime":115678.0,"Position":480.653168,"HyperDash":false},{"StartTime":115732.0,"Position":483.186554,"HyperDash":false},{"StartTime":115785.0,"Position":463.839722,"HyperDash":false},{"StartTime":115875.0,"Position":458.062073,"HyperDash":false}]},{"StartTime":116125.0,"Objects":[{"StartTime":116125.0,"Position":416.0,"HyperDash":false}]},{"StartTime":116375.0,"Objects":[{"StartTime":116375.0,"Position":288.0,"HyperDash":false}]},{"StartTime":116625.0,"Objects":[{"StartTime":116625.0,"Position":164.0,"HyperDash":false}]},{"StartTime":116875.0,"Objects":[{"StartTime":116875.0,"Position":36.0,"HyperDash":false}]},{"StartTime":117125.0,"Objects":[{"StartTime":117125.0,"Position":104.0,"HyperDash":false}]},{"StartTime":117375.0,"Objects":[{"StartTime":117375.0,"Position":232.0,"HyperDash":false}]},{"StartTime":117625.0,"Objects":[{"StartTime":117625.0,"Position":356.0,"HyperDash":false}]},{"StartTime":117875.0,"Objects":[{"StartTime":117875.0,"Position":484.0,"HyperDash":false}]},{"StartTime":118125.0,"Objects":[{"StartTime":118125.0,"Position":356.0,"HyperDash":false}]},{"StartTime":128125.0,"Objects":[{"StartTime":128125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":128250.0,"Objects":[{"StartTime":128250.0,"Position":256.0,"HyperDash":false}]},{"StartTime":128500.0,"Objects":[{"StartTime":128500.0,"Position":336.0,"HyperDash":false}]},{"StartTime":128625.0,"Objects":[{"StartTime":128625.0,"Position":336.0,"HyperDash":false}]},{"StartTime":128875.0,"Objects":[{"StartTime":128875.0,"Position":400.0,"HyperDash":false}]},{"StartTime":129000.0,"Objects":[{"StartTime":129000.0,"Position":400.0,"HyperDash":false}]},{"StartTime":129250.0,"Objects":[{"StartTime":129250.0,"Position":492.0,"HyperDash":false}]},{"StartTime":129375.0,"Objects":[{"StartTime":129375.0,"Position":492.0,"HyperDash":false}]},{"StartTime":129625.0,"Objects":[{"StartTime":129625.0,"Position":440.0,"HyperDash":false},{"StartTime":129678.0,"Position":420.699738,"HyperDash":false},{"StartTime":129731.0,"Position":376.399475,"HyperDash":false},{"StartTime":129784.0,"Position":327.099243,"HyperDash":false},{"StartTime":129874.0,"Position":283.551636,"HyperDash":false}]},{"StartTime":130125.0,"Objects":[{"StartTime":130125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":130250.0,"Objects":[{"StartTime":130250.0,"Position":256.0,"HyperDash":false}]},{"StartTime":130500.0,"Objects":[{"StartTime":130500.0,"Position":176.0,"HyperDash":false}]},{"StartTime":130625.0,"Objects":[{"StartTime":130625.0,"Position":176.0,"HyperDash":false}]},{"StartTime":130875.0,"Objects":[{"StartTime":130875.0,"Position":112.0,"HyperDash":false}]},{"StartTime":131000.0,"Objects":[{"StartTime":131000.0,"Position":112.0,"HyperDash":false}]},{"StartTime":131250.0,"Objects":[{"StartTime":131250.0,"Position":20.0,"HyperDash":false}]},{"StartTime":131375.0,"Objects":[{"StartTime":131375.0,"Position":20.0,"HyperDash":false}]},{"StartTime":131625.0,"Objects":[{"StartTime":131625.0,"Position":72.0,"HyperDash":false},{"StartTime":131678.0,"Position":85.16705,"HyperDash":false},{"StartTime":131732.0,"Position":139.959915,"HyperDash":false},{"StartTime":131785.0,"Position":179.126953,"HyperDash":false},{"StartTime":131875.0,"Position":228.44838,"HyperDash":false}]},{"StartTime":132125.0,"Objects":[{"StartTime":132125.0,"Position":408.0,"HyperDash":false},{"StartTime":132187.0,"Position":432.7211,"HyperDash":false},{"StartTime":132250.0,"Position":463.48645,"HyperDash":false},{"StartTime":132312.0,"Position":484.605652,"HyperDash":false},{"StartTime":132375.0,"Position":511.913147,"HyperDash":false},{"StartTime":132437.0,"Position":511.3131,"HyperDash":false},{"StartTime":132500.0,"Position":512.0,"HyperDash":false},{"StartTime":132562.0,"Position":512.0,"HyperDash":false},{"StartTime":132625.0,"Position":491.9296,"HyperDash":false},{"StartTime":132687.0,"Position":477.671265,"HyperDash":false},{"StartTime":132750.0,"Position":455.869171,"HyperDash":false},{"StartTime":132812.0,"Position":413.826355,"HyperDash":false},{"StartTime":132875.0,"Position":366.962769,"HyperDash":false},{"StartTime":132937.0,"Position":340.888336,"HyperDash":false},{"StartTime":133000.0,"Position":273.617157,"HyperDash":false},{"StartTime":133062.0,"Position":263.5604,"HyperDash":false},{"StartTime":133125.0,"Position":210.586578,"HyperDash":false},{"StartTime":133187.0,"Position":176.064163,"HyperDash":false},{"StartTime":133250.0,"Position":127.187744,"HyperDash":false},{"StartTime":133312.0,"Position":131.32103,"HyperDash":false},{"StartTime":133375.0,"Position":102.106659,"HyperDash":false},{"StartTime":133437.0,"Position":101.403084,"HyperDash":false},{"StartTime":133500.0,"Position":84.85893,"HyperDash":false},{"StartTime":133562.0,"Position":83.863945,"HyperDash":false},{"StartTime":133625.0,"Position":119.323433,"HyperDash":false},{"StartTime":133687.0,"Position":159.490738,"HyperDash":false},{"StartTime":133750.0,"Position":179.476852,"HyperDash":false},{"StartTime":133812.0,"Position":207.3787,"HyperDash":false},{"StartTime":133875.0,"Position":256.6099,"HyperDash":false},{"StartTime":133937.0,"Position":289.899384,"HyperDash":false},{"StartTime":134000.0,"Position":322.431061,"HyperDash":false},{"StartTime":134062.0,"Position":371.9527,"HyperDash":false},{"StartTime":134125.0,"Position":392.617126,"HyperDash":false},{"StartTime":134187.0,"Position":422.877838,"HyperDash":false},{"StartTime":134250.0,"Position":425.129883,"HyperDash":false},{"StartTime":134312.0,"Position":404.693054,"HyperDash":false},{"StartTime":134375.0,"Position":409.929779,"HyperDash":false},{"StartTime":134437.0,"Position":384.0832,"HyperDash":false},{"StartTime":134500.0,"Position":354.885651,"HyperDash":false},{"StartTime":134562.0,"Position":326.547424,"HyperDash":false},{"StartTime":134625.0,"Position":301.508575,"HyperDash":false},{"StartTime":134687.0,"Position":255.1601,"HyperDash":false},{"StartTime":134750.0,"Position":222.486877,"HyperDash":false},{"StartTime":134812.0,"Position":183.853729,"HyperDash":false},{"StartTime":134875.0,"Position":145.138245,"HyperDash":false},{"StartTime":134937.0,"Position":107.848343,"HyperDash":false},{"StartTime":135000.0,"Position":58.21479,"HyperDash":false},{"StartTime":135062.0,"Position":57.82658,"HyperDash":false},{"StartTime":135125.0,"Position":20.1227779,"HyperDash":false},{"StartTime":135187.0,"Position":0.0,"HyperDash":false},{"StartTime":135250.0,"Position":0.0,"HyperDash":false},{"StartTime":135312.0,"Position":0.0,"HyperDash":false},{"StartTime":135375.0,"Position":0.05981236,"HyperDash":false},{"StartTime":135428.0,"Position":14.5409756,"HyperDash":false},{"StartTime":135482.0,"Position":36.1827965,"HyperDash":false},{"StartTime":135535.0,"Position":37.5372772,"HyperDash":false},{"StartTime":135625.0,"Position":103.892265,"HyperDash":false}]},{"StartTime":135875.0,"Objects":[{"StartTime":135875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":136000.0,"Objects":[{"StartTime":136000.0,"Position":256.0,"HyperDash":false}]},{"StartTime":136125.0,"Objects":[{"StartTime":136125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":136375.0,"Objects":[{"StartTime":136375.0,"Position":136.0,"HyperDash":false}]},{"StartTime":136625.0,"Objects":[{"StartTime":136625.0,"Position":132.0,"HyperDash":false}]},{"StartTime":136750.0,"Objects":[{"StartTime":136750.0,"Position":133.0,"HyperDash":false}]},{"StartTime":137000.0,"Objects":[{"StartTime":137000.0,"Position":256.0,"HyperDash":false}]},{"StartTime":137125.0,"Objects":[{"StartTime":137125.0,"Position":255.0,"HyperDash":false}]},{"StartTime":137250.0,"Objects":[{"StartTime":137250.0,"Position":256.0,"HyperDash":false}]},{"StartTime":137375.0,"Objects":[{"StartTime":137375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":137625.0,"Objects":[{"StartTime":137625.0,"Position":380.0,"HyperDash":false}]},{"StartTime":137875.0,"Objects":[{"StartTime":137875.0,"Position":376.0,"HyperDash":false}]},{"StartTime":138125.0,"Objects":[{"StartTime":138125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":138375.0,"Objects":[{"StartTime":138375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":138625.0,"Objects":[{"StartTime":138625.0,"Position":144.0,"HyperDash":false}]},{"StartTime":138750.0,"Objects":[{"StartTime":138750.0,"Position":144.0,"HyperDash":false}]},{"StartTime":139000.0,"Objects":[{"StartTime":139000.0,"Position":256.0,"HyperDash":false}]},{"StartTime":139125.0,"Objects":[{"StartTime":139125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":139250.0,"Objects":[{"StartTime":139250.0,"Position":256.0,"HyperDash":false}]},{"StartTime":139375.0,"Objects":[{"StartTime":139375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":139625.0,"Objects":[{"StartTime":139625.0,"Position":368.0,"HyperDash":false}]},{"StartTime":139875.0,"Objects":[{"StartTime":139875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":140000.0,"Objects":[{"StartTime":140000.0,"Position":256.0,"HyperDash":false}]},{"StartTime":140125.0,"Objects":[{"StartTime":140125.0,"Position":256.0,"HyperDash":false},{"StartTime":140178.0,"Position":227.121277,"HyperDash":false},{"StartTime":140232.0,"Position":201.278854,"HyperDash":false},{"StartTime":140285.0,"Position":210.432343,"HyperDash":false},{"StartTime":140374.0,"Position":256.095947,"HyperDash":false}]},{"StartTime":140625.0,"Objects":[{"StartTime":140625.0,"Position":332.0,"HyperDash":false}]},{"StartTime":140750.0,"Objects":[{"StartTime":140750.0,"Position":332.0,"HyperDash":false}]},{"StartTime":141000.0,"Objects":[{"StartTime":141000.0,"Position":332.0,"HyperDash":false}]},{"StartTime":141125.0,"Objects":[{"StartTime":141125.0,"Position":332.0,"HyperDash":false}]},{"StartTime":141250.0,"Objects":[{"StartTime":141250.0,"Position":332.0,"HyperDash":false}]},{"StartTime":141375.0,"Objects":[{"StartTime":141375.0,"Position":332.0,"HyperDash":false}]},{"StartTime":141625.0,"Objects":[{"StartTime":141625.0,"Position":180.0,"HyperDash":false}]},{"StartTime":141875.0,"Objects":[{"StartTime":141875.0,"Position":180.0,"HyperDash":false}]},{"StartTime":142125.0,"Objects":[{"StartTime":142125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":142375.0,"Objects":[{"StartTime":142375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":142625.0,"Objects":[{"StartTime":142625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":142750.0,"Objects":[{"StartTime":142750.0,"Position":256.0,"HyperDash":false}]},{"StartTime":143000.0,"Objects":[{"StartTime":143000.0,"Position":256.0,"HyperDash":false}]},{"StartTime":143125.0,"Objects":[{"StartTime":143125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":143250.0,"Objects":[{"StartTime":143250.0,"Position":256.0,"HyperDash":false}]},{"StartTime":143375.0,"Objects":[{"StartTime":143375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":143625.0,"Objects":[{"StartTime":143625.0,"Position":188.0,"HyperDash":false}]},{"StartTime":143875.0,"Objects":[{"StartTime":143875.0,"Position":324.0,"HyperDash":false}]},{"StartTime":144000.0,"Objects":[{"StartTime":144000.0,"Position":324.0,"HyperDash":false}]},{"StartTime":144125.0,"Objects":[{"StartTime":144125.0,"Position":324.0,"HyperDash":false},{"StartTime":144178.0,"Position":375.919983,"HyperDash":false},{"StartTime":144232.0,"Position":388.48,"HyperDash":false},{"StartTime":144285.0,"Position":424.4,"HyperDash":false},{"StartTime":144375.0,"Position":484.0,"HyperDash":false}]},{"StartTime":144625.0,"Objects":[{"StartTime":144625.0,"Position":392.0,"HyperDash":false}]},{"StartTime":144750.0,"Objects":[{"StartTime":144750.0,"Position":392.0,"HyperDash":false}]},{"StartTime":145000.0,"Objects":[{"StartTime":145000.0,"Position":324.0,"HyperDash":false}]},{"StartTime":145125.0,"Objects":[{"StartTime":145125.0,"Position":324.0,"HyperDash":false}]},{"StartTime":145250.0,"Objects":[{"StartTime":145250.0,"Position":324.0,"HyperDash":false}]},{"StartTime":145375.0,"Objects":[{"StartTime":145375.0,"Position":324.0,"HyperDash":false}]},{"StartTime":145625.0,"Objects":[{"StartTime":145625.0,"Position":188.0,"HyperDash":false}]},{"StartTime":145875.0,"Objects":[{"StartTime":145875.0,"Position":120.0,"HyperDash":false}]},{"StartTime":146125.0,"Objects":[{"StartTime":146125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":146375.0,"Objects":[{"StartTime":146375.0,"Position":256.0,"HyperDash":false}]},{"StartTime":146625.0,"Objects":[{"StartTime":146625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":146750.0,"Objects":[{"StartTime":146750.0,"Position":256.0,"HyperDash":false}]},{"StartTime":147000.0,"Objects":[{"StartTime":147000.0,"Position":176.0,"HyperDash":false}]},{"StartTime":147125.0,"Objects":[{"StartTime":147125.0,"Position":176.0,"HyperDash":false}]},{"StartTime":147250.0,"Objects":[{"StartTime":147250.0,"Position":176.0,"HyperDash":false}]},{"StartTime":147375.0,"Objects":[{"StartTime":147375.0,"Position":176.0,"HyperDash":false}]},{"StartTime":147625.0,"Objects":[{"StartTime":147625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":147875.0,"Objects":[{"StartTime":147875.0,"Position":336.0,"HyperDash":false}]},{"StartTime":148000.0,"Objects":[{"StartTime":148000.0,"Position":336.0,"HyperDash":false}]},{"StartTime":148125.0,"Objects":[{"StartTime":148125.0,"Position":336.0,"HyperDash":false},{"StartTime":148178.0,"Position":375.538025,"HyperDash":false},{"StartTime":148231.0,"Position":390.979462,"HyperDash":false},{"StartTime":148284.0,"Position":386.895447,"HyperDash":false},{"StartTime":148374.0,"Position":370.6822,"HyperDash":false}]},{"StartTime":148625.0,"Objects":[{"StartTime":148625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":148750.0,"Objects":[{"StartTime":148750.0,"Position":240.0,"HyperDash":false}]},{"StartTime":149000.0,"Objects":[{"StartTime":149000.0,"Position":240.0,"HyperDash":false}]},{"StartTime":149125.0,"Objects":[{"StartTime":149125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":149250.0,"Objects":[{"StartTime":149250.0,"Position":272.0,"HyperDash":false}]},{"StartTime":149375.0,"Objects":[{"StartTime":149375.0,"Position":288.0,"HyperDash":false}]},{"StartTime":149625.0,"Objects":[{"StartTime":149625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":149875.0,"Objects":[{"StartTime":149875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":150125.0,"Objects":[{"StartTime":150125.0,"Position":116.0,"HyperDash":false}]},{"StartTime":150250.0,"Objects":[{"StartTime":150250.0,"Position":120.0,"HyperDash":false}]},{"StartTime":150375.0,"Objects":[{"StartTime":150375.0,"Position":132.0,"HyperDash":false}]},{"StartTime":150500.0,"Objects":[{"StartTime":150500.0,"Position":152.0,"HyperDash":false}]},{"StartTime":150625.0,"Objects":[{"StartTime":150625.0,"Position":176.0,"HyperDash":false}]},{"StartTime":150750.0,"Objects":[{"StartTime":150750.0,"Position":208.0,"HyperDash":false}]},{"StartTime":150875.0,"Objects":[{"StartTime":150875.0,"Position":232.0,"HyperDash":false}]},{"StartTime":151000.0,"Objects":[{"StartTime":151000.0,"Position":248.0,"HyperDash":false}]},{"StartTime":151125.0,"Objects":[{"StartTime":151125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":151250.0,"Objects":[{"StartTime":151250.0,"Position":260.0,"HyperDash":false}]},{"StartTime":151375.0,"Objects":[{"StartTime":151375.0,"Position":272.0,"HyperDash":false}]},{"StartTime":151500.0,"Objects":[{"StartTime":151500.0,"Position":292.0,"HyperDash":false}]},{"StartTime":151625.0,"Objects":[{"StartTime":151625.0,"Position":316.0,"HyperDash":false}]},{"StartTime":151750.0,"Objects":[{"StartTime":151750.0,"Position":348.0,"HyperDash":false}]},{"StartTime":151875.0,"Objects":[{"StartTime":151875.0,"Position":372.0,"HyperDash":false}]},{"StartTime":152000.0,"Objects":[{"StartTime":152000.0,"Position":388.0,"HyperDash":false}]},{"StartTime":152125.0,"Objects":[{"StartTime":152125.0,"Position":404.0,"HyperDash":false},{"StartTime":152178.0,"Position":429.642151,"HyperDash":false},{"StartTime":152232.0,"Position":425.184479,"HyperDash":false},{"StartTime":152285.0,"Position":392.507416,"HyperDash":false},{"StartTime":152375.0,"Position":342.072266,"HyperDash":false}]},{"StartTime":152625.0,"Objects":[{"StartTime":152625.0,"Position":108.0,"HyperDash":false},{"StartTime":152678.0,"Position":112.349617,"HyperDash":false},{"StartTime":152732.0,"Position":112.903786,"HyperDash":false},{"StartTime":152785.0,"Position":119.761673,"HyperDash":false},{"StartTime":152874.0,"Position":169.927719,"HyperDash":false}]},{"StartTime":153125.0,"Objects":[{"StartTime":153125.0,"Position":256.0,"HyperDash":false},{"StartTime":153250.0,"Position":256.0,"HyperDash":false}]},{"StartTime":153375.0,"Objects":[{"StartTime":153375.0,"Position":256.0,"HyperDash":false},{"StartTime":153437.0,"Position":269.0,"HyperDash":false},{"StartTime":153500.0,"Position":241.0,"HyperDash":false},{"StartTime":153562.0,"Position":247.0,"HyperDash":false},{"StartTime":153625.0,"Position":256.0,"HyperDash":false},{"StartTime":153678.0,"Position":244.0,"HyperDash":false},{"StartTime":153732.0,"Position":258.0,"HyperDash":false},{"StartTime":153785.0,"Position":240.0,"HyperDash":false},{"StartTime":153875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":154125.0,"Objects":[{"StartTime":154125.0,"Position":360.0,"HyperDash":false}]},{"StartTime":154250.0,"Objects":[{"StartTime":154250.0,"Position":360.0,"HyperDash":false}]},{"StartTime":154375.0,"Objects":[{"StartTime":154375.0,"Position":360.0,"HyperDash":false}]},{"StartTime":154625.0,"Objects":[{"StartTime":154625.0,"Position":256.0,"HyperDash":false}]},{"StartTime":154750.0,"Objects":[{"StartTime":154750.0,"Position":256.0,"HyperDash":false}]},{"StartTime":154875.0,"Objects":[{"StartTime":154875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":155125.0,"Objects":[{"StartTime":155125.0,"Position":154.0,"HyperDash":false}]},{"StartTime":155250.0,"Objects":[{"StartTime":155250.0,"Position":154.0,"HyperDash":false}]},{"StartTime":155375.0,"Objects":[{"StartTime":155375.0,"Position":155.0,"HyperDash":false},{"StartTime":155437.0,"Position":134.040146,"HyperDash":false},{"StartTime":155500.0,"Position":134.7992,"HyperDash":false},{"StartTime":155562.0,"Position":112.444061,"HyperDash":false},{"StartTime":155625.0,"Position":165.451813,"HyperDash":false},{"StartTime":155678.0,"Position":137.274612,"HyperDash":false},{"StartTime":155732.0,"Position":123.117592,"HyperDash":false},{"StartTime":155785.0,"Position":139.026031,"HyperDash":false},{"StartTime":155874.0,"Position":155.0,"HyperDash":false}]},{"StartTime":156000.0,"Objects":[{"StartTime":156000.0,"Position":163.0,"HyperDash":false}]},{"StartTime":156125.0,"Objects":[{"StartTime":156125.0,"Position":163.0,"HyperDash":false}]},{"StartTime":156250.0,"Objects":[{"StartTime":156250.0,"Position":163.0,"HyperDash":false},{"StartTime":156312.0,"Position":183.5915,"HyperDash":false},{"StartTime":156375.0,"Position":203.198776,"HyperDash":false},{"StartTime":156437.0,"Position":232.230286,"HyperDash":false},{"StartTime":156500.0,"Position":268.618439,"HyperDash":false},{"StartTime":156562.0,"Position":314.36087,"HyperDash":false},{"StartTime":156625.0,"Position":335.6506,"HyperDash":false},{"StartTime":156687.0,"Position":380.1404,"HyperDash":false},{"StartTime":156750.0,"Position":422.05127,"HyperDash":false},{"StartTime":156874.0,"Position":473.144562,"HyperDash":false}]},{"StartTime":157125.0,"Objects":[{"StartTime":157125.0,"Position":320.0,"HyperDash":false},{"StartTime":157187.0,"Position":278.6829,"HyperDash":false},{"StartTime":157250.0,"Position":245.222778,"HyperDash":false},{"StartTime":157312.0,"Position":218.502289,"HyperDash":false},{"StartTime":157375.0,"Position":221.008591,"HyperDash":false},{"StartTime":157428.0,"Position":240.596039,"HyperDash":false},{"StartTime":157482.0,"Position":248.418259,"HyperDash":false},{"StartTime":157535.0,"Position":260.110321,"HyperDash":false},{"StartTime":157624.0,"Position":317.0907,"HyperDash":false}]},{"StartTime":157750.0,"Objects":[{"StartTime":157750.0,"Position":348.0,"HyperDash":false}]},{"StartTime":157875.0,"Objects":[{"StartTime":157875.0,"Position":380.0,"HyperDash":false}]},{"StartTime":158000.0,"Objects":[{"StartTime":158000.0,"Position":404.0,"HyperDash":false}]},{"StartTime":158125.0,"Objects":[{"StartTime":158125.0,"Position":412.0,"HyperDash":false}]},{"StartTime":158250.0,"Objects":[{"StartTime":158250.0,"Position":412.0,"HyperDash":false}]},{"StartTime":158375.0,"Objects":[{"StartTime":158375.0,"Position":404.0,"HyperDash":false}]},{"StartTime":158625.0,"Objects":[{"StartTime":158625.0,"Position":264.0,"HyperDash":false},{"StartTime":158687.0,"Position":234.814957,"HyperDash":false},{"StartTime":158750.0,"Position":191.04628,"HyperDash":false},{"StartTime":158875.0,"Position":264.0,"HyperDash":false}]},{"StartTime":159125.0,"Objects":[{"StartTime":159125.0,"Position":164.0,"HyperDash":false},{"StartTime":159187.0,"Position":197.185043,"HyperDash":false},{"StartTime":159250.0,"Position":236.95372,"HyperDash":false},{"StartTime":159375.0,"Position":164.0,"HyperDash":false}]},{"StartTime":159625.0,"Objects":[{"StartTime":159625.0,"Position":56.0,"HyperDash":false}]},{"StartTime":159875.0,"Objects":[{"StartTime":159875.0,"Position":64.0,"HyperDash":false}]},{"StartTime":160000.0,"Objects":[{"StartTime":160000.0,"Position":64.0,"HyperDash":false}]},{"StartTime":160125.0,"Objects":[{"StartTime":160125.0,"Position":64.0,"HyperDash":false},{"StartTime":160187.0,"Position":64.51918,"HyperDash":false},{"StartTime":160250.0,"Position":26.7402878,"HyperDash":false},{"StartTime":160375.0,"Position":64.0,"HyperDash":false}]},{"StartTime":160500.0,"Objects":[{"StartTime":160500.0,"Position":128.0,"HyperDash":false},{"StartTime":160562.0,"Position":132.164291,"HyperDash":false},{"StartTime":160625.0,"Position":134.379623,"HyperDash":false},{"StartTime":160750.0,"Position":128.0,"HyperDash":false}]},{"StartTime":160875.0,"Objects":[{"StartTime":160875.0,"Position":192.0,"HyperDash":false},{"StartTime":160937.0,"Position":189.164291,"HyperDash":false},{"StartTime":161000.0,"Position":198.379623,"HyperDash":false},{"StartTime":161125.0,"Position":192.0,"HyperDash":false}]},{"StartTime":161250.0,"Objects":[{"StartTime":161250.0,"Position":240.0,"HyperDash":false},{"StartTime":161312.0,"Position":248.7879,"HyperDash":false},{"StartTime":161375.0,"Position":289.975616,"HyperDash":false},{"StartTime":161500.0,"Position":240.0,"HyperDash":false}]},{"StartTime":161625.0,"Objects":[{"StartTime":161625.0,"Position":284.0,"HyperDash":false},{"StartTime":161687.0,"Position":327.2897,"HyperDash":false},{"StartTime":161750.0,"Position":339.019562,"HyperDash":false},{"StartTime":161875.0,"Position":284.0,"HyperDash":false}]},{"StartTime":162000.0,"Objects":[{"StartTime":162000.0,"Position":328.0,"HyperDash":false},{"StartTime":162062.0,"Position":364.361755,"HyperDash":false},{"StartTime":162124.0,"Position":407.040955,"HyperDash":false},{"StartTime":162249.0,"Position":328.0,"HyperDash":false}]},{"StartTime":162375.0,"Objects":[{"StartTime":162375.0,"Position":308.0,"HyperDash":false},{"StartTime":162437.0,"Position":269.638245,"HyperDash":false},{"StartTime":162499.0,"Position":228.959045,"HyperDash":false},{"StartTime":162624.0,"Position":308.0,"HyperDash":false}]},{"StartTime":162750.0,"Objects":[{"StartTime":162750.0,"Position":340.0,"HyperDash":false},{"StartTime":162812.0,"Position":374.361755,"HyperDash":false},{"StartTime":162874.0,"Position":419.040955,"HyperDash":false},{"StartTime":162999.0,"Position":340.0,"HyperDash":false}]},{"StartTime":163125.0,"Objects":[{"StartTime":163125.0,"Position":284.0,"HyperDash":false},{"StartTime":163187.0,"Position":280.849731,"HyperDash":false},{"StartTime":163249.0,"Position":271.649841,"HyperDash":false},{"StartTime":163374.0,"Position":284.0,"HyperDash":false}]},{"StartTime":163500.0,"Objects":[{"StartTime":163500.0,"Position":224.0,"HyperDash":false},{"StartTime":163562.0,"Position":227.849731,"HyperDash":false},{"StartTime":163624.0,"Position":211.649857,"HyperDash":false},{"StartTime":163749.0,"Position":224.0,"HyperDash":false}]},{"StartTime":163875.0,"Objects":[{"StartTime":163875.0,"Position":180.0,"HyperDash":false},{"StartTime":163937.0,"Position":134.564423,"HyperDash":false},{"StartTime":163999.0,"Position":102.8189,"HyperDash":false},{"StartTime":164124.0,"Position":180.0,"HyperDash":false}]},{"StartTime":164250.0,"Objects":[{"StartTime":164250.0,"Position":144.0,"HyperDash":false},{"StartTime":164312.0,"Position":107.832245,"HyperDash":false},{"StartTime":164375.0,"Position":79.14566,"HyperDash":false},{"StartTime":164500.0,"Position":144.0,"HyperDash":false}]},{"StartTime":164625.0,"Objects":[{"StartTime":164625.0,"Position":168.0,"HyperDash":false},{"StartTime":164687.0,"Position":182.167755,"HyperDash":false},{"StartTime":164750.0,"Position":232.85434,"HyperDash":false},{"StartTime":164875.0,"Position":168.0,"HyperDash":false}]},{"StartTime":165000.0,"Objects":[{"StartTime":165000.0,"Position":136.0,"HyperDash":false},{"StartTime":165062.0,"Position":117.871719,"HyperDash":false},{"StartTime":165124.0,"Position":101.605316,"HyperDash":false},{"StartTime":165249.0,"Position":136.0,"HyperDash":false}]},{"StartTime":165375.0,"Objects":[{"StartTime":165375.0,"Position":188.0,"HyperDash":false},{"StartTime":165437.0,"Position":220.128281,"HyperDash":false},{"StartTime":165499.0,"Position":222.394684,"HyperDash":false},{"StartTime":165624.0,"Position":188.0,"HyperDash":false}]},{"StartTime":165750.0,"Objects":[{"StartTime":165750.0,"Position":236.0,"HyperDash":false}]},{"StartTime":165875.0,"Objects":[{"StartTime":165875.0,"Position":236.0,"HyperDash":false}]},{"StartTime":166125.0,"Objects":[{"StartTime":166125.0,"Position":364.0,"HyperDash":false},{"StartTime":166187.0,"Position":369.388123,"HyperDash":false},{"StartTime":166250.0,"Position":391.656616,"HyperDash":false},{"StartTime":166312.0,"Position":357.6028,"HyperDash":false},{"StartTime":166375.0,"Position":309.5534,"HyperDash":false},{"StartTime":166499.0,"Position":282.373474,"HyperDash":false}]},{"StartTime":166625.0,"Objects":[{"StartTime":166625.0,"Position":264.0,"HyperDash":false},{"StartTime":166687.0,"Position":284.388123,"HyperDash":false},{"StartTime":166750.0,"Position":283.656616,"HyperDash":false},{"StartTime":166812.0,"Position":260.602844,"HyperDash":false},{"StartTime":166875.0,"Position":209.5534,"HyperDash":false},{"StartTime":166999.0,"Position":182.373489,"HyperDash":false}]},{"StartTime":167125.0,"Objects":[{"StartTime":167125.0,"Position":192.0,"HyperDash":false}]},{"StartTime":167375.0,"Objects":[{"StartTime":167375.0,"Position":320.0,"HyperDash":false}]},{"StartTime":167625.0,"Objects":[{"StartTime":167625.0,"Position":192.0,"HyperDash":false}]},{"StartTime":167750.0,"Objects":[{"StartTime":167750.0,"Position":256.0,"HyperDash":false}]},{"StartTime":167875.0,"Objects":[{"StartTime":167875.0,"Position":320.0,"HyperDash":false}]},{"StartTime":168125.0,"Objects":[{"StartTime":168125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":168250.0,"Objects":[{"StartTime":168250.0,"Position":193.0,"HyperDash":false},{"StartTime":168308.0,"Position":488.0,"HyperDash":false},{"StartTime":168367.0,"Position":314.0,"HyperDash":false},{"StartTime":168425.0,"Position":135.0,"HyperDash":false},{"StartTime":168484.0,"Position":399.0,"HyperDash":false},{"StartTime":168542.0,"Position":404.0,"HyperDash":false},{"StartTime":168601.0,"Position":152.0,"HyperDash":false},{"StartTime":168660.0,"Position":353.0,"HyperDash":false},{"StartTime":168718.0,"Position":358.0,"HyperDash":false},{"StartTime":168777.0,"Position":447.0,"HyperDash":false},{"StartTime":168835.0,"Position":222.0,"HyperDash":false},{"StartTime":168894.0,"Position":382.0,"HyperDash":false},{"StartTime":168953.0,"Position":433.0,"HyperDash":false},{"StartTime":169011.0,"Position":450.0,"HyperDash":false},{"StartTime":169070.0,"Position":326.0,"HyperDash":false},{"StartTime":169128.0,"Position":414.0,"HyperDash":false},{"StartTime":169187.0,"Position":285.0,"HyperDash":false},{"StartTime":169246.0,"Position":336.0,"HyperDash":false},{"StartTime":169304.0,"Position":509.0,"HyperDash":false},{"StartTime":169363.0,"Position":334.0,"HyperDash":false},{"StartTime":169421.0,"Position":72.0,"HyperDash":false},{"StartTime":169480.0,"Position":425.0,"HyperDash":false},{"StartTime":169539.0,"Position":451.0,"HyperDash":false},{"StartTime":169597.0,"Position":220.0,"HyperDash":false},{"StartTime":169656.0,"Position":25.0,"HyperDash":false},{"StartTime":169714.0,"Position":77.0,"HyperDash":false},{"StartTime":169773.0,"Position":509.0,"HyperDash":false},{"StartTime":169832.0,"Position":90.0,"HyperDash":false},{"StartTime":169890.0,"Position":118.0,"HyperDash":false},{"StartTime":169949.0,"Position":58.0,"HyperDash":false},{"StartTime":170007.0,"Position":12.0,"HyperDash":false},{"StartTime":170066.0,"Position":215.0,"HyperDash":false},{"StartTime":170125.0,"Position":487.0,"HyperDash":false}]},{"StartTime":171125.0,"Objects":[{"StartTime":171125.0,"Position":446.0,"HyperDash":false},{"StartTime":171187.0,"Position":491.0,"HyperDash":false},{"StartTime":171250.0,"Position":459.0,"HyperDash":false},{"StartTime":171312.0,"Position":37.0,"HyperDash":false},{"StartTime":171375.0,"Position":291.0,"HyperDash":false},{"StartTime":171437.0,"Position":315.0,"HyperDash":false},{"StartTime":171500.0,"Position":35.0,"HyperDash":false},{"StartTime":171562.0,"Position":208.0,"HyperDash":false},{"StartTime":171625.0,"Position":504.0,"HyperDash":false},{"StartTime":171687.0,"Position":296.0,"HyperDash":false},{"StartTime":171750.0,"Position":105.0,"HyperDash":false},{"StartTime":171812.0,"Position":488.0,"HyperDash":false},{"StartTime":171875.0,"Position":230.0,"HyperDash":false},{"StartTime":171937.0,"Position":446.0,"HyperDash":false},{"StartTime":172000.0,"Position":241.0,"HyperDash":false},{"StartTime":172062.0,"Position":413.0,"HyperDash":false},{"StartTime":172125.0,"Position":357.0,"HyperDash":false}]},{"StartTime":172375.0,"Objects":[{"StartTime":172375.0,"Position":48.0,"HyperDash":false}]},{"StartTime":172625.0,"Objects":[{"StartTime":172625.0,"Position":20.0,"HyperDash":false},{"StartTime":172678.0,"Position":23.1916313,"HyperDash":false},{"StartTime":172732.0,"Position":25.55497,"HyperDash":false},{"StartTime":172785.0,"Position":75.26404,"HyperDash":false},{"StartTime":172875.0,"Position":108.478035,"HyperDash":false}]},{"StartTime":173125.0,"Objects":[{"StartTime":173125.0,"Position":240.0,"HyperDash":false}]},{"StartTime":173375.0,"Objects":[{"StartTime":173375.0,"Position":200.0,"HyperDash":false}]},{"StartTime":173625.0,"Objects":[{"StartTime":173625.0,"Position":324.0,"HyperDash":false},{"StartTime":173678.0,"Position":349.476471,"HyperDash":false},{"StartTime":173732.0,"Position":378.649323,"HyperDash":false},{"StartTime":173785.0,"Position":384.945282,"HyperDash":false},{"StartTime":173875.0,"Position":412.1426,"HyperDash":false}]},{"StartTime":174000.0,"Objects":[{"StartTime":174000.0,"Position":412.0,"HyperDash":false}]},{"StartTime":174125.0,"Objects":[{"StartTime":174125.0,"Position":412.0,"HyperDash":false},{"StartTime":174178.0,"Position":426.1397,"HyperDash":false},{"StartTime":174232.0,"Position":445.433044,"HyperDash":false},{"StartTime":174285.0,"Position":425.572754,"HyperDash":false},{"StartTime":174375.0,"Position":450.394928,"HyperDash":false}]},{"StartTime":174625.0,"Objects":[{"StartTime":174625.0,"Position":398.0,"HyperDash":false},{"StartTime":174678.0,"Position":380.028442,"HyperDash":false},{"StartTime":174732.0,"Position":327.434753,"HyperDash":false},{"StartTime":174785.0,"Position":306.4632,"HyperDash":false},{"StartTime":174875.0,"Position":242.473724,"HyperDash":false}]},{"StartTime":175000.0,"Objects":[{"StartTime":175000.0,"Position":245.0,"HyperDash":false}]},{"StartTime":175125.0,"Objects":[{"StartTime":175125.0,"Position":245.0,"HyperDash":false},{"StartTime":175178.0,"Position":247.860275,"HyperDash":false},{"StartTime":175232.0,"Position":229.566971,"HyperDash":false},{"StartTime":175285.0,"Position":219.427246,"HyperDash":false},{"StartTime":175375.0,"Position":206.605072,"HyperDash":false}]},{"StartTime":175625.0,"Objects":[{"StartTime":175625.0,"Position":259.0,"HyperDash":false},{"StartTime":175678.0,"Position":271.971558,"HyperDash":false},{"StartTime":175732.0,"Position":338.565247,"HyperDash":false},{"StartTime":175785.0,"Position":339.5368,"HyperDash":false},{"StartTime":175875.0,"Position":414.526276,"HyperDash":false}]},{"StartTime":176125.0,"Objects":[{"StartTime":176125.0,"Position":424.0,"HyperDash":false}]},{"StartTime":176375.0,"Objects":[{"StartTime":176375.0,"Position":272.0,"HyperDash":false}]},{"StartTime":176625.0,"Objects":[{"StartTime":176625.0,"Position":116.0,"HyperDash":false}]},{"StartTime":176875.0,"Objects":[{"StartTime":176875.0,"Position":173.0,"HyperDash":false},{"StartTime":176937.0,"Position":220.433136,"HyperDash":false},{"StartTime":177000.0,"Position":248.325027,"HyperDash":false},{"StartTime":177062.0,"Position":256.667,"HyperDash":false},{"StartTime":177125.0,"Position":270.778076,"HyperDash":false},{"StartTime":177187.0,"Position":271.125366,"HyperDash":false},{"StartTime":177250.0,"Position":267.005768,"HyperDash":false},{"StartTime":177312.0,"Position":259.9013,"HyperDash":false},{"StartTime":177375.0,"Position":212.426208,"HyperDash":false},{"StartTime":177437.0,"Position":239.901321,"HyperDash":false},{"StartTime":177500.0,"Position":249.239349,"HyperDash":false},{"StartTime":177562.0,"Position":285.125366,"HyperDash":false},{"StartTime":177625.0,"Position":270.676758,"HyperDash":false},{"StartTime":177678.0,"Position":275.8453,"HyperDash":false},{"StartTime":177732.0,"Position":255.82901,"HyperDash":false},{"StartTime":177785.0,"Position":207.305466,"HyperDash":false},{"StartTime":177874.0,"Position":173.0,"HyperDash":false}]},{"StartTime":178125.0,"Objects":[{"StartTime":178125.0,"Position":28.0,"HyperDash":false},{"StartTime":178187.0,"Position":78.55116,"HyperDash":false},{"StartTime":178250.0,"Position":102.707985,"HyperDash":false},{"StartTime":178312.0,"Position":129.259155,"HyperDash":false},{"StartTime":178375.0,"Position":179.41597,"HyperDash":false},{"StartTime":178428.0,"Position":226.516159,"HyperDash":false},{"StartTime":178482.0,"Position":240.222,"HyperDash":false},{"StartTime":178535.0,"Position":283.3222,"HyperDash":false},{"StartTime":178625.0,"Position":330.83194,"HyperDash":false}]},{"StartTime":178875.0,"Objects":[{"StartTime":178875.0,"Position":172.0,"HyperDash":false},{"StartTime":178937.0,"Position":221.551163,"HyperDash":false},{"StartTime":179000.0,"Position":253.707977,"HyperDash":false},{"StartTime":179062.0,"Position":274.259155,"HyperDash":false},{"StartTime":179125.0,"Position":323.415955,"HyperDash":false},{"StartTime":179178.0,"Position":344.516174,"HyperDash":false},{"StartTime":179232.0,"Position":379.222,"HyperDash":false},{"StartTime":179285.0,"Position":429.3222,"HyperDash":false},{"StartTime":179375.0,"Position":474.83194,"HyperDash":false}]},{"StartTime":179625.0,"Objects":[{"StartTime":179625.0,"Position":384.0,"HyperDash":false},{"StartTime":179678.0,"Position":348.327026,"HyperDash":false},{"StartTime":179732.0,"Position":316.224579,"HyperDash":false},{"StartTime":179785.0,"Position":267.12973,"HyperDash":false},{"StartTime":179875.0,"Position":244.098541,"HyperDash":false}]},{"StartTime":180000.0,"Objects":[{"StartTime":180000.0,"Position":244.0,"HyperDash":false}]},{"StartTime":180125.0,"Objects":[{"StartTime":180125.0,"Position":244.0,"HyperDash":false},{"StartTime":180178.0,"Position":217.455292,"HyperDash":false},{"StartTime":180232.0,"Position":186.277634,"HyperDash":false},{"StartTime":180285.0,"Position":129.732925,"HyperDash":false},{"StartTime":180375.0,"Position":85.77019,"HyperDash":false}]},{"StartTime":180625.0,"Objects":[{"StartTime":180625.0,"Position":100.0,"HyperDash":false},{"StartTime":180678.0,"Position":146.386475,"HyperDash":false},{"StartTime":180732.0,"Position":185.4029,"HyperDash":false},{"StartTime":180785.0,"Position":189.789368,"HyperDash":false},{"StartTime":180875.0,"Position":257.4834,"HyperDash":false}]},{"StartTime":181000.0,"Objects":[{"StartTime":181000.0,"Position":257.0,"HyperDash":false}]},{"StartTime":181125.0,"Objects":[{"StartTime":181125.0,"Position":256.0,"HyperDash":false},{"StartTime":181178.0,"Position":273.4897,"HyperDash":false},{"StartTime":181231.0,"Position":332.9794,"HyperDash":false},{"StartTime":181284.0,"Position":358.4691,"HyperDash":false},{"StartTime":181374.0,"Position":413.338379,"HyperDash":false}]},{"StartTime":181625.0,"Objects":[{"StartTime":181625.0,"Position":426.0,"HyperDash":false},{"StartTime":181678.0,"Position":383.4294,"HyperDash":false},{"StartTime":181732.0,"Position":353.2254,"HyperDash":false},{"StartTime":181785.0,"Position":325.654816,"HyperDash":false},{"StartTime":181875.0,"Position":267.648163,"HyperDash":false}]},{"StartTime":182000.0,"Objects":[{"StartTime":182000.0,"Position":267.0,"HyperDash":false}]},{"StartTime":182125.0,"Objects":[{"StartTime":182125.0,"Position":267.0,"HyperDash":false},{"StartTime":182178.0,"Position":226.982559,"HyperDash":false},{"StartTime":182232.0,"Position":205.749466,"HyperDash":false},{"StartTime":182285.0,"Position":176.327576,"HyperDash":false},{"StartTime":182375.0,"Position":168.9247,"HyperDash":false}]},{"StartTime":182625.0,"Objects":[{"StartTime":182625.0,"Position":140.0,"HyperDash":false},{"StartTime":182678.0,"Position":155.139557,"HyperDash":false},{"StartTime":182731.0,"Position":203.985977,"HyperDash":false},{"StartTime":182784.0,"Position":216.5605,"HyperDash":false},{"StartTime":182874.0,"Position":238.0753,"HyperDash":false}]},{"StartTime":183125.0,"Objects":[{"StartTime":183125.0,"Position":62.0,"HyperDash":false},{"StartTime":183178.0,"Position":70.6348648,"HyperDash":false},{"StartTime":183232.0,"Position":74.16411,"HyperDash":false},{"StartTime":183285.0,"Position":122.076561,"HyperDash":false},{"StartTime":183375.0,"Position":173.7103,"HyperDash":false}]},{"StartTime":183625.0,"Objects":[{"StartTime":183625.0,"Position":348.0,"HyperDash":false},{"StartTime":183678.0,"Position":324.143158,"HyperDash":false},{"StartTime":183732.0,"Position":333.585327,"HyperDash":false},{"StartTime":183785.0,"Position":270.711,"HyperDash":false},{"StartTime":183874.0,"Position":236.603912,"HyperDash":false}]},{"StartTime":184125.0,"Objects":[{"StartTime":184125.0,"Position":64.0,"HyperDash":false}]},{"StartTime":184250.0,"Objects":[{"StartTime":184250.0,"Position":488.0,"HyperDash":false},{"StartTime":184335.0,"Position":482.0,"HyperDash":false},{"StartTime":184421.0,"Position":321.0,"HyperDash":false},{"StartTime":184507.0,"Position":474.0,"HyperDash":false},{"StartTime":184593.0,"Position":252.0,"HyperDash":false},{"StartTime":184679.0,"Position":247.0,"HyperDash":false},{"StartTime":184765.0,"Position":406.0,"HyperDash":false},{"StartTime":184851.0,"Position":319.0,"HyperDash":false},{"StartTime":184937.0,"Position":253.0,"HyperDash":false},{"StartTime":185023.0,"Position":411.0,"HyperDash":false},{"StartTime":185109.0,"Position":205.0,"HyperDash":false},{"StartTime":185195.0,"Position":54.0,"HyperDash":false},{"StartTime":185281.0,"Position":224.0,"HyperDash":false},{"StartTime":185367.0,"Position":465.0,"HyperDash":false},{"StartTime":185453.0,"Position":432.0,"HyperDash":false},{"StartTime":185539.0,"Position":108.0,"HyperDash":false},{"StartTime":185625.0,"Position":95.0,"HyperDash":false}]},{"StartTime":186125.0,"Objects":[{"StartTime":186125.0,"Position":48.0,"HyperDash":false},{"StartTime":186187.0,"Position":89.47744,"HyperDash":false},{"StartTime":186250.0,"Position":93.06244,"HyperDash":false},{"StartTime":186312.0,"Position":160.382751,"HyperDash":false},{"StartTime":186375.0,"Position":190.718857,"HyperDash":false},{"StartTime":186437.0,"Position":209.265518,"HyperDash":false},{"StartTime":186500.0,"Position":273.188416,"HyperDash":false},{"StartTime":186562.0,"Position":294.6259,"HyperDash":false},{"StartTime":186625.0,"Position":321.75354,"HyperDash":false},{"StartTime":186678.0,"Position":352.728241,"HyperDash":false},{"StartTime":186732.0,"Position":377.1885,"HyperDash":false},{"StartTime":186785.0,"Position":409.4063,"HyperDash":false},{"StartTime":186874.0,"Position":463.955,"HyperDash":false}]},{"StartTime":187125.0,"Objects":[{"StartTime":187125.0,"Position":328.0,"HyperDash":false},{"StartTime":187178.0,"Position":313.795776,"HyperDash":false},{"StartTime":187232.0,"Position":325.474457,"HyperDash":false},{"StartTime":187285.0,"Position":313.270233,"HyperDash":false},{"StartTime":187375.0,"Position":298.734741,"HyperDash":false}]},{"StartTime":187625.0,"Objects":[{"StartTime":187625.0,"Position":184.0,"HyperDash":false},{"StartTime":187678.0,"Position":198.204239,"HyperDash":false},{"StartTime":187732.0,"Position":213.525543,"HyperDash":false},{"StartTime":187785.0,"Position":188.729767,"HyperDash":false},{"StartTime":187875.0,"Position":213.265274,"HyperDash":false}]},{"StartTime":188125.0,"Objects":[{"StartTime":188125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":188250.0,"Objects":[{"StartTime":188250.0,"Position":175.0,"HyperDash":false},{"StartTime":188335.0,"Position":48.0,"HyperDash":false},{"StartTime":188421.0,"Position":307.0,"HyperDash":false},{"StartTime":188507.0,"Position":375.0,"HyperDash":false},{"StartTime":188593.0,"Position":149.0,"HyperDash":false},{"StartTime":188679.0,"Position":250.0,"HyperDash":false},{"StartTime":188765.0,"Position":142.0,"HyperDash":false},{"StartTime":188851.0,"Position":170.0,"HyperDash":false},{"StartTime":188937.0,"Position":281.0,"HyperDash":false},{"StartTime":189023.0,"Position":444.0,"HyperDash":false},{"StartTime":189109.0,"Position":414.0,"HyperDash":false},{"StartTime":189195.0,"Position":321.0,"HyperDash":false},{"StartTime":189281.0,"Position":328.0,"HyperDash":false},{"StartTime":189367.0,"Position":32.0,"HyperDash":false},{"StartTime":189453.0,"Position":259.0,"HyperDash":false},{"StartTime":189539.0,"Position":169.0,"HyperDash":false},{"StartTime":189625.0,"Position":207.0,"HyperDash":false}]},{"StartTime":190125.0,"Objects":[{"StartTime":190125.0,"Position":464.0,"HyperDash":false},{"StartTime":190187.0,"Position":452.522552,"HyperDash":false},{"StartTime":190250.0,"Position":408.937561,"HyperDash":false},{"StartTime":190312.0,"Position":358.617249,"HyperDash":false},{"StartTime":190375.0,"Position":321.281128,"HyperDash":false},{"StartTime":190437.0,"Position":281.7345,"HyperDash":false},{"StartTime":190500.0,"Position":240.811569,"HyperDash":false},{"StartTime":190562.0,"Position":234.374115,"HyperDash":false},{"StartTime":190625.0,"Position":190.246445,"HyperDash":false},{"StartTime":190678.0,"Position":161.271759,"HyperDash":false},{"StartTime":190732.0,"Position":103.811485,"HyperDash":false},{"StartTime":190785.0,"Position":84.59368,"HyperDash":false},{"StartTime":190874.0,"Position":48.04496,"HyperDash":false}]},{"StartTime":191125.0,"Objects":[{"StartTime":191125.0,"Position":184.0,"HyperDash":false},{"StartTime":191178.0,"Position":177.204239,"HyperDash":false},{"StartTime":191232.0,"Position":205.525543,"HyperDash":false},{"StartTime":191285.0,"Position":211.729767,"HyperDash":false},{"StartTime":191375.0,"Position":213.265274,"HyperDash":false}]},{"StartTime":191625.0,"Objects":[{"StartTime":191625.0,"Position":328.0,"HyperDash":false},{"StartTime":191678.0,"Position":303.795776,"HyperDash":false},{"StartTime":191732.0,"Position":318.474457,"HyperDash":false},{"StartTime":191785.0,"Position":296.270233,"HyperDash":false},{"StartTime":191875.0,"Position":298.734741,"HyperDash":false}]},{"StartTime":192125.0,"Objects":[{"StartTime":192125.0,"Position":164.0,"HyperDash":false}]},{"StartTime":192375.0,"Objects":[{"StartTime":192375.0,"Position":28.0,"HyperDash":false}]},{"StartTime":192625.0,"Objects":[{"StartTime":192625.0,"Position":28.0,"HyperDash":false}]},{"StartTime":192875.0,"Objects":[{"StartTime":192875.0,"Position":128.0,"HyperDash":false},{"StartTime":192937.0,"Position":126.887405,"HyperDash":false},{"StartTime":193000.0,"Position":175.597244,"HyperDash":false},{"StartTime":193062.0,"Position":198.553162,"HyperDash":false},{"StartTime":193125.0,"Position":235.7683,"HyperDash":false},{"StartTime":193187.0,"Position":291.259583,"HyperDash":false},{"StartTime":193250.0,"Position":330.488678,"HyperDash":false},{"StartTime":193312.0,"Position":338.450653,"HyperDash":false},{"StartTime":193375.0,"Position":390.71225,"HyperDash":false},{"StartTime":193437.0,"Position":356.065,"HyperDash":false},{"StartTime":193500.0,"Position":315.488678,"HyperDash":false},{"StartTime":193562.0,"Position":279.894,"HyperDash":false},{"StartTime":193625.0,"Position":235.7683,"HyperDash":false},{"StartTime":193678.0,"Position":221.0309,"HyperDash":false},{"StartTime":193732.0,"Position":168.99295,"HyperDash":false},{"StartTime":193785.0,"Position":164.902176,"HyperDash":false},{"StartTime":193875.0,"Position":128.0,"HyperDash":false}]},{"StartTime":194125.0,"Objects":[{"StartTime":194125.0,"Position":276.0,"HyperDash":false},{"StartTime":194187.0,"Position":324.316467,"HyperDash":false},{"StartTime":194250.0,"Position":328.094818,"HyperDash":false},{"StartTime":194312.0,"Position":373.795776,"HyperDash":false},{"StartTime":194375.0,"Position":386.318756,"HyperDash":false},{"StartTime":194428.0,"Position":376.7576,"HyperDash":false},{"StartTime":194482.0,"Position":404.218842,"HyperDash":false},{"StartTime":194535.0,"Position":384.551483,"HyperDash":false},{"StartTime":194624.0,"Position":374.339844,"HyperDash":false}]},{"StartTime":194875.0,"Objects":[{"StartTime":194875.0,"Position":236.0,"HyperDash":false},{"StartTime":194937.0,"Position":201.752014,"HyperDash":false},{"StartTime":195000.0,"Position":162.019058,"HyperDash":false},{"StartTime":195062.0,"Position":146.331146,"HyperDash":false},{"StartTime":195125.0,"Position":125.789307,"HyperDash":false},{"StartTime":195178.0,"Position":127.304863,"HyperDash":false},{"StartTime":195232.0,"Position":133.772476,"HyperDash":false},{"StartTime":195285.0,"Position":111.34684,"HyperDash":false},{"StartTime":195375.0,"Position":137.660187,"HyperDash":false}]},{"StartTime":195625.0,"Objects":[{"StartTime":195625.0,"Position":280.0,"HyperDash":false},{"StartTime":195678.0,"Position":279.7856,"HyperDash":false},{"StartTime":195732.0,"Position":250.37854,"HyperDash":false},{"StartTime":195785.0,"Position":235.164154,"HyperDash":false},{"StartTime":195875.0,"Position":231.818985,"HyperDash":false}]},{"StartTime":196125.0,"Objects":[{"StartTime":196125.0,"Position":104.0,"HyperDash":false}]},{"StartTime":196375.0,"Objects":[{"StartTime":196375.0,"Position":136.0,"HyperDash":false}]},{"StartTime":196625.0,"Objects":[{"StartTime":196625.0,"Position":116.0,"HyperDash":false}]},{"StartTime":196875.0,"Objects":[{"StartTime":196875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":197000.0,"Objects":[{"StartTime":197000.0,"Position":332.0,"HyperDash":false}]},{"StartTime":197125.0,"Objects":[{"StartTime":197125.0,"Position":408.0,"HyperDash":false}]},{"StartTime":197250.0,"Objects":[{"StartTime":197250.0,"Position":392.0,"HyperDash":false}]},{"StartTime":197375.0,"Objects":[{"StartTime":197375.0,"Position":376.0,"HyperDash":false}]},{"StartTime":197625.0,"Objects":[{"StartTime":197625.0,"Position":396.0,"HyperDash":false}]},{"StartTime":197875.0,"Objects":[{"StartTime":197875.0,"Position":256.0,"HyperDash":false}]},{"StartTime":198000.0,"Objects":[{"StartTime":198000.0,"Position":256.0,"HyperDash":false}]},{"StartTime":198125.0,"Objects":[{"StartTime":198125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":198625.0,"Objects":[{"StartTime":198625.0,"Position":136.0,"HyperDash":false}]},{"StartTime":199125.0,"Objects":[{"StartTime":199125.0,"Position":256.0,"HyperDash":false}]},{"StartTime":199625.0,"Objects":[{"StartTime":199625.0,"Position":376.0,"HyperDash":false}]},{"StartTime":200125.0,"Objects":[{"StartTime":200125.0,"Position":256.0,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/112643.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/112643.osu new file mode 100644 index 0000000000..35ef17ae34 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/112643.osu @@ -0,0 +1,582 @@ +osu file format v9 + +[General] +StackLeniency: 0.7 +Mode: 0 + +[Difficulty] +HPDrainRate:7 +CircleSize:5 +OverallDifficulty:8 +ApproachRate:8 +SliderMultiplier:3.2 +SliderTickRate:2 + +[Events] +//Background and Video events +//Break Periods +2,16325,17625 +2,32325,33875 +2,66325,67375 +2,120135,127375 +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples +//Background Colour Transformations +3,100,163,162,255 + +[TimingPoints] +125,500,4,1,0,50,1,0 +36125,-100,4,1,0,50,0,1 +66125,-100,4,1,0,50,0,0 +88125,-100,4,1,0,50,0,1 +120125,-100,4,1,0,50,0,0 +170125,-100,4,2,0,5,0,0 +170250,-100,4,1,0,50,0,0 +172125,-100,4,1,0,50,0,1 +200125,-100,4,1,0,50,0,0 + +[HitObjects] +64,80,2375,5,0 +172,192,2625,1,2 +152,36,2875,1,0 +80,176,3125,1,2 +224,112,3375,1,0 +192,256,3625,1,8 +136,116,3875,1,0 +272,32,4125,2,2,B|376:0|408:56|412:125|320:144|304:176|328:216|368:272|496:208,1,400,6|0 +504,216,4875,2,2,B|376:232|288:280|248:384,1,320 +384,344,5625,1,8 +272,216,5875,1,0 +272,216,6000,1,0 +272,216,6125,1,4 +92,280,6375,5,0 +124,108,6625,1,8 +256,8,6875,1,0 +388,108,7125,1,2 +420,280,7375,1,8 +256,296,7625,1,8 +256,120,7875,1,0 +443,152,8125,2,2,B|397:202|305:219|256:192|203:163|114:181|68:231,1,400,2|0 +24,256,8875,2,2,B|112:227|141:134|122:36|37:1,1,320 +16,132,9625,1,8 +136,280,9875,1,0 +136,280,10000,1,0 +136,280,10125,1,4 +256,172,10375,5,0 +368,56,10625,1,8 +196,116,10875,1,0 +316,116,11125,1,2 +144,56,11375,1,0 +256,0,11625,1,8 +112,128,11875,1,0 +164,280,12125,6,0,B|256:316,1,80,4|2 +100,348,12500,2,0,B|8:312,1,80,0|2 +144,212,12875,2,0,B|52:176,1,80,0|2 +208,144,13250,2,0,B|300:180,1,80,0|2 +332,324,13625,1,8 +180,324,13875,1,0 +256,240,14125,5,4 +256,240,14250,1,2 +324,112,14500,1,0 +324,112,14625,1,2 +192,56,14875,1,4 +192,56,15000,1,2 +256,164,15250,1,0 +256,164,15375,1,2 +256,20,15625,1,8 +120,56,15875,1,0 +256,92,16125,1,6 +20,152,18375,5,0 +180,136,18625,1,8 +52,228,18875,1,0 +120,84,19125,1,2 +128,244,19375,1,0 +48,84,19625,1,8 +192,212,19875,1,0 +300,72,20125,2,4,B|396:36|444:84|396:144|352:184|372:224|416:260|532:224|528:164,1,320,4|0 +472,40,20875,2,2,B|376:72|304:164|272:260|280:320,1,320 +404,352,21625,1,8 +432,196,21875,1,0 +432,196,22000,1,0 +432,196,22125,1,4 +296,100,22375,5,0 +168,196,22625,2,0,B|32:296,1,160,8|0 +268,212,23125,2,0,B|168:76,1,160,2|8 +252,312,23625,2,0,B|388:212,1,160,8|0 +484,96,24125,2,2,B|412:0|320:36|288:120|240:136|200:132|156:116|132:96|80:44,1,400,2|0 +72,24,24875,2,2,B|158:66|148:177|67:253|-19:210,1,320 +56,108,25625,1,8 +176,200,25875,1,0 +176,200,26000,1,0 +176,200,26125,1,4 +316,92,26375,5,0 +464,164,26625,2,0,B|394:224|412:336,1,160,2|0 +232,316,27125,2,0,B|306:256|284:144,1,160,2|8 +136,88,27625,1,8 +60,224,27875,1,0 +212,132,28125,6,0,B|256:32,1,80,4|2 +340,228,28500,2,0,B|384:128,1,80,0|2 +256,284,28875,2,0,B|212:184,1,80,4|2 +128,380,29250,2,0,B|84:280,1,80,0|2 +238,383,29625,2,0,B|406:379,1,160,8|0 +512,267,30125,5,4 +512,267,30250,1,2 +416,152,30500,1,0 +416,152,30625,1,2 +300,264,30875,1,4 +300,264,31000,1,2 +236,100,31250,1,0 +236,100,31375,1,2 +152,256,31625,1,8 +300,160,31875,1,0 +256,332,32125,1,6 +52,52,34625,5,0 +152,164,34875,1,0 +256,56,35125,1,4 +256,56,35625,1,2 +256,56,36125,2,4,B|331:63|364:136|320:224,1,160,4|0 +320,312,36625,1,8 +204,228,36875,1,0 +104,328,37125,2,2,B|24:287|44:188,1,160 +92,60,37625,1,8 +212,148,37875,1,0 +268,104,38000,1,0 +324,60,38125,2,0,B|452:184,1,160,4|0 +504,300,38625,1,8 +364,340,38875,1,0 +232,280,39125,6,2,B|150:282|69:198|105:87|179:53,2,320,2|2|6 +280,148,40375,1,0 +400,228,40625,2,0,B|520:368,1,160,8|0 +480,192,41125,1,2 +324,220,41375,1,2 +168,256,41625,1,8 +72,148,41875,1,2 +48,84,42000,1,2 +96,36,42125,2,0,B|164:108|256:44,1,160,6|0 +400,72,42625,1,2 +440,236,42875,1,2 +464,300,43000,1,2 +416,348,43125,2,0,B|348:276|256:340,1,160,6|0 +112,312,43625,1,2 +140,188,43875,1,0 +52,64,44125,5,6 +208,48,44375,1,0 +344,132,44625,1,8 +448,256,44875,2,2,B|401:321|285:337|217:242|233:163,2,320,2|2|0 +326,211,46125,2,2,B|279:146|163:130|95:225|111:304,1,320,6|0 +230,287,46875,2,2,B|277:352|393:368|461:273|445:194,1,320,6|8 +376,80,47625,1,8 +376,80,48125,6,0,B|304:128|216:96,1,160,4|0 +84,56,48625,1,8 +152,200,48875,1,0 +44,320,49125,2,0,B|121:364|204:320,1,160,4|0 +336,240,49625,5,8 +256,148,49875,1,0 +176,240,50125,1,0 +340,144,50625,1,0 +420,236,50875,1,0 +500,144,51125,1,2 +172,144,51625,1,2 +92,236,51875,1,0 +12,144,52125,6,0,B|160:48,1,160,4|0 +304,76,52625,1,8 +256,228,52875,1,0 +216,112,53125,2,0,B|364:208,1,160,2|0 +508,180,53625,1,8 +460,28,53875,1,0 +344,96,54125,1,2 +228,8,54375,1,0 +153,116,54625,1,2 +72,220,54875,1,0 +180,295,55125,1,2 +284,376,55375,1,0 +359,268,55625,1,2 +440,164,55875,1,0 +352,160,56125,6,0,B|466:294,1,160,4|0 +312,228,56625,1,8 +200,300,56875,1,0 +160,160,57125,2,0,B|46:294,1,160,4|0 +200,228,57625,1,8 +312,300,57875,1,0 +444,208,58125,2,0,B|362:164|380:56,1,160,2|0 +344,12,58500,1,0 +272,4,58625,2,0,B|232:88|120:68,1,160,2|0 +68,176,59125,2,0,B|148:220|132:328,1,160,2|0 +168,372,59500,1,0 +240,380,59625,2,0,B|280:296|392:316,1,160,2|0 +456,176,60125,5,6 +328,80,60375,1,0 +216,196,60625,1,8 +72,136,60875,2,2,B|54:209|91:305|191:336|269:306,2,320,2|2|0 +200,224,62125,2,2,B|182:150|219:54|319:23|397:53,1,320,2|0 +480,179,62875,2,2,B|499:252|462:348|362:379|284:349,1,320,2|0 +136,296,63625,2,0,B|67:220|140:136,1,160,8|0 +256,56,64125,5,6 +284,212,64375,1,0 +440,180,64625,1,8 +420,24,64875,1,0 +300,132,65125,1,6 +272,288,65375,1,0 +116,256,65625,1,8 +136,100,65875,1,0 +256,8,66125,1,4 +256,56,68125,6,0,B|298:128|244:237|123:241|74:173,1,320 +132,80,68875,2,2,B|344:328,1,320 +456,224,69625,1,8 +340,116,69875,1,0 +340,116,70000,1,0 +340,116,70125,1,4 +228,4,70375,5,0 +256,160,70625,2,0,B|186:224|88:168,1,160,2|0 +148,332,71125,2,0,B|216:396|316:340,1,160,2|8 +424,248,71625,1,8 +336,112,71875,1,0 +336,112,72000,1,0 +336,112,72125,1,4 +228,208,72375,2,0,B|139:179|144:80,1,160,0|8 +268,56,72875,2,2,B|272:164|220:272|120:308|72:308,1,320 +24,192,73625,1,8 +92,64,73875,1,0 +92,64,74000,1,0 +92,64,74125,1,4 +224,140,74375,5,0 +340,224,74625,2,0,B|412:211|428:121|363:77,1,160,2|0 +268,192,75125,2,0,B|196:205|180:295|245:339,1,160,2|0 +268,192,75625,2,0,B|104:168,1,160,8|0 +24,52,76125,6,0,B|132:40,1,80 +176,32,76375,1,2 +348,60,76625,1,2 +248,164,76875,1,2 +264,20,77125,1,2 +324,140,77375,1,2 +180,116,77625,1,2 +240,240,77875,1,0 +256,92,78125,1,4 +100,124,78375,5,0 +8,256,78625,2,0,B|64:332|176:304,1,160,8|0 +304,260,79125,2,0,B|248:184|136:212,1,160,2|0 +304,260,79625,1,8 +460,284,79875,1,2 +420,128,80125,6,0,B|332:128,1,80,4|0 +256,124,80375,1,2 +344,260,80625,1,2 +168,260,80875,1,2 +384,192,81125,1,2 +256,260,81375,1,2 +168,124,81625,1,2 +344,124,81875,1,2 +128,192,82125,1,4 +48,192,82250,6,0,B|48:84|152:52,1,160,2|0 +204,44,82625,2,0,B|204:152|308:184,1,160,2|0 +352,160,83000,2,0,B|244:160|212:264,1,160,2|0 +192,316,83375,2,0,B|84:316|52:212,1,160,2|2 +32,88,83875,1,2 +172,8,84125,1,4 +256,192,84250,12,6,86125 +256,192,86250,12,4,87125 +256,100,88125,6,2,B|308:116|368:104|404:16,1,160,6|0 +256,100,88625,1,8 +136,180,88875,1,0 +8,96,89125,2,0,B|-28:168|16:232|68:256,1,160,2|0 +164,312,89625,1,8 +288,236,89875,1,2 +288,236,90000,1,2 +288,236,90125,2,2,B|452:164,1,160,6|0 +476,32,90625,1,8 +332,104,90875,1,0 +180,104,91125,5,6 +36,32,91375,1,8 +56,164,91625,1,8 +56,164,92125,2,0,B|260:208,1,160,6|0 +84,296,92625,1,8 +220,376,92875,1,0 +320,268,93125,2,0,B|524:224,1,160,6|0 +432,80,93625,1,8 +296,152,93875,1,2 +296,152,94000,1,2 +296,152,94125,2,2,B|232:164|176:132|164:52,1,160,6|0 +216,232,94625,2,2,B|280:220|336:252|348:332,1,160,2|0 +341,304,95000,1,0 +341,304,95125,2,0,B|369:84,1,160,2|0 +171,80,95625,2,0,B|143:300,1,160,2|0 +43,358,96125,5,6 +81,219,96375,1,0 +169,332,96625,1,8 +304,272,96875,2,2,B|388:252|426:161|418:63|344:19,2,320,2|2|0 +240,144,98125,2,2,B|219:244|50:229|65:60|168:58,1,320 +240,144,98875,2,2,B|260:43|429:58|414:227|311:229,1,320,2|0 +180,292,99625,2,0,B|80:304|36:208,1,160,2|0 +48,64,100125,6,0,B|224:112,1,160,4|0 +348,52,100625,2,0,B|524:4,1,160,2|0 +504,172,101125,2,0,B|328:124,1,160,2|0 +204,184,101625,2,0,B|28:232,1,160,2|0 +49,226,102000,1,0 +49,226,102125,1,2 +256,324,102625,5,8 +384,256,102875,1,0 +256,188,103125,1,6 +256,188,103625,1,2 +128,256,103875,1,0 +256,324,104125,6,0,B|324:252|432:316,1,160,6|0 +492,168,104625,1,8 +332,188,104875,1,0 +256,60,105125,2,0,B|188:132|80:68,1,160,6|0 +20,216,105625,1,8 +180,196,105875,1,0 +368,156,106125,2,0,B|418:184|462:234|408:296,1,160,2|0 +220,80,106625,2,0,B|248:30|298:-14|360:40,1,160,2|0 +144,228,107125,2,0,B|94:200|50:150|104:88,1,160,2|0 +292,304,107625,2,0,B|264:354|214:398|152:344,1,160,2|0 +44,216,108125,6,0,B|145:221|172:132,1,160,6|0 +304,224,108625,1,8 +408,104,108875,1,0 +468,216,109125,2,0,B|367:221|340:132,1,160,6|0 +208,224,109625,1,8 +104,104,109875,1,0 +256,56,110125,2,0,B|144:180,1,160,2|0 +256,328,110625,2,0,B|368:204,1,160,2|0 +208,244,111125,2,0,B|96:368,1,160,2|0 +304,140,111625,2,0,B|416:16,1,160,2|0 +252,20,112125,5,6 +112,60,112375,1,0 +72,200,112625,1,8 +158,316,112875,2,2,B|236:321|324:259|326:152|278:89,2,320,2|2|0 +176,168,114125,2,2,B|214:236|313:276|405:220|431:145,1,320,2|0 +328,64,114875,2,2,B|259:102|219:201|275:293|350:319,1,320,2|0 +488,340,115625,2,0,B|456:172,1,160,2|0 +416,72,116125,5,6 +288,140,116375,1,0 +164,68,116625,1,8 +36,136,116875,1,0 +104,264,117125,1,6 +232,332,117375,1,0 +356,260,117625,1,8 +484,328,117875,1,0 +356,384,118125,1,6 +256,12,128125,5,4 +256,12,128250,1,2 +336,128,128500,1,0 +336,128,128625,1,2 +400,0,128875,1,0 +400,0,129000,1,2 +492,112,129250,1,0 +492,112,129375,1,2 +440,248,129625,2,2,B|272:284,1,160 +256,108,130125,5,4 +256,108,130250,1,2 +176,224,130500,1,0 +176,224,130625,1,2 +112,96,130875,1,0 +112,96,131000,1,2 +20,208,131250,1,0 +20,208,131375,1,2 +72,344,131625,2,2,B|240:380,1,160 +408,376,132125,6,0,B|512:352|584:248|592:-32|416:-48|256:-80|96:-16|56:88|8:224|88:304|144:336|184:368|256:368|256:368|328:368|368:336|424:304|504:224|456:88|416:-16|256:-80|96:-48|-80:-32|-72:248|0:352|104:376,1,2240,6|0 +256,192,135875,5,2 +256,192,136000,1,0 +256,192,136125,1,4 +136,104,136375,1,0 +132,240,136625,1,8 +133,240,136750,1,0 +256,280,137000,1,0 +255,280,137125,1,8 +256,280,137250,1,0 +256,280,137375,1,0 +380,240,137625,1,8 +376,104,137875,1,0 +256,124,138125,5,4 +256,124,138375,1,0 +144,192,138625,1,8 +144,192,138750,1,0 +256,260,139000,1,0 +256,260,139125,1,8 +256,260,139250,1,0 +256,260,139375,1,0 +368,192,139625,1,8 +256,124,139875,1,0 +256,124,140000,1,0 +256,124,140125,2,2,B|188:112|212:76|188:36|256:20,1,160,6|2 +332,128,140625,5,8 +332,128,140750,1,0 +332,256,141000,1,0 +332,256,141125,1,8 +332,256,141250,1,0 +332,256,141375,1,0 +180,256,141625,1,8 +180,128,141875,1,0 +256,56,142125,5,4 +256,56,142375,1,0 +256,160,142625,1,8 +256,160,142750,1,0 +256,264,143000,1,0 +256,264,143125,1,8 +256,264,143250,1,0 +256,264,143375,1,0 +188,352,143625,1,8 +324,352,143875,1,0 +324,352,144000,1,0 +324,352,144125,2,0,B|492:352,1,160,6|2 +392,280,144625,5,8 +392,280,144750,1,0 +324,192,145000,1,0 +324,192,145125,1,8 +324,192,145250,1,0 +324,192,145375,1,0 +188,192,145625,1,8 +120,280,145875,1,0 +256,288,146125,5,4 +256,288,146375,1,0 +256,176,146625,1,8 +256,176,146750,1,0 +176,96,147000,1,0 +176,96,147125,1,8 +176,96,147250,1,0 +176,96,147375,1,0 +256,16,147625,1,8 +336,96,147875,1,0 +336,96,148000,1,0 +336,96,148125,2,6,B|400:156|388:224|364:248,1,160,6|2 +256,272,148625,5,8 +240,264,148750,1,0 +240,180,149000,1,0 +256,172,149125,1,8 +272,164,149250,1,0 +288,156,149375,1,0 +256,64,149625,1,8 +256,64,149875,1,0 +116,180,150125,5,0 +120,200,150250,1,0 +132,224,150375,1,0 +152,236,150500,1,0 +176,240,150625,1,8 +208,240,150750,1,0 +232,236,150875,1,0 +248,216,151000,1,0 +256,192,151125,1,8 +260,168,151250,1,0 +272,144,151375,1,8 +292,132,151500,1,0 +316,128,151625,1,8 +348,128,151750,1,8 +372,132,151875,1,8 +388,152,152000,1,0 +404,184,152125,6,0,B|436:250|377:334|292:300,1,160,6|0 +108,200,152625,2,0,B|76:134|135:50|220:84,1,160,6|0 +256,192,153125,2,0,B|256:100,1,80,2|0 +256,192,153375,2,0,B|256:368,2,160,2|8|0 +360,60,154125,5,0 +360,60,154250,1,0 +360,60,154375,1,2 +256,12,154625,1,0 +256,12,154750,1,0 +256,12,154875,1,2 +154,64,155125,1,0 +154,64,155250,1,2 +155,63,155375,2,0,B|87:119|115:191|179:211|227:179,2,160,0|8|0 +163,74,156000,5,0 +163,74,156125,1,0 +163,74,156250,2,2,B|174:151|299:265|445:180|473:106,1,400,2|0 +320,80,157125,2,2,B|224:88|184:188|224:288|320:295,1,320 +348,292,157750,1,0 +380,280,157875,1,0 +404,260,158000,1,0 +412,236,158125,1,0 +412,208,158250,1,0 +404,180,158375,1,0 +264,68,158625,2,0,B|184:104,2,80,2|0|2 +164,216,159125,2,0,B|244:180,2,80,2|0|2 +56,144,159625,5,8 +64,276,159875,1,8 +64,276,160000,1,8 +64,276,160125,2,0,B|24:352,2,80,2|0|0 +128,288,160500,2,0,B|136:188,2,80,2|0|0 +192,300,160875,2,0,B|200:400,2,80,2|0|0 +240,256,161250,2,0,B|304:176,2,80,2|0|0 +284,304,161625,2,0,B|356:380,2,80,2|0|0 +328,256,162000,6,0,B|456:236,2,80,0|2|0 +308,192,162375,2,0,B|180:172,2,80,0|2|0 +340,136,162750,2,0,B|468:116,2,80,0|2|0 +284,100,163125,2,0,B|264:-28,2,80,0|2|0 +224,128,163500,2,0,B|204:256,2,80,0|2|0 +180,76,163875,6,0,B|92:52,2,80,2|0|0 +144,132,164250,2,0,B|72:184,2,80,2|0|0 +168,196,164625,2,0,B|240:248,2,80,2|0|0 +136,256,165000,2,0,B|96:340,2,80,2|0|0 +188,296,165375,2,0,B|228:380,2,80,2|0|0 +236,252,165750,1,0 +236,252,165875,1,2 +364,276,166125,6,2,B|408:176|360:156|320:168|296:176|268:132|264:112|272:76|304:52|328:40,1,240,2|0 +264,24,166625,2,2,B|308:124|260:144|220:132|196:124|168:168|164:188|172:224|204:248|228:260,1,240,2|0 +192,280,167125,1,0 +320,376,167375,1,0 +192,376,167625,1,0 +256,328,167750,1,0 +320,280,167875,1,0 +256,124,168125,1,6 +256,192,168250,12,0,170125 +256,192,171125,12,6,172125 +48,56,172375,5,0 +20,184,172625,2,0,B|16:264|92:316|152:304,1,160,8|0 +240,300,173125,1,2 +200,176,173375,1,0 +324,220,173625,2,0,B|360:220|416:258|412:338,1,160,8|0 +412,334,174000,1,0 +412,334,174125,2,0,B|456:156,1,160,6|0 +398,35,174625,2,0,B|220:-8,1,160,2|0 +245,0,175000,1,0 +245,0,175125,2,0,B|201:178,1,160,6|0 +259,299,175625,2,0,B|437:342,1,160,2|0 +424,176,176125,5,6 +272,128,176375,1,0 +116,152,176625,1,8 +173,253,176875,2,2,B|257:233|295:142|287:44|213:0,2,320,2|2|0 +28,204,178125,2,2,B|356:316,1,320 +172,360,178875,2,2,B|500:248,1,320,2|0 +384,148,179625,2,0,B|292:168|224:96|232:44,1,160,2|0 +244,93,180000,1,0 +244,93,180125,6,0,B|64:120,1,160,6|0 +100,268,180625,2,0,B|256:296,1,160,8|0 +257,296,181000,1,0 +256,296,181125,2,0,B|413:267,1,160,6|0 +426,116,181625,2,0,B|267:93,1,160,8|2 +267,93,182000,5,2 +267,93,182125,2,2,B|180:112|168:212,1,160,2|0 +140,380,182625,2,0,B|227:361|239:261,1,160,8|0 +62,169,183125,2,2,B|80:256|180:268,1,160,2|0 +348,296,183625,2,0,B|329:208|229:196,1,160,8|0 +64,172,184125,1,6 +256,192,184250,12,2,185625 +48,188,186125,6,2,B|96:108|256:108|256:192|256:276|416:276|464:196,1,480,2|0 +328,144,187125,2,0,B|296:316,1,160,2|0 +184,240,187625,2,0,B|216:68,1,160,2|0 +256,192,188125,1,6 +256,192,188250,12,2,189625 +464,188,190125,6,2,B|416:108|256:108|256:192|256:276|96:276|48:196,1,480,2|0 +184,144,191125,2,0,B|216:316,1,160,2|0 +328,240,191625,2,0,B|296:68,1,160,2|0 +164,32,192125,5,6 +28,84,192375,1,0 +28,228,192625,1,8 +128,332,192875,2,2,B|160:224|300:172|408:244,2,320,2|2|0 +276,356,194125,2,2,B|384:324|436:184|364:76,1,320 +236,28,194875,2,2,B|128:60|76:200|148:308,1,320,2|0 +280,268,195625,2,0,B|232:116,1,160,2|0 +104,52,196125,5,6 +136,192,196375,1,0 +116,344,196625,1,8 +256,312,196875,1,0 +332,312,197000,1,0 +408,332,197125,1,6 +392,264,197250,1,0 +376,192,197375,1,0 +396,40,197625,1,8 +256,72,197875,5,0 +256,72,198000,1,0 +256,72,198125,1,6 +136,192,198625,1,6 +256,312,199125,1,6 +376,192,199625,1,6 +256,192,200125,1,6 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1284935-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1284935-expected-conversion.json new file mode 100644 index 0000000000..8976f6b066 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1284935-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":707.0,"Objects":[{"StartTime":707.0,"Position":65.0,"HyperDash":false},{"StartTime":759.0,"Position":482.0,"HyperDash":false},{"StartTime":811.0,"Position":164.0,"HyperDash":false},{"StartTime":863.0,"Position":315.0,"HyperDash":false},{"StartTime":915.0,"Position":145.0,"HyperDash":false},{"StartTime":967.0,"Position":159.0,"HyperDash":false},{"StartTime":1019.0,"Position":310.0,"HyperDash":false},{"StartTime":1071.0,"Position":441.0,"HyperDash":false},{"StartTime":1123.0,"Position":428.0,"HyperDash":false},{"StartTime":1175.0,"Position":243.0,"HyperDash":false},{"StartTime":1227.0,"Position":422.0,"HyperDash":false},{"StartTime":1280.0,"Position":481.0,"HyperDash":false},{"StartTime":1332.0,"Position":104.0,"HyperDash":false},{"StartTime":1384.0,"Position":473.0,"HyperDash":false},{"StartTime":1436.0,"Position":135.0,"HyperDash":false},{"StartTime":1488.0,"Position":360.0,"HyperDash":false},{"StartTime":1540.0,"Position":123.0,"HyperDash":false},{"StartTime":1592.0,"Position":42.0,"HyperDash":false},{"StartTime":1644.0,"Position":393.0,"HyperDash":false},{"StartTime":1696.0,"Position":75.0,"HyperDash":false},{"StartTime":1748.0,"Position":377.0,"HyperDash":false},{"StartTime":1800.0,"Position":354.0,"HyperDash":false},{"StartTime":1853.0,"Position":287.0,"HyperDash":false},{"StartTime":1905.0,"Position":361.0,"HyperDash":false},{"StartTime":1957.0,"Position":479.0,"HyperDash":false},{"StartTime":2009.0,"Position":346.0,"HyperDash":false},{"StartTime":2061.0,"Position":266.0,"HyperDash":false},{"StartTime":2113.0,"Position":400.0,"HyperDash":false},{"StartTime":2165.0,"Position":202.0,"HyperDash":false},{"StartTime":2217.0,"Position":500.0,"HyperDash":false},{"StartTime":2269.0,"Position":80.0,"HyperDash":false},{"StartTime":2321.0,"Position":399.0,"HyperDash":false},{"StartTime":2374.0,"Position":455.0,"HyperDash":false}]},{"StartTime":2707.0,"Objects":[{"StartTime":2707.0,"Position":368.0,"HyperDash":false},{"StartTime":2781.0,"Position":333.777771,"HyperDash":false},{"StartTime":2855.0,"Position":339.555542,"HyperDash":false},{"StartTime":2929.0,"Position":289.3333,"HyperDash":false},{"StartTime":3040.0,"Position":268.0,"HyperDash":false}]},{"StartTime":3207.0,"Objects":[{"StartTime":3207.0,"Position":288.0,"HyperDash":false},{"StartTime":3272.0,"Position":291.748444,"HyperDash":false},{"StartTime":3373.0,"Position":300.12677,"HyperDash":false}]},{"StartTime":3707.0,"Objects":[{"StartTime":3707.0,"Position":192.0,"HyperDash":false},{"StartTime":3790.0,"Position":154.075073,"HyperDash":false},{"StartTime":3873.0,"Position":136.150146,"HyperDash":false},{"StartTime":3956.0,"Position":109.225227,"HyperDash":false},{"StartTime":4040.0,"Position":92.0,"HyperDash":false},{"StartTime":4114.0,"Position":105.222221,"HyperDash":false},{"StartTime":4188.0,"Position":131.444443,"HyperDash":false},{"StartTime":4262.0,"Position":153.666656,"HyperDash":false},{"StartTime":4373.0,"Position":192.0,"HyperDash":false}]},{"StartTime":4707.0,"Objects":[{"StartTime":4707.0,"Position":288.0,"HyperDash":false}]},{"StartTime":5041.0,"Objects":[{"StartTime":5041.0,"Position":144.0,"HyperDash":false}]},{"StartTime":5374.0,"Objects":[{"StartTime":5374.0,"Position":304.0,"HyperDash":false},{"StartTime":5457.0,"Position":335.611359,"HyperDash":false},{"StartTime":5540.0,"Position":342.222717,"HyperDash":false},{"StartTime":5623.0,"Position":344.834076,"HyperDash":false},{"StartTime":5707.0,"Position":374.657623,"HyperDash":false},{"StartTime":5790.0,"Position":377.268982,"HyperDash":false},{"StartTime":5873.0,"Position":405.880341,"HyperDash":false},{"StartTime":5956.0,"Position":430.4917,"HyperDash":false},{"StartTime":6040.0,"Position":445.421326,"HyperDash":false},{"StartTime":6123.0,"Position":408.916077,"HyperDash":false},{"StartTime":6206.0,"Position":410.3047,"HyperDash":false},{"StartTime":6289.0,"Position":405.693359,"HyperDash":false},{"StartTime":6373.0,"Position":374.8698,"HyperDash":false},{"StartTime":6447.0,"Position":375.168121,"HyperDash":false},{"StartTime":6522.0,"Position":334.254242,"HyperDash":false},{"StartTime":6596.0,"Position":316.552521,"HyperDash":false},{"StartTime":6707.0,"Position":304.0,"HyperDash":false}]},{"StartTime":7041.0,"Objects":[{"StartTime":7041.0,"Position":208.0,"HyperDash":false}]},{"StartTime":7374.0,"Objects":[{"StartTime":7374.0,"Position":304.0,"HyperDash":false},{"StartTime":7448.0,"Position":293.1427,"HyperDash":false},{"StartTime":7522.0,"Position":328.2854,"HyperDash":false},{"StartTime":7596.0,"Position":323.4281,"HyperDash":false},{"StartTime":7707.0,"Position":318.142151,"HyperDash":false}]},{"StartTime":8041.0,"Objects":[{"StartTime":8041.0,"Position":160.0,"HyperDash":false},{"StartTime":8115.0,"Position":156.777771,"HyperDash":false},{"StartTime":8189.0,"Position":98.55556,"HyperDash":false},{"StartTime":8263.0,"Position":87.33333,"HyperDash":false},{"StartTime":8374.0,"Position":60.0,"HyperDash":false}]},{"StartTime":8541.0,"Objects":[{"StartTime":8541.0,"Position":176.0,"HyperDash":false}]},{"StartTime":8707.0,"Objects":[{"StartTime":8707.0,"Position":160.0,"HyperDash":false},{"StartTime":8790.0,"Position":189.827057,"HyperDash":false},{"StartTime":8873.0,"Position":214.480759,"HyperDash":false},{"StartTime":8956.0,"Position":199.348236,"HyperDash":false},{"StartTime":9040.0,"Position":211.43425,"HyperDash":false},{"StartTime":9114.0,"Position":182.741974,"HyperDash":false},{"StartTime":9188.0,"Position":188.031326,"HyperDash":false},{"StartTime":9262.0,"Position":150.1092,"HyperDash":false},{"StartTime":9373.0,"Position":131.819717,"HyperDash":false}]},{"StartTime":9707.0,"Objects":[{"StartTime":9707.0,"Position":320.0,"HyperDash":false}]},{"StartTime":10041.0,"Objects":[{"StartTime":10041.0,"Position":352.0,"HyperDash":false},{"StartTime":10115.0,"Position":335.777771,"HyperDash":false},{"StartTime":10189.0,"Position":320.555542,"HyperDash":false},{"StartTime":10263.0,"Position":275.3333,"HyperDash":false},{"StartTime":10374.0,"Position":252.0,"HyperDash":false}]},{"StartTime":10707.0,"Objects":[{"StartTime":10707.0,"Position":416.0,"HyperDash":false},{"StartTime":10790.0,"Position":433.640656,"HyperDash":false},{"StartTime":10873.0,"Position":472.2328,"HyperDash":false},{"StartTime":10956.0,"Position":486.15274,"HyperDash":false},{"StartTime":11040.0,"Position":482.899384,"HyperDash":false},{"StartTime":11114.0,"Position":477.456268,"HyperDash":false},{"StartTime":11188.0,"Position":474.261353,"HyperDash":false},{"StartTime":11262.0,"Position":444.9807,"HyperDash":false},{"StartTime":11373.0,"Position":418.860382,"HyperDash":false}]},{"StartTime":11874.0,"Objects":[{"StartTime":11874.0,"Position":224.0,"HyperDash":false}]},{"StartTime":12041.0,"Objects":[{"StartTime":12041.0,"Position":160.0,"HyperDash":false},{"StartTime":12124.0,"Position":129.476608,"HyperDash":false},{"StartTime":12207.0,"Position":139.62587,"HyperDash":false},{"StartTime":12290.0,"Position":110.133484,"HyperDash":false},{"StartTime":12374.0,"Position":120.566429,"HyperDash":false},{"StartTime":12439.0,"Position":147.187912,"HyperDash":false},{"StartTime":12540.0,"Position":159.8762,"HyperDash":false}]},{"StartTime":12707.0,"Objects":[{"StartTime":12707.0,"Position":288.0,"HyperDash":false}]},{"StartTime":13374.0,"Objects":[{"StartTime":13374.0,"Position":464.0,"HyperDash":false},{"StartTime":13457.0,"Position":423.1,"HyperDash":false},{"StartTime":13540.0,"Position":431.2,"HyperDash":false},{"StartTime":13623.0,"Position":392.3,"HyperDash":false},{"StartTime":13707.0,"Position":364.1,"HyperDash":false},{"StartTime":13772.0,"Position":352.6,"HyperDash":false},{"StartTime":13874.0,"Position":314.0,"HyperDash":false}]},{"StartTime":14041.0,"Objects":[{"StartTime":14041.0,"Position":240.0,"HyperDash":false},{"StartTime":14124.0,"Position":215.182037,"HyperDash":false},{"StartTime":14207.0,"Position":213.6612,"HyperDash":false},{"StartTime":14290.0,"Position":180.052521,"HyperDash":false},{"StartTime":14374.0,"Position":198.218033,"HyperDash":false},{"StartTime":14439.0,"Position":203.99968,"HyperDash":false},{"StartTime":14541.0,"Position":239.397186,"HyperDash":false}]},{"StartTime":14707.0,"Objects":[{"StartTime":14707.0,"Position":320.0,"HyperDash":false},{"StartTime":14781.0,"Position":279.777771,"HyperDash":false},{"StartTime":14855.0,"Position":271.555542,"HyperDash":false},{"StartTime":14929.0,"Position":258.3333,"HyperDash":false},{"StartTime":15040.0,"Position":220.0,"HyperDash":false}]},{"StartTime":15374.0,"Objects":[{"StartTime":15374.0,"Position":320.0,"HyperDash":false},{"StartTime":15448.0,"Position":329.8606,"HyperDash":false},{"StartTime":15522.0,"Position":335.721161,"HyperDash":false},{"StartTime":15596.0,"Position":362.581757,"HyperDash":false},{"StartTime":15707.0,"Position":359.87262,"HyperDash":false}]},{"StartTime":16041.0,"Objects":[{"StartTime":16041.0,"Position":192.0,"HyperDash":false},{"StartTime":16115.0,"Position":166.777771,"HyperDash":false},{"StartTime":16189.0,"Position":161.555557,"HyperDash":false},{"StartTime":16263.0,"Position":112.333328,"HyperDash":false},{"StartTime":16374.0,"Position":92.0,"HyperDash":false}]},{"StartTime":16541.0,"Objects":[{"StartTime":16541.0,"Position":208.0,"HyperDash":false}]},{"StartTime":16707.0,"Objects":[{"StartTime":16707.0,"Position":176.0,"HyperDash":false}]},{"StartTime":17041.0,"Objects":[{"StartTime":17041.0,"Position":336.0,"HyperDash":false}]},{"StartTime":17207.0,"Objects":[{"StartTime":17207.0,"Position":288.0,"HyperDash":false},{"StartTime":17290.0,"Position":262.868042,"HyperDash":false},{"StartTime":17373.0,"Position":233.396667,"HyperDash":false},{"StartTime":17456.0,"Position":240.435333,"HyperDash":false},{"StartTime":17540.0,"Position":242.216,"HyperDash":false},{"StartTime":17605.0,"Position":250.097885,"HyperDash":false},{"StartTime":17707.0,"Position":281.828644,"HyperDash":false}]},{"StartTime":18041.0,"Objects":[{"StartTime":18041.0,"Position":480.0,"HyperDash":false}]},{"StartTime":18374.0,"Objects":[{"StartTime":18374.0,"Position":256.0,"HyperDash":false}]},{"StartTime":18707.0,"Objects":[{"StartTime":18707.0,"Position":416.0,"HyperDash":false},{"StartTime":18790.0,"Position":398.254333,"HyperDash":false},{"StartTime":18873.0,"Position":424.508667,"HyperDash":false},{"StartTime":18956.0,"Position":427.763,"HyperDash":false},{"StartTime":19040.0,"Position":425.044525,"HyperDash":false},{"StartTime":19105.0,"Position":408.809967,"HyperDash":false},{"StartTime":19207.0,"Position":429.580353,"HyperDash":false}]},{"StartTime":19374.0,"Objects":[{"StartTime":19374.0,"Position":336.0,"HyperDash":false},{"StartTime":19448.0,"Position":294.777771,"HyperDash":false},{"StartTime":19522.0,"Position":280.555542,"HyperDash":false},{"StartTime":19596.0,"Position":278.3333,"HyperDash":false},{"StartTime":19707.0,"Position":236.0,"HyperDash":false}]},{"StartTime":20041.0,"Objects":[{"StartTime":20041.0,"Position":256.0,"HyperDash":false},{"StartTime":20124.0,"Position":272.817963,"HyperDash":false},{"StartTime":20207.0,"Position":313.3388,"HyperDash":false},{"StartTime":20290.0,"Position":317.947479,"HyperDash":false},{"StartTime":20374.0,"Position":297.781982,"HyperDash":false},{"StartTime":20439.0,"Position":266.000336,"HyperDash":false},{"StartTime":20541.0,"Position":256.6028,"HyperDash":false}]},{"StartTime":20707.0,"Objects":[{"StartTime":20707.0,"Position":196.0,"HyperDash":false},{"StartTime":20781.0,"Position":169.13942,"HyperDash":false},{"StartTime":20855.0,"Position":192.278839,"HyperDash":false},{"StartTime":20929.0,"Position":170.418243,"HyperDash":false},{"StartTime":21040.0,"Position":156.12738,"HyperDash":false}]},{"StartTime":21374.0,"Objects":[{"StartTime":21374.0,"Position":320.0,"HyperDash":false},{"StartTime":21457.0,"Position":344.0784,"HyperDash":false},{"StartTime":21540.0,"Position":350.913055,"HyperDash":false},{"StartTime":21623.0,"Position":346.822418,"HyperDash":false},{"StartTime":21707.0,"Position":357.019379,"HyperDash":false},{"StartTime":21772.0,"Position":358.883179,"HyperDash":false},{"StartTime":21873.0,"Position":327.8019,"HyperDash":false}]},{"StartTime":22041.0,"Objects":[{"StartTime":22041.0,"Position":224.0,"HyperDash":false},{"StartTime":22115.0,"Position":183.777771,"HyperDash":false},{"StartTime":22189.0,"Position":175.555557,"HyperDash":false},{"StartTime":22263.0,"Position":140.333328,"HyperDash":false},{"StartTime":22374.0,"Position":124.0,"HyperDash":false}]},{"StartTime":22541.0,"Objects":[{"StartTime":22541.0,"Position":272.0,"HyperDash":false}]},{"StartTime":22707.0,"Objects":[{"StartTime":22707.0,"Position":204.0,"HyperDash":false}]},{"StartTime":23041.0,"Objects":[{"StartTime":23041.0,"Position":96.0,"HyperDash":false}]},{"StartTime":23374.0,"Objects":[{"StartTime":23374.0,"Position":208.0,"HyperDash":false},{"StartTime":23448.0,"Position":222.1427,"HyperDash":false},{"StartTime":23522.0,"Position":195.2854,"HyperDash":false},{"StartTime":23596.0,"Position":234.428085,"HyperDash":false},{"StartTime":23707.0,"Position":222.142136,"HyperDash":false}]},{"StartTime":24041.0,"Objects":[{"StartTime":24041.0,"Position":80.0,"HyperDash":false},{"StartTime":24124.0,"Position":113.9,"HyperDash":false},{"StartTime":24207.0,"Position":129.8,"HyperDash":false},{"StartTime":24290.0,"Position":153.7,"HyperDash":false},{"StartTime":24374.0,"Position":179.9,"HyperDash":false},{"StartTime":24439.0,"Position":201.4,"HyperDash":false},{"StartTime":24541.0,"Position":230.0,"HyperDash":false}]},{"StartTime":24707.0,"Objects":[{"StartTime":24707.0,"Position":112.0,"HyperDash":false}]},{"StartTime":25041.0,"Objects":[{"StartTime":25041.0,"Position":256.0,"HyperDash":false},{"StartTime":25106.0,"Position":250.808792,"HyperDash":false},{"StartTime":25207.0,"Position":240.188614,"HyperDash":false}]},{"StartTime":25541.0,"Objects":[{"StartTime":25541.0,"Position":352.0,"HyperDash":false},{"StartTime":25606.0,"Position":340.5016,"HyperDash":false},{"StartTime":25707.0,"Position":355.834839,"HyperDash":false}]},{"StartTime":26041.0,"Objects":[{"StartTime":26041.0,"Position":192.0,"HyperDash":false},{"StartTime":26115.0,"Position":191.8573,"HyperDash":false},{"StartTime":26189.0,"Position":173.7146,"HyperDash":false},{"StartTime":26263.0,"Position":175.571915,"HyperDash":false},{"StartTime":26374.0,"Position":177.857864,"HyperDash":false}]},{"StartTime":26707.0,"Objects":[{"StartTime":26707.0,"Position":272.0,"HyperDash":false},{"StartTime":26781.0,"Position":275.222229,"HyperDash":false},{"StartTime":26855.0,"Position":318.444458,"HyperDash":false},{"StartTime":26929.0,"Position":333.6667,"HyperDash":false},{"StartTime":27040.0,"Position":372.0,"HyperDash":false}]},{"StartTime":27207.0,"Objects":[{"StartTime":27207.0,"Position":256.0,"HyperDash":false}]},{"StartTime":27374.0,"Objects":[{"StartTime":27374.0,"Position":288.0,"HyperDash":false}]},{"StartTime":27707.0,"Objects":[{"StartTime":27707.0,"Position":416.0,"HyperDash":false},{"StartTime":27772.0,"Position":401.748444,"HyperDash":false},{"StartTime":27873.0,"Position":428.12677,"HyperDash":false}]},{"StartTime":28207.0,"Objects":[{"StartTime":28207.0,"Position":288.0,"HyperDash":false},{"StartTime":28281.0,"Position":250.777771,"HyperDash":false},{"StartTime":28355.0,"Position":249.555557,"HyperDash":false},{"StartTime":28429.0,"Position":219.333328,"HyperDash":false},{"StartTime":28540.0,"Position":188.0,"HyperDash":false}]},{"StartTime":28707.0,"Objects":[{"StartTime":28707.0,"Position":256.0,"HyperDash":false},{"StartTime":28781.0,"Position":253.111572,"HyperDash":false},{"StartTime":28855.0,"Position":249.223145,"HyperDash":false},{"StartTime":28929.0,"Position":256.334747,"HyperDash":false},{"StartTime":29040.0,"Position":270.0021,"HyperDash":false}]},{"StartTime":29374.0,"Objects":[{"StartTime":29374.0,"Position":128.0,"HyperDash":false},{"StartTime":29457.0,"Position":97.70407,"HyperDash":false},{"StartTime":29540.0,"Position":72.07541,"HyperDash":false},{"StartTime":29623.0,"Position":69.19281,"HyperDash":false},{"StartTime":29707.0,"Position":64.12629,"HyperDash":false},{"StartTime":29781.0,"Position":68.7450943,"HyperDash":false},{"StartTime":29855.0,"Position":93.5549545,"HyperDash":false},{"StartTime":29929.0,"Position":84.38264,"HyperDash":false},{"StartTime":30040.0,"Position":127.072174,"HyperDash":false}]},{"StartTime":30374.0,"Objects":[{"StartTime":30374.0,"Position":320.0,"HyperDash":false}]},{"StartTime":30707.0,"Objects":[{"StartTime":30707.0,"Position":416.0,"HyperDash":false}]},{"StartTime":30874.0,"Objects":[{"StartTime":30874.0,"Position":432.0,"HyperDash":false},{"StartTime":30948.0,"Position":429.1427,"HyperDash":false},{"StartTime":31022.0,"Position":455.2854,"HyperDash":false},{"StartTime":31096.0,"Position":422.4281,"HyperDash":false},{"StartTime":31207.0,"Position":446.142151,"HyperDash":false}]},{"StartTime":31374.0,"Objects":[{"StartTime":31374.0,"Position":320.0,"HyperDash":false}]},{"StartTime":31707.0,"Objects":[{"StartTime":31707.0,"Position":240.0,"HyperDash":false},{"StartTime":31772.0,"Position":250.251556,"HyperDash":false},{"StartTime":31873.0,"Position":227.873215,"HyperDash":false}]},{"StartTime":32041.0,"Objects":[{"StartTime":32041.0,"Position":304.0,"HyperDash":false},{"StartTime":32124.0,"Position":346.659271,"HyperDash":false},{"StartTime":32207.0,"Position":333.21402,"HyperDash":false},{"StartTime":32290.0,"Position":348.822571,"HyperDash":false},{"StartTime":32374.0,"Position":369.8608,"HyperDash":false},{"StartTime":32448.0,"Position":377.38208,"HyperDash":false},{"StartTime":32522.0,"Position":368.24884,"HyperDash":false},{"StartTime":32596.0,"Position":327.2163,"HyperDash":false},{"StartTime":32707.0,"Position":302.6493,"HyperDash":false}]},{"StartTime":33041.0,"Objects":[{"StartTime":33041.0,"Position":32.0,"HyperDash":false}]},{"StartTime":33374.0,"Objects":[{"StartTime":33374.0,"Position":304.0,"HyperDash":false}]},{"StartTime":33541.0,"Objects":[{"StartTime":33541.0,"Position":368.0,"HyperDash":false},{"StartTime":33615.0,"Position":362.176758,"HyperDash":false},{"StartTime":33689.0,"Position":375.77478,"HyperDash":false},{"StartTime":33763.0,"Position":391.632355,"HyperDash":false},{"StartTime":33874.0,"Position":367.9668,"HyperDash":false}]},{"StartTime":34207.0,"Objects":[{"StartTime":34207.0,"Position":80.0,"HyperDash":false}]},{"StartTime":34374.0,"Objects":[{"StartTime":34374.0,"Position":176.0,"HyperDash":false}]},{"StartTime":34541.0,"Objects":[{"StartTime":34541.0,"Position":80.0,"HyperDash":false}]},{"StartTime":34707.0,"Objects":[{"StartTime":34707.0,"Position":200.0,"HyperDash":false}]},{"StartTime":35041.0,"Objects":[{"StartTime":35041.0,"Position":336.0,"HyperDash":false},{"StartTime":35115.0,"Position":338.1427,"HyperDash":false},{"StartTime":35189.0,"Position":342.2854,"HyperDash":false},{"StartTime":35263.0,"Position":338.4281,"HyperDash":false},{"StartTime":35374.0,"Position":350.142151,"HyperDash":false}]},{"StartTime":35707.0,"Objects":[{"StartTime":35707.0,"Position":208.0,"HyperDash":false},{"StartTime":35772.0,"Position":217.808792,"HyperDash":false},{"StartTime":35873.0,"Position":192.188614,"HyperDash":false}]},{"StartTime":36207.0,"Objects":[{"StartTime":36207.0,"Position":336.0,"HyperDash":false},{"StartTime":36272.0,"Position":351.191223,"HyperDash":false},{"StartTime":36373.0,"Position":351.8114,"HyperDash":false}]},{"StartTime":36707.0,"Objects":[{"StartTime":36707.0,"Position":208.0,"HyperDash":false},{"StartTime":36781.0,"Position":178.777771,"HyperDash":false},{"StartTime":36855.0,"Position":179.555557,"HyperDash":false},{"StartTime":36929.0,"Position":125.333328,"HyperDash":false},{"StartTime":37040.0,"Position":108.0,"HyperDash":false}]},{"StartTime":37374.0,"Objects":[{"StartTime":37374.0,"Position":416.0,"HyperDash":false}]},{"StartTime":37541.0,"Objects":[{"StartTime":37541.0,"Position":320.0,"HyperDash":false},{"StartTime":37615.0,"Position":322.379059,"HyperDash":false},{"StartTime":37689.0,"Position":309.7581,"HyperDash":false},{"StartTime":37763.0,"Position":318.137146,"HyperDash":false},{"StartTime":37874.0,"Position":335.205719,"HyperDash":false}]},{"StartTime":38041.0,"Objects":[{"StartTime":38041.0,"Position":208.0,"HyperDash":false}]},{"StartTime":38374.0,"Objects":[{"StartTime":38374.0,"Position":416.0,"HyperDash":false},{"StartTime":38439.0,"Position":410.191223,"HyperDash":false},{"StartTime":38540.0,"Position":431.8114,"HyperDash":false}]},{"StartTime":38874.0,"Objects":[{"StartTime":38874.0,"Position":288.0,"HyperDash":false},{"StartTime":38939.0,"Position":273.808777,"HyperDash":false},{"StartTime":39040.0,"Position":272.1886,"HyperDash":false}]},{"StartTime":39207.0,"Objects":[{"StartTime":39207.0,"Position":336.0,"HyperDash":false},{"StartTime":39281.0,"Position":360.222229,"HyperDash":false},{"StartTime":39355.0,"Position":369.444458,"HyperDash":false},{"StartTime":39429.0,"Position":419.6667,"HyperDash":false},{"StartTime":39540.0,"Position":436.0,"HyperDash":false}]},{"StartTime":39707.0,"Objects":[{"StartTime":39707.0,"Position":320.0,"HyperDash":false}]},{"StartTime":40041.0,"Objects":[{"StartTime":40041.0,"Position":160.0,"HyperDash":false}]},{"StartTime":40374.0,"Objects":[{"StartTime":40374.0,"Position":384.0,"HyperDash":false},{"StartTime":40448.0,"Position":396.1427,"HyperDash":false},{"StartTime":40522.0,"Position":380.2854,"HyperDash":false},{"StartTime":40596.0,"Position":408.4281,"HyperDash":false},{"StartTime":40707.0,"Position":398.142151,"HyperDash":false}]},{"StartTime":41041.0,"Objects":[{"StartTime":41041.0,"Position":112.0,"HyperDash":false}]},{"StartTime":41374.0,"Objects":[{"StartTime":41374.0,"Position":132.0,"HyperDash":false}]},{"StartTime":41541.0,"Objects":[{"StartTime":41541.0,"Position":48.0,"HyperDash":false},{"StartTime":41615.0,"Position":31.8573036,"HyperDash":false},{"StartTime":41689.0,"Position":31.7146072,"HyperDash":false},{"StartTime":41763.0,"Position":40.571907,"HyperDash":false},{"StartTime":41874.0,"Position":33.8578644,"HyperDash":false}]},{"StartTime":42041.0,"Objects":[{"StartTime":42041.0,"Position":160.0,"HyperDash":false}]},{"StartTime":42374.0,"Objects":[{"StartTime":42374.0,"Position":320.0,"HyperDash":false}]},{"StartTime":42707.0,"Objects":[{"StartTime":42707.0,"Position":96.0,"HyperDash":false},{"StartTime":42790.0,"Position":64.13681,"HyperDash":false},{"StartTime":42873.0,"Position":72.60394,"HyperDash":false},{"StartTime":42956.0,"Position":54.03947,"HyperDash":false},{"StartTime":43040.0,"Position":54.30264,"HyperDash":false},{"StartTime":43105.0,"Position":72.19569,"HyperDash":false},{"StartTime":43206.0,"Position":95.39718,"HyperDash":false}]},{"StartTime":43374.0,"Objects":[{"StartTime":43374.0,"Position":224.0,"HyperDash":false}]},{"StartTime":43707.0,"Objects":[{"StartTime":43707.0,"Position":352.0,"HyperDash":false}]},{"StartTime":44040.0,"Objects":[{"StartTime":44040.0,"Position":160.0,"HyperDash":false}]},{"StartTime":44374.0,"Objects":[{"StartTime":44374.0,"Position":304.0,"HyperDash":false},{"StartTime":44457.0,"Position":309.591553,"HyperDash":false},{"StartTime":44540.0,"Position":325.605743,"HyperDash":false},{"StartTime":44623.0,"Position":365.538,"HyperDash":false},{"StartTime":44707.0,"Position":357.421478,"HyperDash":false},{"StartTime":44781.0,"Position":336.6104,"HyperDash":false},{"StartTime":44855.0,"Position":350.104462,"HyperDash":false},{"StartTime":44929.0,"Position":333.432159,"HyperDash":false},{"StartTime":45040.0,"Position":304.669952,"HyperDash":false}]},{"StartTime":45374.0,"Objects":[{"StartTime":45374.0,"Position":136.0,"HyperDash":false},{"StartTime":45457.0,"Position":127.769325,"HyperDash":false},{"StartTime":45540.0,"Position":88.53865,"HyperDash":false},{"StartTime":45623.0,"Position":83.30798,"HyperDash":false},{"StartTime":45707.0,"Position":70.88176,"HyperDash":false},{"StartTime":45790.0,"Position":61.6510925,"HyperDash":false},{"StartTime":45873.0,"Position":38.3226547,"HyperDash":false},{"StartTime":45956.0,"Position":42.4555435,"HyperDash":false},{"StartTime":46040.0,"Position":70.88177,"HyperDash":false},{"StartTime":46114.0,"Position":83.35248,"HyperDash":false},{"StartTime":46188.0,"Position":98.8232,"HyperDash":false},{"StartTime":46262.0,"Position":107.293922,"HyperDash":false},{"StartTime":46373.0,"Position":136.0,"HyperDash":false}]},{"StartTime":46874.0,"Objects":[{"StartTime":46874.0,"Position":368.0,"HyperDash":false},{"StartTime":46957.0,"Position":368.641052,"HyperDash":false},{"StartTime":47040.0,"Position":388.2821,"HyperDash":false},{"StartTime":47123.0,"Position":392.923126,"HyperDash":false},{"StartTime":47207.0,"Position":378.596,"HyperDash":false},{"StartTime":47272.0,"Position":362.664276,"HyperDash":false},{"StartTime":47374.0,"Position":383.9099,"HyperDash":false}]},{"StartTime":47707.0,"Objects":[{"StartTime":47707.0,"Position":160.0,"HyperDash":false}]},{"StartTime":48041.0,"Objects":[{"StartTime":48041.0,"Position":144.0,"HyperDash":false},{"StartTime":48124.0,"Position":140.536209,"HyperDash":false},{"StartTime":48207.0,"Position":120.072418,"HyperDash":false},{"StartTime":48290.0,"Position":77.60862,"HyperDash":false},{"StartTime":48374.0,"Position":81.95851,"HyperDash":false},{"StartTime":48457.0,"Position":51.4947128,"HyperDash":false},{"StartTime":48541.0,"Position":50.8446045,"HyperDash":false},{"StartTime":48624.0,"Position":47.308403,"HyperDash":false},{"StartTime":48707.0,"Position":81.7722,"HyperDash":false},{"StartTime":48781.0,"Position":97.5592041,"HyperDash":false},{"StartTime":48856.0,"Position":126.5325,"HyperDash":false},{"StartTime":48930.0,"Position":134.3195,"HyperDash":false},{"StartTime":49041.0,"Position":144.0,"HyperDash":false}]},{"StartTime":49374.0,"Objects":[{"StartTime":49374.0,"Position":256.0,"HyperDash":false},{"StartTime":49457.0,"Position":275.705048,"HyperDash":false},{"StartTime":49540.0,"Position":297.414978,"HyperDash":false},{"StartTime":49623.0,"Position":295.170868,"HyperDash":false},{"StartTime":49707.0,"Position":311.122,"HyperDash":false},{"StartTime":49790.0,"Position":299.525726,"HyperDash":false},{"StartTime":49873.0,"Position":296.3256,"HyperDash":false},{"StartTime":49956.0,"Position":290.4679,"HyperDash":false},{"StartTime":50040.0,"Position":301.014038,"HyperDash":false},{"StartTime":50114.0,"Position":289.537323,"HyperDash":false},{"StartTime":50189.0,"Position":285.4608,"HyperDash":false},{"StartTime":50263.0,"Position":241.873749,"HyperDash":false},{"StartTime":50373.0,"Position":235.304214,"HyperDash":false}]},{"StartTime":50707.0,"Objects":[{"StartTime":50707.0,"Position":384.0,"HyperDash":false},{"StartTime":50790.0,"Position":399.712433,"HyperDash":false},{"StartTime":50873.0,"Position":415.424866,"HyperDash":false},{"StartTime":50956.0,"Position":442.137268,"HyperDash":false},{"StartTime":51040.0,"Position":459.075165,"HyperDash":false},{"StartTime":51105.0,"Position":484.729462,"HyperDash":false},{"StartTime":51206.0,"Position":496.5,"HyperDash":false}]},{"StartTime":51374.0,"Objects":[{"StartTime":51374.0,"Position":400.0,"HyperDash":false}]},{"StartTime":51874.0,"Objects":[{"StartTime":51874.0,"Position":244.0,"HyperDash":false},{"StartTime":51957.0,"Position":220.5127,"HyperDash":false},{"StartTime":52040.0,"Position":194.025391,"HyperDash":false},{"StartTime":52123.0,"Position":197.538086,"HyperDash":false},{"StartTime":52207.0,"Position":169.828033,"HyperDash":false},{"StartTime":52272.0,"Position":151.350021,"HyperDash":false},{"StartTime":52374.0,"Position":132.630676,"HyperDash":false}]},{"StartTime":52707.0,"Objects":[{"StartTime":52707.0,"Position":208.0,"HyperDash":false},{"StartTime":52781.0,"Position":217.666672,"HyperDash":false},{"StartTime":52855.0,"Position":252.333344,"HyperDash":false},{"StartTime":52929.0,"Position":248.0,"HyperDash":false},{"StartTime":53040.0,"Position":283.0,"HyperDash":false}]},{"StartTime":53373.0,"Objects":[{"StartTime":53373.0,"Position":368.0,"HyperDash":false},{"StartTime":53447.0,"Position":389.547058,"HyperDash":false},{"StartTime":53521.0,"Position":360.0941,"HyperDash":false},{"StartTime":53595.0,"Position":373.641144,"HyperDash":false},{"StartTime":53706.0,"Position":379.4617,"HyperDash":false}]},{"StartTime":54040.0,"Objects":[{"StartTime":54040.0,"Position":255.0,"HyperDash":false},{"StartTime":54114.0,"Position":252.333328,"HyperDash":false},{"StartTime":54188.0,"Position":226.666656,"HyperDash":false},{"StartTime":54262.0,"Position":195.0,"HyperDash":false},{"StartTime":54373.0,"Position":180.0,"HyperDash":false}]},{"StartTime":54707.0,"Objects":[{"StartTime":54707.0,"Position":368.0,"HyperDash":false}]},{"StartTime":55374.0,"Objects":[{"StartTime":55374.0,"Position":160.0,"HyperDash":false},{"StartTime":55448.0,"Position":163.26001,"HyperDash":false},{"StartTime":55522.0,"Position":156.520035,"HyperDash":false},{"StartTime":55596.0,"Position":132.780045,"HyperDash":false},{"StartTime":55707.0,"Position":147.670074,"HyperDash":false}]},{"StartTime":56041.0,"Objects":[{"StartTime":56041.0,"Position":320.0,"HyperDash":false},{"StartTime":56115.0,"Position":345.222229,"HyperDash":false},{"StartTime":56189.0,"Position":369.444458,"HyperDash":false},{"StartTime":56263.0,"Position":402.6667,"HyperDash":false},{"StartTime":56374.0,"Position":420.0,"HyperDash":false}]},{"StartTime":56707.0,"Objects":[{"StartTime":56707.0,"Position":256.0,"HyperDash":false},{"StartTime":56781.0,"Position":245.8573,"HyperDash":false},{"StartTime":56855.0,"Position":239.7146,"HyperDash":false},{"StartTime":56929.0,"Position":253.571915,"HyperDash":false},{"StartTime":57040.0,"Position":241.857864,"HyperDash":false}]},{"StartTime":57207.0,"Objects":[{"StartTime":57207.0,"Position":328.0,"HyperDash":false},{"StartTime":57290.0,"Position":334.131958,"HyperDash":false},{"StartTime":57373.0,"Position":357.603333,"HyperDash":false},{"StartTime":57456.0,"Position":386.564667,"HyperDash":false},{"StartTime":57540.0,"Position":373.784027,"HyperDash":false},{"StartTime":57605.0,"Position":370.9021,"HyperDash":false},{"StartTime":57707.0,"Position":334.171356,"HyperDash":false}]},{"StartTime":58041.0,"Objects":[{"StartTime":58041.0,"Position":176.0,"HyperDash":false},{"StartTime":58115.0,"Position":153.777771,"HyperDash":false},{"StartTime":58189.0,"Position":113.555557,"HyperDash":false},{"StartTime":58263.0,"Position":124.333328,"HyperDash":false},{"StartTime":58374.0,"Position":76.0,"HyperDash":false}]},{"StartTime":58707.0,"Objects":[{"StartTime":58707.0,"Position":208.0,"HyperDash":false},{"StartTime":58790.0,"Position":235.924927,"HyperDash":false},{"StartTime":58873.0,"Position":258.849854,"HyperDash":false},{"StartTime":58956.0,"Position":280.77478,"HyperDash":false},{"StartTime":59040.0,"Position":308.0,"HyperDash":false},{"StartTime":59114.0,"Position":302.777771,"HyperDash":false},{"StartTime":59188.0,"Position":275.555542,"HyperDash":false},{"StartTime":59262.0,"Position":249.333344,"HyperDash":false},{"StartTime":59373.0,"Position":208.0,"HyperDash":false}]},{"StartTime":59707.0,"Objects":[{"StartTime":59707.0,"Position":64.0,"HyperDash":false}]},{"StartTime":59874.0,"Objects":[{"StartTime":59874.0,"Position":128.0,"HyperDash":false},{"StartTime":59948.0,"Position":144.1427,"HyperDash":false},{"StartTime":60022.0,"Position":119.2854,"HyperDash":false},{"StartTime":60096.0,"Position":142.428085,"HyperDash":false},{"StartTime":60207.0,"Position":142.142136,"HyperDash":false}]},{"StartTime":60374.0,"Objects":[{"StartTime":60374.0,"Position":80.0,"HyperDash":false},{"StartTime":60457.0,"Position":73.37541,"HyperDash":false},{"StartTime":60540.0,"Position":27.7508316,"HyperDash":false},{"StartTime":60623.0,"Position":45.1262474,"HyperDash":false},{"StartTime":60707.0,"Position":9.28933,"HyperDash":false},{"StartTime":60781.0,"Position":31.0028038,"HyperDash":false},{"StartTime":60855.0,"Position":29.71629,"HyperDash":false},{"StartTime":60929.0,"Position":68.42977,"HyperDash":false},{"StartTime":61040.0,"Position":80.0,"HyperDash":false}]},{"StartTime":61374.0,"Objects":[{"StartTime":61374.0,"Position":224.0,"HyperDash":false},{"StartTime":61457.0,"Position":240.728989,"HyperDash":false},{"StartTime":61540.0,"Position":281.499359,"HyperDash":false},{"StartTime":61623.0,"Position":285.145782,"HyperDash":false},{"StartTime":61707.0,"Position":295.647522,"HyperDash":false},{"StartTime":61781.0,"Position":273.401184,"HyperDash":false},{"StartTime":61855.0,"Position":266.0076,"HyperDash":false},{"StartTime":61929.0,"Position":282.02597,"HyperDash":false},{"StartTime":62040.0,"Position":233.9523,"HyperDash":false}]},{"StartTime":62374.0,"Objects":[{"StartTime":62374.0,"Position":96.0,"HyperDash":false}]},{"StartTime":62541.0,"Objects":[{"StartTime":62541.0,"Position":32.0,"HyperDash":false},{"StartTime":62615.0,"Position":2.15351486,"HyperDash":false},{"StartTime":62689.0,"Position":34.9851379,"HyperDash":false},{"StartTime":62763.0,"Position":0.0,"HyperDash":false},{"StartTime":62874.0,"Position":30.1482067,"HyperDash":false}]},{"StartTime":63041.0,"Objects":[{"StartTime":63041.0,"Position":92.0,"HyperDash":false},{"StartTime":63115.0,"Position":114.222221,"HyperDash":false},{"StartTime":63189.0,"Position":131.444443,"HyperDash":false},{"StartTime":63263.0,"Position":144.666672,"HyperDash":false},{"StartTime":63374.0,"Position":192.0,"HyperDash":false}]},{"StartTime":63707.0,"Objects":[{"StartTime":63707.0,"Position":468.0,"HyperDash":false}]},{"StartTime":64041.0,"Objects":[{"StartTime":64041.0,"Position":192.0,"HyperDash":false},{"StartTime":64124.0,"Position":153.075073,"HyperDash":false},{"StartTime":64207.0,"Position":159.150146,"HyperDash":false},{"StartTime":64290.0,"Position":101.225227,"HyperDash":false},{"StartTime":64374.0,"Position":92.0,"HyperDash":false},{"StartTime":64448.0,"Position":132.222229,"HyperDash":false},{"StartTime":64522.0,"Position":126.444443,"HyperDash":false},{"StartTime":64596.0,"Position":160.666656,"HyperDash":false},{"StartTime":64707.0,"Position":192.0,"HyperDash":false}]},{"StartTime":65041.0,"Objects":[{"StartTime":65041.0,"Position":336.0,"HyperDash":false},{"StartTime":65124.0,"Position":375.268738,"HyperDash":false},{"StartTime":65207.0,"Position":395.320221,"HyperDash":false},{"StartTime":65290.0,"Position":394.972534,"HyperDash":false},{"StartTime":65374.0,"Position":395.778046,"HyperDash":false},{"StartTime":65448.0,"Position":382.9742,"HyperDash":false},{"StartTime":65522.0,"Position":392.609863,"HyperDash":false},{"StartTime":65596.0,"Position":364.706543,"HyperDash":false},{"StartTime":65707.0,"Position":339.031464,"HyperDash":false}]},{"StartTime":66041.0,"Objects":[{"StartTime":66041.0,"Position":208.0,"HyperDash":false},{"StartTime":66115.0,"Position":218.222229,"HyperDash":false},{"StartTime":66189.0,"Position":241.444443,"HyperDash":false},{"StartTime":66263.0,"Position":289.6667,"HyperDash":false},{"StartTime":66374.0,"Position":308.0,"HyperDash":false}]},{"StartTime":66707.0,"Objects":[{"StartTime":66707.0,"Position":144.0,"HyperDash":false},{"StartTime":66781.0,"Position":125.777779,"HyperDash":false},{"StartTime":66855.0,"Position":106.555557,"HyperDash":false},{"StartTime":66929.0,"Position":90.33333,"HyperDash":false},{"StartTime":67040.0,"Position":44.0,"HyperDash":false}]},{"StartTime":67373.0,"Objects":[{"StartTime":67373.0,"Position":192.0,"HyperDash":false},{"StartTime":67447.0,"Position":186.1427,"HyperDash":false},{"StartTime":67521.0,"Position":209.2854,"HyperDash":false},{"StartTime":67595.0,"Position":193.428085,"HyperDash":false},{"StartTime":67706.0,"Position":206.142136,"HyperDash":false}]},{"StartTime":67874.0,"Objects":[{"StartTime":67874.0,"Position":120.0,"HyperDash":false},{"StartTime":67957.0,"Position":88.82533,"HyperDash":false},{"StartTime":68040.0,"Position":85.3476257,"HyperDash":false},{"StartTime":68123.0,"Position":65.43532,"HyperDash":false},{"StartTime":68207.0,"Position":74.31434,"HyperDash":false},{"StartTime":68272.0,"Position":73.27857,"HyperDash":false},{"StartTime":68373.0,"Position":113.828613,"HyperDash":false}]},{"StartTime":68707.0,"Objects":[{"StartTime":68707.0,"Position":272.0,"HyperDash":false},{"StartTime":68781.0,"Position":296.222229,"HyperDash":false},{"StartTime":68855.0,"Position":335.444458,"HyperDash":false},{"StartTime":68929.0,"Position":338.6667,"HyperDash":false},{"StartTime":69040.0,"Position":372.0,"HyperDash":false}]},{"StartTime":69374.0,"Objects":[{"StartTime":69374.0,"Position":237.0,"HyperDash":false},{"StartTime":69457.0,"Position":218.076126,"HyperDash":false},{"StartTime":69540.0,"Position":170.152252,"HyperDash":false},{"StartTime":69623.0,"Position":155.228363,"HyperDash":false},{"StartTime":69707.0,"Position":137.004211,"HyperDash":false},{"StartTime":69781.0,"Position":167.2255,"HyperDash":false},{"StartTime":69855.0,"Position":161.446777,"HyperDash":false},{"StartTime":69929.0,"Position":188.66806,"HyperDash":false},{"StartTime":70040.0,"Position":237.0,"HyperDash":false}]},{"StartTime":70373.0,"Objects":[{"StartTime":70373.0,"Position":384.0,"HyperDash":false}]},{"StartTime":70540.0,"Objects":[{"StartTime":70540.0,"Position":448.0,"HyperDash":false},{"StartTime":70614.0,"Position":454.1427,"HyperDash":false},{"StartTime":70688.0,"Position":466.2854,"HyperDash":false},{"StartTime":70762.0,"Position":467.4281,"HyperDash":false},{"StartTime":70873.0,"Position":462.142151,"HyperDash":false}]},{"StartTime":71040.0,"Objects":[{"StartTime":71040.0,"Position":400.0,"HyperDash":false},{"StartTime":71123.0,"Position":381.075073,"HyperDash":false},{"StartTime":71206.0,"Position":345.150146,"HyperDash":false},{"StartTime":71289.0,"Position":316.22522,"HyperDash":false},{"StartTime":71373.0,"Position":300.0,"HyperDash":false},{"StartTime":71447.0,"Position":336.222229,"HyperDash":false},{"StartTime":71521.0,"Position":347.444458,"HyperDash":false},{"StartTime":71595.0,"Position":384.666656,"HyperDash":false},{"StartTime":71706.0,"Position":400.0,"HyperDash":false}]},{"StartTime":72040.0,"Objects":[{"StartTime":72040.0,"Position":256.0,"HyperDash":false},{"StartTime":72123.0,"Position":241.4447,"HyperDash":false},{"StartTime":72206.0,"Position":212.00444,"HyperDash":false},{"StartTime":72289.0,"Position":222.925644,"HyperDash":false},{"StartTime":72373.0,"Position":198.3011,"HyperDash":false},{"StartTime":72447.0,"Position":185.363647,"HyperDash":false},{"StartTime":72521.0,"Position":217.711319,"HyperDash":false},{"StartTime":72595.0,"Position":229.9505,"HyperDash":false},{"StartTime":72706.0,"Position":232.077591,"HyperDash":false}]},{"StartTime":73040.0,"Objects":[{"StartTime":73040.0,"Position":400.0,"HyperDash":false}]},{"StartTime":73207.0,"Objects":[{"StartTime":73207.0,"Position":472.0,"HyperDash":false},{"StartTime":73281.0,"Position":462.583252,"HyperDash":false},{"StartTime":73355.0,"Position":487.166534,"HyperDash":false},{"StartTime":73429.0,"Position":462.7498,"HyperDash":false},{"StartTime":73540.0,"Position":479.1247,"HyperDash":false}]},{"StartTime":73707.0,"Objects":[{"StartTime":73707.0,"Position":416.0,"HyperDash":false},{"StartTime":73781.0,"Position":409.777771,"HyperDash":false},{"StartTime":73855.0,"Position":383.555542,"HyperDash":false},{"StartTime":73929.0,"Position":337.3333,"HyperDash":false},{"StartTime":74040.0,"Position":316.0,"HyperDash":false}]},{"StartTime":74373.0,"Objects":[{"StartTime":74373.0,"Position":36.0,"HyperDash":false}]},{"StartTime":74707.0,"Objects":[{"StartTime":74707.0,"Position":304.0,"HyperDash":false},{"StartTime":74790.0,"Position":334.939941,"HyperDash":false},{"StartTime":74873.0,"Position":359.879883,"HyperDash":false},{"StartTime":74956.0,"Position":382.819824,"HyperDash":false},{"StartTime":75040.0,"Position":384.0,"HyperDash":false},{"StartTime":75114.0,"Position":376.222229,"HyperDash":false},{"StartTime":75188.0,"Position":347.444458,"HyperDash":false},{"StartTime":75262.0,"Position":313.666656,"HyperDash":false},{"StartTime":75373.0,"Position":304.0,"HyperDash":false}]},{"StartTime":75707.0,"Objects":[{"StartTime":75707.0,"Position":160.0,"HyperDash":false},{"StartTime":75790.0,"Position":138.731277,"HyperDash":false},{"StartTime":75873.0,"Position":112.679764,"HyperDash":false},{"StartTime":75956.0,"Position":91.02745,"HyperDash":false},{"StartTime":76040.0,"Position":100.221947,"HyperDash":false},{"StartTime":76114.0,"Position":110.025749,"HyperDash":false},{"StartTime":76188.0,"Position":126.390106,"HyperDash":false},{"StartTime":76262.0,"Position":120.293419,"HyperDash":false},{"StartTime":76373.0,"Position":156.968521,"HyperDash":false}]},{"StartTime":76707.0,"Objects":[{"StartTime":76707.0,"Position":304.0,"HyperDash":false},{"StartTime":76781.0,"Position":341.222229,"HyperDash":false},{"StartTime":76855.0,"Position":337.444458,"HyperDash":false},{"StartTime":76929.0,"Position":358.6667,"HyperDash":false},{"StartTime":77040.0,"Position":404.0,"HyperDash":false}]},{"StartTime":77374.0,"Objects":[{"StartTime":77374.0,"Position":8.0,"HyperDash":false}]},{"StartTime":77707.0,"Objects":[{"StartTime":77707.0,"Position":450.0,"HyperDash":false},{"StartTime":77779.0,"Position":231.0,"HyperDash":false},{"StartTime":77852.0,"Position":118.0,"HyperDash":false},{"StartTime":77925.0,"Position":511.0,"HyperDash":false},{"StartTime":77998.0,"Position":333.0,"HyperDash":false},{"StartTime":78071.0,"Position":234.0,"HyperDash":false},{"StartTime":78144.0,"Position":228.0,"HyperDash":false},{"StartTime":78217.0,"Position":302.0,"HyperDash":false},{"StartTime":78290.0,"Position":390.0,"HyperDash":false},{"StartTime":78363.0,"Position":75.0,"HyperDash":false},{"StartTime":78436.0,"Position":506.0,"HyperDash":false},{"StartTime":78509.0,"Position":3.0,"HyperDash":false},{"StartTime":78582.0,"Position":289.0,"HyperDash":false},{"StartTime":78655.0,"Position":217.0,"HyperDash":false},{"StartTime":78728.0,"Position":447.0,"HyperDash":false},{"StartTime":78801.0,"Position":324.0,"HyperDash":false},{"StartTime":78874.0,"Position":183.0,"HyperDash":false},{"StartTime":78946.0,"Position":279.0,"HyperDash":false},{"StartTime":79019.0,"Position":157.0,"HyperDash":false},{"StartTime":79092.0,"Position":501.0,"HyperDash":false},{"StartTime":79165.0,"Position":215.0,"HyperDash":false},{"StartTime":79238.0,"Position":79.0,"HyperDash":false},{"StartTime":79311.0,"Position":337.0,"HyperDash":false},{"StartTime":79384.0,"Position":380.0,"HyperDash":false},{"StartTime":79457.0,"Position":348.0,"HyperDash":false},{"StartTime":79530.0,"Position":225.0,"HyperDash":false},{"StartTime":79603.0,"Position":363.0,"HyperDash":false},{"StartTime":79676.0,"Position":96.0,"HyperDash":false},{"StartTime":79749.0,"Position":104.0,"HyperDash":false},{"StartTime":79822.0,"Position":173.0,"HyperDash":false},{"StartTime":79895.0,"Position":373.0,"HyperDash":false},{"StartTime":79968.0,"Position":424.0,"HyperDash":false},{"StartTime":80041.0,"Position":268.0,"HyperDash":false}]},{"StartTime":82374.0,"Objects":[{"StartTime":82374.0,"Position":368.0,"HyperDash":false}]},{"StartTime":82707.0,"Objects":[{"StartTime":82707.0,"Position":224.0,"HyperDash":false},{"StartTime":82790.0,"Position":220.606583,"HyperDash":false},{"StartTime":82873.0,"Position":192.709763,"HyperDash":false},{"StartTime":82956.0,"Position":172.4607,"HyperDash":false},{"StartTime":83040.0,"Position":181.183929,"HyperDash":false},{"StartTime":83123.0,"Position":190.276581,"HyperDash":false},{"StartTime":83206.0,"Position":175.345276,"HyperDash":false},{"StartTime":83289.0,"Position":168.272736,"HyperDash":false},{"StartTime":83373.0,"Position":181.259979,"HyperDash":false},{"StartTime":83447.0,"Position":182.439926,"HyperDash":false},{"StartTime":83522.0,"Position":186.502777,"HyperDash":false},{"StartTime":83596.0,"Position":213.74353,"HyperDash":false},{"StartTime":83707.0,"Position":224.286057,"HyperDash":false}]},{"StartTime":84041.0,"Objects":[{"StartTime":84041.0,"Position":368.0,"HyperDash":false},{"StartTime":84124.0,"Position":366.238831,"HyperDash":false},{"StartTime":84207.0,"Position":382.477631,"HyperDash":false},{"StartTime":84290.0,"Position":376.716461,"HyperDash":false},{"StartTime":84374.0,"Position":372.9702,"HyperDash":false},{"StartTime":84457.0,"Position":381.209045,"HyperDash":false},{"StartTime":84540.0,"Position":367.447845,"HyperDash":false},{"StartTime":84623.0,"Position":364.686676,"HyperDash":false},{"StartTime":84707.0,"Position":377.94043,"HyperDash":false},{"StartTime":84781.0,"Position":396.044922,"HyperDash":false},{"StartTime":84856.0,"Position":371.164337,"HyperDash":false},{"StartTime":84930.0,"Position":379.268829,"HyperDash":false},{"StartTime":85041.0,"Position":382.925568,"HyperDash":false}]},{"StartTime":85374.0,"Objects":[{"StartTime":85374.0,"Position":240.0,"HyperDash":false},{"StartTime":85457.0,"Position":214.595383,"HyperDash":false},{"StartTime":85540.0,"Position":206.182877,"HyperDash":false},{"StartTime":85623.0,"Position":175.034424,"HyperDash":false},{"StartTime":85707.0,"Position":168.007141,"HyperDash":false},{"StartTime":85781.0,"Position":185.660355,"HyperDash":false},{"StartTime":85855.0,"Position":200.138123,"HyperDash":false},{"StartTime":85929.0,"Position":194.945816,"HyperDash":false},{"StartTime":86040.0,"Position":235.646591,"HyperDash":false}]},{"StartTime":86374.0,"Objects":[{"StartTime":86374.0,"Position":496.0,"HyperDash":false}]},{"StartTime":86707.0,"Objects":[{"StartTime":86707.0,"Position":224.0,"HyperDash":false},{"StartTime":86781.0,"Position":185.777771,"HyperDash":false},{"StartTime":86855.0,"Position":181.555557,"HyperDash":false},{"StartTime":86929.0,"Position":172.333328,"HyperDash":false},{"StartTime":87040.0,"Position":124.0,"HyperDash":false}]},{"StartTime":87374.0,"Objects":[{"StartTime":87374.0,"Position":256.0,"HyperDash":false},{"StartTime":87448.0,"Position":281.222229,"HyperDash":false},{"StartTime":87522.0,"Position":307.444458,"HyperDash":false},{"StartTime":87596.0,"Position":307.6667,"HyperDash":false},{"StartTime":87707.0,"Position":356.0,"HyperDash":false}]},{"StartTime":88041.0,"Objects":[{"StartTime":88041.0,"Position":184.0,"HyperDash":false}]},{"StartTime":88374.0,"Objects":[{"StartTime":88374.0,"Position":352.0,"HyperDash":false},{"StartTime":88448.0,"Position":358.1427,"HyperDash":false},{"StartTime":88522.0,"Position":353.2854,"HyperDash":false},{"StartTime":88596.0,"Position":361.4281,"HyperDash":false},{"StartTime":88707.0,"Position":366.142151,"HyperDash":false}]},{"StartTime":89041.0,"Objects":[{"StartTime":89041.0,"Position":80.0,"HyperDash":false}]},{"StartTime":89374.0,"Objects":[{"StartTime":89374.0,"Position":366.0,"HyperDash":false},{"StartTime":89457.0,"Position":408.9072,"HyperDash":false},{"StartTime":89540.0,"Position":411.8144,"HyperDash":false},{"StartTime":89623.0,"Position":438.7216,"HyperDash":false},{"StartTime":89707.0,"Position":465.928864,"HyperDash":false},{"StartTime":89781.0,"Position":460.722473,"HyperDash":false},{"StartTime":89855.0,"Position":437.516052,"HyperDash":false},{"StartTime":89929.0,"Position":403.309631,"HyperDash":false},{"StartTime":90040.0,"Position":366.0,"HyperDash":false}]},{"StartTime":90374.0,"Objects":[{"StartTime":90374.0,"Position":24.0,"HyperDash":false}]},{"StartTime":90707.0,"Objects":[{"StartTime":90707.0,"Position":368.0,"HyperDash":false},{"StartTime":90781.0,"Position":386.704376,"HyperDash":false},{"StartTime":90855.0,"Position":388.408722,"HyperDash":false},{"StartTime":90929.0,"Position":374.1131,"HyperDash":false},{"StartTime":91040.0,"Position":375.669647,"HyperDash":false}]},{"StartTime":91374.0,"Objects":[{"StartTime":91374.0,"Position":256.0,"HyperDash":false},{"StartTime":91448.0,"Position":246.777771,"HyperDash":false},{"StartTime":91522.0,"Position":220.555557,"HyperDash":false},{"StartTime":91596.0,"Position":188.333328,"HyperDash":false},{"StartTime":91707.0,"Position":156.0,"HyperDash":false}]},{"StartTime":92041.0,"Objects":[{"StartTime":92041.0,"Position":256.0,"HyperDash":false},{"StartTime":92115.0,"Position":291.222229,"HyperDash":false},{"StartTime":92189.0,"Position":285.444458,"HyperDash":false},{"StartTime":92263.0,"Position":313.6667,"HyperDash":false},{"StartTime":92374.0,"Position":356.0,"HyperDash":false}]},{"StartTime":92707.0,"Objects":[{"StartTime":92707.0,"Position":224.0,"HyperDash":false},{"StartTime":92781.0,"Position":189.777771,"HyperDash":false},{"StartTime":92855.0,"Position":181.555557,"HyperDash":false},{"StartTime":92929.0,"Position":141.333328,"HyperDash":false},{"StartTime":93040.0,"Position":124.0,"HyperDash":false}]},{"StartTime":93374.0,"Objects":[{"StartTime":93374.0,"Position":392.0,"HyperDash":false}]},{"StartTime":93707.0,"Objects":[{"StartTime":93707.0,"Position":128.0,"HyperDash":false},{"StartTime":93790.0,"Position":108.075073,"HyperDash":false},{"StartTime":93873.0,"Position":94.15015,"HyperDash":false},{"StartTime":93956.0,"Position":33.2252274,"HyperDash":false},{"StartTime":94040.0,"Position":28.0,"HyperDash":false},{"StartTime":94114.0,"Position":51.22222,"HyperDash":false},{"StartTime":94188.0,"Position":75.44444,"HyperDash":false},{"StartTime":94262.0,"Position":111.666664,"HyperDash":false},{"StartTime":94373.0,"Position":128.0,"HyperDash":false}]},{"StartTime":94707.0,"Objects":[{"StartTime":94707.0,"Position":256.0,"HyperDash":false},{"StartTime":94781.0,"Position":264.704376,"HyperDash":false},{"StartTime":94855.0,"Position":261.408722,"HyperDash":false},{"StartTime":94929.0,"Position":261.1131,"HyperDash":false},{"StartTime":95040.0,"Position":263.669647,"HyperDash":false}]},{"StartTime":95374.0,"Objects":[{"StartTime":95374.0,"Position":24.0,"HyperDash":false}]},{"StartTime":95540.0,"Objects":[{"StartTime":95540.0,"Position":96.0,"HyperDash":false}]},{"StartTime":95707.0,"Objects":[{"StartTime":95707.0,"Position":48.0,"HyperDash":false}]},{"StartTime":96041.0,"Objects":[{"StartTime":96041.0,"Position":168.0,"HyperDash":false},{"StartTime":96115.0,"Position":188.222229,"HyperDash":false},{"StartTime":96189.0,"Position":219.444443,"HyperDash":false},{"StartTime":96263.0,"Position":222.666672,"HyperDash":false},{"StartTime":96374.0,"Position":268.0,"HyperDash":false}]},{"StartTime":96707.0,"Objects":[{"StartTime":96707.0,"Position":152.0,"HyperDash":false},{"StartTime":96781.0,"Position":144.295639,"HyperDash":false},{"StartTime":96855.0,"Position":165.591263,"HyperDash":false},{"StartTime":96929.0,"Position":143.8869,"HyperDash":false},{"StartTime":97040.0,"Position":144.330353,"HyperDash":false}]},{"StartTime":97374.0,"Objects":[{"StartTime":97374.0,"Position":280.0,"HyperDash":false},{"StartTime":97457.0,"Position":300.248535,"HyperDash":false},{"StartTime":97540.0,"Position":317.463043,"HyperDash":false},{"StartTime":97623.0,"Position":329.187042,"HyperDash":false},{"StartTime":97707.0,"Position":369.215424,"HyperDash":false},{"StartTime":97781.0,"Position":392.887115,"HyperDash":false},{"StartTime":97855.0,"Position":394.493958,"HyperDash":false},{"StartTime":97929.0,"Position":416.841644,"HyperDash":false},{"StartTime":98040.0,"Position":422.157837,"HyperDash":false}]},{"StartTime":98707.0,"Objects":[{"StartTime":98707.0,"Position":144.0,"HyperDash":false}]},{"StartTime":99040.0,"Objects":[{"StartTime":99040.0,"Position":229.0,"HyperDash":false},{"StartTime":99138.0,"Position":51.0,"HyperDash":false},{"StartTime":99237.0,"Position":199.0,"HyperDash":false},{"StartTime":99336.0,"Position":208.0,"HyperDash":false},{"StartTime":99435.0,"Position":173.0,"HyperDash":false},{"StartTime":99534.0,"Position":367.0,"HyperDash":false},{"StartTime":99633.0,"Position":193.0,"HyperDash":false},{"StartTime":99732.0,"Position":488.0,"HyperDash":false},{"StartTime":99831.0,"Position":314.0,"HyperDash":false},{"StartTime":99930.0,"Position":135.0,"HyperDash":false},{"StartTime":100029.0,"Position":399.0,"HyperDash":false},{"StartTime":100128.0,"Position":404.0,"HyperDash":false},{"StartTime":100227.0,"Position":152.0,"HyperDash":false},{"StartTime":100326.0,"Position":353.0,"HyperDash":false},{"StartTime":100425.0,"Position":358.0,"HyperDash":false},{"StartTime":100524.0,"Position":447.0,"HyperDash":false},{"StartTime":100623.0,"Position":222.0,"HyperDash":false},{"StartTime":100722.0,"Position":382.0,"HyperDash":false},{"StartTime":100821.0,"Position":433.0,"HyperDash":false},{"StartTime":100920.0,"Position":450.0,"HyperDash":false},{"StartTime":101019.0,"Position":326.0,"HyperDash":false},{"StartTime":101118.0,"Position":414.0,"HyperDash":false},{"StartTime":101216.0,"Position":285.0,"HyperDash":false},{"StartTime":101315.0,"Position":336.0,"HyperDash":false},{"StartTime":101414.0,"Position":509.0,"HyperDash":false},{"StartTime":101513.0,"Position":334.0,"HyperDash":false},{"StartTime":101612.0,"Position":72.0,"HyperDash":false},{"StartTime":101711.0,"Position":425.0,"HyperDash":false},{"StartTime":101810.0,"Position":451.0,"HyperDash":false},{"StartTime":101909.0,"Position":220.0,"HyperDash":false},{"StartTime":102008.0,"Position":25.0,"HyperDash":false},{"StartTime":102107.0,"Position":77.0,"HyperDash":false},{"StartTime":102206.0,"Position":509.0,"HyperDash":false},{"StartTime":102305.0,"Position":90.0,"HyperDash":false},{"StartTime":102404.0,"Position":118.0,"HyperDash":false},{"StartTime":102503.0,"Position":58.0,"HyperDash":false},{"StartTime":102602.0,"Position":12.0,"HyperDash":false},{"StartTime":102701.0,"Position":215.0,"HyperDash":false},{"StartTime":102800.0,"Position":487.0,"HyperDash":false},{"StartTime":102899.0,"Position":446.0,"HyperDash":false},{"StartTime":102998.0,"Position":491.0,"HyperDash":false},{"StartTime":103097.0,"Position":459.0,"HyperDash":false},{"StartTime":103196.0,"Position":37.0,"HyperDash":false},{"StartTime":103294.0,"Position":291.0,"HyperDash":false},{"StartTime":103393.0,"Position":315.0,"HyperDash":false},{"StartTime":103492.0,"Position":35.0,"HyperDash":false},{"StartTime":103591.0,"Position":208.0,"HyperDash":false},{"StartTime":103690.0,"Position":504.0,"HyperDash":false},{"StartTime":103789.0,"Position":296.0,"HyperDash":false},{"StartTime":103888.0,"Position":105.0,"HyperDash":false},{"StartTime":103987.0,"Position":488.0,"HyperDash":false},{"StartTime":104086.0,"Position":230.0,"HyperDash":false},{"StartTime":104185.0,"Position":446.0,"HyperDash":false},{"StartTime":104284.0,"Position":241.0,"HyperDash":false},{"StartTime":104383.0,"Position":413.0,"HyperDash":false},{"StartTime":104482.0,"Position":357.0,"HyperDash":false},{"StartTime":104581.0,"Position":256.0,"HyperDash":false},{"StartTime":104680.0,"Position":192.0,"HyperDash":false},{"StartTime":104779.0,"Position":116.0,"HyperDash":false},{"StartTime":104878.0,"Position":397.0,"HyperDash":false},{"StartTime":104977.0,"Position":422.0,"HyperDash":false},{"StartTime":105076.0,"Position":230.0,"HyperDash":false},{"StartTime":105175.0,"Position":479.0,"HyperDash":false},{"StartTime":105274.0,"Position":276.0,"HyperDash":false},{"StartTime":105373.0,"Position":423.0,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1284935.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1284935.osu new file mode 100644 index 0000000000..a0ed6b190e --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1284935.osu @@ -0,0 +1,210 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 +Mode: 2 + +[Difficulty] +HPDrainRate:3 +CircleSize:2.5 +OverallDifficulty:6 +ApproachRate:6 +SliderMultiplier:1 +SliderTickRate:1 + +[Events] +//Background and Video events +//Break Periods +2,80241,81249 +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples + +[TimingPoints] +41,333.333333333333,4,2,1,50,1,0 +707,-100,4,2,1,50,0,0 +2707,-100,4,2,1,85,0,0 +12040,-86.9565217391304,4,2,1,85,0,0 +12707,-100,4,2,1,85,0,0 +13374,-100,4,2,1,85,0,0 +34207,-100,4,2,1,75,0,0 +34374,-100,4,2,1,65,0,0 +34540,-100,4,2,1,55,0,0 +34707,-100,4,2,1,85,0,0 +45374,-133.333333333333,4,2,1,85,0,0 +54707,-133.333333333333,4,2,1,30,0,0 +56040,-100,4,2,1,85,0,1 +72040,-125,4,2,1,85,0,0 +72707,-100,4,2,1,85,0,0 +74707,-125,4,2,1,85,0,0 +75207,-100,4,2,1,85,0,0 +82374,-200,4,2,1,85,0,0 +85374,-100,4,2,1,85,0,0 +88040,-100,4,2,1,85,0,1 +98707,-100,4,2,1,85,0,0 +99040,-100,4,2,1,20,0,0 + +[HitObjects] +256,192,707,12,0,2374,0:0:0:0: +368,64,2707,6,2,L|256:64,1,100,2|2,0:0|0:0,0:0:0:0: +288,128,3207,2,2,L|304:192,1,50,2|8,0:0|0:0,0:0:0:0: +192,192,3707,6,2,L|64:192,2,100,2|2|2,0:0|0:0|0:0,0:0:0:0: +288,192,4707,1,8,0:0:0:0: +144,128,5041,1,10,0:0:0:0: +304,288,5374,6,6,L|448:144,2,200,6|8|0,0:0|0:0|0:0,0:0:0:0: +208,288,7041,1,0,0:0:0:0: +304,160,7374,2,10,L|320:48,1,100,10|8,0:0|0:0,0:0:0:0: +160,32,8041,6,6,L|48:32,1,100,6|2,0:0|0:0,0:0:0:0: +112,80,8541,1,0,0:0:0:0: +160,128,8707,2,8,P|208:160|128:232,1,200,8|6,0:0|0:0,0:0:0:0: +224,256,9707,5,2,0:0:0:0: +352,224,10041,2,8,L|240:224,1,100,8|2,0:0|0:0,0:0:0:0: +416,336,10707,6,6,P|464:320|416:216,1,200,6|12,0:0|0:0,0:0:0:0: +224,96,11874,1,2,0:0:0:0: +160,96,12041,2,2,P|116:152|160:232,1,172.500003290176,2|2,0:0|0:0,0:0:0:0: +224,232,12707,1,2,0:0:0:0: +464,64,13374,6,6,L|304:64,1,150,6|2,0:0|0:0,0:0:0:0: +240,64,14041,2,8,P|192:112|240:160,1,150,8|2,0:0|0:0,0:0:0:0: +320,160,14707,6,2,L|208:160,1,100,2|0,0:0|0:0,0:0:0:0: +320,256,15374,2,8,L|360:164,1,100,8|8,0:0|0:0,0:0:0:0: +192,64,16041,6,4,L|80:64,1,100,4|2,0:0|0:0,0:0:0:0: +144,80,16541,1,2,0:0:0:0: +192,96,16707,1,8,0:0:0:0: +336,96,17041,1,2,0:0:0:0: +288,96,17207,6,2,P|240:128|288:192,1,150,2|0,0:0|0:0,0:0:0:0: +384,192,18041,1,8,0:0:0:0: +256,192,18374,1,2,0:0:0:0: +416,192,18707,6,6,L|432:16,1,150,6|2,0:0|0:0,0:0:0:0: +336,32,19374,2,8,L|224:32,1,100,8|0,0:0|0:0,0:0:0:0: +256,32,20041,6,2,P|304:80|256:128,1,150,2|2,0:0|0:0,0:0:0:0: +196,128,20707,2,8,L|156:220,1,100,8|8,0:0|0:0,0:0:0:0: +320,224,21374,6,6,P|360:288|320:352,1,150,6|2,0:0|0:0,0:0:0:0: +224,352,22041,2,8,L|112:352,1,100,8|2,0:0|0:0,0:0:0:0: +192,224,22541,1,2,0:0:0:0: +204,272,22707,5,2,0:0:0:0: +96,288,23041,1,0,0:0:0:0: +208,288,23374,2,8,L|224:176,1,100,8|0,0:0|0:0,0:0:0:0: +80,96,24041,6,6,L|240:96,1,150,6|0,0:0|0:0,0:0:0:0: +176,96,24707,1,8,0:0:0:0: +256,128,25041,6,2,L|240:80,1,50,2|0,0:0|0:0,0:0:0:0: +352,96,25541,2,2,L|356:44,1,50,2|2,0:0|0:0,0:0:0:0: +192,176,26041,2,8,L|176:288,1,100,8|8,0:0|0:0,0:0:0:0: +272,336,26707,6,0,L|384:336,1,100,0|2,0:0|0:0,0:0:0:0: +320,288,27207,1,0,0:0:0:0: +272,240,27374,1,8,0:0:0:0: +416,240,27707,2,2,L|432:176,1,50,2|0,0:0|0:0,0:0:0:0: +288,176,28207,6,2,L|176:176,1,100,2|0,0:0|0:0,0:0:0:0: +256,368,28707,2,8,L|270:269,1,100,8|8,0:0|0:0,0:0:0:0: +128,256,29374,6,6,P|64:192|128:128,1,200,6|8,0:0|0:0,0:0:0:0: +224,128,30374,1,2,0:0:0:0: +368,128,30707,5,6,0:0:0:0: +432,128,30874,2,2,L|448:240,1,100,2|0,0:0|0:0,0:0:0:0: +384,256,31374,1,8,0:0:0:0: +240,256,31707,2,8,L|224:192,1,50,8|0,0:0|0:0,0:0:0:0: +304,192,32041,6,14,P|352:176|288:80,1,200,14|12,0:0|0:0,0:0:0:0: +160,80,33041,1,0,0:0:0:0: +304,80,33374,5,12,0:0:0:0: +368,80,33541,2,2,P|380:128|368:176,1,100,2|2,0:0|0:0,0:0:0:0: +224,176,34207,1,8,3:0:0:0: +176,176,34374,1,8,3:0:0:0: +128,176,34541,1,8,3:0:0:0: +200,144,34707,5,6,0:0:0:0: +336,272,35041,2,8,L|352:160,1,100,8|2,0:0|0:0,0:0:0:0: +208,144,35707,2,8,L|192:192,1,50,8|0,0:0|0:0,0:0:0:0: +336,208,36207,2,2,L|352:160,1,50,2|8,0:0|0:0,0:0:0:0: +208,160,36707,2,2,L|96:160,1,100,2|8,0:0|0:0,0:0:0:0: +256,160,37374,5,2,0:0:0:0: +320,160,37541,2,0,L|336:264,1,100,0|2,0:0|0:0,0:0:0:0: +272,272,38041,1,0,0:0:0:0: +416,272,38374,2,8,L|432:224,1,50,8|0,0:0|0:0,0:0:0:0: +288,224,38874,6,2,L|272:176,1,50,2|8,0:0|0:0,0:0:0:0: +336,160,39207,2,2,L|448:160,1,100,2|2,0:0|0:0,0:0:0:0: +384,160,39707,1,8,0:0:0:0: +240,160,40041,5,4,0:0:0:0: +384,64,40374,2,8,L|400:176,1,100,8|2,0:0|0:0,0:0:0:0: +256,176,41041,1,8,0:0:0:0: +112,176,41374,5,2,0:0:0:0: +48,224,41541,2,0,L|32:112,1,100,0|0,0:0|0:0,0:0:0:0: +96,112,42041,1,2,0:0:0:0: +240,112,42374,1,8,0:0:0:0: +96,112,42707,6,4,P|48:160|96:208,1,150,4|2,0:0|0:0,0:0:0:0: +160,208,43374,1,2,0:0:0:0: +288,208,43707,1,8,0:0:0:0: +160,208,44040,5,6,0:0:0:0: +304,288,44374,2,8,P|352:240|288:128,1,200,8|8,0:0|0:0,0:0:0:0: +136,128,45374,6,6,L|24:192,2,112.500004291535,6|0|0,0:0|0:0|0:0,0:0:0:0: +368,128,46874,2,0,L|384:240,1,112.500004291535,0|2,0:0|0:0,0:0:0:0: +272,256,47707,1,0,0:0:0:0: +144,256,48041,6,6,L|48:191,2,112.500004291535,6|0|0,0:0|0:0|0:0,0:0:0:0: +256,256,49374,2,2,P|304:224|224:112,1,225.000008583069,2|8,0:0|0:0,0:0:0:0: +384,96,50707,6,6,L|496:96,1,112.500004291535,6|0,0:0|0:0,0:0:0:0: +448,96,51374,1,8,0:0:0:0: +244,92,51874,2,2,L|132:108,1,112.500004291535,0|2,0:0|0:0,0:0:0:0: +208,288,52707,2,8,L|288:288,1,75.0000028610231,8|0,0:0|0:0,0:0:0:0: +368,288,53373,6,6,L|383:191,1,75.0000028610231,6|2,0:0|0:0,0:0:0:0: +255,192,54040,2,8,L|176:192,1,75.0000028610231,8|2,0:0|0:0,0:0:0:0: +272,80,54707,1,0,0:0:0:0: +160,272,55374,6,2,L|144:176,1,75.0000028610231,2|2,0:0|0:0,0:0:0:0: +320,144,56041,6,6,L|432:144,1,100,6|8,0:0|0:0,0:0:0:0: +256,240,56707,2,2,L|240:128,1,100,2|8,0:0|0:0,0:0:0:0: +328,112,57207,2,0,P|376:144|328:208,1,150,2|8,0:0|0:0,0:0:0:0: +176,208,58041,2,2,L|64:208,1,100,2|8,0:0|0:0,0:0:0:0: +208,208,58707,6,2,L|320:208,2,100,2|8|2,0:0|0:0|0:0,0:0:0:0: +64,208,59707,1,8,0:0:0:0: +128,208,59874,2,0,L|144:96,1,100,0|0,0:0|0:0,0:0:0:0: +80,96,60374,2,8,L|8:168,2,100,8|2|8,0:0|0:0|0:0,0:0:0:0: +224,96,61374,6,6,P|296:152|224:208,1,200,6|2,0:0|0:0,0:0:0:0: +96,224,62374,1,8,0:0:0:0: +32,224,62541,6,0,P|16:168|32:128,1,100,0|2,0:0|0:0,0:0:0:0: +92,112,63041,2,8,L|204:112,1,100,8|2,0:0|0:0,0:0:0:0: +336,112,63707,1,8,0:0:0:0: +192,112,64041,6,2,L|64:112,2,100,2|8|2,0:0|0:0|0:0,0:0:0:0: +336,112,65041,2,8,P|384:144|336:256,1,200,8|8,0:0|0:0,0:0:0:0: +208,256,66041,2,8,L|320:256,1,100,8|8,0:0|0:0,0:0:0:0: +144,160,66707,6,4,L|32:160,1,100,4|8,0:0|0:0,0:0:0:0: +192,256,67373,2,2,L|208:144,1,100,2|8,0:0|0:0,0:0:0:0: +120,128,67874,2,0,P|72:160|120:224,1,150,0|8,0:0|0:0,0:0:0:0: +272,224,68707,2,2,L|384:224,1,100,2|8,0:0|0:0,0:0:0:0: +237,223,69374,6,2,L|128:224,2,100,2|8|2,0:0|0:0|0:0,0:0:0:0: +384,208,70373,1,0,0:0:0:0: +448,208,70540,2,2,L|464:96,1,100,2|2,0:0|0:0,0:0:0:0: +400,96,71040,2,2,L|288:96,2,100,10|8|10,0:0|0:0|0:0,0:0:0:0: +256,96,72040,6,6,P|200:136|232:208,1,160,6|2,0:0|0:0,0:0:0:0: +400,208,73040,1,2,0:0:0:0: +472,208,73207,6,2,L|480:96,1,100,2|0,0:0|0:0,0:0:0:0: +416,80,73707,2,0,L|316:80,1,100,0|8,0:0|0:0,0:0:0:0: +176,80,74373,1,0,0:0:0:0: +304,80,74707,6,6,L|400:80,2,80,6|0|12,0:0|0:0|0:0,0:0:0:0: +160,80,75707,2,0,P|112:112|160:224,1,200,0|2,0:0|0:0,0:0:0:0: +304,224,76707,6,8,L|416:224,1,100,10|8,0:0|0:0,0:0:0:0: +212,224,77374,1,12,0:0:0:0: +256,192,77707,12,2,80041,0:0:0:0: +368,192,82374,5,0,0:0:0:0: +224,192,82707,2,6,P|176:160|224:104,1,150,6|0,0:0|0:0,0:0:0:0: +368,80,84041,2,6,L|384:240,1,150,6|0,0:0|0:0,0:0:0:0: +240,256,85374,6,6,P|168:212|240:160,1,200,6|10,0:0|0:0,0:0:0:0: +368,160,86374,1,0,0:0:0:0: +224,160,86707,6,8,L|112:160,1,100,8|0,0:0|0:0,0:0:0:0: +256,128,87374,2,8,L|368:128,1,100,8|2,0:0|0:0,0:0:0:0: +184,128,88041,5,6,0:0:0:0: +352,128,88374,2,8,L|368:240,1,100,8|8,0:0|0:0,0:0:0:0: +224,240,89041,1,8,0:0:0:0: +366,228,89374,6,0,L|472:224,2,100,2|8|8,0:0|0:0|0:0,0:0:0:0: +248,240,90374,1,8,0:0:0:0: +368,232,90707,6,0,L|376:128,1,100,2|8,0:0|0:0,0:0:0:0: +256,104,91374,2,0,L|152:104,1,100,8|8,0:0|0:0,0:0:0:0: +256,240,92041,6,2,L|368:240,1,100,2|8,0:0|0:0,0:0:0:0: +224,240,92707,2,8,L|120:240,1,100,8|2,0:0|0:0,0:0:0:0: +256,144,93374,5,6,0:0:0:0: +128,144,93707,2,8,L|16:144,2,100,8|8|8,0:0|0:0|0:0,0:0:0:0: +256,144,94707,6,2,L|264:248,1,100,2|8,0:0|0:0,0:0:0:0: +144,312,95374,1,8,0:0:0:0: +96,312,95540,1,8,0:0:0:0: +48,312,95707,1,8,0:0:0:0: +168,208,96041,6,6,L|272:208,1,100,6|0,0:0|0:0,0:0:0:0: +152,104,96707,2,8,L|144:208,1,100 +280,296,97374,6,8,P|369:254|422:171,1,200,10|8,0:0|0:0,0:0:0:0: +144,144,98707,1,14,0:0:0:0: +256,192,99040,12,0,105373,0:0:0:0: diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1431386-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1431386-expected-conversion.json new file mode 100644 index 0000000000..de879d0d1c --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1431386-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":534.0,"Objects":[{"StartTime":534.0,"Position":333.0,"HyperDash":false},{"StartTime":589.0,"Position":336.445465,"HyperDash":false},{"StartTime":645.0,"Position":359.226318,"HyperDash":false},{"StartTime":701.0,"Position":386.604523,"HyperDash":false},{"StartTime":757.0,"Position":424.50647,"HyperDash":false},{"StartTime":813.0,"Position":446.4084,"HyperDash":false},{"StartTime":869.0,"Position":450.310333,"HyperDash":false},{"StartTime":925.0,"Position":468.21228,"HyperDash":false},{"StartTime":981.0,"Position":489.2919,"HyperDash":false},{"StartTime":1032.0,"Position":456.3446,"HyperDash":false},{"StartTime":1084.0,"Position":444.864258,"HyperDash":false},{"StartTime":1135.0,"Position":422.7393,"HyperDash":false},{"StartTime":1187.0,"Position":412.2589,"HyperDash":false},{"StartTime":1238.0,"Position":412.133942,"HyperDash":false},{"StartTime":1290.0,"Position":383.653564,"HyperDash":false},{"StartTime":1341.0,"Position":353.512756,"HyperDash":false},{"StartTime":1429.0,"Position":333.0,"HyperDash":false}]},{"StartTime":1877.0,"Objects":[{"StartTime":1877.0,"Position":182.0,"HyperDash":false}]},{"StartTime":2325.0,"Objects":[{"StartTime":2325.0,"Position":333.0,"HyperDash":false},{"StartTime":2380.0,"Position":357.239044,"HyperDash":false},{"StartTime":2436.0,"Position":352.8279,"HyperDash":false},{"StartTime":2492.0,"Position":382.475677,"HyperDash":false},{"StartTime":2548.0,"Position":429.244324,"HyperDash":false},{"StartTime":2604.0,"Position":448.013,"HyperDash":false},{"StartTime":2660.0,"Position":463.721436,"HyperDash":false},{"StartTime":2716.0,"Position":462.368317,"HyperDash":false},{"StartTime":2772.0,"Position":490.190643,"HyperDash":false},{"StartTime":2823.0,"Position":455.473358,"HyperDash":false},{"StartTime":2875.0,"Position":467.2298,"HyperDash":false},{"StartTime":2926.0,"Position":431.3082,"HyperDash":false},{"StartTime":2978.0,"Position":421.951569,"HyperDash":false},{"StartTime":3029.0,"Position":384.947937,"HyperDash":false},{"StartTime":3081.0,"Position":377.6223,"HyperDash":false},{"StartTime":3132.0,"Position":362.782471,"HyperDash":false},{"StartTime":3220.0,"Position":333.0,"HyperDash":false}]},{"StartTime":3668.0,"Objects":[{"StartTime":3668.0,"Position":182.0,"HyperDash":false}]},{"StartTime":4116.0,"Objects":[{"StartTime":4116.0,"Position":26.0,"HyperDash":false},{"StartTime":4171.0,"Position":40.9041862,"HyperDash":false},{"StartTime":4227.0,"Position":12.82481,"HyperDash":false},{"StartTime":4283.0,"Position":9.745436,"HyperDash":false},{"StartTime":4339.0,"Position":29.67428,"HyperDash":false},{"StartTime":4433.0,"Position":12.1371651,"HyperDash":false},{"StartTime":4563.0,"Position":26.0,"HyperDash":false}]},{"StartTime":5011.0,"Objects":[{"StartTime":5011.0,"Position":20.0,"HyperDash":false},{"StartTime":5104.0,"Position":66.26816,"HyperDash":false},{"StartTime":5234.0,"Position":97.6557159,"HyperDash":false}]},{"StartTime":5459.0,"Objects":[{"StartTime":5459.0,"Position":178.0,"HyperDash":false},{"StartTime":5552.0,"Position":226.229477,"HyperDash":false},{"StartTime":5682.0,"Position":255.569565,"HyperDash":false}]},{"StartTime":5907.0,"Objects":[{"StartTime":5907.0,"Position":308.0,"HyperDash":false},{"StartTime":5990.0,"Position":336.486633,"HyperDash":false},{"StartTime":6074.0,"Position":360.3285,"HyperDash":false},{"StartTime":6158.0,"Position":396.17038,"HyperDash":false},{"StartTime":6242.0,"Position":427.1899,"HyperDash":false},{"StartTime":6317.0,"Position":419.723,"HyperDash":false},{"StartTime":6392.0,"Position":368.078461,"HyperDash":false},{"StartTime":6467.0,"Position":352.433929,"HyperDash":false},{"StartTime":6578.0,"Position":308.0,"HyperDash":false}]},{"StartTime":6802.0,"Objects":[{"StartTime":6802.0,"Position":224.0,"HyperDash":false},{"StartTime":6853.0,"Position":226.916428,"HyperDash":false},{"StartTime":6904.0,"Position":222.886032,"HyperDash":false},{"StartTime":6956.0,"Position":216.946533,"HyperDash":false},{"StartTime":7007.0,"Position":211.428284,"HyperDash":false},{"StartTime":7058.0,"Position":212.341827,"HyperDash":false},{"StartTime":7110.0,"Position":205.693756,"HyperDash":false},{"StartTime":7161.0,"Position":183.379547,"HyperDash":false},{"StartTime":7249.0,"Position":212.117065,"HyperDash":false}]},{"StartTime":7698.0,"Objects":[{"StartTime":7698.0,"Position":372.0,"HyperDash":false},{"StartTime":7791.0,"Position":392.5109,"HyperDash":false},{"StartTime":7921.0,"Position":392.363617,"HyperDash":false}]},{"StartTime":8145.0,"Objects":[{"StartTime":8145.0,"Position":390.0,"HyperDash":false},{"StartTime":8228.0,"Position":407.6116,"HyperDash":false},{"StartTime":8312.0,"Position":434.579956,"HyperDash":false},{"StartTime":8396.0,"Position":497.5483,"HyperDash":false},{"StartTime":8480.0,"Position":509.695038,"HyperDash":false},{"StartTime":8555.0,"Position":475.115967,"HyperDash":false},{"StartTime":8630.0,"Position":472.3585,"HyperDash":false},{"StartTime":8705.0,"Position":432.601044,"HyperDash":false},{"StartTime":8816.0,"Position":390.0,"HyperDash":false}]},{"StartTime":9041.0,"Objects":[{"StartTime":9041.0,"Position":330.0,"HyperDash":false},{"StartTime":9134.0,"Position":286.7251,"HyperDash":false},{"StartTime":9264.0,"Position":250.211823,"HyperDash":false}]},{"StartTime":9489.0,"Objects":[{"StartTime":9489.0,"Position":171.0,"HyperDash":false},{"StartTime":9582.0,"Position":139.4586,"HyperDash":false},{"StartTime":9712.0,"Position":92.77017,"HyperDash":false}]},{"StartTime":9936.0,"Objects":[{"StartTime":9936.0,"Position":9.0,"HyperDash":false},{"StartTime":10019.0,"Position":0.0,"HyperDash":false},{"StartTime":10103.0,"Position":4.53266668,"HyperDash":false},{"StartTime":10187.0,"Position":0.0,"HyperDash":false},{"StartTime":10271.0,"Position":0.02520752,"HyperDash":false},{"StartTime":10346.0,"Position":0.0,"HyperDash":false},{"StartTime":10421.0,"Position":12.0244074,"HyperDash":false},{"StartTime":10496.0,"Position":0.0,"HyperDash":false},{"StartTime":10607.0,"Position":9.0,"HyperDash":false}]},{"StartTime":10832.0,"Objects":[{"StartTime":10832.0,"Position":28.0,"HyperDash":false},{"StartTime":10925.0,"Position":40.7889824,"HyperDash":false},{"StartTime":11055.0,"Position":105.537766,"HyperDash":false}]},{"StartTime":11280.0,"Objects":[{"StartTime":11280.0,"Position":263.0,"HyperDash":false}]},{"StartTime":11728.0,"Objects":[{"StartTime":11728.0,"Position":343.0,"HyperDash":false},{"StartTime":11811.0,"Position":365.302277,"HyperDash":false},{"StartTime":11895.0,"Position":388.675323,"HyperDash":false},{"StartTime":11979.0,"Position":437.668274,"HyperDash":false},{"StartTime":12063.0,"Position":459.2406,"HyperDash":false},{"StartTime":12138.0,"Position":431.2186,"HyperDash":false},{"StartTime":12213.0,"Position":423.446381,"HyperDash":false},{"StartTime":12288.0,"Position":362.9473,"HyperDash":false},{"StartTime":12399.0,"Position":343.0,"HyperDash":false}]},{"StartTime":12623.0,"Objects":[{"StartTime":12623.0,"Position":290.0,"HyperDash":false},{"StartTime":12716.0,"Position":297.645538,"HyperDash":false},{"StartTime":12846.0,"Position":296.3436,"HyperDash":false}]},{"StartTime":13071.0,"Objects":[{"StartTime":13071.0,"Position":265.0,"HyperDash":false},{"StartTime":13164.0,"Position":251.816544,"HyperDash":false},{"StartTime":13294.0,"Position":186.7354,"HyperDash":false}]},{"StartTime":13519.0,"Objects":[{"StartTime":13519.0,"Position":123.0,"HyperDash":false},{"StartTime":13602.0,"Position":103.378716,"HyperDash":false},{"StartTime":13686.0,"Position":73.40055,"HyperDash":false},{"StartTime":13770.0,"Position":37.4223862,"HyperDash":false},{"StartTime":13854.0,"Position":3.26579285,"HyperDash":false},{"StartTime":13929.0,"Position":36.85356,"HyperDash":false},{"StartTime":14004.0,"Position":72.61978,"HyperDash":false},{"StartTime":14079.0,"Position":68.38599,"HyperDash":false},{"StartTime":14190.0,"Position":123.0,"HyperDash":false}]},{"StartTime":14414.0,"Objects":[{"StartTime":14414.0,"Position":371.0,"HyperDash":false}]},{"StartTime":14862.0,"Objects":[{"StartTime":14862.0,"Position":184.0,"HyperDash":false},{"StartTime":14955.0,"Position":212.186356,"HyperDash":false},{"StartTime":15085.0,"Position":261.4036,"HyperDash":false}]},{"StartTime":15310.0,"Objects":[{"StartTime":15310.0,"Position":343.0,"HyperDash":false},{"StartTime":15393.0,"Position":362.374176,"HyperDash":false},{"StartTime":15477.0,"Position":407.102234,"HyperDash":false},{"StartTime":15561.0,"Position":440.8303,"HyperDash":false},{"StartTime":15645.0,"Position":461.735352,"HyperDash":false},{"StartTime":15720.0,"Position":439.369354,"HyperDash":false},{"StartTime":15795.0,"Position":398.826447,"HyperDash":false},{"StartTime":15870.0,"Position":372.283539,"HyperDash":false},{"StartTime":15981.0,"Position":343.0,"HyperDash":false}]},{"StartTime":16205.0,"Objects":[{"StartTime":16205.0,"Position":128.0,"HyperDash":false}]},{"StartTime":16653.0,"Objects":[{"StartTime":16653.0,"Position":219.0,"HyperDash":false},{"StartTime":16746.0,"Position":193.135818,"HyperDash":false},{"StartTime":16876.0,"Position":141.577332,"HyperDash":false}]},{"StartTime":17101.0,"Objects":[{"StartTime":17101.0,"Position":65.0,"HyperDash":false},{"StartTime":17184.0,"Position":56.4695549,"HyperDash":false},{"StartTime":17268.0,"Position":32.94629,"HyperDash":false},{"StartTime":17352.0,"Position":29.3506489,"HyperDash":false},{"StartTime":17436.0,"Position":17.0841427,"HyperDash":false},{"StartTime":17511.0,"Position":18.8012981,"HyperDash":false},{"StartTime":17586.0,"Position":4.30590057,"HyperDash":false},{"StartTime":17661.0,"Position":19.18378,"HyperDash":false},{"StartTime":17772.0,"Position":65.0,"HyperDash":false}]},{"StartTime":17996.0,"Objects":[{"StartTime":17996.0,"Position":144.0,"HyperDash":false},{"StartTime":18089.0,"Position":144.091827,"HyperDash":false},{"StartTime":18219.0,"Position":137.026642,"HyperDash":false}]},{"StartTime":18444.0,"Objects":[{"StartTime":18444.0,"Position":156.0,"HyperDash":false},{"StartTime":18537.0,"Position":195.74173,"HyperDash":false},{"StartTime":18667.0,"Position":233.945068,"HyperDash":false}]},{"StartTime":18892.0,"Objects":[{"StartTime":18892.0,"Position":309.0,"HyperDash":false},{"StartTime":18975.0,"Position":331.4903,"HyperDash":false},{"StartTime":19059.0,"Position":371.3359,"HyperDash":false},{"StartTime":19143.0,"Position":396.1815,"HyperDash":false},{"StartTime":19227.0,"Position":428.204742,"HyperDash":false},{"StartTime":19302.0,"Position":418.734558,"HyperDash":false},{"StartTime":19377.0,"Position":358.08667,"HyperDash":false},{"StartTime":19452.0,"Position":359.438843,"HyperDash":false},{"StartTime":19563.0,"Position":309.0,"HyperDash":false}]},{"StartTime":19787.0,"Objects":[{"StartTime":19787.0,"Position":237.0,"HyperDash":false},{"StartTime":19880.0,"Position":210.372055,"HyperDash":false},{"StartTime":20010.0,"Position":234.5058,"HyperDash":false}]},{"StartTime":20235.0,"Objects":[{"StartTime":20235.0,"Position":296.0,"HyperDash":false},{"StartTime":20328.0,"Position":335.3686,"HyperDash":false},{"StartTime":20458.0,"Position":374.402649,"HyperDash":false}]},{"StartTime":20683.0,"Objects":[{"StartTime":20683.0,"Position":441.0,"HyperDash":false},{"StartTime":20766.0,"Position":438.742676,"HyperDash":false},{"StartTime":20850.0,"Position":413.918945,"HyperDash":false},{"StartTime":20934.0,"Position":420.274963,"HyperDash":false},{"StartTime":21018.0,"Position":440.574921,"HyperDash":false},{"StartTime":21093.0,"Position":428.433563,"HyperDash":false},{"StartTime":21168.0,"Position":429.064026,"HyperDash":false},{"StartTime":21243.0,"Position":410.101563,"HyperDash":false},{"StartTime":21354.0,"Position":441.0,"HyperDash":false}]},{"StartTime":21578.0,"Objects":[{"StartTime":21578.0,"Position":501.0,"HyperDash":false}]},{"StartTime":22026.0,"Objects":[{"StartTime":22026.0,"Position":386.0,"HyperDash":false},{"StartTime":22081.0,"Position":374.485016,"HyperDash":false},{"StartTime":22137.0,"Position":357.487,"HyperDash":false},{"StartTime":22193.0,"Position":318.665863,"HyperDash":false},{"StartTime":22249.0,"Position":311.3857,"HyperDash":false},{"StartTime":22305.0,"Position":300.98407,"HyperDash":false},{"StartTime":22361.0,"Position":266.708557,"HyperDash":false},{"StartTime":22417.0,"Position":256.6825,"HyperDash":false},{"StartTime":22473.0,"Position":240.899826,"HyperDash":false},{"StartTime":22529.0,"Position":227.386124,"HyperDash":false},{"StartTime":22585.0,"Position":225.861679,"HyperDash":false},{"StartTime":22641.0,"Position":185.350357,"HyperDash":false},{"StartTime":22697.0,"Position":169.039291,"HyperDash":false},{"StartTime":22753.0,"Position":131.207657,"HyperDash":false},{"StartTime":22809.0,"Position":115.215012,"HyperDash":false},{"StartTime":22865.0,"Position":108.42057,"HyperDash":false},{"StartTime":22921.0,"Position":89.93976,"HyperDash":false},{"StartTime":22977.0,"Position":126.071373,"HyperDash":false},{"StartTime":23033.0,"Position":140.858871,"HyperDash":false},{"StartTime":23089.0,"Position":159.8509,"HyperDash":false},{"StartTime":23145.0,"Position":166.689056,"HyperDash":false},{"StartTime":23201.0,"Position":205.013,"HyperDash":false},{"StartTime":23257.0,"Position":197.5373,"HyperDash":false},{"StartTime":23313.0,"Position":239.081787,"HyperDash":false},{"StartTime":23369.0,"Position":240.611664,"HyperDash":false},{"StartTime":23420.0,"Position":243.039688,"HyperDash":false},{"StartTime":23472.0,"Position":272.749237,"HyperDash":false},{"StartTime":23523.0,"Position":272.238831,"HyperDash":false},{"StartTime":23575.0,"Position":317.028137,"HyperDash":false},{"StartTime":23626.0,"Position":306.314117,"HyperDash":false},{"StartTime":23678.0,"Position":335.531525,"HyperDash":false},{"StartTime":23729.0,"Position":341.698853,"HyperDash":false},{"StartTime":23817.0,"Position":386.0,"HyperDash":false}]},{"StartTime":24041.0,"Objects":[{"StartTime":24041.0,"Position":465.0,"HyperDash":false}]},{"StartTime":24265.0,"Objects":[{"StartTime":24265.0,"Position":497.0,"HyperDash":false},{"StartTime":24348.0,"Position":488.55304,"HyperDash":false},{"StartTime":24432.0,"Position":484.0766,"HyperDash":false},{"StartTime":24516.0,"Position":480.600128,"HyperDash":false},{"StartTime":24600.0,"Position":487.108948,"HyperDash":false},{"StartTime":24675.0,"Position":484.305328,"HyperDash":false},{"StartTime":24750.0,"Position":486.516449,"HyperDash":false},{"StartTime":24825.0,"Position":507.727539,"HyperDash":false},{"StartTime":24936.0,"Position":497.0,"HyperDash":false}]},{"StartTime":25160.0,"Objects":[{"StartTime":25160.0,"Position":410.0,"HyperDash":false},{"StartTime":25253.0,"Position":380.109436,"HyperDash":false},{"StartTime":25383.0,"Position":332.0014,"HyperDash":false}]},{"StartTime":25608.0,"Objects":[{"StartTime":25608.0,"Position":262.0,"HyperDash":false},{"StartTime":25701.0,"Position":218.3702,"HyperDash":false},{"StartTime":25831.0,"Position":184.296768,"HyperDash":false}]},{"StartTime":26056.0,"Objects":[{"StartTime":26056.0,"Position":136.0,"HyperDash":false},{"StartTime":26139.0,"Position":126.098541,"HyperDash":false},{"StartTime":26223.0,"Position":125.222366,"HyperDash":false},{"StartTime":26307.0,"Position":138.346191,"HyperDash":false},{"StartTime":26391.0,"Position":144.482666,"HyperDash":false},{"StartTime":26466.0,"Position":145.59903,"HyperDash":false},{"StartTime":26541.0,"Position":121.702759,"HyperDash":false},{"StartTime":26616.0,"Position":138.806488,"HyperDash":false},{"StartTime":26727.0,"Position":136.0,"HyperDash":false}]},{"StartTime":26951.0,"Objects":[{"StartTime":26951.0,"Position":67.0,"HyperDash":false}]},{"StartTime":27399.0,"Objects":[{"StartTime":27399.0,"Position":118.0,"HyperDash":false},{"StartTime":27454.0,"Position":149.263077,"HyperDash":false},{"StartTime":27510.0,"Position":152.985062,"HyperDash":false},{"StartTime":27566.0,"Position":191.9209,"HyperDash":false},{"StartTime":27622.0,"Position":186.1002,"HyperDash":false},{"StartTime":27678.0,"Position":201.49527,"HyperDash":false},{"StartTime":27734.0,"Position":213.367828,"HyperDash":false},{"StartTime":27790.0,"Position":256.814331,"HyperDash":false},{"StartTime":27846.0,"Position":246.461456,"HyperDash":false},{"StartTime":27940.0,"Position":268.489075,"HyperDash":false},{"StartTime":28070.0,"Position":233.472458,"HyperDash":false}]},{"StartTime":28295.0,"Objects":[{"StartTime":28295.0,"Position":162.0,"HyperDash":false},{"StartTime":28350.0,"Position":164.220917,"HyperDash":false},{"StartTime":28406.0,"Position":194.79129,"HyperDash":false},{"StartTime":28462.0,"Position":226.3617,"HyperDash":false},{"StartTime":28518.0,"Position":251.932068,"HyperDash":false},{"StartTime":28574.0,"Position":246.033783,"HyperDash":false},{"StartTime":28630.0,"Position":264.13385,"HyperDash":false},{"StartTime":28686.0,"Position":278.233948,"HyperDash":false},{"StartTime":28742.0,"Position":316.344543,"HyperDash":false},{"StartTime":28836.0,"Position":335.418,"HyperDash":false},{"StartTime":28966.0,"Position":395.157867,"HyperDash":false}]},{"StartTime":29190.0,"Objects":[{"StartTime":29190.0,"Position":481.0,"HyperDash":false}]},{"StartTime":29414.0,"Objects":[{"StartTime":29414.0,"Position":499.0,"HyperDash":false}]},{"StartTime":29638.0,"Objects":[{"StartTime":29638.0,"Position":454.0,"HyperDash":false},{"StartTime":29721.0,"Position":464.071655,"HyperDash":false},{"StartTime":29805.0,"Position":475.192383,"HyperDash":false},{"StartTime":29889.0,"Position":456.3131,"HyperDash":false},{"StartTime":29973.0,"Position":470.458374,"HyperDash":false},{"StartTime":30048.0,"Position":459.80368,"HyperDash":false},{"StartTime":30123.0,"Position":473.124451,"HyperDash":false},{"StartTime":30198.0,"Position":468.445251,"HyperDash":false},{"StartTime":30309.0,"Position":454.0,"HyperDash":false}]},{"StartTime":30533.0,"Objects":[{"StartTime":30533.0,"Position":375.0,"HyperDash":false},{"StartTime":30626.0,"Position":348.741882,"HyperDash":false},{"StartTime":30756.0,"Position":297.814758,"HyperDash":false}]},{"StartTime":30981.0,"Objects":[{"StartTime":30981.0,"Position":220.0,"HyperDash":false},{"StartTime":31036.0,"Position":200.494568,"HyperDash":false},{"StartTime":31092.0,"Position":189.8578,"HyperDash":false},{"StartTime":31148.0,"Position":158.568909,"HyperDash":false},{"StartTime":31204.0,"Position":137.831863,"HyperDash":false},{"StartTime":31260.0,"Position":143.862488,"HyperDash":false},{"StartTime":31316.0,"Position":99.86672,"HyperDash":false},{"StartTime":31372.0,"Position":85.05304,"HyperDash":false},{"StartTime":31428.0,"Position":65.47009,"HyperDash":false},{"StartTime":31479.0,"Position":97.9493561,"HyperDash":false},{"StartTime":31531.0,"Position":85.30683,"HyperDash":false},{"StartTime":31582.0,"Position":136.499527,"HyperDash":false},{"StartTime":31634.0,"Position":141.072418,"HyperDash":false},{"StartTime":31685.0,"Position":152.152847,"HyperDash":false},{"StartTime":31737.0,"Position":182.289108,"HyperDash":false},{"StartTime":31788.0,"Position":190.604156,"HyperDash":false},{"StartTime":31876.0,"Position":220.0,"HyperDash":false}]},{"StartTime":32325.0,"Objects":[{"StartTime":32325.0,"Position":365.0,"HyperDash":false}]},{"StartTime":32772.0,"Objects":[{"StartTime":32772.0,"Position":480.0,"HyperDash":false},{"StartTime":32823.0,"Position":493.32843,"HyperDash":false},{"StartTime":32874.0,"Position":464.65686,"HyperDash":false},{"StartTime":32926.0,"Position":458.9525,"HyperDash":false},{"StartTime":32977.0,"Position":466.280945,"HyperDash":false},{"StartTime":33028.0,"Position":453.609375,"HyperDash":false},{"StartTime":33080.0,"Position":465.905029,"HyperDash":false},{"StartTime":33131.0,"Position":473.233459,"HyperDash":false},{"StartTime":33219.0,"Position":465.349182,"HyperDash":false}]},{"StartTime":33444.0,"Objects":[{"StartTime":33444.0,"Position":322.0,"HyperDash":false}]},{"StartTime":33668.0,"Objects":[{"StartTime":33668.0,"Position":323.0,"HyperDash":false},{"StartTime":33761.0,"Position":290.802338,"HyperDash":false},{"StartTime":33891.0,"Position":243.397018,"HyperDash":false}]},{"StartTime":34116.0,"Objects":[{"StartTime":34116.0,"Position":162.0,"HyperDash":false},{"StartTime":34209.0,"Position":126.802353,"HyperDash":false},{"StartTime":34339.0,"Position":82.39702,"HyperDash":false}]},{"StartTime":34563.0,"Objects":[{"StartTime":34563.0,"Position":31.0,"HyperDash":false},{"StartTime":34618.0,"Position":38.3338165,"HyperDash":false},{"StartTime":34674.0,"Position":12.5252123,"HyperDash":false},{"StartTime":34730.0,"Position":24.94529,"HyperDash":false},{"StartTime":34786.0,"Position":0.0,"HyperDash":false},{"StartTime":34842.0,"Position":7.506119,"HyperDash":false},{"StartTime":34898.0,"Position":0.0,"HyperDash":false},{"StartTime":34954.0,"Position":18.1432285,"HyperDash":false},{"StartTime":35010.0,"Position":21.8685,"HyperDash":false},{"StartTime":35061.0,"Position":25.771328,"HyperDash":false},{"StartTime":35113.0,"Position":7.32367039,"HyperDash":false},{"StartTime":35164.0,"Position":0.0,"HyperDash":false},{"StartTime":35216.0,"Position":12.3119221,"HyperDash":false},{"StartTime":35267.0,"Position":14.6618919,"HyperDash":false},{"StartTime":35319.0,"Position":12.9432926,"HyperDash":false},{"StartTime":35370.0,"Position":0.05334282,"HyperDash":false},{"StartTime":35458.0,"Position":31.0,"HyperDash":false}]},{"StartTime":35907.0,"Objects":[{"StartTime":35907.0,"Position":183.0,"HyperDash":false}]},{"StartTime":36354.0,"Objects":[{"StartTime":36354.0,"Position":336.0,"HyperDash":false},{"StartTime":36409.0,"Position":332.550262,"HyperDash":false},{"StartTime":36465.0,"Position":357.661743,"HyperDash":false},{"StartTime":36521.0,"Position":395.893524,"HyperDash":false},{"StartTime":36577.0,"Position":398.9578,"HyperDash":false},{"StartTime":36633.0,"Position":441.6068,"HyperDash":false},{"StartTime":36689.0,"Position":459.563568,"HyperDash":false},{"StartTime":36745.0,"Position":458.55127,"HyperDash":false},{"StartTime":36801.0,"Position":485.465271,"HyperDash":false},{"StartTime":36852.0,"Position":448.681152,"HyperDash":false},{"StartTime":36904.0,"Position":431.13797,"HyperDash":false},{"StartTime":36955.0,"Position":444.931641,"HyperDash":false},{"StartTime":37007.0,"Position":413.575562,"HyperDash":false},{"StartTime":37058.0,"Position":398.977661,"HyperDash":false},{"StartTime":37110.0,"Position":374.650665,"HyperDash":false},{"StartTime":37161.0,"Position":348.4818,"HyperDash":false},{"StartTime":37249.0,"Position":336.0,"HyperDash":false}]},{"StartTime":37474.0,"Objects":[{"StartTime":37474.0,"Position":278.0,"HyperDash":false}]},{"StartTime":37698.0,"Objects":[{"StartTime":37698.0,"Position":218.0,"HyperDash":false},{"StartTime":37791.0,"Position":186.661133,"HyperDash":false},{"StartTime":37921.0,"Position":141.792221,"HyperDash":false}]},{"StartTime":38145.0,"Objects":[{"StartTime":38145.0,"Position":55.0,"HyperDash":false},{"StartTime":38196.0,"Position":55.39138,"HyperDash":false},{"StartTime":38247.0,"Position":17.7827568,"HyperDash":false},{"StartTime":38299.0,"Position":25.8781147,"HyperDash":false},{"StartTime":38350.0,"Position":15.6772919,"HyperDash":false},{"StartTime":38401.0,"Position":46.47647,"HyperDash":false},{"StartTime":38453.0,"Position":19.3305359,"HyperDash":false},{"StartTime":38504.0,"Position":58.12971,"HyperDash":false},{"StartTime":38592.0,"Position":45.9596672,"HyperDash":false}]},{"StartTime":39041.0,"Objects":[{"StartTime":39041.0,"Position":188.0,"HyperDash":false},{"StartTime":39092.0,"Position":206.608627,"HyperDash":false},{"StartTime":39143.0,"Position":207.217239,"HyperDash":false},{"StartTime":39195.0,"Position":212.121887,"HyperDash":false},{"StartTime":39246.0,"Position":222.322708,"HyperDash":false},{"StartTime":39297.0,"Position":209.523529,"HyperDash":false},{"StartTime":39349.0,"Position":205.669464,"HyperDash":false},{"StartTime":39400.0,"Position":188.870285,"HyperDash":false},{"StartTime":39488.0,"Position":197.040329,"HyperDash":false}]},{"StartTime":39936.0,"Objects":[{"StartTime":39936.0,"Position":305.0,"HyperDash":false},{"StartTime":39987.0,"Position":326.221222,"HyperDash":false},{"StartTime":40038.0,"Position":329.12558,"HyperDash":false},{"StartTime":40090.0,"Position":351.555145,"HyperDash":false},{"StartTime":40141.0,"Position":355.340942,"HyperDash":false},{"StartTime":40192.0,"Position":390.523621,"HyperDash":false},{"StartTime":40244.0,"Position":399.5398,"HyperDash":false},{"StartTime":40295.0,"Position":402.617462,"HyperDash":false},{"StartTime":40383.0,"Position":452.46936,"HyperDash":false}]},{"StartTime":40832.0,"Objects":[{"StartTime":40832.0,"Position":486.0,"HyperDash":false},{"StartTime":40915.0,"Position":469.7972,"HyperDash":false},{"StartTime":40999.0,"Position":481.8138,"HyperDash":false},{"StartTime":41083.0,"Position":457.634216,"HyperDash":false},{"StartTime":41167.0,"Position":437.2155,"HyperDash":false},{"StartTime":41242.0,"Position":451.25293,"HyperDash":false},{"StartTime":41317.0,"Position":459.7593,"HyperDash":false},{"StartTime":41392.0,"Position":473.703156,"HyperDash":false},{"StartTime":41503.0,"Position":486.0,"HyperDash":false}]},{"StartTime":41728.0,"Objects":[{"StartTime":41728.0,"Position":415.0,"HyperDash":false},{"StartTime":41783.0,"Position":390.7221,"HyperDash":false},{"StartTime":41839.0,"Position":366.340027,"HyperDash":false},{"StartTime":41895.0,"Position":357.472321,"HyperDash":false},{"StartTime":41951.0,"Position":323.4682,"HyperDash":false},{"StartTime":42007.0,"Position":318.667938,"HyperDash":false},{"StartTime":42063.0,"Position":313.410736,"HyperDash":false},{"StartTime":42119.0,"Position":269.011841,"HyperDash":false},{"StartTime":42175.0,"Position":262.671448,"HyperDash":false},{"StartTime":42226.0,"Position":272.1187,"HyperDash":false},{"StartTime":42278.0,"Position":312.04538,"HyperDash":false},{"StartTime":42329.0,"Position":293.437958,"HyperDash":false},{"StartTime":42381.0,"Position":345.712128,"HyperDash":false},{"StartTime":42432.0,"Position":366.896667,"HyperDash":false},{"StartTime":42484.0,"Position":350.446564,"HyperDash":false},{"StartTime":42535.0,"Position":369.3803,"HyperDash":false},{"StartTime":42623.0,"Position":415.0,"HyperDash":false}]},{"StartTime":43071.0,"Objects":[{"StartTime":43071.0,"Position":353.0,"HyperDash":false}]},{"StartTime":43519.0,"Objects":[{"StartTime":43519.0,"Position":181.0,"HyperDash":false},{"StartTime":43570.0,"Position":174.8302,"HyperDash":false},{"StartTime":43621.0,"Position":156.660385,"HyperDash":false},{"StartTime":43673.0,"Position":141.134308,"HyperDash":false},{"StartTime":43724.0,"Position":99.9645,"HyperDash":false},{"StartTime":43775.0,"Position":75.79469,"HyperDash":false},{"StartTime":43827.0,"Position":67.26861,"HyperDash":false},{"StartTime":43878.0,"Position":66.0988159,"HyperDash":false},{"StartTime":43966.0,"Position":21.7469788,"HyperDash":false}]},{"StartTime":44414.0,"Objects":[{"StartTime":44414.0,"Position":21.0,"HyperDash":false},{"StartTime":44465.0,"Position":38.1698074,"HyperDash":false},{"StartTime":44516.0,"Position":57.3396149,"HyperDash":false},{"StartTime":44568.0,"Position":68.86569,"HyperDash":false},{"StartTime":44619.0,"Position":110.0355,"HyperDash":false},{"StartTime":44670.0,"Position":121.205307,"HyperDash":false},{"StartTime":44722.0,"Position":123.731384,"HyperDash":false},{"StartTime":44773.0,"Position":164.901184,"HyperDash":false},{"StartTime":44861.0,"Position":180.253021,"HyperDash":false}]},{"StartTime":45086.0,"Objects":[{"StartTime":45086.0,"Position":328.0,"HyperDash":false}]},{"StartTime":45310.0,"Objects":[{"StartTime":45310.0,"Position":329.0,"HyperDash":false},{"StartTime":45365.0,"Position":332.211578,"HyperDash":false},{"StartTime":45421.0,"Position":367.175873,"HyperDash":false},{"StartTime":45477.0,"Position":371.022522,"HyperDash":false},{"StartTime":45533.0,"Position":395.233124,"HyperDash":false},{"StartTime":45589.0,"Position":413.246216,"HyperDash":false},{"StartTime":45645.0,"Position":433.6284,"HyperDash":false},{"StartTime":45701.0,"Position":457.874817,"HyperDash":false},{"StartTime":45757.0,"Position":467.659363,"HyperDash":false},{"StartTime":45813.0,"Position":493.610321,"HyperDash":false},{"StartTime":45869.0,"Position":491.524567,"HyperDash":false},{"StartTime":45925.0,"Position":475.219482,"HyperDash":false},{"StartTime":45981.0,"Position":499.624725,"HyperDash":false},{"StartTime":46037.0,"Position":471.774384,"HyperDash":false},{"StartTime":46093.0,"Position":462.734833,"HyperDash":false},{"StartTime":46149.0,"Position":450.75238,"HyperDash":false},{"StartTime":46205.0,"Position":451.0282,"HyperDash":false},{"StartTime":46256.0,"Position":439.419067,"HyperDash":false},{"StartTime":46308.0,"Position":413.8077,"HyperDash":false},{"StartTime":46359.0,"Position":423.184723,"HyperDash":false},{"StartTime":46411.0,"Position":393.298828,"HyperDash":false},{"StartTime":46462.0,"Position":384.2213,"HyperDash":false},{"StartTime":46514.0,"Position":355.668274,"HyperDash":false},{"StartTime":46565.0,"Position":316.77417,"HyperDash":false},{"StartTime":46653.0,"Position":303.770752,"HyperDash":false}]},{"StartTime":47101.0,"Objects":[{"StartTime":47101.0,"Position":257.0,"HyperDash":false},{"StartTime":47184.0,"Position":212.304276,"HyperDash":false},{"StartTime":47268.0,"Position":213.274872,"HyperDash":false},{"StartTime":47352.0,"Position":179.2254,"HyperDash":false},{"StartTime":47436.0,"Position":142.9541,"HyperDash":false},{"StartTime":47511.0,"Position":150.761337,"HyperDash":false},{"StartTime":47586.0,"Position":198.741776,"HyperDash":false},{"StartTime":47661.0,"Position":220.961136,"HyperDash":false},{"StartTime":47772.0,"Position":257.0,"HyperDash":false}]},{"StartTime":47996.0,"Objects":[{"StartTime":47996.0,"Position":336.0,"HyperDash":false}]},{"StartTime":48220.0,"Objects":[{"StartTime":48220.0,"Position":417.0,"HyperDash":false},{"StartTime":48275.0,"Position":444.6565,"HyperDash":false},{"StartTime":48331.0,"Position":441.67038,"HyperDash":false},{"StartTime":48387.0,"Position":472.684265,"HyperDash":false},{"StartTime":48443.0,"Position":496.876831,"HyperDash":false},{"StartTime":48537.0,"Position":462.460815,"HyperDash":false},{"StartTime":48667.0,"Position":417.0,"HyperDash":false}]},{"StartTime":48892.0,"Objects":[{"StartTime":48892.0,"Position":379.0,"HyperDash":false},{"StartTime":48985.0,"Position":356.006134,"HyperDash":false},{"StartTime":49115.0,"Position":302.860016,"HyperDash":false}]},{"StartTime":49339.0,"Objects":[{"StartTime":49339.0,"Position":218.0,"HyperDash":false},{"StartTime":49422.0,"Position":228.320267,"HyperDash":false},{"StartTime":49506.0,"Position":263.682922,"HyperDash":false},{"StartTime":49590.0,"Position":263.529572,"HyperDash":false},{"StartTime":49674.0,"Position":266.142761,"HyperDash":false},{"StartTime":49749.0,"Position":265.0218,"HyperDash":false},{"StartTime":49824.0,"Position":252.383118,"HyperDash":false},{"StartTime":49899.0,"Position":244.59021,"HyperDash":false},{"StartTime":50010.0,"Position":218.0,"HyperDash":false}]},{"StartTime":50235.0,"Objects":[{"StartTime":50235.0,"Position":142.0,"HyperDash":false},{"StartTime":50328.0,"Position":154.293335,"HyperDash":false},{"StartTime":50458.0,"Position":135.509842,"HyperDash":false}]},{"StartTime":50683.0,"Objects":[{"StartTime":50683.0,"Position":75.0,"HyperDash":false},{"StartTime":50734.0,"Position":106.62645,"HyperDash":false},{"StartTime":50785.0,"Position":89.7852249,"HyperDash":false},{"StartTime":50837.0,"Position":105.419983,"HyperDash":false},{"StartTime":50888.0,"Position":153.41716,"HyperDash":false},{"StartTime":50939.0,"Position":166.651077,"HyperDash":false},{"StartTime":50991.0,"Position":157.985535,"HyperDash":false},{"StartTime":51042.0,"Position":194.261,"HyperDash":false},{"StartTime":51130.0,"Position":222.110641,"HyperDash":false}]},{"StartTime":51354.0,"Objects":[{"StartTime":51354.0,"Position":295.0,"HyperDash":false},{"StartTime":51405.0,"Position":294.626465,"HyperDash":false},{"StartTime":51456.0,"Position":306.785217,"HyperDash":false},{"StartTime":51508.0,"Position":347.419983,"HyperDash":false},{"StartTime":51559.0,"Position":363.417175,"HyperDash":false},{"StartTime":51610.0,"Position":396.6511,"HyperDash":false},{"StartTime":51662.0,"Position":408.985535,"HyperDash":false},{"StartTime":51713.0,"Position":417.261,"HyperDash":false},{"StartTime":51801.0,"Position":442.110657,"HyperDash":false}]},{"StartTime":52026.0,"Objects":[{"StartTime":52026.0,"Position":498.0,"HyperDash":false}]},{"StartTime":52474.0,"Objects":[{"StartTime":52474.0,"Position":404.0,"HyperDash":false},{"StartTime":52567.0,"Position":378.721558,"HyperDash":false},{"StartTime":52697.0,"Position":324.2033,"HyperDash":false}]},{"StartTime":52922.0,"Objects":[{"StartTime":52922.0,"Position":251.0,"HyperDash":false},{"StartTime":53005.0,"Position":216.759811,"HyperDash":false},{"StartTime":53089.0,"Position":195.34903,"HyperDash":false},{"StartTime":53173.0,"Position":148.36676,"HyperDash":false},{"StartTime":53257.0,"Position":135.014374,"HyperDash":false},{"StartTime":53332.0,"Position":141.829834,"HyperDash":false},{"StartTime":53407.0,"Position":167.570328,"HyperDash":false},{"StartTime":53482.0,"Position":217.1065,"HyperDash":false},{"StartTime":53593.0,"Position":251.0,"HyperDash":false}]},{"StartTime":53817.0,"Objects":[{"StartTime":53817.0,"Position":298.0,"HyperDash":false},{"StartTime":53910.0,"Position":296.8232,"HyperDash":false},{"StartTime":54040.0,"Position":295.178223,"HyperDash":false}]},{"StartTime":54265.0,"Objects":[{"StartTime":54265.0,"Position":249.0,"HyperDash":false},{"StartTime":54316.0,"Position":240.835571,"HyperDash":false},{"StartTime":54367.0,"Position":194.671127,"HyperDash":false},{"StartTime":54419.0,"Position":191.150528,"HyperDash":false},{"StartTime":54470.0,"Position":170.708618,"HyperDash":false},{"StartTime":54521.0,"Position":161.552643,"HyperDash":false},{"StartTime":54573.0,"Position":158.896118,"HyperDash":false},{"StartTime":54624.0,"Position":134.782074,"HyperDash":false},{"StartTime":54712.0,"Position":92.52641,"HyperDash":false}]},{"StartTime":55160.0,"Objects":[{"StartTime":55160.0,"Position":8.0,"HyperDash":false},{"StartTime":55253.0,"Position":34.09524,"HyperDash":false},{"StartTime":55383.0,"Position":85.37553,"HyperDash":false}]},{"StartTime":55608.0,"Objects":[{"StartTime":55608.0,"Position":165.0,"HyperDash":false},{"StartTime":55701.0,"Position":183.095245,"HyperDash":false},{"StartTime":55831.0,"Position":242.375519,"HyperDash":false}]},{"StartTime":56056.0,"Objects":[{"StartTime":56056.0,"Position":329.0,"HyperDash":false},{"StartTime":56107.0,"Position":349.227417,"HyperDash":false},{"StartTime":56158.0,"Position":353.454865,"HyperDash":false},{"StartTime":56210.0,"Position":358.902435,"HyperDash":false},{"StartTime":56261.0,"Position":360.282623,"HyperDash":false},{"StartTime":56312.0,"Position":376.968658,"HyperDash":false},{"StartTime":56364.0,"Position":354.628937,"HyperDash":false},{"StartTime":56415.0,"Position":382.314972,"HyperDash":false},{"StartTime":56503.0,"Position":361.04776,"HyperDash":false}]},{"StartTime":56951.0,"Objects":[{"StartTime":56951.0,"Position":189.0,"HyperDash":false},{"StartTime":57044.0,"Position":142.707138,"HyperDash":false},{"StartTime":57174.0,"Position":111.099754,"HyperDash":false}]},{"StartTime":57399.0,"Objects":[{"StartTime":57399.0,"Position":44.0,"HyperDash":false},{"StartTime":57492.0,"Position":42.46508,"HyperDash":false},{"StartTime":57622.0,"Position":57.39981,"HyperDash":false}]},{"StartTime":57847.0,"Objects":[{"StartTime":57847.0,"Position":97.0,"HyperDash":false},{"StartTime":57898.0,"Position":128.653931,"HyperDash":false},{"StartTime":57949.0,"Position":137.733063,"HyperDash":false},{"StartTime":58001.0,"Position":141.3299,"HyperDash":false},{"StartTime":58052.0,"Position":175.3739,"HyperDash":false},{"StartTime":58103.0,"Position":188.865829,"HyperDash":false},{"StartTime":58155.0,"Position":184.813812,"HyperDash":false},{"StartTime":58206.0,"Position":222.592514,"HyperDash":false},{"StartTime":58294.0,"Position":246.818512,"HyperDash":false}]},{"StartTime":58742.0,"Objects":[{"StartTime":58742.0,"Position":396.0,"HyperDash":false},{"StartTime":58835.0,"Position":405.3873,"HyperDash":false},{"StartTime":58965.0,"Position":406.520081,"HyperDash":false}]},{"StartTime":59190.0,"Objects":[{"StartTime":59190.0,"Position":473.0,"HyperDash":false},{"StartTime":59283.0,"Position":484.6127,"HyperDash":false},{"StartTime":59413.0,"Position":462.479919,"HyperDash":false}]},{"StartTime":59638.0,"Objects":[{"StartTime":59638.0,"Position":450.0,"HyperDash":false},{"StartTime":59689.0,"Position":425.546051,"HyperDash":false},{"StartTime":59740.0,"Position":404.6286,"HyperDash":false},{"StartTime":59792.0,"Position":403.0906,"HyperDash":false},{"StartTime":59843.0,"Position":359.851471,"HyperDash":false},{"StartTime":59894.0,"Position":346.7696,"HyperDash":false},{"StartTime":59946.0,"Position":349.71637,"HyperDash":false},{"StartTime":59997.0,"Position":332.582275,"HyperDash":false},{"StartTime":60085.0,"Position":296.934937,"HyperDash":false}]},{"StartTime":60310.0,"Objects":[{"StartTime":60310.0,"Position":137.0,"HyperDash":false}]},{"StartTime":60534.0,"Objects":[{"StartTime":60534.0,"Position":127.0,"HyperDash":false},{"StartTime":60627.0,"Position":133.780716,"HyperDash":false},{"StartTime":60757.0,"Position":121.678482,"HyperDash":false}]},{"StartTime":60981.0,"Objects":[{"StartTime":60981.0,"Position":111.0,"HyperDash":false}]},{"StartTime":61429.0,"Objects":[{"StartTime":61429.0,"Position":110.0,"HyperDash":false},{"StartTime":61512.0,"Position":137.803375,"HyperDash":false},{"StartTime":61596.0,"Position":149.4081,"HyperDash":false},{"StartTime":61680.0,"Position":212.379776,"HyperDash":false},{"StartTime":61764.0,"Position":226.716034,"HyperDash":false},{"StartTime":61839.0,"Position":203.918869,"HyperDash":false},{"StartTime":61914.0,"Position":175.198227,"HyperDash":false},{"StartTime":61989.0,"Position":145.558578,"HyperDash":false},{"StartTime":62100.0,"Position":110.0,"HyperDash":false}]},{"StartTime":62325.0,"Objects":[{"StartTime":62325.0,"Position":22.0,"HyperDash":false},{"StartTime":62418.0,"Position":37.5815735,"HyperDash":false},{"StartTime":62548.0,"Position":18.5988235,"HyperDash":false}]},{"StartTime":62772.0,"Objects":[{"StartTime":62772.0,"Position":2.0,"HyperDash":false}]},{"StartTime":62996.0,"Objects":[{"StartTime":62996.0,"Position":76.0,"HyperDash":false}]},{"StartTime":63220.0,"Objects":[{"StartTime":63220.0,"Position":154.0,"HyperDash":false},{"StartTime":63313.0,"Position":199.111572,"HyperDash":false},{"StartTime":63443.0,"Position":232.57634,"HyperDash":false}]},{"StartTime":63668.0,"Objects":[{"StartTime":63668.0,"Position":307.0,"HyperDash":false},{"StartTime":63751.0,"Position":314.019135,"HyperDash":false},{"StartTime":63835.0,"Position":318.026459,"HyperDash":false},{"StartTime":63919.0,"Position":289.0338,"HyperDash":false},{"StartTime":64003.0,"Position":303.035217,"HyperDash":false},{"StartTime":64078.0,"Position":308.915619,"HyperDash":false},{"StartTime":64153.0,"Position":315.801941,"HyperDash":false},{"StartTime":64228.0,"Position":288.688263,"HyperDash":false},{"StartTime":64339.0,"Position":307.0,"HyperDash":false}]},{"StartTime":64563.0,"Objects":[{"StartTime":64563.0,"Position":311.0,"HyperDash":false},{"StartTime":64656.0,"Position":362.111572,"HyperDash":false},{"StartTime":64786.0,"Position":389.576324,"HyperDash":false}]},{"StartTime":65011.0,"Objects":[{"StartTime":65011.0,"Position":435.0,"HyperDash":false},{"StartTime":65062.0,"Position":440.232056,"HyperDash":false},{"StartTime":65113.0,"Position":422.4641,"HyperDash":false},{"StartTime":65165.0,"Position":444.6811,"HyperDash":false},{"StartTime":65216.0,"Position":423.913147,"HyperDash":false},{"StartTime":65267.0,"Position":441.145172,"HyperDash":false},{"StartTime":65319.0,"Position":427.362183,"HyperDash":false},{"StartTime":65370.0,"Position":412.594238,"HyperDash":false},{"StartTime":65458.0,"Position":428.269135,"HyperDash":false}]},{"StartTime":65683.0,"Objects":[{"StartTime":65683.0,"Position":350.0,"HyperDash":false},{"StartTime":65734.0,"Position":314.27713,"HyperDash":false},{"StartTime":65785.0,"Position":300.566528,"HyperDash":false},{"StartTime":65837.0,"Position":315.7566,"HyperDash":false},{"StartTime":65888.0,"Position":262.77713,"HyperDash":false},{"StartTime":65939.0,"Position":282.5542,"HyperDash":false},{"StartTime":65991.0,"Position":226.007614,"HyperDash":false},{"StartTime":66042.0,"Position":227.1106,"HyperDash":false},{"StartTime":66130.0,"Position":197.703339,"HyperDash":false}]},{"StartTime":66354.0,"Objects":[{"StartTime":66354.0,"Position":36.0,"HyperDash":false}]},{"StartTime":66802.0,"Objects":[{"StartTime":66802.0,"Position":44.0,"HyperDash":false},{"StartTime":66895.0,"Position":34.5778,"HyperDash":false},{"StartTime":67025.0,"Position":49.4306221,"HyperDash":false}]},{"StartTime":67250.0,"Objects":[{"StartTime":67250.0,"Position":131.0,"HyperDash":false},{"StartTime":67333.0,"Position":87.4688339,"HyperDash":false},{"StartTime":67417.0,"Position":59.51071,"HyperDash":false},{"StartTime":67501.0,"Position":67.3197,"HyperDash":false},{"StartTime":67585.0,"Position":34.3176,"HyperDash":false},{"StartTime":67660.0,"Position":32.04751,"HyperDash":false},{"StartTime":67735.0,"Position":85.74523,"HyperDash":false},{"StartTime":67810.0,"Position":81.77102,"HyperDash":false},{"StartTime":67921.0,"Position":131.0,"HyperDash":false}]},{"StartTime":68145.0,"Objects":[{"StartTime":68145.0,"Position":206.0,"HyperDash":false},{"StartTime":68238.0,"Position":241.281784,"HyperDash":false},{"StartTime":68368.0,"Position":285.804718,"HyperDash":false}]},{"StartTime":68593.0,"Objects":[{"StartTime":68593.0,"Position":354.0,"HyperDash":false},{"StartTime":68644.0,"Position":371.9797,"HyperDash":false},{"StartTime":68695.0,"Position":374.9594,"HyperDash":false},{"StartTime":68747.0,"Position":363.977966,"HyperDash":false},{"StartTime":68798.0,"Position":348.931732,"HyperDash":false},{"StartTime":68849.0,"Position":335.783875,"HyperDash":false},{"StartTime":68901.0,"Position":349.448822,"HyperDash":false},{"StartTime":68952.0,"Position":338.5818,"HyperDash":false},{"StartTime":69040.0,"Position":346.262146,"HyperDash":false}]},{"StartTime":69489.0,"Objects":[{"StartTime":69489.0,"Position":479.0,"HyperDash":false},{"StartTime":69582.0,"Position":463.7517,"HyperDash":false},{"StartTime":69712.0,"Position":471.2111,"HyperDash":false}]},{"StartTime":69936.0,"Objects":[{"StartTime":69936.0,"Position":395.0,"HyperDash":false},{"StartTime":70029.0,"Position":351.9091,"HyperDash":false},{"StartTime":70159.0,"Position":317.104523,"HyperDash":false}]},{"StartTime":70384.0,"Objects":[{"StartTime":70384.0,"Position":239.0,"HyperDash":false},{"StartTime":70435.0,"Position":235.932266,"HyperDash":false},{"StartTime":70486.0,"Position":206.714127,"HyperDash":false},{"StartTime":70538.0,"Position":191.116684,"HyperDash":false},{"StartTime":70589.0,"Position":179.00943,"HyperDash":false},{"StartTime":70640.0,"Position":139.19429,"HyperDash":false},{"StartTime":70692.0,"Position":141.486526,"HyperDash":false},{"StartTime":70743.0,"Position":106.327019,"HyperDash":false},{"StartTime":70831.0,"Position":87.14302,"HyperDash":false}]},{"StartTime":71280.0,"Objects":[{"StartTime":71280.0,"Position":11.0,"HyperDash":false},{"StartTime":71373.0,"Position":37.1006241,"HyperDash":false},{"StartTime":71503.0,"Position":90.3703156,"HyperDash":false}]},{"StartTime":71728.0,"Objects":[{"StartTime":71728.0,"Position":152.0,"HyperDash":false},{"StartTime":71821.0,"Position":193.100616,"HyperDash":false},{"StartTime":71951.0,"Position":231.370316,"HyperDash":false}]},{"StartTime":72175.0,"Objects":[{"StartTime":72175.0,"Position":271.0,"HyperDash":false},{"StartTime":72226.0,"Position":263.6878,"HyperDash":false},{"StartTime":72277.0,"Position":283.464,"HyperDash":false},{"StartTime":72329.0,"Position":257.4186,"HyperDash":false},{"StartTime":72380.0,"Position":278.35257,"HyperDash":false},{"StartTime":72431.0,"Position":304.125275,"HyperDash":false},{"StartTime":72483.0,"Position":296.814362,"HyperDash":false},{"StartTime":72534.0,"Position":316.538055,"HyperDash":false},{"StartTime":72622.0,"Position":338.266479,"HyperDash":false}]},{"StartTime":72847.0,"Objects":[{"StartTime":72847.0,"Position":505.0,"HyperDash":false}]},{"StartTime":73071.0,"Objects":[{"StartTime":73071.0,"Position":489.0,"HyperDash":false},{"StartTime":73164.0,"Position":469.365631,"HyperDash":false},{"StartTime":73294.0,"Position":482.683167,"HyperDash":false}]},{"StartTime":73519.0,"Objects":[{"StartTime":73519.0,"Position":408.0,"HyperDash":false},{"StartTime":73612.0,"Position":403.634369,"HyperDash":false},{"StartTime":73742.0,"Position":414.316833,"HyperDash":false}]},{"StartTime":73966.0,"Objects":[{"StartTime":73966.0,"Position":482.0,"HyperDash":false},{"StartTime":74017.0,"Position":472.133667,"HyperDash":false},{"StartTime":74068.0,"Position":425.9474,"HyperDash":false},{"StartTime":74120.0,"Position":412.437225,"HyperDash":false},{"StartTime":74171.0,"Position":412.766479,"HyperDash":false},{"StartTime":74222.0,"Position":404.367828,"HyperDash":false},{"StartTime":74274.0,"Position":384.1732,"HyperDash":false},{"StartTime":74325.0,"Position":361.954468,"HyperDash":false},{"StartTime":74413.0,"Position":325.429016,"HyperDash":false}]},{"StartTime":74862.0,"Objects":[{"StartTime":74862.0,"Position":157.0,"HyperDash":false},{"StartTime":74917.0,"Position":132.397827,"HyperDash":false},{"StartTime":74973.0,"Position":108.439255,"HyperDash":false},{"StartTime":75029.0,"Position":111.480682,"HyperDash":false},{"StartTime":75085.0,"Position":77.3439,"HyperDash":false},{"StartTime":75179.0,"Position":113.667587,"HyperDash":false},{"StartTime":75309.0,"Position":157.0,"HyperDash":false}]},{"StartTime":75534.0,"Objects":[{"StartTime":75534.0,"Position":381.0,"HyperDash":false}]},{"StartTime":75757.0,"Objects":[{"StartTime":75757.0,"Position":288.0,"HyperDash":false},{"StartTime":75812.0,"Position":322.1354,"HyperDash":false},{"StartTime":75868.0,"Position":327.117,"HyperDash":false},{"StartTime":75924.0,"Position":334.290924,"HyperDash":false},{"StartTime":75980.0,"Position":378.117737,"HyperDash":false},{"StartTime":76036.0,"Position":383.1031,"HyperDash":false},{"StartTime":76092.0,"Position":388.735718,"HyperDash":false},{"StartTime":76148.0,"Position":435.4911,"HyperDash":false},{"StartTime":76204.0,"Position":437.060059,"HyperDash":false},{"StartTime":76255.0,"Position":440.4263,"HyperDash":false},{"StartTime":76307.0,"Position":393.159027,"HyperDash":false},{"StartTime":76358.0,"Position":398.4255,"HyperDash":false},{"StartTime":76410.0,"Position":353.908081,"HyperDash":false},{"StartTime":76461.0,"Position":365.736755,"HyperDash":false},{"StartTime":76513.0,"Position":343.5878,"HyperDash":false},{"StartTime":76564.0,"Position":302.556519,"HyperDash":false},{"StartTime":76652.0,"Position":288.0,"HyperDash":false}]},{"StartTime":76877.0,"Objects":[{"StartTime":76877.0,"Position":225.0,"HyperDash":false},{"StartTime":76932.0,"Position":237.844727,"HyperDash":false},{"StartTime":76988.0,"Position":244.722977,"HyperDash":false},{"StartTime":77044.0,"Position":249.601242,"HyperDash":false},{"StartTime":77100.0,"Position":232.496277,"HyperDash":false},{"StartTime":77194.0,"Position":239.360245,"HyperDash":false},{"StartTime":77324.0,"Position":225.0,"HyperDash":false}]},{"StartTime":77548.0,"Objects":[{"StartTime":77548.0,"Position":172.0,"HyperDash":false},{"StartTime":77599.0,"Position":161.128448,"HyperDash":false},{"StartTime":77650.0,"Position":135.2569,"HyperDash":false},{"StartTime":77702.0,"Position":147.846878,"HyperDash":false},{"StartTime":77753.0,"Position":143.823837,"HyperDash":false},{"StartTime":77804.0,"Position":137.800812,"HyperDash":false},{"StartTime":77856.0,"Position":146.836151,"HyperDash":false},{"StartTime":77907.0,"Position":164.81311,"HyperDash":false},{"StartTime":77995.0,"Position":162.949844,"HyperDash":false}]},{"StartTime":78444.0,"Objects":[{"StartTime":78444.0,"Position":9.0,"HyperDash":false},{"StartTime":78495.0,"Position":32.8715477,"HyperDash":false},{"StartTime":78546.0,"Position":21.7430954,"HyperDash":false},{"StartTime":78598.0,"Position":50.15313,"HyperDash":false},{"StartTime":78649.0,"Position":21.1761589,"HyperDash":false},{"StartTime":78700.0,"Position":17.19919,"HyperDash":false},{"StartTime":78752.0,"Position":41.16385,"HyperDash":false},{"StartTime":78803.0,"Position":32.186882,"HyperDash":false},{"StartTime":78891.0,"Position":18.05015,"HyperDash":false}]},{"StartTime":79339.0,"Objects":[{"StartTime":79339.0,"Position":186.0,"HyperDash":false},{"StartTime":79390.0,"Position":199.306229,"HyperDash":false},{"StartTime":79441.0,"Position":219.682114,"HyperDash":false},{"StartTime":79493.0,"Position":224.118561,"HyperDash":false},{"StartTime":79544.0,"Position":227.689743,"HyperDash":false},{"StartTime":79595.0,"Position":241.25592,"HyperDash":false},{"StartTime":79647.0,"Position":265.72113,"HyperDash":false},{"StartTime":79698.0,"Position":285.940369,"HyperDash":false},{"StartTime":79786.0,"Position":327.296021,"HyperDash":false}]},{"StartTime":80011.0,"Objects":[{"StartTime":80011.0,"Position":461.0,"HyperDash":false}]},{"StartTime":80235.0,"Objects":[{"StartTime":80235.0,"Position":482.0,"HyperDash":false},{"StartTime":80328.0,"Position":471.961243,"HyperDash":false},{"StartTime":80458.0,"Position":472.315643,"HyperDash":false}]},{"StartTime":80683.0,"Objects":[{"StartTime":80683.0,"Position":392.0,"HyperDash":false},{"StartTime":80776.0,"Position":394.038757,"HyperDash":false},{"StartTime":80906.0,"Position":401.684357,"HyperDash":false}]},{"StartTime":81131.0,"Objects":[{"StartTime":81131.0,"Position":474.0,"HyperDash":false},{"StartTime":81182.0,"Position":450.511719,"HyperDash":false},{"StartTime":81233.0,"Position":460.8919,"HyperDash":false},{"StartTime":81285.0,"Position":418.0802,"HyperDash":false},{"StartTime":81336.0,"Position":403.0688,"HyperDash":false},{"StartTime":81387.0,"Position":402.833557,"HyperDash":false},{"StartTime":81439.0,"Position":375.354675,"HyperDash":false},{"StartTime":81490.0,"Position":367.6674,"HyperDash":false},{"StartTime":81578.0,"Position":323.0545,"HyperDash":false}]},{"StartTime":82026.0,"Objects":[{"StartTime":82026.0,"Position":148.0,"HyperDash":false},{"StartTime":82077.0,"Position":153.363663,"HyperDash":false},{"StartTime":82128.0,"Position":124.853226,"HyperDash":false},{"StartTime":82180.0,"Position":123.51664,"HyperDash":false},{"StartTime":82231.0,"Position":135.651062,"HyperDash":false},{"StartTime":82282.0,"Position":107.183319,"HyperDash":false},{"StartTime":82334.0,"Position":111.284645,"HyperDash":false},{"StartTime":82385.0,"Position":125.730865,"HyperDash":false},{"StartTime":82473.0,"Position":141.718521,"HyperDash":false}]},{"StartTime":82922.0,"Objects":[{"StartTime":82922.0,"Position":287.0,"HyperDash":false},{"StartTime":82977.0,"Position":306.298553,"HyperDash":false},{"StartTime":83033.0,"Position":321.504669,"HyperDash":false},{"StartTime":83089.0,"Position":339.161163,"HyperDash":false},{"StartTime":83145.0,"Position":367.092316,"HyperDash":false},{"StartTime":83201.0,"Position":394.0961,"HyperDash":false},{"StartTime":83257.0,"Position":406.969055,"HyperDash":false},{"StartTime":83313.0,"Position":412.5311,"HyperDash":false},{"StartTime":83369.0,"Position":442.643555,"HyperDash":false},{"StartTime":83425.0,"Position":445.5296,"HyperDash":false},{"StartTime":83481.0,"Position":420.6327,"HyperDash":false},{"StartTime":83537.0,"Position":415.785919,"HyperDash":false},{"StartTime":83593.0,"Position":369.41394,"HyperDash":false},{"StartTime":83649.0,"Position":368.030121,"HyperDash":false},{"StartTime":83705.0,"Position":376.311218,"HyperDash":false},{"StartTime":83761.0,"Position":349.831451,"HyperDash":false},{"StartTime":83817.0,"Position":357.0095,"HyperDash":false},{"StartTime":83868.0,"Position":377.510834,"HyperDash":false},{"StartTime":83920.0,"Position":394.548126,"HyperDash":false},{"StartTime":83971.0,"Position":406.9447,"HyperDash":false},{"StartTime":84023.0,"Position":383.802063,"HyperDash":false},{"StartTime":84074.0,"Position":391.380249,"HyperDash":false},{"StartTime":84126.0,"Position":407.693,"HyperDash":false},{"StartTime":84177.0,"Position":408.468567,"HyperDash":false},{"StartTime":84265.0,"Position":418.7769,"HyperDash":false}]},{"StartTime":84713.0,"Objects":[{"StartTime":84713.0,"Position":242.0,"HyperDash":false},{"StartTime":84796.0,"Position":214.531952,"HyperDash":false},{"StartTime":84880.0,"Position":201.708862,"HyperDash":false},{"StartTime":84964.0,"Position":158.885773,"HyperDash":false},{"StartTime":85048.0,"Position":122.885155,"HyperDash":false},{"StartTime":85123.0,"Position":159.3354,"HyperDash":false},{"StartTime":85198.0,"Position":171.963165,"HyperDash":false},{"StartTime":85273.0,"Position":220.590912,"HyperDash":false},{"StartTime":85384.0,"Position":242.0,"HyperDash":false}]},{"StartTime":85608.0,"Objects":[{"StartTime":85608.0,"Position":277.0,"HyperDash":false},{"StartTime":85659.0,"Position":273.8022,"HyperDash":false},{"StartTime":85710.0,"Position":272.42923,"HyperDash":false},{"StartTime":85762.0,"Position":256.8426,"HyperDash":false},{"StartTime":85813.0,"Position":245.819153,"HyperDash":false},{"StartTime":85864.0,"Position":210.419479,"HyperDash":false},{"StartTime":85916.0,"Position":177.694885,"HyperDash":false},{"StartTime":85967.0,"Position":180.692947,"HyperDash":false},{"StartTime":86055.0,"Position":144.3092,"HyperDash":false}]},{"StartTime":86504.0,"Objects":[{"StartTime":86504.0,"Position":11.0,"HyperDash":false}]},{"StartTime":93668.0,"Objects":[{"StartTime":93668.0,"Position":321.0,"HyperDash":false},{"StartTime":93723.0,"Position":305.388947,"HyperDash":false},{"StartTime":93779.0,"Position":291.399963,"HyperDash":false},{"StartTime":93835.0,"Position":280.52063,"HyperDash":false},{"StartTime":93891.0,"Position":248.606445,"HyperDash":false},{"StartTime":93947.0,"Position":235.53479,"HyperDash":false},{"StartTime":94003.0,"Position":224.107117,"HyperDash":false},{"StartTime":94059.0,"Position":224.84407,"HyperDash":false},{"StartTime":94115.0,"Position":200.017365,"HyperDash":false},{"StartTime":94171.0,"Position":199.067291,"HyperDash":false},{"StartTime":94227.0,"Position":212.384537,"HyperDash":false},{"StartTime":94283.0,"Position":199.112579,"HyperDash":false},{"StartTime":94339.0,"Position":222.5897,"HyperDash":false},{"StartTime":94395.0,"Position":253.0729,"HyperDash":false},{"StartTime":94451.0,"Position":253.947144,"HyperDash":false},{"StartTime":94507.0,"Position":271.304932,"HyperDash":false},{"StartTime":94563.0,"Position":305.412964,"HyperDash":false},{"StartTime":94619.0,"Position":307.6401,"HyperDash":false},{"StartTime":94675.0,"Position":267.302582,"HyperDash":false},{"StartTime":94731.0,"Position":251.416916,"HyperDash":false},{"StartTime":94787.0,"Position":222.898773,"HyperDash":false},{"StartTime":94843.0,"Position":211.3582,"HyperDash":false},{"StartTime":94899.0,"Position":213.529022,"HyperDash":false},{"StartTime":94955.0,"Position":210.1259,"HyperDash":false},{"StartTime":95011.0,"Position":199.942,"HyperDash":false},{"StartTime":95062.0,"Position":191.884583,"HyperDash":false},{"StartTime":95114.0,"Position":201.545059,"HyperDash":false},{"StartTime":95165.0,"Position":236.775665,"HyperDash":false},{"StartTime":95217.0,"Position":265.954834,"HyperDash":false},{"StartTime":95268.0,"Position":272.007324,"HyperDash":false},{"StartTime":95320.0,"Position":299.217743,"HyperDash":false},{"StartTime":95371.0,"Position":310.421265,"HyperDash":false},{"StartTime":95459.0,"Position":321.0,"HyperDash":false}]},{"StartTime":97250.0,"Objects":[{"StartTime":97250.0,"Position":321.0,"HyperDash":false},{"StartTime":97305.0,"Position":349.148315,"HyperDash":false},{"StartTime":97361.0,"Position":367.604675,"HyperDash":false},{"StartTime":97417.0,"Position":379.5581,"HyperDash":false},{"StartTime":97473.0,"Position":395.380951,"HyperDash":false},{"StartTime":97529.0,"Position":430.504242,"HyperDash":false},{"StartTime":97585.0,"Position":442.613251,"HyperDash":false},{"StartTime":97641.0,"Position":458.59317,"HyperDash":false},{"StartTime":97697.0,"Position":467.732544,"HyperDash":false},{"StartTime":97753.0,"Position":444.03418,"HyperDash":false},{"StartTime":97809.0,"Position":450.705536,"HyperDash":false},{"StartTime":97865.0,"Position":456.036621,"HyperDash":false},{"StartTime":97921.0,"Position":460.436,"HyperDash":false},{"StartTime":97977.0,"Position":445.266327,"HyperDash":false},{"StartTime":98033.0,"Position":456.866272,"HyperDash":false},{"StartTime":98089.0,"Position":449.4119,"HyperDash":false},{"StartTime":98145.0,"Position":462.917175,"HyperDash":false},{"StartTime":98201.0,"Position":468.532471,"HyperDash":false},{"StartTime":98257.0,"Position":451.935547,"HyperDash":false},{"StartTime":98313.0,"Position":433.2847,"HyperDash":false},{"StartTime":98369.0,"Position":426.406769,"HyperDash":false},{"StartTime":98425.0,"Position":449.975067,"HyperDash":false},{"StartTime":98481.0,"Position":460.606018,"HyperDash":false},{"StartTime":98537.0,"Position":447.910065,"HyperDash":false},{"StartTime":98593.0,"Position":467.586945,"HyperDash":false},{"StartTime":98644.0,"Position":441.353149,"HyperDash":false},{"StartTime":98696.0,"Position":439.723267,"HyperDash":false},{"StartTime":98747.0,"Position":415.4601,"HyperDash":false},{"StartTime":98799.0,"Position":412.9643,"HyperDash":false},{"StartTime":98850.0,"Position":398.1049,"HyperDash":false},{"StartTime":98902.0,"Position":376.557465,"HyperDash":false},{"StartTime":98953.0,"Position":359.5229,"HyperDash":false},{"StartTime":99041.0,"Position":321.0,"HyperDash":false}]},{"StartTime":100832.0,"Objects":[{"StartTime":100832.0,"Position":321.0,"HyperDash":false},{"StartTime":100887.0,"Position":321.469482,"HyperDash":false},{"StartTime":100943.0,"Position":295.742432,"HyperDash":false},{"StartTime":100999.0,"Position":267.1522,"HyperDash":false},{"StartTime":101055.0,"Position":261.835083,"HyperDash":false},{"StartTime":101111.0,"Position":216.475037,"HyperDash":false},{"StartTime":101167.0,"Position":227.328217,"HyperDash":false},{"StartTime":101223.0,"Position":189.1814,"HyperDash":false},{"StartTime":101279.0,"Position":176.034576,"HyperDash":false},{"StartTime":101335.0,"Position":138.745392,"HyperDash":false},{"StartTime":101391.0,"Position":148.387146,"HyperDash":false},{"StartTime":101447.0,"Position":103.028908,"HyperDash":false},{"StartTime":101503.0,"Position":107.670639,"HyperDash":false},{"StartTime":101559.0,"Position":73.03934,"HyperDash":false},{"StartTime":101615.0,"Position":45.58867,"HyperDash":false},{"StartTime":101671.0,"Position":34.2676964,"HyperDash":false},{"StartTime":101727.0,"Position":31.1845322,"HyperDash":false},{"StartTime":101783.0,"Position":59.98834,"HyperDash":false},{"StartTime":101839.0,"Position":63.2845459,"HyperDash":false},{"StartTime":101895.0,"Position":71.71911,"HyperDash":false},{"StartTime":101951.0,"Position":103.324966,"HyperDash":false},{"StartTime":102007.0,"Position":111.683212,"HyperDash":false},{"StartTime":102063.0,"Position":126.041473,"HyperDash":false},{"StartTime":102119.0,"Position":162.399689,"HyperDash":false},{"StartTime":102175.0,"Position":175.71051,"HyperDash":false},{"StartTime":102226.0,"Position":204.237076,"HyperDash":false},{"StartTime":102278.0,"Position":214.0877,"HyperDash":false},{"StartTime":102329.0,"Position":210.614273,"HyperDash":false},{"StartTime":102381.0,"Position":249.4649,"HyperDash":false},{"StartTime":102432.0,"Position":251.954224,"HyperDash":false},{"StartTime":102484.0,"Position":265.549072,"HyperDash":false},{"StartTime":102535.0,"Position":284.1342,"HyperDash":false},{"StartTime":102623.0,"Position":321.0,"HyperDash":false}]},{"StartTime":102847.0,"Objects":[{"StartTime":102847.0,"Position":385.0,"HyperDash":false}]},{"StartTime":103071.0,"Objects":[{"StartTime":103071.0,"Position":322.0,"HyperDash":false},{"StartTime":103154.0,"Position":309.4082,"HyperDash":false},{"StartTime":103238.0,"Position":253.459869,"HyperDash":false},{"StartTime":103322.0,"Position":230.511536,"HyperDash":false},{"StartTime":103406.0,"Position":202.384949,"HyperDash":false},{"StartTime":103481.0,"Position":227.946259,"HyperDash":false},{"StartTime":103556.0,"Position":269.685852,"HyperDash":false},{"StartTime":103631.0,"Position":282.4254,"HyperDash":false},{"StartTime":103742.0,"Position":322.0,"HyperDash":false}]},{"StartTime":103966.0,"Objects":[{"StartTime":103966.0,"Position":404.0,"HyperDash":false},{"StartTime":104059.0,"Position":389.203644,"HyperDash":false},{"StartTime":104189.0,"Position":389.111877,"HyperDash":false}]},{"StartTime":104414.0,"Objects":[{"StartTime":104414.0,"Position":308.0,"HyperDash":false},{"StartTime":104507.0,"Position":288.7421,"HyperDash":false},{"StartTime":104637.0,"Position":230.940414,"HyperDash":false}]},{"StartTime":104862.0,"Objects":[{"StartTime":104862.0,"Position":164.0,"HyperDash":false},{"StartTime":104945.0,"Position":150.511658,"HyperDash":false},{"StartTime":105029.0,"Position":96.6680145,"HyperDash":false},{"StartTime":105113.0,"Position":58.8243866,"HyperDash":false},{"StartTime":105197.0,"Position":44.8031158,"HyperDash":false},{"StartTime":105272.0,"Position":73.2715759,"HyperDash":false},{"StartTime":105347.0,"Position":112.917679,"HyperDash":false},{"StartTime":105422.0,"Position":127.563766,"HyperDash":false},{"StartTime":105533.0,"Position":164.0,"HyperDash":false}]},{"StartTime":105757.0,"Objects":[{"StartTime":105757.0,"Position":369.0,"HyperDash":false}]},{"StartTime":106205.0,"Objects":[{"StartTime":106205.0,"Position":276.0,"HyperDash":false},{"StartTime":106260.0,"Position":301.5404,"HyperDash":false},{"StartTime":106316.0,"Position":299.28067,"HyperDash":false},{"StartTime":106372.0,"Position":337.27,"HyperDash":false},{"StartTime":106428.0,"Position":348.8408,"HyperDash":false},{"StartTime":106484.0,"Position":372.279419,"HyperDash":false},{"StartTime":106540.0,"Position":407.057281,"HyperDash":false},{"StartTime":106596.0,"Position":399.472778,"HyperDash":false},{"StartTime":106652.0,"Position":415.2087,"HyperDash":false},{"StartTime":106746.0,"Position":444.522675,"HyperDash":false},{"StartTime":106876.0,"Position":427.9771,"HyperDash":false}]},{"StartTime":107101.0,"Objects":[{"StartTime":107101.0,"Position":354.0,"HyperDash":false},{"StartTime":107156.0,"Position":351.361053,"HyperDash":false},{"StartTime":107212.0,"Position":319.711761,"HyperDash":false},{"StartTime":107268.0,"Position":312.725647,"HyperDash":false},{"StartTime":107324.0,"Position":292.795166,"HyperDash":false},{"StartTime":107380.0,"Position":257.278931,"HyperDash":false},{"StartTime":107436.0,"Position":250.434189,"HyperDash":false},{"StartTime":107492.0,"Position":228.3952,"HyperDash":false},{"StartTime":107548.0,"Position":202.1942,"HyperDash":false},{"StartTime":107642.0,"Position":183.650848,"HyperDash":false},{"StartTime":107772.0,"Position":130.2209,"HyperDash":false}]},{"StartTime":107996.0,"Objects":[{"StartTime":107996.0,"Position":55.0,"HyperDash":false}]},{"StartTime":108220.0,"Objects":[{"StartTime":108220.0,"Position":0.0,"HyperDash":false}]},{"StartTime":108444.0,"Objects":[{"StartTime":108444.0,"Position":43.0,"HyperDash":false},{"StartTime":108527.0,"Position":26.517498,"HyperDash":false},{"StartTime":108611.0,"Position":31.01714,"HyperDash":false},{"StartTime":108695.0,"Position":26.516777,"HyperDash":false},{"StartTime":108779.0,"Position":37.0074844,"HyperDash":false},{"StartTime":108854.0,"Position":40.33816,"HyperDash":false},{"StartTime":108929.0,"Position":23.6777725,"HyperDash":false},{"StartTime":109004.0,"Position":46.01738,"HyperDash":false},{"StartTime":109115.0,"Position":43.0,"HyperDash":false}]},{"StartTime":109339.0,"Objects":[{"StartTime":109339.0,"Position":128.0,"HyperDash":false},{"StartTime":109432.0,"Position":177.210678,"HyperDash":false},{"StartTime":109562.0,"Position":204.080414,"HyperDash":false}]},{"StartTime":109787.0,"Objects":[{"StartTime":109787.0,"Position":242.0,"HyperDash":false},{"StartTime":109842.0,"Position":213.635727,"HyperDash":false},{"StartTime":109898.0,"Position":229.2922,"HyperDash":false},{"StartTime":109954.0,"Position":229.500839,"HyperDash":false},{"StartTime":110010.0,"Position":245.173721,"HyperDash":false},{"StartTime":110066.0,"Position":240.366425,"HyperDash":false},{"StartTime":110122.0,"Position":243.8476,"HyperDash":false},{"StartTime":110178.0,"Position":253.385529,"HyperDash":false},{"StartTime":110234.0,"Position":267.757416,"HyperDash":false},{"StartTime":110285.0,"Position":252.804428,"HyperDash":false},{"StartTime":110337.0,"Position":250.689026,"HyperDash":false},{"StartTime":110388.0,"Position":223.27919,"HyperDash":false},{"StartTime":110440.0,"Position":223.56842,"HyperDash":false},{"StartTime":110491.0,"Position":243.800873,"HyperDash":false},{"StartTime":110543.0,"Position":223.941116,"HyperDash":false},{"StartTime":110594.0,"Position":226.059952,"HyperDash":false},{"StartTime":110682.0,"Position":242.0,"HyperDash":false}]},{"StartTime":111131.0,"Objects":[{"StartTime":111131.0,"Position":411.0,"HyperDash":false}]},{"StartTime":111578.0,"Objects":[{"StartTime":111578.0,"Position":503.0,"HyperDash":false},{"StartTime":111629.0,"Position":490.995636,"HyperDash":false},{"StartTime":111680.0,"Position":478.9913,"HyperDash":false},{"StartTime":111732.0,"Position":511.947632,"HyperDash":false},{"StartTime":111783.0,"Position":502.9433,"HyperDash":false},{"StartTime":111834.0,"Position":488.938934,"HyperDash":false},{"StartTime":111886.0,"Position":497.8953,"HyperDash":false},{"StartTime":111937.0,"Position":485.89093,"HyperDash":false},{"StartTime":112025.0,"Position":485.432434,"HyperDash":false}]},{"StartTime":112250.0,"Objects":[{"StartTime":112250.0,"Position":326.0,"HyperDash":false}]},{"StartTime":112474.0,"Objects":[{"StartTime":112474.0,"Position":333.0,"HyperDash":false},{"StartTime":112567.0,"Position":318.79068,"HyperDash":false},{"StartTime":112697.0,"Position":253.369049,"HyperDash":false}]},{"StartTime":112922.0,"Objects":[{"StartTime":112922.0,"Position":175.0,"HyperDash":false},{"StartTime":113015.0,"Position":142.79068,"HyperDash":false},{"StartTime":113145.0,"Position":95.36904,"HyperDash":false}]},{"StartTime":113369.0,"Objects":[{"StartTime":113369.0,"Position":28.0,"HyperDash":false},{"StartTime":113424.0,"Position":14.5683556,"HyperDash":false},{"StartTime":113480.0,"Position":0.0,"HyperDash":false},{"StartTime":113536.0,"Position":28.3534565,"HyperDash":false},{"StartTime":113592.0,"Position":8.926472,"HyperDash":false},{"StartTime":113648.0,"Position":14.8988361,"HyperDash":false},{"StartTime":113704.0,"Position":13.3887749,"HyperDash":false},{"StartTime":113760.0,"Position":28.1702747,"HyperDash":false},{"StartTime":113816.0,"Position":34.34165,"HyperDash":false},{"StartTime":113867.0,"Position":36.0318,"HyperDash":false},{"StartTime":113919.0,"Position":12.4058609,"HyperDash":false},{"StartTime":113970.0,"Position":10.89321,"HyperDash":false},{"StartTime":114022.0,"Position":0.0,"HyperDash":false},{"StartTime":114073.0,"Position":10.8660927,"HyperDash":false},{"StartTime":114125.0,"Position":28.46455,"HyperDash":false},{"StartTime":114176.0,"Position":10.1406345,"HyperDash":false},{"StartTime":114264.0,"Position":28.0,"HyperDash":false}]},{"StartTime":114713.0,"Objects":[{"StartTime":114713.0,"Position":190.0,"HyperDash":false}]},{"StartTime":115160.0,"Objects":[{"StartTime":115160.0,"Position":349.0,"HyperDash":false},{"StartTime":115215.0,"Position":385.515045,"HyperDash":false},{"StartTime":115271.0,"Position":399.481323,"HyperDash":false},{"StartTime":115327.0,"Position":411.652283,"HyperDash":false},{"StartTime":115383.0,"Position":433.181549,"HyperDash":false},{"StartTime":115439.0,"Position":451.6266,"HyperDash":false},{"StartTime":115495.0,"Position":475.6881,"HyperDash":false},{"StartTime":115551.0,"Position":468.64,"HyperDash":false},{"StartTime":115607.0,"Position":501.696655,"HyperDash":false},{"StartTime":115658.0,"Position":478.782867,"HyperDash":false},{"StartTime":115710.0,"Position":452.2243,"HyperDash":false},{"StartTime":115761.0,"Position":461.569977,"HyperDash":false},{"StartTime":115813.0,"Position":418.9793,"HyperDash":false},{"StartTime":115864.0,"Position":433.325836,"HyperDash":false},{"StartTime":115916.0,"Position":398.248627,"HyperDash":false},{"StartTime":115967.0,"Position":379.308319,"HyperDash":false},{"StartTime":116055.0,"Position":349.0,"HyperDash":false}]},{"StartTime":116280.0,"Objects":[{"StartTime":116280.0,"Position":265.0,"HyperDash":false}]},{"StartTime":116504.0,"Objects":[{"StartTime":116504.0,"Position":224.0,"HyperDash":false},{"StartTime":116597.0,"Position":239.949112,"HyperDash":false},{"StartTime":116727.0,"Position":235.867233,"HyperDash":false}]},{"StartTime":116951.0,"Objects":[{"StartTime":116951.0,"Position":320.0,"HyperDash":false},{"StartTime":117002.0,"Position":342.006653,"HyperDash":false},{"StartTime":117053.0,"Position":362.0133,"HyperDash":false},{"StartTime":117105.0,"Position":374.373047,"HyperDash":false},{"StartTime":117156.0,"Position":403.3797,"HyperDash":false},{"StartTime":117207.0,"Position":400.386353,"HyperDash":false},{"StartTime":117259.0,"Position":441.7282,"HyperDash":false},{"StartTime":117310.0,"Position":459.303955,"HyperDash":false},{"StartTime":117398.0,"Position":476.6307,"HyperDash":false}]},{"StartTime":117847.0,"Objects":[{"StartTime":117847.0,"Position":501.0,"HyperDash":false},{"StartTime":117898.0,"Position":485.993347,"HyperDash":false},{"StartTime":117949.0,"Position":475.9867,"HyperDash":false},{"StartTime":118001.0,"Position":440.626953,"HyperDash":false},{"StartTime":118052.0,"Position":413.6203,"HyperDash":false},{"StartTime":118103.0,"Position":415.613647,"HyperDash":false},{"StartTime":118155.0,"Position":403.2718,"HyperDash":false},{"StartTime":118206.0,"Position":374.696045,"HyperDash":false},{"StartTime":118294.0,"Position":344.3693,"HyperDash":false}]},{"StartTime":118742.0,"Objects":[{"StartTime":118742.0,"Position":200.0,"HyperDash":false},{"StartTime":118793.0,"Position":169.013748,"HyperDash":false},{"StartTime":118844.0,"Position":149.781937,"HyperDash":false},{"StartTime":118896.0,"Position":136.378891,"HyperDash":false},{"StartTime":118947.0,"Position":111.942886,"HyperDash":false},{"StartTime":118998.0,"Position":96.68911,"HyperDash":false},{"StartTime":119050.0,"Position":81.5406,"HyperDash":false},{"StartTime":119101.0,"Position":83.7234955,"HyperDash":false},{"StartTime":119189.0,"Position":45.337368,"HyperDash":false}]},{"StartTime":119638.0,"Objects":[{"StartTime":119638.0,"Position":16.0,"HyperDash":false},{"StartTime":119721.0,"Position":11.22777,"HyperDash":false},{"StartTime":119805.0,"Position":40.49443,"HyperDash":false},{"StartTime":119889.0,"Position":35.76109,"HyperDash":false},{"StartTime":119973.0,"Position":29.0471916,"HyperDash":false},{"StartTime":120048.0,"Position":34.1499748,"HyperDash":false},{"StartTime":120123.0,"Position":6.23331642,"HyperDash":false},{"StartTime":120198.0,"Position":34.316658,"HyperDash":false},{"StartTime":120309.0,"Position":16.0,"HyperDash":false}]},{"StartTime":120534.0,"Objects":[{"StartTime":120534.0,"Position":88.0,"HyperDash":false},{"StartTime":120589.0,"Position":99.09209,"HyperDash":false},{"StartTime":120645.0,"Position":138.513123,"HyperDash":false},{"StartTime":120701.0,"Position":128.008957,"HyperDash":false},{"StartTime":120757.0,"Position":153.9049,"HyperDash":false},{"StartTime":120813.0,"Position":191.800842,"HyperDash":false},{"StartTime":120869.0,"Position":199.696777,"HyperDash":false},{"StartTime":120925.0,"Position":239.592712,"HyperDash":false},{"StartTime":120981.0,"Position":242.66629,"HyperDash":false},{"StartTime":121032.0,"Position":217.724426,"HyperDash":false},{"StartTime":121084.0,"Position":218.249634,"HyperDash":false},{"StartTime":121135.0,"Position":197.130127,"HyperDash":false},{"StartTime":121187.0,"Position":167.6553,"HyperDash":false},{"StartTime":121238.0,"Position":142.5358,"HyperDash":false},{"StartTime":121290.0,"Position":147.723633,"HyperDash":false},{"StartTime":121341.0,"Position":129.947342,"HyperDash":false},{"StartTime":121429.0,"Position":88.0,"HyperDash":false}]},{"StartTime":121877.0,"Objects":[{"StartTime":121877.0,"Position":172.0,"HyperDash":false}]},{"StartTime":122325.0,"Objects":[{"StartTime":122325.0,"Position":322.0,"HyperDash":false},{"StartTime":122376.0,"Position":324.2495,"HyperDash":false},{"StartTime":122427.0,"Position":355.02713,"HyperDash":false},{"StartTime":122479.0,"Position":331.196777,"HyperDash":false},{"StartTime":122530.0,"Position":366.3613,"HyperDash":false},{"StartTime":122581.0,"Position":338.580322,"HyperDash":false},{"StartTime":122633.0,"Position":352.663971,"HyperDash":false},{"StartTime":122684.0,"Position":329.8923,"HyperDash":false},{"StartTime":122772.0,"Position":326.6841,"HyperDash":false}]},{"StartTime":123220.0,"Objects":[{"StartTime":123220.0,"Position":150.0,"HyperDash":false},{"StartTime":123271.0,"Position":143.7505,"HyperDash":false},{"StartTime":123322.0,"Position":113.97287,"HyperDash":false},{"StartTime":123374.0,"Position":108.803215,"HyperDash":false},{"StartTime":123425.0,"Position":131.6387,"HyperDash":false},{"StartTime":123476.0,"Position":132.419678,"HyperDash":false},{"StartTime":123528.0,"Position":126.336021,"HyperDash":false},{"StartTime":123579.0,"Position":126.1077,"HyperDash":false},{"StartTime":123667.0,"Position":145.315887,"HyperDash":false}]},{"StartTime":123892.0,"Objects":[{"StartTime":123892.0,"Position":238.0,"HyperDash":false}]},{"StartTime":124116.0,"Objects":[{"StartTime":124116.0,"Position":277.0,"HyperDash":false},{"StartTime":124171.0,"Position":313.7125,"HyperDash":false},{"StartTime":124227.0,"Position":317.3544,"HyperDash":false},{"StartTime":124283.0,"Position":342.320557,"HyperDash":false},{"StartTime":124339.0,"Position":372.249237,"HyperDash":false},{"StartTime":124395.0,"Position":379.760651,"HyperDash":false},{"StartTime":124451.0,"Position":399.5314,"HyperDash":false},{"StartTime":124507.0,"Position":411.37323,"HyperDash":false},{"StartTime":124563.0,"Position":421.66275,"HyperDash":false},{"StartTime":124619.0,"Position":395.6441,"HyperDash":false},{"StartTime":124675.0,"Position":426.7424,"HyperDash":false},{"StartTime":124731.0,"Position":411.428467,"HyperDash":false},{"StartTime":124787.0,"Position":416.085144,"HyperDash":false},{"StartTime":124843.0,"Position":392.878662,"HyperDash":false},{"StartTime":124899.0,"Position":418.688171,"HyperDash":false},{"StartTime":124955.0,"Position":407.149261,"HyperDash":false},{"StartTime":125011.0,"Position":431.788025,"HyperDash":false},{"StartTime":125062.0,"Position":407.509033,"HyperDash":false},{"StartTime":125114.0,"Position":382.834656,"HyperDash":false},{"StartTime":125165.0,"Position":397.090271,"HyperDash":false},{"StartTime":125217.0,"Position":376.698334,"HyperDash":false},{"StartTime":125268.0,"Position":346.500427,"HyperDash":false},{"StartTime":125320.0,"Position":337.9348,"HyperDash":false},{"StartTime":125371.0,"Position":290.849426,"HyperDash":false},{"StartTime":125459.0,"Position":276.2673,"HyperDash":false}]},{"StartTime":125907.0,"Objects":[{"StartTime":125907.0,"Position":121.0,"HyperDash":false},{"StartTime":125990.0,"Position":113.836914,"HyperDash":false},{"StartTime":126074.0,"Position":142.708008,"HyperDash":false},{"StartTime":126158.0,"Position":144.5791,"HyperDash":false},{"StartTime":126242.0,"Position":132.467285,"HyperDash":false},{"StartTime":126317.0,"Position":121.9209,"HyperDash":false},{"StartTime":126392.0,"Position":135.357422,"HyperDash":false},{"StartTime":126467.0,"Position":109.793945,"HyperDash":false},{"StartTime":126578.0,"Position":121.0,"HyperDash":false}]},{"StartTime":126802.0,"Objects":[{"StartTime":126802.0,"Position":75.0,"HyperDash":false}]},{"StartTime":127026.0,"Objects":[{"StartTime":127026.0,"Position":88.0,"HyperDash":false},{"StartTime":127081.0,"Position":65.86594,"HyperDash":false},{"StartTime":127137.0,"Position":35.8985558,"HyperDash":false},{"StartTime":127193.0,"Position":35.9736977,"HyperDash":false},{"StartTime":127249.0,"Position":9.451545,"HyperDash":false},{"StartTime":127343.0,"Position":22.10696,"HyperDash":false},{"StartTime":127473.0,"Position":88.0,"HyperDash":false}]},{"StartTime":127698.0,"Objects":[{"StartTime":127698.0,"Position":171.0,"HyperDash":false},{"StartTime":127791.0,"Position":186.182022,"HyperDash":false},{"StartTime":127921.0,"Position":250.565491,"HyperDash":false}]},{"StartTime":128145.0,"Objects":[{"StartTime":128145.0,"Position":333.0,"HyperDash":false},{"StartTime":128228.0,"Position":360.710541,"HyperDash":false},{"StartTime":128312.0,"Position":382.321838,"HyperDash":false},{"StartTime":128396.0,"Position":416.1943,"HyperDash":false},{"StartTime":128480.0,"Position":447.073883,"HyperDash":false},{"StartTime":128555.0,"Position":406.767181,"HyperDash":false},{"StartTime":128630.0,"Position":392.017242,"HyperDash":false},{"StartTime":128705.0,"Position":359.007629,"HyperDash":false},{"StartTime":128816.0,"Position":333.0,"HyperDash":false}]},{"StartTime":129041.0,"Objects":[{"StartTime":129041.0,"Position":318.0,"HyperDash":false},{"StartTime":129134.0,"Position":308.0215,"HyperDash":false},{"StartTime":129264.0,"Position":313.2559,"HyperDash":false}]},{"StartTime":129489.0,"Objects":[{"StartTime":129489.0,"Position":304.0,"HyperDash":false},{"StartTime":129540.0,"Position":336.395416,"HyperDash":false},{"StartTime":129591.0,"Position":329.216034,"HyperDash":false},{"StartTime":129643.0,"Position":344.559479,"HyperDash":false},{"StartTime":129694.0,"Position":350.5508,"HyperDash":false},{"StartTime":129745.0,"Position":370.245239,"HyperDash":false},{"StartTime":129797.0,"Position":405.714478,"HyperDash":false},{"StartTime":129848.0,"Position":411.953125,"HyperDash":false},{"StartTime":129936.0,"Position":450.890564,"HyperDash":false}]},{"StartTime":130160.0,"Objects":[{"StartTime":130160.0,"Position":506.0,"HyperDash":false},{"StartTime":130211.0,"Position":502.234955,"HyperDash":false},{"StartTime":130262.0,"Position":491.46994,"HyperDash":false},{"StartTime":130314.0,"Position":497.6703,"HyperDash":false},{"StartTime":130365.0,"Position":512.0,"HyperDash":false},{"StartTime":130416.0,"Position":496.1402,"HyperDash":false},{"StartTime":130468.0,"Position":479.340546,"HyperDash":false},{"StartTime":130519.0,"Position":512.0,"HyperDash":false},{"StartTime":130607.0,"Position":490.529968,"HyperDash":false}]},{"StartTime":130832.0,"Objects":[{"StartTime":130832.0,"Position":477.0,"HyperDash":false}]},{"StartTime":131280.0,"Objects":[{"StartTime":131280.0,"Position":308.0,"HyperDash":false},{"StartTime":131373.0,"Position":272.2126,"HyperDash":false},{"StartTime":131503.0,"Position":230.958725,"HyperDash":false}]},{"StartTime":131728.0,"Objects":[{"StartTime":131728.0,"Position":142.0,"HyperDash":false},{"StartTime":131811.0,"Position":136.278381,"HyperDash":false},{"StartTime":131895.0,"Position":128.596268,"HyperDash":false},{"StartTime":131979.0,"Position":142.914154,"HyperDash":false},{"StartTime":132063.0,"Position":155.251785,"HyperDash":false},{"StartTime":132138.0,"Position":138.309143,"HyperDash":false},{"StartTime":132213.0,"Position":167.346741,"HyperDash":false},{"StartTime":132288.0,"Position":142.384354,"HyperDash":false},{"StartTime":132399.0,"Position":142.0,"HyperDash":false}]},{"StartTime":132623.0,"Objects":[{"StartTime":132623.0,"Position":55.0,"HyperDash":false},{"StartTime":132716.0,"Position":62.0249329,"HyperDash":false},{"StartTime":132846.0,"Position":45.4683838,"HyperDash":false}]},{"StartTime":133071.0,"Objects":[{"StartTime":133071.0,"Position":33.0,"HyperDash":false},{"StartTime":133122.0,"Position":34.36902,"HyperDash":false},{"StartTime":133173.0,"Position":49.2077179,"HyperDash":false},{"StartTime":133225.0,"Position":74.56708,"HyperDash":false},{"StartTime":133276.0,"Position":53.8811874,"HyperDash":false},{"StartTime":133327.0,"Position":100.066032,"HyperDash":false},{"StartTime":133379.0,"Position":104.080582,"HyperDash":false},{"StartTime":133430.0,"Position":129.9765,"HyperDash":false},{"StartTime":133518.0,"Position":146.874619,"HyperDash":false}]},{"StartTime":133966.0,"Objects":[{"StartTime":133966.0,"Position":275.0,"HyperDash":false},{"StartTime":134059.0,"Position":303.328827,"HyperDash":false},{"StartTime":134189.0,"Position":354.91748,"HyperDash":false}]},{"StartTime":134414.0,"Objects":[{"StartTime":134414.0,"Position":389.0,"HyperDash":false},{"StartTime":134507.0,"Position":407.328827,"HyperDash":false},{"StartTime":134637.0,"Position":468.91748,"HyperDash":false}]},{"StartTime":134862.0,"Objects":[{"StartTime":134862.0,"Position":503.0,"HyperDash":false},{"StartTime":134913.0,"Position":500.255981,"HyperDash":false},{"StartTime":134964.0,"Position":512.0,"HyperDash":false},{"StartTime":135016.0,"Position":512.0,"HyperDash":false},{"StartTime":135067.0,"Position":510.048553,"HyperDash":false},{"StartTime":135118.0,"Position":501.304535,"HyperDash":false},{"StartTime":135170.0,"Position":512.0,"HyperDash":false},{"StartTime":135221.0,"Position":497.906158,"HyperDash":false},{"StartTime":135309.0,"Position":492.781982,"HyperDash":false}]},{"StartTime":135757.0,"Objects":[{"StartTime":135757.0,"Position":318.0,"HyperDash":false},{"StartTime":135850.0,"Position":298.671173,"HyperDash":false},{"StartTime":135980.0,"Position":238.08252,"HyperDash":false}]},{"StartTime":136205.0,"Objects":[{"StartTime":136205.0,"Position":204.0,"HyperDash":false},{"StartTime":136298.0,"Position":187.671188,"HyperDash":false},{"StartTime":136428.0,"Position":124.082512,"HyperDash":false}]},{"StartTime":136653.0,"Objects":[{"StartTime":136653.0,"Position":49.0,"HyperDash":false},{"StartTime":136704.0,"Position":21.2460976,"HyperDash":false},{"StartTime":136755.0,"Position":43.23652,"HyperDash":false},{"StartTime":136807.0,"Position":42.04418,"HyperDash":false},{"StartTime":136858.0,"Position":2.98967361,"HyperDash":false},{"StartTime":136909.0,"Position":20.9194527,"HyperDash":false},{"StartTime":136961.0,"Position":10.7384281,"HyperDash":false},{"StartTime":137012.0,"Position":13.6708527,"HyperDash":false},{"StartTime":137100.0,"Position":38.2821579,"HyperDash":false}]},{"StartTime":137548.0,"Objects":[{"StartTime":137548.0,"Position":200.0,"HyperDash":false},{"StartTime":137641.0,"Position":223.082932,"HyperDash":false},{"StartTime":137771.0,"Position":220.570145,"HyperDash":false}]},{"StartTime":137996.0,"Objects":[{"StartTime":137996.0,"Position":204.0,"HyperDash":false},{"StartTime":138089.0,"Position":193.917068,"HyperDash":false},{"StartTime":138219.0,"Position":183.429855,"HyperDash":false}]},{"StartTime":138444.0,"Objects":[{"StartTime":138444.0,"Position":270.0,"HyperDash":false},{"StartTime":138495.0,"Position":302.4524,"HyperDash":false},{"StartTime":138546.0,"Position":317.9048,"HyperDash":false},{"StartTime":138598.0,"Position":317.679779,"HyperDash":false},{"StartTime":138649.0,"Position":319.346863,"HyperDash":false},{"StartTime":138700.0,"Position":371.213379,"HyperDash":false},{"StartTime":138752.0,"Position":387.4302,"HyperDash":false},{"StartTime":138803.0,"Position":406.2967,"HyperDash":false},{"StartTime":138891.0,"Position":422.1252,"HyperDash":false}]},{"StartTime":139116.0,"Objects":[{"StartTime":139116.0,"Position":490.0,"HyperDash":false}]},{"StartTime":139339.0,"Objects":[{"StartTime":139339.0,"Position":504.0,"HyperDash":false},{"StartTime":139432.0,"Position":500.723053,"HyperDash":false},{"StartTime":139562.0,"Position":490.562256,"HyperDash":false}]},{"StartTime":139787.0,"Objects":[{"StartTime":139787.0,"Position":370.0,"HyperDash":false}]},{"StartTime":140235.0,"Objects":[{"StartTime":140235.0,"Position":268.0,"HyperDash":false},{"StartTime":140318.0,"Position":268.7403,"HyperDash":false},{"StartTime":140402.0,"Position":257.822449,"HyperDash":false},{"StartTime":140486.0,"Position":262.227783,"HyperDash":false},{"StartTime":140570.0,"Position":276.9804,"HyperDash":false},{"StartTime":140645.0,"Position":293.53894,"HyperDash":false},{"StartTime":140720.0,"Position":260.337555,"HyperDash":false},{"StartTime":140795.0,"Position":257.3968,"HyperDash":false},{"StartTime":140906.0,"Position":268.0,"HyperDash":false}]},{"StartTime":141131.0,"Objects":[{"StartTime":141131.0,"Position":207.0,"HyperDash":false},{"StartTime":141224.0,"Position":178.663437,"HyperDash":false},{"StartTime":141354.0,"Position":127.063927,"HyperDash":false}]},{"StartTime":141578.0,"Objects":[{"StartTime":141578.0,"Position":39.0,"HyperDash":false}]},{"StartTime":141802.0,"Objects":[{"StartTime":141802.0,"Position":8.0,"HyperDash":false}]},{"StartTime":142026.0,"Objects":[{"StartTime":142026.0,"Position":71.0,"HyperDash":false},{"StartTime":142119.0,"Position":106.114151,"HyperDash":false},{"StartTime":142249.0,"Position":149.484,"HyperDash":false}]},{"StartTime":142474.0,"Objects":[{"StartTime":142474.0,"Position":220.0,"HyperDash":false},{"StartTime":142557.0,"Position":238.606583,"HyperDash":false},{"StartTime":142641.0,"Position":276.5699,"HyperDash":false},{"StartTime":142725.0,"Position":317.533142,"HyperDash":false},{"StartTime":142809.0,"Position":339.6748,"HyperDash":false},{"StartTime":142884.0,"Position":301.10022,"HyperDash":false},{"StartTime":142959.0,"Position":303.3473,"HyperDash":false},{"StartTime":143034.0,"Position":253.59436,"HyperDash":false},{"StartTime":143145.0,"Position":220.0,"HyperDash":false}]},{"StartTime":143369.0,"Objects":[{"StartTime":143369.0,"Position":158.0,"HyperDash":false},{"StartTime":143462.0,"Position":168.4163,"HyperDash":false},{"StartTime":143592.0,"Position":155.389526,"HyperDash":false}]},{"StartTime":143817.0,"Objects":[{"StartTime":143817.0,"Position":192.0,"HyperDash":false},{"StartTime":143868.0,"Position":227.725708,"HyperDash":false},{"StartTime":143919.0,"Position":234.856445,"HyperDash":false},{"StartTime":143971.0,"Position":256.358948,"HyperDash":false},{"StartTime":144022.0,"Position":248.750854,"HyperDash":false},{"StartTime":144073.0,"Position":277.396729,"HyperDash":false},{"StartTime":144125.0,"Position":293.7474,"HyperDash":false},{"StartTime":144176.0,"Position":303.68158,"HyperDash":false},{"StartTime":144264.0,"Position":346.96463,"HyperDash":false}]},{"StartTime":144489.0,"Objects":[{"StartTime":144489.0,"Position":431.0,"HyperDash":false},{"StartTime":144540.0,"Position":448.9708,"HyperDash":false},{"StartTime":144591.0,"Position":446.9416,"HyperDash":false},{"StartTime":144643.0,"Position":435.9314,"HyperDash":false},{"StartTime":144694.0,"Position":443.9022,"HyperDash":false},{"StartTime":144745.0,"Position":416.873,"HyperDash":false},{"StartTime":144797.0,"Position":434.8628,"HyperDash":false},{"StartTime":144848.0,"Position":447.8336,"HyperDash":false},{"StartTime":144936.0,"Position":439.508667,"HyperDash":false}]},{"StartTime":145160.0,"Objects":[{"StartTime":145160.0,"Position":456.0,"HyperDash":false}]},{"StartTime":145608.0,"Objects":[{"StartTime":145608.0,"Position":272.0,"HyperDash":false},{"StartTime":145701.0,"Position":244.790558,"HyperDash":false},{"StartTime":145831.0,"Position":193.216751,"HyperDash":false}]},{"StartTime":146056.0,"Objects":[{"StartTime":146056.0,"Position":127.0,"HyperDash":false},{"StartTime":146139.0,"Position":105.417236,"HyperDash":false},{"StartTime":146223.0,"Position":79.47805,"HyperDash":false},{"StartTime":146307.0,"Position":53.5388641,"HyperDash":false},{"StartTime":146391.0,"Position":7.421463,"HyperDash":false},{"StartTime":146466.0,"Position":38.974678,"HyperDash":false},{"StartTime":146541.0,"Position":69.7061,"HyperDash":false},{"StartTime":146616.0,"Position":94.4375,"HyperDash":false},{"StartTime":146727.0,"Position":127.0,"HyperDash":false}]},{"StartTime":146951.0,"Objects":[{"StartTime":146951.0,"Position":193.0,"HyperDash":false},{"StartTime":147044.0,"Position":209.412018,"HyperDash":false},{"StartTime":147174.0,"Position":186.467926,"HyperDash":false}]},{"StartTime":147399.0,"Objects":[{"StartTime":147399.0,"Position":109.0,"HyperDash":false},{"StartTime":147450.0,"Position":145.151154,"HyperDash":false},{"StartTime":147501.0,"Position":151.302292,"HyperDash":false},{"StartTime":147553.0,"Position":163.809341,"HyperDash":false},{"StartTime":147604.0,"Position":170.440277,"HyperDash":false},{"StartTime":147655.0,"Position":216.034668,"HyperDash":false},{"StartTime":147707.0,"Position":210.1249,"HyperDash":false},{"StartTime":147758.0,"Position":230.252777,"HyperDash":false},{"StartTime":147846.0,"Position":266.5323,"HyperDash":false}]},{"StartTime":148295.0,"Objects":[{"StartTime":148295.0,"Position":441.0,"HyperDash":false},{"StartTime":148388.0,"Position":425.532318,"HyperDash":false},{"StartTime":148518.0,"Position":444.6743,"HyperDash":false}]},{"StartTime":148742.0,"Objects":[{"StartTime":148742.0,"Position":482.0,"HyperDash":false},{"StartTime":148835.0,"Position":486.467682,"HyperDash":false},{"StartTime":148965.0,"Position":478.3257,"HyperDash":false}]},{"StartTime":149190.0,"Objects":[{"StartTime":149190.0,"Position":390.0,"HyperDash":false},{"StartTime":149241.0,"Position":390.926971,"HyperDash":false},{"StartTime":149292.0,"Position":346.853943,"HyperDash":false},{"StartTime":149344.0,"Position":355.206665,"HyperDash":false},{"StartTime":149395.0,"Position":318.011047,"HyperDash":false},{"StartTime":149446.0,"Position":311.81546,"HyperDash":false},{"StartTime":149498.0,"Position":296.263062,"HyperDash":false},{"StartTime":149549.0,"Position":268.067444,"HyperDash":false},{"StartTime":149637.0,"Position":235.671082,"HyperDash":false}]},{"StartTime":150086.0,"Objects":[{"StartTime":150086.0,"Position":59.0,"HyperDash":false},{"StartTime":150179.0,"Position":44.27435,"HyperDash":false},{"StartTime":150309.0,"Position":42.77816,"HyperDash":false}]},{"StartTime":150534.0,"Objects":[{"StartTime":150534.0,"Position":94.0,"HyperDash":false},{"StartTime":150627.0,"Position":87.7256546,"HyperDash":false},{"StartTime":150757.0,"Position":110.221848,"HyperDash":false}]},{"StartTime":150981.0,"Objects":[{"StartTime":150981.0,"Position":42.0,"HyperDash":false},{"StartTime":151032.0,"Position":70.85617,"HyperDash":false},{"StartTime":151083.0,"Position":55.8612671,"HyperDash":false},{"StartTime":151135.0,"Position":104.001328,"HyperDash":false},{"StartTime":151186.0,"Position":120.188065,"HyperDash":false},{"StartTime":151237.0,"Position":126.371735,"HyperDash":false},{"StartTime":151289.0,"Position":155.4776,"HyperDash":false},{"StartTime":151340.0,"Position":163.413391,"HyperDash":false},{"StartTime":151428.0,"Position":190.731277,"HyperDash":false}]},{"StartTime":151653.0,"Objects":[{"StartTime":151653.0,"Position":324.0,"HyperDash":false}]},{"StartTime":151877.0,"Objects":[{"StartTime":151877.0,"Position":335.0,"HyperDash":false},{"StartTime":151970.0,"Position":335.9098,"HyperDash":false},{"StartTime":152100.0,"Position":327.590118,"HyperDash":false}]},{"StartTime":152325.0,"Objects":[{"StartTime":152325.0,"Position":264.0,"HyperDash":false},{"StartTime":152418.0,"Position":284.0902,"HyperDash":false},{"StartTime":152548.0,"Position":271.409882,"HyperDash":false}]},{"StartTime":152772.0,"Objects":[{"StartTime":152772.0,"Position":318.0,"HyperDash":false},{"StartTime":152823.0,"Position":332.202423,"HyperDash":false},{"StartTime":152874.0,"Position":339.075562,"HyperDash":false},{"StartTime":152926.0,"Position":384.6346,"HyperDash":false},{"StartTime":152977.0,"Position":390.811829,"HyperDash":false},{"StartTime":153028.0,"Position":421.607452,"HyperDash":false},{"StartTime":153080.0,"Position":434.969727,"HyperDash":false},{"StartTime":153131.0,"Position":424.9186,"HyperDash":false},{"StartTime":153219.0,"Position":465.022461,"HyperDash":false}]},{"StartTime":153668.0,"Objects":[{"StartTime":153668.0,"Position":494.0,"HyperDash":false},{"StartTime":153723.0,"Position":509.7584,"HyperDash":false},{"StartTime":153779.0,"Position":498.566925,"HyperDash":false},{"StartTime":153835.0,"Position":490.375458,"HyperDash":false},{"StartTime":153891.0,"Position":505.209076,"HyperDash":false},{"StartTime":153985.0,"Position":512.0,"HyperDash":false},{"StartTime":154115.0,"Position":494.0,"HyperDash":false}]},{"StartTime":154339.0,"Objects":[{"StartTime":154339.0,"Position":317.0,"HyperDash":false}]},{"StartTime":154563.0,"Objects":[{"StartTime":154563.0,"Position":332.0,"HyperDash":false},{"StartTime":154618.0,"Position":328.824219,"HyperDash":false},{"StartTime":154674.0,"Position":290.48703,"HyperDash":false},{"StartTime":154730.0,"Position":281.624817,"HyperDash":false},{"StartTime":154786.0,"Position":266.622284,"HyperDash":false},{"StartTime":154842.0,"Position":240.852814,"HyperDash":false},{"StartTime":154898.0,"Position":204.669556,"HyperDash":false},{"StartTime":154954.0,"Position":191.449188,"HyperDash":false},{"StartTime":155010.0,"Position":180.362961,"HyperDash":false},{"StartTime":155061.0,"Position":184.570953,"HyperDash":false},{"StartTime":155113.0,"Position":203.339157,"HyperDash":false},{"StartTime":155164.0,"Position":238.630051,"HyperDash":false},{"StartTime":155216.0,"Position":245.871323,"HyperDash":false},{"StartTime":155267.0,"Position":266.0493,"HyperDash":false},{"StartTime":155319.0,"Position":266.5972,"HyperDash":false},{"StartTime":155370.0,"Position":309.515717,"HyperDash":false},{"StartTime":155458.0,"Position":332.0,"HyperDash":false}]},{"StartTime":155683.0,"Objects":[{"StartTime":155683.0,"Position":413.0,"HyperDash":false},{"StartTime":155738.0,"Position":442.436737,"HyperDash":false},{"StartTime":155794.0,"Position":439.2269,"HyperDash":false},{"StartTime":155850.0,"Position":485.017029,"HyperDash":false},{"StartTime":155906.0,"Position":491.9839,"HyperDash":false},{"StartTime":156000.0,"Position":476.9414,"HyperDash":false},{"StartTime":156130.0,"Position":413.0,"HyperDash":false}]},{"StartTime":156354.0,"Objects":[{"StartTime":156354.0,"Position":379.0,"HyperDash":false},{"StartTime":156405.0,"Position":353.171,"HyperDash":false},{"StartTime":156456.0,"Position":333.342,"HyperDash":false},{"StartTime":156508.0,"Position":342.93924,"HyperDash":false},{"StartTime":156559.0,"Position":316.817322,"HyperDash":false},{"StartTime":156610.0,"Position":287.6954,"HyperDash":false},{"StartTime":156662.0,"Position":273.21814,"HyperDash":false},{"StartTime":156713.0,"Position":261.096252,"HyperDash":false},{"StartTime":156801.0,"Position":228.827026,"HyperDash":false}]},{"StartTime":157250.0,"Objects":[{"StartTime":157250.0,"Position":103.0,"HyperDash":false},{"StartTime":157301.0,"Position":109.828995,"HyperDash":false},{"StartTime":157352.0,"Position":131.65799,"HyperDash":false},{"StartTime":157404.0,"Position":139.06076,"HyperDash":false},{"StartTime":157455.0,"Position":150.182678,"HyperDash":false},{"StartTime":157506.0,"Position":189.3046,"HyperDash":false},{"StartTime":157558.0,"Position":199.78186,"HyperDash":false},{"StartTime":157609.0,"Position":219.903763,"HyperDash":false},{"StartTime":157697.0,"Position":253.172974,"HyperDash":false}]},{"StartTime":158145.0,"Objects":[{"StartTime":158145.0,"Position":131.0,"HyperDash":false},{"StartTime":158196.0,"Position":95.01886,"HyperDash":false},{"StartTime":158247.0,"Position":97.78887,"HyperDash":false},{"StartTime":158299.0,"Position":63.4222565,"HyperDash":false},{"StartTime":158350.0,"Position":75.0872,"HyperDash":false},{"StartTime":158401.0,"Position":22.8652954,"HyperDash":false},{"StartTime":158453.0,"Position":45.94365,"HyperDash":false},{"StartTime":158504.0,"Position":0.0,"HyperDash":false},{"StartTime":158592.0,"Position":0.0,"HyperDash":false}]},{"StartTime":158817.0,"Objects":[{"StartTime":158817.0,"Position":29.0,"HyperDash":false}]},{"StartTime":159041.0,"Objects":[{"StartTime":159041.0,"Position":54.0,"HyperDash":false},{"StartTime":159134.0,"Position":95.1591644,"HyperDash":false},{"StartTime":159264.0,"Position":133.5107,"HyperDash":false}]},{"StartTime":159489.0,"Objects":[{"StartTime":159489.0,"Position":194.0,"HyperDash":false},{"StartTime":159582.0,"Position":246.159164,"HyperDash":false},{"StartTime":159712.0,"Position":273.510681,"HyperDash":false}]},{"StartTime":159936.0,"Objects":[{"StartTime":159936.0,"Position":354.0,"HyperDash":false},{"StartTime":159987.0,"Position":380.1903,"HyperDash":false},{"StartTime":160038.0,"Position":355.7923,"HyperDash":false},{"StartTime":160090.0,"Position":369.8352,"HyperDash":false},{"StartTime":160141.0,"Position":386.028046,"HyperDash":false},{"StartTime":160192.0,"Position":388.432159,"HyperDash":false},{"StartTime":160244.0,"Position":401.998,"HyperDash":false},{"StartTime":160295.0,"Position":400.752838,"HyperDash":false},{"StartTime":160383.0,"Position":376.419128,"HyperDash":false}]},{"StartTime":160832.0,"Objects":[{"StartTime":160832.0,"Position":242.0,"HyperDash":false},{"StartTime":160883.0,"Position":217.809677,"HyperDash":false},{"StartTime":160934.0,"Position":242.2077,"HyperDash":false},{"StartTime":160986.0,"Position":224.16481,"HyperDash":false},{"StartTime":161037.0,"Position":196.971954,"HyperDash":false},{"StartTime":161088.0,"Position":210.567825,"HyperDash":false},{"StartTime":161140.0,"Position":211.002029,"HyperDash":false},{"StartTime":161191.0,"Position":221.247162,"HyperDash":false},{"StartTime":161279.0,"Position":219.580887,"HyperDash":false}]},{"StartTime":161728.0,"Objects":[{"StartTime":161728.0,"Position":481.0,"HyperDash":false}]},{"StartTime":162175.0,"Objects":[{"StartTime":162175.0,"Position":182.0,"HyperDash":false},{"StartTime":162268.0,"Position":165.752014,"HyperDash":false},{"StartTime":162398.0,"Position":102.276337,"HyperDash":false}]},{"StartTime":162623.0,"Objects":[{"StartTime":162623.0,"Position":22.0,"HyperDash":false},{"StartTime":162706.0,"Position":2.907238,"HyperDash":false},{"StartTime":162790.0,"Position":4.71697235,"HyperDash":false},{"StartTime":162874.0,"Position":13.9768333,"HyperDash":false},{"StartTime":162958.0,"Position":19.6685238,"HyperDash":false},{"StartTime":163033.0,"Position":4.88709259,"HyperDash":false},{"StartTime":163108.0,"Position":2.06014729,"HyperDash":false},{"StartTime":163183.0,"Position":22.1771469,"HyperDash":false},{"StartTime":163294.0,"Position":22.0,"HyperDash":false}]},{"StartTime":163519.0,"Objects":[{"StartTime":163519.0,"Position":176.0,"HyperDash":false}]},{"StartTime":163966.0,"Objects":[{"StartTime":163966.0,"Position":202.0,"HyperDash":false},{"StartTime":164059.0,"Position":221.322418,"HyperDash":false},{"StartTime":164189.0,"Position":281.902161,"HyperDash":false}]},{"StartTime":164414.0,"Objects":[{"StartTime":164414.0,"Position":355.0,"HyperDash":false},{"StartTime":164497.0,"Position":383.562683,"HyperDash":false},{"StartTime":164581.0,"Position":395.4695,"HyperDash":false},{"StartTime":164665.0,"Position":445.511963,"HyperDash":false},{"StartTime":164749.0,"Position":470.7404,"HyperDash":false},{"StartTime":164824.0,"Position":455.970947,"HyperDash":false},{"StartTime":164899.0,"Position":418.028564,"HyperDash":false},{"StartTime":164974.0,"Position":389.1983,"HyperDash":false},{"StartTime":165085.0,"Position":355.0,"HyperDash":false}]},{"StartTime":165310.0,"Objects":[{"StartTime":165310.0,"Position":76.0,"HyperDash":false}]},{"StartTime":165757.0,"Objects":[{"StartTime":165757.0,"Position":110.0,"HyperDash":false},{"StartTime":165850.0,"Position":113.949112,"HyperDash":false},{"StartTime":165980.0,"Position":121.867233,"HyperDash":false}]},{"StartTime":166205.0,"Objects":[{"StartTime":166205.0,"Position":188.0,"HyperDash":false},{"StartTime":166288.0,"Position":201.562683,"HyperDash":false},{"StartTime":166372.0,"Position":258.4695,"HyperDash":false},{"StartTime":166456.0,"Position":274.511963,"HyperDash":false},{"StartTime":166540.0,"Position":303.7404,"HyperDash":false},{"StartTime":166615.0,"Position":289.970947,"HyperDash":false},{"StartTime":166690.0,"Position":270.028564,"HyperDash":false},{"StartTime":166765.0,"Position":234.1983,"HyperDash":false},{"StartTime":166876.0,"Position":188.0,"HyperDash":false}]},{"StartTime":167101.0,"Objects":[{"StartTime":167101.0,"Position":206.0,"HyperDash":false},{"StartTime":167156.0,"Position":213.034912,"HyperDash":false},{"StartTime":167212.0,"Position":234.0148,"HyperDash":false},{"StartTime":167268.0,"Position":239.378357,"HyperDash":false},{"StartTime":167324.0,"Position":266.7099,"HyperDash":false},{"StartTime":167380.0,"Position":287.58194,"HyperDash":false},{"StartTime":167436.0,"Position":299.568817,"HyperDash":false},{"StartTime":167492.0,"Position":321.242737,"HyperDash":false},{"StartTime":167548.0,"Position":354.299927,"HyperDash":false},{"StartTime":167599.0,"Position":342.30304,"HyperDash":false},{"StartTime":167651.0,"Position":309.123352,"HyperDash":false},{"StartTime":167702.0,"Position":297.94458,"HyperDash":false},{"StartTime":167754.0,"Position":296.413849,"HyperDash":false},{"StartTime":167805.0,"Position":257.5692,"HyperDash":false},{"StartTime":167857.0,"Position":266.036133,"HyperDash":false},{"StartTime":167908.0,"Position":246.8481,"HyperDash":false},{"StartTime":167996.0,"Position":206.0,"HyperDash":false}]},{"StartTime":168332.0,"Objects":[{"StartTime":168332.0,"Position":98.0,"HyperDash":false},{"StartTime":168406.0,"Position":81.25128,"HyperDash":false},{"StartTime":168481.0,"Position":82.4519,"HyperDash":false},{"StartTime":168556.0,"Position":92.65252,"HyperDash":false},{"StartTime":168667.0,"Position":81.0294342,"HyperDash":false}]},{"StartTime":168892.0,"Objects":[{"StartTime":168892.0,"Position":70.0,"HyperDash":false}]},{"StartTime":169339.0,"Objects":[{"StartTime":169339.0,"Position":246.0,"HyperDash":false},{"StartTime":169432.0,"Position":292.1293,"HyperDash":false},{"StartTime":169562.0,"Position":325.439056,"HyperDash":false}]},{"StartTime":169787.0,"Objects":[{"StartTime":169787.0,"Position":385.0,"HyperDash":false},{"StartTime":169870.0,"Position":405.562683,"HyperDash":false},{"StartTime":169954.0,"Position":452.4695,"HyperDash":false},{"StartTime":170038.0,"Position":472.511963,"HyperDash":false},{"StartTime":170122.0,"Position":500.7404,"HyperDash":false},{"StartTime":170197.0,"Position":462.970947,"HyperDash":false},{"StartTime":170272.0,"Position":454.028564,"HyperDash":false},{"StartTime":170347.0,"Position":408.1983,"HyperDash":false},{"StartTime":170458.0,"Position":385.0,"HyperDash":false}]},{"StartTime":170683.0,"Objects":[{"StartTime":170683.0,"Position":106.0,"HyperDash":false}]},{"StartTime":171131.0,"Objects":[{"StartTime":171131.0,"Position":161.0,"HyperDash":false},{"StartTime":171224.0,"Position":131.715057,"HyperDash":false},{"StartTime":171354.0,"Position":81.18773,"HyperDash":false}]},{"StartTime":171578.0,"Objects":[{"StartTime":171578.0,"Position":22.0,"HyperDash":false},{"StartTime":171661.0,"Position":0.907238,"HyperDash":false},{"StartTime":171745.0,"Position":5.71697235,"HyperDash":false},{"StartTime":171829.0,"Position":4.97683334,"HyperDash":false},{"StartTime":171913.0,"Position":19.6685238,"HyperDash":false},{"StartTime":171988.0,"Position":4.88709259,"HyperDash":false},{"StartTime":172063.0,"Position":20.0601463,"HyperDash":false},{"StartTime":172138.0,"Position":0.0,"HyperDash":false},{"StartTime":172249.0,"Position":22.0,"HyperDash":false}]},{"StartTime":172474.0,"Objects":[{"StartTime":172474.0,"Position":196.0,"HyperDash":false}]},{"StartTime":172922.0,"Objects":[{"StartTime":172922.0,"Position":279.0,"HyperDash":false},{"StartTime":173015.0,"Position":321.282318,"HyperDash":false},{"StartTime":173145.0,"Position":358.80603,"HyperDash":false}]},{"StartTime":173369.0,"Objects":[{"StartTime":173369.0,"Position":385.0,"HyperDash":false},{"StartTime":173452.0,"Position":403.562683,"HyperDash":false},{"StartTime":173536.0,"Position":426.4695,"HyperDash":false},{"StartTime":173620.0,"Position":488.511963,"HyperDash":false},{"StartTime":173704.0,"Position":500.7404,"HyperDash":false},{"StartTime":173779.0,"Position":482.970947,"HyperDash":false},{"StartTime":173854.0,"Position":456.028564,"HyperDash":false},{"StartTime":173929.0,"Position":421.1983,"HyperDash":false},{"StartTime":174040.0,"Position":385.0,"HyperDash":false}]},{"StartTime":174265.0,"Objects":[{"StartTime":174265.0,"Position":307.0,"HyperDash":false},{"StartTime":174358.0,"Position":261.853668,"HyperDash":false},{"StartTime":174488.0,"Position":227.52005,"HyperDash":false}]},{"StartTime":174713.0,"Objects":[{"StartTime":174713.0,"Position":148.0,"HyperDash":false},{"StartTime":174796.0,"Position":106.520546,"HyperDash":false},{"StartTime":174880.0,"Position":80.68592,"HyperDash":false},{"StartTime":174964.0,"Position":68.8512955,"HyperDash":false},{"StartTime":175048.0,"Position":28.83908,"HyperDash":false},{"StartTime":175123.0,"Position":59.2995529,"HyperDash":false},{"StartTime":175198.0,"Position":89.9376144,"HyperDash":false},{"StartTime":175273.0,"Position":125.575668,"HyperDash":false},{"StartTime":175384.0,"Position":148.0,"HyperDash":true}]},{"StartTime":175608.0,"Objects":[{"StartTime":175608.0,"Position":439.0,"HyperDash":false}]},{"StartTime":176056.0,"Objects":[{"StartTime":176056.0,"Position":387.0,"HyperDash":false},{"StartTime":176139.0,"Position":390.25885,"HyperDash":false},{"StartTime":176223.0,"Position":407.5621,"HyperDash":false},{"StartTime":176307.0,"Position":401.7594,"HyperDash":false},{"StartTime":176391.0,"Position":410.638336,"HyperDash":false},{"StartTime":176466.0,"Position":423.467438,"HyperDash":false},{"StartTime":176541.0,"Position":433.8284,"HyperDash":false},{"StartTime":176616.0,"Position":391.564453,"HyperDash":false},{"StartTime":176727.0,"Position":387.0,"HyperDash":false}]},{"StartTime":176951.0,"Objects":[{"StartTime":176951.0,"Position":302.0,"HyperDash":false},{"StartTime":177002.0,"Position":276.291016,"HyperDash":false},{"StartTime":177053.0,"Position":253.17688,"HyperDash":false},{"StartTime":177105.0,"Position":259.690979,"HyperDash":false},{"StartTime":177156.0,"Position":233.457672,"HyperDash":false},{"StartTime":177207.0,"Position":200.839844,"HyperDash":false},{"StartTime":177259.0,"Position":195.088776,"HyperDash":false},{"StartTime":177310.0,"Position":160.934647,"HyperDash":false},{"StartTime":177398.0,"Position":146.743591,"HyperDash":false}]},{"StartTime":177623.0,"Objects":[{"StartTime":177623.0,"Position":10.0,"HyperDash":false}]},{"StartTime":177847.0,"Objects":[{"StartTime":177847.0,"Position":93.0,"HyperDash":false},{"StartTime":177902.0,"Position":121.613495,"HyperDash":false},{"StartTime":177958.0,"Position":114.5836,"HyperDash":false},{"StartTime":178014.0,"Position":147.553711,"HyperDash":false},{"StartTime":178070.0,"Position":172.702118,"HyperDash":false},{"StartTime":178164.0,"Position":121.359177,"HyperDash":false},{"StartTime":178294.0,"Position":93.0,"HyperDash":false}]},{"StartTime":178519.0,"Objects":[{"StartTime":178519.0,"Position":20.0,"HyperDash":false},{"StartTime":178570.0,"Position":12.202383,"HyperDash":false},{"StartTime":178621.0,"Position":17.4776764,"HyperDash":false},{"StartTime":178673.0,"Position":22.8649712,"HyperDash":false},{"StartTime":178724.0,"Position":53.9413567,"HyperDash":false},{"StartTime":178775.0,"Position":54.51453,"HyperDash":false},{"StartTime":178827.0,"Position":76.44822,"HyperDash":false},{"StartTime":178878.0,"Position":73.7426147,"HyperDash":false},{"StartTime":178966.0,"Position":117.336555,"HyperDash":false}]},{"StartTime":179190.0,"Objects":[{"StartTime":179190.0,"Position":260.0,"HyperDash":false}]},{"StartTime":179638.0,"Objects":[{"StartTime":179638.0,"Position":381.0,"HyperDash":false},{"StartTime":179731.0,"Position":403.239,"HyperDash":false},{"StartTime":179861.0,"Position":460.702118,"HyperDash":false}]},{"StartTime":180086.0,"Objects":[{"StartTime":180086.0,"Position":499.0,"HyperDash":false},{"StartTime":180169.0,"Position":492.7418,"HyperDash":false},{"StartTime":180253.0,"Position":479.880341,"HyperDash":false},{"StartTime":180337.0,"Position":491.247253,"HyperDash":false},{"StartTime":180421.0,"Position":476.24173,"HyperDash":false},{"StartTime":180496.0,"Position":474.9095,"HyperDash":false},{"StartTime":180571.0,"Position":490.40033,"HyperDash":false},{"StartTime":180646.0,"Position":497.418976,"HyperDash":false},{"StartTime":180757.0,"Position":499.0,"HyperDash":false}]},{"StartTime":180981.0,"Objects":[{"StartTime":180981.0,"Position":350.0,"HyperDash":false}]},{"StartTime":181429.0,"Objects":[{"StartTime":181429.0,"Position":237.0,"HyperDash":false},{"StartTime":181522.0,"Position":219.747375,"HyperDash":false},{"StartTime":181652.0,"Position":157.265228,"HyperDash":false}]},{"StartTime":181877.0,"Objects":[{"StartTime":181877.0,"Position":69.0,"HyperDash":false},{"StartTime":181960.0,"Position":62.7165451,"HyperDash":false},{"StartTime":182044.0,"Position":51.0702744,"HyperDash":false},{"StartTime":182128.0,"Position":58.38651,"HyperDash":false},{"StartTime":182212.0,"Position":46.79955,"HyperDash":false},{"StartTime":182287.0,"Position":57.2412,"HyperDash":false},{"StartTime":182362.0,"Position":36.87273,"HyperDash":false},{"StartTime":182437.0,"Position":68.721756,"HyperDash":false},{"StartTime":182548.0,"Position":69.0,"HyperDash":false}]},{"StartTime":182772.0,"Objects":[{"StartTime":182772.0,"Position":156.0,"HyperDash":false}]},{"StartTime":182996.0,"Objects":[{"StartTime":182996.0,"Position":188.0,"HyperDash":false}]},{"StartTime":183220.0,"Objects":[{"StartTime":183220.0,"Position":258.0,"HyperDash":false},{"StartTime":183271.0,"Position":290.116547,"HyperDash":false},{"StartTime":183322.0,"Position":294.3538,"HyperDash":false},{"StartTime":183374.0,"Position":307.583344,"HyperDash":false},{"StartTime":183425.0,"Position":340.892883,"HyperDash":false},{"StartTime":183476.0,"Position":330.0654,"HyperDash":false},{"StartTime":183528.0,"Position":366.9192,"HyperDash":false},{"StartTime":183579.0,"Position":359.8023,"HyperDash":false},{"StartTime":183667.0,"Position":410.248352,"HyperDash":false}]},{"StartTime":184116.0,"Objects":[{"StartTime":184116.0,"Position":500.0,"HyperDash":false},{"StartTime":184199.0,"Position":507.066162,"HyperDash":false},{"StartTime":184283.0,"Position":497.157227,"HyperDash":false},{"StartTime":184367.0,"Position":504.2483,"HyperDash":false},{"StartTime":184451.0,"Position":508.3518,"HyperDash":false},{"StartTime":184526.0,"Position":505.497223,"HyperDash":false},{"StartTime":184601.0,"Position":509.630219,"HyperDash":false},{"StartTime":184676.0,"Position":505.763184,"HyperDash":false},{"StartTime":184787.0,"Position":500.0,"HyperDash":false}]},{"StartTime":185011.0,"Objects":[{"StartTime":185011.0,"Position":424.0,"HyperDash":false},{"StartTime":185104.0,"Position":408.773682,"HyperDash":false},{"StartTime":185234.0,"Position":345.858856,"HyperDash":false}]},{"StartTime":185459.0,"Objects":[{"StartTime":185459.0,"Position":273.0,"HyperDash":false},{"StartTime":185533.0,"Position":247.05632,"HyperDash":false},{"StartTime":185608.0,"Position":204.745392,"HyperDash":false},{"StartTime":185683.0,"Position":207.012665,"HyperDash":false},{"StartTime":185794.0,"Position":159.200455,"HyperDash":false}]},{"StartTime":186131.0,"Objects":[{"StartTime":186131.0,"Position":66.0,"HyperDash":false},{"StartTime":186182.0,"Position":77.52162,"HyperDash":false},{"StartTime":186233.0,"Position":97.0432358,"HyperDash":false},{"StartTime":186285.0,"Position":113.692917,"HyperDash":false},{"StartTime":186336.0,"Position":147.840286,"HyperDash":false},{"StartTime":186387.0,"Position":162.98764,"HyperDash":false},{"StartTime":186439.0,"Position":155.490845,"HyperDash":false},{"StartTime":186490.0,"Position":179.638214,"HyperDash":false},{"StartTime":186578.0,"Position":217.951324,"HyperDash":false}]},{"StartTime":186802.0,"Objects":[{"StartTime":186802.0,"Position":301.0,"HyperDash":false},{"StartTime":186895.0,"Position":319.187317,"HyperDash":false},{"StartTime":187025.0,"Position":380.578247,"HyperDash":false}]},{"StartTime":187250.0,"Objects":[{"StartTime":187250.0,"Position":468.0,"HyperDash":false},{"StartTime":187333.0,"Position":477.219818,"HyperDash":false},{"StartTime":187417.0,"Position":470.918518,"HyperDash":false},{"StartTime":187501.0,"Position":480.95874,"HyperDash":false},{"StartTime":187585.0,"Position":487.3309,"HyperDash":false},{"StartTime":187660.0,"Position":500.324768,"HyperDash":false},{"StartTime":187735.0,"Position":496.985931,"HyperDash":false},{"StartTime":187810.0,"Position":459.305664,"HyperDash":false},{"StartTime":187921.0,"Position":468.0,"HyperDash":false}]},{"StartTime":188145.0,"Objects":[{"StartTime":188145.0,"Position":372.0,"HyperDash":false}]},{"StartTime":188593.0,"Objects":[{"StartTime":188593.0,"Position":255.0,"HyperDash":false},{"StartTime":188686.0,"Position":237.844971,"HyperDash":false},{"StartTime":188816.0,"Position":175.499252,"HyperDash":false}]},{"StartTime":189041.0,"Objects":[{"StartTime":189041.0,"Position":140.0,"HyperDash":false},{"StartTime":189124.0,"Position":120.208252,"HyperDash":false},{"StartTime":189208.0,"Position":79.71341,"HyperDash":false},{"StartTime":189292.0,"Position":62.945713,"HyperDash":false},{"StartTime":189376.0,"Position":21.8198166,"HyperDash":false},{"StartTime":189451.0,"Position":43.38784,"HyperDash":false},{"StartTime":189526.0,"Position":60.00197,"HyperDash":false},{"StartTime":189601.0,"Position":110.413246,"HyperDash":false},{"StartTime":189712.0,"Position":140.0,"HyperDash":false}]},{"StartTime":189936.0,"Objects":[{"StartTime":189936.0,"Position":409.0,"HyperDash":false}]},{"StartTime":190384.0,"Objects":[{"StartTime":190384.0,"Position":297.0,"HyperDash":false},{"StartTime":190467.0,"Position":334.5554,"HyperDash":false},{"StartTime":190551.0,"Position":360.466858,"HyperDash":false},{"StartTime":190635.0,"Position":367.378357,"HyperDash":false},{"StartTime":190719.0,"Position":416.4679,"HyperDash":false},{"StartTime":190794.0,"Position":383.93924,"HyperDash":false},{"StartTime":190869.0,"Position":350.232544,"HyperDash":false},{"StartTime":190944.0,"Position":345.525879,"HyperDash":false},{"StartTime":191055.0,"Position":297.0,"HyperDash":false}]},{"StartTime":191280.0,"Objects":[{"StartTime":191280.0,"Position":233.0,"HyperDash":false},{"StartTime":191335.0,"Position":238.967834,"HyperDash":false},{"StartTime":191391.0,"Position":212.5211,"HyperDash":false},{"StartTime":191447.0,"Position":237.94754,"HyperDash":false},{"StartTime":191503.0,"Position":229.915482,"HyperDash":false},{"StartTime":191559.0,"Position":237.2686,"HyperDash":false},{"StartTime":191615.0,"Position":275.501129,"HyperDash":false},{"StartTime":191671.0,"Position":284.155334,"HyperDash":false},{"StartTime":191727.0,"Position":303.59848,"HyperDash":false},{"StartTime":191821.0,"Position":353.735931,"HyperDash":false},{"StartTime":191951.0,"Position":381.505432,"HyperDash":false}]},{"StartTime":192175.0,"Objects":[{"StartTime":192175.0,"Position":468.0,"HyperDash":false},{"StartTime":192258.0,"Position":482.7641,"HyperDash":false},{"StartTime":192342.0,"Position":450.513336,"HyperDash":false},{"StartTime":192426.0,"Position":466.262543,"HyperDash":false},{"StartTime":192510.0,"Position":463.004333,"HyperDash":false},{"StartTime":192585.0,"Position":465.113647,"HyperDash":false},{"StartTime":192660.0,"Position":470.2304,"HyperDash":false},{"StartTime":192735.0,"Position":465.3472,"HyperDash":false},{"StartTime":192846.0,"Position":468.0,"HyperDash":false}]},{"StartTime":193071.0,"Objects":[{"StartTime":193071.0,"Position":497.0,"HyperDash":false},{"StartTime":193126.0,"Position":512.0,"HyperDash":false},{"StartTime":193182.0,"Position":490.965454,"HyperDash":false},{"StartTime":193238.0,"Position":505.365143,"HyperDash":false},{"StartTime":193294.0,"Position":498.1796,"HyperDash":false},{"StartTime":193350.0,"Position":458.746429,"HyperDash":false},{"StartTime":193406.0,"Position":465.362274,"HyperDash":false},{"StartTime":193462.0,"Position":428.6823,"HyperDash":false},{"StartTime":193518.0,"Position":425.1735,"HyperDash":false},{"StartTime":193612.0,"Position":399.024475,"HyperDash":false},{"StartTime":193742.0,"Position":347.213928,"HyperDash":false}]},{"StartTime":193966.0,"Objects":[{"StartTime":193966.0,"Position":292.0,"HyperDash":false},{"StartTime":194049.0,"Position":284.2359,"HyperDash":false},{"StartTime":194133.0,"Position":296.486664,"HyperDash":false},{"StartTime":194217.0,"Position":289.737457,"HyperDash":false},{"StartTime":194301.0,"Position":296.995667,"HyperDash":false},{"StartTime":194376.0,"Position":298.886353,"HyperDash":false},{"StartTime":194451.0,"Position":301.7696,"HyperDash":false},{"StartTime":194526.0,"Position":303.6528,"HyperDash":false},{"StartTime":194637.0,"Position":292.0,"HyperDash":false}]},{"StartTime":194862.0,"Objects":[{"StartTime":194862.0,"Position":233.0,"HyperDash":false},{"StartTime":194917.0,"Position":233.672577,"HyperDash":false},{"StartTime":194973.0,"Position":188.020615,"HyperDash":false},{"StartTime":195029.0,"Position":185.026245,"HyperDash":false},{"StartTime":195085.0,"Position":146.409729,"HyperDash":false},{"StartTime":195141.0,"Position":122.932129,"HyperDash":false},{"StartTime":195197.0,"Position":132.166672,"HyperDash":false},{"StartTime":195253.0,"Position":111.858551,"HyperDash":false},{"StartTime":195309.0,"Position":94.3505554,"HyperDash":false},{"StartTime":195403.0,"Position":84.74842,"HyperDash":false},{"StartTime":195533.0,"Position":83.93751,"HyperDash":false}]},{"StartTime":195757.0,"Objects":[{"StartTime":195757.0,"Position":156.0,"HyperDash":false}]},{"StartTime":196205.0,"Objects":[{"StartTime":196205.0,"Position":292.0,"HyperDash":false},{"StartTime":196288.0,"Position":315.547729,"HyperDash":false},{"StartTime":196372.0,"Position":356.451416,"HyperDash":false},{"StartTime":196456.0,"Position":363.355164,"HyperDash":false},{"StartTime":196540.0,"Position":411.436859,"HyperDash":false},{"StartTime":196615.0,"Position":378.9151,"HyperDash":false},{"StartTime":196690.0,"Position":351.215363,"HyperDash":false},{"StartTime":196765.0,"Position":317.515625,"HyperDash":false},{"StartTime":196876.0,"Position":292.0,"HyperDash":false}]},{"StartTime":197101.0,"Objects":[{"StartTime":197101.0,"Position":224.0,"HyperDash":false},{"StartTime":197194.0,"Position":208.802353,"HyperDash":false},{"StartTime":197324.0,"Position":144.397034,"HyperDash":false}]},{"StartTime":197548.0,"Objects":[{"StartTime":197548.0,"Position":66.0,"HyperDash":false},{"StartTime":197631.0,"Position":48.2919579,"HyperDash":false},{"StartTime":197715.0,"Position":35.05251,"HyperDash":false},{"StartTime":197799.0,"Position":38.5374374,"HyperDash":false},{"StartTime":197883.0,"Position":11.4361858,"HyperDash":false},{"StartTime":197958.0,"Position":13.2977371,"HyperDash":false},{"StartTime":198033.0,"Position":27.7316284,"HyperDash":false},{"StartTime":198108.0,"Position":56.1405029,"HyperDash":false},{"StartTime":198219.0,"Position":66.0,"HyperDash":false}]},{"StartTime":198444.0,"Objects":[{"StartTime":198444.0,"Position":42.0,"HyperDash":false},{"StartTime":198499.0,"Position":55.76585,"HyperDash":false},{"StartTime":198555.0,"Position":34.16329,"HyperDash":false},{"StartTime":198611.0,"Position":54.15536,"HyperDash":false},{"StartTime":198667.0,"Position":77.49657,"HyperDash":false},{"StartTime":198723.0,"Position":71.00532,"HyperDash":false},{"StartTime":198779.0,"Position":105.424828,"HyperDash":false},{"StartTime":198835.0,"Position":125.435341,"HyperDash":false},{"StartTime":198891.0,"Position":136.756622,"HyperDash":false},{"StartTime":198985.0,"Position":174.4071,"HyperDash":false},{"StartTime":199115.0,"Position":215.646362,"HyperDash":false}]},{"StartTime":199339.0,"Objects":[{"StartTime":199339.0,"Position":292.0,"HyperDash":false},{"StartTime":199422.0,"Position":330.217377,"HyperDash":false},{"StartTime":199506.0,"Position":361.968842,"HyperDash":false},{"StartTime":199590.0,"Position":372.687,"HyperDash":false},{"StartTime":199674.0,"Position":408.582062,"HyperDash":false},{"StartTime":199749.0,"Position":367.224884,"HyperDash":false},{"StartTime":199824.0,"Position":343.6908,"HyperDash":false},{"StartTime":199899.0,"Position":338.7365,"HyperDash":false},{"StartTime":200010.0,"Position":292.0,"HyperDash":false}]},{"StartTime":200235.0,"Objects":[{"StartTime":200235.0,"Position":235.0,"HyperDash":false},{"StartTime":200290.0,"Position":235.309448,"HyperDash":false},{"StartTime":200346.0,"Position":241.240967,"HyperDash":false},{"StartTime":200402.0,"Position":245.969574,"HyperDash":false},{"StartTime":200458.0,"Position":247.421249,"HyperDash":false},{"StartTime":200514.0,"Position":241.446747,"HyperDash":false},{"StartTime":200570.0,"Position":272.996338,"HyperDash":false},{"StartTime":200626.0,"Position":270.733429,"HyperDash":false},{"StartTime":200682.0,"Position":286.54422,"HyperDash":false},{"StartTime":200776.0,"Position":331.074341,"HyperDash":false},{"StartTime":200906.0,"Position":359.601563,"HyperDash":false}]},{"StartTime":201131.0,"Objects":[{"StartTime":201131.0,"Position":447.0,"HyperDash":false}]},{"StartTime":201578.0,"Objects":[{"StartTime":201578.0,"Position":472.0,"HyperDash":false},{"StartTime":201671.0,"Position":420.90976,"HyperDash":false},{"StartTime":201801.0,"Position":392.654541,"HyperDash":false}]},{"StartTime":202026.0,"Objects":[{"StartTime":202026.0,"Position":323.0,"HyperDash":false},{"StartTime":202109.0,"Position":280.374054,"HyperDash":false},{"StartTime":202193.0,"Position":263.163239,"HyperDash":false},{"StartTime":202277.0,"Position":238.104523,"HyperDash":false},{"StartTime":202361.0,"Position":213.4443,"HyperDash":false},{"StartTime":202436.0,"Position":215.121521,"HyperDash":false},{"StartTime":202511.0,"Position":262.801849,"HyperDash":false},{"StartTime":202586.0,"Position":278.475128,"HyperDash":false},{"StartTime":202697.0,"Position":323.0,"HyperDash":false}]},{"StartTime":202922.0,"Objects":[{"StartTime":202922.0,"Position":370.0,"HyperDash":false}]},{"StartTime":203369.0,"Objects":[{"StartTime":203369.0,"Position":472.0,"HyperDash":false},{"StartTime":203462.0,"Position":457.79657,"HyperDash":false},{"StartTime":203592.0,"Position":459.52298,"HyperDash":false}]},{"StartTime":203817.0,"Objects":[{"StartTime":203817.0,"Position":373.0,"HyperDash":false},{"StartTime":203900.0,"Position":398.412079,"HyperDash":false},{"StartTime":203984.0,"Position":390.1198,"HyperDash":false},{"StartTime":204068.0,"Position":402.6163,"HyperDash":false},{"StartTime":204152.0,"Position":398.979218,"HyperDash":false},{"StartTime":204227.0,"Position":415.909515,"HyperDash":false},{"StartTime":204302.0,"Position":375.485352,"HyperDash":false},{"StartTime":204377.0,"Position":384.754333,"HyperDash":false},{"StartTime":204488.0,"Position":373.0,"HyperDash":false}]},{"StartTime":204713.0,"Objects":[{"StartTime":204713.0,"Position":294.0,"HyperDash":false},{"StartTime":204764.0,"Position":285.134979,"HyperDash":false},{"StartTime":204815.0,"Position":243.269958,"HyperDash":false},{"StartTime":204867.0,"Position":248.074249,"HyperDash":false},{"StartTime":204918.0,"Position":228.209229,"HyperDash":false},{"StartTime":204969.0,"Position":192.333786,"HyperDash":false},{"StartTime":205021.0,"Position":173.952545,"HyperDash":false},{"StartTime":205072.0,"Position":183.9248,"HyperDash":false},{"StartTime":205160.0,"Position":140.818085,"HyperDash":false}]},{"StartTime":205608.0,"Objects":[{"StartTime":205608.0,"Position":29.0,"HyperDash":false},{"StartTime":205659.0,"Position":63.86502,"HyperDash":false},{"StartTime":205710.0,"Position":61.7300453,"HyperDash":false},{"StartTime":205762.0,"Position":82.92575,"HyperDash":false},{"StartTime":205813.0,"Position":97.79077,"HyperDash":false},{"StartTime":205864.0,"Position":130.666214,"HyperDash":false},{"StartTime":205916.0,"Position":148.047455,"HyperDash":false},{"StartTime":205967.0,"Position":142.0752,"HyperDash":false},{"StartTime":206055.0,"Position":182.181915,"HyperDash":false}]},{"StartTime":206280.0,"Objects":[{"StartTime":206280.0,"Position":322.0,"HyperDash":false}]},{"StartTime":206504.0,"Objects":[{"StartTime":206504.0,"Position":344.0,"HyperDash":false},{"StartTime":206587.0,"Position":365.904449,"HyperDash":false},{"StartTime":206671.0,"Position":418.4734,"HyperDash":false},{"StartTime":206755.0,"Position":413.206177,"HyperDash":false},{"StartTime":206839.0,"Position":457.994324,"HyperDash":false},{"StartTime":206914.0,"Position":431.638641,"HyperDash":false},{"StartTime":206989.0,"Position":403.264984,"HyperDash":false},{"StartTime":207064.0,"Position":377.594177,"HyperDash":false},{"StartTime":207175.0,"Position":344.0,"HyperDash":false}]},{"StartTime":207399.0,"Objects":[{"StartTime":207399.0,"Position":294.0,"HyperDash":false},{"StartTime":207454.0,"Position":297.1099,"HyperDash":false},{"StartTime":207510.0,"Position":290.9207,"HyperDash":false},{"StartTime":207566.0,"Position":289.514343,"HyperDash":false},{"StartTime":207622.0,"Position":319.350433,"HyperDash":false},{"StartTime":207678.0,"Position":342.16394,"HyperDash":false},{"StartTime":207734.0,"Position":371.241455,"HyperDash":false},{"StartTime":207790.0,"Position":385.045563,"HyperDash":false},{"StartTime":207846.0,"Position":390.7907,"HyperDash":false},{"StartTime":207897.0,"Position":395.987244,"HyperDash":false},{"StartTime":207949.0,"Position":428.121765,"HyperDash":false},{"StartTime":208000.0,"Position":447.949615,"HyperDash":false},{"StartTime":208052.0,"Position":476.569366,"HyperDash":false},{"StartTime":208103.0,"Position":470.83667,"HyperDash":false},{"StartTime":208155.0,"Position":476.915344,"HyperDash":false},{"StartTime":208206.0,"Position":511.044159,"HyperDash":false},{"StartTime":208294.0,"Position":498.7854,"HyperDash":false}]},{"StartTime":215459.0,"Objects":[{"StartTime":215459.0,"Position":479.0,"HyperDash":false},{"StartTime":215542.0,"Position":229.0,"HyperDash":false},{"StartTime":215626.0,"Position":331.0,"HyperDash":false},{"StartTime":215710.0,"Position":226.0,"HyperDash":false},{"StartTime":215794.0,"Position":205.0,"HyperDash":false},{"StartTime":215878.0,"Position":472.0,"HyperDash":false},{"StartTime":215962.0,"Position":426.0,"HyperDash":false},{"StartTime":216046.0,"Position":340.0,"HyperDash":false},{"StartTime":216130.0,"Position":379.0,"HyperDash":false},{"StartTime":216214.0,"Position":21.0,"HyperDash":false},{"StartTime":216298.0,"Position":302.0,"HyperDash":false},{"StartTime":216382.0,"Position":148.0,"HyperDash":false},{"StartTime":216466.0,"Position":431.0,"HyperDash":false},{"StartTime":216550.0,"Position":424.0,"HyperDash":false},{"StartTime":216634.0,"Position":14.0,"HyperDash":false},{"StartTime":216718.0,"Position":423.0,"HyperDash":false},{"StartTime":216802.0,"Position":16.0,"HyperDash":false},{"StartTime":216885.0,"Position":284.0,"HyperDash":false},{"StartTime":216969.0,"Position":201.0,"HyperDash":false},{"StartTime":217053.0,"Position":29.0,"HyperDash":false},{"StartTime":217137.0,"Position":203.0,"HyperDash":false},{"StartTime":217221.0,"Position":129.0,"HyperDash":false},{"StartTime":217305.0,"Position":285.0,"HyperDash":false},{"StartTime":217389.0,"Position":254.0,"HyperDash":false},{"StartTime":217473.0,"Position":145.0,"HyperDash":false},{"StartTime":217557.0,"Position":230.0,"HyperDash":false},{"StartTime":217641.0,"Position":466.0,"HyperDash":false},{"StartTime":217725.0,"Position":86.0,"HyperDash":false},{"StartTime":217809.0,"Position":434.0,"HyperDash":false},{"StartTime":217893.0,"Position":159.0,"HyperDash":false},{"StartTime":217977.0,"Position":493.0,"HyperDash":false},{"StartTime":218061.0,"Position":191.0,"HyperDash":false},{"StartTime":218145.0,"Position":200.0,"HyperDash":false}]},{"StartTime":219041.0,"Objects":[{"StartTime":219041.0,"Position":205.0,"HyperDash":false},{"StartTime":219092.0,"Position":176.805145,"HyperDash":false},{"StartTime":219143.0,"Position":178.610275,"HyperDash":false},{"StartTime":219195.0,"Position":155.058655,"HyperDash":false},{"StartTime":219246.0,"Position":127.8638,"HyperDash":false},{"StartTime":219297.0,"Position":96.12851,"HyperDash":false},{"StartTime":219349.0,"Position":102.176147,"HyperDash":false},{"StartTime":219400.0,"Position":75.54979,"HyperDash":false},{"StartTime":219488.0,"Position":51.8611755,"HyperDash":false}]},{"StartTime":219936.0,"Objects":[{"StartTime":219936.0,"Position":75.0,"HyperDash":false},{"StartTime":219987.0,"Position":82.19486,"HyperDash":false},{"StartTime":220038.0,"Position":115.389725,"HyperDash":false},{"StartTime":220090.0,"Position":110.941345,"HyperDash":false},{"StartTime":220141.0,"Position":143.1362,"HyperDash":false},{"StartTime":220192.0,"Position":161.87149,"HyperDash":false},{"StartTime":220244.0,"Position":186.823853,"HyperDash":false},{"StartTime":220295.0,"Position":188.4502,"HyperDash":false},{"StartTime":220383.0,"Position":228.138824,"HyperDash":false}]},{"StartTime":220832.0,"Objects":[{"StartTime":220832.0,"Position":337.0,"HyperDash":false},{"StartTime":220915.0,"Position":317.352051,"HyperDash":false},{"StartTime":220999.0,"Position":312.6722,"HyperDash":false},{"StartTime":221083.0,"Position":337.992371,"HyperDash":false},{"StartTime":221167.0,"Position":326.29657,"HyperDash":false},{"StartTime":221242.0,"Position":334.67334,"HyperDash":false},{"StartTime":221317.0,"Position":327.066071,"HyperDash":false},{"StartTime":221392.0,"Position":352.458771,"HyperDash":false},{"StartTime":221503.0,"Position":337.0,"HyperDash":false}]},{"StartTime":221951.0,"Objects":[{"StartTime":221951.0,"Position":457.0,"HyperDash":false},{"StartTime":222006.0,"Position":446.041077,"HyperDash":false},{"StartTime":222062.0,"Position":457.04657,"HyperDash":false},{"StartTime":222118.0,"Position":470.052032,"HyperDash":false},{"StartTime":222174.0,"Position":449.0397,"HyperDash":false},{"StartTime":222268.0,"Position":464.369843,"HyperDash":false},{"StartTime":222398.0,"Position":457.0,"HyperDash":false}]},{"StartTime":222623.0,"Objects":[{"StartTime":222623.0,"Position":495.0,"HyperDash":false}]},{"StartTime":223071.0,"Objects":[{"StartTime":223071.0,"Position":331.0,"HyperDash":false},{"StartTime":223154.0,"Position":317.6592,"HyperDash":false},{"StartTime":223238.0,"Position":271.751648,"HyperDash":false},{"StartTime":223322.0,"Position":250.900024,"HyperDash":false},{"StartTime":223406.0,"Position":215.870728,"HyperDash":false},{"StartTime":223481.0,"Position":250.346268,"HyperDash":false},{"StartTime":223556.0,"Position":287.9995,"HyperDash":false},{"StartTime":223631.0,"Position":302.435822,"HyperDash":false},{"StartTime":223742.0,"Position":331.0,"HyperDash":false}]},{"StartTime":223966.0,"Objects":[{"StartTime":223966.0,"Position":399.0,"HyperDash":false}]},{"StartTime":224414.0,"Objects":[{"StartTime":224414.0,"Position":471.0,"HyperDash":false},{"StartTime":224488.0,"Position":457.712158,"HyperDash":false},{"StartTime":224563.0,"Position":447.379883,"HyperDash":false},{"StartTime":224638.0,"Position":456.0476,"HyperDash":false},{"StartTime":224749.0,"Position":456.115845,"HyperDash":false}]},{"StartTime":225086.0,"Objects":[{"StartTime":225086.0,"Position":326.0,"HyperDash":false},{"StartTime":225137.0,"Position":300.208832,"HyperDash":false},{"StartTime":225188.0,"Position":275.417664,"HyperDash":false},{"StartTime":225240.0,"Position":290.316833,"HyperDash":false},{"StartTime":225291.0,"Position":243.612946,"HyperDash":false},{"StartTime":225342.0,"Position":241.580139,"HyperDash":false},{"StartTime":225394.0,"Position":232.193756,"HyperDash":false},{"StartTime":225445.0,"Position":210.16098,"HyperDash":false},{"StartTime":225533.0,"Position":175.045547,"HyperDash":false}]},{"StartTime":225757.0,"Objects":[{"StartTime":225757.0,"Position":88.0,"HyperDash":false},{"StartTime":225850.0,"Position":65.83169,"HyperDash":false},{"StartTime":225980.0,"Position":74.3185,"HyperDash":false}]},{"StartTime":226205.0,"Objects":[{"StartTime":226205.0,"Position":140.0,"HyperDash":false},{"StartTime":226298.0,"Position":123.645569,"HyperDash":false},{"StartTime":226428.0,"Position":143.945816,"HyperDash":false}]},{"StartTime":226653.0,"Objects":[{"StartTime":226653.0,"Position":116.0,"HyperDash":false},{"StartTime":226736.0,"Position":106.660728,"HyperDash":false},{"StartTime":226820.0,"Position":50.7313728,"HyperDash":false},{"StartTime":226904.0,"Position":15.8698654,"HyperDash":false},{"StartTime":226988.0,"Position":3.21379948,"HyperDash":false},{"StartTime":227063.0,"Position":23.62399,"HyperDash":false},{"StartTime":227138.0,"Position":46.0249748,"HyperDash":false},{"StartTime":227213.0,"Position":81.71385,"HyperDash":false},{"StartTime":227324.0,"Position":116.0,"HyperDash":false}]},{"StartTime":227548.0,"Objects":[{"StartTime":227548.0,"Position":202.0,"HyperDash":false},{"StartTime":227641.0,"Position":228.322632,"HyperDash":false},{"StartTime":227771.0,"Position":281.902618,"HyperDash":false}]},{"StartTime":227996.0,"Objects":[{"StartTime":227996.0,"Position":370.0,"HyperDash":false},{"StartTime":228047.0,"Position":379.322418,"HyperDash":false},{"StartTime":228098.0,"Position":404.644836,"HyperDash":false},{"StartTime":228150.0,"Position":412.9706,"HyperDash":false},{"StartTime":228201.0,"Position":407.2122,"HyperDash":false},{"StartTime":228252.0,"Position":406.4538,"HyperDash":false},{"StartTime":228304.0,"Position":390.660919,"HyperDash":false},{"StartTime":228355.0,"Position":399.902527,"HyperDash":false},{"StartTime":228443.0,"Position":393.8684,"HyperDash":false}]},{"StartTime":228892.0,"Objects":[{"StartTime":228892.0,"Position":291.0,"HyperDash":false},{"StartTime":228985.0,"Position":255.7421,"HyperDash":false},{"StartTime":229115.0,"Position":211.252533,"HyperDash":false}]},{"StartTime":229339.0,"Objects":[{"StartTime":229339.0,"Position":136.0,"HyperDash":false},{"StartTime":229432.0,"Position":97.7420959,"HyperDash":false},{"StartTime":229562.0,"Position":56.25254,"HyperDash":false}]},{"StartTime":229787.0,"Objects":[{"StartTime":229787.0,"Position":20.0,"HyperDash":false},{"StartTime":229838.0,"Position":17.0399265,"HyperDash":false},{"StartTime":229889.0,"Position":21.079855,"HyperDash":false},{"StartTime":229941.0,"Position":25.0421333,"HyperDash":false},{"StartTime":229992.0,"Position":22.7113285,"HyperDash":false},{"StartTime":230043.0,"Position":24.4840775,"HyperDash":false},{"StartTime":230095.0,"Position":14.3101463,"HyperDash":false},{"StartTime":230146.0,"Position":9.403353,"HyperDash":false},{"StartTime":230234.0,"Position":15.3877077,"HyperDash":false}]},{"StartTime":230683.0,"Objects":[{"StartTime":230683.0,"Position":156.0,"HyperDash":false},{"StartTime":230776.0,"Position":173.746826,"HyperDash":false},{"StartTime":230906.0,"Position":186.4041,"HyperDash":false}]},{"StartTime":231131.0,"Objects":[{"StartTime":231131.0,"Position":264.0,"HyperDash":false},{"StartTime":231224.0,"Position":253.253189,"HyperDash":false},{"StartTime":231354.0,"Position":233.595917,"HyperDash":false}]},{"StartTime":231578.0,"Objects":[{"StartTime":231578.0,"Position":262.0,"HyperDash":false},{"StartTime":231629.0,"Position":267.8308,"HyperDash":false},{"StartTime":231680.0,"Position":297.661621,"HyperDash":false},{"StartTime":231732.0,"Position":320.886,"HyperDash":false},{"StartTime":231783.0,"Position":341.016968,"HyperDash":false},{"StartTime":231834.0,"Position":347.147919,"HyperDash":false},{"StartTime":231886.0,"Position":352.6344,"HyperDash":false},{"StartTime":231937.0,"Position":373.76535,"HyperDash":false},{"StartTime":232025.0,"Position":417.05014,"HyperDash":false}]},{"StartTime":232250.0,"Objects":[{"StartTime":232250.0,"Position":479.0,"HyperDash":false}]},{"StartTime":232474.0,"Objects":[{"StartTime":232474.0,"Position":500.0,"HyperDash":false},{"StartTime":232567.0,"Position":485.105865,"HyperDash":false},{"StartTime":232697.0,"Position":481.10556,"HyperDash":false}]},{"StartTime":232922.0,"Objects":[{"StartTime":232922.0,"Position":396.0,"HyperDash":false},{"StartTime":233015.0,"Position":344.7835,"HyperDash":false},{"StartTime":233145.0,"Position":320.1601,"HyperDash":false}]},{"StartTime":233369.0,"Objects":[{"StartTime":233369.0,"Position":264.0,"HyperDash":false},{"StartTime":233420.0,"Position":256.891846,"HyperDash":false},{"StartTime":233471.0,"Position":238.654755,"HyperDash":false},{"StartTime":233523.0,"Position":225.308167,"HyperDash":false},{"StartTime":233574.0,"Position":201.429947,"HyperDash":false},{"StartTime":233625.0,"Position":188.241165,"HyperDash":false},{"StartTime":233677.0,"Position":164.716415,"HyperDash":false},{"StartTime":233728.0,"Position":147.656219,"HyperDash":false},{"StartTime":233816.0,"Position":109.216957,"HyperDash":false}]},{"StartTime":234265.0,"Objects":[{"StartTime":234265.0,"Position":39.0,"HyperDash":false},{"StartTime":234320.0,"Position":18.3255081,"HyperDash":false},{"StartTime":234376.0,"Position":20.620575,"HyperDash":false},{"StartTime":234432.0,"Position":27.915638,"HyperDash":false},{"StartTime":234488.0,"Position":32.19548,"HyperDash":false},{"StartTime":234582.0,"Position":41.0421143,"HyperDash":false},{"StartTime":234712.0,"Position":39.0,"HyperDash":false}]},{"StartTime":234936.0,"Objects":[{"StartTime":234936.0,"Position":214.0,"HyperDash":false}]},{"StartTime":235160.0,"Objects":[{"StartTime":235160.0,"Position":206.0,"HyperDash":false},{"StartTime":235215.0,"Position":221.503036,"HyperDash":false},{"StartTime":235271.0,"Position":257.307831,"HyperDash":false},{"StartTime":235327.0,"Position":245.8396,"HyperDash":false},{"StartTime":235383.0,"Position":280.764069,"HyperDash":false},{"StartTime":235439.0,"Position":285.755646,"HyperDash":false},{"StartTime":235495.0,"Position":305.4811,"HyperDash":false},{"StartTime":235551.0,"Position":349.636,"HyperDash":false},{"StartTime":235607.0,"Position":359.014679,"HyperDash":false},{"StartTime":235658.0,"Position":335.625427,"HyperDash":false},{"StartTime":235710.0,"Position":307.9622,"HyperDash":false},{"StartTime":235761.0,"Position":298.080017,"HyperDash":false},{"StartTime":235813.0,"Position":295.555237,"HyperDash":false},{"StartTime":235864.0,"Position":258.349457,"HyperDash":false},{"StartTime":235916.0,"Position":266.998871,"HyperDash":false},{"StartTime":235967.0,"Position":250.476349,"HyperDash":false},{"StartTime":236055.0,"Position":206.0,"HyperDash":false}]},{"StartTime":236280.0,"Objects":[{"StartTime":236280.0,"Position":136.0,"HyperDash":false},{"StartTime":236335.0,"Position":133.3588,"HyperDash":false},{"StartTime":236391.0,"Position":108.360489,"HyperDash":false},{"StartTime":236447.0,"Position":81.36217,"HyperDash":false},{"StartTime":236503.0,"Position":56.1853027,"HyperDash":false},{"StartTime":236597.0,"Position":85.57534,"HyperDash":false},{"StartTime":236727.0,"Position":136.0,"HyperDash":false}]},{"StartTime":236951.0,"Objects":[{"StartTime":236951.0,"Position":203.0,"HyperDash":false},{"StartTime":237002.0,"Position":235.515,"HyperDash":false},{"StartTime":237053.0,"Position":231.03,"HyperDash":false},{"StartTime":237105.0,"Position":235.849213,"HyperDash":false},{"StartTime":237156.0,"Position":257.413,"HyperDash":false},{"StartTime":237207.0,"Position":301.49884,"HyperDash":false},{"StartTime":237259.0,"Position":304.939331,"HyperDash":false},{"StartTime":237310.0,"Position":305.025177,"HyperDash":false},{"StartTime":237398.0,"Position":353.232178,"HyperDash":false}]},{"StartTime":237847.0,"Objects":[{"StartTime":237847.0,"Position":468.0,"HyperDash":false},{"StartTime":237898.0,"Position":450.485,"HyperDash":false},{"StartTime":237949.0,"Position":421.97,"HyperDash":false},{"StartTime":238001.0,"Position":401.1508,"HyperDash":false},{"StartTime":238052.0,"Position":410.587,"HyperDash":false},{"StartTime":238103.0,"Position":391.50116,"HyperDash":false},{"StartTime":238155.0,"Position":374.060669,"HyperDash":false},{"StartTime":238206.0,"Position":362.974823,"HyperDash":false},{"StartTime":238294.0,"Position":317.767822,"HyperDash":false}]},{"StartTime":238742.0,"Objects":[{"StartTime":238742.0,"Position":180.0,"HyperDash":false},{"StartTime":238793.0,"Position":173.605637,"HyperDash":false},{"StartTime":238844.0,"Position":127.565094,"HyperDash":false},{"StartTime":238896.0,"Position":130.980515,"HyperDash":false},{"StartTime":238947.0,"Position":94.05988,"HyperDash":false},{"StartTime":238998.0,"Position":89.93131,"HyperDash":false},{"StartTime":239050.0,"Position":62.7224731,"HyperDash":false},{"StartTime":239101.0,"Position":61.4846268,"HyperDash":false},{"StartTime":239189.0,"Position":40.9435463,"HyperDash":false}]},{"StartTime":239414.0,"Objects":[{"StartTime":239414.0,"Position":1.0,"HyperDash":false}]},{"StartTime":239638.0,"Objects":[{"StartTime":239638.0,"Position":65.0,"HyperDash":false},{"StartTime":239731.0,"Position":90.205574,"HyperDash":false},{"StartTime":239861.0,"Position":144.621979,"HyperDash":false}]},{"StartTime":240086.0,"Objects":[{"StartTime":240086.0,"Position":205.0,"HyperDash":false},{"StartTime":240179.0,"Position":248.205566,"HyperDash":false},{"StartTime":240309.0,"Position":284.621979,"HyperDash":false}]},{"StartTime":240534.0,"Objects":[{"StartTime":240534.0,"Position":366.0,"HyperDash":false},{"StartTime":240585.0,"Position":363.81723,"HyperDash":false},{"StartTime":240636.0,"Position":373.2125,"HyperDash":false},{"StartTime":240688.0,"Position":391.223755,"HyperDash":false},{"StartTime":240739.0,"Position":374.622253,"HyperDash":false},{"StartTime":240790.0,"Position":403.4788,"HyperDash":false},{"StartTime":240842.0,"Position":402.731842,"HyperDash":false},{"StartTime":240893.0,"Position":374.42746,"HyperDash":false},{"StartTime":240981.0,"Position":383.571564,"HyperDash":false}]},{"StartTime":241429.0,"Objects":[{"StartTime":241429.0,"Position":238.0,"HyperDash":false},{"StartTime":241480.0,"Position":236.18277,"HyperDash":false},{"StartTime":241531.0,"Position":235.787491,"HyperDash":false},{"StartTime":241583.0,"Position":219.776245,"HyperDash":false},{"StartTime":241634.0,"Position":209.377747,"HyperDash":false},{"StartTime":241685.0,"Position":209.52121,"HyperDash":false},{"StartTime":241737.0,"Position":227.268158,"HyperDash":false},{"StartTime":241788.0,"Position":224.572525,"HyperDash":false},{"StartTime":241876.0,"Position":220.428436,"HyperDash":false}]},{"StartTime":242325.0,"Objects":[{"StartTime":242325.0,"Position":297.0,"HyperDash":false},{"StartTime":242380.0,"Position":294.727844,"HyperDash":false},{"StartTime":242436.0,"Position":344.7645,"HyperDash":false},{"StartTime":242492.0,"Position":365.6436,"HyperDash":false},{"StartTime":242548.0,"Position":374.1311,"HyperDash":false},{"StartTime":242604.0,"Position":400.993958,"HyperDash":false},{"StartTime":242660.0,"Position":429.0038,"HyperDash":false},{"StartTime":242716.0,"Position":433.924957,"HyperDash":false},{"StartTime":242772.0,"Position":449.6772,"HyperDash":false},{"StartTime":242823.0,"Position":429.0385,"HyperDash":false},{"StartTime":242875.0,"Position":395.5763,"HyperDash":false},{"StartTime":242926.0,"Position":382.350677,"HyperDash":false},{"StartTime":242978.0,"Position":386.8408,"HyperDash":false},{"StartTime":243029.0,"Position":359.934326,"HyperDash":false},{"StartTime":243081.0,"Position":325.105682,"HyperDash":false},{"StartTime":243132.0,"Position":316.240143,"HyperDash":false},{"StartTime":243220.0,"Position":297.0,"HyperDash":false}]},{"StartTime":243444.0,"Objects":[{"StartTime":243444.0,"Position":216.0,"HyperDash":false}]},{"StartTime":243668.0,"Objects":[{"StartTime":243668.0,"Position":136.0,"HyperDash":false},{"StartTime":243761.0,"Position":118.763763,"HyperDash":false},{"StartTime":243891.0,"Position":56.3044968,"HyperDash":false}]},{"StartTime":244116.0,"Objects":[{"StartTime":244116.0,"Position":2.0,"HyperDash":false},{"StartTime":244167.0,"Position":18.7359352,"HyperDash":false},{"StartTime":244218.0,"Position":16.5715733,"HyperDash":false},{"StartTime":244270.0,"Position":30.5787716,"HyperDash":false},{"StartTime":244321.0,"Position":26.5514565,"HyperDash":false},{"StartTime":244372.0,"Position":31.5832443,"HyperDash":false},{"StartTime":244424.0,"Position":12.6607952,"HyperDash":false},{"StartTime":244475.0,"Position":44.7331161,"HyperDash":false},{"StartTime":244563.0,"Position":29.33616,"HyperDash":false}]},{"StartTime":244787.0,"Objects":[{"StartTime":244787.0,"Position":5.0,"HyperDash":false}]},{"StartTime":245011.0,"Objects":[{"StartTime":245011.0,"Position":64.0,"HyperDash":false},{"StartTime":245104.0,"Position":111.355347,"HyperDash":false},{"StartTime":245234.0,"Position":143.98111,"HyperDash":false}]},{"StartTime":245459.0,"Objects":[{"StartTime":245459.0,"Position":223.0,"HyperDash":false},{"StartTime":245552.0,"Position":239.355347,"HyperDash":false},{"StartTime":245682.0,"Position":302.9811,"HyperDash":false}]},{"StartTime":245907.0,"Objects":[{"StartTime":245907.0,"Position":379.0,"HyperDash":false},{"StartTime":245958.0,"Position":388.482635,"HyperDash":false},{"StartTime":246009.0,"Position":389.9838,"HyperDash":false},{"StartTime":246061.0,"Position":389.548859,"HyperDash":false},{"StartTime":246112.0,"Position":388.03,"HyperDash":false},{"StartTime":246163.0,"Position":411.496918,"HyperDash":false},{"StartTime":246215.0,"Position":407.907623,"HyperDash":false},{"StartTime":246266.0,"Position":408.277283,"HyperDash":false},{"StartTime":246354.0,"Position":392.807251,"HyperDash":false}]},{"StartTime":246802.0,"Objects":[{"StartTime":246802.0,"Position":240.0,"HyperDash":false},{"StartTime":246895.0,"Position":229.351563,"HyperDash":false},{"StartTime":247025.0,"Position":219.99736,"HyperDash":false}]},{"StartTime":247250.0,"Objects":[{"StartTime":247250.0,"Position":152.0,"HyperDash":false},{"StartTime":247343.0,"Position":152.648453,"HyperDash":false},{"StartTime":247473.0,"Position":172.00264,"HyperDash":false}]},{"StartTime":247698.0,"Objects":[{"StartTime":247698.0,"Position":118.0,"HyperDash":false},{"StartTime":247749.0,"Position":132.050278,"HyperDash":false},{"StartTime":247800.0,"Position":136.635315,"HyperDash":false},{"StartTime":247852.0,"Position":178.833282,"HyperDash":false},{"StartTime":247903.0,"Position":187.755432,"HyperDash":false},{"StartTime":247954.0,"Position":198.440247,"HyperDash":false},{"StartTime":248006.0,"Position":210.910767,"HyperDash":false},{"StartTime":248057.0,"Position":231.14679,"HyperDash":false},{"StartTime":248145.0,"Position":263.981781,"HyperDash":false}]},{"StartTime":248593.0,"Objects":[{"StartTime":248593.0,"Position":427.0,"HyperDash":false},{"StartTime":248644.0,"Position":435.745178,"HyperDash":false},{"StartTime":248695.0,"Position":436.695557,"HyperDash":false},{"StartTime":248747.0,"Position":452.8285,"HyperDash":false},{"StartTime":248798.0,"Position":483.6382,"HyperDash":false},{"StartTime":248849.0,"Position":486.1087,"HyperDash":false},{"StartTime":248901.0,"Position":482.262,"HyperDash":false},{"StartTime":248952.0,"Position":487.997559,"HyperDash":false},{"StartTime":249040.0,"Position":466.941925,"HyperDash":false}]},{"StartTime":249489.0,"Objects":[{"StartTime":249489.0,"Position":411.0,"HyperDash":false},{"StartTime":249544.0,"Position":390.345581,"HyperDash":false},{"StartTime":249600.0,"Position":379.354645,"HyperDash":false},{"StartTime":249656.0,"Position":353.452728,"HyperDash":false},{"StartTime":249712.0,"Position":332.7199,"HyperDash":false},{"StartTime":249768.0,"Position":300.245636,"HyperDash":false},{"StartTime":249824.0,"Position":302.125122,"HyperDash":false},{"StartTime":249880.0,"Position":276.454742,"HyperDash":false},{"StartTime":249936.0,"Position":257.2194,"HyperDash":false},{"StartTime":249992.0,"Position":266.046967,"HyperDash":false},{"StartTime":250048.0,"Position":259.874542,"HyperDash":false},{"StartTime":250104.0,"Position":287.702118,"HyperDash":false},{"StartTime":250160.0,"Position":273.529663,"HyperDash":false},{"StartTime":250216.0,"Position":266.357239,"HyperDash":false},{"StartTime":250272.0,"Position":273.1848,"HyperDash":false},{"StartTime":250328.0,"Position":293.0124,"HyperDash":false},{"StartTime":250384.0,"Position":303.839966,"HyperDash":false},{"StartTime":250435.0,"Position":300.715057,"HyperDash":false},{"StartTime":250487.0,"Position":268.3105,"HyperDash":false},{"StartTime":250538.0,"Position":273.694855,"HyperDash":false},{"StartTime":250590.0,"Position":252.267029,"HyperDash":false},{"StartTime":250641.0,"Position":220.7485,"HyperDash":false},{"StartTime":250693.0,"Position":222.549347,"HyperDash":false},{"StartTime":250744.0,"Position":192.463943,"HyperDash":false},{"StartTime":250832.0,"Position":161.041138,"HyperDash":false}]},{"StartTime":251280.0,"Objects":[{"StartTime":251280.0,"Position":21.0,"HyperDash":false},{"StartTime":251363.0,"Position":30.73253,"HyperDash":false},{"StartTime":251447.0,"Position":6.497982,"HyperDash":false},{"StartTime":251531.0,"Position":32.2634354,"HyperDash":false},{"StartTime":251615.0,"Position":32.04535,"HyperDash":false},{"StartTime":251690.0,"Position":26.5926552,"HyperDash":false},{"StartTime":251765.0,"Position":30.1235,"HyperDash":false},{"StartTime":251840.0,"Position":42.65435,"HyperDash":false},{"StartTime":251951.0,"Position":21.0,"HyperDash":false}]},{"StartTime":252175.0,"Objects":[{"StartTime":252175.0,"Position":2.0,"HyperDash":false},{"StartTime":252226.0,"Position":0.0,"HyperDash":false},{"StartTime":252277.0,"Position":0.0,"HyperDash":false},{"StartTime":252329.0,"Position":0.0,"HyperDash":false},{"StartTime":252380.0,"Position":21.3323555,"HyperDash":false},{"StartTime":252431.0,"Position":0.887104034,"HyperDash":false},{"StartTime":252483.0,"Position":30.8665218,"HyperDash":false},{"StartTime":252534.0,"Position":43.70045,"HyperDash":false},{"StartTime":252622.0,"Position":61.3145256,"HyperDash":false}]},{"StartTime":253071.0,"Objects":[{"StartTime":253071.0,"Position":388.0,"HyperDash":false}]},{"StartTime":259563.0,"Objects":[{"StartTime":259563.0,"Position":293.0,"HyperDash":false},{"StartTime":259618.0,"Position":312.5664,"HyperDash":false},{"StartTime":259674.0,"Position":329.488525,"HyperDash":false},{"StartTime":259730.0,"Position":338.410675,"HyperDash":false},{"StartTime":259786.0,"Position":372.510681,"HyperDash":false},{"StartTime":259880.0,"Position":328.247833,"HyperDash":false},{"StartTime":260010.0,"Position":293.0,"HyperDash":false}]},{"StartTime":260235.0,"Objects":[{"StartTime":260235.0,"Position":46.0,"HyperDash":false}]},{"StartTime":260683.0,"Objects":[{"StartTime":260683.0,"Position":115.0,"HyperDash":false},{"StartTime":260766.0,"Position":105.132309,"HyperDash":false},{"StartTime":260850.0,"Position":48.58029,"HyperDash":false},{"StartTime":260934.0,"Position":44.7662964,"HyperDash":false},{"StartTime":261018.0,"Position":0.7381573,"HyperDash":false},{"StartTime":261093.0,"Position":18.1906548,"HyperDash":false},{"StartTime":261168.0,"Position":39.9074936,"HyperDash":false},{"StartTime":261243.0,"Position":61.8353424,"HyperDash":false},{"StartTime":261354.0,"Position":115.0,"HyperDash":false}]},{"StartTime":261578.0,"Objects":[{"StartTime":261578.0,"Position":189.0,"HyperDash":false},{"StartTime":261671.0,"Position":204.326355,"HyperDash":false},{"StartTime":261801.0,"Position":268.91156,"HyperDash":false}]},{"StartTime":262026.0,"Objects":[{"StartTime":262026.0,"Position":334.0,"HyperDash":false},{"StartTime":262119.0,"Position":377.326355,"HyperDash":false},{"StartTime":262249.0,"Position":413.91156,"HyperDash":false}]},{"StartTime":262474.0,"Objects":[{"StartTime":262474.0,"Position":480.0,"HyperDash":false},{"StartTime":262557.0,"Position":478.9318,"HyperDash":false},{"StartTime":262641.0,"Position":487.9834,"HyperDash":false},{"StartTime":262725.0,"Position":487.311127,"HyperDash":false},{"StartTime":262809.0,"Position":470.9604,"HyperDash":false},{"StartTime":262884.0,"Position":456.461121,"HyperDash":false},{"StartTime":262959.0,"Position":484.5402,"HyperDash":false},{"StartTime":263034.0,"Position":483.23175,"HyperDash":false},{"StartTime":263145.0,"Position":480.0,"HyperDash":false}]},{"StartTime":263369.0,"Objects":[{"StartTime":263369.0,"Position":497.0,"HyperDash":false},{"StartTime":263462.0,"Position":512.0,"HyperDash":false},{"StartTime":263592.0,"Position":495.6382,"HyperDash":false}]},{"StartTime":263817.0,"Objects":[{"StartTime":263817.0,"Position":374.0,"HyperDash":false}]},{"StartTime":264265.0,"Objects":[{"StartTime":264265.0,"Position":262.0,"HyperDash":false},{"StartTime":264348.0,"Position":218.500381,"HyperDash":false},{"StartTime":264432.0,"Position":198.645355,"HyperDash":false},{"StartTime":264516.0,"Position":171.790344,"HyperDash":false},{"StartTime":264600.0,"Position":142.7576,"HyperDash":false},{"StartTime":264675.0,"Position":162.23616,"HyperDash":false},{"StartTime":264750.0,"Position":212.892441,"HyperDash":false},{"StartTime":264825.0,"Position":225.5487,"HyperDash":false},{"StartTime":264936.0,"Position":262.0,"HyperDash":false}]},{"StartTime":265160.0,"Objects":[{"StartTime":265160.0,"Position":329.0,"HyperDash":false},{"StartTime":265253.0,"Position":325.012848,"HyperDash":false},{"StartTime":265383.0,"Position":319.4394,"HyperDash":false}]},{"StartTime":265608.0,"Objects":[{"StartTime":265608.0,"Position":254.0,"HyperDash":false},{"StartTime":265701.0,"Position":204.112839,"HyperDash":false},{"StartTime":265831.0,"Position":176.4014,"HyperDash":false}]},{"StartTime":266056.0,"Objects":[{"StartTime":266056.0,"Position":95.0,"HyperDash":false},{"StartTime":266139.0,"Position":98.58954,"HyperDash":false},{"StartTime":266223.0,"Position":69.13798,"HyperDash":false},{"StartTime":266307.0,"Position":66.6864243,"HyperDash":false},{"StartTime":266391.0,"Position":81.214325,"HyperDash":false},{"StartTime":266466.0,"Position":67.27553,"HyperDash":false},{"StartTime":266541.0,"Position":104.357269,"HyperDash":false},{"StartTime":266616.0,"Position":108.439018,"HyperDash":false},{"StartTime":266727.0,"Position":95.0,"HyperDash":false}]},{"StartTime":266951.0,"Objects":[{"StartTime":266951.0,"Position":146.0,"HyperDash":false}]},{"StartTime":267175.0,"Objects":[{"StartTime":267175.0,"Position":210.0,"HyperDash":false}]},{"StartTime":267399.0,"Objects":[{"StartTime":267399.0,"Position":264.0,"HyperDash":false},{"StartTime":267492.0,"Position":314.92868,"HyperDash":false},{"StartTime":267622.0,"Position":342.981537,"HyperDash":false}]},{"StartTime":267847.0,"Objects":[{"StartTime":267847.0,"Position":395.0,"HyperDash":false},{"StartTime":267930.0,"Position":404.894226,"HyperDash":false},{"StartTime":268014.0,"Position":439.331,"HyperDash":false},{"StartTime":268098.0,"Position":471.236,"HyperDash":false},{"StartTime":268182.0,"Position":509.962616,"HyperDash":false},{"StartTime":268257.0,"Position":484.739044,"HyperDash":false},{"StartTime":268332.0,"Position":448.118866,"HyperDash":false},{"StartTime":268407.0,"Position":434.531128,"HyperDash":false},{"StartTime":268518.0,"Position":395.0,"HyperDash":false}]},{"StartTime":268742.0,"Objects":[{"StartTime":268742.0,"Position":326.0,"HyperDash":false},{"StartTime":268797.0,"Position":303.009155,"HyperDash":false},{"StartTime":268853.0,"Position":275.691162,"HyperDash":false},{"StartTime":268909.0,"Position":273.2251,"HyperDash":false},{"StartTime":268965.0,"Position":238.87439,"HyperDash":false},{"StartTime":269021.0,"Position":234.523651,"HyperDash":false},{"StartTime":269077.0,"Position":225.172928,"HyperDash":false},{"StartTime":269133.0,"Position":194.8222,"HyperDash":false},{"StartTime":269189.0,"Position":174.471481,"HyperDash":false},{"StartTime":269283.0,"Position":186.943192,"HyperDash":false},{"StartTime":269413.0,"Position":214.870941,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1431386.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1431386.osu new file mode 100644 index 0000000000..aa82b6ef8c --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1431386.osu @@ -0,0 +1,560 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 +Mode: 0 + +[Difficulty] +HPDrainRate:4 +CircleSize:3.3 +OverallDifficulty:4 +ApproachRate:5 +SliderMultiplier:1.6 +SliderTickRate:1 + +[Events] +//Background and Video events +//Break Periods +2,86704,92468 +2,208494,214259 +2,253271,258363 +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples + +[TimingPoints] +534,447.761194029851,4,2,1,40,1,0 +4116,-100,4,2,1,20,0,0 +4563,-100,4,2,1,40,0,0 +5011,-100,4,2,1,60,0,0 +5459,-100,4,2,1,80,0,0 +5907,-100,4,2,1,80,0,0 +6242,-100,4,2,1,80,0,0 +6578,-100,4,2,1,80,0,0 +6802,-100,4,2,1,80,0,0 +7250,-100,4,2,1,80,0,0 +7922,-100,4,2,1,80,0,0 +14862,-100,4,2,1,80,0,0 +16653,-100,4,2,1,80,0,0 +22922,-100,4,2,1,80,0,0 +23369,-100,4,2,1,80,0,0 +24041,-100,4,2,1,80,0,0 +24265,-100,4,2,1,80,0,0 +26728,-100,4,2,1,80,0,0 +26951,-100,4,2,1,80,0,0 +27175,-100,4,2,1,80,0,0 +27399,-100,4,2,1,80,0,0 +29414,-100,4,2,1,80,0,0 +30981,-100,4,2,1,80,0,0 +38145,-100,4,2,1,80,0,0 +40832,-100,4,2,1,80,0,0 +41728,-100,4,2,1,80,0,0 +45310,-100,4,2,1,40,0,0 +45757,-100,4,2,1,40,0,0 +46205,-100,4,2,1,60,0,0 +46653,-100,4,2,1,80,0,0 +47101,-100,4,2,1,80,0,1 +47436,-100,4,2,1,80,0,1 +47772,-100,4,2,1,80,0,1 +47996,-100,4,2,1,80,0,0 +48892,-100,4,2,1,80,0,1 +56504,-100,4,2,1,80,0,1 +56728,-100,4,2,1,80,0,1 +58295,-100,4,2,1,80,0,1 +58519,-100,4,2,1,80,0,1 +62772,-100,4,2,1,80,0,1 +63220,-100,4,2,1,80,0,1 +63444,-100,4,2,1,80,0,1 +63668,-100,4,2,1,80,0,1 +70832,-100,4,2,1,80,0,1 +71056,-100,4,2,1,80,0,1 +72623,-100,4,2,1,80,0,1 +73071,-100,4,2,1,80,0,1 +73519,-100,4,2,1,80,0,1 +73966,-100,4,2,1,80,0,1 +75757,-100,4,2,1,80,0,1 +75981,-100,4,2,1,80,0,1 +77325,-100,4,2,1,80,0,1 +77548,-100,4,2,1,80,0,0 +80235,-100,4,2,1,80,0,0 +81131,-100,4,2,1,80,0,0 +82922,-100,4,2,1,80,0,0 +84713,-100,4,2,1,80,0,1 +85048,-100,4,2,1,80,0,1 +85384,-100,4,2,1,80,0,1 +85608,-100,4,2,1,80,0,1 +86056,-100,4,2,1,80,0,0 +93444,-100,4,2,1,80,0,0 +93668,-100,4,2,1,80,0,0 +101728,-100,4,2,1,80,0,0 +102175,-100,4,2,0,80,0,0 +102623,-100,4,2,1,80,0,0 +102847,-100,4,2,1,80,0,0 +103071,-100,4,2,1,80,0,0 +116951,-100,4,2,1,80,0,0 +119638,-100,4,2,1,80,0,0 +120534,-100,4,2,1,80,0,0 +124116,-100,4,2,0,80,0,0 +125459,-100,4,2,1,80,0,0 +125907,-100,4,2,1,80,0,1 +126242,-100,4,2,1,80,0,1 +126578,-100,4,2,1,80,0,1 +126802,-100,4,2,1,80,0,0 +127474,-100,4,2,1,80,0,0 +127698,-100,4,2,1,80,0,1 +135310,-100,4,2,1,80,0,1 +135534,-100,4,2,1,80,0,1 +137101,-100,4,2,1,80,0,1 +137325,-100,4,2,1,80,0,1 +142250,-100,4,2,1,80,0,1 +142474,-100,4,2,1,80,0,1 +149638,-100,4,2,1,80,0,1 +149862,-100,4,2,1,80,0,1 +151429,-100,4,2,1,80,0,1 +151877,-100,4,2,1,80,0,1 +152325,-100,4,2,1,80,0,1 +152772,-100,4,2,1,80,0,1 +154563,-100,4,2,1,80,0,1 +154787,-100,4,2,1,80,0,1 +156354,-100,4,2,1,80,0,0 +159041,-100,4,2,1,80,0,0 +159936,-100,4,2,1,80,0,0 +161168,-100,4,2,0,80,0,0 +161728,-100,4,2,1,80,0,0 +162623,-100,4,2,1,80,0,1 +163519,-100,4,2,1,80,0,0 +164414,-100,4,2,1,80,0,1 +165310,-100,4,2,1,80,0,0 +166205,-100,4,2,1,80,0,1 +167101,-100,4,2,1,80,0,0 +168332,-100,4,2,1,80,0,0 +168892,-100,4,2,1,80,0,0 +169787,-100,4,2,1,80,0,1 +170683,-100,4,2,1,80,0,0 +171578,-100,4,2,1,80,0,1 +172474,-100,4,2,1,80,0,0 +173369,-100,4,2,1,80,0,1 +173705,-100,4,2,1,80,0,0 +173929,-100,4,2,0,80,0,0 +174265,-100,4,2,1,80,0,0 +175048,-100,4,2,1,80,0,0 +175608,-100,4,2,1,80,0,0 +175832,-100,4,2,1,80,0,0 +176056,-100,4,2,1,80,0,1 +186131,-100,4,2,1,80,0,1 +186354,-100,4,2,1,80,0,1 +190384,-100,4,2,1,80,0,0 +206504,-100,4,2,1,80,0,0 +206839,-100,4,2,1,80,0,0 +207175,-100,4,2,1,80,0,0 +207399,-100,4,2,1,80,0,0 +207623,-100,4,2,1,80,0,0 +208071,-100,4,2,1,80,0,0 +208295,-100,4,2,1,80,0,0 +219041,-100,4,2,1,80,0,0 +220832,-100,4,2,1,80,0,0 +223071,-100,4,2,1,80,0,1 +223407,-100,4,2,1,80,0,0 +224414,-100,4,2,1,80,0,1 +225086,-100,4,2,1,80,0,1 +225422,-100,4,2,1,80,0,0 +226205,-100,4,2,1,80,0,1 +230235,-100,4,2,1,80,0,1 +230683,-100,4,2,1,80,0,1 +232026,-100,4,2,1,80,0,1 +232474,-100,4,2,1,80,0,1 +232922,-100,4,2,1,80,0,1 +233369,-100,4,2,1,80,0,1 +236951,-100,4,2,1,80,0,0 +239414,-100,4,2,1,80,0,0 +240533,-100,4,2,1,80,0,0 +244116,-100,4,2,1,80,0,0 +247698,-100,4,2,1,80,0,0 +250384,-100,4,2,1,80,0,0 +251280,-100,4,2,1,80,0,0 +251616,-100,4,2,1,80,0,0 +251951,-100,4,2,1,80,0,0 +252175,-100,4,2,1,80,0,0 +252623,-100,4,2,1,80,0,0 +253071,-100,4,2,1,80,0,0 +259563,-100,4,2,1,80,0,0 +260235,-100,4,2,1,80,0,0 +263593,-100,4,2,1,80,0,0 +263817,-100,4,2,1,80,0,0 +267399,-100,4,2,1,80,0,0 +268742,-100,4,2,1,80,0,0 +269414,-100,4,2,1,5,0,0 + +[HitObjects] +333,114,534,6,0,B|379:97|379:97|497:110,2,160,4|0|0,3:2|0:2|0:2,0:0:0:0: +182,204,1877,1,0,0:2:0:0: +333,290,2325,6,0,B|385:301|385:301|441:292|441:292|497:303,2,160,4|0|0,3:2|0:2|0:2,0:0:0:0: +182,204,3668,1,0,0:2:0:0: +26,121,4116,6,0,L|30:34,2,80,12|8|8,3:2|0:2|0:2,0:0:0:0: +20,297,5011,2,0,P|58:297|100:311,1,80,8|8,0:2|0:2,0:0:0:0: +178,348,5459,2,0,P|217:335|258:335,1,80,8|8,0:2|0:2,0:0:0:0: +308,264,5907,6,0,L|445:280,2,120,12|12|12,3:2|3:2|3:2,0:0:0:0: +224,234,6802,2,0,P|197:171|223:66,1,160,12|12,3:2|0:2,0:0:0:0: +372,10,7698,6,0,P|389:44|391:94,1,80,4|8,3:2|0:2,0:0:0:0: +390,173,8145,2,0,L|516:164,2,120,0|0|8,3:2|0:2|0:2,0:0:0:0: +330,237,9041,2,0,L|234:230,1,80,0|8,3:2|0:2,0:0:0:0: +171,190,9489,6,0,P|118:184|79:197,1,80,0|8,3:2|0:2,0:0:0:0: +9,219,9936,2,0,L|0:99,2,120,0|0|8,3:2|0:2|0:2,0:0:0:0: +28,305,10832,2,0,P|67:300|105:311,1,80,0|8,3:2|0:2,0:0:0:0: +184,353,11280,5,0,3:2:0:0: +343,277,11728,2,0,P|404:295|470:285,2,120,0|0|8,3:2|0:2|0:2,0:0:0:0: +290,206,12623,2,0,L|297:118,1,80,0|8,3:2|0:2,0:0:0:0: +265,43,13071,6,0,P|222:34|179:42,1,80,0|8,3:2|0:2,0:0:0:0: +123,100,13519,2,0,L|3:92,2,120,0|0|0,3:2|0:2|0:2,0:0:0:0: +187,160,14414,1,8,0:2:0:0: +184,336,14862,6,0,P|218:348|274:342,1,80,4|2,3:2|0:2,0:0:0:0: +343,310,15310,2,0,L|466:328,2,120,2|2|0,0:2|0:2|3:2,0:0:0:0: +297,234,16205,1,8,0:2:0:0: +219,76,16653,6,0,P|176:72|131:90,1,80,4|8,3:2|0:2,0:0:0:0: +65,129,17101,2,0,P|26:85|17:27,2,120,0|0|8,3:2|0:2|0:2,0:0:0:0: +144,170,17996,2,0,L|137:250,1,80,0|8,3:2|0:2,0:0:0:0: +156,336,18444,6,0,P|198:347|241:341,1,80,0|8,3:2|0:2,0:0:0:0: +309,296,18892,2,0,L|430:310,2,120,0|0|8,3:2|0:2|0:2,0:0:0:0: +237,245,19787,2,0,P|229:197|236:162,1,80,0|8,3:2|0:2,0:0:0:0: +296,103,20235,6,0,P|344:95|379:102,1,80,0|8,3:2|0:2,0:0:0:0: +441,157,20683,2,0,P|423:95|448:35,2,120,0|0|8,3:2|0:2|0:2,0:0:0:0: +501,220,21578,1,0,3:2:0:0: +386,353,22026,6,0,B|304:367|241:304|241:304|164:362|79:328,2,320,0|2|0,3:2|3:2|3:2,0:0:0:0: +465,315,24041,5,12,0:2:0:0: +497,233,24265,2,0,L|486:100,2,120,0|0|8,3:2|0:2|0:2,0:0:0:0: +410,247,25160,2,0,P|365:251|331:241,1,80,0|8,3:2|0:2,0:0:0:0: +262,187,25608,6,0,P|223:176|183:181,1,80,0|8,3:2|0:2,0:0:0:0: +136,254,26056,2,0,L|145:381,2,120,0|0|8,3:2|0:2|0:2,0:0:0:0: +67,198,26951,1,0,3:2:0:0: +118,29,27399,6,0,P|170:19|228:167,1,240,4|8,3:2|0:2,0:0:0:0: +162,107,28295,2,0,B|240:90|240:90|316:114|316:114|409:97,1,240,4|8,3:2|0:2,0:0:0:0: +481,84,29190,5,0,3:2:0:0: +499,170,29414,1,12,0:2:0:0: +454,246,29638,2,0,L|472:376,2,120,8|8|0,0:2|3:2|0:0,0:0:0:0: +375,205,30533,2,0,P|329:207|286:227,1,80,8|8,0:2|0:2,0:0:0:0: +220,263,30981,6,0,P|144:238|52:250,2,160,4|2|2,3:3|3:3|3:3,0:0:0:0: +365,362,32325,1,2,3:3:0:0: +480,229,32772,6,0,L|464:55,1,160,2|2,3:3|3:3,0:0:0:0: +393,18,33444,1,10,0:3:0:0: +323,72,33668,2,0,L|243:64,1,80,2|10,3:3|0:3,0:0:0:0: +162,27,34116,2,0,L|82:35,1,80,2|10,3:3|0:3,0:0:0:0: +31,106,34563,6,0,P|9:176|23:263,2,160,2|2|2,3:3|3:3|3:3,0:0:0:0: +183,194,35907,1,2,3:3:0:0: +336,278,36354,6,0,P|407:241|496:243,2,160,2|2|0,3:3|3:3|3:2,0:0:0:0: +278,344,37474,1,0,3:2:0:0: +218,278,37698,2,0,P|180:262|137:257,1,80,8|0,0:2|0:2,0:0:0:0: +55,272,38145,6,0,B|29:230|29:230|47:114,1,160,4|8,3:2|0:2,0:0:0:0: +188,16,39041,2,0,B|214:58|214:58|196:174,1,160,2|8,3:3|0:2,0:0:0:0: +305,306,39936,6,0,B|348:305|380:330|380:330|405:305|459:300,1,160,2|8,3:3|0:2,0:0:0:0: +486,127,40832,2,0,P|475:67|430:19,2,120,4|12|12,3:2|0:2|0:2,0:0:0:0: +415,180,41728,6,0,P|334:166|260:194,2,160,8|8|8,0:2|0:2|0:2,0:0:0:0: +353,344,43071,1,8,0:2:0:0: +181,303,43519,6,0,L|16:319,1,160,8|8,0:2|0:2,0:0:0:0: +21,142,44414,2,0,L|186:158,1,160,8|8,0:2|0:2,0:0:0:0: +257,114,45086,1,0,3:2:0:0: +329,63,45310,6,0,P|485:169|281:268,1,480,12|8,0:2|0:2,0:0:0:0: +257,114,47101,6,0,B|200:92|200:92|130:110,2,120,12|12|12,0:2|0:2|0:2,0:0:0:0: +336,151,47996,1,12,0:2:0:0: +417,185,48220,2,0,L|507:180,2,80,8|8|8,0:2|0:2|0:2,0:0:0:0: +379,264,48892,6,0,P|338:281|294:280,1,80,4|10,3:2|0:2,0:0:0:0: +218,257,49339,2,0,P|257:302|263:376,2,120,0|0|10,3:2|0:2|0:2,0:0:0:0: +142,210,50235,2,0,L|135:124,1,80,0|10,3:2|0:2,0:0:0:0: +75,65,50683,6,0,P|132:92|231:60,1,160,0|0,3:2|0:0,0:0:0:0: +295,116,51354,2,0,P|352:89|451:121,1,160,10|10,0:2|0:2,0:0:0:0: +498,180,52026,1,0,3:2:0:0: +404,329,52474,6,0,L|320:323,1,80,0|10,3:2|0:2,0:0:0:0: +251,272,52922,2,0,P|206:288|132:281,2,120,0|0|10,3:2|0:2|0:2,0:0:0:0: +298,196,53817,2,0,L|295:111,1,80,0|10,3:2|0:2,0:0:0:0: +249,40,54265,6,0,B|189:34|189:34|145:50|145:50|73:41,1,160,0|0,3:2|3:2,0:0:0:0: +8,197,55160,2,0,P|46:210|95:206,1,80,0|10,3:2|0:2,0:0:0:0: +165,171,55608,2,0,P|203:158|252:162,1,80,0|10,3:2|0:2,0:0:0:0: +329,173,56056,6,0,B|368:223|368:223|361:320,1,160,4|0,3:2|3:2,0:0:0:0: +189,360,56951,2,0,P|146:358|102:342,1,80,0|10,3:2|0:2,0:0:0:0: +44,288,57399,2,0,P|46:245|62:201,1,80,0|10,3:2|0:2,0:0:0:0: +97,131,57847,6,0,B|153:113|203:139|203:139|258:107,1,160,0|0,3:2|3:2,0:0:0:0: +396,20,58742,2,0,L|409:118,1,80,0|10,3:2|0:2,0:0:0:0: +473,156,59190,2,0,L|460:254,1,80,0|10,3:2|0:2,0:0:0:0: +450,322,59638,6,0,P|380:312|293:343,1,160,4|4,3:2|3:2,0:0:0:0: +215,373,60310,1,10,0:2:0:0: +127,363,60534,2,0,L|121:273,1,80,4|10,3:2|0:0,0:0:0:0: +116,195,60981,1,4,3:2:0:0: +110,18,61429,6,0,P|166:33|232:23,2,120,4|0|10,3:2|0:2|0:2,0:0:0:0: +22,13,62325,2,0,L|18:107,1,80,0|0,3:2|0:2,0:0:0:0: +10,180,62772,1,8,0:2:0:0: +76,238,62996,1,0,3:2:0:0: +154,197,63220,6,0,P|194:194|242:207,1,80,0|14,3:2|0:2,0:0:0:0: +307,250,63668,2,0,L|303:371,2,120,0|0|10,3:2|0:2|0:2,0:0:0:0: +311,162,64563,2,0,P|351:159|399:172,1,80,0|10,3:2|0:2,0:0:0:0: +435,243,65011,6,0,L|427:53,1,160,0|0,3:2|0:0,0:0:0:0: +350,41,65683,2,0,P|282:66|196:50,1,160,10|10,0:2|0:2,0:0:0:0: +116,17,66354,1,0,3:2:0:0: +44,177,66802,6,0,P|40:219|56:268,1,80,0|10,3:2|0:2,0:0:0:0: +131,287,67250,2,0,P|83:294|29:360,2,120,0|0|10,3:2|0:2|0:2,0:0:0:0: +206,332,68145,2,0,L|306:325,1,80,0|10,3:2|0:2,0:0:0:0: +354,270,68593,6,0,B|360:215|360:215|340:168|340:168|348:100,1,160,0|0,3:2|3:2,0:0:0:0: +479,230,69489,2,0,L|470:322,1,80,0|10,3:2|0:2,0:0:0:0: +395,354,69936,2,0,P|357:363|307:352,1,80,0|10,3:2|0:2,0:0:0:0: +239,314,70384,6,0,B|179:303|125:325|125:325|84:301,1,160,4|0,3:2|3:2,0:0:0:0: +11,143,71280,2,0,L|114:130,1,80,0|10,3:2|0:2,0:0:0:0: +152,69,71728,2,0,L|255:82,1,80,0|10,3:2|0:0,0:0:0:0: +271,157,72175,6,0,P|271:100|345:26,1,160,4|4,3:2|3:2,0:0:0:0: +425,16,72847,1,10,0:2:0:0: +489,75,73071,2,0,L|481:176,1,80,4|10,3:2|0:2,0:0:0:0: +408,203,73519,2,0,L|416:304,1,80,4|0,3:2|0:0,0:0:0:0: +482,338,73966,6,0,B|402:317|398:370|320:339,1,160,4|0,3:2|3:2,0:0:0:0: +157,287,74862,2,0,L|71:295,2,80,0|10|0,3:2|0:2|3:2,0:0:0:0: +226,231,75534,1,10,0:2:0:0: +288,169,75757,6,0,P|357:197|451:165,2,160,4|0|0,3:2|3:2|3:2,0:0:0:0: +225,106,76877,2,0,L|233:21,2,80,0|8|0,0:0|0:2|3:2,0:0:0:0: +172,176,77548,6,0,B|145:218|145:218|165:339,1,160,4|12,3:2|0:2,0:0:0:0: +9,239,78444,2,0,B|36:197|36:197|16:76,1,160,0|12,3:2|0:2,0:0:0:0: +186,37,79339,6,0,P|236:79|349:68,1,160,0|12,3:2|0:2,0:0:0:0: +405,37,80011,1,0,0:2:0:0: +482,77,80235,2,0,L|472:159,1,80,0|0,3:2|3:2,0:0:0:0: +392,195,80683,2,0,L|402:277,1,80,12|8,0:2|0:2,0:0:0:0: +474,324,81131,6,0,P|422:301|298:337,1,160,6|2,3:2|0:2,0:0:0:0: +148,296,82026,2,0,P|125:244|161:120,1,160,6|2,3:2|0:2,0:0:0:0: +287,44,82922,6,0,B|364:10|444:39|444:39|356:94|357:165|357:165|441:232|413:331,1,480,6|10,3:2|0:2,0:0:0:0: +242,304,84713,6,0,L|111:320,2,120,12|12|12,3:2|3:2|3:2,0:0:0:0: +277,223,85608,2,0,P|214:163|127:159,1,160,12|8,3:2|0:2,0:0:0:0: +11,270,86504,5,4,3:2:0:0: +321,111,93668,6,0,P|261:76|315:180,2,320,4|0|4,3:2|0:0|3:2,0:0:0:0: +321,111,97250,6,0,B|393:147|468:85|468:85|424:181|468:248,2,320,0|0|0,3:2|0:0|3:2,0:0:0:0: +321,111,100832,6,0,B|284:85|246:78|246:78|175:111|175:111|91:89|91:89|56:104|31:129,2,320,0|2|0,3:2|0:2|3:2,0:0:0:0: +385,170,102847,5,12,0:2:0:0: +322,231,103071,2,0,L|185:220,2,120,0|0|8,3:2|0:2|0:2,0:0:0:0: +404,262,103966,2,0,P|401:311|382:350,1,80,0|8,3:2|0:2,0:0:0:0: +308,374,104414,6,0,P|259:371|220:352,1,80,0|8,3:2|0:2,0:0:0:0: +164,300,104862,2,0,L|35:315,2,120,0|0|8,3:2|0:2|0:2,0:0:0:0: +202,221,105757,1,0,3:2:0:0: +276,61,106205,6,0,P|371:63|426:190,1,240,4|8,3:2|0:2,0:0:0:0: +354,230,107101,2,0,B|280:260|202:209|202:209|162:220|122:249,1,240,4|8,3:2|0:2,0:0:0:0: +55,290,107996,5,0,3:2:0:0: +0,220,108220,1,12,0:2:0:0: +43,143,108444,2,0,L|37:23,2,120,8|8|0,0:2|3:2|0:2,0:0:0:0: +128,164,109339,2,0,P|167:161|212:139,1,80,8|8,0:2|0:2,0:0:0:0: +242,64,109787,6,0,P|227:110|276:215,2,160,4|2|2,3:3|3:3|3:3,0:0:0:0: +411,14,111131,1,2,3:3:0:0: +503,163,111578,6,0,L|484:335,1,160,2|2,3:3|3:3,0:0:0:0: +405,360,112250,1,10,0:3:0:0: +333,308,112474,2,0,L|250:316,1,80,2|10,3:3|0:3,0:0:0:0: +175,357,112922,2,0,L|92:349,1,80,2|10,3:3|0:3,0:0:0:0: +28,292,113369,6,0,P|13:201|47:120,2,160,2|2|2,3:3|3:3|3:3,0:0:0:0: +190,222,114713,1,2,3:3:0:0: +349,148,115160,6,0,B|433:133|419:192|504:176,2,160,2|2|0,3:3|3:3|3:2,0:0:0:0: +265,176,116280,1,0,3:2:0:0: +224,254,116504,2,0,L|239:354,1,80,8|0,0:2|0:2,0:0:0:0: +320,357,116951,6,0,B|428:339|428:339|485:355,1,160,4|8,3:2|0:2,0:0:0:0: +501,176,117847,2,0,B|393:194|393:194|336:178,1,160,2|8,3:3|0:2,0:0:0:0: +200,78,118742,6,0,B|159:68|120:86|120:86|86:64|44:71,1,160,2|8,3:3|0:2,0:0:0:0: +16,244,119638,2,0,L|30:372,2,120,4|12|12,3:2|0:2|0:2,0:0:0:0: +88,193,120534,6,0,B|142:216|142:216|266:202,2,160,8|8|8,0:2|0:2|0:2,0:0:0:0: +172,38,121877,1,8,0:2:0:0: +322,129,122325,6,0,P|351:191|322:281,1,160,8|8,0:2|0:2,0:0:0:0: +150,284,123220,2,0,P|121:222|150:132,1,160,8|8,0:2|0:2,0:0:0:0: +194,63,123892,1,0,3:2:0:0: +277,35,124116,6,0,B|353:64|424:16|424:16|380:99|432:172|432:172|347:133|256:169,1,480,4|8,0:2|0:2,0:0:0:0: +121,246,125907,6,0,L|133:371,2,120,12|12|12,3:2|3:2|3:2,0:0:0:0: +104,160,126802,1,12,3:2:0:0: +88,72,127026,2,0,P|49:66|10:73,2,80,8|8|8,0:2|0:2|0:2,0:0:0:0: +171,103,127698,6,0,L|257:94,1,80,4|10,3:2|0:2,0:0:0:0: +333,66,128145,2,0,P|395:41|463:49,2,120,0|0|10,3:2|0:2|0:2,0:0:0:0: +318,153,129041,2,0,L|312:254,1,80,0|10,3:2|0:2,0:0:0:0: +304,320,129489,6,0,P|367:359|471:352,1,160,0|0,3:2|0:0,0:0:0:0: +506,291,130160,2,0,L|489:116,1,160,10|10,0:2|0:2,0:0:0:0: +483,43,130832,1,0,3:2:0:0: +308,67,131280,6,0,P|270:81|216:76,1,80,0|10,3:2|0:2,0:0:0:0: +142,85,131728,2,0,L|157:220,2,120,0|0|10,3:2|0:2|0:2,0:0:0:0: +55,69,132623,2,0,L|43:169,1,80,0|10,3:2|0:2,0:0:0:0: +33,235,133071,6,0,P|65:294|164:331,1,160,0|0,3:2|3:2,0:0:0:0: +275,210,133966,2,0,L|363:214,1,80,0|10,3:2|0:2,0:0:0:0: +389,294,134414,2,0,L|477:290,1,80,0|10,3:2|0:2,0:0:0:0: +503,208,134862,6,0,B|511:92|511:92|489:44,1,160,4|0,3:2|3:2,0:0:0:0: +318,30,135757,2,0,L|230:34,1,80,0|10,3:2|0:2,0:0:0:0: +204,114,136205,2,0,L|116:110,1,80,0|10,3:2|0:2,0:0:0:0: +49,62,136653,6,0,B|15:110|19:171|19:171|42:219,1,160,0|0,3:2|3:2,0:0:0:0: +200,278,137548,2,0,P|215:245|220:193,1,80,0|10,3:2|0:2,0:0:0:0: +204,114,137996,2,0,P|189:81|184:29,1,80,0|10,3:2|0:2,0:0:0:0: +270,23,138444,6,0,B|322:48|322:48|446:22,1,160,4|4,3:2|3:2,0:0:0:0: +490,83,139116,1,10,0:2:0:0: +504,169,139339,2,0,P|503:213|486:254,1,80,4|10,3:2|0:2,0:0:0:0: +428,309,139787,1,4,3:2:0:0: +268,241,140235,6,0,P|272:303|278:371,2,120,4|0|10,3:2|0:2|0:2,0:0:0:0: +207,176,141131,2,0,L|107:180,1,80,0|0,3:2|0:2,0:0:0:0: +39,184,141578,1,8,0:2:0:0: +8,101,141802,1,0,3:2:0:0: +71,40,142026,6,0,P|127:30|162:36,1,80,0|14,3:2|0:2,0:0:0:0: +220,85,142474,2,0,L|342:76,2,120,0|0|10,3:2|0:2|0:2,0:0:0:0: +158,148,143369,2,0,P|150:196|161:241,1,80,0|10,3:2|0:2,0:0:0:0: +192,306,143817,6,0,B|282:279|272:350|373:314,1,160,0|0,3:2|0:0,0:0:0:0: +431,294,144489,2,0,L|440:125,1,160,10|10,0:2|0:2,0:0:0:0: +448,46,145160,1,0,3:2:0:0: +272,31,145608,6,0,P|223:30|180:43,1,80,0|10,3:2|0:2,0:0:0:0: +127,96,146056,2,0,L|8:86,2,120,0|0|10,3:2|0:2|0:2,0:0:0:0: +193,154,146951,2,0,P|194:203|181:246,1,80,0|10,3:2|0:2,0:0:0:0: +109,276,147399,6,0,B|165:270|165:270|212:283|212:283|271:276,1,160,0|0,3:2|3:2,0:0:0:0: +441,253,148295,2,0,L|445:166,1,80,0|10,3:2|0:2,0:0:0:0: +482,93,148742,2,0,L|478:6,1,80,0|10,3:2|0:2,0:0:0:0: +390,23,149190,6,0,B|351:44|351:44|215:33,1,160,4|0,3:2|3:2,0:0:0:0: +59,21,150086,2,0,P|43:61|44:104,1,80,0|10,3:2|0:2,0:0:0:0: +94,169,150534,2,0,P|110:209|109:252,1,80,0|10,3:2|0:2,0:0:0:0: +42,301,150981,6,0,P|112:280|190:309,1,160,4|4,3:2|3:2,0:0:0:0: +257,368,151653,1,10,0:2:0:0: +335,327,151877,2,0,L|327:241,1,80,4|10,3:2|0:2,0:0:0:0: +264,185,152325,2,0,L|272:99,1,80,12|8,0:0|0:0,0:0:0:0: +318,30,152772,6,0,P|392:21|479:78,1,160,4|0,3:2|3:2,0:0:0:0: +494,234,153668,2,0,L|509:340,2,80,0|10|0,3:2|0:2|3:2,0:0:0:0: +413,198,154339,1,10,0:2:0:0: +332,234,154563,6,0,P|275:249|179:220,2,160,4|0|0,3:2|3:2|3:2,0:0:0:0: +413,198,155683,2,0,L|500:212,2,80,0|8|0,0:2|0:2|3:2,0:0:0:0: +379,116,156354,6,0,B|340:88|340:88|200:105,1,160,4|12,3:2|0:2,0:0:0:0: +103,225,157250,2,0,B|142:197|142:197|282:214,1,160,0|12,3:2|0:2,0:0:0:0: +131,338,158145,6,0,P|53:330|-1:274,1,160,0|12,3:2|0:2,0:0:0:0: +14,187,158817,1,0,0:2:0:0: +54,108,159041,2,0,L|144:98,1,80,0|0,3:2|3:2,0:0:0:0: +194,35,159489,2,0,L|284:45,1,80,12|8,0:2|0:2,0:0:0:0: +354,78,159936,6,0,P|379:136|369:252,1,160,4|0,3:2|0:2,0:0:0:0: +242,346,160832,2,0,P|217:288|227:172,1,160,4|0,3:2|0:2,0:0:0:0: +354,78,161728,5,4,3:2:0:0: +182,37,162175,2,0,L|98:30,1,80,10|2,0:2|3:2,0:0:0:0: +22,68,162623,2,0,B|5:128|5:128|20:185,2,120,12|12|12,0:2|0:2|0:2,0:0:0:0: +98,112,163519,5,4,3:2:0:0: +202,253,163966,2,0,L|303:248,1,80,10|2,0:2|3:2,0:0:0:0: +355,199,164414,2,0,B|415:182|415:182|472:197,2,120,12|12|12,0:2|0:2|0:2,0:0:0:0: +274,161,165310,5,4,3:2:0:0: +110,225,165757,2,0,L|125:325,1,80,10|2,0:2|3:2,0:0:0:0: +188,362,166205,2,0,B|248:379|248:379|305:364,2,120,12|12|12,0:2|0:2|0:2,0:0:0:0: +206,275,167101,6,0,P|262:242|380:262,2,160,4|12|12,3:2|0:2|0:2,0:0:0:0: +98,352,168332,2,0,L|78:212,1,120,4|8,3:2|0:2,0:0:0:0: +74,144,168892,5,4,3:2:0:0: +246,110,169339,2,0,L|330:120,1,80,10|2,0:2|3:2,0:0:0:0: +385,184,169787,2,0,B|445:167|445:167|502:182,2,120,12|12|12,0:2|0:2|0:2,0:0:0:0: +304,221,170683,5,4,3:2:0:0: +161,117,171131,2,0,L|59:124,1,80,10|2,0:2|3:2,0:0:0:0: +22,188,171578,2,0,B|5:248|5:248|20:305,2,120,12|12|12,0:2|0:2|0:2,0:0:0:0: +108,207,172474,1,4,3:2:0:0: +279,244,172922,6,0,L|365:238,1,80,10|2,0:2|3:2,0:0:0:0: +385,154,173369,2,0,B|445:171|445:171|502:156,2,120,12|12|4,0:2|0:2|0:3,0:0:0:0: +307,111,174265,6,0,L|211:122,1,80,6|2,0:2|3:2,0:0:0:0: +148,159,174713,2,0,L|5:142,2,120,14|6|0,0:2|3:2|0:2,0:0:0:0: +222,206,175608,1,8,0:2:0:0: +387,266,176056,6,0,P|416:206|409:150,2,120,6|2|8,3:2|0:2|0:2,0:0:0:0: +302,291,176951,2,0,B|212:264|234:332|122:296,1,160,0|2,3:2|3:2,0:0:0:0: +66,266,177623,1,10,0:2:0:0: +93,182,177847,6,0,L|197:173,2,80,2|10|2,3:2|0:2|3:2,0:0:0:0: +20,131,178519,2,0,P|47:58|150:26,1,160,10|8,0:2|0:2,0:0:0:0: +205,17,179190,1,0,3:2:0:0: +381,12,179638,6,0,L|485:21,1,80,2|10,3:2|0:2,0:0:0:0: +499,99,180086,2,0,P|500:152|472:220,2,120,2|2|10,3:2|0:2|0:2,0:0:0:0: +411,111,180981,1,2,3:2:0:0: +237,142,181429,6,0,L|139:134,1,80,2|10,3:2|0:2,0:0:0:0: +69,124,181877,2,0,P|48:55|48:-4,2,120,2|2|12,3:2|0:2|0:2,0:0:0:0: +102,205,182772,1,4,3:2:0:0: +172,258,182996,1,12,0:2:0:0: +258,276,183220,6,0,B|350:261|319:316|412:306,1,160,2|2,3:2|3:2,0:0:0:0: +500,154,184116,2,0,L|509:25,2,120,2|2|10,3:2|0:2|0:2,0:0:0:0: +424,198,185011,6,0,P|354:203|335:196,1,80,2|10,3:2|0:2,0:0:0:0: +273,148,185459,2,0,P|185:136|141:162,1,120,2|2,3:2|0:2,0:0:0:0: +66,243,186131,2,0,B|108:269|108:269|218:257,1,160,14|10,0:2|0:2,0:0:0:0: +301,230,186802,6,0,L|398:240,1,80,2|10,3:2|0:2,0:0:0:0: +468,250,187250,2,0,P|483:178|488:111,2,120,2|2|10,3:2|0:2|0:2,0:0:0:0: +430,329,188145,1,2,3:2:0:0: +255,364,188593,6,0,L|157:353,1,80,2|14,3:2|0:2,0:0:0:0: +140,274,189041,2,0,P|68:289|1:294,2,120,2|2|0,3:2|3:2|0:0,0:0:0:0: +205,215,189936,1,0,3:2:0:0: +297,64,190384,6,0,L|424:52,2,120,4|0|10,3:3|0:2|0:3,0:0:0:0: +233,125,191280,2,0,P|263:228|384:244,1,240,2|10,3:3|0:3,0:0:0:0: +468,231,192175,6,0,L|462:375,2,120,2|0|10,3:3|0:2|0:3,0:0:0:0: +497,146,193071,2,0,P|461:39|348:26,1,240,2|10,3:3|0:3,0:0:0:0: +292,94,193966,6,0,L|298:238,2,120,2|0|10,3:3|0:2|0:3,0:0:0:0: +233,27,194862,2,0,P|120:39|84:147,1,240,2|10,3:3|0:3,0:0:0:0: +120,227,195757,5,2,3:3:0:0: +292,261,196205,2,0,L|436:247,2,120,2|0|10,3:3|0:2|0:3,0:0:0:0: +224,317,197101,2,0,L|124:307,1,80,2|10,3:3|0:3,0:0:0:0: +66,267,197548,6,0,P|49:324|12:370,2,120,6|0|10,3:3|0:2|0:3,0:0:0:0: +42,181,198444,2,0,P|104:79|251:69,1,240,2|10,3:3|0:3,0:0:0:0: +292,100,199339,6,0,B|344:83|344:83|418:94,2,120,2|0|10,3:3|0:2|0:3,0:0:0:0: +235,168,200235,2,0,P|259:282|359:341,1,240,2|10,3:3|0:3,0:0:0:0: +447,330,201131,5,6,3:3:0:0: +472,156,201578,2,0,L|371:143,1,80,2|10,3:3|0:3,0:0:0:0: +323,90,202026,2,0,P|264:83|212:50,2,120,6|0|10,3:3|0:2|0:3,0:0:0:0: +370,15,202922,5,6,3:3:0:0: +472,156,203369,2,0,L|457:251,1,80,2|10,3:3|0:3,0:0:0:0: +373,256,203817,2,0,P|397:327|399:371,2,120,6|0|10,3:3|0:2|0:3,0:0:0:0: +294,214,204713,6,0,B|224:243|224:243|111:225,1,160,6|6,3:3|3:3,0:0:0:0: +29,93,205608,2,0,B|99:64|99:64|212:82,1,160,6|10,3:3|0:3,0:0:0:0: +267,99,206280,1,2,3:3:0:0: +344,141,206504,6,0,P|407:124|472:149,2,120,12|12|12,3:2|3:2|3:2,0:0:0:0: +294,214,207399,2,0,P|325:292|499:216,1,320,12|4,3:2|3:2,0:0:0:0: +256,192,215459,12,6,218145,3:2:0:0: +205,114,219041,6,0,B|119:107|119:107|44:141,1,160,12|12,3:2|3:2,0:0:0:0: +75,311,219936,2,0,B|161:318|161:318|236:284,1,160,12|12,3:2|3:2,0:0:0:0: +337,149,220832,6,0,L|325:15,2,120,12|12|12,3:2|3:2|3:2,0:0:0:0: +457,277,221951,2,0,L|447:377,2,80,0|8|8,3:2|0:2|0:2,0:0:0:0: +471,189,222623,5,2,0:2:0:0: +331,81,223071,2,0,B|279:103|279:103|200:94,2,120,4|8|0,3:2|0:2|3:2,0:0:0:0: +399,26,223966,1,8,0:2:0:0: +471,189,224414,6,0,L|453:333,1,120,12|12,3:2|3:2,0:0:0:0: +326,335,225086,2,0,B|276:306|276:306|149:326,1,160,12|0,3:2|0:2,0:0:0:0: +88,340,225757,2,0,P|75:299|76:251,1,80,8|8,0:2|0:2,0:0:0:0: +140,204,226205,6,0,L|144:123,1,80,4|10,3:2|0:2,0:0:0:0: +116,40,226653,2,0,P|58:49|3:25,2,120,0|0|10,3:2|0:2|0:2,0:0:0:0: +202,21,227548,2,0,L|283:25,1,80,0|10,3:2|0:2,0:0:0:0: +370,29,227996,6,0,B|404:72|404:72|392:196,1,160,0|0,3:2|3:2,0:0:0:0: +291,320,228892,2,0,L|178:329,1,80,0|10,3:2|0:2,0:0:0:0: +136,373,229339,2,0,L|23:364,1,80,0|10,3:2|0:2,0:0:0:0: +20,285,229787,6,0,B|8:231|8:231|24:183|24:183|14:121,1,160,4|0,3:2|3:2,0:0:0:0: +156,24,230683,2,0,P|182:74|187:103,1,80,0|10,3:2|0:2,0:0:0:0: +264,138,231131,2,0,P|238:188|233:217,1,80,0|10,3:2|0:2,0:0:0:0: +262,293,231578,6,0,B|312:314|312:314|440:299,1,160,4|4,3:2|3:2,0:0:0:0: +479,239,232250,1,10,0:2:0:0: +500,153,232474,2,0,P|499:119|481:77,1,80,4|10,3:2|0:2,0:0:0:0: +396,50,232922,2,0,P|362:51|320:69,1,80,4|0,3:2|0:0,0:0:0:0: +264,138,233369,6,0,B|173:153|201:102|101:116,1,160,4|0,3:2|3:2,0:0:0:0: +39,277,234265,2,0,L|32:359,2,80,0|10|0,3:2|0:2|3:2,0:0:0:0: +123,252,234936,1,10,0:2:0:0: +206,225,235160,6,0,P|261:245|383:213,2,160,4|0|0,3:2|3:2|3:2,0:0:0:0: +136,169,236280,2,0,L|48:175,2,80,0|8|0,0:2|0:2|3:2,0:0:0:0: +203,112,236951,6,0,B|253:81|253:81|377:98,1,160,4|12,3:2|0:2,0:0:0:0: +468,228,237847,2,0,B|418:197|418:197|294:214,1,160,0|12,3:2|0:2,0:0:0:0: +180,321,238742,6,0,P|120:328|31:252,1,160,0|12,3:2|0:2,0:0:0:0: +16,188,239414,1,0,0:2:0:0: +65,115,239638,2,0,L|147:107,1,80,0|0,3:2|3:2,0:0:0:0: +205,43,240086,2,0,L|287:51,1,80,12|0,0:2|0:2,0:0:0:0: +366,83,240534,6,0,P|389:155|382:244,1,160,0|12,3:2|0:2,0:0:0:0: +238,338,241429,2,0,P|215:266|222:177,1,160,0|12,3:2|0:2,0:0:0:0: +297,24,242325,6,0,P|369:54|462:47,2,160,0|12|0,3:2|0:2|3:2,0:0:0:0: +216,60,243444,1,0,3:2:0:0: +136,96,243668,2,0,L|56:89,1,80,8|8,0:2|0:2,0:0:0:0: +2,18,244116,6,0,P|26:102|26:206,1,160,4|0,3:2|3:2,0:0:0:0: +5,259,244787,1,8,0:2:0:0: +64,324,245011,2,0,L|156:326,1,80,0|8,3:2|0:2,0:0:0:0: +223,364,245459,2,0,L|315:362,1,80,0|8,3:2|0:2,0:0:0:0: +379,318,245907,6,0,P|395:247|390:145,1,160,4|0,3:2|3:2,0:0:0:0: +240,72,246802,2,0,P|225:116|220:149,1,80,8|8,3:2|3:2,0:0:0:0: +152,205,247250,2,0,P|167:249|172:282,1,80,8|8,3:2|3:2,0:0:0:0: +118,352,247698,6,0,P|174:314|275:316,1,160,4|0,3:2|0:0,0:0:0:0: +427,377,248593,2,0,P|465:321|463:220,1,160,4|0,3:2|0:0,0:0:0:0: +411,63,249489,6,0,B|326:66|257:31|257:31|306:192|306:192|227:143|142:154,1,480,4|10,3:2|0:2,0:0:0:0: +21,259,251280,6,0,L|32:378,2,120,12|12|12,3:2|3:2|3:2,0:0:0:0: +2,173,252175,2,0,P|19:77|84:24,1,160,12|12,3:2|0:2,0:0:0:0: +236,14,253071,5,4,3:2:0:0: +293,276,259563,6,0,L|392:265,2,80,12|4|12,0:2|3:2|0:2,0:0:0:0: +219,324,260235,5,0,3:2:0:0: +115,181,260683,2,0,P|59:205|-18:200,2,120,0|0|8,3:2|0:2|0:2,0:0:0:0: +189,133,261578,2,0,L|274:137,1,80,0|8,3:2|0:2,0:0:0:0: +334,195,262026,6,0,L|419:191,1,80,0|8,3:2|0:2,0:0:0:0: +480,132,262474,2,0,P|469:74|471:13,2,120,0|0|8,3:2|0:2|0:2,0:0:0:0: +497,218,263369,2,0,P|500:258|494:305,1,80,0|8,3:2|0:2,0:0:0:0: +434,361,263817,5,0,3:2:0:0: +262,319,264265,2,0,L|138:333,2,120,0|0|8,3:2|0:2|0:2,0:0:0:0: +329,262,265160,2,0,L|316:154,1,80,0|8,3:2|0:2,0:0:0:0: +254,123,265608,6,0,P|205:120|161:140,1,80,0|8,3:2|0:2,0:0:0:0: +95,164,266056,2,0,L|78:17,2,120,0|0|0,3:2|0:2|0:2,0:0:0:0: +112,250,266951,1,8,0:2:0:0: +178,308,267175,1,8,0:2:0:0: +264,289,267399,6,0,P|301:284|368:300,1,80,2|2,3:2|0:2,0:0:0:0: +395,218,267847,2,0,P|451:236|510:225,2,120,2|2|0,0:2|0:2|3:2,0:0:0:0: +326,162,268742,6,0,B|274:185|274:185|158:154|158:154|231:119,1,240,12|0,0:2|0:0,0:0:0:0: diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1597806-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1597806-expected-conversion.json new file mode 100644 index 0000000000..c14bdf1453 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1597806-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":42.0,"Objects":[{"StartTime":42.0,"Position":288.0,"HyperDash":false},{"StartTime":124.0,"Position":246.095245,"HyperDash":false},{"StartTime":242.0,"Position":217.560577,"HyperDash":false}]},{"StartTime":443.0,"Objects":[{"StartTime":443.0,"Position":125.0,"HyperDash":false},{"StartTime":525.0,"Position":139.98082,"HyperDash":false},{"StartTime":643.0,"Position":171.8503,"HyperDash":false}]},{"StartTime":845.0,"Objects":[{"StartTime":845.0,"Position":95.0,"HyperDash":false},{"StartTime":927.0,"Position":92.32658,"HyperDash":false},{"StartTime":1045.0,"Position":117.385254,"HyperDash":false}]},{"StartTime":1247.0,"Objects":[{"StartTime":1247.0,"Position":250.0,"HyperDash":false},{"StartTime":1329.0,"Position":235.381714,"HyperDash":false},{"StartTime":1447.0,"Position":177.760284,"HyperDash":false}]},{"StartTime":1649.0,"Objects":[{"StartTime":1649.0,"Position":277.0,"HyperDash":false},{"StartTime":1731.0,"Position":323.6183,"HyperDash":false},{"StartTime":1849.0,"Position":349.239716,"HyperDash":false}]},{"StartTime":2051.0,"Objects":[{"StartTime":2051.0,"Position":448.0,"HyperDash":false},{"StartTime":2133.0,"Position":419.48,"HyperDash":false},{"StartTime":2251.0,"Position":376.0,"HyperDash":false}]},{"StartTime":2453.0,"Objects":[{"StartTime":2453.0,"Position":499.0,"HyperDash":false},{"StartTime":2535.0,"Position":501.8066,"HyperDash":false},{"StartTime":2653.0,"Position":496.029449,"HyperDash":false}]},{"StartTime":2855.0,"Objects":[{"StartTime":2855.0,"Position":397.0,"HyperDash":false},{"StartTime":2937.0,"Position":385.11322,"HyperDash":false},{"StartTime":3055.0,"Position":393.946869,"HyperDash":false}]},{"StartTime":3257.0,"Objects":[{"StartTime":3257.0,"Position":295.0,"HyperDash":false},{"StartTime":3339.0,"Position":290.097748,"HyperDash":false},{"StartTime":3457.0,"Position":291.69043,"HyperDash":false}]},{"StartTime":3658.0,"Objects":[{"StartTime":3658.0,"Position":134.0,"HyperDash":false},{"StartTime":3740.0,"Position":152.636856,"HyperDash":false},{"StartTime":3858.0,"Position":208.724045,"HyperDash":false}]},{"StartTime":4060.0,"Objects":[{"StartTime":4060.0,"Position":95.0,"HyperDash":false},{"StartTime":4142.0,"Position":103.823456,"HyperDash":false},{"StartTime":4260.0,"Position":126.276718,"HyperDash":false}]},{"StartTime":4462.0,"Objects":[{"StartTime":4462.0,"Position":217.0,"HyperDash":false},{"StartTime":4544.0,"Position":213.344086,"HyperDash":false},{"StartTime":4662.0,"Position":173.936813,"HyperDash":false}]},{"StartTime":4864.0,"Objects":[{"StartTime":4864.0,"Position":268.0,"HyperDash":false},{"StartTime":4946.0,"Position":265.3267,"HyperDash":false},{"StartTime":5064.0,"Position":265.68042,"HyperDash":false}]},{"StartTime":5266.0,"Objects":[{"StartTime":5266.0,"Position":418.0,"HyperDash":false},{"StartTime":5348.0,"Position":385.913544,"HyperDash":false},{"StartTime":5466.0,"Position":354.807678,"HyperDash":false}]},{"StartTime":5668.0,"Objects":[{"StartTime":5668.0,"Position":356.0,"HyperDash":false},{"StartTime":5750.0,"Position":390.300568,"HyperDash":false},{"StartTime":5868.0,"Position":421.002045,"HyperDash":false}]},{"StartTime":6070.0,"Objects":[{"StartTime":6070.0,"Position":265.0,"HyperDash":false},{"StartTime":6152.0,"Position":215.764069,"HyperDash":false},{"StartTime":6270.0,"Position":191.253845,"HyperDash":false}]},{"StartTime":6472.0,"Objects":[{"StartTime":6472.0,"Position":35.0,"HyperDash":false},{"StartTime":6554.0,"Position":56.2359238,"HyperDash":false},{"StartTime":6672.0,"Position":108.746155,"HyperDash":false}]},{"StartTime":6873.0,"Objects":[{"StartTime":6873.0,"Position":265.0,"HyperDash":false},{"StartTime":6955.0,"Position":252.764084,"HyperDash":false},{"StartTime":7073.0,"Position":191.253845,"HyperDash":false}]},{"StartTime":7275.0,"Objects":[{"StartTime":7275.0,"Position":323.0,"HyperDash":false},{"StartTime":7357.0,"Position":370.397949,"HyperDash":false},{"StartTime":7475.0,"Position":390.779846,"HyperDash":false}]},{"StartTime":7677.0,"Objects":[{"StartTime":7677.0,"Position":493.0,"HyperDash":false},{"StartTime":7759.0,"Position":475.602051,"HyperDash":false},{"StartTime":7877.0,"Position":425.220154,"HyperDash":false}]},{"StartTime":8079.0,"Objects":[{"StartTime":8079.0,"Position":323.0,"HyperDash":false},{"StartTime":8161.0,"Position":345.397949,"HyperDash":false},{"StartTime":8279.0,"Position":390.779846,"HyperDash":false}]},{"StartTime":8481.0,"Objects":[{"StartTime":8481.0,"Position":273.0,"HyperDash":false}]},{"StartTime":8682.0,"Objects":[{"StartTime":8682.0,"Position":187.0,"HyperDash":false}]},{"StartTime":8883.0,"Objects":[{"StartTime":8883.0,"Position":101.0,"HyperDash":false}]},{"StartTime":9084.0,"Objects":[{"StartTime":9084.0,"Position":187.0,"HyperDash":false}]},{"StartTime":9285.0,"Objects":[{"StartTime":9285.0,"Position":101.0,"HyperDash":false}]},{"StartTime":9486.0,"Objects":[{"StartTime":9486.0,"Position":15.0,"HyperDash":false}]},{"StartTime":9687.0,"Objects":[{"StartTime":9687.0,"Position":187.0,"HyperDash":false},{"StartTime":9769.0,"Position":140.010742,"HyperDash":false},{"StartTime":9887.0,"Position":113.855469,"HyperDash":false}]},{"StartTime":10088.0,"Objects":[{"StartTime":10088.0,"Position":264.0,"HyperDash":false},{"StartTime":10170.0,"Position":285.762848,"HyperDash":false},{"StartTime":10288.0,"Position":285.372772,"HyperDash":false}]},{"StartTime":10490.0,"Objects":[{"StartTime":10490.0,"Position":287.0,"HyperDash":false},{"StartTime":10572.0,"Position":302.9239,"HyperDash":false},{"StartTime":10690.0,"Position":338.033844,"HyperDash":false}]},{"StartTime":10892.0,"Objects":[{"StartTime":10892.0,"Position":422.0,"HyperDash":false},{"StartTime":10974.0,"Position":429.5159,"HyperDash":false},{"StartTime":11092.0,"Position":417.753174,"HyperDash":false}]},{"StartTime":11294.0,"Objects":[{"StartTime":11294.0,"Position":287.0,"HyperDash":false},{"StartTime":11376.0,"Position":299.820526,"HyperDash":false},{"StartTime":11494.0,"Position":348.207428,"HyperDash":false}]},{"StartTime":11696.0,"Objects":[{"StartTime":11696.0,"Position":166.0,"HyperDash":false},{"StartTime":11778.0,"Position":186.67955,"HyperDash":false},{"StartTime":11896.0,"Position":232.26709,"HyperDash":false}]},{"StartTime":12098.0,"Objects":[{"StartTime":12098.0,"Position":332.0,"HyperDash":false},{"StartTime":12180.0,"Position":300.8351,"HyperDash":false},{"StartTime":12298.0,"Position":258.427124,"HyperDash":false}]},{"StartTime":12500.0,"Objects":[{"StartTime":12500.0,"Position":394.0,"HyperDash":false},{"StartTime":12582.0,"Position":438.1649,"HyperDash":false},{"StartTime":12700.0,"Position":467.572876,"HyperDash":false}]},{"StartTime":12902.0,"Objects":[{"StartTime":12902.0,"Position":332.0,"HyperDash":false},{"StartTime":12984.0,"Position":286.8351,"HyperDash":false},{"StartTime":13102.0,"Position":258.427124,"HyperDash":false}]},{"StartTime":13303.0,"Objects":[{"StartTime":13303.0,"Position":413.0,"HyperDash":false},{"StartTime":13385.0,"Position":402.2547,"HyperDash":false},{"StartTime":13503.0,"Position":417.4853,"HyperDash":false}]},{"StartTime":13705.0,"Objects":[{"StartTime":13705.0,"Position":327.0,"HyperDash":false},{"StartTime":13787.0,"Position":319.2547,"HyperDash":false},{"StartTime":13905.0,"Position":331.4853,"HyperDash":false}]},{"StartTime":14107.0,"Objects":[{"StartTime":14107.0,"Position":241.0,"HyperDash":false},{"StartTime":14189.0,"Position":262.25473,"HyperDash":false},{"StartTime":14307.0,"Position":245.485291,"HyperDash":false}]},{"StartTime":14509.0,"Objects":[{"StartTime":14509.0,"Position":118.0,"HyperDash":false},{"StartTime":14591.0,"Position":165.460083,"HyperDash":false},{"StartTime":14709.0,"Position":192.2929,"HyperDash":false}]},{"StartTime":14911.0,"Objects":[{"StartTime":14911.0,"Position":297.0,"HyperDash":false},{"StartTime":14993.0,"Position":276.830261,"HyperDash":false},{"StartTime":15111.0,"Position":250.244568,"HyperDash":false}]},{"StartTime":15313.0,"Objects":[{"StartTime":15313.0,"Position":273.0,"HyperDash":false},{"StartTime":15395.0,"Position":254.357025,"HyperDash":false},{"StartTime":15513.0,"Position":244.602539,"HyperDash":false}]},{"StartTime":15715.0,"Objects":[{"StartTime":15715.0,"Position":235.0,"HyperDash":false},{"StartTime":15797.0,"Position":262.7597,"HyperDash":false},{"StartTime":15915.0,"Position":306.7758,"HyperDash":false}]},{"StartTime":16117.0,"Objects":[{"StartTime":16117.0,"Position":441.0,"HyperDash":false},{"StartTime":16199.0,"Position":431.2403,"HyperDash":false},{"StartTime":16317.0,"Position":369.2242,"HyperDash":false}]},{"StartTime":16518.0,"Objects":[{"StartTime":16518.0,"Position":235.0,"HyperDash":false},{"StartTime":16600.0,"Position":257.7597,"HyperDash":false},{"StartTime":16718.0,"Position":306.7758,"HyperDash":false}]},{"StartTime":16920.0,"Objects":[{"StartTime":16920.0,"Position":436.0,"HyperDash":false},{"StartTime":17002.0,"Position":444.7306,"HyperDash":false},{"StartTime":17120.0,"Position":445.098969,"HyperDash":false}]},{"StartTime":17322.0,"Objects":[{"StartTime":17322.0,"Position":345.0,"HyperDash":false},{"StartTime":17404.0,"Position":386.333862,"HyperDash":false},{"StartTime":17522.0,"Position":414.106964,"HyperDash":false}]},{"StartTime":17724.0,"Objects":[{"StartTime":17724.0,"Position":208.0,"HyperDash":false},{"StartTime":17806.0,"Position":249.6,"HyperDash":false},{"StartTime":17924.0,"Position":268.0,"HyperDash":false}]},{"StartTime":18126.0,"Objects":[{"StartTime":18126.0,"Position":187.0,"HyperDash":false}]},{"StartTime":18528.0,"Objects":[{"StartTime":18528.0,"Position":187.0,"HyperDash":false}]},{"StartTime":18930.0,"Objects":[{"StartTime":18930.0,"Position":187.0,"HyperDash":false}]},{"StartTime":19332.0,"Objects":[{"StartTime":19332.0,"Position":187.0,"HyperDash":false}]},{"StartTime":19532.0,"Objects":[{"StartTime":19532.0,"Position":187.0,"HyperDash":false}]},{"StartTime":19733.0,"Objects":[{"StartTime":19733.0,"Position":345.0,"HyperDash":false}]},{"StartTime":19933.0,"Objects":[{"StartTime":19933.0,"Position":257.0,"HyperDash":false}]},{"StartTime":20135.0,"Objects":[{"StartTime":20135.0,"Position":471.0,"HyperDash":false}]},{"StartTime":20335.0,"Objects":[{"StartTime":20335.0,"Position":384.0,"HyperDash":false}]},{"StartTime":20537.0,"Objects":[{"StartTime":20537.0,"Position":284.0,"HyperDash":false}]},{"StartTime":20737.0,"Objects":[{"StartTime":20737.0,"Position":371.0,"HyperDash":false}]},{"StartTime":20938.0,"Objects":[{"StartTime":20938.0,"Position":157.0,"HyperDash":false}]},{"StartTime":21140.0,"Objects":[{"StartTime":21140.0,"Position":244.0,"HyperDash":false}]},{"StartTime":21340.0,"Objects":[{"StartTime":21340.0,"Position":188.0,"HyperDash":false}]},{"StartTime":21542.0,"Objects":[{"StartTime":21542.0,"Position":188.0,"HyperDash":false}]},{"StartTime":21743.0,"Objects":[{"StartTime":21743.0,"Position":345.0,"HyperDash":false}]},{"StartTime":21944.0,"Objects":[{"StartTime":21944.0,"Position":250.0,"HyperDash":false}]},{"StartTime":22145.0,"Objects":[{"StartTime":22145.0,"Position":419.0,"HyperDash":false},{"StartTime":22227.0,"Position":405.25,"HyperDash":false},{"StartTime":22345.0,"Position":344.0,"HyperDash":false}]},{"StartTime":22547.0,"Objects":[{"StartTime":22547.0,"Position":196.0,"HyperDash":false},{"StartTime":22629.0,"Position":241.75,"HyperDash":false},{"StartTime":22747.0,"Position":271.0,"HyperDash":false}]},{"StartTime":22948.0,"Objects":[{"StartTime":22948.0,"Position":419.0,"HyperDash":false}]},{"StartTime":23149.0,"Objects":[{"StartTime":23149.0,"Position":344.0,"HyperDash":false}]},{"StartTime":23350.0,"Objects":[{"StartTime":23350.0,"Position":305.0,"HyperDash":false},{"StartTime":23432.0,"Position":326.616516,"HyperDash":false},{"StartTime":23550.0,"Position":306.871063,"HyperDash":false}]},{"StartTime":23752.0,"Objects":[{"StartTime":23752.0,"Position":240.0,"HyperDash":false},{"StartTime":23834.0,"Position":244.383484,"HyperDash":false},{"StartTime":23952.0,"Position":238.128937,"HyperDash":false}]},{"StartTime":24154.0,"Objects":[{"StartTime":24154.0,"Position":429.0,"HyperDash":false},{"StartTime":24236.0,"Position":381.322876,"HyperDash":false},{"StartTime":24354.0,"Position":354.177734,"HyperDash":false}]},{"StartTime":24556.0,"Objects":[{"StartTime":24556.0,"Position":232.0,"HyperDash":false},{"StartTime":24638.0,"Position":267.677124,"HyperDash":false},{"StartTime":24756.0,"Position":306.822266,"HyperDash":false}]},{"StartTime":24958.0,"Objects":[{"StartTime":24958.0,"Position":429.0,"HyperDash":false},{"StartTime":25040.0,"Position":386.322876,"HyperDash":false},{"StartTime":25158.0,"Position":354.177734,"HyperDash":false}]},{"StartTime":25360.0,"Objects":[{"StartTime":25360.0,"Position":501.0,"HyperDash":false}]},{"StartTime":25561.0,"Objects":[{"StartTime":25561.0,"Position":429.0,"HyperDash":false}]},{"StartTime":25762.0,"Objects":[{"StartTime":25762.0,"Position":491.0,"HyperDash":false},{"StartTime":25844.0,"Position":475.629547,"HyperDash":false},{"StartTime":25962.0,"Position":490.096466,"HyperDash":false}]},{"StartTime":26163.0,"Objects":[{"StartTime":26163.0,"Position":372.0,"HyperDash":false},{"StartTime":26245.0,"Position":390.370453,"HyperDash":false},{"StartTime":26363.0,"Position":372.903534,"HyperDash":false}]},{"StartTime":26565.0,"Objects":[{"StartTime":26565.0,"Position":372.0,"HyperDash":false}]},{"StartTime":26766.0,"Objects":[{"StartTime":26766.0,"Position":431.0,"HyperDash":false}]},{"StartTime":26967.0,"Objects":[{"StartTime":26967.0,"Position":372.0,"HyperDash":false}]},{"StartTime":27168.0,"Objects":[{"StartTime":27168.0,"Position":314.0,"HyperDash":false}]},{"StartTime":27369.0,"Objects":[{"StartTime":27369.0,"Position":254.0,"HyperDash":false}]},{"StartTime":27570.0,"Objects":[{"StartTime":27570.0,"Position":313.0,"HyperDash":false}]},{"StartTime":27771.0,"Objects":[{"StartTime":27771.0,"Position":372.0,"HyperDash":false},{"StartTime":27821.0,"Position":382.6753,"HyperDash":false},{"StartTime":27871.0,"Position":425.3506,"HyperDash":false},{"StartTime":27921.0,"Position":431.0259,"HyperDash":false},{"StartTime":27971.0,"Position":436.7012,"HyperDash":false},{"StartTime":28021.0,"Position":466.3765,"HyperDash":false},{"StartTime":28071.0,"Position":463.0,"HyperDash":false},{"StartTime":28121.0,"Position":473.0,"HyperDash":false},{"StartTime":28172.0,"Position":473.0,"HyperDash":false},{"StartTime":28222.0,"Position":457.0,"HyperDash":false},{"StartTime":28272.0,"Position":481.0,"HyperDash":false},{"StartTime":28322.0,"Position":460.0,"HyperDash":false},{"StartTime":28373.0,"Position":444.1494,"HyperDash":false},{"StartTime":28423.0,"Position":440.474121,"HyperDash":false},{"StartTime":28473.0,"Position":415.7988,"HyperDash":false},{"StartTime":28523.0,"Position":416.1235,"HyperDash":false},{"StartTime":28574.0,"Position":389.0747,"HyperDash":false},{"StartTime":28656.0,"Position":375.447235,"HyperDash":false},{"StartTime":28775.0,"Position":314.0,"HyperDash":false}]},{"StartTime":28977.0,"Objects":[{"StartTime":28977.0,"Position":185.0,"HyperDash":false},{"StartTime":29068.0,"Position":200.514755,"HyperDash":false},{"StartTime":29159.0,"Position":255.02951,"HyperDash":false},{"StartTime":29250.0,"Position":301.5764,"HyperDash":false},{"StartTime":29378.0,"Position":328.3668,"HyperDash":false}]},{"StartTime":29579.0,"Objects":[{"StartTime":29579.0,"Position":256.0,"HyperDash":false},{"StartTime":29679.0,"Position":256.0,"HyperDash":false},{"StartTime":29779.0,"Position":256.0,"HyperDash":false}]},{"StartTime":30182.0,"Objects":[{"StartTime":30182.0,"Position":467.0,"HyperDash":false}]},{"StartTime":30383.0,"Objects":[{"StartTime":30383.0,"Position":395.0,"HyperDash":false}]},{"StartTime":30584.0,"Objects":[{"StartTime":30584.0,"Position":323.0,"HyperDash":false}]},{"StartTime":30785.0,"Objects":[{"StartTime":30785.0,"Position":251.0,"HyperDash":false}]},{"StartTime":30986.0,"Objects":[{"StartTime":30986.0,"Position":179.0,"HyperDash":false}]},{"StartTime":31187.0,"Objects":[{"StartTime":31187.0,"Position":107.0,"HyperDash":false}]},{"StartTime":31388.0,"Objects":[{"StartTime":31388.0,"Position":35.0,"HyperDash":false},{"StartTime":31479.0,"Position":45.0,"HyperDash":false},{"StartTime":31570.0,"Position":45.0,"HyperDash":false},{"StartTime":31661.0,"Position":39.0,"HyperDash":false},{"StartTime":31789.0,"Position":35.0,"HyperDash":false}]},{"StartTime":31991.0,"Objects":[{"StartTime":31991.0,"Position":105.0,"HyperDash":false},{"StartTime":32091.0,"Position":142.5,"HyperDash":false},{"StartTime":32191.0,"Position":105.0,"HyperDash":false}]},{"StartTime":32593.0,"Objects":[{"StartTime":32593.0,"Position":314.0,"HyperDash":false}]},{"StartTime":32794.0,"Objects":[{"StartTime":32794.0,"Position":434.0,"HyperDash":false}]},{"StartTime":32995.0,"Objects":[{"StartTime":32995.0,"Position":314.0,"HyperDash":false}]},{"StartTime":33196.0,"Objects":[{"StartTime":33196.0,"Position":434.0,"HyperDash":false}]},{"StartTime":33397.0,"Objects":[{"StartTime":33397.0,"Position":314.0,"HyperDash":false}]},{"StartTime":33598.0,"Objects":[{"StartTime":33598.0,"Position":434.0,"HyperDash":false}]},{"StartTime":33799.0,"Objects":[{"StartTime":33799.0,"Position":314.0,"HyperDash":false},{"StartTime":33881.0,"Position":336.929565,"HyperDash":false},{"StartTime":33999.0,"Position":352.8526,"HyperDash":false}]},{"StartTime":34201.0,"Objects":[{"StartTime":34201.0,"Position":117.0,"HyperDash":false},{"StartTime":34283.0,"Position":163.741074,"HyperDash":false},{"StartTime":34401.0,"Position":191.978241,"HyperDash":false}]},{"StartTime":34603.0,"Objects":[{"StartTime":34603.0,"Position":56.0,"HyperDash":false},{"StartTime":34685.0,"Position":55.48987,"HyperDash":false},{"StartTime":34803.0,"Position":91.34114,"HyperDash":false}]},{"StartTime":35005.0,"Objects":[{"StartTime":35005.0,"Position":192.0,"HyperDash":false},{"StartTime":35087.0,"Position":172.904892,"HyperDash":false},{"StartTime":35205.0,"Position":152.743652,"HyperDash":false}]},{"StartTime":35407.0,"Objects":[{"StartTime":35407.0,"Position":389.0,"HyperDash":false},{"StartTime":35489.0,"Position":348.2696,"HyperDash":false},{"StartTime":35607.0,"Position":314.0478,"HyperDash":false}]},{"StartTime":35808.0,"Objects":[{"StartTime":35808.0,"Position":450.0,"HyperDash":false},{"StartTime":35890.0,"Position":440.377838,"HyperDash":false},{"StartTime":36008.0,"Position":414.3362,"HyperDash":false}]},{"StartTime":36210.0,"Objects":[{"StartTime":36210.0,"Position":314.0,"HyperDash":false}]},{"StartTime":36612.0,"Objects":[{"StartTime":36612.0,"Position":123.0,"HyperDash":false}]},{"StartTime":36813.0,"Objects":[{"StartTime":36813.0,"Position":230.0,"HyperDash":false}]},{"StartTime":37014.0,"Objects":[{"StartTime":37014.0,"Position":337.0,"HyperDash":false}]},{"StartTime":37215.0,"Objects":[{"StartTime":37215.0,"Position":230.0,"HyperDash":false}]},{"StartTime":37416.0,"Objects":[{"StartTime":37416.0,"Position":232.0,"HyperDash":false}]},{"StartTime":37516.0,"Objects":[{"StartTime":37516.0,"Position":232.0,"HyperDash":false}]},{"StartTime":37617.0,"Objects":[{"StartTime":37617.0,"Position":232.0,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1597806.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1597806.osu new file mode 100644 index 0000000000..b9ce7a927d --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1597806.osu @@ -0,0 +1,152 @@ +osu file format v14 + +[General] +StackLeniency: 0.4 +Mode: 0 + +[Difficulty] +HPDrainRate:6 +CircleSize:7 +OverallDifficulty:8 +ApproachRate:8 +SliderMultiplier:1.5 +SliderTickRate:1 + +[Events] +//Background and Video events +//Break Periods +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples + +[TimingPoints] +42,401.875418620228,3,1,0,50,1,0 +18528,-100,3,1,0,40,0,0 +18930,-100,3,1,0,30,0,0 +19332,-100,3,1,0,50,0,1 +24154,-100,3,1,0,50,0,1 +27771,-100,3,1,0,50,0,0 +28977,-100,3,1,0,50,0,0 +30182,-100,3,1,0,50,0,0 +31388,-100,3,1,0,50,0,0 +32593,-100,3,1,0,50,0,0 +33799,-100,3,1,0,50,0,0 + +[HitObjects] +288,165,42,6,0,P|255:181|209:175,1,75,4|0,0:0|0:0,0:0:0:0: +125,38,443,2,0,P|155:58|173:101,1,75,8|0,0:0|0:0,0:0:0:0: +95,236,845,2,0,P|97:199|125:162,1,75,8|0,0:0|0:0,0:0:0:0: +250,271,1247,6,0,L|164:295,1,75,8|0,0:0|0:0,0:0:0:0: +277,199,1649,2,0,L|363:175,1,75,8|0,0:0|0:0,0:0:0:0: +448,85,2051,2,0,L|376:106,1,75,8|0,0:0|0:0,0:0:0:0: +499,211,2453,6,0,P|502:258|491:298,1,75,4|0,0:0|0:0,0:0:0:0: +397,374,2855,2,0,P|400:336|394:300,1,75,8|0,0:0|0:0,0:0:0:0: +295,211,3257,2,0,P|298:248|292:284,1,75,8|0,0:0|0:0,0:0:0:0: +134,307,3658,6,0,L|227:315,1,75,8|0,0:0|0:0,0:0:0:0: +95,143,4060,2,0,L|134:228,1,75,8|0,0:0|0:0,0:0:0:0: +217,28,4462,2,0,L|163:105,1,75,8|0,0:0|0:0,0:0:0:0: +268,199,4864,6,0,P|261:152|270:113,1,75,4|0,0:0|0:0,0:0:0:0: +418,214,5266,2,0,P|380:243|342:255,1,75,8|0,0:0|0:0,0:0:0:0: +356,76,5668,2,0,P|400:93|429:120,1,75,4|0,0:0|0:0,0:0:0:0: +265,125,6070,6,0,L|184:140,1,75,8|0,0:0|0:0,0:0:0:0: +35,204,6472,2,0,L|116:219,1,75,8|0,0:0|0:0,0:0:0:0: +265,283,6873,2,0,L|184:298,1,75,4|0,0:0|0:0,0:0:0:0: +323,195,7275,6,0,P|366:203|403:237,1,75,8|0,0:0|0:0,0:0:0:0: +493,117,7677,2,0,P|450:125|413:159,1,75,8|0,0:0|0:0,0:0:0:0: +323,39,8079,2,0,P|366:47|403:81,1,75,8|0,0:0|0:0,0:0:0:0: +273,140,8481,5,4,0:0:0:0: +187,31,8682,1,0,0:0:0:0: +101,140,8883,1,8,0:0:0:0: +187,249,9084,1,0,0:0:0:0: +101,358,9285,1,8,0:0:0:0: +15,249,9486,1,0,0:0:0:0: +187,249,9687,6,0,L|112:266,1,75,4|0,0:0|0:0,0:0:0:0: +264,181,10088,2,0,L|286:107,1,75,8|0,0:0|0:0,0:0:0:0: +287,283,10490,2,0,L|339:339,1,75,8|0,0:0|0:0,0:0:0:0: +422,222,10892,6,0,P|425:180|411:133,1,75,8|0,0:0|0:0,0:0:0:0: +287,283,11294,2,0,P|324:264|358:228,1,75,8|0,0:0|0:0,0:0:0:0: +166,196,11696,2,0,P|200:219|248:230,1,75,8|0,0:0|0:0,0:0:0:0: +332,83,12098,6,0,L|236:102,1,75,4|0,0:0|0:0,0:0:0:0: +394,139,12500,2,0,L|490:158,1,75,8|0,0:0|0:0,0:0:0:0: +332,195,12902,2,0,L|236:214,1,75,8|0,0:0|0:0,0:0:0:0: +413,321,13303,6,0,P|419:253|399:213,1,75,8|0,0:0|0:0,0:0:0:0: +327,121,13705,2,0,P|333:189|313:229,1,75,8|0,0:0|0:0,0:0:0:0: +241,321,14107,2,0,P|247:253|227:213,1,75,8|0,0:0|0:0,0:0:0:0: +118,175,14509,6,0,L|212:188,1,75,4|0,0:0|0:0,0:0:0:0: +297,100,14911,2,0,L|238:174,1,75,8|0,0:0|0:0,0:0:0:0: +273,292,15313,2,0,L|237:204,1,75,4|0,0:0|0:0,0:0:0:0: +235,357,15715,6,0,P|272:368|321:351,1,75,8|0,0:0|0:0,0:0:0:0: +441,286,16117,2,0,P|404:297|355:280,1,75,8|0,0:0|0:0,0:0:0:0: +235,215,16518,2,0,P|272:226|321:209,1,75,4|0,0:0|0:0,0:0:0:0: +436,127,16920,6,0,L|447:217,1,75,8|0,0:0|0:0,0:0:0:0: +345,22,17322,2,0,L|428:57,1,75,8|0,0:0|0:0,0:0:0:0: +208,48,17724,2,0,L|280:-6,1,75,4|0,0:0|0:0,0:0:0:0: +187,162,18126,5,4,0:0:0:0: +187,162,18528,1,8,0:0:0:0: +187,162,18930,1,8,0:0:0:0: +187,162,19332,5,4,0:0:0:0: +187,263,19532,1,0,0:0:0:0: +345,107,19733,1,8,0:0:0:0: +257,157,19933,1,0,0:0:0:0: +471,216,20135,1,8,0:0:0:0: +384,165,20335,1,0,0:0:0:0: +284,300,20537,5,4,0:0:0:0: +371,249,20737,1,0,0:0:0:0: +157,190,20938,1,8,0:0:0:0: +244,241,21140,1,0,0:0:0:0: +188,27,21340,1,4,0:0:0:0: +188,127,21542,1,0,0:0:0:0: +345,40,21743,5,4,0:0:0:0: +250,77,21944,1,0,0:0:0:0: +419,147,22145,2,0,L|328:147,1,75,4|0,0:0|0:0,0:0:0:0: +196,219,22547,2,0,L|287:219,1,75,8|0,0:0|0:0,0:0:0:0: +419,291,22948,5,4,0:0:0:0: +344,224,23149,1,0,0:0:0:0: +305,352,23350,2,0,P|310:313|305:269,1,75,4|0,0:0|0:0,0:0:0:0: +240,122,23752,2,0,P|235:161|240:205,1,75,8|0,0:0|0:0,0:0:0:0: +429,207,24154,6,0,L|342:213,1,75,8|0,0:0|0:0,0:0:0:0: +232,272,24556,2,0,L|319:278,1,75,8|0,0:0|0:0,0:0:0:0: +429,337,24958,2,0,L|342:343,1,75,8|0,0:0|0:0,0:0:0:0: +501,280,25360,5,4,0:0:0:0: +429,207,25561,1,0,0:0:0:0: +491,62,25762,2,0,L|490:145,1,75,4|0,0:0|0:0,0:0:0:0: +372,236,26163,2,0,L|373:153,1,75,8|0,0:0|0:0,0:0:0:0: +372,7,26565,5,4,0:0:0:0: +431,121,26766,1,0,0:0:0:0: +372,236,26967,1,8,0:0:0:0: +314,121,27168,1,0,0:0:0:0: +254,236,27369,1,8,0:0:0:0: +313,351,27570,1,0,0:0:0:0: +372,236,27771,6,0,B|473:236|473:236|473:121|473:121|306:121,1,375,4|0,0:0|0:0,0:0:0:0: +185,192,28977,6,0,B|256:214|256:214|328:192,1,150,4|0,0:0|0:0,0:0:0:0: +256,94,29579,6,0,L|256:43,2,37.5,4|0|8,0:0|0:0|0:0,0:0:0:0: +467,188,30182,5,4,0:0:0:0: +395,307,30383,1,0,0:0:0:0: +323,188,30584,1,8,0:0:0:0: +251,307,30785,1,0,0:0:0:0: +179,188,30986,1,8,0:0:0:0: +107,307,31187,1,0,0:0:0:0: +35,188,31388,6,0,L|35:39,1,150,4|0,0:0|0:0,0:0:0:0: +105,116,31991,2,0,L|154:116,2,37.5,4|0|8,0:0|0:0|0:0,0:0:0:0: +314,4,32593,5,4,0:0:0:0: +434,66,32794,1,0,0:0:0:0: +314,128,32995,1,8,0:0:0:0: +434,190,33196,1,0,0:0:0:0: +314,252,33397,1,8,0:0:0:0: +434,314,33598,1,0,0:0:0:0: +314,384,33799,6,0,L|357:313,1,75,4|0,0:0|0:0,0:0:0:0: +117,340,34201,2,0,L|200:342,1,75,8|0,0:0|0:0,0:0:0:0: +56,148,34603,2,0,L|95:221,1,75,8|0,0:0|0:0,0:0:0:0: +192,0,35005,2,0,L|149:70,1,75,4|0,0:0|0:0,0:0:0:0: +389,42,35407,2,0,L|305:39,1,75,8|0,0:0|0:0,0:0:0:0: +450,234,35808,2,0,L|410:160,1,75,8|0,0:0|0:0,0:0:0:0: +314,384,36210,1,4,0:0:0:0: +123,192,36612,5,4,0:0:0:0: +230,327,36813,1,0,0:0:0:0: +337,192,37014,1,8,0:0:0:0: +230,57,37215,1,0,0:0:0:0: +232,193,37416,5,4,0:0:0:0: +232,193,37516,1,0,0:0:0:0: +232,193,37617,1,8,0:0:0:0: diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2190499-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2190499-expected-conversion.json new file mode 100644 index 0000000000..fb919302d9 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2190499-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":1739.0,"Objects":[{"StartTime":1739.0,"Position":367.0,"HyperDash":false},{"StartTime":1817.0,"Position":334.444244,"HyperDash":false},{"StartTime":1896.0,"Position":321.501648,"HyperDash":false},{"StartTime":1975.0,"Position":314.851929,"HyperDash":false},{"StartTime":2054.0,"Position":307.22052,"HyperDash":false},{"StartTime":2124.0,"Position":315.042236,"HyperDash":false},{"StartTime":2194.0,"Position":286.526184,"HyperDash":false},{"StartTime":2264.0,"Position":269.750366,"HyperDash":false},{"StartTime":2370.0,"Position":246.8734,"HyperDash":false}]},{"StartTime":3002.0,"Objects":[{"StartTime":3002.0,"Position":161.0,"HyperDash":false},{"StartTime":3080.0,"Position":160.934677,"HyperDash":false},{"StartTime":3159.0,"Position":181.699768,"HyperDash":false},{"StartTime":3238.0,"Position":189.906845,"HyperDash":false},{"StartTime":3317.0,"Position":212.345535,"HyperDash":false},{"StartTime":3387.0,"Position":233.989639,"HyperDash":false},{"StartTime":3457.0,"Position":230.043228,"HyperDash":false},{"StartTime":3527.0,"Position":221.436966,"HyperDash":false},{"StartTime":3633.0,"Position":233.8208,"HyperDash":false}]},{"StartTime":4265.0,"Objects":[{"StartTime":4265.0,"Position":47.0,"HyperDash":false},{"StartTime":4334.0,"Position":63.4803925,"HyperDash":false},{"StartTime":4404.0,"Position":52.9349747,"HyperDash":false},{"StartTime":4474.0,"Position":77.8079,"HyperDash":false},{"StartTime":4580.0,"Position":111.004265,"HyperDash":false}]},{"StartTime":4897.0,"Objects":[{"StartTime":4897.0,"Position":235.0,"HyperDash":false},{"StartTime":4975.0,"Position":269.900024,"HyperDash":false},{"StartTime":5054.0,"Position":287.131927,"HyperDash":false},{"StartTime":5133.0,"Position":301.489746,"HyperDash":false},{"StartTime":5212.0,"Position":305.972,"HyperDash":false},{"StartTime":5273.0,"Position":301.195129,"HyperDash":false},{"StartTime":5370.0,"Position":334.383362,"HyperDash":false}]},{"StartTime":5528.0,"Objects":[{"StartTime":5528.0,"Position":372.0,"HyperDash":false},{"StartTime":5606.0,"Position":378.156158,"HyperDash":false},{"StartTime":5685.0,"Position":354.0074,"HyperDash":false},{"StartTime":5764.0,"Position":357.6568,"HyperDash":false},{"StartTime":5843.0,"Position":349.1154,"HyperDash":false},{"StartTime":5913.0,"Position":341.790833,"HyperDash":false},{"StartTime":5983.0,"Position":342.120453,"HyperDash":false},{"StartTime":6053.0,"Position":355.1087,"HyperDash":false},{"StartTime":6159.0,"Position":339.322418,"HyperDash":false}]},{"StartTime":6791.0,"Objects":[{"StartTime":6791.0,"Position":55.0,"HyperDash":false},{"StartTime":6860.0,"Position":49.3883972,"HyperDash":false},{"StartTime":6930.0,"Position":57.4187775,"HyperDash":false},{"StartTime":7000.0,"Position":74.47069,"HyperDash":false},{"StartTime":7106.0,"Position":114.237465,"HyperDash":false}]},{"StartTime":7423.0,"Objects":[{"StartTime":7423.0,"Position":240.0,"HyperDash":false},{"StartTime":7501.0,"Position":237.8742,"HyperDash":false},{"StartTime":7580.0,"Position":228.717819,"HyperDash":false},{"StartTime":7659.0,"Position":200.180328,"HyperDash":false},{"StartTime":7738.0,"Position":193.621948,"HyperDash":false},{"StartTime":7799.0,"Position":189.067337,"HyperDash":false},{"StartTime":7896.0,"Position":188.528091,"HyperDash":false}]},{"StartTime":8054.0,"Objects":[{"StartTime":8054.0,"Position":273.0,"HyperDash":false},{"StartTime":8132.0,"Position":306.8755,"HyperDash":false},{"StartTime":8211.0,"Position":298.36087,"HyperDash":false},{"StartTime":8290.0,"Position":326.902954,"HyperDash":false},{"StartTime":8369.0,"Position":340.2283,"HyperDash":false},{"StartTime":8439.0,"Position":337.1718,"HyperDash":false},{"StartTime":8509.0,"Position":325.47818,"HyperDash":false},{"StartTime":8579.0,"Position":310.550323,"HyperDash":false},{"StartTime":8685.0,"Position":273.0,"HyperDash":false}]},{"StartTime":9002.0,"Objects":[{"StartTime":9002.0,"Position":147.0,"HyperDash":false},{"StartTime":9062.0,"Position":147.914444,"HyperDash":false},{"StartTime":9159.0,"Position":110.615738,"HyperDash":false}]},{"StartTime":9318.0,"Objects":[{"StartTime":9318.0,"Position":59.0,"HyperDash":false},{"StartTime":9396.0,"Position":72.10824,"HyperDash":false},{"StartTime":9475.0,"Position":59.51161,"HyperDash":false},{"StartTime":9554.0,"Position":28.2636833,"HyperDash":false},{"StartTime":9633.0,"Position":39.3327179,"HyperDash":false},{"StartTime":9703.0,"Position":49.56317,"HyperDash":false},{"StartTime":9773.0,"Position":37.3222733,"HyperDash":false},{"StartTime":9843.0,"Position":38.56619,"HyperDash":false},{"StartTime":9949.0,"Position":59.0,"HyperDash":false}]},{"StartTime":10265.0,"Objects":[{"StartTime":10265.0,"Position":133.0,"HyperDash":false}]},{"StartTime":10581.0,"Objects":[{"StartTime":10581.0,"Position":494.0,"HyperDash":false},{"StartTime":10659.0,"Position":135.0,"HyperDash":false},{"StartTime":10738.0,"Position":30.0,"HyperDash":false},{"StartTime":10817.0,"Position":11.0,"HyperDash":false},{"StartTime":10896.0,"Position":239.0,"HyperDash":false},{"StartTime":10975.0,"Position":505.0,"HyperDash":false},{"StartTime":11054.0,"Position":353.0,"HyperDash":false},{"StartTime":11133.0,"Position":136.0,"HyperDash":false},{"StartTime":11212.0,"Position":135.0,"HyperDash":false},{"StartTime":11291.0,"Position":346.0,"HyperDash":false},{"StartTime":11370.0,"Position":39.0,"HyperDash":false},{"StartTime":11449.0,"Position":300.0,"HyperDash":false},{"StartTime":11528.0,"Position":398.0,"HyperDash":false},{"StartTime":11607.0,"Position":151.0,"HyperDash":false},{"StartTime":11686.0,"Position":73.0,"HyperDash":false},{"StartTime":11765.0,"Position":311.0,"HyperDash":false},{"StartTime":11844.0,"Position":90.0,"HyperDash":false}]},{"StartTime":13107.0,"Objects":[{"StartTime":13107.0,"Position":264.0,"HyperDash":false},{"StartTime":13185.0,"Position":477.0,"HyperDash":false},{"StartTime":13264.0,"Position":473.0,"HyperDash":false},{"StartTime":13343.0,"Position":120.0,"HyperDash":false},{"StartTime":13422.0,"Position":115.0,"HyperDash":false},{"StartTime":13501.0,"Position":163.0,"HyperDash":false},{"StartTime":13580.0,"Position":447.0,"HyperDash":false},{"StartTime":13659.0,"Position":72.0,"HyperDash":false},{"StartTime":13738.0,"Position":257.0,"HyperDash":false},{"StartTime":13817.0,"Position":153.0,"HyperDash":false},{"StartTime":13896.0,"Position":388.0,"HyperDash":false},{"StartTime":13975.0,"Position":336.0,"HyperDash":false},{"StartTime":14054.0,"Position":13.0,"HyperDash":false},{"StartTime":14133.0,"Position":429.0,"HyperDash":false},{"StartTime":14212.0,"Position":381.0,"HyperDash":false},{"StartTime":14291.0,"Position":186.0,"HyperDash":false},{"StartTime":14370.0,"Position":267.0,"HyperDash":false}]},{"StartTime":15633.0,"Objects":[{"StartTime":15633.0,"Position":74.0,"HyperDash":false},{"StartTime":15711.0,"Position":95.2118149,"HyperDash":false},{"StartTime":15790.0,"Position":106.218536,"HyperDash":false},{"StartTime":15869.0,"Position":134.014709,"HyperDash":false},{"StartTime":15948.0,"Position":138.984512,"HyperDash":false},{"StartTime":16008.0,"Position":164.493423,"HyperDash":false},{"StartTime":16068.0,"Position":149.9466,"HyperDash":false},{"StartTime":16128.0,"Position":159.583786,"HyperDash":false},{"StartTime":16225.0,"Position":198.210388,"HyperDash":false}]},{"StartTime":17844.0,"Objects":[{"StartTime":17844.0,"Position":189.0,"HyperDash":false}]},{"StartTime":18160.0,"Objects":[{"StartTime":18160.0,"Position":189.0,"HyperDash":false},{"StartTime":18220.0,"Position":203.848145,"HyperDash":false},{"StartTime":18317.0,"Position":255.355225,"HyperDash":false}]},{"StartTime":18476.0,"Objects":[{"StartTime":18476.0,"Position":402.0,"HyperDash":false},{"StartTime":18536.0,"Position":355.556671,"HyperDash":false},{"StartTime":18633.0,"Position":335.176544,"HyperDash":false}]},{"StartTime":18791.0,"Objects":[{"StartTime":18791.0,"Position":383.0,"HyperDash":false},{"StartTime":18860.0,"Position":387.0126,"HyperDash":false},{"StartTime":18930.0,"Position":402.196167,"HyperDash":false},{"StartTime":19000.0,"Position":402.402679,"HyperDash":false},{"StartTime":19106.0,"Position":403.979065,"HyperDash":false}]},{"StartTime":19265.0,"Objects":[{"StartTime":19265.0,"Position":254.0,"HyperDash":false}]},{"StartTime":19423.0,"Objects":[{"StartTime":19423.0,"Position":178.0,"HyperDash":false},{"StartTime":19483.0,"Position":149.544052,"HyperDash":false},{"StartTime":19580.0,"Position":105.085159,"HyperDash":false}]},{"StartTime":19739.0,"Objects":[{"StartTime":19739.0,"Position":245.0,"HyperDash":false},{"StartTime":19799.0,"Position":290.012634,"HyperDash":false},{"StartTime":19896.0,"Position":317.941528,"HyperDash":false}]},{"StartTime":20054.0,"Objects":[{"StartTime":20054.0,"Position":287.0,"HyperDash":false},{"StartTime":20123.0,"Position":275.9874,"HyperDash":false},{"StartTime":20193.0,"Position":286.803833,"HyperDash":false},{"StartTime":20263.0,"Position":255.597321,"HyperDash":false},{"StartTime":20369.0,"Position":266.020935,"HyperDash":false}]},{"StartTime":20528.0,"Objects":[{"StartTime":20528.0,"Position":167.0,"HyperDash":false}]},{"StartTime":20686.0,"Objects":[{"StartTime":20686.0,"Position":110.0,"HyperDash":false},{"StartTime":20746.0,"Position":87.56889,"HyperDash":false},{"StartTime":20843.0,"Position":44.4256668,"HyperDash":false}]},{"StartTime":21002.0,"Objects":[{"StartTime":21002.0,"Position":158.0,"HyperDash":false},{"StartTime":21062.0,"Position":174.0545,"HyperDash":false},{"StartTime":21159.0,"Position":223.260239,"HyperDash":false}]},{"StartTime":21318.0,"Objects":[{"StartTime":21318.0,"Position":105.0,"HyperDash":false},{"StartTime":21378.0,"Position":86.56889,"HyperDash":false},{"StartTime":21475.0,"Position":39.4256668,"HyperDash":false}]},{"StartTime":21634.0,"Objects":[{"StartTime":21634.0,"Position":153.0,"HyperDash":false},{"StartTime":21694.0,"Position":191.0545,"HyperDash":false},{"StartTime":21791.0,"Position":218.260239,"HyperDash":false}]},{"StartTime":21949.0,"Objects":[{"StartTime":21949.0,"Position":321.0,"HyperDash":false}]},{"StartTime":22107.0,"Objects":[{"StartTime":22107.0,"Position":372.0,"HyperDash":false}]},{"StartTime":22265.0,"Objects":[{"StartTime":22265.0,"Position":345.0,"HyperDash":false},{"StartTime":22325.0,"Position":332.141785,"HyperDash":false},{"StartTime":22422.0,"Position":327.377563,"HyperDash":false}]},{"StartTime":22581.0,"Objects":[{"StartTime":22581.0,"Position":413.0,"HyperDash":false}]},{"StartTime":22739.0,"Objects":[{"StartTime":22739.0,"Position":442.0,"HyperDash":false}]},{"StartTime":22897.0,"Objects":[{"StartTime":22897.0,"Position":409.0,"HyperDash":false},{"StartTime":22957.0,"Position":364.564,"HyperDash":false},{"StartTime":23054.0,"Position":338.834,"HyperDash":false}]},{"StartTime":23212.0,"Objects":[{"StartTime":23212.0,"Position":205.0,"HyperDash":false},{"StartTime":23272.0,"Position":218.780579,"HyperDash":false},{"StartTime":23369.0,"Position":224.420334,"HyperDash":false}]},{"StartTime":23528.0,"Objects":[{"StartTime":23528.0,"Position":73.0,"HyperDash":false},{"StartTime":23588.0,"Position":68.21941,"HyperDash":false},{"StartTime":23685.0,"Position":53.5796623,"HyperDash":false}]},{"StartTime":23844.0,"Objects":[{"StartTime":23844.0,"Position":240.0,"HyperDash":false},{"StartTime":23904.0,"Position":234.999878,"HyperDash":false},{"StartTime":24001.0,"Position":220.82193,"HyperDash":false}]},{"StartTime":24160.0,"Objects":[{"StartTime":24160.0,"Position":88.0,"HyperDash":false},{"StartTime":24220.0,"Position":60.3964767,"HyperDash":false},{"StartTime":24317.0,"Position":68.93455,"HyperDash":false}]},{"StartTime":24476.0,"Objects":[{"StartTime":24476.0,"Position":206.0,"HyperDash":false},{"StartTime":24536.0,"Position":215.970291,"HyperDash":false},{"StartTime":24633.0,"Position":281.8056,"HyperDash":false}]},{"StartTime":24791.0,"Objects":[{"StartTime":24791.0,"Position":425.0,"HyperDash":false},{"StartTime":24851.0,"Position":385.029724,"HyperDash":false},{"StartTime":24948.0,"Position":349.1944,"HyperDash":false}]},{"StartTime":25107.0,"Objects":[{"StartTime":25107.0,"Position":196.0,"HyperDash":false},{"StartTime":25167.0,"Position":233.970291,"HyperDash":false},{"StartTime":25264.0,"Position":271.8056,"HyperDash":false}]},{"StartTime":25423.0,"Objects":[{"StartTime":25423.0,"Position":415.0,"HyperDash":false}]},{"StartTime":25581.0,"Objects":[{"StartTime":25581.0,"Position":363.0,"HyperDash":false}]},{"StartTime":25739.0,"Objects":[{"StartTime":25739.0,"Position":263.0,"HyperDash":false},{"StartTime":25799.0,"Position":263.286316,"HyperDash":false},{"StartTime":25896.0,"Position":278.260681,"HyperDash":false}]},{"StartTime":26054.0,"Objects":[{"StartTime":26054.0,"Position":418.0,"HyperDash":false},{"StartTime":26114.0,"Position":438.267456,"HyperDash":false},{"StartTime":26211.0,"Position":432.8865,"HyperDash":false}]},{"StartTime":26370.0,"Objects":[{"StartTime":26370.0,"Position":251.0,"HyperDash":false},{"StartTime":26430.0,"Position":272.286316,"HyperDash":false},{"StartTime":26527.0,"Position":266.260681,"HyperDash":false}]},{"StartTime":26686.0,"Objects":[{"StartTime":26686.0,"Position":406.0,"HyperDash":false},{"StartTime":26746.0,"Position":403.267456,"HyperDash":false},{"StartTime":26843.0,"Position":420.8865,"HyperDash":false}]},{"StartTime":27002.0,"Objects":[{"StartTime":27002.0,"Position":326.0,"HyperDash":false},{"StartTime":27102.0,"Position":263.416656,"HyperDash":false},{"StartTime":27238.0,"Position":217.484329,"HyperDash":false}]},{"StartTime":27318.0,"Objects":[{"StartTime":27318.0,"Position":215.0,"HyperDash":false},{"StartTime":27418.0,"Position":244.682343,"HyperDash":false},{"StartTime":27554.0,"Position":323.347046,"HyperDash":false}]},{"StartTime":27633.0,"Objects":[{"StartTime":27633.0,"Position":324.0,"HyperDash":false},{"StartTime":27702.0,"Position":307.889435,"HyperDash":false},{"StartTime":27772.0,"Position":261.760223,"HyperDash":false},{"StartTime":27842.0,"Position":231.054062,"HyperDash":false},{"StartTime":27948.0,"Position":179.503586,"HyperDash":false}]},{"StartTime":28265.0,"Objects":[{"StartTime":28265.0,"Position":65.0,"HyperDash":false},{"StartTime":28334.0,"Position":61.494606,"HyperDash":false},{"StartTime":28404.0,"Position":60.7236862,"HyperDash":false},{"StartTime":28474.0,"Position":89.86115,"HyperDash":false},{"StartTime":28580.0,"Position":97.86348,"HyperDash":false}]},{"StartTime":28739.0,"Objects":[{"StartTime":28739.0,"Position":153.0,"HyperDash":false}]},{"StartTime":28897.0,"Objects":[{"StartTime":28897.0,"Position":153.0,"HyperDash":false}]},{"StartTime":29054.0,"Objects":[{"StartTime":29054.0,"Position":215.0,"HyperDash":false},{"StartTime":29114.0,"Position":247.582169,"HyperDash":false},{"StartTime":29211.0,"Position":279.019775,"HyperDash":false}]},{"StartTime":29370.0,"Objects":[{"StartTime":29370.0,"Position":332.0,"HyperDash":false},{"StartTime":29430.0,"Position":288.006042,"HyperDash":false},{"StartTime":29527.0,"Position":267.917969,"HyperDash":false}]},{"StartTime":29686.0,"Objects":[{"StartTime":29686.0,"Position":371.0,"HyperDash":false}]},{"StartTime":29844.0,"Objects":[{"StartTime":29844.0,"Position":371.0,"HyperDash":false}]},{"StartTime":30002.0,"Objects":[{"StartTime":30002.0,"Position":444.0,"HyperDash":false}]},{"StartTime":30160.0,"Objects":[{"StartTime":30160.0,"Position":444.0,"HyperDash":false},{"StartTime":30220.0,"Position":451.1502,"HyperDash":false},{"StartTime":30317.0,"Position":463.328674,"HyperDash":false}]},{"StartTime":30476.0,"Objects":[{"StartTime":30476.0,"Position":393.0,"HyperDash":false},{"StartTime":30536.0,"Position":364.8498,"HyperDash":false},{"StartTime":30633.0,"Position":373.671326,"HyperDash":false}]},{"StartTime":30791.0,"Objects":[{"StartTime":30791.0,"Position":265.0,"HyperDash":false},{"StartTime":30851.0,"Position":250.101547,"HyperDash":false},{"StartTime":30948.0,"Position":197.232391,"HyperDash":false}]},{"StartTime":31107.0,"Objects":[{"StartTime":31107.0,"Position":80.0,"HyperDash":false},{"StartTime":31167.0,"Position":86.86766,"HyperDash":false},{"StartTime":31264.0,"Position":147.687042,"HyperDash":false}]},{"StartTime":31423.0,"Objects":[{"StartTime":31423.0,"Position":124.0,"HyperDash":false},{"StartTime":31483.0,"Position":115.084091,"HyperDash":false},{"StartTime":31580.0,"Position":56.1867,"HyperDash":false}]},{"StartTime":31739.0,"Objects":[{"StartTime":31739.0,"Position":164.0,"HyperDash":false}]},{"StartTime":31897.0,"Objects":[{"StartTime":31897.0,"Position":164.0,"HyperDash":false},{"StartTime":31957.0,"Position":198.867661,"HyperDash":false},{"StartTime":32054.0,"Position":231.687042,"HyperDash":false}]},{"StartTime":32212.0,"Objects":[{"StartTime":32212.0,"Position":365.0,"HyperDash":false}]},{"StartTime":32370.0,"Objects":[{"StartTime":32370.0,"Position":365.0,"HyperDash":false},{"StartTime":32430.0,"Position":375.563446,"HyperDash":false},{"StartTime":32527.0,"Position":383.7374,"HyperDash":false}]},{"StartTime":32686.0,"Objects":[{"StartTime":32686.0,"Position":488.0,"HyperDash":false},{"StartTime":32755.0,"Position":478.588562,"HyperDash":false},{"StartTime":32825.0,"Position":485.259918,"HyperDash":false},{"StartTime":32895.0,"Position":471.216827,"HyperDash":false},{"StartTime":33001.0,"Position":467.5074,"HyperDash":false}]},{"StartTime":33160.0,"Objects":[{"StartTime":33160.0,"Position":406.0,"HyperDash":false}]},{"StartTime":33318.0,"Objects":[{"StartTime":33318.0,"Position":277.0,"HyperDash":false},{"StartTime":33387.0,"Position":249.144089,"HyperDash":false},{"StartTime":33457.0,"Position":211.528214,"HyperDash":false},{"StartTime":33527.0,"Position":207.367355,"HyperDash":false},{"StartTime":33633.0,"Position":161.097717,"HyperDash":false}]},{"StartTime":33791.0,"Objects":[{"StartTime":33791.0,"Position":283.0,"HyperDash":false}]},{"StartTime":33949.0,"Objects":[{"StartTime":33949.0,"Position":283.0,"HyperDash":false}]},{"StartTime":34107.0,"Objects":[{"StartTime":34107.0,"Position":158.0,"HyperDash":false},{"StartTime":34167.0,"Position":122.620682,"HyperDash":false},{"StartTime":34264.0,"Position":93.20762,"HyperDash":false}]},{"StartTime":34423.0,"Objects":[{"StartTime":34423.0,"Position":19.0,"HyperDash":false},{"StartTime":34483.0,"Position":37.9395447,"HyperDash":false},{"StartTime":34580.0,"Position":83.68275,"HyperDash":false}]},{"StartTime":34739.0,"Objects":[{"StartTime":34739.0,"Position":158.0,"HyperDash":false}]},{"StartTime":34897.0,"Objects":[{"StartTime":34897.0,"Position":158.0,"HyperDash":false}]},{"StartTime":35054.0,"Objects":[{"StartTime":35054.0,"Position":204.0,"HyperDash":false}]},{"StartTime":35212.0,"Objects":[{"StartTime":35212.0,"Position":204.0,"HyperDash":false},{"StartTime":35272.0,"Position":191.310013,"HyperDash":false},{"StartTime":35369.0,"Position":216.139023,"HyperDash":false}]},{"StartTime":35528.0,"Objects":[{"StartTime":35528.0,"Position":345.0,"HyperDash":false},{"StartTime":35588.0,"Position":339.8689,"HyperDash":false},{"StartTime":35685.0,"Position":332.011932,"HyperDash":false}]},{"StartTime":35844.0,"Objects":[{"StartTime":35844.0,"Position":461.0,"HyperDash":false},{"StartTime":35922.0,"Position":426.3106,"HyperDash":false},{"StartTime":36001.0,"Position":379.89856,"HyperDash":false},{"StartTime":36080.0,"Position":350.1098,"HyperDash":false},{"StartTime":36159.0,"Position":330.750031,"HyperDash":false},{"StartTime":36229.0,"Position":366.784668,"HyperDash":false},{"StartTime":36299.0,"Position":388.860931,"HyperDash":false},{"StartTime":36369.0,"Position":414.029144,"HyperDash":false},{"StartTime":36475.0,"Position":461.0,"HyperDash":false}]},{"StartTime":36791.0,"Objects":[{"StartTime":36791.0,"Position":248.0,"HyperDash":false}]},{"StartTime":36949.0,"Objects":[{"StartTime":36949.0,"Position":248.0,"HyperDash":false},{"StartTime":37009.0,"Position":242.674042,"HyperDash":false},{"StartTime":37106.0,"Position":261.06012,"HyperDash":false}]},{"StartTime":37265.0,"Objects":[{"StartTime":37265.0,"Position":189.0,"HyperDash":false}]},{"StartTime":37423.0,"Objects":[{"StartTime":37423.0,"Position":130.0,"HyperDash":false},{"StartTime":37483.0,"Position":98.5721054,"HyperDash":false},{"StartTime":37580.0,"Position":66.22712,"HyperDash":false}]},{"StartTime":37739.0,"Objects":[{"StartTime":37739.0,"Position":32.0,"HyperDash":false}]},{"StartTime":37897.0,"Objects":[{"StartTime":37897.0,"Position":79.0,"HyperDash":false}]},{"StartTime":38054.0,"Objects":[{"StartTime":38054.0,"Position":126.0,"HyperDash":false}]},{"StartTime":38212.0,"Objects":[{"StartTime":38212.0,"Position":67.0,"HyperDash":false}]},{"StartTime":38370.0,"Objects":[{"StartTime":38370.0,"Position":189.0,"HyperDash":false},{"StartTime":38439.0,"Position":208.518677,"HyperDash":false},{"StartTime":38509.0,"Position":230.192429,"HyperDash":false},{"StartTime":38579.0,"Position":265.933716,"HyperDash":false},{"StartTime":38685.0,"Position":294.0829,"HyperDash":false}]},{"StartTime":38844.0,"Objects":[{"StartTime":38844.0,"Position":281.0,"HyperDash":false},{"StartTime":38922.0,"Position":240.8632,"HyperDash":false},{"StartTime":39001.0,"Position":224.955368,"HyperDash":false},{"StartTime":39062.0,"Position":248.9081,"HyperDash":false},{"StartTime":39159.0,"Position":281.0,"HyperDash":false}]},{"StartTime":39318.0,"Objects":[{"StartTime":39318.0,"Position":367.0,"HyperDash":false},{"StartTime":39378.0,"Position":404.815552,"HyperDash":false},{"StartTime":39475.0,"Position":423.320465,"HyperDash":false}]},{"StartTime":39633.0,"Objects":[{"StartTime":39633.0,"Position":493.0,"HyperDash":false}]},{"StartTime":39791.0,"Objects":[{"StartTime":39791.0,"Position":493.0,"HyperDash":false},{"StartTime":39851.0,"Position":484.550964,"HyperDash":false},{"StartTime":39948.0,"Position":499.675018,"HyperDash":false}]},{"StartTime":40107.0,"Objects":[{"StartTime":40107.0,"Position":450.0,"HyperDash":false},{"StartTime":40167.0,"Position":441.449036,"HyperDash":false},{"StartTime":40264.0,"Position":443.324982,"HyperDash":false}]},{"StartTime":40423.0,"Objects":[{"StartTime":40423.0,"Position":379.0,"HyperDash":false}]},{"StartTime":40581.0,"Objects":[{"StartTime":40581.0,"Position":379.0,"HyperDash":false}]},{"StartTime":40739.0,"Objects":[{"StartTime":40739.0,"Position":312.0,"HyperDash":false},{"StartTime":40808.0,"Position":302.798431,"HyperDash":false},{"StartTime":40878.0,"Position":242.555145,"HyperDash":false},{"StartTime":40948.0,"Position":257.265869,"HyperDash":false},{"StartTime":41054.0,"Position":204.051636,"HyperDash":false}]},{"StartTime":41212.0,"Objects":[{"StartTime":41212.0,"Position":120.0,"HyperDash":false},{"StartTime":41290.0,"Position":125.193611,"HyperDash":false},{"StartTime":41369.0,"Position":107.002182,"HyperDash":false},{"StartTime":41430.0,"Position":88.91298,"HyperDash":false},{"StartTime":41527.0,"Position":120.0,"HyperDash":false}]},{"StartTime":41686.0,"Objects":[{"StartTime":41686.0,"Position":195.0,"HyperDash":false},{"StartTime":41746.0,"Position":179.895126,"HyperDash":false},{"StartTime":41843.0,"Position":181.9226,"HyperDash":false}]},{"StartTime":42002.0,"Objects":[{"StartTime":42002.0,"Position":81.0,"HyperDash":false}]},{"StartTime":42160.0,"Objects":[{"StartTime":42160.0,"Position":81.0,"HyperDash":false}]},{"StartTime":42318.0,"Objects":[{"StartTime":42318.0,"Position":157.0,"HyperDash":false}]},{"StartTime":42476.0,"Objects":[{"StartTime":42476.0,"Position":157.0,"HyperDash":false},{"StartTime":42536.0,"Position":192.028351,"HyperDash":false},{"StartTime":42633.0,"Position":217.2575,"HyperDash":false}]},{"StartTime":42791.0,"Objects":[{"StartTime":42791.0,"Position":314.0,"HyperDash":false},{"StartTime":42851.0,"Position":349.048828,"HyperDash":false},{"StartTime":42948.0,"Position":374.311127,"HyperDash":false}]},{"StartTime":43107.0,"Objects":[{"StartTime":43107.0,"Position":224.0,"HyperDash":false},{"StartTime":43176.0,"Position":212.567841,"HyperDash":false},{"StartTime":43246.0,"Position":162.7526,"HyperDash":false},{"StartTime":43316.0,"Position":129.937347,"HyperDash":false},{"StartTime":43422.0,"Position":103.331413,"HyperDash":false}]},{"StartTime":43581.0,"Objects":[{"StartTime":43581.0,"Position":18.0,"HyperDash":false},{"StartTime":43659.0,"Position":17.9562778,"HyperDash":false},{"StartTime":43738.0,"Position":25.988636,"HyperDash":false},{"StartTime":43799.0,"Position":35.9199829,"HyperDash":false},{"StartTime":43896.0,"Position":18.0,"HyperDash":false}]},{"StartTime":44054.0,"Objects":[{"StartTime":44054.0,"Position":118.0,"HyperDash":false},{"StartTime":44114.0,"Position":113.573334,"HyperDash":false},{"StartTime":44211.0,"Position":109.033562,"HyperDash":false}]},{"StartTime":44370.0,"Objects":[{"StartTime":44370.0,"Position":32.0,"HyperDash":false}]},{"StartTime":44528.0,"Objects":[{"StartTime":44528.0,"Position":32.0,"HyperDash":false},{"StartTime":44588.0,"Position":34.55097,"HyperDash":false},{"StartTime":44685.0,"Position":38.6750336,"HyperDash":false}]},{"StartTime":44844.0,"Objects":[{"StartTime":44844.0,"Position":131.0,"HyperDash":false}]},{"StartTime":45002.0,"Objects":[{"StartTime":45002.0,"Position":131.0,"HyperDash":false},{"StartTime":45062.0,"Position":121.323151,"HyperDash":false},{"StartTime":45159.0,"Position":123.99559,"HyperDash":false}]},{"StartTime":45318.0,"Objects":[{"StartTime":45318.0,"Position":215.0,"HyperDash":false}]},{"StartTime":45476.0,"Objects":[{"StartTime":45476.0,"Position":215.0,"HyperDash":false},{"StartTime":45536.0,"Position":253.997345,"HyperDash":false},{"StartTime":45633.0,"Position":275.176361,"HyperDash":false}]},{"StartTime":45791.0,"Objects":[{"StartTime":45791.0,"Position":362.0,"HyperDash":false}]},{"StartTime":45949.0,"Objects":[{"StartTime":45949.0,"Position":362.0,"HyperDash":false}]},{"StartTime":46107.0,"Objects":[{"StartTime":46107.0,"Position":350.0,"HyperDash":false},{"StartTime":46167.0,"Position":360.8421,"HyperDash":false},{"StartTime":46264.0,"Position":354.8202,"HyperDash":false}]},{"StartTime":46423.0,"Objects":[{"StartTime":46423.0,"Position":421.0,"HyperDash":false}]},{"StartTime":46581.0,"Objects":[{"StartTime":46581.0,"Position":421.0,"HyperDash":false}]},{"StartTime":46739.0,"Objects":[{"StartTime":46739.0,"Position":343.0,"HyperDash":false},{"StartTime":46799.0,"Position":312.973572,"HyperDash":false},{"StartTime":46896.0,"Position":282.7475,"HyperDash":false}]},{"StartTime":47054.0,"Objects":[{"StartTime":47054.0,"Position":212.0,"HyperDash":false}]},{"StartTime":47212.0,"Objects":[{"StartTime":47212.0,"Position":176.0,"HyperDash":false}]},{"StartTime":47370.0,"Objects":[{"StartTime":47370.0,"Position":104.0,"HyperDash":false}]},{"StartTime":47449.0,"Objects":[{"StartTime":47449.0,"Position":104.0,"HyperDash":false}]},{"StartTime":47528.0,"Objects":[{"StartTime":47528.0,"Position":104.0,"HyperDash":false},{"StartTime":47628.0,"Position":115.182846,"HyperDash":false},{"StartTime":47764.0,"Position":88.40525,"HyperDash":false}]},{"StartTime":47844.0,"Objects":[{"StartTime":47844.0,"Position":73.0,"HyperDash":false},{"StartTime":47944.0,"Position":61.8171539,"HyperDash":false},{"StartTime":48080.0,"Position":88.59475,"HyperDash":false}]},{"StartTime":48160.0,"Objects":[{"StartTime":48160.0,"Position":108.0,"HyperDash":false}]},{"StartTime":48476.0,"Objects":[{"StartTime":48476.0,"Position":108.0,"HyperDash":false},{"StartTime":48536.0,"Position":130.401276,"HyperDash":false},{"StartTime":48633.0,"Position":176.902069,"HyperDash":false}]},{"StartTime":48791.0,"Objects":[{"StartTime":48791.0,"Position":259.0,"HyperDash":false},{"StartTime":48851.0,"Position":213.142151,"HyperDash":false},{"StartTime":48948.0,"Position":190.198868,"HyperDash":false}]},{"StartTime":49107.0,"Objects":[{"StartTime":49107.0,"Position":329.0,"HyperDash":false},{"StartTime":49176.0,"Position":349.21228,"HyperDash":false},{"StartTime":49246.0,"Position":377.302368,"HyperDash":false},{"StartTime":49316.0,"Position":400.9881,"HyperDash":false},{"StartTime":49422.0,"Position":453.358215,"HyperDash":false}]},{"StartTime":49581.0,"Objects":[{"StartTime":49581.0,"Position":328.0,"HyperDash":false}]},{"StartTime":49739.0,"Objects":[{"StartTime":49739.0,"Position":472.0,"HyperDash":false},{"StartTime":49799.0,"Position":465.138245,"HyperDash":false},{"StartTime":49896.0,"Position":454.808044,"HyperDash":false}]},{"StartTime":50054.0,"Objects":[{"StartTime":50054.0,"Position":324.0,"HyperDash":false},{"StartTime":50114.0,"Position":308.1143,"HyperDash":false},{"StartTime":50211.0,"Position":306.059082,"HyperDash":false}]},{"StartTime":50370.0,"Objects":[{"StartTime":50370.0,"Position":190.0,"HyperDash":false},{"StartTime":50439.0,"Position":173.841064,"HyperDash":false},{"StartTime":50509.0,"Position":134.478058,"HyperDash":false},{"StartTime":50579.0,"Position":91.0187149,"HyperDash":false},{"StartTime":50685.0,"Position":84.73538,"HyperDash":false}]},{"StartTime":50844.0,"Objects":[{"StartTime":50844.0,"Position":206.0,"HyperDash":false}]},{"StartTime":51002.0,"Objects":[{"StartTime":51002.0,"Position":313.0,"HyperDash":false},{"StartTime":51062.0,"Position":334.872467,"HyperDash":false},{"StartTime":51159.0,"Position":326.793732,"HyperDash":false}]},{"StartTime":51318.0,"Objects":[{"StartTime":51318.0,"Position":223.0,"HyperDash":false},{"StartTime":51378.0,"Position":206.316574,"HyperDash":false},{"StartTime":51475.0,"Position":208.626953,"HyperDash":false}]},{"StartTime":51633.0,"Objects":[{"StartTime":51633.0,"Position":268.0,"HyperDash":false},{"StartTime":51693.0,"Position":280.674469,"HyperDash":false},{"StartTime":51790.0,"Position":337.344574,"HyperDash":false}]},{"StartTime":51949.0,"Objects":[{"StartTime":51949.0,"Position":382.0,"HyperDash":false},{"StartTime":52009.0,"Position":340.1904,"HyperDash":false},{"StartTime":52106.0,"Position":312.41745,"HyperDash":false}]},{"StartTime":52265.0,"Objects":[{"StartTime":52265.0,"Position":191.0,"HyperDash":false},{"StartTime":52334.0,"Position":178.67337,"HyperDash":false},{"StartTime":52404.0,"Position":200.576569,"HyperDash":false},{"StartTime":52474.0,"Position":208.688736,"HyperDash":false},{"StartTime":52580.0,"Position":218.40831,"HyperDash":false}]},{"StartTime":52739.0,"Objects":[{"StartTime":52739.0,"Position":145.0,"HyperDash":false}]},{"StartTime":52897.0,"Objects":[{"StartTime":52897.0,"Position":75.0,"HyperDash":false},{"StartTime":52957.0,"Position":116.384956,"HyperDash":false},{"StartTime":53054.0,"Position":143.260788,"HyperDash":false}]},{"StartTime":53212.0,"Objects":[{"StartTime":53212.0,"Position":223.0,"HyperDash":false},{"StartTime":53272.0,"Position":252.329483,"HyperDash":false},{"StartTime":53369.0,"Position":291.407745,"HyperDash":false}]},{"StartTime":53528.0,"Objects":[{"StartTime":53528.0,"Position":423.0,"HyperDash":false}]},{"StartTime":53686.0,"Objects":[{"StartTime":53686.0,"Position":383.0,"HyperDash":false},{"StartTime":53746.0,"Position":384.076538,"HyperDash":false},{"StartTime":53843.0,"Position":360.5784,"HyperDash":false}]},{"StartTime":54002.0,"Objects":[{"StartTime":54002.0,"Position":445.0,"HyperDash":false},{"StartTime":54062.0,"Position":446.261871,"HyperDash":false},{"StartTime":54159.0,"Position":421.7946,"HyperDash":false}]},{"StartTime":54318.0,"Objects":[{"StartTime":54318.0,"Position":346.0,"HyperDash":false}]},{"StartTime":54476.0,"Objects":[{"StartTime":54476.0,"Position":268.0,"HyperDash":false},{"StartTime":54536.0,"Position":223.1805,"HyperDash":false},{"StartTime":54633.0,"Position":196.522339,"HyperDash":false}]},{"StartTime":54791.0,"Objects":[{"StartTime":54791.0,"Position":79.0,"HyperDash":false},{"StartTime":54860.0,"Position":75.98414,"HyperDash":false},{"StartTime":54930.0,"Position":115.252335,"HyperDash":false},{"StartTime":55000.0,"Position":116.421585,"HyperDash":false},{"StartTime":55106.0,"Position":110.769684,"HyperDash":false}]},{"StartTime":55265.0,"Objects":[{"StartTime":55265.0,"Position":38.0,"HyperDash":false},{"StartTime":55325.0,"Position":23.5258217,"HyperDash":false},{"StartTime":55422.0,"Position":54.2080231,"HyperDash":false}]},{"StartTime":55581.0,"Objects":[{"StartTime":55581.0,"Position":189.0,"HyperDash":false}]},{"StartTime":55739.0,"Objects":[{"StartTime":55739.0,"Position":125.0,"HyperDash":false},{"StartTime":55799.0,"Position":137.109589,"HyperDash":false},{"StartTime":55896.0,"Position":141.0302,"HyperDash":false}]},{"StartTime":56054.0,"Objects":[{"StartTime":56054.0,"Position":279.0,"HyperDash":false},{"StartTime":56114.0,"Position":308.761017,"HyperDash":false},{"StartTime":56211.0,"Position":351.217346,"HyperDash":false}]},{"StartTime":56370.0,"Objects":[{"StartTime":56370.0,"Position":470.0,"HyperDash":false},{"StartTime":56430.0,"Position":449.3282,"HyperDash":false},{"StartTime":56527.0,"Position":397.632721,"HyperDash":false}]},{"StartTime":56686.0,"Objects":[{"StartTime":56686.0,"Position":438.0,"HyperDash":false},{"StartTime":56746.0,"Position":427.2124,"HyperDash":false},{"StartTime":56843.0,"Position":445.736,"HyperDash":false}]},{"StartTime":57002.0,"Objects":[{"StartTime":57002.0,"Position":287.0,"HyperDash":false},{"StartTime":57062.0,"Position":284.352478,"HyperDash":false},{"StartTime":57159.0,"Position":294.1198,"HyperDash":false}]},{"StartTime":57318.0,"Objects":[{"StartTime":57318.0,"Position":334.0,"HyperDash":false},{"StartTime":57396.0,"Position":298.0179,"HyperDash":false},{"StartTime":57475.0,"Position":334.0,"HyperDash":false},{"StartTime":57554.0,"Position":298.0179,"HyperDash":false}]},{"StartTime":57633.0,"Objects":[{"StartTime":57633.0,"Position":230.0,"HyperDash":false},{"StartTime":57693.0,"Position":208.152359,"HyperDash":false},{"StartTime":57790.0,"Position":164.896362,"HyperDash":false}]},{"StartTime":57949.0,"Objects":[{"StartTime":57949.0,"Position":42.0,"HyperDash":false},{"StartTime":58009.0,"Position":61.24403,"HyperDash":false},{"StartTime":58106.0,"Position":66.01679,"HyperDash":false}]},{"StartTime":58265.0,"Objects":[{"StartTime":58265.0,"Position":188.0,"HyperDash":false},{"StartTime":58325.0,"Position":163.755981,"HyperDash":false},{"StartTime":58422.0,"Position":163.983215,"HyperDash":false}]},{"StartTime":58581.0,"Objects":[{"StartTime":58581.0,"Position":230.0,"HyperDash":false},{"StartTime":58641.0,"Position":261.006683,"HyperDash":false},{"StartTime":58738.0,"Position":299.391846,"HyperDash":false}]},{"StartTime":58897.0,"Objects":[{"StartTime":58897.0,"Position":146.0,"HyperDash":false},{"StartTime":58957.0,"Position":115.275429,"HyperDash":false},{"StartTime":59054.0,"Position":76.5043,"HyperDash":false}]},{"StartTime":59212.0,"Objects":[{"StartTime":59212.0,"Position":293.0,"HyperDash":false},{"StartTime":59281.0,"Position":302.5204,"HyperDash":false},{"StartTime":59351.0,"Position":304.419159,"HyperDash":false},{"StartTime":59421.0,"Position":314.467,"HyperDash":false},{"StartTime":59527.0,"Position":318.606537,"HyperDash":false}]},{"StartTime":59686.0,"Objects":[{"StartTime":59686.0,"Position":224.0,"HyperDash":false}]},{"StartTime":59844.0,"Objects":[{"StartTime":59844.0,"Position":405.0,"HyperDash":false},{"StartTime":59904.0,"Position":420.876434,"HyperDash":false},{"StartTime":60001.0,"Position":412.2616,"HyperDash":false}]},{"StartTime":60160.0,"Objects":[{"StartTime":60160.0,"Position":500.0,"HyperDash":false},{"StartTime":60220.0,"Position":492.536743,"HyperDash":false},{"StartTime":60317.0,"Position":429.739,"HyperDash":false}]},{"StartTime":60476.0,"Objects":[{"StartTime":60476.0,"Position":303.0,"HyperDash":false},{"StartTime":60545.0,"Position":319.948517,"HyperDash":false},{"StartTime":60615.0,"Position":348.183044,"HyperDash":false},{"StartTime":60685.0,"Position":402.9306,"HyperDash":false},{"StartTime":60791.0,"Position":439.958466,"HyperDash":false}]},{"StartTime":60949.0,"Objects":[{"StartTime":60949.0,"Position":311.0,"HyperDash":false}]},{"StartTime":61107.0,"Objects":[{"StartTime":61107.0,"Position":143.0,"HyperDash":false},{"StartTime":61167.0,"Position":158.994171,"HyperDash":false},{"StartTime":61264.0,"Position":156.7843,"HyperDash":false}]},{"StartTime":61423.0,"Objects":[{"StartTime":61423.0,"Position":63.0,"HyperDash":false},{"StartTime":61483.0,"Position":43.900074,"HyperDash":false},{"StartTime":61580.0,"Position":76.12121,"HyperDash":false}]},{"StartTime":61739.0,"Objects":[{"StartTime":61739.0,"Position":160.0,"HyperDash":false},{"StartTime":61799.0,"Position":167.994171,"HyperDash":false},{"StartTime":61896.0,"Position":173.7843,"HyperDash":false}]},{"StartTime":62055.0,"Objects":[{"StartTime":62055.0,"Position":80.0,"HyperDash":false},{"StartTime":62115.0,"Position":65.90008,"HyperDash":false},{"StartTime":62212.0,"Position":93.12121,"HyperDash":false}]},{"StartTime":62370.0,"Objects":[{"StartTime":62370.0,"Position":184.0,"HyperDash":false},{"StartTime":62439.0,"Position":195.225571,"HyperDash":false},{"StartTime":62509.0,"Position":239.72702,"HyperDash":false},{"StartTime":62579.0,"Position":260.956116,"HyperDash":false},{"StartTime":62685.0,"Position":306.492645,"HyperDash":false}]},{"StartTime":62844.0,"Objects":[{"StartTime":62844.0,"Position":406.0,"HyperDash":false}]},{"StartTime":63002.0,"Objects":[{"StartTime":63002.0,"Position":473.0,"HyperDash":false},{"StartTime":63062.0,"Position":481.5252,"HyperDash":false},{"StartTime":63159.0,"Position":455.637146,"HyperDash":false}]},{"StartTime":63318.0,"Objects":[{"StartTime":63318.0,"Position":331.0,"HyperDash":false},{"StartTime":63378.0,"Position":349.711639,"HyperDash":false},{"StartTime":63475.0,"Position":347.2463,"HyperDash":false}]},{"StartTime":63633.0,"Objects":[{"StartTime":63633.0,"Position":234.0,"HyperDash":false}]},{"StartTime":63791.0,"Objects":[{"StartTime":63791.0,"Position":160.0,"HyperDash":false},{"StartTime":63851.0,"Position":187.355438,"HyperDash":false},{"StartTime":63948.0,"Position":231.69101,"HyperDash":false}]},{"StartTime":64107.0,"Objects":[{"StartTime":64107.0,"Position":147.0,"HyperDash":false},{"StartTime":64167.0,"Position":126.032143,"HyperDash":false},{"StartTime":64264.0,"Position":74.96641,"HyperDash":false}]},{"StartTime":64423.0,"Objects":[{"StartTime":64423.0,"Position":35.0,"HyperDash":false}]},{"StartTime":64581.0,"Objects":[{"StartTime":64581.0,"Position":148.0,"HyperDash":false},{"StartTime":64641.0,"Position":112.032143,"HyperDash":false},{"StartTime":64738.0,"Position":75.96641,"HyperDash":false}]},{"StartTime":64897.0,"Objects":[{"StartTime":64897.0,"Position":18.0,"HyperDash":false}]},{"StartTime":65054.0,"Objects":[{"StartTime":65054.0,"Position":133.0,"HyperDash":false},{"StartTime":65114.0,"Position":141.7638,"HyperDash":false},{"StartTime":65211.0,"Position":148.7033,"HyperDash":false}]},{"StartTime":65370.0,"Objects":[{"StartTime":65370.0,"Position":224.0,"HyperDash":false},{"StartTime":65439.0,"Position":210.371689,"HyperDash":false},{"StartTime":65509.0,"Position":211.28067,"HyperDash":false},{"StartTime":65579.0,"Position":215.692825,"HyperDash":false},{"StartTime":65685.0,"Position":246.723145,"HyperDash":false}]},{"StartTime":65844.0,"Objects":[{"StartTime":65844.0,"Position":367.0,"HyperDash":false},{"StartTime":65904.0,"Position":394.7037,"HyperDash":false},{"StartTime":66001.0,"Position":437.557922,"HyperDash":false}]},{"StartTime":66160.0,"Objects":[{"StartTime":66160.0,"Position":456.0,"HyperDash":false},{"StartTime":66220.0,"Position":443.412323,"HyperDash":false},{"StartTime":66317.0,"Position":430.542175,"HyperDash":false}]},{"StartTime":66476.0,"Objects":[{"StartTime":66476.0,"Position":310.0,"HyperDash":false},{"StartTime":66536.0,"Position":332.587646,"HyperDash":false},{"StartTime":66633.0,"Position":335.457825,"HyperDash":false}]},{"StartTime":66791.0,"Objects":[{"StartTime":66791.0,"Position":452.0,"HyperDash":false},{"StartTime":66851.0,"Position":421.412354,"HyperDash":false},{"StartTime":66948.0,"Position":426.542175,"HyperDash":false}]},{"StartTime":67107.0,"Objects":[{"StartTime":67107.0,"Position":250.0,"HyperDash":false},{"StartTime":67167.0,"Position":259.587646,"HyperDash":false},{"StartTime":67264.0,"Position":275.457825,"HyperDash":false}]},{"StartTime":67423.0,"Objects":[{"StartTime":67423.0,"Position":143.0,"HyperDash":false},{"StartTime":67483.0,"Position":107.965904,"HyperDash":false},{"StartTime":67580.0,"Position":67.02744,"HyperDash":false}]},{"StartTime":67739.0,"Objects":[{"StartTime":67739.0,"Position":8.0,"HyperDash":false},{"StartTime":67799.0,"Position":39.0340958,"HyperDash":false},{"StartTime":67896.0,"Position":83.97256,"HyperDash":false}]},{"StartTime":68054.0,"Objects":[{"StartTime":68054.0,"Position":153.0,"HyperDash":false},{"StartTime":68123.0,"Position":136.712723,"HyperDash":false},{"StartTime":68193.0,"Position":96.94302,"HyperDash":false},{"StartTime":68263.0,"Position":47.1733246,"HyperDash":false},{"StartTime":68369.0,"Position":1.03634644,"HyperDash":false}]},{"StartTime":68686.0,"Objects":[{"StartTime":68686.0,"Position":162.0,"HyperDash":false},{"StartTime":68764.0,"Position":140.279373,"HyperDash":false},{"StartTime":68843.0,"Position":149.194489,"HyperDash":false},{"StartTime":68904.0,"Position":160.855484,"HyperDash":false},{"StartTime":69001.0,"Position":162.0,"HyperDash":false}]},{"StartTime":69160.0,"Objects":[{"StartTime":69160.0,"Position":264.0,"HyperDash":false}]},{"StartTime":69318.0,"Objects":[{"StartTime":69318.0,"Position":264.0,"HyperDash":false},{"StartTime":69387.0,"Position":293.878754,"HyperDash":false},{"StartTime":69457.0,"Position":308.962463,"HyperDash":false},{"StartTime":69527.0,"Position":326.259735,"HyperDash":false},{"StartTime":69633.0,"Position":376.5735,"HyperDash":false}]},{"StartTime":69791.0,"Objects":[{"StartTime":69791.0,"Position":477.0,"HyperDash":false},{"StartTime":69851.0,"Position":473.9998,"HyperDash":false},{"StartTime":69948.0,"Position":451.330017,"HyperDash":false}]},{"StartTime":70107.0,"Objects":[{"StartTime":70107.0,"Position":352.0,"HyperDash":false}]},{"StartTime":70265.0,"Objects":[{"StartTime":70265.0,"Position":352.0,"HyperDash":false},{"StartTime":70325.0,"Position":355.88562,"HyperDash":false},{"StartTime":70422.0,"Position":377.063232,"HyperDash":false}]},{"StartTime":70581.0,"Objects":[{"StartTime":70581.0,"Position":252.0,"HyperDash":false},{"StartTime":70650.0,"Position":243.345444,"HyperDash":false},{"StartTime":70720.0,"Position":190.933167,"HyperDash":false},{"StartTime":70790.0,"Position":181.559875,"HyperDash":false},{"StartTime":70896.0,"Position":139.968262,"HyperDash":false}]},{"StartTime":71212.0,"Objects":[{"StartTime":71212.0,"Position":139.0,"HyperDash":false},{"StartTime":71272.0,"Position":142.551361,"HyperDash":false},{"StartTime":71369.0,"Position":117.237366,"HyperDash":false}]},{"StartTime":71528.0,"Objects":[{"StartTime":71528.0,"Position":197.0,"HyperDash":false}]},{"StartTime":71686.0,"Objects":[{"StartTime":71686.0,"Position":197.0,"HyperDash":false}]},{"StartTime":71844.0,"Objects":[{"StartTime":71844.0,"Position":246.0,"HyperDash":false},{"StartTime":71904.0,"Position":274.251373,"HyperDash":false},{"StartTime":72001.0,"Position":310.825043,"HyperDash":false}]},{"StartTime":72160.0,"Objects":[{"StartTime":72160.0,"Position":382.0,"HyperDash":false}]},{"StartTime":72318.0,"Objects":[{"StartTime":72318.0,"Position":382.0,"HyperDash":false},{"StartTime":72387.0,"Position":375.660431,"HyperDash":false},{"StartTime":72457.0,"Position":397.292725,"HyperDash":false},{"StartTime":72527.0,"Position":400.838165,"HyperDash":false},{"StartTime":72633.0,"Position":413.841949,"HyperDash":false}]},{"StartTime":72791.0,"Objects":[{"StartTime":72791.0,"Position":483.0,"HyperDash":false},{"StartTime":72851.0,"Position":443.387177,"HyperDash":false},{"StartTime":72948.0,"Position":422.070221,"HyperDash":false}]},{"StartTime":73107.0,"Objects":[{"StartTime":73107.0,"Position":316.0,"HyperDash":false}]},{"StartTime":73265.0,"Objects":[{"StartTime":73265.0,"Position":316.0,"HyperDash":false}]},{"StartTime":73423.0,"Objects":[{"StartTime":73423.0,"Position":213.0,"HyperDash":false},{"StartTime":73483.0,"Position":236.624237,"HyperDash":false},{"StartTime":73580.0,"Position":274.115784,"HyperDash":false}]},{"StartTime":73739.0,"Objects":[{"StartTime":73739.0,"Position":151.0,"HyperDash":false},{"StartTime":73808.0,"Position":168.107178,"HyperDash":false},{"StartTime":73878.0,"Position":188.948715,"HyperDash":false},{"StartTime":73948.0,"Position":173.253,"HyperDash":false},{"StartTime":74054.0,"Position":186.276779,"HyperDash":false}]},{"StartTime":74212.0,"Objects":[{"StartTime":74212.0,"Position":71.0,"HyperDash":false}]},{"StartTime":74370.0,"Objects":[{"StartTime":74370.0,"Position":71.0,"HyperDash":false},{"StartTime":74439.0,"Position":73.10717,"HyperDash":false},{"StartTime":74509.0,"Position":99.94872,"HyperDash":false},{"StartTime":74579.0,"Position":82.253,"HyperDash":false},{"StartTime":74685.0,"Position":106.276787,"HyperDash":false}]},{"StartTime":74844.0,"Objects":[{"StartTime":74844.0,"Position":217.0,"HyperDash":false},{"StartTime":74904.0,"Position":195.833557,"HyperDash":false},{"StartTime":75001.0,"Position":203.228043,"HyperDash":false}]},{"StartTime":75160.0,"Objects":[{"StartTime":75160.0,"Position":292.0,"HyperDash":false},{"StartTime":75220.0,"Position":322.137878,"HyperDash":false},{"StartTime":75317.0,"Position":355.583832,"HyperDash":false}]},{"StartTime":75476.0,"Objects":[{"StartTime":75476.0,"Position":470.0,"HyperDash":false}]},{"StartTime":75633.0,"Objects":[{"StartTime":75633.0,"Position":470.0,"HyperDash":false},{"StartTime":75702.0,"Position":451.070618,"HyperDash":false},{"StartTime":75772.0,"Position":423.6466,"HyperDash":false},{"StartTime":75842.0,"Position":385.354156,"HyperDash":false},{"StartTime":75948.0,"Position":339.91098,"HyperDash":false}]},{"StartTime":76265.0,"Objects":[{"StartTime":76265.0,"Position":339.0,"HyperDash":false},{"StartTime":76325.0,"Position":330.2449,"HyperDash":false},{"StartTime":76422.0,"Position":356.729736,"HyperDash":false}]},{"StartTime":76581.0,"Objects":[{"StartTime":76581.0,"Position":274.0,"HyperDash":false}]},{"StartTime":76739.0,"Objects":[{"StartTime":76739.0,"Position":274.0,"HyperDash":false}]},{"StartTime":76897.0,"Objects":[{"StartTime":76897.0,"Position":196.0,"HyperDash":false},{"StartTime":76957.0,"Position":202.336975,"HyperDash":false},{"StartTime":77054.0,"Position":177.609283,"HyperDash":false}]},{"StartTime":77212.0,"Objects":[{"StartTime":77212.0,"Position":76.0,"HyperDash":false},{"StartTime":77272.0,"Position":87.663,"HyperDash":false},{"StartTime":77369.0,"Position":94.3907,"HyperDash":false}]},{"StartTime":77528.0,"Objects":[{"StartTime":77528.0,"Position":193.0,"HyperDash":false},{"StartTime":77588.0,"Position":215.246429,"HyperDash":false},{"StartTime":77685.0,"Position":255.401413,"HyperDash":false}]},{"StartTime":77844.0,"Objects":[{"StartTime":77844.0,"Position":363.0,"HyperDash":false},{"StartTime":77904.0,"Position":335.063263,"HyperDash":false},{"StartTime":78001.0,"Position":300.441956,"HyperDash":false}]},{"StartTime":78160.0,"Objects":[{"StartTime":78160.0,"Position":424.0,"HyperDash":false},{"StartTime":78229.0,"Position":425.201782,"HyperDash":false},{"StartTime":78299.0,"Position":406.9763,"HyperDash":false},{"StartTime":78369.0,"Position":392.725616,"HyperDash":false},{"StartTime":78475.0,"Position":375.221161,"HyperDash":false}]},{"StartTime":78791.0,"Objects":[{"StartTime":78791.0,"Position":375.0,"HyperDash":false}]},{"StartTime":87633.0,"Objects":[{"StartTime":87633.0,"Position":59.0,"HyperDash":false},{"StartTime":87733.0,"Position":109.695786,"HyperDash":false},{"StartTime":87869.0,"Position":154.94931,"HyperDash":false}]},{"StartTime":87949.0,"Objects":[{"StartTime":87949.0,"Position":157.0,"HyperDash":false},{"StartTime":88049.0,"Position":98.90486,"HyperDash":false},{"StartTime":88185.0,"Position":61.01484,"HyperDash":false}]},{"StartTime":88265.0,"Objects":[{"StartTime":88265.0,"Position":65.0,"HyperDash":false},{"StartTime":88365.0,"Position":107.226257,"HyperDash":false},{"StartTime":88501.0,"Position":160.443985,"HyperDash":false}]},{"StartTime":88581.0,"Objects":[{"StartTime":88581.0,"Position":162.0,"HyperDash":false}]},{"StartTime":88897.0,"Objects":[{"StartTime":88897.0,"Position":410.0,"HyperDash":false},{"StartTime":88957.0,"Position":434.139282,"HyperDash":false},{"StartTime":89054.0,"Position":430.5437,"HyperDash":false}]},{"StartTime":89212.0,"Objects":[{"StartTime":89212.0,"Position":329.0,"HyperDash":false}]},{"StartTime":89370.0,"Objects":[{"StartTime":89370.0,"Position":237.0,"HyperDash":false},{"StartTime":89430.0,"Position":206.860718,"HyperDash":false},{"StartTime":89527.0,"Position":216.4563,"HyperDash":false}]},{"StartTime":89686.0,"Objects":[{"StartTime":89686.0,"Position":412.0,"HyperDash":false},{"StartTime":89746.0,"Position":427.040955,"HyperDash":false},{"StartTime":89843.0,"Position":390.8584,"HyperDash":false}]},{"StartTime":90002.0,"Objects":[{"StartTime":90002.0,"Position":224.0,"HyperDash":false},{"StartTime":90071.0,"Position":193.575424,"HyperDash":false},{"StartTime":90141.0,"Position":140.9065,"HyperDash":false},{"StartTime":90211.0,"Position":134.488129,"HyperDash":false},{"StartTime":90317.0,"Position":98.64927,"HyperDash":false}]},{"StartTime":90476.0,"Objects":[{"StartTime":90476.0,"Position":198.0,"HyperDash":false}]},{"StartTime":90633.0,"Objects":[{"StartTime":90633.0,"Position":197.0,"HyperDash":false}]},{"StartTime":90791.0,"Objects":[{"StartTime":90791.0,"Position":85.0,"HyperDash":false},{"StartTime":90851.0,"Position":83.48808,"HyperDash":false},{"StartTime":90948.0,"Position":98.0172348,"HyperDash":false}]},{"StartTime":91107.0,"Objects":[{"StartTime":91107.0,"Position":308.0,"HyperDash":false},{"StartTime":91167.0,"Position":319.751,"HyperDash":false},{"StartTime":91264.0,"Position":319.957062,"HyperDash":false}]},{"StartTime":91423.0,"Objects":[{"StartTime":91423.0,"Position":210.0,"HyperDash":false},{"StartTime":91483.0,"Position":236.879288,"HyperDash":false},{"StartTime":91580.0,"Position":290.540375,"HyperDash":false}]},{"StartTime":91739.0,"Objects":[{"StartTime":91739.0,"Position":196.0,"HyperDash":false}]},{"StartTime":91897.0,"Objects":[{"StartTime":91897.0,"Position":305.0,"HyperDash":false},{"StartTime":91957.0,"Position":317.8793,"HyperDash":false},{"StartTime":92054.0,"Position":385.540375,"HyperDash":false}]},{"StartTime":92212.0,"Objects":[{"StartTime":92212.0,"Position":212.0,"HyperDash":false},{"StartTime":92272.0,"Position":221.879288,"HyperDash":false},{"StartTime":92369.0,"Position":292.540375,"HyperDash":false}]},{"StartTime":92528.0,"Objects":[{"StartTime":92528.0,"Position":446.0,"HyperDash":false},{"StartTime":92597.0,"Position":444.5924,"HyperDash":false},{"StartTime":92667.0,"Position":489.460175,"HyperDash":false},{"StartTime":92737.0,"Position":462.152466,"HyperDash":false},{"StartTime":92843.0,"Position":484.6515,"HyperDash":false}]},{"StartTime":93002.0,"Objects":[{"StartTime":93002.0,"Position":286.0,"HyperDash":false}]},{"StartTime":93160.0,"Objects":[{"StartTime":93160.0,"Position":368.0,"HyperDash":false}]},{"StartTime":93318.0,"Objects":[{"StartTime":93318.0,"Position":268.0,"HyperDash":false},{"StartTime":93378.0,"Position":258.177734,"HyperDash":false},{"StartTime":93475.0,"Position":188.322281,"HyperDash":false}]},{"StartTime":93633.0,"Objects":[{"StartTime":93633.0,"Position":349.0,"HyperDash":false},{"StartTime":93693.0,"Position":301.103668,"HyperDash":false},{"StartTime":93790.0,"Position":269.135223,"HyperDash":false}]},{"StartTime":93949.0,"Objects":[{"StartTime":93949.0,"Position":138.0,"HyperDash":false},{"StartTime":94009.0,"Position":122.494843,"HyperDash":false},{"StartTime":94106.0,"Position":107.101913,"HyperDash":false}]},{"StartTime":94265.0,"Objects":[{"StartTime":94265.0,"Position":148.0,"HyperDash":false}]},{"StartTime":94423.0,"Objects":[{"StartTime":94423.0,"Position":22.0,"HyperDash":false},{"StartTime":94483.0,"Position":32.5051575,"HyperDash":false},{"StartTime":94580.0,"Position":52.89809,"HyperDash":false}]},{"StartTime":94739.0,"Objects":[{"StartTime":94739.0,"Position":243.0,"HyperDash":false},{"StartTime":94799.0,"Position":236.5184,"HyperDash":false},{"StartTime":94896.0,"Position":272.894073,"HyperDash":false}]},{"StartTime":95054.0,"Objects":[{"StartTime":95054.0,"Position":438.0,"HyperDash":false},{"StartTime":95123.0,"Position":388.7492,"HyperDash":false},{"StartTime":95193.0,"Position":392.3104,"HyperDash":false},{"StartTime":95263.0,"Position":331.956,"HyperDash":false},{"StartTime":95369.0,"Position":294.5527,"HyperDash":false}]},{"StartTime":95528.0,"Objects":[{"StartTime":95528.0,"Position":254.0,"HyperDash":false},{"StartTime":95588.0,"Position":290.0384,"HyperDash":false},{"StartTime":95685.0,"Position":283.842346,"HyperDash":false}]},{"StartTime":95844.0,"Objects":[{"StartTime":95844.0,"Position":427.0,"HyperDash":false},{"StartTime":95904.0,"Position":416.4857,"HyperDash":false},{"StartTime":96001.0,"Position":442.904083,"HyperDash":false}]},{"StartTime":96160.0,"Objects":[{"StartTime":96160.0,"Position":279.0,"HyperDash":false},{"StartTime":96220.0,"Position":299.0384,"HyperDash":false},{"StartTime":96317.0,"Position":308.842346,"HyperDash":false}]},{"StartTime":96476.0,"Objects":[{"StartTime":96476.0,"Position":225.0,"HyperDash":false},{"StartTime":96536.0,"Position":210.338287,"HyperDash":false},{"StartTime":96633.0,"Position":143.344467,"HyperDash":false}]},{"StartTime":96791.0,"Objects":[{"StartTime":96791.0,"Position":288.0,"HyperDash":false}]},{"StartTime":96949.0,"Objects":[{"StartTime":96949.0,"Position":180.0,"HyperDash":false},{"StartTime":97009.0,"Position":166.338287,"HyperDash":false},{"StartTime":97106.0,"Position":98.3444748,"HyperDash":false}]},{"StartTime":97265.0,"Objects":[{"StartTime":97265.0,"Position":274.0,"HyperDash":false},{"StartTime":97325.0,"Position":309.692352,"HyperDash":false},{"StartTime":97422.0,"Position":355.842438,"HyperDash":false}]},{"StartTime":97581.0,"Objects":[{"StartTime":97581.0,"Position":417.0,"HyperDash":false}]},{"StartTime":97739.0,"Objects":[{"StartTime":97739.0,"Position":420.0,"HyperDash":false},{"StartTime":97799.0,"Position":396.8472,"HyperDash":false},{"StartTime":97896.0,"Position":380.9233,"HyperDash":false}]},{"StartTime":98054.0,"Objects":[{"StartTime":98054.0,"Position":346.0,"HyperDash":false}]},{"StartTime":98212.0,"Objects":[{"StartTime":98212.0,"Position":299.0,"HyperDash":false}]},{"StartTime":98370.0,"Objects":[{"StartTime":98370.0,"Position":337.0,"HyperDash":false}]},{"StartTime":98528.0,"Objects":[{"StartTime":98528.0,"Position":290.0,"HyperDash":false}]},{"StartTime":98686.0,"Objects":[{"StartTime":98686.0,"Position":170.0,"HyperDash":false},{"StartTime":98746.0,"Position":129.894,"HyperDash":false},{"StartTime":98843.0,"Position":88.38194,"HyperDash":false}]},{"StartTime":99002.0,"Objects":[{"StartTime":99002.0,"Position":45.0,"HyperDash":false},{"StartTime":99062.0,"Position":73.99868,"HyperDash":false},{"StartTime":99159.0,"Position":87.32532,"HyperDash":false}]},{"StartTime":99318.0,"Objects":[{"StartTime":99318.0,"Position":164.0,"HyperDash":false}]},{"StartTime":99476.0,"Objects":[{"StartTime":99476.0,"Position":146.0,"HyperDash":false},{"StartTime":99536.0,"Position":113.96389,"HyperDash":false},{"StartTime":99633.0,"Position":66.87661,"HyperDash":false}]},{"StartTime":99791.0,"Objects":[{"StartTime":99791.0,"Position":163.0,"HyperDash":false},{"StartTime":99851.0,"Position":182.9796,"HyperDash":false},{"StartTime":99948.0,"Position":242.314056,"HyperDash":false}]},{"StartTime":100107.0,"Objects":[{"StartTime":100107.0,"Position":306.0,"HyperDash":false},{"StartTime":100176.0,"Position":272.841949,"HyperDash":false},{"StartTime":100246.0,"Position":282.58606,"HyperDash":false},{"StartTime":100316.0,"Position":262.382751,"HyperDash":false},{"StartTime":100422.0,"Position":258.4074,"HyperDash":false}]},{"StartTime":100581.0,"Objects":[{"StartTime":100581.0,"Position":446.0,"HyperDash":false}]},{"StartTime":100739.0,"Objects":[{"StartTime":100739.0,"Position":376.0,"HyperDash":false},{"StartTime":100799.0,"Position":361.111847,"HyperDash":false},{"StartTime":100896.0,"Position":305.5532,"HyperDash":false}]},{"StartTime":101054.0,"Objects":[{"StartTime":101054.0,"Position":236.0,"HyperDash":false}]},{"StartTime":101212.0,"Objects":[{"StartTime":101212.0,"Position":402.0,"HyperDash":false},{"StartTime":101272.0,"Position":446.655,"HyperDash":false},{"StartTime":101369.0,"Position":481.3611,"HyperDash":false}]},{"StartTime":101528.0,"Objects":[{"StartTime":101528.0,"Position":334.0,"HyperDash":false},{"StartTime":101588.0,"Position":334.394165,"HyperDash":false},{"StartTime":101685.0,"Position":350.023041,"HyperDash":false}]},{"StartTime":101844.0,"Objects":[{"StartTime":101844.0,"Position":219.0,"HyperDash":false}]},{"StartTime":102002.0,"Objects":[{"StartTime":102002.0,"Position":177.0,"HyperDash":false},{"StartTime":102062.0,"Position":159.9585,"HyperDash":false},{"StartTime":102159.0,"Position":98.64363,"HyperDash":false}]},{"StartTime":102318.0,"Objects":[{"StartTime":102318.0,"Position":140.0,"HyperDash":false},{"StartTime":102378.0,"Position":163.494385,"HyperDash":false},{"StartTime":102475.0,"Position":218.169327,"HyperDash":false}]},{"StartTime":102633.0,"Objects":[{"StartTime":102633.0,"Position":22.0,"HyperDash":false},{"StartTime":102702.0,"Position":31.6368866,"HyperDash":false},{"StartTime":102772.0,"Position":59.88773,"HyperDash":false},{"StartTime":102842.0,"Position":57.2475433,"HyperDash":false},{"StartTime":102948.0,"Position":67.89443,"HyperDash":false}]},{"StartTime":103107.0,"Objects":[{"StartTime":103107.0,"Position":182.0,"HyperDash":false}]},{"StartTime":103265.0,"Objects":[{"StartTime":103265.0,"Position":200.0,"HyperDash":false},{"StartTime":103325.0,"Position":221.459839,"HyperDash":false},{"StartTime":103422.0,"Position":217.979309,"HyperDash":false}]},{"StartTime":103581.0,"Objects":[{"StartTime":103581.0,"Position":337.0,"HyperDash":false}]},{"StartTime":103739.0,"Objects":[{"StartTime":103739.0,"Position":331.0,"HyperDash":false},{"StartTime":103799.0,"Position":312.540161,"HyperDash":false},{"StartTime":103896.0,"Position":313.0207,"HyperDash":false}]},{"StartTime":104054.0,"Objects":[{"StartTime":104054.0,"Position":194.0,"HyperDash":false},{"StartTime":104123.0,"Position":231.002213,"HyperDash":false},{"StartTime":104193.0,"Position":276.3082,"HyperDash":false},{"StartTime":104263.0,"Position":277.368225,"HyperDash":false},{"StartTime":104369.0,"Position":325.1272,"HyperDash":false}]},{"StartTime":104528.0,"Objects":[{"StartTime":104528.0,"Position":142.0,"HyperDash":false},{"StartTime":104588.0,"Position":118.666763,"HyperDash":false},{"StartTime":104685.0,"Position":61.4790764,"HyperDash":false}]},{"StartTime":104844.0,"Objects":[{"StartTime":104844.0,"Position":187.0,"HyperDash":false},{"StartTime":104904.0,"Position":140.796371,"HyperDash":false},{"StartTime":105001.0,"Position":106.642426,"HyperDash":false}]},{"StartTime":105160.0,"Objects":[{"StartTime":105160.0,"Position":210.0,"HyperDash":false},{"StartTime":105220.0,"Position":216.543152,"HyperDash":false},{"StartTime":105317.0,"Position":232.886017,"HyperDash":false}]},{"StartTime":105476.0,"Objects":[{"StartTime":105476.0,"Position":339.0,"HyperDash":false},{"StartTime":105536.0,"Position":350.726563,"HyperDash":false},{"StartTime":105633.0,"Position":361.889038,"HyperDash":false}]},{"StartTime":105791.0,"Objects":[{"StartTime":105791.0,"Position":309.0,"HyperDash":false}]},{"StartTime":105949.0,"Objects":[{"StartTime":105949.0,"Position":454.0,"HyperDash":false},{"StartTime":106009.0,"Position":420.0147,"HyperDash":false},{"StartTime":106106.0,"Position":430.975983,"HyperDash":false}]},{"StartTime":106265.0,"Objects":[{"StartTime":106265.0,"Position":246.0,"HyperDash":false},{"StartTime":106325.0,"Position":244.2446,"HyperDash":false},{"StartTime":106422.0,"Position":268.0487,"HyperDash":false}]},{"StartTime":106581.0,"Objects":[{"StartTime":106581.0,"Position":133.0,"HyperDash":false},{"StartTime":106641.0,"Position":103.963638,"HyperDash":false},{"StartTime":106738.0,"Position":49.17154,"HyperDash":false}]},{"StartTime":106897.0,"Objects":[{"StartTime":106897.0,"Position":260.0,"HyperDash":false},{"StartTime":106957.0,"Position":304.036346,"HyperDash":false},{"StartTime":107054.0,"Position":343.828461,"HyperDash":false}]},{"StartTime":107212.0,"Objects":[{"StartTime":107212.0,"Position":127.0,"HyperDash":false},{"StartTime":107272.0,"Position":104.963638,"HyperDash":false},{"StartTime":107369.0,"Position":43.17154,"HyperDash":false}]},{"StartTime":107528.0,"Objects":[{"StartTime":107528.0,"Position":254.0,"HyperDash":false},{"StartTime":107588.0,"Position":292.036346,"HyperDash":false},{"StartTime":107685.0,"Position":337.828461,"HyperDash":false}]},{"StartTime":107844.0,"Objects":[{"StartTime":107844.0,"Position":479.0,"HyperDash":false}]},{"StartTime":108002.0,"Objects":[{"StartTime":108002.0,"Position":411.0,"HyperDash":false}]},{"StartTime":108160.0,"Objects":[{"StartTime":108160.0,"Position":400.0,"HyperDash":false}]},{"StartTime":108318.0,"Objects":[{"StartTime":108318.0,"Position":488.0,"HyperDash":false}]},{"StartTime":108476.0,"Objects":[{"StartTime":108476.0,"Position":319.0,"HyperDash":false},{"StartTime":108536.0,"Position":311.9797,"HyperDash":false},{"StartTime":108633.0,"Position":313.713531,"HyperDash":false}]},{"StartTime":108791.0,"Objects":[{"StartTime":108791.0,"Position":298.0,"HyperDash":false}]},{"StartTime":108949.0,"Objects":[{"StartTime":108949.0,"Position":220.0,"HyperDash":false}]},{"StartTime":109107.0,"Objects":[{"StartTime":109107.0,"Position":163.0,"HyperDash":false}]},{"StartTime":110212.0,"Objects":[{"StartTime":110212.0,"Position":160.0,"HyperDash":false}]},{"StartTime":111002.0,"Objects":[{"StartTime":111002.0,"Position":160.0,"HyperDash":false},{"StartTime":111102.0,"Position":170.38269,"HyperDash":false},{"StartTime":111238.0,"Position":193.050369,"HyperDash":false}]},{"StartTime":111318.0,"Objects":[{"StartTime":111318.0,"Position":214.0,"HyperDash":false},{"StartTime":111378.0,"Position":214.7743,"HyperDash":false},{"StartTime":111475.0,"Position":186.350418,"HyperDash":false}]},{"StartTime":111554.0,"Objects":[{"StartTime":111554.0,"Position":202.0,"HyperDash":false}]},{"StartTime":111633.0,"Objects":[{"StartTime":111633.0,"Position":202.0,"HyperDash":false}]},{"StartTime":112739.0,"Objects":[{"StartTime":112739.0,"Position":197.0,"HyperDash":false}]},{"StartTime":113528.0,"Objects":[{"StartTime":113528.0,"Position":197.0,"HyperDash":false},{"StartTime":113628.0,"Position":234.305908,"HyperDash":false},{"StartTime":113764.0,"Position":282.864716,"HyperDash":false}]},{"StartTime":113844.0,"Objects":[{"StartTime":113844.0,"Position":293.0,"HyperDash":false},{"StartTime":113904.0,"Position":330.591919,"HyperDash":false},{"StartTime":114001.0,"Position":348.628937,"HyperDash":false}]},{"StartTime":114081.0,"Objects":[{"StartTime":114081.0,"Position":413.0,"HyperDash":false}]},{"StartTime":114160.0,"Objects":[{"StartTime":114160.0,"Position":413.0,"HyperDash":false},{"StartTime":114220.0,"Position":422.708557,"HyperDash":false},{"StartTime":114317.0,"Position":432.737671,"HyperDash":false}]},{"StartTime":114476.0,"Objects":[{"StartTime":114476.0,"Position":328.0,"HyperDash":false}]},{"StartTime":114633.0,"Objects":[{"StartTime":114633.0,"Position":388.0,"HyperDash":false},{"StartTime":114693.0,"Position":376.2914,"HyperDash":false},{"StartTime":114790.0,"Position":368.262329,"HyperDash":false}]},{"StartTime":114949.0,"Objects":[{"StartTime":114949.0,"Position":218.0,"HyperDash":false},{"StartTime":115009.0,"Position":239.708572,"HyperDash":false},{"StartTime":115106.0,"Position":237.737671,"HyperDash":false}]},{"StartTime":115265.0,"Objects":[{"StartTime":115265.0,"Position":114.0,"HyperDash":false},{"StartTime":115334.0,"Position":111.665535,"HyperDash":false},{"StartTime":115404.0,"Position":90.91377,"HyperDash":false},{"StartTime":115474.0,"Position":109.772278,"HyperDash":false},{"StartTime":115580.0,"Position":75.52312,"HyperDash":false}]},{"StartTime":115739.0,"Objects":[{"StartTime":115739.0,"Position":206.0,"HyperDash":false},{"StartTime":115799.0,"Position":204.020508,"HyperDash":false},{"StartTime":115896.0,"Position":138.2174,"HyperDash":false}]},{"StartTime":116054.0,"Objects":[{"StartTime":116054.0,"Position":247.0,"HyperDash":false},{"StartTime":116114.0,"Position":266.224854,"HyperDash":false},{"StartTime":116211.0,"Position":314.45578,"HyperDash":false}]},{"StartTime":116370.0,"Objects":[{"StartTime":116370.0,"Position":406.0,"HyperDash":false},{"StartTime":116430.0,"Position":420.447754,"HyperDash":false},{"StartTime":116527.0,"Position":416.6286,"HyperDash":false}]},{"StartTime":116686.0,"Objects":[{"StartTime":116686.0,"Position":477.0,"HyperDash":false},{"StartTime":116746.0,"Position":430.998718,"HyperDash":false},{"StartTime":116843.0,"Position":396.846619,"HyperDash":false}]},{"StartTime":117002.0,"Objects":[{"StartTime":117002.0,"Position":286.0,"HyperDash":false}]},{"StartTime":117160.0,"Objects":[{"StartTime":117160.0,"Position":210.0,"HyperDash":false},{"StartTime":117220.0,"Position":257.991272,"HyperDash":false},{"StartTime":117317.0,"Position":289.721649,"HyperDash":false}]},{"StartTime":117476.0,"Objects":[{"StartTime":117476.0,"Position":205.0,"HyperDash":false},{"StartTime":117536.0,"Position":227.503632,"HyperDash":false},{"StartTime":117633.0,"Position":225.386322,"HyperDash":false}]},{"StartTime":117791.0,"Objects":[{"StartTime":117791.0,"Position":80.0,"HyperDash":false},{"StartTime":117860.0,"Position":86.16029,"HyperDash":false},{"StartTime":117930.0,"Position":123.197845,"HyperDash":false},{"StartTime":118000.0,"Position":137.463959,"HyperDash":false},{"StartTime":118106.0,"Position":126.215904,"HyperDash":false}]},{"StartTime":118265.0,"Objects":[{"StartTime":118265.0,"Position":279.0,"HyperDash":false}]},{"StartTime":118423.0,"Objects":[{"StartTime":118423.0,"Position":243.0,"HyperDash":false}]},{"StartTime":118581.0,"Objects":[{"StartTime":118581.0,"Position":306.0,"HyperDash":false}]},{"StartTime":118739.0,"Objects":[{"StartTime":118739.0,"Position":325.0,"HyperDash":false}]},{"StartTime":118897.0,"Objects":[{"StartTime":118897.0,"Position":330.0,"HyperDash":false}]},{"StartTime":119054.0,"Objects":[{"StartTime":119054.0,"Position":402.0,"HyperDash":false}]},{"StartTime":119528.0,"Objects":[{"StartTime":119528.0,"Position":402.0,"HyperDash":false},{"StartTime":119606.0,"Position":383.853424,"HyperDash":false},{"StartTime":119685.0,"Position":340.13028,"HyperDash":false},{"StartTime":119764.0,"Position":306.988525,"HyperDash":false},{"StartTime":119843.0,"Position":292.3493,"HyperDash":false},{"StartTime":119893.0,"Position":278.4925,"HyperDash":false},{"StartTime":119943.0,"Position":251.240692,"HyperDash":false},{"StartTime":119993.0,"Position":219.893433,"HyperDash":false},{"StartTime":120080.0,"Position":193.130447,"HyperDash":false}]},{"StartTime":120160.0,"Objects":[{"StartTime":120160.0,"Position":184.0,"HyperDash":false},{"StartTime":120238.0,"Position":212.882935,"HyperDash":false},{"StartTime":120317.0,"Position":263.114868,"HyperDash":false},{"StartTime":120396.0,"Position":296.998444,"HyperDash":false},{"StartTime":120475.0,"Position":297.759338,"HyperDash":false},{"StartTime":120525.0,"Position":324.634857,"HyperDash":false},{"StartTime":120575.0,"Position":317.226563,"HyperDash":false},{"StartTime":120625.0,"Position":338.548218,"HyperDash":false},{"StartTime":120712.0,"Position":391.822418,"HyperDash":false}]},{"StartTime":120791.0,"Objects":[{"StartTime":120791.0,"Position":385.0,"HyperDash":false},{"StartTime":120869.0,"Position":362.920959,"HyperDash":false},{"StartTime":120948.0,"Position":322.3987,"HyperDash":false},{"StartTime":121027.0,"Position":293.5699,"HyperDash":false},{"StartTime":121106.0,"Position":276.9177,"HyperDash":false},{"StartTime":121156.0,"Position":259.442749,"HyperDash":false},{"StartTime":121206.0,"Position":226.077545,"HyperDash":false},{"StartTime":121256.0,"Position":199.502151,"HyperDash":false},{"StartTime":121343.0,"Position":177.427231,"HyperDash":false}]},{"StartTime":121423.0,"Objects":[{"StartTime":121423.0,"Position":171.0,"HyperDash":false},{"StartTime":121501.0,"Position":194.8829,"HyperDash":false},{"StartTime":121580.0,"Position":232.113251,"HyperDash":false},{"StartTime":121659.0,"Position":263.966827,"HyperDash":false},{"StartTime":121738.0,"Position":284.605957,"HyperDash":false},{"StartTime":121788.0,"Position":288.396973,"HyperDash":false},{"StartTime":121838.0,"Position":333.9621,"HyperDash":false},{"StartTime":121888.0,"Position":331.2913,"HyperDash":false},{"StartTime":121975.0,"Position":378.575928,"HyperDash":false}]},{"StartTime":122054.0,"Objects":[{"StartTime":122054.0,"Position":373.0,"HyperDash":false},{"StartTime":122132.0,"Position":332.9511,"HyperDash":false},{"StartTime":122211.0,"Position":316.390533,"HyperDash":false},{"StartTime":122290.0,"Position":285.1506,"HyperDash":false},{"StartTime":122369.0,"Position":264.59256,"HyperDash":false},{"StartTime":122419.0,"Position":264.4234,"HyperDash":false},{"StartTime":122469.0,"Position":223.247314,"HyperDash":false},{"StartTime":122519.0,"Position":204.743011,"HyperDash":false},{"StartTime":122606.0,"Position":165.707657,"HyperDash":false}]},{"StartTime":122686.0,"Objects":[{"StartTime":122686.0,"Position":156.0,"HyperDash":false},{"StartTime":122786.0,"Position":141.985443,"HyperDash":false},{"StartTime":122922.0,"Position":110.21537,"HyperDash":false}]},{"StartTime":123002.0,"Objects":[{"StartTime":123002.0,"Position":129.0,"HyperDash":false},{"StartTime":123062.0,"Position":124.04425,"HyperDash":false},{"StartTime":123159.0,"Position":151.706512,"HyperDash":false}]},{"StartTime":123318.0,"Objects":[{"StartTime":123318.0,"Position":247.0,"HyperDash":false}]},{"StartTime":123475.0,"Objects":[{"StartTime":123475.0,"Position":278.0,"HyperDash":false}]},{"StartTime":123633.0,"Objects":[{"StartTime":123633.0,"Position":339.0,"HyperDash":false}]},{"StartTime":123791.0,"Objects":[{"StartTime":123791.0,"Position":272.0,"HyperDash":false}]},{"StartTime":123949.0,"Objects":[{"StartTime":123949.0,"Position":224.0,"HyperDash":false}]},{"StartTime":124107.0,"Objects":[{"StartTime":124107.0,"Position":286.0,"HyperDash":false}]},{"StartTime":124265.0,"Objects":[{"StartTime":124265.0,"Position":374.0,"HyperDash":false},{"StartTime":124325.0,"Position":390.564056,"HyperDash":false},{"StartTime":124422.0,"Position":454.897156,"HyperDash":false}]},{"StartTime":124581.0,"Objects":[{"StartTime":124581.0,"Position":368.0,"HyperDash":false}]},{"StartTime":124739.0,"Objects":[{"StartTime":124739.0,"Position":222.0,"HyperDash":false},{"StartTime":124799.0,"Position":189.435959,"HyperDash":false},{"StartTime":124896.0,"Position":141.102829,"HyperDash":false}]},{"StartTime":125054.0,"Objects":[{"StartTime":125054.0,"Position":62.0,"HyperDash":false},{"StartTime":125114.0,"Position":76.30468,"HyperDash":false},{"StartTime":125211.0,"Position":87.89828,"HyperDash":false}]},{"StartTime":125370.0,"Objects":[{"StartTime":125370.0,"Position":261.0,"HyperDash":false},{"StartTime":125430.0,"Position":244.695313,"HyperDash":false},{"StartTime":125527.0,"Position":235.101715,"HyperDash":false}]},{"StartTime":125686.0,"Objects":[{"StartTime":125686.0,"Position":86.0,"HyperDash":false},{"StartTime":125746.0,"Position":49.1613235,"HyperDash":false},{"StartTime":125843.0,"Position":5.8006506,"HyperDash":false}]},{"StartTime":126002.0,"Objects":[{"StartTime":126002.0,"Position":164.0,"HyperDash":false}]},{"StartTime":126160.0,"Objects":[{"StartTime":126160.0,"Position":235.0,"HyperDash":false},{"StartTime":126220.0,"Position":269.911163,"HyperDash":false},{"StartTime":126317.0,"Position":315.594666,"HyperDash":false}]},{"StartTime":126476.0,"Objects":[{"StartTime":126476.0,"Position":454.0,"HyperDash":false},{"StartTime":126536.0,"Position":435.099762,"HyperDash":false},{"StartTime":126633.0,"Position":373.83255,"HyperDash":false}]},{"StartTime":126791.0,"Objects":[{"StartTime":126791.0,"Position":407.0,"HyperDash":false},{"StartTime":126851.0,"Position":407.6067,"HyperDash":false},{"StartTime":126948.0,"Position":400.7375,"HyperDash":false}]},{"StartTime":127107.0,"Objects":[{"StartTime":127107.0,"Position":274.0,"HyperDash":false},{"StartTime":127167.0,"Position":260.302338,"HyperDash":false},{"StartTime":127264.0,"Position":266.941132,"HyperDash":false}]},{"StartTime":127423.0,"Objects":[{"StartTime":127423.0,"Position":421.0,"HyperDash":false},{"StartTime":127483.0,"Position":428.6067,"HyperDash":false},{"StartTime":127580.0,"Position":414.7375,"HyperDash":false}]},{"StartTime":127739.0,"Objects":[{"StartTime":127739.0,"Position":288.0,"HyperDash":false},{"StartTime":127799.0,"Position":302.302338,"HyperDash":false},{"StartTime":127896.0,"Position":280.941132,"HyperDash":false}]},{"StartTime":128054.0,"Objects":[{"StartTime":128054.0,"Position":247.0,"HyperDash":false}]},{"StartTime":128212.0,"Objects":[{"StartTime":128212.0,"Position":212.0,"HyperDash":false}]},{"StartTime":128370.0,"Objects":[{"StartTime":128370.0,"Position":251.0,"HyperDash":false}]},{"StartTime":128528.0,"Objects":[{"StartTime":128528.0,"Position":216.0,"HyperDash":false}]},{"StartTime":128686.0,"Objects":[{"StartTime":128686.0,"Position":81.0,"HyperDash":false},{"StartTime":128746.0,"Position":91.28703,"HyperDash":false},{"StartTime":128843.0,"Position":86.9844,"HyperDash":false}]},{"StartTime":129002.0,"Objects":[{"StartTime":129002.0,"Position":100.0,"HyperDash":false}]},{"StartTime":129160.0,"Objects":[{"StartTime":129160.0,"Position":163.0,"HyperDash":false}]},{"StartTime":129318.0,"Objects":[{"StartTime":129318.0,"Position":91.0,"HyperDash":false}]},{"StartTime":134370.0,"Objects":[{"StartTime":134370.0,"Position":300.0,"HyperDash":false}]},{"StartTime":135633.0,"Objects":[{"StartTime":135633.0,"Position":300.0,"HyperDash":false}]},{"StartTime":136897.0,"Objects":[{"StartTime":136897.0,"Position":300.0,"HyperDash":false},{"StartTime":136997.0,"Position":279.788757,"HyperDash":false},{"StartTime":137133.0,"Position":203.92157,"HyperDash":false}]},{"StartTime":137212.0,"Objects":[{"StartTime":137212.0,"Position":200.0,"HyperDash":false},{"StartTime":137312.0,"Position":227.884033,"HyperDash":false},{"StartTime":137448.0,"Position":295.992767,"HyperDash":false}]},{"StartTime":137528.0,"Objects":[{"StartTime":137528.0,"Position":293.0,"HyperDash":false},{"StartTime":137628.0,"Position":249.348679,"HyperDash":false},{"StartTime":137764.0,"Position":196.522751,"HyperDash":false}]},{"StartTime":137844.0,"Objects":[{"StartTime":137844.0,"Position":193.0,"HyperDash":false}]},{"StartTime":138160.0,"Objects":[{"StartTime":138160.0,"Position":337.0,"HyperDash":false},{"StartTime":138220.0,"Position":361.473083,"HyperDash":false},{"StartTime":138317.0,"Position":359.068726,"HyperDash":false}]},{"StartTime":138476.0,"Objects":[{"StartTime":138476.0,"Position":277.0,"HyperDash":false}]},{"StartTime":138633.0,"Objects":[{"StartTime":138633.0,"Position":355.0,"HyperDash":false},{"StartTime":138702.0,"Position":356.575073,"HyperDash":false},{"StartTime":138772.0,"Position":392.665771,"HyperDash":false},{"StartTime":138842.0,"Position":386.999573,"HyperDash":false},{"StartTime":138948.0,"Position":381.707275,"HyperDash":false}]},{"StartTime":139107.0,"Objects":[{"StartTime":139107.0,"Position":276.0,"HyperDash":false}]},{"StartTime":139265.0,"Objects":[{"StartTime":139265.0,"Position":276.0,"HyperDash":false}]},{"StartTime":139423.0,"Objects":[{"StartTime":139423.0,"Position":209.0,"HyperDash":false},{"StartTime":139483.0,"Position":198.122162,"HyperDash":false},{"StartTime":139580.0,"Position":145.227173,"HyperDash":false}]},{"StartTime":139739.0,"Objects":[{"StartTime":139739.0,"Position":68.0,"HyperDash":false}]},{"StartTime":139896.0,"Objects":[{"StartTime":139896.0,"Position":213.0,"HyperDash":false},{"StartTime":139965.0,"Position":198.023071,"HyperDash":false},{"StartTime":140035.0,"Position":135.780731,"HyperDash":false},{"StartTime":140105.0,"Position":105.911324,"HyperDash":false},{"StartTime":140211.0,"Position":80.0672455,"HyperDash":false}]},{"StartTime":140370.0,"Objects":[{"StartTime":140370.0,"Position":207.0,"HyperDash":false}]},{"StartTime":140528.0,"Objects":[{"StartTime":140528.0,"Position":207.0,"HyperDash":false}]},{"StartTime":140686.0,"Objects":[{"StartTime":140686.0,"Position":308.0,"HyperDash":false},{"StartTime":140746.0,"Position":291.8725,"HyperDash":false},{"StartTime":140843.0,"Position":295.128967,"HyperDash":false}]},{"StartTime":141002.0,"Objects":[{"StartTime":141002.0,"Position":421.0,"HyperDash":false}]},{"StartTime":141160.0,"Objects":[{"StartTime":141160.0,"Position":293.0,"HyperDash":false},{"StartTime":141229.0,"Position":272.132019,"HyperDash":false},{"StartTime":141299.0,"Position":276.853546,"HyperDash":false},{"StartTime":141369.0,"Position":287.3533,"HyperDash":false},{"StartTime":141475.0,"Position":261.940155,"HyperDash":false}]},{"StartTime":141633.0,"Objects":[{"StartTime":141633.0,"Position":392.0,"HyperDash":false}]},{"StartTime":141791.0,"Objects":[{"StartTime":141791.0,"Position":392.0,"HyperDash":false}]},{"StartTime":142265.0,"Objects":[{"StartTime":142265.0,"Position":392.0,"HyperDash":false},{"StartTime":142365.0,"Position":391.062164,"HyperDash":false},{"StartTime":142501.0,"Position":338.346161,"HyperDash":false}]},{"StartTime":142581.0,"Objects":[{"StartTime":142581.0,"Position":326.0,"HyperDash":false},{"StartTime":142650.0,"Position":311.6683,"HyperDash":false},{"StartTime":142720.0,"Position":268.562744,"HyperDash":false},{"StartTime":142790.0,"Position":260.483276,"HyperDash":false},{"StartTime":142896.0,"Position":203.358475,"HyperDash":false}]},{"StartTime":143212.0,"Objects":[{"StartTime":143212.0,"Position":203.0,"HyperDash":false}]},{"StartTime":144476.0,"Objects":[{"StartTime":144476.0,"Position":214.0,"HyperDash":false}]},{"StartTime":145739.0,"Objects":[{"StartTime":145739.0,"Position":214.0,"HyperDash":false},{"StartTime":145839.0,"Position":245.348236,"HyperDash":false},{"StartTime":145975.0,"Position":258.064423,"HyperDash":false}]},{"StartTime":146054.0,"Objects":[{"StartTime":146054.0,"Position":248.0,"HyperDash":false},{"StartTime":146154.0,"Position":238.651749,"HyperDash":false},{"StartTime":146290.0,"Position":203.935547,"HyperDash":false}]},{"StartTime":146370.0,"Objects":[{"StartTime":146370.0,"Position":218.0,"HyperDash":false},{"StartTime":146470.0,"Position":271.72702,"HyperDash":false},{"StartTime":146606.0,"Position":316.220551,"HyperDash":false}]},{"StartTime":146686.0,"Objects":[{"StartTime":146686.0,"Position":326.0,"HyperDash":false}]},{"StartTime":147002.0,"Objects":[{"StartTime":147002.0,"Position":440.0,"HyperDash":false},{"StartTime":147062.0,"Position":454.914642,"HyperDash":false},{"StartTime":147159.0,"Position":431.926636,"HyperDash":false}]},{"StartTime":147318.0,"Objects":[{"StartTime":147318.0,"Position":346.0,"HyperDash":false}]},{"StartTime":147476.0,"Objects":[{"StartTime":147476.0,"Position":457.0,"HyperDash":false},{"StartTime":147545.0,"Position":452.315582,"HyperDash":false},{"StartTime":147615.0,"Position":433.5778,"HyperDash":false},{"StartTime":147685.0,"Position":450.839966,"HyperDash":false},{"StartTime":147791.0,"Position":440.179871,"HyperDash":false}]},{"StartTime":147949.0,"Objects":[{"StartTime":147949.0,"Position":326.0,"HyperDash":false}]},{"StartTime":148107.0,"Objects":[{"StartTime":148107.0,"Position":326.0,"HyperDash":false}]},{"StartTime":148265.0,"Objects":[{"StartTime":148265.0,"Position":170.0,"HyperDash":false},{"StartTime":148325.0,"Position":169.085358,"HyperDash":false},{"StartTime":148422.0,"Position":178.073349,"HyperDash":false}]},{"StartTime":148581.0,"Objects":[{"StartTime":148581.0,"Position":264.0,"HyperDash":false}]},{"StartTime":148739.0,"Objects":[{"StartTime":148739.0,"Position":153.0,"HyperDash":false},{"StartTime":148808.0,"Position":154.6844,"HyperDash":false},{"StartTime":148878.0,"Position":166.422211,"HyperDash":false},{"StartTime":148948.0,"Position":158.160019,"HyperDash":false},{"StartTime":149054.0,"Position":169.820129,"HyperDash":false}]},{"StartTime":149212.0,"Objects":[{"StartTime":149212.0,"Position":284.0,"HyperDash":false}]},{"StartTime":149370.0,"Objects":[{"StartTime":149370.0,"Position":284.0,"HyperDash":false}]},{"StartTime":149528.0,"Objects":[{"StartTime":149528.0,"Position":403.0,"HyperDash":false},{"StartTime":149588.0,"Position":399.914642,"HyperDash":false},{"StartTime":149685.0,"Position":394.926636,"HyperDash":false}]},{"StartTime":149844.0,"Objects":[{"StartTime":149844.0,"Position":309.0,"HyperDash":false}]},{"StartTime":150002.0,"Objects":[{"StartTime":150002.0,"Position":420.0,"HyperDash":false},{"StartTime":150071.0,"Position":421.315582,"HyperDash":false},{"StartTime":150141.0,"Position":430.5778,"HyperDash":false},{"StartTime":150211.0,"Position":409.839966,"HyperDash":false},{"StartTime":150317.0,"Position":403.179871,"HyperDash":false}]},{"StartTime":150475.0,"Objects":[{"StartTime":150475.0,"Position":289.0,"HyperDash":false}]},{"StartTime":150633.0,"Objects":[{"StartTime":150633.0,"Position":289.0,"HyperDash":false}]},{"StartTime":151107.0,"Objects":[{"StartTime":151107.0,"Position":97.0,"HyperDash":false},{"StartTime":151207.0,"Position":135.296875,"HyperDash":false},{"StartTime":151343.0,"Position":191.738083,"HyperDash":false}]},{"StartTime":151423.0,"Objects":[{"StartTime":151423.0,"Position":198.0,"HyperDash":false},{"StartTime":151492.0,"Position":183.569153,"HyperDash":false},{"StartTime":151562.0,"Position":141.428131,"HyperDash":false},{"StartTime":151632.0,"Position":136.803146,"HyperDash":false},{"StartTime":151738.0,"Position":137.3734,"HyperDash":false}]},{"StartTime":152054.0,"Objects":[{"StartTime":152054.0,"Position":297.0,"HyperDash":false},{"StartTime":152123.0,"Position":331.7846,"HyperDash":false},{"StartTime":152193.0,"Position":338.116882,"HyperDash":false},{"StartTime":152263.0,"Position":352.270721,"HyperDash":false},{"StartTime":152369.0,"Position":408.0906,"HyperDash":false}]},{"StartTime":152528.0,"Objects":[{"StartTime":152528.0,"Position":281.0,"HyperDash":false}]},{"StartTime":152686.0,"Objects":[{"StartTime":152686.0,"Position":446.0,"HyperDash":false},{"StartTime":152755.0,"Position":492.2877,"HyperDash":false},{"StartTime":152825.0,"Position":490.710327,"HyperDash":false},{"StartTime":152895.0,"Position":503.465729,"HyperDash":false},{"StartTime":153001.0,"Position":492.445526,"HyperDash":false}]},{"StartTime":153160.0,"Objects":[{"StartTime":153160.0,"Position":343.0,"HyperDash":false}]},{"StartTime":153318.0,"Objects":[{"StartTime":153318.0,"Position":297.0,"HyperDash":false},{"StartTime":153387.0,"Position":262.8003,"HyperDash":false},{"StartTime":153457.0,"Position":234.212128,"HyperDash":false},{"StartTime":153527.0,"Position":214.138336,"HyperDash":false},{"StartTime":153633.0,"Position":166.492523,"HyperDash":false}]},{"StartTime":153791.0,"Objects":[{"StartTime":153791.0,"Position":116.0,"HyperDash":false},{"StartTime":153860.0,"Position":144.280365,"HyperDash":false},{"StartTime":153930.0,"Position":132.43898,"HyperDash":false},{"StartTime":154000.0,"Position":140.923447,"HyperDash":false},{"StartTime":154106.0,"Position":158.507156,"HyperDash":false}]},{"StartTime":154265.0,"Objects":[{"StartTime":154265.0,"Position":264.0,"HyperDash":false},{"StartTime":154325.0,"Position":233.864212,"HyperDash":false},{"StartTime":154422.0,"Position":235.824112,"HyperDash":false}]},{"StartTime":154581.0,"Objects":[{"StartTime":154581.0,"Position":152.0,"HyperDash":false},{"StartTime":154650.0,"Position":125.809914,"HyperDash":false},{"StartTime":154720.0,"Position":104.5544,"HyperDash":false},{"StartTime":154790.0,"Position":63.7936554,"HyperDash":false},{"StartTime":154896.0,"Position":32.2917,"HyperDash":false}]},{"StartTime":155054.0,"Objects":[{"StartTime":155054.0,"Position":191.0,"HyperDash":false}]},{"StartTime":155212.0,"Objects":[{"StartTime":155212.0,"Position":264.0,"HyperDash":false},{"StartTime":155281.0,"Position":311.2232,"HyperDash":false},{"StartTime":155351.0,"Position":339.435272,"HyperDash":false},{"StartTime":155421.0,"Position":368.023529,"HyperDash":false},{"StartTime":155527.0,"Position":382.984161,"HyperDash":false}]},{"StartTime":155686.0,"Objects":[{"StartTime":155686.0,"Position":212.0,"HyperDash":false}]},{"StartTime":155844.0,"Objects":[{"StartTime":155844.0,"Position":405.0,"HyperDash":false},{"StartTime":155913.0,"Position":398.1627,"HyperDash":false},{"StartTime":155983.0,"Position":377.19342,"HyperDash":false},{"StartTime":156053.0,"Position":363.4817,"HyperDash":false},{"StartTime":156159.0,"Position":358.190857,"HyperDash":false}]},{"StartTime":156318.0,"Objects":[{"StartTime":156318.0,"Position":158.0,"HyperDash":false},{"StartTime":156387.0,"Position":166.012711,"HyperDash":false},{"StartTime":156457.0,"Position":151.858978,"HyperDash":false},{"StartTime":156527.0,"Position":139.665756,"HyperDash":false},{"StartTime":156633.0,"Position":111.16011,"HyperDash":false}]},{"StartTime":156791.0,"Objects":[{"StartTime":156791.0,"Position":9.0,"HyperDash":false},{"StartTime":156851.0,"Position":37.5505524,"HyperDash":false},{"StartTime":156948.0,"Position":77.09072,"HyperDash":false}]},{"StartTime":157107.0,"Objects":[{"StartTime":157107.0,"Position":270.0,"HyperDash":false},{"StartTime":157176.0,"Position":221.1834,"HyperDash":false},{"StartTime":157246.0,"Position":202.467163,"HyperDash":false},{"StartTime":157316.0,"Position":188.839691,"HyperDash":false},{"StartTime":157422.0,"Position":171.541748,"HyperDash":false}]},{"StartTime":157581.0,"Objects":[{"StartTime":157581.0,"Position":288.0,"HyperDash":false},{"StartTime":157650.0,"Position":334.9065,"HyperDash":false},{"StartTime":157720.0,"Position":351.398132,"HyperDash":false},{"StartTime":157790.0,"Position":383.620148,"HyperDash":false},{"StartTime":157896.0,"Position":385.24826,"HyperDash":false}]},{"StartTime":158054.0,"Objects":[{"StartTime":158054.0,"Position":248.0,"HyperDash":false},{"StartTime":158114.0,"Position":269.238434,"HyperDash":false},{"StartTime":158211.0,"Position":320.739136,"HyperDash":false}]},{"StartTime":158370.0,"Objects":[{"StartTime":158370.0,"Position":490.0,"HyperDash":false},{"StartTime":158439.0,"Position":483.703064,"HyperDash":false},{"StartTime":158509.0,"Position":456.281219,"HyperDash":false},{"StartTime":158579.0,"Position":428.409576,"HyperDash":false},{"StartTime":158685.0,"Position":432.63913,"HyperDash":false}]},{"StartTime":158844.0,"Objects":[{"StartTime":158844.0,"Position":467.0,"HyperDash":false},{"StartTime":158913.0,"Position":441.987579,"HyperDash":false},{"StartTime":158983.0,"Position":453.374176,"HyperDash":false},{"StartTime":159053.0,"Position":445.3904,"HyperDash":false},{"StartTime":159159.0,"Position":409.514771,"HyperDash":false}]},{"StartTime":159318.0,"Objects":[{"StartTime":159318.0,"Position":248.0,"HyperDash":false},{"StartTime":159378.0,"Position":264.964264,"HyperDash":false},{"StartTime":159475.0,"Position":321.196442,"HyperDash":false}]},{"StartTime":159633.0,"Objects":[{"StartTime":159633.0,"Position":320.0,"HyperDash":false}]},{"StartTime":160897.0,"Objects":[{"StartTime":160897.0,"Position":118.0,"HyperDash":false},{"StartTime":160997.0,"Position":109.104843,"HyperDash":false},{"StartTime":161133.0,"Position":125.327431,"HyperDash":false}]},{"StartTime":161212.0,"Objects":[{"StartTime":161212.0,"Position":146.0,"HyperDash":false},{"StartTime":161312.0,"Position":129.61203,"HyperDash":false},{"StartTime":161448.0,"Position":138.0044,"HyperDash":false}]},{"StartTime":161528.0,"Objects":[{"StartTime":161528.0,"Position":158.0,"HyperDash":false},{"StartTime":161628.0,"Position":162.38797,"HyperDash":false},{"StartTime":161764.0,"Position":165.9956,"HyperDash":false}]},{"StartTime":161844.0,"Objects":[{"StartTime":161844.0,"Position":185.0,"HyperDash":false}]},{"StartTime":162160.0,"Objects":[{"StartTime":162160.0,"Position":39.0,"HyperDash":false},{"StartTime":162220.0,"Position":18.8607216,"HyperDash":false},{"StartTime":162317.0,"Position":18.4563026,"HyperDash":false}]},{"StartTime":162475.0,"Objects":[{"StartTime":162475.0,"Position":153.0,"HyperDash":false}]},{"StartTime":162633.0,"Objects":[{"StartTime":162633.0,"Position":221.0,"HyperDash":false},{"StartTime":162693.0,"Position":242.139282,"HyperDash":false},{"StartTime":162790.0,"Position":241.543686,"HyperDash":false}]},{"StartTime":162949.0,"Objects":[{"StartTime":162949.0,"Position":64.0,"HyperDash":false},{"StartTime":163009.0,"Position":77.95903,"HyperDash":false},{"StartTime":163106.0,"Position":85.14159,"HyperDash":false}]},{"StartTime":163265.0,"Objects":[{"StartTime":163265.0,"Position":244.0,"HyperDash":false},{"StartTime":163334.0,"Position":288.4246,"HyperDash":false},{"StartTime":163404.0,"Position":325.0935,"HyperDash":false},{"StartTime":163474.0,"Position":362.511841,"HyperDash":false},{"StartTime":163580.0,"Position":369.3507,"HyperDash":false}]},{"StartTime":163739.0,"Objects":[{"StartTime":163739.0,"Position":322.0,"HyperDash":false}]},{"StartTime":163896.0,"Objects":[{"StartTime":163896.0,"Position":282.0,"HyperDash":false}]},{"StartTime":164054.0,"Objects":[{"StartTime":164054.0,"Position":419.0,"HyperDash":false},{"StartTime":164114.0,"Position":421.511932,"HyperDash":false},{"StartTime":164211.0,"Position":405.982758,"HyperDash":false}]},{"StartTime":164370.0,"Objects":[{"StartTime":164370.0,"Position":214.0,"HyperDash":false},{"StartTime":164430.0,"Position":211.248978,"HyperDash":false},{"StartTime":164527.0,"Position":202.042938,"HyperDash":false}]},{"StartTime":164686.0,"Objects":[{"StartTime":164686.0,"Position":295.0,"HyperDash":false},{"StartTime":164746.0,"Position":278.1207,"HyperDash":false},{"StartTime":164843.0,"Position":214.459625,"HyperDash":false}]},{"StartTime":165002.0,"Objects":[{"StartTime":165002.0,"Position":305.0,"HyperDash":false}]},{"StartTime":165160.0,"Objects":[{"StartTime":165160.0,"Position":209.0,"HyperDash":false},{"StartTime":165220.0,"Position":165.120712,"HyperDash":false},{"StartTime":165317.0,"Position":128.459641,"HyperDash":false}]},{"StartTime":165475.0,"Objects":[{"StartTime":165475.0,"Position":294.0,"HyperDash":false},{"StartTime":165535.0,"Position":279.1207,"HyperDash":false},{"StartTime":165632.0,"Position":213.459625,"HyperDash":false}]},{"StartTime":165791.0,"Objects":[{"StartTime":165791.0,"Position":66.0,"HyperDash":false},{"StartTime":165860.0,"Position":45.40761,"HyperDash":false},{"StartTime":165930.0,"Position":38.5398445,"HyperDash":false},{"StartTime":166000.0,"Position":12.8475342,"HyperDash":false},{"StartTime":166106.0,"Position":27.3485184,"HyperDash":false}]},{"StartTime":166265.0,"Objects":[{"StartTime":166265.0,"Position":226.0,"HyperDash":false}]},{"StartTime":166423.0,"Objects":[{"StartTime":166423.0,"Position":144.0,"HyperDash":false}]},{"StartTime":166581.0,"Objects":[{"StartTime":166581.0,"Position":244.0,"HyperDash":false},{"StartTime":166641.0,"Position":283.822266,"HyperDash":false},{"StartTime":166738.0,"Position":323.677734,"HyperDash":false}]},{"StartTime":166896.0,"Objects":[{"StartTime":166896.0,"Position":163.0,"HyperDash":false},{"StartTime":166956.0,"Position":176.896317,"HyperDash":false},{"StartTime":167053.0,"Position":242.864777,"HyperDash":false}]},{"StartTime":167212.0,"Objects":[{"StartTime":167212.0,"Position":374.0,"HyperDash":false},{"StartTime":167272.0,"Position":398.505157,"HyperDash":false},{"StartTime":167369.0,"Position":404.8981,"HyperDash":false}]},{"StartTime":167528.0,"Objects":[{"StartTime":167528.0,"Position":364.0,"HyperDash":false}]},{"StartTime":167686.0,"Objects":[{"StartTime":167686.0,"Position":490.0,"HyperDash":false},{"StartTime":167746.0,"Position":467.494843,"HyperDash":false},{"StartTime":167843.0,"Position":459.101929,"HyperDash":false}]},{"StartTime":168002.0,"Objects":[{"StartTime":168002.0,"Position":269.0,"HyperDash":false},{"StartTime":168062.0,"Position":244.4816,"HyperDash":false},{"StartTime":168159.0,"Position":239.105927,"HyperDash":false}]},{"StartTime":168317.0,"Objects":[{"StartTime":168317.0,"Position":74.0,"HyperDash":false},{"StartTime":168386.0,"Position":97.2507858,"HyperDash":false},{"StartTime":168456.0,"Position":150.689621,"HyperDash":false},{"StartTime":168526.0,"Position":165.044,"HyperDash":false},{"StartTime":168632.0,"Position":217.447281,"HyperDash":false}]},{"StartTime":168791.0,"Objects":[{"StartTime":168791.0,"Position":258.0,"HyperDash":false},{"StartTime":168851.0,"Position":231.961609,"HyperDash":false},{"StartTime":168948.0,"Position":228.157639,"HyperDash":false}]},{"StartTime":169107.0,"Objects":[{"StartTime":169107.0,"Position":85.0,"HyperDash":false},{"StartTime":169167.0,"Position":90.51432,"HyperDash":false},{"StartTime":169264.0,"Position":69.0959244,"HyperDash":false}]},{"StartTime":169423.0,"Objects":[{"StartTime":169423.0,"Position":233.0,"HyperDash":false},{"StartTime":169483.0,"Position":208.961609,"HyperDash":false},{"StartTime":169580.0,"Position":203.157639,"HyperDash":false}]},{"StartTime":169739.0,"Objects":[{"StartTime":169739.0,"Position":296.0,"HyperDash":false},{"StartTime":169799.0,"Position":315.6617,"HyperDash":false},{"StartTime":169896.0,"Position":377.655518,"HyperDash":false}]},{"StartTime":170054.0,"Objects":[{"StartTime":170054.0,"Position":224.0,"HyperDash":false}]},{"StartTime":170212.0,"Objects":[{"StartTime":170212.0,"Position":331.0,"HyperDash":false},{"StartTime":170272.0,"Position":367.6617,"HyperDash":false},{"StartTime":170369.0,"Position":412.655518,"HyperDash":false}]},{"StartTime":170528.0,"Objects":[{"StartTime":170528.0,"Position":238.0,"HyperDash":false},{"StartTime":170588.0,"Position":203.307648,"HyperDash":false},{"StartTime":170685.0,"Position":156.157562,"HyperDash":false}]},{"StartTime":170844.0,"Objects":[{"StartTime":170844.0,"Position":95.0,"HyperDash":false}]},{"StartTime":171002.0,"Objects":[{"StartTime":171002.0,"Position":92.0,"HyperDash":false},{"StartTime":171062.0,"Position":123.152824,"HyperDash":false},{"StartTime":171159.0,"Position":131.076691,"HyperDash":false}]},{"StartTime":171317.0,"Objects":[{"StartTime":171317.0,"Position":243.0,"HyperDash":false}]},{"StartTime":171475.0,"Objects":[{"StartTime":171475.0,"Position":218.0,"HyperDash":false}]},{"StartTime":171633.0,"Objects":[{"StartTime":171633.0,"Position":237.0,"HyperDash":false}]},{"StartTime":171791.0,"Objects":[{"StartTime":171791.0,"Position":212.0,"HyperDash":false}]},{"StartTime":171949.0,"Objects":[{"StartTime":171949.0,"Position":328.0,"HyperDash":false},{"StartTime":172009.0,"Position":361.2498,"HyperDash":false},{"StartTime":172106.0,"Position":407.426453,"HyperDash":false}]},{"StartTime":172265.0,"Objects":[{"StartTime":172265.0,"Position":447.0,"HyperDash":false},{"StartTime":172325.0,"Position":412.0013,"HyperDash":false},{"StartTime":172422.0,"Position":404.674683,"HyperDash":false}]},{"StartTime":172581.0,"Objects":[{"StartTime":172581.0,"Position":349.0,"HyperDash":false}]},{"StartTime":172739.0,"Objects":[{"StartTime":172739.0,"Position":337.0,"HyperDash":false},{"StartTime":172799.0,"Position":372.2498,"HyperDash":false},{"StartTime":172896.0,"Position":416.426453,"HyperDash":false}]},{"StartTime":173054.0,"Objects":[{"StartTime":173054.0,"Position":335.0,"HyperDash":false},{"StartTime":173114.0,"Position":295.0204,"HyperDash":false},{"StartTime":173211.0,"Position":255.685944,"HyperDash":false}]},{"StartTime":173370.0,"Objects":[{"StartTime":173370.0,"Position":195.0,"HyperDash":false},{"StartTime":173439.0,"Position":205.158081,"HyperDash":false},{"StartTime":173509.0,"Position":223.41394,"HyperDash":false},{"StartTime":173579.0,"Position":259.617249,"HyperDash":false},{"StartTime":173685.0,"Position":242.5926,"HyperDash":false}]},{"StartTime":173844.0,"Objects":[{"StartTime":173844.0,"Position":66.0,"HyperDash":false}]},{"StartTime":174002.0,"Objects":[{"StartTime":174002.0,"Position":125.0,"HyperDash":false},{"StartTime":174062.0,"Position":137.888153,"HyperDash":false},{"StartTime":174159.0,"Position":195.446823,"HyperDash":false}]},{"StartTime":174317.0,"Objects":[{"StartTime":174317.0,"Position":276.0,"HyperDash":false}]},{"StartTime":174475.0,"Objects":[{"StartTime":174475.0,"Position":104.0,"HyperDash":false},{"StartTime":174535.0,"Position":70.345,"HyperDash":false},{"StartTime":174632.0,"Position":24.6388855,"HyperDash":false}]},{"StartTime":174791.0,"Objects":[{"StartTime":174791.0,"Position":178.0,"HyperDash":false},{"StartTime":174851.0,"Position":167.60582,"HyperDash":false},{"StartTime":174948.0,"Position":161.976974,"HyperDash":false}]},{"StartTime":175107.0,"Objects":[{"StartTime":175107.0,"Position":293.0,"HyperDash":false}]},{"StartTime":175265.0,"Objects":[{"StartTime":175265.0,"Position":335.0,"HyperDash":false},{"StartTime":175325.0,"Position":377.0415,"HyperDash":false},{"StartTime":175422.0,"Position":413.356354,"HyperDash":false}]},{"StartTime":175581.0,"Objects":[{"StartTime":175581.0,"Position":366.0,"HyperDash":false},{"StartTime":175641.0,"Position":334.5056,"HyperDash":false},{"StartTime":175738.0,"Position":287.8307,"HyperDash":false}]},{"StartTime":175896.0,"Objects":[{"StartTime":175896.0,"Position":490.0,"HyperDash":false},{"StartTime":175965.0,"Position":458.363129,"HyperDash":false},{"StartTime":176035.0,"Position":474.112274,"HyperDash":false},{"StartTime":176105.0,"Position":466.752441,"HyperDash":false},{"StartTime":176211.0,"Position":444.10556,"HyperDash":false}]},{"StartTime":176370.0,"Objects":[{"StartTime":176370.0,"Position":330.0,"HyperDash":false}]},{"StartTime":176528.0,"Objects":[{"StartTime":176528.0,"Position":312.0,"HyperDash":false},{"StartTime":176588.0,"Position":298.540161,"HyperDash":false},{"StartTime":176685.0,"Position":294.0207,"HyperDash":false}]},{"StartTime":176844.0,"Objects":[{"StartTime":176844.0,"Position":175.0,"HyperDash":false}]},{"StartTime":177002.0,"Objects":[{"StartTime":177002.0,"Position":181.0,"HyperDash":false},{"StartTime":177062.0,"Position":170.459839,"HyperDash":false},{"StartTime":177159.0,"Position":198.979309,"HyperDash":false}]},{"StartTime":177317.0,"Objects":[{"StartTime":177317.0,"Position":318.0,"HyperDash":false},{"StartTime":177386.0,"Position":284.9978,"HyperDash":false},{"StartTime":177456.0,"Position":269.6918,"HyperDash":false},{"StartTime":177526.0,"Position":237.63176,"HyperDash":false},{"StartTime":177632.0,"Position":186.8728,"HyperDash":false}]},{"StartTime":177791.0,"Objects":[{"StartTime":177791.0,"Position":370.0,"HyperDash":false},{"StartTime":177851.0,"Position":406.333221,"HyperDash":false},{"StartTime":177948.0,"Position":450.520935,"HyperDash":false}]},{"StartTime":178107.0,"Objects":[{"StartTime":178107.0,"Position":325.0,"HyperDash":false},{"StartTime":178167.0,"Position":339.2036,"HyperDash":false},{"StartTime":178264.0,"Position":405.357574,"HyperDash":false}]},{"StartTime":178423.0,"Objects":[{"StartTime":178423.0,"Position":302.0,"HyperDash":false},{"StartTime":178483.0,"Position":291.456818,"HyperDash":false},{"StartTime":178580.0,"Position":279.113953,"HyperDash":false}]},{"StartTime":178739.0,"Objects":[{"StartTime":178739.0,"Position":173.0,"HyperDash":false},{"StartTime":178799.0,"Position":164.273453,"HyperDash":false},{"StartTime":178896.0,"Position":150.110962,"HyperDash":false}]},{"StartTime":179054.0,"Objects":[{"StartTime":179054.0,"Position":203.0,"HyperDash":false}]},{"StartTime":179212.0,"Objects":[{"StartTime":179212.0,"Position":58.0,"HyperDash":false},{"StartTime":179272.0,"Position":71.98529,"HyperDash":false},{"StartTime":179369.0,"Position":81.024,"HyperDash":false}]},{"StartTime":179528.0,"Objects":[{"StartTime":179528.0,"Position":266.0,"HyperDash":false},{"StartTime":179588.0,"Position":270.755371,"HyperDash":false},{"StartTime":179685.0,"Position":243.9513,"HyperDash":false}]},{"StartTime":179844.0,"Objects":[{"StartTime":179844.0,"Position":379.0,"HyperDash":false},{"StartTime":179904.0,"Position":407.036346,"HyperDash":false},{"StartTime":180001.0,"Position":462.828461,"HyperDash":false}]},{"StartTime":180160.0,"Objects":[{"StartTime":180160.0,"Position":252.0,"HyperDash":false},{"StartTime":180220.0,"Position":217.963638,"HyperDash":false},{"StartTime":180317.0,"Position":168.171539,"HyperDash":false}]},{"StartTime":180475.0,"Objects":[{"StartTime":180475.0,"Position":385.0,"HyperDash":false},{"StartTime":180535.0,"Position":434.036346,"HyperDash":false},{"StartTime":180632.0,"Position":468.828461,"HyperDash":false}]},{"StartTime":180791.0,"Objects":[{"StartTime":180791.0,"Position":258.0,"HyperDash":false},{"StartTime":180851.0,"Position":241.963638,"HyperDash":false},{"StartTime":180948.0,"Position":174.171539,"HyperDash":false}]},{"StartTime":181107.0,"Objects":[{"StartTime":181107.0,"Position":295.0,"HyperDash":false}]},{"StartTime":181265.0,"Objects":[{"StartTime":181265.0,"Position":334.0,"HyperDash":false}]},{"StartTime":181423.0,"Objects":[{"StartTime":181423.0,"Position":306.0,"HyperDash":false}]},{"StartTime":181581.0,"Objects":[{"StartTime":181581.0,"Position":347.0,"HyperDash":false}]},{"StartTime":181739.0,"Objects":[{"StartTime":181739.0,"Position":317.0,"HyperDash":false},{"StartTime":181799.0,"Position":323.0203,"HyperDash":false},{"StartTime":181896.0,"Position":322.286469,"HyperDash":false}]},{"StartTime":182054.0,"Objects":[{"StartTime":182054.0,"Position":237.0,"HyperDash":false}]},{"StartTime":182212.0,"Objects":[{"StartTime":182212.0,"Position":440.0,"HyperDash":false}]},{"StartTime":182370.0,"Objects":[{"StartTime":182370.0,"Position":225.0,"HyperDash":false}]},{"StartTime":183476.0,"Objects":[{"StartTime":183476.0,"Position":173.0,"HyperDash":false}]},{"StartTime":184265.0,"Objects":[{"StartTime":184265.0,"Position":173.0,"HyperDash":false},{"StartTime":184365.0,"Position":228.359283,"HyperDash":false},{"StartTime":184501.0,"Position":263.5279,"HyperDash":false}]},{"StartTime":184581.0,"Objects":[{"StartTime":184581.0,"Position":266.0,"HyperDash":false},{"StartTime":184641.0,"Position":259.91507,"HyperDash":false},{"StartTime":184738.0,"Position":205.594452,"HyperDash":false}]},{"StartTime":184818.0,"Objects":[{"StartTime":184818.0,"Position":180.0,"HyperDash":false}]},{"StartTime":184897.0,"Objects":[{"StartTime":184897.0,"Position":180.0,"HyperDash":false}]},{"StartTime":186002.0,"Objects":[{"StartTime":186002.0,"Position":402.0,"HyperDash":false}]},{"StartTime":186791.0,"Objects":[{"StartTime":186791.0,"Position":402.0,"HyperDash":false},{"StartTime":186891.0,"Position":364.639435,"HyperDash":false},{"StartTime":187027.0,"Position":311.469055,"HyperDash":false}]},{"StartTime":187107.0,"Objects":[{"StartTime":187107.0,"Position":309.0,"HyperDash":false},{"StartTime":187167.0,"Position":345.0628,"HyperDash":false},{"StartTime":187264.0,"Position":369.347656,"HyperDash":false}]},{"StartTime":187344.0,"Objects":[{"StartTime":187344.0,"Position":432.0,"HyperDash":false}]},{"StartTime":187423.0,"Objects":[{"StartTime":187423.0,"Position":432.0,"HyperDash":false},{"StartTime":187483.0,"Position":431.965149,"HyperDash":false},{"StartTime":187580.0,"Position":414.448761,"HyperDash":false}]},{"StartTime":187739.0,"Objects":[{"StartTime":187739.0,"Position":460.0,"HyperDash":false}]},{"StartTime":187897.0,"Objects":[{"StartTime":187897.0,"Position":270.0,"HyperDash":false},{"StartTime":187957.0,"Position":264.1196,"HyperDash":false},{"StartTime":188054.0,"Position":252.031967,"HyperDash":false}]},{"StartTime":188212.0,"Objects":[{"StartTime":188212.0,"Position":345.0,"HyperDash":false},{"StartTime":188272.0,"Position":362.0573,"HyperDash":false},{"StartTime":188369.0,"Position":362.009369,"HyperDash":false}]},{"StartTime":188528.0,"Objects":[{"StartTime":188528.0,"Position":223.0,"HyperDash":false},{"StartTime":188597.0,"Position":173.194031,"HyperDash":false},{"StartTime":188667.0,"Position":151.2194,"HyperDash":false},{"StartTime":188737.0,"Position":127.234268,"HyperDash":false},{"StartTime":188843.0,"Position":90.09637,"HyperDash":false}]},{"StartTime":189002.0,"Objects":[{"StartTime":189002.0,"Position":195.0,"HyperDash":false},{"StartTime":189062.0,"Position":228.972458,"HyperDash":false},{"StartTime":189159.0,"Position":277.218262,"HyperDash":false}]},{"StartTime":189318.0,"Objects":[{"StartTime":189318.0,"Position":315.0,"HyperDash":false},{"StartTime":189378.0,"Position":267.027557,"HyperDash":false},{"StartTime":189475.0,"Position":232.781723,"HyperDash":false}]},{"StartTime":189633.0,"Objects":[{"StartTime":189633.0,"Position":426.0,"HyperDash":false},{"StartTime":189693.0,"Position":416.778778,"HyperDash":false},{"StartTime":189790.0,"Position":397.035126,"HyperDash":false}]},{"StartTime":189949.0,"Objects":[{"StartTime":189949.0,"Position":370.0,"HyperDash":false},{"StartTime":190018.0,"Position":378.220642,"HyperDash":false},{"StartTime":190088.0,"Position":331.990845,"HyperDash":false},{"StartTime":190158.0,"Position":340.3975,"HyperDash":false},{"StartTime":190264.0,"Position":316.019745,"HyperDash":false}]},{"StartTime":190423.0,"Objects":[{"StartTime":190423.0,"Position":190.0,"HyperDash":false},{"StartTime":190483.0,"Position":164.497772,"HyperDash":false},{"StartTime":190580.0,"Position":110.287689,"HyperDash":false}]},{"StartTime":190739.0,"Objects":[{"StartTime":190739.0,"Position":221.0,"HyperDash":false},{"StartTime":190799.0,"Position":269.972839,"HyperDash":false},{"StartTime":190896.0,"Position":300.6956,"HyperDash":false}]},{"StartTime":191054.0,"Objects":[{"StartTime":191054.0,"Position":189.0,"HyperDash":false}]},{"StartTime":191212.0,"Objects":[{"StartTime":191212.0,"Position":378.0,"HyperDash":false},{"StartTime":191281.0,"Position":369.800842,"HyperDash":false},{"StartTime":191351.0,"Position":353.057861,"HyperDash":false},{"StartTime":191421.0,"Position":343.24173,"HyperDash":false},{"StartTime":191527.0,"Position":338.951782,"HyperDash":false}]},{"StartTime":191686.0,"Objects":[{"StartTime":191686.0,"Position":465.0,"HyperDash":false}]},{"StartTime":191844.0,"Objects":[{"StartTime":191844.0,"Position":363.0,"HyperDash":false},{"StartTime":191904.0,"Position":354.1089,"HyperDash":false},{"StartTime":192001.0,"Position":353.0403,"HyperDash":false}]},{"StartTime":192160.0,"Objects":[{"StartTime":192160.0,"Position":421.0,"HyperDash":false}]},{"StartTime":192318.0,"Objects":[{"StartTime":192318.0,"Position":421.0,"HyperDash":false}]},{"StartTime":192791.0,"Objects":[{"StartTime":192791.0,"Position":221.0,"HyperDash":false},{"StartTime":192869.0,"Position":265.146576,"HyperDash":false},{"StartTime":192948.0,"Position":280.86972,"HyperDash":false},{"StartTime":193027.0,"Position":309.011475,"HyperDash":false},{"StartTime":193106.0,"Position":330.6507,"HyperDash":false},{"StartTime":193156.0,"Position":343.5075,"HyperDash":false},{"StartTime":193206.0,"Position":362.759338,"HyperDash":false},{"StartTime":193256.0,"Position":379.106567,"HyperDash":false},{"StartTime":193343.0,"Position":429.869537,"HyperDash":false}]},{"StartTime":193423.0,"Objects":[{"StartTime":193423.0,"Position":439.0,"HyperDash":false},{"StartTime":193501.0,"Position":382.117065,"HyperDash":false},{"StartTime":193580.0,"Position":381.885132,"HyperDash":false},{"StartTime":193659.0,"Position":348.001556,"HyperDash":false},{"StartTime":193738.0,"Position":325.240662,"HyperDash":false},{"StartTime":193788.0,"Position":320.365143,"HyperDash":false},{"StartTime":193838.0,"Position":291.773438,"HyperDash":false},{"StartTime":193888.0,"Position":291.451782,"HyperDash":false},{"StartTime":193975.0,"Position":231.177582,"HyperDash":false}]},{"StartTime":194054.0,"Objects":[{"StartTime":194054.0,"Position":238.0,"HyperDash":false},{"StartTime":194132.0,"Position":276.079041,"HyperDash":false},{"StartTime":194211.0,"Position":303.6013,"HyperDash":false},{"StartTime":194290.0,"Position":315.4301,"HyperDash":false},{"StartTime":194369.0,"Position":346.0823,"HyperDash":false},{"StartTime":194419.0,"Position":358.557251,"HyperDash":false},{"StartTime":194469.0,"Position":382.922455,"HyperDash":false},{"StartTime":194519.0,"Position":399.497833,"HyperDash":false},{"StartTime":194606.0,"Position":445.572784,"HyperDash":false}]},{"StartTime":194686.0,"Objects":[{"StartTime":194686.0,"Position":452.0,"HyperDash":false},{"StartTime":194764.0,"Position":418.1171,"HyperDash":false},{"StartTime":194843.0,"Position":389.886749,"HyperDash":false},{"StartTime":194922.0,"Position":346.033173,"HyperDash":false},{"StartTime":195001.0,"Position":338.394043,"HyperDash":false},{"StartTime":195051.0,"Position":308.603027,"HyperDash":false},{"StartTime":195101.0,"Position":307.0379,"HyperDash":false},{"StartTime":195151.0,"Position":300.7087,"HyperDash":false},{"StartTime":195238.0,"Position":244.424088,"HyperDash":false}]},{"StartTime":195317.0,"Objects":[{"StartTime":195317.0,"Position":250.0,"HyperDash":false},{"StartTime":195395.0,"Position":280.0489,"HyperDash":false},{"StartTime":195474.0,"Position":338.609467,"HyperDash":false},{"StartTime":195553.0,"Position":354.8494,"HyperDash":false},{"StartTime":195632.0,"Position":358.40744,"HyperDash":false},{"StartTime":195682.0,"Position":381.576569,"HyperDash":false},{"StartTime":195732.0,"Position":402.7527,"HyperDash":false},{"StartTime":195782.0,"Position":430.257,"HyperDash":false},{"StartTime":195869.0,"Position":457.292328,"HyperDash":false}]},{"StartTime":195949.0,"Objects":[{"StartTime":195949.0,"Position":461.0,"HyperDash":false},{"StartTime":196049.0,"Position":446.167847,"HyperDash":false},{"StartTime":196185.0,"Position":438.391785,"HyperDash":false}]},{"StartTime":196265.0,"Objects":[{"StartTime":196265.0,"Position":411.0,"HyperDash":false},{"StartTime":196325.0,"Position":383.214722,"HyperDash":false},{"StartTime":196422.0,"Position":343.5428,"HyperDash":false}]},{"StartTime":196581.0,"Objects":[{"StartTime":196581.0,"Position":136.0,"HyperDash":false}]},{"StartTime":196739.0,"Objects":[{"StartTime":196739.0,"Position":314.0,"HyperDash":false}]},{"StartTime":196897.0,"Objects":[{"StartTime":196897.0,"Position":120.0,"HyperDash":false}]},{"StartTime":197055.0,"Objects":[{"StartTime":197055.0,"Position":298.0,"HyperDash":false}]},{"StartTime":197212.0,"Objects":[{"StartTime":197212.0,"Position":104.0,"HyperDash":false},{"StartTime":197272.0,"Position":85.28295,"HyperDash":false},{"StartTime":197369.0,"Position":92.47838,"HyperDash":false}]},{"StartTime":197528.0,"Objects":[{"StartTime":197528.0,"Position":136.0,"HyperDash":false},{"StartTime":197588.0,"Position":176.664658,"HyperDash":false},{"StartTime":197685.0,"Position":211.9784,"HyperDash":false}]},{"StartTime":197844.0,"Objects":[{"StartTime":197844.0,"Position":384.0,"HyperDash":false}]},{"StartTime":198002.0,"Objects":[{"StartTime":198002.0,"Position":317.0,"HyperDash":false},{"StartTime":198062.0,"Position":278.335327,"HyperDash":false},{"StartTime":198159.0,"Position":241.0216,"HyperDash":false}]},{"StartTime":198318.0,"Objects":[{"StartTime":198318.0,"Position":373.0,"HyperDash":false},{"StartTime":198378.0,"Position":422.153229,"HyperDash":false},{"StartTime":198475.0,"Position":448.229248,"HyperDash":false}]},{"StartTime":198633.0,"Objects":[{"StartTime":198633.0,"Position":436.0,"HyperDash":false},{"StartTime":198693.0,"Position":422.984,"HyperDash":false},{"StartTime":198790.0,"Position":412.4418,"HyperDash":false}]},{"StartTime":198949.0,"Objects":[{"StartTime":198949.0,"Position":264.0,"HyperDash":false},{"StartTime":199009.0,"Position":276.016,"HyperDash":false},{"StartTime":199106.0,"Position":287.5582,"HyperDash":false}]},{"StartTime":199265.0,"Objects":[{"StartTime":199265.0,"Position":242.0,"HyperDash":false}]},{"StartTime":199423.0,"Objects":[{"StartTime":199423.0,"Position":414.0,"HyperDash":false},{"StartTime":199483.0,"Position":411.984,"HyperDash":false},{"StartTime":199580.0,"Position":390.4418,"HyperDash":false}]},{"StartTime":199739.0,"Objects":[{"StartTime":199739.0,"Position":214.0,"HyperDash":false},{"StartTime":199799.0,"Position":212.821,"HyperDash":false},{"StartTime":199896.0,"Position":190.064774,"HyperDash":false}]},{"StartTime":200054.0,"Objects":[{"StartTime":200054.0,"Position":38.0,"HyperDash":false},{"StartTime":200114.0,"Position":47.9374542,"HyperDash":false},{"StartTime":200211.0,"Position":48.30301,"HyperDash":false}]},{"StartTime":200370.0,"Objects":[{"StartTime":200370.0,"Position":86.0,"HyperDash":false},{"StartTime":200430.0,"Position":89.79463,"HyperDash":false},{"StartTime":200527.0,"Position":95.92929,"HyperDash":false}]},{"StartTime":200686.0,"Objects":[{"StartTime":200686.0,"Position":48.0,"HyperDash":false},{"StartTime":200746.0,"Position":62.9374542,"HyperDash":false},{"StartTime":200843.0,"Position":58.30301,"HyperDash":false}]},{"StartTime":201002.0,"Objects":[{"StartTime":201002.0,"Position":96.0,"HyperDash":false},{"StartTime":201062.0,"Position":89.79463,"HyperDash":false},{"StartTime":201159.0,"Position":105.929291,"HyperDash":false}]},{"StartTime":201318.0,"Objects":[{"StartTime":201318.0,"Position":223.0,"HyperDash":false}]},{"StartTime":201476.0,"Objects":[{"StartTime":201476.0,"Position":211.0,"HyperDash":false}]},{"StartTime":201633.0,"Objects":[{"StartTime":201633.0,"Position":239.0,"HyperDash":false}]},{"StartTime":201791.0,"Objects":[{"StartTime":201791.0,"Position":227.0,"HyperDash":false}]},{"StartTime":201949.0,"Objects":[{"StartTime":201949.0,"Position":255.0,"HyperDash":false},{"StartTime":202009.0,"Position":263.68692,"HyperDash":false},{"StartTime":202106.0,"Position":243.714127,"HyperDash":false}]},{"StartTime":202265.0,"Objects":[{"StartTime":202265.0,"Position":218.0,"HyperDash":false}]},{"StartTime":202423.0,"Objects":[{"StartTime":202423.0,"Position":309.0,"HyperDash":false}]},{"StartTime":202581.0,"Objects":[{"StartTime":202581.0,"Position":328.0,"HyperDash":false}]},{"StartTime":203528.0,"Objects":[{"StartTime":203528.0,"Position":459.0,"HyperDash":false},{"StartTime":203588.0,"Position":448.977936,"HyperDash":false},{"StartTime":203685.0,"Position":398.758942,"HyperDash":false}]},{"StartTime":203844.0,"Objects":[{"StartTime":203844.0,"Position":305.0,"HyperDash":false}]},{"StartTime":204002.0,"Objects":[{"StartTime":204002.0,"Position":305.0,"HyperDash":false}]},{"StartTime":204160.0,"Objects":[{"StartTime":204160.0,"Position":264.0,"HyperDash":false}]},{"StartTime":204318.0,"Objects":[{"StartTime":204318.0,"Position":264.0,"HyperDash":false}]},{"StartTime":204476.0,"Objects":[{"StartTime":204476.0,"Position":210.0,"HyperDash":false}]},{"StartTime":204633.0,"Objects":[{"StartTime":204633.0,"Position":210.0,"HyperDash":false},{"StartTime":204693.0,"Position":211.007629,"HyperDash":false},{"StartTime":204790.0,"Position":204.786621,"HyperDash":false}]},{"StartTime":204949.0,"Objects":[{"StartTime":204949.0,"Position":62.0,"HyperDash":false},{"StartTime":205009.0,"Position":74.99237,"HyperDash":false},{"StartTime":205106.0,"Position":67.21338,"HyperDash":false}]},{"StartTime":205265.0,"Objects":[{"StartTime":205265.0,"Position":192.0,"HyperDash":false},{"StartTime":205325.0,"Position":214.8626,"HyperDash":false},{"StartTime":205422.0,"Position":262.080139,"HyperDash":false}]},{"StartTime":205581.0,"Objects":[{"StartTime":205581.0,"Position":398.0,"HyperDash":false},{"StartTime":205641.0,"Position":358.8581,"HyperDash":false},{"StartTime":205738.0,"Position":327.74704,"HyperDash":false}]},{"StartTime":205897.0,"Objects":[{"StartTime":205897.0,"Position":407.0,"HyperDash":false}]},{"StartTime":206054.0,"Objects":[{"StartTime":206054.0,"Position":493.0,"HyperDash":false},{"StartTime":206114.0,"Position":493.732544,"HyperDash":false},{"StartTime":206211.0,"Position":478.1135,"HyperDash":false}]},{"StartTime":206370.0,"Objects":[{"StartTime":206370.0,"Position":311.0,"HyperDash":false},{"StartTime":206430.0,"Position":296.786255,"HyperDash":false},{"StartTime":206527.0,"Position":239.579437,"HyperDash":false}]},{"StartTime":206686.0,"Objects":[{"StartTime":206686.0,"Position":76.0,"HyperDash":false}]},{"StartTime":206844.0,"Objects":[{"StartTime":206844.0,"Position":76.0,"HyperDash":false}]},{"StartTime":207002.0,"Objects":[{"StartTime":207002.0,"Position":186.0,"HyperDash":false}]},{"StartTime":207160.0,"Objects":[{"StartTime":207160.0,"Position":186.0,"HyperDash":false},{"StartTime":207220.0,"Position":211.157623,"HyperDash":false},{"StartTime":207317.0,"Position":257.432068,"HyperDash":false}]},{"StartTime":207476.0,"Objects":[{"StartTime":207476.0,"Position":102.0,"HyperDash":false},{"StartTime":207545.0,"Position":104.631119,"HyperDash":false},{"StartTime":207615.0,"Position":116.053741,"HyperDash":false},{"StartTime":207685.0,"Position":129.854782,"HyperDash":false},{"StartTime":207791.0,"Position":145.055069,"HyperDash":false}]},{"StartTime":207949.0,"Objects":[{"StartTime":207949.0,"Position":73.0,"HyperDash":false}]},{"StartTime":208107.0,"Objects":[{"StartTime":208107.0,"Position":73.0,"HyperDash":false}]},{"StartTime":208265.0,"Objects":[{"StartTime":208265.0,"Position":188.0,"HyperDash":false}]},{"StartTime":208423.0,"Objects":[{"StartTime":208423.0,"Position":188.0,"HyperDash":false},{"StartTime":208483.0,"Position":197.04393,"HyperDash":false},{"StartTime":208580.0,"Position":259.303467,"HyperDash":false}]},{"StartTime":208739.0,"Objects":[{"StartTime":208739.0,"Position":356.0,"HyperDash":false}]},{"StartTime":208897.0,"Objects":[{"StartTime":208897.0,"Position":428.0,"HyperDash":false},{"StartTime":208957.0,"Position":429.1922,"HyperDash":false},{"StartTime":209054.0,"Position":459.666473,"HyperDash":false}]},{"StartTime":209212.0,"Objects":[{"StartTime":209212.0,"Position":320.0,"HyperDash":false}]},{"StartTime":209370.0,"Objects":[{"StartTime":209370.0,"Position":320.0,"HyperDash":false}]},{"StartTime":209528.0,"Objects":[{"StartTime":209528.0,"Position":347.0,"HyperDash":false}]},{"StartTime":209686.0,"Objects":[{"StartTime":209686.0,"Position":347.0,"HyperDash":false}]},{"StartTime":209844.0,"Objects":[{"StartTime":209844.0,"Position":228.0,"HyperDash":false}]},{"StartTime":210002.0,"Objects":[{"StartTime":210002.0,"Position":135.0,"HyperDash":false},{"StartTime":210071.0,"Position":121.854248,"HyperDash":false},{"StartTime":210141.0,"Position":131.1977,"HyperDash":false},{"StartTime":210211.0,"Position":101.2941,"HyperDash":false},{"StartTime":210317.0,"Position":107.741356,"HyperDash":false}]},{"StartTime":210476.0,"Objects":[{"StartTime":210476.0,"Position":226.0,"HyperDash":false}]},{"StartTime":210633.0,"Objects":[{"StartTime":210633.0,"Position":226.0,"HyperDash":false}]},{"StartTime":210791.0,"Objects":[{"StartTime":210791.0,"Position":188.0,"HyperDash":false},{"StartTime":210851.0,"Position":221.829361,"HyperDash":false},{"StartTime":210948.0,"Position":216.115952,"HyperDash":false}]},{"StartTime":211107.0,"Objects":[{"StartTime":211107.0,"Position":289.0,"HyperDash":false}]},{"StartTime":211265.0,"Objects":[{"StartTime":211265.0,"Position":289.0,"HyperDash":false}]},{"StartTime":211423.0,"Objects":[{"StartTime":211423.0,"Position":357.0,"HyperDash":false},{"StartTime":211483.0,"Position":351.170654,"HyperDash":false},{"StartTime":211580.0,"Position":328.884064,"HyperDash":false}]},{"StartTime":211739.0,"Objects":[{"StartTime":211739.0,"Position":320.0,"HyperDash":false}]},{"StartTime":211897.0,"Objects":[{"StartTime":211897.0,"Position":420.0,"HyperDash":false},{"StartTime":211966.0,"Position":438.684967,"HyperDash":false},{"StartTime":212036.0,"Position":420.642761,"HyperDash":false},{"StartTime":212106.0,"Position":454.598969,"HyperDash":false},{"StartTime":212212.0,"Position":437.382416,"HyperDash":false}]},{"StartTime":212370.0,"Objects":[{"StartTime":212370.0,"Position":330.0,"HyperDash":false}]},{"StartTime":212528.0,"Objects":[{"StartTime":212528.0,"Position":188.0,"HyperDash":false},{"StartTime":212597.0,"Position":177.5667,"HyperDash":false},{"StartTime":212667.0,"Position":199.229538,"HyperDash":false},{"StartTime":212737.0,"Position":175.06488,"HyperDash":false},{"StartTime":212843.0,"Position":205.139709,"HyperDash":false}]},{"StartTime":213002.0,"Objects":[{"StartTime":213002.0,"Position":89.0,"HyperDash":false}]},{"StartTime":213160.0,"Objects":[{"StartTime":213160.0,"Position":89.0,"HyperDash":false}]},{"StartTime":213318.0,"Objects":[{"StartTime":213318.0,"Position":205.0,"HyperDash":false},{"StartTime":213378.0,"Position":224.953186,"HyperDash":false},{"StartTime":213475.0,"Position":276.3385,"HyperDash":false}]},{"StartTime":213633.0,"Objects":[{"StartTime":213633.0,"Position":355.0,"HyperDash":false}]},{"StartTime":213791.0,"Objects":[{"StartTime":213791.0,"Position":355.0,"HyperDash":false}]},{"StartTime":213949.0,"Objects":[{"StartTime":213949.0,"Position":377.0,"HyperDash":false},{"StartTime":214009.0,"Position":374.1648,"HyperDash":false},{"StartTime":214106.0,"Position":356.636047,"HyperDash":false}]},{"StartTime":214265.0,"Objects":[{"StartTime":214265.0,"Position":229.0,"HyperDash":false},{"StartTime":214325.0,"Position":222.07782,"HyperDash":false},{"StartTime":214422.0,"Position":207.805984,"HyperDash":false}]},{"StartTime":214581.0,"Objects":[{"StartTime":214581.0,"Position":109.0,"HyperDash":false}]},{"StartTime":214739.0,"Objects":[{"StartTime":214739.0,"Position":109.0,"HyperDash":false}]},{"StartTime":214897.0,"Objects":[{"StartTime":214897.0,"Position":176.0,"HyperDash":false},{"StartTime":214957.0,"Position":219.19249,"HyperDash":false},{"StartTime":215054.0,"Position":248.6392,"HyperDash":false}]},{"StartTime":215212.0,"Objects":[{"StartTime":215212.0,"Position":343.0,"HyperDash":false}]},{"StartTime":215370.0,"Objects":[{"StartTime":215370.0,"Position":343.0,"HyperDash":false}]},{"StartTime":215528.0,"Objects":[{"StartTime":215528.0,"Position":304.0,"HyperDash":false}]},{"StartTime":215686.0,"Objects":[{"StartTime":215686.0,"Position":304.0,"HyperDash":false}]},{"StartTime":215844.0,"Objects":[{"StartTime":215844.0,"Position":425.0,"HyperDash":false},{"StartTime":215904.0,"Position":443.940369,"HyperDash":false},{"StartTime":216001.0,"Position":497.363678,"HyperDash":false}]},{"StartTime":216160.0,"Objects":[{"StartTime":216160.0,"Position":386.0,"HyperDash":false},{"StartTime":216220.0,"Position":369.1159,"HyperDash":false},{"StartTime":216317.0,"Position":313.428955,"HyperDash":false}]},{"StartTime":216476.0,"Objects":[{"StartTime":216476.0,"Position":269.0,"HyperDash":false},{"StartTime":216545.0,"Position":292.429657,"HyperDash":false},{"StartTime":216615.0,"Position":293.77887,"HyperDash":false},{"StartTime":216685.0,"Position":296.7586,"HyperDash":false},{"StartTime":216791.0,"Position":316.2445,"HyperDash":false}]},{"StartTime":216949.0,"Objects":[{"StartTime":216949.0,"Position":343.0,"HyperDash":false}]},{"StartTime":217107.0,"Objects":[{"StartTime":217107.0,"Position":192.0,"HyperDash":false},{"StartTime":217167.0,"Position":199.294876,"HyperDash":false},{"StartTime":217264.0,"Position":180.090454,"HyperDash":false}]},{"StartTime":217423.0,"Objects":[{"StartTime":217423.0,"Position":73.0,"HyperDash":false}]},{"StartTime":217581.0,"Objects":[{"StartTime":217581.0,"Position":73.0,"HyperDash":false}]},{"StartTime":217739.0,"Objects":[{"StartTime":217739.0,"Position":197.0,"HyperDash":false},{"StartTime":217808.0,"Position":242.080475,"HyperDash":false},{"StartTime":217878.0,"Position":248.160492,"HyperDash":false},{"StartTime":217948.0,"Position":291.815369,"HyperDash":false},{"StartTime":218054.0,"Position":323.144318,"HyperDash":false}]},{"StartTime":218212.0,"Objects":[{"StartTime":218212.0,"Position":194.0,"HyperDash":false}]},{"StartTime":218370.0,"Objects":[{"StartTime":218370.0,"Position":345.0,"HyperDash":false},{"StartTime":218430.0,"Position":355.6937,"HyperDash":false},{"StartTime":218527.0,"Position":419.238617,"HyperDash":false}]},{"StartTime":218686.0,"Objects":[{"StartTime":218686.0,"Position":416.0,"HyperDash":false},{"StartTime":218746.0,"Position":402.107758,"HyperDash":false},{"StartTime":218843.0,"Position":341.536041,"HyperDash":false}]},{"StartTime":219002.0,"Objects":[{"StartTime":219002.0,"Position":485.0,"HyperDash":false},{"StartTime":219071.0,"Position":454.952484,"HyperDash":false},{"StartTime":219141.0,"Position":458.110535,"HyperDash":false},{"StartTime":219211.0,"Position":430.9237,"HyperDash":false},{"StartTime":219317.0,"Position":435.739746,"HyperDash":false}]},{"StartTime":219476.0,"Objects":[{"StartTime":219476.0,"Position":339.0,"HyperDash":false}]},{"StartTime":219633.0,"Objects":[{"StartTime":219633.0,"Position":374.0,"HyperDash":false},{"StartTime":219702.0,"Position":396.047546,"HyperDash":false},{"StartTime":219772.0,"Position":388.889465,"HyperDash":false},{"StartTime":219842.0,"Position":400.076324,"HyperDash":false},{"StartTime":219948.0,"Position":423.260254,"HyperDash":false}]},{"StartTime":220107.0,"Objects":[{"StartTime":220107.0,"Position":248.0,"HyperDash":false}]},{"StartTime":220265.0,"Objects":[{"StartTime":220265.0,"Position":201.0,"HyperDash":false}]},{"StartTime":220423.0,"Objects":[{"StartTime":220423.0,"Position":201.0,"HyperDash":false}]},{"StartTime":220581.0,"Objects":[{"StartTime":220581.0,"Position":239.0,"HyperDash":false}]},{"StartTime":220739.0,"Objects":[{"StartTime":220739.0,"Position":239.0,"HyperDash":false}]},{"StartTime":220897.0,"Objects":[{"StartTime":220897.0,"Position":122.0,"HyperDash":false},{"StartTime":220957.0,"Position":106.407677,"HyperDash":false},{"StartTime":221054.0,"Position":49.1845436,"HyperDash":false}]},{"StartTime":221212.0,"Objects":[{"StartTime":221212.0,"Position":257.0,"HyperDash":false},{"StartTime":221272.0,"Position":297.787933,"HyperDash":false},{"StartTime":221369.0,"Position":329.733826,"HyperDash":false}]},{"StartTime":221528.0,"Objects":[{"StartTime":221528.0,"Position":442.0,"HyperDash":false},{"StartTime":221588.0,"Position":442.869934,"HyperDash":false},{"StartTime":221685.0,"Position":436.426361,"HyperDash":false}]},{"StartTime":221844.0,"Objects":[{"StartTime":221844.0,"Position":417.0,"HyperDash":false},{"StartTime":221904.0,"Position":411.709747,"HyperDash":false},{"StartTime":222001.0,"Position":411.0072,"HyperDash":false}]},{"StartTime":222160.0,"Objects":[{"StartTime":222160.0,"Position":336.0,"HyperDash":false},{"StartTime":222220.0,"Position":351.869934,"HyperDash":false},{"StartTime":222317.0,"Position":330.426361,"HyperDash":false}]},{"StartTime":222476.0,"Objects":[{"StartTime":222476.0,"Position":311.0,"HyperDash":false},{"StartTime":222536.0,"Position":310.709747,"HyperDash":false},{"StartTime":222633.0,"Position":305.0072,"HyperDash":false}]},{"StartTime":222791.0,"Objects":[{"StartTime":222791.0,"Position":165.0,"HyperDash":false}]},{"StartTime":222949.0,"Objects":[{"StartTime":222949.0,"Position":143.0,"HyperDash":false}]},{"StartTime":223107.0,"Objects":[{"StartTime":223107.0,"Position":156.0,"HyperDash":false}]},{"StartTime":223265.0,"Objects":[{"StartTime":223265.0,"Position":125.0,"HyperDash":false}]},{"StartTime":223423.0,"Objects":[{"StartTime":223423.0,"Position":142.0,"HyperDash":false},{"StartTime":223483.0,"Position":119.964447,"HyperDash":false},{"StartTime":223580.0,"Position":66.02364,"HyperDash":false}]},{"StartTime":223739.0,"Objects":[{"StartTime":223739.0,"Position":209.0,"HyperDash":false}]},{"StartTime":223897.0,"Objects":[{"StartTime":223897.0,"Position":3.0,"HyperDash":false}]},{"StartTime":224054.0,"Objects":[{"StartTime":224054.0,"Position":111.0,"HyperDash":false}]},{"StartTime":234160.0,"Objects":[{"StartTime":234160.0,"Position":82.0,"HyperDash":false}]},{"StartTime":234476.0,"Objects":[{"StartTime":234476.0,"Position":82.0,"HyperDash":false}]},{"StartTime":234791.0,"Objects":[{"StartTime":234791.0,"Position":82.0,"HyperDash":false}]},{"StartTime":235107.0,"Objects":[{"StartTime":235107.0,"Position":82.0,"HyperDash":false}]},{"StartTime":235423.0,"Objects":[{"StartTime":235423.0,"Position":312.0,"HyperDash":false},{"StartTime":235483.0,"Position":357.5692,"HyperDash":false},{"StartTime":235580.0,"Position":391.4958,"HyperDash":false}]},{"StartTime":235739.0,"Objects":[{"StartTime":235739.0,"Position":262.0,"HyperDash":false}]},{"StartTime":235897.0,"Objects":[{"StartTime":235897.0,"Position":170.0,"HyperDash":false},{"StartTime":235957.0,"Position":146.430771,"HyperDash":false},{"StartTime":236054.0,"Position":90.5042,"HyperDash":false}]},{"StartTime":236212.0,"Objects":[{"StartTime":236212.0,"Position":83.0,"HyperDash":false},{"StartTime":236272.0,"Position":102.111885,"HyperDash":false},{"StartTime":236369.0,"Position":108.48745,"HyperDash":false}]},{"StartTime":236528.0,"Objects":[{"StartTime":236528.0,"Position":258.0,"HyperDash":false},{"StartTime":236597.0,"Position":241.951874,"HyperDash":false},{"StartTime":236667.0,"Position":212.802032,"HyperDash":false},{"StartTime":236737.0,"Position":200.97171,"HyperDash":false},{"StartTime":236843.0,"Position":210.516815,"HyperDash":false}]},{"StartTime":237002.0,"Objects":[{"StartTime":237002.0,"Position":327.0,"HyperDash":false}]},{"StartTime":237160.0,"Objects":[{"StartTime":237160.0,"Position":170.0,"HyperDash":false}]},{"StartTime":237318.0,"Objects":[{"StartTime":237318.0,"Position":316.0,"HyperDash":false},{"StartTime":237378.0,"Position":364.7829,"HyperDash":false},{"StartTime":237475.0,"Position":397.227,"HyperDash":false}]},{"StartTime":237633.0,"Objects":[{"StartTime":237633.0,"Position":417.0,"HyperDash":false},{"StartTime":237693.0,"Position":394.217072,"HyperDash":false},{"StartTime":237790.0,"Position":335.773,"HyperDash":false}]},{"StartTime":237949.0,"Objects":[{"StartTime":237949.0,"Position":153.0,"HyperDash":false},{"StartTime":238018.0,"Position":178.837616,"HyperDash":false},{"StartTime":238088.0,"Position":163.454758,"HyperDash":false},{"StartTime":238158.0,"Position":190.438675,"HyperDash":false},{"StartTime":238264.0,"Position":188.068771,"HyperDash":false}]},{"StartTime":238423.0,"Objects":[{"StartTime":238423.0,"Position":81.0,"HyperDash":false},{"StartTime":238483.0,"Position":68.7763062,"HyperDash":false},{"StartTime":238580.0,"Position":95.3198,"HyperDash":false}]},{"StartTime":238739.0,"Objects":[{"StartTime":238739.0,"Position":277.0,"HyperDash":false},{"StartTime":238799.0,"Position":285.009674,"HyperDash":false},{"StartTime":238896.0,"Position":291.003174,"HyperDash":false}]},{"StartTime":239054.0,"Objects":[{"StartTime":239054.0,"Position":429.0,"HyperDash":false},{"StartTime":239123.0,"Position":409.879852,"HyperDash":false},{"StartTime":239193.0,"Position":394.502,"HyperDash":false},{"StartTime":239263.0,"Position":421.1194,"HyperDash":false},{"StartTime":239369.0,"Position":401.762024,"HyperDash":false}]},{"StartTime":239528.0,"Objects":[{"StartTime":239528.0,"Position":252.0,"HyperDash":false}]},{"StartTime":239686.0,"Objects":[{"StartTime":239686.0,"Position":383.0,"HyperDash":false}]},{"StartTime":239844.0,"Objects":[{"StartTime":239844.0,"Position":224.0,"HyperDash":false},{"StartTime":239904.0,"Position":248.6068,"HyperDash":false},{"StartTime":240001.0,"Position":243.923813,"HyperDash":false}]},{"StartTime":240160.0,"Objects":[{"StartTime":240160.0,"Position":282.0,"HyperDash":false},{"StartTime":240220.0,"Position":294.4477,"HyperDash":false},{"StartTime":240317.0,"Position":300.9552,"HyperDash":false}]},{"StartTime":240476.0,"Objects":[{"StartTime":240476.0,"Position":155.0,"HyperDash":false},{"StartTime":240536.0,"Position":139.565125,"HyperDash":false},{"StartTime":240633.0,"Position":75.8260956,"HyperDash":false}]},{"StartTime":240791.0,"Objects":[{"StartTime":240791.0,"Position":177.0,"HyperDash":false}]},{"StartTime":240949.0,"Objects":[{"StartTime":240949.0,"Position":285.0,"HyperDash":false},{"StartTime":241009.0,"Position":297.434875,"HyperDash":false},{"StartTime":241106.0,"Position":364.1739,"HyperDash":false}]},{"StartTime":241265.0,"Objects":[{"StartTime":241265.0,"Position":190.0,"HyperDash":false},{"StartTime":241325.0,"Position":151.565109,"HyperDash":false},{"StartTime":241422.0,"Position":110.826096,"HyperDash":true}]},{"StartTime":241581.0,"Objects":[{"StartTime":241581.0,"Position":350.0,"HyperDash":false},{"StartTime":241650.0,"Position":379.1303,"HyperDash":false},{"StartTime":241720.0,"Position":386.2259,"HyperDash":false},{"StartTime":241790.0,"Position":365.848328,"HyperDash":false},{"StartTime":241896.0,"Position":367.289581,"HyperDash":false}]},{"StartTime":242054.0,"Objects":[{"StartTime":242054.0,"Position":172.0,"HyperDash":false},{"StartTime":242114.0,"Position":207.784363,"HyperDash":false},{"StartTime":242211.0,"Position":249.567841,"HyperDash":false}]},{"StartTime":242370.0,"Objects":[{"StartTime":242370.0,"Position":94.0,"HyperDash":false},{"StartTime":242430.0,"Position":107.155533,"HyperDash":false},{"StartTime":242527.0,"Position":172.076752,"HyperDash":false}]},{"StartTime":242686.0,"Objects":[{"StartTime":242686.0,"Position":256.0,"HyperDash":false},{"StartTime":242746.0,"Position":221.664886,"HyperDash":false},{"StartTime":242843.0,"Position":177.734055,"HyperDash":false}]},{"StartTime":243002.0,"Objects":[{"StartTime":243002.0,"Position":291.0,"HyperDash":false},{"StartTime":243062.0,"Position":288.7001,"HyperDash":false},{"StartTime":243159.0,"Position":309.460449,"HyperDash":false}]},{"StartTime":243318.0,"Objects":[{"StartTime":243318.0,"Position":386.0,"HyperDash":false}]},{"StartTime":243476.0,"Objects":[{"StartTime":243476.0,"Position":225.0,"HyperDash":false},{"StartTime":243536.0,"Position":221.299881,"HyperDash":false},{"StartTime":243633.0,"Position":206.539551,"HyperDash":false}]},{"StartTime":243791.0,"Objects":[{"StartTime":243791.0,"Position":406.0,"HyperDash":false},{"StartTime":243851.0,"Position":381.939,"HyperDash":false},{"StartTime":243948.0,"Position":386.849457,"HyperDash":false}]},{"StartTime":244107.0,"Objects":[{"StartTime":244107.0,"Position":308.0,"HyperDash":false}]},{"StartTime":244265.0,"Objects":[{"StartTime":244265.0,"Position":246.0,"HyperDash":false},{"StartTime":244325.0,"Position":196.524536,"HyperDash":false},{"StartTime":244422.0,"Position":163.999634,"HyperDash":false}]},{"StartTime":244581.0,"Objects":[{"StartTime":244581.0,"Position":89.0,"HyperDash":false}]},{"StartTime":244739.0,"Objects":[{"StartTime":244739.0,"Position":89.0,"HyperDash":false}]},{"StartTime":244897.0,"Objects":[{"StartTime":244897.0,"Position":242.0,"HyperDash":false},{"StartTime":244957.0,"Position":212.524536,"HyperDash":false},{"StartTime":245054.0,"Position":159.999634,"HyperDash":false}]},{"StartTime":245212.0,"Objects":[{"StartTime":245212.0,"Position":189.0,"HyperDash":false}]},{"StartTime":245370.0,"Objects":[{"StartTime":245370.0,"Position":189.0,"HyperDash":false}]},{"StartTime":245528.0,"Objects":[{"StartTime":245528.0,"Position":311.0,"HyperDash":false},{"StartTime":245588.0,"Position":334.7987,"HyperDash":false},{"StartTime":245685.0,"Position":390.993317,"HyperDash":false}]},{"StartTime":245844.0,"Objects":[{"StartTime":245844.0,"Position":400.0,"HyperDash":false}]},{"StartTime":246002.0,"Objects":[{"StartTime":246002.0,"Position":250.0,"HyperDash":false},{"StartTime":246062.0,"Position":220.210785,"HyperDash":false},{"StartTime":246159.0,"Position":170.042587,"HyperDash":false}]},{"StartTime":246318.0,"Objects":[{"StartTime":246318.0,"Position":320.0,"HyperDash":false},{"StartTime":246378.0,"Position":337.9858,"HyperDash":false},{"StartTime":246475.0,"Position":399.7238,"HyperDash":false}]},{"StartTime":246633.0,"Objects":[{"StartTime":246633.0,"Position":488.0,"HyperDash":false},{"StartTime":246693.0,"Position":475.33725,"HyperDash":false},{"StartTime":246790.0,"Position":466.066925,"HyperDash":false}]},{"StartTime":246949.0,"Objects":[{"StartTime":246949.0,"Position":314.0,"HyperDash":false},{"StartTime":247009.0,"Position":298.7121,"HyperDash":false},{"StartTime":247106.0,"Position":292.039,"HyperDash":false}]},{"StartTime":247265.0,"Objects":[{"StartTime":247265.0,"Position":202.0,"HyperDash":false},{"StartTime":247334.0,"Position":159.634674,"HyperDash":false},{"StartTime":247404.0,"Position":149.680634,"HyperDash":false},{"StartTime":247474.0,"Position":95.09981,"HyperDash":false},{"StartTime":247580.0,"Position":69.26001,"HyperDash":false}]},{"StartTime":247739.0,"Objects":[{"StartTime":247739.0,"Position":190.0,"HyperDash":false}]},{"StartTime":247897.0,"Objects":[{"StartTime":247897.0,"Position":200.0,"HyperDash":false}]},{"StartTime":248054.0,"Objects":[{"StartTime":248054.0,"Position":188.0,"HyperDash":false},{"StartTime":248114.0,"Position":208.2239,"HyperDash":false},{"StartTime":248211.0,"Position":262.024536,"HyperDash":false}]},{"StartTime":248370.0,"Objects":[{"StartTime":248370.0,"Position":342.0,"HyperDash":false}]},{"StartTime":248528.0,"Objects":[{"StartTime":248528.0,"Position":338.0,"HyperDash":false},{"StartTime":248588.0,"Position":338.277985,"HyperDash":false},{"StartTime":248685.0,"Position":366.8771,"HyperDash":false}]},{"StartTime":248844.0,"Objects":[{"StartTime":248844.0,"Position":290.0,"HyperDash":false},{"StartTime":248904.0,"Position":284.053131,"HyperDash":false},{"StartTime":249001.0,"Position":319.062073,"HyperDash":false}]},{"StartTime":249160.0,"Objects":[{"StartTime":249160.0,"Position":432.0,"HyperDash":false},{"StartTime":249220.0,"Position":451.277985,"HyperDash":false},{"StartTime":249317.0,"Position":460.877136,"HyperDash":false}]},{"StartTime":249476.0,"Objects":[{"StartTime":249476.0,"Position":384.0,"HyperDash":false},{"StartTime":249536.0,"Position":383.053131,"HyperDash":false},{"StartTime":249633.0,"Position":413.062042,"HyperDash":false}]},{"StartTime":249791.0,"Objects":[{"StartTime":249791.0,"Position":449.0,"HyperDash":false},{"StartTime":249860.0,"Position":463.458252,"HyperDash":false},{"StartTime":249930.0,"Position":466.69632,"HyperDash":false},{"StartTime":250000.0,"Position":482.1586,"HyperDash":false},{"StartTime":250106.0,"Position":487.1767,"HyperDash":false}]},{"StartTime":250265.0,"Objects":[{"StartTime":250265.0,"Position":351.0,"HyperDash":false}]},{"StartTime":250423.0,"Objects":[{"StartTime":250423.0,"Position":312.0,"HyperDash":false}]},{"StartTime":250581.0,"Objects":[{"StartTime":250581.0,"Position":196.0,"HyperDash":false},{"StartTime":250641.0,"Position":227.257263,"HyperDash":false},{"StartTime":250738.0,"Position":222.828583,"HyperDash":false}]},{"StartTime":250897.0,"Objects":[{"StartTime":250897.0,"Position":161.0,"HyperDash":false}]},{"StartTime":251054.0,"Objects":[{"StartTime":251054.0,"Position":88.0,"HyperDash":false},{"StartTime":251114.0,"Position":72.74277,"HyperDash":false},{"StartTime":251211.0,"Position":61.1714363,"HyperDash":false}]},{"StartTime":251370.0,"Objects":[{"StartTime":251370.0,"Position":188.0,"HyperDash":false},{"StartTime":251430.0,"Position":165.064133,"HyperDash":false},{"StartTime":251527.0,"Position":160.9748,"HyperDash":false}]},{"StartTime":251686.0,"Objects":[{"StartTime":251686.0,"Position":206.0,"HyperDash":false},{"StartTime":251746.0,"Position":254.490585,"HyperDash":false},{"StartTime":251843.0,"Position":286.597961,"HyperDash":false}]},{"StartTime":252002.0,"Objects":[{"StartTime":252002.0,"Position":381.0,"HyperDash":false},{"StartTime":252062.0,"Position":344.076172,"HyperDash":false},{"StartTime":252159.0,"Position":300.619,"HyperDash":false}]},{"StartTime":252318.0,"Objects":[{"StartTime":252318.0,"Position":430.0,"HyperDash":false}]},{"StartTime":252476.0,"Objects":[{"StartTime":252476.0,"Position":440.0,"HyperDash":false},{"StartTime":252536.0,"Position":447.263672,"HyperDash":false},{"StartTime":252633.0,"Position":467.223053,"HyperDash":false}]},{"StartTime":252791.0,"Objects":[{"StartTime":252791.0,"Position":349.0,"HyperDash":false},{"StartTime":252851.0,"Position":324.82547,"HyperDash":false},{"StartTime":252948.0,"Position":321.497559,"HyperDash":false}]},{"StartTime":253107.0,"Objects":[{"StartTime":253107.0,"Position":217.0,"HyperDash":false}]},{"StartTime":253265.0,"Objects":[{"StartTime":253265.0,"Position":229.0,"HyperDash":false}]},{"StartTime":253423.0,"Objects":[{"StartTime":253423.0,"Position":235.0,"HyperDash":false}]},{"StartTime":253581.0,"Objects":[{"StartTime":253581.0,"Position":225.0,"HyperDash":false},{"StartTime":253641.0,"Position":189.989166,"HyperDash":false},{"StartTime":253738.0,"Position":150.638168,"HyperDash":false}]},{"StartTime":253897.0,"Objects":[{"StartTime":253897.0,"Position":318.0,"HyperDash":false}]},{"StartTime":254054.0,"Objects":[{"StartTime":254054.0,"Position":337.0,"HyperDash":false}]},{"StartTime":254212.0,"Objects":[{"StartTime":254212.0,"Position":407.0,"HyperDash":false}]},{"StartTime":254291.0,"Objects":[{"StartTime":254291.0,"Position":407.0,"HyperDash":false}]},{"StartTime":254370.0,"Objects":[{"StartTime":254370.0,"Position":407.0,"HyperDash":false},{"StartTime":254430.0,"Position":396.4197,"HyperDash":false},{"StartTime":254527.0,"Position":415.948242,"HyperDash":false}]},{"StartTime":254686.0,"Objects":[{"StartTime":254686.0,"Position":282.0,"HyperDash":false}]},{"StartTime":254844.0,"Objects":[{"StartTime":254844.0,"Position":314.0,"HyperDash":false},{"StartTime":254904.0,"Position":328.5803,"HyperDash":false},{"StartTime":255001.0,"Position":305.051758,"HyperDash":false}]},{"StartTime":255160.0,"Objects":[{"StartTime":255160.0,"Position":150.0,"HyperDash":false}]},{"StartTime":255318.0,"Objects":[{"StartTime":255318.0,"Position":297.0,"HyperDash":true}]},{"StartTime":255476.0,"Objects":[{"StartTime":255476.0,"Position":74.0,"HyperDash":false}]},{"StartTime":255633.0,"Objects":[{"StartTime":255633.0,"Position":184.0,"HyperDash":false}]},{"StartTime":259423.0,"Objects":[{"StartTime":259423.0,"Position":66.0,"HyperDash":false},{"StartTime":259483.0,"Position":83.09656,"HyperDash":false},{"StartTime":259580.0,"Position":123.771538,"HyperDash":false}]},{"StartTime":259739.0,"Objects":[{"StartTime":259739.0,"Position":227.0,"HyperDash":false},{"StartTime":259799.0,"Position":259.148071,"HyperDash":false},{"StartTime":259896.0,"Position":284.876556,"HyperDash":false}]},{"StartTime":260054.0,"Objects":[{"StartTime":260054.0,"Position":374.0,"HyperDash":false}]},{"StartTime":260212.0,"Objects":[{"StartTime":260212.0,"Position":399.0,"HyperDash":false}]},{"StartTime":260370.0,"Objects":[{"StartTime":260370.0,"Position":455.0,"HyperDash":false}]},{"StartTime":260528.0,"Objects":[{"StartTime":260528.0,"Position":396.0,"HyperDash":false}]},{"StartTime":260686.0,"Objects":[{"StartTime":260686.0,"Position":288.0,"HyperDash":false},{"StartTime":260746.0,"Position":257.008453,"HyperDash":false},{"StartTime":260843.0,"Position":211.3641,"HyperDash":false}]},{"StartTime":261002.0,"Objects":[{"StartTime":261002.0,"Position":83.0,"HyperDash":false}]},{"StartTime":261160.0,"Objects":[{"StartTime":261160.0,"Position":120.0,"HyperDash":false},{"StartTime":261220.0,"Position":138.656952,"HyperDash":false},{"StartTime":261317.0,"Position":149.021484,"HyperDash":false}]},{"StartTime":261476.0,"Objects":[{"StartTime":261476.0,"Position":168.0,"HyperDash":false},{"StartTime":261536.0,"Position":191.8636,"HyperDash":false},{"StartTime":261633.0,"Position":196.8266,"HyperDash":false}]},{"StartTime":261791.0,"Objects":[{"StartTime":261791.0,"Position":300.0,"HyperDash":false},{"StartTime":261860.0,"Position":319.492554,"HyperDash":false},{"StartTime":261930.0,"Position":380.197144,"HyperDash":false},{"StartTime":262000.0,"Position":391.054535,"HyperDash":false},{"StartTime":262106.0,"Position":437.8109,"HyperDash":false}]},{"StartTime":262265.0,"Objects":[{"StartTime":262265.0,"Position":319.0,"HyperDash":false},{"StartTime":262325.0,"Position":323.6614,"HyperDash":false},{"StartTime":262422.0,"Position":301.140259,"HyperDash":false}]},{"StartTime":262581.0,"Objects":[{"StartTime":262581.0,"Position":160.0,"HyperDash":false},{"StartTime":262641.0,"Position":149.948944,"HyperDash":false},{"StartTime":262738.0,"Position":141.732,"HyperDash":false}]},{"StartTime":262897.0,"Objects":[{"StartTime":262897.0,"Position":297.0,"HyperDash":false},{"StartTime":262957.0,"Position":272.6614,"HyperDash":false},{"StartTime":263054.0,"Position":279.140259,"HyperDash":false}]},{"StartTime":263212.0,"Objects":[{"StartTime":263212.0,"Position":430.0,"HyperDash":false},{"StartTime":263272.0,"Position":455.104431,"HyperDash":false},{"StartTime":263369.0,"Position":510.4512,"HyperDash":false}]},{"StartTime":263528.0,"Objects":[{"StartTime":263528.0,"Position":401.0,"HyperDash":false}]},{"StartTime":263686.0,"Objects":[{"StartTime":263686.0,"Position":282.0,"HyperDash":false},{"StartTime":263746.0,"Position":270.895569,"HyperDash":false},{"StartTime":263843.0,"Position":201.548782,"HyperDash":false}]},{"StartTime":264002.0,"Objects":[{"StartTime":264002.0,"Position":124.0,"HyperDash":false},{"StartTime":264062.0,"Position":170.993927,"HyperDash":false},{"StartTime":264159.0,"Position":204.329208,"HyperDash":false}]},{"StartTime":264318.0,"Objects":[{"StartTime":264318.0,"Position":93.0,"HyperDash":false}]},{"StartTime":264476.0,"Objects":[{"StartTime":264476.0,"Position":61.0,"HyperDash":false},{"StartTime":264536.0,"Position":72.74982,"HyperDash":false},{"StartTime":264633.0,"Position":76.9942856,"HyperDash":false}]},{"StartTime":264791.0,"Objects":[{"StartTime":264791.0,"Position":229.0,"HyperDash":false},{"StartTime":264851.0,"Position":210.380234,"HyperDash":false},{"StartTime":264948.0,"Position":212.7894,"HyperDash":false}]},{"StartTime":265107.0,"Objects":[{"StartTime":265107.0,"Position":358.0,"HyperDash":false},{"StartTime":265167.0,"Position":382.749847,"HyperDash":false},{"StartTime":265264.0,"Position":373.9943,"HyperDash":false}]},{"StartTime":265423.0,"Objects":[{"StartTime":265423.0,"Position":470.0,"HyperDash":false}]},{"StartTime":265581.0,"Objects":[{"StartTime":265581.0,"Position":470.0,"HyperDash":false}]},{"StartTime":266054.0,"Objects":[{"StartTime":266054.0,"Position":149.0,"HyperDash":false},{"StartTime":266132.0,"Position":167.136108,"HyperDash":false},{"StartTime":266211.0,"Position":211.849609,"HyperDash":false},{"StartTime":266290.0,"Position":233.369949,"HyperDash":false},{"StartTime":266369.0,"Position":230.355377,"HyperDash":false},{"StartTime":266419.0,"Position":248.772461,"HyperDash":false},{"StartTime":266469.0,"Position":258.240936,"HyperDash":false},{"StartTime":266519.0,"Position":225.7301,"HyperDash":false},{"StartTime":266606.0,"Position":243.291763,"HyperDash":false}]},{"StartTime":266686.0,"Objects":[{"StartTime":266686.0,"Position":253.0,"HyperDash":false},{"StartTime":266764.0,"Position":255.375717,"HyperDash":false},{"StartTime":266843.0,"Position":240.91098,"HyperDash":false},{"StartTime":266922.0,"Position":228.636566,"HyperDash":false},{"StartTime":267001.0,"Position":225.662109,"HyperDash":false},{"StartTime":267051.0,"Position":218.509918,"HyperDash":false},{"StartTime":267101.0,"Position":217.651337,"HyperDash":false},{"StartTime":267151.0,"Position":202.171875,"HyperDash":false},{"StartTime":267238.0,"Position":158.415985,"HyperDash":false}]},{"StartTime":267318.0,"Objects":[{"StartTime":267318.0,"Position":168.0,"HyperDash":false},{"StartTime":267396.0,"Position":192.136108,"HyperDash":false},{"StartTime":267475.0,"Position":198.849609,"HyperDash":false},{"StartTime":267554.0,"Position":251.369949,"HyperDash":false},{"StartTime":267633.0,"Position":249.355377,"HyperDash":false},{"StartTime":267683.0,"Position":270.772461,"HyperDash":false},{"StartTime":267733.0,"Position":256.240936,"HyperDash":false},{"StartTime":267783.0,"Position":261.7301,"HyperDash":false},{"StartTime":267870.0,"Position":262.291779,"HyperDash":false}]},{"StartTime":267949.0,"Objects":[{"StartTime":267949.0,"Position":272.0,"HyperDash":false},{"StartTime":268027.0,"Position":258.375732,"HyperDash":false},{"StartTime":268106.0,"Position":255.91098,"HyperDash":false},{"StartTime":268185.0,"Position":277.636566,"HyperDash":false},{"StartTime":268264.0,"Position":244.662109,"HyperDash":false},{"StartTime":268314.0,"Position":220.509918,"HyperDash":false},{"StartTime":268364.0,"Position":225.651337,"HyperDash":false},{"StartTime":268414.0,"Position":209.171875,"HyperDash":false},{"StartTime":268501.0,"Position":177.415985,"HyperDash":false}]},{"StartTime":268581.0,"Objects":[{"StartTime":268581.0,"Position":187.0,"HyperDash":false},{"StartTime":268659.0,"Position":202.237671,"HyperDash":false},{"StartTime":268738.0,"Position":233.0073,"HyperDash":false},{"StartTime":268817.0,"Position":262.5497,"HyperDash":false},{"StartTime":268896.0,"Position":268.5099,"HyperDash":false},{"StartTime":268946.0,"Position":291.870758,"HyperDash":false},{"StartTime":268996.0,"Position":273.257019,"HyperDash":false},{"StartTime":269046.0,"Position":299.637756,"HyperDash":false},{"StartTime":269133.0,"Position":280.97876,"HyperDash":false}]},{"StartTime":269212.0,"Objects":[{"StartTime":269212.0,"Position":294.0,"HyperDash":false},{"StartTime":269312.0,"Position":315.9435,"HyperDash":false},{"StartTime":269448.0,"Position":321.1469,"HyperDash":false}]},{"StartTime":269528.0,"Objects":[{"StartTime":269528.0,"Position":340.0,"HyperDash":false},{"StartTime":269588.0,"Position":365.320465,"HyperDash":false},{"StartTime":269685.0,"Position":377.5064,"HyperDash":false}]},{"StartTime":269844.0,"Objects":[{"StartTime":269844.0,"Position":447.0,"HyperDash":false}]},{"StartTime":270002.0,"Objects":[{"StartTime":270002.0,"Position":465.0,"HyperDash":false}]},{"StartTime":270160.0,"Objects":[{"StartTime":270160.0,"Position":450.0,"HyperDash":false}]},{"StartTime":270318.0,"Objects":[{"StartTime":270318.0,"Position":468.0,"HyperDash":false}]},{"StartTime":270476.0,"Objects":[{"StartTime":270476.0,"Position":344.0,"HyperDash":false},{"StartTime":270536.0,"Position":326.693817,"HyperDash":false},{"StartTime":270633.0,"Position":270.128,"HyperDash":false}]},{"StartTime":270791.0,"Objects":[{"StartTime":270791.0,"Position":146.0,"HyperDash":false},{"StartTime":270851.0,"Position":119.05838,"HyperDash":false},{"StartTime":270948.0,"Position":124.892738,"HyperDash":false}]},{"StartTime":271107.0,"Objects":[{"StartTime":271107.0,"Position":264.0,"HyperDash":false}]},{"StartTime":271265.0,"Objects":[{"StartTime":271265.0,"Position":218.0,"HyperDash":false},{"StartTime":271325.0,"Position":192.312866,"HyperDash":false},{"StartTime":271422.0,"Position":147.21402,"HyperDash":false}]},{"StartTime":271581.0,"Objects":[{"StartTime":271581.0,"Position":245.0,"HyperDash":false},{"StartTime":271641.0,"Position":271.938019,"HyperDash":false},{"StartTime":271738.0,"Position":315.481079,"HyperDash":false}]},{"StartTime":271897.0,"Objects":[{"StartTime":271897.0,"Position":349.0,"HyperDash":false},{"StartTime":271957.0,"Position":327.700134,"HyperDash":false},{"StartTime":272054.0,"Position":336.267517,"HyperDash":false}]},{"StartTime":272212.0,"Objects":[{"StartTime":272212.0,"Position":446.0,"HyperDash":false},{"StartTime":272272.0,"Position":462.1508,"HyperDash":false},{"StartTime":272369.0,"Position":432.882324,"HyperDash":false}]},{"StartTime":272528.0,"Objects":[{"StartTime":272528.0,"Position":324.0,"HyperDash":false}]},{"StartTime":272686.0,"Objects":[{"StartTime":272686.0,"Position":415.0,"HyperDash":false},{"StartTime":272746.0,"Position":460.961884,"HyperDash":false},{"StartTime":272843.0,"Position":493.6076,"HyperDash":false}]},{"StartTime":273002.0,"Objects":[{"StartTime":273002.0,"Position":349.0,"HyperDash":false},{"StartTime":273062.0,"Position":319.039642,"HyperDash":false},{"StartTime":273159.0,"Position":270.206818,"HyperDash":false}]},{"StartTime":273318.0,"Objects":[{"StartTime":273318.0,"Position":148.0,"HyperDash":false},{"StartTime":273378.0,"Position":142.55928,"HyperDash":false},{"StartTime":273475.0,"Position":125.789063,"HyperDash":false}]},{"StartTime":273633.0,"Objects":[{"StartTime":273633.0,"Position":199.0,"HyperDash":false}]},{"StartTime":273791.0,"Objects":[{"StartTime":273791.0,"Position":247.0,"HyperDash":false},{"StartTime":273851.0,"Position":242.4407,"HyperDash":false},{"StartTime":273948.0,"Position":269.210938,"HyperDash":false}]},{"StartTime":274107.0,"Objects":[{"StartTime":274107.0,"Position":242.0,"HyperDash":false}]},{"StartTime":274265.0,"Objects":[{"StartTime":274265.0,"Position":143.0,"HyperDash":false},{"StartTime":274325.0,"Position":126.55928,"HyperDash":false},{"StartTime":274422.0,"Position":120.789063,"HyperDash":false}]},{"StartTime":274581.0,"Objects":[{"StartTime":274581.0,"Position":272.0,"HyperDash":false},{"StartTime":274641.0,"Position":314.038574,"HyperDash":false},{"StartTime":274738.0,"Position":355.8343,"HyperDash":false}]},{"StartTime":274897.0,"Objects":[{"StartTime":274897.0,"Position":488.0,"HyperDash":false},{"StartTime":274957.0,"Position":461.961426,"HyperDash":false},{"StartTime":275054.0,"Position":404.1657,"HyperDash":false}]},{"StartTime":275212.0,"Objects":[{"StartTime":275212.0,"Position":285.0,"HyperDash":false}]},{"StartTime":275370.0,"Objects":[{"StartTime":275370.0,"Position":315.0,"HyperDash":false}]},{"StartTime":275528.0,"Objects":[{"StartTime":275528.0,"Position":283.0,"HyperDash":false}]},{"StartTime":275686.0,"Objects":[{"StartTime":275686.0,"Position":313.0,"HyperDash":false}]},{"StartTime":275844.0,"Objects":[{"StartTime":275844.0,"Position":254.0,"HyperDash":false}]},{"StartTime":278370.0,"Objects":[{"StartTime":278370.0,"Position":71.0,"HyperDash":false},{"StartTime":278470.0,"Position":130.124451,"HyperDash":false},{"StartTime":278606.0,"Position":152.874481,"HyperDash":false}]},{"StartTime":278686.0,"Objects":[{"StartTime":278686.0,"Position":256.0,"HyperDash":false},{"StartTime":278786.0,"Position":279.959045,"HyperDash":false},{"StartTime":278922.0,"Position":336.141327,"HyperDash":false}]},{"StartTime":279002.0,"Objects":[{"StartTime":279002.0,"Position":351.0,"HyperDash":false},{"StartTime":279102.0,"Position":306.33313,"HyperDash":false},{"StartTime":279238.0,"Position":260.928619,"HyperDash":false}]},{"StartTime":279318.0,"Objects":[{"StartTime":279318.0,"Position":149.0,"HyperDash":false},{"StartTime":279418.0,"Position":144.369186,"HyperDash":false},{"StartTime":279554.0,"Position":58.3702,"HyperDash":true}]},{"StartTime":279633.0,"Objects":[{"StartTime":279633.0,"Position":205.0,"HyperDash":false}]},{"StartTime":280265.0,"Objects":[{"StartTime":280265.0,"Position":480.0,"HyperDash":false},{"StartTime":280343.0,"Position":474.7398,"HyperDash":false},{"StartTime":280422.0,"Position":458.350433,"HyperDash":false},{"StartTime":280501.0,"Position":451.037842,"HyperDash":false},{"StartTime":280580.0,"Position":422.829529,"HyperDash":false},{"StartTime":280659.0,"Position":414.7673,"HyperDash":false},{"StartTime":280738.0,"Position":394.904449,"HyperDash":false},{"StartTime":280817.0,"Position":370.3106,"HyperDash":false},{"StartTime":280896.0,"Position":368.073456,"HyperDash":false},{"StartTime":280975.0,"Position":348.296478,"HyperDash":false},{"StartTime":281054.0,"Position":338.1456,"HyperDash":false},{"StartTime":281133.0,"Position":314.726532,"HyperDash":false},{"StartTime":281212.0,"Position":321.195465,"HyperDash":false},{"StartTime":281291.0,"Position":328.64563,"HyperDash":false},{"StartTime":281370.0,"Position":292.093872,"HyperDash":false},{"StartTime":281449.0,"Position":310.49472,"HyperDash":false},{"StartTime":281528.0,"Position":288.733521,"HyperDash":false},{"StartTime":281606.0,"Position":290.7206,"HyperDash":false},{"StartTime":281685.0,"Position":288.1208,"HyperDash":false},{"StartTime":281764.0,"Position":272.7766,"HyperDash":false},{"StartTime":281843.0,"Position":266.504364,"HyperDash":false},{"StartTime":281922.0,"Position":241.107452,"HyperDash":false},{"StartTime":282001.0,"Position":268.358948,"HyperDash":false},{"StartTime":282080.0,"Position":230.079391,"HyperDash":false},{"StartTime":282159.0,"Position":242.0971,"HyperDash":false},{"StartTime":282238.0,"Position":243.277161,"HyperDash":false},{"StartTime":282317.0,"Position":222.536377,"HyperDash":false},{"StartTime":282396.0,"Position":223.8562,"HyperDash":false},{"StartTime":282475.0,"Position":205.2843,"HyperDash":false},{"StartTime":282554.0,"Position":197.88031,"HyperDash":false},{"StartTime":282633.0,"Position":198.803864,"HyperDash":false},{"StartTime":282712.0,"Position":166.135483,"HyperDash":false},{"StartTime":282791.0,"Position":156.019943,"HyperDash":false},{"StartTime":282870.0,"Position":155.553528,"HyperDash":false},{"StartTime":282949.0,"Position":129.81575,"HyperDash":false},{"StartTime":283028.0,"Position":128.8722,"HyperDash":false},{"StartTime":283107.0,"Position":100.768196,"HyperDash":false},{"StartTime":283176.0,"Position":72.3494644,"HyperDash":false},{"StartTime":283246.0,"Position":88.6788,"HyperDash":false},{"StartTime":283316.0,"Position":61.952446,"HyperDash":false},{"StartTime":283422.0,"Position":43.60075,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2190499.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2190499.osu new file mode 100644 index 0000000000..c0df81b7e4 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2190499.osu @@ -0,0 +1,977 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 +Mode: 0 + +[Difficulty] +HPDrainRate:4.7 +CircleSize:3.7 +OverallDifficulty:8.4 +ApproachRate:9 +SliderMultiplier:1.57 +SliderTickRate:1 + +[Events] +//Background and Video events +//Break Periods +2,78991,87033 +2,129518,133770 +2,224254,233560 +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Layer 4 (Overlay) +//Storyboard Sound Samples + +[TimingPoints] +476,315.789473684211,4,2,1,50,1,0 +1739,-212.76595744681,4,2,1,50,0,0 +17054,-212.76595744681,4,2,1,5,0,0 +17133,-212.76595744681,4,2,1,50,0,0 +17370,-212.76595744681,4,2,1,5,0,0 +17449,-212.76595744681,4,2,1,50,0,0 +17686,-212.76595744681,4,2,1,50,0,0 +17765,-212.76595744681,4,2,1,5,0,0 +17844,-212.76595744681,4,2,1,50,0,0 +18160,-103.092783505155,4,2,1,70,0,0 +27239,-212.76595744681,4,2,1,70,0,0 +27318,-103.092783505155,4,2,1,70,0,0 +27554,-212.76595744681,4,2,1,70,0,0 +27633,-103.092783505155,4,2,1,70,0,0 +28265,-114.942528735633,4,2,1,60,0,0 +38370,-129.87012987013,4,2,1,50,0,0 +47765,-129.87012987013,4,2,1,5,0,0 +47844,-129.87012987013,4,2,1,50,0,0 +48081,-129.87012987013,4,2,1,5,0,0 +48160,-129.87012987013,4,2,1,50,0,0 +48476,-103.092783505155,4,2,1,70,0,0 +68686,-114.942528735633,4,2,1,60,0,0 +78791,-129.87012987013,4,2,1,50,0,0 +79344,-129.87012987013,4,2,1,5,0,0 +79423,-129.87012987013,4,2,1,50,0,0 +81870,-129.87012987013,4,2,1,5,0,0 +81949,-129.87012987013,4,2,1,50,0,0 +82502,-129.87012987013,4,2,1,5,0,0 +82581,-129.87012987013,4,2,1,50,0,0 +87633,-114.942528735633,4,2,1,60,0,0 +87870,-114.942528735633,4,2,1,5,0,0 +87949,-114.942528735633,4,2,1,60,0,0 +88186,-114.942528735633,4,2,1,5,0,0 +88265,-114.942528735633,4,2,1,60,0,0 +88502,-114.942528735633,4,2,1,5,0,0 +88581,-114.942528735633,4,2,1,60,0,0 +88897,-93.4579439252336,4,2,1,75,0,0 +109107,-129.87012987013,4,2,1,70,0,0 +111239,-129.87012987013,4,2,1,5,0,0 +111318,-129.87012987013,4,2,1,70,0,0 +113765,-129.87012987013,4,2,1,5,0,0 +113844,-129.87012987013,4,2,1,70,0,0 +114160,-93.4579439252336,4,2,1,75,0,0 +119054,-103.092783505155,4,2,1,75,0,0 +119212,-103.092783505155,4,2,1,70,0,0 +119449,-103.092783505155,4,2,1,5,0,0 +119528,-103.092783505155,4,2,1,70,0,0 +120081,-103.092783505155,4,2,1,5,0,0 +120160,-103.092783505155,4,2,1,70,0,0 +120712,-103.092783505155,4,2,1,5,0,0 +120791,-103.092783505155,4,2,1,70,0,0 +121344,-103.092783505155,4,2,1,5,0,0 +121423,-103.092783505155,4,2,1,70,0,0 +121976,-103.092783505155,4,2,1,5,0,0 +122054,-103.092783505155,4,2,1,70,0,0 +122607,-103.092783505155,4,2,1,5,0,0 +122686,-103.092783505155,4,2,1,70,0,0 +122923,-103.092783505155,4,2,1,5,0,0 +123002,-103.092783505155,4,2,1,70,0,0 +124265,-93.4579439252336,4,2,1,75,0,0 +129318,-129.87012987013,4,2,1,50,0,0 +133502,-129.87012987013,4,2,1,5,0,0 +133581,-129.87012987013,4,2,1,50,0,0 +134370,-114.942528735633,4,2,1,70,0,0 +137133,-114.942528735633,4,2,1,5,0,0 +137212,-114.942528735633,4,2,1,70,0,0 +137449,-114.942528735633,4,2,1,5,0,0 +137528,-114.942528735633,4,2,1,70,0,0 +137765,-114.942528735633,4,2,1,5,0,0 +137844,-114.942528735633,4,2,1,70,0,0 +142502,-114.942528735633,4,2,1,5,0,0 +142581,-114.942528735633,4,2,1,70,0,0 +145976,-114.942528735633,4,2,1,5,0,0 +146054,-114.942528735633,4,2,1,70,0,0 +146291,-114.942528735633,4,2,1,5,0,0 +146370,-114.942528735633,4,2,1,70,0,0 +146607,-114.942528735633,4,2,1,5,0,0 +146686,-114.942528735633,4,2,1,70,0,0 +151344,-114.942528735633,4,2,1,5,0,0 +151423,-114.942528735633,4,2,1,70,0,0 +152054,-103.092783505155,4,2,1,70,0,0 +161133,-103.092783505155,4,2,1,5,0,0 +161212,-103.092783505155,4,2,1,70,0,0 +161449,-103.092783505155,4,2,1,5,0,0 +161528,-103.092783505155,4,2,1,70,0,0 +161765,-103.092783505155,4,2,1,5,0,0 +161844,-103.092783505155,4,2,1,70,0,0 +162160,-93.4579439252336,4,2,1,75,0,0 +182370,-129.87012987013,4,2,1,70,0,0 +184502,-129.87012987013,4,2,1,5,0,0 +184581,-129.87012987013,4,2,1,70,0,0 +187028,-129.87012987013,4,2,1,5,0,0 +187107,-129.87012987013,4,2,1,70,0,0 +187423,-93.4579439252336,4,2,1,75,0,0 +192318,-103.092783505155,4,2,1,75,0,0 +192476,-103.092783505155,4,2,1,70,0,0 +192712,-103.092783505155,4,2,1,5,0,0 +192791,-103.092783505155,4,2,1,70,0,0 +193344,-103.092783505155,4,2,1,5,0,0 +193423,-103.092783505155,4,2,1,70,0,0 +193976,-103.092783505155,4,2,1,5,0,0 +194054,-103.092783505155,4,2,1,70,0,0 +194607,-103.092783505155,4,2,1,5,0,0 +194686,-103.092783505155,4,2,1,70,0,0 +195239,-103.092783505155,4,2,1,5,0,0 +195318,-103.092783505155,4,2,1,70,0,0 +195870,-103.092783505155,4,2,1,5,0,0 +195949,-103.092783505155,4,2,1,70,0,0 +196186,-103.092783505155,4,2,1,5,0,0 +196265,-103.092783505155,4,2,1,70,0,0 +197528,-93.4579439252336,4,2,1,75,0,0 +202581,-129.87012987013,4,2,1,70,0,0 +203844,-103.092783505155,4,2,1,70,0,0 +224054,-129.87012987013,4,2,1,60,0,0 +235423,-93.4579439252336,4,2,1,75,0,0 +255633,-129.87012987013,4,2,1,60,0,0 +260686,-93.4579439252336,4,2,1,75,0,0 +265581,-103.092783505155,4,2,1,75,0,0 +265739,-103.092783505155,4,2,1,70,0,0 +265976,-103.092783505155,4,2,1,5,0,0 +266054,-103.092783505155,4,2,1,70,0,0 +266607,-103.092783505155,4,2,1,5,0,0 +266686,-103.092783505155,4,2,1,70,0,0 +267239,-103.092783505155,4,2,1,5,0,0 +267318,-103.092783505155,4,2,1,70,0,0 +267870,-103.092783505155,4,2,1,5,0,0 +267949,-103.092783505155,4,2,1,70,0,0 +268502,-103.092783505155,4,2,1,5,0,0 +268581,-103.092783505155,4,2,1,70,0,0 +269133,-103.092783505155,4,2,1,5,0,0 +269212,-103.092783505155,4,2,1,70,0,0 +269449,-103.092783505155,4,2,1,5,0,0 +269528,-103.092783505155,4,2,1,70,0,0 +270791,-93.4579439252336,4,2,1,75,0,0 +275844,-129.87012987013,4,2,1,60,0,0 +278370,-93.4579439252336,4,2,1,75,0,0 +278607,-93.4579439252336,4,2,1,5,0,0 +278686,-93.4579439252336,4,2,1,75,0,0 +278923,-93.4579439252336,4,2,1,5,0,0 +279002,-78.7401574803149,4,2,1,75,0,0 +279239,-78.7401574803149,4,2,1,5,0,0 +279318,-78.7401574803149,4,2,1,75,0,0 +279554,-78.7401574803149,4,2,1,5,0,0 +279633,-129.87012987013,4,2,1,70,0,0 +280265,-270.270270270271,4,2,1,70,0,0 +283423,-270.270270270271,4,2,1,10,0,0 + +[HitObjects] +367,158,1739,6,0,B|277:179|338:219|236:236,1,147.579997748108,2|2,0:0|0:0,0:0:0:0: +161,20,3002,6,0,P|188:41|234:156,1,147.579997748108,2|2,0:0|0:0,0:0:0:0: +47,263,4265,6,0,P|91:234|115:230,1,73.789998874054,2|2,0:0|0:0,0:0:0:0: +235,344,4897,2,0,P|299:349|342:311,1,110.684998311081,2|2,0:0|0:0,0:0:0:0: +372,233,5528,2,0,P|351:171|339:79,1,147.579997748108,2|0,0:0|0:0,0:0:0:0: +55,109,6791,6,0,P|89:141|126:149,1,73.789998874054,2|2,0:0|0:0,0:0:0:0: +240,23,7423,2,0,P|203:58|189:121,1,110.684998311081,2|2,0:0|0:0,0:0:0:0: +273,203,8054,2,0,P|300:186|348:175,2,73.789998874054,2|2|2,0:0|0:0|0:0,0:0:0:0: +147,324,9002,2,0,P|124:323|97:314,1,36.894999437027,2|2,0:0|0:0,0:0:0:0: +59,247,9318,6,0,P|51:213|39:175,2,73.789998874054,2|2|2,0:0|0:0|0:0,0:0:0:0: +133,53,10265,1,2,0:0:0:0: +256,192,10581,12,0,11844,0:0:0:0: +256,192,13107,12,0,14370,0:0:0:0: +74,66,15633,6,0,B|151:62|120:116|198:112,1,138.356247888851,2|2,0:0|0:0,0:0:0:0: +189,105,17844,5,4,0:0:0:0: +189,105,18160,6,0,P|222:130|274:136,1,76.1450018009148,6|2,1:2|0:0,0:0:0:0: +402,27,18476,2,0,P|365:36|335:59,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +383,259,18791,2,0,P|400:173|404:106,1,152.29000360183,2|10,1:2|0:2,0:0:0:0: +254,55,19265,1,0,0:0:0:0: +178,227,19423,6,0,P|140:242|92:242,1,76.1450018009148,2|0,1:2|0:0,0:0:0:0: +245,84,19739,2,0,P|282:86|317:100,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +287,315,20054,2,0,P|270:229|266:162,1,152.29000360183,2|8,1:2|0:2,0:0:0:0: +167,252,20528,1,0,0:0:0:0: +110,91,20686,6,0,P|77:65|24:58,1,76.1450018009148,6|0,1:2|0:0,0:0:0:0: +158,225,21002,2,0,P|194:214|223:190,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +105,73,21318,2,0,P|72:47|19:40,1,76.1450018009148,2|0,1:2|0:0,0:0:0:0: +153,207,21634,2,0,P|189:196|218:172,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +321,19,21949,5,6,1:2:0:0: +372,198,22107,1,2,0:0:0:0: +345,14,22265,2,0,P|334:50|326:104,1,76.1450018009148,10|2,0:2|0:0,0:0:0:0: +413,295,22581,1,6,1:2:0:0: +442,141,22739,1,10,0:2:0:0: +409,316,22897,2,0,P|370:337|316:337,1,76.1450018009148,10|2,0:2|0:0,0:0:0:0: +205,239,23212,6,0,P|219:282|226:330,1,76.1450018009148,6|0,1:2|0:0,0:0:0:0: +73,189,23528,2,0,P|59:232|52:280,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +240,312,23844,2,0,P|233:275|221:239,1,76.1450018009148,2|0,1:2|0:0,0:0:0:0: +88,189,24160,2,0,P|76:225|69:262,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +206,54,24476,6,0,L|301:45,1,76.1450018009148,6|0,1:2|0:0,0:0:0:0: +425,174,24791,2,0,L|330:165,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +196,41,25107,2,0,L|291:32,1,76.1450018009148,2|0,1:2|0:0,0:0:0:0: +415,161,25423,1,10,0:2:0:0: +363,43,25581,1,0,0:0:0:0: +263,180,25739,6,0,P|272:216|279:261,1,76.1450018009148,6|0,1:2|0:0,0:0:0:0: +418,374,26054,2,0,P|424:336|433:299,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +251,184,26370,2,0,P|260:220|267:265,1,76.1450018009148,6|0,1:2|0:0,0:0:0:0: +406,378,26686,2,0,P|412:340|421:303,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +326,119,27002,6,0,P|266:96|196:111,1,114.217502701372,14|0,0:2|0:0,0:0:0:0: +215,85,27318,2,0,P|271:80|323:102,1,114.217502701372,8|0,0:2|0:0,0:0:0:0: +324,89,27633,2,0,P|250:68|174:92,1,152.29000360183,12|4,0:2|0:2,0:0:0:0: +65,343,28265,6,0,B|57:248|105:312|97:183,1,136.590001146309,6|8,1:2|0:2,0:0:0:0: +153,332,28739,1,2,1:2:0:0: +153,332,28897,1,2,0:0:0:0: +215,226,29054,2,0,P|247:210|288:209,1,68.2950005731545,2|8,1:2|0:2,0:0:0:0: +332,322,29370,2,0,P|298:319|267:303,1,68.2950005731545,2|0,0:0|1:2,0:0:0:0: +371,217,29686,1,2,0:0:0:0: +371,217,29844,1,10,0:2:0:0: +444,302,30002,1,2,1:2:0:0: +444,302,30160,2,0,P|460:262|462:211,1,68.2950005731545,2|0,0:0|1:2,0:0:0:0: +393,130,30476,2,0,P|377:90|375:39,1,68.2950005731545,10|0,0:2|0:0,0:0:0:0: +265,134,30791,6,0,L|169:122,1,68.2950005731545,2|0,1:2|0:0,0:0:0:0: +80,53,31107,2,0,L|147:44,1,68.2950005731545,10|0,0:2|1:2,0:0:0:0: +124,189,31423,2,0,L|57:181,1,68.2950005731545,2|0,0:0|1:2,0:0:0:0: +164,296,31739,1,10,0:2:0:0: +164,296,31897,2,0,L|231:287,1,68.2950005731545,2|0,0:0|1:2,0:0:0:0: +365,211,32212,1,2,0:0:0:0: +365,211,32370,2,0,P|379:246|384:289,1,68.2950005731545,10|0,0:2|1:2,0:0:0:0: +488,162,32686,2,0,P|472:228|468:310,1,136.590001146309,2|8,0:0|0:2,0:0:0:0: +406,132,33160,1,0,1:2:0:0: +277,224,33318,6,0,B|197:212|245:168|149:160,1,136.590001146309,6|8,1:2|0:2,0:0:0:0: +283,146,33791,1,2,1:2:0:0: +283,146,33949,1,2,0:0:0:0: +158,238,34107,2,0,P|123:253|68:253,1,68.2950005731545,2|8,1:2|0:2,0:0:0:0: +19,126,34423,2,0,P|52:130|83:144,1,68.2950005731545,2|0,1:2|1:2,0:0:0:0: +158,238,34739,1,2,0:0:0:0: +158,238,34897,1,10,0:2:0:0: +204,124,35054,1,2,1:2:0:0: +204,124,35212,2,0,P|213:84|217:31,1,68.2950005731545,2|2,0:0|1:2,0:0:0:0: +345,175,35528,2,0,P|336:141|332:108,1,68.2950005731545,10|0,0:2|1:2,0:0:0:0: +461,237,35844,6,0,P|424:218|324:207,2,136.590001146309,2|10|2,1:2|0:2|1:2,0:0:0:0: +248,360,36791,1,10,0:2:0:0: +248,360,36949,2,0,P|259:318|261:281,1,68.2950005731545,2|8,0:0|0:2,0:0:0:0: +189,145,37265,5,2,1:2:0:0: +130,295,37423,2,0,P|96:312|48:311,1,68.2950005731545,10|0,0:2|1:2,0:0:0:0: +32,119,37739,5,10,0:2:0:0: +79,229,37897,1,0,1:2:0:0: +126,47,38054,5,12,0:2:0:0: +67,202,38212,1,0,1:2:0:0: +189,145,38370,6,0,P|236:139|304:205,1,120.889997601975,4|2,1:2|0:0,0:0:0:0: +281,297,38844,2,0,P|256:311|215:316,2,60.4449988009873,2|2|2,0:0|0:0|0:0,0:0:0:0: +367,240,39318,2,0,P|396:245|423:259,1,60.4449988009873,2|2,0:0|0:0,0:0:0:0: +493,325,39633,1,2,0:0:0:0: +493,325,39791,2,0,L|500:262,1,60.4449988009873,2|2,1:2|0:0,0:0:0:0: +450,183,40107,2,0,L|443:120,1,60.4449988009873,2|2,0:0|0:0,0:0:0:0: +379,41,40423,1,2,1:2:0:0: +379,41,40581,1,2,0:0:0:0: +312,120,40739,6,0,B|229:114|279:80|188:72,1,120.889997601975,2|2,0:0|0:0,0:0:0:0: +120,125,41212,2,0,P|107:98|107:68,2,60.4449988009873,2|2|2,0:0|0:0|0:0,0:0:0:0: +195,158,41686,2,0,P|195:187|182:215,1,60.4449988009873,2|2,0:0|0:0,0:0:0:0: +81,267,42002,1,2,0:0:0:0: +81,267,42160,1,2,0:0:0:0: +157,335,42318,1,2,1:2:0:0: +157,335,42476,2,0,L|233:329,1,60.4449988009873,2|0,0:0|0:0,0:0:0:0: +314,250,42791,2,0,L|374:254,1,60.4449988009873,2|2,1:2|0:0,0:0:0:0: +224,343,43107,6,0,L|92:351,1,120.889997601975,2|0,0:0|0:0,0:0:0:0: +18,308,43581,2,0,L|26:248,2,60.4449988009873,2|2|2,1:2|0:0|0:0,0:0:0:0: +118,245,44054,2,0,L|109:185,1,60.4449988009873,2|2,0:0|0:0,0:0:0:0: +32,119,44370,1,2,0:0:0:0: +32,119,44528,2,0,L|39:56,1,60.4449988009873,2|2,0:0|0:0,0:0:0:0: +131,30,44844,1,2,1:2:0:0: +131,30,45002,2,0,L|124:90,1,60.4449988009873,2|2,0:0|0:0,0:0:0:0: +215,147,45318,1,2,0:0:0:0: +215,147,45476,2,0,L|289:140,1,60.4449988009873,2|2,1:2|0:0,0:0:0:0: +362,98,45791,5,2,0:0:0:0: +362,98,45949,1,2,1:2:0:0: +350,203,46107,2,0,L|356:278,1,60.4449988009873,2|0,0:0|0:0,0:0:0:0: +421,352,46423,1,2,0:0:0:0: +421,352,46581,1,2,1:2:0:0: +343,276,46739,2,0,L|268:282,1,60.4449988009873,2|0,0:0|0:0,0:0:0:0: +212,353,47054,5,2,0:0:0:0: +176,245,47212,1,2,1:2:0:0: +104,346,47370,1,2,0:0:0:0: +104,346,47449,1,2,0:0:0:0: +104,346,47528,2,0,P|96:290|81:231,1,90.6674982014809,2|0,1:2|0:0,0:0:0:0: +73,246,47844,2,0,P|81:190|96:131,1,90.6674982014809,2|0,1:2|0:0,0:0:0:0: +108,144,48160,1,4,0:2:0:0: +108,144,48476,6,0,P|146:167|197:167,1,76.1450018009148,6|0,1:2|0:0,0:0:0:0: +259,24,48791,2,0,P|221:29|190:50,1,76.1450018009148,10|0,0:2|1:2,0:0:0:0: +329,179,49107,2,0,B|429:161|369:117|469:97,1,152.29000360183,2|8,0:0|0:2,0:0:0:0: +328,96,49581,1,0,0:0:0:0: +472,190,49739,6,0,P|462:222|454:274,1,76.1450018009148,6|0,1:2|0:0,0:0:0:0: +324,372,50054,2,0,P|317:334|306:298,1,76.1450018009148,10|0,0:2|1:2,0:0:0:0: +190,174,50370,2,0,P|128:184|85:268,1,152.29000360183,2|8,0:0|0:2,0:0:0:0: +206,294,50844,1,0,0:0:0:0: +313,170,51002,6,0,P|323:125|328:78,1,76.1450018009148,6|0,1:2|0:0,0:0:0:0: +223,271,51318,2,0,P|212:226|208:179,1,76.1450018009148,10|0,0:2|1:2,0:0:0:0: +268,40,51633,2,0,P|302:19|358:19,1,76.1450018009148,2|0,0:0|1:2,0:0:0:0: +382,195,51949,2,0,P|344:189|312:169,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +191,14,52265,6,0,B|176:109|235:65|217:167,1,152.29000360183,6|10,1:2|0:2,0:0:0:0: +145,291,52739,1,0,1:2:0:0: +75,165,52897,2,0,P|106:144|152:135,1,76.1450018009148,2|0,0:0|1:2,0:0:0:0: +223,271,53212,2,0,P|254:292|291:300,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +423,166,53528,5,2,1:2:0:0: +383,316,53686,2,0,P|364:275|364:218,1,76.1450018009148,2|8,0:0|0:2,0:0:0:0: +445,94,54002,2,0,P|439:131|422:165,1,76.1450018009148,2|0,1:2|0:0,0:0:0:0: +346,37,54318,1,2,1:2:0:0: +268,179,54476,2,0,P|230:173|196:156,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +79,28,54791,6,0,P|101:82|110:184,1,152.29000360183,2|10,1:2|0:2,0:0:0:0: +38,334,55265,2,0,P|44:293|61:244,1,76.1450018009148,2|0,1:2|0:0,0:0:0:0: +189,362,55581,1,0,1:2:0:0: +125,198,55739,2,0,P|135:234|141:272,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +279,380,56054,6,0,P|329:379|372:344,1,76.1450018009148,2|0,1:2|0:0,0:0:0:0: +470,222,56370,2,0,P|432:219|397:234,1,76.1450018009148,10|0,0:2|1:2,0:0:0:0: +438,384,56686,2,0,P|444:338|446:293,1,76.1450018009148,2|0,0:0|1:2,0:0:0:0: +287,222,57002,2,0,P|289:259|294:297,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +334,124,57318,6,0,P|311:115|285:110,3,38.0725009004574,6|2|2|2,1:2|0:0|0:0|0:0,0:0:0:0: +230,148,57633,2,0,P|201:173|146:180,1,76.1450018009148,6|2,1:2|0:0,0:0:0:0: +42,81,57949,2,0,P|56:112|68:176,1,76.1450018009148,6|0,1:2|0:0,0:0:0:0: +188,17,58265,2,0,P|174:48|162:112,1,76.1450018009148,14|0,0:2|0:0,0:0:0:0: +230,245,58581,6,0,P|265:266|320:270,1,76.1450018009148,6|0,1:2|0:0,0:0:0:0: +146,162,58897,2,0,P|108:169|76:189,1,76.1450018009148,10|0,0:2|1:2,0:0:0:0: +293,188,59212,2,0,P|315:102|318:24,1,152.29000360183,2|8,0:2|0:2,0:0:0:0: +224,147,59686,1,0,0:0:0:0: +405,82,59844,6,0,P|407:124|415:170,1,76.1450018009148,2|0,1:2|0:0,0:0:0:0: +500,268,60160,2,0,P|467:249|410:247,1,76.1450018009148,10|0,0:2|1:2,0:0:0:0: +303,384,60476,2,0,B|401:376|349:337|442:328,1,152.29000360183,2|8,0:0|0:2,0:0:0:0: +311,298,60949,1,0,0:0:0:0: +143,368,61107,6,0,P|155:325|155:273,1,76.1450018009148,2|0,1:2|0:0,0:0:0:0: +63,156,61423,2,0,P|65:193|76:230,1,76.1450018009148,10|0,0:2|1:2,0:0:0:0: +160,367,61739,2,0,P|172:324|172:272,1,76.1450018009148,2|0,0:0|1:2,0:0:0:0: +80,155,62055,2,0,P|82:192|93:229,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +184,86,62370,6,0,B|260:109|205:146|318:171,1,152.29000360183,2|10,1:2|0:2,0:0:0:0: +406,65,62844,1,0,1:2:0:0: +473,202,63002,2,0,P|462:240|454:292,1,76.1450018009148,2|0,0:0|1:2,0:0:0:0: +331,146,63318,2,0,P|341:184|349:236,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +234,347,63633,1,2,1:2:0:0: +160,216,63791,6,0,P|202:198|234:200,1,76.1450018009148,2|8,0:0|0:2,0:0:0:0: +147,367,64107,2,0,P|109:366|75:350,1,76.1450018009148,2|0,1:2|0:0,0:0:0:0: +35,213,64423,1,2,1:2:0:0: +148,349,64581,2,0,P|110:348|76:332,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +18,190,64897,5,2,1:2:0:0: +133,269,65054,2,0,P|143:231|150:180,1,76.1450018009148,2|8,0:0|0:2,0:0:0:0: +224,55,65370,2,0,P|231:127|249:214,1,152.29000360183,2|0,1:2|1:2,0:0:0:0: +367,345,65844,2,0,P|405:365|463:364,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +456,181,66160,6,0,P|439:219|428:272,1,76.1450018009148,2|0,1:2|0:0,0:0:0:0: +310,127,66476,2,0,P|327:165|338:218,1,76.1450018009148,10|0,0:2|1:2,0:0:0:0: +452,31,66791,2,0,P|435:69|424:122,1,76.1450018009148,2|0,0:0|1:2,0:0:0:0: +250,41,67107,2,0,P|267:79|278:132,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +143,235,67423,6,0,L|54:241,1,76.1450018009148,6|0,1:2|0:0,0:0:0:0: +8,75,67739,2,0,L|97:81,1,76.1450018009148,4|0,1:2|0:0,0:0:0:0: +153,254,68054,2,0,L|-30:266,1,152.29000360183,4|8,1:2|0:2,0:0:0:0: +162,272,68686,6,0,P|153:306|149:343,2,68.2950005731545,6|2|10,1:2|0:0|0:2,0:0:0:0: +264,197,69160,1,2,1:2:0:0: +264,197,69318,2,0,B|339:217|287:248|378:266,1,136.590001146309,2|10,1:2|0:2,0:0:0:0: +477,162,69791,2,0,P|462:186|451:227,1,68.2950005731545,2|0,0:0|1:2,0:0:0:0: +352,127,70107,1,2,1:2:0:0: +352,127,70265,2,0,P|369:156|377:189,1,68.2950005731545,10|0,0:2|1:2,0:0:0:0: +252,75,70581,2,0,B|176:96|234:131|127:146,1,136.590001146309,2|8,1:2|0:2,0:0:0:0: +139,143,71212,6,0,P|125:177|114:231,1,68.2950005731545,2|0,1:2|0:0,0:0:0:0: +197,312,71528,1,10,0:2:0:0: +197,312,71686,1,2,1:2:0:0: +246,212,71844,2,0,P|281:197|322:197,1,68.2950005731545,2|2,1:2|0:0,0:0:0:0: +382,297,72160,1,10,0:2:0:0: +382,297,72318,2,0,P|395:222|417:157,1,136.590001146309,2|0,0:0|1:2,0:0:0:0: +483,40,72791,2,0,P|454:60|408:66,1,68.2950005731545,10|0,0:2|1:2,0:0:0:0: +316,8,73107,1,2,1:2:0:0: +316,8,73265,1,8,0:2:0:0: +213,106,73423,2,0,P|240:125|273:132,1,68.2950005731545,8|0,0:2|0:0,0:0:0:0: +151,36,73739,6,0,P|176:103|187:195,1,136.590001146309,2|10,1:2|0:2,0:0:0:0: +71,297,74212,1,2,1:2:0:0: +71,297,74370,2,0,P|96:230|107:138,1,136.590001146309,2|8,1:2|0:2,0:0:0:0: +217,308,74844,2,0,P|205:264|205:212,1,68.2950005731545,2|0,0:0|1:2,0:0:0:0: +292,129,75160,2,0,P|321:113|364:113,1,68.2950005731545,2|8,1:2|0:2,0:0:0:0: +470,226,75476,1,2,1:2:0:0: +470,226,75633,2,0,P|407:200|322:187,1,136.590001146309,2|10,1:2|0:2,0:0:0:0: +339,187,76265,6,0,P|351:221|357:255,1,68.2950005731545,2|2,1:2|0:0,0:0:0:0: +274,344,76581,1,10,0:2:0:0: +274,344,76739,1,2,1:2:0:0: +196,237,76897,2,0,P|183:277|174:332,1,68.2950005731545,2|0,1:2|0:0,0:0:0:0: +76,200,77212,2,0,P|89:240|98:295,1,68.2950005731545,10|0,0:2|0:0,0:0:0:0: +193,110,77528,6,0,P|225:91|266:91,1,68.2950005731545,2|2,1:2|0:0,0:0:0:0: +363,209,77844,2,0,P|329:205|300:187,1,68.2950005731545,2|2,1:2|0:0,0:0:0:0: +424,69,78160,2,0,P|392:129|373:223,1,136.590001146309,2|10,1:2|0:2,0:0:0:0: +375,195,78791,5,6,0:0:0:0: +59,101,87633,6,0,P|100:79|160:79,1,102.442500859732,2|0,1:2|0:0,0:0:0:0: +157,92,87949,2,0,P|106:92|61:115,1,102.442500859732,2|0,1:2|0:0,0:0:0:0: +65,127,88265,2,0,P|110:103|160:103,1,102.442500859732,2|0,1:2|0:0,0:0:0:0: +162,116,88581,1,6,0:2:0:0: +410,340,88897,6,0,P|428:292|428:236,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +329,109,89212,1,10,0:2:0:0: +237,283,89370,2,0,P|219:235|219:179,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +412,90,89686,2,0,P|407:131|391:170,1,83.9949974366761,2|8,0:2|0:2,0:0:0:0: +224,11,90002,6,0,P|132:31|99:124,1,167.989994873352,2|2,0:2|0:2,0:0:0:0: +198,242,90476,1,8,0:2:0:0: +197,90,90633,1,2,0:2:0:0: +85,257,90791,2,0,P|94:304|99:355,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +308,229,91107,2,0,P|311:187|320:146,1,83.9949974366761,14|0,0:2|0:0,0:0:0:0: +210,341,91423,6,0,P|251:326|325:317,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +196,202,91739,1,10,0:2:0:0: +305,335,91897,2,0,P|346:350|420:359,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +212,222,92212,2,0,P|253:207|327:198,1,83.9949974366761,2|8,0:2|0:2,0:0:0:0: +446,275,92528,6,0,P|480:177|483:88,1,167.989994873352,2|2,0:2|0:2,0:0:0:0: +286,70,93002,1,8,0:2:0:0: +368,232,93160,1,2,0:2:0:0: +268,50,93318,2,0,P|230:33|158:30,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +349,208,93633,2,0,P|310:225|269:230,1,83.9949974366761,14|0,0:2|0:0,0:0:0:0: +138,89,93949,6,0,P|116:133|104:208,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +148,304,94265,1,10,0:2:0:0: +22,167,94423,2,0,P|44:211|56:286,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +243,347,94739,2,0,P|254:306|273:269,1,83.9949974366761,2|8,0:2|0:2,0:0:0:0: +438,109,95054,6,0,B|340:127|418:167|266:192,1,167.989994873352,6|0,0:2|0:0,0:0:0:0: +254,24,95528,2,0,P|277:62|282:122,1,83.9949974366761,10|0,0:2|0:0,0:0:0:0: +427,285,95844,2,0,P|428:243|443:204,1,83.9949974366761,2|0,1:2|0:0,0:0:0:0: +279,25,96160,2,0,P|302:63|307:123,1,83.9949974366761,10|0,0:2|0:0,0:0:0:0: +225,237,96476,6,0,P|184:225|105:216,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +288,132,96791,1,10,0:2:0:0: +180,316,96949,2,0,P|139:328|60:337,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +274,159,97265,2,0,P|315:166|355:177,1,83.9949974366761,2|8,0:2|0:2,0:0:0:0: +417,302,97581,1,2,0:2:0:0: +420,94,97739,6,0,P|393:134|376:202,1,83.9949974366761,2|0,1:2|0:0,0:0:0:0: +346,384,98054,1,10,0:2:0:0: +299,208,98212,1,0,0:0:0:0: +337,355,98370,1,2,1:2:0:0: +290,179,98528,1,0,0:0:0:0: +170,364,98686,2,0,P|124:378|65:374,1,83.9949974366761,10|0,0:2|0:0,0:0:0:0: +45,139,99002,6,0,P|70:172|96:263,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +164,51,99318,1,10,0:2:0:0: +146,275,99476,2,0,P|106:294|39:288,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +163,76,99791,2,0,P|204:78|242:96,1,83.9949974366761,2|8,0:2|0:2,0:0:0:0: +306,272,100107,6,0,P|261:187|261:103,1,167.989994873352,6|2,0:2|0:2,0:0:0:0: +446,105,100581,1,8,0:2:0:0: +376,319,100739,2,0,P|345:348|305:358,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +236,147,101054,1,0,0:0:0:0: +402,242,101212,2,0,P|443:245|481:228,1,83.9949974366761,8|0,0:2|0:0,0:0:0:0: +334,39,101528,6,0,P|346:82|350:135,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +219,239,101844,1,10,0:2:0:0: +177,71,102002,2,0,P|137:51|71:45,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +140,267,102318,2,0,P|181:258|218:239,1,83.9949974366761,2|8,0:2|0:2,0:0:0:0: +22,135,102633,6,0,P|64:254|68:317,1,167.989994873352,6|2,0:2|0:2,0:0:0:0: +182,139,103107,1,8,0:2:0:0: +200,320,103265,2,0,P|209:272|222:225,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +337,118,103581,1,0,0:0:0:0: +331,305,103739,2,0,P|322:257|309:210,1,83.9949974366761,8|0,0:2|0:0,0:0:0:0: +194,51,104054,6,0,B|300:74|225:123|355:155,1,167.989994873352,6|10,1:2|0:2,0:0:0:0: +142,226,104528,2,0,P|91:244|21:238,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +187,83,104844,2,0,P|148:67|106:63,1,83.9949974366761,2|8,0:2|0:2,0:0:0:0: +210,283,105160,6,0,P|229:235|232:181,1,83.9949974366761,6|0,0:2|1:2,0:0:0:0: +339,35,105476,2,0,P|345:76|362:115,1,83.9949974366761,2|8,0:2|0:2,0:0:0:0: +309,282,105791,1,2,0:2:0:0: +454,125,105949,2,0,P|437:163|431:204,1,83.9949974366761,2|0,1:2|0:0,0:0:0:0: +246,91,106265,2,0,P|262:129|268:170,1,83.9949974366761,10|0,0:2|0:0,0:0:0:0: +133,354,106581,6,0,L|22:361,1,83.9949974366761,6|2,1:2|0:2,0:0:0:0: +260,193,106897,2,0,L|371:200,1,83.9949974366761,10|2,0:2|0:2,0:0:0:0: +127,339,107212,2,0,L|16:346,1,83.9949974366761,2|2,1:2|0:2,0:0:0:0: +254,178,107528,2,0,L|365:185,1,83.9949974366761,10|2,0:2|0:2,0:0:0:0: +479,344,107844,5,2,1:2:0:0: +411,172,108002,1,10,0:2:0:0: +400,363,108160,1,2,1:2:0:0: +488,188,108318,1,2,0:2:0:0: +319,384,108476,2,0,L|312:273,1,83.9949974366761,10|10,0:2|0:2,0:0:0:0: +298,87,108791,1,2,1:2:0:0: +220,275,108949,1,10,0:2:0:0: +163,74,109107,5,14,0:2:0:0: +160,0,110212,5,14,0:2:0:0: +160,0,111002,6,0,P|188:57|194:109,1,90.6674982014809,14|0,0:2|0:0,0:0:0:0: +214,98,111318,2,0,P|191:137|182:176,1,60.4449988009873,14|0,0:2|1:2,0:0:0:0: +202,188,111554,1,0,1:2:0:0: +202,188,111633,1,6,1:2:0:0: +197,204,112739,5,14,0:2:0:0: +197,204,113528,2,0,P|242:224|311:221,1,90.6674982014809,14|0,0:0|0:0,0:0:0:0: +293,200,113844,2,0,P|333:181|366:180,1,60.4449988009873,14|0,0:0|1:0,0:0:0:0: +413,235,114081,5,0,1:0:0:0: +413,235,114160,2,0,P|420:193|433:153,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +328,286,114476,1,10,0:2:0:0: +388,95,114633,2,0,P|381:53|368:13,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +218,171,114949,2,0,P|225:129|238:89,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +114,267,115265,6,0,P|99:177|71:93,1,167.989994873352,6|0,0:0|0:0,0:0:0:0: +206,327,115739,2,0,P|174:359|99:370,1,83.9949974366761,10|0,0:2|0:0,0:0:0:0: +247,175,116054,2,0,P|285:190|314:220,1,83.9949974366761,2|0,1:2|0:0,0:0:0:0: +406,380,116370,2,0,P|411:328|422:274,1,83.9949974366761,8|0,0:0|0:0,0:0:0:0: +477,101,116686,6,0,P|432:104|382:131,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +286,270,117002,1,10,0:2:0:0: +210,82,117160,2,0,P|251:84|289:101,1,83.9949974366761,2|0,0:0|1:2,0:0:0:0: +205,284,117476,2,0,P|220:236|227:166,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +80,62,117791,6,0,P|113:131|123:259,1,167.989994873352,2|2,0:0|0:0,0:0:0:0: +279,362,118265,5,10,0:2:0:0: +243,170,118423,1,2,0:0:0:0: +306,359,118581,5,2,1:2:0:0: +325,169,118739,1,2,0:0:0:0: +330,355,118897,5,8,0:2:0:0: +402,171,119054,1,10,0:2:0:0: +402,171,119528,6,0,B|239:156|377:58|170:31,1,266.507506303202,12|0,0:2|0:0,0:0:0:0: +184,44,120160,2,0,B|357:69|233:164|392:180,1,266.507506303202,12|0,0:0|0:0,0:0:0:0: +385,190,120791,2,0,B|227:174|351:79|178:54,1,266.507506303202,12|0,0:0|0:0,0:0:0:0: +171,64,121423,2,0,B|344:89|220:184|378:200,1,266.507506303202,12|0,0:0|0:0,0:0:0:0: +373,211,122054,2,0,B|214:194|338:99|165:74,1,266.507506303202,12|0,0:0|0:0,0:0:0:0: +156,90,122686,2,0,P|127:134|109:220,1,114.217502701372,12|0,0:0|0:0,0:0:0:0: +129,218,123002,6,0,P|144:261|158:324,1,76.1450018009148,0|10,1:2|0:2,0:0:0:0: +247,142,123318,1,8,0:2:0:0: +278,283,123475,1,2,1:2:0:0: +339,100,123633,1,8,0:2:0:0: +272,251,123791,1,8,0:2:0:0: +224,58,123949,1,8,0:2:0:0: +286,225,124107,1,8,0:2:0:0: +374,24,124265,6,0,P|414:9|473:9,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +368,190,124581,1,10,0:2:0:0: +222,28,124739,2,0,P|182:13|123:13,1,83.9949974366761,2|0,0:0|1:2,0:0:0:0: +62,237,125054,2,0,P|82:187|90:129,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +261,271,125370,2,0,P|241:221|233:163,1,83.9949974366761,2|0,0:0|1:2,0:0:0:0: +86,328,125686,2,0,P|37:328|-12:298,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +164,160,126002,1,2,0:0:0:0: +235,355,126160,2,0,P|276:356|315:341,1,83.9949974366761,2|0,1:2|0:0,0:0:0:0: +454,180,126476,2,0,P|415:164|373:166,1,83.9949974366761,10|0,0:2|0:0,0:0:0:0: +407,347,126791,6,0,L|399:240,1,83.9949974366761,6|2,1:2|0:2,0:0:0:0: +274,71,127107,2,0,L|267:154,1,83.9949974366761,14|2,0:2|0:0,0:0:0:0: +421,337,127423,2,0,L|413:230,1,83.9949974366761,6|2,1:2|0:0,0:0:0:0: +288,61,127739,2,0,L|281:144,1,83.9949974366761,14|2,0:2|0:0,0:0:0:0: +247,369,128054,5,2,1:2:0:0: +212,184,128212,1,10,0:2:0:0: +251,384,128370,1,10,0:2:0:0: +216,204,128528,1,2,0:0:0:0: +81,380,128686,2,0,L|87:296,1,83.9949974366761,10|10,0:2|0:2,0:0:0:0: +100,65,129002,1,2,1:2:0:0: +163,261,129160,1,10,0:2:0:0: +91,165,129318,5,4,0:2:0:0: +300,51,134370,5,6,1:2:0:0: +300,51,135633,5,4,1:2:0:0: +300,51,136897,6,0,P|260:72|200:81,1,102.442500859732,4|2,1:2|0:0,0:0:0:0: +200,72,137212,2,0,P|250:64|296:41,1,102.442500859732,4|0,1:2|0:0,0:0:0:0: +293,33,137528,2,0,P|247:55|196:63,1,102.442500859732,6|2,1:2|0:0,0:0:0:0: +193,44,137844,1,12,0:2:0:0: +337,298,138160,6,0,P|355:259|359:217,1,68.2950005731545,6|0,1:2|0:0,0:0:0:0: +277,157,138476,1,10,0:2:0:0: +355,302,138633,2,0,P|379:215|380:139,1,136.590001146309,2|2,1:2|1:2,0:0:0:0: +276,58,139107,1,10,0:2:0:0: +276,58,139265,1,2,1:2:0:0: +209,217,139423,6,0,P|170:235|128:239,1,68.2950005731545,6|0,1:2|0:0,0:0:0:0: +68,157,139739,1,10,0:2:0:0: +213,235,139896,2,0,P|126:259|50:260,1,136.590001146309,2|2,1:2|1:2,0:0:0:0: +207,118,140370,1,10,0:2:0:0: +207,118,140528,1,2,1:2:0:0: +308,272,140686,6,0,P|299:306|295:361,1,68.2950005731545,6|0,1:2|0:0,0:0:0:0: +421,220,141002,1,10,0:2:0:0: +293,252,141160,2,0,P|273:317|262:384,1,136.590001146309,2|2,1:2|1:2,0:0:0:0: +392,137,141633,1,10,0:2:0:0: +392,137,141791,1,2,1:2:0:0: +392,137,142265,6,0,P|384:93|322:62,1,102.442500859732,6|0,1:2|0:0,0:0:0:0: +326,79,142581,2,0,P|281:103|200:75,1,136.590001146309,6|12,1:2|0:2,0:0:0:0: +203,78,143212,5,6,1:2:0:0: +214,89,144476,5,4,1:2:0:0: +214,90,145739,6,0,P|249:146|260:207,1,102.442500859732,6|0,1:2|0:0,0:0:0:0: +248,201,146054,2,0,P|213:257|202:318,1,102.442500859732,6|0,1:2|0:0,0:0:0:0: +218,313,146370,2,0,P|265:294|316:291,1,102.442500859732,6|0,1:2|0:0,0:0:0:0: +326,305,146686,1,14,0:2:0:0: +440,83,147002,6,0,L|430:167,1,68.2950005731545,6|0,1:2|0:0,0:0:0:0: +346,18,147318,1,10,0:2:0:0: +457,94,147476,2,0,L|440:231,1,136.590001146309,2|2,1:2|1:2,0:0:0:0: +326,305,147949,1,10,0:2:0:0: +326,305,148107,1,2,1:2:0:0: +170,162,148265,6,0,L|180:246,1,68.2950005731545,6|0,1:2|0:0,0:0:0:0: +264,97,148581,1,10,0:2:0:0: +153,173,148739,2,0,L|170:310,1,136.590001146309,2|2,1:2|1:2,0:0:0:0: +284,384,149212,1,10,0:2:0:0: +284,384,149370,1,2,1:2:0:0: +403,159,149528,6,0,L|393:243,1,68.2950005731545,6|0,1:2|0:0,0:0:0:0: +309,94,149844,1,10,0:2:0:0: +420,170,150002,2,0,L|403:307,1,136.590001146309,2|2,1:2|1:2,0:0:0:0: +289,381,150475,1,10,0:2:0:0: +289,381,150633,1,2,1:2:0:0: +97,68,151107,6,0,P|140:48|196:63,1,102.442500859732,4|0,1:2|0:0,0:0:0:0: +198,79,151423,2,0,P|154:129|139:218,1,136.590001146309,4|12,1:2|0:0,0:0:0:0: +297,317,152054,6,0,B|391:288|336:242|424:215,1,152.29000360183,6|8,1:2|0:2,0:0:0:0: +281,212,152528,1,0,1:2:0:0: +446,306,152686,2,0,P|490:265|476:153,1,152.29000360183,2|8,0:0|0:2,0:0:0:0: +343,142,153160,1,2,1:2:0:0: +297,317,153318,6,0,P|226:345|155:276,1,152.29000360183,6|8,1:2|0:2,0:0:0:0: +116,157,153791,2,0,P|150:228|158:309,1,152.29000360183,0|0,1:2|1:2,0:0:0:0: +264,170,154265,2,0,P|244:206|235:263,1,76.1450018009148,10|0,0:2|1:2,0:0:0:0: +152,77,154581,6,0,P|84:75|30:158,1,152.29000360183,6|8,1:2|0:2,0:0:0:0: +191,214,155054,1,0,1:2:0:0: +264,60,155212,2,0,P|331:58|385:141,1,152.29000360183,2|8,0:0|0:2,0:0:0:0: +212,171,155686,1,0,1:2:0:0: +405,112,155844,6,0,P|379:165|357:279,1,152.29000360183,6|8,1:2|0:2,0:0:0:0: +158,360,156318,2,0,P|142:285|111:216,1,152.29000360183,0|0,1:2|1:2,0:0:0:0: +9,64,156791,2,0,P|45:87|104:95,1,76.1450018009148,10|0,0:2|1:2,0:0:0:0: +270,12,157107,6,0,P|187:35|179:115,1,152.29000360183,6|8,1:2|0:2,0:0:0:0: +288,228,157581,2,0,P|370:204|378:124,1,152.29000360183,0|0,1:2|1:2,0:0:0:0: +248,83,158054,2,0,P|280:97|327:97,1,76.1450018009148,10|0,0:2|1:2,0:0:0:0: +490,16,158370,6,0,P|451:77|433:186,1,152.29000360183,6|8,1:2|0:2,0:0:0:0: +467,312,158844,2,0,P|449:238|409:173,1,152.29000360183,0|0,1:2|1:2,0:0:0:0: +248,208,159318,2,0,P|292:207|331:188,1,76.1450018009148,8|0,0:2|1:2,0:0:0:0: +320,98,159633,5,2,0:0:0:0: +118,79,160897,6,0,L|127:219,1,114.217502701372,2|0,1:2|0:0,0:0:0:0: +146,197,161212,2,0,L|138:83,1,114.217502701372,2|0,1:2|0:0,0:0:0:0: +158,87,161528,2,0,L|166:201,1,114.217502701372,2|0,1:2|0:0,0:0:0:0: +185,203,161844,1,6,0:2:0:0: +39,359,162160,6,0,P|21:311|21:255,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +153,158,162475,1,10,0:2:0:0: +221,372,162633,2,0,P|239:324|239:268,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +64,135,162949,2,0,P|69:176|85:215,1,83.9949974366761,2|8,0:2|0:2,0:0:0:0: +244,41,163265,6,0,P|336:61|369:154,1,167.989994873352,2|2,0:2|0:2,0:0:0:0: +322,264,163739,1,8,0:2:0:0: +282,124,163896,1,2,0:2:0:0: +419,289,164054,2,0,P|410:336|405:387,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +214,234,164370,2,0,P|211:192|202:151,1,83.9949974366761,14|0,0:2|0:0,0:0:0:0: +295,355,164686,6,0,P|254:340|180:331,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +305,196,165002,1,10,0:2:0:0: +209,350,165160,2,0,P|168:365|94:374,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +294,219,165475,2,0,P|253:204|179:195,1,83.9949974366761,2|8,0:2|0:2,0:0:0:0: +66,275,165791,6,0,P|32:177|29:88,1,167.989994873352,2|2,0:2|0:2,0:0:0:0: +226,70,166265,1,8,0:2:0:0: +144,232,166423,1,2,0:2:0:0: +244,50,166581,2,0,P|282:33|354:30,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +163,208,166896,2,0,P|202:225|243:230,1,83.9949974366761,14|0,0:2|0:0,0:0:0:0: +374,89,167212,6,0,P|396:133|408:208,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +364,304,167528,1,10,0:2:0:0: +490,167,167686,2,0,P|468:211|456:286,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +269,347,168002,2,0,P|258:306|239:269,1,83.9949974366761,2|8,0:2|0:2,0:0:0:0: +74,109,168317,6,0,B|172:127|94:167|246:192,1,167.989994873352,6|0,0:2|0:0,0:0:0:0: +258,24,168791,2,0,P|235:62|230:122,1,83.9949974366761,10|0,0:2|0:0,0:0:0:0: +85,285,169107,2,0,P|84:243|69:204,1,83.9949974366761,2|0,1:2|0:0,0:0:0:0: +233,25,169423,2,0,P|210:63|205:123,1,83.9949974366761,10|0,0:2|0:0,0:0:0:0: +296,252,169739,6,0,P|337:240|416:231,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +224,132,170054,1,10,0:2:0:0: +331,331,170212,2,0,P|372:343|451:352,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +238,159,170528,2,0,P|197:166|157:177,1,83.9949974366761,2|8,0:2|0:2,0:0:0:0: +95,302,170844,1,2,0:2:0:0: +92,94,171002,6,0,P|119:134|136:202,1,83.9949974366761,2|0,1:2|0:0,0:0:0:0: +243,353,171317,1,10,0:2:0:0: +218,162,171475,1,0,0:0:0:0: +237,323,171633,1,2,1:2:0:0: +212,132,171791,1,0,0:0:0:0: +328,311,171949,2,0,P|372:330|433:321,1,83.9949974366761,10|0,0:2|0:0,0:0:0:0: +447,131,172265,6,0,P|422:164|396:255,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +349,97,172581,1,10,0:2:0:0: +337,298,172739,2,0,P|381:317|442:308,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +335,81,173054,2,0,P|294:83|256:101,1,83.9949974366761,2|8,0:2|0:2,0:0:0:0: +195,272,173370,6,0,P|240:187|240:103,1,167.989994873352,6|2,0:2|0:2,0:0:0:0: +66,105,173844,1,8,0:2:0:0: +125,318,174002,2,0,P|156:347|196:357,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +276,147,174317,1,0,0:0:0:0: +104,236,174475,2,0,P|63:239|25:222,1,83.9949974366761,8|0,0:2|0:0,0:0:0:0: +178,39,174791,6,0,P|166:82|162:135,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +293,239,175107,1,10,0:2:0:0: +335,71,175265,2,0,P|375:51|441:45,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +366,284,175581,2,0,P|325:275|288:256,1,83.9949974366761,2|8,0:2|0:2,0:0:0:0: +490,135,175896,6,0,P|448:254|444:317,1,167.989994873352,6|2,0:2|0:2,0:0:0:0: +330,139,176370,1,8,0:2:0:0: +312,320,176528,2,0,P|303:272|290:225,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +175,118,176844,1,0,0:0:0:0: +181,305,177002,2,0,P|190:257|203:210,1,83.9949974366761,8|0,0:2|0:0,0:0:0:0: +318,51,177317,6,0,B|212:74|287:123|157:155,1,167.989994873352,6|10,1:2|0:2,0:0:0:0: +370,226,177791,2,0,P|421:244|491:238,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +325,83,178107,2,0,P|364:67|406:63,1,83.9949974366761,2|8,0:2|0:2,0:0:0:0: +302,283,178423,6,0,P|283:235|280:181,1,83.9949974366761,6|0,0:2|1:2,0:0:0:0: +173,35,178739,2,0,P|167:76|150:115,1,83.9949974366761,2|8,0:2|0:2,0:0:0:0: +203,282,179054,1,2,0:2:0:0: +58,125,179212,2,0,P|75:163|81:204,1,83.9949974366761,2|0,1:2|0:0,0:0:0:0: +266,91,179528,2,0,P|250:129|244:170,1,83.9949974366761,10|0,0:2|0:0,0:0:0:0: +379,354,179844,6,0,L|490:361,1,83.9949974366761,6|2,1:2|0:2,0:0:0:0: +252,193,180160,2,0,L|141:200,1,83.9949974366761,10|2,0:2|0:2,0:0:0:0: +385,339,180475,2,0,L|496:346,1,83.9949974366761,2|2,1:2|0:2,0:0:0:0: +258,178,180791,2,0,L|147:185,1,83.9949974366761,10|2,0:2|0:2,0:0:0:0: +295,333,181107,5,2,1:2:0:0: +334,153,181265,1,10,0:2:0:0: +306,325,181423,1,2,1:2:0:0: +347,148,181581,1,2,0:2:0:0: +317,319,181739,2,0,L|324:208,1,83.9949974366761,10|10,0:2|0:2,0:0:0:0: +237,65,182054,1,2,1:2:0:0: +440,112,182212,1,10,0:2:0:0: +225,77,182370,5,14,0:2:0:0: +173,281,183476,5,14,0:2:0:0: +173,281,184265,6,0,L|263:276,1,90.6674982014809,14|0,0:2|0:0,0:0:0:0: +266,265,184581,2,0,L|183:268,1,60.4449988009873,14|0,0:2|1:2,0:0:0:0: +180,254,184818,1,0,1:2:0:0: +180,254,184897,1,6,1:2:0:0: +402,65,186002,5,14,0:2:0:0: +402,65,186791,6,0,L|311:60,1,90.6674982014809,14|0,0:0|0:0,0:0:0:0: +309,49,187107,2,0,L|397:54,1,60.4449988009873,14|0,0:0|1:0,0:0:0:0: +432,107,187344,5,0,1:0:0:0: +432,107,187423,2,0,P|420:151|413:220,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +460,324,187739,1,10,0:2:0:0: +270,233,187897,2,0,P|263:191|252:151,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +345,361,188212,2,0,P|351:319|362:279,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +223,129,188528,6,0,B|121:153|190:198|70:228,1,167.989994873352,6|0,0:0|0:0,0:0:0:0: +195,36,189002,2,0,P|245:36|304:61,1,83.9949974366761,10|0,0:2|0:0,0:0:0:0: +315,225,189318,2,0,P|265:225|206:200,1,83.9949974366761,2|0,1:2|0:0,0:0:0:0: +426,87,189633,2,0,P|406:131|393:207,1,83.9949974366761,8|0,0:0|0:0,0:0:0:0: +370,384,189949,6,0,P|344:289|302:200,1,167.989994873352,6|10,1:2|0:2,0:0:0:0: +190,82,190423,2,0,P|153:65|108:64,1,83.9949974366761,2|0,0:0|1:2,0:0:0:0: +221,254,190739,2,0,P|262:253|300:236,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +189,116,191054,1,2,0:0:0:0: +378,11,191212,6,0,P|348:92|339:178,1,167.989994873352,2|10,1:2|0:2,0:0:0:0: +465,289,191686,1,2,0:0:0:0: +363,105,191844,2,0,P|354:153|356:219,1,83.9949974366761,2|2,1:2|0:0,0:0:0:0: +421,369,192160,1,8,0:2:0:0: +421,369,192318,1,10,0:2:0:0: +221,263,192791,6,0,B|384:248|246:150|453:123,1,266.507506303202,12|0,0:2|0:0,0:0:0:0: +439,136,193423,2,0,B|266:161|390:256|231:272,1,266.507506303202,12|0,0:0|0:0,0:0:0:0: +238,282,194054,2,0,B|396:266|272:171|445:146,1,266.507506303202,12|0,0:0|0:0,0:0:0:0: +452,156,194686,2,0,B|279:181|403:276|245:292,1,266.507506303202,12|0,0:0|0:0,0:0:0:0: +250,303,195317,2,0,B|409:286|285:191|458:166,1,266.507506303202,12|0,0:0|0:0,0:0:0:0: +461,188,195949,2,0,P|458:133|433:68,1,114.217502701372,12|0,0:0|0:0,0:0:0:0: +411,61,196265,6,0,P|376:85|317:90,1,76.1450018009148,0|10,1:2|0:2,0:0:0:0: +136,47,196581,1,8,0:2:0:0: +314,7,196739,1,2,1:2:0:0: +120,52,196897,1,8,0:2:0:0: +298,12,197055,1,8,0:2:0:0: +104,58,197212,2,0,P|96:101|91:167,1,76.1450018009148,8|8,0:2|0:2,0:0:0:0: +136,317,197528,6,0,P|205:285|247:284,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +384,371,197844,1,10,0:2:0:0: +317,207,198002,2,0,P|248:175|206:174,1,83.9949974366761,2|0,0:0|1:2,0:0:0:0: +373,345,198318,2,0,P|413:334|448:311,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +436,127,198633,6,0,P|419:169|412:229,1,83.9949974366761,2|0,0:0|1:2,0:0:0:0: +264,23,198949,2,0,P|281:65|288:125,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +242,254,199265,1,2,0:0:0:0: +414,124,199423,2,0,P|397:166|390:226,1,83.9949974366761,2|0,1:2|0:0,0:0:0:0: +214,266,199739,2,0,P|206:224|190:186,1,83.9949974366761,10|0,0:2|0:0,0:0:0:0: +38,39,200054,6,0,L|49:128,1,83.9949974366761,6|2,1:2|0:2,0:0:0:0: +86,302,200370,2,0,L|96:218,1,83.9949974366761,14|2,0:2|0:0,0:0:0:0: +48,34,200686,2,0,L|59:123,1,83.9949974366761,6|2,1:2|0:0,0:0:0:0: +96,297,201002,2,0,L|106:213,1,83.9949974366761,14|2,0:2|0:0,0:0:0:0: +223,68,201318,5,2,1:2:0:0: +211,238,201476,1,10,0:2:0:0: +239,61,201633,1,10,0:2:0:0: +227,231,201791,1,2,0:0:0:0: +255,52,201949,2,0,L|239:170,1,83.9949974366761,10|10,0:2|0:2,0:0:0:0: +218,340,202265,1,2,1:2:0:0: +309,179,202423,1,10,0:2:0:0: +328,301,202581,5,6,0:2:0:0: +459,23,203528,6,0,L|374:30,1,60.4449988009873,14|2,0:2|0:2,0:0:0:0: +305,177,203844,5,6,1:2:0:0: +305,177,204002,1,2,0:2:0:0: +264,26,204160,1,10,0:2:0:0: +264,26,204318,1,2,0:2:0:0: +210,186,204476,1,2,1:2:0:0: +210,186,204633,2,0,L|203:288,1,76.1450018009148,2|8,0:2|0:2,0:0:0:0: +62,159,204949,6,0,L|69:261,1,76.1450018009148,2|0,0:0|1:2,0:0:0:0: +192,357,205265,2,0,P|232:356|272:325,1,76.1450018009148,2|8,0:0|0:2,0:0:0:0: +398,216,205581,2,0,P|365:197|327:197,1,76.1450018009148,2|4,0:0|1:2,0:0:0:0: +407,341,205897,1,2,0:0:0:0: +493,184,206054,2,0,P|487:146|478:109,1,76.1450018009148,14|2,0:2|0:0,0:0:0:0: +311,23,206370,6,0,P|278:40|225:40,1,76.1450018009148,4|2,1:2|0:0,0:0:0:0: +76,13,206686,1,10,0:2:0:0: +76,13,206844,1,2,0:0:0:0: +186,145,207002,1,2,1:2:0:0: +186,145,207160,2,0,P|219:162|257:164,1,76.1450018009148,2|10,0:0|0:2,0:0:0:0: +102,30,207476,6,0,P|132:97|145:198,1,152.29000360183,2|0,0:0|0:0,0:0:0:0: +73,352,207949,1,8,0:2:0:0: +73,352,208107,1,0,0:0:0:0: +188,244,208265,1,6,1:2:0:0: +188,244,208423,2,0,P|245:224|279:232,1,76.1450018009148,2|14,0:0|0:2,0:0:0:0: +356,326,208739,1,2,0:0:0:0: +428,170,208897,6,0,P|450:206|462:261,1,76.1450018009148,6|2,1:2|0:0,0:0:0:0: +320,106,209212,1,10,0:2:0:0: +320,106,209370,1,2,0:0:0:0: +347,275,209528,1,2,1:2:0:0: +347,275,209686,1,2,0:0:0:0: +228,135,209844,1,10,0:2:0:0: +135,283,210002,6,0,B|142:192|95:232|109:126,1,152.29000360183,6|2,0:0|0:0,0:0:0:0: +226,12,210476,1,8,0:2:0:0: +226,12,210633,1,2,0:0:0:0: +188,167,210791,2,0,P|210:206|215:264,1,76.1450018009148,2|0,1:2|0:0,0:0:0:0: +289,102,211107,1,10,0:2:0:0: +289,102,211265,1,2,0:0:0:0: +357,254,211423,6,0,P|335:293|330:351,1,76.1450018009148,6|0,1:2|0:0,0:0:0:0: +320,177,211739,1,8,0:2:0:0: +420,337,211897,2,0,P|438:270|437:185,1,152.29000360183,2|2,0:0|0:0,0:0:0:0: +330,24,212370,1,8,0:2:0:0: +188,167,212528,6,0,P|186:242|205:316,1,152.29000360183,6|0,0:0|0:0,0:0:0:0: +89,221,213002,1,12,0:2:0:0: +89,221,213160,1,0,0:0:0:0: +205,316,213318,2,0,P|247:311|292:280,1,76.1450018009148,4|0,1:2|0:0,0:0:0:0: +355,148,213633,1,12,0:2:0:0: +355,148,213791,1,0,0:0:0:0: +377,310,213949,6,0,P|360:265|358:210,1,76.1450018009148,6|2,1:2|0:0,0:0:0:0: +229,84,214265,2,0,P|223:121|208:156,1,76.1450018009148,10|0,0:2|0:0,0:0:0:0: +109,231,214581,1,2,1:2:0:0: +109,231,214739,1,2,0:0:0:0: +176,22,214897,2,0,P|211:7|249:5,1,76.1450018009148,10|4,0:2|0:0,0:0:0:0: +343,176,215212,5,2,1:2:0:0: +343,176,215370,1,2,0:0:0:0: +304,15,215528,1,10,0:2:0:0: +304,15,215686,1,2,0:0:0:0: +425,197,215844,2,0,P|459:212|516:204,1,76.1450018009148,2|0,1:2|0:0,0:0:0:0: +386,33,216160,2,0,P|348:32|313:47,1,76.1450018009148,8|0,0:2|0:0,0:0:0:0: +269,217,216476,6,0,P|303:301|320:394,1,152.29000360183,6|10,1:2|0:2,0:0:0:0: +343,178,216949,1,0,0:0:0:0: +192,259,217107,2,0,P|183:301|180:354,1,76.1450018009148,2|2,1:2|0:0,0:0:0:0: +73,212,217423,1,10,0:2:0:0: +73,212,217581,1,4,0:0:0:0: +197,75,217739,6,0,B|295:94|237:137|354:161,1,152.29000360183,2|8,1:2|0:2,0:0:0:0: +194,159,218212,1,0,0:0:0:0: +345,61,218370,2,0,P|394:48|452:48,1,76.1450018009148,6|0,1:2|0:0,0:0:0:0: +416,260,218686,2,0,P|378:255|341:245,1,76.1450018009148,14|0,0:2|0:0,0:0:0:0: +485,93,219002,6,0,P|451:161|435:252,1,152.29000360183,6|8,1:2|0:2,0:0:0:0: +339,360,219476,1,0,0:0:0:0: +374,147,219633,2,0,P|408:215|424:306,1,152.29000360183,0|10,1:2|0:2,0:0:0:0: +248,368,220107,1,6,0:0:0:0: +201,179,220265,5,2,1:2:0:0: +201,179,220423,1,2,0:0:0:0: +239,341,220581,1,10,0:2:0:0: +239,341,220739,1,2,0:0:0:0: +122,203,220897,2,0,P|88:189|38:184,1,76.1450018009148,2|0,1:2|0:0,0:0:0:0: +257,253,221212,2,0,P|294:247|329:233,1,76.1450018009148,8|0,0:2|0:0,0:0:0:0: +442,40,221528,6,0,L|434:149,1,76.1450018009148,6|2,1:2|0:0,0:0:0:0: +417,284,221844,2,0,L|411:208,1,76.1450018009148,10|2,0:2|0:0,0:0:0:0: +336,36,222160,2,0,L|328:145,1,76.1450018009148,2|2,1:2|0:0,0:0:0:0: +311,280,222476,2,0,L|305:204,1,76.1450018009148,10|2,0:2|0:0,0:0:0:0: +165,91,222791,5,2,1:2:0:0: +143,229,222949,1,10,0:2:0:0: +156,57,223107,1,10,0:2:0:0: +125,249,223265,1,2,0:0:0:0: +142,30,223423,2,0,L|67:25,1,76.1450018009148,2|10,1:2|0:2,0:0:0:0: +209,171,223739,1,2,1:2:0:0: +3,159,223897,1,10,0:2:0:0: +111,129,224054,5,6,0:2:0:0: +82,60,234160,5,2,1:2:0:0: +82,60,234476,5,2,1:2:0:0: +82,60,234791,5,2,1:2:0:0: +82,60,235107,5,6,0:2:0:0: +312,238,235423,6,0,P|360:258|414:258,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +262,105,235739,1,10,0:2:0:0: +170,284,235897,2,0,P|122:304|68:304,1,83.9949974366761,2|0,0:0|1:2,0:0:0:0: +83,113,236212,2,0,P|101:157|110:208,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +258,40,236528,6,0,P|226:117|210:207,1,167.989994873352,2|2,0:0|0:0,0:0:0:0: +327,323,237002,1,10,0:2:0:0: +170,284,237160,1,2,0:0:0:0: +316,147,237318,2,0,P|361:132|413:134,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +417,319,237633,2,0,P|372:304|320:306,1,83.9949974366761,14|0,0:2|0:0,0:0:0:0: +153,376,237949,6,0,P|177:308|188:208,1,167.989994873352,6|10,1:2|0:2,0:0:0:0: +81,67,238423,2,0,P|85:113|102:165,1,83.9949974366761,2|0,0:0|1:2,0:0:0:0: +277,190,238739,2,0,P|288:149|291:107,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +429,281,239054,6,0,P|412:222|402:108,1,167.989994873352,2|2,0:0|0:0,0:0:0:0: +252,12,239528,1,10,0:2:0:0: +383,93,239686,1,2,0:0:0:0: +224,0,239844,2,0,P|237:44|245:90,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +282,275,240160,2,0,P|294:234|301:193,1,83.9949974366761,14|0,0:2|0:0,0:0:0:0: +155,74,240476,6,0,P|110:54|58:51,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +177,214,240791,1,10,0:2:0:0: +285,27,240949,2,0,P|330:7|382:4,1,83.9949974366761,2|0,0:0|1:2,0:0:0:0: +190,181,241265,2,0,P|145:161|93:158,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +350,91,241581,6,0,P|370:154|363:271,1,167.989994873352,6|0,0:0|0:0,0:0:0:0: +172,349,242054,2,0,P|212:328|267:318,1,83.9949974366761,10|0,0:2|0:0,0:0:0:0: +94,180,242370,2,0,P|134:200|189:210,1,83.9949974366761,2|0,1:2|0:0,0:0:0:0: +256,347,242686,2,0,P|215:357|177:376,1,83.9949974366761,10|0,0:2|0:0,0:0:0:0: +291,209,243002,6,0,P|306:160|309:104,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +386,277,243318,1,10,0:2:0:0: +225,165,243476,2,0,P|210:116|207:60,1,83.9949974366761,2|0,0:0|1:2,0:0:0:0: +406,36,243791,2,0,P|400:77|387:117,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +308,225,244107,1,2,0:0:0:0: +246,15,244265,6,0,P|196:10|149:27,1,83.9949974366761,2|2,1:2|0:0,0:0:0:0: +89,217,244581,1,10,0:2:0:0: +89,217,244739,1,2,0:0:0:0: +242,41,244897,2,0,P|192:36|145:53,1,83.9949974366761,2|2,1:2|0:0,0:0:0:0: +189,240,245212,1,10,0:2:0:0: +189,240,245370,1,2,0:0:0:0: +311,93,245528,6,0,P|355:75|401:75,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +400,292,245844,1,10,0:2:0:0: +250,154,246002,2,0,P|211:137|170:134,1,83.9949974366761,2|0,0:0|1:2,0:0:0:0: +320,311,246318,2,0,P|361:308|399:291,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +488,108,246633,6,0,P|474:150|464:206,1,83.9949974366761,6|0,0:0|1:2,0:0:0:0: +314,323,246949,2,0,P|305:281|292:242,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +202,67,247265,2,0,P|163:54|67:145,1,167.989994873352,2|0,0:0|0:0,0:0:0:0: +190,256,247739,1,8,0:2:0:0: +200,100,247897,1,0,0:0:0:0: +188,283,248054,6,0,P|228:311|277:313,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +342,145,248370,1,10,0:2:0:0: +338,350,248528,2,0,P|359:307|368:260,1,83.9949974366761,2|0,0:0|1:2,0:0:0:0: +290,80,248844,2,0,P|300:120|319:158,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +432,320,249160,6,0,P|453:277|462:230,1,83.9949974366761,6|0,0:0|1:2,0:0:0:0: +384,50,249476,2,0,P|394:90|413:128,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +449,329,249791,2,0,P|479:256|487:165,1,167.989994873352,2|0,0:0|0:0,0:0:0:0: +351,34,250265,1,12,0:2:0:0: +312,187,250423,1,0,0:0:0:0: +196,18,250581,6,0,P|215:60|224:126,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +161,257,250897,1,10,0:2:0:0: +88,110,251054,2,0,P|69:152|60:218,1,83.9949974366761,2|0,0:0|1:2,0:0:0:0: +188,336,251370,2,0,P|178:295|161:257,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +206,65,251686,6,0,P|265:46|305:46,1,83.9949974366761,6|0,0:0|1:2,0:0:0:0: +381,245,252002,2,0,P|339:240|300:224,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +430,103,252318,1,2,0:0:0:0: +440,308,252476,2,0,P|463:261|466:209,1,83.9949974366761,2|0,1:2|0:0,0:0:0:0: +349,86,252791,2,0,P|342:127|322:163,1,83.9949974366761,10|0,0:2|0:0,0:0:0:0: +217,345,253107,5,6,1:2:0:0: +229,190,253265,1,2,0:2:0:0: +235,365,253423,1,10,0:2:0:0: +225,169,253581,2,0,P|187:144|119:129,1,83.9949974366761,2|2,0:2|1:2,0:0:0:0: +318,271,253897,1,2,0:2:0:0: +337,90,254054,1,10,0:2:0:0: +407,267,254212,5,2,0:2:0:0: +407,267,254291,1,2,0:2:0:0: +407,267,254370,2,0,L|419:155,1,83.9949974366761,2|10,1:2|0:2,0:0:0:0: +282,25,254686,1,10,0:2:0:0: +314,248,254844,2,0,L|302:136,1,83.9949974366761,2|10,0:2|0:2,0:0:0:0: +150,22,255160,1,10,0:2:0:0: +297,137,255318,1,2,1:2:0:0: +74,180,255476,1,10,0:2:0:0: +184,109,255633,5,6,0:0:0:0: +66,184,259423,6,0,P|114:169|135:169,1,60.4449988009873,6|0,1:2|1:2,0:0:0:0: +227,278,259739,2,0,P|254:289|284:293,1,60.4449988009873,0|0,1:2|1:2,0:0:0:0: +374,106,260054,1,6,1:2:0:0: +399,293,260212,1,2,1:2:0:0: +455,78,260370,1,8,0:2:0:0: +396,261,260528,1,8,0:2:0:0: +288,83,260686,6,0,P|242:58|191:51,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +83,215,261002,1,10,0:2:0:0: +120,39,261160,2,0,P|139:75|150:135,1,83.9949974366761,2|0,0:2|1:2,0:0:0:0: +168,286,261476,2,0,P|177:245|197:208,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +300,62,261791,6,0,B|402:90|337:130|449:151,1,167.989994873352,6|0,0:0|0:0,0:0:0:0: +319,285,262265,2,0,P|306:238|300:177,1,83.9949974366761,10|0,0:2|0:0,0:0:0:0: +160,42,262581,2,0,P|153:83|142:123,1,83.9949974366761,2|0,1:2|0:0,0:0:0:0: +297,272,262897,2,0,P|284:225|278:164,1,83.9949974366761,8|0,0:0|0:0,0:0:0:0: +430,55,263212,6,0,P|470:39|518:40,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +401,194,263528,1,10,0:2:0:0: +282,28,263686,2,0,P|242:12|194:13,1,83.9949974366761,2|0,0:0|1:2,0:0:0:0: +124,200,264002,2,0,P|165:199|204:183,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +93,85,264318,1,2,0:0:0:0: +61,277,264476,6,0,P|72:313|77:364,1,83.9949974366761,2|2,1:2|0:0,0:0:0:0: +229,203,264791,2,0,P|217:239|213:290,1,83.9949974366761,10|2,0:2|0:0,0:0:0:0: +358,126,265107,2,0,P|369:162|374:213,1,83.9949974366761,2|2,1:2|0:0,0:0:0:0: +470,69,265423,1,8,0:2:0:0: +470,69,265581,1,10,0:2:0:0: +149,40,266054,6,0,P|184:78|242:292,1,266.507506303202,12|0,0:2|0:0,0:0:0:0: +253,277,266686,2,0,P|233:146|158:37,1,266.507506303202,12|0,0:0|0:0,0:0:0:0: +168,33,267318,2,0,P|203:71|261:285,1,266.507506303202,12|0,0:0|0:0,0:0:0:0: +272,270,267949,2,0,P|252:139|177:30,1,266.507506303202,12|0,0:0|0:0,0:0:0:0: +187,23,268581,2,0,P|262:131|281:262,1,266.507506303202,12|0,0:0|0:0,0:0:0:0: +294,261,269212,2,0,P|303:193|327:142,1,114.217502701372,12|0,0:0|0:0,0:0:0:0: +340,145,269528,6,0,P|363:175|378:212,1,76.1450018009148,0|10,1:2|0:2,0:0:0:0: +447,373,269844,1,8,0:2:0:0: +465,198,270002,1,2,1:2:0:0: +450,358,270160,1,8,0:2:0:0: +468,183,270318,1,8,0:2:0:0: +344,367,270476,2,0,P|303:380|248:380,1,76.1450018009148,8|8,0:2|0:2,0:0:0:0: +146,242,270791,6,0,P|129:193|126:127,1,83.9949974366761,6|0,1:2|0:0,0:0:0:0: +264,74,271107,1,10,0:2:0:0: +218,287,271265,2,0,P|181:318|119:326,1,83.9949974366761,2|0,0:0|1:2,0:0:0:0: +245,153,271581,2,0,P|284:165|315:193,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +349,382,271897,6,0,P|337:335|337:292,1,83.9949974366761,2|0,0:0|1:2,0:0:0:0: +446,128,272212,2,0,P|444:169|433:210,1,83.9949974366761,2|8,0:0|0:2,0:0:0:0: +324,72,272528,1,2,0:0:0:0: +415,294,272686,2,0,P|464:289|506:260,1,83.9949974366761,2|0,1:2|0:0,0:0:0:0: +349,149,273002,2,0,P|307:151|270:170,1,83.9949974366761,10|0,0:2|0:0,0:0:0:0: +148,303,273318,6,0,P|129:259|127:210,1,83.9949974366761,6|2,1:2|0:2,0:0:0:0: +199,70,273633,1,14,0:2:0:0: +247,249,273791,2,0,P|266:205|268:156,1,83.9949974366761,2|6,0:0|1:2,0:0:0:0: +242,3,274107,1,2,0:0:0:0: +143,195,274265,2,0,P|124:151|122:102,1,83.9949974366761,14|2,0:2|0:0,0:0:0:0: +272,13,274581,6,0,L|385:20,1,83.9949974366761,2|10,1:2|0:2,0:0:0:0: +488,195,274897,2,0,L|375:202,1,83.9949974366761,10|2,0:2|0:0,0:0:0:0: +285,37,275212,1,10,0:2:0:0: +315,233,275370,1,10,0:2:0:0: +283,20,275528,1,2,1:2:0:0: +313,216,275686,1,10,0:2:0:0: +254,127,275844,5,6,0:2:0:0: +71,80,278370,6,0,B|118:74|166:40|166:40|130:88,1,125.992496155014,12|0,0:0|0:0,0:0:0:0: +256,61,278686,2,0,B|242:43|242:43|291:77|337:83,1,125.992496155014,8|0,0:0|0:0,0:0:0:0: +351,186,279002,2,0,B|297:179|242:141|242:141|261:165,1,149.542498859081,12|0,0:0|0:0,0:0:0:0: +149,163,279318,2,0,B|167:138|167:138|112:176|59:183,1,149.542498859081,8|0,0:0|0:0,0:0:0:0: +205,229,279633,5,14,0:2:0:0: +480,25,280265,6,0,B|160:57|384:313|32:345,1,580.900014182129,6|0,1:2|0:0,0:0:0:0: diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2571731-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2571731-expected-conversion.json new file mode 100644 index 0000000000..1817ef4742 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2571731-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":1254.0,"Objects":[{"StartTime":1254.0,"Position":229.0,"HyperDash":false},{"StartTime":1332.0,"Position":187.422012,"HyperDash":false},{"StartTime":1410.0,"Position":169.115814,"HyperDash":false},{"StartTime":1488.0,"Position":162.374466,"HyperDash":false},{"StartTime":1566.0,"Position":160.452332,"HyperDash":false},{"StartTime":1635.0,"Position":181.787521,"HyperDash":false},{"StartTime":1704.0,"Position":177.835266,"HyperDash":false},{"StartTime":1773.0,"Position":194.2059,"HyperDash":false},{"StartTime":1878.0,"Position":230.367538,"HyperDash":false}]},{"StartTime":2191.0,"Objects":[{"StartTime":2191.0,"Position":470.0,"HyperDash":false}]},{"StartTime":2504.0,"Objects":[{"StartTime":2504.0,"Position":228.0,"HyperDash":false},{"StartTime":2573.0,"Position":188.673691,"HyperDash":false},{"StartTime":2642.0,"Position":171.347382,"HyperDash":false},{"StartTime":2711.0,"Position":143.021057,"HyperDash":false},{"StartTime":2816.0,"Position":118.002762,"HyperDash":false}]},{"StartTime":3129.0,"Objects":[{"StartTime":3129.0,"Position":231.0,"HyperDash":false},{"StartTime":3198.0,"Position":251.326309,"HyperDash":false},{"StartTime":3267.0,"Position":293.652618,"HyperDash":false},{"StartTime":3336.0,"Position":307.978943,"HyperDash":false},{"StartTime":3441.0,"Position":340.997253,"HyperDash":false}]},{"StartTime":3754.0,"Objects":[{"StartTime":3754.0,"Position":465.0,"HyperDash":false},{"StartTime":3832.0,"Position":458.7602,"HyperDash":false},{"StartTime":3910.0,"Position":469.2429,"HyperDash":false},{"StartTime":3988.0,"Position":435.549255,"HyperDash":false},{"StartTime":4066.0,"Position":439.174347,"HyperDash":false},{"StartTime":4135.0,"Position":439.5499,"HyperDash":false},{"StartTime":4204.0,"Position":417.896637,"HyperDash":false},{"StartTime":4273.0,"Position":391.087067,"HyperDash":false},{"StartTime":4379.0,"Position":341.072845,"HyperDash":false}]},{"StartTime":4691.0,"Objects":[{"StartTime":4691.0,"Position":131.0,"HyperDash":false}]},{"StartTime":5004.0,"Objects":[{"StartTime":5004.0,"Position":365.0,"HyperDash":false},{"StartTime":5073.0,"Position":357.3771,"HyperDash":false},{"StartTime":5142.0,"Position":348.754242,"HyperDash":false},{"StartTime":5211.0,"Position":379.131348,"HyperDash":false},{"StartTime":5316.0,"Position":366.705231,"HyperDash":false}]},{"StartTime":5629.0,"Objects":[{"StartTime":5629.0,"Position":228.0,"HyperDash":false},{"StartTime":5698.0,"Position":247.324,"HyperDash":false},{"StartTime":5767.0,"Position":280.648,"HyperDash":false},{"StartTime":5836.0,"Position":285.972,"HyperDash":false},{"StartTime":5941.0,"Position":337.9868,"HyperDash":false}]},{"StartTime":6254.0,"Objects":[{"StartTime":6254.0,"Position":197.0,"HyperDash":false},{"StartTime":6332.0,"Position":165.6015,"HyperDash":false},{"StartTime":6410.0,"Position":141.904709,"HyperDash":false},{"StartTime":6488.0,"Position":132.042267,"HyperDash":false},{"StartTime":6566.0,"Position":96.16125,"HyperDash":false},{"StartTime":6635.0,"Position":60.9876633,"HyperDash":false},{"StartTime":6704.0,"Position":51.4006424,"HyperDash":false},{"StartTime":6773.0,"Position":71.82723,"HyperDash":false},{"StartTime":6879.0,"Position":54.095974,"HyperDash":false}]},{"StartTime":7191.0,"Objects":[{"StartTime":7191.0,"Position":283.0,"HyperDash":false}]},{"StartTime":7504.0,"Objects":[{"StartTime":7504.0,"Position":290.0,"HyperDash":false},{"StartTime":7573.0,"Position":310.169,"HyperDash":false},{"StartTime":7642.0,"Position":295.338,"HyperDash":false},{"StartTime":7711.0,"Position":292.507019,"HyperDash":false},{"StartTime":7816.0,"Position":304.3294,"HyperDash":false}]},{"StartTime":8129.0,"Objects":[{"StartTime":8129.0,"Position":48.0,"HyperDash":false}]},{"StartTime":8441.0,"Objects":[{"StartTime":8441.0,"Position":308.0,"HyperDash":false}]},{"StartTime":8754.0,"Objects":[{"StartTime":8754.0,"Position":168.0,"HyperDash":false},{"StartTime":8823.0,"Position":142.687775,"HyperDash":false},{"StartTime":8892.0,"Position":138.375549,"HyperDash":false},{"StartTime":8961.0,"Position":89.06334,"HyperDash":false},{"StartTime":9066.0,"Position":58.0664749,"HyperDash":false}]},{"StartTime":9379.0,"Objects":[{"StartTime":9379.0,"Position":226.0,"HyperDash":false},{"StartTime":9448.0,"Position":255.312714,"HyperDash":false},{"StartTime":9517.0,"Position":287.625427,"HyperDash":false},{"StartTime":9586.0,"Position":315.938171,"HyperDash":false},{"StartTime":9691.0,"Position":335.9358,"HyperDash":false}]},{"StartTime":10004.0,"Objects":[{"StartTime":10004.0,"Position":477.0,"HyperDash":false},{"StartTime":10062.0,"Position":43.0,"HyperDash":false},{"StartTime":10121.0,"Position":494.0,"HyperDash":false},{"StartTime":10179.0,"Position":135.0,"HyperDash":false},{"StartTime":10238.0,"Position":30.0,"HyperDash":false},{"StartTime":10296.0,"Position":11.0,"HyperDash":false},{"StartTime":10355.0,"Position":239.0,"HyperDash":false},{"StartTime":10413.0,"Position":505.0,"HyperDash":false},{"StartTime":10472.0,"Position":353.0,"HyperDash":false},{"StartTime":10531.0,"Position":136.0,"HyperDash":false},{"StartTime":10589.0,"Position":135.0,"HyperDash":false},{"StartTime":10648.0,"Position":346.0,"HyperDash":false},{"StartTime":10706.0,"Position":39.0,"HyperDash":false},{"StartTime":10765.0,"Position":300.0,"HyperDash":false},{"StartTime":10823.0,"Position":398.0,"HyperDash":false},{"StartTime":10882.0,"Position":151.0,"HyperDash":false},{"StartTime":10941.0,"Position":73.0,"HyperDash":false}]},{"StartTime":11254.0,"Objects":[{"StartTime":11254.0,"Position":173.0,"HyperDash":false},{"StartTime":11332.0,"Position":151.138428,"HyperDash":false},{"StartTime":11410.0,"Position":132.025146,"HyperDash":false},{"StartTime":11488.0,"Position":91.37633,"HyperDash":false},{"StartTime":11566.0,"Position":80.290535,"HyperDash":false},{"StartTime":11635.0,"Position":61.6581879,"HyperDash":false},{"StartTime":11704.0,"Position":80.94798,"HyperDash":false},{"StartTime":11773.0,"Position":108.710762,"HyperDash":false},{"StartTime":11879.0,"Position":120.303291,"HyperDash":false}]},{"StartTime":12191.0,"Objects":[{"StartTime":12191.0,"Position":348.0,"HyperDash":false}]},{"StartTime":12504.0,"Objects":[{"StartTime":12504.0,"Position":119.0,"HyperDash":false},{"StartTime":12573.0,"Position":113.579384,"HyperDash":false},{"StartTime":12642.0,"Position":114.15876,"HyperDash":false},{"StartTime":12711.0,"Position":93.7381439,"HyperDash":false},{"StartTime":12816.0,"Position":108.054588,"HyperDash":false}]},{"StartTime":13129.0,"Objects":[{"StartTime":13129.0,"Position":246.0,"HyperDash":false},{"StartTime":13198.0,"Position":207.673676,"HyperDash":false},{"StartTime":13267.0,"Position":196.347351,"HyperDash":false},{"StartTime":13336.0,"Position":190.021027,"HyperDash":false},{"StartTime":13441.0,"Position":136.002686,"HyperDash":false}]},{"StartTime":13754.0,"Objects":[{"StartTime":13754.0,"Position":290.0,"HyperDash":false},{"StartTime":13832.0,"Position":327.611237,"HyperDash":false},{"StartTime":13910.0,"Position":353.671875,"HyperDash":false},{"StartTime":13988.0,"Position":363.8803,"HyperDash":false},{"StartTime":14066.0,"Position":380.478455,"HyperDash":false},{"StartTime":14135.0,"Position":374.964325,"HyperDash":false},{"StartTime":14204.0,"Position":392.930969,"HyperDash":false},{"StartTime":14273.0,"Position":350.32254,"HyperDash":false},{"StartTime":14379.0,"Position":329.753876,"HyperDash":false}]},{"StartTime":14691.0,"Objects":[{"StartTime":14691.0,"Position":80.0,"HyperDash":false}]},{"StartTime":15004.0,"Objects":[{"StartTime":15004.0,"Position":335.0,"HyperDash":false},{"StartTime":15082.0,"Position":297.345367,"HyperDash":false},{"StartTime":15160.0,"Position":285.690735,"HyperDash":false},{"StartTime":15238.0,"Position":243.036133,"HyperDash":false},{"StartTime":15316.0,"Position":228.3815,"HyperDash":false},{"StartTime":15385.0,"Position":192.802429,"HyperDash":false},{"StartTime":15454.0,"Position":180.223328,"HyperDash":false},{"StartTime":15523.0,"Position":144.644241,"HyperDash":false},{"StartTime":15628.0,"Position":121.763031,"HyperDash":false}]},{"StartTime":15941.0,"Objects":[{"StartTime":15941.0,"Position":475.0,"HyperDash":false}]},{"StartTime":16254.0,"Objects":[{"StartTime":16254.0,"Position":120.0,"HyperDash":false},{"StartTime":16332.0,"Position":112.4255,"HyperDash":false},{"StartTime":16410.0,"Position":115.254448,"HyperDash":false},{"StartTime":16488.0,"Position":125.264832,"HyperDash":false},{"StartTime":16566.0,"Position":150.934219,"HyperDash":false},{"StartTime":16635.0,"Position":164.5551,"HyperDash":false},{"StartTime":16704.0,"Position":205.001038,"HyperDash":false},{"StartTime":16773.0,"Position":217.329178,"HyperDash":false},{"StartTime":16879.0,"Position":251.47258,"HyperDash":false}]},{"StartTime":17191.0,"Objects":[{"StartTime":17191.0,"Position":405.0,"HyperDash":false}]},{"StartTime":17504.0,"Objects":[{"StartTime":17504.0,"Position":250.0,"HyperDash":false},{"StartTime":17582.0,"Position":210.776382,"HyperDash":false},{"StartTime":17660.0,"Position":183.552765,"HyperDash":false},{"StartTime":17738.0,"Position":153.329163,"HyperDash":false},{"StartTime":17816.0,"Position":141.10556,"HyperDash":false},{"StartTime":17885.0,"Position":161.187988,"HyperDash":false},{"StartTime":17954.0,"Position":192.2704,"HyperDash":false},{"StartTime":18023.0,"Position":194.352844,"HyperDash":false},{"StartTime":18128.0,"Position":250.0,"HyperDash":false}]},{"StartTime":18441.0,"Objects":[{"StartTime":18441.0,"Position":403.0,"HyperDash":false}]},{"StartTime":18754.0,"Objects":[{"StartTime":18754.0,"Position":250.0,"HyperDash":false},{"StartTime":18832.0,"Position":222.847168,"HyperDash":false},{"StartTime":18910.0,"Position":212.124878,"HyperDash":false},{"StartTime":18988.0,"Position":180.067841,"HyperDash":false},{"StartTime":19066.0,"Position":177.484818,"HyperDash":false},{"StartTime":19135.0,"Position":173.0804,"HyperDash":false},{"StartTime":19204.0,"Position":184.026764,"HyperDash":false},{"StartTime":19273.0,"Position":200.533371,"HyperDash":false},{"StartTime":19378.0,"Position":250.553848,"HyperDash":false}]},{"StartTime":19691.0,"Objects":[{"StartTime":19691.0,"Position":404.0,"HyperDash":false}]},{"StartTime":20004.0,"Objects":[{"StartTime":20004.0,"Position":249.0,"HyperDash":false}]},{"StartTime":20316.0,"Objects":[{"StartTime":20316.0,"Position":241.0,"HyperDash":false}]},{"StartTime":20629.0,"Objects":[{"StartTime":20629.0,"Position":239.0,"HyperDash":false}]},{"StartTime":20941.0,"Objects":[{"StartTime":20941.0,"Position":399.0,"HyperDash":false}]},{"StartTime":21254.0,"Objects":[{"StartTime":21254.0,"Position":240.0,"HyperDash":false},{"StartTime":21332.0,"Position":230.589066,"HyperDash":false},{"StartTime":21410.0,"Position":196.0483,"HyperDash":false},{"StartTime":21488.0,"Position":180.546143,"HyperDash":false},{"StartTime":21566.0,"Position":140.131409,"HyperDash":false},{"StartTime":21635.0,"Position":118.503845,"HyperDash":false},{"StartTime":21704.0,"Position":118.550331,"HyperDash":false},{"StartTime":21773.0,"Position":116.676407,"HyperDash":false},{"StartTime":21878.0,"Position":101.092834,"HyperDash":false}]},{"StartTime":22191.0,"Objects":[{"StartTime":22191.0,"Position":372.0,"HyperDash":false}]},{"StartTime":22504.0,"Objects":[{"StartTime":22504.0,"Position":386.0,"HyperDash":false},{"StartTime":22573.0,"Position":375.75766,"HyperDash":false},{"StartTime":22642.0,"Position":377.51532,"HyperDash":false},{"StartTime":22711.0,"Position":375.273,"HyperDash":false},{"StartTime":22816.0,"Position":398.469452,"HyperDash":false}]},{"StartTime":23129.0,"Objects":[{"StartTime":23129.0,"Position":264.0,"HyperDash":false},{"StartTime":23198.0,"Position":242.675385,"HyperDash":false},{"StartTime":23267.0,"Position":201.350769,"HyperDash":false},{"StartTime":23336.0,"Position":197.026169,"HyperDash":false},{"StartTime":23441.0,"Position":154.010468,"HyperDash":false}]},{"StartTime":23754.0,"Objects":[{"StartTime":23754.0,"Position":292.0,"HyperDash":false},{"StartTime":23832.0,"Position":331.63,"HyperDash":false},{"StartTime":23910.0,"Position":331.0578,"HyperDash":false},{"StartTime":23988.0,"Position":348.8628,"HyperDash":false},{"StartTime":24066.0,"Position":360.9073,"HyperDash":false},{"StartTime":24135.0,"Position":365.5993,"HyperDash":false},{"StartTime":24204.0,"Position":351.536926,"HyperDash":false},{"StartTime":24273.0,"Position":324.124176,"HyperDash":false},{"StartTime":24378.0,"Position":290.904083,"HyperDash":false}]},{"StartTime":24691.0,"Objects":[{"StartTime":24691.0,"Position":24.0,"HyperDash":false}]},{"StartTime":25004.0,"Objects":[{"StartTime":25004.0,"Position":285.0,"HyperDash":false},{"StartTime":25082.0,"Position":313.548859,"HyperDash":false},{"StartTime":25160.0,"Position":324.0977,"HyperDash":false},{"StartTime":25238.0,"Position":345.646545,"HyperDash":false},{"StartTime":25316.0,"Position":375.195374,"HyperDash":false},{"StartTime":25385.0,"Position":374.248322,"HyperDash":false},{"StartTime":25454.0,"Position":330.30127,"HyperDash":false},{"StartTime":25523.0,"Position":315.354218,"HyperDash":false},{"StartTime":25628.0,"Position":285.0,"HyperDash":false}]},{"StartTime":25941.0,"Objects":[{"StartTime":25941.0,"Position":465.0,"HyperDash":false}]},{"StartTime":26254.0,"Objects":[{"StartTime":26254.0,"Position":284.0,"HyperDash":false},{"StartTime":26332.0,"Position":297.848755,"HyperDash":false},{"StartTime":26410.0,"Position":271.5647,"HyperDash":false},{"StartTime":26488.0,"Position":314.9722,"HyperDash":false},{"StartTime":26566.0,"Position":313.667419,"HyperDash":false},{"StartTime":26635.0,"Position":327.281128,"HyperDash":false},{"StartTime":26704.0,"Position":359.75528,"HyperDash":false},{"StartTime":26773.0,"Position":373.334259,"HyperDash":false},{"StartTime":26879.0,"Position":411.1939,"HyperDash":false}]},{"StartTime":27191.0,"Objects":[{"StartTime":27191.0,"Position":108.0,"HyperDash":false}]},{"StartTime":27504.0,"Objects":[{"StartTime":27504.0,"Position":124.0,"HyperDash":false},{"StartTime":27573.0,"Position":136.610931,"HyperDash":false},{"StartTime":27642.0,"Position":107.221848,"HyperDash":false},{"StartTime":27711.0,"Position":100.832764,"HyperDash":false},{"StartTime":27816.0,"Position":113.197212,"HyperDash":false}]},{"StartTime":28129.0,"Objects":[{"StartTime":28129.0,"Position":250.0,"HyperDash":false},{"StartTime":28198.0,"Position":219.04863,"HyperDash":false},{"StartTime":28267.0,"Position":184.097244,"HyperDash":false},{"StartTime":28336.0,"Position":174.145874,"HyperDash":false},{"StartTime":28441.0,"Position":141.69812,"HyperDash":false}]},{"StartTime":28754.0,"Objects":[{"StartTime":28754.0,"Position":284.0,"HyperDash":false},{"StartTime":28832.0,"Position":316.245941,"HyperDash":false},{"StartTime":28910.0,"Position":338.3189,"HyperDash":false},{"StartTime":28988.0,"Position":359.1247,"HyperDash":false},{"StartTime":29066.0,"Position":381.5727,"HyperDash":false},{"StartTime":29135.0,"Position":393.2293,"HyperDash":false},{"StartTime":29204.0,"Position":421.124542,"HyperDash":false},{"StartTime":29273.0,"Position":400.894043,"HyperDash":false},{"StartTime":29379.0,"Position":415.7917,"HyperDash":false}]},{"StartTime":29691.0,"Objects":[{"StartTime":29691.0,"Position":135.0,"HyperDash":false}]},{"StartTime":30004.0,"Objects":[{"StartTime":30004.0,"Position":416.0,"HyperDash":false}]},{"StartTime":30316.0,"Objects":[{"StartTime":30316.0,"Position":456.0,"HyperDash":false}]},{"StartTime":30629.0,"Objects":[{"StartTime":30629.0,"Position":294.0,"HyperDash":false},{"StartTime":30698.0,"Position":268.673065,"HyperDash":false},{"StartTime":30767.0,"Position":233.346161,"HyperDash":false},{"StartTime":30836.0,"Position":224.019226,"HyperDash":false},{"StartTime":30941.0,"Position":184.0,"HyperDash":false}]},{"StartTime":31254.0,"Objects":[{"StartTime":31254.0,"Position":351.0,"HyperDash":false},{"StartTime":31332.0,"Position":388.3603,"HyperDash":false},{"StartTime":31410.0,"Position":400.4129,"HyperDash":false},{"StartTime":31488.0,"Position":406.2265,"HyperDash":false},{"StartTime":31566.0,"Position":441.396118,"HyperDash":false},{"StartTime":31635.0,"Position":452.909637,"HyperDash":false},{"StartTime":31704.0,"Position":447.317657,"HyperDash":false},{"StartTime":31773.0,"Position":421.671265,"HyperDash":false},{"StartTime":31878.0,"Position":416.158752,"HyperDash":false}]},{"StartTime":32191.0,"Objects":[{"StartTime":32191.0,"Position":149.0,"HyperDash":false}]},{"StartTime":32504.0,"Objects":[{"StartTime":32504.0,"Position":144.0,"HyperDash":false},{"StartTime":32582.0,"Position":152.122482,"HyperDash":false},{"StartTime":32660.0,"Position":187.244965,"HyperDash":false},{"StartTime":32738.0,"Position":234.367462,"HyperDash":false},{"StartTime":32816.0,"Position":252.489944,"HyperDash":false},{"StartTime":32885.0,"Position":292.4829,"HyperDash":false},{"StartTime":32954.0,"Position":314.4759,"HyperDash":false},{"StartTime":33023.0,"Position":317.468872,"HyperDash":false},{"StartTime":33129.0,"Position":361.327637,"HyperDash":false}]},{"StartTime":33440.0,"Objects":[{"StartTime":33440.0,"Position":201.0,"HyperDash":false}]},{"StartTime":33597.0,"Objects":[{"StartTime":33597.0,"Position":140.0,"HyperDash":false},{"StartTime":33675.0,"Position":97.84354,"HyperDash":false},{"StartTime":33753.0,"Position":83.79872,"HyperDash":false},{"StartTime":33831.0,"Position":111.743484,"HyperDash":false},{"StartTime":33909.0,"Position":105.502647,"HyperDash":false},{"StartTime":33969.0,"Position":122.537575,"HyperDash":false},{"StartTime":34065.0,"Position":148.435272,"HyperDash":false}]},{"StartTime":34379.0,"Objects":[{"StartTime":34379.0,"Position":239.0,"HyperDash":false},{"StartTime":34448.0,"Position":211.884613,"HyperDash":false},{"StartTime":34517.0,"Position":177.769226,"HyperDash":false},{"StartTime":34586.0,"Position":165.653839,"HyperDash":false},{"StartTime":34691.0,"Position":129.956512,"HyperDash":false}]},{"StartTime":35004.0,"Objects":[{"StartTime":35004.0,"Position":264.0,"HyperDash":false},{"StartTime":35073.0,"Position":298.150146,"HyperDash":false},{"StartTime":35142.0,"Position":294.300323,"HyperDash":false},{"StartTime":35211.0,"Position":332.45047,"HyperDash":false},{"StartTime":35316.0,"Position":373.2007,"HyperDash":false}]},{"StartTime":35629.0,"Objects":[{"StartTime":35629.0,"Position":223.0,"HyperDash":false},{"StartTime":35698.0,"Position":205.019867,"HyperDash":false},{"StartTime":35767.0,"Position":232.039749,"HyperDash":false},{"StartTime":35836.0,"Position":201.059616,"HyperDash":false},{"StartTime":35941.0,"Position":218.568115,"HyperDash":false}]},{"StartTime":36254.0,"Objects":[{"StartTime":36254.0,"Position":379.0,"HyperDash":false}]},{"StartTime":37035.0,"Objects":[{"StartTime":37035.0,"Position":398.0,"HyperDash":false},{"StartTime":37134.0,"Position":416.853973,"HyperDash":false},{"StartTime":37269.0,"Position":402.3821,"HyperDash":false}]},{"StartTime":37504.0,"Objects":[{"StartTime":37504.0,"Position":284.0,"HyperDash":false},{"StartTime":37573.0,"Position":268.747223,"HyperDash":false},{"StartTime":37642.0,"Position":235.494461,"HyperDash":false},{"StartTime":37711.0,"Position":210.2417,"HyperDash":false},{"StartTime":37816.0,"Position":174.335327,"HyperDash":false}]},{"StartTime":38129.0,"Objects":[{"StartTime":38129.0,"Position":305.0,"HyperDash":false},{"StartTime":38198.0,"Position":341.240051,"HyperDash":false},{"StartTime":38267.0,"Position":355.480072,"HyperDash":false},{"StartTime":38336.0,"Position":378.7201,"HyperDash":false},{"StartTime":38441.0,"Position":414.607117,"HyperDash":false}]},{"StartTime":38597.0,"Objects":[{"StartTime":38597.0,"Position":415.0,"HyperDash":false},{"StartTime":38675.0,"Position":374.605652,"HyperDash":false},{"StartTime":38753.0,"Position":363.2113,"HyperDash":false},{"StartTime":38831.0,"Position":320.816956,"HyperDash":false},{"StartTime":38909.0,"Position":305.4226,"HyperDash":false},{"StartTime":38969.0,"Position":265.350037,"HyperDash":false},{"StartTime":39065.0,"Position":250.633942,"HyperDash":false}]},{"StartTime":39379.0,"Objects":[{"StartTime":39379.0,"Position":113.0,"HyperDash":false},{"StartTime":39448.0,"Position":113.075348,"HyperDash":false},{"StartTime":39517.0,"Position":104.150688,"HyperDash":false},{"StartTime":39586.0,"Position":88.2260361,"HyperDash":false},{"StartTime":39691.0,"Position":104.297211,"HyperDash":false}]},{"StartTime":40004.0,"Objects":[{"StartTime":40004.0,"Position":385.0,"HyperDash":false}]},{"StartTime":40316.0,"Objects":[{"StartTime":40316.0,"Position":250.0,"HyperDash":false}]},{"StartTime":40629.0,"Objects":[{"StartTime":40629.0,"Position":256.0,"HyperDash":false}]},{"StartTime":40941.0,"Objects":[{"StartTime":40941.0,"Position":89.0,"HyperDash":false}]},{"StartTime":41254.0,"Objects":[{"StartTime":41254.0,"Position":256.0,"HyperDash":false},{"StartTime":41332.0,"Position":267.961151,"HyperDash":false},{"StartTime":41410.0,"Position":316.030029,"HyperDash":false},{"StartTime":41488.0,"Position":332.626129,"HyperDash":false},{"StartTime":41566.0,"Position":354.210022,"HyperDash":false},{"StartTime":41644.0,"Position":353.98996,"HyperDash":false},{"StartTime":41722.0,"Position":371.766144,"HyperDash":false},{"StartTime":41800.0,"Position":358.108276,"HyperDash":false},{"StartTime":41879.0,"Position":354.210022,"HyperDash":false},{"StartTime":41948.0,"Position":326.1343,"HyperDash":false},{"StartTime":42017.0,"Position":316.00824,"HyperDash":false},{"StartTime":42086.0,"Position":278.452332,"HyperDash":false},{"StartTime":42191.0,"Position":256.0,"HyperDash":false}]},{"StartTime":42504.0,"Objects":[{"StartTime":42504.0,"Position":98.0,"HyperDash":false},{"StartTime":42582.0,"Position":125.961151,"HyperDash":false},{"StartTime":42660.0,"Position":167.030014,"HyperDash":false},{"StartTime":42738.0,"Position":165.626129,"HyperDash":false},{"StartTime":42816.0,"Position":196.210022,"HyperDash":false},{"StartTime":42894.0,"Position":225.98996,"HyperDash":false},{"StartTime":42972.0,"Position":213.766159,"HyperDash":false},{"StartTime":43050.0,"Position":190.108276,"HyperDash":false},{"StartTime":43129.0,"Position":196.210022,"HyperDash":false},{"StartTime":43198.0,"Position":197.134308,"HyperDash":false},{"StartTime":43267.0,"Position":141.00824,"HyperDash":false},{"StartTime":43336.0,"Position":118.452332,"HyperDash":false},{"StartTime":43441.0,"Position":98.0,"HyperDash":false}]},{"StartTime":43754.0,"Objects":[{"StartTime":43754.0,"Position":249.0,"HyperDash":false},{"StartTime":43832.0,"Position":233.529724,"HyperDash":false},{"StartTime":43910.0,"Position":206.059448,"HyperDash":false},{"StartTime":43988.0,"Position":181.589172,"HyperDash":false},{"StartTime":44066.0,"Position":139.1189,"HyperDash":false},{"StartTime":44144.0,"Position":97.64862,"HyperDash":false},{"StartTime":44222.0,"Position":84.00227,"HyperDash":false},{"StartTime":44300.0,"Position":107.296448,"HyperDash":false},{"StartTime":44378.0,"Position":138.766724,"HyperDash":false},{"StartTime":44447.0,"Position":162.067352,"HyperDash":false},{"StartTime":44516.0,"Position":187.367981,"HyperDash":false},{"StartTime":44585.0,"Position":204.66861,"HyperDash":false},{"StartTime":44691.0,"Position":249.0,"HyperDash":false}]},{"StartTime":45004.0,"Objects":[{"StartTime":45004.0,"Position":466.0,"HyperDash":false},{"StartTime":45062.0,"Position":56.0,"HyperDash":false},{"StartTime":45121.0,"Position":109.0,"HyperDash":false},{"StartTime":45179.0,"Position":482.0,"HyperDash":false},{"StartTime":45238.0,"Position":147.0,"HyperDash":false},{"StartTime":45296.0,"Position":285.0,"HyperDash":false},{"StartTime":45355.0,"Position":452.0,"HyperDash":false},{"StartTime":45413.0,"Position":419.0,"HyperDash":false},{"StartTime":45472.0,"Position":269.0,"HyperDash":false},{"StartTime":45531.0,"Position":249.0,"HyperDash":false},{"StartTime":45589.0,"Position":233.0,"HyperDash":false},{"StartTime":45648.0,"Position":449.0,"HyperDash":false},{"StartTime":45706.0,"Position":411.0,"HyperDash":false},{"StartTime":45765.0,"Position":75.0,"HyperDash":false},{"StartTime":45823.0,"Position":474.0,"HyperDash":false},{"StartTime":45882.0,"Position":176.0,"HyperDash":false},{"StartTime":45941.0,"Position":1.0,"HyperDash":false}]},{"StartTime":46254.0,"Objects":[{"StartTime":46254.0,"Position":332.0,"HyperDash":false},{"StartTime":46332.0,"Position":341.35257,"HyperDash":false},{"StartTime":46410.0,"Position":382.281,"HyperDash":false},{"StartTime":46488.0,"Position":401.661774,"HyperDash":false},{"StartTime":46566.0,"Position":424.539063,"HyperDash":false},{"StartTime":46644.0,"Position":451.522461,"HyperDash":false},{"StartTime":46722.0,"Position":436.847626,"HyperDash":false},{"StartTime":46800.0,"Position":445.602142,"HyperDash":false},{"StartTime":46879.0,"Position":424.539063,"HyperDash":false},{"StartTime":46948.0,"Position":396.89386,"HyperDash":false},{"StartTime":47017.0,"Position":399.831055,"HyperDash":false},{"StartTime":47086.0,"Position":370.600555,"HyperDash":false},{"StartTime":47191.0,"Position":332.0,"HyperDash":false}]},{"StartTime":47504.0,"Objects":[{"StartTime":47504.0,"Position":180.0,"HyperDash":false},{"StartTime":47582.0,"Position":148.647415,"HyperDash":false},{"StartTime":47660.0,"Position":119.718979,"HyperDash":false},{"StartTime":47738.0,"Position":115.338226,"HyperDash":false},{"StartTime":47816.0,"Position":87.4609146,"HyperDash":false},{"StartTime":47894.0,"Position":96.4775,"HyperDash":false},{"StartTime":47972.0,"Position":75.15235,"HyperDash":false},{"StartTime":48050.0,"Position":64.3978348,"HyperDash":false},{"StartTime":48129.0,"Position":87.4609146,"HyperDash":false},{"StartTime":48198.0,"Position":116.106155,"HyperDash":false},{"StartTime":48267.0,"Position":126.168938,"HyperDash":false},{"StartTime":48336.0,"Position":133.399445,"HyperDash":false},{"StartTime":48441.0,"Position":180.0,"HyperDash":false}]},{"StartTime":48754.0,"Objects":[{"StartTime":48754.0,"Position":335.0,"HyperDash":false},{"StartTime":48832.0,"Position":313.529724,"HyperDash":false},{"StartTime":48910.0,"Position":282.059448,"HyperDash":false},{"StartTime":48988.0,"Position":233.589188,"HyperDash":false},{"StartTime":49066.0,"Position":225.118927,"HyperDash":false},{"StartTime":49144.0,"Position":202.648651,"HyperDash":false},{"StartTime":49222.0,"Position":170.002289,"HyperDash":false},{"StartTime":49300.0,"Position":213.296478,"HyperDash":false},{"StartTime":49379.0,"Position":225.118927,"HyperDash":false},{"StartTime":49448.0,"Position":239.41954,"HyperDash":false},{"StartTime":49517.0,"Position":280.720154,"HyperDash":false},{"StartTime":49586.0,"Position":280.020782,"HyperDash":false},{"StartTime":49691.0,"Position":335.0,"HyperDash":false}]},{"StartTime":50004.0,"Objects":[{"StartTime":50004.0,"Position":93.0,"HyperDash":false},{"StartTime":50062.0,"Position":267.0,"HyperDash":false},{"StartTime":50121.0,"Position":276.0,"HyperDash":false},{"StartTime":50179.0,"Position":367.0,"HyperDash":false},{"StartTime":50238.0,"Position":409.0,"HyperDash":false},{"StartTime":50296.0,"Position":117.0,"HyperDash":false},{"StartTime":50355.0,"Position":226.0,"HyperDash":false},{"StartTime":50413.0,"Position":469.0,"HyperDash":false},{"StartTime":50472.0,"Position":267.0,"HyperDash":false},{"StartTime":50531.0,"Position":477.0,"HyperDash":false},{"StartTime":50589.0,"Position":282.0,"HyperDash":false},{"StartTime":50648.0,"Position":216.0,"HyperDash":false},{"StartTime":50706.0,"Position":106.0,"HyperDash":false},{"StartTime":50765.0,"Position":353.0,"HyperDash":false},{"StartTime":50823.0,"Position":162.0,"HyperDash":false},{"StartTime":50882.0,"Position":473.0,"HyperDash":false},{"StartTime":50941.0,"Position":260.0,"HyperDash":false}]},{"StartTime":51254.0,"Objects":[{"StartTime":51254.0,"Position":119.0,"HyperDash":false},{"StartTime":51353.0,"Position":121.8725,"HyperDash":false},{"StartTime":51488.0,"Position":106.880455,"HyperDash":false}]},{"StartTime":51722.0,"Objects":[{"StartTime":51722.0,"Position":230.0,"HyperDash":false},{"StartTime":51800.0,"Position":213.529388,"HyperDash":false},{"StartTime":51878.0,"Position":222.058777,"HyperDash":false},{"StartTime":51956.0,"Position":243.588165,"HyperDash":false},{"StartTime":52034.0,"Position":244.117569,"HyperDash":false},{"StartTime":52094.0,"Position":256.8325,"HyperDash":false},{"StartTime":52190.0,"Position":251.176346,"HyperDash":false}]},{"StartTime":52504.0,"Objects":[{"StartTime":52504.0,"Position":373.0,"HyperDash":false},{"StartTime":52603.0,"Position":384.1275,"HyperDash":false},{"StartTime":52738.0,"Position":385.119537,"HyperDash":false}]},{"StartTime":52972.0,"Objects":[{"StartTime":52972.0,"Position":269.0,"HyperDash":false},{"StartTime":53050.0,"Position":243.9549,"HyperDash":false},{"StartTime":53128.0,"Position":227.363922,"HyperDash":false},{"StartTime":53206.0,"Position":246.705673,"HyperDash":false},{"StartTime":53284.0,"Position":238.662781,"HyperDash":false},{"StartTime":53344.0,"Position":267.1444,"HyperDash":false},{"StartTime":53440.0,"Position":274.832428,"HyperDash":false}]},{"StartTime":53754.0,"Objects":[{"StartTime":53754.0,"Position":424.0,"HyperDash":false},{"StartTime":53853.0,"Position":394.183075,"HyperDash":false},{"StartTime":53988.0,"Position":341.705444,"HyperDash":false}]},{"StartTime":54222.0,"Objects":[{"StartTime":54222.0,"Position":228.0,"HyperDash":false},{"StartTime":54300.0,"Position":248.405014,"HyperDash":false},{"StartTime":54378.0,"Position":299.810028,"HyperDash":false},{"StartTime":54456.0,"Position":312.215027,"HyperDash":false},{"StartTime":54534.0,"Position":337.620056,"HyperDash":false},{"StartTime":54594.0,"Position":374.7008,"HyperDash":false},{"StartTime":54690.0,"Position":392.430054,"HyperDash":false}]},{"StartTime":55004.0,"Objects":[{"StartTime":55004.0,"Position":241.0,"HyperDash":false},{"StartTime":55103.0,"Position":294.816925,"HyperDash":false},{"StartTime":55238.0,"Position":323.294556,"HyperDash":false}]},{"StartTime":55472.0,"Objects":[{"StartTime":55472.0,"Position":437.0,"HyperDash":false},{"StartTime":55550.0,"Position":398.595,"HyperDash":false},{"StartTime":55628.0,"Position":394.189972,"HyperDash":false},{"StartTime":55706.0,"Position":340.784973,"HyperDash":false},{"StartTime":55784.0,"Position":327.379944,"HyperDash":false},{"StartTime":55844.0,"Position":318.2992,"HyperDash":false},{"StartTime":55940.0,"Position":272.569946,"HyperDash":false}]},{"StartTime":56254.0,"Objects":[{"StartTime":56254.0,"Position":3.0,"HyperDash":false}]},{"StartTime":56488.0,"Objects":[{"StartTime":56488.0,"Position":260.0,"HyperDash":false},{"StartTime":56587.0,"Position":252.118042,"HyperDash":false},{"StartTime":56722.0,"Position":269.733521,"HyperDash":false}]},{"StartTime":56957.0,"Objects":[{"StartTime":56957.0,"Position":162.0,"HyperDash":false},{"StartTime":57056.0,"Position":138.870941,"HyperDash":false},{"StartTime":57191.0,"Position":81.3313,"HyperDash":false}]},{"StartTime":57504.0,"Objects":[{"StartTime":57504.0,"Position":402.0,"HyperDash":false}]},{"StartTime":57738.0,"Objects":[{"StartTime":57738.0,"Position":363.0,"HyperDash":false},{"StartTime":57837.0,"Position":344.432251,"HyperDash":false},{"StartTime":57972.0,"Position":281.2944,"HyperDash":false}]},{"StartTime":58207.0,"Objects":[{"StartTime":58207.0,"Position":174.0,"HyperDash":false},{"StartTime":58306.0,"Position":158.870941,"HyperDash":false},{"StartTime":58441.0,"Position":93.3313,"HyperDash":false}]},{"StartTime":58754.0,"Objects":[{"StartTime":58754.0,"Position":261.0,"HyperDash":false},{"StartTime":58832.0,"Position":243.6731,"HyperDash":false},{"StartTime":58910.0,"Position":205.346176,"HyperDash":false},{"StartTime":58988.0,"Position":162.019257,"HyperDash":false},{"StartTime":59066.0,"Position":151.692352,"HyperDash":false},{"StartTime":59144.0,"Position":119.365463,"HyperDash":false},{"StartTime":59222.0,"Position":96.86337,"HyperDash":false},{"StartTime":59300.0,"Position":110.015121,"HyperDash":false},{"StartTime":59379.0,"Position":151.692352,"HyperDash":false},{"StartTime":59448.0,"Position":182.866165,"HyperDash":false},{"StartTime":59517.0,"Position":214.039978,"HyperDash":false},{"StartTime":59586.0,"Position":217.213776,"HyperDash":false},{"StartTime":59691.0,"Position":261.0,"HyperDash":false}]},{"StartTime":60004.0,"Objects":[{"StartTime":60004.0,"Position":456.0,"HyperDash":false},{"StartTime":60062.0,"Position":371.0,"HyperDash":false},{"StartTime":60121.0,"Position":73.0,"HyperDash":false},{"StartTime":60179.0,"Position":190.0,"HyperDash":false},{"StartTime":60238.0,"Position":180.0,"HyperDash":false},{"StartTime":60296.0,"Position":461.0,"HyperDash":false},{"StartTime":60355.0,"Position":433.0,"HyperDash":false},{"StartTime":60413.0,"Position":275.0,"HyperDash":false},{"StartTime":60472.0,"Position":395.0,"HyperDash":false},{"StartTime":60531.0,"Position":473.0,"HyperDash":false},{"StartTime":60589.0,"Position":192.0,"HyperDash":false},{"StartTime":60648.0,"Position":362.0,"HyperDash":false},{"StartTime":60706.0,"Position":7.0,"HyperDash":false},{"StartTime":60765.0,"Position":500.0,"HyperDash":false},{"StartTime":60823.0,"Position":487.0,"HyperDash":false},{"StartTime":60882.0,"Position":487.0,"HyperDash":false},{"StartTime":60941.0,"Position":213.0,"HyperDash":false}]},{"StartTime":71254.0,"Objects":[{"StartTime":71254.0,"Position":258.0,"HyperDash":false}]},{"StartTime":71722.0,"Objects":[{"StartTime":71722.0,"Position":69.0,"HyperDash":false},{"StartTime":71800.0,"Position":67.48298,"HyperDash":false},{"StartTime":71878.0,"Position":70.96595,"HyperDash":false},{"StartTime":71956.0,"Position":82.44893,"HyperDash":false},{"StartTime":72034.0,"Position":62.9319038,"HyperDash":false},{"StartTime":72094.0,"Position":61.76496,"HyperDash":false},{"StartTime":72190.0,"Position":59.8978577,"HyperDash":false}]},{"StartTime":72504.0,"Objects":[{"StartTime":72504.0,"Position":381.0,"HyperDash":false}]},{"StartTime":73754.0,"Objects":[{"StartTime":73754.0,"Position":254.0,"HyperDash":false}]},{"StartTime":74222.0,"Objects":[{"StartTime":74222.0,"Position":443.0,"HyperDash":false},{"StartTime":74300.0,"Position":452.517029,"HyperDash":false},{"StartTime":74378.0,"Position":430.034058,"HyperDash":false},{"StartTime":74456.0,"Position":439.5511,"HyperDash":false},{"StartTime":74534.0,"Position":449.068085,"HyperDash":false},{"StartTime":74594.0,"Position":435.235046,"HyperDash":false},{"StartTime":74690.0,"Position":452.102142,"HyperDash":false}]},{"StartTime":75004.0,"Objects":[{"StartTime":75004.0,"Position":131.0,"HyperDash":false}]},{"StartTime":76254.0,"Objects":[{"StartTime":76254.0,"Position":136.0,"HyperDash":false}]},{"StartTime":76722.0,"Objects":[{"StartTime":76722.0,"Position":349.0,"HyperDash":false},{"StartTime":76800.0,"Position":304.5004,"HyperDash":false},{"StartTime":76878.0,"Position":313.000824,"HyperDash":false},{"StartTime":76956.0,"Position":264.501221,"HyperDash":false},{"StartTime":77034.0,"Position":239.001617,"HyperDash":false},{"StartTime":77094.0,"Position":209.848083,"HyperDash":false},{"StartTime":77190.0,"Position":184.002426,"HyperDash":false}]},{"StartTime":77504.0,"Objects":[{"StartTime":77504.0,"Position":350.0,"HyperDash":false}]},{"StartTime":78754.0,"Objects":[{"StartTime":78754.0,"Position":376.0,"HyperDash":false}]},{"StartTime":79222.0,"Objects":[{"StartTime":79222.0,"Position":163.0,"HyperDash":false},{"StartTime":79300.0,"Position":186.499588,"HyperDash":false},{"StartTime":79378.0,"Position":226.999191,"HyperDash":false},{"StartTime":79456.0,"Position":256.498779,"HyperDash":false},{"StartTime":79534.0,"Position":272.998383,"HyperDash":false},{"StartTime":79594.0,"Position":280.151917,"HyperDash":false},{"StartTime":79690.0,"Position":327.997559,"HyperDash":false}]},{"StartTime":80004.0,"Objects":[{"StartTime":80004.0,"Position":11.0,"HyperDash":false}]},{"StartTime":80316.0,"Objects":[{"StartTime":80316.0,"Position":165.0,"HyperDash":false}]},{"StartTime":80629.0,"Objects":[{"StartTime":80629.0,"Position":11.0,"HyperDash":false}]},{"StartTime":80941.0,"Objects":[{"StartTime":80941.0,"Position":192.0,"HyperDash":false}]},{"StartTime":81254.0,"Objects":[{"StartTime":81254.0,"Position":336.0,"HyperDash":false},{"StartTime":81323.0,"Position":296.6767,"HyperDash":false},{"StartTime":81392.0,"Position":293.3534,"HyperDash":false},{"StartTime":81461.0,"Position":281.03006,"HyperDash":false},{"StartTime":81566.0,"Position":226.016357,"HyperDash":false}]},{"StartTime":81879.0,"Objects":[{"StartTime":81879.0,"Position":366.0,"HyperDash":false},{"StartTime":81948.0,"Position":396.3233,"HyperDash":false},{"StartTime":82017.0,"Position":429.6466,"HyperDash":false},{"StartTime":82086.0,"Position":448.96994,"HyperDash":false},{"StartTime":82191.0,"Position":475.983643,"HyperDash":false}]},{"StartTime":82504.0,"Objects":[{"StartTime":82504.0,"Position":156.0,"HyperDash":false}]},{"StartTime":82816.0,"Objects":[{"StartTime":82816.0,"Position":292.0,"HyperDash":false}]},{"StartTime":83129.0,"Objects":[{"StartTime":83129.0,"Position":100.0,"HyperDash":false}]},{"StartTime":83441.0,"Objects":[{"StartTime":83441.0,"Position":248.0,"HyperDash":false}]},{"StartTime":83754.0,"Objects":[{"StartTime":83754.0,"Position":390.0,"HyperDash":false},{"StartTime":83832.0,"Position":372.544983,"HyperDash":false},{"StartTime":83910.0,"Position":344.089935,"HyperDash":false},{"StartTime":83988.0,"Position":308.634918,"HyperDash":false},{"StartTime":84066.0,"Position":280.003876,"HyperDash":false},{"StartTime":84135.0,"Position":301.115051,"HyperDash":false},{"StartTime":84204.0,"Position":341.4022,"HyperDash":false},{"StartTime":84273.0,"Position":359.6893,"HyperDash":false},{"StartTime":84379.0,"Position":390.0,"HyperDash":false}]},{"StartTime":85004.0,"Objects":[{"StartTime":85004.0,"Position":104.0,"HyperDash":false}]},{"StartTime":86254.0,"Objects":[{"StartTime":86254.0,"Position":324.0,"HyperDash":false}]},{"StartTime":86566.0,"Objects":[{"StartTime":86566.0,"Position":422.0,"HyperDash":false}]},{"StartTime":86879.0,"Objects":[{"StartTime":86879.0,"Position":470.0,"HyperDash":false}]},{"StartTime":87191.0,"Objects":[{"StartTime":87191.0,"Position":352.0,"HyperDash":false}]},{"StartTime":87504.0,"Objects":[{"StartTime":87504.0,"Position":287.0,"HyperDash":false},{"StartTime":87573.0,"Position":317.323242,"HyperDash":false},{"StartTime":87642.0,"Position":340.646484,"HyperDash":false},{"StartTime":87711.0,"Position":343.969727,"HyperDash":false},{"StartTime":87816.0,"Position":396.983368,"HyperDash":false}]},{"StartTime":88129.0,"Objects":[{"StartTime":88129.0,"Position":265.0,"HyperDash":false},{"StartTime":88198.0,"Position":237.676239,"HyperDash":false},{"StartTime":88267.0,"Position":223.352478,"HyperDash":false},{"StartTime":88336.0,"Position":206.028717,"HyperDash":false},{"StartTime":88441.0,"Position":155.014313,"HyperDash":false}]},{"StartTime":88754.0,"Objects":[{"StartTime":88754.0,"Position":475.0,"HyperDash":false}]},{"StartTime":89066.0,"Objects":[{"StartTime":89066.0,"Position":341.0,"HyperDash":false}]},{"StartTime":89379.0,"Objects":[{"StartTime":89379.0,"Position":432.0,"HyperDash":false}]},{"StartTime":89691.0,"Objects":[{"StartTime":89691.0,"Position":264.0,"HyperDash":false}]},{"StartTime":90004.0,"Objects":[{"StartTime":90004.0,"Position":255.0,"HyperDash":false},{"StartTime":90062.0,"Position":294.0,"HyperDash":false},{"StartTime":90121.0,"Position":354.0,"HyperDash":false},{"StartTime":90179.0,"Position":270.0,"HyperDash":false},{"StartTime":90238.0,"Position":362.0,"HyperDash":false},{"StartTime":90296.0,"Position":255.0,"HyperDash":false},{"StartTime":90355.0,"Position":203.0,"HyperDash":false},{"StartTime":90413.0,"Position":67.0,"HyperDash":false},{"StartTime":90472.0,"Position":112.0,"HyperDash":false},{"StartTime":90531.0,"Position":326.0,"HyperDash":false},{"StartTime":90589.0,"Position":219.0,"HyperDash":false},{"StartTime":90648.0,"Position":351.0,"HyperDash":false},{"StartTime":90706.0,"Position":477.0,"HyperDash":false},{"StartTime":90765.0,"Position":439.0,"HyperDash":false},{"StartTime":90823.0,"Position":471.0,"HyperDash":false},{"StartTime":90882.0,"Position":449.0,"HyperDash":false},{"StartTime":90941.0,"Position":295.0,"HyperDash":false}]},{"StartTime":91254.0,"Objects":[{"StartTime":91254.0,"Position":140.0,"HyperDash":false},{"StartTime":91332.0,"Position":109.180054,"HyperDash":false},{"StartTime":91410.0,"Position":88.36357,"HyperDash":false},{"StartTime":91488.0,"Position":92.07944,"HyperDash":false},{"StartTime":91566.0,"Position":69.79061,"HyperDash":false},{"StartTime":91635.0,"Position":78.01627,"HyperDash":false},{"StartTime":91704.0,"Position":103.148987,"HyperDash":false},{"StartTime":91773.0,"Position":121.717133,"HyperDash":false},{"StartTime":91878.0,"Position":140.090958,"HyperDash":false}]},{"StartTime":92191.0,"Objects":[{"StartTime":92191.0,"Position":380.0,"HyperDash":false}]},{"StartTime":92504.0,"Objects":[{"StartTime":92504.0,"Position":405.0,"HyperDash":false},{"StartTime":92573.0,"Position":381.6738,"HyperDash":false},{"StartTime":92642.0,"Position":369.347565,"HyperDash":false},{"StartTime":92711.0,"Position":341.021362,"HyperDash":false},{"StartTime":92816.0,"Position":295.0032,"HyperDash":false}]},{"StartTime":93129.0,"Objects":[{"StartTime":93129.0,"Position":154.0,"HyperDash":false},{"StartTime":93198.0,"Position":177.324478,"HyperDash":false},{"StartTime":93267.0,"Position":215.648956,"HyperDash":false},{"StartTime":93336.0,"Position":211.973434,"HyperDash":false},{"StartTime":93441.0,"Position":263.988922,"HyperDash":false}]},{"StartTime":93754.0,"Objects":[{"StartTime":93754.0,"Position":135.0,"HyperDash":false},{"StartTime":93832.0,"Position":153.455765,"HyperDash":false},{"StartTime":93910.0,"Position":177.911545,"HyperDash":false},{"StartTime":93988.0,"Position":219.36731,"HyperDash":false},{"StartTime":94066.0,"Position":244.82309,"HyperDash":false},{"StartTime":94135.0,"Position":274.1109,"HyperDash":false},{"StartTime":94204.0,"Position":309.398682,"HyperDash":false},{"StartTime":94273.0,"Position":297.686462,"HyperDash":false},{"StartTime":94379.0,"Position":354.998169,"HyperDash":false}]},{"StartTime":94691.0,"Objects":[{"StartTime":94691.0,"Position":98.0,"HyperDash":false}]},{"StartTime":95004.0,"Objects":[{"StartTime":95004.0,"Position":354.0,"HyperDash":false},{"StartTime":95073.0,"Position":330.775818,"HyperDash":false},{"StartTime":95142.0,"Position":308.551636,"HyperDash":false},{"StartTime":95211.0,"Position":298.327454,"HyperDash":false},{"StartTime":95316.0,"Position":244.464569,"HyperDash":false}]},{"StartTime":95629.0,"Objects":[{"StartTime":95629.0,"Position":97.0,"HyperDash":false},{"StartTime":95698.0,"Position":100.173164,"HyperDash":false},{"StartTime":95767.0,"Position":91.34632,"HyperDash":false},{"StartTime":95836.0,"Position":85.5194855,"HyperDash":false},{"StartTime":95941.0,"Position":79.69604,"HyperDash":false}]},{"StartTime":96254.0,"Objects":[{"StartTime":96254.0,"Position":238.0,"HyperDash":false},{"StartTime":96332.0,"Position":263.193329,"HyperDash":false},{"StartTime":96410.0,"Position":297.398376,"HyperDash":false},{"StartTime":96488.0,"Position":296.3632,"HyperDash":false},{"StartTime":96566.0,"Position":315.395416,"HyperDash":false},{"StartTime":96635.0,"Position":328.299622,"HyperDash":false},{"StartTime":96704.0,"Position":295.141235,"HyperDash":false},{"StartTime":96773.0,"Position":274.06,"HyperDash":false},{"StartTime":96878.0,"Position":241.776031,"HyperDash":false}]},{"StartTime":97191.0,"Objects":[{"StartTime":97191.0,"Position":497.0,"HyperDash":false}]},{"StartTime":97504.0,"Objects":[{"StartTime":97504.0,"Position":252.0,"HyperDash":false},{"StartTime":97582.0,"Position":214.922928,"HyperDash":false},{"StartTime":97660.0,"Position":177.845856,"HyperDash":false},{"StartTime":97738.0,"Position":185.7688,"HyperDash":false},{"StartTime":97816.0,"Position":143.518143,"HyperDash":false},{"StartTime":97885.0,"Position":175.297363,"HyperDash":false},{"StartTime":97954.0,"Position":187.250168,"HyperDash":false},{"StartTime":98023.0,"Position":222.202957,"HyperDash":false},{"StartTime":98129.0,"Position":252.0,"HyperDash":false}]},{"StartTime":98441.0,"Objects":[{"StartTime":98441.0,"Position":363.0,"HyperDash":false}]},{"StartTime":98754.0,"Objects":[{"StartTime":98754.0,"Position":223.0,"HyperDash":false},{"StartTime":98823.0,"Position":195.875366,"HyperDash":false},{"StartTime":98892.0,"Position":193.750732,"HyperDash":false},{"StartTime":98961.0,"Position":151.626083,"HyperDash":false},{"StartTime":99066.0,"Position":113.914696,"HyperDash":false}]},{"StartTime":99379.0,"Objects":[{"StartTime":99379.0,"Position":494.0,"HyperDash":false}]},{"StartTime":99691.0,"Objects":[{"StartTime":99691.0,"Position":298.0,"HyperDash":false}]},{"StartTime":100004.0,"Objects":[{"StartTime":100004.0,"Position":236.0,"HyperDash":false},{"StartTime":100082.0,"Position":203.256851,"HyperDash":false},{"StartTime":100160.0,"Position":168.009674,"HyperDash":false},{"StartTime":100238.0,"Position":177.094879,"HyperDash":false},{"StartTime":100316.0,"Position":141.201492,"HyperDash":false},{"StartTime":100385.0,"Position":127.635262,"HyperDash":false},{"StartTime":100454.0,"Position":110.29274,"HyperDash":false},{"StartTime":100523.0,"Position":110.453743,"HyperDash":false},{"StartTime":100628.0,"Position":102.687973,"HyperDash":false}]},{"StartTime":100941.0,"Objects":[{"StartTime":100941.0,"Position":349.0,"HyperDash":false}]},{"StartTime":101254.0,"Objects":[{"StartTime":101254.0,"Position":383.0,"HyperDash":false},{"StartTime":101332.0,"Position":419.957733,"HyperDash":false},{"StartTime":101410.0,"Position":426.299622,"HyperDash":false},{"StartTime":101488.0,"Position":445.490662,"HyperDash":false},{"StartTime":101566.0,"Position":455.829254,"HyperDash":false},{"StartTime":101635.0,"Position":451.601563,"HyperDash":false},{"StartTime":101704.0,"Position":456.379,"HyperDash":false},{"StartTime":101773.0,"Position":416.516266,"HyperDash":false},{"StartTime":101879.0,"Position":388.265564,"HyperDash":false}]},{"StartTime":102191.0,"Objects":[{"StartTime":102191.0,"Position":135.0,"HyperDash":false}]},{"StartTime":102504.0,"Objects":[{"StartTime":102504.0,"Position":123.0,"HyperDash":false},{"StartTime":102573.0,"Position":136.53656,"HyperDash":false},{"StartTime":102642.0,"Position":127.073128,"HyperDash":false},{"StartTime":102711.0,"Position":116.609695,"HyperDash":false},{"StartTime":102816.0,"Position":107.339249,"HyperDash":false}]},{"StartTime":103129.0,"Objects":[{"StartTime":103129.0,"Position":252.0,"HyperDash":false},{"StartTime":103198.0,"Position":284.326935,"HyperDash":false},{"StartTime":103267.0,"Position":309.653839,"HyperDash":false},{"StartTime":103336.0,"Position":338.980774,"HyperDash":false},{"StartTime":103441.0,"Position":362.0,"HyperDash":false}]},{"StartTime":103754.0,"Objects":[{"StartTime":103754.0,"Position":215.0,"HyperDash":false},{"StartTime":103832.0,"Position":174.651,"HyperDash":false},{"StartTime":103910.0,"Position":155.666367,"HyperDash":false},{"StartTime":103988.0,"Position":122.457794,"HyperDash":false},{"StartTime":104066.0,"Position":113.4155,"HyperDash":false},{"StartTime":104135.0,"Position":85.44563,"HyperDash":false},{"StartTime":104204.0,"Position":103.505188,"HyperDash":false},{"StartTime":104273.0,"Position":78.09056,"HyperDash":false},{"StartTime":104379.0,"Position":76.11086,"HyperDash":false}]},{"StartTime":104691.0,"Objects":[{"StartTime":104691.0,"Position":353.0,"HyperDash":false}]},{"StartTime":105004.0,"Objects":[{"StartTime":105004.0,"Position":359.0,"HyperDash":false},{"StartTime":105073.0,"Position":360.169861,"HyperDash":false},{"StartTime":105142.0,"Position":374.3397,"HyperDash":false},{"StartTime":105211.0,"Position":369.509552,"HyperDash":false},{"StartTime":105316.0,"Position":368.8115,"HyperDash":false}]},{"StartTime":105629.0,"Objects":[{"StartTime":105629.0,"Position":215.0,"HyperDash":false},{"StartTime":105698.0,"Position":245.2746,"HyperDash":false},{"StartTime":105767.0,"Position":263.5492,"HyperDash":false},{"StartTime":105836.0,"Position":298.8238,"HyperDash":false},{"StartTime":105941.0,"Position":324.7634,"HyperDash":false}]},{"StartTime":106254.0,"Objects":[{"StartTime":106254.0,"Position":164.0,"HyperDash":false},{"StartTime":106332.0,"Position":181.330521,"HyperDash":false},{"StartTime":106410.0,"Position":231.661041,"HyperDash":false},{"StartTime":106488.0,"Position":258.991547,"HyperDash":false},{"StartTime":106566.0,"Position":273.322083,"HyperDash":false},{"StartTime":106635.0,"Position":308.499084,"HyperDash":false},{"StartTime":106704.0,"Position":315.6761,"HyperDash":false},{"StartTime":106773.0,"Position":330.8531,"HyperDash":false},{"StartTime":106878.0,"Position":382.644135,"HyperDash":false}]},{"StartTime":107191.0,"Objects":[{"StartTime":107191.0,"Position":64.0,"HyperDash":false}]},{"StartTime":107504.0,"Objects":[{"StartTime":107504.0,"Position":390.0,"HyperDash":false},{"StartTime":107582.0,"Position":367.983734,"HyperDash":false},{"StartTime":107660.0,"Position":346.967468,"HyperDash":false},{"StartTime":107738.0,"Position":308.9512,"HyperDash":false},{"StartTime":107816.0,"Position":281.934967,"HyperDash":false},{"StartTime":107885.0,"Position":292.833954,"HyperDash":false},{"StartTime":107954.0,"Position":315.732941,"HyperDash":false},{"StartTime":108023.0,"Position":344.631958,"HyperDash":false},{"StartTime":108128.0,"Position":390.0,"HyperDash":false}]},{"StartTime":108441.0,"Objects":[{"StartTime":108441.0,"Position":219.0,"HyperDash":false}]},{"StartTime":108754.0,"Objects":[{"StartTime":108754.0,"Position":99.0,"HyperDash":false},{"StartTime":108823.0,"Position":80.78087,"HyperDash":false},{"StartTime":108892.0,"Position":79.56174,"HyperDash":false},{"StartTime":108961.0,"Position":79.34261,"HyperDash":false},{"StartTime":109066.0,"Position":88.9656754,"HyperDash":false}]},{"StartTime":109379.0,"Objects":[{"StartTime":109379.0,"Position":228.0,"HyperDash":false},{"StartTime":109448.0,"Position":259.2614,"HyperDash":false},{"StartTime":109517.0,"Position":268.522858,"HyperDash":false},{"StartTime":109586.0,"Position":318.7843,"HyperDash":false},{"StartTime":109691.0,"Position":337.703827,"HyperDash":false}]},{"StartTime":110004.0,"Objects":[{"StartTime":110004.0,"Position":183.0,"HyperDash":false},{"StartTime":110082.0,"Position":220.45372,"HyperDash":false},{"StartTime":110160.0,"Position":245.90744,"HyperDash":false},{"StartTime":110238.0,"Position":248.361145,"HyperDash":false},{"StartTime":110316.0,"Position":292.81488,"HyperDash":false},{"StartTime":110385.0,"Position":310.10083,"HyperDash":false},{"StartTime":110454.0,"Position":355.386841,"HyperDash":false},{"StartTime":110523.0,"Position":350.6728,"HyperDash":false},{"StartTime":110628.0,"Position":402.6297,"HyperDash":false}]},{"StartTime":110941.0,"Objects":[{"StartTime":110941.0,"Position":108.0,"HyperDash":false}]},{"StartTime":111254.0,"Objects":[{"StartTime":111254.0,"Position":114.0,"HyperDash":false},{"StartTime":111323.0,"Position":102.780869,"HyperDash":false},{"StartTime":111392.0,"Position":118.561737,"HyperDash":false},{"StartTime":111461.0,"Position":104.342613,"HyperDash":false},{"StartTime":111566.0,"Position":103.965675,"HyperDash":false}]},{"StartTime":111879.0,"Objects":[{"StartTime":111879.0,"Position":243.0,"HyperDash":false},{"StartTime":111948.0,"Position":284.2614,"HyperDash":false},{"StartTime":112017.0,"Position":284.522858,"HyperDash":false},{"StartTime":112086.0,"Position":327.7843,"HyperDash":false},{"StartTime":112191.0,"Position":352.703827,"HyperDash":false}]},{"StartTime":112504.0,"Objects":[{"StartTime":112504.0,"Position":198.0,"HyperDash":false},{"StartTime":112582.0,"Position":234.45372,"HyperDash":false},{"StartTime":112660.0,"Position":250.90744,"HyperDash":false},{"StartTime":112738.0,"Position":271.361145,"HyperDash":false},{"StartTime":112816.0,"Position":307.81488,"HyperDash":false},{"StartTime":112885.0,"Position":345.10083,"HyperDash":false},{"StartTime":112954.0,"Position":347.386841,"HyperDash":false},{"StartTime":113023.0,"Position":393.6728,"HyperDash":false},{"StartTime":113128.0,"Position":417.6297,"HyperDash":false}]},{"StartTime":113441.0,"Objects":[{"StartTime":113441.0,"Position":123.0,"HyperDash":false}]},{"StartTime":113754.0,"Objects":[{"StartTime":113754.0,"Position":151.0,"HyperDash":false}]},{"StartTime":113910.0,"Objects":[{"StartTime":113910.0,"Position":103.0,"HyperDash":false}]},{"StartTime":114379.0,"Objects":[{"StartTime":114379.0,"Position":314.0,"HyperDash":false},{"StartTime":114478.0,"Position":299.4618,"HyperDash":false},{"StartTime":114613.0,"Position":319.818817,"HyperDash":false}]},{"StartTime":114847.0,"Objects":[{"StartTime":114847.0,"Position":211.0,"HyperDash":false},{"StartTime":114925.0,"Position":253.437714,"HyperDash":false},{"StartTime":115003.0,"Position":252.875427,"HyperDash":false},{"StartTime":115081.0,"Position":297.313171,"HyperDash":false},{"StartTime":115159.0,"Position":320.7509,"HyperDash":false},{"StartTime":115219.0,"Position":346.8568,"HyperDash":false},{"StartTime":115315.0,"Position":375.6263,"HyperDash":false}]},{"StartTime":115629.0,"Objects":[{"StartTime":115629.0,"Position":141.0,"HyperDash":false}]},{"StartTime":115941.0,"Objects":[{"StartTime":115941.0,"Position":407.0,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2571731.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2571731.osu new file mode 100644 index 0000000000..bef278e769 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2571731.osu @@ -0,0 +1,277 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 +Mode: 2 + +[Difficulty] +HPDrainRate:2 +CircleSize:2 +OverallDifficulty:6 +ApproachRate:6 +SliderMultiplier:1.1 +SliderTickRate:1 + +[Events] +//Background and Video events +//Break Periods +2,61250,70204 +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Layer 4 (Overlay) +//Storyboard Sound Samples + +[TimingPoints] +4,312.5,4,2,1,70,1,0 +7425,-100,4,2,1,75,0,0 +8754,-100,4,2,1,80,0,0 +9379,-100,4,2,1,80,0,0 +10004,-100,4,2,1,40,0,0 +10472,-100,4,2,1,55,0,0 +10629,-100,4,2,1,65,0,0 +10941,-100,4,2,1,70,0,0 +11254,-100,4,2,3,65,0,0 +12504,-100,4,2,3,65,0,0 +21254,-100,4,2,1,70,0,0 +30629,-100,4,2,1,70,0,0 +30941,-100,4,2,1,70,0,0 +31254,-100,4,2,1,70,0,0 +32191,-100,4,2,1,70,0,0 +32504,-100,4,2,1,70,0,0 +35629,-100,4,2,1,70,0,0 +36254,-100,4,2,1,70,0,0 +37035,-100,4,2,1,70,0,0 +37504,-100,4,2,1,70,0,0 +41722,-100,4,2,1,70,0,0 +42504,-100,4,2,1,70,0,0 +42816,-100,4,2,1,70,0,0 +43754,-100,4,2,1,70,0,0 +44222,-100,4,2,1,70,0,0 +44691,-100,4,2,1,70,0,0 +45004,-100,4,2,1,70,0,0 +45941,-100,4,2,1,70,0,0 +46254,-100,4,2,1,70,0,0 +46722,-100,4,2,1,70,0,0 +47504,-100,4,2,1,70,0,0 +47816,-100,4,2,1,70,0,0 +48441,-100,4,2,1,70,0,0 +48754,-100,4,2,1,70,0,0 +49222,-100,4,2,1,70,0,0 +50004,-100,4,2,1,70,0,0 +50082,-100,4,2,1,25,0,0 +50160,-100,4,2,1,25,0,0 +50238,-100,4,2,1,25,0,0 +50316,-100,4,2,1,25,0,0 +50394,-100,4,2,1,25,0,0 +50472,-100,4,2,1,25,0,0 +50550,-100,4,2,1,25,0,0 +50629,-100,4,2,1,25,0,0 +50707,-100,4,2,1,25,0,0 +50785,-100,4,2,1,25,0,0 +50863,-100,4,2,1,25,0,0 +50941,-100,4,2,1,70,0,0 +51254,-100,4,2,1,70,0,0 +53754,-100,4,2,1,70,0,0 +55004,-100,4,2,1,70,0,0 +56722,-100,4,2,1,70,0,0 +57972,-100,4,2,1,70,0,0 +58754,-100,4,2,1,70,0,0 +59847,-100,4,1,1,65,0,0 +71254,-100,4,2,1,70,0,0 +81254,-100,4,2,1,70,0,0 +83754,-100,4,2,1,70,0,0 +86254,-100,4,2,1,70,0,0 +87504,-100,4,2,1,70,0,0 +91254,-100,4,2,1,70,0,1 +93754,-100,4,2,1,70,0,1 +95004,-100,4,2,1,70,0,1 +100004,-100,4,2,1,70,0,1 +100550,-100,4,2,1,70,0,1 +100941,-100,4,2,1,70,0,1 +103754,-100,4,2,1,70,0,1 +105004,-100,4,2,1,70,0,1 +108754,-100,4,2,1,70,0,1 +110004,-100,4,2,1,70,0,1 +110629,-100,4,2,1,70,0,1 +112504,-100,4,2,1,70,0,1 +113129,-100,4,2,1,70,0,1 +113754,-100,4,2,1,70,0,1 +114379,-100,4,2,1,70,0,1 +114847,-100,4,2,1,70,0,1 +115863,-100,4,2,1,70,0,0 +115941,-100,4,2,1,70,0,1 +116019,-100,4,2,1,70,0,0 + +[HitObjects] +229,264,1254,6,0,P|161:183|254:125,1,220,4|8,1:2|0:0,0:0:0:0: +362,120,2191,1,2,0:0:0:0: +228,119,2504,2,0,L|87:118,1,110,0|2,1:0|0:0,0:0:0:0: +231,216,3129,2,0,L|372:215,1,110,8|2,0:0|0:0,0:0:0:0: +465,214,3754,6,0,P|439:111|303:80,1,220,0|10,1:0|0:0,0:0:0:0: +217,117,4691,1,2,0:0:0:0: +365,123,5004,2,0,L|367:252,1,110,0|2,1:0|0:0,0:0:0:0: +228,313,5629,2,0,L|357:315,1,110,8|2,2:0|0:0,0:0:0:0: +197,303,6254,6,0,P|98:270|59:136,1,220,4|8,1:2|0:0,0:0:0:0: +171,156,7191,1,2,0:0:0:0: +290,138,7504,2,0,L|308:275,1,110,0|2,1:0|0:0,0:0:0:0: +178,249,8129,1,8,0:0:0:0: +308,247,8441,1,2,0:0:0:0: +168,249,8754,6,0,L|53:245,1,110,4|0,1:2|3:0,0:0:0:0: +226,153,9379,2,0,L|343:149,1,110,2|2,1:3|3:2,0:0:0:0: +256,192,10004,12,2,10941,1:3:0:0: +173,329,11254,6,0,P|79:249|178:220,1,220,4|2,0:0|1:3,0:0:0:0: +263,211,12191,1,10,0:0:0:0: +119,212,12504,2,0,L|103:52,1,110,2|8,1:3|0:0,0:0:0:0: +246,65,13129,2,0,L|103:66,1,110,2|8,1:3|0:0,0:0:0:0: +290,64,13754,6,0,P|384:120|284:162,1,220,6|2,1:3|1:3,0:0:0:0: +182,220,14691,1,8,0:0:0:0: +335,208,15004,2,0,L|75:142,1,220,2|2,1:3|1:3,0:0:0:0: +275,153,15941,1,8,0:0:0:0: +120,151,16254,6,0,P|157:258|268:282,1,220,6|2,1:3|1:3,0:0:0:0: +405,290,17191,1,10,0:0:0:0: +250,286,17504,2,0,L|96:264,2,110,6|8|2,1:3|0:0|1:3,0:0:0:0: +403,275,18441,1,8,0:0:0:0: +250,286,18754,6,0,P|186:189|264:160,1,220,6|6,1:3|1:3,0:0:0:0: +404,157,19691,1,8,0:0:0:0: +249,151,20004,5,6,1:3:0:0: +245,233,20316,1,8,0:0:0:0: +240,317,20629,1,8,0:0:0:0: +399,222,20941,1,4,1:2:0:0: +240,317,21254,6,0,P|140:279|114:128,1,220,4|2,1:2|1:3,0:0:0:0: +243,184,22191,1,8,1:0:0:0: +386,178,22504,2,0,L|403:327,1,110,0|8,1:0|1:0,0:0:0:0: +264,338,23129,2,0,L|119:336,1,110,2|8,1:3|1:0,0:0:0:0: +292,300,23754,6,0,P|361:228|270:161,1,220,0|2,1:0|1:3,0:0:0:0: +147,160,24691,1,8,1:0:0:0: +285,124,25004,2,0,L|391:50,2,110,4|10|2,1:0|1:0|1:0,0:0:0:0: +428,128,25941,1,0,1:0:0:0: +284,130,26254,6,0,P|319:238|428:274,1,220,4|2,1:2|1:3,0:0:0:0: +268,276,27191,1,8,1:0:0:0: +124,277,27504,2,0,L|109:125,1,110,0|8,1:0|0:0,0:0:0:0: +250,126,28129,2,0,L|115:102,1,110,2|8,1:3|1:0,0:0:0:0: +284,96,28754,6,0,P|385:143|411:266,1,220,4|2,1:0|1:0,0:0:0:0: +273,240,29691,1,8,1:0:0:0: +416,236,30004,5,4,1:2:0:0: +436,94,30316,1,8,1:0:0:0: +294,75,30629,2,0,L|144:75,1,110,6|0,1:0|3:0,0:0:0:0: +351,138,31254,6,0,P|441:184|405:291,1,220,4|8,0:0|0:0,0:0:0:0: +277,246,32191,1,2,1:2:0:0: +144,299,32504,2,0,L|411:257,1,220,4|8,1:0|1:0,0:0:0:0: +201,244,33440,1,4,1:0:0:0: +140,283,33597,6,0,P|98:231|162:160,1,165,4|8,1:2|0:0,0:0:0:0: +239,112,34379,2,0,L|126:97,1,110,4|8,3:0|1:0,0:0:0:0: +264,173,35004,6,0,L|396:189,1,110,0|8,1:0|1:0,0:0:0:0: +223,227,35629,2,0,L|218:103,1,110,10|2,1:0|1:2,0:0:0:0: +379,115,36254,1,4,1:2:0:0: +398,117,37035,2,0,L|403:211,1,82.5,4|4,1:0|1:0,0:0:0:0: +284,252,37504,6,0,L|169:243,1,110,0|2,1:0|0:0,0:0:0:0: +305,327,38129,2,0,L|423:317,1,110,2|8,0:0|1:2,0:0:0:0: +415,223,38597,6,0,L|233:207,1,165,4|14,1:2|1:0,0:0:0:0: +113,252,39379,2,0,L|103:126,1,110,6|6,1:0|1:0,0:0:0:0: +244,114,40004,5,4,1:2:0:0: +250,185,40316,1,2,2:0:0:0: +253,260,40629,1,2,1:2:0:0: +89,272,40941,1,0,1:0:0:0: +256,305,41254,6,0,P|342:288|366:191,2,165,4|4|8,1:0|1:0|2:0,0:0:0:0: +98,202,42504,2,0,P|184:185|208:88,2,165,4|4|8,1:0|1:0|1:0,0:0:0:0: +249,82,43754,6,0,L|58:83,2,165,4|4|8,1:0|1:0|1:0,0:0:0:0: +256,192,45004,12,2,45941,1:0:0:0: +332,305,46254,6,0,P|396:289|434:187,2,165,4|0|0,1:0|1:0|1:0,0:0:0:0: +180,305,47504,2,0,P|116:289|78:187,2,165,4|4|0,1:0|1:0|1:0,0:0:0:0: +335,231,48754,6,0,L|145:232,2,165,4|4|8,1:0|1:0|1:0,0:0:0:0: +256,192,50004,12,2,50941,0:0:0:0: +119,198,51254,6,0,L|104:299,1,82.5,4|4,1:0|1:0,0:0:0:0: +230,290,51722,2,0,L|252:120,1,165,4|2,1:0|1:0,0:0:0:0: +373,113,52504,6,0,L|388:214,1,82.5,4|4,1:0|1:0,0:0:0:0: +269,207,52972,2,0,P|240:107|282:67,1,165,4|2,1:0|1:0,0:0:0:0: +424,88,53754,6,0,L|325:81,1,82.5,4|4,1:0|1:0,0:0:0:0: +228,196,54222,2,0,L|408:181,1,165,4|2,1:0|1:0,0:0:0:0: +241,238,55004,6,0,L|340:231,1,82.5,4|4,1:0|1:0,0:0:0:0: +437,346,55472,2,0,L|257:331,1,165,2|10,1:0|1:0,0:0:0:0: +130,320,56254,5,4,1:2:0:0: +260,244,56488,2,0,L|272:143,1,82.5,2|2,1:0|1:0,0:0:0:0: +162,106,56957,2,0,L|64:127,1,82.5,2|2,1:0|1:0,0:0:0:0: +233,322,57504,5,2,1:0:0:0: +363,246,57738,2,0,L|270:233,1,82.5,2|2,1:0|1:0,0:0:0:0: +174,230,58207,2,0,L|76:251,1,82.5,2|2,1:0|1:0,0:0:0:0: +261,143,58754,6,0,L|76:124,2,165,4|4|4,1:2|1:2|1:2,0:0:0:0: +256,192,60004,12,0,60941,1:0:0:0: +258,195,71254,5,0,1:0:0:0: +69,186,71722,2,0,L|59:367,1,165,2|2,0:0|0:0,0:0:0:0: +220,198,72504,1,2,0:0:0:0: +254,195,73754,5,4,0:0:0:0: +443,186,74222,2,0,L|453:367,1,165,2|2,0:0|0:0,0:0:0:0: +292,198,75004,1,2,0:0:0:0: +136,196,76254,5,4,0:0:0:0: +349,161,76722,2,0,L|165:160,1,165,2|2,0:0|0:0,0:0:0:0: +350,161,77504,1,2,0:0:0:0: +376,196,78754,5,4,0:0:0:0: +163,161,79222,2,0,L|347:160,1,165,0|2,0:0|0:0,0:0:0:0: +179,253,80004,1,2,0:0:0:0: +88,255,80316,1,2,0:0:0:0: +88,255,80629,1,2,0:0:0:0: +192,256,80941,1,6,1:2:0:0: +336,252,81254,6,0,L|220:254,1,110,4|10,1:2|1:0,0:0:0:0: +366,149,81879,2,0,L|482:151,1,110,4|10,1:2|1:0,0:0:0:0: +319,41,82504,1,4,1:2:0:0: +224,96,82816,1,8,1:0:0:0: +196,202,83129,1,4,1:2:0:0: +248,298,83441,1,10,1:0:0:0: +390,323,83754,6,0,L|271:324,2,110,4|2|2,1:2|0:0|0:0,0:0:0:0: +104,321,85004,1,4,1:2:0:0: +324,329,86254,5,4,1:0:0:0: +422,281,86566,1,4,1:0:0:0: +446,173,86879,1,4,1:0:0:0: +411,68,87191,1,4,1:0:0:0: +287,49,87504,6,0,L|402:51,1,110,4|6,1:0|1:0,0:0:0:0: +265,155,88129,2,0,L|141:153,1,110,4|6,1:0|1:0,0:0:0:0: +308,153,88754,5,2,1:0:0:0: +408,197,89066,1,2,1:0:0:0: +432,304,89379,1,2,1:0:0:0: +348,374,89691,1,2,1:0:0:0: +256,192,90004,12,4,90941,1:2:0:0: +140,282,91254,6,0,P|71:226|156:145,1,220,4|4,1:2|1:0,0:0:0:0: +268,155,92191,1,8,1:0:0:0: +405,152,92504,2,0,L|274:151,1,110,2|10,1:0|1:0,0:0:0:0: +154,250,93129,2,0,L|295:252,1,110,2|10,1:0|1:0,0:0:0:0: +135,329,93754,6,0,L|380:330,1,220,4|0,1:2|1:0,0:0:0:0: +239,290,94691,1,8,1:0:0:0: +354,223,95004,2,0,L|213:210,1,110,4|8,1:0|1:0,0:0:0:0: +97,240,95629,2,0,L|79:127,1,110,2|10,1:0|1:0,0:0:0:0: +238,55,96254,6,0,P|313:95|229:166,1,220,4|0,1:2|1:0,0:0:0:0: +363,205,97191,1,8,1:0:0:0: +252,247,97504,2,0,L|115:270,2,110,2|10|2,1:0|1:0|1:0,0:0:0:0: +363,287,98441,1,10,1:0:0:0: +223,343,98754,6,0,L|92:326,1,110,4|10,1:2|1:0,0:0:0:0: +293,262,99379,1,2,1:0:0:0: +396,244,99691,1,8,1:0:0:0: +236,219,100004,6,0,P|160:186|103:55,1,220,4|0,1:2|1:0,0:0:0:0: +226,68,100941,1,0,1:0:0:0: +383,69,101254,6,0,P|456:139|387:208,1,220,4|2,1:2|1:0,0:0:0:0: +261,244,102191,1,10,1:0:0:0: +123,311,102504,2,0,L|102:165,1,110,2|10,1:0|1:0,0:0:0:0: +252,178,103129,2,0,L|386:178,1,110,2|10,1:0|1:0,0:0:0:0: +215,263,103754,6,0,P|123:241|79:117,1,220,4|2,1:2|1:0,0:0:0:0: +216,121,104691,1,10,1:0:0:0: +359,106,105004,2,0,L|371:240,1,110,4|8,1:0|1:0,0:0:0:0: +215,312,105629,2,0,L|352:321,1,110,2|10,1:0|1:0,0:0:0:0: +164,359,106254,6,0,L|424:330,1,220,4|2,1:2|1:0,0:0:0:0: +244,297,107191,1,10,1:0:0:0: +390,278,107504,2,0,L|269:255,2,110,2|10|2,1:0|1:0|1:0,0:0:0:0: +244,276,108441,1,10,1:0:0:0: +99,281,108754,6,0,L|87:150,1,110,4|10,1:2|1:0,0:0:0:0: +228,146,109379,2,0,L|364:136,1,110,2|10,1:0|1:0,0:0:0:0: +183,278,110004,6,0,L|424:264,1,220,4|2,1:2|0:0,0:0:0:0: +266,255,110941,1,10,1:0:0:0: +114,283,111254,6,0,L|102:152,1,110,4|10,0:0|1:0,0:0:0:0: +243,148,111879,2,0,L|379:138,1,110,2|10,1:0|1:0,0:0:0:0: +198,280,112504,6,0,L|439:266,1,220,4|2,1:2|1:0,0:0:0:0: +281,257,113441,1,8,1:0:0:0: +137,295,113754,5,4,1:2:0:0: +127,239,113910,1,4,1:2:0:0: +314,108,114379,2,0,L|321:207,1,82.5,4|4,0:0|0:0,0:0:0:0: +211,254,114847,6,0,L|389:266,1,165,6|6,1:0|1:0,0:0:0:0: +265,275,115629,1,4,1:0:0:0: +407,299,115941,1,4,1:2:0:0: diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2768615-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2768615-expected-conversion.json new file mode 100644 index 0000000000..2ebebdbe7a --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2768615-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":514.0,"Objects":[{"StartTime":514.0,"Position":6.0,"HyperDash":false},{"StartTime":586.0,"Position":0.0,"HyperDash":false},{"StartTime":695.0,"Position":8.064062,"HyperDash":false}]},{"StartTime":877.0,"Objects":[{"StartTime":877.0,"Position":20.0,"HyperDash":false}]},{"StartTime":1059.0,"Objects":[{"StartTime":1059.0,"Position":14.0,"HyperDash":false},{"StartTime":1131.0,"Position":3.79381847,"HyperDash":false},{"StartTime":1240.0,"Position":15.99557,"HyperDash":false}]},{"StartTime":1604.0,"Objects":[{"StartTime":1604.0,"Position":21.0,"HyperDash":false},{"StartTime":1685.0,"Position":19.13056,"HyperDash":false},{"StartTime":1767.0,"Position":38.2750778,"HyperDash":false},{"StartTime":1849.0,"Position":41.4195938,"HyperDash":false},{"StartTime":1967.0,"Position":26.0665855,"HyperDash":false}]},{"StartTime":2332.0,"Objects":[{"StartTime":2332.0,"Position":28.0,"HyperDash":false}]},{"StartTime":2513.0,"Objects":[{"StartTime":2513.0,"Position":27.0,"HyperDash":false},{"StartTime":2576.0,"Position":28.8067379,"HyperDash":false},{"StartTime":2640.0,"Position":27.6262817,"HyperDash":false},{"StartTime":2703.0,"Position":14.43302,"HyperDash":false},{"StartTime":2767.0,"Position":22.2525616,"HyperDash":false},{"StartTime":2831.0,"Position":19.0721054,"HyperDash":false},{"StartTime":2894.0,"Position":27.8788433,"HyperDash":false},{"StartTime":2958.0,"Position":46.6983871,"HyperDash":false},{"StartTime":3058.0,"Position":33.9789238,"HyperDash":false}]},{"StartTime":3423.0,"Objects":[{"StartTime":3423.0,"Position":46.0,"HyperDash":false},{"StartTime":3495.0,"Position":50.821064,"HyperDash":false},{"StartTime":3604.0,"Position":48.064064,"HyperDash":false}]},{"StartTime":3786.0,"Objects":[{"StartTime":3786.0,"Position":60.0,"HyperDash":false}]},{"StartTime":3968.0,"Objects":[{"StartTime":3968.0,"Position":54.0,"HyperDash":false},{"StartTime":4040.0,"Position":45.79382,"HyperDash":false},{"StartTime":4149.0,"Position":55.99557,"HyperDash":false}]},{"StartTime":4513.0,"Objects":[{"StartTime":4513.0,"Position":61.0,"HyperDash":false},{"StartTime":4585.0,"Position":66.82106,"HyperDash":false},{"StartTime":4694.0,"Position":63.064064,"HyperDash":false}]},{"StartTime":4877.0,"Objects":[{"StartTime":4877.0,"Position":65.0,"HyperDash":false},{"StartTime":4967.0,"Position":66.0687,"HyperDash":false},{"StartTime":5058.0,"Position":65.0,"HyperDash":false}]},{"StartTime":5241.0,"Objects":[{"StartTime":5241.0,"Position":68.0,"HyperDash":false},{"StartTime":5313.0,"Position":49.8210678,"HyperDash":false},{"StartTime":5422.0,"Position":70.064064,"HyperDash":false}]},{"StartTime":5604.0,"Objects":[{"StartTime":5604.0,"Position":77.0,"HyperDash":false},{"StartTime":5660.0,"Position":67.75663,"HyperDash":false},{"StartTime":5717.0,"Position":94.50892,"HyperDash":false},{"StartTime":5774.0,"Position":93.26121,"HyperDash":false},{"StartTime":5831.0,"Position":76.0135,"HyperDash":false},{"StartTime":5926.0,"Position":89.42635,"HyperDash":false},{"StartTime":6058.0,"Position":77.0,"HyperDash":false}]},{"StartTime":6332.0,"Objects":[{"StartTime":6332.0,"Position":96.0,"HyperDash":false}]},{"StartTime":6513.0,"Objects":[{"StartTime":6513.0,"Position":80.0,"HyperDash":false}]},{"StartTime":6877.0,"Objects":[{"StartTime":6877.0,"Position":108.0,"HyperDash":false}]},{"StartTime":7059.0,"Objects":[{"StartTime":7059.0,"Position":96.0,"HyperDash":false},{"StartTime":7131.0,"Position":91.4434738,"HyperDash":false},{"StartTime":7240.0,"Position":98.95893,"HyperDash":false}]},{"StartTime":7423.0,"Objects":[{"StartTime":7423.0,"Position":101.0,"HyperDash":false},{"StartTime":7495.0,"Position":79.8501,"HyperDash":false},{"StartTime":7604.0,"Position":96.87991,"HyperDash":false}]},{"StartTime":7786.0,"Objects":[{"StartTime":7786.0,"Position":115.0,"HyperDash":false}]},{"StartTime":7968.0,"Objects":[{"StartTime":7968.0,"Position":95.0,"HyperDash":false}]},{"StartTime":8150.0,"Objects":[{"StartTime":8150.0,"Position":127.0,"HyperDash":false}]},{"StartTime":8332.0,"Objects":[{"StartTime":8332.0,"Position":110.0,"HyperDash":false},{"StartTime":8404.0,"Position":123.196411,"HyperDash":false},{"StartTime":8513.0,"Position":105.877426,"HyperDash":false}]},{"StartTime":8695.0,"Objects":[{"StartTime":8695.0,"Position":110.0,"HyperDash":false},{"StartTime":8767.0,"Position":104.810783,"HyperDash":false},{"StartTime":8876.0,"Position":113.827438,"HyperDash":false}]},{"StartTime":9241.0,"Objects":[{"StartTime":9241.0,"Position":138.0,"HyperDash":false}]},{"StartTime":9423.0,"Objects":[{"StartTime":9423.0,"Position":131.0,"HyperDash":false},{"StartTime":9495.0,"Position":133.615219,"HyperDash":false},{"StartTime":9604.0,"Position":128.964035,"HyperDash":false}]},{"StartTime":9786.0,"Objects":[{"StartTime":9786.0,"Position":143.0,"HyperDash":false}]},{"StartTime":9968.0,"Objects":[{"StartTime":9968.0,"Position":136.0,"HyperDash":false},{"StartTime":10027.0,"Position":122.678848,"HyperDash":false},{"StartTime":10086.0,"Position":134.039719,"HyperDash":false},{"StartTime":10145.0,"Position":136.077042,"HyperDash":false},{"StartTime":10240.0,"Position":132.939224,"HyperDash":false}]},{"StartTime":10332.0,"Objects":[{"StartTime":10332.0,"Position":139.0,"HyperDash":false},{"StartTime":10391.0,"Position":156.013535,"HyperDash":false},{"StartTime":10450.0,"Position":128.715408,"HyperDash":false},{"StartTime":10509.0,"Position":125.074265,"HyperDash":false},{"StartTime":10604.0,"Position":141.969208,"HyperDash":false}]},{"StartTime":10695.0,"Objects":[{"StartTime":10695.0,"Position":150.0,"HyperDash":false}]},{"StartTime":10877.0,"Objects":[{"StartTime":10877.0,"Position":146.0,"HyperDash":false}]},{"StartTime":11059.0,"Objects":[{"StartTime":11059.0,"Position":156.0,"HyperDash":false}]},{"StartTime":11241.0,"Objects":[{"StartTime":11241.0,"Position":150.0,"HyperDash":false}]},{"StartTime":11423.0,"Objects":[{"StartTime":11423.0,"Position":156.0,"HyperDash":false},{"StartTime":11513.0,"Position":158.989288,"HyperDash":false}]},{"StartTime":11604.0,"Objects":[{"StartTime":11604.0,"Position":157.0,"HyperDash":false}]},{"StartTime":11695.0,"Objects":[{"StartTime":11695.0,"Position":163.0,"HyperDash":false}]},{"StartTime":11786.0,"Objects":[{"StartTime":11786.0,"Position":161.0,"HyperDash":false},{"StartTime":11876.0,"Position":161.977341,"HyperDash":false}]},{"StartTime":11968.0,"Objects":[{"StartTime":11968.0,"Position":165.0,"HyperDash":false},{"StartTime":12058.0,"Position":165.977341,"HyperDash":false}]},{"StartTime":12150.0,"Objects":[{"StartTime":12150.0,"Position":166.0,"HyperDash":false},{"StartTime":12222.0,"Position":150.82106,"HyperDash":false},{"StartTime":12331.0,"Position":168.064056,"HyperDash":false}]},{"StartTime":12513.0,"Objects":[{"StartTime":12513.0,"Position":180.0,"HyperDash":false}]},{"StartTime":12695.0,"Objects":[{"StartTime":12695.0,"Position":174.0,"HyperDash":false},{"StartTime":12747.0,"Position":187.463669,"HyperDash":false},{"StartTime":12799.0,"Position":191.927322,"HyperDash":false},{"StartTime":12851.0,"Position":174.390991,"HyperDash":false},{"StartTime":12904.0,"Position":168.863571,"HyperDash":false},{"StartTime":12956.0,"Position":175.32724,"HyperDash":false},{"StartTime":13008.0,"Position":195.7909,"HyperDash":false},{"StartTime":13060.0,"Position":171.254562,"HyperDash":false},{"StartTime":13149.0,"Position":178.048141,"HyperDash":false}]},{"StartTime":13241.0,"Objects":[{"StartTime":13241.0,"Position":183.0,"HyperDash":false},{"StartTime":13313.0,"Position":187.17894,"HyperDash":false},{"StartTime":13422.0,"Position":180.935944,"HyperDash":false}]},{"StartTime":13604.0,"Objects":[{"StartTime":13604.0,"Position":191.0,"HyperDash":false}]},{"StartTime":13786.0,"Objects":[{"StartTime":13786.0,"Position":185.0,"HyperDash":false}]},{"StartTime":13968.0,"Objects":[{"StartTime":13968.0,"Position":197.0,"HyperDash":false}]},{"StartTime":14150.0,"Objects":[{"StartTime":14150.0,"Position":191.0,"HyperDash":false},{"StartTime":14213.0,"Position":204.4671,"HyperDash":false},{"StartTime":14277.0,"Position":208.941635,"HyperDash":false},{"StartTime":14340.0,"Position":209.408737,"HyperDash":false},{"StartTime":14404.0,"Position":207.88327,"HyperDash":false},{"StartTime":14468.0,"Position":210.357788,"HyperDash":false},{"StartTime":14531.0,"Position":202.82489,"HyperDash":false},{"StartTime":14595.0,"Position":177.299423,"HyperDash":false},{"StartTime":14695.0,"Position":195.040863,"HyperDash":false}]},{"StartTime":15059.0,"Objects":[{"StartTime":15059.0,"Position":217.0,"HyperDash":false}]},{"StartTime":15150.0,"Objects":[{"StartTime":15150.0,"Position":197.0,"HyperDash":false}]},{"StartTime":15240.0,"Objects":[{"StartTime":15240.0,"Position":219.0,"HyperDash":false}]},{"StartTime":15422.0,"Objects":[{"StartTime":15422.0,"Position":209.0,"HyperDash":false}]},{"StartTime":15604.0,"Objects":[{"StartTime":15604.0,"Position":214.0,"HyperDash":false},{"StartTime":15656.0,"Position":219.463669,"HyperDash":false},{"StartTime":15708.0,"Position":202.927322,"HyperDash":false},{"StartTime":15760.0,"Position":200.390991,"HyperDash":false},{"StartTime":15813.0,"Position":233.863571,"HyperDash":false},{"StartTime":15865.0,"Position":208.32724,"HyperDash":false},{"StartTime":15917.0,"Position":232.7909,"HyperDash":false},{"StartTime":15969.0,"Position":220.254562,"HyperDash":false},{"StartTime":16058.0,"Position":218.048141,"HyperDash":false}]},{"StartTime":16150.0,"Objects":[{"StartTime":16150.0,"Position":223.0,"HyperDash":false},{"StartTime":16218.0,"Position":212.065735,"HyperDash":false},{"StartTime":16286.0,"Position":221.13147,"HyperDash":false},{"StartTime":16422.0,"Position":223.0,"HyperDash":false}]},{"StartTime":16513.0,"Objects":[{"StartTime":16513.0,"Position":231.0,"HyperDash":false}]},{"StartTime":16695.0,"Objects":[{"StartTime":16695.0,"Position":227.0,"HyperDash":false}]},{"StartTime":16785.0,"Objects":[{"StartTime":16785.0,"Position":233.0,"HyperDash":false}]},{"StartTime":16877.0,"Objects":[{"StartTime":16877.0,"Position":231.0,"HyperDash":false},{"StartTime":16949.0,"Position":232.82106,"HyperDash":false},{"StartTime":17058.0,"Position":233.064056,"HyperDash":false}]},{"StartTime":17241.0,"Objects":[{"StartTime":17241.0,"Position":236.0,"HyperDash":false},{"StartTime":17297.0,"Position":226.466782,"HyperDash":false},{"StartTime":17354.0,"Position":236.9419,"HyperDash":false},{"StartTime":17411.0,"Position":219.417,"HyperDash":false},{"StartTime":17468.0,"Position":237.89212,"HyperDash":false},{"StartTime":17563.0,"Position":221.100266,"HyperDash":false},{"StartTime":17695.0,"Position":236.0,"HyperDash":false}]},{"StartTime":18150.0,"Objects":[{"StartTime":18150.0,"Position":254.0,"HyperDash":false}]},{"StartTime":18331.0,"Objects":[{"StartTime":18331.0,"Position":242.0,"HyperDash":false}]},{"StartTime":18695.0,"Objects":[{"StartTime":18695.0,"Position":264.0,"HyperDash":false}]},{"StartTime":18877.0,"Objects":[{"StartTime":18877.0,"Position":250.0,"HyperDash":false}]},{"StartTime":19059.0,"Objects":[{"StartTime":19059.0,"Position":261.0,"HyperDash":false},{"StartTime":19149.0,"Position":273.538757,"HyperDash":false},{"StartTime":19240.0,"Position":265.1201,"HyperDash":false},{"StartTime":19313.0,"Position":252.953827,"HyperDash":false},{"StartTime":19422.0,"Position":261.0,"HyperDash":false}]},{"StartTime":19604.0,"Objects":[{"StartTime":19604.0,"Position":267.0,"HyperDash":false}]},{"StartTime":19786.0,"Objects":[{"StartTime":19786.0,"Position":271.0,"HyperDash":false}]},{"StartTime":19876.0,"Objects":[{"StartTime":19876.0,"Position":269.0,"HyperDash":false}]},{"StartTime":19968.0,"Objects":[{"StartTime":19968.0,"Position":271.0,"HyperDash":false},{"StartTime":20058.0,"Position":271.71347,"HyperDash":false},{"StartTime":20149.0,"Position":271.0,"HyperDash":false}]},{"StartTime":20331.0,"Objects":[{"StartTime":20331.0,"Position":278.0,"HyperDash":false}]},{"StartTime":20422.0,"Objects":[{"StartTime":20422.0,"Position":276.0,"HyperDash":false},{"StartTime":20494.0,"Position":256.991028,"HyperDash":false},{"StartTime":20603.0,"Position":276.034363,"HyperDash":false}]},{"StartTime":20695.0,"Objects":[{"StartTime":20695.0,"Position":281.0,"HyperDash":false},{"StartTime":20767.0,"Position":276.1846,"HyperDash":false},{"StartTime":20876.0,"Position":282.9045,"HyperDash":false}]},{"StartTime":21059.0,"Objects":[{"StartTime":21059.0,"Position":290.0,"HyperDash":false}]},{"StartTime":21240.0,"Objects":[{"StartTime":21240.0,"Position":291.0,"HyperDash":false},{"StartTime":21312.0,"Position":274.790222,"HyperDash":false},{"StartTime":21421.0,"Position":290.691162,"HyperDash":false}]},{"StartTime":21604.0,"Objects":[{"StartTime":21604.0,"Position":301.0,"HyperDash":false}]},{"StartTime":21786.0,"Objects":[{"StartTime":21786.0,"Position":296.0,"HyperDash":false},{"StartTime":21858.0,"Position":307.443481,"HyperDash":false},{"StartTime":21967.0,"Position":298.958923,"HyperDash":false}]},{"StartTime":22150.0,"Objects":[{"StartTime":22150.0,"Position":301.0,"HyperDash":false},{"StartTime":22222.0,"Position":295.69693,"HyperDash":false},{"StartTime":22331.0,"Position":296.97644,"HyperDash":false}]},{"StartTime":22513.0,"Objects":[{"StartTime":22513.0,"Position":315.0,"HyperDash":false}]},{"StartTime":22695.0,"Objects":[{"StartTime":22695.0,"Position":307.0,"HyperDash":false}]},{"StartTime":22786.0,"Objects":[{"StartTime":22786.0,"Position":315.0,"HyperDash":false}]},{"StartTime":22877.0,"Objects":[{"StartTime":22877.0,"Position":307.0,"HyperDash":false}]},{"StartTime":22968.0,"Objects":[{"StartTime":22968.0,"Position":319.0,"HyperDash":false}]},{"StartTime":23059.0,"Objects":[{"StartTime":23059.0,"Position":309.0,"HyperDash":false}]},{"StartTime":23150.0,"Objects":[{"StartTime":23150.0,"Position":321.0,"HyperDash":false}]},{"StartTime":23240.0,"Objects":[{"StartTime":23240.0,"Position":316.0,"HyperDash":false},{"StartTime":23330.0,"Position":305.998932,"HyperDash":false}]},{"StartTime":23421.0,"Objects":[{"StartTime":23421.0,"Position":332.0,"HyperDash":false}]},{"StartTime":23604.0,"Objects":[{"StartTime":23604.0,"Position":319.0,"HyperDash":false},{"StartTime":23694.0,"Position":319.977325,"HyperDash":false}]},{"StartTime":23786.0,"Objects":[{"StartTime":23786.0,"Position":323.0,"HyperDash":false},{"StartTime":23876.0,"Position":323.977325,"HyperDash":false}]},{"StartTime":23968.0,"Objects":[{"StartTime":23968.0,"Position":332.0,"HyperDash":false}]},{"StartTime":24150.0,"Objects":[{"StartTime":24150.0,"Position":328.0,"HyperDash":false},{"StartTime":24222.0,"Position":344.178925,"HyperDash":false},{"StartTime":24331.0,"Position":325.935944,"HyperDash":false}]},{"StartTime":24513.0,"Objects":[{"StartTime":24513.0,"Position":336.0,"HyperDash":false}]},{"StartTime":24695.0,"Objects":[{"StartTime":24695.0,"Position":340.0,"HyperDash":false}]},{"StartTime":24787.0,"Objects":[{"StartTime":24787.0,"Position":337.0,"HyperDash":false},{"StartTime":24877.0,"Position":336.002228,"HyperDash":false}]},{"StartTime":25059.0,"Objects":[{"StartTime":25059.0,"Position":341.0,"HyperDash":false},{"StartTime":25131.0,"Position":323.178925,"HyperDash":false},{"StartTime":25240.0,"Position":338.935944,"HyperDash":false}]},{"StartTime":25422.0,"Objects":[{"StartTime":25422.0,"Position":363.0,"HyperDash":false}]},{"StartTime":25604.0,"Objects":[{"StartTime":25604.0,"Position":351.0,"HyperDash":false},{"StartTime":25694.0,"Position":351.997772,"HyperDash":false},{"StartTime":25785.0,"Position":351.0,"HyperDash":false}]},{"StartTime":25968.0,"Objects":[{"StartTime":25968.0,"Position":356.0,"HyperDash":false}]},{"StartTime":26059.0,"Objects":[{"StartTime":26059.0,"Position":354.0,"HyperDash":false}]},{"StartTime":26149.0,"Objects":[{"StartTime":26149.0,"Position":356.0,"HyperDash":false},{"StartTime":26239.0,"Position":362.0235,"HyperDash":false},{"StartTime":26330.0,"Position":358.064056,"HyperDash":false},{"StartTime":26403.0,"Position":376.239563,"HyperDash":false},{"StartTime":26512.0,"Position":356.0,"HyperDash":false}]},{"StartTime":26877.0,"Objects":[{"StartTime":26877.0,"Position":374.0,"HyperDash":false}]},{"StartTime":27059.0,"Objects":[{"StartTime":27059.0,"Position":364.0,"HyperDash":false}]},{"StartTime":27149.0,"Objects":[{"StartTime":27149.0,"Position":376.0,"HyperDash":false}]},{"StartTime":27240.0,"Objects":[{"StartTime":27240.0,"Position":371.0,"HyperDash":false},{"StartTime":27330.0,"Position":371.0,"HyperDash":false},{"StartTime":27421.0,"Position":371.0,"HyperDash":false}]},{"StartTime":27604.0,"Objects":[{"StartTime":27604.0,"Position":381.0,"HyperDash":false}]},{"StartTime":27696.0,"Objects":[{"StartTime":27696.0,"Position":377.0,"HyperDash":false},{"StartTime":27786.0,"Position":377.71347,"HyperDash":false}]},{"StartTime":27968.0,"Objects":[{"StartTime":27968.0,"Position":381.0,"HyperDash":false},{"StartTime":28040.0,"Position":390.178925,"HyperDash":false},{"StartTime":28149.0,"Position":378.935944,"HyperDash":false}]},{"StartTime":28331.0,"Objects":[{"StartTime":28331.0,"Position":393.0,"HyperDash":false}]},{"StartTime":28513.0,"Objects":[{"StartTime":28513.0,"Position":385.0,"HyperDash":false}]},{"StartTime":28604.0,"Objects":[{"StartTime":28604.0,"Position":395.0,"HyperDash":false}]},{"StartTime":28695.0,"Objects":[{"StartTime":28695.0,"Position":391.0,"HyperDash":false},{"StartTime":28767.0,"Position":401.821075,"HyperDash":false},{"StartTime":28876.0,"Position":393.064056,"HyperDash":false}]},{"StartTime":29059.0,"Objects":[{"StartTime":29059.0,"Position":398.0,"HyperDash":false},{"StartTime":29115.0,"Position":401.513763,"HyperDash":false},{"StartTime":29172.0,"Position":404.01886,"HyperDash":false},{"StartTime":29229.0,"Position":412.523956,"HyperDash":false},{"StartTime":29286.0,"Position":396.029053,"HyperDash":false},{"StartTime":29381.0,"Position":381.8539,"HyperDash":false},{"StartTime":29513.0,"Position":398.0,"HyperDash":false}]},{"StartTime":29786.0,"Objects":[{"StartTime":29786.0,"Position":416.0,"HyperDash":false}]},{"StartTime":29967.0,"Objects":[{"StartTime":29967.0,"Position":400.0,"HyperDash":false}]},{"StartTime":30331.0,"Objects":[{"StartTime":30331.0,"Position":426.0,"HyperDash":false}]},{"StartTime":30513.0,"Objects":[{"StartTime":30513.0,"Position":408.0,"HyperDash":false}]},{"StartTime":30605.0,"Objects":[{"StartTime":30605.0,"Position":418.0,"HyperDash":false},{"StartTime":30695.0,"Position":418.71347,"HyperDash":false}]},{"StartTime":30877.0,"Objects":[{"StartTime":30877.0,"Position":421.0,"HyperDash":false},{"StartTime":30949.0,"Position":422.1499,"HyperDash":false},{"StartTime":31058.0,"Position":425.1201,"HyperDash":false}]},{"StartTime":31240.0,"Objects":[{"StartTime":31240.0,"Position":427.0,"HyperDash":false}]},{"StartTime":31422.0,"Objects":[{"StartTime":31422.0,"Position":431.0,"HyperDash":false}]},{"StartTime":31513.0,"Objects":[{"StartTime":31513.0,"Position":429.0,"HyperDash":false}]},{"StartTime":31603.0,"Objects":[{"StartTime":31603.0,"Position":433.0,"HyperDash":false}]},{"StartTime":31695.0,"Objects":[{"StartTime":31695.0,"Position":419.0,"HyperDash":false}]},{"StartTime":31786.0,"Objects":[{"StartTime":31786.0,"Position":434.0,"HyperDash":false},{"StartTime":31858.0,"Position":444.725983,"HyperDash":false},{"StartTime":31967.0,"Position":435.019226,"HyperDash":false}]},{"StartTime":32149.0,"Objects":[{"StartTime":32149.0,"Position":442.0,"HyperDash":false},{"StartTime":32221.0,"Position":443.390778,"HyperDash":false},{"StartTime":32330.0,"Position":440.069153,"HyperDash":false}]},{"StartTime":32695.0,"Objects":[{"StartTime":32695.0,"Position":452.0,"HyperDash":false}]},{"StartTime":32877.0,"Objects":[{"StartTime":32877.0,"Position":451.0,"HyperDash":false},{"StartTime":32949.0,"Position":434.066742,"HyperDash":false},{"StartTime":33058.0,"Position":450.7317,"HyperDash":false}]},{"StartTime":33240.0,"Objects":[{"StartTime":33240.0,"Position":461.0,"HyperDash":false}]},{"StartTime":33422.0,"Objects":[{"StartTime":33422.0,"Position":451.0,"HyperDash":false}]},{"StartTime":33513.0,"Objects":[{"StartTime":33513.0,"Position":457.0,"HyperDash":false},{"StartTime":33585.0,"Position":444.147736,"HyperDash":false},{"StartTime":33694.0,"Position":445.292816,"HyperDash":false}]},{"StartTime":33786.0,"Objects":[{"StartTime":33786.0,"Position":479.0,"HyperDash":false}]},{"StartTime":33877.0,"Objects":[{"StartTime":33877.0,"Position":462.0,"HyperDash":false},{"StartTime":33967.0,"Position":462.0,"HyperDash":false}]},{"StartTime":34149.0,"Objects":[{"StartTime":34149.0,"Position":470.0,"HyperDash":false}]},{"StartTime":34331.0,"Objects":[{"StartTime":34331.0,"Position":450.0,"HyperDash":false}]},{"StartTime":34422.0,"Objects":[{"StartTime":34422.0,"Position":450.0,"HyperDash":false}]},{"StartTime":34513.0,"Objects":[{"StartTime":34513.0,"Position":490.0,"HyperDash":false}]},{"StartTime":34604.0,"Objects":[{"StartTime":34604.0,"Position":472.0,"HyperDash":false}]},{"StartTime":34695.0,"Objects":[{"StartTime":34695.0,"Position":489.0,"HyperDash":false}]},{"StartTime":34877.0,"Objects":[{"StartTime":34877.0,"Position":476.0,"HyperDash":false},{"StartTime":34967.0,"Position":474.8665,"HyperDash":false}]},{"StartTime":35059.0,"Objects":[{"StartTime":35059.0,"Position":483.0,"HyperDash":false}]},{"StartTime":35240.0,"Objects":[{"StartTime":35240.0,"Position":479.0,"HyperDash":false},{"StartTime":35330.0,"Position":479.977325,"HyperDash":false}]},{"StartTime":35422.0,"Objects":[{"StartTime":35422.0,"Position":483.0,"HyperDash":false},{"StartTime":35512.0,"Position":483.977325,"HyperDash":false}]},{"StartTime":35604.0,"Objects":[{"StartTime":35604.0,"Position":287.0,"HyperDash":false},{"StartTime":35692.0,"Position":361.0,"HyperDash":false},{"StartTime":35780.0,"Position":479.0,"HyperDash":false},{"StartTime":35868.0,"Position":346.0,"HyperDash":false},{"StartTime":35956.0,"Position":266.0,"HyperDash":false},{"StartTime":36044.0,"Position":400.0,"HyperDash":false},{"StartTime":36132.0,"Position":202.0,"HyperDash":false},{"StartTime":36220.0,"Position":500.0,"HyperDash":false},{"StartTime":36308.0,"Position":80.0,"HyperDash":false},{"StartTime":36396.0,"Position":399.0,"HyperDash":false},{"StartTime":36484.0,"Position":455.0,"HyperDash":false},{"StartTime":36572.0,"Position":105.0,"HyperDash":false},{"StartTime":36660.0,"Position":100.0,"HyperDash":false},{"StartTime":36748.0,"Position":195.0,"HyperDash":false},{"StartTime":36836.0,"Position":106.0,"HyperDash":false},{"StartTime":36924.0,"Position":305.0,"HyperDash":false},{"StartTime":37013.0,"Position":225.0,"HyperDash":false}]},{"StartTime":37059.0,"Objects":[{"StartTime":37059.0,"Position":79.0,"HyperDash":false},{"StartTime":37124.0,"Position":38.0,"HyperDash":false},{"StartTime":37189.0,"Position":99.0,"HyperDash":false},{"StartTime":37254.0,"Position":79.0,"HyperDash":false},{"StartTime":37320.0,"Position":169.0,"HyperDash":false},{"StartTime":37385.0,"Position":238.0,"HyperDash":false},{"StartTime":37450.0,"Position":511.0,"HyperDash":false},{"StartTime":37516.0,"Position":58.0,"HyperDash":false},{"StartTime":37581.0,"Position":368.0,"HyperDash":false},{"StartTime":37646.0,"Position":52.0,"HyperDash":false},{"StartTime":37712.0,"Position":327.0,"HyperDash":false},{"StartTime":37777.0,"Position":226.0,"HyperDash":false},{"StartTime":37842.0,"Position":110.0,"HyperDash":false},{"StartTime":37908.0,"Position":3.0,"HyperDash":false},{"StartTime":37973.0,"Position":26.0,"HyperDash":false},{"StartTime":38038.0,"Position":173.0,"HyperDash":false},{"StartTime":38104.0,"Position":18.0,"HyperDash":false},{"StartTime":38169.0,"Position":310.0,"HyperDash":false},{"StartTime":38234.0,"Position":394.0,"HyperDash":false},{"StartTime":38299.0,"Position":406.0,"HyperDash":false},{"StartTime":38365.0,"Position":262.0,"HyperDash":false},{"StartTime":38430.0,"Position":278.0,"HyperDash":false},{"StartTime":38495.0,"Position":171.0,"HyperDash":false},{"StartTime":38561.0,"Position":22.0,"HyperDash":false},{"StartTime":38626.0,"Position":187.0,"HyperDash":false},{"StartTime":38691.0,"Position":124.0,"HyperDash":false},{"StartTime":38757.0,"Position":454.0,"HyperDash":false},{"StartTime":38822.0,"Position":16.0,"HyperDash":false},{"StartTime":38887.0,"Position":61.0,"HyperDash":false},{"StartTime":38953.0,"Position":161.0,"HyperDash":false},{"StartTime":39018.0,"Position":243.0,"HyperDash":false},{"StartTime":39083.0,"Position":375.0,"HyperDash":false},{"StartTime":39149.0,"Position":247.0,"HyperDash":false}]},{"StartTime":40695.0,"Objects":[{"StartTime":40695.0,"Position":496.0,"HyperDash":false},{"StartTime":40767.0,"Position":489.6517,"HyperDash":false},{"StartTime":40876.0,"Position":495.903229,"HyperDash":false}]},{"StartTime":41059.0,"Objects":[{"StartTime":41059.0,"Position":510.0,"HyperDash":false}]},{"StartTime":41150.0,"Objects":[{"StartTime":41150.0,"Position":498.0,"HyperDash":false}]},{"StartTime":41240.0,"Objects":[{"StartTime":41240.0,"Position":505.0,"HyperDash":false},{"StartTime":41285.0,"Position":505.934265,"HyperDash":false},{"StartTime":41330.0,"Position":505.0,"HyperDash":false},{"StartTime":41376.0,"Position":505.934265,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2768615.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2768615.osu new file mode 100644 index 0000000000..19439172cd --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2768615.osu @@ -0,0 +1,200 @@ +osu file format v14 + +[General] +StackLeniency: 0.4 +Mode: 0 + +[Difficulty] +HPDrainRate:4.5 +CircleSize:9 +OverallDifficulty:8 +ApproachRate:8 +SliderMultiplier:1.2 +SliderTickRate:1 + +[Events] +//Background and Video events +//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] +514,727.272727272727,4,2,1,50,1,0 +9241,-83.3333333333333,4,2,1,50,0,0 +9968,-100,4,2,1,50,0,0 +10241,-100,4,2,99,5,0,0 +10332,-100,4,2,1,50,0,0 +10604,-100,4,2,99,5,0,0 +10695,-100,4,2,1,50,0,0 +11423,-66.6666666666667,4,2,1,50,0,0 +12150,-100,4,2,1,50,0,0 +13150,-100,4,2,99,5,0,0 +13241,-100,4,2,1,50,0,0 +16059,-100,4,2,99,5,0,0 +16150,-100,4,2,1,50,0,0 +17241,909.090909090909,4,2,1,50,1,0 +17241,-83.3333333333333,4,2,1,50,0,0 +18150,727.272727272727,4,2,1,50,1,0 +20604,-100,4,2,99,5,0,0 +20695,-100,4,2,1,50,0,0 +21059,-83.3333333333333,4,2,1,50,0,0 +21786,-100,4,2,1,50,0,0 +23240,-66.6666666666667,4,2,1,50,0,0 +23968,-100,4,2,1,50,0,0 +32695,-83.3333333333333,4,2,1,50,0,0 +33422,-100,4,2,1,50,0,0 +33695,-100,4,2,99,5,0,0 +33786,-100,4,2,1,50,0,0 +34877,-66.6666666666667,4,2,1,50,0,0 +37013,-100,4,2,1,45,0,0 +39149,-100,4,2,1,40,0,0 +40695,-66.6666666666667,4,2,1,35,0,0 + +[HitObjects] +6,0,514,6,0,L|8:29,1,30,2|0,3:2|0:0,0:0:0:0: +14,66,877,1,0,0:0:0:0: +14,66,1059,2,0,L|16:36,1,30,0|0,0:0|1:0,0:0:0:0: +21,31,1604,6,0,L|26:90,1,60,2|0,0:0|3:0,0:0:0:0: +27,186,2332,1,0,3:0:0:0: +27,186,2513,2,0,L|34:96,1,90,2|0,0:0|0:0,0:0:0:0: +46,269,3423,38,0,L|48:298,1,30,2|0,3:2|0:0,0:0:0:0: +54,335,3786,1,0,0:0:0:0: +54,335,3968,2,0,L|56:305,1,30,0|0,0:0|1:0,0:0:0:0: +61,300,4513,6,0,L|63:271,1,30,2|0,0:0|0:0,0:0:0:0: +65,200,4877,2,0,L|66:186,2,15,0|0|0,3:2|0:0|0:0,0:0:0:0: +68,265,5241,2,0,L|70:236,1,30,2|0,3:2|0:0,0:0:0:0: +77,335,5604,6,0,L|76:373,2,37.5,0|0|0,1:0|3:0|3:0,0:0:0:0: +86,152,6332,21,2,3:2:0:0: +88,199,6513,1,2,0:0:0:0: +94,157,6877,5,2,0:0:0:0: +96,204,7059,2,0,P|100:218|99:233,1,30,2|0,1:2|0:0,0:0:0:0: +101,161,7423,2,0,P|96:146|97:131,1,30,2|0,0:2|0:1,0:0:0:0: +106,85,7786,37,0,3:3:0:0: +105,49,7968,1,0,0:3:0:0: +111,82,8150,1,2,3:3:0:0: +110,45,8332,2,0,P|110:30|106:16,1,30,2|0,0:3|0:0,0:0:0:0: +110,87,8695,2,0,P|110:102|114:117,1,30,2|0,0:3|0:0,0:0:0:0: +126,290,9241,5,4,3:3:0:0: +131,232,9423,2,0,B|134:220|124:211|124:211|129:218|129:223,1,35.9999989013672,8|0,0:3|0:0,0:0:0:0: +136,297,9786,37,4,0:3:0:0: +136,234,9968,2,0,P|138:212|133:190,1,45,2|0,1:2|0:0,0:0:0:0: +139,191,10332,2,0,P|144:212|142:235,1,45,2|0,0:0|0:0,0:0:0:0: +146,264,10695,5,2,3:1:0:0: +148,306,10877,1,2,0:1:0:0: +151,267,11059,1,2,3:1:0:0: +153,309,11241,1,2,0:1:0:0: +156,351,11423,6,0,B|158:362|158:362|159:354|159:350,1,22.5000008583069,2|0,1:0|0:0,0:0:0:0: +158,311,11604,1,2,0:3:0:0: +160,289,11695,1,2,0:3:0:0: +161,267,11786,2,0,L|162:244,1,22.5000008583069,2|0,0:3|0:0,0:0:0:0: +165,268,11968,2,0,L|166:245,1,22.5000008583069,2|0,0:3|0:0,0:0:0:0: +166,187,12150,22,0,L|168:158,1,30,2|0,3:2|0:0,0:0:0:0: +174,121,12513,1,2,0:0:0:0: +174,121,12695,2,0,L|178:195,1,75,2|0,0:0|0:0,0:0:0:0: +183,199,13241,2,0,L|181:170,1,30,2|0,0:0|0:0,0:0:0:0: +186,72,13604,5,0,3:0:0:0: +188,35,13786,1,0,3:0:0:0: +191,0,13968,1,0,3:2:0:0: +191,0,14150,2,0,L|195:89,1,90,2|2,0:0|0:0,0:0:0:0: +206,181,15059,37,2,3:2:0:0: +207,167,15150,1,2,0:0:0:0: +208,152,15240,1,2,0:0:0:0: +214,115,15422,1,2,0:0:0:0: +214,115,15604,2,0,L|218:189,1,75,2|0,0:0|0:0,0:0:0:0: +223,193,16150,6,0,L|221:169,2,22.5,2|2|2,0:0|0:0|0:0,0:0:0:0: +226,228,16513,1,2,3:1:0:0: +229,263,16695,1,2,0:1:0:0: +230,277,16785,1,2,0:1:0:0: +231,292,16877,2,0,L|233:321,1,30,2|0,3:1|0:0,0:0:0:0: +236,218,17241,6,0,L|238:180,2,35.9999989013672,0|0|0,1:0|3:0|3:0,0:0:0:0: +246,362,18150,21,2,3:2:0:0: +248,315,18331,1,2,0:0:0:0: +253,358,18695,5,2,0:0:0:0: +257,310,18877,1,2,1:2:0:0: +261,354,19059,2,0,P|266:369|265:384,2,30,2|0|0,0:0|0:1|0:0,0:0:0:0: +266,305,19604,37,0,3:0:0:0: +269,254,19786,1,2,0:3:0:0: +270,240,19876,1,2,0:3:0:0: +271,225,19968,2,0,L|272:204,2,15,2|2|2,3:1|0:1|0:1,0:0:0:0: +275,301,20331,1,2,1:0:0:0: +276,316,20422,2,0,B|277:328|277:328|275:332|275:332|276:345,1,30,2|0,0:1|0:0,0:0:0:0: +281,349,20695,2,0,B|278:335|287:332|282:316,1,30,2|0,0:1|0:0,0:0:0:0: +286,142,21059,5,4,3:0:0:0: +291,199,21240,2,0,B|291:209|283:214|283:214|281:204|291:199,1,35.9999989013672,8|0,0:3|0:0,0:0:0:0: +296,139,21604,37,4,0:3:0:0: +296,197,21786,2,0,P|300:211|299:226,1,30,2|0,1:1|0:0,0:0:0:0: +301,291,22150,2,0,P|297:276|297:261,1,30,2|0,0:1|0:0,0:0:0:0: +306,136,22513,5,2,3:3:0:0: +311,97,22695,1,2,0:3:0:0: +311,97,22786,1,2,0:3:0:0: +311,97,22877,1,2,3:1:0:0: +313,106,22968,1,2,0:1:0:0: +314,115,23059,1,2,0:1:0:0: +315,124,23150,1,2,0:1:0:0: +316,133,23240,6,0,B|308:125|308:125|306:136,1,22.5000008583069,2|0,1:1|0:0,0:0:0:0: +319,168,23421,5,2,0:1:0:0: +319,201,23604,38,0,L|320:224,1,22.5000008583069,2|0,0:3|0:0,0:0:0:0: +323,200,23786,2,0,L|324:223,1,22.5000008583069,2|0,0:3|0:0,0:0:0:0: +328,297,23968,21,2,3:2:0:0: +328,297,24150,2,0,L|326:268,1,30,2|0,0:0|0:0,0:0:0:0: +331,373,24513,1,2,0:0:0:0: +338,344,24695,1,2,1:0:0:0: +337,329,24787,2,0,L|336:314,1,15,2|0,0:1|0:0,0:0:0:0: +341,239,25059,2,0,L|339:210,1,30,2|0,0:1|0:0,0:0:0:0: +351,122,25422,5,2,3:2:0:0: +351,122,25604,2,0,L|352:107,2,15,2|2|2,0:0|0:0|3:2,0:0:0:0: +354,195,25968,1,2,0:3:0:0: +355,180,26059,1,2,0:3:0:0: +356,165,26149,2,0,L|358:136,2,30,2|0|0,1:3|0:0|0:0,0:0:0:0: +366,79,26877,37,2,3:2:0:0: +369,44,27059,1,2,0:1:0:0: +370,30,27149,1,2,0:1:0:0: +371,15,27240,2,0,L|371:0,2,15,2|2|2,0:1|0:1|0:1,0:0:0:0: +376,101,27604,1,2,1:1:0:0: +377,86,27696,2,0,L|378:65,1,15,2|0,0:1|0:0,0:0:0:0: +381,138,27968,2,0,L|379:167,1,30,2|0,0:1|0:0,0:0:0:0: +386,277,28331,5,2,3:3:0:0: +389,242,28513,1,2,0:3:0:0: +390,227,28604,1,2,0:3:0:0: +391,212,28695,2,0,L|393:183,1,30,2|2,0:3|0:3,0:0:0:0: +398,293,29059,6,0,L|396:331,2,37.5,2|2|2,1:0|3:1|3:1,0:0:0:0: +406,83,29786,21,2,3:2:0:0: +408,130,29967,1,2,0:0:0:0: +413,87,30331,5,2,0:0:0:0: +417,135,30513,1,2,1:0:0:0: +418,150,30605,2,0,L|419:171,1,15,2|0,0:1|0:0,0:0:0:0: +421,91,30877,2,0,P|426:76|425:61,1,30,2|0,0:1|0:0,0:0:0:0: +426,140,31240,37,2,3:2:0:0: +429,193,31422,1,2,0:1:0:0: +430,208,31513,1,2,0:1:0:0: +431,223,31603,1,2,0:1:0:0: +433,237,31695,1,2,0:1:0:0: +434,252,31786,2,0,P|436:237|435:222,1,30,2|0,0:2|1:0,0:0:0:0: +442,296,32149,2,0,P|439:310|440:325,1,30,2|0,0:0|0:0,0:0:0:0: +446,120,32695,5,4,3:0:0:0: +451,63,32877,2,0,B|448:54|448:54|441:49|441:49|443:57|443:57|451:63,1,35.9999989013672,8|0,0:3|0:0,0:0:0:0: +456,123,33240,37,4,0:3:0:0: +456,65,33422,1,2,1:0:0:0: +457,50,33513,2,0,P|451:31|443:20,1,30,2|0,0:1|0:0,0:0:0:0: +461,0,33786,1,2,0:1:0:0: +462,15,33877,2,0,L|462:29,1,15,2|0,0:1|0:0,0:0:0:0: +466,127,34149,5,2,3:3:0:0: +470,180,34331,1,2,0:3:0:0: +470,180,34422,1,2,0:3:0:0: +470,180,34513,1,2,3:1:0:0: +471,171,34604,1,2,0:1:0:0: +472,162,34695,1,2,0:1:0:0: +476,130,34877,6,0,B|486:125|486:125|481:127|475:126,1,22.5000008583069,2|0,1:1|0:0,0:0:0:0: +479,95,35059,5,2,0:1:0:0: +479,62,35240,38,0,L|480:39,1,22.5000008583069,2|0,0:3|0:0,0:0:0:0: +483,63,35422,2,0,L|484:40,1,22.5000008583069,2|0,0:3|0:0,0:0:0:0: +256,192,35604,12,4,37013,0:3:0:0: +256,192,37059,12,4,39149,0:3:0:0: +496,360,40695,6,0,B|498:344|498:344|495:329|495:329|496:314,1,45.0000017166138,2|0,3:1|0:0,0:0:0:0: +503,270,41059,1,2,0:1:0:0: +504,262,41150,1,2,0:1:0:0: +505,254,41240,2,0,L|506:242,3,11.2500004291535,2|0|0|0,0:1|0:0|0:0|0:0,0:0:0:0: diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2781126-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2781126-expected-conversion.json new file mode 100644 index 0000000000..e03f6ae672 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2781126-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":313.0,"Objects":[{"StartTime":313.0,"Position":65.0,"HyperDash":false},{"StartTime":366.0,"Position":482.0,"HyperDash":false},{"StartTime":420.0,"Position":164.0,"HyperDash":false},{"StartTime":474.0,"Position":315.0,"HyperDash":false},{"StartTime":528.0,"Position":145.0,"HyperDash":false},{"StartTime":582.0,"Position":159.0,"HyperDash":false},{"StartTime":636.0,"Position":310.0,"HyperDash":false},{"StartTime":690.0,"Position":441.0,"HyperDash":false},{"StartTime":744.0,"Position":428.0,"HyperDash":false},{"StartTime":797.0,"Position":243.0,"HyperDash":false},{"StartTime":851.0,"Position":422.0,"HyperDash":false},{"StartTime":905.0,"Position":481.0,"HyperDash":false},{"StartTime":959.0,"Position":104.0,"HyperDash":false},{"StartTime":1013.0,"Position":473.0,"HyperDash":false},{"StartTime":1067.0,"Position":135.0,"HyperDash":false},{"StartTime":1121.0,"Position":360.0,"HyperDash":false},{"StartTime":1175.0,"Position":123.0,"HyperDash":false}]},{"StartTime":1348.0,"Objects":[{"StartTime":1348.0,"Position":224.0,"HyperDash":false}]},{"StartTime":1434.0,"Objects":[{"StartTime":1434.0,"Position":177.0,"HyperDash":false}]},{"StartTime":1520.0,"Objects":[{"StartTime":1520.0,"Position":179.0,"HyperDash":false}]},{"StartTime":1606.0,"Objects":[{"StartTime":1606.0,"Position":227.0,"HyperDash":false}]},{"StartTime":1693.0,"Objects":[{"StartTime":1693.0,"Position":292.0,"HyperDash":false},{"StartTime":1779.0,"Position":295.206116,"HyperDash":true}]},{"StartTime":1865.0,"Objects":[{"StartTime":1865.0,"Position":116.0,"HyperDash":false},{"StartTime":1951.0,"Position":99.0,"HyperDash":false},{"StartTime":2037.0,"Position":117.970421,"HyperDash":false},{"StartTime":2105.0,"Position":172.025635,"HyperDash":false},{"StartTime":2209.0,"Position":206.639481,"HyperDash":false}]},{"StartTime":2296.0,"Objects":[{"StartTime":2296.0,"Position":116.0,"HyperDash":false}]},{"StartTime":2382.0,"Objects":[{"StartTime":2382.0,"Position":26.0,"HyperDash":false},{"StartTime":2450.0,"Position":34.6324959,"HyperDash":false},{"StartTime":2554.0,"Position":22.54102,"HyperDash":true}]},{"StartTime":2727.0,"Objects":[{"StartTime":2727.0,"Position":292.0,"HyperDash":false},{"StartTime":2795.0,"Position":337.5814,"HyperDash":false},{"StartTime":2899.0,"Position":382.0,"HyperDash":false}]},{"StartTime":2986.0,"Objects":[{"StartTime":2986.0,"Position":328.0,"HyperDash":false}]},{"StartTime":3072.0,"Objects":[{"StartTime":3072.0,"Position":276.0,"HyperDash":false}]},{"StartTime":3244.0,"Objects":[{"StartTime":3244.0,"Position":448.0,"HyperDash":false}]},{"StartTime":3417.0,"Objects":[{"StartTime":3417.0,"Position":268.0,"HyperDash":false},{"StartTime":3485.0,"Position":236.41861,"HyperDash":false},{"StartTime":3589.0,"Position":178.0,"HyperDash":false}]},{"StartTime":3675.0,"Objects":[{"StartTime":3675.0,"Position":244.0,"HyperDash":false}]},{"StartTime":3762.0,"Objects":[{"StartTime":3762.0,"Position":178.0,"HyperDash":false},{"StartTime":3830.0,"Position":149.41861,"HyperDash":false},{"StartTime":3934.0,"Position":88.0,"HyperDash":true}]},{"StartTime":4106.0,"Objects":[{"StartTime":4106.0,"Position":444.0,"HyperDash":false},{"StartTime":4192.0,"Position":447.737061,"HyperDash":false}]},{"StartTime":4279.0,"Objects":[{"StartTime":4279.0,"Position":376.0,"HyperDash":false},{"StartTime":4365.0,"Position":372.262939,"HyperDash":false}]},{"StartTime":4451.0,"Objects":[{"StartTime":4451.0,"Position":300.0,"HyperDash":false}]},{"StartTime":4624.0,"Objects":[{"StartTime":4624.0,"Position":472.0,"HyperDash":false},{"StartTime":4710.0,"Position":475.451355,"HyperDash":true}]},{"StartTime":4796.0,"Objects":[{"StartTime":4796.0,"Position":296.0,"HyperDash":false},{"StartTime":4864.0,"Position":285.639862,"HyperDash":false},{"StartTime":4968.0,"Position":274.157928,"HyperDash":false}]},{"StartTime":5055.0,"Objects":[{"StartTime":5055.0,"Position":366.0,"HyperDash":false}]},{"StartTime":5141.0,"Objects":[{"StartTime":5141.0,"Position":456.0,"HyperDash":false},{"StartTime":5209.0,"Position":405.4186,"HyperDash":false},{"StartTime":5313.0,"Position":366.0,"HyperDash":true}]},{"StartTime":5486.0,"Objects":[{"StartTime":5486.0,"Position":112.0,"HyperDash":false},{"StartTime":5554.0,"Position":144.58139,"HyperDash":false},{"StartTime":5658.0,"Position":202.0,"HyperDash":false}]},{"StartTime":5744.0,"Objects":[{"StartTime":5744.0,"Position":268.0,"HyperDash":false}]},{"StartTime":5831.0,"Objects":[{"StartTime":5831.0,"Position":202.0,"HyperDash":false}]},{"StartTime":6003.0,"Objects":[{"StartTime":6003.0,"Position":360.0,"HyperDash":false}]},{"StartTime":6175.0,"Objects":[{"StartTime":6175.0,"Position":192.0,"HyperDash":false},{"StartTime":6243.0,"Position":146.41861,"HyperDash":false},{"StartTime":6347.0,"Position":102.0,"HyperDash":false}]},{"StartTime":6434.0,"Objects":[{"StartTime":6434.0,"Position":172.0,"HyperDash":false}]},{"StartTime":6520.0,"Objects":[{"StartTime":6520.0,"Position":102.0,"HyperDash":false},{"StartTime":6588.0,"Position":71.4186,"HyperDash":false},{"StartTime":6692.0,"Position":12.0,"HyperDash":true}]},{"StartTime":6865.0,"Objects":[{"StartTime":6865.0,"Position":288.0,"HyperDash":false}]},{"StartTime":6951.0,"Objects":[{"StartTime":6951.0,"Position":335.0,"HyperDash":false}]},{"StartTime":7037.0,"Objects":[{"StartTime":7037.0,"Position":333.0,"HyperDash":false}]},{"StartTime":7124.0,"Objects":[{"StartTime":7124.0,"Position":285.0,"HyperDash":false}]},{"StartTime":7210.0,"Objects":[{"StartTime":7210.0,"Position":220.0,"HyperDash":false},{"StartTime":7296.0,"Position":216.793884,"HyperDash":false}]},{"StartTime":7382.0,"Objects":[{"StartTime":7382.0,"Position":320.0,"HyperDash":false}]},{"StartTime":7555.0,"Objects":[{"StartTime":7555.0,"Position":204.0,"HyperDash":true}]},{"StartTime":7727.0,"Objects":[{"StartTime":7727.0,"Position":456.0,"HyperDash":false}]},{"StartTime":7813.0,"Objects":[{"StartTime":7813.0,"Position":460.0,"HyperDash":false}]},{"StartTime":7900.0,"Objects":[{"StartTime":7900.0,"Position":464.0,"HyperDash":false},{"StartTime":7968.0,"Position":437.4186,"HyperDash":false},{"StartTime":8072.0,"Position":374.0,"HyperDash":true}]},{"StartTime":8244.0,"Objects":[{"StartTime":8244.0,"Position":120.0,"HyperDash":false},{"StartTime":8312.0,"Position":159.58139,"HyperDash":false},{"StartTime":8416.0,"Position":210.0,"HyperDash":false}]},{"StartTime":8503.0,"Objects":[{"StartTime":8503.0,"Position":280.0,"HyperDash":false}]},{"StartTime":8589.0,"Objects":[{"StartTime":8589.0,"Position":348.0,"HyperDash":false}]},{"StartTime":8762.0,"Objects":[{"StartTime":8762.0,"Position":176.0,"HyperDash":false}]},{"StartTime":8934.0,"Objects":[{"StartTime":8934.0,"Position":354.0,"HyperDash":false},{"StartTime":9002.0,"Position":379.5814,"HyperDash":false},{"StartTime":9106.0,"Position":444.0,"HyperDash":false}]},{"StartTime":9193.0,"Objects":[{"StartTime":9193.0,"Position":374.0,"HyperDash":false}]},{"StartTime":9279.0,"Objects":[{"StartTime":9279.0,"Position":306.0,"HyperDash":false},{"StartTime":9347.0,"Position":331.5814,"HyperDash":false},{"StartTime":9451.0,"Position":396.0,"HyperDash":true}]},{"StartTime":9624.0,"Objects":[{"StartTime":9624.0,"Position":148.0,"HyperDash":false},{"StartTime":9710.0,"Position":104.34359,"HyperDash":false}]},{"StartTime":9796.0,"Objects":[{"StartTime":9796.0,"Position":176.0,"HyperDash":false},{"StartTime":9882.0,"Position":219.6564,"HyperDash":false}]},{"StartTime":9969.0,"Objects":[{"StartTime":9969.0,"Position":148.0,"HyperDash":false}]},{"StartTime":10141.0,"Objects":[{"StartTime":10141.0,"Position":308.0,"HyperDash":false}]},{"StartTime":10313.0,"Objects":[{"StartTime":10313.0,"Position":140.0,"HyperDash":true}]},{"StartTime":10486.0,"Objects":[{"StartTime":10486.0,"Position":396.0,"HyperDash":false},{"StartTime":10572.0,"Position":441.0,"HyperDash":false},{"StartTime":10658.0,"Position":396.0,"HyperDash":false}]},{"StartTime":10831.0,"Objects":[{"StartTime":10831.0,"Position":228.0,"HyperDash":true}]},{"StartTime":11003.0,"Objects":[{"StartTime":11003.0,"Position":460.0,"HyperDash":false},{"StartTime":11089.0,"Position":482.326263,"HyperDash":false}]},{"StartTime":11175.0,"Objects":[{"StartTime":11175.0,"Position":392.0,"HyperDash":false},{"StartTime":11261.0,"Position":414.326263,"HyperDash":false}]},{"StartTime":11348.0,"Objects":[{"StartTime":11348.0,"Position":324.0,"HyperDash":false},{"StartTime":11434.0,"Position":345.614166,"HyperDash":false}]},{"StartTime":11520.0,"Objects":[{"StartTime":11520.0,"Position":260.0,"HyperDash":false},{"StartTime":11606.0,"Position":282.326263,"HyperDash":false}]},{"StartTime":11693.0,"Objects":[{"StartTime":11693.0,"Position":384.0,"HyperDash":false}]},{"StartTime":11865.0,"Objects":[{"StartTime":11865.0,"Position":220.0,"HyperDash":false},{"StartTime":11951.0,"Position":175.0,"HyperDash":true}]},{"StartTime":12037.0,"Objects":[{"StartTime":12037.0,"Position":400.0,"HyperDash":false},{"StartTime":12123.0,"Position":463.25,"HyperDash":false},{"StartTime":12209.0,"Position":488.0,"HyperDash":false},{"StartTime":12295.0,"Position":488.0,"HyperDash":true}]},{"StartTime":12382.0,"Objects":[{"StartTime":12382.0,"Position":284.0,"HyperDash":false},{"StartTime":12450.0,"Position":255.41861,"HyperDash":false},{"StartTime":12554.0,"Position":194.0,"HyperDash":false}]},{"StartTime":12641.0,"Objects":[{"StartTime":12641.0,"Position":264.0,"HyperDash":true}]},{"StartTime":12727.0,"Objects":[{"StartTime":12727.0,"Position":436.0,"HyperDash":false}]},{"StartTime":12900.0,"Objects":[{"StartTime":12900.0,"Position":328.0,"HyperDash":false},{"StartTime":12986.0,"Position":324.793884,"HyperDash":false}]},{"StartTime":13072.0,"Objects":[{"StartTime":13072.0,"Position":424.0,"HyperDash":false},{"StartTime":13140.0,"Position":437.3675,"HyperDash":false},{"StartTime":13244.0,"Position":427.458984,"HyperDash":false}]},{"StartTime":13331.0,"Objects":[{"StartTime":13331.0,"Position":360.0,"HyperDash":true}]},{"StartTime":13417.0,"Objects":[{"StartTime":13417.0,"Position":208.0,"HyperDash":false},{"StartTime":13485.0,"Position":174.41861,"HyperDash":false},{"StartTime":13589.0,"Position":118.0,"HyperDash":false}]},{"StartTime":13762.0,"Objects":[{"StartTime":13762.0,"Position":292.0,"HyperDash":false},{"StartTime":13830.0,"Position":274.545563,"HyperDash":false},{"StartTime":13934.0,"Position":295.909363,"HyperDash":false}]},{"StartTime":14020.0,"Objects":[{"StartTime":14020.0,"Position":228.0,"HyperDash":true}]},{"StartTime":14106.0,"Objects":[{"StartTime":14106.0,"Position":408.0,"HyperDash":false},{"StartTime":14174.0,"Position":426.5814,"HyperDash":false},{"StartTime":14278.0,"Position":498.0,"HyperDash":true}]},{"StartTime":14451.0,"Objects":[{"StartTime":14451.0,"Position":228.0,"HyperDash":false},{"StartTime":14519.0,"Position":266.5814,"HyperDash":false},{"StartTime":14623.0,"Position":318.0,"HyperDash":true}]},{"StartTime":14796.0,"Objects":[{"StartTime":14796.0,"Position":48.0,"HyperDash":false},{"StartTime":14864.0,"Position":91.5814,"HyperDash":false},{"StartTime":14968.0,"Position":138.0,"HyperDash":true}]},{"StartTime":15141.0,"Objects":[{"StartTime":15141.0,"Position":392.0,"HyperDash":false},{"StartTime":15227.0,"Position":394.993347,"HyperDash":false}]},{"StartTime":15313.0,"Objects":[{"StartTime":15313.0,"Position":320.0,"HyperDash":false},{"StartTime":15399.0,"Position":317.006653,"HyperDash":true}]},{"StartTime":15486.0,"Objects":[{"StartTime":15486.0,"Position":488.0,"HyperDash":false}]},{"StartTime":15658.0,"Objects":[{"StartTime":15658.0,"Position":388.0,"HyperDash":false},{"StartTime":15744.0,"Position":343.0,"HyperDash":false}]},{"StartTime":15831.0,"Objects":[{"StartTime":15831.0,"Position":240.0,"HyperDash":false},{"StartTime":15899.0,"Position":231.454437,"HyperDash":false},{"StartTime":16003.0,"Position":236.090652,"HyperDash":false}]},{"StartTime":16089.0,"Objects":[{"StartTime":16089.0,"Position":304.0,"HyperDash":true}]},{"StartTime":16175.0,"Objects":[{"StartTime":16175.0,"Position":132.0,"HyperDash":false},{"StartTime":16243.0,"Position":99.4186,"HyperDash":false},{"StartTime":16347.0,"Position":42.0,"HyperDash":true}]},{"StartTime":16520.0,"Objects":[{"StartTime":16520.0,"Position":312.0,"HyperDash":false},{"StartTime":16588.0,"Position":274.4186,"HyperDash":false},{"StartTime":16692.0,"Position":222.0,"HyperDash":false}]},{"StartTime":16779.0,"Objects":[{"StartTime":16779.0,"Position":152.0,"HyperDash":true}]},{"StartTime":16865.0,"Objects":[{"StartTime":16865.0,"Position":328.0,"HyperDash":false},{"StartTime":16933.0,"Position":309.4186,"HyperDash":false},{"StartTime":17037.0,"Position":238.0,"HyperDash":false}]},{"StartTime":17210.0,"Objects":[{"StartTime":17210.0,"Position":328.0,"HyperDash":false}]},{"StartTime":17382.0,"Objects":[{"StartTime":17382.0,"Position":164.0,"HyperDash":false},{"StartTime":17468.0,"Position":160.54866,"HyperDash":true}]},{"StartTime":17555.0,"Objects":[{"StartTime":17555.0,"Position":336.0,"HyperDash":false},{"StartTime":17623.0,"Position":354.5814,"HyperDash":false},{"StartTime":17727.0,"Position":426.0,"HyperDash":true}]},{"StartTime":17900.0,"Objects":[{"StartTime":17900.0,"Position":152.0,"HyperDash":false}]},{"StartTime":17986.0,"Objects":[{"StartTime":17986.0,"Position":155.0,"HyperDash":false}]},{"StartTime":18072.0,"Objects":[{"StartTime":18072.0,"Position":192.0,"HyperDash":false}]},{"StartTime":18158.0,"Objects":[{"StartTime":18158.0,"Position":252.0,"HyperDash":true}]},{"StartTime":18244.0,"Objects":[{"StartTime":18244.0,"Position":404.0,"HyperDash":false},{"StartTime":18312.0,"Position":416.481262,"HyperDash":false},{"StartTime":18416.0,"Position":407.746735,"HyperDash":true}]},{"StartTime":18589.0,"Objects":[{"StartTime":18589.0,"Position":156.0,"HyperDash":false},{"StartTime":18657.0,"Position":101.4186,"HyperDash":false},{"StartTime":18761.0,"Position":66.0,"HyperDash":false}]},{"StartTime":18848.0,"Objects":[{"StartTime":18848.0,"Position":136.0,"HyperDash":true}]},{"StartTime":18934.0,"Objects":[{"StartTime":18934.0,"Position":304.0,"HyperDash":false},{"StartTime":19002.0,"Position":346.5814,"HyperDash":false},{"StartTime":19106.0,"Position":394.0,"HyperDash":true}]},{"StartTime":19279.0,"Objects":[{"StartTime":19279.0,"Position":120.0,"HyperDash":false},{"StartTime":19347.0,"Position":113.063881,"HyperDash":false},{"StartTime":19451.0,"Position":111.495346,"HyperDash":false}]},{"StartTime":19537.0,"Objects":[{"StartTime":19537.0,"Position":180.0,"HyperDash":true}]},{"StartTime":19624.0,"Objects":[{"StartTime":19624.0,"Position":360.0,"HyperDash":false},{"StartTime":19692.0,"Position":315.4186,"HyperDash":false},{"StartTime":19796.0,"Position":270.0,"HyperDash":true}]},{"StartTime":19969.0,"Objects":[{"StartTime":19969.0,"Position":32.0,"HyperDash":false},{"StartTime":20037.0,"Position":60.581398,"HyperDash":false},{"StartTime":20141.0,"Position":122.0,"HyperDash":false}]},{"StartTime":20227.0,"Objects":[{"StartTime":20227.0,"Position":188.0,"HyperDash":true}]},{"StartTime":20313.0,"Objects":[{"StartTime":20313.0,"Position":16.0,"HyperDash":false},{"StartTime":20381.0,"Position":51.581398,"HyperDash":false},{"StartTime":20485.0,"Position":106.0,"HyperDash":true}]},{"StartTime":20658.0,"Objects":[{"StartTime":20658.0,"Position":368.0,"HyperDash":false},{"StartTime":20744.0,"Position":320.104462,"HyperDash":false},{"StartTime":20830.0,"Position":260.0,"HyperDash":false},{"StartTime":20916.0,"Position":298.686646,"HyperDash":false},{"StartTime":21002.0,"Position":368.0,"HyperDash":false},{"StartTime":21070.0,"Position":333.8027,"HyperDash":false},{"StartTime":21175.0,"Position":260.0,"HyperDash":true}]},{"StartTime":21348.0,"Objects":[{"StartTime":21348.0,"Position":496.0,"HyperDash":false}]},{"StartTime":21520.0,"Objects":[{"StartTime":21520.0,"Position":324.0,"HyperDash":false}]},{"StartTime":21693.0,"Objects":[{"StartTime":21693.0,"Position":496.0,"HyperDash":false}]},{"StartTime":21865.0,"Objects":[{"StartTime":21865.0,"Position":388.0,"HyperDash":false},{"StartTime":21951.0,"Position":343.0,"HyperDash":true}]},{"StartTime":22037.0,"Objects":[{"StartTime":22037.0,"Position":144.0,"HyperDash":false}]},{"StartTime":22210.0,"Objects":[{"StartTime":22210.0,"Position":252.0,"HyperDash":false},{"StartTime":22296.0,"Position":231.875381,"HyperDash":false}]},{"StartTime":22382.0,"Objects":[{"StartTime":22382.0,"Position":312.0,"HyperDash":false},{"StartTime":22468.0,"Position":291.8754,"HyperDash":false}]},{"StartTime":22555.0,"Objects":[{"StartTime":22555.0,"Position":372.0,"HyperDash":false},{"StartTime":22641.0,"Position":351.8754,"HyperDash":true}]},{"StartTime":22727.0,"Objects":[{"StartTime":22727.0,"Position":180.0,"HyperDash":false},{"StartTime":22795.0,"Position":226.58139,"HyperDash":false},{"StartTime":22899.0,"Position":270.0,"HyperDash":false}]},{"StartTime":22986.0,"Objects":[{"StartTime":22986.0,"Position":208.0,"HyperDash":true}]},{"StartTime":23072.0,"Objects":[{"StartTime":23072.0,"Position":436.0,"HyperDash":false},{"StartTime":23158.0,"Position":486.800659,"HyperDash":false},{"StartTime":23244.0,"Position":494.721924,"HyperDash":false},{"StartTime":23330.0,"Position":435.854675,"HyperDash":true}]},{"StartTime":23417.0,"Objects":[{"StartTime":23417.0,"Position":208.0,"HyperDash":false},{"StartTime":23503.0,"Position":163.75,"HyperDash":false},{"StartTime":23589.0,"Position":95.5,"HyperDash":false},{"StartTime":23657.0,"Position":134.976746,"HyperDash":false},{"StartTime":23761.0,"Position":208.0,"HyperDash":false}]},{"StartTime":23934.0,"Objects":[{"StartTime":23934.0,"Position":312.0,"HyperDash":false}]},{"StartTime":24020.0,"Objects":[{"StartTime":24020.0,"Position":220.0,"HyperDash":false}]},{"StartTime":24106.0,"Objects":[{"StartTime":24106.0,"Position":128.0,"HyperDash":false},{"StartTime":24174.0,"Position":164.58139,"HyperDash":false},{"StartTime":24278.0,"Position":218.0,"HyperDash":false}]},{"StartTime":24451.0,"Objects":[{"StartTime":24451.0,"Position":392.0,"HyperDash":false}]},{"StartTime":24537.0,"Objects":[{"StartTime":24537.0,"Position":444.0,"HyperDash":false}]},{"StartTime":24624.0,"Objects":[{"StartTime":24624.0,"Position":444.0,"HyperDash":false}]},{"StartTime":24710.0,"Objects":[{"StartTime":24710.0,"Position":392.0,"HyperDash":true}]},{"StartTime":24796.0,"Objects":[{"StartTime":24796.0,"Position":212.0,"HyperDash":false},{"StartTime":24882.0,"Position":244.0,"HyperDash":false},{"StartTime":24968.0,"Position":302.0,"HyperDash":false},{"StartTime":25036.0,"Position":269.4186,"HyperDash":false},{"StartTime":25140.0,"Position":212.0,"HyperDash":false}]},{"StartTime":25313.0,"Objects":[{"StartTime":25313.0,"Position":320.0,"HyperDash":false}]},{"StartTime":25400.0,"Objects":[{"StartTime":25400.0,"Position":384.0,"HyperDash":false}]},{"StartTime":25486.0,"Objects":[{"StartTime":25486.0,"Position":284.0,"HyperDash":false},{"StartTime":25554.0,"Position":267.4186,"HyperDash":false},{"StartTime":25658.0,"Position":194.0,"HyperDash":true}]},{"StartTime":25831.0,"Objects":[{"StartTime":25831.0,"Position":448.0,"HyperDash":false},{"StartTime":25917.0,"Position":444.548645,"HyperDash":false}]},{"StartTime":26003.0,"Objects":[{"StartTime":26003.0,"Position":344.0,"HyperDash":false},{"StartTime":26089.0,"Position":299.0,"HyperDash":true}]},{"StartTime":26175.0,"Objects":[{"StartTime":26175.0,"Position":128.0,"HyperDash":false},{"StartTime":26261.0,"Position":80.0,"HyperDash":false},{"StartTime":26347.0,"Position":38.0,"HyperDash":false},{"StartTime":26415.0,"Position":73.58139,"HyperDash":false},{"StartTime":26519.0,"Position":128.0,"HyperDash":false}]},{"StartTime":26693.0,"Objects":[{"StartTime":26693.0,"Position":236.0,"HyperDash":false}]},{"StartTime":26779.0,"Objects":[{"StartTime":26779.0,"Position":299.0,"HyperDash":false}]},{"StartTime":26865.0,"Objects":[{"StartTime":26865.0,"Position":362.0,"HyperDash":false}]},{"StartTime":27037.0,"Objects":[{"StartTime":27037.0,"Position":196.0,"HyperDash":false}]},{"StartTime":27210.0,"Objects":[{"StartTime":27210.0,"Position":352.0,"HyperDash":false}]},{"StartTime":27296.0,"Objects":[{"StartTime":27296.0,"Position":352.0,"HyperDash":false}]},{"StartTime":27382.0,"Objects":[{"StartTime":27382.0,"Position":312.0,"HyperDash":false}]},{"StartTime":27469.0,"Objects":[{"StartTime":27469.0,"Position":248.0,"HyperDash":true}]},{"StartTime":27555.0,"Objects":[{"StartTime":27555.0,"Position":412.0,"HyperDash":false},{"StartTime":27641.0,"Position":349.0,"HyperDash":false},{"StartTime":27727.0,"Position":322.0,"HyperDash":false},{"StartTime":27795.0,"Position":343.5814,"HyperDash":false},{"StartTime":27899.0,"Position":412.0,"HyperDash":false}]},{"StartTime":28072.0,"Objects":[{"StartTime":28072.0,"Position":304.0,"HyperDash":false}]},{"StartTime":28158.0,"Objects":[{"StartTime":28158.0,"Position":396.0,"HyperDash":false}]},{"StartTime":28244.0,"Objects":[{"StartTime":28244.0,"Position":488.0,"HyperDash":false},{"StartTime":28312.0,"Position":451.4186,"HyperDash":false},{"StartTime":28416.0,"Position":398.0,"HyperDash":true}]},{"StartTime":28589.0,"Objects":[{"StartTime":28589.0,"Position":88.0,"HyperDash":false}]},{"StartTime":28934.0,"Objects":[{"StartTime":28934.0,"Position":340.0,"HyperDash":false},{"StartTime":29002.0,"Position":358.545563,"HyperDash":false},{"StartTime":29106.0,"Position":343.909363,"HyperDash":false}]},{"StartTime":29279.0,"Objects":[{"StartTime":29279.0,"Position":172.0,"HyperDash":false},{"StartTime":29347.0,"Position":182.577881,"HyperDash":false},{"StartTime":29451.0,"Position":168.402878,"HyperDash":false}]},{"StartTime":29537.0,"Objects":[{"StartTime":29537.0,"Position":268.0,"HyperDash":false}]},{"StartTime":29624.0,"Objects":[{"StartTime":29624.0,"Position":368.0,"HyperDash":false},{"StartTime":29692.0,"Position":343.4186,"HyperDash":false},{"StartTime":29796.0,"Position":278.0,"HyperDash":false}]},{"StartTime":29969.0,"Objects":[{"StartTime":29969.0,"Position":452.0,"HyperDash":false},{"StartTime":30055.0,"Position":459.397949,"HyperDash":false},{"StartTime":30141.0,"Position":452.0,"HyperDash":true}]},{"StartTime":30313.0,"Objects":[{"StartTime":30313.0,"Position":200.0,"HyperDash":false},{"StartTime":30381.0,"Position":196.454437,"HyperDash":false},{"StartTime":30485.0,"Position":196.090652,"HyperDash":false}]},{"StartTime":30658.0,"Objects":[{"StartTime":30658.0,"Position":368.0,"HyperDash":false},{"StartTime":30726.0,"Position":349.4186,"HyperDash":false},{"StartTime":30830.0,"Position":278.0,"HyperDash":false}]},{"StartTime":30917.0,"Objects":[{"StartTime":30917.0,"Position":380.0,"HyperDash":false}]},{"StartTime":31003.0,"Objects":[{"StartTime":31003.0,"Position":480.0,"HyperDash":false},{"StartTime":31071.0,"Position":435.4186,"HyperDash":false},{"StartTime":31175.0,"Position":390.0,"HyperDash":true}]},{"StartTime":31348.0,"Objects":[{"StartTime":31348.0,"Position":128.0,"HyperDash":false},{"StartTime":31434.0,"Position":124.54866,"HyperDash":false}]},{"StartTime":31520.0,"Objects":[{"StartTime":31520.0,"Position":228.0,"HyperDash":false},{"StartTime":31606.0,"Position":273.0,"HyperDash":true}]},{"StartTime":31693.0,"Objects":[{"StartTime":31693.0,"Position":88.0,"HyperDash":false},{"StartTime":31761.0,"Position":101.632492,"HyperDash":false},{"StartTime":31865.0,"Position":84.5410156,"HyperDash":false}]},{"StartTime":32037.0,"Objects":[{"StartTime":32037.0,"Position":256.0,"HyperDash":false},{"StartTime":32105.0,"Position":278.5814,"HyperDash":false},{"StartTime":32209.0,"Position":346.0,"HyperDash":false}]},{"StartTime":32296.0,"Objects":[{"StartTime":32296.0,"Position":246.0,"HyperDash":false}]},{"StartTime":32382.0,"Objects":[{"StartTime":32382.0,"Position":148.0,"HyperDash":false},{"StartTime":32450.0,"Position":101.4186,"HyperDash":false},{"StartTime":32554.0,"Position":58.0,"HyperDash":false}]},{"StartTime":32727.0,"Objects":[{"StartTime":32727.0,"Position":232.0,"HyperDash":false}]},{"StartTime":32813.0,"Objects":[{"StartTime":32813.0,"Position":180.0,"HyperDash":false}]},{"StartTime":32900.0,"Objects":[{"StartTime":32900.0,"Position":124.0,"HyperDash":true}]},{"StartTime":33072.0,"Objects":[{"StartTime":33072.0,"Position":376.0,"HyperDash":false},{"StartTime":33140.0,"Position":415.5814,"HyperDash":false},{"StartTime":33244.0,"Position":466.0,"HyperDash":false}]},{"StartTime":33417.0,"Objects":[{"StartTime":33417.0,"Position":300.0,"HyperDash":false},{"StartTime":33485.0,"Position":323.5814,"HyperDash":false},{"StartTime":33589.0,"Position":390.0,"HyperDash":false}]},{"StartTime":33762.0,"Objects":[{"StartTime":33762.0,"Position":220.0,"HyperDash":false},{"StartTime":33830.0,"Position":200.41861,"HyperDash":false},{"StartTime":33934.0,"Position":130.0,"HyperDash":true}]},{"StartTime":34106.0,"Objects":[{"StartTime":34106.0,"Position":416.0,"HyperDash":false},{"StartTime":34149.0,"Position":438.5,"HyperDash":false},{"StartTime":34192.0,"Position":416.0,"HyperDash":false},{"StartTime":34235.0,"Position":438.5,"HyperDash":false},{"StartTime":34278.0,"Position":416.0,"HyperDash":false},{"StartTime":34321.0,"Position":438.5,"HyperDash":false},{"StartTime":34364.0,"Position":416.0,"HyperDash":false},{"StartTime":34407.0,"Position":438.5,"HyperDash":true}]},{"StartTime":34451.0,"Objects":[{"StartTime":34451.0,"Position":265.0,"HyperDash":false},{"StartTime":34519.0,"Position":199.6279,"HyperDash":false},{"StartTime":34623.0,"Position":130.0,"HyperDash":false}]},{"StartTime":34796.0,"Objects":[{"StartTime":34796.0,"Position":300.0,"HyperDash":false},{"StartTime":34864.0,"Position":300.496857,"HyperDash":false},{"StartTime":34968.0,"Position":303.786133,"HyperDash":false}]},{"StartTime":35141.0,"Objects":[{"StartTime":35141.0,"Position":140.0,"HyperDash":true}]},{"StartTime":35313.0,"Objects":[{"StartTime":35313.0,"Position":376.0,"HyperDash":false}]},{"StartTime":35486.0,"Objects":[{"StartTime":35486.0,"Position":268.0,"HyperDash":false},{"StartTime":35554.0,"Position":253.518738,"HyperDash":false},{"StartTime":35658.0,"Position":264.253265,"HyperDash":true}]},{"StartTime":35831.0,"Objects":[{"StartTime":35831.0,"Position":496.0,"HyperDash":false},{"StartTime":35899.0,"Position":454.4186,"HyperDash":false},{"StartTime":36003.0,"Position":406.0,"HyperDash":false}]},{"StartTime":36175.0,"Objects":[{"StartTime":36175.0,"Position":236.0,"HyperDash":false},{"StartTime":36243.0,"Position":192.41861,"HyperDash":false},{"StartTime":36347.0,"Position":146.0,"HyperDash":true}]},{"StartTime":36520.0,"Objects":[{"StartTime":36520.0,"Position":400.0,"HyperDash":false}]},{"StartTime":36693.0,"Objects":[{"StartTime":36693.0,"Position":236.0,"HyperDash":true}]},{"StartTime":36865.0,"Objects":[{"StartTime":36865.0,"Position":476.0,"HyperDash":false}]},{"StartTime":36951.0,"Objects":[{"StartTime":36951.0,"Position":476.0,"HyperDash":false}]},{"StartTime":37037.0,"Objects":[{"StartTime":37037.0,"Position":434.0,"HyperDash":false}]},{"StartTime":37124.0,"Objects":[{"StartTime":37124.0,"Position":369.0,"HyperDash":true}]},{"StartTime":37210.0,"Objects":[{"StartTime":37210.0,"Position":196.0,"HyperDash":false},{"StartTime":37278.0,"Position":151.41861,"HyperDash":false},{"StartTime":37382.0,"Position":106.0,"HyperDash":false}]},{"StartTime":37555.0,"Objects":[{"StartTime":37555.0,"Position":272.0,"HyperDash":false},{"StartTime":37623.0,"Position":302.5814,"HyperDash":false},{"StartTime":37727.0,"Position":362.0,"HyperDash":false}]},{"StartTime":37900.0,"Objects":[{"StartTime":37900.0,"Position":196.0,"HyperDash":true}]},{"StartTime":38072.0,"Objects":[{"StartTime":38072.0,"Position":432.0,"HyperDash":false}]},{"StartTime":38244.0,"Objects":[{"StartTime":38244.0,"Position":324.0,"HyperDash":false}]},{"StartTime":38331.0,"Objects":[{"StartTime":38331.0,"Position":272.0,"HyperDash":false}]},{"StartTime":38417.0,"Objects":[{"StartTime":38417.0,"Position":224.0,"HyperDash":true}]},{"StartTime":38589.0,"Objects":[{"StartTime":38589.0,"Position":488.0,"HyperDash":false},{"StartTime":38657.0,"Position":483.690765,"HyperDash":false},{"StartTime":38761.0,"Position":489.747253,"HyperDash":false}]},{"StartTime":38934.0,"Objects":[{"StartTime":38934.0,"Position":324.0,"HyperDash":false},{"StartTime":39002.0,"Position":339.316925,"HyperDash":false},{"StartTime":39106.0,"Position":327.331055,"HyperDash":true}]},{"StartTime":39279.0,"Objects":[{"StartTime":39279.0,"Position":88.0,"HyperDash":false}]},{"StartTime":39451.0,"Objects":[{"StartTime":39451.0,"Position":256.0,"HyperDash":true}]},{"StartTime":39624.0,"Objects":[{"StartTime":39624.0,"Position":16.0,"HyperDash":true}]},{"StartTime":39969.0,"Objects":[{"StartTime":39969.0,"Position":428.0,"HyperDash":false},{"StartTime":40055.0,"Position":475.928162,"HyperDash":false},{"StartTime":40141.0,"Position":473.713928,"HyperDash":false},{"StartTime":40227.0,"Position":429.731,"HyperDash":false}]},{"StartTime":40313.0,"Objects":[{"StartTime":40313.0,"Position":328.0,"HyperDash":false},{"StartTime":40399.0,"Position":262.213257,"HyperDash":false},{"StartTime":40485.0,"Position":239.814941,"HyperDash":false},{"StartTime":40571.0,"Position":239.657425,"HyperDash":true}]},{"StartTime":40658.0,"Objects":[{"StartTime":40658.0,"Position":412.0,"HyperDash":false},{"StartTime":40744.0,"Position":464.25,"HyperDash":false},{"StartTime":40830.0,"Position":497.294128,"HyperDash":false},{"StartTime":40916.0,"Position":499.8483,"HyperDash":true}]},{"StartTime":41003.0,"Objects":[{"StartTime":41003.0,"Position":272.0,"HyperDash":false},{"StartTime":41089.0,"Position":253.0,"HyperDash":false},{"StartTime":41175.0,"Position":300.4706,"HyperDash":false},{"StartTime":41261.0,"Position":356.6626,"HyperDash":true}]},{"StartTime":41348.0,"Objects":[{"StartTime":41348.0,"Position":116.0,"HyperDash":false},{"StartTime":41434.0,"Position":72.26963,"HyperDash":false},{"StartTime":41520.0,"Position":61.0594635,"HyperDash":false},{"StartTime":41606.0,"Position":119.336884,"HyperDash":true}]},{"StartTime":41693.0,"Objects":[{"StartTime":41693.0,"Position":340.0,"HyperDash":false},{"StartTime":41779.0,"Position":288.5,"HyperDash":false},{"StartTime":41865.0,"Position":204.999985,"HyperDash":false},{"StartTime":41951.0,"Position":137.5,"HyperDash":true}]},{"StartTime":42037.0,"Objects":[{"StartTime":42037.0,"Position":312.0,"HyperDash":false}]},{"StartTime":42210.0,"Objects":[{"StartTime":42210.0,"Position":164.0,"HyperDash":false}]},{"StartTime":42382.0,"Objects":[{"StartTime":42382.0,"Position":324.0,"HyperDash":false}]},{"StartTime":42555.0,"Objects":[{"StartTime":42555.0,"Position":152.0,"HyperDash":true}]},{"StartTime":42727.0,"Objects":[{"StartTime":42727.0,"Position":404.0,"HyperDash":false}]},{"StartTime":42813.0,"Objects":[{"StartTime":42813.0,"Position":460.0,"HyperDash":false}]},{"StartTime":42900.0,"Objects":[{"StartTime":42900.0,"Position":460.0,"HyperDash":false}]},{"StartTime":42986.0,"Objects":[{"StartTime":42986.0,"Position":404.0,"HyperDash":true}]},{"StartTime":43072.0,"Objects":[{"StartTime":43072.0,"Position":208.0,"HyperDash":false},{"StartTime":43158.0,"Position":204.54866,"HyperDash":false}]},{"StartTime":43244.0,"Objects":[{"StartTime":43244.0,"Position":280.0,"HyperDash":false},{"StartTime":43330.0,"Position":283.451355,"HyperDash":true}]},{"StartTime":43417.0,"Objects":[{"StartTime":43417.0,"Position":104.0,"HyperDash":false},{"StartTime":43503.0,"Position":59.0,"HyperDash":true}]},{"StartTime":43589.0,"Objects":[{"StartTime":43589.0,"Position":240.0,"HyperDash":false},{"StartTime":43675.0,"Position":285.0,"HyperDash":true}]},{"StartTime":43762.0,"Objects":[{"StartTime":43762.0,"Position":80.0,"HyperDash":false},{"StartTime":43848.0,"Position":125.0,"HyperDash":true}]},{"StartTime":43934.0,"Objects":[{"StartTime":43934.0,"Position":372.0,"HyperDash":false},{"StartTime":44020.0,"Position":327.0,"HyperDash":true}]},{"StartTime":44106.0,"Objects":[{"StartTime":44106.0,"Position":124.0,"HyperDash":true}]},{"StartTime":44279.0,"Objects":[{"StartTime":44279.0,"Position":368.0,"HyperDash":true}]},{"StartTime":44451.0,"Objects":[{"StartTime":44451.0,"Position":116.0,"HyperDash":false},{"StartTime":44537.0,"Position":71.0,"HyperDash":false}]},{"StartTime":44624.0,"Objects":[{"StartTime":44624.0,"Position":172.0,"HyperDash":false},{"StartTime":44710.0,"Position":127.0,"HyperDash":true}]},{"StartTime":44796.0,"Objects":[{"StartTime":44796.0,"Position":300.0,"HyperDash":false},{"StartTime":44882.0,"Position":356.5,"HyperDash":false},{"StartTime":44968.0,"Position":435.0,"HyperDash":false},{"StartTime":45011.0,"Position":468.75,"HyperDash":true}]},{"StartTime":45141.0,"Objects":[{"StartTime":45141.0,"Position":260.0,"HyperDash":false},{"StartTime":45227.0,"Position":345.5,"HyperDash":false},{"StartTime":45313.0,"Position":395.0,"HyperDash":false},{"StartTime":45356.0,"Position":428.75,"HyperDash":true}]},{"StartTime":45486.0,"Objects":[{"StartTime":45486.0,"Position":176.0,"HyperDash":false}]},{"StartTime":45507.0,"Objects":[{"StartTime":45507.0,"Position":158.0,"HyperDash":false}]},{"StartTime":45529.0,"Objects":[{"StartTime":45529.0,"Position":143.0,"HyperDash":false}]},{"StartTime":45550.0,"Objects":[{"StartTime":45550.0,"Position":129.0,"HyperDash":false}]},{"StartTime":45572.0,"Objects":[{"StartTime":45572.0,"Position":119.0,"HyperDash":false}]},{"StartTime":45594.0,"Objects":[{"StartTime":45594.0,"Position":111.0,"HyperDash":false}]},{"StartTime":45615.0,"Objects":[{"StartTime":45615.0,"Position":108.0,"HyperDash":false}]},{"StartTime":45637.0,"Objects":[{"StartTime":45637.0,"Position":108.0,"HyperDash":false}]},{"StartTime":45658.0,"Objects":[{"StartTime":45658.0,"Position":112.0,"HyperDash":false}]},{"StartTime":45680.0,"Objects":[{"StartTime":45680.0,"Position":120.0,"HyperDash":false}]},{"StartTime":45701.0,"Objects":[{"StartTime":45701.0,"Position":131.0,"HyperDash":false}]},{"StartTime":45723.0,"Objects":[{"StartTime":45723.0,"Position":145.0,"HyperDash":false}]},{"StartTime":45744.0,"Objects":[{"StartTime":45744.0,"Position":161.0,"HyperDash":false}]},{"StartTime":45766.0,"Objects":[{"StartTime":45766.0,"Position":178.0,"HyperDash":false}]},{"StartTime":45787.0,"Objects":[{"StartTime":45787.0,"Position":196.0,"HyperDash":false}]},{"StartTime":45809.0,"Objects":[{"StartTime":45809.0,"Position":214.0,"HyperDash":false}]},{"StartTime":45831.0,"Objects":[{"StartTime":45831.0,"Position":240.0,"HyperDash":false}]},{"StartTime":45852.0,"Objects":[{"StartTime":45852.0,"Position":257.0,"HyperDash":false}]},{"StartTime":45874.0,"Objects":[{"StartTime":45874.0,"Position":275.0,"HyperDash":false}]},{"StartTime":45895.0,"Objects":[{"StartTime":45895.0,"Position":291.0,"HyperDash":false}]},{"StartTime":45917.0,"Objects":[{"StartTime":45917.0,"Position":304.0,"HyperDash":false}]},{"StartTime":45938.0,"Objects":[{"StartTime":45938.0,"Position":315.0,"HyperDash":false}]},{"StartTime":45960.0,"Objects":[{"StartTime":45960.0,"Position":323.0,"HyperDash":false}]},{"StartTime":45981.0,"Objects":[{"StartTime":45981.0,"Position":327.0,"HyperDash":false}]},{"StartTime":46003.0,"Objects":[{"StartTime":46003.0,"Position":327.0,"HyperDash":false}]},{"StartTime":46025.0,"Objects":[{"StartTime":46025.0,"Position":324.0,"HyperDash":false}]},{"StartTime":46046.0,"Objects":[{"StartTime":46046.0,"Position":317.0,"HyperDash":false}]},{"StartTime":46068.0,"Objects":[{"StartTime":46068.0,"Position":306.0,"HyperDash":false}]},{"StartTime":46089.0,"Objects":[{"StartTime":46089.0,"Position":293.0,"HyperDash":false}]},{"StartTime":46111.0,"Objects":[{"StartTime":46111.0,"Position":277.0,"HyperDash":false}]},{"StartTime":46132.0,"Objects":[{"StartTime":46132.0,"Position":260.0,"HyperDash":true}]},{"StartTime":46175.0,"Objects":[{"StartTime":46175.0,"Position":76.0,"HyperDash":false},{"StartTime":46243.0,"Position":41.627903,"HyperDash":false},{"StartTime":46347.0,"Position":75.00001,"HyperDash":false}]},{"StartTime":46434.0,"Objects":[{"StartTime":46434.0,"Position":120.0,"HyperDash":true}]},{"StartTime":46520.0,"Objects":[{"StartTime":46520.0,"Position":280.0,"HyperDash":false},{"StartTime":46588.0,"Position":298.5814,"HyperDash":false},{"StartTime":46692.0,"Position":370.0,"HyperDash":false}]},{"StartTime":46779.0,"Objects":[{"StartTime":46779.0,"Position":324.0,"HyperDash":true}]},{"StartTime":46865.0,"Objects":[{"StartTime":46865.0,"Position":152.0,"HyperDash":false},{"StartTime":46951.0,"Position":107.0,"HyperDash":false}]},{"StartTime":47037.0,"Objects":[{"StartTime":47037.0,"Position":172.0,"HyperDash":false},{"StartTime":47123.0,"Position":127.0,"HyperDash":true}]},{"StartTime":47210.0,"Objects":[{"StartTime":47210.0,"Position":336.0,"HyperDash":false}]},{"StartTime":47253.0,"Objects":[{"StartTime":47253.0,"Position":363.0,"HyperDash":false}]},{"StartTime":47296.0,"Objects":[{"StartTime":47296.0,"Position":384.0,"HyperDash":false}]},{"StartTime":47339.0,"Objects":[{"StartTime":47339.0,"Position":393.0,"HyperDash":false}]},{"StartTime":47382.0,"Objects":[{"StartTime":47382.0,"Position":389.0,"HyperDash":false}]},{"StartTime":47425.0,"Objects":[{"StartTime":47425.0,"Position":372.0,"HyperDash":false}]},{"StartTime":47469.0,"Objects":[{"StartTime":47469.0,"Position":347.0,"HyperDash":true}]},{"StartTime":47555.0,"Objects":[{"StartTime":47555.0,"Position":168.0,"HyperDash":false},{"StartTime":47623.0,"Position":126.41861,"HyperDash":false},{"StartTime":47727.0,"Position":78.0,"HyperDash":false}]},{"StartTime":47900.0,"Objects":[{"StartTime":47900.0,"Position":244.0,"HyperDash":false},{"StartTime":47968.0,"Position":214.41861,"HyperDash":false},{"StartTime":48072.0,"Position":154.0,"HyperDash":true}]},{"StartTime":48244.0,"Objects":[{"StartTime":48244.0,"Position":400.0,"HyperDash":false},{"StartTime":48330.0,"Position":403.451355,"HyperDash":false}]},{"StartTime":48503.0,"Objects":[{"StartTime":48503.0,"Position":312.0,"HyperDash":true}]},{"StartTime":48589.0,"Objects":[{"StartTime":48589.0,"Position":140.0,"HyperDash":false}]},{"StartTime":48762.0,"Objects":[{"StartTime":48762.0,"Position":248.0,"HyperDash":true}]},{"StartTime":48934.0,"Objects":[{"StartTime":48934.0,"Position":16.0,"HyperDash":false},{"StartTime":49020.0,"Position":61.0,"HyperDash":false}]},{"StartTime":49193.0,"Objects":[{"StartTime":49193.0,"Position":160.0,"HyperDash":true}]},{"StartTime":49279.0,"Objects":[{"StartTime":49279.0,"Position":16.0,"HyperDash":false},{"StartTime":49365.0,"Position":20.0741081,"HyperDash":false}]},{"StartTime":49451.0,"Objects":[{"StartTime":49451.0,"Position":76.0,"HyperDash":false},{"StartTime":49537.0,"Position":121.0,"HyperDash":true}]},{"StartTime":49624.0,"Objects":[{"StartTime":49624.0,"Position":304.0,"HyperDash":false}]},{"StartTime":49667.0,"Objects":[{"StartTime":49667.0,"Position":317.0,"HyperDash":false}]},{"StartTime":49710.0,"Objects":[{"StartTime":49710.0,"Position":326.0,"HyperDash":false}]},{"StartTime":49753.0,"Objects":[{"StartTime":49753.0,"Position":328.0,"HyperDash":false}]},{"StartTime":49796.0,"Objects":[{"StartTime":49796.0,"Position":325.0,"HyperDash":false}]},{"StartTime":49839.0,"Objects":[{"StartTime":49839.0,"Position":316.0,"HyperDash":false}]},{"StartTime":49882.0,"Objects":[{"StartTime":49882.0,"Position":301.0,"HyperDash":true}]},{"StartTime":49969.0,"Objects":[{"StartTime":49969.0,"Position":120.0,"HyperDash":false}]},{"StartTime":50055.0,"Objects":[{"StartTime":50055.0,"Position":52.0,"HyperDash":false}]},{"StartTime":50141.0,"Objects":[{"StartTime":50141.0,"Position":120.0,"HyperDash":false}]},{"StartTime":50313.0,"Objects":[{"StartTime":50313.0,"Position":288.0,"HyperDash":false}]},{"StartTime":50400.0,"Objects":[{"StartTime":50400.0,"Position":332.0,"HyperDash":false}]},{"StartTime":50486.0,"Objects":[{"StartTime":50486.0,"Position":328.0,"HyperDash":false}]},{"StartTime":50572.0,"Objects":[{"StartTime":50572.0,"Position":280.0,"HyperDash":true}]},{"StartTime":50658.0,"Objects":[{"StartTime":50658.0,"Position":104.0,"HyperDash":false},{"StartTime":50744.0,"Position":59.0,"HyperDash":false}]},{"StartTime":50831.0,"Objects":[{"StartTime":50831.0,"Position":104.0,"HyperDash":false},{"StartTime":50917.0,"Position":149.0,"HyperDash":true}]},{"StartTime":51003.0,"Objects":[{"StartTime":51003.0,"Position":328.0,"HyperDash":false},{"StartTime":51071.0,"Position":375.5814,"HyperDash":false},{"StartTime":51175.0,"Position":334.0,"HyperDash":false}]},{"StartTime":51262.0,"Objects":[{"StartTime":51262.0,"Position":280.0,"HyperDash":true}]},{"StartTime":51348.0,"Objects":[{"StartTime":51348.0,"Position":128.0,"HyperDash":true},{"StartTime":51405.0,"Position":204.775,"HyperDash":false},{"StartTime":51498.0,"Position":364.25,"HyperDash":false}]},{"StartTime":51520.0,"Objects":[{"StartTime":51520.0,"Position":364.0,"HyperDash":false},{"StartTime":51588.0,"Position":296.6279,"HyperDash":false},{"StartTime":51692.0,"Position":229.0,"HyperDash":true}]},{"StartTime":51736.0,"Objects":[{"StartTime":51736.0,"Position":368.0,"HyperDash":false},{"StartTime":51865.0,"Position":435.5,"HyperDash":false}]},{"StartTime":51951.0,"Objects":[{"StartTime":51951.0,"Position":380.0,"HyperDash":true}]},{"StartTime":52037.0,"Objects":[{"StartTime":52037.0,"Position":204.0,"HyperDash":false},{"StartTime":52123.0,"Position":136.5,"HyperDash":false}]},{"StartTime":52210.0,"Objects":[{"StartTime":52210.0,"Position":223.0,"HyperDash":false},{"StartTime":52296.0,"Position":155.5,"HyperDash":true}]},{"StartTime":52382.0,"Objects":[{"StartTime":52382.0,"Position":388.0,"HyperDash":false},{"StartTime":52468.0,"Position":455.5,"HyperDash":false}]},{"StartTime":52555.0,"Objects":[{"StartTime":52555.0,"Position":368.0,"HyperDash":false},{"StartTime":52641.0,"Position":435.5,"HyperDash":true}]},{"StartTime":52727.0,"Objects":[{"StartTime":52727.0,"Position":224.0,"HyperDash":false}]},{"StartTime":52770.0,"Objects":[{"StartTime":52770.0,"Position":194.0,"HyperDash":false}]},{"StartTime":52813.0,"Objects":[{"StartTime":52813.0,"Position":169.0,"HyperDash":false}]},{"StartTime":52856.0,"Objects":[{"StartTime":52856.0,"Position":149.0,"HyperDash":false}]},{"StartTime":52900.0,"Objects":[{"StartTime":52900.0,"Position":137.0,"HyperDash":false}]},{"StartTime":52943.0,"Objects":[{"StartTime":52943.0,"Position":134.0,"HyperDash":false}]},{"StartTime":52986.0,"Objects":[{"StartTime":52986.0,"Position":141.0,"HyperDash":true}]},{"StartTime":53072.0,"Objects":[{"StartTime":53072.0,"Position":368.0,"HyperDash":true},{"StartTime":53140.0,"Position":260.0465,"HyperDash":false},{"StartTime":53244.0,"Position":143.0,"HyperDash":true}]},{"StartTime":53417.0,"Objects":[{"StartTime":53417.0,"Position":444.0,"HyperDash":true},{"StartTime":53485.0,"Position":358.0465,"HyperDash":false},{"StartTime":53589.0,"Position":219.0,"HyperDash":true}]},{"StartTime":53762.0,"Objects":[{"StartTime":53762.0,"Position":488.0,"HyperDash":false},{"StartTime":53830.0,"Position":475.545563,"HyperDash":false},{"StartTime":53934.0,"Position":491.909363,"HyperDash":false}]},{"StartTime":54106.0,"Objects":[{"StartTime":54106.0,"Position":336.0,"HyperDash":false}]},{"StartTime":54193.0,"Objects":[{"StartTime":54193.0,"Position":280.0,"HyperDash":false}]},{"StartTime":54279.0,"Objects":[{"StartTime":54279.0,"Position":228.0,"HyperDash":false}]},{"StartTime":54451.0,"Objects":[{"StartTime":54451.0,"Position":392.0,"HyperDash":false},{"StartTime":54494.0,"Position":394.4847,"HyperDash":true}]},{"StartTime":54624.0,"Objects":[{"StartTime":54624.0,"Position":188.0,"HyperDash":false},{"StartTime":54667.0,"Position":185.089874,"HyperDash":true}]},{"StartTime":54796.0,"Objects":[{"StartTime":54796.0,"Position":408.0,"HyperDash":false},{"StartTime":54882.0,"Position":363.0,"HyperDash":true}]},{"StartTime":54969.0,"Objects":[{"StartTime":54969.0,"Position":136.0,"HyperDash":false},{"StartTime":55055.0,"Position":181.0,"HyperDash":true}]},{"StartTime":55141.0,"Objects":[{"StartTime":55141.0,"Position":384.0,"HyperDash":false},{"StartTime":55198.0,"Position":345.1965,"HyperDash":false},{"StartTime":55255.0,"Position":294.0,"HyperDash":false},{"StartTime":55370.0,"Position":384.0,"HyperDash":true}]},{"StartTime":55486.0,"Objects":[{"StartTime":55486.0,"Position":172.0,"HyperDash":false}]},{"StartTime":55601.0,"Objects":[{"StartTime":55601.0,"Position":280.0,"HyperDash":false}]},{"StartTime":55716.0,"Objects":[{"StartTime":55716.0,"Position":388.0,"HyperDash":true}]},{"StartTime":55831.0,"Objects":[{"StartTime":55831.0,"Position":164.0,"HyperDash":false},{"StartTime":55888.0,"Position":147.131012,"HyperDash":false},{"StartTime":55945.0,"Position":104.0,"HyperDash":false},{"StartTime":56060.0,"Position":164.0,"HyperDash":true}]},{"StartTime":56175.0,"Objects":[{"StartTime":56175.0,"Position":340.0,"HyperDash":false}]},{"StartTime":56290.0,"Objects":[{"StartTime":56290.0,"Position":412.0,"HyperDash":false}]},{"StartTime":56405.0,"Objects":[{"StartTime":56405.0,"Position":412.0,"HyperDash":true}]},{"StartTime":56520.0,"Objects":[{"StartTime":56520.0,"Position":212.0,"HyperDash":false},{"StartTime":56606.0,"Position":148.1791,"HyperDash":false},{"StartTime":56692.0,"Position":144.4465,"HyperDash":false},{"StartTime":56778.0,"Position":188.107758,"HyperDash":false},{"StartTime":56864.0,"Position":241.294647,"HyperDash":false},{"StartTime":56950.0,"Position":307.139038,"HyperDash":false},{"StartTime":57037.0,"Position":323.301819,"HyperDash":false},{"StartTime":57123.0,"Position":297.7406,"HyperDash":true}]},{"StartTime":57210.0,"Objects":[{"StartTime":57210.0,"Position":128.0,"HyperDash":false}]},{"StartTime":57231.0,"Objects":[{"StartTime":57231.0,"Position":112.0,"HyperDash":false}]},{"StartTime":57253.0,"Objects":[{"StartTime":57253.0,"Position":97.0,"HyperDash":false}]},{"StartTime":57275.0,"Objects":[{"StartTime":57275.0,"Position":83.0,"HyperDash":false}]},{"StartTime":57296.0,"Objects":[{"StartTime":57296.0,"Position":70.0,"HyperDash":false}]},{"StartTime":57318.0,"Objects":[{"StartTime":57318.0,"Position":57.0,"HyperDash":false}]},{"StartTime":57339.0,"Objects":[{"StartTime":57339.0,"Position":46.0,"HyperDash":false}]},{"StartTime":57361.0,"Objects":[{"StartTime":57361.0,"Position":35.0,"HyperDash":false}]},{"StartTime":57382.0,"Objects":[{"StartTime":57382.0,"Position":26.0,"HyperDash":false}]},{"StartTime":57404.0,"Objects":[{"StartTime":57404.0,"Position":19.0,"HyperDash":false}]},{"StartTime":57425.0,"Objects":[{"StartTime":57425.0,"Position":13.0,"HyperDash":false}]},{"StartTime":57447.0,"Objects":[{"StartTime":57447.0,"Position":8.0,"HyperDash":false}]},{"StartTime":57469.0,"Objects":[{"StartTime":57469.0,"Position":5.0,"HyperDash":false}]},{"StartTime":57490.0,"Objects":[{"StartTime":57490.0,"Position":3.0,"HyperDash":false}]},{"StartTime":57512.0,"Objects":[{"StartTime":57512.0,"Position":3.0,"HyperDash":false}]},{"StartTime":57533.0,"Objects":[{"StartTime":57533.0,"Position":5.0,"HyperDash":false}]},{"StartTime":57555.0,"Objects":[{"StartTime":57555.0,"Position":8.0,"HyperDash":false}]},{"StartTime":57576.0,"Objects":[{"StartTime":57576.0,"Position":12.0,"HyperDash":false}]},{"StartTime":57598.0,"Objects":[{"StartTime":57598.0,"Position":18.0,"HyperDash":false}]},{"StartTime":57619.0,"Objects":[{"StartTime":57619.0,"Position":26.0,"HyperDash":false}]},{"StartTime":57641.0,"Objects":[{"StartTime":57641.0,"Position":35.0,"HyperDash":false}]},{"StartTime":57662.0,"Objects":[{"StartTime":57662.0,"Position":45.0,"HyperDash":false}]},{"StartTime":57684.0,"Objects":[{"StartTime":57684.0,"Position":56.0,"HyperDash":false}]},{"StartTime":57706.0,"Objects":[{"StartTime":57706.0,"Position":69.0,"HyperDash":false}]},{"StartTime":57727.0,"Objects":[{"StartTime":57727.0,"Position":82.0,"HyperDash":false}]},{"StartTime":57749.0,"Objects":[{"StartTime":57749.0,"Position":96.0,"HyperDash":false}]},{"StartTime":57770.0,"Objects":[{"StartTime":57770.0,"Position":111.0,"HyperDash":false}]},{"StartTime":57792.0,"Objects":[{"StartTime":57792.0,"Position":126.0,"HyperDash":false}]},{"StartTime":57813.0,"Objects":[{"StartTime":57813.0,"Position":142.0,"HyperDash":false}]},{"StartTime":57835.0,"Objects":[{"StartTime":57835.0,"Position":158.0,"HyperDash":false}]},{"StartTime":57856.0,"Objects":[{"StartTime":57856.0,"Position":174.0,"HyperDash":true}]},{"StartTime":57900.0,"Objects":[{"StartTime":57900.0,"Position":312.0,"HyperDash":false},{"StartTime":57968.0,"Position":371.3721,"HyperDash":false},{"StartTime":58072.0,"Position":447.0,"HyperDash":false}]},{"StartTime":58158.0,"Objects":[{"StartTime":58158.0,"Position":392.0,"HyperDash":true}]},{"StartTime":58244.0,"Objects":[{"StartTime":58244.0,"Position":216.0,"HyperDash":false},{"StartTime":58330.0,"Position":159.75,"HyperDash":false}]},{"StartTime":58417.0,"Objects":[{"StartTime":58417.0,"Position":232.0,"HyperDash":false},{"StartTime":58503.0,"Position":175.75,"HyperDash":true}]},{"StartTime":58589.0,"Objects":[{"StartTime":58589.0,"Position":20.0,"HyperDash":false},{"StartTime":58657.0,"Position":49.581398,"HyperDash":false},{"StartTime":58761.0,"Position":110.0,"HyperDash":false}]},{"StartTime":58934.0,"Objects":[{"StartTime":58934.0,"Position":276.0,"HyperDash":false},{"StartTime":59002.0,"Position":233.41861,"HyperDash":false},{"StartTime":59106.0,"Position":186.0,"HyperDash":true}]},{"StartTime":59279.0,"Objects":[{"StartTime":59279.0,"Position":440.0,"HyperDash":false}]},{"StartTime":59322.0,"Objects":[{"StartTime":59322.0,"Position":466.0,"HyperDash":false}]},{"StartTime":59365.0,"Objects":[{"StartTime":59365.0,"Position":484.0,"HyperDash":false}]},{"StartTime":59408.0,"Objects":[{"StartTime":59408.0,"Position":491.0,"HyperDash":false}]},{"StartTime":59451.0,"Objects":[{"StartTime":59451.0,"Position":484.0,"HyperDash":false}]},{"StartTime":59537.0,"Objects":[{"StartTime":59537.0,"Position":428.0,"HyperDash":true}]},{"StartTime":59624.0,"Objects":[{"StartTime":59624.0,"Position":260.0,"HyperDash":false},{"StartTime":59710.0,"Position":215.0,"HyperDash":false},{"StartTime":59796.0,"Position":260.0,"HyperDash":true}]},{"StartTime":59969.0,"Objects":[{"StartTime":59969.0,"Position":494.0,"HyperDash":false},{"StartTime":60055.0,"Position":497.451355,"HyperDash":false}]},{"StartTime":60227.0,"Objects":[{"StartTime":60227.0,"Position":392.0,"HyperDash":true}]},{"StartTime":60313.0,"Objects":[{"StartTime":60313.0,"Position":212.0,"HyperDash":false}]},{"StartTime":60486.0,"Objects":[{"StartTime":60486.0,"Position":356.0,"HyperDash":true}]},{"StartTime":60658.0,"Objects":[{"StartTime":60658.0,"Position":104.0,"HyperDash":false},{"StartTime":60744.0,"Position":100.262955,"HyperDash":false}]},{"StartTime":60917.0,"Objects":[{"StartTime":60917.0,"Position":204.0,"HyperDash":true}]},{"StartTime":61003.0,"Objects":[{"StartTime":61003.0,"Position":384.0,"HyperDash":false},{"StartTime":61089.0,"Position":339.0,"HyperDash":true}]},{"StartTime":61175.0,"Objects":[{"StartTime":61175.0,"Position":159.0,"HyperDash":false},{"StartTime":61261.0,"Position":226.5,"HyperDash":true}]},{"StartTime":61348.0,"Objects":[{"StartTime":61348.0,"Position":72.0,"HyperDash":false}]},{"StartTime":61434.0,"Objects":[{"StartTime":61434.0,"Position":9.0,"HyperDash":false}]},{"StartTime":61520.0,"Objects":[{"StartTime":61520.0,"Position":9.0,"HyperDash":false}]},{"StartTime":61606.0,"Objects":[{"StartTime":61606.0,"Position":70.0,"HyperDash":true}]},{"StartTime":61693.0,"Objects":[{"StartTime":61693.0,"Position":250.0,"HyperDash":false},{"StartTime":61761.0,"Position":304.5814,"HyperDash":false},{"StartTime":61865.0,"Position":340.0,"HyperDash":false}]},{"StartTime":62037.0,"Objects":[{"StartTime":62037.0,"Position":184.0,"HyperDash":false},{"StartTime":62105.0,"Position":143.41861,"HyperDash":false},{"StartTime":62209.0,"Position":178.0,"HyperDash":false}]},{"StartTime":62296.0,"Objects":[{"StartTime":62296.0,"Position":232.0,"HyperDash":true}]},{"StartTime":62382.0,"Objects":[{"StartTime":62382.0,"Position":384.0,"HyperDash":true},{"StartTime":62439.0,"Position":294.225,"HyperDash":false},{"StartTime":62532.0,"Position":147.749985,"HyperDash":false}]},{"StartTime":62555.0,"Objects":[{"StartTime":62555.0,"Position":148.0,"HyperDash":false},{"StartTime":62623.0,"Position":216.3721,"HyperDash":false},{"StartTime":62727.0,"Position":283.0,"HyperDash":true}]},{"StartTime":62770.0,"Objects":[{"StartTime":62770.0,"Position":144.0,"HyperDash":false},{"StartTime":62899.0,"Position":76.5,"HyperDash":false}]},{"StartTime":62986.0,"Objects":[{"StartTime":62986.0,"Position":132.0,"HyperDash":true}]},{"StartTime":63072.0,"Objects":[{"StartTime":63072.0,"Position":300.0,"HyperDash":false},{"StartTime":63158.0,"Position":345.0,"HyperDash":true}]},{"StartTime":63244.0,"Objects":[{"StartTime":63244.0,"Position":184.0,"HyperDash":false},{"StartTime":63330.0,"Position":229.0,"HyperDash":true}]},{"StartTime":63417.0,"Objects":[{"StartTime":63417.0,"Position":64.0,"HyperDash":false},{"StartTime":63503.0,"Position":19.0,"HyperDash":true}]},{"StartTime":63589.0,"Objects":[{"StartTime":63589.0,"Position":184.0,"HyperDash":false},{"StartTime":63675.0,"Position":139.0,"HyperDash":true}]},{"StartTime":63762.0,"Objects":[{"StartTime":63762.0,"Position":345.0,"HyperDash":false}]},{"StartTime":63805.0,"Objects":[{"StartTime":63805.0,"Position":375.0,"HyperDash":false}]},{"StartTime":63848.0,"Objects":[{"StartTime":63848.0,"Position":400.0,"HyperDash":false}]},{"StartTime":63891.0,"Objects":[{"StartTime":63891.0,"Position":420.0,"HyperDash":false}]},{"StartTime":63934.0,"Objects":[{"StartTime":63934.0,"Position":432.0,"HyperDash":false}]},{"StartTime":63977.0,"Objects":[{"StartTime":63977.0,"Position":435.0,"HyperDash":false}]},{"StartTime":64020.0,"Objects":[{"StartTime":64020.0,"Position":428.0,"HyperDash":true}]},{"StartTime":64106.0,"Objects":[{"StartTime":64106.0,"Position":224.0,"HyperDash":true},{"StartTime":64174.0,"Position":296.9535,"HyperDash":false},{"StartTime":64278.0,"Position":449.0,"HyperDash":true}]},{"StartTime":64451.0,"Objects":[{"StartTime":64451.0,"Position":148.0,"HyperDash":true},{"StartTime":64519.0,"Position":253.953491,"HyperDash":false},{"StartTime":64623.0,"Position":373.0,"HyperDash":true}]},{"StartTime":64796.0,"Objects":[{"StartTime":64796.0,"Position":120.0,"HyperDash":true}]},{"StartTime":64911.0,"Objects":[{"StartTime":64911.0,"Position":324.0,"HyperDash":true}]},{"StartTime":65026.0,"Objects":[{"StartTime":65026.0,"Position":120.0,"HyperDash":true}]},{"StartTime":65141.0,"Objects":[{"StartTime":65141.0,"Position":336.0,"HyperDash":false}]},{"StartTime":65256.0,"Objects":[{"StartTime":65256.0,"Position":222.0,"HyperDash":false}]},{"StartTime":65371.0,"Objects":[{"StartTime":65371.0,"Position":108.0,"HyperDash":true}]},{"StartTime":65486.0,"Objects":[{"StartTime":65486.0,"Position":336.0,"HyperDash":false}]},{"StartTime":65601.0,"Objects":[{"StartTime":65601.0,"Position":444.0,"HyperDash":false}]},{"StartTime":65716.0,"Objects":[{"StartTime":65716.0,"Position":336.0,"HyperDash":true}]},{"StartTime":65831.0,"Objects":[{"StartTime":65831.0,"Position":144.0,"HyperDash":false}]},{"StartTime":65946.0,"Objects":[{"StartTime":65946.0,"Position":252.0,"HyperDash":false}]},{"StartTime":66060.0,"Objects":[{"StartTime":66060.0,"Position":144.0,"HyperDash":true}]},{"StartTime":66175.0,"Objects":[{"StartTime":66175.0,"Position":360.0,"HyperDash":false},{"StartTime":66243.0,"Position":398.5814,"HyperDash":false},{"StartTime":66347.0,"Position":450.0,"HyperDash":false}]},{"StartTime":66434.0,"Objects":[{"StartTime":66434.0,"Position":396.0,"HyperDash":true}]},{"StartTime":66520.0,"Objects":[{"StartTime":66520.0,"Position":224.0,"HyperDash":false}]},{"StartTime":66693.0,"Objects":[{"StartTime":66693.0,"Position":388.0,"HyperDash":true}]},{"StartTime":66865.0,"Objects":[{"StartTime":66865.0,"Position":124.0,"HyperDash":false},{"StartTime":66951.0,"Position":120.54866,"HyperDash":false}]},{"StartTime":67037.0,"Objects":[{"StartTime":67037.0,"Position":204.0,"HyperDash":false},{"StartTime":67123.0,"Position":200.54866,"HyperDash":true}]},{"StartTime":67210.0,"Objects":[{"StartTime":67210.0,"Position":368.0,"HyperDash":false}]},{"StartTime":67382.0,"Objects":[{"StartTime":67382.0,"Position":204.0,"HyperDash":true}]},{"StartTime":67555.0,"Objects":[{"StartTime":67555.0,"Position":476.0,"HyperDash":false}]},{"StartTime":67900.0,"Objects":[{"StartTime":67900.0,"Position":188.0,"HyperDash":false}]},{"StartTime":68244.0,"Objects":[{"StartTime":68244.0,"Position":488.0,"HyperDash":false}]},{"StartTime":68417.0,"Objects":[{"StartTime":68417.0,"Position":356.0,"HyperDash":false},{"StartTime":68503.0,"Position":423.5,"HyperDash":true}]},{"StartTime":68589.0,"Objects":[{"StartTime":68589.0,"Position":172.0,"HyperDash":false},{"StartTime":68657.0,"Position":166.454437,"HyperDash":false},{"StartTime":68761.0,"Position":168.090652,"HyperDash":true}]},{"StartTime":68934.0,"Objects":[{"StartTime":68934.0,"Position":484.0,"HyperDash":false}]},{"StartTime":69279.0,"Objects":[{"StartTime":69279.0,"Position":368.0,"HyperDash":false},{"StartTime":69343.0,"Position":52.0,"HyperDash":false},{"StartTime":69408.0,"Position":327.0,"HyperDash":false},{"StartTime":69472.0,"Position":226.0,"HyperDash":false},{"StartTime":69537.0,"Position":110.0,"HyperDash":false},{"StartTime":69602.0,"Position":3.0,"HyperDash":false},{"StartTime":69666.0,"Position":26.0,"HyperDash":false},{"StartTime":69731.0,"Position":173.0,"HyperDash":false},{"StartTime":69796.0,"Position":18.0,"HyperDash":false},{"StartTime":69860.0,"Position":310.0,"HyperDash":false},{"StartTime":69925.0,"Position":394.0,"HyperDash":false},{"StartTime":69990.0,"Position":406.0,"HyperDash":false},{"StartTime":70054.0,"Position":262.0,"HyperDash":false},{"StartTime":70119.0,"Position":278.0,"HyperDash":false},{"StartTime":70184.0,"Position":171.0,"HyperDash":false},{"StartTime":70248.0,"Position":22.0,"HyperDash":false},{"StartTime":70313.0,"Position":187.0,"HyperDash":false},{"StartTime":70378.0,"Position":124.0,"HyperDash":false},{"StartTime":70442.0,"Position":454.0,"HyperDash":false},{"StartTime":70507.0,"Position":16.0,"HyperDash":false},{"StartTime":70572.0,"Position":61.0,"HyperDash":false},{"StartTime":70636.0,"Position":161.0,"HyperDash":false},{"StartTime":70701.0,"Position":243.0,"HyperDash":false},{"StartTime":70766.0,"Position":375.0,"HyperDash":false},{"StartTime":70830.0,"Position":247.0,"HyperDash":false},{"StartTime":70895.0,"Position":162.0,"HyperDash":false},{"StartTime":70960.0,"Position":383.0,"HyperDash":false},{"StartTime":71024.0,"Position":127.0,"HyperDash":false},{"StartTime":71089.0,"Position":161.0,"HyperDash":false},{"StartTime":71154.0,"Position":332.0,"HyperDash":false},{"StartTime":71218.0,"Position":356.0,"HyperDash":false},{"StartTime":71283.0,"Position":362.0,"HyperDash":false},{"StartTime":71348.0,"Position":347.0,"HyperDash":false}]},{"StartTime":71693.0,"Objects":[{"StartTime":71693.0,"Position":232.0,"HyperDash":false},{"StartTime":71714.0,"Position":229.7937,"HyperDash":false},{"StartTime":71736.0,"Position":232.0,"HyperDash":false},{"StartTime":71757.0,"Position":229.7937,"HyperDash":false},{"StartTime":71779.0,"Position":232.0,"HyperDash":false},{"StartTime":71800.0,"Position":229.7937,"HyperDash":false},{"StartTime":71822.0,"Position":232.0,"HyperDash":false},{"StartTime":71843.0,"Position":229.7937,"HyperDash":false},{"StartTime":71865.0,"Position":232.0,"HyperDash":false},{"StartTime":71886.0,"Position":229.7937,"HyperDash":false},{"StartTime":71908.0,"Position":232.0,"HyperDash":false},{"StartTime":71930.0,"Position":229.7937,"HyperDash":false},{"StartTime":71951.0,"Position":232.0,"HyperDash":false},{"StartTime":71973.0,"Position":229.7937,"HyperDash":false},{"StartTime":71994.0,"Position":232.0,"HyperDash":false}]},{"StartTime":72037.0,"Objects":[{"StartTime":72037.0,"Position":272.0,"HyperDash":false},{"StartTime":72058.0,"Position":277.46347,"HyperDash":false},{"StartTime":72080.0,"Position":272.0,"HyperDash":false},{"StartTime":72101.0,"Position":277.46347,"HyperDash":false},{"StartTime":72123.0,"Position":272.0,"HyperDash":false},{"StartTime":72144.0,"Position":277.46347,"HyperDash":false},{"StartTime":72166.0,"Position":272.0,"HyperDash":false},{"StartTime":72187.0,"Position":277.46347,"HyperDash":false},{"StartTime":72209.0,"Position":272.0,"HyperDash":false},{"StartTime":72230.0,"Position":277.46347,"HyperDash":false},{"StartTime":72252.0,"Position":272.0,"HyperDash":false},{"StartTime":72274.0,"Position":277.46347,"HyperDash":false},{"StartTime":72295.0,"Position":272.0,"HyperDash":false},{"StartTime":72317.0,"Position":277.46347,"HyperDash":false},{"StartTime":72338.0,"Position":272.0,"HyperDash":false}]},{"StartTime":72382.0,"Objects":[{"StartTime":72382.0,"Position":316.0,"HyperDash":false},{"StartTime":72403.0,"Position":324.5015,"HyperDash":false},{"StartTime":72425.0,"Position":316.0,"HyperDash":false},{"StartTime":72446.0,"Position":324.5015,"HyperDash":false},{"StartTime":72468.0,"Position":316.0,"HyperDash":false},{"StartTime":72489.0,"Position":324.5015,"HyperDash":false},{"StartTime":72511.0,"Position":316.0,"HyperDash":false},{"StartTime":72532.0,"Position":324.5015,"HyperDash":false},{"StartTime":72554.0,"Position":316.0,"HyperDash":false},{"StartTime":72575.0,"Position":324.5015,"HyperDash":false},{"StartTime":72597.0,"Position":316.0,"HyperDash":false},{"StartTime":72619.0,"Position":324.5015,"HyperDash":false},{"StartTime":72640.0,"Position":316.0,"HyperDash":false},{"StartTime":72662.0,"Position":324.5015,"HyperDash":false},{"StartTime":72683.0,"Position":316.0,"HyperDash":false}]},{"StartTime":72727.0,"Objects":[{"StartTime":72727.0,"Position":360.0,"HyperDash":false},{"StartTime":72748.0,"Position":368.5015,"HyperDash":false},{"StartTime":72770.0,"Position":360.0,"HyperDash":false},{"StartTime":72791.0,"Position":368.5015,"HyperDash":false},{"StartTime":72813.0,"Position":360.0,"HyperDash":false},{"StartTime":72834.0,"Position":368.5015,"HyperDash":false},{"StartTime":72856.0,"Position":360.0,"HyperDash":false},{"StartTime":72877.0,"Position":368.5015,"HyperDash":false},{"StartTime":72899.0,"Position":360.0,"HyperDash":false},{"StartTime":72920.0,"Position":368.5015,"HyperDash":false},{"StartTime":72942.0,"Position":360.0,"HyperDash":false},{"StartTime":72964.0,"Position":368.5015,"HyperDash":false},{"StartTime":72985.0,"Position":360.0,"HyperDash":false},{"StartTime":73007.0,"Position":368.5015,"HyperDash":false},{"StartTime":73028.0,"Position":360.0,"HyperDash":true}]},{"StartTime":73072.0,"Objects":[{"StartTime":73072.0,"Position":256.0,"HyperDash":false}]},{"StartTime":73094.0,"Objects":[{"StartTime":73094.0,"Position":244.0,"HyperDash":false}]},{"StartTime":73115.0,"Objects":[{"StartTime":73115.0,"Position":233.0,"HyperDash":false}]},{"StartTime":73137.0,"Objects":[{"StartTime":73137.0,"Position":224.0,"HyperDash":false}]},{"StartTime":73158.0,"Objects":[{"StartTime":73158.0,"Position":215.0,"HyperDash":false}]},{"StartTime":73180.0,"Objects":[{"StartTime":73180.0,"Position":209.0,"HyperDash":false}]},{"StartTime":73201.0,"Objects":[{"StartTime":73201.0,"Position":205.0,"HyperDash":false}]},{"StartTime":73223.0,"Objects":[{"StartTime":73223.0,"Position":202.0,"HyperDash":false}]},{"StartTime":73244.0,"Objects":[{"StartTime":73244.0,"Position":203.0,"HyperDash":false}]},{"StartTime":73266.0,"Objects":[{"StartTime":73266.0,"Position":205.0,"HyperDash":false}]},{"StartTime":73287.0,"Objects":[{"StartTime":73287.0,"Position":210.0,"HyperDash":false}]},{"StartTime":73309.0,"Objects":[{"StartTime":73309.0,"Position":217.0,"HyperDash":false}]},{"StartTime":73331.0,"Objects":[{"StartTime":73331.0,"Position":226.0,"HyperDash":false}]},{"StartTime":73352.0,"Objects":[{"StartTime":73352.0,"Position":236.0,"HyperDash":false}]},{"StartTime":73374.0,"Objects":[{"StartTime":73374.0,"Position":247.0,"HyperDash":false}]},{"StartTime":73395.0,"Objects":[{"StartTime":73395.0,"Position":258.0,"HyperDash":false}]},{"StartTime":73417.0,"Objects":[{"StartTime":73417.0,"Position":270.0,"HyperDash":false}]},{"StartTime":73438.0,"Objects":[{"StartTime":73438.0,"Position":281.0,"HyperDash":false}]},{"StartTime":73460.0,"Objects":[{"StartTime":73460.0,"Position":291.0,"HyperDash":false}]},{"StartTime":73481.0,"Objects":[{"StartTime":73481.0,"Position":300.0,"HyperDash":false}]},{"StartTime":73503.0,"Objects":[{"StartTime":73503.0,"Position":307.0,"HyperDash":false}]},{"StartTime":73525.0,"Objects":[{"StartTime":73525.0,"Position":313.0,"HyperDash":false}]},{"StartTime":73546.0,"Objects":[{"StartTime":73546.0,"Position":316.0,"HyperDash":false}]},{"StartTime":73568.0,"Objects":[{"StartTime":73568.0,"Position":317.0,"HyperDash":false}]},{"StartTime":73589.0,"Objects":[{"StartTime":73589.0,"Position":315.0,"HyperDash":false}]},{"StartTime":73611.0,"Objects":[{"StartTime":73611.0,"Position":311.0,"HyperDash":false}]},{"StartTime":73632.0,"Objects":[{"StartTime":73632.0,"Position":305.0,"HyperDash":false}]},{"StartTime":73654.0,"Objects":[{"StartTime":73654.0,"Position":297.0,"HyperDash":false}]},{"StartTime":73675.0,"Objects":[{"StartTime":73675.0,"Position":288.0,"HyperDash":false}]},{"StartTime":73697.0,"Objects":[{"StartTime":73697.0,"Position":277.0,"HyperDash":false}]},{"StartTime":73719.0,"Objects":[{"StartTime":73719.0,"Position":266.0,"HyperDash":true}]},{"StartTime":73762.0,"Objects":[{"StartTime":73762.0,"Position":164.0,"HyperDash":false}]},{"StartTime":73783.0,"Objects":[{"StartTime":73783.0,"Position":153.0,"HyperDash":false}]},{"StartTime":73805.0,"Objects":[{"StartTime":73805.0,"Position":143.0,"HyperDash":false}]},{"StartTime":73826.0,"Objects":[{"StartTime":73826.0,"Position":133.0,"HyperDash":false}]},{"StartTime":73848.0,"Objects":[{"StartTime":73848.0,"Position":124.0,"HyperDash":false}]},{"StartTime":73869.0,"Objects":[{"StartTime":73869.0,"Position":115.0,"HyperDash":false}]},{"StartTime":73891.0,"Objects":[{"StartTime":73891.0,"Position":108.0,"HyperDash":false}]},{"StartTime":73912.0,"Objects":[{"StartTime":73912.0,"Position":101.0,"HyperDash":false}]},{"StartTime":73934.0,"Objects":[{"StartTime":73934.0,"Position":95.0,"HyperDash":false}]},{"StartTime":73956.0,"Objects":[{"StartTime":73956.0,"Position":90.0,"HyperDash":false}]},{"StartTime":73977.0,"Objects":[{"StartTime":73977.0,"Position":85.0,"HyperDash":false}]},{"StartTime":73999.0,"Objects":[{"StartTime":73999.0,"Position":82.0,"HyperDash":false}]},{"StartTime":74020.0,"Objects":[{"StartTime":74020.0,"Position":80.0,"HyperDash":false}]},{"StartTime":74042.0,"Objects":[{"StartTime":74042.0,"Position":79.0,"HyperDash":false}]},{"StartTime":74063.0,"Objects":[{"StartTime":74063.0,"Position":79.0,"HyperDash":true}]},{"StartTime":74106.0,"Objects":[{"StartTime":74106.0,"Position":180.0,"HyperDash":false}]},{"StartTime":74128.0,"Objects":[{"StartTime":74128.0,"Position":190.0,"HyperDash":false}]},{"StartTime":74150.0,"Objects":[{"StartTime":74150.0,"Position":200.0,"HyperDash":false}]},{"StartTime":74171.0,"Objects":[{"StartTime":74171.0,"Position":210.0,"HyperDash":false}]},{"StartTime":74193.0,"Objects":[{"StartTime":74193.0,"Position":219.0,"HyperDash":false}]},{"StartTime":74214.0,"Objects":[{"StartTime":74214.0,"Position":228.0,"HyperDash":false}]},{"StartTime":74236.0,"Objects":[{"StartTime":74236.0,"Position":235.0,"HyperDash":false}]},{"StartTime":74257.0,"Objects":[{"StartTime":74257.0,"Position":242.0,"HyperDash":false}]},{"StartTime":74279.0,"Objects":[{"StartTime":74279.0,"Position":248.0,"HyperDash":false}]},{"StartTime":74300.0,"Objects":[{"StartTime":74300.0,"Position":253.0,"HyperDash":false}]},{"StartTime":74322.0,"Objects":[{"StartTime":74322.0,"Position":258.0,"HyperDash":false}]},{"StartTime":74344.0,"Objects":[{"StartTime":74344.0,"Position":261.0,"HyperDash":false}]},{"StartTime":74365.0,"Objects":[{"StartTime":74365.0,"Position":263.0,"HyperDash":false}]},{"StartTime":74387.0,"Objects":[{"StartTime":74387.0,"Position":264.0,"HyperDash":false}]},{"StartTime":74408.0,"Objects":[{"StartTime":74408.0,"Position":264.0,"HyperDash":true}]},{"StartTime":74451.0,"Objects":[{"StartTime":74451.0,"Position":148.0,"HyperDash":false},{"StartTime":74519.0,"Position":111.4186,"HyperDash":false},{"StartTime":74623.0,"Position":58.0,"HyperDash":false}]},{"StartTime":74796.0,"Objects":[{"StartTime":74796.0,"Position":196.0,"HyperDash":false},{"StartTime":74864.0,"Position":187.840836,"HyperDash":false},{"StartTime":74968.0,"Position":193.068,"HyperDash":false}]},{"StartTime":75141.0,"Objects":[{"StartTime":75141.0,"Position":328.0,"HyperDash":false},{"StartTime":75209.0,"Position":324.84082,"HyperDash":false},{"StartTime":75313.0,"Position":325.068,"HyperDash":false}]},{"StartTime":75486.0,"Objects":[{"StartTime":75486.0,"Position":228.0,"HyperDash":false}]},{"StartTime":75658.0,"Objects":[{"StartTime":75658.0,"Position":396.0,"HyperDash":true}]},{"StartTime":75831.0,"Objects":[{"StartTime":75831.0,"Position":124.0,"HyperDash":false}]},{"StartTime":76003.0,"Objects":[{"StartTime":76003.0,"Position":36.0,"HyperDash":false}]},{"StartTime":76175.0,"Objects":[{"StartTime":76175.0,"Position":36.0,"HyperDash":false}]},{"StartTime":76348.0,"Objects":[{"StartTime":76348.0,"Position":124.0,"HyperDash":false}]},{"StartTime":76520.0,"Objects":[{"StartTime":76520.0,"Position":292.0,"HyperDash":false},{"StartTime":76588.0,"Position":308.15918,"HyperDash":false},{"StartTime":76692.0,"Position":294.932,"HyperDash":false}]},{"StartTime":76865.0,"Objects":[{"StartTime":76865.0,"Position":192.0,"HyperDash":false},{"StartTime":76933.0,"Position":210.48027,"HyperDash":false},{"StartTime":77037.0,"Position":195.744232,"HyperDash":false}]},{"StartTime":77210.0,"Objects":[{"StartTime":77210.0,"Position":368.0,"HyperDash":false},{"StartTime":77296.0,"Position":391.6784,"HyperDash":false},{"StartTime":77382.0,"Position":424.106964,"HyperDash":false},{"StartTime":77450.0,"Position":426.0137,"HyperDash":false},{"StartTime":77554.0,"Position":368.760162,"HyperDash":false}]},{"StartTime":77727.0,"Objects":[{"StartTime":77727.0,"Position":272.0,"HyperDash":false}]},{"StartTime":77900.0,"Objects":[{"StartTime":77900.0,"Position":176.0,"HyperDash":false},{"StartTime":77968.0,"Position":181.840836,"HyperDash":false},{"StartTime":78072.0,"Position":173.068,"HyperDash":false}]},{"StartTime":78244.0,"Objects":[{"StartTime":78244.0,"Position":272.0,"HyperDash":false}]},{"StartTime":78417.0,"Objects":[{"StartTime":78417.0,"Position":104.0,"HyperDash":true}]},{"StartTime":78589.0,"Objects":[{"StartTime":78589.0,"Position":380.0,"HyperDash":false},{"StartTime":78675.0,"Position":393.75,"HyperDash":false},{"StartTime":78761.0,"Position":447.5,"HyperDash":false},{"StartTime":78829.0,"Position":419.813965,"HyperDash":false},{"StartTime":78933.0,"Position":380.0,"HyperDash":false}]},{"StartTime":79106.0,"Objects":[{"StartTime":79106.0,"Position":284.0,"HyperDash":false}]},{"StartTime":79279.0,"Objects":[{"StartTime":79279.0,"Position":116.0,"HyperDash":false},{"StartTime":79347.0,"Position":99.84083,"HyperDash":false},{"StartTime":79451.0,"Position":113.067986,"HyperDash":false}]},{"StartTime":79624.0,"Objects":[{"StartTime":79624.0,"Position":216.0,"HyperDash":false},{"StartTime":79692.0,"Position":223.68605,"HyperDash":false},{"StartTime":79796.0,"Position":283.5,"HyperDash":false}]},{"StartTime":79882.0,"Objects":[{"StartTime":79882.0,"Position":324.0,"HyperDash":true}]},{"StartTime":79969.0,"Objects":[{"StartTime":79969.0,"Position":152.0,"HyperDash":false},{"StartTime":80055.0,"Position":111.0,"HyperDash":false},{"StartTime":80141.0,"Position":62.0,"HyperDash":false},{"StartTime":80209.0,"Position":99.58139,"HyperDash":false},{"StartTime":80313.0,"Position":152.0,"HyperDash":false}]},{"StartTime":80486.0,"Objects":[{"StartTime":80486.0,"Position":248.0,"HyperDash":false}]},{"StartTime":80658.0,"Objects":[{"StartTime":80658.0,"Position":416.0,"HyperDash":false},{"StartTime":80726.0,"Position":434.4803,"HyperDash":false},{"StartTime":80830.0,"Position":419.744232,"HyperDash":false}]},{"StartTime":81003.0,"Objects":[{"StartTime":81003.0,"Position":324.0,"HyperDash":false},{"StartTime":81071.0,"Position":308.313965,"HyperDash":false},{"StartTime":81175.0,"Position":256.5,"HyperDash":false}]},{"StartTime":81262.0,"Objects":[{"StartTime":81262.0,"Position":208.0,"HyperDash":true}]},{"StartTime":81348.0,"Objects":[{"StartTime":81348.0,"Position":384.0,"HyperDash":false},{"StartTime":81434.0,"Position":431.0,"HyperDash":false},{"StartTime":81520.0,"Position":474.0,"HyperDash":false},{"StartTime":81588.0,"Position":446.4186,"HyperDash":false},{"StartTime":81692.0,"Position":384.0,"HyperDash":false}]},{"StartTime":81865.0,"Objects":[{"StartTime":81865.0,"Position":212.0,"HyperDash":true}]},{"StartTime":82037.0,"Objects":[{"StartTime":82037.0,"Position":444.0,"HyperDash":false},{"StartTime":82105.0,"Position":438.4026,"HyperDash":false},{"StartTime":82209.0,"Position":447.547729,"HyperDash":true}]},{"StartTime":82382.0,"Objects":[{"StartTime":82382.0,"Position":212.0,"HyperDash":false}]},{"StartTime":82469.0,"Objects":[{"StartTime":82469.0,"Position":172.0,"HyperDash":false}]},{"StartTime":82555.0,"Objects":[{"StartTime":82555.0,"Position":132.0,"HyperDash":true}]},{"StartTime":82727.0,"Objects":[{"StartTime":82727.0,"Position":432.0,"HyperDash":false},{"StartTime":82813.0,"Position":480.699646,"HyperDash":false},{"StartTime":82899.0,"Position":500.151184,"HyperDash":false},{"StartTime":82967.0,"Position":468.371918,"HyperDash":false},{"StartTime":83071.0,"Position":432.553162,"HyperDash":false}]},{"StartTime":83244.0,"Objects":[{"StartTime":83244.0,"Position":272.0,"HyperDash":false}]},{"StartTime":83417.0,"Objects":[{"StartTime":83417.0,"Position":440.0,"HyperDash":false},{"StartTime":83485.0,"Position":421.4803,"HyperDash":false},{"StartTime":83589.0,"Position":443.744232,"HyperDash":true}]},{"StartTime":83762.0,"Objects":[{"StartTime":83762.0,"Position":200.0,"HyperDash":false}]},{"StartTime":83934.0,"Objects":[{"StartTime":83934.0,"Position":352.0,"HyperDash":true}]},{"StartTime":84106.0,"Objects":[{"StartTime":84106.0,"Position":104.0,"HyperDash":false},{"StartTime":84192.0,"Position":62.0,"HyperDash":false},{"StartTime":84278.0,"Position":14.0,"HyperDash":false},{"StartTime":84346.0,"Position":49.58139,"HyperDash":false},{"StartTime":84450.0,"Position":104.0,"HyperDash":false}]},{"StartTime":84624.0,"Objects":[{"StartTime":84624.0,"Position":272.0,"HyperDash":false}]},{"StartTime":84796.0,"Objects":[{"StartTime":84796.0,"Position":112.0,"HyperDash":false},{"StartTime":84864.0,"Position":120.667366,"HyperDash":false},{"StartTime":84968.0,"Position":108.629211,"HyperDash":false}]},{"StartTime":85055.0,"Objects":[{"StartTime":85055.0,"Position":164.0,"HyperDash":false}]},{"StartTime":85141.0,"Objects":[{"StartTime":85141.0,"Position":216.0,"HyperDash":false},{"StartTime":85209.0,"Position":224.68605,"HyperDash":false},{"StartTime":85313.0,"Position":283.5,"HyperDash":true}]},{"StartTime":85486.0,"Objects":[{"StartTime":85486.0,"Position":32.0,"HyperDash":false},{"StartTime":85572.0,"Position":0.299288034,"HyperDash":false},{"StartTime":85658.0,"Position":4.98187447,"HyperDash":false},{"StartTime":85744.0,"Position":36.15001,"HyperDash":false}]},{"StartTime":85831.0,"Objects":[{"StartTime":85831.0,"Position":108.0,"HyperDash":false},{"StartTime":85899.0,"Position":145.58139,"HyperDash":false},{"StartTime":86003.0,"Position":198.0,"HyperDash":false}]},{"StartTime":86175.0,"Objects":[{"StartTime":86175.0,"Position":20.0,"HyperDash":false}]},{"StartTime":86348.0,"Objects":[{"StartTime":86348.0,"Position":128.0,"HyperDash":false},{"StartTime":86434.0,"Position":173.0,"HyperDash":true}]},{"StartTime":86520.0,"Objects":[{"StartTime":86520.0,"Position":344.0,"HyperDash":false},{"StartTime":86588.0,"Position":325.4186,"HyperDash":false},{"StartTime":86692.0,"Position":254.0,"HyperDash":false}]},{"StartTime":86865.0,"Objects":[{"StartTime":86865.0,"Position":436.0,"HyperDash":false},{"StartTime":86933.0,"Position":448.3675,"HyperDash":false},{"StartTime":87037.0,"Position":439.458984,"HyperDash":false}]},{"StartTime":87124.0,"Objects":[{"StartTime":87124.0,"Position":375.0,"HyperDash":false}]},{"StartTime":87210.0,"Objects":[{"StartTime":87210.0,"Position":312.0,"HyperDash":false}]},{"StartTime":87382.0,"Objects":[{"StartTime":87382.0,"Position":472.0,"HyperDash":false}]},{"StartTime":87555.0,"Objects":[{"StartTime":87555.0,"Position":300.0,"HyperDash":false},{"StartTime":87623.0,"Position":293.518738,"HyperDash":false},{"StartTime":87727.0,"Position":296.253265,"HyperDash":false}]},{"StartTime":87813.0,"Objects":[{"StartTime":87813.0,"Position":360.0,"HyperDash":true}]},{"StartTime":87900.0,"Objects":[{"StartTime":87900.0,"Position":196.0,"HyperDash":false},{"StartTime":87968.0,"Position":147.41861,"HyperDash":false},{"StartTime":88072.0,"Position":106.0,"HyperDash":false}]},{"StartTime":88244.0,"Objects":[{"StartTime":88244.0,"Position":276.0,"HyperDash":false},{"StartTime":88312.0,"Position":320.5814,"HyperDash":false},{"StartTime":88416.0,"Position":366.0,"HyperDash":false}]},{"StartTime":88503.0,"Objects":[{"StartTime":88503.0,"Position":312.0,"HyperDash":false}]},{"StartTime":88589.0,"Objects":[{"StartTime":88589.0,"Position":260.0,"HyperDash":false}]},{"StartTime":88762.0,"Objects":[{"StartTime":88762.0,"Position":440.0,"HyperDash":true}]},{"StartTime":88934.0,"Objects":[{"StartTime":88934.0,"Position":192.0,"HyperDash":false},{"StartTime":89002.0,"Position":158.41861,"HyperDash":false},{"StartTime":89106.0,"Position":102.0,"HyperDash":false}]},{"StartTime":89193.0,"Objects":[{"StartTime":89193.0,"Position":164.0,"HyperDash":false}]},{"StartTime":89279.0,"Objects":[{"StartTime":89279.0,"Position":228.0,"HyperDash":false},{"StartTime":89347.0,"Position":188.41861,"HyperDash":false},{"StartTime":89451.0,"Position":138.0,"HyperDash":false}]},{"StartTime":89624.0,"Objects":[{"StartTime":89624.0,"Position":306.0,"HyperDash":false},{"StartTime":89692.0,"Position":334.5814,"HyperDash":false},{"StartTime":89796.0,"Position":396.0,"HyperDash":false}]},{"StartTime":89882.0,"Objects":[{"StartTime":89882.0,"Position":450.0,"HyperDash":false}]},{"StartTime":89969.0,"Objects":[{"StartTime":89969.0,"Position":396.0,"HyperDash":false}]},{"StartTime":90141.0,"Objects":[{"StartTime":90141.0,"Position":228.0,"HyperDash":false}]},{"StartTime":90313.0,"Objects":[{"StartTime":90313.0,"Position":396.0,"HyperDash":false},{"StartTime":90381.0,"Position":408.481262,"HyperDash":false},{"StartTime":90485.0,"Position":399.746735,"HyperDash":false}]},{"StartTime":90572.0,"Objects":[{"StartTime":90572.0,"Position":332.0,"HyperDash":false}]},{"StartTime":90658.0,"Objects":[{"StartTime":90658.0,"Position":264.0,"HyperDash":false},{"StartTime":90726.0,"Position":289.5814,"HyperDash":false},{"StartTime":90830.0,"Position":354.0,"HyperDash":false}]},{"StartTime":91003.0,"Objects":[{"StartTime":91003.0,"Position":184.0,"HyperDash":false},{"StartTime":91071.0,"Position":167.41861,"HyperDash":false},{"StartTime":91175.0,"Position":94.0,"HyperDash":false}]},{"StartTime":91262.0,"Objects":[{"StartTime":91262.0,"Position":148.0,"HyperDash":false}]},{"StartTime":91348.0,"Objects":[{"StartTime":91348.0,"Position":200.0,"HyperDash":false}]},{"StartTime":91520.0,"Objects":[{"StartTime":91520.0,"Position":32.0,"HyperDash":true}]},{"StartTime":91693.0,"Objects":[{"StartTime":91693.0,"Position":296.0,"HyperDash":false},{"StartTime":91761.0,"Position":318.5814,"HyperDash":false},{"StartTime":91865.0,"Position":302.0,"HyperDash":false}]},{"StartTime":91951.0,"Objects":[{"StartTime":91951.0,"Position":240.0,"HyperDash":false}]},{"StartTime":92037.0,"Objects":[{"StartTime":92037.0,"Position":136.0,"HyperDash":false},{"StartTime":92123.0,"Position":133.503845,"HyperDash":false}]},{"StartTime":92210.0,"Objects":[{"StartTime":92210.0,"Position":196.0,"HyperDash":false},{"StartTime":92296.0,"Position":199.206116,"HyperDash":true}]},{"StartTime":92382.0,"Objects":[{"StartTime":92382.0,"Position":48.0,"HyperDash":false},{"StartTime":92450.0,"Position":10.418602,"HyperDash":false},{"StartTime":92554.0,"Position":50.0,"HyperDash":false}]},{"StartTime":92641.0,"Objects":[{"StartTime":92641.0,"Position":120.0,"HyperDash":false}]},{"StartTime":92727.0,"Objects":[{"StartTime":92727.0,"Position":188.0,"HyperDash":false}]},{"StartTime":92900.0,"Objects":[{"StartTime":92900.0,"Position":360.0,"HyperDash":true}]},{"StartTime":93072.0,"Objects":[{"StartTime":93072.0,"Position":123.0,"HyperDash":false},{"StartTime":93140.0,"Position":135.518738,"HyperDash":false},{"StartTime":93244.0,"Position":119.25325,"HyperDash":false}]},{"StartTime":93331.0,"Objects":[{"StartTime":93331.0,"Position":188.0,"HyperDash":true}]},{"StartTime":93417.0,"Objects":[{"StartTime":93417.0,"Position":368.0,"HyperDash":false},{"StartTime":93503.0,"Position":413.0,"HyperDash":false},{"StartTime":93589.0,"Position":368.0,"HyperDash":true}]},{"StartTime":93762.0,"Objects":[{"StartTime":93762.0,"Position":96.0,"HyperDash":false}]},{"StartTime":93848.0,"Objects":[{"StartTime":93848.0,"Position":53.0,"HyperDash":false}]},{"StartTime":93934.0,"Objects":[{"StartTime":93934.0,"Position":45.0,"HyperDash":false}]},{"StartTime":94020.0,"Objects":[{"StartTime":94020.0,"Position":75.0,"HyperDash":false}]},{"StartTime":94106.0,"Objects":[{"StartTime":94106.0,"Position":128.0,"HyperDash":false}]},{"StartTime":94279.0,"Objects":[{"StartTime":94279.0,"Position":316.0,"HyperDash":true}]},{"StartTime":94451.0,"Objects":[{"StartTime":94451.0,"Position":48.0,"HyperDash":false},{"StartTime":94519.0,"Position":51.57788,"HyperDash":false},{"StartTime":94623.0,"Position":44.4028778,"HyperDash":false}]},{"StartTime":94710.0,"Objects":[{"StartTime":94710.0,"Position":112.0,"HyperDash":true}]},{"StartTime":94796.0,"Objects":[{"StartTime":94796.0,"Position":300.0,"HyperDash":false}]},{"StartTime":94969.0,"Objects":[{"StartTime":94969.0,"Position":416.0,"HyperDash":false},{"StartTime":95055.0,"Position":371.0,"HyperDash":true}]},{"StartTime":95141.0,"Objects":[{"StartTime":95141.0,"Position":180.0,"HyperDash":false}]},{"StartTime":95227.0,"Objects":[{"StartTime":95227.0,"Position":128.0,"HyperDash":false}]},{"StartTime":95313.0,"Objects":[{"StartTime":95313.0,"Position":76.0,"HyperDash":false}]},{"StartTime":95486.0,"Objects":[{"StartTime":95486.0,"Position":248.0,"HyperDash":false}]},{"StartTime":95658.0,"Objects":[{"StartTime":95658.0,"Position":68.0,"HyperDash":true}]},{"StartTime":95831.0,"Objects":[{"StartTime":95831.0,"Position":348.0,"HyperDash":false},{"StartTime":95899.0,"Position":373.5814,"HyperDash":false},{"StartTime":96003.0,"Position":438.0,"HyperDash":true}]},{"StartTime":96175.0,"Objects":[{"StartTime":96175.0,"Position":176.0,"HyperDash":false},{"StartTime":96261.0,"Position":102.839455,"HyperDash":false},{"StartTime":96347.0,"Position":70.72701,"HyperDash":false},{"StartTime":96433.0,"Position":95.68428,"HyperDash":false},{"StartTime":96519.0,"Position":200.009659,"HyperDash":false},{"StartTime":96605.0,"Position":276.00058,"HyperDash":false},{"StartTime":96692.0,"Position":280.8676,"HyperDash":false},{"StartTime":96821.0,"Position":179.5454,"HyperDash":false}]},{"StartTime":96865.0,"Objects":[{"StartTime":96865.0,"Position":156.0,"HyperDash":false},{"StartTime":96951.0,"Position":90.61737,"HyperDash":false},{"StartTime":97037.0,"Position":78.41168,"HyperDash":false},{"StartTime":97123.0,"Position":117.060234,"HyperDash":false},{"StartTime":97209.0,"Position":173.374588,"HyperDash":false},{"StartTime":97295.0,"Position":216.741837,"HyperDash":false},{"StartTime":97382.0,"Position":244.734863,"HyperDash":false},{"StartTime":97511.0,"Position":177.973221,"HyperDash":false}]},{"StartTime":97555.0,"Objects":[{"StartTime":97555.0,"Position":144.0,"HyperDash":false},{"StartTime":97641.0,"Position":112.369415,"HyperDash":false},{"StartTime":97727.0,"Position":88.02037,"HyperDash":false},{"StartTime":97813.0,"Position":103.779022,"HyperDash":false},{"StartTime":97899.0,"Position":143.185013,"HyperDash":false},{"StartTime":97985.0,"Position":200.21698,"HyperDash":false},{"StartTime":98072.0,"Position":207.316086,"HyperDash":false},{"StartTime":98140.0,"Position":203.132828,"HyperDash":false},{"StartTime":98244.0,"Position":161.018463,"HyperDash":false}]},{"StartTime":99279.0,"Objects":[{"StartTime":99279.0,"Position":164.0,"HyperDash":false}]},{"StartTime":99451.0,"Objects":[{"StartTime":99451.0,"Position":324.0,"HyperDash":false},{"StartTime":99494.0,"Position":333.138123,"HyperDash":false},{"StartTime":99537.0,"Position":324.0,"HyperDash":false},{"StartTime":99580.0,"Position":333.138123,"HyperDash":true}]},{"StartTime":99624.0,"Objects":[{"StartTime":99624.0,"Position":204.0,"HyperDash":false},{"StartTime":99692.0,"Position":148.6279,"HyperDash":false},{"StartTime":99796.0,"Position":69.0,"HyperDash":true}]},{"StartTime":99969.0,"Objects":[{"StartTime":99969.0,"Position":340.0,"HyperDash":false},{"StartTime":100037.0,"Position":303.6279,"HyperDash":false},{"StartTime":100141.0,"Position":205.0,"HyperDash":true}]},{"StartTime":100313.0,"Objects":[{"StartTime":100313.0,"Position":472.0,"HyperDash":true}]},{"StartTime":100658.0,"Objects":[{"StartTime":100658.0,"Position":64.0,"HyperDash":false},{"StartTime":100744.0,"Position":10.0,"HyperDash":false},{"StartTime":100830.0,"Position":64.0,"HyperDash":true}]},{"StartTime":101003.0,"Objects":[{"StartTime":101003.0,"Position":336.0,"HyperDash":false}]},{"StartTime":101175.0,"Objects":[{"StartTime":101175.0,"Position":176.0,"HyperDash":true}]},{"StartTime":101348.0,"Objects":[{"StartTime":101348.0,"Position":448.0,"HyperDash":false},{"StartTime":101416.0,"Position":502.697662,"HyperDash":false},{"StartTime":101520.0,"Position":444.0,"HyperDash":false}]},{"StartTime":101606.0,"Objects":[{"StartTime":101606.0,"Position":384.0,"HyperDash":true}]},{"StartTime":101693.0,"Objects":[{"StartTime":101693.0,"Position":220.0,"HyperDash":false},{"StartTime":101761.0,"Position":270.697662,"HyperDash":false},{"StartTime":101865.0,"Position":328.0,"HyperDash":false}]},{"StartTime":101951.0,"Objects":[{"StartTime":101951.0,"Position":264.0,"HyperDash":true}]},{"StartTime":102037.0,"Objects":[{"StartTime":102037.0,"Position":112.0,"HyperDash":false}]},{"StartTime":102124.0,"Objects":[{"StartTime":102124.0,"Position":56.0,"HyperDash":false}]},{"StartTime":102210.0,"Objects":[{"StartTime":102210.0,"Position":56.0,"HyperDash":true}]},{"StartTime":102382.0,"Objects":[{"StartTime":102382.0,"Position":344.0,"HyperDash":true}]},{"StartTime":102555.0,"Objects":[{"StartTime":102555.0,"Position":56.0,"HyperDash":true}]},{"StartTime":102727.0,"Objects":[{"StartTime":102727.0,"Position":368.0,"HyperDash":false},{"StartTime":102795.0,"Position":407.851135,"HyperDash":false},{"StartTime":102899.0,"Position":390.870453,"HyperDash":false}]},{"StartTime":102986.0,"Objects":[{"StartTime":102986.0,"Position":332.0,"HyperDash":true}]},{"StartTime":103072.0,"Objects":[{"StartTime":103072.0,"Position":168.0,"HyperDash":false},{"StartTime":103140.0,"Position":110.302322,"HyperDash":false},{"StartTime":103244.0,"Position":60.0,"HyperDash":false}]},{"StartTime":103331.0,"Objects":[{"StartTime":103331.0,"Position":120.0,"HyperDash":true}]},{"StartTime":103417.0,"Objects":[{"StartTime":103417.0,"Position":304.0,"HyperDash":false}]},{"StartTime":103503.0,"Objects":[{"StartTime":103503.0,"Position":364.0,"HyperDash":false}]},{"StartTime":103589.0,"Objects":[{"StartTime":103589.0,"Position":424.0,"HyperDash":true}]},{"StartTime":103762.0,"Objects":[{"StartTime":103762.0,"Position":152.0,"HyperDash":false}]},{"StartTime":103934.0,"Objects":[{"StartTime":103934.0,"Position":316.0,"HyperDash":true}]},{"StartTime":104106.0,"Objects":[{"StartTime":104106.0,"Position":56.0,"HyperDash":false},{"StartTime":104174.0,"Position":18.3023262,"HyperDash":false},{"StartTime":104278.0,"Position":59.9999962,"HyperDash":false}]},{"StartTime":104365.0,"Objects":[{"StartTime":104365.0,"Position":116.0,"HyperDash":true}]},{"StartTime":104451.0,"Objects":[{"StartTime":104451.0,"Position":304.0,"HyperDash":false},{"StartTime":104519.0,"Position":357.697662,"HyperDash":false},{"StartTime":104623.0,"Position":412.0,"HyperDash":false}]},{"StartTime":104710.0,"Objects":[{"StartTime":104710.0,"Position":356.0,"HyperDash":true}]},{"StartTime":104796.0,"Objects":[{"StartTime":104796.0,"Position":168.0,"HyperDash":false},{"StartTime":104882.0,"Position":114.0,"HyperDash":false},{"StartTime":104968.0,"Position":168.0,"HyperDash":true}]},{"StartTime":105141.0,"Objects":[{"StartTime":105141.0,"Position":440.0,"HyperDash":true}]},{"StartTime":105313.0,"Objects":[{"StartTime":105313.0,"Position":144.0,"HyperDash":true}]},{"StartTime":105486.0,"Objects":[{"StartTime":105486.0,"Position":468.0,"HyperDash":false},{"StartTime":105554.0,"Position":451.0,"HyperDash":false},{"StartTime":105658.0,"Position":412.0,"HyperDash":false}]},{"StartTime":105744.0,"Objects":[{"StartTime":105744.0,"Position":360.0,"HyperDash":true}]},{"StartTime":105831.0,"Objects":[{"StartTime":105831.0,"Position":164.0,"HyperDash":false},{"StartTime":105899.0,"Position":224.697678,"HyperDash":false},{"StartTime":106003.0,"Position":272.0,"HyperDash":false}]},{"StartTime":106089.0,"Objects":[{"StartTime":106089.0,"Position":212.0,"HyperDash":true}]},{"StartTime":106175.0,"Objects":[{"StartTime":106175.0,"Position":24.0,"HyperDash":false}]},{"StartTime":106262.0,"Objects":[{"StartTime":106262.0,"Position":20.0,"HyperDash":false}]},{"StartTime":106348.0,"Objects":[{"StartTime":106348.0,"Position":16.0,"HyperDash":true}]},{"StartTime":106520.0,"Objects":[{"StartTime":106520.0,"Position":296.0,"HyperDash":false}]},{"StartTime":106693.0,"Objects":[{"StartTime":106693.0,"Position":132.0,"HyperDash":true}]},{"StartTime":106865.0,"Objects":[{"StartTime":106865.0,"Position":400.0,"HyperDash":false},{"StartTime":106933.0,"Position":443.234375,"HyperDash":false},{"StartTime":107037.0,"Position":447.13623,"HyperDash":false}]},{"StartTime":107124.0,"Objects":[{"StartTime":107124.0,"Position":388.0,"HyperDash":true}]},{"StartTime":107210.0,"Objects":[{"StartTime":107210.0,"Position":196.0,"HyperDash":false},{"StartTime":107278.0,"Position":142.302322,"HyperDash":false},{"StartTime":107382.0,"Position":88.0,"HyperDash":false}]},{"StartTime":107469.0,"Objects":[{"StartTime":107469.0,"Position":148.0,"HyperDash":true}]},{"StartTime":107555.0,"Objects":[{"StartTime":107555.0,"Position":304.0,"HyperDash":false}]},{"StartTime":107641.0,"Objects":[{"StartTime":107641.0,"Position":358.0,"HyperDash":false}]},{"StartTime":107727.0,"Objects":[{"StartTime":107727.0,"Position":412.0,"HyperDash":true}]},{"StartTime":107900.0,"Objects":[{"StartTime":107900.0,"Position":136.0,"HyperDash":true}]},{"StartTime":108072.0,"Objects":[{"StartTime":108072.0,"Position":432.0,"HyperDash":true}]},{"StartTime":108244.0,"Objects":[{"StartTime":108244.0,"Position":160.0,"HyperDash":false},{"StartTime":108312.0,"Position":129.302322,"HyperDash":false},{"StartTime":108416.0,"Position":52.0,"HyperDash":false}]},{"StartTime":108503.0,"Objects":[{"StartTime":108503.0,"Position":112.0,"HyperDash":true}]},{"StartTime":108589.0,"Objects":[{"StartTime":108589.0,"Position":300.0,"HyperDash":false},{"StartTime":108657.0,"Position":249.302338,"HyperDash":false},{"StartTime":108761.0,"Position":192.0,"HyperDash":false}]},{"StartTime":108848.0,"Objects":[{"StartTime":108848.0,"Position":248.0,"HyperDash":true}]},{"StartTime":108934.0,"Objects":[{"StartTime":108934.0,"Position":436.0,"HyperDash":false},{"StartTime":109020.0,"Position":490.0,"HyperDash":false},{"StartTime":109106.0,"Position":436.0,"HyperDash":true}]},{"StartTime":109279.0,"Objects":[{"StartTime":109279.0,"Position":164.0,"HyperDash":false}]},{"StartTime":109451.0,"Objects":[{"StartTime":109451.0,"Position":324.0,"HyperDash":true}]},{"StartTime":109624.0,"Objects":[{"StartTime":109624.0,"Position":52.0,"HyperDash":false},{"StartTime":109692.0,"Position":35.4452477,"HyperDash":false},{"StartTime":109796.0,"Position":52.0779152,"HyperDash":false}]},{"StartTime":109882.0,"Objects":[{"StartTime":109882.0,"Position":112.0,"HyperDash":true}]},{"StartTime":109969.0,"Objects":[{"StartTime":109969.0,"Position":316.0,"HyperDash":false},{"StartTime":110037.0,"Position":270.302338,"HyperDash":false},{"StartTime":110141.0,"Position":208.0,"HyperDash":false}]},{"StartTime":110227.0,"Objects":[{"StartTime":110227.0,"Position":268.0,"HyperDash":true}]},{"StartTime":110313.0,"Objects":[{"StartTime":110313.0,"Position":456.0,"HyperDash":false},{"StartTime":110381.0,"Position":440.422455,"HyperDash":false},{"StartTime":110485.0,"Position":459.598,"HyperDash":false}]},{"StartTime":110658.0,"Objects":[{"StartTime":110658.0,"Position":292.0,"HyperDash":false},{"StartTime":110726.0,"Position":273.422455,"HyperDash":false},{"StartTime":110830.0,"Position":295.598,"HyperDash":true}]},{"StartTime":111003.0,"Objects":[{"StartTime":111003.0,"Position":32.0,"HyperDash":false}]},{"StartTime":111118.0,"Objects":[{"StartTime":111118.0,"Position":140.0,"HyperDash":false}]},{"StartTime":111233.0,"Objects":[{"StartTime":111233.0,"Position":248.0,"HyperDash":true}]},{"StartTime":111348.0,"Objects":[{"StartTime":111348.0,"Position":44.0,"HyperDash":false},{"StartTime":111405.0,"Position":62.84279,"HyperDash":false},{"StartTime":111462.0,"Position":116.0,"HyperDash":false},{"StartTime":111577.0,"Position":44.0,"HyperDash":true}]},{"StartTime":111693.0,"Objects":[{"StartTime":111693.0,"Position":320.0,"HyperDash":false}]},{"StartTime":111779.0,"Objects":[{"StartTime":111779.0,"Position":392.0,"HyperDash":false}]},{"StartTime":111865.0,"Objects":[{"StartTime":111865.0,"Position":464.0,"HyperDash":true}]},{"StartTime":112037.0,"Objects":[{"StartTime":112037.0,"Position":196.0,"HyperDash":false}]},{"StartTime":112210.0,"Objects":[{"StartTime":112210.0,"Position":364.0,"HyperDash":true}]},{"StartTime":112382.0,"Objects":[{"StartTime":112382.0,"Position":92.0,"HyperDash":false},{"StartTime":112450.0,"Position":150.697678,"HyperDash":false},{"StartTime":112554.0,"Position":200.0,"HyperDash":false}]},{"StartTime":112641.0,"Objects":[{"StartTime":112641.0,"Position":140.0,"HyperDash":true}]},{"StartTime":112727.0,"Objects":[{"StartTime":112727.0,"Position":356.0,"HyperDash":false},{"StartTime":112795.0,"Position":379.697662,"HyperDash":false},{"StartTime":112899.0,"Position":352.0,"HyperDash":false}]},{"StartTime":112986.0,"Objects":[{"StartTime":112986.0,"Position":292.0,"HyperDash":true}]},{"StartTime":113072.0,"Objects":[{"StartTime":113072.0,"Position":96.0,"HyperDash":false}]},{"StartTime":113158.0,"Objects":[{"StartTime":113158.0,"Position":36.0,"HyperDash":false}]},{"StartTime":113244.0,"Objects":[{"StartTime":113244.0,"Position":96.0,"HyperDash":true}]},{"StartTime":113417.0,"Objects":[{"StartTime":113417.0,"Position":368.0,"HyperDash":true}]},{"StartTime":113589.0,"Objects":[{"StartTime":113589.0,"Position":72.0,"HyperDash":true}]},{"StartTime":113762.0,"Objects":[{"StartTime":113762.0,"Position":364.0,"HyperDash":false},{"StartTime":113830.0,"Position":340.302338,"HyperDash":false},{"StartTime":113934.0,"Position":256.0,"HyperDash":false}]},{"StartTime":114020.0,"Objects":[{"StartTime":114020.0,"Position":316.0,"HyperDash":true}]},{"StartTime":114106.0,"Objects":[{"StartTime":114106.0,"Position":120.0,"HyperDash":false},{"StartTime":114174.0,"Position":143.697678,"HyperDash":false},{"StartTime":114278.0,"Position":228.0,"HyperDash":false}]},{"StartTime":114365.0,"Objects":[{"StartTime":114365.0,"Position":168.0,"HyperDash":true}]},{"StartTime":114451.0,"Objects":[{"StartTime":114451.0,"Position":384.0,"HyperDash":false}]},{"StartTime":114537.0,"Objects":[{"StartTime":114537.0,"Position":444.0,"HyperDash":false}]},{"StartTime":114624.0,"Objects":[{"StartTime":114624.0,"Position":444.0,"HyperDash":true}]},{"StartTime":114796.0,"Objects":[{"StartTime":114796.0,"Position":176.0,"HyperDash":false}]},{"StartTime":114969.0,"Objects":[{"StartTime":114969.0,"Position":344.0,"HyperDash":true}]},{"StartTime":115141.0,"Objects":[{"StartTime":115141.0,"Position":76.0,"HyperDash":false},{"StartTime":115209.0,"Position":33.3023262,"HyperDash":false},{"StartTime":115313.0,"Position":20.0,"HyperDash":false}]},{"StartTime":115400.0,"Objects":[{"StartTime":115400.0,"Position":80.0,"HyperDash":true}]},{"StartTime":115486.0,"Objects":[{"StartTime":115486.0,"Position":284.0,"HyperDash":false},{"StartTime":115554.0,"Position":236.302322,"HyperDash":false},{"StartTime":115658.0,"Position":176.0,"HyperDash":false}]},{"StartTime":115744.0,"Objects":[{"StartTime":115744.0,"Position":236.0,"HyperDash":true}]},{"StartTime":115831.0,"Objects":[{"StartTime":115831.0,"Position":28.0,"HyperDash":false},{"StartTime":115917.0,"Position":82.0,"HyperDash":false},{"StartTime":116003.0,"Position":28.0,"HyperDash":true}]},{"StartTime":116175.0,"Objects":[{"StartTime":116175.0,"Position":300.0,"HyperDash":false}]},{"StartTime":116348.0,"Objects":[{"StartTime":116348.0,"Position":132.0,"HyperDash":true}]},{"StartTime":116520.0,"Objects":[{"StartTime":116520.0,"Position":408.0,"HyperDash":false},{"StartTime":116588.0,"Position":351.302338,"HyperDash":false},{"StartTime":116692.0,"Position":300.0,"HyperDash":false}]},{"StartTime":116779.0,"Objects":[{"StartTime":116779.0,"Position":360.0,"HyperDash":true}]},{"StartTime":116865.0,"Objects":[{"StartTime":116865.0,"Position":156.0,"HyperDash":false},{"StartTime":116933.0,"Position":184.697678,"HyperDash":false},{"StartTime":117037.0,"Position":264.0,"HyperDash":false}]},{"StartTime":117124.0,"Objects":[{"StartTime":117124.0,"Position":204.0,"HyperDash":true}]},{"StartTime":117210.0,"Objects":[{"StartTime":117210.0,"Position":384.0,"HyperDash":false}]},{"StartTime":117296.0,"Objects":[{"StartTime":117296.0,"Position":444.0,"HyperDash":false}]},{"StartTime":117382.0,"Objects":[{"StartTime":117382.0,"Position":504.0,"HyperDash":true}]},{"StartTime":117555.0,"Objects":[{"StartTime":117555.0,"Position":228.0,"HyperDash":false},{"StartTime":117623.0,"Position":287.697662,"HyperDash":false},{"StartTime":117727.0,"Position":336.0,"HyperDash":true}]},{"StartTime":117900.0,"Objects":[{"StartTime":117900.0,"Position":60.0,"HyperDash":false},{"StartTime":117968.0,"Position":86.69768,"HyperDash":false},{"StartTime":118072.0,"Position":168.0,"HyperDash":false}]},{"StartTime":118158.0,"Objects":[{"StartTime":118158.0,"Position":108.0,"HyperDash":true}]},{"StartTime":118244.0,"Objects":[{"StartTime":118244.0,"Position":324.0,"HyperDash":false},{"StartTime":118312.0,"Position":384.697662,"HyperDash":false},{"StartTime":118416.0,"Position":380.0,"HyperDash":false}]},{"StartTime":118503.0,"Objects":[{"StartTime":118503.0,"Position":320.0,"HyperDash":true}]},{"StartTime":118589.0,"Objects":[{"StartTime":118589.0,"Position":132.0,"HyperDash":false}]},{"StartTime":118675.0,"Objects":[{"StartTime":118675.0,"Position":72.0,"HyperDash":false}]},{"StartTime":118762.0,"Objects":[{"StartTime":118762.0,"Position":132.0,"HyperDash":true}]},{"StartTime":118934.0,"Objects":[{"StartTime":118934.0,"Position":428.0,"HyperDash":true}]},{"StartTime":119106.0,"Objects":[{"StartTime":119106.0,"Position":80.0,"HyperDash":true}]},{"StartTime":119279.0,"Objects":[{"StartTime":119279.0,"Position":352.0,"HyperDash":false},{"StartTime":119347.0,"Position":288.6279,"HyperDash":false},{"StartTime":119451.0,"Position":217.0,"HyperDash":false}]},{"StartTime":119537.0,"Objects":[{"StartTime":119537.0,"Position":148.0,"HyperDash":true}]},{"StartTime":119624.0,"Objects":[{"StartTime":119624.0,"Position":388.0,"HyperDash":false},{"StartTime":119692.0,"Position":336.6279,"HyperDash":false},{"StartTime":119796.0,"Position":253.0,"HyperDash":false}]},{"StartTime":119882.0,"Objects":[{"StartTime":119882.0,"Position":320.0,"HyperDash":true}]},{"StartTime":119969.0,"Objects":[{"StartTime":119969.0,"Position":100.0,"HyperDash":false},{"StartTime":120055.0,"Position":46.0,"HyperDash":false},{"StartTime":120141.0,"Position":100.0,"HyperDash":true}]},{"StartTime":120313.0,"Objects":[{"StartTime":120313.0,"Position":384.0,"HyperDash":true}]},{"StartTime":120486.0,"Objects":[{"StartTime":120486.0,"Position":112.0,"HyperDash":true}]},{"StartTime":120658.0,"Objects":[{"StartTime":120658.0,"Position":408.0,"HyperDash":false},{"StartTime":120726.0,"Position":466.697662,"HyperDash":false},{"StartTime":120830.0,"Position":412.0,"HyperDash":false}]},{"StartTime":120917.0,"Objects":[{"StartTime":120917.0,"Position":348.0,"HyperDash":true}]},{"StartTime":121003.0,"Objects":[{"StartTime":121003.0,"Position":132.0,"HyperDash":false},{"StartTime":121071.0,"Position":77.837204,"HyperDash":false},{"StartTime":121175.0,"Position":127.999992,"HyperDash":false}]},{"StartTime":121262.0,"Objects":[{"StartTime":121262.0,"Position":196.0,"HyperDash":true}]},{"StartTime":121348.0,"Objects":[{"StartTime":121348.0,"Position":384.0,"HyperDash":false},{"StartTime":121434.0,"Position":387.368439,"HyperDash":true}]},{"StartTime":121520.0,"Objects":[{"StartTime":121520.0,"Position":188.0,"HyperDash":false},{"StartTime":121606.0,"Position":184.631577,"HyperDash":true}]},{"StartTime":121693.0,"Objects":[{"StartTime":121693.0,"Position":400.0,"HyperDash":false},{"StartTime":121779.0,"Position":346.0,"HyperDash":true}]},{"StartTime":121865.0,"Objects":[{"StartTime":121865.0,"Position":128.0,"HyperDash":false},{"StartTime":121951.0,"Position":124.407974,"HyperDash":true}]},{"StartTime":122037.0,"Objects":[{"StartTime":122037.0,"Position":336.0,"HyperDash":false},{"StartTime":122123.0,"Position":282.0,"HyperDash":true}]},{"StartTime":122210.0,"Objects":[{"StartTime":122210.0,"Position":484.0,"HyperDash":false},{"StartTime":122296.0,"Position":486.696625,"HyperDash":true}]},{"StartTime":122382.0,"Objects":[{"StartTime":122382.0,"Position":272.0,"HyperDash":false},{"StartTime":122468.0,"Position":326.0,"HyperDash":true}]},{"StartTime":122555.0,"Objects":[{"StartTime":122555.0,"Position":108.0,"HyperDash":false},{"StartTime":122641.0,"Position":54.0,"HyperDash":true}]},{"StartTime":122727.0,"Objects":[{"StartTime":122727.0,"Position":280.0,"HyperDash":false}]},{"StartTime":122813.0,"Objects":[{"StartTime":122813.0,"Position":347.0,"HyperDash":false}]},{"StartTime":122900.0,"Objects":[{"StartTime":122900.0,"Position":415.0,"HyperDash":false}]},{"StartTime":123072.0,"Objects":[{"StartTime":123072.0,"Position":256.0,"HyperDash":false}]},{"StartTime":123158.0,"Objects":[{"StartTime":123158.0,"Position":308.0,"HyperDash":false}]},{"StartTime":123244.0,"Objects":[{"StartTime":123244.0,"Position":360.0,"HyperDash":false}]},{"StartTime":123417.0,"Objects":[{"StartTime":123417.0,"Position":228.0,"HyperDash":false}]},{"StartTime":123503.0,"Objects":[{"StartTime":123503.0,"Position":260.0,"HyperDash":false}]},{"StartTime":123589.0,"Objects":[{"StartTime":123589.0,"Position":292.0,"HyperDash":false}]},{"StartTime":123762.0,"Objects":[{"StartTime":123762.0,"Position":188.0,"HyperDash":false}]},{"StartTime":123848.0,"Objects":[{"StartTime":123848.0,"Position":196.0,"HyperDash":false}]},{"StartTime":123934.0,"Objects":[{"StartTime":123934.0,"Position":204.0,"HyperDash":false}]},{"StartTime":124106.0,"Objects":[{"StartTime":124106.0,"Position":311.0,"HyperDash":false},{"StartTime":124170.0,"Position":216.0,"HyperDash":false},{"StartTime":124235.0,"Position":310.0,"HyperDash":false},{"StartTime":124299.0,"Position":397.0,"HyperDash":false},{"StartTime":124364.0,"Position":214.0,"HyperDash":false},{"StartTime":124429.0,"Position":505.0,"HyperDash":false},{"StartTime":124493.0,"Position":173.0,"HyperDash":false},{"StartTime":124558.0,"Position":295.0,"HyperDash":false},{"StartTime":124623.0,"Position":199.0,"HyperDash":false},{"StartTime":124687.0,"Position":494.0,"HyperDash":false},{"StartTime":124752.0,"Position":293.0,"HyperDash":false},{"StartTime":124817.0,"Position":115.0,"HyperDash":false},{"StartTime":124881.0,"Position":412.0,"HyperDash":false},{"StartTime":124946.0,"Position":506.0,"HyperDash":false},{"StartTime":125011.0,"Position":293.0,"HyperDash":false},{"StartTime":125075.0,"Position":346.0,"HyperDash":false},{"StartTime":125140.0,"Position":117.0,"HyperDash":false},{"StartTime":125205.0,"Position":285.0,"HyperDash":false},{"StartTime":125269.0,"Position":17.0,"HyperDash":false},{"StartTime":125334.0,"Position":238.0,"HyperDash":false},{"StartTime":125399.0,"Position":222.0,"HyperDash":false},{"StartTime":125463.0,"Position":450.0,"HyperDash":false},{"StartTime":125528.0,"Position":67.0,"HyperDash":false},{"StartTime":125593.0,"Position":219.0,"HyperDash":false},{"StartTime":125657.0,"Position":307.0,"HyperDash":false},{"StartTime":125722.0,"Position":367.0,"HyperDash":false},{"StartTime":125787.0,"Position":412.0,"HyperDash":false},{"StartTime":125851.0,"Position":413.0,"HyperDash":false},{"StartTime":125916.0,"Position":143.0,"HyperDash":false},{"StartTime":125981.0,"Position":339.0,"HyperDash":false},{"StartTime":126045.0,"Position":342.0,"HyperDash":false},{"StartTime":126110.0,"Position":249.0,"HyperDash":false},{"StartTime":126175.0,"Position":235.0,"HyperDash":false},{"StartTime":126239.0,"Position":323.0,"HyperDash":false},{"StartTime":126304.0,"Position":365.0,"HyperDash":false},{"StartTime":126368.0,"Position":74.0,"HyperDash":false},{"StartTime":126433.0,"Position":281.0,"HyperDash":false},{"StartTime":126498.0,"Position":398.0,"HyperDash":false},{"StartTime":126562.0,"Position":335.0,"HyperDash":false},{"StartTime":126627.0,"Position":388.0,"HyperDash":false},{"StartTime":126692.0,"Position":228.0,"HyperDash":false},{"StartTime":126756.0,"Position":323.0,"HyperDash":false},{"StartTime":126821.0,"Position":441.0,"HyperDash":false},{"StartTime":126886.0,"Position":442.0,"HyperDash":false},{"StartTime":126950.0,"Position":278.0,"HyperDash":false},{"StartTime":127015.0,"Position":90.0,"HyperDash":false},{"StartTime":127080.0,"Position":409.0,"HyperDash":false},{"StartTime":127144.0,"Position":377.0,"HyperDash":false},{"StartTime":127209.0,"Position":457.0,"HyperDash":false},{"StartTime":127274.0,"Position":409.0,"HyperDash":false},{"StartTime":127338.0,"Position":43.0,"HyperDash":false},{"StartTime":127403.0,"Position":162.0,"HyperDash":false},{"StartTime":127468.0,"Position":341.0,"HyperDash":false},{"StartTime":127532.0,"Position":72.0,"HyperDash":false},{"StartTime":127597.0,"Position":135.0,"HyperDash":false},{"StartTime":127662.0,"Position":252.0,"HyperDash":false},{"StartTime":127726.0,"Position":446.0,"HyperDash":false},{"StartTime":127791.0,"Position":284.0,"HyperDash":false},{"StartTime":127856.0,"Position":70.0,"HyperDash":false},{"StartTime":127920.0,"Position":494.0,"HyperDash":false},{"StartTime":127985.0,"Position":463.0,"HyperDash":false},{"StartTime":128050.0,"Position":277.0,"HyperDash":false},{"StartTime":128114.0,"Position":425.0,"HyperDash":false},{"StartTime":128179.0,"Position":281.0,"HyperDash":false},{"StartTime":128244.0,"Position":3.0,"HyperDash":false},{"StartTime":128308.0,"Position":346.0,"HyperDash":false},{"StartTime":128373.0,"Position":350.0,"HyperDash":false},{"StartTime":128437.0,"Position":217.0,"HyperDash":false},{"StartTime":128502.0,"Position":455.0,"HyperDash":false},{"StartTime":128567.0,"Position":229.0,"HyperDash":false},{"StartTime":128631.0,"Position":51.0,"HyperDash":false},{"StartTime":128696.0,"Position":199.0,"HyperDash":false},{"StartTime":128761.0,"Position":208.0,"HyperDash":false},{"StartTime":128825.0,"Position":173.0,"HyperDash":false},{"StartTime":128890.0,"Position":367.0,"HyperDash":false},{"StartTime":128955.0,"Position":193.0,"HyperDash":false},{"StartTime":129019.0,"Position":488.0,"HyperDash":false},{"StartTime":129084.0,"Position":314.0,"HyperDash":false},{"StartTime":129149.0,"Position":135.0,"HyperDash":false},{"StartTime":129213.0,"Position":399.0,"HyperDash":false},{"StartTime":129278.0,"Position":404.0,"HyperDash":false},{"StartTime":129343.0,"Position":152.0,"HyperDash":false},{"StartTime":129407.0,"Position":353.0,"HyperDash":false},{"StartTime":129472.0,"Position":358.0,"HyperDash":false},{"StartTime":129537.0,"Position":447.0,"HyperDash":false},{"StartTime":129601.0,"Position":222.0,"HyperDash":false},{"StartTime":129666.0,"Position":382.0,"HyperDash":false},{"StartTime":129731.0,"Position":433.0,"HyperDash":false},{"StartTime":129795.0,"Position":450.0,"HyperDash":false},{"StartTime":129860.0,"Position":326.0,"HyperDash":false},{"StartTime":129925.0,"Position":414.0,"HyperDash":false},{"StartTime":129989.0,"Position":285.0,"HyperDash":false},{"StartTime":130054.0,"Position":336.0,"HyperDash":false},{"StartTime":130119.0,"Position":509.0,"HyperDash":false},{"StartTime":130183.0,"Position":334.0,"HyperDash":false},{"StartTime":130248.0,"Position":72.0,"HyperDash":false},{"StartTime":130313.0,"Position":425.0,"HyperDash":false},{"StartTime":130377.0,"Position":451.0,"HyperDash":false},{"StartTime":130442.0,"Position":220.0,"HyperDash":false},{"StartTime":130506.0,"Position":25.0,"HyperDash":false},{"StartTime":130571.0,"Position":77.0,"HyperDash":false},{"StartTime":130636.0,"Position":509.0,"HyperDash":false},{"StartTime":130700.0,"Position":90.0,"HyperDash":false},{"StartTime":130765.0,"Position":118.0,"HyperDash":false},{"StartTime":130830.0,"Position":58.0,"HyperDash":false},{"StartTime":130894.0,"Position":12.0,"HyperDash":false},{"StartTime":130959.0,"Position":215.0,"HyperDash":false},{"StartTime":131024.0,"Position":487.0,"HyperDash":false},{"StartTime":131088.0,"Position":446.0,"HyperDash":false},{"StartTime":131153.0,"Position":491.0,"HyperDash":false},{"StartTime":131218.0,"Position":459.0,"HyperDash":false},{"StartTime":131282.0,"Position":37.0,"HyperDash":false},{"StartTime":131347.0,"Position":291.0,"HyperDash":false},{"StartTime":131412.0,"Position":315.0,"HyperDash":false},{"StartTime":131476.0,"Position":35.0,"HyperDash":false},{"StartTime":131541.0,"Position":208.0,"HyperDash":false},{"StartTime":131606.0,"Position":504.0,"HyperDash":false},{"StartTime":131670.0,"Position":296.0,"HyperDash":false},{"StartTime":131735.0,"Position":105.0,"HyperDash":false},{"StartTime":131800.0,"Position":488.0,"HyperDash":false},{"StartTime":131864.0,"Position":230.0,"HyperDash":false},{"StartTime":131929.0,"Position":446.0,"HyperDash":false},{"StartTime":131994.0,"Position":241.0,"HyperDash":false},{"StartTime":132058.0,"Position":413.0,"HyperDash":false},{"StartTime":132123.0,"Position":357.0,"HyperDash":false},{"StartTime":132188.0,"Position":256.0,"HyperDash":false},{"StartTime":132252.0,"Position":192.0,"HyperDash":false},{"StartTime":132317.0,"Position":116.0,"HyperDash":false},{"StartTime":132382.0,"Position":397.0,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2781126.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2781126.osu new file mode 100644 index 0000000000..af7cd296d7 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/2781126.osu @@ -0,0 +1,908 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 +Mode: 2 + +[Difficulty] +HPDrainRate:6 +CircleSize:4.5 +OverallDifficulty:9.5 +ApproachRate:9.5 +SliderMultiplier:1.8 +SliderTickRate:2 + +[Events] +//Background and Video events +//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] +-31,344.827586206897,4,2,1,15,1,0 +486,-100,4,2,1,50,0,0 +658,-100,4,2,1,55,0,0 +831,-100,4,2,1,60,0,0 +1003,-100,4,2,1,65,0,0 +1175,-100,4,2,1,5,0,0 +1348,-100,4,2,1,80,0,0 +11089,-100,4,2,2,40,0,0 +11175,-100,4,2,2,45,0,0 +11262,-100,4,2,2,50,0,0 +11348,-100,4,2,2,55,0,0 +11434,-100,4,2,2,60,0,0 +11520,-100,4,2,2,65,0,0 +11606,-100,4,2,2,70,0,0 +11693,-100,4,2,1,75,0,0 +11865,-100,4,2,1,70,0,0 +12037,-80,4,2,1,75,0,0 +12296,-100,4,2,1,100,0,0 +12382,-100,4,2,1,85,0,0 +20658,-83.3333333333333,4,2,1,85,0,0 +21175,-100,4,2,1,85,0,0 +22037,-100,4,2,1,80,0,0 +22210,-100,4,2,2,50,0,0 +22727,-100,4,2,1,80,0,0 +23072,-66.6666666666667,4,2,1,100,0,0 +23331,-100,4,2,1,100,0,0 +23417,-80,4,2,1,75,0,0 +23762,-100,4,2,1,100,0,0 +34451,-66.6666666666667,4,2,1,80,0,0 +34624,-100,4,2,1,80,0,0 +39969,-80,4,2,1,80,0,0 +40658,-80,4,2,1,100,0,0 +41348,-66.6666666666667,4,2,1,100,0,0 +42037,-100,4,2,1,100,0,0 +44106,-66.6666666666667,4,2,1,70,0,0 +44279,-100,4,2,1,100,0,0 +44796,-66.6666666666667,4,2,1,100,0,0 +46175,-66.6666666666667,4,2,1,90,0,1 +46348,-100,4,2,1,90,0,1 +51348,-33.3333333333333,4,2,1,90,0,1 +51520,-66.6666666666667,4,2,1,90,0,1 +51693,-100,4,2,1,100,0,1 +52037,-66.6666666666667,4,2,1,100,0,1 +52727,-100,4,2,1,100,0,1 +53072,-40,4,2,1,100,0,1 +53762,-100,4,2,1,100,0,1 +55141,-66.6666666666667,4,2,1,100,0,1 +55371,-100,4,2,1,100,0,1 +56520,-66.6666666666667,4,2,1,90,0,0 +56606,-66.6666666666667,4,2,2,47,0,0 +56693,-66.6666666666667,4,2,1,54,0,0 +56779,-66.6666666666667,4,2,2,61,0,0 +56865,-66.6666666666667,4,2,1,68,0,0 +56951,-66.6666666666667,4,2,2,75,0,0 +57037,-66.6666666666667,4,2,1,81,0,0 +57124,-66.6666666666667,4,2,2,88,0,0 +57210,-100,4,2,1,90,0,0 +57900,-66.6666666666667,4,2,1,90,0,1 +58072,-100,4,2,1,90,0,1 +58244,-80,4,2,1,90,0,1 +58589,-100,4,2,1,90,0,1 +61175,-66.6666666666667,4,2,1,90,0,1 +61348,-100,4,2,1,90,0,1 +62382,-33.3333333333333,4,2,1,100,0,1 +62555,-66.6666666666667,4,2,1,100,0,1 +62770,-100,4,2,1,100,0,1 +64106,-40,4,2,1,100,0,1 +64796,-100,4,2,1,100,0,1 +66175,-100,4,2,1,80,0,0 +68417,-66.6666666666667,4,2,1,80,0,0 +68589,-100,4,2,1,80,0,0 +68934,-100,4,2,1,70,0,1 +69020,-100,4,2,1,10,0,0 +71003,-100,4,2,1,15,0,0 +71693,-100,4,2,1,20,0,0 +71865,-100,4,2,1,23,0,0 +72037,-100,4,2,1,26,0,0 +72210,-100,4,2,1,29,0,0 +72382,-100,4,2,1,32,0,0 +72555,-100,4,2,1,35,0,0 +72727,-100,4,2,1,38,0,0 +72900,-100,4,2,1,41,0,0 +73072,-100,4,2,1,44,0,0 +73244,-100,4,2,1,47,0,0 +73417,-100,4,2,1,50,0,0 +73589,-100,4,2,1,53,0,0 +73762,-100,4,2,1,56,0,0 +73934,-100,4,2,1,59,0,0 +74106,-100,4,2,1,62,0,0 +74279,-100,4,2,1,65,0,0 +74451,-100,4,2,1,40,0,0 +74624,-133.333333333333,4,2,1,40,0,0 +77210,-100,4,2,1,40,0,0 +77555,-133.333333333333,4,2,1,40,0,0 +79969,-100,4,2,1,40,0,0 +80313,-133.333333333333,4,2,1,40,0,0 +81348,-100,4,2,1,40,0,0 +81692,-133.333333333333,4,2,1,40,0,0 +82727,-86.9565217391304,4,2,1,40,0,0 +83072,-133.333333333333,4,2,1,40,0,0 +84106,-100,4,2,1,40,0,0 +84450,-133.333333333333,4,2,1,40,0,0 +85486,-100,4,2,2,50,0,0 +96175,-50,4,2,2,60,0,0 +96520,-50,4,2,1,55,0,0 +96822,-50,4,2,1,5,0,0 +96865,-66.6666666666667,4,2,1,50,0,0 +97210,-66.6666666666667,4,2,1,45,0,0 +97512,-66.6666666666667,4,2,1,5,0,0 +97555,-100,4,2,1,40,0,0 +97900,-100,4,2,1,35,0,0 +98244,-100,4,2,1,5,0,0 +99279,-100,4,2,1,75,0,0 +99624,-66.6666666666667,4,2,1,75,0,0 +99796,-66.6666666666667,4,2,1,75,0,0 +100313,-100,4,2,1,75,0,0 +100658,-83.3333333333333,4,2,1,90,0,1 +111578,-83.3333333333333,4,2,1,90,0,0 +111693,-83.3333333333333,4,2,1,90,0,1 +119279,-66.6666666666667,4,2,1,90,0,1 +119969,-83.3333333333333,4,2,1,90,0,1 +121003,-50,4,2,1,90,0,1 +121175,-83.3333333333333,4,2,1,90,0,1 +122727,-100,4,2,1,90,0,0 +123072,-100,4,2,1,50,0,0 +123417,-100,4,2,1,46,0,0 +123762,-100,4,2,1,42,0,0 +124106,-100,4,2,1,38,0,0 +124451,-100,4,2,1,35,0,0 +124796,-100,4,2,1,32,0,0 +125141,-100,4,2,1,29,0,0 +125486,-100,4,2,1,26,0,0 +125831,-100,4,2,1,24,0,0 +126175,-100,4,2,1,22,0,0 +126520,-100,4,2,1,20,0,0 +126865,-100,4,2,1,18,0,0 +127210,-100,4,2,1,16,0,0 +127555,-100,4,2,1,14,0,0 +127900,-100,4,2,1,12,0,0 +128244,-100,4,2,1,10,0,0 +128934,-100,4,2,1,9,0,0 +129624,-100,4,2,1,8,0,0 +130313,-100,4,2,1,7,0,0 +131003,-100,4,2,1,6,0,0 +131693,-100,4,2,1,5,0,0 + +[HitObjects] +256,192,313,12,2,1175,0:0:0:0: +224,192,1348,5,6,3:2:0:0: +177,157,1434,1,0,0:0:0:0: +179,100,1520,1,2,0:2:0:0: +227,68,1606,1,2,0:2:0:0: +292,68,1693,2,0,L|296:12,1,45,2|0,0:0|0:0,0:0:0:0: +116,192,1865,2,0,B|116:280|116:280|208:296,1,180,0|2,3:0|0:1,0:0:0:0: +116,280,2296,1,0,0:0:0:0: +26,264,2382,2,0,L|22:160,1,90,0|2,3:2|0:3,0:0:0:0: +292,192,2727,6,0,L|384:192,1,90,2|2,3:2|0:3,0:0:0:0: +328,192,2986,1,2,0:3:0:0: +276,192,3072,1,2,0:0:0:0: +448,192,3244,1,0,3:0:0:0: +268,96,3417,2,0,B|176:96,1,90,0|2,0:0|0:3,0:0:0:0: +244,96,3675,1,2,0:3:0:0: +178,96,3762,2,0,L|82:96,1,90,0|2,3:0|0:3,0:0:0:0: +444,304,4106,6,0,L|448:256,1,45,2|2,3:2|0:2,0:0:0:0: +376,256,4279,2,0,L|372:208,1,45,2|2,0:2|0:2,0:0:0:0: +300,192,4451,1,2,0:0:0:0: +472,136,4624,2,0,L|476:84,1,45,0|0,3:0|0:0,0:0:0:0: +296,28,4796,2,0,P|264:72|280:108,1,90,0|2,0:0|0:1,0:0:0:0: +366,152,5055,1,0,0:0:0:0: +456,211,5141,2,0,L|352:211,1,90,0|2,3:2|0:3,0:0:0:0: +112,192,5486,6,0,L|208:192,1,90,2|2,3:2|0:3,0:0:0:0: +268,192,5744,1,2,0:3:0:0: +202,192,5831,1,2,0:0:0:0: +360,192,6003,1,0,3:0:0:0: +192,284,6175,2,0,L|100:284,1,90,0|2,0:0|0:3,0:0:0:0: +172,284,6434,1,2,0:3:0:0: +102,284,6520,2,0,L|10:284,1,90,0|2,3:0|0:3,0:0:0:0: +288,284,6865,5,2,3:2:0:0: +335,249,6951,1,2,0:2:0:0: +333,192,7037,1,2,0:2:0:0: +285,160,7124,1,2,0:2:0:0: +220,160,7210,2,0,L|216:104,1,45,2|0,0:0|0:0,0:0:0:0: +320,56,7382,1,0,3:0:0:0: +204,56,7555,1,0,0:0:0:0: +456,52,7727,1,2,0:1:0:0: +460,104,7813,1,0,0:0:0:0: +464,160,7900,2,0,L|372:160,1,90,0|2,3:2|0:3,0:0:0:0: +120,160,8244,6,0,B|212:160,1,90,2|2,3:2|0:3,0:0:0:0: +280,160,8503,1,2,0:3:0:0: +348,160,8589,1,2,0:0:0:0: +176,160,8762,1,0,3:0:0:0: +354,160,8934,2,0,L|446:160,1,90,0|2,0:0|0:3,0:0:0:0: +374,160,9193,1,2,0:3:0:0: +306,160,9279,2,0,L|406:160,1,90,0|2,3:0|0:3,0:0:0:0: +148,56,9624,6,0,L|100:44,1,45,2|2,3:2|0:2,0:0:0:0: +176,120,9796,2,0,L|224:108,1,45,2|2,0:2|0:2,0:0:0:0: +148,56,9969,1,2,0:0:0:0: +308,56,10141,1,0,3:0:0:0: +140,120,10313,1,0,0:0:0:0: +396,192,10486,2,0,L|440:192,2,45,2|0|0,0:1|0:0|3:2,0:0:0:0: +228,192,10831,1,2,0:3:0:0: +460,312,11003,6,0,L|484:270,1,45,0|2,3:3|0:3,0:0:0:0: +392,288,11175,2,0,L|416:246,1,45,2|2,0:3|0:3,0:0:0:0: +324,264,11348,2,0,L|347:222,1,45,2|2,0:3|0:3,0:0:0:0: +260,232,11520,2,0,L|284:190,1,45,2|2,0:3|0:3,0:0:0:0: +384,192,11693,1,0,3:0:0:0: +220,188,11865,2,0,L|156:188,1,45,8|0,0:2|3:0,0:0:0:0: +400,192,12037,2,0,B|488:192|488:192|488:108,1,168.75,8|0,3:2|0:0,0:0:0:0: +284,56,12382,6,0,L|192:56,1,90,6|2,3:2|0:2,0:0:0:0: +264,56,12641,1,2,0:2:0:0: +436,56,12727,1,10,3:2:0:0: +328,56,12900,2,0,L|324:112,1,45,0|0,3:0|0:0,3:3:0:0: +424,112,13072,2,0,L|428:216,1,90,0|2,0:0|0:1,0:0:0:0: +360,200,13331,1,2,0:3:0:0: +208,200,13417,2,0,L|116:200,1,90,8|2,3:2|0:3,0:0:0:0: +292,200,13762,6,0,L|296:292,1,90,2|2,3:2|0:3,0:0:0:0: +228,292,14020,1,2,0:3:0:0: +408,288,14106,2,0,L|508:288,1,90,10|0,3:2|3:0,0:0:0:0: +228,192,14451,2,0,L|324:192,1,90,8|2,3:2|3:3,0:0:0:0: +48,192,14796,2,0,L|140:192,1,90,8|2,3:2|0:3,0:0:0:0: +392,192,15141,6,0,L|396:132,1,45,2|2,3:2|0:2,0:0:0:0: +320,120,15313,2,0,L|316:60,1,45,2|2,0:2|0:2,0:0:0:0: +488,60,15486,1,10,3:2:0:0: +388,60,15658,2,0,L|332:60,1,45,0|0,3:0|0:0,3:3:0:0: +240,60,15831,2,0,L|236:152,1,90,0|2,0:0|0:1,0:0:0:0: +304,152,16089,1,2,0:3:0:0: +132,152,16175,2,0,L|36:152,1,90,8|2,3:2|0:3,0:0:0:0: +312,256,16520,6,0,L|216:256,1,90,10|2,3:2|0:3,0:0:0:0: +152,256,16779,1,2,0:3:0:0: +328,328,16865,2,0,L|236:328,1,90,10|0,3:2|3:0,0:0:0:0: +328,328,17210,1,0,0:0:0:0: +164,328,17382,2,0,L|160:276,1,45,2|2,0:3|0:3,0:0:0:0: +336,240,17555,2,0,L|440:240,1,90,8|2,3:2|0:3,0:0:0:0: +152,56,17900,5,10,3:2:0:0: +155,114,17986,1,2,0:2:0:0: +192,160,18072,1,2,0:2:0:0: +252,168,18158,1,2,0:2:0:0: +404,168,18244,2,0,L|408:72,1,90,10|2,0:2|3:2,0:0:0:0: +156,232,18589,2,0,L|64:232,1,90,8|2,0:0|0:3,0:0:0:0: +136,232,18848,1,2,0:3:0:0: +304,232,18934,2,0,L|396:232,1,90,8|0,3:2|0:3,0:0:0:0: +120,76,19279,6,0,P|100:120|120:168,1,90,8|0,3:2|0:0,0:0:0:0: +180,160,19537,1,0,0:0:0:0: +360,160,19624,2,0,L|268:160,1,90,8|0,0:0|3:0,0:0:0:0: +32,316,19969,2,0,L|132:316,1,90,8|2,3:2|0:3,0:0:0:0: +188,316,20227,1,0,0:0:0:0: +16,232,20313,2,0,L|116:232,1,90,8|2,3:2|0:3,0:0:0:0: +368,232,20658,6,0,L|256:232,3,107.999996704102,10|10|10|8,3:2|0:2|3:2|3:2,0:0:0:0: +496,232,21348,1,8,0:0:0:0: +324,232,21520,1,8,3:2:0:0: +496,232,21693,1,8,3:2:0:0: +388,232,21865,2,0,L|332:232,1,45,8|8,3:2|3:2,0:0:0:0: +144,232,22037,5,2,3:2:0:0: +252,232,22210,2,0,L|232:192,1,45,2|0,3:0|0:0,0:0:0:0: +312,164,22382,2,0,L|292:124,1,45,2|0,3:0|0:0,0:0:0:0: +372,96,22555,2,0,L|352:56,1,45,2|0,3:0|0:0,0:0:0:0: +180,56,22727,2,0,L|276:56,1,90,2|8,3:2|0:0,0:0:0:0: +208,56,22986,1,0,3:0:0:0: +436,56,23072,2,0,P|504:104|436:168,1,202.500007724762,8|0,3:2|0:0,0:0:0:0: +208,192,23417,6,0,L|92:192,2,112.5,6|2|2,3:2|0:0|0:0,0:0:0:0: +312,192,23934,1,2,0:0:0:0: +220,192,24020,1,2,0:0:0:0: +128,192,24106,2,0,L|220:192,1,90,2|2,0:0|0:1,0:0:0:0: +392,192,24451,1,0,3:0:0:0: +444,176,24537,1,2,0:0:0:0: +444,120,24624,1,2,0:0:0:0: +392,100,24710,1,2,0:0:0:0: +212,276,24796,6,0,L|308:276,2,90,2|2|2,0:2|0:2|0:2,0:0:0:0: +320,276,25313,1,2,0:2:0:0: +384,276,25400,1,2,0:2:0:0: +284,352,25486,2,0,L|192:352,1,90,2|2,0:2|0:0,0:0:0:0: +448,276,25831,2,0,L|444:224,1,45,2|2,3:2|0:2,0:0:0:0: +344,192,26003,2,0,L|300:192,1,45,2|2,0:2|0:2,0:0:0:0: +128,192,26175,6,0,L|28:192,2,90,2|2|2,3:2|0:0|0:0,0:0:0:0: +236,192,26693,1,2,0:0:0:0: +299,192,26779,1,2,0:0:0:0: +362,192,26865,1,2,0:0:0:0: +196,192,27037,1,2,0:1:0:0: +352,192,27210,1,2,3:2:0:0: +352,128,27296,1,2,0:2:0:0: +312,80,27382,1,2,0:2:0:0: +248,80,27469,1,2,0:2:0:0: +412,80,27555,6,0,L|320:80,2,90,2|2|2,0:2|0:0|0:0,0:0:0:0: +304,80,28072,1,2,0:0:0:0: +396,80,28158,1,2,0:0:0:0: +488,80,28244,2,0,L|396:80,1,90,2|2,0:0|0:0,0:0:0:0: +88,80,28589,1,8,3:2:0:0: +340,80,28934,6,0,L|344:172,1,90,2|2,3:2|0:2,0:0:0:0: +172,192,29279,2,0,L|168:292,1,90,2|2,3:2|0:2,0:0:0:0: +268,284,29537,1,2,0:2:0:0: +368,284,29624,2,0,L|268:284,1,90,2|2,3:2|0:1,0:0:0:0: +452,284,29969,2,0,L|460:236,2,45,0|0|2,3:2|0:0|0:2,0:0:0:0: +200,372,30313,6,0,L|196:280,1,90,2|2,3:2|0:2,0:0:0:0: +368,160,30658,2,0,L|264:160,1,90,2|2,3:2|0:2,0:0:0:0: +380,160,30917,1,2,0:2:0:0: +480,160,31003,2,0,L|374:160,1,90,2|2,3:2|0:2,0:0:0:0: +128,192,31348,2,0,L|124:140,1,45,2|2,3:2|0:2,0:0:0:0: +228,104,31520,2,0,L|292:104,1,45,2|2,0:2|0:2,0:0:0:0: +88,148,31693,6,0,L|84:252,1,90,2|2,3:2|0:2,0:0:0:0: +256,236,32037,2,0,L|352:236,1,90,2|2,3:2|0:2,0:0:0:0: +246,236,32296,1,2,0:2:0:0: +148,236,32382,2,0,L|48:236,1,90,2|2,3:2|0:2,0:0:0:0: +232,68,32727,1,0,3:2:0:0: +180,68,32813,1,0,0:0:0:0: +124,68,32900,1,2,0:2:0:0: +376,68,33072,6,0,L|476:68,1,90,2|2,3:2|0:2,0:0:0:0: +300,192,33417,2,0,L|396:192,1,90,2|2,3:2|0:2,0:0:0:0: +220,192,33762,2,0,L|128:192,1,90,2|2,3:2|0:2,0:0:0:0: +416,192,34106,2,0,L|448:192,7,22.5,0|0|0|0|0|0|0|0,3:0|3:0|3:0|3:0|3:0|3:0|3:0|0:0,0:0:0:0: +265,192,34451,6,0,L|129:192,1,135.000005149842,10|2,3:2|0:2,0:0:0:0: +300,192,34796,2,0,L|304:97,1,90,2|2,3:2|0:2,0:0:0:0: +140,100,35141,1,10,3:2:0:0: +376,100,35313,1,2,0:1:0:0: +268,100,35486,2,0,L|264:196,1,90,0|2,3:2|0:2,0:0:0:0: +496,192,35831,6,0,L|404:192,1,90,10|2,3:2|0:2,0:0:0:0: +236,192,36175,2,0,L|140:192,1,90,2|2,3:2|0:2,0:0:0:0: +400,256,36520,1,10,3:2:0:0: +236,256,36693,1,2,0:2:0:0: +476,256,36865,1,2,3:2:0:0: +476,322,36951,1,0,0:0:0:0: +434,372,37037,1,2,0:2:0:0: +369,383,37124,1,0,0:0:0:0: +196,384,37210,6,0,L|104:384,1,90,10|2,3:2|0:2,0:0:0:0: +272,384,37555,2,0,L|368:384,1,90,2|2,3:2|0:2,0:0:0:0: +196,384,37900,1,10,3:2:0:0: +432,384,38072,1,2,0:1:0:0: +324,384,38244,1,0,3:2:0:0: +272,384,38331,1,0,0:0:0:0: +224,384,38417,1,2,0:2:0:0: +488,384,38589,6,0,L|490:281,1,90,10|2,3:2|0:2,0:0:0:0: +324,296,38934,2,0,L|328:188,1,90,2|2,3:2|0:2,0:0:0:0: +88,204,39279,1,10,3:2:0:0: +256,204,39451,1,2,0:2:0:0: +16,204,39624,1,10,3:2:0:0: +428,208,39969,6,0,P|480:152|428:92,1,168.75,8|2,3:2|0:0,0:0:0:0: +328,92,40313,2,0,P|256:120|240:204,1,168.75,10|0,3:2|0:0,0:0:0:0: +412,208,40658,2,0,B|496:208|496:208|500:296,1,168.75,8|0,3:2|0:0,0:0:0:0: +272,376,41003,2,0,B|272:292|272:292|360:288,1,168.75,8|0,3:2|0:0,0:0:0:0: +116,296,41348,6,0,P|52:224|120:176,1,202.500007724762,8|0,3:2|0:0,0:0:0:0: +340,176,41693,2,0,L|132:176,1,202.500007724762,8|0,3:2|0:0,0:0:0:0: +312,96,42037,1,8,3:2:0:0: +164,96,42210,1,8,3:2:0:0: +324,96,42382,1,8,3:2:0:0: +152,96,42555,1,8,3:2:0:0: +404,96,42727,5,8,3:2:0:0: +460,128,42813,1,8,3:2:0:0: +460,192,42900,1,8,3:2:0:0: +404,224,42986,1,8,3:2:0:0: +208,192,43072,2,0,L|204:244,1,45,8|8,3:2|3:2,0:0:0:0: +280,240,43244,2,0,L|284:292,1,45,8|8,3:2|3:2,0:0:0:0: +104,328,43417,6,8,L|48:328,1,45,8|8,3:2|3:2,0:0:0:0: +240,364,43589,2,8,L|300:364,1,45,8|8,3:2|3:2,0:0:0:0: +80,192,43762,2,0,L|136:192,1,45,8|8,3:2|3:2,0:0:0:0: +372,224,43934,2,0,L|316:224,1,45,8|8,3:2|3:2,0:0:0:0: +124,44,44106,5,8,3:2:0:0: +368,44,44279,1,2,0:0:0:0: +116,44,44451,2,0,L|64:44,1,45,2|2,0:0|0:0,0:0:0:0: +172,116,44624,2,0,L|112:116,1,45,2|2,0:0|0:0,0:0:0:0: +300,116,44796,2,0,L|476:116,1,168.750006437302,2|2,0:0|0:0,0:0:0:0: +260,192,45141,2,0,L|428:192,1,168.750006437302,2|2,0:0|0:0,0:0:0:0: +176,328,45486,5,2,3:2:0:0: +158,322,45507,1,0,0:0:0:0: +143,313,45529,1,0,0:0:0:0: +129,301,45550,1,0,0:0:0:0: +119,287,45572,1,0,0:0:0:0: +111,270,45594,1,0,0:0:0:0: +108,253,45615,1,0,0:0:0:0: +108,235,45637,1,0,0:0:0:0: +112,217,45658,1,0,0:0:0:0: +120,201,45680,1,0,0:0:0:0: +131,187,45701,1,0,0:0:0:0: +145,175,45723,1,0,0:0:0:0: +161,167,45744,1,0,0:0:0:0: +178,162,45766,1,0,0:0:0:0: +196,161,45787,1,0,0:0:0:0: +214,164,45809,1,0,0:0:0:0: +240,168,45831,1,0,0:0:0:0: +257,167,45852,1,0,0:0:0:0: +275,162,45874,1,0,0:0:0:0: +291,153,45895,1,0,0:0:0:0: +304,142,45917,1,0,0:0:0:0: +315,128,45938,1,0,0:0:0:0: +323,111,45960,1,0,0:0:0:0: +327,94,45981,1,0,0:0:0:0: +327,76,46003,1,0,0:0:0:0: +324,58,46025,1,0,0:0:0:0: +317,42,46046,1,0,0:0:0:0: +306,27,46068,1,0,0:0:0:0: +293,16,46089,1,0,0:0:0:0: +277,7,46111,1,0,0:0:0:0: +260,1,46132,1,0,0:0:0:0: +76,52,46175,6,0,B|8:52|8:52|80:52,1,135.000005149842,4|2,3:2|0:0,0:0:0:0: +120,52,46434,1,2,0:0:0:0: +280,52,46520,2,0,L|376:52,1,90,8|2,3:2|0:0,0:0:0:0: +324,52,46779,1,2,0:0:0:0: +152,136,46865,2,0,L|96:136,1,45,2|2,3:2|0:0,0:0:0:0: +172,208,47037,2,0,L|112:208,1,45,2|2,0:0|0:0,0:0:0:0: +336,192,47210,1,8,3:2:0:0: +363,202,47253,1,0,0:0:0:0: +384,224,47296,1,0,0:0:0:0: +393,252,47339,1,0,0:0:0:0: +389,282,47382,1,0,0:0:0:0: +372,306,47425,1,0,0:0:0:0: +347,322,47469,1,0,0:0:0:0: +168,324,47555,6,0,L|76:324,1,90,2|0,3:2|0:0,0:0:0:0: +244,208,47900,2,0,L|152:208,1,90,10|2,3:2|0:3,0:0:0:0: +400,208,48244,2,0,L|404:156,1,45,2|2,3:2|0:2,0:0:0:0: +312,76,48503,1,2,0:2:0:0: +140,76,48589,1,10,0:2:0:0: +248,76,48762,1,2,0:2:0:0: +16,76,48934,6,0,L|60:76,1,45,2|2,3:2|0:0,0:0:0:0: +160,76,49193,1,2,3:2:0:0: +16,76,49279,2,0,L|20:120,1,45,10|2,3:2|0:2,0:0:0:0: +76,164,49451,2,0,L|140:164,1,45,2|2,0:2|0:2,0:0:0:0: +304,192,49624,5,0,3:0:0:0: +317,209,49667,1,0,0:0:0:0: +326,230,49710,1,0,0:0:0:0: +328,252,49753,1,0,0:0:0:0: +325,274,49796,1,2,0:0:0:0: +316,295,49839,1,0,0:0:0:0: +301,312,49882,1,0,0:0:0:0: +120,312,49969,1,8,3:2:0:0: +52,312,50055,1,2,0:0:0:0: +120,312,50141,1,2,0:0:0:0: +288,312,50313,5,2,3:2:0:0: +332,273,50400,1,2,3:2:0:0: +328,215,50486,1,2,3:2:0:0: +280,184,50572,1,2,3:2:0:0: +104,92,50658,2,0,L|60:92,1,45,10|2,3:2|0:3,0:0:0:0: +104,184,50831,2,0,L|148:184,1,45,2|0,0:3|0:0,0:0:0:0: +328,215,51003,6,0,B|376:215|376:215|324:215,1,90,4|2,3:2|0:0,0:0:0:0: +280,215,51262,1,2,0:0:0:0: +128,296,51348,2,0,L|364:296,1,236.250009012223,8|0,3:2|0:0,0:0:0:0: +364,296,51520,2,0,L|224:296,1,135.000005149842,2|2,0:0|0:0,0:0:0:0: +368,144,51736,2,0,L|440:144,1,67.5,2|2,0:0|3:2,0:0:0:0: +380,144,51951,1,2,0:0:0:0: +204,64,52037,2,0,L|128:64,1,67.5000025749208,10|0,0:0|0:0,0:0:0:0: +223,64,52210,2,0,L|148:64,1,67.5000025749208,2|2,0:2|0:2,0:0:0:0: +388,240,52382,6,0,L|464:240,1,67.5000025749208,2|2,3:2|0:2,0:0:0:0: +368,144,52555,2,0,L|436:144,1,67.5000025749208,2|2,0:2|0:2,0:0:0:0: +224,144,52727,1,10,3:2:0:0: +194,150,52770,1,0,0:0:0:0: +169,165,52813,1,0,0:0:0:0: +149,188,52856,1,0,0:0:0:0: +137,215,52900,1,2,0:3:0:0: +134,245,52943,1,0,0:0:0:0: +141,274,52986,1,0,0:0:0:0: +368,348,53072,2,0,B|144:348,1,225,10|0,3:2|0:0,0:0:0:0: +444,272,53417,2,0,B|220:272,1,225,10|2,3:2|0:3,0:0:0:0: +488,184,53762,6,0,L|492:276,1,90,2|2,3:2|0:3,0:0:0:0: +336,184,54106,1,8,3:2:0:0: +280,184,54193,1,2,0:3:0:0: +228,184,54279,1,2,0:3:0:0: +392,276,54451,2,0,L|396:312,1,22.5,2|0,3:2|0:0,0:0:0:0: +188,328,54624,2,0,L|185:305,1,22.5,2|0,0:3|0:0,0:0:0:0: +408,108,54796,2,0,L|356:108,1,45,10|2,3:2|0:3,0:0:0:0: +136,176,54969,2,0,L|188:176,1,45,2|0,0:0|0:0,0:0:0:0: +384,192,55141,6,0,L|292:192,2,90.0000034332277,2|2|2,3:2|0:2|0:2,0:0:0:0: +172,272,55486,1,2,3:2:0:0: +280,272,55601,1,2,0:2:0:0: +388,272,55716,1,2,0:2:0:0: +164,192,55831,2,0,L|96:192,2,60,2|2|2,3:2|0:2|0:2,0:0:0:0: +340,192,56175,1,2,3:2:0:0: +412,192,56290,1,2,0:2:0:0: +412,120,56405,1,2,0:2:0:0: +212,120,56520,6,0,P|160:260|288:136,1,472.500018024445,0|0,3:2|0:0,0:0:0:0: +128,40,57210,5,2,3:2:0:0: +112,44,57231,1,0,0:0:0:0: +97,50,57253,1,0,0:0:0:0: +83,58,57275,1,0,0:0:0:0: +70,67,57296,1,0,0:0:0:0: +57,77,57318,1,0,0:0:0:0: +46,89,57339,1,0,0:0:0:0: +35,101,57361,1,0,0:0:0:0: +26,114,57382,1,0,0:0:0:0: +19,129,57404,1,0,0:0:0:0: +13,143,57425,1,0,0:0:0:0: +8,159,57447,1,0,0:0:0:0: +5,175,57469,1,0,0:0:0:0: +3,191,57490,1,0,0:0:0:0: +3,207,57512,1,0,0:0:0:0: +5,223,57533,1,0,0:0:0:0: +8,239,57555,1,0,0:0:0:0: +12,254,57576,1,0,0:0:0:0: +18,269,57598,1,0,0:0:0:0: +26,283,57619,1,0,0:0:0:0: +35,297,57641,1,0,0:0:0:0: +45,309,57662,1,0,0:0:0:0: +56,321,57684,1,0,0:0:0:0: +69,331,57706,1,0,0:0:0:0: +82,340,57727,1,0,0:0:0:0: +96,348,57749,1,0,0:0:0:0: +111,354,57770,1,0,0:0:0:0: +126,359,57792,1,0,0:0:0:0: +142,362,57813,1,0,0:0:0:0: +158,364,57835,1,0,0:0:0:0: +174,364,57856,1,0,0:0:0:0: +312,364,57900,6,0,L|448:364,1,135.000005149842,12|2,3:2|0:0,0:0:0:0: +392,364,58158,1,2,0:0:0:0: +216,192,58244,2,0,L|160:192,1,56.25,10|0,3:2|0:0,0:0:0:0: +232,124,58417,2,0,L|176:124,1,56.25,2|2,0:0|0:0,0:0:0:0: +20,192,58589,2,0,L|112:192,1,90,2|2,3:2|0:0,0:0:0:0: +276,264,58934,2,0,L|180:264,1,90,8|2,3:2|0:3,0:0:0:0: +440,264,59279,5,0,3:0:0:0: +466,250,59322,1,0,0:0:0:0: +484,226,59365,1,0,0:0:0:0: +491,198,59408,1,0,0:0:0:0: +484,168,59451,1,2,0:3:0:0: +428,128,59537,1,0,0:0:0:0: +260,128,59624,2,0,L|216:128,2,45,8|2|2,3:2|0:0|0:0,0:0:0:0: +494,129,59969,2,0,L|498:181,1,45,2|2,3:2|0:2,0:0:0:0: +392,260,60227,1,2,0:2:0:0: +212,260,60313,1,10,3:2:0:0: +356,260,60486,1,2,0:2:0:0: +104,64,60658,6,0,L|100:112,1,45,2|2,3:2|0:2,0:0:0:0: +204,192,60917,1,2,0:2:0:0: +384,128,61003,2,0,L|340:128,1,45,10|2,3:2|0:2,0:0:0:0: +159,192,61175,2,0,L|240:192,1,67.5000025749208,2|2,0:2|0:2,0:0:0:0: +72,192,61348,5,2,3:2:0:0: +9,228,61434,1,2,3:2:0:0: +9,300,61520,1,2,3:2:0:0: +70,336,61606,1,2,3:2:0:0: +250,272,61693,2,0,L|350:272,1,90,10|0,3:2|0:0,0:0:0:0: +184,215,62037,6,0,B|136:215|136:215|188:215,1,90,6|2,3:2|0:3,0:0:0:0: +232,215,62296,1,2,0:3:0:0: +384,296,62382,2,0,L|148:296,1,236.250009012223,8|0,0:0|0:0,0:0:0:0: +148,296,62555,2,0,L|288:296,1,135.000005149842,2|2,0:0|3:2,0:0:0:0: +144,144,62770,2,0,L|72:144,1,67.5,2|2,0:0|0:0,0:0:0:0: +132,144,62986,1,2,0:0:0:0: +300,64,63072,2,0,L|344:64,1,45,8|0,3:2|0:0,0:0:0:0: +184,192,63244,2,0,L|232:192,1,45,2|2,0:0|0:0,0:0:0:0: +64,64,63417,6,0,L|20:64,1,45,2|2,3:2|0:0,0:0:0:0: +184,192,63589,2,0,L|140:192,1,45,2|2,0:0|0:0,0:0:0:0: +345,64,63762,1,10,0:0:0:0: +375,70,63805,1,0,0:0:0:0: +400,85,63848,1,0,0:0:0:0: +420,108,63891,1,0,0:0:0:0: +432,135,63934,1,0,0:0:0:0: +435,165,63977,1,0,0:0:0:0: +428,194,64020,1,0,0:0:0:0: +224,344,64106,2,0,B|448:344,1,225,8|2,3:2|3:2,0:0:0:0: +148,268,64451,2,0,B|372:268,1,225,8|0,3:2|0:0,0:0:0:0: +120,344,64796,5,6,3:2:0:0: +324,344,64911,1,2,0:0:0:0: +120,344,65026,1,2,0:0:0:0: +336,168,65141,1,10,3:2:0:0: +222,168,65256,1,2,0:0:0:0: +108,168,65371,1,2,0:0:0:0: +336,92,65486,1,2,3:2:0:0: +444,92,65601,1,2,0:0:0:0: +336,92,65716,1,2,0:0:0:0: +144,92,65831,1,10,3:2:0:0: +252,92,65946,1,2,0:0:0:0: +144,92,66060,1,2,0:0:0:0: +360,288,66175,6,0,L|468:288,1,90,2|2,0:0|0:0,0:0:0:0: +396,288,66434,1,2,0:0:0:0: +224,192,66520,1,2,0:0:0:0: +388,192,66693,1,2,0:0:0:0: +124,316,66865,2,0,L|120:264,1,45,2|2,0:0|0:0,0:0:0:0: +204,352,67037,2,0,L|200:300,1,45,2|2,0:0|0:0,0:0:0:0: +368,192,67210,1,2,0:0:0:0: +204,192,67382,1,2,0:0:0:0: +476,192,67555,5,6,3:2:0:0: +188,192,67900,1,6,3:2:0:0: +488,192,68244,1,0,3:0:0:0: +356,192,68417,2,0,L|424:192,1,67.5000025749208,0|0,3:0|3:0,0:0:0:0: +172,192,68589,2,0,L|168:100,1,90,8|2,0:0|0:0,0:0:0:0: +484,60,68934,5,4,3:2:0:0: +256,192,69279,12,0,71348,0:0:0:0: +232,196,71693,6,0,L|228:176,14,11.25,2|0|0|0|0|0|0|0|0|0|0|0|0|0|0,0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0,0:0:0:0: +272,164,72037,2,0,L|282:146,14,11.25,2|0|0|0|0|0|0|0|0|0|0|0|0|0|0,0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0,0:0:0:0: +316,216,72382,6,0,L|331:203,14,11.25,2|0|0|0|0|0|0|0|0|0|0|0|0|0|0,0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0,0:0:0:0: +360,140,72727,2,0,L|375:127,14,11.25,2|0|0|0|0|0|0|0|0|0|0|0|0|0|0,0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0,0:0:0:0: +256,76,73072,5,2,0:0:0:0: +244,78,73094,1,0,0:0:0:0: +233,82,73115,1,0,0:0:0:0: +224,88,73137,1,0,0:0:0:0: +215,96,73158,1,0,0:0:0:0: +209,106,73180,1,0,0:0:0:0: +205,117,73201,1,0,0:0:0:0: +202,128,73223,1,0,0:0:0:0: +203,140,73244,1,0,0:0:0:0: +205,151,73266,1,0,0:0:0:0: +210,162,73287,1,0,0:0:0:0: +217,171,73309,1,0,0:0:0:0: +226,179,73331,1,0,0:0:0:0: +236,184,73352,1,0,0:0:0:0: +247,188,73374,1,0,0:0:0:0: +258,190,73395,1,0,0:0:0:0: +270,189,73417,1,0,0:0:0:0: +281,185,73438,1,0,0:0:0:0: +291,180,73460,1,0,0:0:0:0: +300,173,73481,1,0,0:0:0:0: +307,164,73503,1,0,0:0:0:0: +313,153,73525,1,0,0:0:0:0: +316,142,73546,1,0,0:0:0:0: +317,131,73568,1,0,0:0:0:0: +315,119,73589,1,0,0:0:0:0: +311,108,73611,1,0,0:0:0:0: +305,98,73632,1,0,0:0:0:0: +297,90,73654,1,0,0:0:0:0: +288,83,73675,1,0,0:0:0:0: +277,78,73697,1,0,0:0:0:0: +266,76,73719,1,0,0:0:0:0: +164,20,73762,5,2,0:0:0:0: +153,23,73783,1,0,0:0:0:0: +143,28,73805,1,0,0:0:0:0: +133,34,73826,1,0,0:0:0:0: +124,40,73848,1,0,0:0:0:0: +115,48,73869,1,0,0:0:0:0: +108,56,73891,1,0,0:0:0:0: +101,65,73912,1,0,0:0:0:0: +95,74,73934,1,0,0:0:0:0: +90,84,73956,1,0,0:0:0:0: +85,95,73977,1,0,0:0:0:0: +82,105,73999,1,0,0:0:0:0: +80,116,74020,1,0,0:0:0:0: +79,128,74042,1,0,0:0:0:0: +79,139,74063,1,0,0:0:0:0: +180,148,74106,1,2,0:0:0:0: +190,151,74128,1,0,0:0:0:0: +200,156,74150,1,0,0:0:0:0: +210,162,74171,1,0,0:0:0:0: +219,168,74193,1,0,0:0:0:0: +228,176,74214,1,0,0:0:0:0: +235,184,74236,1,0,0:0:0:0: +242,193,74257,1,0,0:0:0:0: +248,202,74279,1,0,0:0:0:0: +253,212,74300,1,0,0:0:0:0: +258,223,74322,1,0,0:0:0:0: +261,233,74344,1,0,0:0:0:0: +263,244,74365,1,0,0:0:0:0: +264,256,74387,1,0,0:0:0:0: +264,267,74408,1,0,0:0:0:0: +148,236,74451,6,0,L|52:236,1,90,6|0,0:1|0:0,0:0:0:0: +196,124,74796,2,0,L|192:32,1,67.5000025749208 +328,208,75141,2,0,L|324:116,1,67.5000025749208,0|0,0:0|0:0,0:0:0:0: +228,168,75486,1,2,0:0:0:0: +396,208,75658,1,2,0:0:0:0: +124,168,75831,5,2,0:0:0:0: +36,168,76003,1,0,0:0:0:0: +36,80,76175,1,0,0:0:0:0: +124,80,76348,1,0,0:0:0:0: +292,80,76520,2,0,L|296:172,1,67.5000025749208,2|0,0:0|0:0,0:0:0:0: +192,224,76865,2,0,L|196:152,1,67.5000025749208,0|0,0:0|0:0,0:0:0:0: +368,148,77210,6,0,P|424:204|368:268,1,180,2|0,0:0|0:0,0:0:0:0: +272,268,77727,1,0,0:0:0:0: +176,268,77900,2,0,L|172:360,1,67.5000025749208,0|0,0:0|0:0,0:0:0:0: +272,268,78244,1,2,0:0:0:0: +104,336,78417,1,2,0:0:0:0: +380,268,78589,6,0,L|456:268,2,67.5000025749208,2|0|0,0:0|0:0|0:0,0:0:0:0: +284,268,79106,1,0,0:0:0:0: +116,268,79279,2,0,L|112:176,1,67.5000025749208,2|0,0:0|0:0,0:0:0:0: +216,192,79624,2,0,L|312:192,1,67.5000025749208,0|0,0:0|0:0,0:0:0:0: +324,192,79882,1,0,0:0:0:0: +152,96,79969,6,0,B|56:96,2,90,2|0|0,0:0|0:0|0:0,0:0:0:0: +248,96,80486,1,0,0:0:0:0: +416,96,80658,2,0,L|420:168,1,67.5000025749208,2|0,0:0|0:0,0:0:0:0: +324,192,81003,2,0,L|252:192,1,67.5000025749208,0|0,0:0|0:0,0:0:0:0: +208,192,81262,1,0,0:0:0:0: +384,256,81348,6,0,B|480:256,2,90,2|0|0,0:0|0:0|0:0,0:0:0:0: +212,296,81865,1,2,0:0:0:0: +444,360,82037,2,0,L|448:284,1,67.5000025749208,2|0,0:0|0:0,0:0:0:0: +212,296,82382,1,2,0:0:0:0: +172,296,82469,1,0,0:0:0:0: +132,296,82555,1,0,0:0:0:0: +432,24,82727,6,0,P|500:80|432:148,1,207.000003948212,2|0,0:0|0:0,0:0:0:0: +272,148,83244,1,0,0:0:0:0: +440,148,83417,2,0,L|444:220,1,67.5000025749208,0|0,0:0|0:0,0:0:0:0: +200,148,83762,1,2,0:0:0:0: +352,148,83934,1,2,0:0:0:0: +104,148,84106,6,0,B|8:148,2,90,2|0|0,0:0|0:0|0:0,0:0:0:0: +272,196,84624,1,0,0:0:0:0: +112,148,84796,2,0,L|108:228,1,67.5000025749208,2|2,0:0|0:0,0:0:0:0: +164,216,85055,1,2,0:0:0:0: +216,216,85141,2,0,L|292:216,1,67.5000025749208,2|2,0:0|0:0,0:0:0:0: +32,216,85486,6,0,P|0:264|36:324,1,135,6|0,3:1|0:0,0:0:0:0: +108,324,85831,2,0,L|216:324,1,90,0|2,3:2|0:2,0:0:0:0: +20,324,86175,1,2,3:2:0:0: +128,324,86348,2,0,L|180:324,1,45,0|0,0:2|0:0,0:0:0:0: +344,192,86520,2,0,L|248:192,1,90,2|2,3:2|0:2,0:0:0:0: +436,312,86865,6,0,L|440:208,1,90,2|2,3:2|0:2,0:0:0:0: +375,208,87124,1,2,0:2:0:0: +312,192,87210,1,2,3:2:0:0: +472,192,87382,1,2,0:2:0:0: +300,192,87555,2,0,L|296:96,1,90,2|2,3:2|0:2,0:0:0:0: +360,100,87813,1,2,0:2:0:0: +196,100,87900,2,0,L|104:100,1,90,2|2,3:2|0:2,0:0:0:0: +276,16,88244,6,0,L|368:16,1,90,2|0,3:2|0:0,0:0:0:0: +312,16,88503,1,0,0:0:0:0: +260,16,88589,1,0,3:2:0:0: +440,16,88762,1,2,0:2:0:0: +192,16,88934,2,0,L|100:16,1,90,2|0,3:2|0:2,0:0:0:0: +164,16,89193,1,0,0:0:0:0: +228,16,89279,2,0,L|136:16,1,90,2|2,3:2|0:2,0:0:0:0: +306,112,89624,6,0,L|414:112,1,90,2|2,3:2|0:2,0:0:0:0: +450,112,89882,1,2,0:2:0:0: +396,112,89969,1,2,3:2:0:0: +228,112,90141,1,2,0:2:0:0: +396,112,90313,2,0,L|400:208,1,90,2|0,3:2|0:2,0:0:0:0: +332,204,90572,1,0,0:0:0:0: +264,204,90658,2,0,L|360:204,1,90,2|0,3:2|0:2,0:0:0:0: +184,204,91003,6,0,L|80:204,1,90,2|2,3:2|0:2,0:0:0:0: +148,204,91262,1,0,0:0:0:0: +200,204,91348,1,2,3:2:0:0: +32,204,91520,1,2,0:2:0:0: +296,204,91693,2,0,B|344:204|344:204|296:204,1,90,2|2,3:2|0:2,0:0:0:0: +240,204,91951,1,0,0:0:0:0: +136,204,92037,2,0,L|132:132,1,45,0|0,3:2|0:0,0:0:0:0: +196,112,92210,2,0,L|200:168,1,45,0|0,0:2|0:0,0:0:0:0: +48,204,92382,6,0,B|4:204|4:204|52:204,1,90,2|2,3:2|0:2,0:0:0:0: +120,204,92641,1,0,0:0:0:0: +188,204,92727,1,0,3:2:0:0: +360,204,92900,1,2,0:2:0:0: +123,293,93072,2,0,L|119:197,1,90,2|2,3:2|0:2,0:0:0:0: +188,204,93331,1,2,0:2:0:0: +368,204,93417,2,0,L|424:204,2,45,2|2|2,3:2|0:2|0:2,0:0:0:0: +96,204,93762,5,2,3:2:0:0: +53,169,93848,1,0,0:0:0:0: +45,114,93934,1,2,0:2:0:0: +75,69,94020,1,0,0:0:0:0: +128,55,94106,1,2,3:2:0:0: +316,56,94279,1,2,0:2:0:0: +48,52,94451,2,0,L|44:152,1,90,2|0,3:2|0:2,0:0:0:0: +112,160,94710,1,0,0:0:0:0: +300,160,94796,1,2,3:2:0:0: +416,160,94969,2,0,L|352:160,1,45,2|0,0:2|0:0,0:0:0:0: +180,232,95141,5,2,3:2:0:0: +128,232,95227,1,0,0:0:0:0: +76,232,95313,1,2,0:2:0:0: +248,232,95486,1,2,3:2:0:0: +68,232,95658,1,2,0:2:0:0: +348,232,95831,2,0,L|440:232,1,90,2|2,3:2|0:2,0:0:0:0: +176,232,96175,6,0,P|176:16|180:232,1,675,2|0,3:2|0:0,0:0:0:0: +156,232,96865,2,0,P|160:64|168:232,1,506.250019311906,0|0,0:0|0:0,0:0:0:0: +144,232,97555,2,0,P|148:112|152:232,1,360,0|0,0:0|0:0,0:0:0:0: +164,320,99279,5,0,3:0:0:0: +324,320,99451,2,0,L|340:284,3,22.5,8|8|8|8,0:2|0:2|0:2|0:2,0:0:0:0: +204,320,99624,2,0,L|64:320,1,135.000005149842,8|0,3:2|3:0,0:0:0:0: +340,228,99969,2,0,L|200:228,1,135.000005149842,8|0,3:2|3:0,0:0:0:0: +472,228,100313,1,8,3:2:0:0: +64,172,100658,6,0,L|8:172,2,53.9999983520508,4|2|2,3:2|0:0|0:0,0:0:0:0: +336,228,101003,1,10,3:2:0:0: +176,228,101175,1,2,0:0:0:0: +448,228,101348,2,0,B|500:228|500:228|444:228,1,107.999996704102,2|2,3:2|3:2,0:0:0:0: +384,228,101606,1,2,0:0:0:0: +220,128,101693,2,0,L|328:128,1,107.999996704102,8|2,3:2|0:0,0:0:0:0: +264,128,101951,1,2,0:0:0:0: +112,128,102037,5,2,3:2:0:0: +56,128,102124,1,2,0:0:0:0: +56,180,102210,1,2,0:0:0:0: +344,252,102382,1,8,3:2:0:0: +56,180,102555,1,2,0:0:0:0: +368,252,102727,2,0,P|400:304|388:352,1,107.999996704102,0|2,3:0|3:2,0:0:0:0: +332,348,102986,1,2,0:2:0:0: +168,348,103072,2,0,L|56:348,1,107.999996704102,10|2,3:2|0:0,0:0:0:0: +120,348,103331,1,0,0:0:0:0: +304,192,103417,5,0,3:2:0:0: +364,192,103503,1,2,0:0:0:0: +424,192,103589,1,2,0:0:0:0: +152,192,103762,1,10,3:2:0:0: +316,192,103934,1,2,0:0:0:0: +56,192,104106,2,0,B|4:192|4:192|60:192,1,107.999996704102,2|2,3:2|3:2,0:0:0:0: +116,192,104365,1,2,0:0:0:0: +304,192,104451,2,0,L|416:192,1,107.999996704102,8|2,3:2|0:0,0:0:0:0: +356,192,104710,1,2,0:0:0:0: +168,112,104796,6,0,L|112:112,2,53.9999983520508,2|2|2,3:2|0:0|0:0,0:0:0:0: +440,112,105141,1,8,3:2:0:0: +144,112,105313,1,2,0:0:0:0: +468,112,105486,2,0,B|468:60|468:60|412:60,1,107.999996704102,0|2,3:0|3:2,0:0:0:0: +360,60,105744,1,2,0:2:0:0: +164,192,105831,2,0,L|276:192,1,107.999996704102,10|2,3:2|0:0,0:0:0:0: +212,192,106089,1,2,0:0:0:0: +24,192,106175,5,2,3:2:0:0: +20,132,106262,1,2,0:0:0:0: +16,72,106348,1,2,0:0:0:0: +296,72,106520,1,8,3:2:0:0: +132,72,106693,1,2,0:0:0:0: +400,72,106865,2,0,P|448:108|440:164,1,107.999996704102,0|2,3:0|3:2,0:0:0:0: +388,192,107124,1,2,0:2:0:0: +196,192,107210,2,0,L|88:192,1,107.999996704102,10|2,3:2|0:0,0:0:0:0: +148,192,107469,1,2,0:0:0:0: +304,12,107555,5,2,3:2:0:0: +358,12,107641,1,2,0:0:0:0: +412,12,107727,1,2,0:0:0:0: +136,12,107900,1,8,3:2:0:0: +432,12,108072,1,2,0:0:0:0: +160,116,108244,2,0,L|52:116,1,107.999996704102,0|2,3:0|3:2,0:0:0:0: +112,116,108503,1,2,0:2:0:0: +300,192,108589,2,0,L|188:192,1,107.999996704102,10|2,3:2|0:0,0:0:0:0: +248,192,108848,1,2,0:0:0:0: +436,192,108934,6,0,L|496:192,2,53.9999983520508,2|2|2,3:2|0:0|0:0,0:0:0:0: +164,192,109279,1,8,3:2:0:0: +324,192,109451,1,2,0:0:0:0: +52,192,109624,2,0,P|24:235|60:280,1,107.999996704102,0|2,3:0|3:2,0:0:0:0: +112,276,109882,1,2,0:2:0:0: +316,276,109969,2,0,L|204:276,1,107.999996704102,10|2,3:2|0:0,0:0:0:0: +268,276,110227,1,2,0:0:0:0: +456,272,110313,6,0,L|460:152,1,107.999996704102,2|2,3:2|0:0,0:0:0:0: +292,276,110658,2,0,L|296:156,1,107.999996704102,8|2,3:2|0:0,0:0:0:0: +32,168,111003,1,8,3:2:0:0: +140,168,111118,1,2,0:0:0:0: +248,168,111233,1,2,0:0:0:0: +44,168,111348,2,0,L|124:168,2,71.9999978027344,10|2|2,3:2|0:2|0:2,0:0:0:0: +320,168,111693,5,4,3:2:0:0: +392,168,111779,1,2,0:0:0:0: +464,168,111865,1,2,0:0:0:0: +196,168,112037,1,10,3:2:0:0: +364,168,112210,1,2,0:0:0:0: +92,80,112382,2,0,L|204:80,1,107.999996704102,2|2,3:2|3:2,0:0:0:0: +140,80,112641,1,2,0:0:0:0: +356,80,112727,2,0,B|408:80|408:80|352:80,1,107.999996704102,8|2,3:2|0:0,0:0:0:0: +292,80,112986,1,2,0:0:0:0: +96,168,113072,5,2,3:2:0:0: +36,168,113158,1,2,0:0:0:0: +96,168,113244,1,2,0:0:0:0: +368,168,113417,1,8,3:2:0:0: +72,168,113589,1,2,0:0:0:0: +364,264,113762,2,0,L|252:264,1,107.999996704102,0|2,3:0|3:2,0:0:0:0: +316,264,114020,1,2,0:2:0:0: +120,344,114106,2,0,L|228:344,1,107.999996704102,10|2,3:2|0:0,0:0:0:0: +168,344,114365,1,2,0:0:0:0: +384,264,114451,5,2,3:2:0:0: +444,264,114537,1,2,0:0:0:0: +444,324,114624,1,2,0:0:0:0: +176,344,114796,1,8,3:2:0:0: +344,344,114969,1,2,0:0:0:0: +76,292,115141,2,0,B|20:292|20:292|20:344,1,107.999996704102,0|2,3:0|3:2,0:0:0:0: +80,344,115400,1,2,0:2:0:0: +284,192,115486,2,0,L|176:192,1,107.999996704102,10|2,3:2|0:0,0:0:0:0: +236,192,115744,1,2,0:0:0:0: +28,192,115831,6,0,L|84:192,2,53.9999983520508,2|2|2,3:2|0:0|0:0,0:0:0:0: +300,192,116175,1,8,3:2:0:0: +132,192,116348,1,2,0:0:0:0: +408,192,116520,2,0,L|300:192,1,107.999996704102,0|2,3:0|3:2,0:0:0:0: +360,192,116779,1,2,0:2:0:0: +156,84,116865,2,0,L|268:84,1,107.999996704102,10|2,3:2|0:0,0:0:0:0: +204,84,117124,1,2,0:0:0:0: +384,84,117210,5,10,3:2:0:0: +444,84,117296,1,2,0:0:0:0: +504,84,117382,1,2,0:0:0:0: +228,284,117555,2,0,L|344:284,1,107.999996704102,8|2,3:2|0:0,0:0:0:0: +60,192,117900,2,0,L|169:192,1,107.999996704102,8|2,3:2|3:2,0:0:0:0: +108,192,118158,1,2,0:2:0:0: +324,192,118244,2,0,B|380:192|380:192|380:140,1,107.999996704102,10|2,3:2|0:0,0:0:0:0: +320,112,118503,1,2,0:0:0:0: +132,112,118589,5,10,3:2:0:0: +72,112,118675,1,2,0:0:0:0: +132,112,118762,1,2,0:0:0:0: +428,140,118934,1,8,3:2:0:0: +80,112,119106,1,2,0:0:0:0: +352,192,119279,2,0,L|216:192,1,135.000005149842,8|2,3:2|3:2,0:0:0:0: +148,192,119537,1,2,0:2:0:0: +388,264,119624,2,0,L|252:264,1,135.000005149842,10|2,3:2|0:0,0:0:0:0: +320,264,119882,1,2,0:0:0:0: +100,264,119969,6,0,L|40:264,2,53.9999983520508,10|2|10,3:2|0:0|0:2,0:0:0:0: +384,192,120313,1,8,3:2:0:0: +112,192,120486,1,10,0:2:0:0: +408,192,120658,2,0,B|464:192|464:192|404:192,1,107.999996704102,8|10,3:2|3:2,0:0:0:0: +348,192,120917,1,2,0:2:0:0: +132,96,121003,2,0,B|40:96|40:96|134:96,1,180,10|10,3:2|0:2,0:0:0:0: +196,96,121262,1,2,0:0:0:0: +384,96,121348,6,0,L|388:160,1,53.9999983520508,10|0,3:2|0:0,0:0:0:0: +188,192,121520,2,0,L|184:256,1,53.9999983520508,10|0,0:2|0:0,0:0:0:0: +400,248,121693,2,0,L|336:248,1,53.9999983520508,8|0,3:2|0:0,0:0:0:0: +128,192,121865,2,0,L|124:252,1,53.9999983520508,10|0,0:2|0:0,0:0:0:0: +336,96,122037,6,0,L|276:96,1,53.9999983520508,8|0,3:2|0:0,0:0:0:0: +484,96,122210,2,0,L|488:176,1,53.9999983520508,10|2,3:2|0:2,0:0:0:0: +272,192,122382,2,0,L|328:192,1,53.9999983520508,10|0,3:2|0:0,0:0:0:0: +108,192,122555,2,0,L|52:192,1,53.9999983520508,8|0,0:2|0:0,0:0:0:0: +280,272,122727,5,8,3:2:0:0: +347,272,122813,1,0,0:0:0:0: +415,272,122900,1,0,0:0:0:0: +256,192,123072,1,2,0:0:0:0: +308,192,123158,1,0,0:0:0:0: +360,192,123244,1,0,0:0:0:0: +228,112,123417,5,2,0:0:0:0: +260,112,123503,1,0,0:0:0:0: +292,112,123589,1,0,0:0:0:0: +188,28,123762,1,2,0:0:0:0: +196,28,123848,1,0,0:0:0:0: +204,28,123934,1,0,0:0:0:0: +256,192,124106,12,0,132382,0:0:0:0: diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3152510-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3152510-expected-conversion.json new file mode 100644 index 0000000000..990550408d --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3152510-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":512.0,"Objects":[{"StartTime":512.0,"Position":368.0,"HyperDash":false},{"StartTime":573.0,"Position":353.0,"HyperDash":false},{"StartTime":670.0,"Position":368.0,"HyperDash":true}]},{"StartTime":829.0,"Objects":[{"StartTime":829.0,"Position":136.0,"HyperDash":false}]},{"StartTime":988.0,"Objects":[{"StartTime":988.0,"Position":272.0,"HyperDash":false}]},{"StartTime":1146.0,"Objects":[{"StartTime":1146.0,"Position":136.0,"HyperDash":false},{"StartTime":1207.0,"Position":125.0,"HyperDash":false},{"StartTime":1304.0,"Position":136.0,"HyperDash":true}]},{"StartTime":1464.0,"Objects":[{"StartTime":1464.0,"Position":368.0,"HyperDash":true}]},{"StartTime":1623.0,"Objects":[{"StartTime":1623.0,"Position":136.0,"HyperDash":false},{"StartTime":1702.0,"Position":87.4122238,"HyperDash":false},{"StartTime":1781.0,"Position":64.73344,"HyperDash":false},{"StartTime":1842.0,"Position":87.7600861,"HyperDash":false},{"StartTime":1940.0,"Position":119.014381,"HyperDash":false}]},{"StartTime":2019.0,"Objects":[{"StartTime":2019.0,"Position":176.0,"HyperDash":true}]},{"StartTime":2099.0,"Objects":[{"StartTime":2099.0,"Position":368.0,"HyperDash":false}]},{"StartTime":2258.0,"Objects":[{"StartTime":2258.0,"Position":232.0,"HyperDash":false}]},{"StartTime":2416.0,"Objects":[{"StartTime":2416.0,"Position":368.0,"HyperDash":false},{"StartTime":2477.0,"Position":369.0,"HyperDash":false},{"StartTime":2574.0,"Position":368.0,"HyperDash":true}]},{"StartTime":2734.0,"Objects":[{"StartTime":2734.0,"Position":136.0,"HyperDash":false},{"StartTime":2795.0,"Position":98.3227844,"HyperDash":false},{"StartTime":2892.0,"Position":41.0,"HyperDash":true}]},{"StartTime":3051.0,"Objects":[{"StartTime":3051.0,"Position":280.0,"HyperDash":false},{"StartTime":3112.0,"Position":301.677216,"HyperDash":false},{"StartTime":3209.0,"Position":375.0,"HyperDash":true}]},{"StartTime":3369.0,"Objects":[{"StartTime":3369.0,"Position":136.0,"HyperDash":false}]},{"StartTime":3527.0,"Objects":[{"StartTime":3527.0,"Position":272.0,"HyperDash":false}]},{"StartTime":3686.0,"Objects":[{"StartTime":3686.0,"Position":136.0,"HyperDash":false},{"StartTime":3747.0,"Position":128.0,"HyperDash":false},{"StartTime":3844.0,"Position":136.0,"HyperDash":true}]},{"StartTime":4004.0,"Objects":[{"StartTime":4004.0,"Position":384.0,"HyperDash":true}]},{"StartTime":4162.0,"Objects":[{"StartTime":4162.0,"Position":136.0,"HyperDash":false},{"StartTime":4241.0,"Position":171.350159,"HyperDash":false},{"StartTime":4320.0,"Position":230.700317,"HyperDash":false},{"StartTime":4381.0,"Position":281.261841,"HyperDash":false},{"StartTime":4479.0,"Position":326.0,"HyperDash":false}]},{"StartTime":4559.0,"Objects":[{"StartTime":4559.0,"Position":272.0,"HyperDash":true}]},{"StartTime":4638.0,"Objects":[{"StartTime":4638.0,"Position":80.0,"HyperDash":false}]},{"StartTime":4797.0,"Objects":[{"StartTime":4797.0,"Position":216.0,"HyperDash":false}]},{"StartTime":4956.0,"Objects":[{"StartTime":4956.0,"Position":80.0,"HyperDash":false},{"StartTime":5017.0,"Position":84.0,"HyperDash":false},{"StartTime":5114.0,"Position":80.0,"HyperDash":true}]},{"StartTime":5273.0,"Objects":[{"StartTime":5273.0,"Position":312.0,"HyperDash":false},{"StartTime":5334.0,"Position":266.322784,"HyperDash":false},{"StartTime":5431.0,"Position":217.0,"HyperDash":true}]},{"StartTime":5591.0,"Objects":[{"StartTime":5591.0,"Position":456.0,"HyperDash":false},{"StartTime":5652.0,"Position":461.0,"HyperDash":false},{"StartTime":5749.0,"Position":456.0,"HyperDash":true}]},{"StartTime":5908.0,"Objects":[{"StartTime":5908.0,"Position":216.0,"HyperDash":false}]},{"StartTime":6067.0,"Objects":[{"StartTime":6067.0,"Position":352.0,"HyperDash":false}]},{"StartTime":6226.0,"Objects":[{"StartTime":6226.0,"Position":216.0,"HyperDash":false},{"StartTime":6287.0,"Position":197.0,"HyperDash":false},{"StartTime":6384.0,"Position":216.0,"HyperDash":true}]},{"StartTime":6543.0,"Objects":[{"StartTime":6543.0,"Position":456.0,"HyperDash":true}]},{"StartTime":6702.0,"Objects":[{"StartTime":6702.0,"Position":216.0,"HyperDash":false},{"StartTime":6781.0,"Position":163.345444,"HyperDash":false},{"StartTime":6860.0,"Position":152.1179,"HyperDash":false},{"StartTime":6921.0,"Position":177.651291,"HyperDash":false},{"StartTime":7019.0,"Position":209.232849,"HyperDash":false}]},{"StartTime":7099.0,"Objects":[{"StartTime":7099.0,"Position":264.0,"HyperDash":true}]},{"StartTime":7178.0,"Objects":[{"StartTime":7178.0,"Position":456.0,"HyperDash":false}]},{"StartTime":7337.0,"Objects":[{"StartTime":7337.0,"Position":320.0,"HyperDash":false}]},{"StartTime":7496.0,"Objects":[{"StartTime":7496.0,"Position":456.0,"HyperDash":false},{"StartTime":7557.0,"Position":469.0,"HyperDash":false},{"StartTime":7654.0,"Position":456.0,"HyperDash":true}]},{"StartTime":7813.0,"Objects":[{"StartTime":7813.0,"Position":216.0,"HyperDash":false},{"StartTime":7874.0,"Position":171.322784,"HyperDash":false},{"StartTime":7971.0,"Position":121.0,"HyperDash":true}]},{"StartTime":8131.0,"Objects":[{"StartTime":8131.0,"Position":368.0,"HyperDash":false},{"StartTime":8192.0,"Position":351.0,"HyperDash":false},{"StartTime":8289.0,"Position":368.0,"HyperDash":true}]},{"StartTime":8448.0,"Objects":[{"StartTime":8448.0,"Position":128.0,"HyperDash":false}]},{"StartTime":8607.0,"Objects":[{"StartTime":8607.0,"Position":264.0,"HyperDash":false}]},{"StartTime":8765.0,"Objects":[{"StartTime":8765.0,"Position":128.0,"HyperDash":false},{"StartTime":8826.0,"Position":141.0,"HyperDash":false},{"StartTime":8923.0,"Position":128.0,"HyperDash":true}]},{"StartTime":9083.0,"Objects":[{"StartTime":9083.0,"Position":368.0,"HyperDash":true}]},{"StartTime":9242.0,"Objects":[{"StartTime":9242.0,"Position":128.0,"HyperDash":false},{"StartTime":9321.0,"Position":170.350159,"HyperDash":false},{"StartTime":9400.0,"Position":222.700317,"HyperDash":false},{"StartTime":9461.0,"Position":244.261841,"HyperDash":false},{"StartTime":9559.0,"Position":318.0,"HyperDash":false}]},{"StartTime":9638.0,"Objects":[{"StartTime":9638.0,"Position":264.0,"HyperDash":true}]},{"StartTime":9718.0,"Objects":[{"StartTime":9718.0,"Position":72.0,"HyperDash":false}]},{"StartTime":9877.0,"Objects":[{"StartTime":9877.0,"Position":208.0,"HyperDash":false}]},{"StartTime":10035.0,"Objects":[{"StartTime":10035.0,"Position":72.0,"HyperDash":false},{"StartTime":10096.0,"Position":68.0,"HyperDash":false},{"StartTime":10193.0,"Position":72.0,"HyperDash":true}]},{"StartTime":10353.0,"Objects":[{"StartTime":10353.0,"Position":312.0,"HyperDash":false},{"StartTime":10414.0,"Position":274.322784,"HyperDash":false},{"StartTime":10511.0,"Position":217.0,"HyperDash":true}]},{"StartTime":10670.0,"Objects":[{"StartTime":10670.0,"Position":464.0,"HyperDash":false},{"StartTime":10731.0,"Position":478.0,"HyperDash":false},{"StartTime":10828.0,"Position":464.0,"HyperDash":true}]},{"StartTime":10988.0,"Objects":[{"StartTime":10988.0,"Position":224.0,"HyperDash":false},{"StartTime":11049.0,"Position":209.0,"HyperDash":false},{"StartTime":11146.0,"Position":224.0,"HyperDash":false}]},{"StartTime":11305.0,"Objects":[{"StartTime":11305.0,"Position":360.0,"HyperDash":false}]},{"StartTime":11464.0,"Objects":[{"StartTime":11464.0,"Position":224.0,"HyperDash":true}]},{"StartTime":11623.0,"Objects":[{"StartTime":11623.0,"Position":464.0,"HyperDash":false}]},{"StartTime":11781.0,"Objects":[{"StartTime":11781.0,"Position":328.0,"HyperDash":false},{"StartTime":11842.0,"Position":309.0,"HyperDash":false},{"StartTime":11939.0,"Position":328.0,"HyperDash":false}]},{"StartTime":12099.0,"Objects":[{"StartTime":12099.0,"Position":464.0,"HyperDash":false},{"StartTime":12160.0,"Position":448.0,"HyperDash":false},{"StartTime":12257.0,"Position":464.0,"HyperDash":false}]},{"StartTime":12416.0,"Objects":[{"StartTime":12416.0,"Position":328.0,"HyperDash":false},{"StartTime":12477.0,"Position":377.677216,"HyperDash":false},{"StartTime":12574.0,"Position":423.0,"HyperDash":false}]},{"StartTime":12734.0,"Objects":[{"StartTime":12734.0,"Position":288.0,"HyperDash":false}]},{"StartTime":12892.0,"Objects":[{"StartTime":12892.0,"Position":424.0,"HyperDash":false},{"StartTime":12953.0,"Position":441.0,"HyperDash":false},{"StartTime":13050.0,"Position":424.0,"HyperDash":true}]},{"StartTime":13210.0,"Objects":[{"StartTime":13210.0,"Position":192.0,"HyperDash":false},{"StartTime":13271.0,"Position":191.0,"HyperDash":false},{"StartTime":13368.0,"Position":192.0,"HyperDash":true}]},{"StartTime":13527.0,"Objects":[{"StartTime":13527.0,"Position":424.0,"HyperDash":false},{"StartTime":13588.0,"Position":417.0,"HyperDash":false},{"StartTime":13685.0,"Position":424.0,"HyperDash":false}]},{"StartTime":13845.0,"Objects":[{"StartTime":13845.0,"Position":288.0,"HyperDash":false}]},{"StartTime":14004.0,"Objects":[{"StartTime":14004.0,"Position":424.0,"HyperDash":true}]},{"StartTime":14162.0,"Objects":[{"StartTime":14162.0,"Position":184.0,"HyperDash":false}]},{"StartTime":14321.0,"Objects":[{"StartTime":14321.0,"Position":320.0,"HyperDash":false},{"StartTime":14382.0,"Position":319.0,"HyperDash":false},{"StartTime":14479.0,"Position":320.0,"HyperDash":true}]},{"StartTime":14638.0,"Objects":[{"StartTime":14638.0,"Position":88.0,"HyperDash":false},{"StartTime":14699.0,"Position":107.0,"HyperDash":false},{"StartTime":14796.0,"Position":88.0,"HyperDash":false}]},{"StartTime":14956.0,"Objects":[{"StartTime":14956.0,"Position":224.0,"HyperDash":false}]},{"StartTime":15115.0,"Objects":[{"StartTime":15115.0,"Position":88.0,"HyperDash":false},{"StartTime":15176.0,"Position":82.0,"HyperDash":false},{"StartTime":15273.0,"Position":88.0,"HyperDash":true}]},{"StartTime":15432.0,"Objects":[{"StartTime":15432.0,"Position":328.0,"HyperDash":false},{"StartTime":15493.0,"Position":369.677216,"HyperDash":false},{"StartTime":15590.0,"Position":423.0,"HyperDash":true}]},{"StartTime":15750.0,"Objects":[{"StartTime":15750.0,"Position":192.0,"HyperDash":false}]},{"StartTime":15908.0,"Objects":[{"StartTime":15908.0,"Position":328.0,"HyperDash":false}]},{"StartTime":16067.0,"Objects":[{"StartTime":16067.0,"Position":192.0,"HyperDash":false},{"StartTime":16128.0,"Position":168.322784,"HyperDash":false},{"StartTime":16225.0,"Position":97.0,"HyperDash":false}]},{"StartTime":16385.0,"Objects":[{"StartTime":16385.0,"Position":232.0,"HyperDash":false}]},{"StartTime":16543.0,"Objects":[{"StartTime":16543.0,"Position":96.0,"HyperDash":true}]},{"StartTime":16702.0,"Objects":[{"StartTime":16702.0,"Position":336.0,"HyperDash":false}]},{"StartTime":16861.0,"Objects":[{"StartTime":16861.0,"Position":200.0,"HyperDash":false},{"StartTime":16922.0,"Position":217.0,"HyperDash":false},{"StartTime":17019.0,"Position":200.0,"HyperDash":true}]},{"StartTime":17178.0,"Objects":[{"StartTime":17178.0,"Position":440.0,"HyperDash":false}]},{"StartTime":17337.0,"Objects":[{"StartTime":17337.0,"Position":304.0,"HyperDash":false}]},{"StartTime":17496.0,"Objects":[{"StartTime":17496.0,"Position":408.0,"HyperDash":false},{"StartTime":17557.0,"Position":461.677216,"HyperDash":false},{"StartTime":17654.0,"Position":503.0,"HyperDash":false}]},{"StartTime":17813.0,"Objects":[{"StartTime":17813.0,"Position":360.0,"HyperDash":false}]},{"StartTime":17972.0,"Objects":[{"StartTime":17972.0,"Position":496.0,"HyperDash":false},{"StartTime":18033.0,"Position":511.0,"HyperDash":false},{"StartTime":18130.0,"Position":496.0,"HyperDash":true}]},{"StartTime":18289.0,"Objects":[{"StartTime":18289.0,"Position":256.0,"HyperDash":false},{"StartTime":18350.0,"Position":236.322784,"HyperDash":false},{"StartTime":18447.0,"Position":161.0,"HyperDash":true}]},{"StartTime":18607.0,"Objects":[{"StartTime":18607.0,"Position":392.0,"HyperDash":false},{"StartTime":18668.0,"Position":401.0,"HyperDash":false},{"StartTime":18765.0,"Position":392.0,"HyperDash":false}]},{"StartTime":18924.0,"Objects":[{"StartTime":18924.0,"Position":256.0,"HyperDash":false}]},{"StartTime":19083.0,"Objects":[{"StartTime":19083.0,"Position":392.0,"HyperDash":true}]},{"StartTime":19242.0,"Objects":[{"StartTime":19242.0,"Position":152.0,"HyperDash":false}]},{"StartTime":19400.0,"Objects":[{"StartTime":19400.0,"Position":288.0,"HyperDash":false},{"StartTime":19461.0,"Position":271.0,"HyperDash":false},{"StartTime":19558.0,"Position":288.0,"HyperDash":true}]},{"StartTime":19718.0,"Objects":[{"StartTime":19718.0,"Position":48.0,"HyperDash":false},{"StartTime":19779.0,"Position":53.0,"HyperDash":false},{"StartTime":19876.0,"Position":48.0,"HyperDash":false}]},{"StartTime":20035.0,"Objects":[{"StartTime":20035.0,"Position":168.0,"HyperDash":false}]},{"StartTime":20194.0,"Objects":[{"StartTime":20194.0,"Position":48.0,"HyperDash":false},{"StartTime":20273.0,"Position":83.30042,"HyperDash":false},{"StartTime":20352.0,"Position":142.600845,"HyperDash":false},{"StartTime":20431.0,"Position":207.90126,"HyperDash":false},{"StartTime":20511.0,"Position":237.800415,"HyperDash":false},{"StartTime":20572.0,"Position":290.323517,"HyperDash":false},{"StartTime":20670.0,"Position":333.0,"HyperDash":true}]},{"StartTime":20829.0,"Objects":[{"StartTime":20829.0,"Position":88.0,"HyperDash":false},{"StartTime":20890.0,"Position":91.0,"HyperDash":false},{"StartTime":20987.0,"Position":88.0,"HyperDash":false}]},{"StartTime":21146.0,"Objects":[{"StartTime":21146.0,"Position":232.0,"HyperDash":false},{"StartTime":21207.0,"Position":222.0,"HyperDash":false},{"StartTime":21304.0,"Position":232.0,"HyperDash":false}]},{"StartTime":21464.0,"Objects":[{"StartTime":21464.0,"Position":88.0,"HyperDash":false},{"StartTime":21525.0,"Position":125.677216,"HyperDash":false},{"StartTime":21622.0,"Position":183.0,"HyperDash":false}]},{"StartTime":21781.0,"Objects":[{"StartTime":21781.0,"Position":320.0,"HyperDash":false}]},{"StartTime":21940.0,"Objects":[{"StartTime":21940.0,"Position":184.0,"HyperDash":false},{"StartTime":22001.0,"Position":174.0,"HyperDash":false},{"StartTime":22098.0,"Position":184.0,"HyperDash":false}]},{"StartTime":22258.0,"Objects":[{"StartTime":22258.0,"Position":320.0,"HyperDash":false},{"StartTime":22319.0,"Position":320.0,"HyperDash":false},{"StartTime":22416.0,"Position":320.0,"HyperDash":false}]},{"StartTime":22575.0,"Objects":[{"StartTime":22575.0,"Position":184.0,"HyperDash":false},{"StartTime":22636.0,"Position":166.0,"HyperDash":false},{"StartTime":22733.0,"Position":184.0,"HyperDash":false}]},{"StartTime":22892.0,"Objects":[{"StartTime":22892.0,"Position":320.0,"HyperDash":false}]},{"StartTime":23051.0,"Objects":[{"StartTime":23051.0,"Position":184.0,"HyperDash":false},{"StartTime":23112.0,"Position":131.322784,"HyperDash":false},{"StartTime":23209.0,"Position":89.0,"HyperDash":true}]},{"StartTime":23369.0,"Objects":[{"StartTime":23369.0,"Position":328.0,"HyperDash":false},{"StartTime":23448.0,"Position":383.3004,"HyperDash":false},{"StartTime":23527.0,"Position":422.60083,"HyperDash":false},{"StartTime":23607.0,"Position":470.5,"HyperDash":false}]},{"StartTime":23686.0,"Objects":[{"StartTime":23686.0,"Position":416.0,"HyperDash":false}]},{"StartTime":23845.0,"Objects":[{"StartTime":23845.0,"Position":280.0,"HyperDash":false},{"StartTime":23924.0,"Position":212.649841,"HyperDash":false},{"StartTime":24003.0,"Position":185.0,"HyperDash":false},{"StartTime":24064.0,"Position":216.261841,"HyperDash":false},{"StartTime":24162.0,"Position":280.0,"HyperDash":false}]},{"StartTime":24321.0,"Objects":[{"StartTime":24321.0,"Position":424.0,"HyperDash":false}]},{"StartTime":24480.0,"Objects":[{"StartTime":24480.0,"Position":288.0,"HyperDash":false},{"StartTime":24559.0,"Position":324.350159,"HyperDash":false},{"StartTime":24638.0,"Position":382.700317,"HyperDash":false},{"StartTime":24699.0,"Position":417.261841,"HyperDash":false},{"StartTime":24797.0,"Position":478.0,"HyperDash":false}]},{"StartTime":24956.0,"Objects":[{"StartTime":24956.0,"Position":360.0,"HyperDash":false}]},{"StartTime":25115.0,"Objects":[{"StartTime":25115.0,"Position":224.0,"HyperDash":false}]},{"StartTime":25273.0,"Objects":[{"StartTime":25273.0,"Position":360.0,"HyperDash":false},{"StartTime":25352.0,"Position":360.0,"HyperDash":false}]},{"StartTime":25432.0,"Objects":[{"StartTime":25432.0,"Position":288.0,"HyperDash":false},{"StartTime":25511.0,"Position":288.0,"HyperDash":true}]},{"StartTime":25591.0,"Objects":[{"StartTime":25591.0,"Position":448.0,"HyperDash":false},{"StartTime":25652.0,"Position":465.0,"HyperDash":false},{"StartTime":25749.0,"Position":448.0,"HyperDash":true}]},{"StartTime":25908.0,"Objects":[{"StartTime":25908.0,"Position":208.0,"HyperDash":false},{"StartTime":25969.0,"Position":154.322784,"HyperDash":false},{"StartTime":26066.0,"Position":113.0,"HyperDash":false}]},{"StartTime":26226.0,"Objects":[{"StartTime":26226.0,"Position":248.0,"HyperDash":false},{"StartTime":26287.0,"Position":289.677216,"HyperDash":false},{"StartTime":26384.0,"Position":343.0,"HyperDash":false}]},{"StartTime":26543.0,"Objects":[{"StartTime":26543.0,"Position":208.0,"HyperDash":false},{"StartTime":26604.0,"Position":227.0,"HyperDash":false},{"StartTime":26701.0,"Position":208.0,"HyperDash":false}]},{"StartTime":26861.0,"Objects":[{"StartTime":26861.0,"Position":344.0,"HyperDash":false}]},{"StartTime":27019.0,"Objects":[{"StartTime":27019.0,"Position":208.0,"HyperDash":false},{"StartTime":27080.0,"Position":181.322784,"HyperDash":false},{"StartTime":27177.0,"Position":113.0,"HyperDash":false}]},{"StartTime":27337.0,"Objects":[{"StartTime":27337.0,"Position":248.0,"HyperDash":false},{"StartTime":27398.0,"Position":294.677216,"HyperDash":false},{"StartTime":27495.0,"Position":343.0,"HyperDash":false}]},{"StartTime":27654.0,"Objects":[{"StartTime":27654.0,"Position":208.0,"HyperDash":false}]},{"StartTime":27813.0,"Objects":[{"StartTime":27813.0,"Position":344.0,"HyperDash":false}]},{"StartTime":27972.0,"Objects":[{"StartTime":27972.0,"Position":208.0,"HyperDash":false}]},{"StartTime":28131.0,"Objects":[{"StartTime":28131.0,"Position":344.0,"HyperDash":false},{"StartTime":28192.0,"Position":384.677216,"HyperDash":false},{"StartTime":28289.0,"Position":439.0,"HyperDash":true}]},{"StartTime":28448.0,"Objects":[{"StartTime":28448.0,"Position":208.0,"HyperDash":false},{"StartTime":28527.0,"Position":167.699585,"HyperDash":false},{"StartTime":28606.0,"Position":113.399155,"HyperDash":false},{"StartTime":28686.0,"Position":65.5,"HyperDash":false}]},{"StartTime":28765.0,"Objects":[{"StartTime":28765.0,"Position":120.0,"HyperDash":false}]},{"StartTime":28924.0,"Objects":[{"StartTime":28924.0,"Position":256.0,"HyperDash":false},{"StartTime":29003.0,"Position":288.350159,"HyperDash":false},{"StartTime":29082.0,"Position":351.0,"HyperDash":false},{"StartTime":29143.0,"Position":311.738159,"HyperDash":false},{"StartTime":29241.0,"Position":256.0,"HyperDash":false}]},{"StartTime":29400.0,"Objects":[{"StartTime":29400.0,"Position":112.0,"HyperDash":false}]},{"StartTime":29559.0,"Objects":[{"StartTime":29559.0,"Position":248.0,"HyperDash":false},{"StartTime":29638.0,"Position":190.649841,"HyperDash":false},{"StartTime":29717.0,"Position":153.299683,"HyperDash":false},{"StartTime":29778.0,"Position":125.738174,"HyperDash":false},{"StartTime":29876.0,"Position":58.0,"HyperDash":false}]},{"StartTime":30035.0,"Objects":[{"StartTime":30035.0,"Position":192.0,"HyperDash":false}]},{"StartTime":30194.0,"Objects":[{"StartTime":30194.0,"Position":328.0,"HyperDash":false}]},{"StartTime":30353.0,"Objects":[{"StartTime":30353.0,"Position":192.0,"HyperDash":false},{"StartTime":30414.0,"Position":196.0,"HyperDash":false},{"StartTime":30511.0,"Position":192.0,"HyperDash":true}]},{"StartTime":30670.0,"Objects":[{"StartTime":30670.0,"Position":432.0,"HyperDash":false},{"StartTime":30749.0,"Position":384.5,"HyperDash":true}]},{"StartTime":30829.0,"Objects":[{"StartTime":30829.0,"Position":192.0,"HyperDash":false},{"StartTime":30908.0,"Position":144.5,"HyperDash":true}]},{"StartTime":30988.0,"Objects":[{"StartTime":30988.0,"Position":336.0,"HyperDash":false},{"StartTime":31049.0,"Position":326.0,"HyperDash":false},{"StartTime":31146.0,"Position":336.0,"HyperDash":false}]},{"StartTime":31305.0,"Objects":[{"StartTime":31305.0,"Position":208.0,"HyperDash":false},{"StartTime":31366.0,"Position":198.0,"HyperDash":false},{"StartTime":31463.0,"Position":208.0,"HyperDash":false}]},{"StartTime":31623.0,"Objects":[{"StartTime":31623.0,"Position":80.0,"HyperDash":false},{"StartTime":31684.0,"Position":87.0,"HyperDash":false},{"StartTime":31781.0,"Position":80.0,"HyperDash":false}]},{"StartTime":31940.0,"Objects":[{"StartTime":31940.0,"Position":208.0,"HyperDash":false}]},{"StartTime":32099.0,"Objects":[{"StartTime":32099.0,"Position":80.0,"HyperDash":false},{"StartTime":32160.0,"Position":130.677216,"HyperDash":false},{"StartTime":32257.0,"Position":175.0,"HyperDash":false}]},{"StartTime":32416.0,"Objects":[{"StartTime":32416.0,"Position":296.0,"HyperDash":false},{"StartTime":32477.0,"Position":303.0,"HyperDash":false},{"StartTime":32574.0,"Position":296.0,"HyperDash":false}]},{"StartTime":32734.0,"Objects":[{"StartTime":32734.0,"Position":176.0,"HyperDash":false},{"StartTime":32795.0,"Position":188.0,"HyperDash":false},{"StartTime":32892.0,"Position":176.0,"HyperDash":false}]},{"StartTime":33051.0,"Objects":[{"StartTime":33051.0,"Position":296.0,"HyperDash":false},{"StartTime":33130.0,"Position":250.649841,"HyperDash":false},{"StartTime":33209.0,"Position":201.0,"HyperDash":false},{"StartTime":33270.0,"Position":218.261841,"HyperDash":false},{"StartTime":33368.0,"Position":296.0,"HyperDash":true}]},{"StartTime":33527.0,"Objects":[{"StartTime":33527.0,"Position":48.0,"HyperDash":false}]},{"StartTime":33686.0,"Objects":[{"StartTime":33686.0,"Position":160.0,"HyperDash":false}]},{"StartTime":33845.0,"Objects":[{"StartTime":33845.0,"Position":272.0,"HyperDash":false}]},{"StartTime":34004.0,"Objects":[{"StartTime":34004.0,"Position":160.0,"HyperDash":false}]},{"StartTime":34162.0,"Objects":[{"StartTime":34162.0,"Position":304.0,"HyperDash":false},{"StartTime":34241.0,"Position":332.587769,"HyperDash":false},{"StartTime":34320.0,"Position":375.266571,"HyperDash":false},{"StartTime":34381.0,"Position":377.239929,"HyperDash":false},{"StartTime":34479.0,"Position":320.985657,"HyperDash":false}]},{"StartTime":34638.0,"Objects":[{"StartTime":34638.0,"Position":184.0,"HyperDash":false},{"StartTime":34717.0,"Position":224.350159,"HyperDash":false},{"StartTime":34796.0,"Position":278.700317,"HyperDash":false},{"StartTime":34857.0,"Position":313.261841,"HyperDash":false},{"StartTime":34955.0,"Position":374.0,"HyperDash":false}]},{"StartTime":35035.0,"Objects":[{"StartTime":35035.0,"Position":440.0,"HyperDash":false}]},{"StartTime":35115.0,"Objects":[{"StartTime":35115.0,"Position":376.0,"HyperDash":false}]},{"StartTime":35273.0,"Objects":[{"StartTime":35273.0,"Position":224.0,"HyperDash":false}]},{"StartTime":35432.0,"Objects":[{"StartTime":35432.0,"Position":368.0,"HyperDash":false},{"StartTime":35511.0,"Position":430.6414,"HyperDash":false},{"StartTime":35590.0,"Position":439.319336,"HyperDash":false},{"StartTime":35651.0,"Position":442.987732,"HyperDash":false},{"StartTime":35749.0,"Position":381.729523,"HyperDash":false}]},{"StartTime":35908.0,"Objects":[{"StartTime":35908.0,"Position":288.0,"HyperDash":true}]},{"StartTime":36067.0,"Objects":[{"StartTime":36067.0,"Position":72.0,"HyperDash":false}]},{"StartTime":36146.0,"Objects":[{"StartTime":36146.0,"Position":16.0,"HyperDash":false}]},{"StartTime":36226.0,"Objects":[{"StartTime":36226.0,"Position":16.0,"HyperDash":false}]},{"StartTime":36305.0,"Objects":[{"StartTime":36305.0,"Position":72.0,"HyperDash":true}]},{"StartTime":36385.0,"Objects":[{"StartTime":36385.0,"Position":264.0,"HyperDash":false}]},{"StartTime":36464.0,"Objects":[{"StartTime":36464.0,"Position":328.0,"HyperDash":false}]},{"StartTime":36543.0,"Objects":[{"StartTime":36543.0,"Position":264.0,"HyperDash":false}]},{"StartTime":36623.0,"Objects":[{"StartTime":36623.0,"Position":200.0,"HyperDash":true}]},{"StartTime":36702.0,"Objects":[{"StartTime":36702.0,"Position":392.0,"HyperDash":false},{"StartTime":36781.0,"Position":439.5,"HyperDash":true}]},{"StartTime":36861.0,"Objects":[{"StartTime":36861.0,"Position":232.0,"HyperDash":false},{"StartTime":36940.0,"Position":226.108353,"HyperDash":false}]},{"StartTime":37019.0,"Objects":[{"StartTime":37019.0,"Position":304.0,"HyperDash":false},{"StartTime":37098.0,"Position":315.520447,"HyperDash":true}]},{"StartTime":37178.0,"Objects":[{"StartTime":37178.0,"Position":104.0,"HyperDash":false},{"StartTime":37257.0,"Position":56.5,"HyperDash":true}]},{"StartTime":37337.0,"Objects":[{"StartTime":37337.0,"Position":264.0,"HyperDash":false},{"StartTime":37416.0,"Position":279.0208,"HyperDash":false}]},{"StartTime":37496.0,"Objects":[{"StartTime":37496.0,"Position":208.0,"HyperDash":false},{"StartTime":37575.0,"Position":201.282486,"HyperDash":true}]},{"StartTime":37654.0,"Objects":[{"StartTime":37654.0,"Position":392.0,"HyperDash":false}]},{"StartTime":37734.0,"Objects":[{"StartTime":37734.0,"Position":448.0,"HyperDash":false}]},{"StartTime":37813.0,"Objects":[{"StartTime":37813.0,"Position":448.0,"HyperDash":false}]},{"StartTime":37892.0,"Objects":[{"StartTime":37892.0,"Position":392.0,"HyperDash":true}]},{"StartTime":37972.0,"Objects":[{"StartTime":37972.0,"Position":192.0,"HyperDash":false},{"StartTime":38051.0,"Position":239.5,"HyperDash":true}]},{"StartTime":38131.0,"Objects":[{"StartTime":38131.0,"Position":410.0,"HyperDash":false},{"StartTime":38210.0,"Position":457.5,"HyperDash":true}]},{"StartTime":38289.0,"Objects":[{"StartTime":38289.0,"Position":264.0,"HyperDash":false},{"StartTime":38368.0,"Position":216.5,"HyperDash":true}]},{"StartTime":38448.0,"Objects":[{"StartTime":38448.0,"Position":448.0,"HyperDash":false},{"StartTime":38527.0,"Position":495.5,"HyperDash":true}]},{"StartTime":38607.0,"Objects":[{"StartTime":38607.0,"Position":296.0,"HyperDash":false}]},{"StartTime":38924.0,"Objects":[{"StartTime":38924.0,"Position":440.0,"HyperDash":false}]},{"StartTime":39242.0,"Objects":[{"StartTime":39242.0,"Position":296.0,"HyperDash":false}]},{"StartTime":39559.0,"Objects":[{"StartTime":39559.0,"Position":152.0,"HyperDash":false}]},{"StartTime":39877.0,"Objects":[{"StartTime":39877.0,"Position":352.0,"HyperDash":false},{"StartTime":39956.0,"Position":285.84314,"HyperDash":false},{"StartTime":40035.0,"Position":257.328156,"HyperDash":false},{"StartTime":40114.0,"Position":311.126862,"HyperDash":false},{"StartTime":40194.0,"Position":352.0,"HyperDash":false},{"StartTime":40273.0,"Position":315.962524,"HyperDash":false},{"StartTime":40353.0,"Position":257.328156,"HyperDash":false},{"StartTime":40432.0,"Position":295.60437,"HyperDash":false},{"StartTime":40511.0,"Position":352.0,"HyperDash":false},{"StartTime":40572.0,"Position":308.8265,"HyperDash":false},{"StartTime":40670.0,"Position":257.328156,"HyperDash":false}]},{"StartTime":40829.0,"Objects":[{"StartTime":40829.0,"Position":432.0,"HyperDash":true},{"StartTime":40890.0,"Position":349.476257,"HyperDash":false},{"StartTime":40987.0,"Position":218.25,"HyperDash":true}]},{"StartTime":41146.0,"Objects":[{"StartTime":41146.0,"Position":440.0,"HyperDash":false},{"StartTime":41225.0,"Position":485.905121,"HyperDash":false},{"StartTime":41304.0,"Position":484.021484,"HyperDash":false},{"StartTime":41384.0,"Position":448.07428,"HyperDash":true}]},{"StartTime":41464.0,"Objects":[{"StartTime":41464.0,"Position":256.0,"HyperDash":false}]},{"StartTime":41623.0,"Objects":[{"StartTime":41623.0,"Position":400.0,"HyperDash":true}]},{"StartTime":41781.0,"Objects":[{"StartTime":41781.0,"Position":168.0,"HyperDash":false},{"StartTime":41842.0,"Position":176.0,"HyperDash":false},{"StartTime":41939.0,"Position":168.0,"HyperDash":true}]},{"StartTime":42099.0,"Objects":[{"StartTime":42099.0,"Position":400.0,"HyperDash":false}]},{"StartTime":42258.0,"Objects":[{"StartTime":42258.0,"Position":256.0,"HyperDash":false},{"StartTime":42319.0,"Position":267.0,"HyperDash":false},{"StartTime":42416.0,"Position":256.0,"HyperDash":false}]},{"StartTime":42575.0,"Objects":[{"StartTime":42575.0,"Position":400.0,"HyperDash":false}]},{"StartTime":42734.0,"Objects":[{"StartTime":42734.0,"Position":256.0,"HyperDash":true}]},{"StartTime":42892.0,"Objects":[{"StartTime":42892.0,"Position":488.0,"HyperDash":false},{"StartTime":42953.0,"Position":480.0,"HyperDash":false},{"StartTime":43050.0,"Position":488.0,"HyperDash":true}]},{"StartTime":43210.0,"Objects":[{"StartTime":43210.0,"Position":256.0,"HyperDash":false}]},{"StartTime":43369.0,"Objects":[{"StartTime":43369.0,"Position":368.0,"HyperDash":false}]},{"StartTime":43527.0,"Objects":[{"StartTime":43527.0,"Position":480.0,"HyperDash":true}]},{"StartTime":43686.0,"Objects":[{"StartTime":43686.0,"Position":256.0,"HyperDash":false},{"StartTime":43747.0,"Position":223.322784,"HyperDash":false},{"StartTime":43844.0,"Position":161.0,"HyperDash":true}]},{"StartTime":44004.0,"Objects":[{"StartTime":44004.0,"Position":392.0,"HyperDash":false}]},{"StartTime":44162.0,"Objects":[{"StartTime":44162.0,"Position":248.0,"HyperDash":false},{"StartTime":44223.0,"Position":260.0,"HyperDash":false},{"StartTime":44320.0,"Position":248.0,"HyperDash":true}]},{"StartTime":44480.0,"Objects":[{"StartTime":44480.0,"Position":480.0,"HyperDash":false},{"StartTime":44541.0,"Position":475.0,"HyperDash":false},{"StartTime":44638.0,"Position":480.0,"HyperDash":true}]},{"StartTime":44797.0,"Objects":[{"StartTime":44797.0,"Position":248.0,"HyperDash":false},{"StartTime":44858.0,"Position":285.677216,"HyperDash":false},{"StartTime":44955.0,"Position":343.0,"HyperDash":true}]},{"StartTime":45115.0,"Objects":[{"StartTime":45115.0,"Position":104.0,"HyperDash":false},{"StartTime":45194.0,"Position":104.0,"HyperDash":true}]},{"StartTime":45273.0,"Objects":[{"StartTime":45273.0,"Position":296.0,"HyperDash":false}]},{"StartTime":45432.0,"Objects":[{"StartTime":45432.0,"Position":160.0,"HyperDash":true}]},{"StartTime":45591.0,"Objects":[{"StartTime":45591.0,"Position":392.0,"HyperDash":false},{"StartTime":45652.0,"Position":379.0,"HyperDash":false},{"StartTime":45749.0,"Position":392.0,"HyperDash":true}]},{"StartTime":45908.0,"Objects":[{"StartTime":45908.0,"Position":160.0,"HyperDash":true},{"StartTime":45969.0,"Position":245.523743,"HyperDash":false},{"StartTime":46066.0,"Position":373.75,"HyperDash":true}]},{"StartTime":46226.0,"Objects":[{"StartTime":46226.0,"Position":136.0,"HyperDash":false},{"StartTime":46305.0,"Position":111.869118,"HyperDash":false},{"StartTime":46384.0,"Position":80.52514,"HyperDash":false},{"StartTime":46464.0,"Position":110.225082,"HyperDash":true}]},{"StartTime":46543.0,"Objects":[{"StartTime":46543.0,"Position":304.0,"HyperDash":false}]},{"StartTime":46702.0,"Objects":[{"StartTime":46702.0,"Position":160.0,"HyperDash":true}]},{"StartTime":46861.0,"Objects":[{"StartTime":46861.0,"Position":400.0,"HyperDash":false},{"StartTime":46922.0,"Position":400.0,"HyperDash":false},{"StartTime":47019.0,"Position":400.0,"HyperDash":true}]},{"StartTime":47178.0,"Objects":[{"StartTime":47178.0,"Position":160.0,"HyperDash":false}]},{"StartTime":47337.0,"Objects":[{"StartTime":47337.0,"Position":296.0,"HyperDash":false},{"StartTime":47398.0,"Position":314.677216,"HyperDash":false},{"StartTime":47495.0,"Position":391.0,"HyperDash":false}]},{"StartTime":47654.0,"Objects":[{"StartTime":47654.0,"Position":248.0,"HyperDash":false}]},{"StartTime":47734.0,"Objects":[{"StartTime":47734.0,"Position":304.0,"HyperDash":false}]},{"StartTime":47813.0,"Objects":[{"StartTime":47813.0,"Position":360.0,"HyperDash":true}]},{"StartTime":47972.0,"Objects":[{"StartTime":47972.0,"Position":136.0,"HyperDash":false},{"StartTime":48033.0,"Position":122.0,"HyperDash":false},{"StartTime":48130.0,"Position":136.0,"HyperDash":true}]},{"StartTime":48289.0,"Objects":[{"StartTime":48289.0,"Position":376.0,"HyperDash":false}]},{"StartTime":48448.0,"Objects":[{"StartTime":48448.0,"Position":264.0,"HyperDash":false}]},{"StartTime":48607.0,"Objects":[{"StartTime":48607.0,"Position":152.0,"HyperDash":true}]},{"StartTime":48765.0,"Objects":[{"StartTime":48765.0,"Position":392.0,"HyperDash":false},{"StartTime":48826.0,"Position":391.0,"HyperDash":false},{"StartTime":48923.0,"Position":392.0,"HyperDash":true}]},{"StartTime":49083.0,"Objects":[{"StartTime":49083.0,"Position":160.0,"HyperDash":false}]},{"StartTime":49241.0,"Objects":[{"StartTime":49241.0,"Position":304.0,"HyperDash":false},{"StartTime":49302.0,"Position":321.0,"HyperDash":false},{"StartTime":49399.0,"Position":304.0,"HyperDash":true}]},{"StartTime":49559.0,"Objects":[{"StartTime":49559.0,"Position":64.0,"HyperDash":false},{"StartTime":49620.0,"Position":76.0,"HyperDash":false},{"StartTime":49717.0,"Position":64.0,"HyperDash":true}]},{"StartTime":49877.0,"Objects":[{"StartTime":49877.0,"Position":304.0,"HyperDash":false},{"StartTime":49938.0,"Position":278.322784,"HyperDash":false},{"StartTime":50035.0,"Position":209.0,"HyperDash":true}]},{"StartTime":50194.0,"Objects":[{"StartTime":50194.0,"Position":448.0,"HyperDash":false},{"StartTime":50255.0,"Position":446.0,"HyperDash":false},{"StartTime":50352.0,"Position":448.0,"HyperDash":true}]},{"StartTime":50511.0,"Objects":[{"StartTime":50511.0,"Position":208.0,"HyperDash":false},{"StartTime":50590.0,"Position":160.5,"HyperDash":true}]},{"StartTime":50670.0,"Objects":[{"StartTime":50670.0,"Position":352.0,"HyperDash":false},{"StartTime":50731.0,"Position":369.0,"HyperDash":false},{"StartTime":50828.0,"Position":352.0,"HyperDash":true}]},{"StartTime":50988.0,"Objects":[{"StartTime":50988.0,"Position":128.0,"HyperDash":true},{"StartTime":51049.0,"Position":201.523743,"HyperDash":false},{"StartTime":51146.0,"Position":341.75,"HyperDash":true}]},{"StartTime":51305.0,"Objects":[{"StartTime":51305.0,"Position":104.0,"HyperDash":false},{"StartTime":51384.0,"Position":76.12657,"HyperDash":false},{"StartTime":51463.0,"Position":49.38173,"HyperDash":false},{"StartTime":51543.0,"Position":79.5740662,"HyperDash":true}]},{"StartTime":51623.0,"Objects":[{"StartTime":51623.0,"Position":272.0,"HyperDash":false}]},{"StartTime":51781.0,"Objects":[{"StartTime":51781.0,"Position":128.0,"HyperDash":true}]},{"StartTime":51940.0,"Objects":[{"StartTime":51940.0,"Position":368.0,"HyperDash":false},{"StartTime":52001.0,"Position":357.0,"HyperDash":false},{"StartTime":52098.0,"Position":368.0,"HyperDash":true}]},{"StartTime":52258.0,"Objects":[{"StartTime":52258.0,"Position":128.0,"HyperDash":false}]},{"StartTime":52416.0,"Objects":[{"StartTime":52416.0,"Position":272.0,"HyperDash":false},{"StartTime":52477.0,"Position":276.0,"HyperDash":false},{"StartTime":52574.0,"Position":272.0,"HyperDash":false}]},{"StartTime":52734.0,"Objects":[{"StartTime":52734.0,"Position":128.0,"HyperDash":false}]},{"StartTime":52813.0,"Objects":[{"StartTime":52813.0,"Position":184.0,"HyperDash":false}]},{"StartTime":52892.0,"Objects":[{"StartTime":52892.0,"Position":240.0,"HyperDash":true}]},{"StartTime":53051.0,"Objects":[{"StartTime":53051.0,"Position":16.0,"HyperDash":false},{"StartTime":53112.0,"Position":4.0,"HyperDash":false},{"StartTime":53209.0,"Position":16.0,"HyperDash":true}]},{"StartTime":53369.0,"Objects":[{"StartTime":53369.0,"Position":264.0,"HyperDash":false}]},{"StartTime":53527.0,"Objects":[{"StartTime":53527.0,"Position":152.0,"HyperDash":false}]},{"StartTime":53686.0,"Objects":[{"StartTime":53686.0,"Position":40.0,"HyperDash":true}]},{"StartTime":53845.0,"Objects":[{"StartTime":53845.0,"Position":280.0,"HyperDash":false},{"StartTime":53906.0,"Position":296.0,"HyperDash":false},{"StartTime":54003.0,"Position":280.0,"HyperDash":true}]},{"StartTime":54162.0,"Objects":[{"StartTime":54162.0,"Position":56.0,"HyperDash":false}]},{"StartTime":54321.0,"Objects":[{"StartTime":54321.0,"Position":184.0,"HyperDash":false},{"StartTime":54382.0,"Position":208.677216,"HyperDash":false},{"StartTime":54479.0,"Position":279.0,"HyperDash":true}]},{"StartTime":54638.0,"Objects":[{"StartTime":54638.0,"Position":32.0,"HyperDash":false},{"StartTime":54699.0,"Position":31.0,"HyperDash":false},{"StartTime":54796.0,"Position":32.0,"HyperDash":true}]},{"StartTime":54956.0,"Objects":[{"StartTime":54956.0,"Position":264.0,"HyperDash":false},{"StartTime":55017.0,"Position":287.677216,"HyperDash":false},{"StartTime":55114.0,"Position":359.0,"HyperDash":true}]},{"StartTime":55274.0,"Objects":[{"StartTime":55274.0,"Position":120.0,"HyperDash":false},{"StartTime":55353.0,"Position":120.0,"HyperDash":true}]},{"StartTime":55432.0,"Objects":[{"StartTime":55432.0,"Position":312.0,"HyperDash":false}]},{"StartTime":55591.0,"Objects":[{"StartTime":55591.0,"Position":176.0,"HyperDash":true}]},{"StartTime":55750.0,"Objects":[{"StartTime":55750.0,"Position":408.0,"HyperDash":false},{"StartTime":55811.0,"Position":402.0,"HyperDash":false},{"StartTime":55908.0,"Position":408.0,"HyperDash":true}]},{"StartTime":56067.0,"Objects":[{"StartTime":56067.0,"Position":136.0,"HyperDash":true},{"StartTime":56128.0,"Position":210.523743,"HyperDash":false},{"StartTime":56225.0,"Position":349.75,"HyperDash":true}]},{"StartTime":56385.0,"Objects":[{"StartTime":56385.0,"Position":112.0,"HyperDash":false},{"StartTime":56464.0,"Position":63.0948868,"HyperDash":false},{"StartTime":56543.0,"Position":67.97851,"HyperDash":false},{"StartTime":56623.0,"Position":103.92572,"HyperDash":true}]},{"StartTime":56702.0,"Objects":[{"StartTime":56702.0,"Position":296.0,"HyperDash":false}]},{"StartTime":56861.0,"Objects":[{"StartTime":56861.0,"Position":152.0,"HyperDash":true}]},{"StartTime":57019.0,"Objects":[{"StartTime":57019.0,"Position":392.0,"HyperDash":false},{"StartTime":57080.0,"Position":387.0,"HyperDash":false},{"StartTime":57177.0,"Position":392.0,"HyperDash":true}]},{"StartTime":57337.0,"Objects":[{"StartTime":57337.0,"Position":152.0,"HyperDash":false}]},{"StartTime":57496.0,"Objects":[{"StartTime":57496.0,"Position":296.0,"HyperDash":false},{"StartTime":57557.0,"Position":346.677216,"HyperDash":false},{"StartTime":57654.0,"Position":391.0,"HyperDash":false}]},{"StartTime":57813.0,"Objects":[{"StartTime":57813.0,"Position":248.0,"HyperDash":false}]},{"StartTime":57972.0,"Objects":[{"StartTime":57972.0,"Position":392.0,"HyperDash":true}]},{"StartTime":58131.0,"Objects":[{"StartTime":58131.0,"Position":152.0,"HyperDash":false},{"StartTime":58192.0,"Position":155.0,"HyperDash":false},{"StartTime":58289.0,"Position":152.0,"HyperDash":true}]},{"StartTime":58448.0,"Objects":[{"StartTime":58448.0,"Position":392.0,"HyperDash":false}]},{"StartTime":58607.0,"Objects":[{"StartTime":58607.0,"Position":280.0,"HyperDash":false}]},{"StartTime":58765.0,"Objects":[{"StartTime":58765.0,"Position":168.0,"HyperDash":true}]},{"StartTime":58924.0,"Objects":[{"StartTime":58924.0,"Position":392.0,"HyperDash":false}]},{"StartTime":59083.0,"Objects":[{"StartTime":59083.0,"Position":248.0,"HyperDash":false},{"StartTime":59144.0,"Position":236.0,"HyperDash":false},{"StartTime":59241.0,"Position":248.0,"HyperDash":true}]},{"StartTime":59400.0,"Objects":[{"StartTime":59400.0,"Position":488.0,"HyperDash":false},{"StartTime":59461.0,"Position":476.0,"HyperDash":false},{"StartTime":59558.0,"Position":488.0,"HyperDash":true}]},{"StartTime":59718.0,"Objects":[{"StartTime":59718.0,"Position":248.0,"HyperDash":false},{"StartTime":59779.0,"Position":233.0,"HyperDash":false},{"StartTime":59876.0,"Position":248.0,"HyperDash":true}]},{"StartTime":60035.0,"Objects":[{"StartTime":60035.0,"Position":488.0,"HyperDash":false},{"StartTime":60114.0,"Position":436.649841,"HyperDash":false},{"StartTime":60193.0,"Position":393.299683,"HyperDash":false},{"StartTime":60254.0,"Position":337.738159,"HyperDash":false},{"StartTime":60352.0,"Position":298.0,"HyperDash":false}]},{"StartTime":60511.0,"Objects":[{"StartTime":60511.0,"Position":448.0,"HyperDash":false},{"StartTime":60572.0,"Position":448.0,"HyperDash":false},{"StartTime":60669.0,"Position":448.0,"HyperDash":true}]},{"StartTime":60829.0,"Objects":[{"StartTime":60829.0,"Position":200.0,"HyperDash":true}]},{"StartTime":60988.0,"Objects":[{"StartTime":60988.0,"Position":448.0,"HyperDash":false},{"StartTime":61067.0,"Position":495.5,"HyperDash":true}]},{"StartTime":61146.0,"Objects":[{"StartTime":61146.0,"Position":304.0,"HyperDash":false},{"StartTime":61225.0,"Position":256.5,"HyperDash":true}]},{"StartTime":61305.0,"Objects":[{"StartTime":61305.0,"Position":448.0,"HyperDash":false},{"StartTime":61384.0,"Position":495.5,"HyperDash":true}]},{"StartTime":61464.0,"Objects":[{"StartTime":61464.0,"Position":304.0,"HyperDash":false},{"StartTime":61543.0,"Position":273.8691,"HyperDash":false},{"StartTime":61622.0,"Position":248.525131,"HyperDash":false},{"StartTime":61702.0,"Position":278.225067,"HyperDash":true}]},{"StartTime":61781.0,"Objects":[{"StartTime":61781.0,"Position":448.0,"HyperDash":false},{"StartTime":61860.0,"Position":503.905121,"HyperDash":false},{"StartTime":61939.0,"Position":492.021484,"HyperDash":false},{"StartTime":62019.0,"Position":456.07428,"HyperDash":true}]},{"StartTime":62099.0,"Objects":[{"StartTime":62099.0,"Position":272.0,"HyperDash":false}]},{"StartTime":62258.0,"Objects":[{"StartTime":62258.0,"Position":408.0,"HyperDash":true}]},{"StartTime":62416.0,"Objects":[{"StartTime":62416.0,"Position":168.0,"HyperDash":false}]},{"StartTime":62575.0,"Objects":[{"StartTime":62575.0,"Position":312.0,"HyperDash":false},{"StartTime":62636.0,"Position":301.0,"HyperDash":false},{"StartTime":62733.0,"Position":312.0,"HyperDash":true}]},{"StartTime":62892.0,"Objects":[{"StartTime":62892.0,"Position":72.0,"HyperDash":false},{"StartTime":62953.0,"Position":58.0,"HyperDash":false},{"StartTime":63050.0,"Position":72.0,"HyperDash":true}]},{"StartTime":63210.0,"Objects":[{"StartTime":63210.0,"Position":312.0,"HyperDash":false}]},{"StartTime":63369.0,"Objects":[{"StartTime":63369.0,"Position":176.0,"HyperDash":false},{"StartTime":63430.0,"Position":194.0,"HyperDash":false},{"StartTime":63527.0,"Position":176.0,"HyperDash":false}]},{"StartTime":63686.0,"Objects":[{"StartTime":63686.0,"Position":312.0,"HyperDash":false}]},{"StartTime":63845.0,"Objects":[{"StartTime":63845.0,"Position":176.0,"HyperDash":true}]},{"StartTime":64004.0,"Objects":[{"StartTime":64004.0,"Position":408.0,"HyperDash":false},{"StartTime":64083.0,"Position":460.8734,"HyperDash":false},{"StartTime":64162.0,"Position":462.618256,"HyperDash":false},{"StartTime":64242.0,"Position":432.4259,"HyperDash":true}]},{"StartTime":64321.0,"Objects":[{"StartTime":64321.0,"Position":240.0,"HyperDash":false}]},{"StartTime":64480.0,"Objects":[{"StartTime":64480.0,"Position":376.0,"HyperDash":true}]},{"StartTime":64638.0,"Objects":[{"StartTime":64638.0,"Position":136.0,"HyperDash":false},{"StartTime":64699.0,"Position":119.0,"HyperDash":false},{"StartTime":64796.0,"Position":136.0,"HyperDash":false}]},{"StartTime":64956.0,"Objects":[{"StartTime":64956.0,"Position":272.0,"HyperDash":true}]},{"StartTime":65115.0,"Objects":[{"StartTime":65115.0,"Position":32.0,"HyperDash":false},{"StartTime":65176.0,"Position":26.0,"HyperDash":false},{"StartTime":65273.0,"Position":32.0,"HyperDash":true}]},{"StartTime":65432.0,"Objects":[{"StartTime":65432.0,"Position":272.0,"HyperDash":false},{"StartTime":65493.0,"Position":314.677216,"HyperDash":false},{"StartTime":65590.0,"Position":367.0,"HyperDash":true}]},{"StartTime":65750.0,"Objects":[{"StartTime":65750.0,"Position":128.0,"HyperDash":false}]},{"StartTime":65908.0,"Objects":[{"StartTime":65908.0,"Position":264.0,"HyperDash":false}]},{"StartTime":66067.0,"Objects":[{"StartTime":66067.0,"Position":128.0,"HyperDash":false},{"StartTime":66128.0,"Position":140.0,"HyperDash":false},{"StartTime":66225.0,"Position":128.0,"HyperDash":false}]},{"StartTime":66385.0,"Objects":[{"StartTime":66385.0,"Position":264.0,"HyperDash":true}]},{"StartTime":66543.0,"Objects":[{"StartTime":66543.0,"Position":32.0,"HyperDash":false},{"StartTime":66604.0,"Position":19.0,"HyperDash":false},{"StartTime":66701.0,"Position":32.0,"HyperDash":true}]},{"StartTime":66861.0,"Objects":[{"StartTime":66861.0,"Position":280.0,"HyperDash":false}]},{"StartTime":67019.0,"Objects":[{"StartTime":67019.0,"Position":144.0,"HyperDash":false}]},{"StartTime":67178.0,"Objects":[{"StartTime":67178.0,"Position":280.0,"HyperDash":false},{"StartTime":67239.0,"Position":302.677216,"HyperDash":false},{"StartTime":67336.0,"Position":375.0,"HyperDash":true}]},{"StartTime":67496.0,"Objects":[{"StartTime":67496.0,"Position":136.0,"HyperDash":false}]},{"StartTime":67654.0,"Objects":[{"StartTime":67654.0,"Position":272.0,"HyperDash":false},{"StartTime":67733.0,"Position":292.355682,"HyperDash":false},{"StartTime":67812.0,"Position":317.325684,"HyperDash":false},{"StartTime":67892.0,"Position":284.836639,"HyperDash":true}]},{"StartTime":67972.0,"Objects":[{"StartTime":67972.0,"Position":96.0,"HyperDash":false},{"StartTime":68033.0,"Position":82.0,"HyperDash":false},{"StartTime":68130.0,"Position":96.0,"HyperDash":true}]},{"StartTime":68289.0,"Objects":[{"StartTime":68289.0,"Position":328.0,"HyperDash":false}]},{"StartTime":68448.0,"Objects":[{"StartTime":68448.0,"Position":192.0,"HyperDash":false}]},{"StartTime":68607.0,"Objects":[{"StartTime":68607.0,"Position":328.0,"HyperDash":true}]},{"StartTime":68765.0,"Objects":[{"StartTime":68765.0,"Position":96.0,"HyperDash":false}]},{"StartTime":68924.0,"Objects":[{"StartTime":68924.0,"Position":232.0,"HyperDash":true}]},{"StartTime":69083.0,"Objects":[{"StartTime":69083.0,"Position":472.0,"HyperDash":false},{"StartTime":69144.0,"Position":478.0,"HyperDash":false},{"StartTime":69241.0,"Position":472.0,"HyperDash":false}]},{"StartTime":69400.0,"Objects":[{"StartTime":69400.0,"Position":368.0,"HyperDash":true}]},{"StartTime":69559.0,"Objects":[{"StartTime":69559.0,"Position":152.0,"HyperDash":false}]},{"StartTime":69718.0,"Objects":[{"StartTime":69718.0,"Position":288.0,"HyperDash":false}]},{"StartTime":69877.0,"Objects":[{"StartTime":69877.0,"Position":152.0,"HyperDash":true}]},{"StartTime":70035.0,"Objects":[{"StartTime":70035.0,"Position":384.0,"HyperDash":false}]},{"StartTime":70194.0,"Objects":[{"StartTime":70194.0,"Position":248.0,"HyperDash":false},{"StartTime":70255.0,"Position":261.0,"HyperDash":false},{"StartTime":70352.0,"Position":248.0,"HyperDash":false}]},{"StartTime":70511.0,"Objects":[{"StartTime":70511.0,"Position":384.0,"HyperDash":false}]},{"StartTime":70670.0,"Objects":[{"StartTime":70670.0,"Position":248.0,"HyperDash":false},{"StartTime":70749.0,"Position":194.869125,"HyperDash":false},{"StartTime":70828.0,"Position":192.525131,"HyperDash":false},{"StartTime":70908.0,"Position":222.225082,"HyperDash":true}]},{"StartTime":70988.0,"Objects":[{"StartTime":70988.0,"Position":416.0,"HyperDash":false},{"StartTime":71049.0,"Position":426.0,"HyperDash":false},{"StartTime":71146.0,"Position":416.0,"HyperDash":false}]},{"StartTime":71226.0,"Objects":[{"StartTime":71226.0,"Position":352.0,"HyperDash":true}]},{"StartTime":71305.0,"Objects":[{"StartTime":71305.0,"Position":168.0,"HyperDash":false},{"StartTime":71384.0,"Position":120.5,"HyperDash":true}]},{"StartTime":71464.0,"Objects":[{"StartTime":71464.0,"Position":312.0,"HyperDash":false},{"StartTime":71543.0,"Position":359.5,"HyperDash":true}]},{"StartTime":71623.0,"Objects":[{"StartTime":71623.0,"Position":168.0,"HyperDash":false},{"StartTime":71684.0,"Position":140.322784,"HyperDash":false},{"StartTime":71781.0,"Position":73.0,"HyperDash":true}]},{"StartTime":71940.0,"Objects":[{"StartTime":71940.0,"Position":312.0,"HyperDash":false}]},{"StartTime":72099.0,"Objects":[{"StartTime":72099.0,"Position":168.0,"HyperDash":false},{"StartTime":72160.0,"Position":138.322784,"HyperDash":false},{"StartTime":72257.0,"Position":73.0,"HyperDash":true}]},{"StartTime":72416.0,"Objects":[{"StartTime":72416.0,"Position":312.0,"HyperDash":false},{"StartTime":72477.0,"Position":310.0,"HyperDash":false},{"StartTime":72574.0,"Position":312.0,"HyperDash":false}]},{"StartTime":72734.0,"Objects":[{"StartTime":72734.0,"Position":176.0,"HyperDash":true}]},{"StartTime":72892.0,"Objects":[{"StartTime":72892.0,"Position":416.0,"HyperDash":false}]},{"StartTime":73051.0,"Objects":[{"StartTime":73051.0,"Position":280.0,"HyperDash":false},{"StartTime":73112.0,"Position":286.0,"HyperDash":false},{"StartTime":73209.0,"Position":280.0,"HyperDash":false}]},{"StartTime":73369.0,"Objects":[{"StartTime":73369.0,"Position":416.0,"HyperDash":true}]},{"StartTime":73527.0,"Objects":[{"StartTime":73527.0,"Position":176.0,"HyperDash":false},{"StartTime":73606.0,"Position":130.644318,"HyperDash":false},{"StartTime":73685.0,"Position":130.674316,"HyperDash":false},{"StartTime":73765.0,"Position":163.163345,"HyperDash":true}]},{"StartTime":73845.0,"Objects":[{"StartTime":73845.0,"Position":352.0,"HyperDash":false},{"StartTime":73906.0,"Position":371.0,"HyperDash":false},{"StartTime":74003.0,"Position":352.0,"HyperDash":true}]},{"StartTime":74162.0,"Objects":[{"StartTime":74162.0,"Position":104.0,"HyperDash":false}]},{"StartTime":74321.0,"Objects":[{"StartTime":74321.0,"Position":240.0,"HyperDash":false},{"StartTime":74382.0,"Position":235.0,"HyperDash":false},{"StartTime":74479.0,"Position":240.0,"HyperDash":false}]},{"StartTime":74638.0,"Objects":[{"StartTime":74638.0,"Position":104.0,"HyperDash":true}]},{"StartTime":74797.0,"Objects":[{"StartTime":74797.0,"Position":344.0,"HyperDash":false}]},{"StartTime":74956.0,"Objects":[{"StartTime":74956.0,"Position":208.0,"HyperDash":false}]},{"StartTime":75115.0,"Objects":[{"StartTime":75115.0,"Position":344.0,"HyperDash":true}]},{"StartTime":75273.0,"Objects":[{"StartTime":75273.0,"Position":104.0,"HyperDash":false},{"StartTime":75334.0,"Position":104.0,"HyperDash":false},{"StartTime":75431.0,"Position":104.0,"HyperDash":false}]},{"StartTime":75591.0,"Objects":[{"StartTime":75591.0,"Position":240.0,"HyperDash":true}]},{"StartTime":75750.0,"Objects":[{"StartTime":75750.0,"Position":16.0,"HyperDash":false}]},{"StartTime":75908.0,"Objects":[{"StartTime":75908.0,"Position":152.0,"HyperDash":false}]},{"StartTime":76067.0,"Objects":[{"StartTime":76067.0,"Position":16.0,"HyperDash":false},{"StartTime":76128.0,"Position":31.0,"HyperDash":false},{"StartTime":76225.0,"Position":16.0,"HyperDash":true}]},{"StartTime":76385.0,"Objects":[{"StartTime":76385.0,"Position":256.0,"HyperDash":false},{"StartTime":76446.0,"Position":276.677216,"HyperDash":false},{"StartTime":76543.0,"Position":351.0,"HyperDash":true}]},{"StartTime":76702.0,"Objects":[{"StartTime":76702.0,"Position":112.0,"HyperDash":false}]},{"StartTime":76861.0,"Objects":[{"StartTime":76861.0,"Position":248.0,"HyperDash":false}]},{"StartTime":77019.0,"Objects":[{"StartTime":77019.0,"Position":112.0,"HyperDash":false},{"StartTime":77080.0,"Position":129.0,"HyperDash":false},{"StartTime":77177.0,"Position":112.0,"HyperDash":false}]},{"StartTime":77258.0,"Objects":[{"StartTime":77258.0,"Position":176.0,"HyperDash":true}]},{"StartTime":77337.0,"Objects":[{"StartTime":77337.0,"Position":368.0,"HyperDash":false},{"StartTime":77398.0,"Position":371.0,"HyperDash":false},{"StartTime":77495.0,"Position":368.0,"HyperDash":false}]},{"StartTime":77654.0,"Objects":[{"StartTime":77654.0,"Position":232.0,"HyperDash":false}]},{"StartTime":77813.0,"Objects":[{"StartTime":77813.0,"Position":368.0,"HyperDash":true}]},{"StartTime":77972.0,"Objects":[{"StartTime":77972.0,"Position":80.0,"HyperDash":false}]},{"StartTime":79242.0,"Objects":[{"StartTime":79242.0,"Position":64.0,"HyperDash":false},{"StartTime":79303.0,"Position":60.0,"HyperDash":false},{"StartTime":79400.0,"Position":64.0,"HyperDash":true}]},{"StartTime":79559.0,"Objects":[{"StartTime":79559.0,"Position":296.0,"HyperDash":false}]},{"StartTime":79718.0,"Objects":[{"StartTime":79718.0,"Position":160.0,"HyperDash":false}]},{"StartTime":79876.0,"Objects":[{"StartTime":79876.0,"Position":296.0,"HyperDash":false},{"StartTime":79937.0,"Position":304.0,"HyperDash":false},{"StartTime":80034.0,"Position":296.0,"HyperDash":true}]},{"StartTime":80194.0,"Objects":[{"StartTime":80194.0,"Position":64.0,"HyperDash":true}]},{"StartTime":80353.0,"Objects":[{"StartTime":80353.0,"Position":296.0,"HyperDash":false},{"StartTime":80432.0,"Position":340.5878,"HyperDash":false},{"StartTime":80511.0,"Position":367.266571,"HyperDash":false},{"StartTime":80572.0,"Position":349.239929,"HyperDash":false},{"StartTime":80670.0,"Position":312.985657,"HyperDash":false}]},{"StartTime":80749.0,"Objects":[{"StartTime":80749.0,"Position":256.0,"HyperDash":true}]},{"StartTime":80829.0,"Objects":[{"StartTime":80829.0,"Position":64.0,"HyperDash":false}]},{"StartTime":80988.0,"Objects":[{"StartTime":80988.0,"Position":200.0,"HyperDash":false}]},{"StartTime":81146.0,"Objects":[{"StartTime":81146.0,"Position":64.0,"HyperDash":false},{"StartTime":81207.0,"Position":48.0,"HyperDash":false},{"StartTime":81304.0,"Position":64.0,"HyperDash":true}]},{"StartTime":81464.0,"Objects":[{"StartTime":81464.0,"Position":296.0,"HyperDash":false},{"StartTime":81525.0,"Position":325.677216,"HyperDash":false},{"StartTime":81622.0,"Position":391.0,"HyperDash":true}]},{"StartTime":81781.0,"Objects":[{"StartTime":81781.0,"Position":152.0,"HyperDash":false},{"StartTime":81842.0,"Position":97.3227844,"HyperDash":false},{"StartTime":81939.0,"Position":57.0,"HyperDash":true}]},{"StartTime":82099.0,"Objects":[{"StartTime":82099.0,"Position":296.0,"HyperDash":false}]},{"StartTime":82257.0,"Objects":[{"StartTime":82257.0,"Position":160.0,"HyperDash":false}]},{"StartTime":82416.0,"Objects":[{"StartTime":82416.0,"Position":296.0,"HyperDash":false},{"StartTime":82477.0,"Position":292.0,"HyperDash":false},{"StartTime":82574.0,"Position":296.0,"HyperDash":true}]},{"StartTime":82734.0,"Objects":[{"StartTime":82734.0,"Position":48.0,"HyperDash":true}]},{"StartTime":82892.0,"Objects":[{"StartTime":82892.0,"Position":296.0,"HyperDash":false},{"StartTime":82971.0,"Position":253.649841,"HyperDash":false},{"StartTime":83050.0,"Position":201.299683,"HyperDash":false},{"StartTime":83111.0,"Position":162.738174,"HyperDash":false},{"StartTime":83209.0,"Position":106.0,"HyperDash":false}]},{"StartTime":83289.0,"Objects":[{"StartTime":83289.0,"Position":160.0,"HyperDash":true}]},{"StartTime":83368.0,"Objects":[{"StartTime":83368.0,"Position":352.0,"HyperDash":false}]},{"StartTime":83527.0,"Objects":[{"StartTime":83527.0,"Position":216.0,"HyperDash":false}]},{"StartTime":83686.0,"Objects":[{"StartTime":83686.0,"Position":352.0,"HyperDash":false},{"StartTime":83747.0,"Position":368.0,"HyperDash":false},{"StartTime":83844.0,"Position":352.0,"HyperDash":true}]},{"StartTime":84003.0,"Objects":[{"StartTime":84003.0,"Position":120.0,"HyperDash":false},{"StartTime":84064.0,"Position":80.3227844,"HyperDash":false},{"StartTime":84161.0,"Position":25.0,"HyperDash":true}]},{"StartTime":84321.0,"Objects":[{"StartTime":84321.0,"Position":264.0,"HyperDash":false}]},{"StartTime":84480.0,"Objects":[{"StartTime":84480.0,"Position":128.0,"HyperDash":true}]},{"StartTime":84638.0,"Objects":[{"StartTime":84638.0,"Position":368.0,"HyperDash":false}]},{"StartTime":84797.0,"Objects":[{"StartTime":84797.0,"Position":464.0,"HyperDash":false}]},{"StartTime":84956.0,"Objects":[{"StartTime":84956.0,"Position":464.0,"HyperDash":false}]},{"StartTime":85115.0,"Objects":[{"StartTime":85115.0,"Position":368.0,"HyperDash":false}]},{"StartTime":85273.0,"Objects":[{"StartTime":85273.0,"Position":232.0,"HyperDash":true}]},{"StartTime":85432.0,"Objects":[{"StartTime":85432.0,"Position":472.0,"HyperDash":false},{"StartTime":85493.0,"Position":486.0,"HyperDash":false},{"StartTime":85590.0,"Position":472.0,"HyperDash":true}]},{"StartTime":85750.0,"Objects":[{"StartTime":85750.0,"Position":232.0,"HyperDash":false},{"StartTime":85811.0,"Position":219.0,"HyperDash":false},{"StartTime":85908.0,"Position":232.0,"HyperDash":false}]},{"StartTime":86067.0,"Objects":[{"StartTime":86067.0,"Position":368.0,"HyperDash":false}]},{"StartTime":86226.0,"Objects":[{"StartTime":86226.0,"Position":232.0,"HyperDash":false},{"StartTime":86287.0,"Position":194.322784,"HyperDash":false},{"StartTime":86384.0,"Position":137.0,"HyperDash":false}]},{"StartTime":86543.0,"Objects":[{"StartTime":86543.0,"Position":272.0,"HyperDash":false},{"StartTime":86604.0,"Position":296.677216,"HyperDash":false},{"StartTime":86701.0,"Position":367.0,"HyperDash":true}]},{"StartTime":86861.0,"Objects":[{"StartTime":86861.0,"Position":128.0,"HyperDash":false}]},{"StartTime":87019.0,"Objects":[{"StartTime":87019.0,"Position":264.0,"HyperDash":true}]},{"StartTime":87178.0,"Objects":[{"StartTime":87178.0,"Position":24.0,"HyperDash":false}]},{"StartTime":87337.0,"Objects":[{"StartTime":87337.0,"Position":24.0,"HyperDash":false}]},{"StartTime":87496.0,"Objects":[{"StartTime":87496.0,"Position":160.0,"HyperDash":false}]},{"StartTime":87654.0,"Objects":[{"StartTime":87654.0,"Position":24.0,"HyperDash":true}]},{"StartTime":87813.0,"Objects":[{"StartTime":87813.0,"Position":272.0,"HyperDash":true}]},{"StartTime":87972.0,"Objects":[{"StartTime":87972.0,"Position":24.0,"HyperDash":false}]},{"StartTime":88131.0,"Objects":[{"StartTime":88131.0,"Position":295.0,"HyperDash":false},{"StartTime":88210.0,"Position":311.0,"HyperDash":false},{"StartTime":88289.0,"Position":17.0,"HyperDash":false},{"StartTime":88368.0,"Position":467.0,"HyperDash":false},{"StartTime":88448.0,"Position":30.0,"HyperDash":false},{"StartTime":88527.0,"Position":218.0,"HyperDash":false},{"StartTime":88606.0,"Position":26.0,"HyperDash":false},{"StartTime":88686.0,"Position":16.0,"HyperDash":false},{"StartTime":88765.0,"Position":248.0,"HyperDash":false},{"StartTime":88844.0,"Position":100.0,"HyperDash":false},{"StartTime":88924.0,"Position":24.0,"HyperDash":false},{"StartTime":89003.0,"Position":66.0,"HyperDash":false},{"StartTime":89082.0,"Position":97.0,"HyperDash":false},{"StartTime":89162.0,"Position":267.0,"HyperDash":false},{"StartTime":89241.0,"Position":116.0,"HyperDash":false},{"StartTime":89320.0,"Position":451.0,"HyperDash":false},{"StartTime":89400.0,"Position":414.0,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3152510.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3152510.osu new file mode 100644 index 0000000000..9d65d5cc19 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3152510.osu @@ -0,0 +1,468 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 +Mode: 2 + +[Difficulty] +HPDrainRate:6 +CircleSize:4.2 +OverallDifficulty:9.2 +ApproachRate:9.2 +SliderMultiplier:1.9 +SliderTickRate:2 + +[Events] +//Background and Video events +//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] +512,317.460317460317,4,2,1,70,1,0 +2972,-100,4,2,1,5,0,0 +3051,-100,4,2,1,70,0,0 +8051,-100,4,2,1,5,0,0 +8131,-100,4,2,1,70,0,0 +10591,-100,4,2,1,5,0,0 +10670,-100,4,2,1,60,0,0 +12019,-100,4,2,1,5,0,0 +12099,-100,4,2,1,60,0,0 +12654,-100,4,2,1,5,0,0 +12734,-100,4,2,1,60,0,0 +14559,-100,4,2,1,5,0,0 +14638,-100,4,2,1,60,0,0 +17734,-100,4,2,1,5,0,0 +17813,-100,4,2,1,60,0,0 +21385,-100,4,2,1,5,0,0 +21464,-100,4,2,1,60,0,0 +22178,-100,4,2,1,5,0,0 +22257,-100,4,2,1,60,0,0 +30988,-100,4,2,1,50,0,0 +40829,-44.4444444444445,4,2,1,80,0,0 +41067,-44.4444444444445,4,2,1,5,0,0 +41146,-100,4,2,1,80,0,1 +41385,-100,4,2,1,5,0,1 +41464,-100,4,2,1,80,0,1 +45908,-44.4444444444445,4,2,1,80,0,1 +46146,-44.4444444444445,4,2,1,5,0,1 +46226,-100,4,2,1,80,0,1 +46464,-100,4,2,1,5,0,1 +46543,-100,4,2,1,80,0,1 +50988,-44.4444444444445,4,2,1,80,0,1 +51226,-44.4444444444445,4,2,1,5,0,1 +51305,-100,4,2,1,80,0,1 +51543,-100,4,2,1,5,0,1 +51622,-100,4,2,1,80,0,1 +56067,-44.4444444444445,4,2,1,80,0,1 +56305,-44.4444444444445,4,2,1,5,0,1 +56385,-100,4,2,1,80,0,1 +56623,-100,4,2,1,5,0,1 +56702,-100,4,2,1,80,0,1 +61464,-100,4,2,1,70,0,0 +63607,-100,4,2,1,5,0,0 +63686,-100,4,2,1,80,0,0 +66305,-100,4,2,1,5,0,0 +66384,-100,4,2,1,80,0,0 +77972,-100,4,2,1,60,0,0 +79242,-100,4,2,1,70,0,0 +84321,-100,4,2,1,60,0,0 +85670,-100,4,2,1,5,0,0 +85750,-100,4,2,1,60,0,0 +85988,-100,4,2,1,5,0,0 +86068,-100,4,2,1,60,0,0 +88131,-100,4,2,1,50,0,0 +88289,-100,4,2,1,45,0,0 +88448,-100,4,2,1,40,0,0 +88607,-100,4,2,1,35,0,0 +88765,-100,4,2,1,30,0,0 +88924,-100,4,2,1,25,0,0 +89083,-100,4,2,1,20,0,0 +89242,-100,4,2,1,15,0,0 +89400,-100,4,2,1,10,0,0 + +[HitObjects] +368,312,512,6,0,L|368:200,1,95,6|0,3:2|0:2,0:0:0:0: +136,152,829,1,8,0:2:0:0: +272,152,988,1,8,0:2:0:0: +136,192,1146,2,0,L|136:304,1,95,0|0,3:2|3:2,0:2:0:0: +368,96,1464,1,8,0:2:0:0: +136,256,1623,6,0,P|64:208|136:152,1,190,2|0,3:2|3:2,0:2:0:0: +176,144,2019,1,0,3:2:0:0: +368,160,2099,1,8,0:2:0:0: +232,112,2258,1,8,0:2:0:0: +368,224,2416,2,0,L|368:344,1,95,0|0,3:2|3:2,0:0:0:0: +136,152,2734,2,0,L|32:152,1,95,8|0,0:2|0:0,0:2:0:0: +280,176,3051,6,0,L|384:176,1,95,2|0,3:2|3:2,0:2:0:0: +136,96,3369,1,8,0:2:0:0: +272,96,3527,1,8,0:2:0:0: +136,160,3686,2,0,L|136:280,1,95,0|0,3:2|3:2,0:0:0:0: +384,56,4004,1,8,0:2:0:0: +136,216,4162,6,0,L|344:216,1,190,2|0,3:2|3:2,0:2:0:0: +272,168,4559,1,0,3:2:0:0: +80,136,4638,1,8,0:2:0:0: +216,96,4797,1,8,0:2:0:0: +80,192,4956,2,0,L|80:304,1,95,0|0,3:2|3:2,0:0:0:0: +312,144,5273,2,0,L|192:144,1,95,8|0,0:2|0:2,0:2:0:0: +456,184,5591,6,0,L|456:80,1,95,2|0,3:2|3:2,0:0:0:0: +216,264,5908,1,8,0:2:0:0: +352,264,6067,1,8,0:2:0:0: +216,264,6226,2,0,L|216:168,1,95,0|0,3:2|3:2,0:2:0:0: +456,144,6543,1,8,0:2:0:0: +216,184,6702,6,0,P|152:128|216:64,1,190,2|0,3:2|3:2,0:2:0:0: +264,56,7099,1,0,3:2:0:0: +456,184,7178,1,8,0:2:0:0: +320,152,7337,1,8,0:2:0:0: +456,224,7496,2,0,L|456:320,1,95,0|0,3:2|3:2,0:2:0:0: +216,192,7813,2,0,L|112:192,1,95,8|0,0:2|0:0,0:2:0:0: +368,184,8131,6,0,L|368:80,1,95,2|0,3:2|3:2,0:0:0:0: +128,272,8448,1,8,0:2:0:0: +264,264,8607,1,8,0:2:0:0: +128,216,8765,2,0,L|128:120,1,95,0|0,3:2|3:2,0:0:0:0: +368,136,9083,1,8,0:2:0:0: +128,272,9242,6,0,L|344:272,1,190,2|0,3:2|3:2,0:2:0:0: +264,224,9638,1,0,3:2:0:0: +72,144,9718,1,8,0:2:0:0: +208,128,9877,1,8,0:2:0:0: +72,200,10035,2,0,L|72:312,1,95,0|0,3:2|3:2,0:2:0:0: +312,288,10353,2,0,L|208:288,1,95,8|0,0:2|0:0,0:2:0:0: +464,192,10670,6,0,L|464:88,1,95,2|0,3:2|0:0,0:2:0:0: +224,192,10988,2,0,L|224:80,1,95,8|0,0:2|0:0,0:2:0:0: +360,200,11305,1,0,0:2:0:0: +224,192,11464,1,0,0:2:0:0: +464,320,11623,1,8,0:2:0:0: +328,264,11781,6,0,L|328:168,1,95,2|0,3:2|0:0,0:2:0:0: +464,232,12099,2,0,L|464:128,1,95,0|8,3:2|0:2,0:2:0:0: +328,184,12416,2,0,L|432:184,1,95,2|0,0:2|0:0,0:2:0:0: +288,120,12734,1,0,0:2:0:0: +424,128,12892,2,0,L|424:16,1,95,8|0,0:2|0:2,0:0:0:0: +192,192,13210,6,0,L|192:88,1,95,2|0,3:2|0:0,0:2:0:0: +424,200,13527,2,0,L|424:88,1,95,8|0,0:2|0:2,0:2:0:0: +288,176,13845,1,0,0:2:0:0: +424,176,14004,1,0,0:2:0:0: +184,288,14162,1,8,0:2:0:0: +320,248,14321,6,0,L|320:136,1,95,2|0,3:2|0:0,0:2:0:0: +88,176,14638,2,0,L|88:72,1,95,0|8,3:2|0:2,0:0:0:0: +224,176,14956,1,0,0:2:0:0: +88,224,15115,2,0,L|88:128,1,95,2|0,0:2|0:0,0:0:0:0: +328,224,15432,2,0,L|424:224,1,95,8|0,0:2|0:0,0:0:0:0: +192,184,15750,5,2,3:2:0:0: +328,168,15908,1,0,0:0:0:0: +192,240,16067,2,0,L|80:240,1,95,8|0,0:2|0:2,0:2:0:0: +232,168,16385,1,2,0:2:0:0: +96,144,16543,1,0,0:2:0:0: +336,288,16702,1,8,0:2:0:0: +200,256,16861,6,0,L|200:152,1,95,2|0,3:2|0:2,0:2:0:0: +440,168,17178,1,0,3:2:0:0: +304,160,17337,1,8,0:2:0:0: +408,160,17496,2,0,L|504:160,1,95 +360,192,17813,1,0,0:2:0:0: +496,144,17972,2,0,L|496:40,1,95,8|0,0:2|0:2,0:0:0:0: +256,288,18289,6,0,L|128:288,1,95,2|0,3:2|0:0,0:2:0:0: +392,256,18607,2,0,L|392:152,1,95,8|0,0:2|0:2,0:2:0:0: +256,224,18924,1,0,0:2:0:0: +392,224,19083,1,0,0:2:0:0: +152,288,19242,1,8,0:2:0:0: +288,224,19400,6,0,L|288:120,1,95,2|0,3:2|0:0,0:2:0:0: +48,192,19718,2,0,L|48:96,1,95 +168,168,20035,1,0,0:0:0:0: +48,248,20194,2,0,L|344:248,1,285 +88,320,20829,6,0,L|88:224,1,95,6|0,3:2|0:0,0:2:0:0: +232,176,21146,2,0,L|232:80,1,95,8|0,0:2|0:0,0:2:0:0: +88,176,21464,2,0,L|200:176,1,95,0|0,0:2|3:2,0:0:0:0: +320,168,21781,1,8,0:2:0:0: +184,312,21940,6,0,L|184:200,1,95,2|0,3:2|0:0,0:2:0:0: +320,224,22258,2,0,L|320:128,1,95,0|8,3:2|0:2,0:2:0:0: +184,336,22575,2,0,L|184:208,1,95,2|0,3:2|0:0,0:2:0:0: +320,280,22892,1,0,3:2:0:0: +184,264,23051,2,0,L|80:264,1,95,8|0,0:2|0:2,0:2:0:0: +328,216,23369,6,0,L|488:216,1,142.5,6|2,3:2|3:2,0:0:0:0: +416,160,23686,1,0,0:0:0:0: +280,120,23845,2,0,L|184:120,2,95,2|2|0,3:2|0:2|3:2,0:0:0:0: +424,232,24321,1,8,0:2:0:0: +288,176,24480,6,0,L|480:176,1,190,2|2,3:2|3:2,0:2:0:0: +360,120,24956,1,8,0:2:0:0: +224,280,25115,1,0,3:2:0:0: +360,224,25273,2,0,L|360:176,1,47.5,0|0,3:2|3:0,3:0:0:0: +288,152,25432,2,0,L|288:88,1,47.5,0|0,3:0|3:0,3:0:0:0: +448,176,25591,2,0,L|448:56,1,95,8|0,0:2|0:0,0:0:0:0: +208,312,25908,6,0,L|96:312,1,95,2|0,3:2|3:2,0:2:0:0: +248,240,26226,2,0,L|352:240,1,95,8|0,0:2|3:2,0:2:0:0: +208,184,26543,2,0,L|208:80,1,95,0|0,0:2|3:2,0:0:0:0: +344,80,26861,1,8,0:2:0:0: +208,240,27019,6,0,L|104:240,1,95,2|0,3:2|0:2,0:2:0:0: +248,176,27337,2,0,L|352:176,1,95,0|8,3:2|0:2,0:2:0:0: +208,80,27654,1,0,0:2:0:0: +344,248,27813,1,0,3:2:0:0: +208,152,27972,1,0,3:2:0:0: +344,152,28131,2,0,L|456:152,1,95,8|0,0:2|0:2,0:2:0:0: +208,216,28448,6,0,L|48:216,1,142.5,6|2,3:2|3:2,0:2:0:0: +120,160,28765,1,0,0:0:0:0: +256,120,28924,2,0,L|352:120,2,95,2|0|0,3:2|0:2|3:2,0:0:0:0: +112,232,29400,1,8,0:2:0:0: +248,176,29559,6,0,L|56:176,1,190,2|2,3:2|3:2,0:2:0:0: +192,128,30035,1,0,0:0:0:0: +328,184,30194,1,2,3:2:0:0: +192,200,30353,2,0,L|192:104,1,95,8|0,0:2|3:2,0:0:0:0: +432,184,30670,2,0,L|368:184,1,47.5,8|8,0:2|0:2,0:0:0:0: +192,256,30829,2,0,L|136:256,1,47.5,8|8,0:2|0:2,0:0:0:0: +336,304,30988,6,0,L|336:192,1,95,2|0,3:2|0:2,0:2:0:0: +208,176,31305,2,0,L|208:80,1,95,0|0,3:2|0:2,0:2:0:0: +80,192,31623,2,0,L|80:288,1,95,0|0,3:2|0:2,0:2:0:0: +208,224,31940,1,0,3:2:0:0: +80,192,32099,6,0,L|184:192,1,95,0|2,0:2|3:2,0:0:0:0: +296,176,32416,2,0,L|296:56,1,95,0|0,0:2|3:2,0:0:0:0: +176,128,32734,2,0,L|176:24,1,95,0|0,0:2|3:2,0:0:0:0: +296,224,33051,2,0,L|184:224,2,95,0|0|0,0:2|3:2|0:2,0:0:0:0: +48,144,33527,5,0,3:2:0:0: +160,144,33686,1,2,0:2:0:0: +272,144,33845,1,0,3:2:0:0: +160,144,34004,1,2,0:2:0:0: +304,272,34162,2,0,P|376:216|304:168,1,190,0|0,3:2|3:2,0:0:0:0: +184,160,34638,6,0,L|408:160,1,190,2|2,0:2|0:2,0:2:0:0: +440,160,35035,1,0,0:0:0:0: +376,120,35115,1,0,3:2:0:0: +224,248,35273,1,0,0:2:0:0: +368,184,35432,2,0,P|440:136|368:88,1,190,0|0,3:2|3:2,0:2:0:0: +288,80,35908,1,0,0:2:0:0: +72,328,36067,5,4,3:2:0:0: +16,296,36146,1,0,3:0:0:0: +16,240,36226,1,0,3:0:0:0: +72,208,36305,1,0,3:0:0:0: +264,168,36385,1,8,3:0:0:0: +328,168,36464,1,0,3:0:0:0: +264,168,36543,1,0,3:0:0:0: +200,168,36623,1,0,3:0:0:0: +392,272,36702,6,0,L|440:272,1,47.5,8|0,3:0|3:0,0:0:0:0: +232,280,36861,2,0,L|224:216,1,47.5,0|0,3:0|3:0,0:0:0:0: +304,208,37019,2,0,L|320:144,1,47.5,0|0,3:0|3:0,0:0:0:0: +104,96,37178,2,0,L|40:96,1,47.5,0|0,3:0|3:0,0:0:0:0: +264,344,37337,6,0,L|280:296,1,47.5,8|0,3:0|3:0,0:0:0:0: +208,264,37496,2,0,L|200:208,1,47.5,0|0,3:0|3:0,0:0:0:0: +392,192,37654,1,8,3:0:0:0: +448,152,37734,1,0,3:0:0:0: +448,96,37813,1,0,3:0:0:0: +392,64,37892,1,0,3:0:0:0: +192,192,37972,6,0,L|272:192,1,47.5,8|0,3:0|3:0,0:0:0:0: +410,263,38131,2,0,L|458:263,1,47.5,0|0,3:0|3:0,0:0:0:0: +264,160,38289,2,0,L|208:160,1,47.5,8|0,0:0|3:0,0:0:0:0: +448,208,38448,2,0,L|496:208,1,47.5,0|0,3:0|3:0,0:0:0:0: +296,224,38607,5,0,3:2:0:0: +440,152,38924,1,0,3:2:0:0: +296,160,39242,1,0,3:2:0:0: +152,128,39559,1,0,3:2:0:0: +352,264,39877,6,0,L|256:256,5,95 +432,192,40829,6,0,L|184:192,1,213.750008153916,2|0,0:2|0:0,0:2:0:0: +440,264,41146,6,0,P|488:216|440:168,1,142.5,6|0,3:2|0:0,0:2:0:0: +256,168,41464,1,8,0:2:0:0: +400,128,41623,1,8,0:2:0:0: +168,248,41781,2,0,L|168:152,1,95,0|0,3:2|3:2,0:0:0:0: +400,192,42099,1,8,0:2:0:0: +256,136,42258,6,0,L|256:32,1,95,2|0,3:2|0:2,0:2:0:0: +400,248,42575,1,0,3:2:0:0: +256,200,42734,1,8,0:2:0:0: +488,192,42892,2,0,L|488:96,1,95,8|0,0:2|3:2,0:2:0:0: +256,136,43210,1,0,3:2:0:0: +368,136,43369,1,8,0:2:0:0: +480,136,43527,1,0,0:2:0:0: +256,136,43686,6,0,L|136:136,1,95,2|0,3:2|3:2,0:0:0:0: +392,296,44004,1,8,0:2:0:0: +248,248,44162,2,0,L|248:136,1,95,8|0,0:2|3:2,0:0:0:0: +480,184,44480,2,0,L|480:80,1,95,0|8,3:2|0:2,0:0:0:0: +248,248,44797,6,0,L|352:248,1,95,2|0,3:2|0:0,0:2:0:0: +104,176,45115,2,0,L|104:104,1,47.5,0|0,3:2|3:2,0:0:0:0: +296,176,45273,1,8,0:2:0:0: +160,112,45432,1,8,0:2:0:0: +392,200,45591,2,0,L|392:88,1,95,0|0,3:2|3:2,0:0:0:0: +160,176,45908,2,0,L|376:176,1,213.750008153916,10|0,0:2|0:0,0:2:0:0: +136,288,46226,6,0,P|80:232|136:192,1,142.5,6|0,3:2|0:0,0:2:0:0: +304,192,46543,1,8,0:2:0:0: +160,128,46702,1,8,0:2:0:0: +400,296,46861,2,0,L|400:192,1,95,0|0,3:2|3:2,0:0:0:0: +160,72,47178,1,8,0:2:0:0: +296,72,47337,6,0,L|408:72,1,95,2|0,3:2|0:2,0:2:0:0: +248,168,47654,1,0,3:2:0:0: +304,152,47734,1,0,3:2:0:0: +360,128,47813,1,8,0:2:0:0: +136,224,47972,2,0,L|136:128,1,95,8|0,0:2|3:2,0:0:0:0: +376,64,48289,1,0,3:2:0:0: +264,64,48448,1,8,0:2:0:0: +152,64,48607,1,0,0:2:0:0: +392,168,48765,6,0,L|392:72,1,95,2|0,3:2|3:2,0:2:0:0: +160,320,49083,1,8,0:2:0:0: +304,272,49241,2,0,L|304:160,1,95,8|0,0:2|3:2,0:2:0:0: +64,208,49559,2,0,L|64:112,1,95,0|8,3:2|0:2,0:0:0:0: +304,272,49877,6,0,L|200:272,1,95,2|0,3:2|0:0,0:2:0:0: +448,192,50194,2,0,L|448:80,1,95,2|8,3:2|0:2,0:2:0:0: +208,96,50511,2,0,L|144:96,1,47.5,8|0,0:2|0:0,0:0:0:0: +352,96,50670,2,0,L|352:200,1,95,0|0,3:2|3:2,0:2:0:0: +128,160,50988,2,0,L|360:160,1,213.750008153916,2|0,3:2|0:0,0:2:0:0: +104,288,51305,6,0,P|48:240|104:192,1,142.5,6|0,0:2|0:0,0:2:0:0: +272,176,51623,1,8,0:2:0:0: +128,120,51781,1,8,0:2:0:0: +368,280,51940,2,0,L|368:176,1,95,0|0,3:2|3:2,0:0:0:0: +128,184,52258,1,8,0:2:0:0: +272,184,52416,6,0,L|272:80,1,95,2|0,3:2|0:2,0:2:0:0: +128,120,52734,1,0,3:2:0:0: +184,112,52813,1,0,3:2:0:0: +240,96,52892,1,8,0:2:0:0: +16,312,53051,2,0,L|16:208,1,95,8|0,0:2|3:2,0:0:0:0: +264,168,53369,1,0,3:2:0:0: +152,168,53527,1,8,0:2:0:0: +40,168,53686,1,0,0:2:0:0: +280,256,53845,6,0,L|280:136,1,95,2|0,3:2|3:2,0:0:0:0: +56,240,54162,1,8,0:2:0:0: +184,232,54321,2,0,L|304:232,1,95,8|0,0:2|3:2,0:0:0:0: +32,320,54638,2,0,L|32:224,1,95,0|8,3:2|0:2,0:0:0:0: +264,248,54956,6,0,L|368:248,1,95,2|0,3:2|0:0,0:2:0:0: +120,176,55274,2,0,L|120:104,1,47.5,2|0,3:2|0:0,0:0:0:0: +312,176,55432,1,8,0:2:0:0: +176,112,55591,1,8,0:2:0:0: +408,200,55750,2,0,L|408:88,1,95,0|0,0:0|3:2,0:0:0:0: +136,288,56067,2,0,L|352:288,1,213.750008153916,10|0,0:2|0:0,0:2:0:0: +112,224,56385,6,0,P|64:176|112:128,1,142.5,6|0,3:2|0:0,0:2:0:0: +296,192,56702,1,8,0:2:0:0: +152,128,56861,1,8,0:2:0:0: +392,296,57019,2,0,L|392:192,1,95,0|0,3:2|3:2,0:2:0:0: +152,184,57337,1,8,0:2:0:0: +296,192,57496,6,0,L|416:192,1,95,2|0,3:2|0:2,0:2:0:0: +248,120,57813,1,0,3:2:0:0: +392,80,57972,1,8,0:2:0:0: +152,288,58131,2,0,L|152:192,1,95,8|0,0:2|3:2,0:2:0:0: +392,184,58448,1,0,3:2:0:0: +280,192,58607,1,8,0:2:0:0: +168,192,58765,1,0,0:2:0:0: +392,272,58924,5,2,3:2:0:0: +248,224,59083,2,0,L|248:120,1,95,0|8,3:2|0:2,0:0:0:0: +488,192,59400,2,0,L|488:96,1,95,10|0,0:2|3:2,0:2:0:0: +248,160,59718,2,0,L|248:56,1,95,2|8,3:2|0:2,0:2:0:0: +488,256,60035,6,0,L|280:256,1,190,2|2,3:2|3:2,0:0:0:0: +448,336,60511,2,0,L|448:232,1,95,2|0,0:2|3:2,0:0:0:0: +200,200,60829,1,8,0:2:0:0: +448,336,60988,2,0,L|504:336,1,47.5,8|8,0:2|0:2,0:0:0:0: +304,208,61146,2,0,L|224:208,1,47.5,8|8,0:2|0:2,0:0:0:0: +448,280,61305,2,0,L|496:280,1,47.5,8|8,0:2|0:2,0:0:0:0: +304,288,61464,6,0,P|248:232|304:192,1,142.5,6|0,3:2|0:0,0:2:0:0: +448,224,61781,2,0,P|496:176|448:128,1,142.5,8|0,0:2|0:0,0:2:0:0: +272,184,62099,1,0,3:2:0:0: +408,128,62258,1,0,3:2:0:0: +168,200,62416,1,8,0:2:0:0: +312,152,62575,6,0,L|312:48,1,95,2|0,3:2|3:2,0:2:0:0: +72,144,62892,2,0,L|72:32,1,95,0|8,0:2|0:2,0:2:0:0: +312,304,63210,1,0,0:2:0:0: +176,232,63369,2,0,L|176:128,1,95,0|0,3:2|0:0,0:2:0:0: +312,232,63686,1,8,0:2:0:0: +176,232,63845,1,0,0:2:0:0: +408,232,64004,6,0,P|464:184|408:136,1,142.5,2|0,3:2|0:0,0:2:0:0: +240,120,64321,1,8,0:2:0:0: +376,64,64480,1,0,0:2:0:0: +136,272,64638,2,0,L|136:168,1,95,0|0,3:2|0:2,0:0:0:0: +272,288,64956,1,8,0:2:0:0: +32,192,65115,6,0,L|32:88,1,95,2|0,3:2|3:2,0:2:0:0: +272,136,65432,2,0,L|368:136,1,95,0|8,0:2|0:2,0:2:0:0: +128,240,65750,1,0,0:2:0:0: +264,192,65908,1,0,3:2:0:0: +128,184,66067,2,0,L|128:80,1,95,8|0,0:2|0:0,0:2:0:0: +264,128,66385,1,10,0:2:0:0: +32,144,66543,6,0,L|32:40,1,95,0|0,0:0|0:2,0:0:0:0: +280,240,66861,1,8,0:2:0:0: +144,240,67019,1,0,0:2:0:0: +280,240,67178,2,0,L|384:240,1,95,0|0,3:2|0:2,0:0:0:0: +136,104,67496,1,8,0:2:0:0: +272,136,67654,6,0,P|320:80|272:32,1,142.5,2|0,3:2|0:0,0:2:0:0: +96,80,67972,2,0,L|96:176,1,95,0|8,0:2|0:2,0:2:0:0: +328,232,68289,1,0,0:2:0:0: +192,224,68448,1,0,3:2:0:0: +328,232,68607,1,0,0:2:0:0: +96,152,68765,1,8,0:2:0:0: +232,136,68924,1,2,3:2:0:0: +472,296,69083,6,0,L|472:176,1,95,0|0,3:2|0:2,0:0:0:0: +368,168,69400,1,8,0:2:0:0: +152,192,69559,1,0,0:2:0:0: +288,160,69718,1,0,3:2:0:0: +152,144,69877,1,0,0:2:0:0: +384,184,70035,1,8,0:2:0:0: +248,168,70194,6,0,L|248:56,1,95,2|0,3:2|0:0,0:2:0:0: +384,120,70511,1,0,0:2:0:0: +248,112,70670,2,0,P|192:56|248:16,1,142.5,8|0,0:2|0:0,0:2:0:0: +416,128,70988,2,0,L|416:24,1,95,0|8,3:2|0:2,0:0:0:0: +352,128,71226,1,8,0:2:0:0: +168,192,71305,2,0,L|96:192,1,47.5,8|8,0:2|0:2,0:0:0:0: +312,208,71464,2,0,L|384:208,1,47.5,8|8,0:2|0:2,0:0:0:0: +168,272,71623,6,0,L|64:272,1,95,6|0,3:2|0:2,0:2:0:0: +312,312,71940,1,8,0:2:0:0: +168,248,72099,2,0,L|56:248,1,95,0|0,3:2|3:2,0:2:0:0: +312,200,72416,2,0,L|312:88,1,95,0|8,0:2|0:2,0:2:0:0: +176,80,72734,1,2,3:2:0:0: +416,264,72892,5,0,3:2:0:0: +280,192,73051,2,0,L|280:80,1,95,0|8,0:2|0:2,0:0:0:0: +416,200,73369,1,0,0:2:0:0: +176,184,73527,2,0,P|128:128|176:80,1,142.5,0|0,3:2|0:0,0:2:0:0: +352,192,73845,2,0,L|352:88,1,95,8|2,0:2|3:2,0:0:0:0: +104,192,74162,5,0,3:2:0:0: +240,144,74321,2,0,L|240:48,1,95,0|8,0:2|0:2,0:2:0:0: +104,104,74638,1,0,0:2:0:0: +344,304,74797,1,0,3:2:0:0: +208,256,74956,1,0,0:2:0:0: +344,240,75115,1,8,0:2:0:0: +104,184,75273,6,0,L|104:80,1,95,2|0,3:2|3:2,0:2:0:0: +240,72,75591,1,0,0:2:0:0: +16,312,75750,1,8,0:2:0:0: +152,320,75908,1,0,0:2:0:0: +16,264,76067,2,0,L|16:168,1,95,0|0,3:2|0:2,0:0:0:0: +256,192,76385,2,0,L|376:192,1,95,10|0,0:2|0:0,0:2:0:0: +112,128,76702,5,0,3:2:0:0: +248,120,76861,1,8,0:2:0:0: +112,176,77019,2,0,L|112:280,1,95,0|0,0:2|3:2,0:0:0:0: +176,176,77258,1,0,3:2:0:0: +368,176,77337,2,0,L|368:80,1,95,8|0,0:2|0:2,0:2:0:0: +232,152,77654,1,0,0:2:0:0: +368,160,77813,1,0,0:2:0:0: +80,96,77972,5,10,0:2:0:0: +64,296,79242,6,0,L|64:184,1,95,6|0,3:2|3:2,0:0:0:0: +296,136,79559,1,8,0:2:0:0: +160,136,79718,1,8,0:2:0:0: +296,176,79876,2,0,L|296:288,1,95,0|0,3:2|3:2,0:2:0:0: +64,80,80194,1,8,0:2:0:0: +296,240,80353,6,0,P|368:192|296:136,1,190,2|0,3:2|3:2,0:2:0:0: +256,128,80749,1,0,3:2:0:0: +64,144,80829,1,8,0:2:0:0: +200,96,80988,1,8,0:2:0:0: +64,208,81146,2,0,L|64:328,1,95,0|0,3:2|3:2,0:2:0:0: +296,136,81464,2,0,L|400:136,1,95,8|0,0:2|0:2,0:2:0:0: +152,160,81781,6,0,L|48:160,1,95,2|0,3:2|3:2,0:2:0:0: +296,80,82099,1,8,0:2:0:0: +160,80,82257,1,8,0:2:0:0: +296,144,82416,2,0,L|296:264,1,95,0|0,3:2|3:2,0:0:0:0: +48,40,82734,1,8,0:2:0:0: +296,200,82892,6,0,L|88:200,1,190,2|0,3:2|3:2,0:2:0:0: +160,152,83289,1,0,3:2:0:0: +352,120,83368,1,8,0:2:0:0: +216,80,83527,1,8,0:2:0:0: +352,176,83686,2,0,L|352:288,1,95,0|0,3:2|3:2,0:2:0:0: +120,128,84003,2,0,L|16:128,1,95,8|0,0:2|0:2,0:0:0:0: +264,232,84321,5,10,0:2:0:0: +128,152,84480,1,8,0:2:0:0: +368,320,84638,1,0,3:2:0:0: +464,272,84797,1,8,0:2:0:0: +464,184,84956,1,0,3:2:0:0: +368,136,85115,1,0,0:2:0:0: +232,104,85273,1,8,0:2:0:0: +472,344,85432,6,0,L|472:240,1,95,10|0,0:2|0:0,0:2:0:0: +232,160,85750,2,0,L|232:40,1,95,10|0,0:2|0:0,0:2:0:0: +368,144,86067,1,8,3:2:0:0: +232,208,86226,2,0,L|136:208,1,95,0|0,3:2|0:2,0:2:0:0: +272,64,86543,2,0,L|400:64,1,95,0|0,3:2|0:2,0:2:0:0: +128,320,86861,5,10,0:0:0:0: +264,272,87019,1,8,0:0:0:0: +24,224,87178,1,2,3:0:0:0: +24,128,87337,1,2,0:0:0:0: +160,104,87496,1,8,3:0:0:0: +24,104,87654,1,0,3:0:0:0: +272,144,87813,1,8,0:0:0:0: +24,56,87972,5,8,0:0:0:0: +256,192,88131,12,0,89400,0:0:0:0: diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3227428-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3227428-expected-conversion.json new file mode 100644 index 0000000000..bd1c6d658f --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3227428-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":22.0,"Objects":[{"StartTime":22.0,"Position":206.0,"HyperDash":false}]},{"StartTime":362.0,"Objects":[{"StartTime":362.0,"Position":137.0,"HyperDash":false},{"StartTime":447.0,"Position":104.571175,"HyperDash":false},{"StartTime":532.0,"Position":91.14235,"HyperDash":false},{"StartTime":617.0,"Position":81.71353,"HyperDash":false},{"StartTime":702.0,"Position":67.18218,"HyperDash":false},{"StartTime":778.0,"Position":97.66308,"HyperDash":false},{"StartTime":854.0,"Position":115.24649,"HyperDash":false},{"StartTime":930.0,"Position":114.82991,"HyperDash":false},{"StartTime":1043.0,"Position":137.0,"HyperDash":false}]},{"StartTime":1385.0,"Objects":[{"StartTime":1385.0,"Position":220.0,"HyperDash":false},{"StartTime":1465.0,"Position":233.326752,"HyperDash":false},{"StartTime":1546.0,"Position":231.079025,"HyperDash":false},{"StartTime":1626.0,"Position":246.1162,"HyperDash":false},{"StartTime":1707.0,"Position":246.012085,"HyperDash":false},{"StartTime":1788.0,"Position":253.358887,"HyperDash":false},{"StartTime":1868.0,"Position":266.303955,"HyperDash":false},{"StartTime":1949.0,"Position":247.094482,"HyperDash":false},{"StartTime":2066.0,"Position":224.02179,"HyperDash":false}]},{"StartTime":2408.0,"Objects":[{"StartTime":2408.0,"Position":160.0,"HyperDash":false},{"StartTime":2493.0,"Position":133.573441,"HyperDash":false},{"StartTime":2578.0,"Position":130.146881,"HyperDash":false},{"StartTime":2663.0,"Position":88.72033,"HyperDash":false},{"StartTime":2748.0,"Position":90.19126,"HyperDash":false},{"StartTime":2824.0,"Position":96.67014,"HyperDash":false},{"StartTime":2900.0,"Position":139.251526,"HyperDash":false},{"StartTime":2976.0,"Position":153.832932,"HyperDash":false},{"StartTime":3089.0,"Position":160.0,"HyperDash":false}]},{"StartTime":3772.0,"Objects":[{"StartTime":3772.0,"Position":340.0,"HyperDash":false}]},{"StartTime":4112.0,"Objects":[{"StartTime":4112.0,"Position":401.0,"HyperDash":false},{"StartTime":4192.0,"Position":414.4298,"HyperDash":false},{"StartTime":4273.0,"Position":393.865021,"HyperDash":false},{"StartTime":4353.0,"Position":385.29483,"HyperDash":false},{"StartTime":4434.0,"Position":415.730042,"HyperDash":false},{"StartTime":4515.0,"Position":398.165222,"HyperDash":false},{"StartTime":4595.0,"Position":407.595062,"HyperDash":false},{"StartTime":4676.0,"Position":389.030243,"HyperDash":false},{"StartTime":4793.0,"Position":404.658875,"HyperDash":false}]},{"StartTime":5135.0,"Objects":[{"StartTime":5135.0,"Position":343.0,"HyperDash":false},{"StartTime":5211.0,"Position":324.279724,"HyperDash":false},{"StartTime":5287.0,"Position":312.955536,"HyperDash":false},{"StartTime":5363.0,"Position":314.093536,"HyperDash":false},{"StartTime":5475.0,"Position":280.640778,"HyperDash":false}]},{"StartTime":5817.0,"Objects":[{"StartTime":5817.0,"Position":189.0,"HyperDash":false},{"StartTime":5902.0,"Position":156.58606,"HyperDash":false},{"StartTime":5987.0,"Position":135.172119,"HyperDash":false},{"StartTime":6072.0,"Position":120.758179,"HyperDash":false},{"StartTime":6157.0,"Position":119.241791,"HyperDash":false},{"StartTime":6233.0,"Position":147.709473,"HyperDash":false},{"StartTime":6309.0,"Position":167.279587,"HyperDash":false},{"StartTime":6385.0,"Position":164.8497,"HyperDash":false},{"StartTime":6498.0,"Position":189.0,"HyperDash":false}]},{"StartTime":6840.0,"Objects":[{"StartTime":6840.0,"Position":208.0,"HyperDash":false},{"StartTime":6920.0,"Position":217.418747,"HyperDash":false},{"StartTime":7001.0,"Position":240.042725,"HyperDash":false},{"StartTime":7081.0,"Position":276.4615,"HyperDash":false},{"StartTime":7162.0,"Position":268.085449,"HyperDash":false},{"StartTime":7243.0,"Position":295.709442,"HyperDash":false},{"StartTime":7323.0,"Position":320.128174,"HyperDash":false},{"StartTime":7404.0,"Position":340.752167,"HyperDash":false},{"StartTime":7521.0,"Position":347.7646,"HyperDash":false}]},{"StartTime":7862.0,"Objects":[{"StartTime":7862.0,"Position":416.0,"HyperDash":false},{"StartTime":7947.0,"Position":441.4566,"HyperDash":false},{"StartTime":8032.0,"Position":446.637848,"HyperDash":false},{"StartTime":8117.0,"Position":454.495941,"HyperDash":false},{"StartTime":8202.0,"Position":442.012817,"HyperDash":false},{"StartTime":8278.0,"Position":447.07373,"HyperDash":false},{"StartTime":8354.0,"Position":416.0334,"HyperDash":false},{"StartTime":8430.0,"Position":431.9387,"HyperDash":false},{"StartTime":8543.0,"Position":416.0,"HyperDash":false}]},{"StartTime":9226.0,"Objects":[{"StartTime":9226.0,"Position":275.0,"HyperDash":false}]},{"StartTime":9567.0,"Objects":[{"StartTime":9567.0,"Position":208.0,"HyperDash":false},{"StartTime":9652.0,"Position":187.257431,"HyperDash":false},{"StartTime":9737.0,"Position":176.772141,"HyperDash":false},{"StartTime":9822.0,"Position":203.593216,"HyperDash":false},{"StartTime":9907.0,"Position":172.761276,"HyperDash":false},{"StartTime":9992.0,"Position":193.308182,"HyperDash":false},{"StartTime":10077.0,"Position":178.273483,"HyperDash":false},{"StartTime":10162.0,"Position":164.69072,"HyperDash":false},{"StartTime":10248.0,"Position":175.555847,"HyperDash":false},{"StartTime":10328.0,"Position":175.714554,"HyperDash":false},{"StartTime":10409.0,"Position":165.140945,"HyperDash":false},{"StartTime":10490.0,"Position":176.860687,"HyperDash":false},{"StartTime":10571.0,"Position":161.863419,"HyperDash":false},{"StartTime":10651.0,"Position":168.073318,"HyperDash":false},{"StartTime":10732.0,"Position":197.565277,"HyperDash":false},{"StartTime":10813.0,"Position":183.264725,"HyperDash":false},{"StartTime":10930.0,"Position":208.0,"HyperDash":false}]},{"StartTime":11272.0,"Objects":[{"StartTime":11272.0,"Position":272.0,"HyperDash":false},{"StartTime":11348.0,"Position":267.478119,"HyperDash":false},{"StartTime":11424.0,"Position":297.956238,"HyperDash":false},{"StartTime":11500.0,"Position":307.4344,"HyperDash":false},{"StartTime":11612.0,"Position":341.244232,"HyperDash":false}]},{"StartTime":11953.0,"Objects":[{"StartTime":11953.0,"Position":397.0,"HyperDash":false},{"StartTime":12038.0,"Position":422.321472,"HyperDash":false},{"StartTime":12123.0,"Position":429.5693,"HyperDash":false},{"StartTime":12208.0,"Position":465.729126,"HyperDash":false},{"StartTime":12293.0,"Position":465.889526,"HyperDash":false},{"StartTime":12369.0,"Position":433.739624,"HyperDash":false},{"StartTime":12445.0,"Position":440.41095,"HyperDash":false},{"StartTime":12521.0,"Position":439.010223,"HyperDash":false},{"StartTime":12634.0,"Position":397.0,"HyperDash":false}]},{"StartTime":12976.0,"Objects":[{"StartTime":12976.0,"Position":309.0,"HyperDash":false},{"StartTime":13052.0,"Position":315.9078,"HyperDash":false},{"StartTime":13128.0,"Position":313.544037,"HyperDash":false},{"StartTime":13204.0,"Position":305.913849,"HyperDash":false},{"StartTime":13316.0,"Position":300.853455,"HyperDash":false}]},{"StartTime":13658.0,"Objects":[{"StartTime":13658.0,"Position":226.0,"HyperDash":false},{"StartTime":13738.0,"Position":220.477478,"HyperDash":false},{"StartTime":13819.0,"Position":214.759888,"HyperDash":false},{"StartTime":13899.0,"Position":167.911179,"HyperDash":false},{"StartTime":13980.0,"Position":163.303925,"HyperDash":false},{"StartTime":14061.0,"Position":139.940048,"HyperDash":false},{"StartTime":14141.0,"Position":139.7893,"HyperDash":false},{"StartTime":14222.0,"Position":130.218536,"HyperDash":false},{"StartTime":14339.0,"Position":106.02227,"HyperDash":false}]},{"StartTime":14681.0,"Objects":[{"StartTime":14681.0,"Position":71.0,"HyperDash":false}]},{"StartTime":15022.0,"Objects":[{"StartTime":15022.0,"Position":109.0,"HyperDash":false},{"StartTime":15102.0,"Position":125.26535,"HyperDash":false},{"StartTime":15183.0,"Position":125.837524,"HyperDash":false},{"StartTime":15263.0,"Position":141.6249,"HyperDash":false},{"StartTime":15344.0,"Position":175.222168,"HyperDash":false},{"StartTime":15425.0,"Position":198.591125,"HyperDash":false},{"StartTime":15505.0,"Position":206.724686,"HyperDash":false},{"StartTime":15586.0,"Position":225.222351,"HyperDash":false},{"StartTime":15703.0,"Position":228.196915,"HyperDash":false}]},{"StartTime":16044.0,"Objects":[{"StartTime":16044.0,"Position":305.0,"HyperDash":false},{"StartTime":16120.0,"Position":322.564636,"HyperDash":false},{"StartTime":16196.0,"Position":317.036682,"HyperDash":false},{"StartTime":16272.0,"Position":334.3818,"HyperDash":false},{"StartTime":16384.0,"Position":373.692017,"HyperDash":false}]},{"StartTime":16726.0,"Objects":[{"StartTime":16726.0,"Position":416.0,"HyperDash":false},{"StartTime":16811.0,"Position":436.3275,"HyperDash":false},{"StartTime":16896.0,"Position":458.65506,"HyperDash":false},{"StartTime":16981.0,"Position":460.982574,"HyperDash":false},{"StartTime":17066.0,"Position":485.412048,"HyperDash":false},{"StartTime":17142.0,"Position":473.021118,"HyperDash":false},{"StartTime":17218.0,"Position":452.528259,"HyperDash":false},{"StartTime":17294.0,"Position":456.035431,"HyperDash":false},{"StartTime":17407.0,"Position":416.0,"HyperDash":false}]},{"StartTime":17749.0,"Objects":[{"StartTime":17749.0,"Position":338.0,"HyperDash":false},{"StartTime":17829.0,"Position":314.194427,"HyperDash":false},{"StartTime":17910.0,"Position":335.3038,"HyperDash":false},{"StartTime":17990.0,"Position":298.49823,"HyperDash":false},{"StartTime":18071.0,"Position":319.4706,"HyperDash":false},{"StartTime":18152.0,"Position":321.945831,"HyperDash":false},{"StartTime":18232.0,"Position":300.43985,"HyperDash":false},{"StartTime":18313.0,"Position":300.91507,"HyperDash":false},{"StartTime":18430.0,"Position":305.712616,"HyperDash":false}]},{"StartTime":18772.0,"Objects":[{"StartTime":18772.0,"Position":293.0,"HyperDash":false}]},{"StartTime":19112.0,"Objects":[{"StartTime":19112.0,"Position":201.0,"HyperDash":false},{"StartTime":19192.0,"Position":184.726288,"HyperDash":false},{"StartTime":19273.0,"Position":174.249146,"HyperDash":false},{"StartTime":19353.0,"Position":136.975433,"HyperDash":false},{"StartTime":19434.0,"Position":143.498291,"HyperDash":false},{"StartTime":19515.0,"Position":130.021179,"HyperDash":false},{"StartTime":19595.0,"Position":94.91766,"HyperDash":false},{"StartTime":19676.0,"Position":90.74364,"HyperDash":false},{"StartTime":19793.0,"Position":63.3811569,"HyperDash":false}]},{"StartTime":20476.0,"Objects":[{"StartTime":20476.0,"Position":129.0,"HyperDash":false},{"StartTime":20556.0,"Position":157.2956,"HyperDash":false},{"StartTime":20637.0,"Position":156.794876,"HyperDash":false},{"StartTime":20717.0,"Position":179.090469,"HyperDash":false},{"StartTime":20798.0,"Position":181.589752,"HyperDash":false},{"StartTime":20879.0,"Position":214.089035,"HyperDash":false},{"StartTime":20959.0,"Position":246.179916,"HyperDash":false},{"StartTime":21040.0,"Position":240.353943,"HyperDash":false},{"StartTime":21157.0,"Position":266.716431,"HyperDash":false}]},{"StartTime":21499.0,"Objects":[{"StartTime":21499.0,"Position":352.0,"HyperDash":false},{"StartTime":21584.0,"Position":366.69278,"HyperDash":false},{"StartTime":21669.0,"Position":363.83432,"HyperDash":false},{"StartTime":21754.0,"Position":383.387665,"HyperDash":false},{"StartTime":21839.0,"Position":413.4099,"HyperDash":false},{"StartTime":21915.0,"Position":398.248138,"HyperDash":false},{"StartTime":21991.0,"Position":402.284668,"HyperDash":false},{"StartTime":22067.0,"Position":383.640961,"HyperDash":false},{"StartTime":22180.0,"Position":352.0,"HyperDash":false}]},{"StartTime":22522.0,"Objects":[{"StartTime":22522.0,"Position":337.0,"HyperDash":false}]},{"StartTime":22862.0,"Objects":[{"StartTime":22862.0,"Position":412.0,"HyperDash":false},{"StartTime":22938.0,"Position":422.0278,"HyperDash":false},{"StartTime":23014.0,"Position":407.799652,"HyperDash":false},{"StartTime":23090.0,"Position":425.315735,"HyperDash":false},{"StartTime":23202.0,"Position":405.6633,"HyperDash":false}]},{"StartTime":23885.0,"Objects":[{"StartTime":23885.0,"Position":214.0,"HyperDash":false},{"StartTime":23970.0,"Position":199.7902,"HyperDash":false},{"StartTime":24055.0,"Position":219.068054,"HyperDash":false},{"StartTime":24140.0,"Position":186.837479,"HyperDash":false},{"StartTime":24225.0,"Position":196.081757,"HyperDash":false},{"StartTime":24301.0,"Position":188.375519,"HyperDash":false},{"StartTime":24377.0,"Position":207.0842,"HyperDash":false},{"StartTime":24453.0,"Position":195.185028,"HyperDash":false},{"StartTime":24566.0,"Position":214.0,"HyperDash":false}]},{"StartTime":24908.0,"Objects":[{"StartTime":24908.0,"Position":301.0,"HyperDash":false},{"StartTime":24988.0,"Position":317.5747,"HyperDash":false},{"StartTime":25069.0,"Position":290.1566,"HyperDash":false},{"StartTime":25149.0,"Position":301.7313,"HyperDash":false},{"StartTime":25230.0,"Position":290.313171,"HyperDash":false},{"StartTime":25311.0,"Position":297.89505,"HyperDash":false},{"StartTime":25391.0,"Position":296.469727,"HyperDash":false},{"StartTime":25472.0,"Position":296.051636,"HyperDash":false},{"StartTime":25589.0,"Position":305.89212,"HyperDash":false}]},{"StartTime":25931.0,"Objects":[{"StartTime":25931.0,"Position":302.0,"HyperDash":false}]},{"StartTime":26612.0,"Objects":[{"StartTime":26612.0,"Position":131.0,"HyperDash":false}]},{"StartTime":26953.0,"Objects":[{"StartTime":26953.0,"Position":67.0,"HyperDash":false},{"StartTime":27029.0,"Position":61.19864,"HyperDash":false},{"StartTime":27105.0,"Position":60.3972778,"HyperDash":false},{"StartTime":27181.0,"Position":78.59592,"HyperDash":false},{"StartTime":27293.0,"Position":63.4149666,"HyperDash":false}]},{"StartTime":27635.0,"Objects":[{"StartTime":27635.0,"Position":96.0,"HyperDash":false},{"StartTime":27720.0,"Position":101.143433,"HyperDash":false},{"StartTime":27805.0,"Position":88.28687,"HyperDash":false},{"StartTime":27890.0,"Position":90.4303055,"HyperDash":false},{"StartTime":27975.0,"Position":104.586349,"HyperDash":false},{"StartTime":28051.0,"Position":87.68248,"HyperDash":false},{"StartTime":28127.0,"Position":96.76599,"HyperDash":false},{"StartTime":28203.0,"Position":101.84951,"HyperDash":false},{"StartTime":28316.0,"Position":96.0,"HyperDash":false}]},{"StartTime":28658.0,"Objects":[{"StartTime":28658.0,"Position":165.0,"HyperDash":false},{"StartTime":28738.0,"Position":161.813614,"HyperDash":false},{"StartTime":28819.0,"Position":196.82489,"HyperDash":false},{"StartTime":28899.0,"Position":225.6385,"HyperDash":false},{"StartTime":28980.0,"Position":225.64978,"HyperDash":false},{"StartTime":29061.0,"Position":260.60907,"HyperDash":false},{"StartTime":29141.0,"Position":251.337646,"HyperDash":false},{"StartTime":29222.0,"Position":265.2628,"HyperDash":false},{"StartTime":29339.0,"Position":299.265778,"HyperDash":false}]},{"StartTime":29681.0,"Objects":[{"StartTime":29681.0,"Position":385.0,"HyperDash":false},{"StartTime":29766.0,"Position":388.458282,"HyperDash":false},{"StartTime":29851.0,"Position":437.916565,"HyperDash":false},{"StartTime":29936.0,"Position":447.374817,"HyperDash":false},{"StartTime":30021.0,"Position":454.9358,"HyperDash":false},{"StartTime":30097.0,"Position":458.428741,"HyperDash":false},{"StartTime":30173.0,"Position":406.819,"HyperDash":false},{"StartTime":30249.0,"Position":402.209229,"HyperDash":false},{"StartTime":30362.0,"Position":385.0,"HyperDash":false}]},{"StartTime":31044.0,"Objects":[{"StartTime":31044.0,"Position":202.0,"HyperDash":false}]},{"StartTime":31385.0,"Objects":[{"StartTime":31385.0,"Position":197.0,"HyperDash":false},{"StartTime":31470.0,"Position":185.578781,"HyperDash":false},{"StartTime":31555.0,"Position":174.157562,"HyperDash":false},{"StartTime":31640.0,"Position":131.736343,"HyperDash":false},{"StartTime":31725.0,"Position":113.315117,"HyperDash":false},{"StartTime":31810.0,"Position":90.8939,"HyperDash":false},{"StartTime":31895.0,"Position":95.47269,"HyperDash":false},{"StartTime":31980.0,"Position":61.0514679,"HyperDash":false},{"StartTime":32066.0,"Position":57.3228149,"HyperDash":false},{"StartTime":32146.0,"Position":79.6167755,"HyperDash":false},{"StartTime":32227.0,"Position":103.21817,"HyperDash":false},{"StartTime":32308.0,"Position":96.81957,"HyperDash":false},{"StartTime":32389.0,"Position":116.420967,"HyperDash":false},{"StartTime":32469.0,"Position":149.817413,"HyperDash":false},{"StartTime":32550.0,"Position":165.418808,"HyperDash":false},{"StartTime":32631.0,"Position":180.0202,"HyperDash":false},{"StartTime":32748.0,"Position":197.0,"HyperDash":false}]},{"StartTime":33090.0,"Objects":[{"StartTime":33090.0,"Position":285.0,"HyperDash":false},{"StartTime":33175.0,"Position":283.775879,"HyperDash":false},{"StartTime":33260.0,"Position":292.551727,"HyperDash":false},{"StartTime":33345.0,"Position":281.3276,"HyperDash":false},{"StartTime":33430.0,"Position":288.108032,"HyperDash":false},{"StartTime":33506.0,"Position":280.418884,"HyperDash":false},{"StartTime":33582.0,"Position":305.725159,"HyperDash":false},{"StartTime":33658.0,"Position":281.031433,"HyperDash":false},{"StartTime":33771.0,"Position":285.0,"HyperDash":false}]},{"StartTime":34112.0,"Objects":[{"StartTime":34112.0,"Position":286.0,"HyperDash":false},{"StartTime":34188.0,"Position":286.694733,"HyperDash":false},{"StartTime":34264.0,"Position":302.389465,"HyperDash":false},{"StartTime":34340.0,"Position":272.0842,"HyperDash":false},{"StartTime":34452.0,"Position":289.108032,"HyperDash":false}]},{"StartTime":34794.0,"Objects":[{"StartTime":34794.0,"Position":373.0,"HyperDash":false},{"StartTime":34870.0,"Position":405.631622,"HyperDash":false},{"StartTime":34946.0,"Position":407.263245,"HyperDash":false},{"StartTime":35022.0,"Position":415.8949,"HyperDash":false},{"StartTime":35134.0,"Position":442.930969,"HyperDash":false}]},{"StartTime":35476.0,"Objects":[{"StartTime":35476.0,"Position":453.0,"HyperDash":false},{"StartTime":35556.0,"Position":463.4278,"HyperDash":false},{"StartTime":35637.0,"Position":456.885925,"HyperDash":false},{"StartTime":35717.0,"Position":475.313721,"HyperDash":false},{"StartTime":35798.0,"Position":450.771881,"HyperDash":false},{"StartTime":35879.0,"Position":441.79422,"HyperDash":false},{"StartTime":35959.0,"Position":445.1267,"HyperDash":false},{"StartTime":36040.0,"Position":428.388367,"HyperDash":false},{"StartTime":36157.0,"Position":438.09967,"HyperDash":false}]},{"StartTime":36499.0,"Objects":[{"StartTime":36499.0,"Position":362.0,"HyperDash":false}]},{"StartTime":36840.0,"Objects":[{"StartTime":36840.0,"Position":304.0,"HyperDash":false},{"StartTime":36920.0,"Position":297.5722,"HyperDash":false},{"StartTime":37001.0,"Position":304.114075,"HyperDash":false},{"StartTime":37081.0,"Position":297.686279,"HyperDash":false},{"StartTime":37162.0,"Position":292.228119,"HyperDash":false},{"StartTime":37243.0,"Position":315.20578,"HyperDash":false},{"StartTime":37323.0,"Position":301.8733,"HyperDash":false},{"StartTime":37404.0,"Position":324.611633,"HyperDash":false},{"StartTime":37521.0,"Position":318.90033,"HyperDash":false}]},{"StartTime":38203.0,"Objects":[{"StartTime":38203.0,"Position":160.0,"HyperDash":false},{"StartTime":38279.0,"Position":131.357956,"HyperDash":false},{"StartTime":38355.0,"Position":127.715912,"HyperDash":false},{"StartTime":38431.0,"Position":101.07386,"HyperDash":false},{"StartTime":38543.0,"Position":90.02242,"HyperDash":false}]},{"StartTime":38885.0,"Objects":[{"StartTime":38885.0,"Position":48.0,"HyperDash":false},{"StartTime":38970.0,"Position":51.7274666,"HyperDash":false},{"StartTime":39055.0,"Position":59.45493,"HyperDash":false},{"StartTime":39140.0,"Position":46.1823959,"HyperDash":false},{"StartTime":39225.0,"Position":50.91414,"HyperDash":false},{"StartTime":39301.0,"Position":30.2679787,"HyperDash":false},{"StartTime":39377.0,"Position":53.61754,"HyperDash":false},{"StartTime":39453.0,"Position":53.9671021,"HyperDash":false},{"StartTime":39566.0,"Position":48.0,"HyperDash":false}]},{"StartTime":40249.0,"Objects":[{"StartTime":40249.0,"Position":219.0,"HyperDash":false},{"StartTime":40325.0,"Position":234.6352,"HyperDash":false},{"StartTime":40401.0,"Position":232.270386,"HyperDash":false},{"StartTime":40477.0,"Position":246.905579,"HyperDash":false},{"StartTime":40589.0,"Position":288.94693,"HyperDash":false}]},{"StartTime":40931.0,"Objects":[{"StartTime":40931.0,"Position":379.0,"HyperDash":false},{"StartTime":41016.0,"Position":385.054565,"HyperDash":false},{"StartTime":41101.0,"Position":420.911255,"HyperDash":false},{"StartTime":41186.0,"Position":418.812378,"HyperDash":false},{"StartTime":41271.0,"Position":453.0934,"HyperDash":false},{"StartTime":41356.0,"Position":459.2142,"HyperDash":false},{"StartTime":41441.0,"Position":442.7846,"HyperDash":false},{"StartTime":41526.0,"Position":431.531372,"HyperDash":false},{"StartTime":41612.0,"Position":447.3299,"HyperDash":false},{"StartTime":41692.0,"Position":455.0114,"HyperDash":false},{"StartTime":41773.0,"Position":432.572144,"HyperDash":false},{"StartTime":41854.0,"Position":423.492432,"HyperDash":false},{"StartTime":41935.0,"Position":402.304352,"HyperDash":false},{"StartTime":42015.0,"Position":376.832733,"HyperDash":false},{"StartTime":42096.0,"Position":372.3723,"HyperDash":false},{"StartTime":42177.0,"Position":366.8342,"HyperDash":false},{"StartTime":42294.0,"Position":334.281647,"HyperDash":false}]},{"StartTime":42976.0,"Objects":[{"StartTime":42976.0,"Position":172.0,"HyperDash":false},{"StartTime":43056.0,"Position":148.8692,"HyperDash":false},{"StartTime":43137.0,"Position":157.674286,"HyperDash":false},{"StartTime":43217.0,"Position":139.543488,"HyperDash":false},{"StartTime":43298.0,"Position":162.348572,"HyperDash":false},{"StartTime":43379.0,"Position":134.066162,"HyperDash":false},{"StartTime":43459.0,"Position":174.156235,"HyperDash":false},{"StartTime":43540.0,"Position":170.297409,"HyperDash":false},{"StartTime":43657.0,"Position":167.279129,"HyperDash":false}]},{"StartTime":43999.0,"Objects":[{"StartTime":43999.0,"Position":255.0,"HyperDash":false},{"StartTime":44084.0,"Position":272.431122,"HyperDash":false},{"StartTime":44169.0,"Position":288.862274,"HyperDash":false},{"StartTime":44254.0,"Position":319.2934,"HyperDash":false},{"StartTime":44339.0,"Position":324.827057,"HyperDash":false},{"StartTime":44415.0,"Position":311.344116,"HyperDash":false},{"StartTime":44491.0,"Position":294.758636,"HyperDash":false},{"StartTime":44567.0,"Position":265.173157,"HyperDash":false},{"StartTime":44680.0,"Position":255.0,"HyperDash":false}]},{"StartTime":45022.0,"Objects":[{"StartTime":45022.0,"Position":163.0,"HyperDash":false},{"StartTime":45098.0,"Position":150.362976,"HyperDash":false},{"StartTime":45174.0,"Position":119.747879,"HyperDash":false},{"StartTime":45250.0,"Position":109.167084,"HyperDash":false},{"StartTime":45362.0,"Position":93.2945,"HyperDash":false}]},{"StartTime":45703.0,"Objects":[{"StartTime":45703.0,"Position":81.0,"HyperDash":false},{"StartTime":45779.0,"Position":67.62006,"HyperDash":false},{"StartTime":45855.0,"Position":93.24382,"HyperDash":false},{"StartTime":45931.0,"Position":89.85092,"HyperDash":false},{"StartTime":46043.0,"Position":98.2657852,"HyperDash":false}]},{"StartTime":46385.0,"Objects":[{"StartTime":46385.0,"Position":123.0,"HyperDash":false},{"StartTime":46465.0,"Position":115.7731,"HyperDash":false},{"StartTime":46546.0,"Position":133.424759,"HyperDash":false},{"StartTime":46626.0,"Position":169.264114,"HyperDash":false},{"StartTime":46707.0,"Position":177.250015,"HyperDash":false},{"StartTime":46788.0,"Position":189.772232,"HyperDash":false},{"StartTime":46868.0,"Position":199.175552,"HyperDash":false},{"StartTime":46949.0,"Position":219.42218,"HyperDash":false},{"StartTime":47066.0,"Position":250.349442,"HyperDash":false}]},{"StartTime":47408.0,"Objects":[{"StartTime":47408.0,"Position":339.0,"HyperDash":false},{"StartTime":47484.0,"Position":348.605347,"HyperDash":false},{"StartTime":47560.0,"Position":359.2107,"HyperDash":false},{"StartTime":47636.0,"Position":385.816,"HyperDash":false},{"StartTime":47748.0,"Position":408.813354,"HyperDash":false}]},{"StartTime":48431.0,"Objects":[{"StartTime":48431.0,"Position":436.0,"HyperDash":false},{"StartTime":48511.0,"Position":410.2269,"HyperDash":false},{"StartTime":48592.0,"Position":411.575226,"HyperDash":false},{"StartTime":48672.0,"Position":410.73587,"HyperDash":false},{"StartTime":48753.0,"Position":368.749969,"HyperDash":false},{"StartTime":48834.0,"Position":357.227753,"HyperDash":false},{"StartTime":48914.0,"Position":363.824432,"HyperDash":false},{"StartTime":48995.0,"Position":311.57782,"HyperDash":false},{"StartTime":49112.0,"Position":308.650574,"HyperDash":false}]},{"StartTime":49453.0,"Objects":[{"StartTime":49453.0,"Position":217.0,"HyperDash":false},{"StartTime":49538.0,"Position":226.735519,"HyperDash":false},{"StartTime":49623.0,"Position":224.662048,"HyperDash":false},{"StartTime":49708.0,"Position":184.780319,"HyperDash":false},{"StartTime":49793.0,"Position":197.063889,"HyperDash":false},{"StartTime":49869.0,"Position":185.218567,"HyperDash":false},{"StartTime":49945.0,"Position":217.554169,"HyperDash":false},{"StartTime":50021.0,"Position":222.04332,"HyperDash":false},{"StartTime":50134.0,"Position":217.0,"HyperDash":false}]},{"StartTime":50476.0,"Objects":[{"StartTime":50476.0,"Position":153.0,"HyperDash":false},{"StartTime":50552.0,"Position":152.801224,"HyperDash":false},{"StartTime":50628.0,"Position":114.521843,"HyperDash":false},{"StartTime":50704.0,"Position":93.1696854,"HyperDash":false},{"StartTime":50816.0,"Position":84.42975,"HyperDash":false}]},{"StartTime":51158.0,"Objects":[{"StartTime":51158.0,"Position":115.0,"HyperDash":false},{"StartTime":51238.0,"Position":126.432373,"HyperDash":false},{"StartTime":51319.0,"Position":159.057648,"HyperDash":false},{"StartTime":51399.0,"Position":160.49,"HyperDash":false},{"StartTime":51480.0,"Position":177.372131,"HyperDash":false},{"StartTime":51561.0,"Position":186.782,"HyperDash":false},{"StartTime":51641.0,"Position":225.989288,"HyperDash":false},{"StartTime":51722.0,"Position":235.399139,"HyperDash":false},{"StartTime":51839.0,"Position":250.10228,"HyperDash":false}]},{"StartTime":52181.0,"Objects":[{"StartTime":52181.0,"Position":339.0,"HyperDash":false}]},{"StartTime":52862.0,"Objects":[{"StartTime":52862.0,"Position":347.0,"HyperDash":false}]},{"StartTime":53203.0,"Objects":[{"StartTime":53203.0,"Position":253.0,"HyperDash":false},{"StartTime":53288.0,"Position":228.749466,"HyperDash":false},{"StartTime":53373.0,"Position":234.498917,"HyperDash":false},{"StartTime":53458.0,"Position":185.284058,"HyperDash":false},{"StartTime":53543.0,"Position":183.852417,"HyperDash":false},{"StartTime":53628.0,"Position":151.420776,"HyperDash":false},{"StartTime":53713.0,"Position":139.989136,"HyperDash":false},{"StartTime":53798.0,"Position":123.557495,"HyperDash":false},{"StartTime":53884.0,"Position":118.835892,"HyperDash":false},{"StartTime":53964.0,"Position":126.204315,"HyperDash":false},{"StartTime":54045.0,"Position":151.862686,"HyperDash":false},{"StartTime":54126.0,"Position":154.521072,"HyperDash":false},{"StartTime":54207.0,"Position":198.179474,"HyperDash":false},{"StartTime":54287.0,"Position":205.644547,"HyperDash":false},{"StartTime":54368.0,"Position":202.816391,"HyperDash":false},{"StartTime":54449.0,"Position":244.255142,"HyperDash":false},{"StartTime":54566.0,"Position":253.0,"HyperDash":false}]},{"StartTime":54908.0,"Objects":[{"StartTime":54908.0,"Position":343.0,"HyperDash":false}]},{"StartTime":55249.0,"Objects":[{"StartTime":55249.0,"Position":418.0,"HyperDash":false},{"StartTime":55325.0,"Position":411.540649,"HyperDash":false},{"StartTime":55401.0,"Position":412.081329,"HyperDash":false},{"StartTime":55477.0,"Position":412.621979,"HyperDash":false},{"StartTime":55589.0,"Position":429.366119,"HyperDash":false}]},{"StartTime":55931.0,"Objects":[{"StartTime":55931.0,"Position":415.0,"HyperDash":false},{"StartTime":56011.0,"Position":388.6705,"HyperDash":false},{"StartTime":56092.0,"Position":384.1857,"HyperDash":false},{"StartTime":56172.0,"Position":357.960541,"HyperDash":false},{"StartTime":56253.0,"Position":367.598083,"HyperDash":false},{"StartTime":56334.0,"Position":339.3097,"HyperDash":false},{"StartTime":56414.0,"Position":332.302979,"HyperDash":false},{"StartTime":56495.0,"Position":306.186432,"HyperDash":false},{"StartTime":56612.0,"Position":278.082428,"HyperDash":false}]},{"StartTime":56953.0,"Objects":[{"StartTime":56953.0,"Position":187.0,"HyperDash":false}]},{"StartTime":57294.0,"Objects":[{"StartTime":57294.0,"Position":96.0,"HyperDash":false},{"StartTime":57374.0,"Position":78.94491,"HyperDash":false},{"StartTime":57455.0,"Position":76.87663,"HyperDash":false},{"StartTime":57535.0,"Position":104.821541,"HyperDash":false},{"StartTime":57616.0,"Position":98.75326,"HyperDash":false},{"StartTime":57697.0,"Position":72.68498,"HyperDash":false},{"StartTime":57777.0,"Position":93.62989,"HyperDash":false},{"StartTime":57858.0,"Position":89.56161,"HyperDash":false},{"StartTime":57975.0,"Position":87.01854,"HyperDash":false}]},{"StartTime":58317.0,"Objects":[{"StartTime":58317.0,"Position":149.0,"HyperDash":false}]},{"StartTime":58658.0,"Objects":[{"StartTime":58658.0,"Position":239.0,"HyperDash":false},{"StartTime":58738.0,"Position":248.055084,"HyperDash":false},{"StartTime":58819.0,"Position":226.123367,"HyperDash":false},{"StartTime":58899.0,"Position":238.178467,"HyperDash":false},{"StartTime":58980.0,"Position":242.246735,"HyperDash":false},{"StartTime":59061.0,"Position":251.315018,"HyperDash":false},{"StartTime":59141.0,"Position":233.370117,"HyperDash":false},{"StartTime":59222.0,"Position":253.438385,"HyperDash":false},{"StartTime":59339.0,"Position":247.981461,"HyperDash":false}]},{"StartTime":60022.0,"Objects":[{"StartTime":60022.0,"Position":365.0,"HyperDash":false},{"StartTime":60098.0,"Position":378.011719,"HyperDash":false},{"StartTime":60174.0,"Position":393.785217,"HyperDash":false},{"StartTime":60250.0,"Position":402.2842,"HyperDash":false},{"StartTime":60362.0,"Position":430.07663,"HyperDash":false}]},{"StartTime":60703.0,"Objects":[{"StartTime":60703.0,"Position":436.0,"HyperDash":false},{"StartTime":60779.0,"Position":419.315674,"HyperDash":false},{"StartTime":60855.0,"Position":421.518646,"HyperDash":false},{"StartTime":60931.0,"Position":408.615723,"HyperDash":false},{"StartTime":61043.0,"Position":369.475067,"HyperDash":false}]},{"StartTime":61385.0,"Objects":[{"StartTime":61385.0,"Position":294.0,"HyperDash":false},{"StartTime":61465.0,"Position":267.3266,"HyperDash":false},{"StartTime":61546.0,"Position":256.9999,"HyperDash":false},{"StartTime":61626.0,"Position":281.482758,"HyperDash":false},{"StartTime":61707.0,"Position":273.83606,"HyperDash":false},{"StartTime":61788.0,"Position":247.226837,"HyperDash":false},{"StartTime":61868.0,"Position":269.576416,"HyperDash":false},{"StartTime":61949.0,"Position":261.865753,"HyperDash":false},{"StartTime":62066.0,"Position":290.626923,"HyperDash":false}]},{"StartTime":62408.0,"Objects":[{"StartTime":62408.0,"Position":368.0,"HyperDash":false}]},{"StartTime":62749.0,"Objects":[{"StartTime":62749.0,"Position":451.0,"HyperDash":false},{"StartTime":62829.0,"Position":437.402985,"HyperDash":false},{"StartTime":62910.0,"Position":465.8735,"HyperDash":false},{"StartTime":62990.0,"Position":468.8882,"HyperDash":false},{"StartTime":63071.0,"Position":481.67627,"HyperDash":false},{"StartTime":63152.0,"Position":473.464355,"HyperDash":false},{"StartTime":63232.0,"Position":462.279724,"HyperDash":false},{"StartTime":63313.0,"Position":466.06778,"HyperDash":false},{"StartTime":63430.0,"Position":454.872772,"HyperDash":false}]},{"StartTime":64112.0,"Objects":[{"StartTime":64112.0,"Position":288.0,"HyperDash":false},{"StartTime":64192.0,"Position":265.466431,"HyperDash":false},{"StartTime":64273.0,"Position":257.738678,"HyperDash":false},{"StartTime":64353.0,"Position":225.165848,"HyperDash":false},{"StartTime":64434.0,"Position":205.661438,"HyperDash":false},{"StartTime":64515.0,"Position":188.157013,"HyperDash":false},{"StartTime":64595.0,"Position":194.856354,"HyperDash":false},{"StartTime":64676.0,"Position":175.351929,"HyperDash":false},{"StartTime":64793.0,"Position":151.512222,"HyperDash":false}]},{"StartTime":65135.0,"Objects":[{"StartTime":65135.0,"Position":124.0,"HyperDash":false},{"StartTime":65220.0,"Position":116.567741,"HyperDash":false},{"StartTime":65305.0,"Position":71.23689,"HyperDash":false},{"StartTime":65390.0,"Position":69.06891,"HyperDash":false},{"StartTime":65475.0,"Position":55.0260773,"HyperDash":false},{"StartTime":65551.0,"Position":64.0663147,"HyperDash":false},{"StartTime":65627.0,"Position":87.38679,"HyperDash":false},{"StartTime":65703.0,"Position":117.8468,"HyperDash":false},{"StartTime":65816.0,"Position":124.0,"HyperDash":false}]},{"StartTime":66158.0,"Objects":[{"StartTime":66158.0,"Position":212.0,"HyperDash":false}]},{"StartTime":66499.0,"Objects":[{"StartTime":66499.0,"Position":190.0,"HyperDash":false},{"StartTime":66575.0,"Position":206.978012,"HyperDash":false},{"StartTime":66651.0,"Position":197.713776,"HyperDash":false},{"StartTime":66727.0,"Position":197.188354,"HyperDash":false},{"StartTime":66839.0,"Position":222.507156,"HyperDash":false}]},{"StartTime":67522.0,"Objects":[{"StartTime":67522.0,"Position":400.0,"HyperDash":false},{"StartTime":67598.0,"Position":417.733978,"HyperDash":false},{"StartTime":67674.0,"Position":418.6181,"HyperDash":false},{"StartTime":67750.0,"Position":419.6206,"HyperDash":false},{"StartTime":67862.0,"Position":432.309723,"HyperDash":false}]},{"StartTime":68203.0,"Objects":[{"StartTime":68203.0,"Position":441.0,"HyperDash":false},{"StartTime":68283.0,"Position":421.1902,"HyperDash":false},{"StartTime":68364.0,"Position":424.512329,"HyperDash":false},{"StartTime":68444.0,"Position":387.838,"HyperDash":false},{"StartTime":68525.0,"Position":400.321259,"HyperDash":false},{"StartTime":68606.0,"Position":351.733856,"HyperDash":false},{"StartTime":68686.0,"Position":346.847382,"HyperDash":false},{"StartTime":68767.0,"Position":347.826874,"HyperDash":false},{"StartTime":68884.0,"Position":315.044037,"HyperDash":false}]},{"StartTime":69226.0,"Objects":[{"StartTime":69226.0,"Position":271.0,"HyperDash":false},{"StartTime":69302.0,"Position":260.591034,"HyperDash":false},{"StartTime":69378.0,"Position":230.182068,"HyperDash":false},{"StartTime":69454.0,"Position":230.7731,"HyperDash":false},{"StartTime":69566.0,"Position":202.065155,"HyperDash":false}]},{"StartTime":70249.0,"Objects":[{"StartTime":70249.0,"Position":71.0,"HyperDash":false},{"StartTime":70329.0,"Position":88.54798,"HyperDash":false},{"StartTime":70410.0,"Position":83.29032,"HyperDash":false},{"StartTime":70490.0,"Position":120.8383,"HyperDash":false},{"StartTime":70571.0,"Position":138.779388,"HyperDash":false},{"StartTime":70652.0,"Position":166.2048,"HyperDash":false},{"StartTime":70732.0,"Position":159.427444,"HyperDash":false},{"StartTime":70813.0,"Position":172.852844,"HyperDash":false},{"StartTime":70930.0,"Position":206.578461,"HyperDash":false}]},{"StartTime":71272.0,"Objects":[{"StartTime":71272.0,"Position":285.0,"HyperDash":false},{"StartTime":71357.0,"Position":295.671265,"HyperDash":false},{"StartTime":71442.0,"Position":272.7821,"HyperDash":false},{"StartTime":71527.0,"Position":278.3155,"HyperDash":false},{"StartTime":71612.0,"Position":290.256958,"HyperDash":false},{"StartTime":71688.0,"Position":299.285034,"HyperDash":false},{"StartTime":71764.0,"Position":299.052734,"HyperDash":false},{"StartTime":71840.0,"Position":298.55127,"HyperDash":false},{"StartTime":71953.0,"Position":285.0,"HyperDash":false}]},{"StartTime":72294.0,"Objects":[{"StartTime":72294.0,"Position":257.0,"HyperDash":false},{"StartTime":72370.0,"Position":270.334442,"HyperDash":false},{"StartTime":72446.0,"Position":266.123962,"HyperDash":false},{"StartTime":72522.0,"Position":313.321533,"HyperDash":false},{"StartTime":72634.0,"Position":319.88623,"HyperDash":false}]},{"StartTime":72976.0,"Objects":[{"StartTime":72976.0,"Position":367.0,"HyperDash":false},{"StartTime":73056.0,"Position":386.222748,"HyperDash":false},{"StartTime":73137.0,"Position":399.63,"HyperDash":false},{"StartTime":73217.0,"Position":410.031372,"HyperDash":false},{"StartTime":73298.0,"Position":440.0546,"HyperDash":false},{"StartTime":73379.0,"Position":442.924225,"HyperDash":false},{"StartTime":73459.0,"Position":443.179749,"HyperDash":false},{"StartTime":73540.0,"Position":448.717773,"HyperDash":false},{"StartTime":73657.0,"Position":429.740936,"HyperDash":false}]},{"StartTime":73999.0,"Objects":[{"StartTime":73999.0,"Position":368.0,"HyperDash":false}]},{"StartTime":74681.0,"Objects":[{"StartTime":74681.0,"Position":2.0,"HyperDash":false}]},{"StartTime":75022.0,"Objects":[{"StartTime":75022.0,"Position":108.0,"HyperDash":false},{"StartTime":75107.0,"Position":87.6573639,"HyperDash":false},{"StartTime":75192.0,"Position":106.552719,"HyperDash":false},{"StartTime":75277.0,"Position":115.749847,"HyperDash":false},{"StartTime":75362.0,"Position":95.05077,"HyperDash":false},{"StartTime":75447.0,"Position":141.007874,"HyperDash":false},{"StartTime":75532.0,"Position":142.951065,"HyperDash":false},{"StartTime":75617.0,"Position":142.0284,"HyperDash":false},{"StartTime":75703.0,"Position":170.5651,"HyperDash":false},{"StartTime":75783.0,"Position":166.401367,"HyperDash":false},{"StartTime":75864.0,"Position":130.8922,"HyperDash":false},{"StartTime":75945.0,"Position":135.213562,"HyperDash":false},{"StartTime":76026.0,"Position":111.1313,"HyperDash":false},{"StartTime":76106.0,"Position":89.331665,"HyperDash":false},{"StartTime":76187.0,"Position":82.0756454,"HyperDash":false},{"StartTime":76268.0,"Position":84.71052,"HyperDash":false},{"StartTime":76385.0,"Position":108.0,"HyperDash":false}]},{"StartTime":76726.0,"Objects":[{"StartTime":76726.0,"Position":185.0,"HyperDash":false}]},{"StartTime":77067.0,"Objects":[{"StartTime":77067.0,"Position":134.0,"HyperDash":false},{"StartTime":77152.0,"Position":132.526932,"HyperDash":false},{"StartTime":77237.0,"Position":80.05387,"HyperDash":false},{"StartTime":77322.0,"Position":100.580811,"HyperDash":false},{"StartTime":77407.0,"Position":64.00496,"HyperDash":false},{"StartTime":77483.0,"Position":60.5251465,"HyperDash":false},{"StartTime":77559.0,"Position":95.1481247,"HyperDash":false},{"StartTime":77635.0,"Position":105.7711,"HyperDash":false},{"StartTime":77748.0,"Position":134.0,"HyperDash":false}]},{"StartTime":78090.0,"Objects":[{"StartTime":78090.0,"Position":225.0,"HyperDash":false},{"StartTime":78166.0,"Position":226.044952,"HyperDash":false},{"StartTime":78242.0,"Position":241.340271,"HyperDash":false},{"StartTime":78318.0,"Position":287.819031,"HyperDash":false},{"StartTime":78430.0,"Position":293.820984,"HyperDash":false}]},{"StartTime":78772.0,"Objects":[{"StartTime":78772.0,"Position":461.0,"HyperDash":false}]},{"StartTime":79112.0,"Objects":[{"StartTime":79112.0,"Position":429.0,"HyperDash":false},{"StartTime":79192.0,"Position":416.857025,"HyperDash":false},{"StartTime":79273.0,"Position":453.389954,"HyperDash":false},{"StartTime":79353.0,"Position":426.5255,"HyperDash":false},{"StartTime":79434.0,"Position":438.278229,"HyperDash":false},{"StartTime":79515.0,"Position":450.6304,"HyperDash":false},{"StartTime":79595.0,"Position":448.640656,"HyperDash":false},{"StartTime":79676.0,"Position":431.2529,"HyperDash":false},{"StartTime":79793.0,"Position":418.566681,"HyperDash":false}]},{"StartTime":80135.0,"Objects":[{"StartTime":80135.0,"Position":330.0,"HyperDash":false}]},{"StartTime":80476.0,"Objects":[{"StartTime":80476.0,"Position":239.0,"HyperDash":false},{"StartTime":80556.0,"Position":231.143,"HyperDash":false},{"StartTime":80637.0,"Position":222.610062,"HyperDash":false},{"StartTime":80717.0,"Position":240.474518,"HyperDash":false},{"StartTime":80798.0,"Position":227.721756,"HyperDash":false},{"StartTime":80879.0,"Position":221.3696,"HyperDash":false},{"StartTime":80959.0,"Position":225.35936,"HyperDash":false},{"StartTime":81040.0,"Position":255.747116,"HyperDash":false},{"StartTime":81157.0,"Position":249.43335,"HyperDash":false}]},{"StartTime":81840.0,"Objects":[{"StartTime":81840.0,"Position":372.0,"HyperDash":false},{"StartTime":81916.0,"Position":360.517242,"HyperDash":false},{"StartTime":81992.0,"Position":348.0345,"HyperDash":false},{"StartTime":82068.0,"Position":338.5517,"HyperDash":false},{"StartTime":82180.0,"Position":302.735016,"HyperDash":false}]},{"StartTime":82522.0,"Objects":[{"StartTime":82522.0,"Position":222.0,"HyperDash":false},{"StartTime":82607.0,"Position":195.592834,"HyperDash":false},{"StartTime":82692.0,"Position":198.185669,"HyperDash":false},{"StartTime":82777.0,"Position":161.7785,"HyperDash":false},{"StartTime":82862.0,"Position":152.268951,"HyperDash":false},{"StartTime":82938.0,"Position":160.730591,"HyperDash":false},{"StartTime":83014.0,"Position":191.294647,"HyperDash":false},{"StartTime":83090.0,"Position":194.8587,"HyperDash":false},{"StartTime":83203.0,"Position":222.0,"HyperDash":false}]},{"StartTime":83885.0,"Objects":[{"StartTime":83885.0,"Position":374.0,"HyperDash":false},{"StartTime":83961.0,"Position":382.5809,"HyperDash":false},{"StartTime":84037.0,"Position":345.4707,"HyperDash":false},{"StartTime":84113.0,"Position":351.689972,"HyperDash":false},{"StartTime":84225.0,"Position":335.561218,"HyperDash":false}]},{"StartTime":84567.0,"Objects":[{"StartTime":84567.0,"Position":246.0,"HyperDash":false},{"StartTime":84652.0,"Position":250.610764,"HyperDash":false},{"StartTime":84737.0,"Position":219.0491,"HyperDash":false},{"StartTime":84822.0,"Position":214.657715,"HyperDash":false},{"StartTime":84907.0,"Position":183.720428,"HyperDash":false},{"StartTime":84992.0,"Position":188.454575,"HyperDash":false},{"StartTime":85077.0,"Position":201.005173,"HyperDash":false},{"StartTime":85162.0,"Position":173.53,"HyperDash":false},{"StartTime":85248.0,"Position":196.9969,"HyperDash":false},{"StartTime":85333.0,"Position":206.210022,"HyperDash":false},{"StartTime":85418.0,"Position":224.027588,"HyperDash":false},{"StartTime":85503.0,"Position":233.208817,"HyperDash":false},{"StartTime":85589.0,"Position":242.6152,"HyperDash":false},{"StartTime":85674.0,"Position":246.61525,"HyperDash":false},{"StartTime":85759.0,"Position":258.8727,"HyperDash":false},{"StartTime":85844.0,"Position":298.942719,"HyperDash":false},{"StartTime":85930.0,"Position":302.666138,"HyperDash":false},{"StartTime":86015.0,"Position":288.350464,"HyperDash":false},{"StartTime":86100.0,"Position":286.2681,"HyperDash":false},{"StartTime":86185.0,"Position":256.988831,"HyperDash":false},{"StartTime":86271.0,"Position":229.786652,"HyperDash":false},{"StartTime":86356.0,"Position":240.490692,"HyperDash":false},{"StartTime":86441.0,"Position":214.260208,"HyperDash":false},{"StartTime":86526.0,"Position":191.387848,"HyperDash":false},{"StartTime":86612.0,"Position":197.0563,"HyperDash":false},{"StartTime":86692.0,"Position":212.729446,"HyperDash":false},{"StartTime":86773.0,"Position":181.9589,"HyperDash":false},{"StartTime":86854.0,"Position":206.823822,"HyperDash":false},{"StartTime":86935.0,"Position":185.277771,"HyperDash":false},{"StartTime":87015.0,"Position":222.114685,"HyperDash":false},{"StartTime":87096.0,"Position":227.322708,"HyperDash":false},{"StartTime":87177.0,"Position":214.614655,"HyperDash":false},{"StartTime":87294.0,"Position":246.0,"HyperDash":false}]},{"StartTime":87465.0,"Objects":[{"StartTime":87465.0,"Position":408.0,"HyperDash":false},{"StartTime":87547.0,"Position":243.0,"HyperDash":false},{"StartTime":87630.0,"Position":78.0,"HyperDash":false},{"StartTime":87712.0,"Position":172.0,"HyperDash":false},{"StartTime":87795.0,"Position":450.0,"HyperDash":false},{"StartTime":87877.0,"Position":231.0,"HyperDash":false},{"StartTime":87960.0,"Position":118.0,"HyperDash":false},{"StartTime":88042.0,"Position":511.0,"HyperDash":false},{"StartTime":88125.0,"Position":333.0,"HyperDash":false},{"StartTime":88208.0,"Position":234.0,"HyperDash":false},{"StartTime":88290.0,"Position":228.0,"HyperDash":false},{"StartTime":88373.0,"Position":302.0,"HyperDash":false},{"StartTime":88455.0,"Position":390.0,"HyperDash":false},{"StartTime":88538.0,"Position":75.0,"HyperDash":false},{"StartTime":88620.0,"Position":506.0,"HyperDash":false},{"StartTime":88703.0,"Position":3.0,"HyperDash":false},{"StartTime":88786.0,"Position":289.0,"HyperDash":false},{"StartTime":88868.0,"Position":217.0,"HyperDash":false},{"StartTime":88951.0,"Position":447.0,"HyperDash":false},{"StartTime":89033.0,"Position":324.0,"HyperDash":false},{"StartTime":89116.0,"Position":183.0,"HyperDash":false},{"StartTime":89198.0,"Position":279.0,"HyperDash":false},{"StartTime":89281.0,"Position":157.0,"HyperDash":false},{"StartTime":89363.0,"Position":501.0,"HyperDash":false},{"StartTime":89446.0,"Position":215.0,"HyperDash":false},{"StartTime":89529.0,"Position":79.0,"HyperDash":false},{"StartTime":89611.0,"Position":337.0,"HyperDash":false},{"StartTime":89694.0,"Position":380.0,"HyperDash":false},{"StartTime":89776.0,"Position":348.0,"HyperDash":false},{"StartTime":89859.0,"Position":225.0,"HyperDash":false},{"StartTime":89941.0,"Position":363.0,"HyperDash":false},{"StartTime":90024.0,"Position":96.0,"HyperDash":false},{"StartTime":90107.0,"Position":104.0,"HyperDash":false},{"StartTime":90189.0,"Position":173.0,"HyperDash":false},{"StartTime":90272.0,"Position":373.0,"HyperDash":false},{"StartTime":90354.0,"Position":424.0,"HyperDash":false},{"StartTime":90437.0,"Position":268.0,"HyperDash":false},{"StartTime":90519.0,"Position":373.0,"HyperDash":false},{"StartTime":90602.0,"Position":436.0,"HyperDash":false},{"StartTime":90684.0,"Position":190.0,"HyperDash":false},{"StartTime":90767.0,"Position":419.0,"HyperDash":false},{"StartTime":90850.0,"Position":158.0,"HyperDash":false},{"StartTime":90932.0,"Position":143.0,"HyperDash":false},{"StartTime":91015.0,"Position":266.0,"HyperDash":false},{"StartTime":91097.0,"Position":166.0,"HyperDash":false},{"StartTime":91180.0,"Position":297.0,"HyperDash":false},{"StartTime":91262.0,"Position":198.0,"HyperDash":false},{"StartTime":91345.0,"Position":241.0,"HyperDash":false},{"StartTime":91428.0,"Position":477.0,"HyperDash":false},{"StartTime":91510.0,"Position":371.0,"HyperDash":false},{"StartTime":91593.0,"Position":152.0,"HyperDash":false},{"StartTime":91675.0,"Position":321.0,"HyperDash":false},{"StartTime":91758.0,"Position":303.0,"HyperDash":false},{"StartTime":91840.0,"Position":259.0,"HyperDash":false},{"StartTime":91923.0,"Position":186.0,"HyperDash":false},{"StartTime":92005.0,"Position":140.0,"HyperDash":false},{"StartTime":92088.0,"Position":207.0,"HyperDash":false},{"StartTime":92171.0,"Position":278.0,"HyperDash":false},{"StartTime":92253.0,"Position":223.0,"HyperDash":false},{"StartTime":92336.0,"Position":389.0,"HyperDash":false},{"StartTime":92418.0,"Position":245.0,"HyperDash":false},{"StartTime":92501.0,"Position":400.0,"HyperDash":false},{"StartTime":92583.0,"Position":445.0,"HyperDash":false},{"StartTime":92666.0,"Position":443.0,"HyperDash":false},{"StartTime":92749.0,"Position":245.0,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3227428.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3227428.osu new file mode 100644 index 0000000000..7f9cdb97cc --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3227428.osu @@ -0,0 +1,142 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 +Mode: 0 + +[Difficulty] +HPDrainRate:4 +CircleSize:3.5 +OverallDifficulty:4 +ApproachRate:4 +SliderMultiplier:1.4 +SliderTickRate:1 + +[Events] +//Background and Video events +//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] +22,681.818181818182,4,2,1,60,1,0 +9908,-100,4,2,1,40,0,0 +10931,-100,4,2,1,67,0,0 +31726,-100,4,2,1,40,0,0 +33090,-100,4,2,1,67,0,0 +43658,-100,4,2,1,74,0,0 +53544,-100,4,2,1,50,0,0 +54908,-100,4,2,1,74,0,0 +75362,-100,4,2,1,50,0,0 +76726,-100,4,2,1,74,0,0 +86612,-100,4,2,1,67,0,0 +87294,-100,4,2,1,40,0,0 +87465,-100,4,2,1,67,0,0 +90022,-100,4,2,1,57,0,0 +91385,-100,4,2,1,37,0,0 +92067,-100,4,2,1,17,0,0 +92749,-100,4,2,1,5,0,0 + +[HitObjects] +206,12,22,5,0,2:0:0:0: +137,71,362,2,0,L|54:77,2,70,2|0|0,2:0|2:0|2:0,0:0:0:0: +220,108,1385,2,0,P|258:171|211:223,1,140,2|0,0:0|0:0,0:0:0:0: +160,283,2408,2,0,L|79:277,2,70,0|2|2,0:0|0:0|0:1,0:0:0:0: +340,303,3772,1,0,0:0:0:0: +401,235,4112,2,0,L|405:82,1,140,2|0,0:0|0:0,0:0:0:0: +343,27,5135,2,0,P|309:41|263:72,1,70,0|2,0:0|0:1,0:0:0:0: +189,63,5817,6,0,L|93:55,2,70,2|0|0,0:0|0:0|0:0,0:0:0:0: +208,151,6840,2,0,B|363:142,1,140,2|0,0:0|0:0,0:0:0:0: +416,202,7862,2,0,P|436:245|446:291,2,70,0|2|2,0:0|0:0|0:1,0:0:0:0: +275,86,9226,1,0,0:0:0:0: +208,151,9567,2,0,P|187:194|177:297,2,140,6|0|2,0:0|0:0|0:1,0:0:0:0: +272,87,11272,6,0,L|353:99,1,70,2|0,0:0|0:0,0:0:0:0: +397,169,11953,2,0,P|431:164|465:157,2,70,0|2|0,0:0|0:1|0:0,0:0:0:0: +309,196,12976,2,0,P|302:241|301:280,1,70 +226,317,13658,2,0,P|162:340|106:303,1,140,2|0,0:0|0:0,0:0:0:0: +71,218,14681,1,0,0:0:0:0: +109,135,15022,2,0,P|172:111|228:148,1,140,2|0,0:0|0:0,0:0:0:0: +305,192,16044,2,0,P|342:187|384:176,1,70,0|2,0:0|0:1,0:0:0:0: +416,99,16726,6,0,L|508:111,2,70,2|0|0,0:0|0:0|0:0,0:0:0:0: +338,58,17749,2,0,B|313:113|313:113|305:200,1,140,2|0,0:0|0:0,0:0:0:0: +293,287,18772,1,0,0:0:0:0: +201,278,19112,2,0,B|112:265|112:265|63:277,1,140,2|0,0:0|0:0,0:0:0:0: +129,107,20476,2,0,B|217:119|217:119|266:107,1,140,2|0,0:0|0:0,0:0:0:0: +352,75,21499,6,0,P|393:51|436:33,2,70,0|2|2,0:0|0:1|0:0,0:0:0:0: +337,165,22522,1,2,0:0:0:0: +412,214,22862,2,0,P|409:254|403:303,1,70,0|2,0:0|0:0,0:0:0:0: +214,306,23885,2,0,P|205:276|195:233,2,70,0|0|2,0:0|0:0|0:0,0:0:0:0: +301,331,24908,2,0,L|306:188,1,140,2|0,0:1|0:0,0:0:0:0: +302,99,25931,1,2,0:0:0:0: +131,34,26612,1,0,0:0:0:0: +67,99,26953,2,0,L|63:177,1,70,0|2,0:0|0:1,0:0:0:0: +96,254,27635,6,0,L|107:343,2,70,2|0|0,0:0|0:0|0:0,0:0:0:0: +165,194,28658,2,0,B|235:174|235:174|307:196,1,140,2|0,0:1|0:0,0:0:0:0: +385,223,29681,2,0,L|455:220,2,70,0|2|2,0:0|0:0|0:0,0:0:0:0: +202,223,31044,1,0,0:0:0:0: +197,132,31385,2,0,L|50:122,2,140,6|0|0,0:0|0:0|0:0,0:0:0:0: +285,111,33090,6,0,L|289:21,2,70,2|0|0,0:0|0:0|0:0,0:0:0:0: +286,202,34112,2,0,L|290:292,1,70,2|0,0:0|0:0,0:0:0:0: +373,306,34794,2,0,L|463:302,1,70 +453,212,35476,2,0,B|463:145|463:145|434:66,1,140,2|0,0:0|0:0,0:0:0:0: +362,25,36499,1,2,0:0:0:0: +304,95,36840,2,0,B|294:162|294:162|323:241,1,140,2|0,0:0|0:0,0:0:0:0: +160,319,38203,6,0,L|81:317,1,70,2|0,0:0|0:0,0:0:0:0: +48,235,38885,2,0,L|51:163,2,70,0|0|2,0:0|0:0|0:0,0:0:0:0: +219,295,40249,2,0,L|296:292,1,70,2|0,0:0|0:0,0:0:0:0: +379,284,40931,2,2,P|450:216|324:142,1,280,2|0,0:0|0:0,0:0:0:0: +172,210,42976,6,0,B|150:143|150:143|169:69,1,140,2|6,0:0|0:0,0:0:0:0: +255,54,43999,2,0,L|326:59,2,70,2|2|0,0:0|0:0|0:0,0:0:0:0: +163,56,45022,2,0,P|126:58|80:64,1,70,2|0,0:0|0:0,0:0:0:0: +81,153,45703,2,0,P|97:210|99:230,1,70 +123,308,46385,2,0,P|154:284|260:294,1,140,2|0,0:0|0:0,0:0:0:0: +339,307,47408,2,0,L|421:313,1,70,0|2,0:0|0:0,0:0:0:0: +436,132,48431,2,0,P|405:108|299:118,1,140,0|2,0:0|0:1,0:0:0:0: +217,111,49453,6,0,P|205:72|196:40,2,70,2|2|0,0:0|0:0|0:0,0:0:0:0: +153,175,50476,2,0,P|123:182|77:190,1,70,2|0,0:0|0:0,0:0:0:0: +115,274,51158,2,0,B|172:253|172:253|259:268,1,140,0|2,0:0|0:0,0:0:0:0: +339,247,52181,1,0,0:0:0:0: +343,65,52862,1,0,0:0:0:0: +253,81,53203,2,0,B|202:89|202:89|113:57,2,140,6|0|2,0:0|0:0|0:1,0:0:0:0: +343,65,54908,5,2,0:0:0:0: +418,116,55249,2,0,L|431:195,1,70 +415,279,55931,2,0,P|350:269|263:246,1,140,2|0,0:0|0:0,0:0:0:0: +187,254,56953,1,0,0:0:0:0: +96,242,57294,2,0,L|87:102,1,140,2|0,0:0|0:0,0:0:0:0: +149,35,58317,1,2,0:0:0:0: +239,29,58658,2,0,L|248:169,1,140,2|0,0:0|0:0,0:0:0:0: +365,304,60022,6,0,P|406:290|435:276,1,70,2|2,0:1|0:0,0:0:0:0: +436,187,60703,2,0,P|405:176|357:162,1,70 +294,217,61385,2,0,P|268:168|295:86,1,140,2|0,0:0|0:0,0:0:0:0: +368,43,62408,1,0,0:0:0:0: +451,79,62749,2,0,B|467:125|467:125|454:222,1,140,2|0,0:0|0:0,0:0:0:0: +288,290,64112,2,0,B|242:306|242:306|145:293,1,140 +124,206,65135,6,0,P|80:211|48:219,2,70,0|2|2,0:0|0:1|0:0,0:0:0:0: +212,184,66158,1,2,0:0:0:0: +190,95,66499,2,0,P|205:62|224:31,1,70,2|2,0:0|0:0,0:0:0:0: +400,67,67522,2,0,P|418:96|432:128,1,70 +441,219,68203,2,0,P|398:242|305:204,1,140,2|0,0:0|0:0,0:0:0:0: +271,136,69226,2,0,L|186:151,1,70,0|2,0:0|0:0,0:0:0:0: +71,275,70249,2,0,B|129:295|129:295|225:279,1,140,0|2,0:0|0:1,0:0:0:0: +285,236,71272,6,0,P|291:273|290:308,2,70,2|2|0,0:0|0:0|0:0,0:0:0:0: +257,150,72294,2,0,P|287:133|322:119,1,70,2|0,0:0|0:0,0:0:0:0: +367,42,72976,2,0,P|415:63|420:159,1,140,0|2,0:0|0:0,0:0:0:0: +368,210,73999,1,0,0:0:0:0: +185,209,74681,1,0,0:0:0:0: +108,159,75022,2,0,P|112:92|171:59,2,140,6|0|2,0:0|0:0|0:1,0:0:0:0: +185,209,76726,5,2,0:0:0:0: +134,284,77067,2,0,L|50:283,2,70,0|0|2,0:0|0:0|0:0,0:0:0:0: +225,289,78090,2,0,P|264:280|309:278,1,70 +385,274,78772,1,0,0:0:0:0: +429,194,79112,2,0,P|436:124|409:39,1,140,2|0,0:0|0:0,0:0:0:0: +330,33,80135,1,2,0:0:0:0: +239,38,80476,2,0,P|232:108|259:193,1,140,2|0,0:0|0:0,0:0:0:0: +372,316,81840,6,0,L|283:303,1,70,2|0,0:0|0:0,0:0:0:0: +222,262,82522,2,0,L|131:270,2,70,0|0|2,0:0|0:0|0:0,0:0:0:0: +374,161,83885,2,0,P|356:130|335:102,1,70 +246,110,84567,2,0,P|214:138|321:303,2,280,2|0|2,0:0|0:0|0:1,0:0:0:0: +256,192,87465,12,0,92749,0:0:0:0: diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3524302-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3524302-expected-conversion.json new file mode 100644 index 0000000000..b4ccc8da8f --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3524302-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":14259.0,"Objects":[{"StartTime":14259.0,"Position":65.0,"HyperDash":false},{"StartTime":14354.0,"Position":482.0,"HyperDash":false},{"StartTime":14450.0,"Position":164.0,"HyperDash":false},{"StartTime":14546.0,"Position":315.0,"HyperDash":false},{"StartTime":14642.0,"Position":145.0,"HyperDash":false},{"StartTime":14738.0,"Position":159.0,"HyperDash":false},{"StartTime":14833.0,"Position":310.0,"HyperDash":false},{"StartTime":14929.0,"Position":441.0,"HyperDash":false},{"StartTime":15025.0,"Position":428.0,"HyperDash":false},{"StartTime":15121.0,"Position":243.0,"HyperDash":false},{"StartTime":15217.0,"Position":422.0,"HyperDash":false},{"StartTime":15312.0,"Position":481.0,"HyperDash":false},{"StartTime":15408.0,"Position":104.0,"HyperDash":false},{"StartTime":15504.0,"Position":473.0,"HyperDash":false},{"StartTime":15600.0,"Position":135.0,"HyperDash":false},{"StartTime":15696.0,"Position":360.0,"HyperDash":false},{"StartTime":15792.0,"Position":123.0,"HyperDash":false},{"StartTime":15887.0,"Position":42.0,"HyperDash":false},{"StartTime":15983.0,"Position":393.0,"HyperDash":false},{"StartTime":16079.0,"Position":75.0,"HyperDash":false},{"StartTime":16175.0,"Position":377.0,"HyperDash":false},{"StartTime":16271.0,"Position":354.0,"HyperDash":false},{"StartTime":16366.0,"Position":287.0,"HyperDash":false},{"StartTime":16462.0,"Position":361.0,"HyperDash":false},{"StartTime":16558.0,"Position":479.0,"HyperDash":false},{"StartTime":16654.0,"Position":346.0,"HyperDash":false},{"StartTime":16750.0,"Position":266.0,"HyperDash":false},{"StartTime":16845.0,"Position":400.0,"HyperDash":false},{"StartTime":16941.0,"Position":202.0,"HyperDash":false},{"StartTime":17037.0,"Position":500.0,"HyperDash":false},{"StartTime":17133.0,"Position":80.0,"HyperDash":false},{"StartTime":17229.0,"Position":399.0,"HyperDash":false},{"StartTime":17325.0,"Position":455.0,"HyperDash":false}]},{"StartTime":17763.0,"Objects":[{"StartTime":17763.0,"Position":166.0,"HyperDash":false},{"StartTime":17854.0,"Position":153.171234,"HyperDash":false},{"StartTime":17981.0,"Position":164.014587,"HyperDash":false}]},{"StartTime":18201.0,"Objects":[{"StartTime":18201.0,"Position":358.0,"HyperDash":false},{"StartTime":18292.0,"Position":374.828766,"HyperDash":false},{"StartTime":18419.0,"Position":359.9854,"HyperDash":false}]},{"StartTime":18639.0,"Objects":[{"StartTime":18639.0,"Position":165.0,"HyperDash":false},{"StartTime":18730.0,"Position":95.399826,"HyperDash":false},{"StartTime":18857.0,"Position":27.0127716,"HyperDash":false}]},{"StartTime":18967.0,"Objects":[{"StartTime":18967.0,"Position":137.0,"HyperDash":false},{"StartTime":19076.0,"Position":205.993164,"HyperDash":false}]},{"StartTime":19296.0,"Objects":[{"StartTime":19296.0,"Position":25.0,"HyperDash":false}]},{"StartTime":19515.0,"Objects":[{"StartTime":19515.0,"Position":314.0,"HyperDash":false}]},{"StartTime":19624.0,"Objects":[{"StartTime":19624.0,"Position":350.0,"HyperDash":false}]},{"StartTime":19734.0,"Objects":[{"StartTime":19734.0,"Position":312.0,"HyperDash":false}]},{"StartTime":19953.0,"Objects":[{"StartTime":19953.0,"Position":118.0,"HyperDash":false},{"StartTime":20044.0,"Position":174.604065,"HyperDash":false},{"StartTime":20171.0,"Position":255.996536,"HyperDash":false}]},{"StartTime":20390.0,"Objects":[{"StartTime":20390.0,"Position":449.0,"HyperDash":false},{"StartTime":20481.0,"Position":437.183441,"HyperDash":false},{"StartTime":20608.0,"Position":451.835022,"HyperDash":false}]},{"StartTime":20828.0,"Objects":[{"StartTime":20828.0,"Position":271.0,"HyperDash":false}]},{"StartTime":21047.0,"Objects":[{"StartTime":21047.0,"Position":451.0,"HyperDash":true}]},{"StartTime":21266.0,"Objects":[{"StartTime":21266.0,"Position":133.0,"HyperDash":false}]},{"StartTime":21376.0,"Objects":[{"StartTime":21376.0,"Position":97.0,"HyperDash":false}]},{"StartTime":21485.0,"Objects":[{"StartTime":21485.0,"Position":136.0,"HyperDash":false}]},{"StartTime":21704.0,"Objects":[{"StartTime":21704.0,"Position":329.0,"HyperDash":false},{"StartTime":21795.0,"Position":323.8056,"HyperDash":false},{"StartTime":21922.0,"Position":330.929871,"HyperDash":false}]},{"StartTime":22142.0,"Objects":[{"StartTime":22142.0,"Position":136.0,"HyperDash":false},{"StartTime":22233.0,"Position":185.6055,"HyperDash":false},{"StartTime":22360.0,"Position":274.0,"HyperDash":false}]},{"StartTime":22471.0,"Objects":[{"StartTime":22471.0,"Position":385.0,"HyperDash":false},{"StartTime":22580.0,"Position":316.0,"HyperDash":false}]},{"StartTime":22799.0,"Objects":[{"StartTime":22799.0,"Position":136.0,"HyperDash":false}]},{"StartTime":23018.0,"Objects":[{"StartTime":23018.0,"Position":425.0,"HyperDash":false}]},{"StartTime":23128.0,"Objects":[{"StartTime":23128.0,"Position":461.0,"HyperDash":false}]},{"StartTime":23237.0,"Objects":[{"StartTime":23237.0,"Position":421.0,"HyperDash":false}]},{"StartTime":23456.0,"Objects":[{"StartTime":23456.0,"Position":227.0,"HyperDash":false},{"StartTime":23547.0,"Position":216.765884,"HyperDash":false},{"StartTime":23674.0,"Position":224.043533,"HyperDash":false}]},{"StartTime":23894.0,"Objects":[{"StartTime":23894.0,"Position":404.0,"HyperDash":false}]},{"StartTime":24113.0,"Objects":[{"StartTime":24113.0,"Position":224.0,"HyperDash":false}]},{"StartTime":24332.0,"Objects":[{"StartTime":24332.0,"Position":417.0,"HyperDash":false},{"StartTime":24423.0,"Position":412.811279,"HyperDash":false},{"StartTime":24550.0,"Position":418.943481,"HyperDash":false}]},{"StartTime":24661.0,"Objects":[{"StartTime":24661.0,"Position":341.0,"HyperDash":true}]},{"StartTime":24770.0,"Objects":[{"StartTime":24770.0,"Position":107.0,"HyperDash":false}]},{"StartTime":24880.0,"Objects":[{"StartTime":24880.0,"Position":69.0,"HyperDash":false}]},{"StartTime":24989.0,"Objects":[{"StartTime":24989.0,"Position":111.0,"HyperDash":false}]},{"StartTime":25208.0,"Objects":[{"StartTime":25208.0,"Position":304.0,"HyperDash":false},{"StartTime":25299.0,"Position":299.828766,"HyperDash":false},{"StartTime":25426.0,"Position":305.9854,"HyperDash":false}]},{"StartTime":25646.0,"Objects":[{"StartTime":25646.0,"Position":111.0,"HyperDash":false},{"StartTime":25737.0,"Position":124.585579,"HyperDash":false},{"StartTime":25864.0,"Position":110.007217,"HyperDash":false}]},{"StartTime":25974.0,"Objects":[{"StartTime":25974.0,"Position":220.0,"HyperDash":false},{"StartTime":26083.0,"Position":289.0,"HyperDash":false}]},{"StartTime":26303.0,"Objects":[{"StartTime":26303.0,"Position":108.0,"HyperDash":false}]},{"StartTime":26522.0,"Objects":[{"StartTime":26522.0,"Position":397.0,"HyperDash":false}]},{"StartTime":26631.0,"Objects":[{"StartTime":26631.0,"Position":432.0,"HyperDash":false}]},{"StartTime":26741.0,"Objects":[{"StartTime":26741.0,"Position":395.0,"HyperDash":false}]},{"StartTime":26960.0,"Objects":[{"StartTime":26960.0,"Position":215.0,"HyperDash":false}]},{"StartTime":27179.0,"Objects":[{"StartTime":27179.0,"Position":395.0,"HyperDash":false}]},{"StartTime":27398.0,"Objects":[{"StartTime":27398.0,"Position":201.0,"HyperDash":false},{"StartTime":27489.0,"Position":203.591461,"HyperDash":false},{"StartTime":27616.0,"Position":200.0213,"HyperDash":false}]},{"StartTime":27836.0,"Objects":[{"StartTime":27836.0,"Position":380.0,"HyperDash":false}]},{"StartTime":28055.0,"Objects":[{"StartTime":28055.0,"Position":200.0,"HyperDash":false}]},{"StartTime":28164.0,"Objects":[{"StartTime":28164.0,"Position":131.0,"HyperDash":true}]},{"StartTime":28274.0,"Objects":[{"StartTime":28274.0,"Position":365.0,"HyperDash":false},{"StartTime":28328.0,"Position":386.782776,"HyperDash":false},{"StartTime":28383.0,"Position":416.967926,"HyperDash":false},{"StartTime":28437.0,"Position":433.199036,"HyperDash":false},{"StartTime":28492.0,"Position":453.031036,"HyperDash":false},{"StartTime":28583.0,"Position":429.885376,"HyperDash":false},{"StartTime":28711.0,"Position":350.852478,"HyperDash":false}]},{"StartTime":28931.0,"Objects":[{"StartTime":28931.0,"Position":170.0,"HyperDash":false}]},{"StartTime":29150.0,"Objects":[{"StartTime":29150.0,"Position":349.0,"HyperDash":false},{"StartTime":29204.0,"Position":363.954376,"HyperDash":false},{"StartTime":29259.0,"Position":416.5623,"HyperDash":false},{"StartTime":29313.0,"Position":458.929749,"HyperDash":false},{"StartTime":29368.0,"Position":469.713531,"HyperDash":false},{"StartTime":29459.0,"Position":506.401428,"HyperDash":false},{"StartTime":29587.0,"Position":474.2096,"HyperDash":false}]},{"StartTime":30026.0,"Objects":[{"StartTime":30026.0,"Position":114.0,"HyperDash":false}]},{"StartTime":30244.0,"Objects":[{"StartTime":30244.0,"Position":292.0,"HyperDash":false}]},{"StartTime":30463.0,"Objects":[{"StartTime":30463.0,"Position":114.0,"HyperDash":false},{"StartTime":30554.0,"Position":104.591461,"HyperDash":false},{"StartTime":30681.0,"Position":113.0213,"HyperDash":false}]},{"StartTime":30901.0,"Objects":[{"StartTime":30901.0,"Position":307.0,"HyperDash":false},{"StartTime":30992.0,"Position":296.817017,"HyperDash":false},{"StartTime":31119.0,"Position":308.957245,"HyperDash":false}]},{"StartTime":31230.0,"Objects":[{"StartTime":31230.0,"Position":197.0,"HyperDash":false},{"StartTime":31339.0,"Position":128.007462,"HyperDash":false}]},{"StartTime":31558.0,"Objects":[{"StartTime":31558.0,"Position":417.0,"HyperDash":false},{"StartTime":31667.0,"Position":417.932343,"HyperDash":true}]},{"StartTime":31777.0,"Objects":[{"StartTime":31777.0,"Position":148.0,"HyperDash":false}]},{"StartTime":31887.0,"Objects":[{"StartTime":31887.0,"Position":78.0,"HyperDash":false}]},{"StartTime":31996.0,"Objects":[{"StartTime":31996.0,"Position":148.0,"HyperDash":false}]},{"StartTime":32215.0,"Objects":[{"StartTime":32215.0,"Position":341.0,"HyperDash":false},{"StartTime":32306.0,"Position":339.731537,"HyperDash":false},{"StartTime":32433.0,"Position":342.362183,"HyperDash":false}]},{"StartTime":32544.0,"Objects":[{"StartTime":32544.0,"Position":265.0,"HyperDash":false}]},{"StartTime":32653.0,"Objects":[{"StartTime":32653.0,"Position":155.0,"HyperDash":false},{"StartTime":32744.0,"Position":115.395584,"HyperDash":false},{"StartTime":32871.0,"Position":17.0026245,"HyperDash":false}]},{"StartTime":32982.0,"Objects":[{"StartTime":32982.0,"Position":93.0,"HyperDash":true}]},{"StartTime":33091.0,"Objects":[{"StartTime":33091.0,"Position":292.0,"HyperDash":false}]},{"StartTime":33310.0,"Objects":[{"StartTime":33310.0,"Position":112.0,"HyperDash":false},{"StartTime":33419.0,"Position":110.057106,"HyperDash":true}]},{"StartTime":33529.0,"Objects":[{"StartTime":33529.0,"Position":327.0,"HyperDash":false}]},{"StartTime":33639.0,"Objects":[{"StartTime":33639.0,"Position":396.0,"HyperDash":false}]},{"StartTime":33748.0,"Objects":[{"StartTime":33748.0,"Position":327.0,"HyperDash":false}]},{"StartTime":33967.0,"Objects":[{"StartTime":33967.0,"Position":133.0,"HyperDash":false},{"StartTime":34058.0,"Position":142.165222,"HyperDash":false},{"StartTime":34185.0,"Position":131.000214,"HyperDash":false}]},{"StartTime":34296.0,"Objects":[{"StartTime":34296.0,"Position":207.0,"HyperDash":false}]},{"StartTime":34405.0,"Objects":[{"StartTime":34405.0,"Position":316.0,"HyperDash":false},{"StartTime":34496.0,"Position":277.3945,"HyperDash":false},{"StartTime":34623.0,"Position":178.0,"HyperDash":false}]},{"StartTime":34734.0,"Objects":[{"StartTime":34734.0,"Position":254.0,"HyperDash":true}]},{"StartTime":34843.0,"Objects":[{"StartTime":34843.0,"Position":453.0,"HyperDash":false},{"StartTime":34934.0,"Position":448.526672,"HyperDash":false},{"StartTime":35061.0,"Position":455.260864,"HyperDash":false}]},{"StartTime":35172.0,"Objects":[{"StartTime":35172.0,"Position":378.0,"HyperDash":true}]},{"StartTime":35281.0,"Objects":[{"StartTime":35281.0,"Position":145.0,"HyperDash":false}]},{"StartTime":35390.0,"Objects":[{"StartTime":35390.0,"Position":76.0,"HyperDash":false}]},{"StartTime":35500.0,"Objects":[{"StartTime":35500.0,"Position":145.0,"HyperDash":false}]},{"StartTime":35719.0,"Objects":[{"StartTime":35719.0,"Position":338.0,"HyperDash":false},{"StartTime":35810.0,"Position":332.840851,"HyperDash":false},{"StartTime":35937.0,"Position":340.014374,"HyperDash":false}]},{"StartTime":36047.0,"Objects":[{"StartTime":36047.0,"Position":263.0,"HyperDash":false}]},{"StartTime":36157.0,"Objects":[{"StartTime":36157.0,"Position":165.0,"HyperDash":false}]},{"StartTime":36266.0,"Objects":[{"StartTime":36266.0,"Position":263.0,"HyperDash":false}]},{"StartTime":36376.0,"Objects":[{"StartTime":36376.0,"Position":339.0,"HyperDash":false}]},{"StartTime":36485.0,"Objects":[{"StartTime":36485.0,"Position":263.0,"HyperDash":true}]},{"StartTime":36595.0,"Objects":[{"StartTime":36595.0,"Position":61.0,"HyperDash":false},{"StartTime":36686.0,"Position":51.31877,"HyperDash":false},{"StartTime":36813.0,"Position":59.2572021,"HyperDash":false}]},{"StartTime":36923.0,"Objects":[{"StartTime":36923.0,"Position":135.0,"HyperDash":true}]},{"StartTime":37033.0,"Objects":[{"StartTime":37033.0,"Position":371.0,"HyperDash":false}]},{"StartTime":37142.0,"Objects":[{"StartTime":37142.0,"Position":439.0,"HyperDash":false}]},{"StartTime":37252.0,"Objects":[{"StartTime":37252.0,"Position":371.0,"HyperDash":false}]},{"StartTime":37471.0,"Objects":[{"StartTime":37471.0,"Position":177.0,"HyperDash":false},{"StartTime":37562.0,"Position":246.6055,"HyperDash":false},{"StartTime":37689.0,"Position":315.0,"HyperDash":false}]},{"StartTime":37799.0,"Objects":[{"StartTime":37799.0,"Position":238.0,"HyperDash":false}]},{"StartTime":37909.0,"Objects":[{"StartTime":37909.0,"Position":127.0,"HyperDash":false},{"StartTime":38000.0,"Position":113.171227,"HyperDash":false},{"StartTime":38127.0,"Position":125.014595,"HyperDash":false}]},{"StartTime":38237.0,"Objects":[{"StartTime":38237.0,"Position":201.0,"HyperDash":true}]},{"StartTime":38347.0,"Objects":[{"StartTime":38347.0,"Position":402.0,"HyperDash":false},{"StartTime":38438.0,"Position":395.763641,"HyperDash":false},{"StartTime":38565.0,"Position":404.707062,"HyperDash":false}]},{"StartTime":38675.0,"Objects":[{"StartTime":38675.0,"Position":328.0,"HyperDash":true}]},{"StartTime":38785.0,"Objects":[{"StartTime":38785.0,"Position":92.0,"HyperDash":false}]},{"StartTime":38894.0,"Objects":[{"StartTime":38894.0,"Position":23.0,"HyperDash":false}]},{"StartTime":39004.0,"Objects":[{"StartTime":39004.0,"Position":92.0,"HyperDash":false}]},{"StartTime":39223.0,"Objects":[{"StartTime":39223.0,"Position":285.0,"HyperDash":false},{"StartTime":39314.0,"Position":323.6055,"HyperDash":false},{"StartTime":39441.0,"Position":423.0,"HyperDash":false}]},{"StartTime":39551.0,"Objects":[{"StartTime":39551.0,"Position":346.0,"HyperDash":false}]},{"StartTime":39661.0,"Objects":[{"StartTime":39661.0,"Position":235.0,"HyperDash":false},{"StartTime":39770.0,"Position":234.054886,"HyperDash":false}]},{"StartTime":39880.0,"Objects":[{"StartTime":39880.0,"Position":344.0,"HyperDash":false},{"StartTime":39989.0,"Position":345.9429,"HyperDash":true}]},{"StartTime":40099.0,"Objects":[{"StartTime":40099.0,"Position":144.0,"HyperDash":false},{"StartTime":40190.0,"Position":89.39449,"HyperDash":false},{"StartTime":40317.0,"Position":6.0,"HyperDash":false}]},{"StartTime":40427.0,"Objects":[{"StartTime":40427.0,"Position":82.0,"HyperDash":true}]},{"StartTime":40536.0,"Objects":[{"StartTime":40536.0,"Position":315.0,"HyperDash":false}]},{"StartTime":40646.0,"Objects":[{"StartTime":40646.0,"Position":384.0,"HyperDash":false}]},{"StartTime":40755.0,"Objects":[{"StartTime":40755.0,"Position":315.0,"HyperDash":false}]},{"StartTime":40974.0,"Objects":[{"StartTime":40974.0,"Position":121.0,"HyperDash":false},{"StartTime":41065.0,"Position":106.171227,"HyperDash":false},{"StartTime":41192.0,"Position":119.014595,"HyperDash":false}]},{"StartTime":41303.0,"Objects":[{"StartTime":41303.0,"Position":195.0,"HyperDash":true}]},{"StartTime":41412.0,"Objects":[{"StartTime":41412.0,"Position":394.0,"HyperDash":false}]},{"StartTime":41631.0,"Objects":[{"StartTime":41631.0,"Position":214.0,"HyperDash":false}]},{"StartTime":41741.0,"Objects":[{"StartTime":41741.0,"Position":144.0,"HyperDash":false}]},{"StartTime":41850.0,"Objects":[{"StartTime":41850.0,"Position":214.0,"HyperDash":false}]},{"StartTime":42069.0,"Objects":[{"StartTime":42069.0,"Position":407.0,"HyperDash":false},{"StartTime":42178.0,"Position":476.0,"HyperDash":true}]},{"StartTime":42288.0,"Objects":[{"StartTime":42288.0,"Position":240.0,"HyperDash":false}]},{"StartTime":42398.0,"Objects":[{"StartTime":42398.0,"Position":170.0,"HyperDash":false}]},{"StartTime":42507.0,"Objects":[{"StartTime":42507.0,"Position":240.0,"HyperDash":false}]},{"StartTime":42726.0,"Objects":[{"StartTime":42726.0,"Position":419.0,"HyperDash":false}]},{"StartTime":42945.0,"Objects":[{"StartTime":42945.0,"Position":129.0,"HyperDash":false},{"StartTime":43054.0,"Position":128.028259,"HyperDash":false}]},{"StartTime":43164.0,"Objects":[{"StartTime":43164.0,"Position":238.0,"HyperDash":false},{"StartTime":43255.0,"Position":301.604065,"HyperDash":false},{"StartTime":43382.0,"Position":375.996582,"HyperDash":false}]},{"StartTime":43493.0,"Objects":[{"StartTime":43493.0,"Position":299.0,"HyperDash":false}]},{"StartTime":43602.0,"Objects":[{"StartTime":43602.0,"Position":195.0,"HyperDash":false}]},{"StartTime":43821.0,"Objects":[{"StartTime":43821.0,"Position":374.0,"HyperDash":false}]},{"StartTime":43931.0,"Objects":[{"StartTime":43931.0,"Position":376.0,"HyperDash":true}]},{"StartTime":44040.0,"Objects":[{"StartTime":44040.0,"Position":108.0,"HyperDash":false}]},{"StartTime":44150.0,"Objects":[{"StartTime":44150.0,"Position":106.0,"HyperDash":false}]},{"StartTime":44259.0,"Objects":[{"StartTime":44259.0,"Position":209.0,"HyperDash":false}]},{"StartTime":44478.0,"Objects":[{"StartTime":44478.0,"Position":388.0,"HyperDash":false}]},{"StartTime":44697.0,"Objects":[{"StartTime":44697.0,"Position":195.0,"HyperDash":false}]},{"StartTime":44916.0,"Objects":[{"StartTime":44916.0,"Position":484.0,"HyperDash":false}]},{"StartTime":45026.0,"Objects":[{"StartTime":45026.0,"Position":407.0,"HyperDash":false}]},{"StartTime":45244.0,"Objects":[{"StartTime":45244.0,"Position":213.0,"HyperDash":false}]},{"StartTime":45354.0,"Objects":[{"StartTime":45354.0,"Position":316.0,"HyperDash":false},{"StartTime":45445.0,"Position":386.604126,"HyperDash":false},{"StartTime":45572.0,"Position":453.996674,"HyperDash":true}]},{"StartTime":45792.0,"Objects":[{"StartTime":45792.0,"Position":103.0,"HyperDash":false},{"StartTime":45846.0,"Position":59.25476,"HyperDash":false},{"StartTime":45901.0,"Position":32.3233032,"HyperDash":false},{"StartTime":45955.0,"Position":30.0666771,"HyperDash":false},{"StartTime":46010.0,"Position":14.6513605,"HyperDash":false},{"StartTime":46101.0,"Position":45.94827,"HyperDash":false},{"StartTime":46229.0,"Position":114.217232,"HyperDash":false}]},{"StartTime":46449.0,"Objects":[{"StartTime":46449.0,"Position":294.0,"HyperDash":false},{"StartTime":46503.0,"Position":260.4281,"HyperDash":false},{"StartTime":46558.0,"Position":236.271561,"HyperDash":false},{"StartTime":46612.0,"Position":192.699677,"HyperDash":false},{"StartTime":46667.0,"Position":166.543121,"HyperDash":false},{"StartTime":46758.0,"Position":132.338623,"HyperDash":false},{"StartTime":46886.0,"Position":38.5015564,"HyperDash":false}]},{"StartTime":47106.0,"Objects":[{"StartTime":47106.0,"Position":204.0,"HyperDash":false}]},{"StartTime":47325.0,"Objects":[{"StartTime":47325.0,"Position":38.0,"HyperDash":true}]},{"StartTime":47544.0,"Objects":[{"StartTime":47544.0,"Position":355.0,"HyperDash":false},{"StartTime":47598.0,"Position":383.787079,"HyperDash":false},{"StartTime":47653.0,"Position":419.23172,"HyperDash":false},{"StartTime":47707.0,"Position":453.6615,"HyperDash":false},{"StartTime":47762.0,"Position":443.4226,"HyperDash":false},{"StartTime":47853.0,"Position":433.484253,"HyperDash":false},{"StartTime":47981.0,"Position":340.2439,"HyperDash":false}]},{"StartTime":48201.0,"Objects":[{"StartTime":48201.0,"Position":173.0,"HyperDash":false}]},{"StartTime":48420.0,"Objects":[{"StartTime":48420.0,"Position":338.0,"HyperDash":false},{"StartTime":48474.0,"Position":355.147217,"HyperDash":false},{"StartTime":48529.0,"Position":351.588867,"HyperDash":false},{"StartTime":48583.0,"Position":355.8642,"HyperDash":false},{"StartTime":48638.0,"Position":329.530029,"HyperDash":false},{"StartTime":48729.0,"Position":296.7339,"HyperDash":false},{"StartTime":48857.0,"Position":203.29097,"HyperDash":false}]},{"StartTime":49077.0,"Objects":[{"StartTime":49077.0,"Position":369.0,"HyperDash":true}]},{"StartTime":49296.0,"Objects":[{"StartTime":49296.0,"Position":51.0,"HyperDash":false},{"StartTime":49387.0,"Position":38.1829834,"HyperDash":false},{"StartTime":49514.0,"Position":49.04275,"HyperDash":false}]},{"StartTime":49734.0,"Objects":[{"StartTime":49734.0,"Position":229.0,"HyperDash":false},{"StartTime":49825.0,"Position":270.604065,"HyperDash":false},{"StartTime":49952.0,"Position":366.996582,"HyperDash":false}]},{"StartTime":50172.0,"Objects":[{"StartTime":50172.0,"Position":186.0,"HyperDash":false},{"StartTime":50263.0,"Position":121.395981,"HyperDash":false},{"StartTime":50390.0,"Position":48.00357,"HyperDash":false}]},{"StartTime":50609.0,"Objects":[{"StartTime":50609.0,"Position":227.0,"HyperDash":false}]},{"StartTime":50828.0,"Objects":[{"StartTime":50828.0,"Position":47.0,"HyperDash":true}]},{"StartTime":51047.0,"Objects":[{"StartTime":51047.0,"Position":347.0,"HyperDash":false},{"StartTime":51101.0,"Position":362.642029,"HyperDash":false},{"StartTime":51156.0,"Position":410.800537,"HyperDash":false},{"StartTime":51210.0,"Position":450.264282,"HyperDash":false},{"StartTime":51265.0,"Position":470.407257,"HyperDash":false},{"StartTime":51356.0,"Position":491.032837,"HyperDash":false},{"StartTime":51484.0,"Position":477.784576,"HyperDash":false}]},{"StartTime":51923.0,"Objects":[{"StartTime":51923.0,"Position":118.0,"HyperDash":false},{"StartTime":52014.0,"Position":119.348648,"HyperDash":false},{"StartTime":52141.0,"Position":119.0904,"HyperDash":false}]},{"StartTime":52361.0,"Objects":[{"StartTime":52361.0,"Position":313.0,"HyperDash":false}]},{"StartTime":52580.0,"Objects":[{"StartTime":52580.0,"Position":119.0,"HyperDash":true}]},{"StartTime":52799.0,"Objects":[{"StartTime":52799.0,"Position":436.0,"HyperDash":false},{"StartTime":52853.0,"Position":399.0876,"HyperDash":false},{"StartTime":52908.0,"Position":381.54715,"HyperDash":false},{"StartTime":52962.0,"Position":320.634735,"HyperDash":false},{"StartTime":53017.0,"Position":299.0943,"HyperDash":false},{"StartTime":53108.0,"Position":229.9456,"HyperDash":false},{"StartTime":53236.0,"Position":161.560608,"HyperDash":false}]},{"StartTime":53456.0,"Objects":[{"StartTime":53456.0,"Position":452.0,"HyperDash":false}]},{"StartTime":53566.0,"Objects":[{"StartTime":53566.0,"Position":489.0,"HyperDash":false}]},{"StartTime":53675.0,"Objects":[{"StartTime":53675.0,"Position":454.0,"HyperDash":false}]},{"StartTime":53894.0,"Objects":[{"StartTime":53894.0,"Position":274.0,"HyperDash":false}]},{"StartTime":54113.0,"Objects":[{"StartTime":54113.0,"Position":454.0,"HyperDash":false},{"StartTime":54204.0,"Position":399.395721,"HyperDash":false},{"StartTime":54331.0,"Position":316.00293,"HyperDash":true}]},{"StartTime":54551.0,"Objects":[{"StartTime":54551.0,"Position":24.0,"HyperDash":false},{"StartTime":54605.0,"Position":67.96123,"HyperDash":false},{"StartTime":54660.0,"Position":88.55135,"HyperDash":false},{"StartTime":54714.0,"Position":106.512581,"HyperDash":false},{"StartTime":54769.0,"Position":161.1027,"HyperDash":false},{"StartTime":54860.0,"Position":223.333679,"HyperDash":false},{"StartTime":54988.0,"Position":298.834351,"HyperDash":false}]},{"StartTime":55208.0,"Objects":[{"StartTime":55208.0,"Position":104.0,"HyperDash":false}]},{"StartTime":55317.0,"Objects":[{"StartTime":55317.0,"Position":62.0,"HyperDash":false}]},{"StartTime":55427.0,"Objects":[{"StartTime":55427.0,"Position":104.0,"HyperDash":false}]},{"StartTime":55646.0,"Objects":[{"StartTime":55646.0,"Position":393.0,"HyperDash":false},{"StartTime":55737.0,"Position":340.600342,"HyperDash":false},{"StartTime":55864.0,"Position":267.4712,"HyperDash":false}]},{"StartTime":56084.0,"Objects":[{"StartTime":56084.0,"Position":87.0,"HyperDash":true}]},{"StartTime":56303.0,"Objects":[{"StartTime":56303.0,"Position":432.0,"HyperDash":false},{"StartTime":56357.0,"Position":388.775055,"HyperDash":false},{"StartTime":56412.0,"Position":359.8976,"HyperDash":false},{"StartTime":56466.0,"Position":338.6523,"HyperDash":false},{"StartTime":56521.0,"Position":318.247742,"HyperDash":false},{"StartTime":56612.0,"Position":256.532,"HyperDash":false},{"StartTime":56740.0,"Position":183.343277,"HyperDash":false}]},{"StartTime":56960.0,"Objects":[{"StartTime":56960.0,"Position":365.0,"HyperDash":false}]},{"StartTime":57179.0,"Objects":[{"StartTime":57179.0,"Position":75.0,"HyperDash":false},{"StartTime":57270.0,"Position":148.586823,"HyperDash":false},{"StartTime":57397.0,"Position":212.955231,"HyperDash":false}]},{"StartTime":57617.0,"Objects":[{"StartTime":57617.0,"Position":407.0,"HyperDash":false},{"StartTime":57708.0,"Position":422.1916,"HyperDash":false},{"StartTime":57835.0,"Position":409.854553,"HyperDash":true}]},{"StartTime":58055.0,"Objects":[{"StartTime":58055.0,"Position":118.0,"HyperDash":false},{"StartTime":58109.0,"Position":145.079269,"HyperDash":false},{"StartTime":58164.0,"Position":167.789642,"HyperDash":false},{"StartTime":58218.0,"Position":202.8689,"HyperDash":false},{"StartTime":58273.0,"Position":255.579269,"HyperDash":false},{"StartTime":58328.0,"Position":291.2896,"HyperDash":false},{"StartTime":58383.0,"Position":325.0,"HyperDash":false},{"StartTime":58437.0,"Position":294.920746,"HyperDash":false},{"StartTime":58492.0,"Position":256.210358,"HyperDash":false},{"StartTime":58583.0,"Position":185.780487,"HyperDash":false},{"StartTime":58711.0,"Position":118.0,"HyperDash":false}]},{"StartTime":58931.0,"Objects":[{"StartTime":58931.0,"Position":312.0,"HyperDash":false},{"StartTime":58985.0,"Position":312.3719,"HyperDash":false},{"StartTime":59040.0,"Position":318.289,"HyperDash":false},{"StartTime":59094.0,"Position":279.119,"HyperDash":false},{"StartTime":59149.0,"Position":279.84906,"HyperDash":false},{"StartTime":59203.0,"Position":240.6435,"HyperDash":false},{"StartTime":59258.0,"Position":241.519592,"HyperDash":false},{"StartTime":59313.0,"Position":180.829834,"HyperDash":false},{"StartTime":59368.0,"Position":166.661133,"HyperDash":false},{"StartTime":59459.0,"Position":118.512878,"HyperDash":false},{"StartTime":59587.0,"Position":33.8594971,"HyperDash":true}]},{"StartTime":59807.0,"Objects":[{"StartTime":59807.0,"Position":380.0,"HyperDash":false},{"StartTime":59898.0,"Position":403.4091,"HyperDash":false},{"StartTime":60025.0,"Position":380.555023,"HyperDash":false}]},{"StartTime":60135.0,"Objects":[{"StartTime":60135.0,"Position":290.0,"HyperDash":false}]},{"StartTime":60244.0,"Objects":[{"StartTime":60244.0,"Position":380.0,"HyperDash":false},{"StartTime":60353.0,"Position":381.815155,"HyperDash":true}]},{"StartTime":60463.0,"Objects":[{"StartTime":60463.0,"Position":180.0,"HyperDash":false},{"StartTime":60572.0,"Position":111.0,"HyperDash":true}]},{"StartTime":60682.0,"Objects":[{"StartTime":60682.0,"Position":346.0,"HyperDash":false},{"StartTime":60791.0,"Position":346.0,"HyperDash":true}]},{"StartTime":60901.0,"Objects":[{"StartTime":60901.0,"Position":144.0,"HyperDash":true}]},{"StartTime":61011.0,"Objects":[{"StartTime":61011.0,"Position":345.0,"HyperDash":false}]},{"StartTime":61120.0,"Objects":[{"StartTime":61120.0,"Position":441.0,"HyperDash":false},{"StartTime":61211.0,"Position":474.310272,"HyperDash":false},{"StartTime":61338.0,"Position":439.1717,"HyperDash":false}]},{"StartTime":61449.0,"Objects":[{"StartTime":61449.0,"Position":355.0,"HyperDash":true}]},{"StartTime":61558.0,"Objects":[{"StartTime":61558.0,"Position":121.0,"HyperDash":false},{"StartTime":61667.0,"Position":120.041756,"HyperDash":true}]},{"StartTime":61777.0,"Objects":[{"StartTime":61777.0,"Position":321.0,"HyperDash":true}]},{"StartTime":61887.0,"Objects":[{"StartTime":61887.0,"Position":120.0,"HyperDash":false}]},{"StartTime":61996.0,"Objects":[{"StartTime":61996.0,"Position":23.0,"HyperDash":false},{"StartTime":62087.0,"Position":92.6042938,"HyperDash":false},{"StartTime":62214.0,"Position":160.997086,"HyperDash":false}]},{"StartTime":62325.0,"Objects":[{"StartTime":62325.0,"Position":63.0,"HyperDash":true}]},{"StartTime":62434.0,"Objects":[{"StartTime":62434.0,"Position":296.0,"HyperDash":false},{"StartTime":62543.0,"Position":296.971741,"HyperDash":false}]},{"StartTime":62653.0,"Objects":[{"StartTime":62653.0,"Position":199.0,"HyperDash":true}]},{"StartTime":62763.0,"Objects":[{"StartTime":62763.0,"Position":400.0,"HyperDash":false}]},{"StartTime":62872.0,"Objects":[{"StartTime":62872.0,"Position":303.0,"HyperDash":false},{"StartTime":62963.0,"Position":294.5297,"HyperDash":false},{"StartTime":63090.0,"Position":354.147156,"HyperDash":false}]},{"StartTime":63201.0,"Objects":[{"StartTime":63201.0,"Position":438.0,"HyperDash":true}]},{"StartTime":63310.0,"Objects":[{"StartTime":63310.0,"Position":204.0,"HyperDash":false},{"StartTime":63401.0,"Position":148.549332,"HyperDash":false},{"StartTime":63528.0,"Position":93.9026642,"HyperDash":false}]},{"StartTime":63639.0,"Objects":[{"StartTime":63639.0,"Position":184.0,"HyperDash":false}]},{"StartTime":63748.0,"Objects":[{"StartTime":63748.0,"Position":93.0,"HyperDash":false},{"StartTime":63857.0,"Position":92.17863,"HyperDash":true}]},{"StartTime":63967.0,"Objects":[{"StartTime":63967.0,"Position":293.0,"HyperDash":false},{"StartTime":64076.0,"Position":293.919922,"HyperDash":true}]},{"StartTime":64186.0,"Objects":[{"StartTime":64186.0,"Position":93.0,"HyperDash":true}]},{"StartTime":64296.0,"Objects":[{"StartTime":64296.0,"Position":293.0,"HyperDash":false},{"StartTime":64405.0,"Position":362.0,"HyperDash":true}]},{"StartTime":64515.0,"Objects":[{"StartTime":64515.0,"Position":160.0,"HyperDash":false}]},{"StartTime":64624.0,"Objects":[{"StartTime":64624.0,"Position":63.0,"HyperDash":false},{"StartTime":64715.0,"Position":16.49675,"HyperDash":false},{"StartTime":64842.0,"Position":70.69653,"HyperDash":false}]},{"StartTime":64953.0,"Objects":[{"StartTime":64953.0,"Position":154.0,"HyperDash":true}]},{"StartTime":65062.0,"Objects":[{"StartTime":65062.0,"Position":387.0,"HyperDash":false},{"StartTime":65171.0,"Position":318.007446,"HyperDash":true}]},{"StartTime":65281.0,"Objects":[{"StartTime":65281.0,"Position":116.0,"HyperDash":true}]},{"StartTime":65390.0,"Objects":[{"StartTime":65390.0,"Position":318.0,"HyperDash":false}]},{"StartTime":65500.0,"Objects":[{"StartTime":65500.0,"Position":415.0,"HyperDash":false},{"StartTime":65591.0,"Position":455.432068,"HyperDash":false},{"StartTime":65718.0,"Position":412.315582,"HyperDash":false}]},{"StartTime":65828.0,"Objects":[{"StartTime":65828.0,"Position":315.0,"HyperDash":true}]},{"StartTime":65938.0,"Objects":[{"StartTime":65938.0,"Position":79.0,"HyperDash":false},{"StartTime":66047.0,"Position":78.01439,"HyperDash":false}]},{"StartTime":66157.0,"Objects":[{"StartTime":66157.0,"Position":175.0,"HyperDash":true}]},{"StartTime":66266.0,"Objects":[{"StartTime":66266.0,"Position":374.0,"HyperDash":false}]},{"StartTime":66376.0,"Objects":[{"StartTime":66376.0,"Position":276.0,"HyperDash":false},{"StartTime":66467.0,"Position":321.6042,"HyperDash":false},{"StartTime":66594.0,"Position":413.996857,"HyperDash":false}]},{"StartTime":66704.0,"Objects":[{"StartTime":66704.0,"Position":331.0,"HyperDash":true}]},{"StartTime":66814.0,"Objects":[{"StartTime":66814.0,"Position":60.0,"HyperDash":false},{"StartTime":66905.0,"Position":21.5649185,"HyperDash":false},{"StartTime":67032.0,"Position":61.75552,"HyperDash":false}]},{"StartTime":67142.0,"Objects":[{"StartTime":67142.0,"Position":151.0,"HyperDash":false}]},{"StartTime":67252.0,"Objects":[{"StartTime":67252.0,"Position":61.0,"HyperDash":true}]},{"StartTime":67471.0,"Objects":[{"StartTime":67471.0,"Position":378.0,"HyperDash":false}]},{"StartTime":67580.0,"Objects":[{"StartTime":67580.0,"Position":422.0,"HyperDash":false}]},{"StartTime":67690.0,"Objects":[{"StartTime":67690.0,"Position":381.0,"HyperDash":false}]},{"StartTime":67799.0,"Objects":[{"StartTime":67799.0,"Position":305.0,"HyperDash":false}]},{"StartTime":67909.0,"Objects":[{"StartTime":67909.0,"Position":194.0,"HyperDash":false},{"StartTime":68018.0,"Position":193.103973,"HyperDash":true}]},{"StartTime":68128.0,"Objects":[{"StartTime":68128.0,"Position":428.0,"HyperDash":false},{"StartTime":68219.0,"Position":351.3945,"HyperDash":false},{"StartTime":68346.0,"Position":290.0,"HyperDash":false}]},{"StartTime":68456.0,"Objects":[{"StartTime":68456.0,"Position":373.0,"HyperDash":true}]},{"StartTime":68566.0,"Objects":[{"StartTime":68566.0,"Position":137.0,"HyperDash":false},{"StartTime":68675.0,"Position":135.057114,"HyperDash":false}]},{"StartTime":68785.0,"Objects":[{"StartTime":68785.0,"Position":245.0,"HyperDash":false},{"StartTime":68894.0,"Position":245.896027,"HyperDash":true}]},{"StartTime":69004.0,"Objects":[{"StartTime":69004.0,"Position":44.0,"HyperDash":false},{"StartTime":69095.0,"Position":103.604172,"HyperDash":false},{"StartTime":69222.0,"Position":181.9968,"HyperDash":false}]},{"StartTime":69332.0,"Objects":[{"StartTime":69332.0,"Position":98.0,"HyperDash":true}]},{"StartTime":69442.0,"Objects":[{"StartTime":69442.0,"Position":333.0,"HyperDash":false},{"StartTime":69551.0,"Position":334.768646,"HyperDash":true}]},{"StartTime":69661.0,"Objects":[{"StartTime":69661.0,"Position":133.0,"HyperDash":false}]},{"StartTime":69880.0,"Objects":[{"StartTime":69880.0,"Position":326.0,"HyperDash":false}]},{"StartTime":70099.0,"Objects":[{"StartTime":70099.0,"Position":133.0,"HyperDash":false},{"StartTime":70208.0,"Position":131.084076,"HyperDash":true}]},{"StartTime":70317.0,"Objects":[{"StartTime":70317.0,"Position":398.0,"HyperDash":false},{"StartTime":70371.0,"Position":358.896545,"HyperDash":false},{"StartTime":70426.0,"Position":310.16153,"HyperDash":false},{"StartTime":70480.0,"Position":280.058075,"HyperDash":false},{"StartTime":70535.0,"Position":260.323059,"HyperDash":false},{"StartTime":70626.0,"Position":200.8524,"HyperDash":false},{"StartTime":70754.0,"Position":122.014557,"HyperDash":true}]},{"StartTime":70974.0,"Objects":[{"StartTime":70974.0,"Position":468.0,"HyperDash":false},{"StartTime":71028.0,"Position":427.894928,"HyperDash":false},{"StartTime":71083.0,"Position":386.1583,"HyperDash":false},{"StartTime":71137.0,"Position":356.053223,"HyperDash":false},{"StartTime":71192.0,"Position":330.3166,"HyperDash":false},{"StartTime":71283.0,"Position":261.843262,"HyperDash":false},{"StartTime":71411.0,"Position":192.001617,"HyperDash":false}]},{"StartTime":71631.0,"Objects":[{"StartTime":71631.0,"Position":483.0,"HyperDash":false},{"StartTime":71722.0,"Position":425.3945,"HyperDash":false},{"StartTime":71849.0,"Position":345.0,"HyperDash":true}]},{"StartTime":72069.0,"Objects":[{"StartTime":72069.0,"Position":26.0,"HyperDash":false},{"StartTime":72123.0,"Position":46.07927,"HyperDash":false},{"StartTime":72178.0,"Position":95.7896347,"HyperDash":false},{"StartTime":72232.0,"Position":143.8689,"HyperDash":false},{"StartTime":72287.0,"Position":163.579269,"HyperDash":false},{"StartTime":72342.0,"Position":192.289627,"HyperDash":false},{"StartTime":72397.0,"Position":233.0,"HyperDash":false},{"StartTime":72451.0,"Position":215.920746,"HyperDash":false},{"StartTime":72506.0,"Position":164.210358,"HyperDash":false},{"StartTime":72597.0,"Position":121.780487,"HyperDash":false},{"StartTime":72725.0,"Position":26.0,"HyperDash":true}]},{"StartTime":72945.0,"Objects":[{"StartTime":72945.0,"Position":344.0,"HyperDash":false},{"StartTime":72999.0,"Position":395.749939,"HyperDash":false},{"StartTime":73054.0,"Position":392.115662,"HyperDash":false},{"StartTime":73108.0,"Position":414.29538,"HyperDash":false},{"StartTime":73163.0,"Position":437.262848,"HyperDash":false},{"StartTime":73254.0,"Position":425.923279,"HyperDash":false},{"StartTime":73382.0,"Position":338.6331,"HyperDash":false}]},{"StartTime":73493.0,"Objects":[{"StartTime":73493.0,"Position":247.0,"HyperDash":false}]},{"StartTime":73602.0,"Objects":[{"StartTime":73602.0,"Position":338.0,"HyperDash":true}]},{"StartTime":73712.0,"Objects":[{"StartTime":73712.0,"Position":102.0,"HyperDash":true}]},{"StartTime":73821.0,"Objects":[{"StartTime":73821.0,"Position":338.0,"HyperDash":false},{"StartTime":73912.0,"Position":386.8002,"HyperDash":false},{"StartTime":74039.0,"Position":335.152557,"HyperDash":false}]},{"StartTime":74150.0,"Objects":[{"StartTime":74150.0,"Position":244.0,"HyperDash":false}]},{"StartTime":74259.0,"Objects":[{"StartTime":74259.0,"Position":334.0,"HyperDash":false},{"StartTime":74368.0,"Position":334.958252,"HyperDash":true}]},{"StartTime":74478.0,"Objects":[{"StartTime":74478.0,"Position":133.0,"HyperDash":false},{"StartTime":74587.0,"Position":131.253723,"HyperDash":true}]},{"StartTime":74697.0,"Objects":[{"StartTime":74697.0,"Position":366.0,"HyperDash":false},{"StartTime":74806.0,"Position":366.896027,"HyperDash":true}]},{"StartTime":74916.0,"Objects":[{"StartTime":74916.0,"Position":165.0,"HyperDash":true}]},{"StartTime":75026.0,"Objects":[{"StartTime":75026.0,"Position":366.0,"HyperDash":false}]},{"StartTime":75135.0,"Objects":[{"StartTime":75135.0,"Position":462.0,"HyperDash":false},{"StartTime":75226.0,"Position":396.3945,"HyperDash":false},{"StartTime":75353.0,"Position":324.0,"HyperDash":false}]},{"StartTime":75463.0,"Objects":[{"StartTime":75463.0,"Position":407.0,"HyperDash":true}]},{"StartTime":75573.0,"Objects":[{"StartTime":75573.0,"Position":171.0,"HyperDash":false},{"StartTime":75682.0,"Position":169.3576,"HyperDash":true}]},{"StartTime":75792.0,"Objects":[{"StartTime":75792.0,"Position":370.0,"HyperDash":true}]},{"StartTime":75901.0,"Objects":[{"StartTime":75901.0,"Position":170.0,"HyperDash":false}]},{"StartTime":76011.0,"Objects":[{"StartTime":76011.0,"Position":72.0,"HyperDash":false},{"StartTime":76102.0,"Position":31.1678276,"HyperDash":false},{"StartTime":76229.0,"Position":82.8498,"HyperDash":false}]},{"StartTime":76339.0,"Objects":[{"StartTime":76339.0,"Position":179.0,"HyperDash":true}]},{"StartTime":76449.0,"Objects":[{"StartTime":76449.0,"Position":414.0,"HyperDash":false},{"StartTime":76558.0,"Position":483.0,"HyperDash":false}]},{"StartTime":76668.0,"Objects":[{"StartTime":76668.0,"Position":385.0,"HyperDash":true}]},{"StartTime":76777.0,"Objects":[{"StartTime":76777.0,"Position":185.0,"HyperDash":false}]},{"StartTime":76887.0,"Objects":[{"StartTime":76887.0,"Position":282.0,"HyperDash":false},{"StartTime":76978.0,"Position":335.60437,"HyperDash":false},{"StartTime":77105.0,"Position":419.9973,"HyperDash":false}]},{"StartTime":77215.0,"Objects":[{"StartTime":77215.0,"Position":336.0,"HyperDash":true}]},{"StartTime":77325.0,"Objects":[{"StartTime":77325.0,"Position":100.0,"HyperDash":false},{"StartTime":77416.0,"Position":88.294014,"HyperDash":false},{"StartTime":77543.0,"Position":102.248474,"HyperDash":false}]},{"StartTime":77653.0,"Objects":[{"StartTime":77653.0,"Position":192.0,"HyperDash":false}]},{"StartTime":77763.0,"Objects":[{"StartTime":77763.0,"Position":102.0,"HyperDash":false},{"StartTime":77872.0,"Position":100.2084,"HyperDash":true}]},{"StartTime":77982.0,"Objects":[{"StartTime":77982.0,"Position":301.0,"HyperDash":false},{"StartTime":78091.0,"Position":370.0,"HyperDash":true}]},{"StartTime":78201.0,"Objects":[{"StartTime":78201.0,"Position":134.0,"HyperDash":false},{"StartTime":78310.0,"Position":133.028259,"HyperDash":true}]},{"StartTime":78420.0,"Objects":[{"StartTime":78420.0,"Position":334.0,"HyperDash":true}]},{"StartTime":78529.0,"Objects":[{"StartTime":78529.0,"Position":135.0,"HyperDash":false}]},{"StartTime":78639.0,"Objects":[{"StartTime":78639.0,"Position":37.0,"HyperDash":false},{"StartTime":78730.0,"Position":18.6601868,"HyperDash":false},{"StartTime":78857.0,"Position":64.53217,"HyperDash":false}]},{"StartTime":78967.0,"Objects":[{"StartTime":78967.0,"Position":147.0,"HyperDash":true}]},{"StartTime":79077.0,"Objects":[{"StartTime":79077.0,"Position":382.0,"HyperDash":false},{"StartTime":79186.0,"Position":384.028534,"HyperDash":false}]},{"StartTime":79296.0,"Objects":[{"StartTime":79296.0,"Position":273.0,"HyperDash":false},{"StartTime":79405.0,"Position":270.971466,"HyperDash":true}]},{"StartTime":79515.0,"Objects":[{"StartTime":79515.0,"Position":472.0,"HyperDash":false},{"StartTime":79624.0,"Position":473.915924,"HyperDash":true}]},{"StartTime":79734.0,"Objects":[{"StartTime":79734.0,"Position":203.0,"HyperDash":false},{"StartTime":79843.0,"Position":134.006836,"HyperDash":false}]},{"StartTime":79953.0,"Objects":[{"StartTime":79953.0,"Position":244.0,"HyperDash":false},{"StartTime":80062.0,"Position":313.0,"HyperDash":true}]},{"StartTime":80172.0,"Objects":[{"StartTime":80172.0,"Position":111.0,"HyperDash":false},{"StartTime":80281.0,"Position":108.002831,"HyperDash":true}]},{"StartTime":80390.0,"Objects":[{"StartTime":80390.0,"Position":307.0,"HyperDash":false},{"StartTime":80499.0,"Position":376.0,"HyperDash":true}]},{"StartTime":80609.0,"Objects":[{"StartTime":80609.0,"Position":140.0,"HyperDash":false},{"StartTime":80718.0,"Position":71.0,"HyperDash":true}]},{"StartTime":80828.0,"Objects":[{"StartTime":80828.0,"Position":341.0,"HyperDash":false},{"StartTime":80919.0,"Position":398.6055,"HyperDash":false},{"StartTime":81046.0,"Position":479.0,"HyperDash":false}]},{"StartTime":81157.0,"Objects":[{"StartTime":81157.0,"Position":388.0,"HyperDash":false}]},{"StartTime":81266.0,"Objects":[{"StartTime":81266.0,"Position":476.0,"HyperDash":true}]},{"StartTime":81485.0,"Objects":[{"StartTime":81485.0,"Position":161.0,"HyperDash":false}]},{"StartTime":81595.0,"Objects":[{"StartTime":81595.0,"Position":124.0,"HyperDash":false}]},{"StartTime":81704.0,"Objects":[{"StartTime":81704.0,"Position":166.0,"HyperDash":false}]},{"StartTime":81814.0,"Objects":[{"StartTime":81814.0,"Position":242.0,"HyperDash":false}]},{"StartTime":81923.0,"Objects":[{"StartTime":81923.0,"Position":351.0,"HyperDash":false},{"StartTime":82032.0,"Position":351.9999,"HyperDash":true}]},{"StartTime":82142.0,"Objects":[{"StartTime":82142.0,"Position":150.0,"HyperDash":false}]},{"StartTime":82252.0,"Objects":[{"StartTime":82252.0,"Position":74.0,"HyperDash":false}]},{"StartTime":82361.0,"Objects":[{"StartTime":82361.0,"Position":84.0,"HyperDash":false}]},{"StartTime":82471.0,"Objects":[{"StartTime":82471.0,"Position":166.0,"HyperDash":true}]},{"StartTime":82580.0,"Objects":[{"StartTime":82580.0,"Position":399.0,"HyperDash":false}]},{"StartTime":82690.0,"Objects":[{"StartTime":82690.0,"Position":442.0,"HyperDash":false}]},{"StartTime":82799.0,"Objects":[{"StartTime":82799.0,"Position":399.0,"HyperDash":false}]},{"StartTime":82909.0,"Objects":[{"StartTime":82909.0,"Position":316.0,"HyperDash":false}]},{"StartTime":83018.0,"Objects":[{"StartTime":83018.0,"Position":206.0,"HyperDash":false},{"StartTime":83127.0,"Position":204.184845,"HyperDash":false}]},{"StartTime":83237.0,"Objects":[{"StartTime":83237.0,"Position":315.0,"HyperDash":false},{"StartTime":83346.0,"Position":315.971741,"HyperDash":true}]},{"StartTime":83456.0,"Objects":[{"StartTime":83456.0,"Position":80.0,"HyperDash":false},{"StartTime":83565.0,"Position":78.18484,"HyperDash":false}]},{"StartTime":83675.0,"Objects":[{"StartTime":83675.0,"Position":182.0,"HyperDash":false}]},{"StartTime":83894.0,"Objects":[{"StartTime":83894.0,"Position":375.0,"HyperDash":true}]},{"StartTime":84113.0,"Objects":[{"StartTime":84113.0,"Position":57.0,"HyperDash":false}]},{"StartTime":84223.0,"Objects":[{"StartTime":84223.0,"Position":133.0,"HyperDash":true}]},{"StartTime":84332.0,"Objects":[{"StartTime":84332.0,"Position":366.0,"HyperDash":false}]},{"StartTime":84442.0,"Objects":[{"StartTime":84442.0,"Position":405.0,"HyperDash":false}]},{"StartTime":84551.0,"Objects":[{"StartTime":84551.0,"Position":361.0,"HyperDash":false}]},{"StartTime":84661.0,"Objects":[{"StartTime":84661.0,"Position":284.0,"HyperDash":false}]},{"StartTime":84770.0,"Objects":[{"StartTime":84770.0,"Position":174.0,"HyperDash":false},{"StartTime":84879.0,"Position":172.184845,"HyperDash":true}]},{"StartTime":84989.0,"Objects":[{"StartTime":84989.0,"Position":442.0,"HyperDash":false}]},{"StartTime":85099.0,"Objects":[{"StartTime":85099.0,"Position":358.0,"HyperDash":false}]},{"StartTime":85208.0,"Objects":[{"StartTime":85208.0,"Position":321.0,"HyperDash":false}]},{"StartTime":85317.0,"Objects":[{"StartTime":85317.0,"Position":365.0,"HyperDash":false}]},{"StartTime":85427.0,"Objects":[{"StartTime":85427.0,"Position":475.0,"HyperDash":false},{"StartTime":85536.0,"Position":475.919922,"HyperDash":true}]},{"StartTime":85646.0,"Objects":[{"StartTime":85646.0,"Position":274.0,"HyperDash":false},{"StartTime":85755.0,"Position":273.103973,"HyperDash":false}]},{"StartTime":85865.0,"Objects":[{"StartTime":85865.0,"Position":363.0,"HyperDash":false}]},{"StartTime":85974.0,"Objects":[{"StartTime":85974.0,"Position":273.0,"HyperDash":true}]},{"StartTime":86084.0,"Objects":[{"StartTime":86084.0,"Position":71.0,"HyperDash":false},{"StartTime":86193.0,"Position":70.21596,"HyperDash":true}]},{"StartTime":86303.0,"Objects":[{"StartTime":86303.0,"Position":305.0,"HyperDash":false},{"StartTime":86412.0,"Position":305.0,"HyperDash":true}]},{"StartTime":86522.0,"Objects":[{"StartTime":86522.0,"Position":103.0,"HyperDash":true}]},{"StartTime":86631.0,"Objects":[{"StartTime":86631.0,"Position":305.0,"HyperDash":false},{"StartTime":86740.0,"Position":373.995,"HyperDash":true}]},{"StartTime":86960.0,"Objects":[{"StartTime":86960.0,"Position":55.0,"HyperDash":false},{"StartTime":87014.0,"Position":76.89231,"HyperDash":false},{"StartTime":87069.0,"Position":136.4433,"HyperDash":false},{"StartTime":87123.0,"Position":166.220535,"HyperDash":false},{"StartTime":87178.0,"Position":189.010239,"HyperDash":false},{"StartTime":87232.0,"Position":225.342209,"HyperDash":false},{"StartTime":87287.0,"Position":199.378647,"HyperDash":false},{"StartTime":87342.0,"Position":204.410217,"HyperDash":false},{"StartTime":87397.0,"Position":181.37085,"HyperDash":false},{"StartTime":87488.0,"Position":110.687065,"HyperDash":false},{"StartTime":87616.0,"Position":48.63235,"HyperDash":true}]},{"StartTime":87836.0,"Objects":[{"StartTime":87836.0,"Position":398.0,"HyperDash":false}]},{"StartTime":101412.0,"Objects":[{"StartTime":101412.0,"Position":77.0,"HyperDash":false}]},{"StartTime":101850.0,"Objects":[{"StartTime":101850.0,"Position":435.0,"HyperDash":false},{"StartTime":101941.0,"Position":437.939,"HyperDash":false},{"StartTime":102068.0,"Position":434.39502,"HyperDash":false}]},{"StartTime":102288.0,"Objects":[{"StartTime":102288.0,"Position":240.0,"HyperDash":false},{"StartTime":102379.0,"Position":174.395935,"HyperDash":false},{"StartTime":102506.0,"Position":102.003464,"HyperDash":false}]},{"StartTime":102726.0,"Objects":[{"StartTime":102726.0,"Position":296.0,"HyperDash":false},{"StartTime":102817.0,"Position":355.604065,"HyperDash":false},{"StartTime":102944.0,"Position":433.996521,"HyperDash":false}]},{"StartTime":103055.0,"Objects":[{"StartTime":103055.0,"Position":322.0,"HyperDash":false},{"StartTime":103164.0,"Position":253.0,"HyperDash":false}]},{"StartTime":103383.0,"Objects":[{"StartTime":103383.0,"Position":433.0,"HyperDash":false}]},{"StartTime":103602.0,"Objects":[{"StartTime":103602.0,"Position":145.0,"HyperDash":false}]},{"StartTime":103712.0,"Objects":[{"StartTime":103712.0,"Position":228.0,"HyperDash":false}]},{"StartTime":103821.0,"Objects":[{"StartTime":103821.0,"Position":283.0,"HyperDash":false}]},{"StartTime":104040.0,"Objects":[{"StartTime":104040.0,"Position":89.0,"HyperDash":false},{"StartTime":104131.0,"Position":77.58258,"HyperDash":false},{"StartTime":104258.0,"Position":88.00002,"HyperDash":false}]},{"StartTime":104478.0,"Objects":[{"StartTime":104478.0,"Position":268.0,"HyperDash":false}]},{"StartTime":104697.0,"Objects":[{"StartTime":104697.0,"Position":88.0,"HyperDash":false}]},{"StartTime":104916.0,"Objects":[{"StartTime":104916.0,"Position":281.0,"HyperDash":false},{"StartTime":105007.0,"Position":355.604126,"HyperDash":false},{"StartTime":105134.0,"Position":418.9967,"HyperDash":false}]},{"StartTime":105354.0,"Objects":[{"StartTime":105354.0,"Position":129.0,"HyperDash":false}]},{"StartTime":105463.0,"Objects":[{"StartTime":105463.0,"Position":211.0,"HyperDash":false}]},{"StartTime":105573.0,"Objects":[{"StartTime":105573.0,"Position":266.0,"HyperDash":false}]},{"StartTime":105792.0,"Objects":[{"StartTime":105792.0,"Position":72.0,"HyperDash":false},{"StartTime":105883.0,"Position":80.618515,"HyperDash":false},{"StartTime":106010.0,"Position":71.08611,"HyperDash":false}]},{"StartTime":106230.0,"Objects":[{"StartTime":106230.0,"Position":265.0,"HyperDash":false},{"StartTime":106321.0,"Position":197.395813,"HyperDash":false},{"StartTime":106448.0,"Position":127.003143,"HyperDash":false}]},{"StartTime":106558.0,"Objects":[{"StartTime":106558.0,"Position":237.0,"HyperDash":false},{"StartTime":106667.0,"Position":306.0,"HyperDash":false}]},{"StartTime":106887.0,"Objects":[{"StartTime":106887.0,"Position":126.0,"HyperDash":false}]},{"StartTime":107106.0,"Objects":[{"StartTime":107106.0,"Position":415.0,"HyperDash":false}]},{"StartTime":107215.0,"Objects":[{"StartTime":107215.0,"Position":332.0,"HyperDash":false}]},{"StartTime":107325.0,"Objects":[{"StartTime":107325.0,"Position":276.0,"HyperDash":false}]},{"StartTime":107544.0,"Objects":[{"StartTime":107544.0,"Position":469.0,"HyperDash":false},{"StartTime":107635.0,"Position":484.411469,"HyperDash":false},{"StartTime":107762.0,"Position":469.9857,"HyperDash":false}]},{"StartTime":107982.0,"Objects":[{"StartTime":107982.0,"Position":289.0,"HyperDash":false}]},{"StartTime":108201.0,"Objects":[{"StartTime":108201.0,"Position":469.0,"HyperDash":false}]},{"StartTime":108420.0,"Objects":[{"StartTime":108420.0,"Position":275.0,"HyperDash":false},{"StartTime":108511.0,"Position":208.3945,"HyperDash":false},{"StartTime":108638.0,"Position":137.0,"HyperDash":false}]},{"StartTime":108858.0,"Objects":[{"StartTime":108858.0,"Position":428.0,"HyperDash":false}]},{"StartTime":108967.0,"Objects":[{"StartTime":108967.0,"Position":345.0,"HyperDash":false}]},{"StartTime":109077.0,"Objects":[{"StartTime":109077.0,"Position":289.0,"HyperDash":false}]},{"StartTime":109296.0,"Objects":[{"StartTime":109296.0,"Position":482.0,"HyperDash":false},{"StartTime":109387.0,"Position":471.822845,"HyperDash":false},{"StartTime":109514.0,"Position":483.971222,"HyperDash":false}]},{"StartTime":109734.0,"Objects":[{"StartTime":109734.0,"Position":291.0,"HyperDash":false},{"StartTime":109825.0,"Position":335.604,"HyperDash":false},{"StartTime":109952.0,"Position":428.9964,"HyperDash":false}]},{"StartTime":110062.0,"Objects":[{"StartTime":110062.0,"Position":318.0,"HyperDash":false},{"StartTime":110171.0,"Position":249.005829,"HyperDash":false}]},{"StartTime":110390.0,"Objects":[{"StartTime":110390.0,"Position":428.0,"HyperDash":false}]},{"StartTime":110609.0,"Objects":[{"StartTime":110609.0,"Position":138.0,"HyperDash":false}]},{"StartTime":110719.0,"Objects":[{"StartTime":110719.0,"Position":215.0,"HyperDash":false}]},{"StartTime":110828.0,"Objects":[{"StartTime":110828.0,"Position":277.0,"HyperDash":false}]},{"StartTime":111047.0,"Objects":[{"StartTime":111047.0,"Position":83.0,"HyperDash":false},{"StartTime":111138.0,"Position":130.6055,"HyperDash":false},{"StartTime":111265.0,"Position":221.0,"HyperDash":false}]},{"StartTime":111485.0,"Objects":[{"StartTime":111485.0,"Position":26.0,"HyperDash":false},{"StartTime":111576.0,"Position":27.5795326,"HyperDash":false},{"StartTime":111703.0,"Position":24.9927273,"HyperDash":false}]},{"StartTime":111923.0,"Objects":[{"StartTime":111923.0,"Position":205.0,"HyperDash":false}]},{"StartTime":112142.0,"Objects":[{"StartTime":112142.0,"Position":25.0,"HyperDash":false}]},{"StartTime":112361.0,"Objects":[{"StartTime":112361.0,"Position":314.0,"HyperDash":false}]},{"StartTime":112471.0,"Objects":[{"StartTime":112471.0,"Position":230.0,"HyperDash":false}]},{"StartTime":112580.0,"Objects":[{"StartTime":112580.0,"Position":314.0,"HyperDash":false},{"StartTime":112634.0,"Position":339.679535,"HyperDash":false},{"StartTime":112689.0,"Position":397.080627,"HyperDash":false},{"StartTime":112743.0,"Position":405.857666,"HyperDash":false},{"StartTime":112798.0,"Position":405.816559,"HyperDash":false},{"StartTime":112889.0,"Position":384.664124,"HyperDash":false},{"StartTime":113017.0,"Position":303.560425,"HyperDash":false}]},{"StartTime":113237.0,"Objects":[{"StartTime":113237.0,"Position":109.0,"HyperDash":false},{"StartTime":113291.0,"Position":59.27102,"HyperDash":false},{"StartTime":113346.0,"Position":27.2394676,"HyperDash":false},{"StartTime":113400.0,"Position":36.17946,"HyperDash":false},{"StartTime":113455.0,"Position":19.1602039,"HyperDash":false},{"StartTime":113546.0,"Position":28.68512,"HyperDash":false},{"StartTime":113674.0,"Position":122.962029,"HyperDash":false}]},{"StartTime":114113.0,"Objects":[{"StartTime":114113.0,"Position":482.0,"HyperDash":false}]},{"StartTime":114332.0,"Objects":[{"StartTime":114332.0,"Position":288.0,"HyperDash":false}]},{"StartTime":114551.0,"Objects":[{"StartTime":114551.0,"Position":482.0,"HyperDash":false},{"StartTime":114642.0,"Position":428.3945,"HyperDash":false},{"StartTime":114769.0,"Position":344.0,"HyperDash":false}]},{"StartTime":114989.0,"Objects":[{"StartTime":114989.0,"Position":149.0,"HyperDash":false},{"StartTime":115080.0,"Position":207.6055,"HyperDash":false},{"StartTime":115207.0,"Position":287.0,"HyperDash":false}]},{"StartTime":115317.0,"Objects":[{"StartTime":115317.0,"Position":397.0,"HyperDash":false},{"StartTime":115426.0,"Position":328.004547,"HyperDash":false}]},{"StartTime":115646.0,"Objects":[{"StartTime":115646.0,"Position":133.0,"HyperDash":false},{"StartTime":115755.0,"Position":132.092178,"HyperDash":true}]},{"StartTime":115865.0,"Objects":[{"StartTime":115865.0,"Position":367.0,"HyperDash":false}]},{"StartTime":115974.0,"Objects":[{"StartTime":115974.0,"Position":284.0,"HyperDash":false}]},{"StartTime":116084.0,"Objects":[{"StartTime":116084.0,"Position":228.0,"HyperDash":false}]},{"StartTime":116303.0,"Objects":[{"StartTime":116303.0,"Position":421.0,"HyperDash":false},{"StartTime":116394.0,"Position":429.822845,"HyperDash":false},{"StartTime":116521.0,"Position":422.971222,"HyperDash":false}]},{"StartTime":116631.0,"Objects":[{"StartTime":116631.0,"Position":346.0,"HyperDash":false}]},{"StartTime":116741.0,"Objects":[{"StartTime":116741.0,"Position":235.0,"HyperDash":false},{"StartTime":116832.0,"Position":277.6042,"HyperDash":false},{"StartTime":116959.0,"Position":372.996857,"HyperDash":false}]},{"StartTime":117069.0,"Objects":[{"StartTime":117069.0,"Position":296.0,"HyperDash":true}]},{"StartTime":117179.0,"Objects":[{"StartTime":117179.0,"Position":94.0,"HyperDash":false}]},{"StartTime":117398.0,"Objects":[{"StartTime":117398.0,"Position":273.0,"HyperDash":false},{"StartTime":117507.0,"Position":341.99353,"HyperDash":true}]},{"StartTime":117617.0,"Objects":[{"StartTime":117617.0,"Position":129.0,"HyperDash":false}]},{"StartTime":117726.0,"Objects":[{"StartTime":117726.0,"Position":60.0,"HyperDash":false}]},{"StartTime":117836.0,"Objects":[{"StartTime":117836.0,"Position":131.0,"HyperDash":false}]},{"StartTime":118055.0,"Objects":[{"StartTime":118055.0,"Position":324.0,"HyperDash":false},{"StartTime":118146.0,"Position":262.3945,"HyperDash":false},{"StartTime":118273.0,"Position":186.0,"HyperDash":false}]},{"StartTime":118383.0,"Objects":[{"StartTime":118383.0,"Position":262.0,"HyperDash":false}]},{"StartTime":118493.0,"Objects":[{"StartTime":118493.0,"Position":372.0,"HyperDash":false},{"StartTime":118584.0,"Position":427.036163,"HyperDash":false},{"StartTime":118711.0,"Position":476.603577,"HyperDash":false}]},{"StartTime":118821.0,"Objects":[{"StartTime":118821.0,"Position":400.0,"HyperDash":true}]},{"StartTime":118931.0,"Objects":[{"StartTime":118931.0,"Position":198.0,"HyperDash":false}]},{"StartTime":119150.0,"Objects":[{"StartTime":119150.0,"Position":391.0,"HyperDash":false},{"StartTime":119259.0,"Position":391.8414,"HyperDash":true}]},{"StartTime":119369.0,"Objects":[{"StartTime":119369.0,"Position":156.0,"HyperDash":false}]},{"StartTime":119478.0,"Objects":[{"StartTime":119478.0,"Position":238.0,"HyperDash":false}]},{"StartTime":119588.0,"Objects":[{"StartTime":119588.0,"Position":293.0,"HyperDash":false}]},{"StartTime":119807.0,"Objects":[{"StartTime":119807.0,"Position":99.0,"HyperDash":false},{"StartTime":119898.0,"Position":105.171227,"HyperDash":false},{"StartTime":120025.0,"Position":97.014595,"HyperDash":false}]},{"StartTime":120135.0,"Objects":[{"StartTime":120135.0,"Position":174.0,"HyperDash":false}]},{"StartTime":120244.0,"Objects":[{"StartTime":120244.0,"Position":283.0,"HyperDash":false}]},{"StartTime":120354.0,"Objects":[{"StartTime":120354.0,"Position":333.0,"HyperDash":false}]},{"StartTime":120463.0,"Objects":[{"StartTime":120463.0,"Position":283.0,"HyperDash":false}]},{"StartTime":120573.0,"Objects":[{"StartTime":120573.0,"Position":185.0,"HyperDash":true}]},{"StartTime":120682.0,"Objects":[{"StartTime":120682.0,"Position":384.0,"HyperDash":false},{"StartTime":120773.0,"Position":427.280121,"HyperDash":false},{"StartTime":120900.0,"Position":482.186859,"HyperDash":false}]},{"StartTime":121011.0,"Objects":[{"StartTime":121011.0,"Position":412.0,"HyperDash":true}]},{"StartTime":121120.0,"Objects":[{"StartTime":121120.0,"Position":178.0,"HyperDash":false}]},{"StartTime":121230.0,"Objects":[{"StartTime":121230.0,"Position":108.0,"HyperDash":false}]},{"StartTime":121339.0,"Objects":[{"StartTime":121339.0,"Position":178.0,"HyperDash":false}]},{"StartTime":121558.0,"Objects":[{"StartTime":121558.0,"Position":371.0,"HyperDash":false},{"StartTime":121649.0,"Position":320.3945,"HyperDash":false},{"StartTime":121776.0,"Position":233.0,"HyperDash":false}]},{"StartTime":121887.0,"Objects":[{"StartTime":121887.0,"Position":309.0,"HyperDash":false}]},{"StartTime":121996.0,"Objects":[{"StartTime":121996.0,"Position":418.0,"HyperDash":false},{"StartTime":122087.0,"Position":443.873138,"HyperDash":false},{"StartTime":122214.0,"Position":414.947174,"HyperDash":false}]},{"StartTime":122325.0,"Objects":[{"StartTime":122325.0,"Position":337.0,"HyperDash":true}]},{"StartTime":122434.0,"Objects":[{"StartTime":122434.0,"Position":137.0,"HyperDash":false},{"StartTime":122525.0,"Position":79.57886,"HyperDash":false},{"StartTime":122652.0,"Position":25.39234,"HyperDash":false}]},{"StartTime":122763.0,"Objects":[{"StartTime":122763.0,"Position":102.0,"HyperDash":true}]},{"StartTime":122872.0,"Objects":[{"StartTime":122872.0,"Position":335.0,"HyperDash":false}]},{"StartTime":122982.0,"Objects":[{"StartTime":122982.0,"Position":251.0,"HyperDash":false}]},{"StartTime":123091.0,"Objects":[{"StartTime":123091.0,"Position":196.0,"HyperDash":false}]},{"StartTime":123310.0,"Objects":[{"StartTime":123310.0,"Position":389.0,"HyperDash":false},{"StartTime":123401.0,"Position":399.5055,"HyperDash":false},{"StartTime":123528.0,"Position":387.780823,"HyperDash":false}]},{"StartTime":123639.0,"Objects":[{"StartTime":123639.0,"Position":312.0,"HyperDash":false}]},{"StartTime":123748.0,"Objects":[{"StartTime":123748.0,"Position":202.0,"HyperDash":false},{"StartTime":123839.0,"Position":146.4552,"HyperDash":false},{"StartTime":123966.0,"Position":122.737045,"HyperDash":false}]},{"StartTime":124077.0,"Objects":[{"StartTime":124077.0,"Position":200.0,"HyperDash":true}]},{"StartTime":124186.0,"Objects":[{"StartTime":124186.0,"Position":399.0,"HyperDash":false}]},{"StartTime":124405.0,"Objects":[{"StartTime":124405.0,"Position":219.0,"HyperDash":false},{"StartTime":124514.0,"Position":150.0,"HyperDash":true}]},{"StartTime":124624.0,"Objects":[{"StartTime":124624.0,"Position":386.0,"HyperDash":false}]},{"StartTime":124734.0,"Objects":[{"StartTime":124734.0,"Position":455.0,"HyperDash":false}]},{"StartTime":124843.0,"Objects":[{"StartTime":124843.0,"Position":386.0,"HyperDash":false}]},{"StartTime":125062.0,"Objects":[{"StartTime":125062.0,"Position":192.0,"HyperDash":false},{"StartTime":125153.0,"Position":149.893311,"HyperDash":false},{"StartTime":125280.0,"Position":68.0014954,"HyperDash":false}]},{"StartTime":125390.0,"Objects":[{"StartTime":125390.0,"Position":144.0,"HyperDash":true}]},{"StartTime":125500.0,"Objects":[{"StartTime":125500.0,"Position":345.0,"HyperDash":false},{"StartTime":125591.0,"Position":419.1067,"HyperDash":false},{"StartTime":125718.0,"Position":468.9985,"HyperDash":false}]},{"StartTime":125828.0,"Objects":[{"StartTime":125828.0,"Position":393.0,"HyperDash":false}]},{"StartTime":125938.0,"Objects":[{"StartTime":125938.0,"Position":282.0,"HyperDash":false}]},{"StartTime":126157.0,"Objects":[{"StartTime":126157.0,"Position":475.0,"HyperDash":false},{"StartTime":126266.0,"Position":475.9078,"HyperDash":true}]},{"StartTime":126376.0,"Objects":[{"StartTime":126376.0,"Position":240.0,"HyperDash":false}]},{"StartTime":126485.0,"Objects":[{"StartTime":126485.0,"Position":322.0,"HyperDash":false}]},{"StartTime":126595.0,"Objects":[{"StartTime":126595.0,"Position":377.0,"HyperDash":false}]},{"StartTime":126814.0,"Objects":[{"StartTime":126814.0,"Position":183.0,"HyperDash":false}]},{"StartTime":127033.0,"Objects":[{"StartTime":127033.0,"Position":472.0,"HyperDash":false}]},{"StartTime":127142.0,"Objects":[{"StartTime":127142.0,"Position":389.0,"HyperDash":false}]},{"StartTime":127252.0,"Objects":[{"StartTime":127252.0,"Position":333.0,"HyperDash":false}]},{"StartTime":127471.0,"Objects":[{"StartTime":127471.0,"Position":153.0,"HyperDash":false},{"StartTime":127580.0,"Position":152.067657,"HyperDash":false}]},{"StartTime":127690.0,"Objects":[{"StartTime":127690.0,"Position":256.0,"HyperDash":false}]},{"StartTime":127909.0,"Objects":[{"StartTime":127909.0,"Position":76.0,"HyperDash":true}]},{"StartTime":128128.0,"Objects":[{"StartTime":128128.0,"Position":421.0,"HyperDash":false}]},{"StartTime":128237.0,"Objects":[{"StartTime":128237.0,"Position":423.0,"HyperDash":false}]},{"StartTime":128347.0,"Objects":[{"StartTime":128347.0,"Position":319.0,"HyperDash":false}]},{"StartTime":128566.0,"Objects":[{"StartTime":128566.0,"Position":139.0,"HyperDash":false}]},{"StartTime":128785.0,"Objects":[{"StartTime":128785.0,"Position":332.0,"HyperDash":false}]},{"StartTime":129004.0,"Objects":[{"StartTime":129004.0,"Position":42.0,"HyperDash":false}]},{"StartTime":129113.0,"Objects":[{"StartTime":129113.0,"Position":111.0,"HyperDash":false}]},{"StartTime":129332.0,"Objects":[{"StartTime":129332.0,"Position":304.0,"HyperDash":false},{"StartTime":129386.0,"Position":253.920715,"HyperDash":false},{"StartTime":129441.0,"Position":217.210358,"HyperDash":false},{"StartTime":129495.0,"Position":213.1311,"HyperDash":false},{"StartTime":129550.0,"Position":166.420731,"HyperDash":false},{"StartTime":129660.0,"Position":97.0,"HyperDash":true}]},{"StartTime":129880.0,"Objects":[{"StartTime":129880.0,"Position":408.0,"HyperDash":false},{"StartTime":129934.0,"Position":421.643433,"HyperDash":false},{"StartTime":129989.0,"Position":469.5894,"HyperDash":false},{"StartTime":130043.0,"Position":472.515442,"HyperDash":false},{"StartTime":130098.0,"Position":489.2183,"HyperDash":false},{"StartTime":130189.0,"Position":462.087952,"HyperDash":false},{"StartTime":130317.0,"Position":381.479523,"HyperDash":false}]},{"StartTime":130536.0,"Objects":[{"StartTime":130536.0,"Position":188.0,"HyperDash":false},{"StartTime":130590.0,"Position":224.105255,"HyperDash":false},{"StartTime":130645.0,"Position":273.8421,"HyperDash":false},{"StartTime":130699.0,"Position":301.947357,"HyperDash":false},{"StartTime":130754.0,"Position":325.6842,"HyperDash":false},{"StartTime":130845.0,"Position":391.1579,"HyperDash":false},{"StartTime":130973.0,"Position":464.0,"HyperDash":false}]},{"StartTime":131193.0,"Objects":[{"StartTime":131193.0,"Position":283.0,"HyperDash":false}]},{"StartTime":131412.0,"Objects":[{"StartTime":131412.0,"Position":463.0,"HyperDash":true}]},{"StartTime":131631.0,"Objects":[{"StartTime":131631.0,"Position":145.0,"HyperDash":false},{"StartTime":131685.0,"Position":104.253967,"HyperDash":false},{"StartTime":131740.0,"Position":82.46871,"HyperDash":false},{"StartTime":131794.0,"Position":46.8594933,"HyperDash":false},{"StartTime":131849.0,"Position":58.7363319,"HyperDash":false},{"StartTime":131940.0,"Position":65.76372,"HyperDash":false},{"StartTime":132068.0,"Position":161.933884,"HyperDash":false}]},{"StartTime":132288.0,"Objects":[{"StartTime":132288.0,"Position":342.0,"HyperDash":false}]},{"StartTime":132507.0,"Objects":[{"StartTime":132507.0,"Position":148.0,"HyperDash":false},{"StartTime":132598.0,"Position":150.628357,"HyperDash":false},{"StartTime":132725.0,"Position":147.1097,"HyperDash":false}]},{"StartTime":132945.0,"Objects":[{"StartTime":132945.0,"Position":327.0,"HyperDash":false}]},{"StartTime":133164.0,"Objects":[{"StartTime":133164.0,"Position":147.0,"HyperDash":true}]},{"StartTime":133383.0,"Objects":[{"StartTime":133383.0,"Position":464.0,"HyperDash":false},{"StartTime":133437.0,"Position":470.84375,"HyperDash":false},{"StartTime":133492.0,"Position":474.752625,"HyperDash":false},{"StartTime":133546.0,"Position":428.8388,"HyperDash":false},{"StartTime":133601.0,"Position":419.0386,"HyperDash":false},{"StartTime":133711.0,"Position":351.443878,"HyperDash":false}]},{"StartTime":133821.0,"Objects":[{"StartTime":133821.0,"Position":240.0,"HyperDash":false},{"StartTime":133875.0,"Position":265.918579,"HyperDash":false},{"StartTime":133930.0,"Position":308.4777,"HyperDash":false},{"StartTime":133984.0,"Position":356.101166,"HyperDash":false},{"StartTime":134039.0,"Position":367.7393,"HyperDash":false},{"StartTime":134130.0,"Position":392.480377,"HyperDash":false},{"StartTime":134258.0,"Position":390.924835,"HyperDash":false}]},{"StartTime":134478.0,"Objects":[{"StartTime":134478.0,"Position":196.0,"HyperDash":false},{"StartTime":134569.0,"Position":183.414413,"HyperDash":false},{"StartTime":134696.0,"Position":196.992783,"HyperDash":false}]},{"StartTime":134916.0,"Objects":[{"StartTime":134916.0,"Position":391.0,"HyperDash":true}]},{"StartTime":135135.0,"Objects":[{"StartTime":135135.0,"Position":73.0,"HyperDash":false},{"StartTime":135189.0,"Position":110.225349,"HyperDash":false},{"StartTime":135244.0,"Position":131.973389,"HyperDash":false},{"StartTime":135298.0,"Position":154.19873,"HyperDash":false},{"StartTime":135353.0,"Position":186.946777,"HyperDash":false},{"StartTime":135444.0,"Position":152.597885,"HyperDash":false},{"StartTime":135572.0,"Position":74.8510361,"HyperDash":false}]},{"StartTime":136011.0,"Objects":[{"StartTime":136011.0,"Position":434.0,"HyperDash":false},{"StartTime":136102.0,"Position":424.411469,"HyperDash":false},{"StartTime":136229.0,"Position":434.9857,"HyperDash":false}]},{"StartTime":136449.0,"Objects":[{"StartTime":136449.0,"Position":227.0,"HyperDash":false}]},{"StartTime":136668.0,"Objects":[{"StartTime":136668.0,"Position":434.0,"HyperDash":true}]},{"StartTime":136887.0,"Objects":[{"StartTime":136887.0,"Position":116.0,"HyperDash":false},{"StartTime":136941.0,"Position":169.105255,"HyperDash":false},{"StartTime":136996.0,"Position":171.8421,"HyperDash":false},{"StartTime":137050.0,"Position":216.947357,"HyperDash":false},{"StartTime":137105.0,"Position":253.6842,"HyperDash":false},{"StartTime":137196.0,"Position":316.1579,"HyperDash":false},{"StartTime":137324.0,"Position":392.0,"HyperDash":true}]},{"StartTime":137544.0,"Objects":[{"StartTime":137544.0,"Position":100.0,"HyperDash":false}]},{"StartTime":137653.0,"Objects":[{"StartTime":137653.0,"Position":182.0,"HyperDash":false}]},{"StartTime":137763.0,"Objects":[{"StartTime":137763.0,"Position":242.0,"HyperDash":false}]},{"StartTime":137982.0,"Objects":[{"StartTime":137982.0,"Position":62.0,"HyperDash":false}]},{"StartTime":138201.0,"Objects":[{"StartTime":138201.0,"Position":241.0,"HyperDash":false},{"StartTime":138292.0,"Position":173.399414,"HyperDash":false},{"StartTime":138419.0,"Position":103.011795,"HyperDash":true}]},{"StartTime":138639.0,"Objects":[{"StartTime":138639.0,"Position":421.0,"HyperDash":false},{"StartTime":138693.0,"Position":392.894928,"HyperDash":false},{"StartTime":138748.0,"Position":354.1583,"HyperDash":false},{"StartTime":138802.0,"Position":299.053223,"HyperDash":false},{"StartTime":138857.0,"Position":283.3166,"HyperDash":false},{"StartTime":138948.0,"Position":230.843246,"HyperDash":false},{"StartTime":139076.0,"Position":145.001617,"HyperDash":false}]},{"StartTime":139296.0,"Objects":[{"StartTime":139296.0,"Position":339.0,"HyperDash":false},{"StartTime":139405.0,"Position":339.884552,"HyperDash":false}]},{"StartTime":139515.0,"Objects":[{"StartTime":139515.0,"Position":235.0,"HyperDash":false}]},{"StartTime":139734.0,"Objects":[{"StartTime":139734.0,"Position":55.0,"HyperDash":false}]},{"StartTime":139953.0,"Objects":[{"StartTime":139953.0,"Position":344.0,"HyperDash":false},{"StartTime":140044.0,"Position":417.604126,"HyperDash":false},{"StartTime":140171.0,"Position":481.9967,"HyperDash":true}]},{"StartTime":140390.0,"Objects":[{"StartTime":140390.0,"Position":136.0,"HyperDash":false},{"StartTime":140481.0,"Position":128.599976,"HyperDash":false},{"StartTime":140608.0,"Position":135.041687,"HyperDash":false}]},{"StartTime":140828.0,"Objects":[{"StartTime":140828.0,"Position":328.0,"HyperDash":false}]},{"StartTime":141047.0,"Objects":[{"StartTime":141047.0,"Position":135.0,"HyperDash":false}]},{"StartTime":141266.0,"Objects":[{"StartTime":141266.0,"Position":342.0,"HyperDash":false}]},{"StartTime":141485.0,"Objects":[{"StartTime":141485.0,"Position":493.0,"HyperDash":false}]},{"StartTime":141704.0,"Objects":[{"StartTime":141704.0,"Position":299.0,"HyperDash":false}]},{"StartTime":141923.0,"Objects":[{"StartTime":141923.0,"Position":91.0,"HyperDash":false}]},{"StartTime":142142.0,"Objects":[{"StartTime":142142.0,"Position":380.0,"HyperDash":false},{"StartTime":142196.0,"Position":335.923767,"HyperDash":false},{"StartTime":142251.0,"Position":318.2165,"HyperDash":false},{"StartTime":142305.0,"Position":259.140259,"HyperDash":false},{"StartTime":142360.0,"Position":242.432953,"HyperDash":false},{"StartTime":142415.0,"Position":215.7257,"HyperDash":false},{"StartTime":142470.0,"Position":173.0184,"HyperDash":false},{"StartTime":142524.0,"Position":215.09462,"HyperDash":false},{"StartTime":142579.0,"Position":241.801926,"HyperDash":false},{"StartTime":142670.0,"Position":299.226685,"HyperDash":false},{"StartTime":142798.0,"Position":380.0,"HyperDash":false}]},{"StartTime":143018.0,"Objects":[{"StartTime":143018.0,"Position":185.0,"HyperDash":false},{"StartTime":143072.0,"Position":198.796173,"HyperDash":false},{"StartTime":143127.0,"Position":265.955566,"HyperDash":false},{"StartTime":143181.0,"Position":287.965,"HyperDash":false},{"StartTime":143236.0,"Position":318.749146,"HyperDash":false},{"StartTime":143290.0,"Position":347.800385,"HyperDash":false},{"StartTime":143345.0,"Position":395.071777,"HyperDash":false},{"StartTime":143400.0,"Position":413.8512,"HyperDash":false},{"StartTime":143455.0,"Position":420.164917,"HyperDash":false},{"StartTime":143546.0,"Position":449.768,"HyperDash":false},{"StartTime":143674.0,"Position":428.7935,"HyperDash":true}]},{"StartTime":143894.0,"Objects":[{"StartTime":143894.0,"Position":82.0,"HyperDash":false},{"StartTime":143985.0,"Position":35.4371643,"HyperDash":false},{"StartTime":144112.0,"Position":83.57783,"HyperDash":false}]},{"StartTime":144223.0,"Objects":[{"StartTime":144223.0,"Position":174.0,"HyperDash":false}]},{"StartTime":144332.0,"Objects":[{"StartTime":144332.0,"Position":84.0,"HyperDash":false},{"StartTime":144441.0,"Position":83.06765,"HyperDash":true}]},{"StartTime":144551.0,"Objects":[{"StartTime":144551.0,"Position":284.0,"HyperDash":false},{"StartTime":144660.0,"Position":353.0,"HyperDash":true}]},{"StartTime":144770.0,"Objects":[{"StartTime":144770.0,"Position":117.0,"HyperDash":false},{"StartTime":144879.0,"Position":48.0,"HyperDash":true}]},{"StartTime":144989.0,"Objects":[{"StartTime":144989.0,"Position":249.0,"HyperDash":true}]},{"StartTime":145099.0,"Objects":[{"StartTime":145099.0,"Position":48.0,"HyperDash":false}]},{"StartTime":145208.0,"Objects":[{"StartTime":145208.0,"Position":144.0,"HyperDash":false},{"StartTime":145299.0,"Position":184.508865,"HyperDash":false},{"StartTime":145426.0,"Position":138.552429,"HyperDash":false}]},{"StartTime":145536.0,"Objects":[{"StartTime":145536.0,"Position":55.0,"HyperDash":true}]},{"StartTime":145646.0,"Objects":[{"StartTime":145646.0,"Position":290.0,"HyperDash":false},{"StartTime":145755.0,"Position":358.994629,"HyperDash":true}]},{"StartTime":145865.0,"Objects":[{"StartTime":145865.0,"Position":157.0,"HyperDash":true}]},{"StartTime":145974.0,"Objects":[{"StartTime":145974.0,"Position":356.0,"HyperDash":false}]},{"StartTime":146084.0,"Objects":[{"StartTime":146084.0,"Position":453.0,"HyperDash":false},{"StartTime":146175.0,"Position":406.3945,"HyperDash":false},{"StartTime":146302.0,"Position":315.0,"HyperDash":false}]},{"StartTime":146412.0,"Objects":[{"StartTime":146412.0,"Position":412.0,"HyperDash":true}]},{"StartTime":146522.0,"Objects":[{"StartTime":146522.0,"Position":176.0,"HyperDash":false}]},{"StartTime":146631.0,"Objects":[{"StartTime":146631.0,"Position":272.0,"HyperDash":false},{"StartTime":146740.0,"Position":272.9078,"HyperDash":true}]},{"StartTime":146850.0,"Objects":[{"StartTime":146850.0,"Position":71.0,"HyperDash":false}]},{"StartTime":146960.0,"Objects":[{"StartTime":146960.0,"Position":168.0,"HyperDash":false},{"StartTime":147051.0,"Position":93.39449,"HyperDash":false},{"StartTime":147178.0,"Position":30.0000153,"HyperDash":false}]},{"StartTime":147288.0,"Objects":[{"StartTime":147288.0,"Position":113.0,"HyperDash":true}]},{"StartTime":147398.0,"Objects":[{"StartTime":147398.0,"Position":348.0,"HyperDash":false},{"StartTime":147489.0,"Position":401.9966,"HyperDash":false},{"StartTime":147616.0,"Position":345.685974,"HyperDash":false}]},{"StartTime":147726.0,"Objects":[{"StartTime":147726.0,"Position":255.0,"HyperDash":false}]},{"StartTime":147836.0,"Objects":[{"StartTime":147836.0,"Position":345.0,"HyperDash":false},{"StartTime":147945.0,"Position":347.028534,"HyperDash":true}]},{"StartTime":148055.0,"Objects":[{"StartTime":148055.0,"Position":145.0,"HyperDash":false}]},{"StartTime":148164.0,"Objects":[{"StartTime":148164.0,"Position":76.0,"HyperDash":true}]},{"StartTime":148274.0,"Objects":[{"StartTime":148274.0,"Position":280.0,"HyperDash":false},{"StartTime":148383.0,"Position":349.0,"HyperDash":true}]},{"StartTime":148493.0,"Objects":[{"StartTime":148493.0,"Position":147.0,"HyperDash":true}]},{"StartTime":148602.0,"Objects":[{"StartTime":148602.0,"Position":346.0,"HyperDash":false}]},{"StartTime":148712.0,"Objects":[{"StartTime":148712.0,"Position":248.0,"HyperDash":false},{"StartTime":148803.0,"Position":196.3945,"HyperDash":false},{"StartTime":148930.0,"Position":110.0,"HyperDash":false}]},{"StartTime":149040.0,"Objects":[{"StartTime":149040.0,"Position":193.0,"HyperDash":true}]},{"StartTime":149150.0,"Objects":[{"StartTime":149150.0,"Position":428.0,"HyperDash":false},{"StartTime":149241.0,"Position":448.54718,"HyperDash":false},{"StartTime":149368.0,"Position":427.29248,"HyperDash":true}]},{"StartTime":149478.0,"Objects":[{"StartTime":149478.0,"Position":226.0,"HyperDash":false}]},{"StartTime":149588.0,"Objects":[{"StartTime":149588.0,"Position":323.0,"HyperDash":false},{"StartTime":149679.0,"Position":392.6055,"HyperDash":false},{"StartTime":149806.0,"Position":461.0,"HyperDash":false}]},{"StartTime":149916.0,"Objects":[{"StartTime":149916.0,"Position":377.0,"HyperDash":true}]},{"StartTime":150026.0,"Objects":[{"StartTime":150026.0,"Position":141.0,"HyperDash":false}]},{"StartTime":150135.0,"Objects":[{"StartTime":150135.0,"Position":237.0,"HyperDash":false},{"StartTime":150244.0,"Position":238.915924,"HyperDash":true}]},{"StartTime":150354.0,"Objects":[{"StartTime":150354.0,"Position":37.0,"HyperDash":false}]},{"StartTime":150463.0,"Objects":[{"StartTime":150463.0,"Position":133.0,"HyperDash":false},{"StartTime":150554.0,"Position":160.2725,"HyperDash":false},{"StartTime":150681.0,"Position":126.154518,"HyperDash":false}]},{"StartTime":150792.0,"Objects":[{"StartTime":150792.0,"Position":42.0,"HyperDash":true}]},{"StartTime":150901.0,"Objects":[{"StartTime":150901.0,"Position":309.0,"HyperDash":false},{"StartTime":150992.0,"Position":376.6055,"HyperDash":false},{"StartTime":151119.0,"Position":447.0,"HyperDash":false}]},{"StartTime":151230.0,"Objects":[{"StartTime":151230.0,"Position":356.0,"HyperDash":false}]},{"StartTime":151339.0,"Objects":[{"StartTime":151339.0,"Position":445.0,"HyperDash":true}]},{"StartTime":151558.0,"Objects":[{"StartTime":151558.0,"Position":127.0,"HyperDash":false}]},{"StartTime":151668.0,"Objects":[{"StartTime":151668.0,"Position":203.0,"HyperDash":false}]},{"StartTime":151777.0,"Objects":[{"StartTime":151777.0,"Position":239.0,"HyperDash":false}]},{"StartTime":151887.0,"Objects":[{"StartTime":151887.0,"Position":196.0,"HyperDash":false}]},{"StartTime":151996.0,"Objects":[{"StartTime":151996.0,"Position":86.0,"HyperDash":false},{"StartTime":152105.0,"Position":84.23135,"HyperDash":true}]},{"StartTime":152215.0,"Objects":[{"StartTime":152215.0,"Position":285.0,"HyperDash":false},{"StartTime":152306.0,"Position":224.395935,"HyperDash":false},{"StartTime":152433.0,"Position":147.003464,"HyperDash":false}]},{"StartTime":152544.0,"Objects":[{"StartTime":152544.0,"Position":230.0,"HyperDash":true}]},{"StartTime":152653.0,"Objects":[{"StartTime":152653.0,"Position":463.0,"HyperDash":false},{"StartTime":152762.0,"Position":394.006836,"HyperDash":false}]},{"StartTime":152872.0,"Objects":[{"StartTime":152872.0,"Position":284.0,"HyperDash":false},{"StartTime":152981.0,"Position":282.231354,"HyperDash":true}]},{"StartTime":153091.0,"Objects":[{"StartTime":153091.0,"Position":483.0,"HyperDash":false},{"StartTime":153182.0,"Position":408.3958,"HyperDash":false},{"StartTime":153309.0,"Position":345.0032,"HyperDash":false}]},{"StartTime":153420.0,"Objects":[{"StartTime":153420.0,"Position":428.0,"HyperDash":true}]},{"StartTime":153529.0,"Objects":[{"StartTime":153529.0,"Position":227.0,"HyperDash":false},{"StartTime":153638.0,"Position":226.115463,"HyperDash":false}]},{"StartTime":153748.0,"Objects":[{"StartTime":153748.0,"Position":323.0,"HyperDash":false}]},{"StartTime":153967.0,"Objects":[{"StartTime":153967.0,"Position":33.0,"HyperDash":false},{"StartTime":154058.0,"Position":11.8165741,"HyperDash":false},{"StartTime":154185.0,"Position":30.1649818,"HyperDash":false}]},{"StartTime":154296.0,"Objects":[{"StartTime":154296.0,"Position":114.0,"HyperDash":true}]},{"StartTime":154405.0,"Objects":[{"StartTime":154405.0,"Position":381.0,"HyperDash":false},{"StartTime":154459.0,"Position":329.8956,"HyperDash":false},{"StartTime":154514.0,"Position":328.159637,"HyperDash":false},{"StartTime":154568.0,"Position":259.055237,"HyperDash":false},{"StartTime":154623.0,"Position":243.31926,"HyperDash":false},{"StartTime":154714.0,"Position":166.847,"HyperDash":false},{"StartTime":154842.0,"Position":105.006927,"HyperDash":true}]},{"StartTime":155062.0,"Objects":[{"StartTime":155062.0,"Position":451.0,"HyperDash":false},{"StartTime":155116.0,"Position":474.1808,"HyperDash":false},{"StartTime":155171.0,"Position":482.115234,"HyperDash":false},{"StartTime":155225.0,"Position":473.658417,"HyperDash":false},{"StartTime":155280.0,"Position":475.76123,"HyperDash":false},{"StartTime":155371.0,"Position":450.3246,"HyperDash":false},{"StartTime":155499.0,"Position":354.987061,"HyperDash":true}]},{"StartTime":155719.0,"Objects":[{"StartTime":155719.0,"Position":22.0,"HyperDash":false},{"StartTime":155810.0,"Position":63.60431,"HyperDash":false},{"StartTime":155937.0,"Position":159.997131,"HyperDash":true}]},{"StartTime":156157.0,"Objects":[{"StartTime":156157.0,"Position":478.0,"HyperDash":false},{"StartTime":156211.0,"Position":461.9211,"HyperDash":false},{"StartTime":156266.0,"Position":399.211151,"HyperDash":false},{"StartTime":156320.0,"Position":377.132263,"HyperDash":false},{"StartTime":156375.0,"Position":340.4223,"HyperDash":false},{"StartTime":156430.0,"Position":322.712341,"HyperDash":false},{"StartTime":156485.0,"Position":271.00235,"HyperDash":false},{"StartTime":156539.0,"Position":309.0812,"HyperDash":false},{"StartTime":156594.0,"Position":339.7912,"HyperDash":false},{"StartTime":156685.0,"Position":387.220428,"HyperDash":false},{"StartTime":156813.0,"Position":478.0,"HyperDash":true}]},{"StartTime":157033.0,"Objects":[{"StartTime":157033.0,"Position":159.0,"HyperDash":false},{"StartTime":157087.0,"Position":134.242828,"HyperDash":false},{"StartTime":157142.0,"Position":89.84937,"HyperDash":false},{"StartTime":157196.0,"Position":60.5968933,"HyperDash":false},{"StartTime":157251.0,"Position":65.38586,"HyperDash":false},{"StartTime":157342.0,"Position":103.223328,"HyperDash":false},{"StartTime":157470.0,"Position":163.359787,"HyperDash":false}]},{"StartTime":157580.0,"Objects":[{"StartTime":157580.0,"Position":254.0,"HyperDash":false}]},{"StartTime":157690.0,"Objects":[{"StartTime":157690.0,"Position":163.0,"HyperDash":true}]},{"StartTime":157799.0,"Objects":[{"StartTime":157799.0,"Position":396.0,"HyperDash":true}]},{"StartTime":157909.0,"Objects":[{"StartTime":157909.0,"Position":163.0,"HyperDash":false},{"StartTime":158000.0,"Position":136.677887,"HyperDash":false},{"StartTime":158127.0,"Position":164.098557,"HyperDash":false}]},{"StartTime":158237.0,"Objects":[{"StartTime":158237.0,"Position":255.0,"HyperDash":false}]},{"StartTime":158347.0,"Objects":[{"StartTime":158347.0,"Position":164.0,"HyperDash":false},{"StartTime":158456.0,"Position":162.135818,"HyperDash":true}]},{"StartTime":158566.0,"Objects":[{"StartTime":158566.0,"Position":363.0,"HyperDash":false},{"StartTime":158675.0,"Position":363.919922,"HyperDash":true}]},{"StartTime":158785.0,"Objects":[{"StartTime":158785.0,"Position":128.0,"HyperDash":false},{"StartTime":158894.0,"Position":196.994614,"HyperDash":true}]},{"StartTime":159004.0,"Objects":[{"StartTime":159004.0,"Position":398.0,"HyperDash":true}]},{"StartTime":159113.0,"Objects":[{"StartTime":159113.0,"Position":198.0,"HyperDash":false}]},{"StartTime":159223.0,"Objects":[{"StartTime":159223.0,"Position":100.0,"HyperDash":false},{"StartTime":159314.0,"Position":80.50117,"HyperDash":false},{"StartTime":159441.0,"Position":104.636375,"HyperDash":false}]},{"StartTime":159551.0,"Objects":[{"StartTime":159551.0,"Position":187.0,"HyperDash":true}]},{"StartTime":159661.0,"Objects":[{"StartTime":159661.0,"Position":422.0,"HyperDash":false},{"StartTime":159770.0,"Position":353.00705,"HyperDash":true}]},{"StartTime":159880.0,"Objects":[{"StartTime":159880.0,"Position":151.0,"HyperDash":true}]},{"StartTime":159989.0,"Objects":[{"StartTime":159989.0,"Position":350.0,"HyperDash":false}]},{"StartTime":160099.0,"Objects":[{"StartTime":160099.0,"Position":254.0,"HyperDash":false},{"StartTime":160190.0,"Position":324.6055,"HyperDash":false},{"StartTime":160317.0,"Position":392.0,"HyperDash":false}]},{"StartTime":160427.0,"Objects":[{"StartTime":160427.0,"Position":296.0,"HyperDash":true}]},{"StartTime":160536.0,"Objects":[{"StartTime":160536.0,"Position":62.0,"HyperDash":false},{"StartTime":160645.0,"Position":61.054882,"HyperDash":false}]},{"StartTime":160755.0,"Objects":[{"StartTime":160755.0,"Position":171.0,"HyperDash":false},{"StartTime":160864.0,"Position":240.0,"HyperDash":true}]},{"StartTime":160974.0,"Objects":[{"StartTime":160974.0,"Position":441.0,"HyperDash":false},{"StartTime":161065.0,"Position":460.246124,"HyperDash":false},{"StartTime":161192.0,"Position":438.9324,"HyperDash":false}]},{"StartTime":161303.0,"Objects":[{"StartTime":161303.0,"Position":354.0,"HyperDash":true}]},{"StartTime":161412.0,"Objects":[{"StartTime":161412.0,"Position":120.0,"HyperDash":false},{"StartTime":161503.0,"Position":188.6055,"HyperDash":false},{"StartTime":161630.0,"Position":258.0,"HyperDash":false}]},{"StartTime":161741.0,"Objects":[{"StartTime":161741.0,"Position":167.0,"HyperDash":false}]},{"StartTime":161850.0,"Objects":[{"StartTime":161850.0,"Position":256.0,"HyperDash":false},{"StartTime":161959.0,"Position":256.873352,"HyperDash":true}]},{"StartTime":162069.0,"Objects":[{"StartTime":162069.0,"Position":55.0,"HyperDash":false},{"StartTime":162178.0,"Position":53.2083969,"HyperDash":true}]},{"StartTime":162288.0,"Objects":[{"StartTime":162288.0,"Position":288.0,"HyperDash":false},{"StartTime":162397.0,"Position":357.0,"HyperDash":true}]},{"StartTime":162507.0,"Objects":[{"StartTime":162507.0,"Position":155.0,"HyperDash":true}]},{"StartTime":162617.0,"Objects":[{"StartTime":162617.0,"Position":356.0,"HyperDash":false}]},{"StartTime":162726.0,"Objects":[{"StartTime":162726.0,"Position":452.0,"HyperDash":false},{"StartTime":162817.0,"Position":467.2106,"HyperDash":false},{"StartTime":162944.0,"Position":448.8102,"HyperDash":false}]},{"StartTime":163055.0,"Objects":[{"StartTime":163055.0,"Position":364.0,"HyperDash":true}]},{"StartTime":163164.0,"Objects":[{"StartTime":163164.0,"Position":130.0,"HyperDash":false},{"StartTime":163273.0,"Position":128.231354,"HyperDash":false}]},{"StartTime":163383.0,"Objects":[{"StartTime":163383.0,"Position":239.0,"HyperDash":false},{"StartTime":163492.0,"Position":240.915924,"HyperDash":true}]},{"StartTime":163602.0,"Objects":[{"StartTime":163602.0,"Position":39.0,"HyperDash":false},{"StartTime":163711.0,"Position":108.0,"HyperDash":true}]},{"StartTime":163821.0,"Objects":[{"StartTime":163821.0,"Position":378.0,"HyperDash":false},{"StartTime":163930.0,"Position":379.0146,"HyperDash":false}]},{"StartTime":164040.0,"Objects":[{"StartTime":164040.0,"Position":268.0,"HyperDash":false},{"StartTime":164149.0,"Position":199.0,"HyperDash":true}]},{"StartTime":164259.0,"Objects":[{"StartTime":164259.0,"Position":400.0,"HyperDash":false},{"StartTime":164368.0,"Position":401.8897,"HyperDash":true}]},{"StartTime":164478.0,"Objects":[{"StartTime":164478.0,"Position":200.0,"HyperDash":false},{"StartTime":164587.0,"Position":131.0,"HyperDash":true}]},{"StartTime":164697.0,"Objects":[{"StartTime":164697.0,"Position":366.0,"HyperDash":false},{"StartTime":164806.0,"Position":434.995453,"HyperDash":true}]},{"StartTime":164916.0,"Objects":[{"StartTime":164916.0,"Position":164.0,"HyperDash":false},{"StartTime":165007.0,"Position":99.39598,"HyperDash":false},{"StartTime":165134.0,"Position":26.00357,"HyperDash":false}]},{"StartTime":165244.0,"Objects":[{"StartTime":165244.0,"Position":116.0,"HyperDash":false}]},{"StartTime":165354.0,"Objects":[{"StartTime":165354.0,"Position":27.0,"HyperDash":true}]},{"StartTime":165573.0,"Objects":[{"StartTime":165573.0,"Position":344.0,"HyperDash":false}]},{"StartTime":165682.0,"Objects":[{"StartTime":165682.0,"Position":381.0,"HyperDash":false}]},{"StartTime":165792.0,"Objects":[{"StartTime":165792.0,"Position":339.0,"HyperDash":false}]},{"StartTime":165901.0,"Objects":[{"StartTime":165901.0,"Position":263.0,"HyperDash":false}]},{"StartTime":166011.0,"Objects":[{"StartTime":166011.0,"Position":152.0,"HyperDash":false},{"StartTime":166120.0,"Position":151.092178,"HyperDash":true}]},{"StartTime":166230.0,"Objects":[{"StartTime":166230.0,"Position":352.0,"HyperDash":false}]},{"StartTime":166339.0,"Objects":[{"StartTime":166339.0,"Position":427.0,"HyperDash":false}]},{"StartTime":166449.0,"Objects":[{"StartTime":166449.0,"Position":464.0,"HyperDash":false}]},{"StartTime":166558.0,"Objects":[{"StartTime":166558.0,"Position":425.0,"HyperDash":true}]},{"StartTime":166668.0,"Objects":[{"StartTime":166668.0,"Position":189.0,"HyperDash":false}]},{"StartTime":166777.0,"Objects":[{"StartTime":166777.0,"Position":116.0,"HyperDash":false}]},{"StartTime":166887.0,"Objects":[{"StartTime":166887.0,"Position":125.0,"HyperDash":false}]},{"StartTime":166996.0,"Objects":[{"StartTime":166996.0,"Position":199.0,"HyperDash":false}]},{"StartTime":167106.0,"Objects":[{"StartTime":167106.0,"Position":309.0,"HyperDash":false},{"StartTime":167215.0,"Position":310.768646,"HyperDash":false}]},{"StartTime":167325.0,"Objects":[{"StartTime":167325.0,"Position":199.0,"HyperDash":false},{"StartTime":167434.0,"Position":197.084076,"HyperDash":true}]},{"StartTime":167544.0,"Objects":[{"StartTime":167544.0,"Position":398.0,"HyperDash":false},{"StartTime":167653.0,"Position":467.0,"HyperDash":false}]},{"StartTime":167763.0,"Objects":[{"StartTime":167763.0,"Position":356.0,"HyperDash":false},{"StartTime":167872.0,"Position":287.00647,"HyperDash":true}]},{"StartTime":167982.0,"Objects":[{"StartTime":167982.0,"Position":85.0,"HyperDash":false},{"StartTime":168091.0,"Position":16.0,"HyperDash":false}]},{"StartTime":168201.0,"Objects":[{"StartTime":168201.0,"Position":126.0,"HyperDash":false},{"StartTime":168310.0,"Position":195.0,"HyperDash":true}]},{"StartTime":168420.0,"Objects":[{"StartTime":168420.0,"Position":430.0,"HyperDash":false},{"StartTime":168474.0,"Position":467.7612,"HyperDash":false},{"StartTime":168529.0,"Position":476.801575,"HyperDash":false},{"StartTime":168583.0,"Position":504.865875,"HyperDash":false},{"StartTime":168638.0,"Position":482.8523,"HyperDash":false},{"StartTime":168729.0,"Position":447.068665,"HyperDash":false},{"StartTime":168857.0,"Position":367.438934,"HyperDash":false}]},{"StartTime":169077.0,"Objects":[{"StartTime":169077.0,"Position":174.0,"HyperDash":false}]},{"StartTime":169186.0,"Objects":[{"StartTime":169186.0,"Position":99.0,"HyperDash":false}]},{"StartTime":169296.0,"Objects":[{"StartTime":169296.0,"Position":67.0,"HyperDash":false}]},{"StartTime":169405.0,"Objects":[{"StartTime":169405.0,"Position":101.0,"HyperDash":false}]},{"StartTime":169515.0,"Objects":[{"StartTime":169515.0,"Position":176.0,"HyperDash":false}]},{"StartTime":169734.0,"Objects":[{"StartTime":169734.0,"Position":465.0,"HyperDash":false},{"StartTime":169825.0,"Position":484.828766,"HyperDash":false},{"StartTime":169952.0,"Position":466.9854,"HyperDash":false}]},{"StartTime":170062.0,"Objects":[{"StartTime":170062.0,"Position":390.0,"HyperDash":true}]},{"StartTime":170172.0,"Objects":[{"StartTime":170172.0,"Position":154.0,"HyperDash":false},{"StartTime":170226.0,"Position":188.078888,"HyperDash":false},{"StartTime":170281.0,"Position":228.788879,"HyperDash":false},{"StartTime":170335.0,"Position":239.867767,"HyperDash":false},{"StartTime":170390.0,"Position":291.577759,"HyperDash":false},{"StartTime":170500.0,"Position":360.997742,"HyperDash":true}]},{"StartTime":170609.0,"Objects":[{"StartTime":170609.0,"Position":127.0,"HyperDash":false},{"StartTime":170700.0,"Position":112.127007,"HyperDash":false},{"StartTime":170827.0,"Position":125.797905,"HyperDash":false}]},{"StartTime":170938.0,"Objects":[{"StartTime":170938.0,"Position":202.0,"HyperDash":true}]},{"StartTime":171047.0,"Objects":[{"StartTime":171047.0,"Position":401.0,"HyperDash":false},{"StartTime":171101.0,"Position":350.353882,"HyperDash":false},{"StartTime":171156.0,"Position":321.8849,"HyperDash":false},{"StartTime":171210.0,"Position":305.955536,"HyperDash":false},{"StartTime":171265.0,"Position":268.51535,"HyperDash":false},{"StartTime":171319.0,"Position":246.017654,"HyperDash":false},{"StartTime":171374.0,"Position":211.42424,"HyperDash":false},{"StartTime":171429.0,"Position":173.4286,"HyperDash":false},{"StartTime":171484.0,"Position":155.9888,"HyperDash":false},{"StartTime":171575.0,"Position":145.032578,"HyperDash":false},{"StartTime":171703.0,"Position":125.051888,"HyperDash":false}]},{"StartTime":171923.0,"Objects":[{"StartTime":171923.0,"Position":416.0,"HyperDash":false}]},{"StartTime":178712.0,"Objects":[{"StartTime":178712.0,"Position":85.0,"HyperDash":true}]},{"StartTime":178931.0,"Objects":[{"StartTime":178931.0,"Position":402.0,"HyperDash":false},{"StartTime":179022.0,"Position":430.926239,"HyperDash":false},{"StartTime":179149.0,"Position":400.1261,"HyperDash":false}]},{"StartTime":179259.0,"Objects":[{"StartTime":179259.0,"Position":323.0,"HyperDash":false}]},{"StartTime":179369.0,"Objects":[{"StartTime":179369.0,"Position":212.0,"HyperDash":false},{"StartTime":179460.0,"Position":173.1731,"HyperDash":false},{"StartTime":179587.0,"Position":94.04442,"HyperDash":false}]},{"StartTime":179697.0,"Objects":[{"StartTime":179697.0,"Position":170.0,"HyperDash":false}]},{"StartTime":179807.0,"Objects":[{"StartTime":179807.0,"Position":280.0,"HyperDash":false},{"StartTime":179898.0,"Position":342.6055,"HyperDash":false},{"StartTime":180025.0,"Position":418.0,"HyperDash":false}]},{"StartTime":180135.0,"Objects":[{"StartTime":180135.0,"Position":307.0,"HyperDash":false}]},{"StartTime":180244.0,"Objects":[{"StartTime":180244.0,"Position":238.0,"HyperDash":false}]},{"StartTime":180354.0,"Objects":[{"StartTime":180354.0,"Position":307.0,"HyperDash":false}]},{"StartTime":180463.0,"Objects":[{"StartTime":180463.0,"Position":417.0,"HyperDash":false},{"StartTime":180572.0,"Position":417.896027,"HyperDash":true}]},{"StartTime":180682.0,"Objects":[{"StartTime":180682.0,"Position":216.0,"HyperDash":false}]},{"StartTime":180792.0,"Objects":[{"StartTime":180792.0,"Position":313.0,"HyperDash":false}]},{"StartTime":180901.0,"Objects":[{"StartTime":180901.0,"Position":381.0,"HyperDash":false}]},{"StartTime":181011.0,"Objects":[{"StartTime":181011.0,"Position":313.0,"HyperDash":false}]},{"StartTime":181120.0,"Objects":[{"StartTime":181120.0,"Position":203.0,"HyperDash":false}]},{"StartTime":181230.0,"Objects":[{"StartTime":181230.0,"Position":133.0,"HyperDash":false}]},{"StartTime":181339.0,"Objects":[{"StartTime":181339.0,"Position":203.0,"HyperDash":false}]},{"StartTime":181558.0,"Objects":[{"StartTime":181558.0,"Position":396.0,"HyperDash":false},{"StartTime":181649.0,"Position":414.144623,"HyperDash":false},{"StartTime":181776.0,"Position":397.136444,"HyperDash":false}]},{"StartTime":181887.0,"Objects":[{"StartTime":181887.0,"Position":320.0,"HyperDash":false}]},{"StartTime":181996.0,"Objects":[{"StartTime":181996.0,"Position":210.0,"HyperDash":false},{"StartTime":182087.0,"Position":169.395859,"HyperDash":false},{"StartTime":182214.0,"Position":72.00328,"HyperDash":false}]},{"StartTime":182325.0,"Objects":[{"StartTime":182325.0,"Position":148.0,"HyperDash":true}]},{"StartTime":182434.0,"Objects":[{"StartTime":182434.0,"Position":347.0,"HyperDash":false}]},{"StartTime":182544.0,"Objects":[{"StartTime":182544.0,"Position":416.0,"HyperDash":false}]},{"StartTime":182653.0,"Objects":[{"StartTime":182653.0,"Position":347.0,"HyperDash":false}]},{"StartTime":182872.0,"Objects":[{"StartTime":182872.0,"Position":154.0,"HyperDash":false}]},{"StartTime":182982.0,"Objects":[{"StartTime":182982.0,"Position":85.0,"HyperDash":false}]},{"StartTime":183091.0,"Objects":[{"StartTime":183091.0,"Position":154.0,"HyperDash":false}]},{"StartTime":183310.0,"Objects":[{"StartTime":183310.0,"Position":347.0,"HyperDash":false},{"StartTime":183401.0,"Position":374.666382,"HyperDash":false},{"StartTime":183528.0,"Position":343.605865,"HyperDash":false}]},{"StartTime":183639.0,"Objects":[{"StartTime":183639.0,"Position":231.0,"HyperDash":false}]},{"StartTime":183748.0,"Objects":[{"StartTime":183748.0,"Position":162.0,"HyperDash":false}]},{"StartTime":183858.0,"Objects":[{"StartTime":183858.0,"Position":231.0,"HyperDash":false}]},{"StartTime":183967.0,"Objects":[{"StartTime":183967.0,"Position":343.0,"HyperDash":false},{"StartTime":184076.0,"Position":344.8897,"HyperDash":true}]},{"StartTime":184186.0,"Objects":[{"StartTime":184186.0,"Position":143.0,"HyperDash":false}]},{"StartTime":184405.0,"Objects":[{"StartTime":184405.0,"Position":323.0,"HyperDash":false}]},{"StartTime":184624.0,"Objects":[{"StartTime":184624.0,"Position":143.0,"HyperDash":false},{"StartTime":184715.0,"Position":105.191986,"HyperDash":false},{"StartTime":184842.0,"Position":143.952225,"HyperDash":false}]},{"StartTime":184953.0,"Objects":[{"StartTime":184953.0,"Position":221.0,"HyperDash":true}]},{"StartTime":185062.0,"Objects":[{"StartTime":185062.0,"Position":421.0,"HyperDash":false},{"StartTime":185116.0,"Position":402.9211,"HyperDash":false},{"StartTime":185171.0,"Position":371.211121,"HyperDash":false},{"StartTime":185225.0,"Position":307.1322,"HyperDash":false},{"StartTime":185280.0,"Position":283.422241,"HyperDash":false},{"StartTime":185335.0,"Position":234.712234,"HyperDash":false},{"StartTime":185390.0,"Position":214.002228,"HyperDash":false},{"StartTime":185444.0,"Position":264.081116,"HyperDash":false},{"StartTime":185499.0,"Position":282.791138,"HyperDash":false},{"StartTime":185590.0,"Position":328.2204,"HyperDash":false},{"StartTime":185718.0,"Position":421.0,"HyperDash":true}]},{"StartTime":185938.0,"Objects":[{"StartTime":185938.0,"Position":102.0,"HyperDash":false},{"StartTime":186029.0,"Position":81.6439056,"HyperDash":false},{"StartTime":186156.0,"Position":105.267693,"HyperDash":false}]},{"StartTime":186266.0,"Objects":[{"StartTime":186266.0,"Position":181.0,"HyperDash":false}]},{"StartTime":186376.0,"Objects":[{"StartTime":186376.0,"Position":291.0,"HyperDash":false},{"StartTime":186467.0,"Position":364.6055,"HyperDash":false},{"StartTime":186594.0,"Position":429.0,"HyperDash":false}]},{"StartTime":186704.0,"Objects":[{"StartTime":186704.0,"Position":352.0,"HyperDash":true}]},{"StartTime":186814.0,"Objects":[{"StartTime":186814.0,"Position":150.0,"HyperDash":false},{"StartTime":186905.0,"Position":147.9285,"HyperDash":false},{"StartTime":187032.0,"Position":146.246689,"HyperDash":false}]},{"StartTime":187142.0,"Objects":[{"StartTime":187142.0,"Position":257.0,"HyperDash":false}]},{"StartTime":187252.0,"Objects":[{"StartTime":187252.0,"Position":325.0,"HyperDash":false}]},{"StartTime":187361.0,"Objects":[{"StartTime":187361.0,"Position":253.0,"HyperDash":false}]},{"StartTime":187471.0,"Objects":[{"StartTime":187471.0,"Position":141.0,"HyperDash":false},{"StartTime":187580.0,"Position":72.0,"HyperDash":true}]},{"StartTime":187690.0,"Objects":[{"StartTime":187690.0,"Position":307.0,"HyperDash":false},{"StartTime":187781.0,"Position":334.582428,"HyperDash":false},{"StartTime":187908.0,"Position":308.8075,"HyperDash":false}]},{"StartTime":188128.0,"Objects":[{"StartTime":188128.0,"Position":113.0,"HyperDash":false},{"StartTime":188219.0,"Position":99.06281,"HyperDash":false},{"StartTime":188346.0,"Position":114.246552,"HyperDash":false}]},{"StartTime":188456.0,"Objects":[{"StartTime":188456.0,"Position":190.0,"HyperDash":true}]},{"StartTime":188566.0,"Objects":[{"StartTime":188566.0,"Position":391.0,"HyperDash":false}]},{"StartTime":188785.0,"Objects":[{"StartTime":188785.0,"Position":211.0,"HyperDash":false}]},{"StartTime":189004.0,"Objects":[{"StartTime":189004.0,"Position":390.0,"HyperDash":false},{"StartTime":189095.0,"Position":373.8,"HyperDash":false},{"StartTime":189222.0,"Position":391.916473,"HyperDash":true}]},{"StartTime":189442.0,"Objects":[{"StartTime":189442.0,"Position":73.0,"HyperDash":false}]},{"StartTime":189551.0,"Objects":[{"StartTime":189551.0,"Position":39.0,"HyperDash":false}]},{"StartTime":189661.0,"Objects":[{"StartTime":189661.0,"Position":76.0,"HyperDash":false}]},{"StartTime":189770.0,"Objects":[{"StartTime":189770.0,"Position":158.0,"HyperDash":false}]},{"StartTime":189880.0,"Objects":[{"StartTime":189880.0,"Position":268.0,"HyperDash":false},{"StartTime":189971.0,"Position":212.3957,"HyperDash":false},{"StartTime":190098.0,"Position":130.002914,"HyperDash":false}]},{"StartTime":190208.0,"Objects":[{"StartTime":190208.0,"Position":213.0,"HyperDash":true}]},{"StartTime":190317.0,"Objects":[{"StartTime":190317.0,"Position":412.0,"HyperDash":false},{"StartTime":190408.0,"Position":424.883728,"HyperDash":false},{"StartTime":190535.0,"Position":410.9749,"HyperDash":false}]},{"StartTime":190646.0,"Objects":[{"StartTime":190646.0,"Position":320.0,"HyperDash":false}]},{"StartTime":190755.0,"Objects":[{"StartTime":190755.0,"Position":230.0,"HyperDash":false}]},{"StartTime":190974.0,"Objects":[{"StartTime":190974.0,"Position":409.0,"HyperDash":true}]},{"StartTime":191193.0,"Objects":[{"StartTime":191193.0,"Position":91.0,"HyperDash":false},{"StartTime":191247.0,"Position":44.74952,"HyperDash":false},{"StartTime":191302.0,"Position":25.7194824,"HyperDash":false},{"StartTime":191356.0,"Position":28.6760178,"HyperDash":false},{"StartTime":191411.0,"Position":24.610136,"HyperDash":false},{"StartTime":191502.0,"Position":53.48176,"HyperDash":false},{"StartTime":191630.0,"Position":137.592667,"HyperDash":false}]},{"StartTime":191850.0,"Objects":[{"StartTime":191850.0,"Position":344.0,"HyperDash":false}]},{"StartTime":191960.0,"Objects":[{"StartTime":191960.0,"Position":427.0,"HyperDash":false}]},{"StartTime":192069.0,"Objects":[{"StartTime":192069.0,"Position":344.0,"HyperDash":false}]},{"StartTime":192288.0,"Objects":[{"StartTime":192288.0,"Position":138.0,"HyperDash":false}]},{"StartTime":192507.0,"Objects":[{"StartTime":192507.0,"Position":427.0,"HyperDash":false},{"StartTime":192598.0,"Position":442.391876,"HyperDash":false},{"StartTime":192725.0,"Position":427.938751,"HyperDash":true}]},{"StartTime":192945.0,"Objects":[{"StartTime":192945.0,"Position":81.0,"HyperDash":false},{"StartTime":193036.0,"Position":144.887146,"HyperDash":false},{"StartTime":193163.0,"Position":260.4,"HyperDash":false}]},{"StartTime":193383.0,"Objects":[{"StartTime":193383.0,"Position":81.0,"HyperDash":true},{"StartTime":193474.0,"Position":189.970917,"HyperDash":false},{"StartTime":193601.0,"Position":370.798462,"HyperDash":false}]},{"StartTime":193821.0,"Objects":[{"StartTime":193821.0,"Position":190.0,"HyperDash":false},{"StartTime":193912.0,"Position":279.887146,"HyperDash":false},{"StartTime":194039.0,"Position":369.4,"HyperDash":false}]},{"StartTime":194259.0,"Objects":[{"StartTime":194259.0,"Position":78.0,"HyperDash":true},{"StartTime":194350.0,"Position":207.970978,"HyperDash":false},{"StartTime":194477.0,"Position":367.798584,"HyperDash":true}]},{"StartTime":194697.0,"Objects":[{"StartTime":194697.0,"Position":76.0,"HyperDash":false},{"StartTime":194788.0,"Position":77.1591339,"HyperDash":false},{"StartTime":194915.0,"Position":73.98562,"HyperDash":false}]},{"StartTime":195135.0,"Objects":[{"StartTime":195135.0,"Position":365.0,"HyperDash":true},{"StartTime":195226.0,"Position":253.0291,"HyperDash":false},{"StartTime":195353.0,"Position":75.2016,"HyperDash":true}]},{"StartTime":195573.0,"Objects":[{"StartTime":195573.0,"Position":394.0,"HyperDash":false},{"StartTime":195664.0,"Position":392.411469,"HyperDash":false},{"StartTime":195791.0,"Position":394.9857,"HyperDash":false}]},{"StartTime":196011.0,"Objects":[{"StartTime":196011.0,"Position":105.0,"HyperDash":true},{"StartTime":196102.0,"Position":210.9709,"HyperDash":false},{"StartTime":196229.0,"Position":394.7984,"HyperDash":true}]},{"StartTime":196449.0,"Objects":[{"StartTime":196449.0,"Position":75.0,"HyperDash":true}]},{"StartTime":196668.0,"Objects":[{"StartTime":196668.0,"Position":422.0,"HyperDash":true},{"StartTime":196722.0,"Position":331.3793,"HyperDash":false},{"StartTime":196777.0,"Position":264.4323,"HyperDash":false},{"StartTime":196831.0,"Position":194.811615,"HyperDash":false},{"StartTime":196886.0,"Position":132.201477,"HyperDash":false},{"StartTime":196977.0,"Position":246.232452,"HyperDash":false},{"StartTime":197105.0,"Position":422.0,"HyperDash":true}]},{"StartTime":197325.0,"Objects":[{"StartTime":197325.0,"Position":75.0,"HyperDash":true},{"StartTime":197379.0,"Position":144.6207,"HyperDash":false},{"StartTime":197434.0,"Position":211.567688,"HyperDash":false},{"StartTime":197488.0,"Position":310.1884,"HyperDash":false},{"StartTime":197543.0,"Position":364.798523,"HyperDash":false},{"StartTime":197634.0,"Position":238.767548,"HyperDash":false},{"StartTime":197762.0,"Position":75.0,"HyperDash":true}]},{"StartTime":197982.0,"Objects":[{"StartTime":197982.0,"Position":395.0,"HyperDash":true}]},{"StartTime":198201.0,"Objects":[{"StartTime":198201.0,"Position":47.0,"HyperDash":true},{"StartTime":198292.0,"Position":164.970886,"HyperDash":false},{"StartTime":198419.0,"Position":336.7984,"HyperDash":false}]},{"StartTime":198639.0,"Objects":[{"StartTime":198639.0,"Position":142.0,"HyperDash":false},{"StartTime":198730.0,"Position":237.6467,"HyperDash":false},{"StartTime":198857.0,"Position":335.197571,"HyperDash":true}]},{"StartTime":199077.0,"Objects":[{"StartTime":199077.0,"Position":26.0,"HyperDash":true}]},{"StartTime":199296.0,"Objects":[{"StartTime":199296.0,"Position":371.0,"HyperDash":false},{"StartTime":199350.0,"Position":333.0469,"HyperDash":false},{"StartTime":199405.0,"Position":303.045837,"HyperDash":false},{"StartTime":199459.0,"Position":275.5022,"HyperDash":false},{"StartTime":199514.0,"Position":251.71991,"HyperDash":false},{"StartTime":199605.0,"Position":278.949951,"HyperDash":false},{"StartTime":199733.0,"Position":378.108917,"HyperDash":true}]},{"StartTime":199953.0,"Objects":[{"StartTime":199953.0,"Position":56.0,"HyperDash":false},{"StartTime":200007.0,"Position":103.078979,"HyperDash":false},{"StartTime":200062.0,"Position":109.78904,"HyperDash":false},{"StartTime":200116.0,"Position":145.868011,"HyperDash":false},{"StartTime":200171.0,"Position":193.578079,"HyperDash":false},{"StartTime":200226.0,"Position":229.288147,"HyperDash":false},{"StartTime":200281.0,"Position":262.99823,"HyperDash":false},{"StartTime":200335.0,"Position":225.91925,"HyperDash":false},{"StartTime":200390.0,"Position":194.209167,"HyperDash":false},{"StartTime":200481.0,"Position":139.779785,"HyperDash":false},{"StartTime":200609.0,"Position":56.0,"HyperDash":false}]},{"StartTime":200828.0,"Objects":[{"StartTime":200828.0,"Position":249.0,"HyperDash":false},{"StartTime":200937.0,"Position":250.56778,"HyperDash":false}]},{"StartTime":201047.0,"Objects":[{"StartTime":201047.0,"Position":160.0,"HyperDash":false}]},{"StartTime":201157.0,"Objects":[{"StartTime":201157.0,"Position":250.0,"HyperDash":true}]},{"StartTime":201266.0,"Objects":[{"StartTime":201266.0,"Position":50.0,"HyperDash":false}]},{"StartTime":201376.0,"Objects":[{"StartTime":201376.0,"Position":139.0,"HyperDash":false}]},{"StartTime":201485.0,"Objects":[{"StartTime":201485.0,"Position":50.0,"HyperDash":true}]},{"StartTime":201595.0,"Objects":[{"StartTime":201595.0,"Position":285.0,"HyperDash":true}]},{"StartTime":201704.0,"Objects":[{"StartTime":201704.0,"Position":50.0,"HyperDash":false},{"StartTime":201813.0,"Position":48.2537231,"HyperDash":true}]},{"StartTime":201923.0,"Objects":[{"StartTime":201923.0,"Position":249.0,"HyperDash":true}]},{"StartTime":202033.0,"Objects":[{"StartTime":202033.0,"Position":48.0,"HyperDash":false}]},{"StartTime":202142.0,"Objects":[{"StartTime":202142.0,"Position":141.0,"HyperDash":false},{"StartTime":202233.0,"Position":181.263123,"HyperDash":false},{"StartTime":202360.0,"Position":140.921326,"HyperDash":false}]},{"StartTime":202471.0,"Objects":[{"StartTime":202471.0,"Position":45.0,"HyperDash":true}]},{"StartTime":202580.0,"Objects":[{"StartTime":202580.0,"Position":278.0,"HyperDash":false}]},{"StartTime":202690.0,"Objects":[{"StartTime":202690.0,"Position":180.0,"HyperDash":false},{"StartTime":202799.0,"Position":179.028259,"HyperDash":true}]},{"StartTime":202909.0,"Objects":[{"StartTime":202909.0,"Position":380.0,"HyperDash":false}]},{"StartTime":203018.0,"Objects":[{"StartTime":203018.0,"Position":283.0,"HyperDash":false},{"StartTime":203109.0,"Position":343.604553,"HyperDash":false},{"StartTime":203236.0,"Position":420.997742,"HyperDash":false}]},{"StartTime":203347.0,"Objects":[{"StartTime":203347.0,"Position":337.0,"HyperDash":true}]},{"StartTime":203456.0,"Objects":[{"StartTime":203456.0,"Position":103.0,"HyperDash":false},{"StartTime":203547.0,"Position":60.25659,"HyperDash":false},{"StartTime":203674.0,"Position":111.501694,"HyperDash":false}]},{"StartTime":203785.0,"Objects":[{"StartTime":203785.0,"Position":202.0,"HyperDash":false}]},{"StartTime":203894.0,"Objects":[{"StartTime":203894.0,"Position":111.0,"HyperDash":false},{"StartTime":204003.0,"Position":109.296814,"HyperDash":true}]},{"StartTime":204113.0,"Objects":[{"StartTime":204113.0,"Position":310.0,"HyperDash":false},{"StartTime":204222.0,"Position":378.995667,"HyperDash":true}]},{"StartTime":204332.0,"Objects":[{"StartTime":204332.0,"Position":177.0,"HyperDash":true}]},{"StartTime":204442.0,"Objects":[{"StartTime":204442.0,"Position":378.0,"HyperDash":false},{"StartTime":204551.0,"Position":378.932343,"HyperDash":true}]},{"StartTime":204661.0,"Objects":[{"StartTime":204661.0,"Position":177.0,"HyperDash":false}]},{"StartTime":204770.0,"Objects":[{"StartTime":204770.0,"Position":80.0,"HyperDash":false},{"StartTime":204861.0,"Position":65.8601456,"HyperDash":false},{"StartTime":204988.0,"Position":78.31786,"HyperDash":false}]},{"StartTime":205099.0,"Objects":[{"StartTime":205099.0,"Position":162.0,"HyperDash":true}]},{"StartTime":205208.0,"Objects":[{"StartTime":205208.0,"Position":395.0,"HyperDash":false},{"StartTime":205317.0,"Position":326.0,"HyperDash":true}]},{"StartTime":205427.0,"Objects":[{"StartTime":205427.0,"Position":124.0,"HyperDash":true}]},{"StartTime":205536.0,"Objects":[{"StartTime":205536.0,"Position":323.0,"HyperDash":false}]},{"StartTime":205646.0,"Objects":[{"StartTime":205646.0,"Position":420.0,"HyperDash":false},{"StartTime":205737.0,"Position":379.3955,"HyperDash":false},{"StartTime":205864.0,"Position":282.002441,"HyperDash":false}]},{"StartTime":205974.0,"Objects":[{"StartTime":205974.0,"Position":379.0,"HyperDash":true}]},{"StartTime":206084.0,"Objects":[{"StartTime":206084.0,"Position":143.0,"HyperDash":false},{"StartTime":206193.0,"Position":74.02588,"HyperDash":false}]},{"StartTime":206303.0,"Objects":[{"StartTime":206303.0,"Position":171.0,"HyperDash":true}]},{"StartTime":206412.0,"Objects":[{"StartTime":206412.0,"Position":370.0,"HyperDash":false}]},{"StartTime":206522.0,"Objects":[{"StartTime":206522.0,"Position":467.0,"HyperDash":false},{"StartTime":206613.0,"Position":501.909729,"HyperDash":false},{"StartTime":206740.0,"Position":463.333649,"HyperDash":false}]},{"StartTime":206850.0,"Objects":[{"StartTime":206850.0,"Position":380.0,"HyperDash":true}]},{"StartTime":206960.0,"Objects":[{"StartTime":206960.0,"Position":109.0,"HyperDash":false},{"StartTime":207051.0,"Position":184.6055,"HyperDash":false},{"StartTime":207178.0,"Position":247.0,"HyperDash":false}]},{"StartTime":207288.0,"Objects":[{"StartTime":207288.0,"Position":156.0,"HyperDash":false}]},{"StartTime":207398.0,"Objects":[{"StartTime":207398.0,"Position":65.0,"HyperDash":true}]},{"StartTime":207617.0,"Objects":[{"StartTime":207617.0,"Position":382.0,"HyperDash":false}]},{"StartTime":207726.0,"Objects":[{"StartTime":207726.0,"Position":420.0,"HyperDash":false}]},{"StartTime":207836.0,"Objects":[{"StartTime":207836.0,"Position":378.0,"HyperDash":false}]},{"StartTime":207945.0,"Objects":[{"StartTime":207945.0,"Position":302.0,"HyperDash":false}]},{"StartTime":208055.0,"Objects":[{"StartTime":208055.0,"Position":191.0,"HyperDash":false},{"StartTime":208164.0,"Position":190.092178,"HyperDash":true}]},{"StartTime":208274.0,"Objects":[{"StartTime":208274.0,"Position":391.0,"HyperDash":false},{"StartTime":208365.0,"Position":402.309845,"HyperDash":false},{"StartTime":208492.0,"Position":381.4403,"HyperDash":false}]},{"StartTime":208602.0,"Objects":[{"StartTime":208602.0,"Position":298.0,"HyperDash":true}]},{"StartTime":208712.0,"Objects":[{"StartTime":208712.0,"Position":62.0,"HyperDash":false},{"StartTime":208821.0,"Position":61.1154556,"HyperDash":false}]},{"StartTime":208931.0,"Objects":[{"StartTime":208931.0,"Position":172.0,"HyperDash":false},{"StartTime":209040.0,"Position":240.99353,"HyperDash":true}]},{"StartTime":209150.0,"Objects":[{"StartTime":209150.0,"Position":442.0,"HyperDash":false},{"StartTime":209241.0,"Position":460.81012,"HyperDash":false},{"StartTime":209368.0,"Position":438.616364,"HyperDash":false}]},{"StartTime":209478.0,"Objects":[{"StartTime":209478.0,"Position":355.0,"HyperDash":true}]},{"StartTime":209588.0,"Objects":[{"StartTime":209588.0,"Position":119.0,"HyperDash":false},{"StartTime":209697.0,"Position":116.205,"HyperDash":false}]},{"StartTime":209807.0,"Objects":[{"StartTime":209807.0,"Position":220.0,"HyperDash":false}]},{"StartTime":210026.0,"Objects":[{"StartTime":210026.0,"Position":413.0,"HyperDash":false}]},{"StartTime":210244.0,"Objects":[{"StartTime":210244.0,"Position":124.0,"HyperDash":false},{"StartTime":210353.0,"Position":55.0,"HyperDash":true}]},{"StartTime":210463.0,"Objects":[{"StartTime":210463.0,"Position":325.0,"HyperDash":false},{"StartTime":210517.0,"Position":370.597351,"HyperDash":false},{"StartTime":210572.0,"Position":383.999176,"HyperDash":false},{"StartTime":210626.0,"Position":443.559265,"HyperDash":false},{"StartTime":210681.0,"Position":452.158966,"HyperDash":false},{"StartTime":210772.0,"Position":494.5323,"HyperDash":false},{"StartTime":210900.0,"Position":484.299774,"HyperDash":true}]},{"StartTime":211120.0,"Objects":[{"StartTime":211120.0,"Position":165.0,"HyperDash":false},{"StartTime":211174.0,"Position":212.105072,"HyperDash":false},{"StartTime":211229.0,"Position":213.841736,"HyperDash":false},{"StartTime":211283.0,"Position":247.946808,"HyperDash":false},{"StartTime":211338.0,"Position":302.683472,"HyperDash":false},{"StartTime":211429.0,"Position":349.15686,"HyperDash":false},{"StartTime":211557.0,"Position":440.9985,"HyperDash":true}]},{"StartTime":211777.0,"Objects":[{"StartTime":211777.0,"Position":149.0,"HyperDash":false},{"StartTime":211868.0,"Position":93.3959351,"HyperDash":false},{"StartTime":211995.0,"Position":11.0034637,"HyperDash":true}]},{"StartTime":212215.0,"Objects":[{"StartTime":212215.0,"Position":357.0,"HyperDash":false},{"StartTime":212269.0,"Position":341.920715,"HyperDash":false},{"StartTime":212324.0,"Position":294.210358,"HyperDash":false},{"StartTime":212378.0,"Position":264.1311,"HyperDash":false},{"StartTime":212433.0,"Position":219.420731,"HyperDash":false},{"StartTime":212488.0,"Position":202.710373,"HyperDash":false},{"StartTime":212543.0,"Position":150.0,"HyperDash":false},{"StartTime":212597.0,"Position":190.079254,"HyperDash":false},{"StartTime":212652.0,"Position":218.789642,"HyperDash":false},{"StartTime":212743.0,"Position":290.2195,"HyperDash":false},{"StartTime":212871.0,"Position":357.0,"HyperDash":true}]},{"StartTime":213091.0,"Objects":[{"StartTime":213091.0,"Position":65.0,"HyperDash":false},{"StartTime":213145.0,"Position":117.105263,"HyperDash":false},{"StartTime":213200.0,"Position":132.8421,"HyperDash":false},{"StartTime":213254.0,"Position":151.947357,"HyperDash":false},{"StartTime":213309.0,"Position":202.6842,"HyperDash":false},{"StartTime":213400.0,"Position":256.1579,"HyperDash":false},{"StartTime":213528.0,"Position":341.0,"HyperDash":false}]},{"StartTime":213639.0,"Objects":[{"StartTime":213639.0,"Position":250.0,"HyperDash":false}]},{"StartTime":213748.0,"Objects":[{"StartTime":213748.0,"Position":339.0,"HyperDash":true}]},{"StartTime":213858.0,"Objects":[{"StartTime":213858.0,"Position":103.0,"HyperDash":true}]},{"StartTime":213967.0,"Objects":[{"StartTime":213967.0,"Position":339.0,"HyperDash":false},{"StartTime":214058.0,"Position":364.006348,"HyperDash":false},{"StartTime":214185.0,"Position":336.10022,"HyperDash":false}]},{"StartTime":214296.0,"Objects":[{"StartTime":214296.0,"Position":245.0,"HyperDash":false}]},{"StartTime":214405.0,"Objects":[{"StartTime":214405.0,"Position":334.0,"HyperDash":false},{"StartTime":214514.0,"Position":335.746277,"HyperDash":true}]},{"StartTime":214624.0,"Objects":[{"StartTime":214624.0,"Position":134.0,"HyperDash":false},{"StartTime":214733.0,"Position":65.0045547,"HyperDash":true}]},{"StartTime":214843.0,"Objects":[{"StartTime":214843.0,"Position":300.0,"HyperDash":false},{"StartTime":214952.0,"Position":300.896027,"HyperDash":true}]},{"StartTime":215062.0,"Objects":[{"StartTime":215062.0,"Position":99.0,"HyperDash":true}]},{"StartTime":215172.0,"Objects":[{"StartTime":215172.0,"Position":300.0,"HyperDash":false}]},{"StartTime":215281.0,"Objects":[{"StartTime":215281.0,"Position":203.0,"HyperDash":false},{"StartTime":215372.0,"Position":151.402954,"HyperDash":false},{"StartTime":215499.0,"Position":65.02028,"HyperDash":false}]},{"StartTime":215609.0,"Objects":[{"StartTime":215609.0,"Position":148.0,"HyperDash":true}]},{"StartTime":215719.0,"Objects":[{"StartTime":215719.0,"Position":383.0,"HyperDash":false},{"StartTime":215828.0,"Position":314.0,"HyperDash":true}]},{"StartTime":215938.0,"Objects":[{"StartTime":215938.0,"Position":112.0,"HyperDash":true}]},{"StartTime":216047.0,"Objects":[{"StartTime":216047.0,"Position":311.0,"HyperDash":false}]},{"StartTime":216157.0,"Objects":[{"StartTime":216157.0,"Position":408.0,"HyperDash":false},{"StartTime":216248.0,"Position":431.067078,"HyperDash":false},{"StartTime":216375.0,"Position":402.494934,"HyperDash":false}]},{"StartTime":216485.0,"Objects":[{"StartTime":216485.0,"Position":305.0,"HyperDash":true}]},{"StartTime":216595.0,"Objects":[{"StartTime":216595.0,"Position":69.0,"HyperDash":false},{"StartTime":216704.0,"Position":68.16873,"HyperDash":false}]},{"StartTime":216814.0,"Objects":[{"StartTime":216814.0,"Position":179.0,"HyperDash":false},{"StartTime":216923.0,"Position":247.995117,"HyperDash":true}]},{"StartTime":217033.0,"Objects":[{"StartTime":217033.0,"Position":449.0,"HyperDash":false},{"StartTime":217142.0,"Position":380.0034,"HyperDash":true}]},{"StartTime":217252.0,"Objects":[{"StartTime":217252.0,"Position":178.0,"HyperDash":false},{"StartTime":217361.0,"Position":109.0,"HyperDash":true}]},{"StartTime":217471.0,"Objects":[{"StartTime":217471.0,"Position":344.0,"HyperDash":false},{"StartTime":217562.0,"Position":286.3945,"HyperDash":false},{"StartTime":217689.0,"Position":206.0,"HyperDash":false}]},{"StartTime":217799.0,"Objects":[{"StartTime":217799.0,"Position":289.0,"HyperDash":false}]},{"StartTime":217909.0,"Objects":[{"StartTime":217909.0,"Position":206.0,"HyperDash":false},{"StartTime":218018.0,"Position":205.092178,"HyperDash":true}]},{"StartTime":218128.0,"Objects":[{"StartTime":218128.0,"Position":406.0,"HyperDash":false},{"StartTime":218237.0,"Position":474.99353,"HyperDash":true}]},{"StartTime":218347.0,"Objects":[{"StartTime":218347.0,"Position":239.0,"HyperDash":false},{"StartTime":218456.0,"Position":170.005249,"HyperDash":true}]},{"StartTime":218566.0,"Objects":[{"StartTime":218566.0,"Position":371.0,"HyperDash":true}]},{"StartTime":218675.0,"Objects":[{"StartTime":218675.0,"Position":170.0,"HyperDash":false}]},{"StartTime":218785.0,"Objects":[{"StartTime":218785.0,"Position":267.0,"HyperDash":false},{"StartTime":218876.0,"Position":329.6045,"HyperDash":false},{"StartTime":219003.0,"Position":404.997559,"HyperDash":false}]},{"StartTime":219113.0,"Objects":[{"StartTime":219113.0,"Position":321.0,"HyperDash":true}]},{"StartTime":219223.0,"Objects":[{"StartTime":219223.0,"Position":85.0,"HyperDash":false},{"StartTime":219332.0,"Position":85.0,"HyperDash":true}]},{"StartTime":219442.0,"Objects":[{"StartTime":219442.0,"Position":286.0,"HyperDash":false},{"StartTime":219551.0,"Position":354.996,"HyperDash":true}]},{"StartTime":219661.0,"Objects":[{"StartTime":219661.0,"Position":119.0,"HyperDash":false},{"StartTime":219770.0,"Position":50.0000076,"HyperDash":true}]},{"StartTime":219880.0,"Objects":[{"StartTime":219880.0,"Position":320.0,"HyperDash":false}]},{"StartTime":219989.0,"Objects":[{"StartTime":219989.0,"Position":399.0,"HyperDash":false}]},{"StartTime":220099.0,"Objects":[{"StartTime":220099.0,"Position":402.0,"HyperDash":false}]},{"StartTime":220208.0,"Objects":[{"StartTime":220208.0,"Position":327.0,"HyperDash":true}]},{"StartTime":220317.0,"Objects":[{"StartTime":220317.0,"Position":129.0,"HyperDash":false},{"StartTime":220426.0,"Position":129.0,"HyperDash":true}]},{"StartTime":220536.0,"Objects":[{"StartTime":220536.0,"Position":330.0,"HyperDash":false},{"StartTime":220645.0,"Position":398.953857,"HyperDash":true}]},{"StartTime":220755.0,"Objects":[{"StartTime":220755.0,"Position":163.0,"HyperDash":false},{"StartTime":220864.0,"Position":94.00001,"HyperDash":true}]},{"StartTime":220974.0,"Objects":[{"StartTime":220974.0,"Position":364.0,"HyperDash":false}]},{"StartTime":221084.0,"Objects":[{"StartTime":221084.0,"Position":439.0,"HyperDash":false}]},{"StartTime":221193.0,"Objects":[{"StartTime":221193.0,"Position":426.0,"HyperDash":false}]},{"StartTime":221303.0,"Objects":[{"StartTime":221303.0,"Position":350.0,"HyperDash":false}]},{"StartTime":221412.0,"Objects":[{"StartTime":221412.0,"Position":240.0,"HyperDash":false},{"StartTime":221521.0,"Position":239.148209,"HyperDash":true}]},{"StartTime":221631.0,"Objects":[{"StartTime":221631.0,"Position":440.0,"HyperDash":false}]},{"StartTime":221741.0,"Objects":[{"StartTime":221741.0,"Position":472.0,"HyperDash":false}]},{"StartTime":221850.0,"Objects":[{"StartTime":221850.0,"Position":434.0,"HyperDash":false}]},{"StartTime":221960.0,"Objects":[{"StartTime":221960.0,"Position":357.0,"HyperDash":true}]},{"StartTime":222069.0,"Objects":[{"StartTime":222069.0,"Position":157.0,"HyperDash":false},{"StartTime":222178.0,"Position":88.06657,"HyperDash":true}]},{"StartTime":222288.0,"Objects":[{"StartTime":222288.0,"Position":289.0,"HyperDash":false},{"StartTime":222379.0,"Position":364.60202,"HyperDash":false},{"StartTime":222506.0,"Position":426.991669,"HyperDash":false}]},{"StartTime":222617.0,"Objects":[{"StartTime":222617.0,"Position":343.0,"HyperDash":true}]},{"StartTime":222726.0,"Objects":[{"StartTime":222726.0,"Position":109.0,"HyperDash":false},{"StartTime":222817.0,"Position":84.0503159,"HyperDash":false},{"StartTime":222944.0,"Position":116.766586,"HyperDash":false}]},{"StartTime":223055.0,"Objects":[{"StartTime":223055.0,"Position":207.0,"HyperDash":false}]},{"StartTime":223164.0,"Objects":[{"StartTime":223164.0,"Position":117.0,"HyperDash":false},{"StartTime":223273.0,"Position":114.6221,"HyperDash":true}]},{"StartTime":223383.0,"Objects":[{"StartTime":223383.0,"Position":315.0,"HyperDash":false},{"StartTime":223492.0,"Position":383.995117,"HyperDash":true}]},{"StartTime":223602.0,"Objects":[{"StartTime":223602.0,"Position":148.0,"HyperDash":false},{"StartTime":223711.0,"Position":145.971466,"HyperDash":false}]},{"StartTime":223821.0,"Objects":[{"StartTime":223821.0,"Position":256.0,"HyperDash":false},{"StartTime":223930.0,"Position":325.0,"HyperDash":true}]},{"StartTime":224040.0,"Objects":[{"StartTime":224040.0,"Position":123.0,"HyperDash":false},{"StartTime":224149.0,"Position":192.0,"HyperDash":true}]},{"StartTime":224259.0,"Objects":[{"StartTime":224259.0,"Position":393.0,"HyperDash":false},{"StartTime":224368.0,"Position":393.896027,"HyperDash":true}]},{"StartTime":224478.0,"Objects":[{"StartTime":224478.0,"Position":158.0,"HyperDash":false}]},{"StartTime":224588.0,"Objects":[{"StartTime":224588.0,"Position":82.0,"HyperDash":false}]},{"StartTime":224697.0,"Objects":[{"StartTime":224697.0,"Position":44.0,"HyperDash":false}]},{"StartTime":224807.0,"Objects":[{"StartTime":224807.0,"Position":86.0,"HyperDash":true}]},{"StartTime":224916.0,"Objects":[{"StartTime":224916.0,"Position":285.0,"HyperDash":false},{"StartTime":225025.0,"Position":353.996,"HyperDash":true}]},{"StartTime":225135.0,"Objects":[{"StartTime":225135.0,"Position":83.0,"HyperDash":false}]},{"StartTime":225244.0,"Objects":[{"StartTime":225244.0,"Position":41.0,"HyperDash":false}]},{"StartTime":225354.0,"Objects":[{"StartTime":225354.0,"Position":82.0,"HyperDash":false}]},{"StartTime":225463.0,"Objects":[{"StartTime":225463.0,"Position":157.0,"HyperDash":false}]},{"StartTime":225573.0,"Objects":[{"StartTime":225573.0,"Position":267.0,"HyperDash":false},{"StartTime":225682.0,"Position":267.0,"HyperDash":true}]},{"StartTime":225792.0,"Objects":[{"StartTime":225792.0,"Position":65.0,"HyperDash":false},{"StartTime":225901.0,"Position":64.19773,"HyperDash":false}]},{"StartTime":226011.0,"Objects":[{"StartTime":226011.0,"Position":154.0,"HyperDash":false}]},{"StartTime":226120.0,"Objects":[{"StartTime":226120.0,"Position":64.0,"HyperDash":true}]},{"StartTime":226230.0,"Objects":[{"StartTime":226230.0,"Position":299.0,"HyperDash":false}]},{"StartTime":226449.0,"Objects":[{"StartTime":226449.0,"Position":105.0,"HyperDash":false},{"StartTime":226558.0,"Position":104.115456,"HyperDash":true}]},{"StartTime":226668.0,"Objects":[{"StartTime":226668.0,"Position":305.0,"HyperDash":true}]},{"StartTime":226777.0,"Objects":[{"StartTime":226777.0,"Position":104.0,"HyperDash":false},{"StartTime":226886.0,"Position":35.0059738,"HyperDash":true}]},{"StartTime":227106.0,"Objects":[{"StartTime":227106.0,"Position":383.0,"HyperDash":false},{"StartTime":227160.0,"Position":350.499268,"HyperDash":false},{"StartTime":227215.0,"Position":324.281738,"HyperDash":false},{"StartTime":227269.0,"Position":266.25296,"HyperDash":false},{"StartTime":227324.0,"Position":247.835876,"HyperDash":false},{"StartTime":227378.0,"Position":218.959808,"HyperDash":false},{"StartTime":227433.0,"Position":168.075058,"HyperDash":false},{"StartTime":227488.0,"Position":136.432785,"HyperDash":false},{"StartTime":227543.0,"Position":126.625404,"HyperDash":false},{"StartTime":227597.0,"Position":101.627563,"HyperDash":false},{"StartTime":227652.0,"Position":86.03102,"HyperDash":false},{"StartTime":227707.0,"Position":60.6709824,"HyperDash":false},{"StartTime":227762.0,"Position":57.8545761,"HyperDash":false},{"StartTime":227853.0,"Position":59.5702324,"HyperDash":false},{"StartTime":227981.0,"Position":63.0289955,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3524302.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3524302.osu new file mode 100644 index 0000000000..36f52c4ae2 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3524302.osu @@ -0,0 +1,889 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 +Mode: 2 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:9.2 +ApproachRate:9.2 +SliderMultiplier:2.76 +SliderTickRate:2 + +[Events] +//Background and Video events +//Break Periods +2,88036,100842 +2,172123,178142 +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Layer 4 (Overlay) +//Storyboard Sound Samples + +[TimingPoints] +245,437.956204379562,4,2,1,30,1,0 +17763,-100,4,2,1,65,0,0 +31777,-100,4,2,1,70,0,0 +45792,-100,4,2,1,75,0,0 +52799,-100,4,2,1,80,0,0 +59807,-100,4,2,1,85,0,1 +86960,-90.9090909090909,4,2,1,80,0,1 +87836,-100,4,2,1,75,0,0 +101850,-100,4,2,1,65,0,0 +115865,-100,4,2,1,70,0,0 +129880,-100,4,2,1,75,0,0 +136887,-100,4,2,1,80,0,0 +140828,-100,4,2,1,60,0,0 +141485,-100,4,2,1,65,0,0 +141704,-100,4,2,1,70,0,0 +141923,-100,4,2,1,75,0,0 +142142,-100,4,2,1,80,0,0 +143894,-100,4,2,1,85,0,1 +171923,-100,4,2,1,75,0,0 +178931,-100,4,2,1,75,0,0 +192945,-76.9230769230769,4,2,1,85,0,1 +193383,-47.6190476190476,4,2,1,85,0,1 +193821,-76.9230769230769,4,2,1,85,0,1 +194259,-47.6190476190476,4,2,1,85,0,1 +194697,-100,4,2,1,85,0,1 +195135,-47.6190476190476,4,2,1,85,0,1 +195573,-100,4,2,1,85,0,1 +196011,-47.6190476190476,4,2,1,85,0,1 +196449,-100,4,2,1,85,0,1 +196668,-47.6190476190476,4,2,1,85,0,1 +198639,-71.4285714285714,4,2,2,85,0,1 +199077,-100,4,2,2,85,0,1 +199296,-76.9230769230769,4,2,1,85,0,1 +199953,-100,4,2,1,80,0,0 +201704,-100,4,2,1,85,0,1 +227982,-100,4,2,1,30,0,0 + +[HitObjects] +256,192,14259,12,0,17325,0:0:0:0: +166,339,17763,6,0,L|164:200,1,138,2|0,1:2|0:0,0:0:0:0: +358,201,18201,2,0,L|360:62,1,138,0|0,1:2|0:0,0:0:0:0: +165,63,18639,2,0,L|18:65,1,138,2|2,1:2|0:0,0:0:0:0: +137,64,18967,2,0,L|208:65,1,69,2|0,0:0|1:2,0:0:0:0: +25,64,19296,1,2,0:0:0:0: +314,64,19515,5,2,1:2:0:0: +350,130,19624,1,0,0:0:0:0: +312,196,19734,1,2,0:0:0:0: +118,196,19953,2,0,L|259:197,1,138,2|2,1:2|0:0,0:0:0:0: +449,196,20390,2,0,L|452:342,1,138,2|2,1:2|0:0,0:0:0:0: +271,333,20828,1,2,1:2:0:0: +451,333,21047,1,2,0:0:0:0: +133,333,21266,5,2,1:2:0:0: +97,265,21376,1,0,0:0:0:0: +136,200,21485,1,0,0:0:0:0: +329,200,21704,2,0,L|331:57,1,138,0|0,1:2|0:0,0:0:0:0: +136,62,22142,2,0,L|297:62,1,138,2|2,1:2|0:0,0:0:0:0: +385,62,22471,2,0,L|294:62,1,69,2|0,0:0|1:2,0:0:0:0: +136,62,22799,1,2,0:0:0:0: +425,62,23018,5,2,1:2:0:0: +461,128,23128,1,0,0:0:0:0: +421,192,23237,1,2,0:0:0:0: +227,192,23456,2,0,L|224:332,1,138,2|2,1:2|0:0,0:0:0:0: +404,329,23894,1,2,1:2:0:0: +224,329,24113,1,2,0:0:0:0: +417,329,24332,2,0,L|419:187,1,138,2|2,1:2|0:0,0:0:0:0: +341,191,24661,1,2,0:0:0:0: +107,191,24770,5,2,1:2:0:0: +69,124,24880,1,0,0:0:0:0: +111,61,24989,1,0,0:0:0:0: +304,61,25208,2,0,L|306:200,1,138,0|0,1:2|0:0,0:0:0:0: +111,198,25646,2,0,L|110:337,1,138,2|0,1:2|0:0,0:0:0:0: +220,335,25974,2,0,L|292:335,1,69,2|0,0:0|1:2,0:0:0:0: +108,335,26303,1,2,0:0:0:0: +397,335,26522,5,2,1:2:0:0: +432,268,26631,1,0,0:0:0:0: +395,200,26741,1,2,0:0:0:0: +215,200,26960,1,2,1:2:0:0: +395,200,27179,1,2,0:0:0:0: +201,200,27398,2,0,L|200:59,1,138,2|0,1:2|0:0,0:0:0:0: +380,62,27836,1,0,1:2:0:0: +200,62,28055,1,2,0:0:0:0: +131,62,28164,1,2,0:0:0:0: +365,62,28274,6,0,P|452:120|350:202,1,276,2|0,1:2|0:0,0:0:0:0: +170,202,28931,1,2,0:0:0:0: +349,202,29150,2,0,P|415:208|474:382,1,276,2|0,0:0|0:0,0:0:0:0: +114,381,30026,5,0,1:2:0:0: +292,381,30244,1,8,0:3:0:0: +114,381,30463,2,0,L|113:240,1,138,8|0,0:3|0:0,0:0:0:0: +307,243,30901,2,0,L|309:102,1,138,4|0,0:3|0:0,0:0:0:0: +197,105,31230,2,0,L|129:106,1,69,4|0,0:3|1:2,0:0:0:0: +417,106,31558,2,0,L|418:180,1,69,0|0,3:2|0:0,0:0:0:0: +148,174,31777,5,2,1:2:0:0: +78,174,31887,1,0,0:0:0:0: +148,174,31996,1,0,0:0:0:0: +341,174,32215,2,0,P|354:234|340:315,1,138,0|0,3:2|0:0,0:0:0:0: +265,311,32544,1,0,1:2:0:0: +155,311,32653,2,0,L|-7:310,1,138,2|2,0:0|1:2,0:0:0:0: +93,310,32982,1,2,0:0:0:0: +292,310,33091,1,0,3:2:0:0: +112,310,33310,2,0,L|110:239,1,69,2|0,0:0|0:0,0:0:0:0: +327,242,33529,5,2,1:2:0:0: +396,242,33639,1,0,0:0:0:0: +327,242,33748,1,0,0:0:0:0: +133,242,33967,2,0,L|131:104,1,138,2|2,3:2|0:0,0:0:0:0: +207,104,34296,1,0,1:2:0:0: +316,104,34405,2,0,L|170:104,1,138,2|2,0:0|1:2,0:0:0:0: +254,104,34734,1,0,0:0:0:0: +453,104,34843,2,0,P|466:169|455:240,1,138,2|2,3:2|0:0,0:0:0:0: +378,239,35172,1,2,0:0:0:0: +145,239,35281,5,2,1:2:0:0: +76,239,35390,1,0,0:0:0:0: +145,239,35500,1,0,0:0:0:0: +338,239,35719,2,0,L|340:102,1,138,0|0,3:2|0:0,0:0:0:0: +263,101,36047,1,0,1:2:0:0: +165,101,36157,1,2,0:0:0:0: +263,101,36266,1,2,0:0:0:0: +339,101,36376,1,2,1:2:0:0: +263,101,36485,1,2,0:0:0:0: +61,101,36595,2,0,P|45:160|61:238,1,138,0|2,3:2|0:0,0:0:0:0: +135,234,36923,1,0,0:0:0:0: +371,233,37033,5,2,1:2:0:0: +439,233,37142,1,0,0:0:0:0: +371,233,37252,1,0,0:0:0:0: +177,233,37471,2,0,L|318:233,1,138,2|0,3:2|0:0,0:0:0:0: +238,233,37799,1,0,1:2:0:0: +127,233,37909,2,0,L|125:94,1,138,2|2,0:0|1:2,0:0:0:0: +201,95,38237,1,0,0:0:0:0: +402,95,38347,2,0,P|410:157|404:236,1,138,2|2,3:2|0:0,0:0:0:0: +328,232,38675,1,0,0:0:0:0: +92,233,38785,5,2,1:2:0:0: +23,233,38894,1,0,0:0:0:0: +92,233,39004,1,0,0:0:0:0: +285,233,39223,2,0,L|430:233,1,138,0|0,3:2|0:0,0:0:0:0: +346,233,39551,1,0,1:2:0:0: +235,233,39661,2,0,L|234:160,1,69,2|2,0:0|0:0,0:0:0:0: +344,164,39880,2,0,L|346:93,1,69,2|2,1:2|0:0,0:0:0:0: +144,95,40099,2,0,L|5:95,1,138,0|2,3:2|0:0,0:0:0:0: +82,95,40427,1,0,0:0:0:0: +315,95,40536,5,2,1:2:0:0: +384,95,40646,1,0,0:0:0:0: +315,95,40755,1,2,0:0:0:0: +121,95,40974,2,0,L|119:234,1,138,2|2,3:2|0:0,0:0:0:0: +195,232,41303,1,0,1:2:0:0: +394,232,41412,1,2,0:0:0:0: +214,232,41631,1,0,1:2:0:0: +144,232,41741,1,0,0:0:0:0: +214,232,41850,1,0,3:2:0:0: +407,232,42069,2,0,L|492:232,1,69,2|2,0:0|0:0,0:0:0:0: +240,232,42288,5,2,1:2:0:0: +170,232,42398,1,0,0:0:0:0: +240,232,42507,1,0,0:0:0:0: +419,232,42726,1,2,3:2:0:0: +129,232,42945,2,0,L|128:161,1,69,2|0,0:0|1:2,0:0:0:0: +238,163,43164,2,0,L|380:164,1,138,2|2,0:0|1:2,0:0:0:0: +299,163,43493,1,0,0:0:0:0: +195,163,43602,1,2,3:2:0:0: +374,163,43821,1,2,0:0:0:0: +376,93,43931,1,0,0:0:0:0: +108,163,44040,5,6,1:2:0:0: +106,93,44150,1,2,0:0:0:0: +209,93,44259,1,0,3:2:0:0: +388,93,44478,1,0,3:2:0:0: +195,93,44697,1,2,1:2:0:0: +484,93,44916,1,8,0:3:0:0: +407,93,45026,1,8,0:3:0:0: +213,93,45244,1,8,0:3:0:0: +316,93,45354,2,0,L|460:94,1,138,2|4,0:0|0:3,0:0:0:0: +103,93,45792,6,0,P|17:149|121:239,1,276,6|0,1:2|0:0,0:0:0:0: +294,241,46449,2,0,L|37:136,1,276,2|2,0:0|0:0,0:0:0:0: +204,136,47106,1,2,0:0:0:0: +38,136,47325,1,2,0:0:0:0: +355,136,47544,6,0,P|438:178|341:272,1,276,6|0,1:2|0:0,0:0:0:0: +173,271,48201,1,0,0:0:0:0: +338,271,48420,2,0,P|355:199|200:122,1,276,2|2,0:0|0:0,0:0:0:0: +369,120,49077,1,2,0:0:0:0: +51,120,49296,6,0,L|49:261,1,138,6|2,1:2|0:0,0:0:0:0: +229,257,49734,2,0,L|371:256,1,138,2|2,0:0|0:0,0:0:0:0: +186,256,50172,2,0,L|47:255,1,138,2|2,0:0|0:0,0:0:0:0: +227,255,50609,1,2,0:0:0:0: +47,255,50828,1,2,0:0:0:0: +347,254,51047,6,0,P|438:243|478:85,1,276,6|0,1:2|0:0,0:0:0:0: +118,84,51923,2,0,P|103:147|121:221,1,138,2|2,3:2|3:2,0:0:0:0: +313,217,52361,1,8,0:3:0:0: +119,217,52580,1,8,0:3:0:0: +436,217,52799,6,0,L|127:184,1,276,2|2,1:2|3:2,0:0:0:0: +452,187,53456,1,2,0:0:0:0: +489,128,53566,1,0,1:2:0:0: +454,68,53675,1,0,0:0:0:0: +274,68,53894,1,2,1:2:0:0: +454,68,54113,2,0,L|301:69,1,138,2|2,3:2|0:0,0:0:0:0: +24,68,54551,6,0,L|306:94,1,276,0|0,1:2|3:2,0:0:0:0: +104,93,55208,1,0,0:0:0:0: +62,93,55317,1,0,1:2:0:0: +104,93,55427,1,2,0:0:0:0: +393,93,55646,2,0,L|266:151,1,138,2|0,1:2|3:2,0:0:0:0: +87,150,56084,1,2,0:0:0:0: +432,116,56303,6,0,P|308:196|181:218,1,276,6|2,1:2|3:2,0:0:0:0: +365,218,56960,1,2,1:2:0:0: +75,218,57179,2,0,L|232:214,1,138,2|2,3:2|1:2,0:0:0:0: +407,214,57617,2,0,L|410:69,1,138,2|2,3:2|0:0,0:0:0:0: +118,76,58055,6,0,L|335:76,2,207,2|2|2,1:2|0:0|0:0,0:0:0:0: +312,76,58931,2,0,P|275:213|34:256,1,414,2|0,0:0|0:0,0:0:0:0: +380,255,59807,6,0,P|404:186|380:128,1,138,6|0,1:2|0:0,0:0:0:0: +290,128,60135,1,2,0:0:0:0: +380,128,60244,2,0,L|382:52,1,69,0|0,3:2|0:0,0:0:0:0: +180,59,60463,2,0,L|96:59,1,69,2|0,0:0|1:2,0:0:0:0: +346,59,60682,6,0,L|346:144,1,69,2|2,0:0|0:0,0:0:0:0: +144,128,60901,1,2,1:2:0:0: +345,128,61011,1,2,0:0:0:0: +441,128,61120,2,0,P|475:194|424:240,1,138,0|2,3:2|0:0,0:0:0:0: +355,236,61449,1,0,0:0:0:0: +121,236,61558,6,0,L|120:164,1,69,2|2,1:2|0:0,0:0:0:0: +321,167,61777,1,2,0:0:0:0: +120,167,61887,1,2,0:0:0:0: +23,167,61996,2,0,L|177:166,1,138,0|2,3:2|0:0,0:0:0:0: +63,166,62325,1,0,1:2:0:0: +296,166,62434,6,0,L|297:95,1,69,2|2,0:0|0:0,0:0:0:0: +199,97,62653,1,0,1:2:0:0: +400,97,62763,1,2,0:0:0:0: +303,97,62872,2,0,P|293:153|354:193,1,138,0|2,3:2|0:0,0:0:0:0: +438,192,63201,1,0,0:0:0:0: +204,192,63310,6,0,P|133:187|94:138,1,138,2|0,1:2|0:0,0:0:0:0: +184,137,63639,1,2,0:0:0:0: +93,137,63748,2,0,L|92:53,1,69,0|0,3:2|0:0,0:0:0:0: +293,68,63967,2,0,L|294:143,1,69,2|0,0:0|1:2,0:0:0:0: +93,137,64186,5,2,0:0:0:0: +293,136,64296,2,0,L|361:136,1,69,2|0,0:0|1:2,0:0:0:0: +160,136,64515,1,2,0:0:0:0: +63,136,64624,2,0,P|29:83|79:30,1,138,0|2,3:2|0:0,0:0:0:0: +154,31,64953,1,0,0:0:0:0: +387,31,65062,6,0,L|319:30,1,69,2|2,1:2|0:0,0:0:0:0: +116,29,65281,1,2,0:0:0:0: +318,29,65390,1,2,0:0:0:0: +415,29,65500,2,0,P|452:91|413:129,1,138,0|2,3:2|0:0,0:0:0:0: +315,129,65828,1,0,1:2:0:0: +79,129,65938,6,0,L|78:59,1,69,2|2,0:0|0:0,0:0:0:0: +175,60,66157,1,0,1:2:0:0: +374,60,66266,1,2,0:0:0:0: +276,60,66376,2,0,L|424:61,1,138,0|2,3:2|0:0,0:0:0:0: +331,60,66704,1,0,0:0:0:0: +60,60,66814,6,0,P|28:123|66:176,1,138,6|0,1:2|0:0,0:0:0:0: +151,173,67142,1,2,0:0:0:0: +61,173,67252,1,0,3:2:0:0: +378,173,67471,5,2,1:2:0:0: +422,111,67580,1,0,0:0:0:0: +381,46,67690,1,0,0:0:0:0: +305,44,67799,1,0,0:0:0:0: +194,44,67909,2,0,L|193:121,1,69,0|0,1:2|0:0,0:0:0:0: +428,112,68128,2,0,L|288:112,1,138,2|2,3:2|0:0,0:0:0:0: +373,112,68456,1,0,0:0:0:0: +137,112,68566,6,0,L|135:183,1,69,2|0,1:2|0:0,0:0:0:0: +245,181,68785,2,0,L|246:258,1,69,2|0,0:0|0:0,0:0:0:0: +44,249,69004,2,0,L|191:248,1,138,2|2,3:2|1:2,0:0:0:0: +98,248,69332,1,0,0:0:0:0: +333,248,69442,6,0,L|335:170,1,69,2|0,1:2|0:0,0:0:0:0: +133,179,69661,1,2,1:2:0:0: +326,179,69880,1,2,3:2:0:0: +133,179,70099,2,0,L|131:251,1,69,2|0,0:0|0:0,0:0:0:0: +398,247,70317,6,0,L|106:250,1,276,6|2,1:2|3:2,0:0:0:0: +468,249,70974,2,0,L|177:250,1,276,6|0,1:2|1:2,0:0:0:0: +483,249,71631,2,0,L|334:249,1,138,2|2,3:2|0:0,0:0:0:0: +26,249,72069,6,0,L|243:249,2,207,6|8|8,1:2|0:3|0:3,0:0:0:0: +344,249,72945,2,0,P|434:201|334:113,1,276,6|0,1:2|3:2,0:0:0:0: +247,111,73493,1,0,3:2:0:0: +338,111,73602,1,0,3:2:0:0: +102,111,73712,1,0,3:2:0:0: +338,111,73821,6,0,P|372:156|334:220,1,138,6|0,1:2|0:0,0:0:0:0: +244,219,74150,1,2,0:0:0:0: +334,219,74259,2,0,L|335:147,1,69,0|0,3:2|0:0,0:0:0:0: +133,150,74478,2,0,L|131:71,1,69,2|0,0:0|1:2,0:0:0:0: +366,81,74697,6,0,L|367:158,1,69,2|2,0:0|0:0,0:0:0:0: +165,149,74916,1,2,1:2:0:0: +366,149,75026,1,2,0:0:0:0: +462,149,75135,2,0,L|296:149,1,138,0|2,3:2|0:0,0:0:0:0: +407,149,75463,1,0,0:0:0:0: +171,149,75573,6,0,L|169:233,1,69,2|2,1:2|0:0,0:0:0:0: +370,217,75792,1,2,0:0:0:0: +170,217,75901,1,2,0:0:0:0: +72,217,76011,2,0,P|46:151|98:97,1,138,0|2,3:2|0:0,0:0:0:0: +179,102,76339,1,0,1:2:0:0: +414,102,76449,6,0,L|491:102,1,69,2|2,0:0|0:0,0:0:0:0: +385,102,76668,1,0,1:2:0:0: +185,102,76777,1,2,0:0:0:0: +282,102,76887,2,0,L|442:101,1,138,0|2,3:2|0:0,0:0:0:0: +336,101,77215,1,0,0:0:0:0: +100,101,77325,6,0,P|75:169|105:227,1,138,2|0,1:2|0:0,0:0:0:0: +192,224,77653,1,2,0:0:0:0: +102,224,77763,2,0,L|100:301,1,69,0|0,3:2|0:0,0:0:0:0: +301,292,77982,2,0,L|394:292,1,69,2|0,0:0|1:2,0:0:0:0: +134,292,78201,6,0,L|133:221,1,69,2|2,0:0|0:0,0:0:0:0: +334,223,78420,1,2,1:2:0:0: +135,223,78529,1,2,0:0:0:0: +37,223,78639,2,0,P|21:160|69:106,1,138,0|2,3:2|0:0,0:0:0:0: +147,107,78967,1,0,0:0:0:0: +382,107,79077,6,0,L|384:175,1,69,2|0,1:2|0:0,0:0:0:0: +273,175,79296,2,0,L|271:243,1,69,2|0,1:2|0:0,0:0:0:0: +472,243,79515,2,0,L|474:315,1,69,2|0,3:2|0:0,0:0:0:0: +203,311,79734,6,0,L|132:312,1,69,6|0,1:2|0:0,0:0:0:0: +244,311,79953,2,0,L|317:311,1,69,2|0,0:0|0:0,0:0:0:0: +111,311,80172,2,0,L|108:242,1,69,8|0,2:3|0:0,0:0:0:0: +307,242,80390,2,0,L|385:242,1,69,8|0,2:3|0:0,0:0:0:0: +140,242,80609,2,0,L|69:242,1,69,4|0,2:3|0:0,0:0:0:0: +341,242,80828,6,0,L|495:242,1,138,6|0,1:2|0:0,0:0:0:0: +388,242,81157,1,2,0:0:0:0: +476,242,81266,1,0,3:2:0:0: +161,242,81485,5,2,1:2:0:0: +124,175,81595,1,0,0:0:0:0: +166,112,81704,1,0,0:0:0:0: +242,106,81814,1,0,0:0:0:0: +351,106,81923,2,0,L|352:37,1,69,0|0,1:2|0:0,0:0:0:0: +150,37,82142,1,2,3:2:0:0: +74,50,82252,1,0,0:0:0:0: +84,124,82361,1,0,0:0:0:0: +166,131,82471,1,0,0:0:0:0: +399,131,82580,5,2,1:2:0:0: +442,193,82690,1,0,0:0:0:0: +399,255,82799,1,0,0:0:0:0: +316,261,82909,1,2,0:0:0:0: +206,261,83018,2,0,L|204:185,1,69,0|0,3:2|0:0,0:0:0:0: +315,192,83237,2,0,L|316:121,1,69,2|0,1:2|0:0,0:0:0:0: +80,123,83456,6,0,L|78:47,1,69,2|0,1:2|0:0,0:0:0:0: +182,54,83675,1,2,1:2:0:0: +375,54,83894,1,2,3:2:0:0: +57,54,84113,1,2,0:0:0:0: +133,54,84223,1,0,0:0:0:0: +366,54,84332,5,2,1:2:0:0: +405,119,84442,1,0,0:0:0:0: +361,180,84551,1,0,0:0:0:0: +284,180,84661,1,0,0:0:0:0: +174,180,84770,2,0,L|172:256,1,69,0|0,3:2|0:0,0:0:0:0: +442,248,84989,5,6,1:2:0:0: +358,248,85099,1,0,0:0:0:0: +321,183,85208,1,0,0:0:0:0: +365,123,85317,1,0,0:0:0:0: +475,123,85427,2,0,L|476:48,1,69,0|0,1:2|0:0,0:0:0:0: +274,54,85646,2,0,L|273:131,1,69,0|0,3:2|0:0,0:0:0:0: +363,122,85865,1,0,0:0:0:0: +273,122,85974,1,0,0:0:0:0: +71,122,86084,6,0,L|70:210,1,69,0|0,1:2|0:0,0:0:0:0: +305,190,86303,2,0,L|305:270,1,69,8|0,0:3|0:0,0:0:0:0: +103,259,86522,1,0,3:2:0:0: +305,259,86631,2,0,L|388:258,1,69,8|2,0:3|0:0,0:0:0:0: +55,258,86960,2,0,P|215:211|49:153,1,455.400013897705,2|0,1:2|0:0,0:0:0:0: +398,117,87836,5,6,1:2:0:0: +77,106,101412,5,0,3:2:0:0: +435,106,101850,6,0,P|450:162|434:240,1,138,2|0,1:2|0:0,0:0:0:0: +240,239,102288,2,0,L|99:240,1,138,0|0,1:2|0:0,0:0:0:0: +296,239,102726,2,0,L|437:238,1,138,2|0,1:2|0:0,0:0:0:0: +322,238,103055,2,0,L|243:238,1,69,2|0,0:0|1:2,0:0:0:0: +433,238,103383,1,2,0:0:0:0: +145,242,103602,5,2,1:2:0:0: +228,242,103712,1,0,0:0:0:0: +283,242,103821,1,2,0:0:0:0: +89,242,104040,2,0,L|88:104,1,138,2|2,1:2|0:0,0:0:0:0: +268,104,104478,1,2,1:2:0:0: +88,104,104697,1,2,0:0:0:0: +281,104,104916,2,0,L|426:105,1,138,2|0,1:2|0:0,0:0:0:0: +129,104,105354,5,2,1:2:0:0: +211,104,105463,1,0,0:0:0:0: +266,104,105573,1,0,0:0:0:0: +72,104,105792,2,0,L|71:255,1,138,0|0,1:2|0:0,0:0:0:0: +265,241,106230,2,0,L|117:242,1,138,2|2,1:2|0:0,0:0:0:0: +237,241,106558,2,0,L|307:241,1,69,2|0,0:0|1:2,0:0:0:0: +126,240,106887,1,2,0:0:0:0: +415,240,107106,5,2,1:2:0:0: +332,240,107215,1,0,0:0:0:0: +276,240,107325,1,2,0:0:0:0: +469,240,107544,2,0,L|470:100,1,138,2|2,1:2|0:0,0:0:0:0: +289,102,107982,1,2,1:2:0:0: +469,102,108201,1,2,0:0:0:0: +275,102,108420,2,0,L|138:102,1,138,2|0,1:2|0:0,0:0:0:0: +428,102,108858,5,2,1:2:0:0: +345,102,108967,1,0,0:0:0:0: +289,102,109077,1,0,0:0:0:0: +482,102,109296,2,0,L|484:242,1,138,0|0,1:2|0:0,0:0:0:0: +291,239,109734,2,0,L|429:240,1,138,2|0,1:2|0:0,0:0:0:0: +318,239,110062,2,0,L|241:238,1,69,2|0,0:0|1:2,0:0:0:0: +428,239,110390,1,2,0:0:0:0: +138,239,110609,5,2,1:2:0:0: +215,239,110719,1,0,0:0:0:0: +277,239,110828,1,2,0:0:0:0: +83,239,111047,2,0,L|229:239,1,138,2|2,1:2|0:0,0:0:0:0: +26,239,111485,2,0,L|25:102,1,138,2|0,1:2|0:0,0:0:0:0: +205,101,111923,1,0,1:2:0:0: +25,101,112142,1,2,0:0:0:0: +314,101,112361,5,2,1:2:0:0: +230,101,112471,1,2,0:0:0:0: +314,101,112580,2,0,P|399:137|304:230,1,276,2|2,0:0|0:0,0:0:0:0: +109,229,113237,2,0,P|23:186|123:101,1,276,2|0,0:0|0:0,0:0:0:0: +482,100,114113,5,0,1:2:0:0: +288,100,114332,1,8,0:3:0:0: +482,100,114551,2,0,L|324:100,1,138,8|0,0:3|0:0,0:0:0:0: +149,100,114989,2,0,L|292:100,1,138,4|0,0:3|0:0,0:0:0:0: +397,100,115317,2,0,L|310:101,1,69,4|0,0:3|1:2,0:0:0:0: +133,100,115646,2,0,L|132:176,1,69,0|0,3:2|0:0,0:0:0:0: +367,168,115865,5,2,1:2:0:0: +284,168,115974,1,0,0:0:0:0: +228,168,116084,1,0,0:0:0:0: +421,168,116303,2,0,L|423:308,1,138,0|0,3:2|0:0,0:0:0:0: +346,305,116631,1,0,1:2:0:0: +235,305,116741,2,0,L|383:306,1,138,2|2,0:0|1:2,0:0:0:0: +296,305,117069,1,2,0:0:0:0: +94,305,117179,1,0,3:2:0:0: +273,305,117398,2,0,L|346:306,1,69,2|0,0:0|0:0,0:0:0:0: +129,304,117617,5,2,1:2:0:0: +60,304,117726,1,0,0:0:0:0: +131,304,117836,1,0,0:0:0:0: +324,304,118055,2,0,L|177:304,1,138,2|2,3:2|0:0,0:0:0:0: +262,304,118383,1,0,1:2:0:0: +372,304,118493,2,0,P|443:286|477:233,1,138,2|2,0:0|1:2,0:0:0:0: +400,234,118821,1,0,0:0:0:0: +198,234,118931,1,2,3:2:0:0: +391,234,119150,2,0,L|392:152,1,69,0|0,0:0|0:0,0:0:0:0: +156,165,119369,5,2,1:2:0:0: +238,165,119478,1,0,0:0:0:0: +293,165,119588,1,0,0:0:0:0: +99,165,119807,2,0,L|97:26,1,138,0|0,3:2|0:0,0:0:0:0: +174,27,120135,1,0,1:2:0:0: +283,27,120244,1,2,0:0:0:0: +333,79,120354,1,2,0:0:0:0: +283,27,120463,1,2,1:2:0:0: +185,27,120573,1,2,0:0:0:0: +384,27,120682,2,0,P|442:41|483:113,1,138,0|2,3:2|0:0,0:0:0:0: +412,104,121011,1,0,0:0:0:0: +178,104,121120,5,2,1:2:0:0: +108,104,121230,1,0,0:0:0:0: +178,104,121339,1,0,0:0:0:0: +371,104,121558,2,0,L|224:104,1,138,2|0,3:2|0:0,0:0:0:0: +309,104,121887,1,0,1:2:0:0: +418,104,121996,2,0,P|446:171|408:227,1,138,2|2,0:0|1:2,0:0:0:0: +337,222,122325,1,0,0:0:0:0: +137,222,122434,2,0,P|64:206|23:153,1,138,2|0,3:2|0:0,0:0:0:0: +102,159,122763,1,0,0:0:0:0: +335,159,122872,5,2,1:2:0:0: +251,159,122982,1,0,0:0:0:0: +196,159,123091,1,0,0:0:0:0: +389,159,123310,2,0,P|406:239|386:293,1,138,0|0,3:2|0:0,0:0:0:0: +312,290,123639,1,0,1:2:0:0: +202,290,123748,2,0,P|128:246|123:199,1,138,2|2,0:0|1:2,0:0:0:0: +200,162,124077,1,2,0:0:0:0: +399,161,124186,1,0,3:2:0:0: +219,92,124405,2,0,L|148:92,1,69,2|0,0:0|0:0,0:0:0:0: +386,227,124624,5,2,1:2:0:0: +455,227,124734,1,0,0:0:0:0: +386,227,124843,1,2,0:0:0:0: +192,227,125062,2,0,P|106:213|67:181,1,138,2|2,3:2|0:0,0:0:0:0: +144,182,125390,1,0,1:2:0:0: +345,182,125500,2,0,P|431:168|470:136,1,138,2|0,0:0|1:2,0:0:0:0: +393,137,125828,1,0,0:0:0:0: +282,137,125938,1,0,3:2:0:0: +475,137,126157,2,0,L|476:213,1,69,2|0,0:0|0:0,0:0:0:0: +240,205,126376,5,2,1:2:0:0: +322,205,126485,1,0,0:0:0:0: +377,205,126595,1,0,0:0:0:0: +183,205,126814,1,2,3:2:0:0: +472,205,127033,1,2,0:0:0:0: +389,205,127142,1,0,1:2:0:0: +333,205,127252,1,0,0:0:0:0: +153,205,127471,2,0,L|152:131,1,69,2|0,1:2|0:0,0:0:0:0: +256,136,127690,1,2,3:2:0:0: +76,136,127909,1,2,0:0:0:0: +421,136,128128,5,6,1:2:0:0: +423,67,128237,1,2,0:0:0:0: +319,67,128347,1,2,3:2:0:0: +139,67,128566,1,2,3:2:0:0: +332,67,128785,1,2,1:2:0:0: +42,67,129004,1,8,0:3:0:0: +111,67,129113,1,8,0:3:0:0: +304,67,129332,2,0,L|72:67,1,207,8|4,0:3|0:3,0:0:0:0: +408,67,129880,6,0,P|490:129|379:199,1,276,6|0,1:2|0:0,0:0:0:0: +188,200,130536,2,0,L|483:200,1,276,2|2,0:0|0:0,0:0:0:0: +283,200,131193,1,2,0:0:0:0: +463,200,131412,1,2,0:0:0:0: +145,200,131631,6,0,P|59:138|164:60,1,276,6|0,1:2|0:0,0:0:0:0: +342,59,132288,1,0,0:0:0:0: +148,59,132507,2,0,L|147:214,1,138,2|2,0:0|0:0,0:0:0:0: +327,196,132945,1,2,0:0:0:0: +147,196,133164,1,2,0:0:0:0: +464,196,133383,6,0,P|469:249|351:316,1,207,6|0,1:2|0:0,0:0:0:0: +240,316,133821,2,0,P|354:311|391:173,1,276,2|2,0:0|0:0,0:0:0:0: +196,172,134478,2,0,L|197:33,1,138,2|2,0:0|0:0,0:0:0:0: +391,34,134916,1,2,0:0:0:0: +73,34,135135,6,0,B|188:112|188:112|68:30,1,276,6|0,1:2|0:0,0:0:0:0: +434,34,136011,2,0,L|435:174,1,138,2|2,3:2|3:2,0:0:0:0: +227,171,136449,1,8,0:3:0:0: +434,171,136668,1,8,0:3:0:0: +116,171,136887,6,0,L|412:171,1,276,2|2,1:2|3:2,0:0:0:0: +100,171,137544,1,2,0:0:0:0: +182,171,137653,1,0,1:2:0:0: +242,171,137763,1,0,0:0:0:0: +62,171,137982,1,2,1:2:0:0: +241,171,138201,2,0,L|88:169,1,138,2|2,3:2|0:0,0:0:0:0: +421,169,138639,6,0,L|128:168,1,276,0|0,1:2|3:2,0:0:0:0: +339,168,139296,2,0,L|340:90,1,69,0|0,0:0|1:2,0:0:0:0: +235,99,139515,1,2,0:0:0:0: +55,99,139734,1,2,1:2:0:0: +344,99,139953,2,0,L|489:98,1,138,0|2,3:2|0:0,0:0:0:0: +136,98,140390,6,0,L|135:242,1,138,6|2,1:2|0:0,0:0:0:0: +328,235,140828,1,2,3:2:0:0: +135,235,141047,1,2,3:2:0:0: +342,235,141266,1,2,3:2:0:0: +493,235,141485,1,2,3:2:0:0: +299,235,141704,1,2,3:2:0:0: +91,235,141923,1,2,3:2:0:0: +380,235,142142,6,0,L|155:232,2,207,2|2|2,1:2|0:0|0:0,0:0:0:0: +185,235,143018,2,0,P|347:232|428:19,1,414,2|0,0:0|0:0,0:0:0:0: +82,21,143894,6,0,P|50:85|84:135,1,138,6|0,1:2|0:0,0:0:0:0: +174,134,144223,1,2,0:0:0:0: +84,134,144332,2,0,L|83:208,1,69,0|0,3:2|0:0,0:0:0:0: +284,202,144551,2,0,L|368:202,1,69,2|0,0:0|1:2,0:0:0:0: +117,202,144770,6,0,L|46:202,1,69,2|2,0:0|0:0,0:0:0:0: +249,202,144989,1,2,1:2:0:0: +48,202,145099,1,2,0:0:0:0: +144,202,145208,2,0,P|180:157|139:100,1,138,0|2,3:2|0:0,0:0:0:0: +55,99,145536,1,0,0:0:0:0: +290,99,145646,6,0,L|370:98,1,69,2|2,1:2|0:0,0:0:0:0: +157,98,145865,1,2,0:0:0:0: +356,98,145974,1,2,0:0:0:0: +453,98,146084,2,0,L|277:98,1,138,0|2,3:2|0:0,0:0:0:0: +412,98,146412,1,0,1:2:0:0: +176,98,146522,5,2,0:0:0:0: +272,98,146631,2,0,L|273:174,1,69,2|0,0:0|1:2,0:0:0:0: +71,166,146850,1,2,0:0:0:0: +168,166,146960,2,0,L|27:166,1,138,0|2,3:2|0:0,0:0:0:0: +113,166,147288,1,0,0:0:0:0: +348,166,147398,6,0,P|385:115|346:62,1,138,2|0,1:2|0:0,0:0:0:0: +255,61,147726,1,2,0:0:0:0: +345,61,147836,2,0,L|347:129,1,69,0|0,3:2|0:0,0:0:0:0: +145,129,148055,1,2,0:0:0:0: +76,129,148164,1,0,1:2:0:0: +280,97,148274,6,0,L|360:97,1,69,2|2,0:0|0:0,0:0:0:0: +147,97,148493,1,2,1:2:0:0: +346,97,148602,1,2,0:0:0:0: +248,97,148712,2,0,L|103:97,1,138,0|2,3:2|0:0,0:0:0:0: +193,97,149040,1,0,0:0:0:0: +428,97,149150,6,0,P|459:168|420:215,1,138,2|0,1:2|0:0,0:0:0:0: +226,211,149478,1,2,0:0:0:0: +323,211,149588,2,0,L|466:211,1,138,0|2,3:2|0:0,0:0:0:0: +377,211,149916,1,0,1:2:0:0: +141,211,150026,5,2,0:0:0:0: +237,211,150135,2,0,L|239:139,1,69,2|0,0:0|1:2,0:0:0:0: +37,142,150354,1,2,0:0:0:0: +133,142,150463,2,0,P|166:75|119:40,1,138,0|2,3:2|0:0,0:0:0:0: +42,40,150792,1,0,0:0:0:0: +309,40,150901,6,0,L|465:40,1,138,6|0,1:2|0:0,0:0:0:0: +356,40,151230,1,2,0:0:0:0: +445,40,151339,1,0,3:2:0:0: +127,40,151558,5,2,1:2:0:0: +203,45,151668,1,0,0:0:0:0: +239,111,151777,1,0,0:0:0:0: +196,174,151887,1,0,0:0:0:0: +86,174,151996,2,0,L|84:252,1,69,0|0,1:2|0:0,0:0:0:0: +285,242,152215,2,0,L|144:241,1,138,2|2,3:2|0:0,0:0:0:0: +230,241,152544,1,0,0:0:0:0: +463,241,152653,6,0,L|392:240,1,69,0|0,1:2|0:0,0:0:0:0: +284,242,152872,2,0,L|282:164,1,69,2|0,0:0|0:0,0:0:0:0: +483,173,153091,2,0,L|336:172,1,138,2|2,3:2|1:2,0:0:0:0: +428,172,153420,1,0,0:0:0:0: +227,171,153529,6,0,L|226:93,1,69,2|0,1:2|0:0,0:0:0:0: +323,102,153748,1,2,1:2:0:0: +33,102,153967,2,0,L|30:248,1,138,2|2,3:2|0:0,0:0:0:0: +114,239,154296,1,0,0:0:0:0: +381,239,154405,6,0,L|99:237,1,276,6|2,1:2|3:2,0:0:0:0: +451,237,155062,2,0,P|488:148|355:78,1,276,6|0,1:2|1:2,0:0:0:0: +22,80,155719,2,0,L|177:81,1,138,2|2,3:2|0:0,0:0:0:0: +478,80,156157,6,0,L|268:81,2,207,6|8|8,1:2|0:3|0:3,0:0:0:0: +159,80,157033,2,0,P|66:140|166:218,1,276,6|0,1:2|3:2,0:0:0:0: +254,218,157580,1,0,3:2:0:0: +163,218,157690,1,0,3:2:0:0: +396,218,157799,1,0,3:2:0:0: +163,218,157909,6,0,P|132:155|167:100,1,138,6|0,1:2|0:0,0:0:0:0: +255,100,158237,1,2,0:0:0:0: +164,100,158347,2,0,L|162:174,1,69,0|0,3:2|0:0,0:0:0:0: +363,168,158566,2,0,L|364:243,1,69,2|0,0:0|1:2,0:0:0:0: +128,236,158785,6,0,L|208:237,1,69,2|2,0:0|0:0,0:0:0:0: +398,236,159004,1,2,1:2:0:0: +198,236,159113,1,2,0:0:0:0: +100,236,159223,2,0,P|73:178|105:116,1,138,0|2,3:2|0:0,0:0:0:0: +187,116,159551,1,0,0:0:0:0: +422,116,159661,6,0,L|352:115,1,69,2|0,1:2|0:0,0:0:0:0: +151,115,159880,1,2,0:0:0:0: +350,115,159989,1,2,0:0:0:0: +254,115,160099,2,0,L|426:115,1,138,0|2,3:2|0:0,0:0:0:0: +296,115,160427,1,0,1:2:0:0: +62,115,160536,6,0,L|61:188,1,69,2|0,0:0|0:0,0:0:0:0: +171,183,160755,2,0,L|250:183,1,69,2|0,1:2|0:0,0:0:0:0: +441,183,160974,2,0,P|470:243|434:305,1,138,2|2,3:2|0:0,0:0:0:0: +354,301,161303,1,0,0:0:0:0: +120,301,161412,6,0,L|271:301,1,138,2|0,1:2|0:0,0:0:0:0: +167,301,161741,1,2,0:0:0:0: +256,301,161850,2,0,L|257:222,1,69,0|0,3:2|0:0,0:0:0:0: +55,232,162069,2,0,L|53:155,1,69,2|0,0:0|1:2,0:0:0:0: +288,163,162288,6,0,L|363:163,1,69,2|0,0:0|0:0,0:0:0:0: +155,163,162507,1,2,1:2:0:0: +356,163,162617,1,2,0:0:0:0: +452,163,162726,2,0,P|475:235|443:293,1,138,0|2,3:2|0:0,0:0:0:0: +364,287,163055,1,0,0:0:0:0: +130,287,163164,6,0,L|128:209,1,69,2|0,1:2|0:0,0:0:0:0: +239,218,163383,2,0,L|241:146,1,69,2|0,1:2|0:0,0:0:0:0: +39,149,163602,2,0,L|120:149,1,69,2|0,3:2|0:0,0:0:0:0: +378,149,163821,6,0,L|379:81,1,69,6|0,1:2|0:0,0:0:0:0: +268,80,164040,2,0,L|172:80,1,69,2|0,0:0|0:0,0:0:0:0: +400,80,164259,2,0,L|402:153,1,69,8|0,2:3|0:0,0:0:0:0: +200,148,164478,2,0,L|112:148,1,69,8|0,2:3|0:0,0:0:0:0: +366,148,164697,2,0,L|453:149,1,69,4|0,2:3|0:0,0:0:0:0: +164,148,164916,6,0,L|25:149,1,138,6|0,1:2|0:0,0:0:0:0: +116,148,165244,1,2,0:0:0:0: +27,148,165354,1,0,3:2:0:0: +344,148,165573,5,2,1:2:0:0: +381,213,165682,1,0,0:0:0:0: +339,277,165792,1,0,0:0:0:0: +263,277,165901,1,0,0:0:0:0: +152,277,166011,2,0,L|151:353,1,69,0|0,1:2|0:0,0:0:0:0: +352,345,166230,1,2,3:2:0:0: +427,345,166339,1,0,0:0:0:0: +464,278,166449,1,0,0:0:0:0: +425,212,166558,1,0,0:0:0:0: +189,212,166668,5,2,1:2:0:0: +116,189,166777,1,0,0:0:0:0: +125,113,166887,1,0,0:0:0:0: +199,102,166996,1,2,0:0:0:0: +309,102,167106,2,0,L|311:180,1,69,0|0,3:2|0:0,0:0:0:0: +199,170,167325,2,0,L|197:242,1,69,2|0,1:2|0:0,0:0:0:0: +398,238,167544,6,0,L|483:238,1,69,2|0,1:2|0:0,0:0:0:0: +356,238,167763,2,0,L|283:237,1,69,2|0,1:2|0:0,0:0:0:0: +85,237,167982,2,0,L|11:237,1,69,2|0,3:2|0:0,0:0:0:0: +126,237,168201,2,0,L|206:237,1,69,2|0,0:0|0:0,0:0:0:0: +430,237,168420,6,0,P|487:176|366:86,1,276,2|0,1:2|3:2,0:0:0:0: +174,89,169077,1,2,1:2:0:0: +99,98,169186,1,0,0:0:0:0: +67,167,169296,1,0,0:0:0:0: +101,234,169405,1,0,0:0:0:0: +176,243,169515,1,0,1:2:0:0: +465,243,169734,2,0,L|467:104,1,138,0|0,3:2|1:2,0:0:0:0: +390,105,170062,1,0,0:0:0:0: +154,105,170172,6,0,L|367:106,1,207,2|2,1:2|0:0,0:0:0:0: +127,105,170609,2,0,P|104:181|130:237,1,138,0|2,3:2|0:0,0:0:0:0: +202,232,170938,1,2,0:0:0:0: +401,232,171047,2,0,P|176:204|125:49,1,414,2|0,1:2|0:0,0:0:0:0: +416,48,171923,5,2,1:2:0:0: +85,274,178712,5,0,3:2:0:0: +402,274,178931,6,0,P|428:204|398:150,1,138,2|2,1:2|0:0,0:0:0:0: +323,151,179259,1,2,0:0:0:0: +212,151,179369,2,0,P|134:143|92:99,1,138,2|2,1:2|0:0,0:0:0:0: +170,102,179697,1,2,0:0:0:0: +280,102,179807,2,0,L|429:102,1,138,2|2,1:2|0:0,0:0:0:0: +307,102,180135,1,2,0:0:0:0: +238,102,180244,1,0,1:2:0:0: +307,102,180354,1,2,0:0:0:0: +417,102,180463,2,0,L|418:179,1,69,2|0,0:0|0:0,0:0:0:0: +216,159,180682,5,2,1:2:0:0: +313,159,180792,1,2,0:0:0:0: +381,159,180901,1,2,0:0:0:0: +313,159,181011,1,2,0:0:0:0: +203,159,181120,1,2,1:2:0:0: +133,159,181230,1,2,0:0:0:0: +203,159,181339,1,2,0:0:0:0: +396,159,181558,2,0,P|422:224|388:292,1,138,2|2,1:2|0:0,0:0:0:0: +320,283,181887,1,2,0:0:0:0: +210,283,181996,2,0,L|65:282,1,138,0|0,1:2|0:0,0:0:0:0: +148,282,182325,1,0,0:0:0:0: +347,282,182434,5,2,1:2:0:0: +416,282,182544,1,2,0:0:0:0: +347,282,182653,1,2,0:0:0:0: +154,282,182872,1,2,1:2:0:0: +85,282,182982,1,2,0:0:0:0: +154,282,183091,1,2,0:0:0:0: +347,282,183310,2,0,P|373:217|342:159,1,138,2|2,1:2|0:0,0:0:0:0: +231,160,183639,1,2,0:0:0:0: +162,160,183748,1,0,1:2:0:0: +231,160,183858,1,2,0:0:0:0: +343,160,183967,2,0,L|345:87,1,69,2|0,0:0|0:0,0:0:0:0: +143,91,184186,5,2,1:2:0:0: +323,91,184405,1,8,0:3:0:0: +143,91,184624,2,0,P|118:168|149:218,1,138,8|2,0:3|0:0,0:0:0:0: +221,213,184953,1,2,0:0:0:0: +421,270,185062,2,0,L|206:271,2,207,4|4|0,0:3|0:3|3:2,0:0:0:0: +102,270,185938,6,0,P|72:198|110:155,1,138,2|2,1:2|0:0,0:0:0:0: +181,157,186266,1,2,0:0:0:0: +291,157,186376,2,0,L|432:157,1,138,2|2,3:2|0:0,0:0:0:0: +352,157,186704,1,2,1:2:0:0: +150,157,186814,2,0,P|128:221|149:291,1,138,2|2,0:0|1:2,0:0:0:0: +257,286,187142,1,2,0:0:0:0: +325,227,187252,1,0,3:2:0:0: +253,155,187361,1,2,0:0:0:0: +141,155,187471,2,0,L|52:155,1,69,2|0,0:0|0:0,0:0:0:0: +307,155,187690,6,0,P|325:214|306:292,1,138,0|0,1:2|0:0,0:0:0:0: +113,292,188128,2,0,P|100:235|115:156,1,138,0|0,3:2|0:0,0:0:0:0: +190,157,188456,1,0,1:2:0:0: +391,157,188566,1,0,0:0:0:0: +211,157,188785,1,0,1:2:0:0: +390,157,189004,2,0,L|392:13,1,138,0|0,3:2|0:0,0:0:0:0: +73,19,189442,5,2,1:2:0:0: +39,86,189551,1,2,0:0:0:0: +76,152,189661,1,2,0:0:0:0: +158,152,189770,1,2,0:0:0:0: +268,152,189880,2,0,L|114:153,1,138,2|2,3:2|0:0,0:0:0:0: +213,152,190208,1,2,1:2:0:0: +412,152,190317,2,0,P|430:226|409:286,1,138,2|2,0:0|1:2,0:0:0:0: +320,282,190646,1,2,0:0:0:0: +230,282,190755,1,0,3:2:0:0: +409,282,190974,1,2,0:0:0:0: +91,282,191193,6,0,P|23:224|137:141,1,276,0|0,1:2|3:2,0:0:0:0: +344,141,191850,1,0,1:2:0:0: +427,141,191960,1,0,1:2:0:0: +344,141,192069,1,2,3:2:0:0: +138,141,192288,1,0,1:2:0:0: +427,141,192507,2,0,L|428:288,1,138,2|0,3:2|0:0,0:0:0:0: +81,278,192945,6,0,L|266:278,1,179.39999178772,6|2,1:2|1:1,0:0:0:0: +81,278,193383,2,0,L|388:279,1,289.799991156006,2|2,1:1|1:1,0:0:0:0: +190,278,193821,2,0,L|381:278,1,179.39999178772,2|2,1:2|1:1,0:0:0:0: +78,278,194259,2,0,L|401:277,1,289.799991156006,2|2,1:1|1:1,0:0:0:0: +76,277,194697,6,0,L|74:140,1,138,2|2,1:2|1:1,0:0:0:0: +365,139,195135,2,0,L|59:138,1,289.799991156006,2|2,1:1|1:1,0:0:0:0: +394,138,195573,2,0,L|395:278,1,138,2|2,1:2|1:1,0:0:0:0: +105,276,196011,2,0,L|411:277,1,289.799991156006,2|2,1:1|1:1,0:0:0:0: +75,276,196449,5,2,3:2:0:0: +422,276,196668,2,0,L|108:275,2,289.799991156006,6|6|6,1:1|1:1|1:1,0:0:0:0: +75,276,197325,2,0,L|389:275,2,289.799991156006,6|6|6,1:1|1:1|1:1,0:0:0:0: +395,276,197982,1,6,1:1:0:0: +47,276,198201,6,0,L|349:277,1,289.799991156006,6|6,1:1|1:1,0:0:0:0: +142,276,198639,2,0,L|342:277,1,193.199994104004,14|14,2:3|2:3,0:0:0:0: +26,277,199077,1,14,2:3:0:0: +371,277,199296,2,0,P|254:202|378:86,1,358.79998357544,6|0,1:2|0:0,0:0:0:0: +56,81,199953,6,0,L|297:80,2,207,6|2|2,1:2|0:0|0:0,0:0:0:0: +249,81,200828,2,0,L|251:169,1,69,2|2,0:0|0:0,0:0:0:0: +160,149,201047,1,2,0:0:0:0: +250,149,201157,1,2,0:0:0:0: +50,149,201266,1,0,3:2:0:0: +139,149,201376,1,0,3:2:0:0: +50,149,201485,1,2,3:2:0:0: +285,149,201595,1,0,3:2:0:0: +50,149,201704,6,0,L|48:228,1,69,6|2,1:2|0:0,0:0:0:0: +249,217,201923,1,2,0:0:0:0: +48,217,202033,1,2,0:0:0:0: +141,217,202142,2,0,P|172:281|134:338,1,138,0|2,3:2|0:0,0:0:0:0: +45,333,202471,1,0,1:2:0:0: +278,333,202580,5,2,0:0:0:0: +180,333,202690,2,0,L|179:262,1,69,2|0,0:0|1:2,0:0:0:0: +380,264,202909,1,2,0:0:0:0: +283,264,203018,2,0,L|457:265,1,138,0|2,3:2|0:0,0:0:0:0: +337,264,203347,1,0,0:0:0:0: +103,264,203456,6,0,P|72:200|117:155,1,138,0|0,1:2|0:0,0:0:0:0: +202,156,203785,1,2,0:0:0:0: +111,156,203894,2,0,L|109:75,1,69,0|0,3:2|0:0,0:0:0:0: +310,87,204113,2,0,L|399:86,1,69,2|0,0:0|1:2,0:0:0:0: +177,86,204332,5,2,0:0:0:0: +378,86,204442,2,0,L|379:160,1,69,2|0,0:0|1:2,0:0:0:0: +177,154,204661,1,2,0:0:0:0: +80,154,204770,2,0,P|55:217|80:282,1,138,0|2,3:2|0:0,0:0:0:0: +162,280,205099,1,0,0:0:0:0: +395,280,205208,6,0,L|312:280,1,69,2|2,1:2|0:0,0:0:0:0: +124,280,205427,1,2,0:0:0:0: +323,280,205536,1,2,0:0:0:0: +420,280,205646,2,0,L|252:279,1,138,0|2,3:2|0:0,0:0:0:0: +379,279,205974,1,0,1:2:0:0: +143,279,206084,6,0,L|70:281,1,69,2|2,0:0|0:0,0:0:0:0: +171,280,206303,1,0,1:2:0:0: +370,280,206412,1,2,0:0:0:0: +467,280,206522,2,0,P|494:213|463:160,1,138,0|2,3:2|0:0,0:0:0:0: +380,160,206850,1,0,0:0:0:0: +109,160,206960,6,0,L|259:160,1,138,6|0,1:2|0:0,0:0:0:0: +156,160,207288,1,2,0:0:0:0: +65,160,207398,1,0,3:2:0:0: +382,160,207617,5,2,1:2:0:0: +420,224,207726,1,0,0:0:0:0: +378,288,207836,1,0,0:0:0:0: +302,288,207945,1,0,0:0:0:0: +191,288,208055,2,0,L|190:212,1,69,0|0,1:2|0:0,0:0:0:0: +391,219,208274,2,0,P|417:155|379:101,1,138,2|2,3:2|0:0,0:0:0:0: +298,102,208602,1,0,0:0:0:0: +62,102,208712,6,0,L|61:180,1,69,0|0,1:2|0:0,0:0:0:0: +172,170,208931,2,0,L|245:169,1,69,2|0,0:0|0:0,0:0:0:0: +442,169,209150,2,0,P|466:237|434:297,1,138,2|2,3:2|1:2,0:0:0:0: +355,292,209478,1,0,0:0:0:0: +119,292,209588,6,0,L|116:218,1,69,2|0,0:0|0:0,0:0:0:0: +220,223,209807,1,2,1:2:0:0: +413,223,210026,1,2,3:2:0:0: +124,223,210244,2,0,L|48:223,1,69,2|2,0:0|0:0,0:0:0:0: +325,223,210463,6,0,P|407:220|484:62,1,276,6|2,1:2|3:2,0:0:0:0: +165,63,211120,2,0,L|469:62,1,276,6|0,1:2|1:2,0:0:0:0: +149,62,211777,2,0,L|8:61,1,138,2|2,3:2|0:0,0:0:0:0: +357,61,212215,6,0,L|142:61,2,207,6|8|8,1:2|0:3|0:3,0:0:0:0: +65,61,213091,2,0,L|375:61,1,276,6|0,1:2|3:2,0:0:0:0: +250,61,213639,1,0,3:2:0:0: +339,61,213748,1,0,3:2:0:0: +103,61,213858,1,0,3:2:0:0: +339,61,213967,6,0,P|366:130|332:184,1,138,6|0,1:2|0:0,0:0:0:0: +245,180,214296,1,2,0:0:0:0: +334,180,214405,2,0,L|336:259,1,69,0|0,3:2|0:0,0:0:0:0: +134,248,214624,2,0,L|47:249,1,69,2|0,0:0|1:2,0:0:0:0: +300,248,214843,6,0,L|301:171,1,69,2|0,0:0|0:0,0:0:0:0: +99,179,215062,1,2,1:2:0:0: +300,179,215172,1,2,0:0:0:0: +203,179,215281,2,0,L|28:176,1,138,0|2,3:2|0:0,0:0:0:0: +148,176,215609,1,0,0:0:0:0: +383,176,215719,6,0,L|290:176,1,69,2|2,1:2|0:0,0:0:0:0: +112,176,215938,1,2,0:0:0:0: +311,176,216047,1,2,0:0:0:0: +408,176,216157,2,0,P|437:111|399:59,1,138,0|2,3:2|0:0,0:0:0:0: +305,60,216485,1,0,1:2:0:0: +69,60,216595,6,0,L|68:143,1,69,2|0,0:0|0:0,0:0:0:0: +179,128,216814,2,0,L|263:129,1,69,2|0,1:2|0:0,0:0:0:0: +449,128,217033,2,0,L|348:129,1,69,2|0,3:2|0:0,0:0:0:0: +178,128,217252,2,0,L|86:128,1,69,2|0,0:0|0:0,0:0:0:0: +344,128,217471,6,0,L|197:128,1,138,2|0,1:2|0:0,0:0:0:0: +289,128,217799,1,2,0:0:0:0: +206,128,217909,2,0,L|205:204,1,69,0|0,3:2|0:0,0:0:0:0: +406,196,218128,2,0,L|479:195,1,69,2|0,0:0|1:2,0:0:0:0: +239,195,218347,6,0,L|158:196,1,69,2|0,0:0|0:0,0:0:0:0: +371,195,218566,1,2,1:2:0:0: +170,195,218675,1,2,0:0:0:0: +267,195,218785,2,0,L|435:196,1,138,0|2,3:2|0:0,0:0:0:0: +321,195,219113,1,0,0:0:0:0: +85,195,219223,6,0,L|85:273,1,69,2|0,1:2|0:0,0:0:0:0: +286,264,219442,2,0,L|379:265,1,69,2|0,1:2|0:0,0:0:0:0: +119,264,219661,2,0,L|37:264,1,69,2|0,3:2|0:0,0:0:0:0: +320,264,219880,5,6,1:2:0:0: +399,257,219989,1,0,0:0:0:0: +402,180,220099,1,0,0:0:0:0: +327,170,220208,1,0,0:0:0:0: +129,120,220317,2,0,L|129:48,1,69,8|0,0:3|0:0,0:0:0:0: +330,51,220536,2,0,L|412:48,1,69,8|0,0:3|0:0,0:0:0:0: +163,48,220755,2,0,L|80:48,1,69,4|0,0:3|0:0,0:0:0:0: +364,52,220974,5,6,1:2:0:0: +439,64,221084,1,0,0:0:0:0: +426,139,221193,1,0,0:0:0:0: +350,146,221303,1,2,0:0:0:0: +240,146,221412,2,0,L|239:227,1,69,0|0,3:2|0:0,0:0:0:0: +440,214,221631,5,2,1:2:0:0: +472,282,221741,1,0,0:0:0:0: +434,346,221850,1,0,0:0:0:0: +357,352,221960,1,0,0:0:0:0: +157,352,222069,2,0,L|66:348,1,69,2|0,1:2|0:0,0:0:0:0: +289,348,222288,2,0,L|471:346,1,138,2|2,3:2|0:0,0:0:0:0: +343,346,222617,1,0,0:0:0:0: +109,346,222726,6,0,P|83:283|123:224,1,138,2|0,1:2|0:0,0:0:0:0: +207,227,223055,1,2,0:0:0:0: +117,227,223164,2,0,L|114:140,1,69,0|0,3:2|0:0,0:0:0:0: +315,158,223383,2,0,L|399:159,1,69,2|0,1:2|0:0,0:0:0:0: +148,158,223602,6,0,L|146:226,1,69,2|0,0:0|0:0,0:0:0:0: +256,226,223821,2,0,L|346:226,1,69,2|0,1:2|0:0,0:0:0:0: +123,226,224040,2,0,L|209:226,1,69,2|0,3:2|0:0,0:0:0:0: +393,226,224259,2,0,L|394:149,1,69,2|0,0:0|0:0,0:0:0:0: +158,157,224478,5,2,1:2:0:0: +82,163,224588,1,0,0:0:0:0: +44,228,224697,1,0,0:0:0:0: +86,291,224807,1,0,0:0:0:0: +285,291,224916,2,0,L|378:292,1,69,0|0,3:2|0:0,0:0:0:0: +83,291,225135,5,6,1:2:0:0: +41,227,225244,1,0,0:0:0:0: +82,163,225354,1,0,0:0:0:0: +157,156,225463,1,0,0:0:0:0: +267,156,225573,2,0,L|267:86,1,69,0|0,1:2|0:0,0:0:0:0: +65,87,225792,2,0,L|64:173,1,69,2|0,3:2|0:0,0:0:0:0: +154,155,226011,1,2,0:0:0:0: +64,155,226120,1,2,0:0:0:0: +299,155,226230,5,2,1:2:0:0: +105,155,226449,2,0,L|104:233,1,69,8|0,0:3|0:0,0:0:0:0: +305,223,226668,1,0,3:2:0:0: +104,223,226777,2,0,L|28:224,1,69,8|2,0:3|0:0,0:0:0:0: +383,353,227106,6,0,P|161:330|63:49,1,552,6|2,1:2|0:0,0:0:0:0: diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3644427-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3644427-expected-conversion.json new file mode 100644 index 0000000000..9d4210c71e --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3644427-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":22.0,"Objects":[{"StartTime":22.0,"Position":28.0,"HyperDash":false},{"StartTime":90.0,"Position":34.2658119,"HyperDash":false},{"StartTime":158.0,"Position":28.0,"HyperDash":false},{"StartTime":226.0,"Position":34.2658119,"HyperDash":false},{"StartTime":294.0,"Position":28.0,"HyperDash":false},{"StartTime":362.0,"Position":34.2658119,"HyperDash":false}]},{"StartTime":431.0,"Objects":[{"StartTime":431.0,"Position":106.0,"HyperDash":false},{"StartTime":499.0,"Position":114.369232,"HyperDash":false},{"StartTime":567.0,"Position":106.0,"HyperDash":false},{"StartTime":635.0,"Position":114.369232,"HyperDash":false},{"StartTime":703.0,"Position":106.0,"HyperDash":false},{"StartTime":771.0,"Position":114.369232,"HyperDash":false}]},{"StartTime":840.0,"Objects":[{"StartTime":840.0,"Position":207.0,"HyperDash":false},{"StartTime":891.0,"Position":229.046875,"HyperDash":false},{"StartTime":942.0,"Position":270.0929,"HyperDash":false},{"StartTime":993.0,"Position":315.1283,"HyperDash":false},{"StartTime":1044.0,"Position":355.531,"HyperDash":false},{"StartTime":1128.0,"Position":309.8878,"HyperDash":false},{"StartTime":1249.0,"Position":207.0,"HyperDash":false}]},{"StartTime":1385.0,"Objects":[{"StartTime":1385.0,"Position":313.0,"HyperDash":false},{"StartTime":1453.0,"Position":346.417664,"HyperDash":false},{"StartTime":1521.0,"Position":313.0,"HyperDash":false},{"StartTime":1589.0,"Position":346.417664,"HyperDash":false},{"StartTime":1657.0,"Position":313.0,"HyperDash":false},{"StartTime":1725.0,"Position":346.417664,"HyperDash":false}]},{"StartTime":1794.0,"Objects":[{"StartTime":1794.0,"Position":347.0,"HyperDash":false},{"StartTime":1862.0,"Position":379.8631,"HyperDash":false},{"StartTime":1930.0,"Position":347.0,"HyperDash":false},{"StartTime":1998.0,"Position":379.8631,"HyperDash":false},{"StartTime":2066.0,"Position":347.0,"HyperDash":false},{"StartTime":2134.0,"Position":379.8631,"HyperDash":false}]},{"StartTime":2203.0,"Objects":[{"StartTime":2203.0,"Position":415.0,"HyperDash":false},{"StartTime":2254.0,"Position":441.134857,"HyperDash":false},{"StartTime":2305.0,"Position":433.726776,"HyperDash":false},{"StartTime":2356.0,"Position":437.454437,"HyperDash":false},{"StartTime":2407.0,"Position":447.7087,"HyperDash":false},{"StartTime":2491.0,"Position":417.7042,"HyperDash":false},{"StartTime":2612.0,"Position":415.0,"HyperDash":false}]},{"StartTime":2749.0,"Objects":[{"StartTime":2749.0,"Position":235.0,"HyperDash":false},{"StartTime":2817.0,"Position":201.582336,"HyperDash":false},{"StartTime":2885.0,"Position":235.0,"HyperDash":false},{"StartTime":2953.0,"Position":201.582336,"HyperDash":false},{"StartTime":3021.0,"Position":235.0,"HyperDash":false},{"StartTime":3089.0,"Position":201.582336,"HyperDash":false}]},{"StartTime":3158.0,"Objects":[{"StartTime":3158.0,"Position":219.0,"HyperDash":false},{"StartTime":3226.0,"Position":229.565125,"HyperDash":false},{"StartTime":3294.0,"Position":219.0,"HyperDash":false},{"StartTime":3362.0,"Position":229.565125,"HyperDash":false},{"StartTime":3430.0,"Position":219.0,"HyperDash":false},{"StartTime":3498.0,"Position":229.565125,"HyperDash":false}]},{"StartTime":3567.0,"Objects":[{"StartTime":3567.0,"Position":299.0,"HyperDash":false},{"StartTime":3618.0,"Position":253.857819,"HyperDash":false},{"StartTime":3669.0,"Position":212.7368,"HyperDash":false},{"StartTime":3720.0,"Position":183.691315,"HyperDash":false},{"StartTime":3771.0,"Position":150.281219,"HyperDash":false},{"StartTime":3855.0,"Position":224.9364,"HyperDash":false},{"StartTime":3976.0,"Position":299.0,"HyperDash":false}]},{"StartTime":4112.0,"Objects":[{"StartTime":4112.0,"Position":234.0,"HyperDash":false},{"StartTime":4180.0,"Position":201.015152,"HyperDash":false},{"StartTime":4248.0,"Position":234.0,"HyperDash":false},{"StartTime":4316.0,"Position":201.015152,"HyperDash":false},{"StartTime":4384.0,"Position":234.0,"HyperDash":false},{"StartTime":4452.0,"Position":201.015152,"HyperDash":false}]},{"StartTime":4522.0,"Objects":[{"StartTime":4522.0,"Position":135.0,"HyperDash":false},{"StartTime":4590.0,"Position":102.015152,"HyperDash":false},{"StartTime":4658.0,"Position":135.0,"HyperDash":false},{"StartTime":4726.0,"Position":102.015152,"HyperDash":false},{"StartTime":4794.0,"Position":135.0,"HyperDash":false},{"StartTime":4862.0,"Position":102.015152,"HyperDash":false}]},{"StartTime":4931.0,"Objects":[{"StartTime":4931.0,"Position":35.0,"HyperDash":false},{"StartTime":4982.0,"Position":48.13485,"HyperDash":false},{"StartTime":5033.0,"Position":43.7267761,"HyperDash":false},{"StartTime":5084.0,"Position":63.454422,"HyperDash":false},{"StartTime":5135.0,"Position":67.7087,"HyperDash":false},{"StartTime":5219.0,"Position":33.7041931,"HyperDash":false},{"StartTime":5340.0,"Position":35.0,"HyperDash":false}]},{"StartTime":5476.0,"Objects":[{"StartTime":5476.0,"Position":22.0,"HyperDash":false},{"StartTime":5544.0,"Position":18.9217854,"HyperDash":false},{"StartTime":5612.0,"Position":22.0,"HyperDash":false},{"StartTime":5680.0,"Position":18.9217854,"HyperDash":false},{"StartTime":5748.0,"Position":22.0,"HyperDash":false},{"StartTime":5816.0,"Position":18.9217854,"HyperDash":false}]},{"StartTime":5885.0,"Objects":[{"StartTime":5885.0,"Position":120.0,"HyperDash":false},{"StartTime":5953.0,"Position":152.061676,"HyperDash":false},{"StartTime":6021.0,"Position":120.0,"HyperDash":false},{"StartTime":6089.0,"Position":152.061676,"HyperDash":false},{"StartTime":6157.0,"Position":120.0,"HyperDash":false},{"StartTime":6225.0,"Position":152.061676,"HyperDash":false}]},{"StartTime":6294.0,"Objects":[{"StartTime":6294.0,"Position":187.0,"HyperDash":false},{"StartTime":6345.0,"Position":140.953125,"HyperDash":false},{"StartTime":6396.0,"Position":130.9071,"HyperDash":false},{"StartTime":6447.0,"Position":92.8717041,"HyperDash":false},{"StartTime":6498.0,"Position":38.469,"HyperDash":false},{"StartTime":6582.0,"Position":112.112221,"HyperDash":false},{"StartTime":6703.0,"Position":187.0,"HyperDash":false}]},{"StartTime":6840.0,"Objects":[{"StartTime":6840.0,"Position":363.0,"HyperDash":false},{"StartTime":6908.0,"Position":359.921783,"HyperDash":false},{"StartTime":6976.0,"Position":363.0,"HyperDash":false},{"StartTime":7044.0,"Position":359.921783,"HyperDash":false},{"StartTime":7112.0,"Position":363.0,"HyperDash":false},{"StartTime":7180.0,"Position":359.921783,"HyperDash":false}]},{"StartTime":7249.0,"Objects":[{"StartTime":7249.0,"Position":411.0,"HyperDash":false},{"StartTime":7317.0,"Position":443.061676,"HyperDash":false},{"StartTime":7385.0,"Position":411.0,"HyperDash":false},{"StartTime":7453.0,"Position":443.061676,"HyperDash":false},{"StartTime":7521.0,"Position":411.0,"HyperDash":false},{"StartTime":7589.0,"Position":443.061676,"HyperDash":false}]},{"StartTime":7658.0,"Objects":[{"StartTime":7658.0,"Position":355.0,"HyperDash":false},{"StartTime":7709.0,"Position":356.134857,"HyperDash":false},{"StartTime":7760.0,"Position":355.726776,"HyperDash":false},{"StartTime":7811.0,"Position":391.454437,"HyperDash":false},{"StartTime":7862.0,"Position":387.7087,"HyperDash":false},{"StartTime":7946.0,"Position":367.7042,"HyperDash":false},{"StartTime":8067.0,"Position":355.0,"HyperDash":false}]},{"StartTime":8203.0,"Objects":[{"StartTime":8203.0,"Position":502.0,"HyperDash":false},{"StartTime":8271.0,"Position":508.2658,"HyperDash":false},{"StartTime":8339.0,"Position":502.0,"HyperDash":false},{"StartTime":8407.0,"Position":508.2658,"HyperDash":false},{"StartTime":8475.0,"Position":502.0,"HyperDash":false},{"StartTime":8543.0,"Position":508.2658,"HyperDash":false}]},{"StartTime":8612.0,"Objects":[{"StartTime":8612.0,"Position":419.0,"HyperDash":false},{"StartTime":8680.0,"Position":429.565125,"HyperDash":false},{"StartTime":8748.0,"Position":419.0,"HyperDash":false},{"StartTime":8816.0,"Position":429.565125,"HyperDash":false},{"StartTime":8884.0,"Position":419.0,"HyperDash":false},{"StartTime":8952.0,"Position":429.565125,"HyperDash":false}]},{"StartTime":9022.0,"Objects":[{"StartTime":9022.0,"Position":364.0,"HyperDash":false},{"StartTime":9073.0,"Position":405.046875,"HyperDash":false},{"StartTime":9124.0,"Position":423.0929,"HyperDash":false},{"StartTime":9175.0,"Position":471.1283,"HyperDash":false},{"StartTime":9226.0,"Position":512.0,"HyperDash":false},{"StartTime":9310.0,"Position":450.8878,"HyperDash":false},{"StartTime":9431.0,"Position":364.0,"HyperDash":false}]},{"StartTime":9567.0,"Objects":[{"StartTime":9567.0,"Position":233.0,"HyperDash":false},{"StartTime":9635.0,"Position":226.21344,"HyperDash":false},{"StartTime":9703.0,"Position":233.0,"HyperDash":false},{"StartTime":9771.0,"Position":226.21344,"HyperDash":false},{"StartTime":9839.0,"Position":233.0,"HyperDash":false},{"StartTime":9907.0,"Position":226.21344,"HyperDash":false}]},{"StartTime":9976.0,"Objects":[{"StartTime":9976.0,"Position":284.0,"HyperDash":false},{"StartTime":10044.0,"Position":302.4323,"HyperDash":false},{"StartTime":10112.0,"Position":284.0,"HyperDash":false},{"StartTime":10180.0,"Position":302.4323,"HyperDash":false},{"StartTime":10248.0,"Position":284.0,"HyperDash":false},{"StartTime":10316.0,"Position":302.4323,"HyperDash":false}]},{"StartTime":10385.0,"Objects":[{"StartTime":10385.0,"Position":245.0,"HyperDash":false},{"StartTime":10436.0,"Position":221.437592,"HyperDash":false},{"StartTime":10487.0,"Position":156.41156,"HyperDash":false},{"StartTime":10538.0,"Position":124.576111,"HyperDash":false},{"StartTime":10589.0,"Position":129.145676,"HyperDash":false},{"StartTime":10673.0,"Position":143.747086,"HyperDash":false},{"StartTime":10794.0,"Position":245.0,"HyperDash":false}]},{"StartTime":12021.0,"Objects":[{"StartTime":12021.0,"Position":407.0,"HyperDash":false},{"StartTime":12157.0,"Position":430.1819,"HyperDash":false}]},{"StartTime":12225.0,"Objects":[{"StartTime":12225.0,"Position":484.0,"HyperDash":false}]},{"StartTime":12293.0,"Objects":[{"StartTime":12293.0,"Position":484.0,"HyperDash":false},{"StartTime":12429.0,"Position":405.168243,"HyperDash":false}]},{"StartTime":12566.0,"Objects":[{"StartTime":12566.0,"Position":387.0,"HyperDash":false},{"StartTime":12617.0,"Position":436.7446,"HyperDash":false},{"StartTime":12668.0,"Position":476.301575,"HyperDash":false},{"StartTime":12719.0,"Position":481.9031,"HyperDash":false},{"StartTime":12770.0,"Position":487.317719,"HyperDash":false},{"StartTime":12854.0,"Position":463.006,"HyperDash":false},{"StartTime":12975.0,"Position":387.0,"HyperDash":false}]},{"StartTime":13111.0,"Objects":[{"StartTime":13111.0,"Position":274.0,"HyperDash":false},{"StartTime":13247.0,"Position":173.621216,"HyperDash":false}]},{"StartTime":13316.0,"Objects":[{"StartTime":13316.0,"Position":124.0,"HyperDash":false}]},{"StartTime":13384.0,"Objects":[{"StartTime":13384.0,"Position":124.0,"HyperDash":false},{"StartTime":13520.0,"Position":23.6840134,"HyperDash":false}]},{"StartTime":13657.0,"Objects":[{"StartTime":13657.0,"Position":24.0,"HyperDash":false},{"StartTime":13741.0,"Position":80.13487,"HyperDash":false},{"StartTime":13861.0,"Position":108.188271,"HyperDash":false}]},{"StartTime":14066.0,"Objects":[{"StartTime":14066.0,"Position":229.0,"HyperDash":false}]},{"StartTime":14202.0,"Objects":[{"StartTime":14202.0,"Position":328.0,"HyperDash":false},{"StartTime":14338.0,"Position":300.487976,"HyperDash":false}]},{"StartTime":14407.0,"Objects":[{"StartTime":14407.0,"Position":256.0,"HyperDash":false}]},{"StartTime":14475.0,"Objects":[{"StartTime":14475.0,"Position":256.0,"HyperDash":false},{"StartTime":14611.0,"Position":333.4403,"HyperDash":false}]},{"StartTime":14748.0,"Objects":[{"StartTime":14748.0,"Position":378.0,"HyperDash":false},{"StartTime":14799.0,"Position":434.77832,"HyperDash":false},{"StartTime":14850.0,"Position":445.8225,"HyperDash":false},{"StartTime":14901.0,"Position":485.582428,"HyperDash":false},{"StartTime":14952.0,"Position":497.584961,"HyperDash":false},{"StartTime":15036.0,"Position":476.6938,"HyperDash":false},{"StartTime":15157.0,"Position":378.0,"HyperDash":false}]},{"StartTime":15293.0,"Objects":[{"StartTime":15293.0,"Position":277.0,"HyperDash":false},{"StartTime":15429.0,"Position":252.447769,"HyperDash":false}]},{"StartTime":15498.0,"Objects":[{"StartTime":15498.0,"Position":212.0,"HyperDash":false}]},{"StartTime":15566.0,"Objects":[{"StartTime":15566.0,"Position":212.0,"HyperDash":false},{"StartTime":15702.0,"Position":236.552231,"HyperDash":false}]},{"StartTime":15839.0,"Objects":[{"StartTime":15839.0,"Position":256.0,"HyperDash":false},{"StartTime":15923.0,"Position":331.136047,"HyperDash":false},{"StartTime":16043.0,"Position":396.792,"HyperDash":false}]},{"StartTime":16248.0,"Objects":[{"StartTime":16248.0,"Position":473.0,"HyperDash":false}]},{"StartTime":16384.0,"Objects":[{"StartTime":16384.0,"Position":486.0,"HyperDash":false},{"StartTime":16520.0,"Position":397.151581,"HyperDash":false}]},{"StartTime":16589.0,"Objects":[{"StartTime":16589.0,"Position":382.0,"HyperDash":false}]},{"StartTime":16657.0,"Objects":[{"StartTime":16657.0,"Position":382.0,"HyperDash":false},{"StartTime":16793.0,"Position":297.671143,"HyperDash":false}]},{"StartTime":16930.0,"Objects":[{"StartTime":16930.0,"Position":201.0,"HyperDash":false},{"StartTime":16981.0,"Position":215.6601,"HyperDash":false},{"StartTime":17032.0,"Position":193.010483,"HyperDash":false},{"StartTime":17083.0,"Position":161.445236,"HyperDash":false},{"StartTime":17134.0,"Position":106.410675,"HyperDash":false},{"StartTime":17218.0,"Position":175.619278,"HyperDash":false},{"StartTime":17339.0,"Position":201.0,"HyperDash":false}]},{"StartTime":17475.0,"Objects":[{"StartTime":17475.0,"Position":40.0,"HyperDash":false},{"StartTime":17611.0,"Position":56.7687,"HyperDash":false}]},{"StartTime":17680.0,"Objects":[{"StartTime":17680.0,"Position":97.0,"HyperDash":false}]},{"StartTime":17748.0,"Objects":[{"StartTime":17748.0,"Position":97.0,"HyperDash":false},{"StartTime":17884.0,"Position":197.612183,"HyperDash":false}]},{"StartTime":18021.0,"Objects":[{"StartTime":18021.0,"Position":275.0,"HyperDash":false},{"StartTime":18105.0,"Position":227.99115,"HyperDash":false},{"StartTime":18225.0,"Position":263.429932,"HyperDash":false}]},{"StartTime":18430.0,"Objects":[{"StartTime":18430.0,"Position":415.0,"HyperDash":false}]},{"StartTime":18566.0,"Objects":[{"StartTime":18566.0,"Position":355.0,"HyperDash":false},{"StartTime":18702.0,"Position":450.052368,"HyperDash":false}]},{"StartTime":18771.0,"Objects":[{"StartTime":18771.0,"Position":486.0,"HyperDash":false}]},{"StartTime":18839.0,"Objects":[{"StartTime":18839.0,"Position":486.0,"HyperDash":false},{"StartTime":18975.0,"Position":451.9095,"HyperDash":false}]},{"StartTime":19111.0,"Objects":[{"StartTime":19111.0,"Position":476.0,"HyperDash":false},{"StartTime":19162.0,"Position":467.7854,"HyperDash":false},{"StartTime":19213.0,"Position":421.959442,"HyperDash":false},{"StartTime":19264.0,"Position":381.8976,"HyperDash":false},{"StartTime":19315.0,"Position":360.902435,"HyperDash":false},{"StartTime":19399.0,"Position":438.64032,"HyperDash":false},{"StartTime":19520.0,"Position":476.0,"HyperDash":false}]},{"StartTime":19657.0,"Objects":[{"StartTime":19657.0,"Position":306.0,"HyperDash":false},{"StartTime":19793.0,"Position":210.46254,"HyperDash":false}]},{"StartTime":19861.0,"Objects":[{"StartTime":19861.0,"Position":161.0,"HyperDash":false}]},{"StartTime":19930.0,"Objects":[{"StartTime":19930.0,"Position":161.0,"HyperDash":false},{"StartTime":20066.0,"Position":196.729462,"HyperDash":false}]},{"StartTime":20202.0,"Objects":[{"StartTime":20202.0,"Position":127.0,"HyperDash":false},{"StartTime":20338.0,"Position":32.14918,"HyperDash":false}]},{"StartTime":20475.0,"Objects":[{"StartTime":20475.0,"Position":41.0,"HyperDash":false}]},{"StartTime":20543.0,"Objects":[{"StartTime":20543.0,"Position":48.0,"HyperDash":false}]},{"StartTime":20611.0,"Objects":[{"StartTime":20611.0,"Position":64.0,"HyperDash":false}]},{"StartTime":20679.0,"Objects":[{"StartTime":20679.0,"Position":86.0,"HyperDash":false}]},{"StartTime":20748.0,"Objects":[{"StartTime":20748.0,"Position":111.0,"HyperDash":false},{"StartTime":20884.0,"Position":197.677109,"HyperDash":false}]},{"StartTime":20952.0,"Objects":[{"StartTime":20952.0,"Position":249.0,"HyperDash":false}]},{"StartTime":21021.0,"Objects":[{"StartTime":21021.0,"Position":249.0,"HyperDash":false},{"StartTime":21157.0,"Position":350.174561,"HyperDash":false}]},{"StartTime":21293.0,"Objects":[{"StartTime":21293.0,"Position":451.0,"HyperDash":false},{"StartTime":21377.0,"Position":450.080383,"HyperDash":false},{"StartTime":21497.0,"Position":406.784882,"HyperDash":false}]},{"StartTime":21702.0,"Objects":[{"StartTime":21702.0,"Position":398.0,"HyperDash":false}]},{"StartTime":21839.0,"Objects":[{"StartTime":21839.0,"Position":337.0,"HyperDash":false},{"StartTime":21975.0,"Position":245.5466,"HyperDash":false}]},{"StartTime":22043.0,"Objects":[{"StartTime":22043.0,"Position":202.0,"HyperDash":false}]},{"StartTime":22111.0,"Objects":[{"StartTime":22111.0,"Position":202.0,"HyperDash":false},{"StartTime":22247.0,"Position":175.162018,"HyperDash":false}]},{"StartTime":22384.0,"Objects":[{"StartTime":22384.0,"Position":7.0,"HyperDash":false}]},{"StartTime":22589.0,"Objects":[{"StartTime":22589.0,"Position":7.0,"HyperDash":false}]},{"StartTime":22793.0,"Objects":[{"StartTime":22793.0,"Position":7.0,"HyperDash":false}]},{"StartTime":22930.0,"Objects":[{"StartTime":22930.0,"Position":61.0,"HyperDash":false},{"StartTime":23066.0,"Position":50.69364,"HyperDash":false}]},{"StartTime":23134.0,"Objects":[{"StartTime":23134.0,"Position":92.0,"HyperDash":false}]},{"StartTime":23202.0,"Objects":[{"StartTime":23202.0,"Position":92.0,"HyperDash":false},{"StartTime":23338.0,"Position":179.7528,"HyperDash":false}]},{"StartTime":23475.0,"Objects":[{"StartTime":23475.0,"Position":262.0,"HyperDash":false},{"StartTime":23559.0,"Position":335.717,"HyperDash":false},{"StartTime":23679.0,"Position":354.8034,"HyperDash":false}]},{"StartTime":23884.0,"Objects":[{"StartTime":23884.0,"Position":467.0,"HyperDash":false}]},{"StartTime":24021.0,"Objects":[{"StartTime":24021.0,"Position":430.0,"HyperDash":false},{"StartTime":24157.0,"Position":329.387817,"HyperDash":false}]},{"StartTime":24225.0,"Objects":[{"StartTime":24225.0,"Position":284.0,"HyperDash":false}]},{"StartTime":24293.0,"Objects":[{"StartTime":24293.0,"Position":284.0,"HyperDash":false},{"StartTime":24429.0,"Position":261.101257,"HyperDash":false}]},{"StartTime":24566.0,"Objects":[{"StartTime":24566.0,"Position":386.0,"HyperDash":false}]},{"StartTime":24771.0,"Objects":[{"StartTime":24771.0,"Position":386.0,"HyperDash":false}]},{"StartTime":24975.0,"Objects":[{"StartTime":24975.0,"Position":386.0,"HyperDash":false}]},{"StartTime":25111.0,"Objects":[{"StartTime":25111.0,"Position":432.0,"HyperDash":false},{"StartTime":25247.0,"Position":447.35553,"HyperDash":false}]},{"StartTime":25316.0,"Objects":[{"StartTime":25316.0,"Position":416.0,"HyperDash":false}]},{"StartTime":25384.0,"Objects":[{"StartTime":25384.0,"Position":416.0,"HyperDash":false},{"StartTime":25520.0,"Position":316.536438,"HyperDash":false}]},{"StartTime":25657.0,"Objects":[{"StartTime":25657.0,"Position":219.0,"HyperDash":false},{"StartTime":25741.0,"Position":178.386673,"HyperDash":false},{"StartTime":25861.0,"Position":167.07811,"HyperDash":false}]},{"StartTime":26066.0,"Objects":[{"StartTime":26066.0,"Position":40.0,"HyperDash":false}]},{"StartTime":26202.0,"Objects":[{"StartTime":26202.0,"Position":28.0,"HyperDash":false},{"StartTime":26338.0,"Position":101.876366,"HyperDash":false}]},{"StartTime":26407.0,"Objects":[{"StartTime":26407.0,"Position":125.0,"HyperDash":false}]},{"StartTime":26475.0,"Objects":[{"StartTime":26475.0,"Position":125.0,"HyperDash":false},{"StartTime":26611.0,"Position":142.871735,"HyperDash":false}]},{"StartTime":26748.0,"Objects":[{"StartTime":26748.0,"Position":221.0,"HyperDash":false}]},{"StartTime":26953.0,"Objects":[{"StartTime":26953.0,"Position":221.0,"HyperDash":false}]},{"StartTime":27157.0,"Objects":[{"StartTime":27157.0,"Position":221.0,"HyperDash":false}]},{"StartTime":27293.0,"Objects":[{"StartTime":27293.0,"Position":379.0,"HyperDash":false},{"StartTime":27429.0,"Position":479.272156,"HyperDash":false}]},{"StartTime":27498.0,"Objects":[{"StartTime":27498.0,"Position":510.0,"HyperDash":false}]},{"StartTime":27566.0,"Objects":[{"StartTime":27566.0,"Position":510.0,"HyperDash":false},{"StartTime":27702.0,"Position":491.238281,"HyperDash":false}]},{"StartTime":27839.0,"Objects":[{"StartTime":27839.0,"Position":503.0,"HyperDash":false},{"StartTime":27923.0,"Position":457.324524,"HyperDash":false},{"StartTime":28043.0,"Position":381.6685,"HyperDash":false}]},{"StartTime":28248.0,"Objects":[{"StartTime":28248.0,"Position":256.0,"HyperDash":false}]},{"StartTime":28384.0,"Objects":[{"StartTime":28384.0,"Position":190.0,"HyperDash":false}]},{"StartTime":28521.0,"Objects":[{"StartTime":28521.0,"Position":269.0,"HyperDash":false}]},{"StartTime":28589.0,"Objects":[{"StartTime":28589.0,"Position":272.0,"HyperDash":false}]},{"StartTime":28657.0,"Objects":[{"StartTime":28657.0,"Position":275.0,"HyperDash":false},{"StartTime":28793.0,"Position":264.133636,"HyperDash":false}]},{"StartTime":28930.0,"Objects":[{"StartTime":28930.0,"Position":179.0,"HyperDash":false}]},{"StartTime":28998.0,"Objects":[{"StartTime":28998.0,"Position":154.0,"HyperDash":false}]},{"StartTime":29066.0,"Objects":[{"StartTime":29066.0,"Position":135.0,"HyperDash":false}]},{"StartTime":29134.0,"Objects":[{"StartTime":29134.0,"Position":122.0,"HyperDash":false}]},{"StartTime":29202.0,"Objects":[{"StartTime":29202.0,"Position":118.0,"HyperDash":false},{"StartTime":29270.0,"Position":107.667114,"HyperDash":false},{"StartTime":29338.0,"Position":118.0,"HyperDash":false},{"StartTime":29406.0,"Position":107.667114,"HyperDash":false}]},{"StartTime":29475.0,"Objects":[{"StartTime":29475.0,"Position":45.0,"HyperDash":false},{"StartTime":29543.0,"Position":4.39538574,"HyperDash":false},{"StartTime":29611.0,"Position":45.0,"HyperDash":false},{"StartTime":29679.0,"Position":4.39538574,"HyperDash":false}]},{"StartTime":29748.0,"Objects":[{"StartTime":29748.0,"Position":102.0,"HyperDash":false},{"StartTime":29816.0,"Position":142.604614,"HyperDash":false},{"StartTime":29884.0,"Position":102.0,"HyperDash":false},{"StartTime":29952.0,"Position":142.604614,"HyperDash":false}]},{"StartTime":30021.0,"Objects":[{"StartTime":30021.0,"Position":193.0,"HyperDash":false},{"StartTime":30089.0,"Position":205.21228,"HyperDash":false},{"StartTime":30157.0,"Position":193.0,"HyperDash":false},{"StartTime":30225.0,"Position":205.21228,"HyperDash":false}]},{"StartTime":30293.0,"Objects":[{"StartTime":30293.0,"Position":291.0,"HyperDash":false},{"StartTime":30361.0,"Position":302.9382,"HyperDash":false},{"StartTime":30429.0,"Position":291.0,"HyperDash":false},{"StartTime":30497.0,"Position":302.9382,"HyperDash":false}]},{"StartTime":30566.0,"Objects":[{"StartTime":30566.0,"Position":391.0,"HyperDash":false}]},{"StartTime":30634.0,"Objects":[{"StartTime":30634.0,"Position":400.0,"HyperDash":false}]},{"StartTime":30702.0,"Objects":[{"StartTime":30702.0,"Position":409.0,"HyperDash":false}]},{"StartTime":30839.0,"Objects":[{"StartTime":30839.0,"Position":434.0,"HyperDash":false}]},{"StartTime":30907.0,"Objects":[{"StartTime":30907.0,"Position":425.0,"HyperDash":false}]},{"StartTime":30975.0,"Objects":[{"StartTime":30975.0,"Position":416.0,"HyperDash":false}]},{"StartTime":31111.0,"Objects":[{"StartTime":31111.0,"Position":512.0,"HyperDash":false},{"StartTime":31179.0,"Position":499.154633,"HyperDash":false},{"StartTime":31247.0,"Position":512.0,"HyperDash":false}]},{"StartTime":31384.0,"Objects":[{"StartTime":31384.0,"Position":435.0,"HyperDash":false},{"StartTime":31452.0,"Position":446.9382,"HyperDash":false},{"StartTime":31520.0,"Position":435.0,"HyperDash":false}]},{"StartTime":31657.0,"Objects":[{"StartTime":31657.0,"Position":381.0,"HyperDash":false},{"StartTime":31725.0,"Position":340.211151,"HyperDash":false},{"StartTime":31793.0,"Position":381.0,"HyperDash":false},{"StartTime":31861.0,"Position":340.211151,"HyperDash":false}]},{"StartTime":31930.0,"Objects":[{"StartTime":31930.0,"Position":251.0,"HyperDash":false},{"StartTime":31998.0,"Position":210.395386,"HyperDash":false},{"StartTime":32066.0,"Position":251.0,"HyperDash":false},{"StartTime":32134.0,"Position":210.395386,"HyperDash":false}]},{"StartTime":32202.0,"Objects":[{"StartTime":32202.0,"Position":146.0,"HyperDash":false},{"StartTime":32270.0,"Position":158.21228,"HyperDash":false},{"StartTime":32338.0,"Position":146.0,"HyperDash":false},{"StartTime":32406.0,"Position":158.21228,"HyperDash":false}]},{"StartTime":32475.0,"Objects":[{"StartTime":32475.0,"Position":56.0,"HyperDash":false},{"StartTime":32543.0,"Position":68.21229,"HyperDash":false},{"StartTime":32611.0,"Position":56.0,"HyperDash":false},{"StartTime":32679.0,"Position":68.21229,"HyperDash":false}]},{"StartTime":32748.0,"Objects":[{"StartTime":32748.0,"Position":22.0,"HyperDash":false}]},{"StartTime":32816.0,"Objects":[{"StartTime":32816.0,"Position":25.0,"HyperDash":false}]},{"StartTime":32884.0,"Objects":[{"StartTime":32884.0,"Position":28.0,"HyperDash":false}]},{"StartTime":33021.0,"Objects":[{"StartTime":33021.0,"Position":93.0,"HyperDash":false}]},{"StartTime":33089.0,"Objects":[{"StartTime":33089.0,"Position":90.0,"HyperDash":false}]},{"StartTime":33157.0,"Objects":[{"StartTime":33157.0,"Position":87.0,"HyperDash":false}]},{"StartTime":33293.0,"Objects":[{"StartTime":33293.0,"Position":168.0,"HyperDash":false}]},{"StartTime":33361.0,"Objects":[{"StartTime":33361.0,"Position":176.0,"HyperDash":false}]},{"StartTime":33430.0,"Objects":[{"StartTime":33430.0,"Position":184.0,"HyperDash":false},{"StartTime":33566.0,"Position":268.439758,"HyperDash":false}]},{"StartTime":33839.0,"Objects":[{"StartTime":33839.0,"Position":274.0,"HyperDash":false},{"StartTime":33907.0,"Position":261.78772,"HyperDash":false},{"StartTime":33975.0,"Position":274.0,"HyperDash":false},{"StartTime":34043.0,"Position":261.78772,"HyperDash":false}]},{"StartTime":34112.0,"Objects":[{"StartTime":34112.0,"Position":330.0,"HyperDash":false},{"StartTime":34180.0,"Position":342.21228,"HyperDash":false},{"StartTime":34248.0,"Position":330.0,"HyperDash":false},{"StartTime":34316.0,"Position":342.21228,"HyperDash":false}]},{"StartTime":34384.0,"Objects":[{"StartTime":34384.0,"Position":422.0,"HyperDash":false},{"StartTime":34452.0,"Position":462.788849,"HyperDash":false},{"StartTime":34520.0,"Position":422.0,"HyperDash":false},{"StartTime":34588.0,"Position":462.788849,"HyperDash":false}]},{"StartTime":34657.0,"Objects":[{"StartTime":34657.0,"Position":461.0,"HyperDash":false},{"StartTime":34725.0,"Position":501.6046,"HyperDash":false},{"StartTime":34793.0,"Position":461.0,"HyperDash":false},{"StartTime":34861.0,"Position":501.6046,"HyperDash":false}]},{"StartTime":34930.0,"Objects":[{"StartTime":34930.0,"Position":448.0,"HyperDash":false}]},{"StartTime":34998.0,"Objects":[{"StartTime":34998.0,"Position":439.0,"HyperDash":false}]},{"StartTime":35066.0,"Objects":[{"StartTime":35066.0,"Position":430.0,"HyperDash":false}]},{"StartTime":35202.0,"Objects":[{"StartTime":35202.0,"Position":321.0,"HyperDash":false}]},{"StartTime":35270.0,"Objects":[{"StartTime":35270.0,"Position":312.0,"HyperDash":false}]},{"StartTime":35338.0,"Objects":[{"StartTime":35338.0,"Position":303.0,"HyperDash":false}]},{"StartTime":35475.0,"Objects":[{"StartTime":35475.0,"Position":269.0,"HyperDash":false},{"StartTime":35543.0,"Position":228.395386,"HyperDash":false},{"StartTime":35611.0,"Position":269.0,"HyperDash":false}]},{"StartTime":35748.0,"Objects":[{"StartTime":35748.0,"Position":162.0,"HyperDash":false},{"StartTime":35816.0,"Position":202.788834,"HyperDash":false},{"StartTime":35884.0,"Position":162.0,"HyperDash":false}]},{"StartTime":36021.0,"Objects":[{"StartTime":36021.0,"Position":87.0,"HyperDash":false},{"StartTime":36089.0,"Position":99.21229,"HyperDash":false},{"StartTime":36157.0,"Position":87.0,"HyperDash":false},{"StartTime":36225.0,"Position":99.21229,"HyperDash":false}]},{"StartTime":36294.0,"Objects":[{"StartTime":36294.0,"Position":31.0,"HyperDash":false},{"StartTime":36362.0,"Position":18.7877159,"HyperDash":false},{"StartTime":36430.0,"Position":31.0,"HyperDash":false},{"StartTime":36498.0,"Position":18.7877159,"HyperDash":false}]},{"StartTime":36566.0,"Objects":[{"StartTime":36566.0,"Position":101.0,"HyperDash":false},{"StartTime":36634.0,"Position":141.788834,"HyperDash":false},{"StartTime":36702.0,"Position":101.0,"HyperDash":false},{"StartTime":36770.0,"Position":141.788834,"HyperDash":false}]},{"StartTime":36839.0,"Objects":[{"StartTime":36839.0,"Position":184.0,"HyperDash":false},{"StartTime":36907.0,"Position":224.604614,"HyperDash":false},{"StartTime":36975.0,"Position":184.0,"HyperDash":false},{"StartTime":37043.0,"Position":224.604614,"HyperDash":false}]},{"StartTime":37111.0,"Objects":[{"StartTime":37111.0,"Position":304.0,"HyperDash":false}]},{"StartTime":37179.0,"Objects":[{"StartTime":37179.0,"Position":307.0,"HyperDash":false}]},{"StartTime":37247.0,"Objects":[{"StartTime":37247.0,"Position":310.0,"HyperDash":false}]},{"StartTime":37384.0,"Objects":[{"StartTime":37384.0,"Position":392.0,"HyperDash":false}]},{"StartTime":37452.0,"Objects":[{"StartTime":37452.0,"Position":395.0,"HyperDash":false}]},{"StartTime":37520.0,"Objects":[{"StartTime":37520.0,"Position":398.0,"HyperDash":false}]},{"StartTime":37657.0,"Objects":[{"StartTime":37657.0,"Position":341.0,"HyperDash":false},{"StartTime":37725.0,"Position":356.784119,"HyperDash":false},{"StartTime":37793.0,"Position":341.0,"HyperDash":false},{"StartTime":37861.0,"Position":356.784119,"HyperDash":false}]},{"StartTime":37930.0,"Objects":[{"StartTime":37930.0,"Position":352.0,"HyperDash":false},{"StartTime":37998.0,"Position":367.784119,"HyperDash":false},{"StartTime":38066.0,"Position":352.0,"HyperDash":false},{"StartTime":38134.0,"Position":367.784119,"HyperDash":false}]},{"StartTime":38202.0,"Objects":[{"StartTime":38202.0,"Position":449.0,"HyperDash":false},{"StartTime":38261.0,"Position":474.06,"HyperDash":false},{"StartTime":38320.0,"Position":479.854156,"HyperDash":false},{"StartTime":38379.0,"Position":497.1734,"HyperDash":false},{"StartTime":38474.0,"Position":487.267334,"HyperDash":false}]},{"StartTime":38748.0,"Objects":[{"StartTime":38748.0,"Position":487.0,"HyperDash":false},{"StartTime":38807.0,"Position":440.148621,"HyperDash":false},{"StartTime":38866.0,"Position":413.297272,"HyperDash":false},{"StartTime":38925.0,"Position":408.4459,"HyperDash":false},{"StartTime":39020.0,"Position":353.9903,"HyperDash":false}]},{"StartTime":39293.0,"Objects":[{"StartTime":39293.0,"Position":403.0,"HyperDash":false},{"StartTime":39352.0,"Position":361.224365,"HyperDash":false},{"StartTime":39411.0,"Position":329.028229,"HyperDash":false},{"StartTime":39470.0,"Position":319.025146,"HyperDash":false},{"StartTime":39565.0,"Position":277.9407,"HyperDash":false}]},{"StartTime":39702.0,"Objects":[{"StartTime":39702.0,"Position":277.0,"HyperDash":false}]},{"StartTime":39839.0,"Objects":[{"StartTime":39839.0,"Position":155.0,"HyperDash":false},{"StartTime":39975.0,"Position":184.883255,"HyperDash":false}]},{"StartTime":40111.0,"Objects":[{"StartTime":40111.0,"Position":65.0,"HyperDash":false}]},{"StartTime":40384.0,"Objects":[{"StartTime":40384.0,"Position":65.0,"HyperDash":false},{"StartTime":40520.0,"Position":148.5545,"HyperDash":false}]},{"StartTime":40657.0,"Objects":[{"StartTime":40657.0,"Position":90.0,"HyperDash":false},{"StartTime":40793.0,"Position":6.445488,"HyperDash":false}]},{"StartTime":40930.0,"Objects":[{"StartTime":40930.0,"Position":180.0,"HyperDash":false}]},{"StartTime":41066.0,"Objects":[{"StartTime":41066.0,"Position":280.0,"HyperDash":false}]},{"StartTime":41134.0,"Objects":[{"StartTime":41134.0,"Position":280.0,"HyperDash":false}]},{"StartTime":41202.0,"Objects":[{"StartTime":41202.0,"Position":280.0,"HyperDash":false},{"StartTime":41338.0,"Position":363.5545,"HyperDash":false}]},{"StartTime":41475.0,"Objects":[{"StartTime":41475.0,"Position":208.0,"HyperDash":false}]},{"StartTime":41611.0,"Objects":[{"StartTime":41611.0,"Position":208.0,"HyperDash":false}]},{"StartTime":41748.0,"Objects":[{"StartTime":41748.0,"Position":372.0,"HyperDash":false},{"StartTime":41884.0,"Position":288.4455,"HyperDash":false}]},{"StartTime":42021.0,"Objects":[{"StartTime":42021.0,"Position":170.0,"HyperDash":false},{"StartTime":42157.0,"Position":187.164719,"HyperDash":false}]},{"StartTime":42293.0,"Objects":[{"StartTime":42293.0,"Position":64.0,"HyperDash":false},{"StartTime":42361.0,"Position":71.60263,"HyperDash":false},{"StartTime":42429.0,"Position":64.0,"HyperDash":false},{"StartTime":42497.0,"Position":71.60263,"HyperDash":false}]},{"StartTime":42566.0,"Objects":[{"StartTime":42566.0,"Position":25.0,"HyperDash":false},{"StartTime":42625.0,"Position":29.524353,"HyperDash":false},{"StartTime":42684.0,"Position":56.72582,"HyperDash":false},{"StartTime":42743.0,"Position":46.7086868,"HyperDash":false},{"StartTime":42838.0,"Position":32.0564842,"HyperDash":false}]},{"StartTime":43111.0,"Objects":[{"StartTime":43111.0,"Position":32.0,"HyperDash":false},{"StartTime":43170.0,"Position":77.73514,"HyperDash":false},{"StartTime":43229.0,"Position":72.4702759,"HyperDash":false},{"StartTime":43288.0,"Position":123.205421,"HyperDash":false},{"StartTime":43383.0,"Position":164.473862,"HyperDash":false}]},{"StartTime":43657.0,"Objects":[{"StartTime":43657.0,"Position":420.0,"HyperDash":false},{"StartTime":43716.0,"Position":410.224365,"HyperDash":false},{"StartTime":43775.0,"Position":376.028229,"HyperDash":false},{"StartTime":43834.0,"Position":351.025146,"HyperDash":false},{"StartTime":43929.0,"Position":294.9407,"HyperDash":false}]},{"StartTime":44066.0,"Objects":[{"StartTime":44066.0,"Position":294.0,"HyperDash":false}]},{"StartTime":44202.0,"Objects":[{"StartTime":44202.0,"Position":204.0,"HyperDash":false},{"StartTime":44338.0,"Position":217.130188,"HyperDash":false}]},{"StartTime":44475.0,"Objects":[{"StartTime":44475.0,"Position":381.0,"HyperDash":false}]},{"StartTime":44748.0,"Objects":[{"StartTime":44748.0,"Position":381.0,"HyperDash":false},{"StartTime":44884.0,"Position":392.2908,"HyperDash":false}]},{"StartTime":45021.0,"Objects":[{"StartTime":45021.0,"Position":500.0,"HyperDash":false},{"StartTime":45157.0,"Position":488.7092,"HyperDash":false}]},{"StartTime":45293.0,"Objects":[{"StartTime":45293.0,"Position":285.0,"HyperDash":false}]},{"StartTime":45430.0,"Objects":[{"StartTime":45430.0,"Position":397.0,"HyperDash":false}]},{"StartTime":45498.0,"Objects":[{"StartTime":45498.0,"Position":397.0,"HyperDash":false}]},{"StartTime":45566.0,"Objects":[{"StartTime":45566.0,"Position":397.0,"HyperDash":false},{"StartTime":45702.0,"Position":385.7092,"HyperDash":false}]},{"StartTime":45839.0,"Objects":[{"StartTime":45839.0,"Position":208.0,"HyperDash":false}]},{"StartTime":45907.0,"Objects":[{"StartTime":45907.0,"Position":208.0,"HyperDash":false}]},{"StartTime":45975.0,"Objects":[{"StartTime":45975.0,"Position":208.0,"HyperDash":false},{"StartTime":46111.0,"Position":131.34523,"HyperDash":false}]},{"StartTime":46248.0,"Objects":[{"StartTime":46248.0,"Position":47.0,"HyperDash":false}]},{"StartTime":46316.0,"Objects":[{"StartTime":46316.0,"Position":54.0,"HyperDash":false}]},{"StartTime":46384.0,"Objects":[{"StartTime":46384.0,"Position":61.0,"HyperDash":false}]},{"StartTime":46521.0,"Objects":[{"StartTime":46521.0,"Position":118.0,"HyperDash":false},{"StartTime":46589.0,"Position":111.337379,"HyperDash":false},{"StartTime":46657.0,"Position":118.0,"HyperDash":false},{"StartTime":46725.0,"Position":111.337379,"HyperDash":false},{"StartTime":46793.0,"Position":118.0,"HyperDash":false},{"StartTime":46861.0,"Position":111.337379,"HyperDash":false}]},{"StartTime":46930.0,"Objects":[{"StartTime":46930.0,"Position":186.0,"HyperDash":false},{"StartTime":47066.0,"Position":274.623718,"HyperDash":false}]},{"StartTime":47202.0,"Objects":[{"StartTime":47202.0,"Position":446.0,"HyperDash":false},{"StartTime":47338.0,"Position":357.889038,"HyperDash":false}]},{"StartTime":47475.0,"Objects":[{"StartTime":47475.0,"Position":367.0,"HyperDash":false},{"StartTime":47611.0,"Position":390.840118,"HyperDash":false}]},{"StartTime":47748.0,"Objects":[{"StartTime":47748.0,"Position":297.0,"HyperDash":false},{"StartTime":47884.0,"Position":319.863068,"HyperDash":false}]},{"StartTime":48021.0,"Objects":[{"StartTime":48021.0,"Position":243.0,"HyperDash":false},{"StartTime":48157.0,"Position":143.595367,"HyperDash":false}]},{"StartTime":48293.0,"Objects":[{"StartTime":48293.0,"Position":188.0,"HyperDash":false}]},{"StartTime":48430.0,"Objects":[{"StartTime":48430.0,"Position":188.0,"HyperDash":false}]},{"StartTime":48566.0,"Objects":[{"StartTime":48566.0,"Position":59.0,"HyperDash":false},{"StartTime":48702.0,"Position":43.64902,"HyperDash":false}]},{"StartTime":48839.0,"Objects":[{"StartTime":48839.0,"Position":174.0,"HyperDash":false},{"StartTime":48975.0,"Position":273.404633,"HyperDash":false}]},{"StartTime":49111.0,"Objects":[{"StartTime":49111.0,"Position":423.0,"HyperDash":false},{"StartTime":49247.0,"Position":415.1793,"HyperDash":false}]},{"StartTime":49384.0,"Objects":[{"StartTime":49384.0,"Position":346.0,"HyperDash":false},{"StartTime":49520.0,"Position":433.371735,"HyperDash":true}]},{"StartTime":49657.0,"Objects":[{"StartTime":49657.0,"Position":217.0,"HyperDash":false}]},{"StartTime":49793.0,"Objects":[{"StartTime":49793.0,"Position":208.0,"HyperDash":false}]},{"StartTime":49861.0,"Objects":[{"StartTime":49861.0,"Position":208.0,"HyperDash":false}]},{"StartTime":49930.0,"Objects":[{"StartTime":49930.0,"Position":208.0,"HyperDash":false},{"StartTime":50066.0,"Position":107.101242,"HyperDash":false}]},{"StartTime":50202.0,"Objects":[{"StartTime":50202.0,"Position":45.0,"HyperDash":false}]},{"StartTime":50338.0,"Objects":[{"StartTime":50338.0,"Position":108.0,"HyperDash":false}]},{"StartTime":50475.0,"Objects":[{"StartTime":50475.0,"Position":107.0,"HyperDash":false}]},{"StartTime":50611.0,"Objects":[{"StartTime":50611.0,"Position":44.0,"HyperDash":false}]},{"StartTime":50748.0,"Objects":[{"StartTime":50748.0,"Position":70.0,"HyperDash":false},{"StartTime":50807.0,"Position":117.635452,"HyperDash":false},{"StartTime":50866.0,"Position":164.2709,"HyperDash":false},{"StartTime":50925.0,"Position":211.774979,"HyperDash":false},{"StartTime":51020.0,"Position":266.492462,"HyperDash":false}]},{"StartTime":51157.0,"Objects":[{"StartTime":51157.0,"Position":441.0,"HyperDash":false}]},{"StartTime":51225.0,"Objects":[{"StartTime":51225.0,"Position":434.0,"HyperDash":false}]},{"StartTime":51293.0,"Objects":[{"StartTime":51293.0,"Position":427.0,"HyperDash":false},{"StartTime":51429.0,"Position":405.05188,"HyperDash":false}]},{"StartTime":51566.0,"Objects":[{"StartTime":51566.0,"Position":482.0,"HyperDash":false},{"StartTime":51702.0,"Position":460.05188,"HyperDash":false}]},{"StartTime":51839.0,"Objects":[{"StartTime":51839.0,"Position":357.0,"HyperDash":false},{"StartTime":51975.0,"Position":265.6038,"HyperDash":false}]},{"StartTime":52111.0,"Objects":[{"StartTime":52111.0,"Position":119.0,"HyperDash":false},{"StartTime":52247.0,"Position":210.2502,"HyperDash":false}]},{"StartTime":52384.0,"Objects":[{"StartTime":52384.0,"Position":164.0,"HyperDash":false},{"StartTime":52520.0,"Position":74.00247,"HyperDash":false}]},{"StartTime":52657.0,"Objects":[{"StartTime":52657.0,"Position":0.0,"HyperDash":false}]},{"StartTime":52793.0,"Objects":[{"StartTime":52793.0,"Position":0.0,"HyperDash":false}]},{"StartTime":52930.0,"Objects":[{"StartTime":52930.0,"Position":124.0,"HyperDash":false},{"StartTime":53066.0,"Position":225.212341,"HyperDash":false}]},{"StartTime":53202.0,"Objects":[{"StartTime":53202.0,"Position":316.0,"HyperDash":false},{"StartTime":53338.0,"Position":303.34845,"HyperDash":false}]},{"StartTime":53475.0,"Objects":[{"StartTime":53475.0,"Position":332.0,"HyperDash":false},{"StartTime":53611.0,"Position":415.923523,"HyperDash":false}]},{"StartTime":53748.0,"Objects":[{"StartTime":53748.0,"Position":512.0,"HyperDash":false},{"StartTime":53884.0,"Position":428.076477,"HyperDash":false}]},{"StartTime":54021.0,"Objects":[{"StartTime":54021.0,"Position":512.0,"HyperDash":false}]},{"StartTime":54157.0,"Objects":[{"StartTime":54157.0,"Position":363.0,"HyperDash":false}]},{"StartTime":54225.0,"Objects":[{"StartTime":54225.0,"Position":363.0,"HyperDash":false}]},{"StartTime":54293.0,"Objects":[{"StartTime":54293.0,"Position":363.0,"HyperDash":false},{"StartTime":54429.0,"Position":262.3189,"HyperDash":false}]},{"StartTime":54566.0,"Objects":[{"StartTime":54566.0,"Position":308.0,"HyperDash":false}]},{"StartTime":54634.0,"Objects":[{"StartTime":54634.0,"Position":269.0,"HyperDash":false}]},{"StartTime":54702.0,"Objects":[{"StartTime":54702.0,"Position":227.0,"HyperDash":false}]},{"StartTime":54770.0,"Objects":[{"StartTime":54770.0,"Position":193.0,"HyperDash":false}]},{"StartTime":54838.0,"Objects":[{"StartTime":54838.0,"Position":175.0,"HyperDash":false}]},{"StartTime":54975.0,"Objects":[{"StartTime":54975.0,"Position":81.0,"HyperDash":false}]},{"StartTime":55043.0,"Objects":[{"StartTime":55043.0,"Position":74.0,"HyperDash":false}]},{"StartTime":55111.0,"Objects":[{"StartTime":55111.0,"Position":67.0,"HyperDash":false}]},{"StartTime":55248.0,"Objects":[{"StartTime":55248.0,"Position":18.0,"HyperDash":false},{"StartTime":55316.0,"Position":25.9951439,"HyperDash":false},{"StartTime":55384.0,"Position":18.0,"HyperDash":false},{"StartTime":55452.0,"Position":25.9951439,"HyperDash":false},{"StartTime":55520.0,"Position":18.0,"HyperDash":false},{"StartTime":55588.0,"Position":25.9951439,"HyperDash":false}]},{"StartTime":55657.0,"Objects":[{"StartTime":55657.0,"Position":87.0,"HyperDash":false},{"StartTime":55725.0,"Position":127.788834,"HyperDash":false},{"StartTime":55793.0,"Position":87.0,"HyperDash":false},{"StartTime":55861.0,"Position":127.788834,"HyperDash":false}]},{"StartTime":55929.0,"Objects":[{"StartTime":55929.0,"Position":175.0,"HyperDash":false},{"StartTime":55997.0,"Position":215.604614,"HyperDash":false},{"StartTime":56065.0,"Position":175.0,"HyperDash":false},{"StartTime":56133.0,"Position":215.604614,"HyperDash":false}]},{"StartTime":56202.0,"Objects":[{"StartTime":56202.0,"Position":295.0,"HyperDash":false},{"StartTime":56270.0,"Position":307.21228,"HyperDash":false},{"StartTime":56338.0,"Position":295.0,"HyperDash":false},{"StartTime":56406.0,"Position":307.21228,"HyperDash":false}]},{"StartTime":56475.0,"Objects":[{"StartTime":56475.0,"Position":265.0,"HyperDash":false},{"StartTime":56543.0,"Position":252.78772,"HyperDash":false},{"StartTime":56611.0,"Position":265.0,"HyperDash":false},{"StartTime":56679.0,"Position":252.78772,"HyperDash":false}]},{"StartTime":56748.0,"Objects":[{"StartTime":56748.0,"Position":327.0,"HyperDash":false}]},{"StartTime":56816.0,"Objects":[{"StartTime":56816.0,"Position":336.0,"HyperDash":false}]},{"StartTime":56884.0,"Objects":[{"StartTime":56884.0,"Position":345.0,"HyperDash":false}]},{"StartTime":57021.0,"Objects":[{"StartTime":57021.0,"Position":414.0,"HyperDash":false}]},{"StartTime":57089.0,"Objects":[{"StartTime":57089.0,"Position":423.0,"HyperDash":false}]},{"StartTime":57157.0,"Objects":[{"StartTime":57157.0,"Position":432.0,"HyperDash":false}]},{"StartTime":57293.0,"Objects":[{"StartTime":57293.0,"Position":502.0,"HyperDash":false},{"StartTime":57361.0,"Position":489.78772,"HyperDash":false},{"StartTime":57429.0,"Position":502.0,"HyperDash":false}]},{"StartTime":57566.0,"Objects":[{"StartTime":57566.0,"Position":431.0,"HyperDash":false},{"StartTime":57634.0,"Position":443.21228,"HyperDash":false},{"StartTime":57702.0,"Position":431.0,"HyperDash":false}]},{"StartTime":57839.0,"Objects":[{"StartTime":57839.0,"Position":356.0,"HyperDash":false},{"StartTime":57907.0,"Position":343.78772,"HyperDash":false},{"StartTime":57975.0,"Position":356.0,"HyperDash":false},{"StartTime":58043.0,"Position":343.78772,"HyperDash":false}]},{"StartTime":58112.0,"Objects":[{"StartTime":58112.0,"Position":294.0,"HyperDash":false},{"StartTime":58180.0,"Position":334.7076,"HyperDash":false},{"StartTime":58248.0,"Position":294.0,"HyperDash":false},{"StartTime":58316.0,"Position":334.7076,"HyperDash":false}]},{"StartTime":58384.0,"Objects":[{"StartTime":58384.0,"Position":205.0,"HyperDash":false},{"StartTime":58452.0,"Position":192.78772,"HyperDash":false},{"StartTime":58520.0,"Position":205.0,"HyperDash":false},{"StartTime":58588.0,"Position":192.78772,"HyperDash":false}]},{"StartTime":58657.0,"Objects":[{"StartTime":58657.0,"Position":151.0,"HyperDash":false},{"StartTime":58725.0,"Position":110.292381,"HyperDash":false},{"StartTime":58793.0,"Position":151.0,"HyperDash":false},{"StartTime":58861.0,"Position":110.292381,"HyperDash":false}]},{"StartTime":58930.0,"Objects":[{"StartTime":58930.0,"Position":21.0,"HyperDash":false}]},{"StartTime":58998.0,"Objects":[{"StartTime":58998.0,"Position":18.0,"HyperDash":false}]},{"StartTime":59066.0,"Objects":[{"StartTime":59066.0,"Position":15.0,"HyperDash":false}]},{"StartTime":59202.0,"Objects":[{"StartTime":59202.0,"Position":96.0,"HyperDash":false}]},{"StartTime":59270.0,"Objects":[{"StartTime":59270.0,"Position":93.0,"HyperDash":false}]},{"StartTime":59338.0,"Objects":[{"StartTime":59338.0,"Position":90.0,"HyperDash":false}]},{"StartTime":59475.0,"Objects":[{"StartTime":59475.0,"Position":38.0,"HyperDash":false}]},{"StartTime":59543.0,"Objects":[{"StartTime":59543.0,"Position":41.0,"HyperDash":false}]},{"StartTime":59611.0,"Objects":[{"StartTime":59611.0,"Position":44.0,"HyperDash":false},{"StartTime":59747.0,"Position":35.8773422,"HyperDash":false}]},{"StartTime":60021.0,"Objects":[{"StartTime":60021.0,"Position":227.0,"HyperDash":false},{"StartTime":60089.0,"Position":214.78772,"HyperDash":false},{"StartTime":60157.0,"Position":227.0,"HyperDash":false},{"StartTime":60225.0,"Position":214.78772,"HyperDash":false}]},{"StartTime":60294.0,"Objects":[{"StartTime":60294.0,"Position":257.0,"HyperDash":false},{"StartTime":60362.0,"Position":269.21228,"HyperDash":false},{"StartTime":60430.0,"Position":257.0,"HyperDash":false},{"StartTime":60498.0,"Position":269.21228,"HyperDash":false}]},{"StartTime":60566.0,"Objects":[{"StartTime":60566.0,"Position":357.0,"HyperDash":false},{"StartTime":60634.0,"Position":397.788849,"HyperDash":false},{"StartTime":60702.0,"Position":357.0,"HyperDash":false},{"StartTime":60770.0,"Position":397.788849,"HyperDash":false}]},{"StartTime":60838.0,"Objects":[{"StartTime":60838.0,"Position":445.0,"HyperDash":false},{"StartTime":60906.0,"Position":485.6046,"HyperDash":false},{"StartTime":60974.0,"Position":445.0,"HyperDash":false},{"StartTime":61042.0,"Position":485.6046,"HyperDash":false}]},{"StartTime":61111.0,"Objects":[{"StartTime":61111.0,"Position":496.0,"HyperDash":false}]},{"StartTime":61179.0,"Objects":[{"StartTime":61179.0,"Position":493.0,"HyperDash":false}]},{"StartTime":61247.0,"Objects":[{"StartTime":61247.0,"Position":490.0,"HyperDash":false}]},{"StartTime":61384.0,"Objects":[{"StartTime":61384.0,"Position":420.0,"HyperDash":false}]},{"StartTime":61452.0,"Objects":[{"StartTime":61452.0,"Position":417.0,"HyperDash":false}]},{"StartTime":61521.0,"Objects":[{"StartTime":61521.0,"Position":414.0,"HyperDash":false}]},{"StartTime":61657.0,"Objects":[{"StartTime":61657.0,"Position":389.0,"HyperDash":false},{"StartTime":61725.0,"Position":348.2924,"HyperDash":false},{"StartTime":61793.0,"Position":389.0,"HyperDash":false}]},{"StartTime":61930.0,"Objects":[{"StartTime":61930.0,"Position":277.0,"HyperDash":false},{"StartTime":61998.0,"Position":236.292389,"HyperDash":false},{"StartTime":62066.0,"Position":277.0,"HyperDash":false}]},{"StartTime":62202.0,"Objects":[{"StartTime":62202.0,"Position":161.0,"HyperDash":false},{"StartTime":62270.0,"Position":148.78772,"HyperDash":false},{"StartTime":62338.0,"Position":161.0,"HyperDash":false},{"StartTime":62406.0,"Position":148.78772,"HyperDash":false}]},{"StartTime":62475.0,"Objects":[{"StartTime":62475.0,"Position":142.0,"HyperDash":false},{"StartTime":62543.0,"Position":101.292381,"HyperDash":false},{"StartTime":62611.0,"Position":142.0,"HyperDash":false},{"StartTime":62679.0,"Position":101.292381,"HyperDash":false}]},{"StartTime":62748.0,"Objects":[{"StartTime":62748.0,"Position":2.0,"HyperDash":false},{"StartTime":62816.0,"Position":14.212285,"HyperDash":false},{"StartTime":62884.0,"Position":2.0,"HyperDash":false},{"StartTime":62952.0,"Position":14.212285,"HyperDash":false}]},{"StartTime":63021.0,"Objects":[{"StartTime":63021.0,"Position":0.0,"HyperDash":false},{"StartTime":63089.0,"Position":40.70762,"HyperDash":false},{"StartTime":63157.0,"Position":0.0,"HyperDash":false},{"StartTime":63225.0,"Position":40.70762,"HyperDash":false}]},{"StartTime":63293.0,"Objects":[{"StartTime":63293.0,"Position":95.0,"HyperDash":false}]},{"StartTime":63361.0,"Objects":[{"StartTime":63361.0,"Position":104.0,"HyperDash":false}]},{"StartTime":63429.0,"Objects":[{"StartTime":63429.0,"Position":113.0,"HyperDash":false}]},{"StartTime":63566.0,"Objects":[{"StartTime":63566.0,"Position":189.0,"HyperDash":false}]},{"StartTime":63634.0,"Objects":[{"StartTime":63634.0,"Position":198.0,"HyperDash":false}]},{"StartTime":63702.0,"Objects":[{"StartTime":63702.0,"Position":207.0,"HyperDash":false}]},{"StartTime":63839.0,"Objects":[{"StartTime":63839.0,"Position":281.0,"HyperDash":false},{"StartTime":63907.0,"Position":322.273315,"HyperDash":false},{"StartTime":63975.0,"Position":281.0,"HyperDash":false},{"StartTime":64043.0,"Position":322.273315,"HyperDash":false}]},{"StartTime":64111.0,"Objects":[{"StartTime":64111.0,"Position":362.0,"HyperDash":false},{"StartTime":64179.0,"Position":403.273315,"HyperDash":false},{"StartTime":64247.0,"Position":362.0,"HyperDash":false},{"StartTime":64315.0,"Position":403.273315,"HyperDash":false}]},{"StartTime":64384.0,"Objects":[{"StartTime":64384.0,"Position":478.0,"HyperDash":false},{"StartTime":64443.0,"Position":442.243439,"HyperDash":false},{"StartTime":64502.0,"Position":440.1484,"HyperDash":false},{"StartTime":64561.0,"Position":427.0997,"HyperDash":false},{"StartTime":64656.0,"Position":444.9422,"HyperDash":false}]},{"StartTime":64930.0,"Objects":[{"StartTime":64930.0,"Position":485.0,"HyperDash":false},{"StartTime":64989.0,"Position":461.072876,"HyperDash":false},{"StartTime":65048.0,"Position":436.145752,"HyperDash":false},{"StartTime":65107.0,"Position":402.2186,"HyperDash":false},{"StartTime":65202.0,"Position":351.641022,"HyperDash":false}]},{"StartTime":65475.0,"Objects":[{"StartTime":65475.0,"Position":222.0,"HyperDash":false},{"StartTime":65534.0,"Position":184.205688,"HyperDash":false},{"StartTime":65593.0,"Position":161.582535,"HyperDash":false},{"StartTime":65652.0,"Position":155.982361,"HyperDash":false},{"StartTime":65747.0,"Position":104.778061,"HyperDash":false}]},{"StartTime":65884.0,"Objects":[{"StartTime":65884.0,"Position":104.0,"HyperDash":false}]},{"StartTime":66021.0,"Objects":[{"StartTime":66021.0,"Position":16.0,"HyperDash":false},{"StartTime":66157.0,"Position":28.7026157,"HyperDash":false}]},{"StartTime":66225.0,"Objects":[{"StartTime":66225.0,"Position":28.0,"HyperDash":false}]},{"StartTime":66293.0,"Objects":[{"StartTime":66293.0,"Position":28.0,"HyperDash":false}]},{"StartTime":66566.0,"Objects":[{"StartTime":66566.0,"Position":90.0,"HyperDash":false},{"StartTime":66702.0,"Position":76.934906,"HyperDash":false}]},{"StartTime":66839.0,"Objects":[{"StartTime":66839.0,"Position":256.0,"HyperDash":false},{"StartTime":66975.0,"Position":242.9349,"HyperDash":false}]},{"StartTime":67111.0,"Objects":[{"StartTime":67111.0,"Position":186.0,"HyperDash":false}]},{"StartTime":67248.0,"Objects":[{"StartTime":67248.0,"Position":273.0,"HyperDash":false}]},{"StartTime":67316.0,"Objects":[{"StartTime":67316.0,"Position":273.0,"HyperDash":false}]},{"StartTime":67384.0,"Objects":[{"StartTime":67384.0,"Position":273.0,"HyperDash":false},{"StartTime":67520.0,"Position":357.364716,"HyperDash":false}]},{"StartTime":67657.0,"Objects":[{"StartTime":67657.0,"Position":471.0,"HyperDash":false}]},{"StartTime":67793.0,"Objects":[{"StartTime":67793.0,"Position":471.0,"HyperDash":false}]},{"StartTime":67930.0,"Objects":[{"StartTime":67930.0,"Position":392.0,"HyperDash":false},{"StartTime":68066.0,"Position":307.582184,"HyperDash":false}]},{"StartTime":68202.0,"Objects":[{"StartTime":68202.0,"Position":165.0,"HyperDash":false},{"StartTime":68338.0,"Position":178.0651,"HyperDash":false}]},{"StartTime":68475.0,"Objects":[{"StartTime":68475.0,"Position":266.0,"HyperDash":false},{"StartTime":68543.0,"Position":307.8938,"HyperDash":false},{"StartTime":68611.0,"Position":266.0,"HyperDash":false},{"StartTime":68679.0,"Position":307.8938,"HyperDash":false}]},{"StartTime":68748.0,"Objects":[{"StartTime":68748.0,"Position":358.0,"HyperDash":false},{"StartTime":68807.0,"Position":396.968262,"HyperDash":false},{"StartTime":68866.0,"Position":418.199738,"HyperDash":false},{"StartTime":68925.0,"Position":452.599548,"HyperDash":false},{"StartTime":69020.0,"Position":484.638855,"HyperDash":false}]},{"StartTime":69293.0,"Objects":[{"StartTime":69293.0,"Position":447.0,"HyperDash":false},{"StartTime":69352.0,"Position":453.674744,"HyperDash":false},{"StartTime":69411.0,"Position":437.3495,"HyperDash":false},{"StartTime":69470.0,"Position":444.024231,"HyperDash":false},{"StartTime":69565.0,"Position":468.551361,"HyperDash":false}]},{"StartTime":69839.0,"Objects":[{"StartTime":69839.0,"Position":343.0,"HyperDash":false},{"StartTime":69898.0,"Position":329.563446,"HyperDash":false},{"StartTime":69957.0,"Position":311.8805,"HyperDash":false},{"StartTime":70016.0,"Position":271.0514,"HyperDash":false},{"StartTime":70111.0,"Position":243.183487,"HyperDash":false}]},{"StartTime":70248.0,"Objects":[{"StartTime":70248.0,"Position":216.0,"HyperDash":false}]},{"StartTime":70316.0,"Objects":[{"StartTime":70316.0,"Position":216.0,"HyperDash":false}]},{"StartTime":70384.0,"Objects":[{"StartTime":70384.0,"Position":216.0,"HyperDash":false},{"StartTime":70520.0,"Position":154.538864,"HyperDash":false}]},{"StartTime":70657.0,"Objects":[{"StartTime":70657.0,"Position":58.0,"HyperDash":false}]},{"StartTime":70930.0,"Objects":[{"StartTime":70930.0,"Position":58.0,"HyperDash":false},{"StartTime":71066.0,"Position":48.7692032,"HyperDash":false}]},{"StartTime":71202.0,"Objects":[{"StartTime":71202.0,"Position":129.0,"HyperDash":false},{"StartTime":71338.0,"Position":138.2308,"HyperDash":false}]},{"StartTime":71475.0,"Objects":[{"StartTime":71475.0,"Position":132.0,"HyperDash":false}]},{"StartTime":71611.0,"Objects":[{"StartTime":71611.0,"Position":228.0,"HyperDash":false}]},{"StartTime":71680.0,"Objects":[{"StartTime":71680.0,"Position":228.0,"HyperDash":false}]},{"StartTime":71748.0,"Objects":[{"StartTime":71748.0,"Position":228.0,"HyperDash":false},{"StartTime":71884.0,"Position":312.5163,"HyperDash":false}]},{"StartTime":72021.0,"Objects":[{"StartTime":72021.0,"Position":382.0,"HyperDash":false}]},{"StartTime":72089.0,"Objects":[{"StartTime":72089.0,"Position":414.0,"HyperDash":false}]},{"StartTime":72157.0,"Objects":[{"StartTime":72157.0,"Position":448.0,"HyperDash":false}]},{"StartTime":72225.0,"Objects":[{"StartTime":72225.0,"Position":478.0,"HyperDash":false}]},{"StartTime":72293.0,"Objects":[{"StartTime":72293.0,"Position":500.0,"HyperDash":false}]},{"StartTime":72430.0,"Objects":[{"StartTime":72430.0,"Position":453.0,"HyperDash":false}]},{"StartTime":72498.0,"Objects":[{"StartTime":72498.0,"Position":449.0,"HyperDash":false}]},{"StartTime":72566.0,"Objects":[{"StartTime":72566.0,"Position":445.0,"HyperDash":false},{"StartTime":72634.0,"Position":427.8085,"HyperDash":false},{"StartTime":72702.0,"Position":445.0,"HyperDash":false}]},{"StartTime":72839.0,"Objects":[{"StartTime":72839.0,"Position":486.0,"HyperDash":false},{"StartTime":72907.0,"Position":502.9824,"HyperDash":false}]},{"StartTime":72975.0,"Objects":[{"StartTime":72975.0,"Position":414.0,"HyperDash":false},{"StartTime":73043.0,"Position":430.9824,"HyperDash":false}]},{"StartTime":73111.0,"Objects":[{"StartTime":73111.0,"Position":344.0,"HyperDash":false}]},{"StartTime":75293.0,"Objects":[{"StartTime":75293.0,"Position":62.0,"HyperDash":false}]},{"StartTime":76930.0,"Objects":[{"StartTime":76930.0,"Position":403.0,"HyperDash":false},{"StartTime":77020.0,"Position":467.2785,"HyperDash":false},{"StartTime":77111.0,"Position":403.0,"HyperDash":false},{"StartTime":77202.0,"Position":467.2785,"HyperDash":false},{"StartTime":77293.0,"Position":403.0,"HyperDash":false},{"StartTime":77384.0,"Position":467.2785,"HyperDash":false}]},{"StartTime":77475.0,"Objects":[{"StartTime":77475.0,"Position":412.0,"HyperDash":false},{"StartTime":77565.0,"Position":439.85,"HyperDash":false},{"StartTime":77656.0,"Position":412.0,"HyperDash":false}]},{"StartTime":77748.0,"Objects":[{"StartTime":77748.0,"Position":320.0,"HyperDash":false},{"StartTime":77838.0,"Position":313.270081,"HyperDash":false},{"StartTime":77929.0,"Position":320.0,"HyperDash":false}]},{"StartTime":78021.0,"Objects":[{"StartTime":78021.0,"Position":248.0,"HyperDash":false},{"StartTime":78111.0,"Position":275.85,"HyperDash":false},{"StartTime":78202.0,"Position":248.0,"HyperDash":false}]},{"StartTime":78294.0,"Objects":[{"StartTime":78294.0,"Position":156.0,"HyperDash":false},{"StartTime":78384.0,"Position":149.56723,"HyperDash":false},{"StartTime":78475.0,"Position":156.0,"HyperDash":false}]},{"StartTime":78566.0,"Objects":[{"StartTime":78566.0,"Position":97.0,"HyperDash":false}]},{"StartTime":78657.0,"Objects":[{"StartTime":78657.0,"Position":89.0,"HyperDash":false},{"StartTime":78747.0,"Position":22.422142,"HyperDash":false}]},{"StartTime":78839.0,"Objects":[{"StartTime":78839.0,"Position":10.0,"HyperDash":false}]},{"StartTime":78930.0,"Objects":[{"StartTime":78930.0,"Position":52.0,"HyperDash":false}]},{"StartTime":79021.0,"Objects":[{"StartTime":79021.0,"Position":106.0,"HyperDash":false}]},{"StartTime":79111.0,"Objects":[{"StartTime":79111.0,"Position":154.0,"HyperDash":false},{"StartTime":79170.0,"Position":200.598,"HyperDash":false},{"StartTime":79229.0,"Position":235.269073,"HyperDash":false},{"StartTime":79288.0,"Position":279.5065,"HyperDash":false},{"StartTime":79383.0,"Position":258.247284,"HyperDash":false}]},{"StartTime":79657.0,"Objects":[{"StartTime":79657.0,"Position":258.0,"HyperDash":false},{"StartTime":79747.0,"Position":190.279266,"HyperDash":false},{"StartTime":79838.0,"Position":258.0,"HyperDash":false}]},{"StartTime":79930.0,"Objects":[{"StartTime":79930.0,"Position":226.0,"HyperDash":false},{"StartTime":80020.0,"Position":158.966843,"HyperDash":false},{"StartTime":80111.0,"Position":226.0,"HyperDash":false}]},{"StartTime":80202.0,"Objects":[{"StartTime":80202.0,"Position":287.0,"HyperDash":false},{"StartTime":80292.0,"Position":354.0113,"HyperDash":false},{"StartTime":80383.0,"Position":287.0,"HyperDash":false}]},{"StartTime":80475.0,"Objects":[{"StartTime":80475.0,"Position":293.0,"HyperDash":false},{"StartTime":80565.0,"Position":354.718628,"HyperDash":false},{"StartTime":80656.0,"Position":293.0,"HyperDash":false}]},{"StartTime":80748.0,"Objects":[{"StartTime":80748.0,"Position":218.0,"HyperDash":false}]},{"StartTime":80839.0,"Objects":[{"StartTime":80839.0,"Position":209.0,"HyperDash":false},{"StartTime":80929.0,"Position":195.476837,"HyperDash":false}]},{"StartTime":81021.0,"Objects":[{"StartTime":81021.0,"Position":256.0,"HyperDash":false}]},{"StartTime":81111.0,"Objects":[{"StartTime":81111.0,"Position":299.0,"HyperDash":false}]},{"StartTime":81202.0,"Objects":[{"StartTime":81202.0,"Position":352.0,"HyperDash":false}]},{"StartTime":81293.0,"Objects":[{"StartTime":81293.0,"Position":398.0,"HyperDash":false},{"StartTime":81352.0,"Position":388.6871,"HyperDash":false},{"StartTime":81411.0,"Position":437.698456,"HyperDash":false},{"StartTime":81470.0,"Position":430.344421,"HyperDash":false},{"StartTime":81565.0,"Position":462.164764,"HyperDash":false}]},{"StartTime":81839.0,"Objects":[{"StartTime":81839.0,"Position":462.0,"HyperDash":false},{"StartTime":81929.0,"Position":398.4922,"HyperDash":false},{"StartTime":82020.0,"Position":462.0,"HyperDash":false}]},{"StartTime":82111.0,"Objects":[{"StartTime":82111.0,"Position":347.0,"HyperDash":false},{"StartTime":82201.0,"Position":301.8704,"HyperDash":false},{"StartTime":82292.0,"Position":347.0,"HyperDash":false}]},{"StartTime":82384.0,"Objects":[{"StartTime":82384.0,"Position":368.0,"HyperDash":false},{"StartTime":82474.0,"Position":323.2633,"HyperDash":false},{"StartTime":82565.0,"Position":368.0,"HyperDash":false}]},{"StartTime":82657.0,"Objects":[{"StartTime":82657.0,"Position":238.0,"HyperDash":false},{"StartTime":82747.0,"Position":223.616516,"HyperDash":false},{"StartTime":82838.0,"Position":238.0,"HyperDash":false}]},{"StartTime":82930.0,"Objects":[{"StartTime":82930.0,"Position":135.0,"HyperDash":false}]},{"StartTime":83021.0,"Objects":[{"StartTime":83021.0,"Position":139.0,"HyperDash":false},{"StartTime":83111.0,"Position":190.412811,"HyperDash":false}]},{"StartTime":83202.0,"Objects":[{"StartTime":83202.0,"Position":41.0,"HyperDash":false}]},{"StartTime":83293.0,"Objects":[{"StartTime":83293.0,"Position":83.0,"HyperDash":false}]},{"StartTime":83384.0,"Objects":[{"StartTime":83384.0,"Position":103.0,"HyperDash":false}]},{"StartTime":83475.0,"Objects":[{"StartTime":83475.0,"Position":99.0,"HyperDash":false},{"StartTime":83534.0,"Position":103.780617,"HyperDash":false},{"StartTime":83593.0,"Position":126.401306,"HyperDash":false},{"StartTime":83652.0,"Position":141.544952,"HyperDash":false},{"StartTime":83747.0,"Position":219.928558,"HyperDash":false}]},{"StartTime":84021.0,"Objects":[{"StartTime":84021.0,"Position":219.0,"HyperDash":false},{"StartTime":84111.0,"Position":155.1237,"HyperDash":false},{"StartTime":84202.0,"Position":219.0,"HyperDash":false}]},{"StartTime":84293.0,"Objects":[{"StartTime":84293.0,"Position":237.0,"HyperDash":false},{"StartTime":84383.0,"Position":181.530167,"HyperDash":false},{"StartTime":84474.0,"Position":237.0,"HyperDash":false}]},{"StartTime":84566.0,"Objects":[{"StartTime":84566.0,"Position":291.0,"HyperDash":false},{"StartTime":84656.0,"Position":354.876282,"HyperDash":false},{"StartTime":84747.0,"Position":291.0,"HyperDash":false}]},{"StartTime":84839.0,"Objects":[{"StartTime":84839.0,"Position":273.0,"HyperDash":false},{"StartTime":84929.0,"Position":328.1262,"HyperDash":false},{"StartTime":85020.0,"Position":273.0,"HyperDash":false}]},{"StartTime":85111.0,"Objects":[{"StartTime":85111.0,"Position":210.0,"HyperDash":false}]},{"StartTime":85202.0,"Objects":[{"StartTime":85202.0,"Position":199.0,"HyperDash":false},{"StartTime":85292.0,"Position":175.375092,"HyperDash":false}]},{"StartTime":85384.0,"Objects":[{"StartTime":85384.0,"Position":227.0,"HyperDash":false}]},{"StartTime":85475.0,"Objects":[{"StartTime":85475.0,"Position":280.0,"HyperDash":false}]},{"StartTime":85566.0,"Objects":[{"StartTime":85566.0,"Position":326.0,"HyperDash":false}]},{"StartTime":85657.0,"Objects":[{"StartTime":85657.0,"Position":380.0,"HyperDash":false},{"StartTime":85708.0,"Position":410.039581,"HyperDash":false},{"StartTime":85759.0,"Position":454.079163,"HyperDash":false},{"StartTime":85810.0,"Position":496.148,"HyperDash":false},{"StartTime":85861.0,"Position":512.0,"HyperDash":false},{"StartTime":85945.0,"Position":452.8782,"HyperDash":false},{"StartTime":86066.0,"Position":380.0,"HyperDash":false}]},{"StartTime":86202.0,"Objects":[{"StartTime":86202.0,"Position":414.0,"HyperDash":false},{"StartTime":86270.0,"Position":406.751984,"HyperDash":false},{"StartTime":86338.0,"Position":414.0,"HyperDash":false},{"StartTime":86406.0,"Position":406.751984,"HyperDash":false}]},{"StartTime":86475.0,"Objects":[{"StartTime":86475.0,"Position":313.0,"HyperDash":false},{"StartTime":86543.0,"Position":320.248016,"HyperDash":false},{"StartTime":86611.0,"Position":313.0,"HyperDash":false},{"StartTime":86679.0,"Position":320.248016,"HyperDash":false}]},{"StartTime":86748.0,"Objects":[{"StartTime":86748.0,"Position":229.0,"HyperDash":false},{"StartTime":86816.0,"Position":236.248016,"HyperDash":false}]},{"StartTime":86884.0,"Objects":[{"StartTime":86884.0,"Position":140.0,"HyperDash":false},{"StartTime":86952.0,"Position":147.248016,"HyperDash":false}]},{"StartTime":87021.0,"Objects":[{"StartTime":87021.0,"Position":51.0,"HyperDash":false},{"StartTime":87089.0,"Position":58.2480125,"HyperDash":false},{"StartTime":87157.0,"Position":51.0,"HyperDash":false},{"StartTime":87225.0,"Position":58.2480125,"HyperDash":false}]},{"StartTime":87293.0,"Objects":[{"StartTime":87293.0,"Position":41.0,"HyperDash":false},{"StartTime":87361.0,"Position":0.0,"HyperDash":false},{"StartTime":87429.0,"Position":41.0,"HyperDash":false}]},{"StartTime":87566.0,"Objects":[{"StartTime":87566.0,"Position":111.0,"HyperDash":false}]},{"StartTime":87634.0,"Objects":[{"StartTime":87634.0,"Position":119.0,"HyperDash":false}]},{"StartTime":87702.0,"Objects":[{"StartTime":87702.0,"Position":127.0,"HyperDash":false}]},{"StartTime":87839.0,"Objects":[{"StartTime":87839.0,"Position":152.0,"HyperDash":false},{"StartTime":87907.0,"Position":110.122604,"HyperDash":false},{"StartTime":87975.0,"Position":152.0,"HyperDash":false}]},{"StartTime":88112.0,"Objects":[{"StartTime":88112.0,"Position":222.0,"HyperDash":false}]},{"StartTime":88180.0,"Objects":[{"StartTime":88180.0,"Position":230.0,"HyperDash":false}]},{"StartTime":88248.0,"Objects":[{"StartTime":88248.0,"Position":238.0,"HyperDash":false}]},{"StartTime":88384.0,"Objects":[{"StartTime":88384.0,"Position":295.0,"HyperDash":false},{"StartTime":88452.0,"Position":336.8774,"HyperDash":false},{"StartTime":88520.0,"Position":295.0,"HyperDash":false},{"StartTime":88588.0,"Position":336.8774,"HyperDash":false}]},{"StartTime":88657.0,"Objects":[{"StartTime":88657.0,"Position":334.0,"HyperDash":false},{"StartTime":88725.0,"Position":375.8774,"HyperDash":false},{"StartTime":88793.0,"Position":334.0,"HyperDash":false},{"StartTime":88861.0,"Position":375.8774,"HyperDash":false}]},{"StartTime":88930.0,"Objects":[{"StartTime":88930.0,"Position":464.0,"HyperDash":false},{"StartTime":88998.0,"Position":471.248016,"HyperDash":false}]},{"StartTime":89066.0,"Objects":[{"StartTime":89066.0,"Position":449.0,"HyperDash":false},{"StartTime":89134.0,"Position":456.248016,"HyperDash":false}]},{"StartTime":89202.0,"Objects":[{"StartTime":89202.0,"Position":434.0,"HyperDash":false},{"StartTime":89270.0,"Position":441.248016,"HyperDash":false},{"StartTime":89338.0,"Position":434.0,"HyperDash":false},{"StartTime":89406.0,"Position":441.248016,"HyperDash":false}]},{"StartTime":89475.0,"Objects":[{"StartTime":89475.0,"Position":362.0,"HyperDash":false}]},{"StartTime":89543.0,"Objects":[{"StartTime":89543.0,"Position":360.0,"HyperDash":false}]},{"StartTime":89611.0,"Objects":[{"StartTime":89611.0,"Position":358.0,"HyperDash":false}]},{"StartTime":89748.0,"Objects":[{"StartTime":89748.0,"Position":288.0,"HyperDash":false}]},{"StartTime":89816.0,"Objects":[{"StartTime":89816.0,"Position":286.0,"HyperDash":false}]},{"StartTime":89884.0,"Objects":[{"StartTime":89884.0,"Position":284.0,"HyperDash":false}]},{"StartTime":90021.0,"Objects":[{"StartTime":90021.0,"Position":201.0,"HyperDash":false}]},{"StartTime":90089.0,"Objects":[{"StartTime":90089.0,"Position":193.0,"HyperDash":false}]},{"StartTime":90158.0,"Objects":[{"StartTime":90158.0,"Position":185.0,"HyperDash":false},{"StartTime":90294.0,"Position":100.560234,"HyperDash":false}]},{"StartTime":90566.0,"Objects":[{"StartTime":90566.0,"Position":67.0,"HyperDash":false},{"StartTime":90634.0,"Position":25.1226,"HyperDash":false},{"StartTime":90702.0,"Position":67.0,"HyperDash":false},{"StartTime":90770.0,"Position":25.1226,"HyperDash":false}]},{"StartTime":90839.0,"Objects":[{"StartTime":90839.0,"Position":50.0,"HyperDash":false},{"StartTime":90907.0,"Position":8.122601,"HyperDash":false},{"StartTime":90975.0,"Position":50.0,"HyperDash":false},{"StartTime":91043.0,"Position":8.122601,"HyperDash":false}]},{"StartTime":91111.0,"Objects":[{"StartTime":91111.0,"Position":147.0,"HyperDash":false},{"StartTime":91179.0,"Position":139.751984,"HyperDash":false}]},{"StartTime":91247.0,"Objects":[{"StartTime":91247.0,"Position":236.0,"HyperDash":false},{"StartTime":91315.0,"Position":228.751984,"HyperDash":false}]},{"StartTime":91384.0,"Objects":[{"StartTime":91384.0,"Position":325.0,"HyperDash":false},{"StartTime":91452.0,"Position":317.751984,"HyperDash":false},{"StartTime":91520.0,"Position":325.0,"HyperDash":false},{"StartTime":91588.0,"Position":317.751984,"HyperDash":false}]},{"StartTime":91657.0,"Objects":[{"StartTime":91657.0,"Position":257.0,"HyperDash":false},{"StartTime":91725.0,"Position":249.751984,"HyperDash":false},{"StartTime":91793.0,"Position":257.0,"HyperDash":false}]},{"StartTime":91930.0,"Objects":[{"StartTime":91930.0,"Position":154.0,"HyperDash":false}]},{"StartTime":91998.0,"Objects":[{"StartTime":91998.0,"Position":156.0,"HyperDash":false}]},{"StartTime":92066.0,"Objects":[{"StartTime":92066.0,"Position":158.0,"HyperDash":false}]},{"StartTime":92203.0,"Objects":[{"StartTime":92203.0,"Position":231.0,"HyperDash":false},{"StartTime":92271.0,"Position":238.248016,"HyperDash":false},{"StartTime":92339.0,"Position":231.0,"HyperDash":false}]},{"StartTime":92476.0,"Objects":[{"StartTime":92476.0,"Position":327.0,"HyperDash":false}]},{"StartTime":92544.0,"Objects":[{"StartTime":92544.0,"Position":329.0,"HyperDash":false}]},{"StartTime":92612.0,"Objects":[{"StartTime":92612.0,"Position":331.0,"HyperDash":false}]},{"StartTime":92748.0,"Objects":[{"StartTime":92748.0,"Position":431.0,"HyperDash":false},{"StartTime":92816.0,"Position":423.751984,"HyperDash":false},{"StartTime":92884.0,"Position":431.0,"HyperDash":false},{"StartTime":92952.0,"Position":423.751984,"HyperDash":false}]},{"StartTime":93021.0,"Objects":[{"StartTime":93021.0,"Position":503.0,"HyperDash":false},{"StartTime":93089.0,"Position":495.047729,"HyperDash":false},{"StartTime":93157.0,"Position":503.0,"HyperDash":false},{"StartTime":93225.0,"Position":495.047729,"HyperDash":false}]},{"StartTime":93293.0,"Objects":[{"StartTime":93293.0,"Position":457.0,"HyperDash":false},{"StartTime":93361.0,"Position":498.8774,"HyperDash":false}]},{"StartTime":93429.0,"Objects":[{"StartTime":93429.0,"Position":371.0,"HyperDash":false},{"StartTime":93497.0,"Position":412.8774,"HyperDash":false}]},{"StartTime":93566.0,"Objects":[{"StartTime":93566.0,"Position":286.0,"HyperDash":false},{"StartTime":93634.0,"Position":327.8774,"HyperDash":false},{"StartTime":93702.0,"Position":286.0,"HyperDash":false},{"StartTime":93770.0,"Position":327.8774,"HyperDash":false}]},{"StartTime":93839.0,"Objects":[{"StartTime":93839.0,"Position":195.0,"HyperDash":false}]},{"StartTime":93907.0,"Objects":[{"StartTime":93907.0,"Position":193.0,"HyperDash":false}]},{"StartTime":93975.0,"Objects":[{"StartTime":93975.0,"Position":191.0,"HyperDash":false}]},{"StartTime":94112.0,"Objects":[{"StartTime":94112.0,"Position":118.0,"HyperDash":false}]},{"StartTime":94180.0,"Objects":[{"StartTime":94180.0,"Position":120.0,"HyperDash":false}]},{"StartTime":94248.0,"Objects":[{"StartTime":94248.0,"Position":122.0,"HyperDash":false}]},{"StartTime":94385.0,"Objects":[{"StartTime":94385.0,"Position":145.0,"HyperDash":false}]},{"StartTime":94453.0,"Objects":[{"StartTime":94453.0,"Position":143.0,"HyperDash":false}]},{"StartTime":94522.0,"Objects":[{"StartTime":94522.0,"Position":141.0,"HyperDash":false},{"StartTime":94658.0,"Position":150.743042,"HyperDash":false}]},{"StartTime":94930.0,"Objects":[{"StartTime":94930.0,"Position":48.0,"HyperDash":false}]},{"StartTime":94998.0,"Objects":[{"StartTime":94998.0,"Position":41.0,"HyperDash":false}]},{"StartTime":95066.0,"Objects":[{"StartTime":95066.0,"Position":34.0,"HyperDash":false},{"StartTime":95134.0,"Position":75.8533,"HyperDash":false},{"StartTime":95202.0,"Position":34.0,"HyperDash":false},{"StartTime":95270.0,"Position":75.8533,"HyperDash":false}]},{"StartTime":95339.0,"Objects":[{"StartTime":95339.0,"Position":77.0,"HyperDash":false},{"StartTime":95407.0,"Position":118.8533,"HyperDash":false}]},{"StartTime":95475.0,"Objects":[{"StartTime":95475.0,"Position":37.0,"HyperDash":false},{"StartTime":95543.0,"Position":78.8533,"HyperDash":false},{"StartTime":95611.0,"Position":37.0,"HyperDash":false},{"StartTime":95679.0,"Position":78.8533,"HyperDash":false},{"StartTime":95747.0,"Position":37.0,"HyperDash":false},{"StartTime":95815.0,"Position":78.8533,"HyperDash":false},{"StartTime":95884.0,"Position":37.0,"HyperDash":false},{"StartTime":95952.0,"Position":78.8533,"HyperDash":false},{"StartTime":96020.0,"Position":37.0,"HyperDash":false}]},{"StartTime":104748.0,"Objects":[{"StartTime":104748.0,"Position":285.0,"HyperDash":false},{"StartTime":104884.0,"Position":196.3763,"HyperDash":false}]},{"StartTime":105020.0,"Objects":[{"StartTime":105020.0,"Position":372.0,"HyperDash":false},{"StartTime":105156.0,"Position":460.110962,"HyperDash":false}]},{"StartTime":105293.0,"Objects":[{"StartTime":105293.0,"Position":483.0,"HyperDash":false},{"StartTime":105429.0,"Position":506.840118,"HyperDash":false}]},{"StartTime":105566.0,"Objects":[{"StartTime":105566.0,"Position":381.0,"HyperDash":false},{"StartTime":105702.0,"Position":403.863068,"HyperDash":false}]},{"StartTime":105839.0,"Objects":[{"StartTime":105839.0,"Position":336.0,"HyperDash":false},{"StartTime":105975.0,"Position":236.595367,"HyperDash":false}]},{"StartTime":106111.0,"Objects":[{"StartTime":106111.0,"Position":190.0,"HyperDash":false}]},{"StartTime":106248.0,"Objects":[{"StartTime":106248.0,"Position":190.0,"HyperDash":false}]},{"StartTime":106384.0,"Objects":[{"StartTime":106384.0,"Position":66.0,"HyperDash":false},{"StartTime":106520.0,"Position":50.64902,"HyperDash":false}]},{"StartTime":106657.0,"Objects":[{"StartTime":106657.0,"Position":160.0,"HyperDash":false},{"StartTime":106793.0,"Position":256.028931,"HyperDash":false}]},{"StartTime":106929.0,"Objects":[{"StartTime":106929.0,"Position":419.0,"HyperDash":false},{"StartTime":107065.0,"Position":411.1793,"HyperDash":false}]},{"StartTime":107202.0,"Objects":[{"StartTime":107202.0,"Position":350.0,"HyperDash":false},{"StartTime":107338.0,"Position":437.371735,"HyperDash":false}]},{"StartTime":107475.0,"Objects":[{"StartTime":107475.0,"Position":500.0,"HyperDash":false}]},{"StartTime":107611.0,"Objects":[{"StartTime":107611.0,"Position":387.0,"HyperDash":false}]},{"StartTime":107679.0,"Objects":[{"StartTime":107679.0,"Position":387.0,"HyperDash":false}]},{"StartTime":107748.0,"Objects":[{"StartTime":107748.0,"Position":387.0,"HyperDash":false},{"StartTime":107884.0,"Position":286.101257,"HyperDash":false}]},{"StartTime":108020.0,"Objects":[{"StartTime":108020.0,"Position":126.0,"HyperDash":false}]},{"StartTime":108156.0,"Objects":[{"StartTime":108156.0,"Position":139.0,"HyperDash":false}]},{"StartTime":108293.0,"Objects":[{"StartTime":108293.0,"Position":213.0,"HyperDash":false}]},{"StartTime":108429.0,"Objects":[{"StartTime":108429.0,"Position":301.0,"HyperDash":false}]},{"StartTime":108566.0,"Objects":[{"StartTime":108566.0,"Position":267.0,"HyperDash":false},{"StartTime":108625.0,"Position":232.172058,"HyperDash":false},{"StartTime":108684.0,"Position":191.248871,"HyperDash":false},{"StartTime":108743.0,"Position":129.18779,"HyperDash":false},{"StartTime":108838.0,"Position":67.07219,"HyperDash":false}]},{"StartTime":108975.0,"Objects":[{"StartTime":108975.0,"Position":55.0,"HyperDash":false}]},{"StartTime":109043.0,"Objects":[{"StartTime":109043.0,"Position":44.0,"HyperDash":false}]},{"StartTime":109111.0,"Objects":[{"StartTime":109111.0,"Position":35.0,"HyperDash":false},{"StartTime":109247.0,"Position":134.610657,"HyperDash":false}]},{"StartTime":109384.0,"Objects":[{"StartTime":109384.0,"Position":279.0,"HyperDash":false},{"StartTime":109520.0,"Position":378.779877,"HyperDash":false}]},{"StartTime":109657.0,"Objects":[{"StartTime":109657.0,"Position":474.0,"HyperDash":false},{"StartTime":109793.0,"Position":414.009949,"HyperDash":false}]},{"StartTime":109929.0,"Objects":[{"StartTime":109929.0,"Position":357.0,"HyperDash":false},{"StartTime":110065.0,"Position":448.250183,"HyperDash":false}]},{"StartTime":110202.0,"Objects":[{"StartTime":110202.0,"Position":499.0,"HyperDash":false},{"StartTime":110338.0,"Position":409.002472,"HyperDash":false}]},{"StartTime":110475.0,"Objects":[{"StartTime":110475.0,"Position":280.0,"HyperDash":false}]},{"StartTime":110611.0,"Objects":[{"StartTime":110611.0,"Position":280.0,"HyperDash":false}]},{"StartTime":110748.0,"Objects":[{"StartTime":110748.0,"Position":357.0,"HyperDash":false},{"StartTime":110884.0,"Position":344.34845,"HyperDash":false}]},{"StartTime":111020.0,"Objects":[{"StartTime":111020.0,"Position":209.0,"HyperDash":false},{"StartTime":111156.0,"Position":196.34845,"HyperDash":false}]},{"StartTime":111293.0,"Objects":[{"StartTime":111293.0,"Position":65.0,"HyperDash":false},{"StartTime":111429.0,"Position":148.923523,"HyperDash":false}]},{"StartTime":111566.0,"Objects":[{"StartTime":111566.0,"Position":80.0,"HyperDash":false},{"StartTime":111702.0,"Position":78.81489,"HyperDash":false}]},{"StartTime":111839.0,"Objects":[{"StartTime":111839.0,"Position":148.0,"HyperDash":false}]},{"StartTime":111975.0,"Objects":[{"StartTime":111975.0,"Position":269.0,"HyperDash":false}]},{"StartTime":112043.0,"Objects":[{"StartTime":112043.0,"Position":269.0,"HyperDash":false}]},{"StartTime":112111.0,"Objects":[{"StartTime":112111.0,"Position":269.0,"HyperDash":false},{"StartTime":112247.0,"Position":369.6811,"HyperDash":false}]},{"StartTime":112384.0,"Objects":[{"StartTime":112384.0,"Position":369.0,"HyperDash":false}]},{"StartTime":112452.0,"Objects":[{"StartTime":112452.0,"Position":410.0,"HyperDash":false}]},{"StartTime":112520.0,"Objects":[{"StartTime":112520.0,"Position":450.0,"HyperDash":false}]},{"StartTime":112588.0,"Objects":[{"StartTime":112588.0,"Position":478.0,"HyperDash":false}]},{"StartTime":112656.0,"Objects":[{"StartTime":112656.0,"Position":487.0,"HyperDash":false}]},{"StartTime":112793.0,"Objects":[{"StartTime":112793.0,"Position":413.0,"HyperDash":false}]},{"StartTime":112861.0,"Objects":[{"StartTime":112861.0,"Position":371.0,"HyperDash":false}]},{"StartTime":112929.0,"Objects":[{"StartTime":112929.0,"Position":329.0,"HyperDash":false}]},{"StartTime":113066.0,"Objects":[{"StartTime":113066.0,"Position":259.0,"HyperDash":false},{"StartTime":113134.0,"Position":208.630585,"HyperDash":false},{"StartTime":113202.0,"Position":259.0,"HyperDash":false},{"StartTime":113270.0,"Position":208.630585,"HyperDash":false},{"StartTime":113338.0,"Position":259.0,"HyperDash":false},{"StartTime":113406.0,"Position":208.630585,"HyperDash":false},{"StartTime":113475.0,"Position":259.0,"HyperDash":false}]},{"StartTime":117839.0,"Objects":[{"StartTime":117839.0,"Position":352.0,"HyperDash":false},{"StartTime":117907.0,"Position":367.7046,"HyperDash":false},{"StartTime":117975.0,"Position":377.8776,"HyperDash":false},{"StartTime":118043.0,"Position":353.339722,"HyperDash":false},{"StartTime":118111.0,"Position":341.5588,"HyperDash":false},{"StartTime":118170.0,"Position":357.394043,"HyperDash":false},{"StartTime":118229.0,"Position":351.709229,"HyperDash":false},{"StartTime":118288.0,"Position":368.7251,"HyperDash":false},{"StartTime":118384.0,"Position":352.0,"HyperDash":false}]},{"StartTime":118521.0,"Objects":[{"StartTime":118521.0,"Position":435.0,"HyperDash":false}]},{"StartTime":118657.0,"Objects":[{"StartTime":118657.0,"Position":435.0,"HyperDash":false},{"StartTime":118716.0,"Position":424.944855,"HyperDash":false},{"StartTime":118775.0,"Position":373.775269,"HyperDash":false},{"StartTime":118834.0,"Position":349.8368,"HyperDash":false},{"StartTime":118929.0,"Position":316.293427,"HyperDash":false}]},{"StartTime":119203.0,"Objects":[{"StartTime":119203.0,"Position":353.0,"HyperDash":false}]},{"StartTime":119339.0,"Objects":[{"StartTime":119339.0,"Position":353.0,"HyperDash":false},{"StartTime":119398.0,"Position":364.062378,"HyperDash":false},{"StartTime":119457.0,"Position":397.124756,"HyperDash":false},{"StartTime":119516.0,"Position":439.1871,"HyperDash":false},{"StartTime":119611.0,"Position":486.982452,"HyperDash":true}]},{"StartTime":119748.0,"Objects":[{"StartTime":119748.0,"Position":273.0,"HyperDash":false}]},{"StartTime":120021.0,"Objects":[{"StartTime":120021.0,"Position":90.0,"HyperDash":false},{"StartTime":120089.0,"Position":108.62011,"HyperDash":false},{"StartTime":120157.0,"Position":95.3407,"HyperDash":false},{"StartTime":120225.0,"Position":76.87965,"HyperDash":false},{"StartTime":120293.0,"Position":40.5374374,"HyperDash":false},{"StartTime":120352.0,"Position":60.58837,"HyperDash":false},{"StartTime":120411.0,"Position":96.3111343,"HyperDash":false},{"StartTime":120470.0,"Position":80.33538,"HyperDash":false},{"StartTime":120566.0,"Position":90.0,"HyperDash":false}]},{"StartTime":120703.0,"Objects":[{"StartTime":120703.0,"Position":128.0,"HyperDash":false}]},{"StartTime":120839.0,"Objects":[{"StartTime":120839.0,"Position":128.0,"HyperDash":false},{"StartTime":120975.0,"Position":68.21395,"HyperDash":false}]},{"StartTime":121112.0,"Objects":[{"StartTime":121112.0,"Position":14.0,"HyperDash":false},{"StartTime":121180.0,"Position":34.0660934,"HyperDash":false},{"StartTime":121248.0,"Position":24.13219,"HyperDash":false},{"StartTime":121384.0,"Position":14.0,"HyperDash":false}]},{"StartTime":121521.0,"Objects":[{"StartTime":121521.0,"Position":68.0,"HyperDash":false},{"StartTime":121580.0,"Position":75.36682,"HyperDash":false},{"StartTime":121639.0,"Position":102.431969,"HyperDash":false},{"StartTime":121698.0,"Position":145.603821,"HyperDash":false},{"StartTime":121793.0,"Position":188.698318,"HyperDash":false}]},{"StartTime":121930.0,"Objects":[{"StartTime":121930.0,"Position":267.0,"HyperDash":false}]},{"StartTime":122202.0,"Objects":[{"StartTime":122202.0,"Position":267.0,"HyperDash":false},{"StartTime":122261.0,"Position":230.862274,"HyperDash":false},{"StartTime":122320.0,"Position":245.4149,"HyperDash":false},{"StartTime":122379.0,"Position":216.465454,"HyperDash":false},{"StartTime":122474.0,"Position":252.568588,"HyperDash":false}]},{"StartTime":122611.0,"Objects":[{"StartTime":122611.0,"Position":252.0,"HyperDash":false},{"StartTime":122670.0,"Position":237.2295,"HyperDash":false},{"StartTime":122729.0,"Position":198.886948,"HyperDash":false},{"StartTime":122788.0,"Position":171.432022,"HyperDash":false},{"StartTime":122883.0,"Position":120.432274,"HyperDash":false}]},{"StartTime":123021.0,"Objects":[{"StartTime":123021.0,"Position":58.0,"HyperDash":false},{"StartTime":123157.0,"Position":78.36528,"HyperDash":false}]},{"StartTime":123293.0,"Objects":[{"StartTime":123293.0,"Position":6.0,"HyperDash":false},{"StartTime":123429.0,"Position":88.6607361,"HyperDash":false}]},{"StartTime":123566.0,"Objects":[{"StartTime":123566.0,"Position":156.0,"HyperDash":false},{"StartTime":123702.0,"Position":224.188141,"HyperDash":false}]},{"StartTime":123839.0,"Objects":[{"StartTime":123839.0,"Position":349.0,"HyperDash":false}]},{"StartTime":123975.0,"Objects":[{"StartTime":123975.0,"Position":375.0,"HyperDash":false}]},{"StartTime":124111.0,"Objects":[{"StartTime":124111.0,"Position":456.0,"HyperDash":false},{"StartTime":124195.0,"Position":453.9612,"HyperDash":false},{"StartTime":124315.0,"Position":470.4772,"HyperDash":false}]},{"StartTime":124384.0,"Objects":[{"StartTime":124384.0,"Position":498.0,"HyperDash":false},{"StartTime":124443.0,"Position":452.638641,"HyperDash":false},{"StartTime":124502.0,"Position":424.858124,"HyperDash":false},{"StartTime":124561.0,"Position":402.805267,"HyperDash":false},{"StartTime":124656.0,"Position":400.806458,"HyperDash":false}]},{"StartTime":124793.0,"Objects":[{"StartTime":124793.0,"Position":400.0,"HyperDash":false}]},{"StartTime":124930.0,"Objects":[{"StartTime":124930.0,"Position":320.0,"HyperDash":false},{"StartTime":125020.0,"Position":265.6432,"HyperDash":false},{"StartTime":125111.0,"Position":320.0,"HyperDash":false}]},{"StartTime":125202.0,"Objects":[{"StartTime":125202.0,"Position":226.0,"HyperDash":false},{"StartTime":125292.0,"Position":184.534943,"HyperDash":false},{"StartTime":125383.0,"Position":226.0,"HyperDash":false}]},{"StartTime":125475.0,"Objects":[{"StartTime":125475.0,"Position":165.0,"HyperDash":false},{"StartTime":125565.0,"Position":148.008514,"HyperDash":false},{"StartTime":125656.0,"Position":165.0,"HyperDash":false}]},{"StartTime":125748.0,"Objects":[{"StartTime":125748.0,"Position":64.0,"HyperDash":false},{"StartTime":125838.0,"Position":76.2514648,"HyperDash":false},{"StartTime":125929.0,"Position":64.0,"HyperDash":false}]},{"StartTime":126021.0,"Objects":[{"StartTime":126021.0,"Position":98.0,"HyperDash":false},{"StartTime":126111.0,"Position":42.3349533,"HyperDash":false},{"StartTime":126202.0,"Position":98.0,"HyperDash":false}]},{"StartTime":126293.0,"Objects":[{"StartTime":126293.0,"Position":168.0,"HyperDash":false}]},{"StartTime":126384.0,"Objects":[{"StartTime":126384.0,"Position":176.0,"HyperDash":false},{"StartTime":126474.0,"Position":231.724014,"HyperDash":false}]},{"StartTime":126566.0,"Objects":[{"StartTime":126566.0,"Position":294.0,"HyperDash":false},{"StartTime":126625.0,"Position":304.065277,"HyperDash":false},{"StartTime":126684.0,"Position":289.130554,"HyperDash":false},{"StartTime":126743.0,"Position":270.195831,"HyperDash":false},{"StartTime":126838.0,"Position":275.86026,"HyperDash":false}]},{"StartTime":126975.0,"Objects":[{"StartTime":126975.0,"Position":269.0,"HyperDash":false},{"StartTime":127034.0,"Position":238.030014,"HyperDash":false},{"StartTime":127093.0,"Position":206.798035,"HyperDash":false},{"StartTime":127152.0,"Position":183.373825,"HyperDash":false},{"StartTime":127247.0,"Position":128.954315,"HyperDash":false}]},{"StartTime":127384.0,"Objects":[{"StartTime":127384.0,"Position":128.0,"HyperDash":false},{"StartTime":127443.0,"Position":104.877335,"HyperDash":false},{"StartTime":127502.0,"Position":66.8338852,"HyperDash":false},{"StartTime":127561.0,"Position":81.92623,"HyperDash":false},{"StartTime":127656.0,"Position":101.414925,"HyperDash":false}]},{"StartTime":127930.0,"Objects":[{"StartTime":127930.0,"Position":102.0,"HyperDash":false},{"StartTime":128066.0,"Position":185.98468,"HyperDash":false}]},{"StartTime":128202.0,"Objects":[{"StartTime":128202.0,"Position":268.0,"HyperDash":false},{"StartTime":128338.0,"Position":276.750061,"HyperDash":false}]},{"StartTime":128475.0,"Objects":[{"StartTime":128475.0,"Position":220.0,"HyperDash":false}]},{"StartTime":128611.0,"Objects":[{"StartTime":128611.0,"Position":246.0,"HyperDash":false}]},{"StartTime":128748.0,"Objects":[{"StartTime":128748.0,"Position":272.0,"HyperDash":false},{"StartTime":128838.0,"Position":259.471741,"HyperDash":false},{"StartTime":128929.0,"Position":272.0,"HyperDash":false}]},{"StartTime":129021.0,"Objects":[{"StartTime":129021.0,"Position":341.0,"HyperDash":false},{"StartTime":129111.0,"Position":356.802368,"HyperDash":false},{"StartTime":129202.0,"Position":341.0,"HyperDash":false}]},{"StartTime":129293.0,"Objects":[{"StartTime":129293.0,"Position":374.0,"HyperDash":false},{"StartTime":129383.0,"Position":414.349274,"HyperDash":false},{"StartTime":129474.0,"Position":374.0,"HyperDash":false}]},{"StartTime":129566.0,"Objects":[{"StartTime":129566.0,"Position":363.0,"HyperDash":false},{"StartTime":129656.0,"Position":417.3568,"HyperDash":false},{"StartTime":129747.0,"Position":363.0,"HyperDash":false}]},{"StartTime":129839.0,"Objects":[{"StartTime":129839.0,"Position":399.0,"HyperDash":false}]},{"StartTime":129930.0,"Objects":[{"StartTime":129930.0,"Position":363.0,"HyperDash":false}]},{"StartTime":130021.0,"Objects":[{"StartTime":130021.0,"Position":319.0,"HyperDash":false}]},{"StartTime":130111.0,"Objects":[{"StartTime":130111.0,"Position":274.0,"HyperDash":false}]},{"StartTime":130202.0,"Objects":[{"StartTime":130202.0,"Position":233.0,"HyperDash":false}]},{"StartTime":130293.0,"Objects":[{"StartTime":130293.0,"Position":188.0,"HyperDash":false}]},{"StartTime":130384.0,"Objects":[{"StartTime":130384.0,"Position":144.0,"HyperDash":false},{"StartTime":130443.0,"Position":136.688782,"HyperDash":false},{"StartTime":130502.0,"Position":118.278656,"HyperDash":false},{"StartTime":130561.0,"Position":153.723068,"HyperDash":false},{"StartTime":130656.0,"Position":190.433411,"HyperDash":false}]},{"StartTime":130793.0,"Objects":[{"StartTime":130793.0,"Position":282.0,"HyperDash":false}]},{"StartTime":130861.0,"Objects":[{"StartTime":130861.0,"Position":282.0,"HyperDash":false}]},{"StartTime":130930.0,"Objects":[{"StartTime":130930.0,"Position":282.0,"HyperDash":false},{"StartTime":130989.0,"Position":284.273651,"HyperDash":false},{"StartTime":131048.0,"Position":293.547333,"HyperDash":false},{"StartTime":131107.0,"Position":301.820984,"HyperDash":false},{"StartTime":131202.0,"Position":264.598328,"HyperDash":false}]},{"StartTime":131339.0,"Objects":[{"StartTime":131339.0,"Position":264.0,"HyperDash":false},{"StartTime":131398.0,"Position":248.803833,"HyperDash":false},{"StartTime":131457.0,"Position":204.483932,"HyperDash":false},{"StartTime":131516.0,"Position":177.141281,"HyperDash":false},{"StartTime":131611.0,"Position":107.439949,"HyperDash":false}]},{"StartTime":131748.0,"Objects":[{"StartTime":131748.0,"Position":107.0,"HyperDash":false},{"StartTime":131884.0,"Position":136.185135,"HyperDash":false}]},{"StartTime":132021.0,"Objects":[{"StartTime":132021.0,"Position":88.0,"HyperDash":false},{"StartTime":132080.0,"Position":51.873764,"HyperDash":false},{"StartTime":132139.0,"Position":55.46241,"HyperDash":false},{"StartTime":132198.0,"Position":72.92975,"HyperDash":false},{"StartTime":132293.0,"Position":100.14119,"HyperDash":false}]},{"StartTime":132430.0,"Objects":[{"StartTime":132430.0,"Position":100.0,"HyperDash":false},{"StartTime":132489.0,"Position":75.71915,"HyperDash":false},{"StartTime":132548.0,"Position":18.4710484,"HyperDash":false},{"StartTime":132607.0,"Position":27.815239,"HyperDash":false},{"StartTime":132702.0,"Position":100.250526,"HyperDash":false}]},{"StartTime":132839.0,"Objects":[{"StartTime":132839.0,"Position":100.0,"HyperDash":false},{"StartTime":132975.0,"Position":179.952286,"HyperDash":false}]},{"StartTime":133111.0,"Objects":[{"StartTime":133111.0,"Position":246.0,"HyperDash":false},{"StartTime":133247.0,"Position":327.362976,"HyperDash":false}]},{"StartTime":133384.0,"Objects":[{"StartTime":133384.0,"Position":390.0,"HyperDash":false}]},{"StartTime":133521.0,"Objects":[{"StartTime":133521.0,"Position":472.0,"HyperDash":false}]},{"StartTime":133657.0,"Objects":[{"StartTime":133657.0,"Position":491.0,"HyperDash":false}]},{"StartTime":133793.0,"Objects":[{"StartTime":133793.0,"Position":439.0,"HyperDash":false}]},{"StartTime":133930.0,"Objects":[{"StartTime":133930.0,"Position":420.0,"HyperDash":false}]},{"StartTime":134066.0,"Objects":[{"StartTime":134066.0,"Position":461.0,"HyperDash":false}]},{"StartTime":134202.0,"Objects":[{"StartTime":134202.0,"Position":448.0,"HyperDash":false}]},{"StartTime":134339.0,"Objects":[{"StartTime":134339.0,"Position":381.0,"HyperDash":false}]},{"StartTime":134475.0,"Objects":[{"StartTime":134475.0,"Position":296.0,"HyperDash":false}]},{"StartTime":134611.0,"Objects":[{"StartTime":134611.0,"Position":214.0,"HyperDash":false}]},{"StartTime":134748.0,"Objects":[{"StartTime":134748.0,"Position":164.0,"HyperDash":false},{"StartTime":134884.0,"Position":83.35544,"HyperDash":false}]},{"StartTime":135021.0,"Objects":[{"StartTime":135021.0,"Position":19.0,"HyperDash":false},{"StartTime":135157.0,"Position":99.57382,"HyperDash":false}]},{"StartTime":135293.0,"Objects":[{"StartTime":135293.0,"Position":25.0,"HyperDash":false},{"StartTime":135352.0,"Position":41.8271523,"HyperDash":false},{"StartTime":135411.0,"Position":95.72167,"HyperDash":false},{"StartTime":135470.0,"Position":108.490532,"HyperDash":false},{"StartTime":135565.0,"Position":179.471237,"HyperDash":false}]},{"StartTime":135702.0,"Objects":[{"StartTime":135702.0,"Position":252.0,"HyperDash":false}]},{"StartTime":135839.0,"Objects":[{"StartTime":135839.0,"Position":252.0,"HyperDash":false},{"StartTime":135975.0,"Position":241.337753,"HyperDash":false}]},{"StartTime":136111.0,"Objects":[{"StartTime":136111.0,"Position":175.0,"HyperDash":false},{"StartTime":136247.0,"Position":185.662247,"HyperDash":false}]},{"StartTime":136384.0,"Objects":[{"StartTime":136384.0,"Position":138.0,"HyperDash":false}]},{"StartTime":136521.0,"Objects":[{"StartTime":136521.0,"Position":194.0,"HyperDash":false}]},{"StartTime":136657.0,"Objects":[{"StartTime":136657.0,"Position":278.0,"HyperDash":false}]},{"StartTime":136793.0,"Objects":[{"StartTime":136793.0,"Position":360.0,"HyperDash":false}]},{"StartTime":136930.0,"Objects":[{"StartTime":136930.0,"Position":407.0,"HyperDash":false}]},{"StartTime":137066.0,"Objects":[{"StartTime":137066.0,"Position":447.0,"HyperDash":false}]},{"StartTime":137202.0,"Objects":[{"StartTime":137202.0,"Position":367.0,"HyperDash":false}]},{"StartTime":137338.0,"Objects":[{"StartTime":137338.0,"Position":407.0,"HyperDash":false}]},{"StartTime":137475.0,"Objects":[{"StartTime":137475.0,"Position":280.0,"HyperDash":false}]},{"StartTime":137611.0,"Objects":[{"StartTime":137611.0,"Position":194.0,"HyperDash":false}]},{"StartTime":137748.0,"Objects":[{"StartTime":137748.0,"Position":207.0,"HyperDash":false}]},{"StartTime":137884.0,"Objects":[{"StartTime":137884.0,"Position":293.0,"HyperDash":false}]},{"StartTime":138021.0,"Objects":[{"StartTime":138021.0,"Position":198.0,"HyperDash":false},{"StartTime":138080.0,"Position":186.6536,"HyperDash":false},{"StartTime":138139.0,"Position":165.980225,"HyperDash":false},{"StartTime":138198.0,"Position":108.5129,"HyperDash":false},{"StartTime":138293.0,"Position":60.4876747,"HyperDash":false}]},{"StartTime":138566.0,"Objects":[{"StartTime":138566.0,"Position":20.0,"HyperDash":false}]},{"StartTime":138657.0,"Objects":[{"StartTime":138657.0,"Position":67.0,"HyperDash":false}]},{"StartTime":138748.0,"Objects":[{"StartTime":138748.0,"Position":122.0,"HyperDash":false}]},{"StartTime":138839.0,"Objects":[{"StartTime":138839.0,"Position":178.0,"HyperDash":false}]},{"StartTime":138930.0,"Objects":[{"StartTime":138930.0,"Position":221.0,"HyperDash":false}]},{"StartTime":139021.0,"Objects":[{"StartTime":139021.0,"Position":244.0,"HyperDash":false}]},{"StartTime":139111.0,"Objects":[{"StartTime":139111.0,"Position":248.0,"HyperDash":false},{"StartTime":139201.0,"Position":233.246613,"HyperDash":false},{"StartTime":139292.0,"Position":248.0,"HyperDash":false}]},{"StartTime":139384.0,"Objects":[{"StartTime":139384.0,"Position":327.0,"HyperDash":false},{"StartTime":139468.0,"Position":372.042328,"HyperDash":false},{"StartTime":139588.0,"Position":453.388519,"HyperDash":false}]},{"StartTime":139657.0,"Objects":[{"StartTime":139657.0,"Position":489.0,"HyperDash":false},{"StartTime":139716.0,"Position":500.969269,"HyperDash":false},{"StartTime":139775.0,"Position":484.081482,"HyperDash":false},{"StartTime":139834.0,"Position":452.1301,"HyperDash":false},{"StartTime":139929.0,"Position":387.50766,"HyperDash":false}]},{"StartTime":140066.0,"Objects":[{"StartTime":140066.0,"Position":311.0,"HyperDash":false},{"StartTime":140125.0,"Position":300.9206,"HyperDash":false},{"StartTime":140184.0,"Position":285.442963,"HyperDash":false},{"StartTime":140243.0,"Position":239.63205,"HyperDash":false},{"StartTime":140338.0,"Position":189.411591,"HyperDash":false}]},{"StartTime":140475.0,"Objects":[{"StartTime":140475.0,"Position":118.0,"HyperDash":false},{"StartTime":140611.0,"Position":39.25152,"HyperDash":false}]},{"StartTime":140748.0,"Objects":[{"StartTime":140748.0,"Position":13.0,"HyperDash":false}]},{"StartTime":140884.0,"Objects":[{"StartTime":140884.0,"Position":93.0,"HyperDash":false}]},{"StartTime":141021.0,"Objects":[{"StartTime":141021.0,"Position":30.0,"HyperDash":false}]},{"StartTime":141157.0,"Objects":[{"StartTime":141157.0,"Position":91.0,"HyperDash":false},{"StartTime":141216.0,"Position":120.467026,"HyperDash":false},{"StartTime":141275.0,"Position":182.934052,"HyperDash":false},{"StartTime":141334.0,"Position":189.6184,"HyperDash":false},{"StartTime":141429.0,"Position":253.543488,"HyperDash":false}]},{"StartTime":141566.0,"Objects":[{"StartTime":141566.0,"Position":253.0,"HyperDash":false},{"StartTime":141702.0,"Position":252.173325,"HyperDash":false}]},{"StartTime":141839.0,"Objects":[{"StartTime":141839.0,"Position":302.0,"HyperDash":false},{"StartTime":141898.0,"Position":271.164337,"HyperDash":false},{"StartTime":141957.0,"Position":265.036316,"HyperDash":false},{"StartTime":142016.0,"Position":225.854385,"HyperDash":false},{"StartTime":142111.0,"Position":258.0618,"HyperDash":false}]},{"StartTime":142248.0,"Objects":[{"StartTime":142248.0,"Position":329.0,"HyperDash":false}]},{"StartTime":142384.0,"Objects":[{"StartTime":142384.0,"Position":401.0,"HyperDash":false},{"StartTime":142474.0,"Position":456.101929,"HyperDash":false},{"StartTime":142565.0,"Position":401.0,"HyperDash":false}]},{"StartTime":142657.0,"Objects":[{"StartTime":142657.0,"Position":430.0,"HyperDash":false},{"StartTime":142747.0,"Position":485.101929,"HyperDash":false},{"StartTime":142838.0,"Position":430.0,"HyperDash":false}]},{"StartTime":142930.0,"Objects":[{"StartTime":142930.0,"Position":474.0,"HyperDash":false}]},{"StartTime":143020.0,"Objects":[{"StartTime":143020.0,"Position":433.0,"HyperDash":false}]},{"StartTime":143111.0,"Objects":[{"StartTime":143111.0,"Position":389.0,"HyperDash":false}]},{"StartTime":143202.0,"Objects":[{"StartTime":143202.0,"Position":356.0,"HyperDash":false}]},{"StartTime":143293.0,"Objects":[{"StartTime":143293.0,"Position":347.0,"HyperDash":false}]},{"StartTime":143384.0,"Objects":[{"StartTime":143384.0,"Position":363.0,"HyperDash":false}]},{"StartTime":143475.0,"Objects":[{"StartTime":143475.0,"Position":403.0,"HyperDash":false},{"StartTime":143565.0,"Position":458.0956,"HyperDash":false}]},{"StartTime":143657.0,"Objects":[{"StartTime":143657.0,"Position":315.0,"HyperDash":false}]},{"StartTime":143748.0,"Objects":[{"StartTime":143748.0,"Position":303.0,"HyperDash":false},{"StartTime":143838.0,"Position":247.904388,"HyperDash":false}]},{"StartTime":143930.0,"Objects":[{"StartTime":143930.0,"Position":152.0,"HyperDash":false}]},{"StartTime":144021.0,"Objects":[{"StartTime":144021.0,"Position":140.0,"HyperDash":false},{"StartTime":144080.0,"Position":123.703255,"HyperDash":false},{"StartTime":144139.0,"Position":84.1756058,"HyperDash":false},{"StartTime":144198.0,"Position":55.1062,"HyperDash":false},{"StartTime":144293.0,"Position":34.28666,"HyperDash":false}]},{"StartTime":144430.0,"Objects":[{"StartTime":144430.0,"Position":34.0,"HyperDash":false},{"StartTime":144489.0,"Position":39.2494774,"HyperDash":false},{"StartTime":144548.0,"Position":42.4028931,"HyperDash":false},{"StartTime":144607.0,"Position":83.5909,"HyperDash":false},{"StartTime":144702.0,"Position":124.747566,"HyperDash":false}]},{"StartTime":144839.0,"Objects":[{"StartTime":144839.0,"Position":151.0,"HyperDash":false}]},{"StartTime":144975.0,"Objects":[{"StartTime":144975.0,"Position":151.0,"HyperDash":false}]},{"StartTime":145111.0,"Objects":[{"StartTime":145111.0,"Position":91.0,"HyperDash":false},{"StartTime":145247.0,"Position":6.988411,"HyperDash":false}]},{"StartTime":145384.0,"Objects":[{"StartTime":145384.0,"Position":124.0,"HyperDash":false},{"StartTime":145520.0,"Position":208.0116,"HyperDash":false}]},{"StartTime":145657.0,"Objects":[{"StartTime":145657.0,"Position":284.0,"HyperDash":false}]},{"StartTime":145793.0,"Objects":[{"StartTime":145793.0,"Position":330.0,"HyperDash":false}]},{"StartTime":145930.0,"Objects":[{"StartTime":145930.0,"Position":412.0,"HyperDash":false}]},{"StartTime":146066.0,"Objects":[{"StartTime":146066.0,"Position":494.0,"HyperDash":false}]},{"StartTime":146202.0,"Objects":[{"StartTime":146202.0,"Position":422.0,"HyperDash":false},{"StartTime":146261.0,"Position":374.5958,"HyperDash":false},{"StartTime":146320.0,"Position":340.97818,"HyperDash":false},{"StartTime":146379.0,"Position":321.774567,"HyperDash":false},{"StartTime":146474.0,"Position":273.590363,"HyperDash":false}]},{"StartTime":146611.0,"Objects":[{"StartTime":146611.0,"Position":273.0,"HyperDash":false}]},{"StartTime":146748.0,"Objects":[{"StartTime":146748.0,"Position":242.0,"HyperDash":false},{"StartTime":146884.0,"Position":179.186676,"HyperDash":false}]},{"StartTime":147021.0,"Objects":[{"StartTime":147021.0,"Position":33.0,"HyperDash":false},{"StartTime":147157.0,"Position":95.18677,"HyperDash":false}]},{"StartTime":147293.0,"Objects":[{"StartTime":147293.0,"Position":120.0,"HyperDash":false},{"StartTime":147383.0,"Position":174.276825,"HyperDash":false},{"StartTime":147474.0,"Position":120.0,"HyperDash":false}]},{"StartTime":147566.0,"Objects":[{"StartTime":147566.0,"Position":83.0,"HyperDash":false},{"StartTime":147702.0,"Position":0.0,"HyperDash":false}]},{"StartTime":147839.0,"Objects":[{"StartTime":147839.0,"Position":175.0,"HyperDash":false}]},{"StartTime":147975.0,"Objects":[{"StartTime":147975.0,"Position":256.0,"HyperDash":false}]},{"StartTime":148111.0,"Objects":[{"StartTime":148111.0,"Position":195.0,"HyperDash":false}]},{"StartTime":148248.0,"Objects":[{"StartTime":148248.0,"Position":300.0,"HyperDash":false}]},{"StartTime":148316.0,"Objects":[{"StartTime":148316.0,"Position":300.0,"HyperDash":false}]},{"StartTime":148384.0,"Objects":[{"StartTime":148384.0,"Position":300.0,"HyperDash":false},{"StartTime":148452.0,"Position":241.037445,"HyperDash":false},{"StartTime":148520.0,"Position":208.872025,"HyperDash":false},{"StartTime":148588.0,"Position":167.7589,"HyperDash":false},{"StartTime":148656.0,"Position":112.687119,"HyperDash":false},{"StartTime":148724.0,"Position":64.53349,"HyperDash":false},{"StartTime":148792.0,"Position":45.59254,"HyperDash":false},{"StartTime":148860.0,"Position":57.11486,"HyperDash":false},{"StartTime":148929.0,"Position":103.807991,"HyperDash":false},{"StartTime":148997.0,"Position":140.8881,"HyperDash":false},{"StartTime":149065.0,"Position":203.1637,"HyperDash":false},{"StartTime":149133.0,"Position":175.605743,"HyperDash":false},{"StartTime":149202.0,"Position":157.292023,"HyperDash":false},{"StartTime":149261.0,"Position":179.837387,"HyperDash":false},{"StartTime":149320.0,"Position":205.033386,"HyperDash":false},{"StartTime":149379.0,"Position":230.499939,"HyperDash":false},{"StartTime":149474.0,"Position":318.759552,"HyperDash":false}]},{"StartTime":149611.0,"Objects":[{"StartTime":149611.0,"Position":416.0,"HyperDash":false},{"StartTime":149679.0,"Position":459.112885,"HyperDash":false},{"StartTime":149747.0,"Position":483.116028,"HyperDash":false},{"StartTime":149883.0,"Position":416.0,"HyperDash":false}]},{"StartTime":150021.0,"Objects":[{"StartTime":150021.0,"Position":318.0,"HyperDash":false}]},{"StartTime":150157.0,"Objects":[{"StartTime":150157.0,"Position":318.0,"HyperDash":false}]},{"StartTime":150293.0,"Objects":[{"StartTime":150293.0,"Position":395.0,"HyperDash":false},{"StartTime":150429.0,"Position":388.707062,"HyperDash":false}]},{"StartTime":150566.0,"Objects":[{"StartTime":150566.0,"Position":502.0,"HyperDash":false}]},{"StartTime":150702.0,"Objects":[{"StartTime":150702.0,"Position":388.0,"HyperDash":false}]},{"StartTime":150839.0,"Objects":[{"StartTime":150839.0,"Position":388.0,"HyperDash":false}]},{"StartTime":150975.0,"Objects":[{"StartTime":150975.0,"Position":354.0,"HyperDash":false},{"StartTime":151043.0,"Position":308.8965,"HyperDash":false},{"StartTime":151111.0,"Position":257.082336,"HyperDash":false},{"StartTime":151179.0,"Position":200.233047,"HyperDash":false},{"StartTime":151247.0,"Position":179.148392,"HyperDash":false},{"StartTime":151315.0,"Position":121.330429,"HyperDash":false},{"StartTime":151383.0,"Position":91.3323,"HyperDash":false},{"StartTime":151451.0,"Position":105.334328,"HyperDash":false},{"StartTime":151520.0,"Position":163.270889,"HyperDash":false},{"StartTime":151588.0,"Position":222.52066,"HyperDash":false},{"StartTime":151656.0,"Position":236.967346,"HyperDash":false},{"StartTime":151724.0,"Position":197.664108,"HyperDash":false},{"StartTime":151793.0,"Position":170.657684,"HyperDash":false},{"StartTime":151929.0,"Position":122.385834,"HyperDash":false}]},{"StartTime":152066.0,"Objects":[{"StartTime":152066.0,"Position":37.0,"HyperDash":false},{"StartTime":152134.0,"Position":38.1493454,"HyperDash":false},{"StartTime":152202.0,"Position":25.2637386,"HyperDash":false},{"StartTime":152338.0,"Position":37.0,"HyperDash":false}]},{"StartTime":152475.0,"Objects":[{"StartTime":152475.0,"Position":73.0,"HyperDash":false},{"StartTime":152611.0,"Position":125.983765,"HyperDash":false}]},{"StartTime":152748.0,"Objects":[{"StartTime":152748.0,"Position":211.0,"HyperDash":false},{"StartTime":152807.0,"Position":232.132385,"HyperDash":false},{"StartTime":152866.0,"Position":265.062622,"HyperDash":false},{"StartTime":152925.0,"Position":293.685852,"HyperDash":false},{"StartTime":153020.0,"Position":353.2395,"HyperDash":false}]},{"StartTime":153157.0,"Objects":[{"StartTime":153157.0,"Position":499.0,"HyperDash":false},{"StartTime":153216.0,"Position":449.435883,"HyperDash":false},{"StartTime":153275.0,"Position":424.8718,"HyperDash":false},{"StartTime":153334.0,"Position":399.307678,"HyperDash":false},{"StartTime":153429.0,"Position":330.433258,"HyperDash":false}]},{"StartTime":153566.0,"Objects":[{"StartTime":153566.0,"Position":279.0,"HyperDash":false},{"StartTime":153702.0,"Position":299.1634,"HyperDash":false}]},{"StartTime":153839.0,"Objects":[{"StartTime":153839.0,"Position":236.0,"HyperDash":false}]},{"StartTime":153975.0,"Objects":[{"StartTime":153975.0,"Position":299.0,"HyperDash":false}]},{"StartTime":154111.0,"Objects":[{"StartTime":154111.0,"Position":375.0,"HyperDash":false}]},{"StartTime":154248.0,"Objects":[{"StartTime":154248.0,"Position":448.0,"HyperDash":false},{"StartTime":154316.0,"Position":448.704071,"HyperDash":false},{"StartTime":154384.0,"Position":459.51297,"HyperDash":false},{"StartTime":154452.0,"Position":410.957947,"HyperDash":false},{"StartTime":154520.0,"Position":385.861572,"HyperDash":false},{"StartTime":154570.0,"Position":337.801727,"HyperDash":false},{"StartTime":154657.0,"Position":317.3621,"HyperDash":false}]},{"StartTime":154930.0,"Objects":[{"StartTime":154930.0,"Position":41.0,"HyperDash":false}]},{"StartTime":155020.0,"Objects":[{"StartTime":155020.0,"Position":28.0,"HyperDash":false}]},{"StartTime":155111.0,"Objects":[{"StartTime":155111.0,"Position":40.0,"HyperDash":false}]},{"StartTime":155202.0,"Objects":[{"StartTime":155202.0,"Position":72.0,"HyperDash":false}]},{"StartTime":155293.0,"Objects":[{"StartTime":155293.0,"Position":115.0,"HyperDash":false}]},{"StartTime":155384.0,"Objects":[{"StartTime":155384.0,"Position":158.0,"HyperDash":false}]},{"StartTime":155475.0,"Objects":[{"StartTime":155475.0,"Position":198.0,"HyperDash":false}]},{"StartTime":155565.0,"Objects":[{"StartTime":155565.0,"Position":254.0,"HyperDash":false}]},{"StartTime":155656.0,"Objects":[{"StartTime":155656.0,"Position":309.0,"HyperDash":false}]},{"StartTime":155747.0,"Objects":[{"StartTime":155747.0,"Position":356.0,"HyperDash":false}]},{"StartTime":155838.0,"Objects":[{"StartTime":155838.0,"Position":392.0,"HyperDash":false}]},{"StartTime":155929.0,"Objects":[{"StartTime":155929.0,"Position":411.0,"HyperDash":false}]},{"StartTime":156021.0,"Objects":[{"StartTime":156021.0,"Position":411.0,"HyperDash":false},{"StartTime":156089.0,"Position":395.962219,"HyperDash":false},{"StartTime":156157.0,"Position":339.266174,"HyperDash":false},{"StartTime":156225.0,"Position":303.955,"HyperDash":false},{"StartTime":156293.0,"Position":318.589355,"HyperDash":false},{"StartTime":156361.0,"Position":368.6844,"HyperDash":false},{"StartTime":156429.0,"Position":387.036835,"HyperDash":false},{"StartTime":156497.0,"Position":393.426025,"HyperDash":false},{"StartTime":156566.0,"Position":373.163116,"HyperDash":false},{"StartTime":156625.0,"Position":341.85965,"HyperDash":false},{"StartTime":156684.0,"Position":283.9819,"HyperDash":false},{"StartTime":156743.0,"Position":246.838165,"HyperDash":false},{"StartTime":156839.0,"Position":212.31163,"HyperDash":false}]},{"StartTime":156907.0,"Objects":[{"StartTime":156907.0,"Position":213.0,"HyperDash":false}]},{"StartTime":156975.0,"Objects":[{"StartTime":156975.0,"Position":214.0,"HyperDash":false}]},{"StartTime":157043.0,"Objects":[{"StartTime":157043.0,"Position":215.0,"HyperDash":false}]},{"StartTime":157111.0,"Objects":[{"StartTime":157111.0,"Position":216.0,"HyperDash":false},{"StartTime":157247.0,"Position":114.869156,"HyperDash":false}]},{"StartTime":157384.0,"Objects":[{"StartTime":157384.0,"Position":3.0,"HyperDash":false},{"StartTime":157520.0,"Position":104.052589,"HyperDash":false}]},{"StartTime":157657.0,"Objects":[{"StartTime":157657.0,"Position":124.0,"HyperDash":false},{"StartTime":157793.0,"Position":225.052582,"HyperDash":false}]},{"StartTime":157930.0,"Objects":[{"StartTime":157930.0,"Position":13.0,"HyperDash":false},{"StartTime":158066.0,"Position":114.052589,"HyperDash":false}]},{"StartTime":158202.0,"Objects":[{"StartTime":158202.0,"Position":134.0,"HyperDash":false},{"StartTime":158338.0,"Position":235.052582,"HyperDash":false}]},{"StartTime":158475.0,"Objects":[{"StartTime":158475.0,"Position":23.0,"HyperDash":false},{"StartTime":158611.0,"Position":124.052589,"HyperDash":false}]},{"StartTime":158748.0,"Objects":[{"StartTime":158748.0,"Position":144.0,"HyperDash":false},{"StartTime":158884.0,"Position":245.052582,"HyperDash":false}]},{"StartTime":159021.0,"Objects":[{"StartTime":159021.0,"Position":33.0,"HyperDash":false},{"StartTime":159157.0,"Position":134.052582,"HyperDash":false}]},{"StartTime":159293.0,"Objects":[{"StartTime":159293.0,"Position":154.0,"HyperDash":false},{"StartTime":159429.0,"Position":255.052582,"HyperDash":false}]},{"StartTime":159566.0,"Objects":[{"StartTime":159566.0,"Position":43.0,"HyperDash":false},{"StartTime":159702.0,"Position":144.052582,"HyperDash":false}]},{"StartTime":159839.0,"Objects":[{"StartTime":159839.0,"Position":164.0,"HyperDash":false},{"StartTime":159975.0,"Position":265.052582,"HyperDash":false}]},{"StartTime":160112.0,"Objects":[{"StartTime":160112.0,"Position":53.0,"HyperDash":false},{"StartTime":160248.0,"Position":154.052582,"HyperDash":false}]},{"StartTime":160384.0,"Objects":[{"StartTime":160384.0,"Position":174.0,"HyperDash":false},{"StartTime":160520.0,"Position":275.052582,"HyperDash":false}]},{"StartTime":160657.0,"Objects":[{"StartTime":160657.0,"Position":63.0,"HyperDash":false},{"StartTime":160793.0,"Position":164.052582,"HyperDash":false}]},{"StartTime":160930.0,"Objects":[{"StartTime":160930.0,"Position":184.0,"HyperDash":false},{"StartTime":161066.0,"Position":285.052582,"HyperDash":true}]},{"StartTime":161202.0,"Objects":[{"StartTime":161202.0,"Position":73.0,"HyperDash":false},{"StartTime":161338.0,"Position":174.052582,"HyperDash":false}]},{"StartTime":161475.0,"Objects":[{"StartTime":161475.0,"Position":300.0,"HyperDash":false},{"StartTime":161611.0,"Position":401.130859,"HyperDash":false}]},{"StartTime":161748.0,"Objects":[{"StartTime":161748.0,"Position":512.0,"HyperDash":false},{"StartTime":161884.0,"Position":410.818481,"HyperDash":false}]},{"StartTime":162021.0,"Objects":[{"StartTime":162021.0,"Position":391.0,"HyperDash":false},{"StartTime":162157.0,"Position":289.818481,"HyperDash":false}]},{"StartTime":162294.0,"Objects":[{"StartTime":162294.0,"Position":502.0,"HyperDash":false},{"StartTime":162430.0,"Position":400.818481,"HyperDash":false}]},{"StartTime":162566.0,"Objects":[{"StartTime":162566.0,"Position":381.0,"HyperDash":false},{"StartTime":162702.0,"Position":279.818481,"HyperDash":false}]},{"StartTime":162839.0,"Objects":[{"StartTime":162839.0,"Position":492.0,"HyperDash":false},{"StartTime":162975.0,"Position":390.818481,"HyperDash":false}]},{"StartTime":163112.0,"Objects":[{"StartTime":163112.0,"Position":371.0,"HyperDash":false},{"StartTime":163248.0,"Position":269.818481,"HyperDash":false}]},{"StartTime":163385.0,"Objects":[{"StartTime":163385.0,"Position":482.0,"HyperDash":false},{"StartTime":163521.0,"Position":380.818481,"HyperDash":false}]},{"StartTime":163657.0,"Objects":[{"StartTime":163657.0,"Position":361.0,"HyperDash":false},{"StartTime":163793.0,"Position":259.818481,"HyperDash":false}]},{"StartTime":163930.0,"Objects":[{"StartTime":163930.0,"Position":472.0,"HyperDash":false},{"StartTime":164066.0,"Position":370.818481,"HyperDash":false}]},{"StartTime":164203.0,"Objects":[{"StartTime":164203.0,"Position":351.0,"HyperDash":false},{"StartTime":164339.0,"Position":249.818481,"HyperDash":false}]},{"StartTime":164476.0,"Objects":[{"StartTime":164476.0,"Position":462.0,"HyperDash":false},{"StartTime":164612.0,"Position":360.818481,"HyperDash":false}]},{"StartTime":164748.0,"Objects":[{"StartTime":164748.0,"Position":341.0,"HyperDash":false},{"StartTime":164884.0,"Position":239.818481,"HyperDash":false}]},{"StartTime":165021.0,"Objects":[{"StartTime":165021.0,"Position":452.0,"HyperDash":false},{"StartTime":165157.0,"Position":350.818481,"HyperDash":false}]},{"StartTime":165294.0,"Objects":[{"StartTime":165294.0,"Position":331.0,"HyperDash":false},{"StartTime":165430.0,"Position":229.818481,"HyperDash":false}]},{"StartTime":165566.0,"Objects":[{"StartTime":165566.0,"Position":396.0,"HyperDash":false}]},{"StartTime":165702.0,"Objects":[{"StartTime":165702.0,"Position":216.0,"HyperDash":false}]},{"StartTime":165771.0,"Objects":[{"StartTime":165771.0,"Position":216.0,"HyperDash":false}]},{"StartTime":165839.0,"Objects":[{"StartTime":165839.0,"Position":216.0,"HyperDash":false},{"StartTime":165975.0,"Position":229.287262,"HyperDash":false}]},{"StartTime":166112.0,"Objects":[{"StartTime":166112.0,"Position":103.0,"HyperDash":false},{"StartTime":166248.0,"Position":89.1300354,"HyperDash":false}]},{"StartTime":166385.0,"Objects":[{"StartTime":166385.0,"Position":218.0,"HyperDash":false},{"StartTime":166521.0,"Position":204.130035,"HyperDash":false}]},{"StartTime":166658.0,"Objects":[{"StartTime":166658.0,"Position":91.0,"HyperDash":false},{"StartTime":166794.0,"Position":77.1300354,"HyperDash":false}]},{"StartTime":166930.0,"Objects":[{"StartTime":166930.0,"Position":206.0,"HyperDash":false},{"StartTime":167066.0,"Position":192.130035,"HyperDash":false}]},{"StartTime":167203.0,"Objects":[{"StartTime":167203.0,"Position":79.0,"HyperDash":false},{"StartTime":167339.0,"Position":65.1300354,"HyperDash":false}]},{"StartTime":167476.0,"Objects":[{"StartTime":167476.0,"Position":194.0,"HyperDash":false},{"StartTime":167612.0,"Position":180.130035,"HyperDash":false}]},{"StartTime":167749.0,"Objects":[{"StartTime":167749.0,"Position":67.0,"HyperDash":false},{"StartTime":167885.0,"Position":53.1300354,"HyperDash":false}]},{"StartTime":168021.0,"Objects":[{"StartTime":168021.0,"Position":182.0,"HyperDash":false},{"StartTime":168157.0,"Position":168.130035,"HyperDash":false}]},{"StartTime":168294.0,"Objects":[{"StartTime":168294.0,"Position":55.0,"HyperDash":false},{"StartTime":168430.0,"Position":41.1300354,"HyperDash":false}]},{"StartTime":168567.0,"Objects":[{"StartTime":168567.0,"Position":170.0,"HyperDash":false},{"StartTime":168703.0,"Position":156.130035,"HyperDash":false}]},{"StartTime":168840.0,"Objects":[{"StartTime":168840.0,"Position":43.0,"HyperDash":false},{"StartTime":168976.0,"Position":29.1300373,"HyperDash":false}]},{"StartTime":169112.0,"Objects":[{"StartTime":169112.0,"Position":158.0,"HyperDash":false},{"StartTime":169248.0,"Position":144.130035,"HyperDash":false}]},{"StartTime":169385.0,"Objects":[{"StartTime":169385.0,"Position":31.0,"HyperDash":false},{"StartTime":169521.0,"Position":17.1300373,"HyperDash":false}]},{"StartTime":169658.0,"Objects":[{"StartTime":169658.0,"Position":146.0,"HyperDash":false},{"StartTime":169794.0,"Position":132.130035,"HyperDash":false}]},{"StartTime":169930.0,"Objects":[{"StartTime":169930.0,"Position":19.0,"HyperDash":false},{"StartTime":170066.0,"Position":5.13003731,"HyperDash":true}]},{"StartTime":170202.0,"Objects":[{"StartTime":170202.0,"Position":280.0,"HyperDash":false},{"StartTime":170338.0,"Position":266.712738,"HyperDash":false}]},{"StartTime":170475.0,"Objects":[{"StartTime":170475.0,"Position":393.0,"HyperDash":false},{"StartTime":170611.0,"Position":406.869965,"HyperDash":false}]},{"StartTime":170748.0,"Objects":[{"StartTime":170748.0,"Position":278.0,"HyperDash":false},{"StartTime":170884.0,"Position":291.869965,"HyperDash":false}]},{"StartTime":171021.0,"Objects":[{"StartTime":171021.0,"Position":405.0,"HyperDash":false},{"StartTime":171157.0,"Position":418.869965,"HyperDash":false}]},{"StartTime":171293.0,"Objects":[{"StartTime":171293.0,"Position":290.0,"HyperDash":false},{"StartTime":171429.0,"Position":303.869965,"HyperDash":false}]},{"StartTime":171566.0,"Objects":[{"StartTime":171566.0,"Position":417.0,"HyperDash":false},{"StartTime":171702.0,"Position":430.869965,"HyperDash":false}]},{"StartTime":171839.0,"Objects":[{"StartTime":171839.0,"Position":302.0,"HyperDash":false},{"StartTime":171975.0,"Position":315.869965,"HyperDash":false}]},{"StartTime":172112.0,"Objects":[{"StartTime":172112.0,"Position":429.0,"HyperDash":false},{"StartTime":172248.0,"Position":442.869965,"HyperDash":false}]},{"StartTime":172384.0,"Objects":[{"StartTime":172384.0,"Position":512.0,"HyperDash":false}]},{"StartTime":173278.0,"Objects":[{"StartTime":173278.0,"Position":512.0,"HyperDash":false},{"StartTime":173333.0,"Position":461.544647,"HyperDash":false},{"StartTime":173389.0,"Position":440.884155,"HyperDash":false},{"StartTime":173444.0,"Position":394.892883,"HyperDash":false},{"StartTime":173500.0,"Position":373.234924,"HyperDash":false},{"StartTime":173611.0,"Position":383.5925,"HyperDash":false}]},{"StartTime":173722.0,"Objects":[{"StartTime":173722.0,"Position":327.0,"HyperDash":false},{"StartTime":173796.0,"Position":271.28595,"HyperDash":false},{"StartTime":173870.0,"Position":327.0,"HyperDash":false},{"StartTime":173944.0,"Position":271.28595,"HyperDash":false},{"StartTime":174018.0,"Position":327.0,"HyperDash":false},{"StartTime":174092.0,"Position":271.28595,"HyperDash":false}]},{"StartTime":174166.0,"Objects":[{"StartTime":174166.0,"Position":178.0,"HyperDash":false},{"StartTime":174240.0,"Position":233.714035,"HyperDash":false},{"StartTime":174314.0,"Position":178.0,"HyperDash":false},{"StartTime":174388.0,"Position":233.714035,"HyperDash":false},{"StartTime":174462.0,"Position":178.0,"HyperDash":false},{"StartTime":174536.0,"Position":233.714035,"HyperDash":false}]},{"StartTime":174611.0,"Objects":[{"StartTime":174611.0,"Position":92.0,"HyperDash":false},{"StartTime":174685.0,"Position":36.285965,"HyperDash":false},{"StartTime":174759.0,"Position":92.0,"HyperDash":false}]},{"StartTime":174833.0,"Objects":[{"StartTime":174833.0,"Position":99.0,"HyperDash":false},{"StartTime":174907.0,"Position":43.285965,"HyperDash":false},{"StartTime":174981.0,"Position":99.0,"HyperDash":false}]},{"StartTime":175055.0,"Objects":[{"StartTime":175055.0,"Position":179.0,"HyperDash":false},{"StartTime":175166.0,"Position":178.317825,"HyperDash":false}]},{"StartTime":175278.0,"Objects":[{"StartTime":175278.0,"Position":84.0,"HyperDash":false}]},{"StartTime":175389.0,"Objects":[{"StartTime":175389.0,"Position":84.0,"HyperDash":false}]},{"StartTime":175500.0,"Objects":[{"StartTime":175500.0,"Position":84.0,"HyperDash":false},{"StartTime":175611.0,"Position":0.0,"HyperDash":false}]},{"StartTime":175722.0,"Objects":[{"StartTime":175722.0,"Position":176.0,"HyperDash":false},{"StartTime":175833.0,"Position":260.304535,"HyperDash":false}]},{"StartTime":175944.0,"Objects":[{"StartTime":175944.0,"Position":378.0,"HyperDash":false}]},{"StartTime":176055.0,"Objects":[{"StartTime":176055.0,"Position":359.0,"HyperDash":false}]},{"StartTime":176166.0,"Objects":[{"StartTime":176166.0,"Position":380.0,"HyperDash":false}]},{"StartTime":176278.0,"Objects":[{"StartTime":176278.0,"Position":437.0,"HyperDash":false}]},{"StartTime":176389.0,"Objects":[{"StartTime":176389.0,"Position":504.0,"HyperDash":false},{"StartTime":176500.0,"Position":500.1198,"HyperDash":false}]},{"StartTime":176611.0,"Objects":[{"StartTime":176611.0,"Position":464.0,"HyperDash":false},{"StartTime":176722.0,"Position":395.9769,"HyperDash":false}]},{"StartTime":176833.0,"Objects":[{"StartTime":176833.0,"Position":223.0,"HyperDash":false},{"StartTime":176888.0,"Position":222.049377,"HyperDash":false},{"StartTime":176944.0,"Position":265.432465,"HyperDash":false},{"StartTime":176999.0,"Position":288.0414,"HyperDash":false},{"StartTime":177055.0,"Position":311.180634,"HyperDash":false},{"StartTime":177166.0,"Position":320.170959,"HyperDash":false}]},{"StartTime":177278.0,"Objects":[{"StartTime":177278.0,"Position":314.0,"HyperDash":false}]},{"StartTime":177389.0,"Objects":[{"StartTime":177389.0,"Position":393.0,"HyperDash":false}]},{"StartTime":177500.0,"Objects":[{"StartTime":177500.0,"Position":393.0,"HyperDash":false},{"StartTime":177611.0,"Position":476.258362,"HyperDash":true}]},{"StartTime":177722.0,"Objects":[{"StartTime":177722.0,"Position":238.0,"HyperDash":false}]},{"StartTime":177833.0,"Objects":[{"StartTime":177833.0,"Position":238.0,"HyperDash":false}]},{"StartTime":177944.0,"Objects":[{"StartTime":177944.0,"Position":238.0,"HyperDash":false},{"StartTime":178055.0,"Position":154.741638,"HyperDash":false}]},{"StartTime":178166.0,"Objects":[{"StartTime":178166.0,"Position":51.0,"HyperDash":false},{"StartTime":178277.0,"Position":38.63025,"HyperDash":false}]},{"StartTime":178389.0,"Objects":[{"StartTime":178389.0,"Position":136.0,"HyperDash":false},{"StartTime":178500.0,"Position":149.298233,"HyperDash":false}]},{"StartTime":178611.0,"Objects":[{"StartTime":178611.0,"Position":311.0,"HyperDash":false},{"StartTime":178685.0,"Position":365.846741,"HyperDash":false},{"StartTime":178759.0,"Position":311.0,"HyperDash":false}]},{"StartTime":178833.0,"Objects":[{"StartTime":178833.0,"Position":361.0,"HyperDash":false},{"StartTime":178907.0,"Position":415.431976,"HyperDash":false},{"StartTime":178981.0,"Position":361.0,"HyperDash":false}]},{"StartTime":179055.0,"Objects":[{"StartTime":179055.0,"Position":368.0,"HyperDash":false},{"StartTime":179129.0,"Position":407.3476,"HyperDash":false},{"StartTime":179203.0,"Position":368.0,"HyperDash":false}]},{"StartTime":179278.0,"Objects":[{"StartTime":179278.0,"Position":330.0,"HyperDash":false},{"StartTime":179352.0,"Position":344.074615,"HyperDash":false},{"StartTime":179426.0,"Position":330.0,"HyperDash":false}]},{"StartTime":179500.0,"Objects":[{"StartTime":179500.0,"Position":442.0,"HyperDash":false}]},{"StartTime":179574.0,"Objects":[{"StartTime":179574.0,"Position":442.0,"HyperDash":false}]},{"StartTime":179648.0,"Objects":[{"StartTime":179648.0,"Position":442.0,"HyperDash":false}]},{"StartTime":179722.0,"Objects":[{"StartTime":179722.0,"Position":442.0,"HyperDash":false},{"StartTime":179796.0,"Position":427.7541,"HyperDash":false},{"StartTime":179870.0,"Position":442.0,"HyperDash":false}]},{"StartTime":179944.0,"Objects":[{"StartTime":179944.0,"Position":488.0,"HyperDash":false},{"StartTime":180037.0,"Position":417.2783,"HyperDash":false},{"StartTime":180166.0,"Position":350.759735,"HyperDash":false}]},{"StartTime":180389.0,"Objects":[{"StartTime":180389.0,"Position":114.0,"HyperDash":false},{"StartTime":180500.0,"Position":42.9713974,"HyperDash":false}]},{"StartTime":180611.0,"Objects":[{"StartTime":180611.0,"Position":0.0,"HyperDash":false},{"StartTime":180722.0,"Position":70.18777,"HyperDash":false}]},{"StartTime":180833.0,"Objects":[{"StartTime":180833.0,"Position":124.0,"HyperDash":false},{"StartTime":180944.0,"Position":110.17556,"HyperDash":false}]},{"StartTime":181055.0,"Objects":[{"StartTime":181055.0,"Position":201.0,"HyperDash":false},{"StartTime":181166.0,"Position":214.824432,"HyperDash":false}]},{"StartTime":181278.0,"Objects":[{"StartTime":181278.0,"Position":350.0,"HyperDash":false},{"StartTime":181389.0,"Position":414.6709,"HyperDash":false}]},{"StartTime":181500.0,"Objects":[{"StartTime":181500.0,"Position":497.0,"HyperDash":false},{"StartTime":181555.0,"Position":495.534149,"HyperDash":false},{"StartTime":181611.0,"Position":512.0,"HyperDash":false},{"StartTime":181722.0,"Position":497.0,"HyperDash":false}]},{"StartTime":181833.0,"Objects":[{"StartTime":181833.0,"Position":414.0,"HyperDash":false}]},{"StartTime":181944.0,"Objects":[{"StartTime":181944.0,"Position":414.0,"HyperDash":false},{"StartTime":182055.0,"Position":339.763519,"HyperDash":false}]},{"StartTime":182166.0,"Objects":[{"StartTime":182166.0,"Position":254.0,"HyperDash":false}]},{"StartTime":182278.0,"Objects":[{"StartTime":182278.0,"Position":186.0,"HyperDash":false}]},{"StartTime":182389.0,"Objects":[{"StartTime":182389.0,"Position":123.0,"HyperDash":false}]},{"StartTime":182500.0,"Objects":[{"StartTime":182500.0,"Position":89.0,"HyperDash":false}]},{"StartTime":182611.0,"Objects":[{"StartTime":182611.0,"Position":101.0,"HyperDash":false},{"StartTime":182666.0,"Position":108.090492,"HyperDash":false},{"StartTime":182722.0,"Position":101.513573,"HyperDash":false},{"StartTime":182777.0,"Position":95.97452,"HyperDash":false},{"StartTime":182833.0,"Position":76.8446,"HyperDash":false},{"StartTime":182944.0,"Position":79.74396,"HyperDash":false}]},{"StartTime":183055.0,"Objects":[{"StartTime":183055.0,"Position":0.0,"HyperDash":false},{"StartTime":183166.0,"Position":73.6922455,"HyperDash":false}]},{"StartTime":183278.0,"Objects":[{"StartTime":183278.0,"Position":176.0,"HyperDash":false},{"StartTime":183389.0,"Position":242.907639,"HyperDash":false}]},{"StartTime":183500.0,"Objects":[{"StartTime":183500.0,"Position":353.0,"HyperDash":false},{"StartTime":183611.0,"Position":361.0935,"HyperDash":false}]},{"StartTime":183722.0,"Objects":[{"StartTime":183722.0,"Position":473.0,"HyperDash":false},{"StartTime":183833.0,"Position":464.9065,"HyperDash":false}]},{"StartTime":183944.0,"Objects":[{"StartTime":183944.0,"Position":447.0,"HyperDash":false}]},{"StartTime":184055.0,"Objects":[{"StartTime":184055.0,"Position":447.0,"HyperDash":false}]},{"StartTime":184166.0,"Objects":[{"StartTime":184166.0,"Position":447.0,"HyperDash":false}]},{"StartTime":184277.0,"Objects":[{"StartTime":184277.0,"Position":463.0,"HyperDash":false}]},{"StartTime":184388.0,"Objects":[{"StartTime":184388.0,"Position":487.0,"HyperDash":false},{"StartTime":184499.0,"Position":478.9065,"HyperDash":false}]},{"StartTime":184611.0,"Objects":[{"StartTime":184611.0,"Position":344.0,"HyperDash":false},{"StartTime":184722.0,"Position":335.9065,"HyperDash":false}]},{"StartTime":184833.0,"Objects":[{"StartTime":184833.0,"Position":233.0,"HyperDash":false},{"StartTime":184944.0,"Position":153.960419,"HyperDash":false}]},{"StartTime":185055.0,"Objects":[{"StartTime":185055.0,"Position":19.0,"HyperDash":false},{"StartTime":185166.0,"Position":98.20671,"HyperDash":false}]},{"StartTime":185278.0,"Objects":[{"StartTime":185278.0,"Position":224.0,"HyperDash":false}]},{"StartTime":185389.0,"Objects":[{"StartTime":185389.0,"Position":229.0,"HyperDash":false}]},{"StartTime":185500.0,"Objects":[{"StartTime":185500.0,"Position":203.0,"HyperDash":false}]},{"StartTime":185611.0,"Objects":[{"StartTime":185611.0,"Position":148.0,"HyperDash":false}]},{"StartTime":185722.0,"Objects":[{"StartTime":185722.0,"Position":80.0,"HyperDash":false},{"StartTime":185833.0,"Position":31.5825539,"HyperDash":true}]},{"StartTime":185944.0,"Objects":[{"StartTime":185944.0,"Position":227.0,"HyperDash":false},{"StartTime":186018.0,"Position":266.7068,"HyperDash":false},{"StartTime":186092.0,"Position":227.0,"HyperDash":false}]},{"StartTime":186166.0,"Objects":[{"StartTime":186166.0,"Position":306.0,"HyperDash":false},{"StartTime":186240.0,"Position":360.619873,"HyperDash":false},{"StartTime":186314.0,"Position":306.0,"HyperDash":false}]},{"StartTime":186388.0,"Objects":[{"StartTime":186388.0,"Position":358.0,"HyperDash":false},{"StartTime":186462.0,"Position":412.8009,"HyperDash":false},{"StartTime":186536.0,"Position":358.0,"HyperDash":false}]},{"StartTime":186611.0,"Objects":[{"StartTime":186611.0,"Position":366.0,"HyperDash":false},{"StartTime":186685.0,"Position":406.4224,"HyperDash":false},{"StartTime":186759.0,"Position":366.0,"HyperDash":false}]},{"StartTime":186833.0,"Objects":[{"StartTime":186833.0,"Position":512.0,"HyperDash":false}]},{"StartTime":186907.0,"Objects":[{"StartTime":186907.0,"Position":512.0,"HyperDash":false}]},{"StartTime":186981.0,"Objects":[{"StartTime":186981.0,"Position":512.0,"HyperDash":false}]},{"StartTime":187055.0,"Objects":[{"StartTime":187055.0,"Position":512.0,"HyperDash":false},{"StartTime":187129.0,"Position":471.5776,"HyperDash":false},{"StartTime":187203.0,"Position":512.0,"HyperDash":false}]},{"StartTime":187277.0,"Objects":[{"StartTime":187277.0,"Position":469.0,"HyperDash":false},{"StartTime":187333.0,"Position":428.048767,"HyperDash":false},{"StartTime":187425.0,"Position":370.993652,"HyperDash":false}]},{"StartTime":187500.0,"Objects":[{"StartTime":187500.0,"Position":346.0,"HyperDash":false},{"StartTime":187555.0,"Position":324.884857,"HyperDash":false},{"StartTime":187611.0,"Position":281.551849,"HyperDash":false},{"StartTime":187666.0,"Position":288.422363,"HyperDash":false},{"StartTime":187722.0,"Position":306.796173,"HyperDash":false},{"StartTime":187833.0,"Position":357.976379,"HyperDash":false}]},{"StartTime":187944.0,"Objects":[{"StartTime":187944.0,"Position":326.0,"HyperDash":false}]},{"StartTime":188055.0,"Objects":[{"StartTime":188055.0,"Position":397.0,"HyperDash":false},{"StartTime":188166.0,"Position":479.230957,"HyperDash":true}]},{"StartTime":188278.0,"Objects":[{"StartTime":188278.0,"Position":269.0,"HyperDash":false}]},{"StartTime":188389.0,"Objects":[{"StartTime":188389.0,"Position":269.0,"HyperDash":false},{"StartTime":188500.0,"Position":220.272171,"HyperDash":false}]},{"StartTime":188611.0,"Objects":[{"StartTime":188611.0,"Position":209.0,"HyperDash":false},{"StartTime":188722.0,"Position":124.709274,"HyperDash":false}]},{"StartTime":188833.0,"Objects":[{"StartTime":188833.0,"Position":13.0,"HyperDash":false},{"StartTime":188944.0,"Position":97.2907257,"HyperDash":false}]},{"StartTime":189055.0,"Objects":[{"StartTime":189055.0,"Position":163.0,"HyperDash":false},{"StartTime":189166.0,"Position":78.7092743,"HyperDash":false}]},{"StartTime":189277.0,"Objects":[{"StartTime":189277.0,"Position":133.0,"HyperDash":false},{"StartTime":189388.0,"Position":217.403992,"HyperDash":false}]},{"StartTime":189499.0,"Objects":[{"StartTime":189499.0,"Position":248.0,"HyperDash":false},{"StartTime":189573.0,"Position":288.0694,"HyperDash":false},{"StartTime":189647.0,"Position":248.0,"HyperDash":false}]},{"StartTime":189721.0,"Objects":[{"StartTime":189721.0,"Position":309.0,"HyperDash":false},{"StartTime":189795.0,"Position":323.2212,"HyperDash":false},{"StartTime":189869.0,"Position":309.0,"HyperDash":false}]},{"StartTime":189944.0,"Objects":[{"StartTime":189944.0,"Position":414.0,"HyperDash":false},{"StartTime":190018.0,"Position":398.833527,"HyperDash":false},{"StartTime":190092.0,"Position":414.0,"HyperDash":false}]},{"StartTime":190166.0,"Objects":[{"StartTime":190166.0,"Position":468.0,"HyperDash":false},{"StartTime":190240.0,"Position":482.2459,"HyperDash":false},{"StartTime":190314.0,"Position":468.0,"HyperDash":false},{"StartTime":190388.0,"Position":482.2459,"HyperDash":false},{"StartTime":190462.0,"Position":468.0,"HyperDash":false},{"StartTime":190536.0,"Position":482.2459,"HyperDash":false}]},{"StartTime":190611.0,"Objects":[{"StartTime":190611.0,"Position":408.0,"HyperDash":false},{"StartTime":190685.0,"Position":422.909973,"HyperDash":false},{"StartTime":190759.0,"Position":408.0,"HyperDash":false}]},{"StartTime":190833.0,"Objects":[{"StartTime":190833.0,"Position":399.0,"HyperDash":false},{"StartTime":190907.0,"Position":413.2212,"HyperDash":false},{"StartTime":190981.0,"Position":399.0,"HyperDash":false}]},{"StartTime":191055.0,"Objects":[{"StartTime":191055.0,"Position":311.0,"HyperDash":false},{"StartTime":191148.0,"Position":357.4903,"HyperDash":false},{"StartTime":191277.0,"Position":394.428223,"HyperDash":false}]},{"StartTime":191389.0,"Objects":[{"StartTime":191389.0,"Position":272.0,"HyperDash":false}]},{"StartTime":191500.0,"Objects":[{"StartTime":191500.0,"Position":272.0,"HyperDash":false},{"StartTime":191611.0,"Position":336.857483,"HyperDash":false}]},{"StartTime":191722.0,"Objects":[{"StartTime":191722.0,"Position":461.0,"HyperDash":false},{"StartTime":191833.0,"Position":383.333038,"HyperDash":false}]},{"StartTime":191944.0,"Objects":[{"StartTime":191944.0,"Position":215.0,"HyperDash":false}]},{"StartTime":192055.0,"Objects":[{"StartTime":192055.0,"Position":189.0,"HyperDash":false}]},{"StartTime":192166.0,"Objects":[{"StartTime":192166.0,"Position":157.0,"HyperDash":false}]},{"StartTime":192277.0,"Objects":[{"StartTime":192277.0,"Position":123.0,"HyperDash":false}]},{"StartTime":192389.0,"Objects":[{"StartTime":192389.0,"Position":89.0,"HyperDash":false},{"StartTime":192500.0,"Position":17.9128532,"HyperDash":false}]},{"StartTime":192611.0,"Objects":[{"StartTime":192611.0,"Position":54.0,"HyperDash":false},{"StartTime":192722.0,"Position":52.84656,"HyperDash":false}]},{"StartTime":192833.0,"Objects":[{"StartTime":192833.0,"Position":208.0,"HyperDash":false},{"StartTime":192944.0,"Position":194.175568,"HyperDash":false}]},{"StartTime":193055.0,"Objects":[{"StartTime":193055.0,"Position":275.0,"HyperDash":false},{"StartTime":193166.0,"Position":288.824432,"HyperDash":false}]},{"StartTime":193277.0,"Objects":[{"StartTime":193277.0,"Position":415.0,"HyperDash":false}]},{"StartTime":193389.0,"Objects":[{"StartTime":193389.0,"Position":461.0,"HyperDash":false}]},{"StartTime":193500.0,"Objects":[{"StartTime":193500.0,"Position":458.0,"HyperDash":false}]},{"StartTime":193611.0,"Objects":[{"StartTime":193611.0,"Position":413.0,"HyperDash":false}]},{"StartTime":193722.0,"Objects":[{"StartTime":193722.0,"Position":329.0,"HyperDash":false},{"StartTime":193833.0,"Position":246.696991,"HyperDash":false}]},{"StartTime":193944.0,"Objects":[{"StartTime":193944.0,"Position":377.0,"HyperDash":false},{"StartTime":194055.0,"Position":459.303,"HyperDash":false}]},{"StartTime":194166.0,"Objects":[{"StartTime":194166.0,"Position":491.0,"HyperDash":false},{"StartTime":194259.0,"Position":480.782471,"HyperDash":false},{"StartTime":194388.0,"Position":427.072449,"HyperDash":true}]},{"StartTime":194611.0,"Objects":[{"StartTime":194611.0,"Position":51.0,"HyperDash":false},{"StartTime":194666.0,"Position":105.644623,"HyperDash":false},{"StartTime":194722.0,"Position":112.4984,"HyperDash":false},{"StartTime":194777.0,"Position":153.00119,"HyperDash":false},{"StartTime":194833.0,"Position":192.190445,"HyperDash":false},{"StartTime":194926.0,"Position":250.960892,"HyperDash":false},{"StartTime":195055.0,"Position":334.876526,"HyperDash":false}]},{"StartTime":195166.0,"Objects":[{"StartTime":195166.0,"Position":165.0,"HyperDash":false}]},{"StartTime":195277.0,"Objects":[{"StartTime":195277.0,"Position":201.0,"HyperDash":false},{"StartTime":195388.0,"Position":256.006256,"HyperDash":true}]},{"StartTime":195500.0,"Objects":[{"StartTime":195500.0,"Position":47.0,"HyperDash":false},{"StartTime":195611.0,"Position":70.89899,"HyperDash":false}]},{"StartTime":195722.0,"Objects":[{"StartTime":195722.0,"Position":238.0,"HyperDash":false}]},{"StartTime":195833.0,"Objects":[{"StartTime":195833.0,"Position":320.0,"HyperDash":false}]},{"StartTime":195944.0,"Objects":[{"StartTime":195944.0,"Position":402.0,"HyperDash":false}]},{"StartTime":196055.0,"Objects":[{"StartTime":196055.0,"Position":462.0,"HyperDash":false}]},{"StartTime":196166.0,"Objects":[{"StartTime":196166.0,"Position":484.0,"HyperDash":false},{"StartTime":196222.0,"Position":495.415375,"HyperDash":false},{"StartTime":196314.0,"Position":425.076019,"HyperDash":false}]},{"StartTime":196389.0,"Objects":[{"StartTime":196389.0,"Position":354.0,"HyperDash":false},{"StartTime":196463.0,"Position":360.907166,"HyperDash":false},{"StartTime":196537.0,"Position":354.0,"HyperDash":false}]},{"StartTime":196611.0,"Objects":[{"StartTime":196611.0,"Position":290.0,"HyperDash":false},{"StartTime":196685.0,"Position":296.907166,"HyperDash":false},{"StartTime":196759.0,"Position":290.0,"HyperDash":false},{"StartTime":196833.0,"Position":296.907166,"HyperDash":false}]},{"StartTime":196907.0,"Objects":[{"StartTime":196907.0,"Position":242.0,"HyperDash":false},{"StartTime":196981.0,"Position":233.986115,"HyperDash":false}]},{"StartTime":197055.0,"Objects":[{"StartTime":197055.0,"Position":192.0,"HyperDash":false},{"StartTime":197129.0,"Position":199.028641,"HyperDash":false},{"StartTime":197203.0,"Position":192.0,"HyperDash":false}]},{"StartTime":197277.0,"Objects":[{"StartTime":197277.0,"Position":108.0,"HyperDash":false},{"StartTime":197351.0,"Position":51.770916,"HyperDash":false},{"StartTime":197425.0,"Position":108.0,"HyperDash":false},{"StartTime":197499.0,"Position":51.770916,"HyperDash":false},{"StartTime":197573.0,"Position":108.0,"HyperDash":false},{"StartTime":197647.0,"Position":51.770916,"HyperDash":false}]},{"StartTime":197722.0,"Objects":[{"StartTime":197722.0,"Position":0.0,"HyperDash":false},{"StartTime":197815.0,"Position":60.3444443,"HyperDash":false},{"StartTime":197944.0,"Position":111.30835,"HyperDash":false}]},{"StartTime":198166.0,"Objects":[{"StartTime":198166.0,"Position":391.0,"HyperDash":false},{"StartTime":198240.0,"Position":446.9797,"HyperDash":false},{"StartTime":198314.0,"Position":391.0,"HyperDash":false},{"StartTime":198388.0,"Position":446.9797,"HyperDash":false},{"StartTime":198462.0,"Position":391.0,"HyperDash":false},{"StartTime":198536.0,"Position":446.9797,"HyperDash":false}]},{"StartTime":198611.0,"Objects":[{"StartTime":198611.0,"Position":317.0,"HyperDash":false}]},{"StartTime":198685.0,"Objects":[{"StartTime":198685.0,"Position":317.0,"HyperDash":false}]},{"StartTime":198759.0,"Objects":[{"StartTime":198759.0,"Position":317.0,"HyperDash":false}]},{"StartTime":198833.0,"Objects":[{"StartTime":198833.0,"Position":317.0,"HyperDash":false},{"StartTime":198907.0,"Position":261.0203,"HyperDash":false},{"StartTime":198981.0,"Position":317.0,"HyperDash":false}]},{"StartTime":199055.0,"Objects":[{"StartTime":199055.0,"Position":392.0,"HyperDash":false},{"StartTime":199129.0,"Position":400.7968,"HyperDash":false},{"StartTime":199203.0,"Position":392.0,"HyperDash":false},{"StartTime":199277.0,"Position":400.7968,"HyperDash":false},{"StartTime":199351.0,"Position":392.0,"HyperDash":false},{"StartTime":199425.0,"Position":400.7968,"HyperDash":false}]},{"StartTime":199500.0,"Objects":[{"StartTime":199500.0,"Position":494.0,"HyperDash":false},{"StartTime":199574.0,"Position":485.2032,"HyperDash":false},{"StartTime":199648.0,"Position":494.0,"HyperDash":false},{"StartTime":199722.0,"Position":485.2032,"HyperDash":false},{"StartTime":199796.0,"Position":494.0,"HyperDash":false},{"StartTime":199870.0,"Position":485.2032,"HyperDash":false}]},{"StartTime":199944.0,"Objects":[{"StartTime":199944.0,"Position":400.0,"HyperDash":false},{"StartTime":200018.0,"Position":344.0203,"HyperDash":false},{"StartTime":200092.0,"Position":400.0,"HyperDash":false},{"StartTime":200166.0,"Position":344.0203,"HyperDash":false},{"StartTime":200240.0,"Position":400.0,"HyperDash":false},{"StartTime":200314.0,"Position":344.0203,"HyperDash":false}]},{"StartTime":200389.0,"Objects":[{"StartTime":200389.0,"Position":267.0,"HyperDash":false}]},{"StartTime":200463.0,"Objects":[{"StartTime":200463.0,"Position":267.0,"HyperDash":false}]},{"StartTime":200537.0,"Objects":[{"StartTime":200537.0,"Position":267.0,"HyperDash":false}]},{"StartTime":200611.0,"Objects":[{"StartTime":200611.0,"Position":267.0,"HyperDash":false},{"StartTime":200685.0,"Position":211.0203,"HyperDash":false},{"StartTime":200759.0,"Position":267.0,"HyperDash":false}]},{"StartTime":200833.0,"Objects":[{"StartTime":200833.0,"Position":121.0,"HyperDash":false},{"StartTime":200907.0,"Position":112.203186,"HyperDash":false},{"StartTime":200981.0,"Position":121.0,"HyperDash":false},{"StartTime":201055.0,"Position":112.203186,"HyperDash":false},{"StartTime":201129.0,"Position":121.0,"HyperDash":false},{"StartTime":201203.0,"Position":112.203186,"HyperDash":false}]},{"StartTime":201277.0,"Objects":[{"StartTime":201277.0,"Position":179.0,"HyperDash":false},{"StartTime":201351.0,"Position":170.203186,"HyperDash":false},{"StartTime":201425.0,"Position":179.0,"HyperDash":false}]},{"StartTime":201500.0,"Objects":[{"StartTime":201500.0,"Position":67.0,"HyperDash":false},{"StartTime":201574.0,"Position":75.796814,"HyperDash":false},{"StartTime":201648.0,"Position":67.0,"HyperDash":false}]},{"StartTime":201722.0,"Objects":[{"StartTime":201722.0,"Position":11.0,"HyperDash":false}]},{"StartTime":201776.0,"Objects":[{"StartTime":201776.0,"Position":88.0,"HyperDash":false},{"StartTime":201830.0,"Position":257.0,"HyperDash":false},{"StartTime":201885.0,"Position":175.0,"HyperDash":false},{"StartTime":201940.0,"Position":38.0,"HyperDash":false},{"StartTime":201994.0,"Position":283.0,"HyperDash":false},{"StartTime":202049.0,"Position":138.0,"HyperDash":false},{"StartTime":202104.0,"Position":102.0,"HyperDash":false},{"StartTime":202158.0,"Position":494.0,"HyperDash":false},{"StartTime":202213.0,"Position":54.0,"HyperDash":false},{"StartTime":202268.0,"Position":29.0,"HyperDash":false},{"StartTime":202322.0,"Position":69.0,"HyperDash":false},{"StartTime":202377.0,"Position":110.0,"HyperDash":false},{"StartTime":202432.0,"Position":167.0,"HyperDash":false},{"StartTime":202486.0,"Position":56.0,"HyperDash":false},{"StartTime":202541.0,"Position":10.0,"HyperDash":false},{"StartTime":202596.0,"Position":308.0,"HyperDash":false},{"StartTime":202651.0,"Position":288.0,"HyperDash":false},{"StartTime":202705.0,"Position":57.0,"HyperDash":false},{"StartTime":202760.0,"Position":258.0,"HyperDash":false},{"StartTime":202815.0,"Position":180.0,"HyperDash":false},{"StartTime":202869.0,"Position":198.0,"HyperDash":false},{"StartTime":202924.0,"Position":211.0,"HyperDash":false},{"StartTime":202979.0,"Position":503.0,"HyperDash":false},{"StartTime":203033.0,"Position":324.0,"HyperDash":false},{"StartTime":203088.0,"Position":20.0,"HyperDash":false},{"StartTime":203143.0,"Position":169.0,"HyperDash":false},{"StartTime":203197.0,"Position":93.0,"HyperDash":false},{"StartTime":203252.0,"Position":267.0,"HyperDash":false},{"StartTime":203307.0,"Position":276.0,"HyperDash":false},{"StartTime":203361.0,"Position":367.0,"HyperDash":false},{"StartTime":203416.0,"Position":409.0,"HyperDash":false},{"StartTime":203471.0,"Position":117.0,"HyperDash":false},{"StartTime":203526.0,"Position":226.0,"HyperDash":false},{"StartTime":203580.0,"Position":469.0,"HyperDash":false},{"StartTime":203635.0,"Position":267.0,"HyperDash":false},{"StartTime":203690.0,"Position":477.0,"HyperDash":false},{"StartTime":203744.0,"Position":282.0,"HyperDash":false},{"StartTime":203799.0,"Position":216.0,"HyperDash":false},{"StartTime":203854.0,"Position":106.0,"HyperDash":false},{"StartTime":203908.0,"Position":353.0,"HyperDash":false},{"StartTime":203963.0,"Position":162.0,"HyperDash":false},{"StartTime":204018.0,"Position":473.0,"HyperDash":false},{"StartTime":204072.0,"Position":260.0,"HyperDash":false},{"StartTime":204127.0,"Position":367.0,"HyperDash":false},{"StartTime":204182.0,"Position":409.0,"HyperDash":false},{"StartTime":204236.0,"Position":145.0,"HyperDash":false},{"StartTime":204291.0,"Position":330.0,"HyperDash":false},{"StartTime":204346.0,"Position":104.0,"HyperDash":false},{"StartTime":204401.0,"Position":412.0,"HyperDash":false},{"StartTime":204455.0,"Position":104.0,"HyperDash":false},{"StartTime":204510.0,"Position":396.0,"HyperDash":false},{"StartTime":204565.0,"Position":192.0,"HyperDash":false},{"StartTime":204619.0,"Position":446.0,"HyperDash":false},{"StartTime":204674.0,"Position":110.0,"HyperDash":false},{"StartTime":204729.0,"Position":372.0,"HyperDash":false},{"StartTime":204783.0,"Position":100.0,"HyperDash":false},{"StartTime":204838.0,"Position":161.0,"HyperDash":false},{"StartTime":204893.0,"Position":456.0,"HyperDash":false},{"StartTime":204947.0,"Position":187.0,"HyperDash":false},{"StartTime":205002.0,"Position":99.0,"HyperDash":false},{"StartTime":205057.0,"Position":197.0,"HyperDash":false},{"StartTime":205111.0,"Position":116.0,"HyperDash":false},{"StartTime":205166.0,"Position":496.0,"HyperDash":false},{"StartTime":205221.0,"Position":143.0,"HyperDash":false},{"StartTime":205276.0,"Position":431.0,"HyperDash":false}]},{"StartTime":207943.0,"Objects":[{"StartTime":207943.0,"Position":171.0,"HyperDash":false},{"StartTime":208011.0,"Position":175.536011,"HyperDash":false},{"StartTime":208079.0,"Position":171.0,"HyperDash":false},{"StartTime":208147.0,"Position":175.536011,"HyperDash":false},{"StartTime":208215.0,"Position":171.0,"HyperDash":false},{"StartTime":208283.0,"Position":175.536011,"HyperDash":false},{"StartTime":208352.0,"Position":171.0,"HyperDash":false},{"StartTime":208420.0,"Position":175.536011,"HyperDash":false},{"StartTime":208488.0,"Position":171.0,"HyperDash":false},{"StartTime":208556.0,"Position":175.536011,"HyperDash":false},{"StartTime":208624.0,"Position":171.0,"HyperDash":false},{"StartTime":208693.0,"Position":175.536011,"HyperDash":false},{"StartTime":208761.0,"Position":171.0,"HyperDash":false},{"StartTime":208829.0,"Position":175.536011,"HyperDash":false},{"StartTime":208897.0,"Position":171.0,"HyperDash":false},{"StartTime":208965.0,"Position":175.536011,"HyperDash":false},{"StartTime":209033.0,"Position":171.0,"HyperDash":false},{"StartTime":209102.0,"Position":175.536011,"HyperDash":false},{"StartTime":209170.0,"Position":171.0,"HyperDash":false},{"StartTime":209238.0,"Position":175.536011,"HyperDash":false},{"StartTime":209306.0,"Position":171.0,"HyperDash":false},{"StartTime":209374.0,"Position":175.536011,"HyperDash":false},{"StartTime":209443.0,"Position":171.0,"HyperDash":false},{"StartTime":209511.0,"Position":175.536011,"HyperDash":false},{"StartTime":209579.0,"Position":171.0,"HyperDash":false},{"StartTime":209647.0,"Position":175.536011,"HyperDash":false},{"StartTime":209715.0,"Position":171.0,"HyperDash":false},{"StartTime":209783.0,"Position":175.536011,"HyperDash":false},{"StartTime":209852.0,"Position":171.0,"HyperDash":false},{"StartTime":209920.0,"Position":175.536011,"HyperDash":false},{"StartTime":209988.0,"Position":171.0,"HyperDash":false},{"StartTime":210056.0,"Position":175.536011,"HyperDash":false}]},{"StartTime":210124.0,"Objects":[{"StartTime":210124.0,"Position":85.0,"HyperDash":false}]},{"StartTime":210329.0,"Objects":[{"StartTime":210329.0,"Position":73.0,"HyperDash":false}]},{"StartTime":210533.0,"Objects":[{"StartTime":210533.0,"Position":243.0,"HyperDash":false}]},{"StartTime":210670.0,"Objects":[{"StartTime":210670.0,"Position":122.0,"HyperDash":false}]},{"StartTime":210875.0,"Objects":[{"StartTime":210875.0,"Position":61.0,"HyperDash":false}]},{"StartTime":211079.0,"Objects":[{"StartTime":211079.0,"Position":246.0,"HyperDash":false}]},{"StartTime":211215.0,"Objects":[{"StartTime":211215.0,"Position":294.0,"HyperDash":false},{"StartTime":211283.0,"Position":253.395386,"HyperDash":false},{"StartTime":211351.0,"Position":294.0,"HyperDash":false},{"StartTime":211419.0,"Position":253.395386,"HyperDash":false}]},{"StartTime":211488.0,"Objects":[{"StartTime":211488.0,"Position":369.0,"HyperDash":false},{"StartTime":211556.0,"Position":409.5123,"HyperDash":false},{"StartTime":211624.0,"Position":369.0,"HyperDash":false},{"StartTime":211692.0,"Position":409.5123,"HyperDash":false}]},{"StartTime":211761.0,"Objects":[{"StartTime":211761.0,"Position":319.0,"HyperDash":false},{"StartTime":211829.0,"Position":306.78772,"HyperDash":false},{"StartTime":211897.0,"Position":319.0,"HyperDash":false},{"StartTime":211965.0,"Position":306.78772,"HyperDash":false}]},{"StartTime":212033.0,"Objects":[{"StartTime":212033.0,"Position":221.0,"HyperDash":false},{"StartTime":212101.0,"Position":209.0618,"HyperDash":false},{"StartTime":212169.0,"Position":221.0,"HyperDash":false},{"StartTime":212237.0,"Position":209.0618,"HyperDash":false}]},{"StartTime":212306.0,"Objects":[{"StartTime":212306.0,"Position":121.0,"HyperDash":false}]},{"StartTime":212374.0,"Objects":[{"StartTime":212374.0,"Position":112.0,"HyperDash":false}]},{"StartTime":212442.0,"Objects":[{"StartTime":212442.0,"Position":103.0,"HyperDash":false}]},{"StartTime":212579.0,"Objects":[{"StartTime":212579.0,"Position":78.0,"HyperDash":false}]},{"StartTime":212647.0,"Objects":[{"StartTime":212647.0,"Position":87.0,"HyperDash":false}]},{"StartTime":212715.0,"Objects":[{"StartTime":212715.0,"Position":96.0,"HyperDash":false}]},{"StartTime":212851.0,"Objects":[{"StartTime":212851.0,"Position":0.0,"HyperDash":false},{"StartTime":212919.0,"Position":12.8453636,"HyperDash":false},{"StartTime":212987.0,"Position":0.0,"HyperDash":false}]},{"StartTime":213124.0,"Objects":[{"StartTime":213124.0,"Position":77.0,"HyperDash":false},{"StartTime":213192.0,"Position":65.0618057,"HyperDash":false},{"StartTime":213260.0,"Position":77.0,"HyperDash":false}]},{"StartTime":213397.0,"Objects":[{"StartTime":213397.0,"Position":131.0,"HyperDash":false},{"StartTime":213465.0,"Position":171.788834,"HyperDash":false},{"StartTime":213533.0,"Position":131.0,"HyperDash":false},{"StartTime":213601.0,"Position":171.788834,"HyperDash":false}]},{"StartTime":213670.0,"Objects":[{"StartTime":213670.0,"Position":261.0,"HyperDash":false},{"StartTime":213738.0,"Position":301.6046,"HyperDash":false},{"StartTime":213806.0,"Position":261.0,"HyperDash":false},{"StartTime":213874.0,"Position":301.6046,"HyperDash":false}]},{"StartTime":213942.0,"Objects":[{"StartTime":213942.0,"Position":366.0,"HyperDash":false},{"StartTime":214010.0,"Position":353.78772,"HyperDash":false},{"StartTime":214078.0,"Position":366.0,"HyperDash":false},{"StartTime":214146.0,"Position":353.78772,"HyperDash":false}]},{"StartTime":214215.0,"Objects":[{"StartTime":214215.0,"Position":456.0,"HyperDash":false},{"StartTime":214283.0,"Position":443.78772,"HyperDash":false},{"StartTime":214351.0,"Position":456.0,"HyperDash":false},{"StartTime":214419.0,"Position":443.78772,"HyperDash":false}]},{"StartTime":214488.0,"Objects":[{"StartTime":214488.0,"Position":490.0,"HyperDash":false}]},{"StartTime":214556.0,"Objects":[{"StartTime":214556.0,"Position":487.0,"HyperDash":false}]},{"StartTime":214624.0,"Objects":[{"StartTime":214624.0,"Position":484.0,"HyperDash":false}]},{"StartTime":214761.0,"Objects":[{"StartTime":214761.0,"Position":419.0,"HyperDash":false}]},{"StartTime":214829.0,"Objects":[{"StartTime":214829.0,"Position":422.0,"HyperDash":false}]},{"StartTime":214897.0,"Objects":[{"StartTime":214897.0,"Position":425.0,"HyperDash":false}]},{"StartTime":215033.0,"Objects":[{"StartTime":215033.0,"Position":344.0,"HyperDash":false}]},{"StartTime":215101.0,"Objects":[{"StartTime":215101.0,"Position":336.0,"HyperDash":false}]},{"StartTime":215170.0,"Objects":[{"StartTime":215170.0,"Position":328.0,"HyperDash":false},{"StartTime":215306.0,"Position":243.560242,"HyperDash":false}]},{"StartTime":215579.0,"Objects":[{"StartTime":215579.0,"Position":238.0,"HyperDash":false},{"StartTime":215647.0,"Position":250.21228,"HyperDash":false},{"StartTime":215715.0,"Position":238.0,"HyperDash":false},{"StartTime":215783.0,"Position":250.21228,"HyperDash":false}]},{"StartTime":215852.0,"Objects":[{"StartTime":215852.0,"Position":182.0,"HyperDash":false},{"StartTime":215920.0,"Position":169.78772,"HyperDash":false},{"StartTime":215988.0,"Position":182.0,"HyperDash":false},{"StartTime":216056.0,"Position":169.78772,"HyperDash":false}]},{"StartTime":216124.0,"Objects":[{"StartTime":216124.0,"Position":90.0,"HyperDash":false},{"StartTime":216192.0,"Position":49.2111626,"HyperDash":false},{"StartTime":216260.0,"Position":90.0,"HyperDash":false},{"StartTime":216328.0,"Position":49.2111626,"HyperDash":false}]},{"StartTime":216397.0,"Objects":[{"StartTime":216397.0,"Position":51.0,"HyperDash":false},{"StartTime":216465.0,"Position":10.3953857,"HyperDash":false},{"StartTime":216533.0,"Position":51.0,"HyperDash":false},{"StartTime":216601.0,"Position":10.3953857,"HyperDash":false}]},{"StartTime":216670.0,"Objects":[{"StartTime":216670.0,"Position":64.0,"HyperDash":false}]},{"StartTime":216738.0,"Objects":[{"StartTime":216738.0,"Position":73.0,"HyperDash":false}]},{"StartTime":216806.0,"Objects":[{"StartTime":216806.0,"Position":82.0,"HyperDash":false}]},{"StartTime":216942.0,"Objects":[{"StartTime":216942.0,"Position":191.0,"HyperDash":false}]},{"StartTime":217010.0,"Objects":[{"StartTime":217010.0,"Position":200.0,"HyperDash":false}]},{"StartTime":217078.0,"Objects":[{"StartTime":217078.0,"Position":209.0,"HyperDash":false}]},{"StartTime":217215.0,"Objects":[{"StartTime":217215.0,"Position":243.0,"HyperDash":false},{"StartTime":217283.0,"Position":283.6046,"HyperDash":false},{"StartTime":217351.0,"Position":243.0,"HyperDash":false}]},{"StartTime":217488.0,"Objects":[{"StartTime":217488.0,"Position":350.0,"HyperDash":false},{"StartTime":217556.0,"Position":309.211151,"HyperDash":false},{"StartTime":217624.0,"Position":350.0,"HyperDash":false}]},{"StartTime":217761.0,"Objects":[{"StartTime":217761.0,"Position":425.0,"HyperDash":false},{"StartTime":217829.0,"Position":412.78772,"HyperDash":false},{"StartTime":217897.0,"Position":425.0,"HyperDash":false},{"StartTime":217965.0,"Position":412.78772,"HyperDash":false}]},{"StartTime":218034.0,"Objects":[{"StartTime":218034.0,"Position":481.0,"HyperDash":false},{"StartTime":218102.0,"Position":493.21228,"HyperDash":false},{"StartTime":218170.0,"Position":481.0,"HyperDash":false},{"StartTime":218238.0,"Position":493.21228,"HyperDash":false}]},{"StartTime":218306.0,"Objects":[{"StartTime":218306.0,"Position":411.0,"HyperDash":false},{"StartTime":218374.0,"Position":370.211151,"HyperDash":false},{"StartTime":218442.0,"Position":411.0,"HyperDash":false},{"StartTime":218510.0,"Position":370.211151,"HyperDash":false}]},{"StartTime":218579.0,"Objects":[{"StartTime":218579.0,"Position":328.0,"HyperDash":false},{"StartTime":218647.0,"Position":287.3954,"HyperDash":false},{"StartTime":218715.0,"Position":328.0,"HyperDash":false},{"StartTime":218783.0,"Position":287.3954,"HyperDash":false}]},{"StartTime":218851.0,"Objects":[{"StartTime":218851.0,"Position":208.0,"HyperDash":false}]},{"StartTime":218919.0,"Objects":[{"StartTime":218919.0,"Position":205.0,"HyperDash":false}]},{"StartTime":218987.0,"Objects":[{"StartTime":218987.0,"Position":202.0,"HyperDash":false}]},{"StartTime":219124.0,"Objects":[{"StartTime":219124.0,"Position":120.0,"HyperDash":false}]},{"StartTime":219192.0,"Objects":[{"StartTime":219192.0,"Position":117.0,"HyperDash":false}]},{"StartTime":219260.0,"Objects":[{"StartTime":219260.0,"Position":114.0,"HyperDash":false}]},{"StartTime":219397.0,"Objects":[{"StartTime":219397.0,"Position":44.0,"HyperDash":false},{"StartTime":219465.0,"Position":51.91918,"HyperDash":false},{"StartTime":219533.0,"Position":44.0,"HyperDash":false},{"StartTime":219601.0,"Position":51.91918,"HyperDash":false},{"StartTime":219669.0,"Position":44.0,"HyperDash":false},{"StartTime":219737.0,"Position":51.91918,"HyperDash":false},{"StartTime":219806.0,"Position":44.0,"HyperDash":false},{"StartTime":219874.0,"Position":51.91918,"HyperDash":false}]},{"StartTime":219943.0,"Objects":[{"StartTime":219943.0,"Position":142.0,"HyperDash":false}]},{"StartTime":220011.0,"Objects":[{"StartTime":220011.0,"Position":146.0,"HyperDash":false}]},{"StartTime":220079.0,"Objects":[{"StartTime":220079.0,"Position":151.0,"HyperDash":false},{"StartTime":220147.0,"Position":192.8533,"HyperDash":false},{"StartTime":220215.0,"Position":151.0,"HyperDash":false},{"StartTime":220283.0,"Position":192.8533,"HyperDash":false}]},{"StartTime":220352.0,"Objects":[{"StartTime":220352.0,"Position":269.0,"HyperDash":false},{"StartTime":220420.0,"Position":310.8533,"HyperDash":false}]},{"StartTime":220488.0,"Objects":[{"StartTime":220488.0,"Position":320.0,"HyperDash":false},{"StartTime":220556.0,"Position":361.8533,"HyperDash":false},{"StartTime":220624.0,"Position":320.0,"HyperDash":false},{"StartTime":220692.0,"Position":361.8533,"HyperDash":false},{"StartTime":220760.0,"Position":320.0,"HyperDash":false},{"StartTime":220828.0,"Position":361.8533,"HyperDash":false},{"StartTime":220897.0,"Position":320.0,"HyperDash":false},{"StartTime":220965.0,"Position":361.8533,"HyperDash":false},{"StartTime":221033.0,"Position":320.0,"HyperDash":false}]},{"StartTime":222670.0,"Objects":[{"StartTime":222670.0,"Position":364.0,"HyperDash":false},{"StartTime":222738.0,"Position":404.113983,"HyperDash":false},{"StartTime":222806.0,"Position":364.0,"HyperDash":false},{"StartTime":222874.0,"Position":404.113983,"HyperDash":false},{"StartTime":222942.0,"Position":364.0,"HyperDash":false},{"StartTime":223010.0,"Position":404.113983,"HyperDash":false},{"StartTime":223079.0,"Position":364.0,"HyperDash":false},{"StartTime":223147.0,"Position":404.113983,"HyperDash":false}]},{"StartTime":223215.0,"Objects":[{"StartTime":223215.0,"Position":487.0,"HyperDash":false},{"StartTime":223351.0,"Position":471.39093,"HyperDash":false}]},{"StartTime":223488.0,"Objects":[{"StartTime":223488.0,"Position":437.0,"HyperDash":false},{"StartTime":223624.0,"Position":421.39093,"HyperDash":false}]},{"StartTime":223761.0,"Objects":[{"StartTime":223761.0,"Position":314.0,"HyperDash":false}]},{"StartTime":223897.0,"Objects":[{"StartTime":223897.0,"Position":240.0,"HyperDash":false}]},{"StartTime":223965.0,"Objects":[{"StartTime":223965.0,"Position":240.0,"HyperDash":false}]},{"StartTime":224033.0,"Objects":[{"StartTime":224033.0,"Position":240.0,"HyperDash":false},{"StartTime":224169.0,"Position":156.4455,"HyperDash":false}]},{"StartTime":224306.0,"Objects":[{"StartTime":224306.0,"Position":37.0,"HyperDash":false}]},{"StartTime":224443.0,"Objects":[{"StartTime":224443.0,"Position":37.0,"HyperDash":false}]},{"StartTime":224579.0,"Objects":[{"StartTime":224579.0,"Position":142.0,"HyperDash":false},{"StartTime":224715.0,"Position":225.463379,"HyperDash":false}]},{"StartTime":224852.0,"Objects":[{"StartTime":224852.0,"Position":304.0,"HyperDash":false},{"StartTime":224988.0,"Position":287.910675,"HyperDash":false}]},{"StartTime":225124.0,"Objects":[{"StartTime":225124.0,"Position":164.0,"HyperDash":false},{"StartTime":225192.0,"Position":172.139191,"HyperDash":false},{"StartTime":225260.0,"Position":164.0,"HyperDash":false},{"StartTime":225328.0,"Position":172.139191,"HyperDash":false}]},{"StartTime":225397.0,"Objects":[{"StartTime":225397.0,"Position":84.0,"HyperDash":false},{"StartTime":225533.0,"Position":144.172775,"HyperDash":false}]},{"StartTime":225670.0,"Objects":[{"StartTime":225670.0,"Position":86.0,"HyperDash":false},{"StartTime":225806.0,"Position":25.8272362,"HyperDash":false}]},{"StartTime":225943.0,"Objects":[{"StartTime":225943.0,"Position":39.0,"HyperDash":false},{"StartTime":226079.0,"Position":47.27571,"HyperDash":false}]},{"StartTime":226215.0,"Objects":[{"StartTime":226215.0,"Position":137.0,"HyperDash":false},{"StartTime":226351.0,"Position":128.724289,"HyperDash":false}]},{"StartTime":226488.0,"Objects":[{"StartTime":226488.0,"Position":237.0,"HyperDash":false},{"StartTime":226624.0,"Position":321.596161,"HyperDash":false}]},{"StartTime":226761.0,"Objects":[{"StartTime":226761.0,"Position":361.0,"HyperDash":false}]},{"StartTime":226897.0,"Objects":[{"StartTime":226897.0,"Position":361.0,"HyperDash":false}]},{"StartTime":227033.0,"Objects":[{"StartTime":227033.0,"Position":488.0,"HyperDash":false},{"StartTime":227169.0,"Position":479.724274,"HyperDash":false}]},{"StartTime":227306.0,"Objects":[{"StartTime":227306.0,"Position":429.0,"HyperDash":false},{"StartTime":227442.0,"Position":437.275726,"HyperDash":false}]},{"StartTime":227579.0,"Objects":[{"StartTime":227579.0,"Position":361.0,"HyperDash":false},{"StartTime":227715.0,"Position":346.8173,"HyperDash":false}]},{"StartTime":227852.0,"Objects":[{"StartTime":227852.0,"Position":195.0,"HyperDash":false},{"StartTime":227988.0,"Position":179.865,"HyperDash":false}]},{"StartTime":228124.0,"Objects":[{"StartTime":228124.0,"Position":211.0,"HyperDash":false}]},{"StartTime":228261.0,"Objects":[{"StartTime":228261.0,"Position":131.0,"HyperDash":false}]},{"StartTime":228329.0,"Objects":[{"StartTime":228329.0,"Position":131.0,"HyperDash":false}]},{"StartTime":228397.0,"Objects":[{"StartTime":228397.0,"Position":131.0,"HyperDash":false},{"StartTime":228533.0,"Position":46.3490829,"HyperDash":false}]},{"StartTime":228670.0,"Objects":[{"StartTime":228670.0,"Position":67.0,"HyperDash":false}]},{"StartTime":228738.0,"Objects":[{"StartTime":228738.0,"Position":59.0,"HyperDash":false}]},{"StartTime":228806.0,"Objects":[{"StartTime":228806.0,"Position":63.0,"HyperDash":false}]},{"StartTime":228874.0,"Objects":[{"StartTime":228874.0,"Position":79.0,"HyperDash":false}]},{"StartTime":228942.0,"Objects":[{"StartTime":228942.0,"Position":104.0,"HyperDash":false}]},{"StartTime":229079.0,"Objects":[{"StartTime":229079.0,"Position":210.0,"HyperDash":false}]},{"StartTime":229147.0,"Objects":[{"StartTime":229147.0,"Position":224.0,"HyperDash":false}]},{"StartTime":229215.0,"Objects":[{"StartTime":229215.0,"Position":238.0,"HyperDash":false},{"StartTime":229283.0,"Position":199.132248,"HyperDash":false},{"StartTime":229351.0,"Position":238.0,"HyperDash":false}]},{"StartTime":229488.0,"Objects":[{"StartTime":229488.0,"Position":353.0,"HyperDash":false},{"StartTime":229556.0,"Position":336.0176,"HyperDash":false}]},{"StartTime":229624.0,"Objects":[{"StartTime":229624.0,"Position":425.0,"HyperDash":false},{"StartTime":229692.0,"Position":408.0176,"HyperDash":false}]},{"StartTime":229760.0,"Objects":[{"StartTime":229760.0,"Position":495.0,"HyperDash":false}]},{"StartTime":231943.0,"Objects":[{"StartTime":231943.0,"Position":221.0,"HyperDash":false}]},{"StartTime":233579.0,"Objects":[{"StartTime":233579.0,"Position":102.0,"HyperDash":false},{"StartTime":233669.0,"Position":37.721508,"HyperDash":false},{"StartTime":233760.0,"Position":102.0,"HyperDash":false},{"StartTime":233851.0,"Position":37.721508,"HyperDash":false},{"StartTime":233942.0,"Position":102.0,"HyperDash":false},{"StartTime":234033.0,"Position":37.721508,"HyperDash":false}]},{"StartTime":234124.0,"Objects":[{"StartTime":234124.0,"Position":93.0,"HyperDash":false},{"StartTime":234214.0,"Position":65.15,"HyperDash":false},{"StartTime":234305.0,"Position":93.0,"HyperDash":false}]},{"StartTime":234397.0,"Objects":[{"StartTime":234397.0,"Position":185.0,"HyperDash":false},{"StartTime":234487.0,"Position":191.729935,"HyperDash":false},{"StartTime":234578.0,"Position":185.0,"HyperDash":false}]},{"StartTime":234670.0,"Objects":[{"StartTime":234670.0,"Position":257.0,"HyperDash":false},{"StartTime":234760.0,"Position":229.150009,"HyperDash":false},{"StartTime":234851.0,"Position":257.0,"HyperDash":false}]},{"StartTime":234943.0,"Objects":[{"StartTime":234943.0,"Position":349.0,"HyperDash":false},{"StartTime":235033.0,"Position":355.43277,"HyperDash":false},{"StartTime":235124.0,"Position":349.0,"HyperDash":false}]},{"StartTime":235215.0,"Objects":[{"StartTime":235215.0,"Position":431.0,"HyperDash":false}]},{"StartTime":235306.0,"Objects":[{"StartTime":235306.0,"Position":439.0,"HyperDash":false},{"StartTime":235396.0,"Position":505.57785,"HyperDash":false}]},{"StartTime":235488.0,"Objects":[{"StartTime":235488.0,"Position":502.0,"HyperDash":false}]},{"StartTime":235579.0,"Objects":[{"StartTime":235579.0,"Position":460.0,"HyperDash":false}]},{"StartTime":235670.0,"Objects":[{"StartTime":235670.0,"Position":406.0,"HyperDash":false}]},{"StartTime":235760.0,"Objects":[{"StartTime":235760.0,"Position":358.0,"HyperDash":false},{"StartTime":235819.0,"Position":304.872559,"HyperDash":false},{"StartTime":235878.0,"Position":274.370667,"HyperDash":false},{"StartTime":235937.0,"Position":254.265121,"HyperDash":false},{"StartTime":236032.0,"Position":204.708969,"HyperDash":false}]},{"StartTime":236306.0,"Objects":[{"StartTime":236306.0,"Position":204.0,"HyperDash":false},{"StartTime":236396.0,"Position":271.720734,"HyperDash":false},{"StartTime":236487.0,"Position":204.0,"HyperDash":false}]},{"StartTime":236579.0,"Objects":[{"StartTime":236579.0,"Position":161.0,"HyperDash":false},{"StartTime":236669.0,"Position":228.033157,"HyperDash":false},{"StartTime":236760.0,"Position":161.0,"HyperDash":false}]},{"StartTime":236852.0,"Objects":[{"StartTime":236852.0,"Position":77.0,"HyperDash":false},{"StartTime":236942.0,"Position":9.279259,"HyperDash":false},{"StartTime":237033.0,"Position":77.0,"HyperDash":false}]},{"StartTime":237125.0,"Objects":[{"StartTime":237125.0,"Position":120.0,"HyperDash":false},{"StartTime":237215.0,"Position":52.9668427,"HyperDash":false},{"StartTime":237306.0,"Position":120.0,"HyperDash":false}]},{"StartTime":237397.0,"Objects":[{"StartTime":237397.0,"Position":194.0,"HyperDash":false}]},{"StartTime":237488.0,"Objects":[{"StartTime":237488.0,"Position":203.0,"HyperDash":false},{"StartTime":237578.0,"Position":216.523163,"HyperDash":false}]},{"StartTime":237670.0,"Objects":[{"StartTime":237670.0,"Position":296.0,"HyperDash":false}]},{"StartTime":237760.0,"Objects":[{"StartTime":237760.0,"Position":349.0,"HyperDash":false}]},{"StartTime":237851.0,"Objects":[{"StartTime":237851.0,"Position":391.0,"HyperDash":false}]},{"StartTime":237942.0,"Objects":[{"StartTime":237942.0,"Position":400.0,"HyperDash":false},{"StartTime":238001.0,"Position":373.357147,"HyperDash":false},{"StartTime":238060.0,"Position":359.412537,"HyperDash":false},{"StartTime":238119.0,"Position":345.3219,"HyperDash":false},{"StartTime":238214.0,"Position":385.5706,"HyperDash":false}]},{"StartTime":238488.0,"Objects":[{"StartTime":238488.0,"Position":385.0,"HyperDash":false},{"StartTime":238578.0,"Position":370.624329,"HyperDash":false},{"StartTime":238669.0,"Position":385.0,"HyperDash":false}]},{"StartTime":238761.0,"Objects":[{"StartTime":238761.0,"Position":276.0,"HyperDash":false},{"StartTime":238851.0,"Position":295.94812,"HyperDash":false},{"StartTime":238942.0,"Position":276.0,"HyperDash":false}]},{"StartTime":239033.0,"Objects":[{"StartTime":239033.0,"Position":188.0,"HyperDash":false},{"StartTime":239123.0,"Position":208.0458,"HyperDash":false},{"StartTime":239214.0,"Position":188.0,"HyperDash":false}]},{"StartTime":239306.0,"Objects":[{"StartTime":239306.0,"Position":129.0,"HyperDash":false},{"StartTime":239396.0,"Position":177.393921,"HyperDash":false},{"StartTime":239487.0,"Position":129.0,"HyperDash":false}]},{"StartTime":239579.0,"Objects":[{"StartTime":239579.0,"Position":38.0,"HyperDash":false}]},{"StartTime":239670.0,"Objects":[{"StartTime":239670.0,"Position":32.0,"HyperDash":false},{"StartTime":239760.0,"Position":54.9123878,"HyperDash":false}]},{"StartTime":239851.0,"Objects":[{"StartTime":239851.0,"Position":20.0,"HyperDash":false}]},{"StartTime":239942.0,"Objects":[{"StartTime":239942.0,"Position":57.0,"HyperDash":false}]},{"StartTime":240033.0,"Objects":[{"StartTime":240033.0,"Position":108.0,"HyperDash":false}]},{"StartTime":240124.0,"Objects":[{"StartTime":240124.0,"Position":161.0,"HyperDash":false},{"StartTime":240183.0,"Position":220.613419,"HyperDash":false},{"StartTime":240242.0,"Position":252.59671,"HyperDash":false},{"StartTime":240301.0,"Position":306.131134,"HyperDash":false},{"StartTime":240396.0,"Position":360.49115,"HyperDash":false}]},{"StartTime":240670.0,"Objects":[{"StartTime":240670.0,"Position":360.0,"HyperDash":false},{"StartTime":240760.0,"Position":296.123718,"HyperDash":false},{"StartTime":240851.0,"Position":360.0,"HyperDash":false}]},{"StartTime":240942.0,"Objects":[{"StartTime":240942.0,"Position":460.0,"HyperDash":false},{"StartTime":241032.0,"Position":404.530151,"HyperDash":false},{"StartTime":241123.0,"Position":460.0,"HyperDash":false}]},{"StartTime":241215.0,"Objects":[{"StartTime":241215.0,"Position":448.0,"HyperDash":false},{"StartTime":241305.0,"Position":511.876282,"HyperDash":false},{"StartTime":241396.0,"Position":448.0,"HyperDash":false}]},{"StartTime":241488.0,"Objects":[{"StartTime":241488.0,"Position":430.0,"HyperDash":false},{"StartTime":241578.0,"Position":485.1262,"HyperDash":false},{"StartTime":241669.0,"Position":430.0,"HyperDash":false}]},{"StartTime":241760.0,"Objects":[{"StartTime":241760.0,"Position":365.0,"HyperDash":false}]},{"StartTime":241852.0,"Objects":[{"StartTime":241852.0,"Position":354.0,"HyperDash":false},{"StartTime":241942.0,"Position":330.3751,"HyperDash":false}]},{"StartTime":242033.0,"Objects":[{"StartTime":242033.0,"Position":244.0,"HyperDash":false}]},{"StartTime":242124.0,"Objects":[{"StartTime":242124.0,"Position":191.0,"HyperDash":false}]},{"StartTime":242215.0,"Objects":[{"StartTime":242215.0,"Position":145.0,"HyperDash":false}]},{"StartTime":242306.0,"Objects":[{"StartTime":242306.0,"Position":91.0,"HyperDash":false},{"StartTime":242396.0,"Position":116.832222,"HyperDash":false},{"StartTime":242487.0,"Position":96.35042,"HyperDash":false},{"StartTime":242560.0,"Position":123.330132,"HyperDash":false},{"StartTime":242669.0,"Position":91.0,"HyperDash":false}]},{"StartTime":242852.0,"Objects":[{"StartTime":242852.0,"Position":33.0,"HyperDash":false},{"StartTime":242920.0,"Position":40.2480125,"HyperDash":false},{"StartTime":242988.0,"Position":33.0,"HyperDash":false},{"StartTime":243056.0,"Position":40.2480125,"HyperDash":false}]},{"StartTime":243125.0,"Objects":[{"StartTime":243125.0,"Position":134.0,"HyperDash":false},{"StartTime":243193.0,"Position":126.751991,"HyperDash":false},{"StartTime":243261.0,"Position":134.0,"HyperDash":false},{"StartTime":243329.0,"Position":126.751991,"HyperDash":false}]},{"StartTime":243397.0,"Objects":[{"StartTime":243397.0,"Position":228.0,"HyperDash":false},{"StartTime":243465.0,"Position":269.713348,"HyperDash":false}]},{"StartTime":243534.0,"Objects":[{"StartTime":243534.0,"Position":251.0,"HyperDash":false},{"StartTime":243602.0,"Position":292.713348,"HyperDash":false}]},{"StartTime":243671.0,"Objects":[{"StartTime":243671.0,"Position":276.0,"HyperDash":false},{"StartTime":243739.0,"Position":317.713348,"HyperDash":false},{"StartTime":243807.0,"Position":276.0,"HyperDash":false},{"StartTime":243875.0,"Position":317.713348,"HyperDash":false}]},{"StartTime":243943.0,"Objects":[{"StartTime":243943.0,"Position":388.0,"HyperDash":false},{"StartTime":244011.0,"Position":380.751984,"HyperDash":false},{"StartTime":244079.0,"Position":388.0,"HyperDash":false}]},{"StartTime":244216.0,"Objects":[{"StartTime":244216.0,"Position":409.0,"HyperDash":false}]},{"StartTime":244284.0,"Objects":[{"StartTime":244284.0,"Position":407.0,"HyperDash":false}]},{"StartTime":244352.0,"Objects":[{"StartTime":244352.0,"Position":405.0,"HyperDash":false}]},{"StartTime":244489.0,"Objects":[{"StartTime":244489.0,"Position":495.0,"HyperDash":false},{"StartTime":244557.0,"Position":502.248016,"HyperDash":false},{"StartTime":244625.0,"Position":495.0,"HyperDash":false}]},{"StartTime":244762.0,"Objects":[{"StartTime":244762.0,"Position":426.0,"HyperDash":false}]},{"StartTime":244830.0,"Objects":[{"StartTime":244830.0,"Position":428.0,"HyperDash":false}]},{"StartTime":244898.0,"Objects":[{"StartTime":244898.0,"Position":430.0,"HyperDash":false}]},{"StartTime":245034.0,"Objects":[{"StartTime":245034.0,"Position":370.0,"HyperDash":false},{"StartTime":245102.0,"Position":328.1226,"HyperDash":false},{"StartTime":245170.0,"Position":370.0,"HyperDash":false},{"StartTime":245238.0,"Position":328.1226,"HyperDash":false}]},{"StartTime":245307.0,"Objects":[{"StartTime":245307.0,"Position":331.0,"HyperDash":false},{"StartTime":245375.0,"Position":289.1226,"HyperDash":false},{"StartTime":245443.0,"Position":331.0,"HyperDash":false},{"StartTime":245511.0,"Position":289.1226,"HyperDash":false}]},{"StartTime":245579.0,"Objects":[{"StartTime":245579.0,"Position":229.0,"HyperDash":false},{"StartTime":245647.0,"Position":235.986954,"HyperDash":false}]},{"StartTime":245716.0,"Objects":[{"StartTime":245716.0,"Position":140.0,"HyperDash":false},{"StartTime":245784.0,"Position":146.986954,"HyperDash":false}]},{"StartTime":245853.0,"Objects":[{"StartTime":245853.0,"Position":50.0,"HyperDash":false},{"StartTime":245921.0,"Position":56.9869576,"HyperDash":false},{"StartTime":245989.0,"Position":50.0,"HyperDash":false},{"StartTime":246057.0,"Position":56.9869576,"HyperDash":false}]},{"StartTime":246124.0,"Objects":[{"StartTime":246124.0,"Position":120.0,"HyperDash":false}]},{"StartTime":246193.0,"Objects":[{"StartTime":246193.0,"Position":122.0,"HyperDash":false}]},{"StartTime":246261.0,"Objects":[{"StartTime":246261.0,"Position":124.0,"HyperDash":false}]},{"StartTime":246397.0,"Objects":[{"StartTime":246397.0,"Position":171.0,"HyperDash":false}]},{"StartTime":246465.0,"Objects":[{"StartTime":246465.0,"Position":173.0,"HyperDash":false}]},{"StartTime":246533.0,"Objects":[{"StartTime":246533.0,"Position":175.0,"HyperDash":false}]},{"StartTime":246670.0,"Objects":[{"StartTime":246670.0,"Position":123.0,"HyperDash":false}]},{"StartTime":246738.0,"Objects":[{"StartTime":246738.0,"Position":125.0,"HyperDash":false}]},{"StartTime":246806.0,"Objects":[{"StartTime":246806.0,"Position":127.0,"HyperDash":false},{"StartTime":246942.0,"Position":118.059486,"HyperDash":false}]},{"StartTime":247215.0,"Objects":[{"StartTime":247215.0,"Position":289.0,"HyperDash":false},{"StartTime":247283.0,"Position":330.8774,"HyperDash":false},{"StartTime":247351.0,"Position":289.0,"HyperDash":false},{"StartTime":247419.0,"Position":330.8774,"HyperDash":false}]},{"StartTime":247488.0,"Objects":[{"StartTime":247488.0,"Position":306.0,"HyperDash":false},{"StartTime":247556.0,"Position":347.8774,"HyperDash":false},{"StartTime":247624.0,"Position":306.0,"HyperDash":false},{"StartTime":247692.0,"Position":347.8774,"HyperDash":false}]},{"StartTime":247761.0,"Objects":[{"StartTime":247761.0,"Position":440.0,"HyperDash":false},{"StartTime":247829.0,"Position":447.248016,"HyperDash":false}]},{"StartTime":247897.0,"Objects":[{"StartTime":247897.0,"Position":425.0,"HyperDash":false},{"StartTime":247965.0,"Position":432.248016,"HyperDash":false}]},{"StartTime":248033.0,"Objects":[{"StartTime":248033.0,"Position":410.0,"HyperDash":false},{"StartTime":248101.0,"Position":417.248016,"HyperDash":false},{"StartTime":248169.0,"Position":410.0,"HyperDash":false},{"StartTime":248237.0,"Position":417.248016,"HyperDash":false}]},{"StartTime":248306.0,"Objects":[{"StartTime":248306.0,"Position":346.0,"HyperDash":false},{"StartTime":248374.0,"Position":304.1226,"HyperDash":false},{"StartTime":248442.0,"Position":346.0,"HyperDash":false}]},{"StartTime":248579.0,"Objects":[{"StartTime":248579.0,"Position":287.0,"HyperDash":false}]},{"StartTime":248647.0,"Objects":[{"StartTime":248647.0,"Position":279.0,"HyperDash":false}]},{"StartTime":248715.0,"Objects":[{"StartTime":248715.0,"Position":271.0,"HyperDash":false}]},{"StartTime":248852.0,"Objects":[{"StartTime":248852.0,"Position":193.0,"HyperDash":false},{"StartTime":248920.0,"Position":151.1226,"HyperDash":false},{"StartTime":248988.0,"Position":193.0,"HyperDash":false}]},{"StartTime":249124.0,"Objects":[{"StartTime":249124.0,"Position":139.0,"HyperDash":false}]},{"StartTime":249194.0,"Objects":[{"StartTime":249194.0,"Position":131.0,"HyperDash":false}]},{"StartTime":249261.0,"Objects":[{"StartTime":249261.0,"Position":123.0,"HyperDash":false}]},{"StartTime":249397.0,"Objects":[{"StartTime":249397.0,"Position":53.0,"HyperDash":false},{"StartTime":249465.0,"Position":60.2480125,"HyperDash":false},{"StartTime":249533.0,"Position":53.0,"HyperDash":false},{"StartTime":249601.0,"Position":60.2480125,"HyperDash":false}]},{"StartTime":249670.0,"Objects":[{"StartTime":249670.0,"Position":0.0,"HyperDash":false},{"StartTime":249738.0,"Position":7.952265,"HyperDash":false},{"StartTime":249806.0,"Position":0.0,"HyperDash":false},{"StartTime":249874.0,"Position":7.952265,"HyperDash":false}]},{"StartTime":249943.0,"Objects":[{"StartTime":249943.0,"Position":41.0,"HyperDash":false},{"StartTime":250011.0,"Position":0.0,"HyperDash":false}]},{"StartTime":250079.0,"Objects":[{"StartTime":250079.0,"Position":127.0,"HyperDash":false},{"StartTime":250147.0,"Position":85.1226044,"HyperDash":false}]},{"StartTime":250215.0,"Objects":[{"StartTime":250215.0,"Position":212.0,"HyperDash":false},{"StartTime":250283.0,"Position":170.1226,"HyperDash":false},{"StartTime":250351.0,"Position":212.0,"HyperDash":false},{"StartTime":250419.0,"Position":170.1226,"HyperDash":false}]},{"StartTime":250488.0,"Objects":[{"StartTime":250488.0,"Position":210.0,"HyperDash":false}]},{"StartTime":250556.0,"Objects":[{"StartTime":250556.0,"Position":212.0,"HyperDash":false}]},{"StartTime":250624.0,"Objects":[{"StartTime":250624.0,"Position":214.0,"HyperDash":false}]},{"StartTime":250761.0,"Objects":[{"StartTime":250761.0,"Position":295.0,"HyperDash":false}]},{"StartTime":250829.0,"Objects":[{"StartTime":250829.0,"Position":293.0,"HyperDash":false}]},{"StartTime":250898.0,"Objects":[{"StartTime":250898.0,"Position":291.0,"HyperDash":false}]},{"StartTime":251033.0,"Objects":[{"StartTime":251033.0,"Position":235.0,"HyperDash":false}]},{"StartTime":251102.0,"Objects":[{"StartTime":251102.0,"Position":237.0,"HyperDash":false}]},{"StartTime":251170.0,"Objects":[{"StartTime":251170.0,"Position":239.0,"HyperDash":false},{"StartTime":251238.0,"Position":231.8979,"HyperDash":false},{"StartTime":251306.0,"Position":239.0,"HyperDash":false},{"StartTime":251374.0,"Position":231.8979,"HyperDash":false},{"StartTime":251442.0,"Position":239.0,"HyperDash":false},{"StartTime":251510.0,"Position":231.8979,"HyperDash":false}]},{"StartTime":251579.0,"Objects":[{"StartTime":251579.0,"Position":229.0,"HyperDash":false},{"StartTime":251715.0,"Position":317.623718,"HyperDash":false}]},{"StartTime":251852.0,"Objects":[{"StartTime":251852.0,"Position":475.0,"HyperDash":false},{"StartTime":251988.0,"Position":386.889038,"HyperDash":false}]},{"StartTime":252124.0,"Objects":[{"StartTime":252124.0,"Position":440.0,"HyperDash":false},{"StartTime":252260.0,"Position":463.840118,"HyperDash":false}]},{"StartTime":252397.0,"Objects":[{"StartTime":252397.0,"Position":297.0,"HyperDash":false},{"StartTime":252533.0,"Position":319.863068,"HyperDash":false}]},{"StartTime":252670.0,"Objects":[{"StartTime":252670.0,"Position":205.0,"HyperDash":false},{"StartTime":252806.0,"Position":105.595367,"HyperDash":false}]},{"StartTime":252942.0,"Objects":[{"StartTime":252942.0,"Position":42.0,"HyperDash":false}]},{"StartTime":253079.0,"Objects":[{"StartTime":253079.0,"Position":42.0,"HyperDash":false}]},{"StartTime":253215.0,"Objects":[{"StartTime":253215.0,"Position":1.0,"HyperDash":false},{"StartTime":253351.0,"Position":97.26073,"HyperDash":false}]},{"StartTime":253488.0,"Objects":[{"StartTime":253488.0,"Position":248.0,"HyperDash":false},{"StartTime":253624.0,"Position":148.595367,"HyperDash":true}]},{"StartTime":253760.0,"Objects":[{"StartTime":253760.0,"Position":408.0,"HyperDash":false},{"StartTime":253896.0,"Position":487.4551,"HyperDash":false}]},{"StartTime":254033.0,"Objects":[{"StartTime":254033.0,"Position":318.0,"HyperDash":false},{"StartTime":254169.0,"Position":309.7604,"HyperDash":false}]},{"StartTime":254306.0,"Objects":[{"StartTime":254306.0,"Position":202.0,"HyperDash":false}]},{"StartTime":254442.0,"Objects":[{"StartTime":254442.0,"Position":295.0,"HyperDash":false}]},{"StartTime":254510.0,"Objects":[{"StartTime":254510.0,"Position":295.0,"HyperDash":false}]},{"StartTime":254579.0,"Objects":[{"StartTime":254579.0,"Position":295.0,"HyperDash":false},{"StartTime":254715.0,"Position":395.898743,"HyperDash":false}]},{"StartTime":254851.0,"Objects":[{"StartTime":254851.0,"Position":486.0,"HyperDash":false}]},{"StartTime":254987.0,"Objects":[{"StartTime":254987.0,"Position":423.0,"HyperDash":false}]},{"StartTime":255124.0,"Objects":[{"StartTime":255124.0,"Position":424.0,"HyperDash":false}]},{"StartTime":255260.0,"Objects":[{"StartTime":255260.0,"Position":487.0,"HyperDash":false}]},{"StartTime":255397.0,"Objects":[{"StartTime":255397.0,"Position":412.0,"HyperDash":false},{"StartTime":255456.0,"Position":367.364532,"HyperDash":false},{"StartTime":255515.0,"Position":308.7291,"HyperDash":false},{"StartTime":255574.0,"Position":291.225,"HyperDash":false},{"StartTime":255669.0,"Position":215.507538,"HyperDash":false}]},{"StartTime":255806.0,"Objects":[{"StartTime":255806.0,"Position":80.0,"HyperDash":false}]},{"StartTime":255874.0,"Objects":[{"StartTime":255874.0,"Position":87.0,"HyperDash":false}]},{"StartTime":255942.0,"Objects":[{"StartTime":255942.0,"Position":94.0,"HyperDash":false},{"StartTime":256078.0,"Position":115.948105,"HyperDash":false}]},{"StartTime":256215.0,"Objects":[{"StartTime":256215.0,"Position":14.0,"HyperDash":false},{"StartTime":256351.0,"Position":35.94811,"HyperDash":false}]},{"StartTime":256488.0,"Objects":[{"StartTime":256488.0,"Position":172.0,"HyperDash":false},{"StartTime":256624.0,"Position":263.280975,"HyperDash":false}]},{"StartTime":256760.0,"Objects":[{"StartTime":256760.0,"Position":238.0,"HyperDash":false},{"StartTime":256896.0,"Position":146.7498,"HyperDash":false}]},{"StartTime":257033.0,"Objects":[{"StartTime":257033.0,"Position":115.0,"HyperDash":false},{"StartTime":257169.0,"Position":205.031708,"HyperDash":false}]},{"StartTime":257306.0,"Objects":[{"StartTime":257306.0,"Position":342.0,"HyperDash":false}]},{"StartTime":257442.0,"Objects":[{"StartTime":257442.0,"Position":342.0,"HyperDash":false}]},{"StartTime":257579.0,"Objects":[{"StartTime":257579.0,"Position":455.0,"HyperDash":false},{"StartTime":257715.0,"Position":467.65155,"HyperDash":false}]},{"StartTime":257851.0,"Objects":[{"StartTime":257851.0,"Position":381.0,"HyperDash":false},{"StartTime":257987.0,"Position":393.65155,"HyperDash":false}]},{"StartTime":258124.0,"Objects":[{"StartTime":258124.0,"Position":267.0,"HyperDash":false},{"StartTime":258260.0,"Position":183.076477,"HyperDash":false}]},{"StartTime":258397.0,"Objects":[{"StartTime":258397.0,"Position":95.0,"HyperDash":false},{"StartTime":258533.0,"Position":11.07647,"HyperDash":false}]},{"StartTime":258670.0,"Objects":[{"StartTime":258670.0,"Position":101.0,"HyperDash":false}]},{"StartTime":258806.0,"Objects":[{"StartTime":258806.0,"Position":22.0,"HyperDash":false}]},{"StartTime":258874.0,"Objects":[{"StartTime":258874.0,"Position":22.0,"HyperDash":false}]},{"StartTime":258942.0,"Objects":[{"StartTime":258942.0,"Position":22.0,"HyperDash":false},{"StartTime":259078.0,"Position":5.65008163,"HyperDash":false}]},{"StartTime":259215.0,"Objects":[{"StartTime":259215.0,"Position":158.0,"HyperDash":false}]},{"StartTime":259283.0,"Objects":[{"StartTime":259283.0,"Position":197.0,"HyperDash":false}]},{"StartTime":259351.0,"Objects":[{"StartTime":259351.0,"Position":239.0,"HyperDash":false}]},{"StartTime":259419.0,"Objects":[{"StartTime":259419.0,"Position":273.0,"HyperDash":false}]},{"StartTime":259487.0,"Objects":[{"StartTime":259487.0,"Position":291.0,"HyperDash":false}]},{"StartTime":259624.0,"Objects":[{"StartTime":259624.0,"Position":405.0,"HyperDash":false}]},{"StartTime":259692.0,"Objects":[{"StartTime":259692.0,"Position":415.0,"HyperDash":false}]},{"StartTime":259761.0,"Objects":[{"StartTime":259761.0,"Position":425.0,"HyperDash":false},{"StartTime":259829.0,"Position":436.342346,"HyperDash":false},{"StartTime":259897.0,"Position":425.0,"HyperDash":false}]},{"StartTime":260033.0,"Objects":[{"StartTime":260033.0,"Position":355.0,"HyperDash":false},{"StartTime":260101.0,"Position":366.342346,"HyperDash":false},{"StartTime":260169.0,"Position":355.0,"HyperDash":false},{"StartTime":260237.0,"Position":366.342346,"HyperDash":false}]},{"StartTime":260306.0,"Objects":[{"StartTime":260306.0,"Position":376.0,"HyperDash":false},{"StartTime":260442.0,"Position":287.376282,"HyperDash":false}]},{"StartTime":260578.0,"Objects":[{"StartTime":260578.0,"Position":112.0,"HyperDash":false},{"StartTime":260714.0,"Position":200.110962,"HyperDash":false}]},{"StartTime":260851.0,"Objects":[{"StartTime":260851.0,"Position":240.0,"HyperDash":false},{"StartTime":260987.0,"Position":140.825165,"HyperDash":false}]},{"StartTime":261124.0,"Objects":[{"StartTime":261124.0,"Position":1.0,"HyperDash":false},{"StartTime":261260.0,"Position":100.404633,"HyperDash":false}]},{"StartTime":261397.0,"Objects":[{"StartTime":261397.0,"Position":296.0,"HyperDash":false},{"StartTime":261533.0,"Position":196.595367,"HyperDash":false}]},{"StartTime":261669.0,"Objects":[{"StartTime":261669.0,"Position":324.0,"HyperDash":false}]},{"StartTime":261806.0,"Objects":[{"StartTime":261806.0,"Position":324.0,"HyperDash":false}]},{"StartTime":261942.0,"Objects":[{"StartTime":261942.0,"Position":445.0,"HyperDash":false},{"StartTime":262078.0,"Position":460.350983,"HyperDash":false}]},{"StartTime":262215.0,"Objects":[{"StartTime":262215.0,"Position":360.0,"HyperDash":false},{"StartTime":262351.0,"Position":456.028931,"HyperDash":false}]},{"StartTime":262487.0,"Objects":[{"StartTime":262487.0,"Position":274.0,"HyperDash":false},{"StartTime":262623.0,"Position":194.151871,"HyperDash":false}]},{"StartTime":262760.0,"Objects":[{"StartTime":262760.0,"Position":38.0,"HyperDash":false},{"StartTime":262896.0,"Position":125.37175,"HyperDash":false}]},{"StartTime":263033.0,"Objects":[{"StartTime":263033.0,"Position":194.0,"HyperDash":false}]},{"StartTime":263169.0,"Objects":[{"StartTime":263169.0,"Position":312.0,"HyperDash":false}]},{"StartTime":263237.0,"Objects":[{"StartTime":263237.0,"Position":312.0,"HyperDash":false}]},{"StartTime":263306.0,"Objects":[{"StartTime":263306.0,"Position":312.0,"HyperDash":false},{"StartTime":263442.0,"Position":412.898743,"HyperDash":false}]},{"StartTime":263578.0,"Objects":[{"StartTime":263578.0,"Position":503.0,"HyperDash":false}]},{"StartTime":263714.0,"Objects":[{"StartTime":263714.0,"Position":456.0,"HyperDash":false}]},{"StartTime":263851.0,"Objects":[{"StartTime":263851.0,"Position":367.0,"HyperDash":false}]},{"StartTime":263987.0,"Objects":[{"StartTime":263987.0,"Position":292.0,"HyperDash":false}]},{"StartTime":264124.0,"Objects":[{"StartTime":264124.0,"Position":206.0,"HyperDash":false},{"StartTime":264183.0,"Position":158.319275,"HyperDash":false},{"StartTime":264242.0,"Position":120.702431,"HyperDash":false},{"StartTime":264301.0,"Position":92.96848,"HyperDash":false},{"StartTime":264396.0,"Position":18.7677212,"HyperDash":false}]},{"StartTime":264533.0,"Objects":[{"StartTime":264533.0,"Position":173.0,"HyperDash":false}]},{"StartTime":264601.0,"Objects":[{"StartTime":264601.0,"Position":166.0,"HyperDash":false}]},{"StartTime":264669.0,"Objects":[{"StartTime":264669.0,"Position":159.0,"HyperDash":false},{"StartTime":264805.0,"Position":137.0519,"HyperDash":false}]},{"StartTime":264942.0,"Objects":[{"StartTime":264942.0,"Position":302.0,"HyperDash":false},{"StartTime":265078.0,"Position":280.834564,"HyperDash":false}]},{"StartTime":265215.0,"Objects":[{"StartTime":265215.0,"Position":399.0,"HyperDash":false},{"StartTime":265351.0,"Position":434.304535,"HyperDash":false}]},{"StartTime":265487.0,"Objects":[{"StartTime":265487.0,"Position":496.0,"HyperDash":false},{"StartTime":265623.0,"Position":404.622,"HyperDash":false}]},{"StartTime":265760.0,"Objects":[{"StartTime":265760.0,"Position":362.0,"HyperDash":false},{"StartTime":265896.0,"Position":452.0317,"HyperDash":false}]},{"StartTime":266033.0,"Objects":[{"StartTime":266033.0,"Position":288.0,"HyperDash":false}]},{"StartTime":266169.0,"Objects":[{"StartTime":266169.0,"Position":288.0,"HyperDash":false}]},{"StartTime":266306.0,"Objects":[{"StartTime":266306.0,"Position":171.0,"HyperDash":false},{"StartTime":266442.0,"Position":158.34845,"HyperDash":false}]},{"StartTime":266578.0,"Objects":[{"StartTime":266578.0,"Position":251.0,"HyperDash":false},{"StartTime":266714.0,"Position":238.34845,"HyperDash":false}]},{"StartTime":266851.0,"Objects":[{"StartTime":266851.0,"Position":56.0,"HyperDash":false},{"StartTime":266987.0,"Position":104.910339,"HyperDash":false}]},{"StartTime":267124.0,"Objects":[{"StartTime":267124.0,"Position":35.0,"HyperDash":false},{"StartTime":267260.0,"Position":33.814888,"HyperDash":false}]},{"StartTime":267397.0,"Objects":[{"StartTime":267397.0,"Position":123.0,"HyperDash":false}]},{"StartTime":267533.0,"Objects":[{"StartTime":267533.0,"Position":253.0,"HyperDash":false}]},{"StartTime":267601.0,"Objects":[{"StartTime":267601.0,"Position":253.0,"HyperDash":false}]},{"StartTime":267669.0,"Objects":[{"StartTime":267669.0,"Position":253.0,"HyperDash":false},{"StartTime":267805.0,"Position":353.6811,"HyperDash":false}]},{"StartTime":267942.0,"Objects":[{"StartTime":267942.0,"Position":463.0,"HyperDash":false}]},{"StartTime":268010.0,"Objects":[{"StartTime":268010.0,"Position":489.0,"HyperDash":false}]},{"StartTime":268078.0,"Objects":[{"StartTime":268078.0,"Position":498.0,"HyperDash":false}]},{"StartTime":268146.0,"Objects":[{"StartTime":268146.0,"Position":485.0,"HyperDash":false}]},{"StartTime":268214.0,"Objects":[{"StartTime":268214.0,"Position":455.0,"HyperDash":false}]},{"StartTime":268352.0,"Objects":[{"StartTime":268352.0,"Position":419.0,"HyperDash":false}]},{"StartTime":268420.0,"Objects":[{"StartTime":268420.0,"Position":403.0,"HyperDash":false}]},{"StartTime":268488.0,"Objects":[{"StartTime":268488.0,"Position":372.0,"HyperDash":false}]},{"StartTime":268556.0,"Objects":[{"StartTime":268556.0,"Position":332.0,"HyperDash":false}]},{"StartTime":268624.0,"Objects":[{"StartTime":268624.0,"Position":292.0,"HyperDash":false}]},{"StartTime":268761.0,"Objects":[{"StartTime":268761.0,"Position":231.0,"HyperDash":false},{"StartTime":268829.0,"Position":180.408112,"HyperDash":false},{"StartTime":268897.0,"Position":231.0,"HyperDash":false},{"StartTime":268965.0,"Position":180.408112,"HyperDash":false}]},{"StartTime":269033.0,"Objects":[{"StartTime":269033.0,"Position":96.0,"HyperDash":false},{"StartTime":269099.0,"Position":107.997719,"HyperDash":false},{"StartTime":269166.0,"Position":130.581879,"HyperDash":false},{"StartTime":269232.0,"Position":149.897186,"HyperDash":false},{"StartTime":269299.0,"Position":175.084061,"HyperDash":false},{"StartTime":269365.0,"Position":167.6238,"HyperDash":false},{"StartTime":269432.0,"Position":173.461578,"HyperDash":false},{"StartTime":269498.0,"Position":185.410263,"HyperDash":false},{"StartTime":269565.0,"Position":178.44928,"HyperDash":false},{"StartTime":269655.0,"Position":167.081726,"HyperDash":false},{"StartTime":269746.0,"Position":170.346115,"HyperDash":false},{"StartTime":269837.0,"Position":137.438,"HyperDash":false},{"StartTime":269964.0,"Position":125.546143,"HyperDash":false}]},{"StartTime":270097.0,"Objects":[{"StartTime":270097.0,"Position":121.0,"HyperDash":false},{"StartTime":270163.0,"Position":78.13265,"HyperDash":false},{"StartTime":270230.0,"Position":95.43977,"HyperDash":false},{"StartTime":270296.0,"Position":65.59505,"HyperDash":false},{"StartTime":270363.0,"Position":71.33265,"HyperDash":false},{"StartTime":270429.0,"Position":73.41984,"HyperDash":false},{"StartTime":270496.0,"Position":98.806366,"HyperDash":false},{"StartTime":270562.0,"Position":139.458054,"HyperDash":false},{"StartTime":270629.0,"Position":162.000076,"HyperDash":false},{"StartTime":270686.0,"Position":174.872726,"HyperDash":false},{"StartTime":270744.0,"Position":199.77951,"HyperDash":false},{"StartTime":270801.0,"Position":218.731812,"HyperDash":false},{"StartTime":270895.0,"Position":252.733368,"HyperDash":false}]},{"StartTime":271028.0,"Objects":[{"StartTime":271028.0,"Position":319.0,"HyperDash":false}]},{"StartTime":271161.0,"Objects":[{"StartTime":271161.0,"Position":312.0,"HyperDash":false},{"StartTime":271223.0,"Position":302.2162,"HyperDash":false},{"StartTime":271285.0,"Position":302.676941,"HyperDash":false},{"StartTime":271347.0,"Position":283.679169,"HyperDash":false},{"StartTime":271409.0,"Position":290.484436,"HyperDash":false},{"StartTime":271471.0,"Position":288.101379,"HyperDash":false},{"StartTime":271533.0,"Position":295.433258,"HyperDash":false},{"StartTime":271595.0,"Position":306.336884,"HyperDash":false},{"StartTime":271693.0,"Position":324.652863,"HyperDash":false}]},{"StartTime":271959.0,"Objects":[{"StartTime":271959.0,"Position":400.0,"HyperDash":false}]},{"StartTime":272225.0,"Objects":[{"StartTime":272225.0,"Position":400.0,"HyperDash":false},{"StartTime":272313.0,"Position":405.1424,"HyperDash":false},{"StartTime":272402.0,"Position":408.331879,"HyperDash":false},{"StartTime":272472.0,"Position":402.036774,"HyperDash":false},{"StartTime":272579.0,"Position":400.0,"HyperDash":false}]},{"StartTime":272758.0,"Objects":[{"StartTime":272758.0,"Position":442.0,"HyperDash":false},{"StartTime":272846.0,"Position":459.1424,"HyperDash":false},{"StartTime":272935.0,"Position":450.331879,"HyperDash":false},{"StartTime":273005.0,"Position":454.036774,"HyperDash":false},{"StartTime":273112.0,"Position":442.0,"HyperDash":false}]},{"StartTime":273290.0,"Objects":[{"StartTime":273290.0,"Position":512.0,"HyperDash":false},{"StartTime":273355.0,"Position":498.977875,"HyperDash":false},{"StartTime":273420.0,"Position":478.2446,"HyperDash":false},{"StartTime":273485.0,"Position":437.965363,"HyperDash":false},{"StartTime":273551.0,"Position":433.034943,"HyperDash":false},{"StartTime":273616.0,"Position":428.07312,"HyperDash":false},{"StartTime":273681.0,"Position":423.756653,"HyperDash":false},{"StartTime":273746.0,"Position":401.5979,"HyperDash":false},{"StartTime":273848.0,"Position":401.115448,"HyperDash":false}]},{"StartTime":274048.0,"Objects":[{"StartTime":274048.0,"Position":303.0,"HyperDash":false},{"StartTime":274129.0,"Position":308.57135,"HyperDash":false},{"StartTime":274247.0,"Position":297.033356,"HyperDash":false}]},{"StartTime":274498.0,"Objects":[{"StartTime":274498.0,"Position":202.0,"HyperDash":false},{"StartTime":274560.0,"Position":191.209839,"HyperDash":false},{"StartTime":274622.0,"Position":190.373,"HyperDash":false},{"StartTime":274747.0,"Position":202.0,"HyperDash":false}]},{"StartTime":274873.0,"Objects":[{"StartTime":274873.0,"Position":105.0,"HyperDash":false},{"StartTime":274939.0,"Position":120.3023,"HyperDash":false},{"StartTime":275006.0,"Position":107.624329,"HyperDash":false},{"StartTime":275139.0,"Position":105.0,"HyperDash":false}]},{"StartTime":275273.0,"Objects":[{"StartTime":275273.0,"Position":31.0,"HyperDash":false},{"StartTime":275349.0,"Position":42.15374,"HyperDash":false},{"StartTime":275426.0,"Position":47.4684143,"HyperDash":false},{"StartTime":275485.0,"Position":28.1921768,"HyperDash":false},{"StartTime":275580.0,"Position":31.0,"HyperDash":false}]},{"StartTime":275734.0,"Objects":[{"StartTime":275734.0,"Position":0.0,"HyperDash":false},{"StartTime":275813.0,"Position":0.0,"HyperDash":false},{"StartTime":275893.0,"Position":25.7255154,"HyperDash":false},{"StartTime":275955.0,"Position":16.8062725,"HyperDash":false},{"StartTime":276053.0,"Position":0.0,"HyperDash":false}]},{"StartTime":276254.0,"Objects":[{"StartTime":276254.0,"Position":21.0,"HyperDash":false}]},{"StartTime":276419.0,"Objects":[{"StartTime":276419.0,"Position":354.0,"HyperDash":false},{"StartTime":276494.0,"Position":270.0,"HyperDash":false},{"StartTime":276569.0,"Position":362.0,"HyperDash":false},{"StartTime":276645.0,"Position":255.0,"HyperDash":false},{"StartTime":276720.0,"Position":203.0,"HyperDash":false},{"StartTime":276795.0,"Position":67.0,"HyperDash":false},{"StartTime":276871.0,"Position":112.0,"HyperDash":false},{"StartTime":276946.0,"Position":326.0,"HyperDash":false},{"StartTime":277021.0,"Position":219.0,"HyperDash":false},{"StartTime":277097.0,"Position":351.0,"HyperDash":false},{"StartTime":277172.0,"Position":477.0,"HyperDash":false},{"StartTime":277247.0,"Position":439.0,"HyperDash":false},{"StartTime":277323.0,"Position":471.0,"HyperDash":false},{"StartTime":277398.0,"Position":449.0,"HyperDash":false},{"StartTime":277473.0,"Position":295.0,"HyperDash":false},{"StartTime":277549.0,"Position":217.0,"HyperDash":false},{"StartTime":277624.0,"Position":308.0,"HyperDash":false},{"StartTime":277699.0,"Position":430.0,"HyperDash":false},{"StartTime":277775.0,"Position":73.0,"HyperDash":false},{"StartTime":277850.0,"Position":53.0,"HyperDash":false},{"StartTime":277925.0,"Position":276.0,"HyperDash":false},{"StartTime":278001.0,"Position":289.0,"HyperDash":false},{"StartTime":278076.0,"Position":104.0,"HyperDash":false},{"StartTime":278151.0,"Position":212.0,"HyperDash":false},{"StartTime":278227.0,"Position":359.0,"HyperDash":false},{"StartTime":278302.0,"Position":500.0,"HyperDash":false},{"StartTime":278377.0,"Position":467.0,"HyperDash":false},{"StartTime":278453.0,"Position":303.0,"HyperDash":false},{"StartTime":278528.0,"Position":29.0,"HyperDash":false},{"StartTime":278603.0,"Position":482.0,"HyperDash":false},{"StartTime":278679.0,"Position":379.0,"HyperDash":false},{"StartTime":278754.0,"Position":93.0,"HyperDash":false},{"StartTime":278830.0,"Position":266.0,"HyperDash":false},{"StartTime":278905.0,"Position":342.0,"HyperDash":false},{"StartTime":278980.0,"Position":423.0,"HyperDash":false},{"StartTime":279056.0,"Position":190.0,"HyperDash":false},{"StartTime":279131.0,"Position":266.0,"HyperDash":false},{"StartTime":279206.0,"Position":56.0,"HyperDash":false},{"StartTime":279282.0,"Position":164.0,"HyperDash":false},{"StartTime":279357.0,"Position":44.0,"HyperDash":false},{"StartTime":279432.0,"Position":68.0,"HyperDash":false},{"StartTime":279508.0,"Position":476.0,"HyperDash":false},{"StartTime":279583.0,"Position":237.0,"HyperDash":false},{"StartTime":279658.0,"Position":146.0,"HyperDash":false},{"StartTime":279734.0,"Position":99.0,"HyperDash":false},{"StartTime":279809.0,"Position":52.0,"HyperDash":false},{"StartTime":279884.0,"Position":294.0,"HyperDash":false},{"StartTime":279960.0,"Position":346.0,"HyperDash":false},{"StartTime":280035.0,"Position":256.0,"HyperDash":false},{"StartTime":280110.0,"Position":353.0,"HyperDash":false},{"StartTime":280186.0,"Position":85.0,"HyperDash":false},{"StartTime":280261.0,"Position":473.0,"HyperDash":false},{"StartTime":280336.0,"Position":55.0,"HyperDash":false},{"StartTime":280412.0,"Position":158.0,"HyperDash":false},{"StartTime":280487.0,"Position":97.0,"HyperDash":false},{"StartTime":280562.0,"Position":288.0,"HyperDash":false},{"StartTime":280638.0,"Position":236.0,"HyperDash":false},{"StartTime":280713.0,"Position":226.0,"HyperDash":false},{"StartTime":280788.0,"Position":317.0,"HyperDash":false},{"StartTime":280864.0,"Position":227.0,"HyperDash":false},{"StartTime":280939.0,"Position":507.0,"HyperDash":false},{"StartTime":281014.0,"Position":144.0,"HyperDash":false},{"StartTime":281090.0,"Position":409.0,"HyperDash":false},{"StartTime":281165.0,"Position":76.0,"HyperDash":false},{"StartTime":281241.0,"Position":193.0,"HyperDash":false},{"StartTime":281316.0,"Position":456.0,"HyperDash":false},{"StartTime":281391.0,"Position":161.0,"HyperDash":false},{"StartTime":281467.0,"Position":417.0,"HyperDash":false},{"StartTime":281542.0,"Position":157.0,"HyperDash":false},{"StartTime":281617.0,"Position":464.0,"HyperDash":false},{"StartTime":281693.0,"Position":462.0,"HyperDash":false},{"StartTime":281768.0,"Position":254.0,"HyperDash":false},{"StartTime":281843.0,"Position":103.0,"HyperDash":false},{"StartTime":281919.0,"Position":125.0,"HyperDash":false},{"StartTime":281994.0,"Position":485.0,"HyperDash":false},{"StartTime":282069.0,"Position":350.0,"HyperDash":false},{"StartTime":282145.0,"Position":206.0,"HyperDash":false},{"StartTime":282220.0,"Position":285.0,"HyperDash":false},{"StartTime":282295.0,"Position":390.0,"HyperDash":false},{"StartTime":282371.0,"Position":463.0,"HyperDash":false},{"StartTime":282446.0,"Position":447.0,"HyperDash":false},{"StartTime":282521.0,"Position":126.0,"HyperDash":false},{"StartTime":282597.0,"Position":44.0,"HyperDash":false},{"StartTime":282672.0,"Position":451.0,"HyperDash":false},{"StartTime":282747.0,"Position":278.0,"HyperDash":false},{"StartTime":282823.0,"Position":24.0,"HyperDash":false},{"StartTime":282898.0,"Position":367.0,"HyperDash":false},{"StartTime":282973.0,"Position":221.0,"HyperDash":false},{"StartTime":283049.0,"Position":439.0,"HyperDash":false},{"StartTime":283124.0,"Position":243.0,"HyperDash":false},{"StartTime":283199.0,"Position":213.0,"HyperDash":false},{"StartTime":283275.0,"Position":120.0,"HyperDash":false},{"StartTime":283350.0,"Position":379.0,"HyperDash":false},{"StartTime":283425.0,"Position":353.0,"HyperDash":false},{"StartTime":283501.0,"Position":496.0,"HyperDash":false},{"StartTime":283576.0,"Position":288.0,"HyperDash":false},{"StartTime":283652.0,"Position":163.0,"HyperDash":false},{"StartTime":283727.0,"Position":314.0,"HyperDash":false},{"StartTime":283802.0,"Position":296.0,"HyperDash":false},{"StartTime":283878.0,"Position":488.0,"HyperDash":false},{"StartTime":283953.0,"Position":482.0,"HyperDash":false},{"StartTime":284028.0,"Position":321.0,"HyperDash":false},{"StartTime":284104.0,"Position":474.0,"HyperDash":false},{"StartTime":284179.0,"Position":252.0,"HyperDash":false},{"StartTime":284254.0,"Position":247.0,"HyperDash":false},{"StartTime":284330.0,"Position":406.0,"HyperDash":false},{"StartTime":284405.0,"Position":319.0,"HyperDash":false},{"StartTime":284480.0,"Position":253.0,"HyperDash":false},{"StartTime":284556.0,"Position":411.0,"HyperDash":false},{"StartTime":284631.0,"Position":205.0,"HyperDash":false},{"StartTime":284706.0,"Position":54.0,"HyperDash":false},{"StartTime":284782.0,"Position":224.0,"HyperDash":false},{"StartTime":284857.0,"Position":465.0,"HyperDash":false},{"StartTime":284932.0,"Position":432.0,"HyperDash":false},{"StartTime":285008.0,"Position":108.0,"HyperDash":false},{"StartTime":285083.0,"Position":95.0,"HyperDash":false},{"StartTime":285158.0,"Position":436.0,"HyperDash":false},{"StartTime":285234.0,"Position":61.0,"HyperDash":false},{"StartTime":285309.0,"Position":234.0,"HyperDash":false},{"StartTime":285384.0,"Position":394.0,"HyperDash":false},{"StartTime":285460.0,"Position":86.0,"HyperDash":false},{"StartTime":285535.0,"Position":491.0,"HyperDash":false},{"StartTime":285610.0,"Position":416.0,"HyperDash":false},{"StartTime":285686.0,"Position":44.0,"HyperDash":false},{"StartTime":285761.0,"Position":29.0,"HyperDash":false},{"StartTime":285836.0,"Position":402.0,"HyperDash":false},{"StartTime":285912.0,"Position":115.0,"HyperDash":false},{"StartTime":285987.0,"Position":87.0,"HyperDash":false}]},{"StartTime":286725.0,"Objects":[{"StartTime":286725.0,"Position":80.0,"HyperDash":false},{"StartTime":286776.0,"Position":116.003235,"HyperDash":false},{"StartTime":286827.0,"Position":150.517319,"HyperDash":false},{"StartTime":286878.0,"Position":201.896988,"HyperDash":false},{"StartTime":286930.0,"Position":241.944443,"HyperDash":false},{"StartTime":286981.0,"Position":259.183777,"HyperDash":false},{"StartTime":287032.0,"Position":320.093781,"HyperDash":false},{"StartTime":287084.0,"Position":319.821442,"HyperDash":false},{"StartTime":287135.0,"Position":270.5175,"HyperDash":false},{"StartTime":287186.0,"Position":225.266876,"HyperDash":false},{"StartTime":287238.0,"Position":212.995529,"HyperDash":false},{"StartTime":287289.0,"Position":225.29332,"HyperDash":false},{"StartTime":287340.0,"Position":285.537354,"HyperDash":false},{"StartTime":287392.0,"Position":301.644073,"HyperDash":false},{"StartTime":287443.0,"Position":366.0163,"HyperDash":false},{"StartTime":287494.0,"Position":394.099243,"HyperDash":false},{"StartTime":287582.0,"Position":465.1608,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3644427.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3644427.osu new file mode 100644 index 0000000000..522f8d5a4a --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3644427.osu @@ -0,0 +1,1450 @@ +osu file format v14 + +[General] +StackLeniency: 0.8 +Mode: 0 + +[Difficulty] +HPDrainRate:5 +CircleSize:3 +OverallDifficulty:8 +ApproachRate:9.2 +SliderMultiplier:1.7 +SliderTickRate:1 + +[Events] +//Background and Video events +//Break Periods +2,96220,104148 +2,113675,117239 +2,205476,207343 +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Layer 4 (Overlay) +//Storyboard Sound Samples + +[TimingPoints] +22,272.727272727273,5,2,1,50,1,0 +22,-125,4,2,1,50,0,0 +840,-83.3333333333333,4,2,1,50,0,0 +1385,-125,4,2,1,50,0,0 +2203,-83.3333333333333,4,2,1,50,0,0 +2749,-125,4,2,1,50,0,0 +3567,-83.3333333333333,4,2,1,50,0,0 +4112,-125,4,2,1,50,0,0 +4931,-83.3333333333333,4,2,1,50,0,0 +5476,-125,4,2,1,50,0,0 +6294,-83.3333333333333,4,2,1,50,0,0 +6840,-125,4,2,1,50,0,0 +7658,-83.3333333333333,4,2,1,50,0,0 +8203,-125,4,2,1,50,0,0 +9022,-83.3333333333333,4,2,1,50,0,0 +9567,-125,4,2,1,50,0,0 +10385,-83.3333333333333,4,2,1,50,0,0 +10931,-125,4,2,1,50,0,0 +12021,272.727272727273,4,2,1,70,1,0 +12021,-83.3333333333333,4,2,1,70,0,0 +29475,-100,4,2,1,70,0,0 +38202,-125,4,2,1,50,0,0 +40384,-100,4,2,1,70,0,0 +42566,-125,4,2,1,50,0,0 +44748,-100,4,2,1,70,0,0 +46930,-83.3333333333333,4,2,1,80,0,1 +48702,-83.3333333333333,4,2,2,80,0,1 +48771,-83.3333333333333,4,2,1,80,0,1 +48975,-83.3333333333333,4,2,2,80,0,1 +49043,-83.3333333333333,4,2,1,80,0,1 +53066,-83.3333333333333,4,2,2,80,0,1 +53134,-83.3333333333333,4,2,1,80,0,1 +55657,-100,4,2,1,70,0,0 +64384,-125,4,2,1,60,0,0 +66566,-100,4,2,1,80,0,0 +68748,-125,4,2,1,60,0,0 +70930,-100,4,2,1,80,0,0 +73111,-100,4,2,1,50,0,0 +74202,-100,4,2,3,50,0,0 +74293,-100,4,2,2,50,0,0 +74475,-100,4,2,3,50,0,0 +74566,-100,4,2,2,50,0,0 +74748,-100,4,2,3,50,0,0 +74839,-100,4,2,2,50,0,0 +75021,-100,4,2,3,50,0,0 +75111,-100,4,2,2,50,0,0 +75293,-100,4,2,1,50,0,0 +76384,-100,4,2,4,50,0,0 +76475,-100,4,2,1,50,0,0 +76657,-100,4,2,4,50,0,0 +76748,-100,4,2,1,50,0,0 +76930,-83.3333333333333,4,2,1,55,0,0 +77475,-83.3333333333333,4,2,1,65,0,0 +86202,-100,4,2,1,75,0,0 +96021,-100,4,2,1,40,0,0 +103657,-100,4,2,2,50,0,0 +104202,-100,4,2,1,60,0,0 +104748,-83.3333333333333,4,2,1,80,0,1 +107066,-83.3333333333333,4,2,2,80,0,1 +107134,-83.3333333333333,4,2,1,80,0,1 +107339,-83.3333333333333,4,2,2,80,0,1 +107407,-83.3333333333333,4,2,1,80,0,1 +107611,-83.3333333333333,4,2,2,80,0,1 +107680,-83.3333333333333,4,2,1,80,0,1 +107884,-83.3333333333333,4,2,2,80,0,1 +107952,-83.3333333333333,4,2,1,80,0,1 +108157,-83.3333333333333,4,2,2,80,0,1 +108225,-83.3333333333333,4,2,1,80,0,1 +108430,-83.3333333333333,4,2,2,80,0,1 +108498,-83.3333333333333,4,2,1,80,0,1 +111430,-83.3333333333333,4,2,2,80,0,1 +111498,-83.3333333333333,4,2,1,80,0,1 +111702,-83.3333333333333,4,2,2,80,0,1 +111771,-83.3333333333333,4,2,1,80,0,1 +111975,-83.3333333333333,4,2,2,80,0,1 +112043,-83.3333333333333,4,2,1,80,0,1 +112248,-83.3333333333333,4,2,2,80,0,1 +112316,-83.3333333333333,4,2,1,80,0,1 +113475,-125,4,2,3,50,0,0 +113748,-125,4,2,4,50,0,0 +117839,-125,4,2,3,50,0,0 +117975,-125,4,2,1,50,0,0 +118111,-125,4,2,4,50,0,0 +118248,-125,4,2,1,50,0,0 +118384,-125,4,2,4,50,0,0 +118521,-125,4,2,1,50,0,0 +118657,-125,4,2,4,50,0,0 +118793,-125,4,2,1,50,0,0 +118930,-125,4,2,4,50,0,0 +119066,-125,4,2,1,50,0,0 +119202,-125,4,2,4,50,0,0 +119339,-125,4,2,1,50,0,0 +119475,-125,4,2,4,50,0,0 +119611,-125,4,2,1,50,0,0 +119748,-125,4,2,4,50,0,0 +119884,-125,4,2,1,50,0,0 +120021,-125,4,2,4,50,0,0 +120157,-125,4,2,1,50,0,0 +120293,-125,4,2,4,50,0,0 +120430,-125,4,2,1,50,0,0 +120566,-125,4,2,4,50,0,0 +120702,-125,4,2,1,50,0,0 +120839,-125,4,2,4,50,0,0 +120975,-125,4,2,1,50,0,0 +121111,-125,4,2,4,50,0,0 +121248,-125,4,2,1,50,0,0 +121384,-125,4,2,4,50,0,0 +121521,-125,4,2,1,50,0,0 +121657,-125,4,2,4,50,0,0 +121793,-125,4,2,1,50,0,0 +121930,-125,4,2,4,50,0,0 +122066,-125,4,2,1,50,0,0 +122202,-100,4,2,1,60,0,0 +148384,-83.3333333333333,4,2,1,60,0,0 +149611,-100,4,2,1,60,0,0 +150975,-83.3333333333333,4,2,1,60,0,0 +152066,-100,4,2,1,60,0,0 +156021,-83.3333333333333,4,2,1,60,0,0 +157111,-83.3333333333333,4,2,1,65,0,0 +172384,-83.3333333333333,4,2,3,65,0,0 +172566,-83.3333333333333,4,2,2,65,0,0 +172930,-83.3333333333333,4,2,1,65,0,0 +173067,210.526315789474,4,2,1,65,1,0 +173277,222.222222222222,4,2,1,85,1,0 +173277,-100,4,2,1,85,0,1 +207943,272.727272727273,4,2,1,50,1,1 +207943,-125,4,2,1,50,0,0 +211215,-100,4,2,1,70,0,0 +219943,-100,4,2,1,60,0,0 +223215,-100,4,2,1,80,0,0 +227715,-100,4,2,2,80,0,0 +227783,-100,4,2,1,80,0,0 +227988,-100,4,2,2,80,0,0 +228056,-100,4,2,1,80,0,0 +228261,-100,4,2,2,80,0,0 +228329,-100,4,2,1,80,0,0 +228533,-100,4,2,2,80,0,0 +228602,-100,4,2,1,80,0,0 +229761,-100,4,2,1,50,0,0 +230852,-100,4,2,3,50,0,0 +230943,-100,4,2,2,50,0,0 +231124,-100,4,2,3,50,0,0 +231215,-100,4,2,2,50,0,0 +231397,-100,4,2,3,50,0,0 +231488,-100,4,2,2,50,0,0 +231670,-100,4,2,3,50,0,0 +231761,-100,4,2,2,50,0,0 +231943,-100,4,2,1,50,0,0 +233033,-100,4,2,3,50,0,0 +233124,-100,4,2,1,50,0,0 +233306,-100,4,2,3,50,0,0 +233397,-100,4,2,1,50,0,0 +233579,-83.3333333333333,4,2,1,50,0,0 +234124,-83.3333333333333,4,2,1,65,0,0 +242852,-100,4,2,1,75,0,0 +251579,-83.3333333333333,4,2,1,80,0,1 +253897,-83.3333333333333,4,2,2,80,0,1 +253965,-83.3333333333333,4,2,1,80,0,1 +254170,-83.3333333333333,4,2,2,80,0,1 +254238,-83.3333333333333,4,2,1,80,0,1 +254443,-83.3333333333333,4,2,2,80,0,1 +254511,-83.3333333333333,4,2,1,80,0,1 +254715,-83.3333333333333,4,2,2,80,0,1 +254783,-83.3333333333333,4,2,1,80,0,1 +254988,-83.3333333333333,4,2,2,80,0,1 +255056,-83.3333333333333,4,2,1,80,0,1 +255261,-83.3333333333333,4,2,2,80,0,1 +255329,-83.3333333333333,4,2,1,80,0,1 +258261,-83.3333333333333,4,2,2,80,0,1 +258329,-83.3333333333333,4,2,1,80,0,1 +258533,-83.3333333333333,4,2,2,80,0,1 +258602,-83.3333333333333,4,2,1,80,0,1 +258806,-83.3333333333333,4,2,2,80,0,1 +258874,-83.3333333333333,4,2,1,80,0,1 +259079,-83.3333333333333,4,2,2,80,0,1 +259147,-100,4,2,1,80,0,1 +260033,-100,4,2,1,80,0,1 +260306,-83.3333333333333,4,2,1,90,0,1 +260313,-83.3333333333333,4,2,1,90,0,1 +262624,-83.3333333333333,4,2,2,90,0,1 +262693,-83.3333333333333,4,2,1,90,0,1 +262897,-83.3333333333333,4,2,2,90,0,1 +262965,-83.3333333333333,4,2,1,90,0,1 +263170,-83.3333333333333,4,2,2,90,0,1 +263238,-83.3333333333333,4,2,1,90,0,1 +263443,-83.3333333333333,4,2,2,90,0,1 +263511,-83.3333333333333,4,2,1,90,0,1 +263715,-83.3333333333333,4,2,2,90,0,1 +263783,-83.3333333333333,4,2,1,90,0,1 +263988,-83.3333333333333,4,2,2,90,0,1 +264056,-83.3333333333333,4,2,1,90,0,1 +266988,-83.3333333333333,4,2,2,90,0,1 +267056,-83.3333333333333,4,2,1,90,0,1 +267261,-83.3333333333333,4,2,2,90,0,1 +267329,-83.3333333333333,4,2,1,90,0,1 +267533,-83.3333333333333,4,2,2,90,0,1 +267602,-83.3333333333333,4,2,1,90,0,1 +267806,-83.3333333333333,4,2,2,90,0,1 +267874,-83.3333333333333,4,2,1,90,0,1 +269033,532.150776053215,4,2,1,60,1,1 +269033,-100,4,2,1,60,0,0 +269965,-100,4,2,1,5,0,0 +270097,-66.6666666666667,4,2,1,60,0,0 +272211,-100,4,2,1,60,0,0 +273282,-100,4,2,1,60,0,0 +273290,558.139534883721,4,2,1,60,1,0 +273848,600,4,2,1,60,1,0 +273848,-100,4,2,1,60,0,0 +274248,750,4,2,1,60,1,0 +274269,-100,4,2,1,60,0,0 +274498,750,4,2,1,60,1,0 +274498,-100,4,2,1,60,0,0 +274873,800,4,2,1,60,1,0 +274873,-100,4,2,1,60,0,0 +275273,923.076923076923,4,2,1,60,1,0 +275273,-100,4,2,1,60,0,0 +275734,960,4,2,1,60,1,0 +275734,-100,4,2,1,60,0,0 +276054,1200,4,2,1,60,1,0 +276254,995.850622406639,4,2,1,70,1,0 +276254,-100,4,2,1,70,0,0 +276257,-100,4,2,1,70,0,0 +277249,764.331210191083,4,2,1,70,1,0 +277257,-100,4,2,1,70,0,0 +277503,693.64161849711,4,2,1,70,1,0 +277737,-100,4,2,1,70,0,0 +278181,-100,4,2,1,70,0,0 +278196,631.578947368421,4,2,1,70,1,0 +278609,-100,4,2,1,70,0,0 +278617,588.235294117647,4,2,1,70,1,0 +278813,545.454545454546,4,2,1,70,1,0 +279009,-100,4,2,1,70,0,0 +279358,521.739130434783,4,2,1,70,1,0 +279372,-100,4,2,1,70,0,0 +279687,-100,4,2,1,70,0,0 +279705,718.562874251497,4,2,1,70,1,0 +279944,666.666666666667,4,2,1,70,1,0 +279947,-100,4,2,1,70,0,0 +280170,-100,4,2,1,70,0,0 +280604,-100,4,2,1,70,0,0 +280610,558.139534883721,4,2,1,70,1,0 +280889,521.739130434783,4,2,1,70,1,0 +281149,576.923076923077,4,2,1,70,1,0 +281436,-100,4,2,1,70,0,0 +281437,609.137055837563,4,2,1,70,1,0 +281736,-100,4,2,1,70,0,0 +281741,631.578947368421,4,2,1,70,1,0 +281843,-100,4,2,1,70,0,0 +282056,406.779661016949,4,2,1,70,1,0 +282259,415.22491349481,4,2,1,70,1,0 +282669,-100,4,2,1,70,0,0 +282674,428.571428571429,4,2,1,70,1,0 +283097,-100,4,2,1,70,0,0 +283497,-100,4,2,1,70,0,0 +283531,400,4,2,1,70,1,0 +283931,375,4,2,1,70,1,0 +284118,436.363636363636,4,2,1,70,1,0 +284247,-100,4,2,1,70,0,0 +284554,461.538461538462,4,2,1,70,1,0 +284647,-100,4,2,1,70,0,0 +285015,480,4,2,1,70,1,0 +285247,-100,4,2,1,70,0,0 +285255,500,4,2,1,70,1,0 +285599,-100,4,2,1,70,0,0 +285741,-100,4,2,1,70,0,0 +285755,369.230769230769,4,2,1,70,1,0 +286124,601.642483981269,4,2,1,70,1,0 +286725,857.142857142857,4,2,1,90,1,0 +286725,-25,4,2,1,90,0,0 + +[HitObjects] +28,123,22,6,0,L|40:187,5,34,6|2|2|2|2|2,3:2|1:2|1:2|3:2|1:2|1:2,0:0:0:0: +106,58,431,2,0,L|122:-5,5,34,2|2|2|2|2|2,3:2|1:2|1:2|3:2|1:2|1:2,0:0:0:0: +207,61,840,2,0,B|280:43|280:43|288:45|288:45|385:21,2,152.999995330811,6|6|6,3:2|3:2|3:2,0:0:0:0: +313,147,1385,6,0,L|377:159,5,34,6|2|2|2|2|2,3:2|1:2|1:2|3:2|1:2|1:2,0:0:0:0: +347,252,1794,2,0,L|396:239,5,34,2|2|2|2|2|2,3:2|1:2|1:2|3:2|1:2|1:2,0:0:0:0: +415,328,2203,2,0,B|433:255|433:255|431:247|431:247|455:150,2,152.999995330811,6|6|6,3:2|3:2|3:2,0:0:0:0: +235,343,2749,6,0,L|171:331,5,34,6|2|2|2|2|2,3:2|1:2|1:2|3:2|1:2|1:2,0:0:0:0: +219,239,3158,2,0,L|236:187,5,34,2|2|2|2|2|2,3:2|1:2|1:2|3:2|1:2|1:2,0:0:0:0: +299,136,3567,2,0,B|231:152|231:152|223:150|223:150|150:168,2,152.999995330811,6|6|6,3:2|3:2|3:2,0:0:0:0: +234,11,4112,6,0,L|182:-2,5,34,6|2|2|2|2|2,3:2|1:2|1:2|3:2|1:2|1:2,0:0:0:0: +135,70,4522,2,0,L|83:83,5,34,2|2|2|2|2|2,3:2|1:2|1:2|3:2|1:2|1:2,0:0:0:0: +35,15,4931,2,0,B|53:88|53:88|51:96|51:96|75:193,2,152.999995330811,6|6|6,3:2|3:2|3:2,0:0:0:0: +22,251,5476,6,0,L|17:306,5,34,6|2|2|2|2|2,3:2|1:2|1:2|3:2|1:2|1:2,0:0:0:0: +120,238,5885,2,0,L|171:256,5,34,2|2|2|2|2|2,3:2|1:2|1:2|3:2|1:2|1:2,0:0:0:0: +187,333,6294,2,0,B|114:351|114:351|106:349|106:349|9:373,2,152.999995330811,6|6|6,3:2|3:2|3:2,0:0:0:0: +363,340,6840,6,0,L|358:285,5,34,6|2|2|2|2|2,3:2|1:2|1:2|3:2|1:2|1:2,0:0:0:0: +411,223,7249,2,0,L|462:205,5,34,2|2|2|2|2|2,3:2|1:2|1:2|3:2|1:2|1:2,0:0:0:0: +355,148,7658,2,0,B|373:75|373:75|371:67|371:67|395:-30,2,152.999995330811,6|6|6,3:2|3:2|3:2,0:0:0:0: +502,158,8203,6,0,L|514:222,5,34,6|2|2|2|2|2,3:2|1:2|1:2|3:2|1:2|1:2,0:0:0:0: +419,236,8612,2,0,L|436:288,5,34,2|2|2|2|2|2,3:2|1:2|1:2|3:2|1:2|1:2,0:0:0:0: +364,341,9022,2,0,B|437:359|437:359|445:357|445:357|542:381,2,152.999995330811,6|6|6,3:2|3:2|3:2,0:0:0:0: +233,235,9567,6,0,L|222:181,5,34,6|2|2|2|2|2,3:2|1:2|1:2|3:2|1:2|1:2,0:0:0:0: +284,125,9976,2,0,L|304:94,5,34,2|2|2|2|2|2,3:2|1:2|1:2|3:2|1:2|1:2,0:0:0:0: +245,16,10385,6,0,P|171:23|132:125,2,152.999995330811,6|6|6,3:2|3:2|3:2,0:0:0:0: +407,374,12021,6,0,P|406:316|461:265,1,101.999996887207,6|8,3:2|2:2,0:0:0:0: +484,281,12225,1,2,3:2:0:0: +484,281,12293,2,0,P|429:260|401:212,1,101.999996887207,0|8,3:3|2:2,0:0:0:0: +387,125,12566,2,0,P|462:119|484:41,2,152.999995330811,2|2|10,3:2|3:2|3:2,0:0:0:0: +274,30,13111,6,0,L|141:54,1,101.999996887207,2|8,3:2|3:2,0:0:0:0: +124,33,13316,1,2,3:2:0:0: +124,33,13384,2,0,L|-1:56,1,101.999996887207,0|10,3:3|3:2,0:0:0:0: +24,154,13657,2,0,P|81:177|106:268,1,152.999995330811,2|2,3:2|3:2,0:0:0:0: +229,353,14066,1,10,3:2:0:0: +328,376,14202,6,0,P|324:316|293:277,1,101.999996887207,2|8,3:2|3:2,0:0:0:0: +256,265,14407,1,2,3:2:0:0: +256,265,14475,2,0,P|306:242|339:189,1,101.999996887207,0|10,3:3|3:2,0:0:0:0: +378,113,14748,2,0,P|449:120|500:192,2,152.999995330811,2|2|10,3:2|3:2|3:2,0:0:0:0: +277,8,15293,6,0,L|246:133,1,101.999996887207,2|8,3:2|3:2,0:0:0:0: +212,137,15498,1,2,3:2:0:0: +212,137,15566,2,0,L|243:262,1,101.999996887207,0|10,3:3|3:2,0:0:0:0: +256,336,15839,2,0,P|314:314|423:379,1,152.999995330811,2|2,3:2|3:2,0:0:0:0: +473,159,16248,1,10,3:2:0:0: +486,58,16384,6,0,P|431:61|387:116,1,101.999996887207,6|8,3:2|3:2,0:0:0:0: +382,142,16589,1,2,3:2:0:0: +382,142,16657,2,0,P|336:101|269:103,1,101.999996887207,0|10,3:3|3:2,0:0:0:0: +201,131,16930,2,0,P|189:74|105:47,2,152.999995330811,2|2|10,3:2|3:2|3:2,0:0:0:0: +40,174,17475,6,0,L|63:312,1,101.999996887207,2|8,3:2|3:2,0:0:0:0: +97,307,17680,1,2,3:2:0:0: +97,307,17748,2,0,L|235:284,1,101.999996887207,0|10,3:3|3:2,0:0:0:0: +275,223,18021,2,0,P|243:290|273:374,1,152.999995330811,2|2,3:2|3:2,0:0:0:0: +415,382,18430,1,10,3:2:0:0: +355,299,18566,6,0,P|394:279|466:297,1,101.999996887207,2|8,3:2|3:2,0:0:0:0: +486,250,18771,1,2,3:2:0:0: +486,250,18839,2,0,P|453:208|460:142,1,101.999996887207,0|10,3:3|3:2,0:0:0:0: +476,62,19111,2,0,P|444:116|342:98,2,152.999995330811,2|2|10,3:2|3:2|3:2,0:0:0:0: +306,4,19657,6,0,L|183:50,1,101.999996887207,6|8,3:2|3:2,0:0:0:0: +161,32,19861,1,2,3:2:0:0: +161,32,19930,2,0,L|207:155,1,101.999996887207,0|10,3:3|3:2,0:0:0:0: +127,201,20202,2,0,P|67:223|6:192,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +41,380,20475,1,0,1:1:0:0: +48,355,20543,1,8,2:3:0:0: +64,336,20611,1,8,2:3:0:0: +86,323,20679,1,4,2:3:0:0: +111,319,20748,6,0,P|172:336|208:385,1,101.999996887207,6|8,3:2|3:2,0:0:0:0: +249,382,20952,1,2,3:2:0:0: +249,382,21021,2,0,L|374:366,1,101.999996887207,0|10,3:3|3:2,0:0:0:0: +451,381,21293,2,0,P|460:339|385:240,1,152.999995330811,2|2,3:2|3:2,0:0:0:0: +398,95,21702,1,10,3:2:0:0: +337,177,21839,6,0,P|288:208|226:199,1,101.999996887207,2|8,3:2|3:2,0:0:0:0: +202,192,22043,1,2,3:2:0:0: +202,192,22111,2,0,L|172:82,1,101.999996887207,0|10,3:3|3:2,0:0:0:0: +7,86,22384,1,2,3:2:0:0: +7,86,22589,1,2,3:2:0:0: +7,86,22793,1,10,3:2:0:0: +61,245,22930,6,0,L|48:373,1,101.999996887207,2|8,3:2|3:2,0:0:0:0: +92,384,23134,1,2,3:2:0:0: +92,384,23202,2,0,P|149:373|187:330,1,101.999996887207,0|10,3:3|3:2,0:0:0:0: +262,283,23475,2,0,P|328:313|350:411,1,152.999995330811,2|2,3:2|3:2,0:0:0:0: +467,280,23884,1,10,3:2:0:0: +430,184,24021,6,0,L|310:204,1,101.999996887207,2|8,3:2|3:2,0:0:0:0: +284,192,24225,1,2,3:2:0:0: +284,192,24293,2,0,P|257:131|272:74,1,101.999996887207,0|10,3:3|3:2,0:0:0:0: +386,4,24566,1,2,3:2:0:0: +386,4,24771,1,2,3:2:0:0: +386,4,24975,1,10,3:2:0:0: +432,136,25111,6,0,P|427:195|465:245,1,101.999996887207,6|8,3:2|3:2,0:0:0:0: +416,272,25316,1,2,3:2:0:0: +416,272,25384,2,0,L|306:247,1,101.999996887207,0|10,3:3|3:2,0:0:0:0: +219,215,25657,2,0,P|172:266|191:388,1,152.999995330811,2|2,3:2|3:2,0:0:0:0: +40,259,26066,1,10,3:2:0:0: +28,157,26202,6,0,P|69:144|104:73,1,101.999996887207,2|8,3:2|3:2,0:0:0:0: +125,53,26407,1,2,3:2:0:0: +125,53,26475,2,0,L|146:171,1,101.999996887207,0|10,3:3|3:2,0:0:0:0: +221,307,26748,1,2,3:2:0:0: +221,307,26953,1,2,3:2:0:0: +221,307,27157,1,10,3:2:0:0: +379,281,27293,6,0,L|497:303,1,101.999996887207,2|8,3:2|3:2,0:0:0:0: +510,259,27498,1,2,3:2:0:0: +510,259,27566,2,0,P|514:209|471:147,1,101.999996887207,0|10,3:3|3:2,0:0:0:0: +503,62,27839,2,0,P|461:116|373:111,1,152.999995330811,2|2,3:2|3:2,0:0:0:0: +256,28,28248,1,10,3:2:0:0: +190,105,28384,5,8,2:3:0:0: +269,169,28521,1,4,2:3:0:0: +272,178,28589,1,8,2:3:0:0: +275,187,28657,2,0,L|260:327,1,101.999996887207,8|8,2:3|2:3,0:0:0:0: +179,345,28930,1,8,2:3:0:0: +154,338,28998,1,8,2:3:0:0: +135,322,29066,1,4,2:3:0:0: +122,300,29134,1,8,2:3:0:0: +118,275,29202,2,0,L|106:333,3,50.9999984436036,8|4|8|8,2:3|2:3|2:3|2:3,0:0:0:0: +45,207,29475,6,0,L|-10:224,3,42.5,14|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +102,137,29748,2,0,L|157:154,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +193,228,30021,2,0,L|205:268,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +291,311,30293,2,0,L|303:270,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +391,243,30566,5,10,3:2:0:0: +400,246,30634,1,0,3:3:0:0: +409,249,30702,1,0,3:3:0:0: +434,344,30839,1,10,3:2:0:0: +425,347,30907,1,0,3:3:0:0: +416,350,30975,1,0,3:3:0:0: +512,269,31111,2,0,L|499:228,2,42.5,10|0|0,3:2|3:3|3:3,0:0:0:0: +435,152,31384,2,0,L|447:111,2,42.5,10|0|0,3:2|3:3|3:3,0:0:0:0: +381,34,31657,6,0,L|340:46,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +251,83,31930,2,0,L|196:66,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +146,137,32202,2,0,L|158:177,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +56,112,32475,2,0,L|68:72,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +22,199,32748,5,10,3:2:0:0: +25,208,32816,1,0,3:3:0:0: +28,217,32884,1,0,3:3:0:0: +93,292,33021,1,10,3:2:0:0: +90,301,33089,1,0,3:3:0:0: +87,310,33157,1,0,3:3:0:0: +168,367,33293,1,10,3:2:0:0: +176,365,33361,1,0,3:3:0:0: +184,363,33430,2,0,L|288:375,1,85,0|10,3:3|3:2,0:0:0:0: +274,168,33839,6,0,L|262:128,3,42.5,14|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +330,66,34112,2,0,L|342:26,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +422,109,34384,2,0,L|463:121,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +461,218,34657,2,0,L|516:201,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +448,314,34930,5,10,3:2:0:0: +439,311,34998,1,0,3:3:0:0: +430,308,35066,1,0,3:3:0:0: +321,262,35202,1,10,3:2:0:0: +312,265,35270,1,0,3:3:0:0: +303,268,35338,1,0,3:3:0:0: +269,366,35475,2,0,L|214:349,2,42.5,10|0|0,3:2|3:3|3:3,0:0:0:0: +162,271,35748,2,0,L|203:259,2,42.5,10|0|0,3:2|3:3|3:3,0:0:0:0: +87,207,36021,6,0,L|99:167,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +31,105,36294,2,0,L|19:65,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +101,9,36566,2,0,L|142:21,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +184,108,36839,2,0,L|239:91,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +304,31,37111,5,10,3:2:0:0: +307,22,37179,1,0,3:3:0:0: +310,13,37247,1,0,3:3:0:0: +392,90,37384,1,10,3:2:0:0: +395,99,37452,1,0,3:3:0:0: +398,108,37520,1,0,3:3:0:0: +341,194,37657,2,0,L|363:249,3,42.5,8|8|8|8,2:3|2:3|2:3|2:3,0:0:0:0: +352,320,37930,2,0,L|374:375,3,42.5,4|4|4|4,2:3|2:3|2:3|2:3,0:0:0:0: +449,384,38202,6,0,P|490:343|470:247,1,136,6|2,3:2|2:2,0:0:0:0: +487,268,38748,2,0,L|351:239,1,136,2|2,1:2|2:2,0:0:0:0: +403,58,39293,2,0,B|330:66|368:102|248:108,1,136,2|2,3:2|3:2,0:0:0:0: +277,105,39702,1,2,3:2:0:0: +155,6,39839,2,0,P|184:59|184:92,1,68,2|0,1:2|1:1,0:0:0:0: +65,163,40111,1,2,1:2:0:0: +65,163,40384,6,0,L|156:180,1,85,6|2,3:2|1:2,0:0:0:0: +90,336,40657,2,0,L|-1:353,1,85,0|2,3:3|1:2,0:0:0:0: +180,280,40930,1,2,3:2:0:0: +280,304,41066,1,2,1:2:0:0: +280,304,41134,1,2,2:2:0:0: +280,304,41202,2,0,L|371:321,1,85,2|2,3:2|1:2,0:0:0:0: +208,384,41475,5,2,3:2:0:0: +208,384,41611,1,2,1:2:0:0: +372,304,41748,2,0,L|281:287,1,85,2|2,3:2|1:2,0:0:0:0: +170,216,42021,2,0,L|190:119,1,85,2|0,3:2|1:1,0:0:0:0: +64,75,42293,2,0,L|72:31,3,42.5,8|8|4|4,2:3|2:3|2:3|2:3,0:0:0:0: +25,148,42566,6,0,P|49:229|11:298,1,136,6|2,3:2|2:2,0:0:0:0: +32,274,43111,2,0,L|187:310,1,136,2|2,1:2|2:2,0:0:0:0: +420,179,43657,2,0,B|347:187|385:223|265:229,1,136,2|2,3:2|3:2,0:0:0:0: +294,226,44066,1,2,3:2:0:0: +204,146,44202,2,0,P|204:111|236:62,1,68,2|0,1:2|1:1,0:0:0:0: +381,14,44475,1,2,1:2:0:0: +381,14,44748,6,0,L|394:111,1,85,6|2,3:2|1:2,0:0:0:0: +500,237,45021,2,0,L|487:334,1,85,2|2,2:2|1:2,0:0:0:0: +285,242,45293,1,2,2:2:0:0: +397,200,45430,1,2,1:2:0:0: +397,200,45498,1,2,3:2:0:0: +397,200,45566,2,0,L|384:297,1,85,2|2,2:2|1:2,0:0:0:0: +208,318,45839,5,0,1:1:0:0: +208,318,45907,1,0,1:1:0:0: +208,318,45975,2,0,P|166:292|113:291,1,85,8|4,2:3|2:3,0:0:0:0: +47,227,46248,1,0,1:1:0:0: +54,185,46316,1,0,1:1:0:0: +61,143,46384,1,8,2:3:0:0: +118,57,46521,2,0,L|108:-6,5,42.5,8|8|4|4|4|4,2:3|2:3|2:3|2:3|2:3|2:3,0:0:0:0: +186,106,46930,6,0,P|246:93|289:35,1,101.999996887207,6|0,3:2|1:1,0:0:0:0: +446,47,47202,2,0,P|407:14|357:7,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +367,108,47475,2,0,L|392:212,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +297,383,47748,2,0,L|320:283,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +243,216,48021,6,0,L|143:239,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +188,88,48293,1,2,3:2:0:0: +188,88,48430,1,2,1:2:0:0: +59,159,48566,2,0,P|39:239|63:287,1,101.999996887207,2|0,3:2|3:3,0:0:0:0: +174,359,48839,2,0,L|274:382,1,101.999996887207,2|0,3:2|3:3,0:0:0:0: +423,310,49111,6,0,P|430:244|402:199,1,101.999996887207,6|2,3:2|1:2,0:0:0:0: +346,71,49384,2,0,P|399:110|452:108,1,101.999996887207,2|2,3:2|1:2,0:0:0:0: +217,12,49657,1,2,3:2:0:0: +208,152,49793,1,2,1:2:0:0: +208,152,49861,1,2,2:2:0:0: +208,152,49930,2,0,L|73:172,1,101.999996887207,2|2,3:2|1:2,0:0:0:0: +45,14,50202,5,2,3:2:0:0: +108,77,50338,1,2,1:2:0:0: +107,167,50475,1,2,3:2:0:0: +44,230,50611,1,2,1:2:0:0: +70,316,50748,2,0,B|165:332|165:332|180:346|180:346|302:361,1,203.999993774414,8|4,3:3|2:3,0:0:0:0: +441,286,51157,5,4,2:3:0:0: +434,296,51225,1,4,2:3:0:0: +427,306,51293,2,0,L|401:188,1,101.999996887207,6|0,3:2|1:1,0:0:0:0: +482,12,51566,2,0,L|456:130,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +357,113,51839,2,0,P|316:142|257:142,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +119,20,52111,2,0,P|169:22|210:51,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +164,143,52384,6,0,P|123:174|31:168,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +0,304,52657,1,2,3:2:0:0: +0,304,52793,1,2,1:2:0:0: +124,339,52930,2,0,L|236:353,1,101.999996887207,2|0,3:2|3:3,0:0:0:0: +316,242,53202,2,0,L|302:130,1,101.999996887207,2|0,3:2|3:1,0:0:0:0: +332,0,53475,6,0,P|389:17|424:69,1,101.999996887207,6|0,3:2|1:1,0:0:0:0: +512,147,53748,2,0,P|455:164|420:216,1,101.999996887207,8|0,2:3|1:1,0:0:0:0: +512,332,54021,1,0,3:3:0:0: +363,319,54157,1,0,1:1:0:0: +363,319,54225,1,0,2:2:0:0: +363,319,54293,2,0,L|246:300,1,101.999996887207,4|0,3:3|1:1,0:0:0:0: +308,164,54566,5,0,3:3:0:0: +269,181,54634,1,0,3:3:0:0: +227,177,54702,1,0,1:1:0:0: +193,153,54770,1,0,1:1:0:0: +175,116,54838,1,8,2:3:0:0: +81,73,54975,1,0,1:1:0:0: +74,115,55043,1,0,1:1:0:0: +67,157,55111,1,4,2:3:0:0: +18,247,55248,2,0,L|28:310,5,50.9999984436036,0|0|8|8|4|4,1:1|1:1|2:3|2:3|2:3|2:3,0:0:0:0: +87,361,55657,6,0,L|128:349,3,42.5,14|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +175,263,55929,2,0,L|230:280,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +295,228,56202,2,0,L|307:188,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +265,105,56475,2,0,L|253:65,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +327,8,56748,5,10,3:2:0:0: +336,11,56816,1,0,3:3:0:0: +345,14,56884,1,0,3:3:0:0: +414,83,57021,1,10,3:2:0:0: +423,80,57089,1,0,3:3:0:0: +432,77,57157,1,0,3:3:0:0: +502,143,57293,2,0,L|490:183,2,42.5,10|0|0,3:2|3:3|3:3,0:0:0:0: +431,255,57566,2,0,L|443:295,2,42.5,10|0|0,3:2|3:3|3:3,0:0:0:0: +356,334,57839,6,0,L|344:374,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +294,256,58112,2,0,L|334:244,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +205,299,58384,2,0,L|193:259,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +151,377,58657,2,0,L|111:365,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +21,328,58930,5,10,3:2:0:0: +18,337,58998,1,0,3:3:0:0: +15,346,59066,1,0,3:3:0:0: +96,263,59202,1,10,3:2:0:0: +93,254,59270,1,0,3:3:0:0: +90,245,59338,1,0,3:3:0:0: +38,161,59475,1,10,3:2:0:0: +41,152,59543,1,0,3:3:0:0: +44,143,59611,2,0,L|32:18,1,85,0|10,3:3|3:2,0:0:0:0: +227,20,60021,6,0,L|215:60,3,42.5,14|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +257,143,60294,2,0,L|269:183,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +357,143,60566,2,0,L|398:131,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +445,45,60838,2,0,L|500:62,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +496,149,61111,5,10,3:2:0:0: +493,158,61179,1,0,3:3:0:0: +490,167,61247,1,0,3:3:0:0: +420,245,61384,1,10,3:2:0:0: +417,236,61452,1,0,3:3:0:0: +414,227,61521,1,0,3:3:0:0: +389,337,61657,2,0,L|349:325,2,42.5,10|0|0,3:2|3:3|3:3,0:0:0:0: +277,266,61930,2,0,L|237:278,2,42.5,10|0|0,3:2|3:3|3:3,0:0:0:0: +161,214,62202,6,0,L|149:174,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +142,307,62475,2,0,L|102:295,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +2,292,62748,2,0,L|14:252,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +0,158,63021,2,0,L|40:146,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +95,70,63293,5,10,3:2:0:0: +104,73,63361,1,0,3:3:0:0: +113,76,63429,1,0,3:3:0:0: +189,141,63566,1,10,3:2:0:0: +198,138,63634,1,0,3:3:0:0: +207,135,63702,1,0,3:3:0:0: +281,59,63839,2,0,L|338:73,3,42.5,8|8|8|8,2:3|2:3|2:3|2:3,0:0:0:0: +362,142,64111,2,0,L|419:156,3,42.5,4|4|4|4,2:3|2:3|2:3|2:3,0:0:0:0: +478,112,64384,6,0,P|441:165|461:260,1,136,6|2,3:2|1:2,0:0:0:0: +485,364,64930,2,0,L|325:332,1,136,2|0,3:2|1:1,0:0:0:0: +222,294,65475,2,0,B|156:309|190:338|97:360,1,136,2|2,3:2|1:2,0:0:0:0: +104,358,65884,1,2,3:2:0:0: +16,285,66021,2,0,P|18:244|44:201,1,68,2|0,3:2|1:1,0:0:0:0: +28,219,66225,1,0,1:1:0:0: +28,219,66293,1,10,2:3:0:0: +90,145,66566,6,0,L|76:55,1,85,6|0,3:2|1:1,0:0:0:0: +256,0,66839,2,0,L|242:90,1,85,0|0,3:2|1:1,0:0:0:0: +186,179,67111,1,0,3:3:0:0: +273,263,67248,1,2,1:2:0:0: +273,263,67316,1,2,3:2:0:0: +273,263,67384,2,0,L|395:248,1,85,2|2,3:2|1:2,0:0:0:0: +471,151,67657,5,2,3:2:0:0: +471,151,67793,1,2,1:2:0:0: +392,272,67930,2,0,L|307:282,1,85,2|2,3:2|1:2,0:0:0:0: +165,327,68202,2,0,L|179:237,1,85,2|0,3:2|1:1,0:0:0:0: +266,112,68475,2,0,L|307:119,3,42.5,8|8|4|4,3:3|2:3|2:3|2:3,0:0:0:0: +358,51,68748,6,0,P|439:27|508:65,1,136,6|2,3:2|1:2,0:0:0:0: +447,174,69293,2,0,L|473:336,1,136,2|2,3:2|1:2,0:0:0:0: +343,253,69839,2,0,B|308:188|278:221|230:145,1,136,2|2,3:2|1:2,0:0:0:0: +216,58,70248,1,0,1:1:0:0: +216,58,70316,1,0,1:1:0:0: +216,58,70384,2,0,P|177:80|140:84,1,68,8|8,2:3|2:3,0:0:0:0: +58,36,70657,1,4,2:3:0:0: +58,36,70930,6,0,L|45:155,1,85,6|2,3:2|1:2,0:0:0:0: +129,284,71202,2,0,L|142:403,1,85,2|2,3:2|1:2,0:0:0:0: +132,180,71475,1,2,3:2:0:0: +228,241,71611,1,2,1:2:0:0: +228,241,71680,1,2,3:2:0:0: +228,241,71748,2,0,L|312:250,1,85,2|2,3:2|1:2,0:0:0:0: +382,363,72021,5,2,3:2:0:0: +414,371,72089,1,0,1:1:0:0: +448,367,72157,1,0,1:1:0:0: +478,351,72225,1,0,1:1:0:0: +500,326,72293,1,0,1:1:0:0: +453,220,72430,1,0,1:1:0:0: +449,206,72498,1,0,1:1:0:0: +445,192,72566,2,0,L|422:244,2,42.5,0|0|0,3:3|1:1|1:1,0:0:0:0: +486,110,72839,2,0,L|503:71,1,42.5,0|0,1:1|1:1,0:0:0:0: +414,68,72975,2,0,L|431:29,1,42.5,0|0,1:1|1:1,0:0:0:0: +344,23,73111,5,6,3:2:0:0: +62,180,75293,1,6,3:2:0:0: +403,350,76930,2,0,P|452:342|476:326,5,67.9999979248048,2|2|2|8|8|4,1:2|1:2|1:2|2:3|2:3|2:3,0:0:0:0: +412,257,77475,6,0,P|419:224|443:195,2,67.9999979248048,6|2|2,3:2|2:2|2:2,0:0:0:0: +320,230,77748,2,0,P|309:197|315:160,2,67.9999979248048,10|2|2,2:2|2:2|2:2,0:0:0:0: +248,289,78021,2,0,P|255:322|279:351,2,67.9999979248048,2|2|2,3:2|2:2|2:2,0:0:0:0: +156,316,78294,2,0,P|145:348|151:385,2,67.9999979248048,10|2|2,2:2|2:2|2:2,0:0:0:0: +97,240,78566,5,2,3:2:0:0: +89,250,78657,2,0,L|12:266,1,67.9999979248048,2|2,2:2|2:2,0:0:0:0: +10,169,78839,1,10,2:2:0:0: +52,134,78930,1,2,2:2:0:0: +106,132,79021,1,2,2:2:0:0: +154,154,79111,2,0,P|231:144|238:9,1,203.999993774414,2|10,3:2|2:2,0:0:0:0: +258,34,79657,6,0,L|170:26,2,67.9999979248048,2|2|2,3:2|2:2|2:2,0:0:0:0: +226,127,79930,2,0,L|138:142,2,67.9999979248048,10|2|2,2:2|2:2|2:2,0:0:0:0: +287,204,80202,2,0,L|374:219,2,67.9999979248048,2|2|2,3:2|2:2|2:2,0:0:0:0: +293,302,80475,2,0,L|373:339,2,67.9999979248048,10|2|2,2:2|2:2|2:2,0:0:0:0: +218,362,80748,5,2,3:2:0:0: +209,352,80839,2,0,P|194:313|204:265,1,67.9999979248048,2|2,2:2|2:2,0:0:0:0: +256,215,81021,1,10,2:2:0:0: +299,183,81111,1,2,2:2:0:0: +352,172,81202,1,2,2:2:0:0: +398,143,81293,2,0,B|402:238|466:224|462:346,1,203.999993774414,10|2,3:2|1:2,0:0:0:0: +462,332,81839,6,0,P|421:340|377:374,2,67.9999979248048,6|2|2,3:2|2:2|2:2,0:0:0:0: +347,273,82111,2,0,P|315:300|294:351,2,67.9999979248048,10|2|2,2:2|2:2|2:2,0:0:0:0: +368,179,82384,2,0,P|336:151|315:100,2,67.9999979248048,2|2|2,3:2|2:2|2:2,0:0:0:0: +238,172,82657,2,0,P|224:132|231:77,2,67.9999979248048,10|2|2,2:2|2:2|2:2,0:0:0:0: +135,75,82930,5,2,3:2:0:0: +139,58,83021,2,0,P|156:36|228:13,1,67.9999979248048,2|2,2:2|2:2,0:0:0:0: +41,127,83202,1,10,2:2:0:0: +83,161,83293,1,2,2:2:0:0: +103,211,83384,1,2,2:2:0:0: +99,265,83475,2,0,P|143:371|254:349,1,203.999993774414,2|10,3:2|2:2,0:0:0:0: +219,374,84021,6,0,L|156:351,2,67.9999979248048,2|2|2,3:2|2:2|2:2,0:0:0:0: +237,275,84293,2,0,L|182:236,2,67.9999979248048,10|2|2,2:2|2:2|2:2,0:0:0:0: +291,189,84566,2,0,L|354:166,2,67.9999979248048,2|2|2,3:2|2:2|2:2,0:0:0:0: +273,90,84839,2,0,L|327:51,2,67.9999979248048,10|2|2,2:2|2:2|2:2,0:0:0:0: +210,14,85111,5,2,3:2:0:0: +199,27,85202,2,0,P|177:68|182:118,1,67.9999979248048,2|2,2:2|2:2,0:0:0:0: +227,174,85384,1,2,1:2:0:0: +280,183,85475,1,2,1:2:0:0: +326,210,85566,1,2,1:2:0:0: +380,206,85657,2,0,B|477:182|477:182|551:217,2,152.999995330811,6|6|2,1:2|1:2|1:2,0:0:0:0: +414,298,86202,6,0,L|405:350,3,42.5,6|0|8|0,3:2|3:3|3:2|0:0,0:0:0:0: +313,333,86475,2,0,L|322:385,3,42.5,2|0|8|0,3:2|0:0|3:2|0:0,0:0:0:0: +229,285,86748,6,0,L|238:233,1,42.5,2|0,3:2|3:3,0:0:0:0: +140,308,86884,2,0,L|149:256,1,42.5,8|0,3:2|0:0,0:0:0:0: +51,334,87021,2,0,L|60:282,3,42.5,2|0|8|0,3:2|0:0|3:2|0:0,0:0:0:0: +41,200,87293,6,0,L|-11:209,2,42.5,2|0|8,3:2|3:3|3:2,0:0:0:0: +111,132,87566,1,2,3:2:0:0: +119,134,87634,1,0,3:3:0:0: +127,136,87702,1,8,3:2:0:0: +152,45,87839,2,0,L|100:36,2,42.5,2|0|8,3:2|3:3|3:2,0:0:0:0: +222,113,88112,1,2,3:2:0:0: +230,111,88180,1,0,3:3:0:0: +238,109,88248,1,8,3:2:0:0: +295,32,88384,6,0,L|347:23,3,42.5,2|0|8|0,3:2|3:3|3:2|0:0,0:0:0:0: +334,129,88657,2,0,L|386:138,3,42.5,2|0|8|0,3:2|0:0|3:2|0:0,0:0:0:0: +464,98,88930,6,0,L|473:150,1,42.5,2|0,3:2|3:3,0:0:0:0: +449,184,89066,2,0,L|458:236,1,42.5,8|0,3:2|0:0,0:0:0:0: +434,270,89202,2,0,L|443:322,3,42.5,2|0|8|0,3:2|0:0|3:2|0:0,0:0:0:0: +362,365,89475,5,2,3:2:0:0: +360,372,89543,1,0,3:3:0:0: +358,381,89611,1,8,3:2:0:0: +288,302,89748,1,2,3:2:0:0: +286,295,89816,1,0,3:3:0:0: +284,286,89884,1,8,3:2:0:0: +201,348,90021,1,2,3:2:0:0: +193,346,90089,1,0,3:3:0:0: +185,344,90158,2,0,L|81:356,1,85,8|2,3:2|3:2,0:0:0:0: +67,179,90566,6,0,L|15:170,3,42.5,6|0|8|0,3:2|3:3|3:2|0:0,0:0:0:0: +50,69,90839,2,0,L|-2:78,3,42.5,2|0|8|0,3:2|0:0|3:2|0:0,0:0:0:0: +147,88,91111,6,0,L|138:36,1,42.5,2|0,3:2|3:3,0:0:0:0: +236,111,91247,2,0,L|227:59,1,42.5,8|0,3:2|0:0,0:0:0:0: +325,137,91384,2,0,L|316:85,3,42.5,2|0|8|0,3:2|0:0|3:2|0:0,0:0:0:0: +257,207,91657,6,0,L|248:259,2,42.5,2|0|8,3:2|3:3|3:2,0:0:0:0: +154,263,91930,1,2,3:2:0:0: +156,271,91998,1,0,3:3:0:0: +158,279,92066,1,8,3:2:0:0: +231,342,92203,2,0,L|240:394,2,42.5,2|0|8,3:2|3:3|3:2,0:0:0:0: +327,324,92476,1,2,3:2:0:0: +329,316,92544,1,0,3:3:0:0: +331,308,92612,1,8,3:2:0:0: +431,315,92748,6,0,L|422:367,3,42.5,2|0|8|0,3:2|3:3|3:2|0:0,0:0:0:0: +503,248,93021,2,0,L|495:206,3,42.5,2|0|8|0,3:2|0:0|3:2|0:0,0:0:0:0: +457,113,93293,6,0,L|509:122,1,42.5,2|0,3:2|3:3,0:0:0:0: +371,79,93429,2,0,L|423:88,1,42.5,8|0,3:2|0:0,0:0:0:0: +286,47,93566,2,0,L|338:56,3,42.5,2|0|8|0,3:2|0:0|3:2|0:0,0:0:0:0: +195,22,93839,5,2,3:2:0:0: +193,29,93907,1,0,3:3:0:0: +191,38,93975,1,8,3:2:0:0: +118,104,94112,1,2,3:2:0:0: +120,111,94180,1,0,3:3:0:0: +122,120,94248,1,8,3:2:0:0: +145,217,94385,1,2,3:2:0:0: +143,225,94453,1,0,3:3:0:0: +141,233,94522,2,0,L|153:337,1,85,8|2,3:2|3:2,0:0:0:0: +48,13,94930,5,0,1:1:0:0: +41,21,94998,1,0,1:1:0:0: +34,29,95066,2,0,L|85:20,3,42.5,0|0|0|0,1:1|1:1|1:1|1:1,0:0:0:0: +77,103,95339,2,0,L|128:94,1,42.5,0|0,1:1|1:1,0:0:0:0: +37,192,95475,2,0,L|88:183,8,42.5,0|0|0|0|0|0|0|0|6,1:1|1:1|1:1|1:1|1:1|1:1|1:1|1:1|2:2,0:0:0:0: +285,375,104748,6,0,P|225:362|182:304,1,101.999996887207,6|0,3:2|1:1,0:0:0:0: +372,333,105020,2,0,P|411:300|461:293,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +483,207,105293,2,0,L|508:103,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +381,19,105566,2,0,L|404:119,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +336,191,105839,6,0,L|236:214,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +190,349,106111,1,2,3:2:0:0: +190,349,106248,1,2,1:2:0:0: +66,289,106384,2,0,P|46:209|70:161,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +160,78,106657,2,0,P|210:83|256:62,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +419,106,106929,6,0,P|426:40|398:-5,1,101.999996887207,6|2,3:2|3:2,0:0:0:0: +350,180,107202,2,0,P|403:219|456:217,1,101.999996887207,2|2,3:2|3:2,0:0:0:0: +500,297,107475,1,2,3:2:0:0: +387,370,107611,1,2,3:2:0:0: +387,370,107679,1,2,3:2:0:0: +387,370,107748,2,0,L|252:390,1,101.999996887207,2|2,3:2|3:2,0:0:0:0: +126,374,108020,5,2,3:2:0:0: +139,286,108156,1,2,3:2:0:0: +213,233,108293,1,2,3:2:0:0: +301,247,108429,1,2,3:2:0:0: +267,163,108566,2,0,B|156:202|174:128|41:180,1,203.999993774414,2|8,3:2|2:3,0:0:0:0: +55,35,108975,5,4,2:3:0:0: +44,28,109043,1,4,2:3:0:0: +35,21,109111,2,0,L|153:-5,1,101.999996887207,6|0,3:2|1:1,0:0:0:0: +279,66,109384,2,0,L|378:87,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +474,77,109657,2,0,P|455:30|405:-1,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +357,183,109929,2,0,P|407:185|448:214,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +499,342,110202,6,0,P|458:373|366:367,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +280,304,110475,1,2,3:2:0:0: +280,304,110611,1,2,1:2:0:0: +357,183,110748,2,0,L|343:71,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +209,0,111020,2,0,L|195:112,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +65,166,111293,6,0,P|122:183|157:235,1,101.999996887207,6|2,3:2|3:2,0:0:0:0: +80,384,111566,2,0,P|66:326|93:269,1,101.999996887207,2|2,3:2|3:2,0:0:0:0: +148,213,111839,1,2,3:2:0:0: +269,287,111975,1,2,3:2:0:0: +269,287,112043,1,2,3:2:0:0: +269,287,112111,2,0,L|386:268,1,101.999996887207,2|2,3:2|3:2,0:0:0:0: +369,170,112384,5,8,2:3:0:0: +410,177,112452,1,8,2:3:0:0: +450,164,112520,1,8,2:3:0:0: +478,133,112588,1,8,2:3:0:0: +487,93,112656,1,4,2:3:0:0: +413,21,112793,1,4,2:3:0:0: +371,14,112861,1,4,2:3:0:0: +329,7,112929,1,8,2:3:0:0: +259,85,113066,2,0,L|196:95,6,50.9999984436036,8|8|4|4|4|4|6,2:3|2:3|2:3|2:3|2:3|2:3|3:2,0:0:0:0: +352,256,117839,6,0,P|366:320|331:396,2,136,6|2|2,3:2|1:3|3:3,0:0:0:0: +435,212,118521,1,2,3:2:0:0: +435,212,118657,2,0,P|363:208|306:147,1,136,2|2,1:3|3:3,0:0:0:0: +353,23,119203,1,2,1:3:0:0: +353,23,119339,2,0,L|508:50,1,136,2|2,3:2|3:2,0:0:0:0: +273,80,119748,1,2,1:3:0:0: +90,125,120021,6,0,P|84:60|27:-1,2,136,2|2|2,3:3|1:3|2:3,0:0:0:0: +128,215,120703,1,2,3:2:0:0: +128,215,120839,2,0,P|74:237|59:256,1,68,2|2,1:3|3:2,0:0:0:0: +14,317,121112,2,0,L|25:390,2,68,2|2|2,3:3|3:2|1:3,0:0:0:0: +68,243,121521,2,0,P|141:288|214:276,1,136,2|0,3:2|3:0,0:0:0:0: +267,337,121930,1,2,1:3:0:0: +267,337,122202,6,0,P|231:282|271:168,1,170,6|2,3:2|1:2,0:0:0:0: +252,185,122611,2,0,P|214:243|97:224,1,170,2|2,2:2|3:2,0:0:0:0: +58,185,123021,2,0,P|61:139|92:90,1,85,2|2,1:2|2:2,0:0:0:0: +6,0,123293,6,0,L|102:23,1,85,2|2,3:2|2:2,0:0:0:0: +156,71,123566,2,0,B|186:37|186:37|261:16,1,85,2|2,1:2|3:2,0:0:0:0: +349,103,123839,1,2,2:2:0:0: +375,21,123975,1,2,3:2:0:0: +456,45,124111,2,0,L|472:185,1,127.5,2|0,1:2|0:0,0:0:0:0: +498,203,124384,6,0,P|450:212|405:327,1,170,2|2,3:2|1:2,0:0:0:0: +400,312,124793,1,0,0:0:0:0: +320,342,124930,2,0,P|288:345|244:372,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +226,280,125202,2,0,P|199:298|175:343,2,56.6666666666667,2|2|2,1:2|2:2|2:2,0:0:0:0: +165,218,125475,6,0,P|151:188|152:137,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +64,166,125748,2,0,P|67:133|94:90,2,56.6666666666667,2|2|2,1:2|2:2|2:2,0:0:0:0: +98,29,126021,2,0,P|65:26|18:45,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +168,81,126293,1,2,1:2:0:0: +176,84,126384,2,0,P|208:86|256:67,1,56.6666666666667,2|2,2:2|2:2,0:0:0:0: +294,22,126566,6,0,L|272:227,1,170,6|2,3:2|1:2,0:0:0:0: +269,279,126975,2,0,P|216:221|108:227,1,170,2|2,2:2|3:2,0:0:0:0: +128,216,127384,2,0,P|84:282|118:385,1,170,2|2,1:2|3:2,0:0:0:0: +102,367,127930,6,0,L|211:350,1,85,2|2,1:2|3:2,0:0:0:0: +268,375,128202,2,0,B|286:335|286:335|274:283,1,85,2|2,2:2|3:2,0:0:0:0: +220,230,128475,1,2,1:2:0:0: +246,149,128611,1,2,2:2:0:0: +272,67,128748,6,0,P|269:35|242:-9,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +341,119,129021,2,0,P|354:89|353:38,2,56.6666666666667,2|2|2,1:2|2:2|2:2,0:0:0:0: +374,198,129293,2,0,P|400:179|424:134,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +363,283,129566,2,0,P|395:280|439:253,2,56.6666666666667,2|2|2,1:2|2:2|2:2,0:0:0:0: +399,365,129839,1,2,3:2:0:0: +363,336,129930,1,2,2:2:0:0: +319,321,130021,1,2,2:2:0:0: +274,327,130111,1,2,1:2:0:0: +233,348,130202,1,2,2:2:0:0: +188,355,130293,1,2,2:2:0:0: +144,341,130384,2,0,P|120:293|207:221,1,170,2|2,3:2|1:2,0:0:0:0: +282,129,130793,5,0,1:1:0:0: +282,129,130861,1,0,1:1:0:0: +282,129,130930,2,0,B|317:20|317:20|237:48,1,170,6|2,3:2|1:2,0:0:0:0: +264,38,131339,2,0,P|186:59|98:14,1,170,2|2,2:2|3:2,0:0:0:0: +107,24,131748,2,0,P|133:66|130:126,1,85,2|0,1:2|2:2,0:0:0:0: +88,171,132021,6,0,P|62:230|115:333,1,170,2|0,3:2|1:1,0:0:0:0: +100,322,132430,2,0,B|51:323|21:355|21:355|63:331|120:358,1,170,2|0,3:2|3:3,0:0:0:0: +100,350,132839,2,0,P|148:352|184:332,1,85,2|0,1:2|2:2,0:0:0:0: +246,281,133111,6,0,L|332:307,1,85,2|0,3:2|0:0,0:0:0:0: +390,362,133384,1,0,1:1:0:0: +472,339,133521,1,2,2:2:0:0: +491,256,133657,1,2,3:2:0:0: +439,188,133793,1,2,3:2:0:0: +420,104,133930,1,2,1:2:0:0: +461,29,134066,1,2,3:2:0:0: +448,181,134202,5,0,3:3:0:0: +381,127,134339,1,2,3:2:0:0: +296,115,134475,1,0,1:1:0:0: +214,139,134611,1,2,3:2:0:0: +164,208,134748,2,0,P|121:226|70:220,1,85,2|0,2:2|3:3,0:0:0:0: +19,113,135021,2,0,P|61:112|99:129,1,85,2|0,1:2|3:3,0:0:0:0: +25,309,135293,6,0,B|122:323|78:369|209:375,1,170,6|0,3:2|1:1,0:0:0:0: +252,328,135702,1,2,2:2:0:0: +252,328,135839,2,0,L|241:241,1,85,2|2,3:2|3:2,0:0:0:0: +175,190,136111,2,0,L|186:103,1,85,2|2,1:2|2:2,0:0:0:0: +138,34,136384,5,2,3:2:0:0: +194,98,136521,1,2,2:2:0:0: +278,109,136657,1,2,1:2:0:0: +360,89,136793,1,2,3:2:0:0: +407,17,136930,5,2,2:2:0:0: +447,139,137066,1,2,3:2:0:0: +367,239,137202,1,2,1:2:0:0: +407,361,137338,1,2,2:2:0:0: +280,384,137475,5,2,3:2:0:0: +194,371,137611,1,2,2:2:0:0: +207,285,137748,1,2,1:2:0:0: +293,298,137884,1,2,2:2:0:0: +198,273,138021,2,0,P|184:301|47:327,1,170,2|2,3:2|1:2,0:0:0:0: +20,80,138566,5,2,3:2:0:0: +67,49,138657,1,2,2:2:0:0: +122,40,138748,1,2,2:2:0:0: +178,47,138839,1,2,1:2:0:0: +221,83,138930,1,2,2:2:0:0: +244,135,139021,1,2,2:2:0:0: +248,190,139111,2,0,P|240:230|225:257,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +327,154,139384,6,0,L|485:175,1,127.5,8|4,2:3|2:3,0:0:0:0: +489,146,139657,2,0,P|448:57|374:68,1,170,6|2,3:2|1:2,0:0:0:0: +311,20,140066,2,0,P|284:80|187:82,1,170,2|2,2:2|3:2,0:0:0:0: +118,35,140475,2,0,P|72:33|32:60,1,85,2|2,1:2|2:2,0:0:0:0: +13,133,140748,5,2,3:2:0:0: +93,158,140884,1,2,2:2:0:0: +30,216,141021,1,2,1:2:0:0: +91,338,141157,2,0,B|171:350|171:350|180:362|180:362|285:375,1,170,2|2,3:2|3:2,0:0:0:0: +253,371,141566,2,0,B|265:333|265:333|249:279,1,85,2|2,1:2|2:2,0:0:0:0: +302,220,141839,6,0,P|255:180|262:73,1,170,2|2,3:2|1:2,0:0:0:0: +329,31,142248,1,0,0:0:0:0: +401,75,142384,2,0,L|476:57,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +430,153,142657,2,0,L|505:135,2,56.6666666666667,2|2|2,1:2|2:2|2:2,0:0:0:0: +474,226,142930,1,2,3:2:0:0: +433,207,143020,1,2,2:2:0:0: +389,215,143111,1,2,2:2:0:0: +356,246,143202,1,2,1:2:0:0: +347,289,143293,1,2,2:2:0:0: +363,331,143384,1,2,2:2:0:0: +403,353,143475,6,0,L|482:334,1,56.6666666666667,2|2,3:2|2:2,0:0:0:0: +315,310,143657,1,2,2:2:0:0: +303,314,143748,2,0,L|224:333,1,56.6666666666667,2|2,1:2|2:2,0:0:0:0: +152,306,143930,1,2,2:2:0:0: +140,310,144021,6,0,B|90:324|70:373|70:373|26:351|36:287,1,170,6|2,3:2|1:2,0:0:0:0: +34,314,144430,2,0,P|40:249|156:209,1,170,2|2,2:2|3:2,0:0:0:0: +151,40,144839,1,2,1:2:0:0: +151,40,144975,1,2,2:2:0:0: +91,111,145111,6,0,L|0:97,1,85,2|2,3:2|2:2,0:0:0:0: +124,200,145384,2,0,L|215:186,1,85,2|2,1:2|3:2,0:0:0:0: +284,148,145657,1,2,2:2:0:0: +330,77,145793,1,2,3:2:0:0: +412,55,145930,1,2,1:2:0:0: +494,75,146066,1,2,2:2:0:0: +422,196,146202,6,0,B|333:210|378:259|237:279,1,170,2|2,3:2|1:2,0:0:0:0: +273,272,146611,1,2,2:2:0:0: +242,384,146748,2,0,P|204:342|143:323,1,85,2|2,3:2|3:2,0:0:0:0: +33,327,147021,2,0,P|69:305|95:272,1,85,2|2,1:2|3:2,0:0:0:0: +120,188,147293,6,0,L|190:167,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +83,110,147566,2,0,L|-14:91,1,85,2|0,1:2|0:0,0:0:0:0: +175,0,147839,1,2,3:2:0:0: +256,22,147975,1,2,1:2:0:0: +195,80,148111,1,2,1:2:0:0: +300,176,148248,5,0,1:1:0:0: +300,176,148316,1,0,1:1:0:0: +300,176,148384,2,0,B|165:59|28:174|28:174|85:282|220:240|220:240|95:264|150:399|277:387|218:278|354:337,1,815.999975097657,6|0,3:2|3:3,0:0:0:0: +416,358,149611,2,0,P|476:322|492:287,2,85,2|2|2,2:2|1:2|3:2,0:0:0:0: +318,324,150021,1,2,2:2:0:0: +318,324,150157,1,2,3:2:0:0: +395,257,150293,2,0,P|383:208|403:147,1,85,2|2,1:2|2:2,0:0:0:0: +502,55,150566,5,2,3:2:0:0: +388,174,150702,1,2,2:2:0:0: +388,174,150839,1,2,1:2:0:0: +354,23,150975,2,0,B|185:40|253:129|72:146|72:146|193:127|252:221|252:221|114:248|122:369,1,713.99997821045,2|0,2:2|1:1,0:0:0:0: +37,281,152066,2,0,P|24:322|28:375,2,85,2|2|2,3:2|2:2|3:2,0:0:0:0: +73,147,152475,2,0,P|120:193|129:237,1,85,2|2,3:2|3:2,0:0:0:0: +211,372,152748,6,0,P|247:328|376:346,1,170,4|2,3:2|1:2,0:0:0:0: +499,342,153157,2,0,L|323:365,1,170,2|2,2:2|3:2,0:0:0:0: +279,292,153566,2,0,L|300:206,1,85,2|2,1:2|2:2,0:0:0:0: +236,151,153839,5,2,3:2:0:0: +299,209,153975,1,2,2:2:0:0: +375,172,154111,1,2,1:2:0:0: +448,128,154248,2,0,B|479:97|461:40|461:40|346:20|305:110,1,255,2|0,3:2|1:1,0:0:0:0: +41,18,154930,5,2,3:2:0:0: +28,61,155020,1,2,2:2:0:0: +40,104,155111,1,2,2:2:0:0: +72,135,155202,1,2,1:2:0:0: +115,146,155293,1,2,2:2:0:0: +158,134,155384,1,2,2:2:0:0: +198,111,155475,1,2,3:2:0:0: +254,104,155565,1,2,2:2:0:0: +309,117,155656,1,2,2:2:0:0: +356,146,155747,1,2,1:2:0:0: +392,190,155838,1,2,2:2:0:0: +411,243,155929,1,2,2:2:0:0: +411,300,156021,6,0,B|389:376|282:346|282:264|334:228|334:228|440:151|406:51|345:3|259:6|200:62|212:132,1,611.999981323243,2|8,3:2|2:3,0:0:0:0: +213,110,156907,1,8,2:3:0:0: +214,120,156975,1,4,2:3:0:0: +215,130,157043,1,4,2:3:0:0: +216,140,157111,6,0,L|79:122,1,101.999996887207,6|0,3:2|2:2,0:0:0:0: +3,253,157384,2,0,L|105:267,1,101.999996887207,2|0,1:2|2:2,0:0:0:0: +124,138,157657,2,0,L|226:152,1,101.999996887207,2|0,3:2|3:3,0:0:0:0: +13,265,157930,2,0,L|115:279,1,101.999996887207,2|0,1:2|2:2,0:0:0:0: +134,150,158202,2,0,L|236:164,1,101.999996887207,2|0,3:2|2:2,0:0:0:0: +23,277,158475,2,0,L|125:291,1,101.999996887207,2|0,1:2|3:3,0:0:0:0: +144,162,158748,2,0,L|246:176,1,101.999996887207,2|0,2:2|3:3,0:0:0:0: +33,289,159021,2,0,L|135:303,1,101.999996887207,2|0,1:2|2:2,0:0:0:0: +154,174,159293,2,0,L|256:188,1,101.999996887207,2|0,3:2|2:2,0:0:0:0: +43,301,159566,2,0,L|145:315,1,101.999996887207,2|0,1:2|2:2,0:0:0:0: +164,186,159839,2,0,L|266:200,1,101.999996887207,2|0,3:2|3:3,0:0:0:0: +53,313,160112,2,0,L|155:327,1,101.999996887207,2|0,1:2|3:3,0:0:0:0: +174,198,160384,2,0,L|276:212,1,101.999996887207,2|0,3:2|3:3,0:0:0:0: +63,325,160657,2,0,L|165:339,1,101.999996887207,2|0,1:2|3:3,0:0:0:0: +184,210,160930,2,0,L|286:224,1,101.999996887207,2|0,2:2|3:3,0:0:0:0: +73,337,161202,2,0,L|175:351,1,101.999996887207,2|0,1:2|3:3,0:0:0:0: +300,105,161475,6,0,L|437:87,1,101.999996887207,6|0,3:2|2:2,0:0:0:0: +512,218,161748,2,0,L|410:231,1,101.999996887207,2|0,1:2|2:2,0:0:0:0: +391,103,162021,2,0,L|289:116,1,101.999996887207,2|0,3:2|3:3,0:0:0:0: +502,230,162294,2,0,L|400:243,1,101.999996887207,2|0,1:2|2:2,0:0:0:0: +381,115,162566,2,0,L|279:128,1,101.999996887207,2|0,3:2|2:2,0:0:0:0: +492,242,162839,2,0,L|390:255,1,101.999996887207,2|0,1:2|3:3,0:0:0:0: +371,127,163112,2,0,L|269:140,1,101.999996887207,2|0,2:2|3:3,0:0:0:0: +482,254,163385,2,0,L|380:267,1,101.999996887207,2|0,1:2|2:2,0:0:0:0: +361,139,163657,2,0,L|259:152,1,101.999996887207,2|0,3:2|2:2,0:0:0:0: +472,266,163930,2,0,L|370:279,1,101.999996887207,2|0,1:2|2:2,0:0:0:0: +351,151,164203,2,0,L|249:164,1,101.999996887207,2|0,3:2|3:3,0:0:0:0: +462,278,164476,2,0,L|360:291,1,101.999996887207,2|0,1:2|3:3,0:0:0:0: +341,163,164748,2,0,L|239:176,1,101.999996887207,2|0,3:2|3:3,0:0:0:0: +452,290,165021,2,0,L|350:303,1,101.999996887207,2|0,1:2|3:3,0:0:0:0: +331,175,165294,2,0,L|229:188,1,101.999996887207,2|0,2:2|3:3,0:0:0:0: +396,99,165566,1,2,1:2:0:0: +216,86,165702,5,0,1:1:0:0: +216,86,165771,1,0,1:1:0:0: +216,86,165839,2,0,L|234:223,1,101.999996887207,6|0,3:2|2:2,0:0:0:0: +103,299,166112,2,0,L|89:197,1,101.999996887207,2|0,1:2|2:2,0:0:0:0: +218,178,166385,2,0,L|204:76,1,101.999996887207,2|0,3:2|3:3,0:0:0:0: +91,289,166658,2,0,L|77:187,1,101.999996887207,2|0,1:2|2:2,0:0:0:0: +206,168,166930,2,0,L|192:66,1,101.999996887207,2|0,3:2|2:2,0:0:0:0: +79,279,167203,2,0,L|65:177,1,101.999996887207,2|0,1:2|3:3,0:0:0:0: +194,158,167476,2,0,L|180:56,1,101.999996887207,2|0,2:2|3:3,0:0:0:0: +67,269,167749,2,0,L|53:167,1,101.999996887207,2|0,1:2|2:2,0:0:0:0: +182,148,168021,2,0,L|168:46,1,101.999996887207,2|0,3:2|2:2,0:0:0:0: +55,259,168294,2,0,L|41:157,1,101.999996887207,2|0,1:2|2:2,0:0:0:0: +170,138,168567,2,0,L|156:36,1,101.999996887207,2|0,3:2|3:3,0:0:0:0: +43,249,168840,2,0,L|29:147,1,101.999996887207,2|0,1:2|3:3,0:0:0:0: +158,128,169112,2,0,L|144:26,1,101.999996887207,2|0,3:2|3:3,0:0:0:0: +31,239,169385,2,0,L|17:137,1,101.999996887207,2|0,1:2|3:3,0:0:0:0: +146,118,169658,2,0,L|132:16,1,101.999996887207,2|0,2:2|3:3,0:0:0:0: +19,229,169930,2,0,L|5:127,1,101.999996887207,2|0,1:2|3:3,0:0:0:0: +280,171,170202,6,0,L|262:308,1,101.999996887207,6|0,3:2|2:2,0:0:0:0: +393,384,170475,2,0,L|407:282,1,101.999996887207,2|0,1:2|2:2,0:0:0:0: +278,263,170748,2,0,L|292:161,1,101.999996887207,2|0,3:2|3:3,0:0:0:0: +405,374,171021,2,0,L|419:272,1,101.999996887207,2|0,1:2|2:2,0:0:0:0: +290,253,171293,2,0,L|304:151,1,101.999996887207,2|0,3:2|2:2,0:0:0:0: +417,364,171566,2,0,L|431:262,1,101.999996887207,2|0,1:2|3:3,0:0:0:0: +302,243,171839,2,0,L|316:141,1,101.999996887207,2|0,2:2|3:3,0:0:0:0: +429,354,172112,2,0,L|443:252,1,101.999996887207,2|0,1:2|2:2,0:0:0:0: +512,181,172384,1,2,3:3:0:0: +512,181,173278,6,0,P|452:146|386:277,1,255,6|0,3:2|0:0,0:0:0:0: +327,334,173722,2,0,L|257:321,5,56.6666666666667,0|0|0|2|2|2,3:3|0:0|0:0|3:2|2:2|2:2,0:0:0:0: +178,230,174166,2,0,L|248:217,5,56.6666666666667,2|2|2|2|2|2,3:2|2:2|2:2|3:2|2:2|2:2,0:0:0:0: +92,334,174611,2,0,L|22:321,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +99,348,174833,2,0,L|29:335,2,56.6666666666667,2|0|0,3:2|0:0|0:0,0:0:0:0: +179,312,175055,6,0,P|188:278|169:215,1,85,2|2,3:2|1:2,0:0:0:0: +84,148,175278,1,2,3:2:0:0: +84,148,175389,1,2,1:2:0:0: +84,148,175500,2,0,L|-17:135,1,85,2|2,3:2|1:2,0:0:0:0: +176,61,175722,2,0,L|277:48,1,85,2|2,3:2|1:2,0:0:0:0: +378,32,175944,1,2,3:2:0:0: +359,97,176055,1,0,0:0:0:0: +380,161,176166,1,2,3:2:0:0: +437,198,176278,1,0,0:0:0:0: +504,198,176389,2,0,P|513:147|489:106,1,85,2|0,3:2|0:0,0:0:0:0: +464,293,176611,2,0,P|415:310|391:351,1,85,2|0,3:2|0:0,0:0:0:0: +223,292,176833,6,0,B|246:357|246:357|352:294|309:142,1,255,2|2,3:2|1:2,0:0:0:0: +314,26,177278,1,2,3:2:0:0: +393,73,177389,1,2,1:2:0:0: +393,73,177500,2,0,L|500:51,1,85,2|2,3:2|1:2,0:0:0:0: +238,144,177722,5,2,3:2:0:0: +238,144,177833,1,2,1:2:0:0: +238,144,177944,2,0,L|131:122,1,85,2|2,3:2|1:2,0:0:0:0: +51,179,178166,2,0,P|53:134|32:88,1,85,2|0,3:2|0:0,0:0:0:0: +136,321,178389,2,0,P|134:279|149:240,1,85,2|0,3:2|0:0,0:0:0:0: +311,365,178611,6,0,L|388:385,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +361,293,178833,2,0,L|437:271,2,56.6666666666667,2|0|0,3:2|0:0|0:0,0:0:0:0: +368,205,179055,2,0,L|423:148,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +330,125,179278,2,0,L|350:47,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +442,29,179500,5,2,3:2:0:0: +442,29,179574,1,2,2:2:0:0: +442,29,179648,1,2,2:2:0:0: +442,29,179722,2,0,L|422:106,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +488,149,179944,2,0,B|406:177|450:214|340:247,1,170,2|2,3:2|1:2,0:0:0:0: +114,91,180389,6,0,P|80:60|39:51,1,85,6|2,3:2|1:2,0:0:0:0: +0,130,180611,2,0,P|30:160|71:171,1,85,2|2,3:2|1:2,0:0:0:0: +124,301,180833,2,0,L|109:392,1,85,2|2,3:2|1:2,0:0:0:0: +201,378,181055,2,0,L|216:287,1,85,2|2,3:2|1:2,0:0:0:0: +350,243,181278,2,0,L|418:301,1,85,2|2,3:2|1:2,0:0:0:0: +497,261,181500,2,0,L|513:173,2,85,2|2|2,3:2|1:2|3:2,0:0:0:0: +414,298,181833,1,2,1:2:0:0: +414,298,181944,2,0,P|365:311|334:341,1,85,2|0,3:2|0:0,0:0:0:0: +254,216,182166,5,2,3:2:0:0: +186,206,182278,1,2,1:2:0:0: +123,233,182389,1,2,3:2:0:0: +89,291,182500,1,2,1:2:0:0: +101,357,182611,2,0,B|135:293|107:231|93:241|46:187|83:107,1,255,2|0,3:2|1:1,0:0:0:0: +0,29,183055,6,0,P|27:53|84:63,1,85,2|0,3:2|0:0,0:0:0:0: +176,171,183278,2,0,P|210:159|247:115,1,85,2|2,3:2|1:2,0:0:0:0: +353,40,183500,2,0,L|364:155,1,85,2|2,3:2|1:2,0:0:0:0: +473,10,183722,2,0,L|462:125,1,85,2|2,3:2|1:2,0:0:0:0: +447,199,183944,5,2,3:2:0:0: +447,199,184055,1,0,0:0:0:0: +447,199,184166,1,2,3:2:0:0: +463,223,184277,1,0,0:0:0:0: +487,237,184388,2,0,L|476:352,1,85,2|2,3:2|1:2,0:0:0:0: +344,381,184611,2,0,L|333:266,1,85,2|2,3:2|1:2,0:0:0:0: +233,174,184833,6,0,P|186:180|144:208,1,85,2|2,3:2|1:2,0:0:0:0: +19,319,185055,2,0,P|56:339|98:343,1,85,2|2,3:2|1:2,0:0:0:0: +224,268,185278,1,2,3:2:0:0: +229,200,185389,1,2,1:2:0:0: +203,136,185500,1,2,3:2:0:0: +148,95,185611,1,0,0:0:0:0: +80,84,185722,2,0,P|45:119|29:167,1,85,2|0,3:2|0:0,0:0:0:0: +227,49,185944,6,0,L|282:-7,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +306,84,186166,2,0,L|382:63,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +358,156,186388,2,0,L|434:176,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +366,244,186611,2,0,L|423:300,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +512,269,186833,5,2,3:2:0:0: +512,269,186907,1,2,2:2:0:0: +512,269,186981,1,2,0:0:0:0: +512,269,187055,2,0,L|455:213,2,56.6666666666667,2|0|0,1:2|0:0|0:0,0:0:0:0: +469,351,187277,2,0,P|423:346|367:392,1,113.333333333333,8|0,2:3|0:0,0:0:0:0: +346,383,187500,6,0,B|296:353|296:353|274:238|376:162,1,255,6|0,3:2|1:1,0:0:0:0: +326,22,187944,1,2,3:2:0:0: +397,68,188055,2,0,P|439:74|505:42,1,85,2|0,1:2|3:3,0:0:0:0: +269,143,188278,1,2,1:2:0:0: +269,143,188389,2,0,P|236:175|218:221,1,85,2|2,3:2|1:2,0:0:0:0: +209,352,188611,6,0,L|109:339,1,85,2|2,3:2|1:2,0:0:0:0: +13,230,188833,2,0,L|113:217,1,85,2|2,3:2|1:2,0:0:0:0: +163,98,189055,2,0,L|63:85,1,85,2|2,3:2|1:2,0:0:0:0: +133,9,189277,6,0,L|217:19,1,85,2|2,3:2|1:2,0:0:0:0: +248,145,189499,2,0,L|288:105,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +309,248,189721,2,0,L|323:194,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +414,304,189944,2,0,L|399:250,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +468,194,190166,6,0,L|488:117,5,56.6666666666667,2|2|2|2|2|2,3:2|2:2|2:2|3:2|2:2|2:2,0:0:0:0: +408,16,190611,2,0,L|423:71,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +399,25,190833,2,0,L|413:79,2,56.6666666666667,2|0|0,3:2|0:0|0:0,0:0:0:0: +311,21,191055,6,0,P|386:53|353:174,1,170,2|2,3:2|3:2,0:0:0:0: +272,212,191389,1,2,1:2:0:0: +272,212,191500,2,0,P|303:227|343:276,1,85,2|2,3:2|1:2,0:0:0:0: +461,327,191722,2,0,P|432:346|370:356,1,85,2|2,3:2|1:2,0:0:0:0: +215,380,191944,1,2,3:2:0:0: +189,357,192055,1,2,1:2:0:0: +157,343,192166,1,2,3:2:0:0: +123,340,192277,1,2,1:2:0:0: +89,347,192389,2,0,P|49:335|11:294,1,85,2|0,3:2|1:1,0:0:0:0: +54,172,192611,2,0,P|44:131|60:77,1,85,2|0,3:2|1:0,0:0:0:0: +208,24,192833,2,0,L|193:115,1,85,2|2,3:2|1:2,0:0:0:0: +275,157,193055,2,0,L|290:66,1,85,2|2,3:2|1:2,0:0:0:0: +415,27,193277,5,2,3:2:0:0: +461,98,193389,1,2,1:2:0:0: +458,182,193500,1,2,3:2:0:0: +413,254,193611,1,2,1:2:0:0: +329,269,193722,2,0,P|286:264|227:290,1,85,2|0,3:2|0:0,0:0:0:0: +377,373,193944,2,0,P|420:378|479:352,1,85,2|0,3:2|0:0,0:0:0:0: +491,288,194166,2,0,B|475:189|434:241|422:89,1,170,2|0,3:2|1:1,0:0:0:0: +51,35,194611,6,0,B|97:71|166:63|166:63|220:147|220:147|287:120|391:189,1,340,6|0,3:2|3:3,0:0:0:0: +165,279,195166,1,2,1:2:0:0: +201,189,195277,2,0,P|241:220|260:277,1,85,2|2,3:2|1:2,0:0:0:0: +47,321,195500,2,0,P|53:270|93:225,1,85,2|2,3:2|1:2,0:0:0:0: +238,346,195722,5,2,3:2:0:0: +320,365,195833,1,2,1:2:0:0: +402,345,195944,1,2,3:2:0:0: +462,285,196055,1,2,1:2:0:0: +484,203,196166,2,0,P|479:158|404:126,1,113.333333333333,2|2,3:2|0:0,0:0:0:0: +354,57,196389,6,0,L|361:0,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +290,124,196611,2,0,L|297:67,3,56.6666666666667,2|2|2|2,3:2|2:2|2:2|3:2,0:0:0:0: +242,209,196907,2,0,L|234:265,1,56.6666666666667,2|2,2:2|2:2,0:0:0:0: +192,279,197055,2,0,L|199:335,2,56.6666666666667,2|2|2,3:2|2:2|2:2,0:0:0:0: +108,239,197277,2,0,L|52:232,5,56.6666666666667,2|2|2|2|2|2,3:2|2:2|2:2|3:2|2:2|2:2,0:0:0:0: +0,305,197722,2,0,P|65:299|94:417,1,170,2|2,3:2|3:2,0:0:0:0: +391,327,198166,6,0,L|461:316,5,56.6666666666667,2|2|2|2|2|2,3:2|0:2|0:2|3:2|0:2|0:2,0:0:0:0: +317,265,198611,1,2,3:2:0:0: +317,265,198685,1,2,0:2:0:0: +317,265,198759,1,2,0:2:0:0: +317,265,198833,2,0,L|247:254,2,56.6666666666667,2|2|0,3:2|0:0|0:0,0:0:0:0: +392,180,199055,2,0,L|403:110,5,56.6666666666667,2|2|2|2|0|0,3:2|0:2|0:2|3:2|0:0|0:0,0:0:0:0: +494,85,199500,2,0,L|483:15,5,56.6666666666667,2|2|2|2|0|0,3:2|0:2|0:2|3:2|0:0|0:0,0:0:0:0: +400,124,199944,6,0,L|330:113,5,56.6666666666667,2|2|2|2|2|2,3:2|0:2|0:2|3:2|0:2|0:2,0:0:0:0: +267,59,200389,1,2,3:2:0:0: +267,59,200463,1,2,0:2:0:0: +267,59,200537,1,2,0:2:0:0: +267,59,200611,2,0,L|197:70,2,56.6666666666667,2|2|0,3:2|0:0|0:0,0:0:0:0: +121,115,200833,2,0,L|110:45,5,56.6666666666667,2|2|2|2|2|2,3:2|0:2|0:2|3:2|0:2|0:2,0:0:0:0: +179,202,201277,2,0,L|168:272,2,56.6666666666667,2|0|0,1:2|0:0|0:0,0:0:0:0: +67,245,201500,2,0,L|78:315,2,56.6666666666667,8|0|0,2:3|0:0|0:0,0:0:0:0: +11,377,201722,5,4,3:2:0:0: +256,192,201776,12,4,205276,3:2:0:0: +171,17,207943,6,0,L|178:69,31,34,0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0,3:3|0:0|0:0|0:0|0:0|3:3|0:0|0:0|0:0|0:0|3:3|0:0|0:0|0:0|0:0|3:3|0:0|0:0|0:0|0:0|3:3|0:0|0:0|0:0|0:0|3:3|0:0|0:0|0:0|0:0|3:3|0:0,0:0:0:0: +85,45,210124,5,8,3:3:0:0: +73,234,210329,1,4,3:3:0:0: +243,150,210533,1,8,3:3:0:0: +122,74,210670,5,8,3:3:0:0: +61,252,210875,1,4,3:3:0:0: +246,215,211079,1,8,3:3:0:0: +294,296,211215,6,0,L|239:313,3,42.5,14|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +369,234,211488,2,0,L|410:247,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +319,156,211761,2,0,L|307:116,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +221,73,212033,2,0,L|209:114,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +121,141,212306,5,10,3:2:0:0: +112,138,212374,1,0,3:3:0:0: +103,135,212442,1,0,3:3:0:0: +78,40,212579,1,10,3:2:0:0: +87,37,212647,1,0,3:3:0:0: +96,34,212715,1,0,3:3:0:0: +0,115,212851,2,0,L|13:156,2,42.5,10|0|0,3:2|3:3|3:3,0:0:0:0: +77,232,213124,2,0,L|65:273,2,42.5,10|0|0,3:2|3:3|3:3,0:0:0:0: +131,350,213397,6,0,L|172:338,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +261,301,213670,2,0,L|316:318,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +366,247,213942,2,0,L|354:207,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +456,272,214215,2,0,L|444:312,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +490,185,214488,5,10,3:2:0:0: +487,176,214556,1,0,3:3:0:0: +484,167,214624,1,0,3:3:0:0: +419,92,214761,1,10,3:2:0:0: +422,83,214829,1,0,3:3:0:0: +425,74,214897,1,0,3:3:0:0: +344,17,215033,1,10,3:2:0:0: +336,19,215101,1,0,3:3:0:0: +328,21,215170,2,0,L|224:9,1,85,0|10,3:3|3:2,0:0:0:0: +238,216,215579,6,0,L|250:256,3,42.5,14|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +182,318,215852,2,0,L|170:358,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +90,275,216124,2,0,L|49:263,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +51,166,216397,2,0,L|-4:183,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +64,70,216670,5,10,3:2:0:0: +73,73,216738,1,0,3:3:0:0: +82,76,216806,1,0,3:3:0:0: +191,122,216942,1,10,3:2:0:0: +200,119,217010,1,0,3:3:0:0: +209,116,217078,1,0,3:3:0:0: +243,18,217215,2,0,L|298:35,2,42.5,10|0|0,3:2|3:3|3:3,0:0:0:0: +350,113,217488,2,0,L|309:125,2,42.5,10|0|0,3:2|3:3|3:3,0:0:0:0: +425,177,217761,6,0,L|413:217,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +481,279,218034,2,0,L|493:319,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +411,375,218306,2,0,L|370:363,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +328,276,218579,2,0,L|273:293,3,42.5,10|0|0|0,3:2|3:3|3:3|3:3,0:0:0:0: +208,353,218851,5,10,3:2:0:0: +205,362,218919,1,0,3:3:0:0: +202,371,218987,1,0,3:3:0:0: +120,294,219124,1,10,3:2:0:0: +117,285,219192,1,0,3:3:0:0: +114,276,219260,1,0,3:3:0:0: +44,203,219397,2,0,L|55:145,7,42.5,10|0|0|0|0|0|0|0,3:2|3:3|3:3|3:3|3:3|3:3|3:3|3:3,0:0:0:0: +142,171,219943,5,0,1:1:0:0: +146,181,220011,1,0,1:1:0:0: +151,190,220079,2,0,L|202:199,3,42.5,0|0|0|0,1:1|1:1|1:1|1:1,0:0:0:0: +269,153,220352,2,0,L|320:162,1,42.5,0|0,1:1|1:1,0:0:0:0: +320,248,220488,2,0,L|371:257,8,42.5,0|0|0|0|0|0|0|0|0,1:1|1:1|1:1|1:1|1:1|1:1|1:1|1:1|0:0,0:0:0:0: +364,28,222670,6,0,L|424:7,7,42.5,4|8|8|8|4|4|4|4,1:2|2:3|2:3|2:3|2:3|2:3|2:3|2:3,0:0:0:0: +487,58,223215,2,0,L|470:149,1,85,6|2,3:2|1:2,0:0:0:0: +437,312,223488,2,0,L|420:221,1,85,0|2,3:2|1:2,0:0:0:0: +314,245,223761,1,2,3:2:0:0: +240,320,223897,1,2,1:2:0:0: +240,320,223965,1,2,3:2:0:0: +240,320,224033,2,0,L|149:337,1,85,2|2,3:2|1:2,0:0:0:0: +37,266,224306,5,2,3:2:0:0: +37,266,224443,1,2,1:2:0:0: +142,352,224579,2,0,L|225:336,1,85,2|2,3:2|1:2,0:0:0:0: +304,206,224852,2,0,L|288:123,1,85,2|0,3:2|1:1,0:0:0:0: +164,41,225124,2,0,L|172:0,3,42.5,0|0|0|0,3:3|3:3|1:1|3:3,0:0:0:0: +84,68,225397,6,0,P|125:92|149:148,1,85,2|0,3:2|1:1,0:0:0:0: +86,190,225670,2,0,P|45:166|21:110,1,85,2|0,3:2|1:1,0:0:0:0: +39,266,225943,2,0,L|48:358,1,85,2|0,3:2|1:1,0:0:0:0: +137,365,226215,2,0,L|128:273,1,85,2|0,3:2|1:1,0:0:0:0: +237,209,226488,6,0,L|329:218,1,85,2|0,3:2|1:1,0:0:0:0: +361,127,226761,1,2,3:2:0:0: +361,127,226897,1,2,1:2:0:0: +488,185,227033,2,0,L|479:277,1,85,2|0,3:2|1:1,0:0:0:0: +429,362,227306,2,0,L|438:270,1,85,2|0,3:2|1:1,0:0:0:0: +361,127,227579,6,0,P|344:82|354:27,1,85,6|2,3:2|3:2,0:0:0:0: +195,127,227852,2,0,P|196:169|180:208,1,85,0|2,3:3|3:2,0:0:0:0: +211,346,228124,1,2,3:2:0:0: +131,297,228261,1,2,3:2:0:0: +131,297,228329,1,2,3:2:0:0: +131,297,228397,2,0,L|32:288,1,85,2|2,3:2|3:2,0:0:0:0: +67,158,228670,5,8,2:3:0:0: +59,126,228738,1,8,2:3:0:0: +63,92,228806,1,8,2:3:0:0: +79,62,228874,1,8,2:3:0:0: +104,40,228942,1,4,2:3:0:0: +210,91,229079,1,4,2:3:0:0: +224,95,229147,1,4,2:3:0:0: +238,99,229215,2,0,L|186:122,2,42.5,8|8|8,2:3|2:3|2:3,0:0:0:0: +353,24,229488,2,0,L|336:63,1,42.5,4|4,2:3|2:3,0:0:0:0: +425,66,229624,2,0,L|408:105,1,42.5,4|4,2:3|2:3,0:0:0:0: +495,111,229760,5,6,3:2:0:0: +221,375,231943,1,6,3:2:0:0: +102,54,233579,2,0,P|53:62|29:78,5,67.9999979248048,2|2|2|10|10|6,1:2|1:2|1:2|2:3|2:3|2:3,0:0:0:0: +93,147,234124,6,0,P|86:180|62:209,2,67.9999979248048,6|2|2,3:2|2:2|2:2,0:0:0:0: +185,174,234397,2,0,P|196:207|190:244,2,67.9999979248048,10|2|2,2:2|2:2|2:2,0:0:0:0: +257,115,234670,2,0,P|250:82|226:53,2,67.9999979248048,2|2|2,3:2|2:2|2:2,0:0:0:0: +349,88,234943,2,0,P|360:56|354:19,2,67.9999979248048,10|2|2,2:2|2:2|2:2,0:0:0:0: +431,140,235215,5,2,3:2:0:0: +439,130,235306,2,0,L|516:114,1,67.9999979248048,2|2,2:2|2:2,0:0:0:0: +502,215,235488,1,10,2:2:0:0: +460,250,235579,1,2,2:2:0:0: +406,252,235670,1,2,2:2:0:0: +358,230,235760,2,0,P|289:219|204:322,1,203.999993774414,2|10,3:2|2:2,0:0:0:0: +204,309,236306,6,0,L|292:317,2,67.9999979248048,2|2|2,3:2|2:2|2:2,0:0:0:0: +161,221,236579,2,0,L|249:206,2,67.9999979248048,10|2|2,2:2|2:2|2:2,0:0:0:0: +77,165,236852,2,0,L|-11:173,2,67.9999979248048,2|2|2,3:2|2:2|2:2,0:0:0:0: +120,77,237125,2,0,L|32:62,2,67.9999979248048,10|2|2,2:2|2:2|2:2,0:0:0:0: +194,12,237397,5,2,3:2:0:0: +203,22,237488,2,0,P|218:61|208:109,1,67.9999979248048,2|2,2:2|2:2,0:0:0:0: +296,151,237670,1,10,2:2:0:0: +349,144,237760,1,2,2:2:0:0: +391,109,237851,1,2,2:2:0:0: +400,55,237942,2,0,P|349:167|431:250,1,203.999993774414,10|2,3:2|1:2,0:0:0:0: +385,228,238488,6,0,P|371:267|378:322,2,67.9999979248048,6|2|2,3:2|2:2|2:2,0:0:0:0: +276,298,238761,2,0,P|283:339|317:382,2,67.9999979248048,10|2|2,2:2|2:2|2:2,0:0:0:0: +188,248,239033,2,0,P|196:206|229:162,2,67.9999979248048,2|2|2,3:2|2:2|2:2,0:0:0:0: +129,131,239306,2,0,P|156:98|207:77,2,67.9999979248048,10|2|2,2:2|2:2|2:2,0:0:0:0: +38,119,239579,5,2,3:2:0:0: +32,135,239670,2,0,P|35:162|86:218,1,67.9999979248048,2|2,2:2|2:2,0:0:0:0: +20,291,239851,1,10,2:2:0:0: +57,251,239942,1,2,2:2:0:0: +108,235,240033,1,2,2:2:0:0: +161,244,240124,2,0,B|269:295|276:214|401:275,1,203.999993774414,2|10,3:2|2:2,0:0:0:0: +360,258,240670,6,0,L|297:281,2,67.9999979248048,2|2|2,3:2|2:2|2:2,0:0:0:0: +460,308,240942,2,0,L|405:347,2,67.9999979248048,10|2|2,2:2|2:2|2:2,0:0:0:0: +448,213,241215,2,0,L|511:190,2,67.9999979248048,2|2|2,3:2|2:2|2:2,0:0:0:0: +430,114,241488,2,0,L|484:75,2,67.9999979248048,10|2|2,2:2|2:2|2:2,0:0:0:0: +365,38,241760,5,2,3:2:0:0: +354,51,241852,2,0,P|332:92|337:142,1,67.9999979248048,2|2,2:2|2:2,0:0:0:0: +244,165,242033,1,2,1:2:0:0: +191,156,242124,1,2,1:2:0:0: +145,129,242215,1,2,1:2:0:0: +91,133,242306,2,0,B|109:32|109:32|82:-34,2,135.99999584961,6|0|0,1:2|0:0|0:0,0:0:0:0: +33,221,242852,6,0,L|42:273,3,42.5,6|0|8|0,3:2|3:3|3:2|0:0,0:0:0:0: +134,256,243125,2,0,L|125:308,3,42.5,2|0|8|0,3:2|0:0|3:2|0:0,0:0:0:0: +228,299,243397,6,0,L|269:291,1,42.5,2|0,3:2|3:3,0:0:0:0: +251,210,243534,2,0,L|292:202,1,42.5,8|0,3:2|0:0,0:0:0:0: +276,120,243671,2,0,L|317:112,3,42.5,2|0|8|0,3:2|0:0|3:2|0:0,0:0:0:0: +388,48,243943,6,0,L|379:-4,2,42.5,2|0|8,3:2|3:3|3:2,0:0:0:0: +409,139,244216,1,2,3:2:0:0: +407,147,244284,1,0,3:3:0:0: +405,155,244352,1,8,3:2:0:0: +495,191,244489,2,0,L|504:139,2,42.5,2|0|8,3:2|3:3|3:2,0:0:0:0: +426,254,244762,1,2,3:2:0:0: +428,262,244830,1,0,3:3:0:0: +430,270,244898,1,8,3:2:0:0: +370,354,245034,6,0,L|318:363,3,42.5,2|0|8|0,3:2|3:3|3:2|0:0,0:0:0:0: +331,257,245307,2,0,L|279:248,3,42.5,2|0|8|0,3:2|0:0|3:2|0:0,0:0:0:0: +229,187,245579,6,0,L|236:145,1,42.5,2|0,3:2|3:3,0:0:0:0: +140,210,245716,2,0,L|147:168,1,42.5,8|0,3:2|0:0,0:0:0:0: +50,235,245853,2,0,L|57:193,3,42.5,2|0|8|0,3:2|0:0|3:2|0:0,0:0:0:0: +120,299,246124,5,2,3:2:0:0: +122,306,246193,1,0,3:3:0:0: +124,315,246261,1,8,3:2:0:0: +171,218,246397,1,2,3:2:0:0: +173,211,246465,1,0,3:3:0:0: +175,202,246533,1,8,3:2:0:0: +123,119,246670,1,2,3:2:0:0: +125,111,246738,1,0,3:3:0:0: +127,103,246806,2,0,L|116:-1,1,85,8|2,3:2|3:2,0:0:0:0: +289,8,247215,6,0,L|341:17,3,42.5,6|0|8|0,3:2|3:3|3:2|0:0,0:0:0:0: +306,118,247488,2,0,L|358:109,3,42.5,2|0|8|0,3:2|0:0|3:2|0:0,0:0:0:0: +440,82,247761,6,0,L|449:134,1,42.5,2|0,3:2|3:3,0:0:0:0: +425,168,247897,2,0,L|434:220,1,42.5,8|0,3:2|0:0,0:0:0:0: +410,254,248033,2,0,L|419:306,3,42.5,2|0|8|0,3:2|0:0|3:2|0:0,0:0:0:0: +346,361,248306,6,0,L|294:352,2,42.5,2|0|8,3:2|3:3|3:2,0:0:0:0: +287,258,248579,1,2,3:2:0:0: +279,260,248647,1,0,3:3:0:0: +271,262,248715,1,8,3:2:0:0: +193,320,248852,2,0,L|141:329,2,42.5,2|0|8,3:2|3:3|3:2,0:0:0:0: +139,231,249124,1,2,3:2:0:0: +131,229,249194,1,0,3:3:0:0: +123,227,249261,1,8,3:2:0:0: +53,294,249397,6,0,L|62:346,3,42.5,2|0|8|0,3:2|3:3|3:2|0:0,0:0:0:0: +0,214,249670,2,0,L|8:172,3,42.5,2|0|8|0,3:2|0:0|3:2|0:0,0:0:0:0: +41,78,249943,6,0,L|-11:87,1,42.5,2|0,3:2|3:3,0:0:0:0: +127,44,250079,2,0,L|75:53,1,42.5,8|0,3:2|0:0,0:0:0:0: +212,12,250215,2,0,L|160:21,3,42.5,2|0|8|0,3:2|0:0|3:2|0:0,0:0:0:0: +210,113,250488,5,2,3:2:0:0: +212,120,250556,1,0,3:3:0:0: +214,129,250624,1,8,3:2:0:0: +295,186,250761,1,2,3:2:0:0: +293,193,250829,1,0,3:3:0:0: +291,202,250898,1,8,3:2:0:0: +235,284,251033,1,2,3:2:0:0: +237,292,251102,1,0,3:3:0:0: +239,300,251170,2,0,L|229:359,5,42.5,8|0|2|0|8|0,3:2|3:3|3:2|3:3|3:2|3:3,0:0:0:0: +229,205,251579,6,0,P|289:218|332:276,1,101.999996887207,6|0,3:2|1:1,0:0:0:0: +475,279,251852,2,0,P|436:312|386:319,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +440,188,252124,2,0,L|465:84,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +297,1,252397,2,0,L|320:101,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +205,178,252670,6,0,L|105:155,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +42,63,252942,1,2,3:2:0:0: +42,63,253079,1,2,1:2:0:0: +1,237,253215,2,0,P|81:257|129:233,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +248,325,253488,2,0,L|148:348,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +408,308,253760,6,0,P|468:334|493:381,1,101.999996887207,6|2,3:2|3:2,0:0:0:0: +318,250,254033,2,0,P|300:202|310:153,1,101.999996887207,2|2,3:2|3:2,0:0:0:0: +202,8,254306,1,2,3:2:0:0: +295,60,254442,1,2,3:2:0:0: +295,60,254510,1,2,3:2:0:0: +295,60,254579,2,0,L|430:40,1,101.999996887207,2|2,3:2|3:2,0:0:0:0: +486,147,254851,5,2,3:2:0:0: +423,210,254987,1,2,3:2:0:0: +424,300,255124,1,2,3:2:0:0: +487,363,255260,1,2,3:2:0:0: +412,309,255397,2,0,B|317:325|317:325|302:339|302:339|180:354,1,203.999993774414,2|8,3:2|2:3,0:0:0:0: +80,349,255806,5,4,2:3:0:0: +87,359,255874,1,4,2:3:0:0: +94,369,255942,2,0,L|120:251,1,101.999996887207,6|0,3:2|1:1,0:0:0:0: +14,99,256215,2,0,L|40:217,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +172,177,256488,2,0,P|222:174|263:145,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +238,37,256760,2,0,P|188:39|147:68,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +115,269,257033,6,0,P|164:276|205:307,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +342,384,257306,1,2,3:2:0:0: +342,384,257442,1,2,1:2:0:0: +455,305,257579,2,0,L|469:193,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +381,25,257851,2,0,L|395:137,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +267,206,258124,6,0,P|210:189|175:137,1,101.999996887207,6|2,3:2|3:2,0:0:0:0: +95,26,258397,2,0,P|38:43|3:95,1,101.999996887207,2|2,3:2|3:2,0:0:0:0: +101,216,258670,1,2,3:2:0:0: +22,284,258806,1,2,3:2:0:0: +22,284,258874,1,2,3:2:0:0: +22,284,258942,2,0,L|3:401,1,101.999996887207,2|2,3:2|3:2,0:0:0:0: +158,357,259215,5,8,2:3:0:0: +197,374,259283,1,8,2:3:0:0: +239,370,259351,1,8,2:3:0:0: +273,346,259419,1,8,2:3:0:0: +291,309,259487,1,4,2:3:0:0: +405,309,259624,1,4,2:3:0:0: +415,315,259692,1,4,2:3:0:0: +425,321,259761,2,0,L|443:386,2,42.5,8|8|8,2:3|2:3|2:3,0:0:0:0: +355,215,260033,2,0,L|373:150,3,42.5,4|4|4|4,2:3|2:3|2:3|2:3,0:0:0:0: +376,74,260306,6,0,P|316:87|273:145,1,101.999996887207,6|0,3:2|1:1,0:0:0:0: +112,21,260578,2,0,P|151:54|201:61,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +240,204,260851,2,0,L|136:229,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +1,306,261124,2,0,L|101:329,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +296,380,261397,6,0,L|196:357,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +324,269,261669,1,2,3:2:0:0: +324,269,261806,1,2,1:2:0:0: +445,346,261942,2,0,P|465:266|441:218,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +360,112,262215,2,0,P|410:107|456:128,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +274,175,262487,6,0,P|213:148|188:101,1,101.999996887207,6|2,3:2|3:2,0:0:0:0: +38,82,262760,2,0,P|91:43|144:45,1,101.999996887207,2|2,3:2|3:2,0:0:0:0: +194,119,263033,1,2,3:2:0:0: +312,17,263169,1,2,3:2:0:0: +312,17,263237,1,2,3:2:0:0: +312,17,263306,2,0,L|447:37,1,101.999996887207,2|2,3:2|3:2,0:0:0:0: +503,159,263578,5,2,3:2:0:0: +456,234,263714,1,2,3:2:0:0: +367,254,263851,1,2,3:2:0:0: +292,207,263987,1,2,3:2:0:0: +206,230,264124,2,0,B|88:237|134:298|-8:302,1,203.999993774414,2|8,3:2|2:3,0:0:0:0: +173,364,264533,5,4,2:3:0:0: +166,375,264601,1,4,2:3:0:0: +159,384,264669,2,0,L|133:266,1,101.999996887207,6|0,3:2|1:1,0:0:0:0: +302,214,264942,2,0,L|281:313,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +399,384,265215,2,0,P|430:344|432:285,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +496,158,265487,2,0,P|455:187|404:189,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +362,12,265760,6,0,P|411:19|452:50,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +288,107,266033,1,2,3:2:0:0: +288,107,266169,1,2,1:2:0:0: +171,18,266306,2,0,L|157:130,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +251,304,266578,2,0,L|237:192,1,101.999996887207,2|0,3:2|1:1,0:0:0:0: +56,123,266851,6,0,P|68:171|104:206,1,101.999996887207,6|2,3:2|3:2,0:0:0:0: +35,378,267124,2,0,P|21:320|48:263,1,101.999996887207,2|2,3:2|3:2,0:0:0:0: +123,331,267397,1,2,3:2:0:0: +253,263,267533,1,2,3:2:0:0: +253,263,267601,1,2,3:2:0:0: +253,263,267669,2,0,L|370:282,1,101.999996887207,2|2,3:2|3:2,0:0:0:0: +463,369,267942,5,8,2:3:0:0: +489,336,268010,1,8,2:3:0:0: +498,295,268078,1,8,2:3:0:0: +485,256,268146,1,8,2:3:0:0: +455,228,268214,1,4,2:3:0:0: +419,94,268352,1,4,2:3:0:0: +403,133,268420,1,4,2:3:0:0: +372,161,268488,1,8,2:3:0:0: +332,169,268556,1,8,2:3:0:0: +292,157,268624,1,8,2:3:0:0: +231,79,268761,2,0,L|176:72,3,50.9999984436036,4|4|4|4,2:3|2:3|2:3|2:3,0:0:0:0: +96,25,269033,6,0,P|145:65|95:296,1,297.5,6|0,3:2|0:0,0:0:0:0: +121,370,270097,2,0,P|70:261|250:314,1,382.500014591218,6|0,3:2|3:3,0:0:0:0: +319,356,271028,1,0,3:3:0:0: +312,347,271161,6,0,P|281:282|332:105,1,255.000009727478,6|0,1:2|3:3,0:0:0:0: +400,56,271959,1,0,3:3:0:0: +400,56,272225,2,0,L|411:-18,2,56.6666666666667,0|0|0,1:1|1:1|1:1,0:0:0:0: +442,224,272758,2,0,L|453:150,2,56.6666666666667,0|0|0,1:1|1:1|1:1,0:0:0:0: +512,288,273290,6,0,P|443:291|403:383,1,170,6|2,3:2|3:2,0:0:0:0: +303,339,274048,2,0,L|294:254,1,56.6666666666667,0|0,3:3|3:3,0:0:0:0: +202,300,274498,6,0,L|184:260,2,28.3333333333333,0|0|0,1:1|1:1|1:1,0:0:0:0: +105,278,274873,2,0,L|109:235,2,28.3333333333333,8|8|8,2:3|2:3|2:3,0:0:0:0: +31,211,275273,2,0,L|56:176,2,28.3333333333333,4|4|4,2:3|2:3|2:3,0:0:0:0: +0,115,275734,2,0,L|39:97,2,28.3333333333333,4|0|0,2:3|3:3|3:3,0:0:0:0: +21,17,276254,5,6,3:2:0:0: +256,192,276419,12,4,286062,2:3:0:0: +80,113,286725,6,0,B|137:185|228:143|230:143|231:143|330:119|372:183|321:239|260:239|196:214|196:214|299:265|347:186|469:261,1,680,6|4,3:2|3:2,0:0:0:0: diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3689906-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3689906-expected-conversion.json new file mode 100644 index 0000000000..31743d99ac --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3689906-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":390.0,"Objects":[{"StartTime":390.0,"Position":124.0,"HyperDash":false},{"StartTime":480.0,"Position":109.0,"HyperDash":false},{"StartTime":571.0,"Position":124.0,"HyperDash":false},{"StartTime":644.0,"Position":121.0,"HyperDash":false},{"StartTime":753.0,"Position":124.0,"HyperDash":false}]},{"StartTime":935.0,"Objects":[{"StartTime":935.0,"Position":208.0,"HyperDash":false}]},{"StartTime":1117.0,"Objects":[{"StartTime":1117.0,"Position":380.0,"HyperDash":false},{"StartTime":1207.0,"Position":395.0,"HyperDash":false},{"StartTime":1298.0,"Position":380.0,"HyperDash":false},{"StartTime":1371.0,"Position":381.0,"HyperDash":false},{"StartTime":1480.0,"Position":380.0,"HyperDash":false}]},{"StartTime":1844.0,"Objects":[{"StartTime":1844.0,"Position":208.0,"HyperDash":false}]},{"StartTime":2208.0,"Objects":[{"StartTime":2208.0,"Position":360.0,"HyperDash":false}]},{"StartTime":2390.0,"Objects":[{"StartTime":2390.0,"Position":188.0,"HyperDash":false}]},{"StartTime":2480.0,"Objects":[{"StartTime":2480.0,"Position":152.0,"HyperDash":false}]},{"StartTime":2571.0,"Objects":[{"StartTime":2571.0,"Position":112.0,"HyperDash":false},{"StartTime":2643.0,"Position":111.0,"HyperDash":false},{"StartTime":2752.0,"Position":112.0,"HyperDash":false}]},{"StartTime":2935.0,"Objects":[{"StartTime":2935.0,"Position":196.0,"HyperDash":false}]},{"StartTime":3117.0,"Objects":[{"StartTime":3117.0,"Position":280.0,"HyperDash":false}]},{"StartTime":3299.0,"Objects":[{"StartTime":3299.0,"Position":196.0,"HyperDash":false}]},{"StartTime":3480.0,"Objects":[{"StartTime":3480.0,"Position":288.0,"HyperDash":false},{"StartTime":3570.0,"Position":273.0,"HyperDash":false},{"StartTime":3661.0,"Position":288.0,"HyperDash":false},{"StartTime":3734.0,"Position":276.0,"HyperDash":false},{"StartTime":3843.0,"Position":288.0,"HyperDash":false}]},{"StartTime":4026.0,"Objects":[{"StartTime":4026.0,"Position":116.0,"HyperDash":false}]},{"StartTime":4390.0,"Objects":[{"StartTime":4390.0,"Position":300.0,"HyperDash":false}]},{"StartTime":4753.0,"Objects":[{"StartTime":4753.0,"Position":28.0,"HyperDash":false},{"StartTime":4825.0,"Position":24.0,"HyperDash":false},{"StartTime":4934.0,"Position":28.0,"HyperDash":false}]},{"StartTime":5117.0,"Objects":[{"StartTime":5117.0,"Position":112.0,"HyperDash":false}]},{"StartTime":5299.0,"Objects":[{"StartTime":5299.0,"Position":20.0,"HyperDash":false}]},{"StartTime":5480.0,"Objects":[{"StartTime":5480.0,"Position":192.0,"HyperDash":false},{"StartTime":5570.0,"Position":248.148758,"HyperDash":false},{"StartTime":5661.0,"Position":277.0,"HyperDash":false},{"StartTime":5734.0,"Position":247.046844,"HyperDash":false},{"StartTime":5843.0,"Position":192.0,"HyperDash":false}]},{"StartTime":6208.0,"Objects":[{"StartTime":6208.0,"Position":484.0,"HyperDash":false},{"StartTime":6298.0,"Position":475.0,"HyperDash":false},{"StartTime":6389.0,"Position":484.0,"HyperDash":false},{"StartTime":6462.0,"Position":465.0,"HyperDash":false},{"StartTime":6571.0,"Position":484.0,"HyperDash":false}]},{"StartTime":6753.0,"Objects":[{"StartTime":6753.0,"Position":400.0,"HyperDash":false}]},{"StartTime":6935.0,"Objects":[{"StartTime":6935.0,"Position":228.0,"HyperDash":false},{"StartTime":7025.0,"Position":219.0,"HyperDash":false},{"StartTime":7116.0,"Position":228.0,"HyperDash":false},{"StartTime":7189.0,"Position":245.0,"HyperDash":false},{"StartTime":7298.0,"Position":228.0,"HyperDash":false}]},{"StartTime":7662.0,"Objects":[{"StartTime":7662.0,"Position":396.0,"HyperDash":false}]},{"StartTime":8026.0,"Objects":[{"StartTime":8026.0,"Position":244.0,"HyperDash":false}]},{"StartTime":8208.0,"Objects":[{"StartTime":8208.0,"Position":416.0,"HyperDash":false}]},{"StartTime":8298.0,"Objects":[{"StartTime":8298.0,"Position":452.0,"HyperDash":false}]},{"StartTime":8389.0,"Objects":[{"StartTime":8389.0,"Position":492.0,"HyperDash":false},{"StartTime":8461.0,"Position":505.0,"HyperDash":false},{"StartTime":8570.0,"Position":492.0,"HyperDash":false}]},{"StartTime":8753.0,"Objects":[{"StartTime":8753.0,"Position":396.0,"HyperDash":false}]},{"StartTime":8935.0,"Objects":[{"StartTime":8935.0,"Position":304.0,"HyperDash":false}]},{"StartTime":9117.0,"Objects":[{"StartTime":9117.0,"Position":212.0,"HyperDash":false}]},{"StartTime":9298.0,"Objects":[{"StartTime":9298.0,"Position":312.0,"HyperDash":false},{"StartTime":9388.0,"Position":304.0,"HyperDash":false},{"StartTime":9479.0,"Position":312.0,"HyperDash":false},{"StartTime":9552.0,"Position":325.0,"HyperDash":false},{"StartTime":9661.0,"Position":312.0,"HyperDash":false}]},{"StartTime":9844.0,"Objects":[{"StartTime":9844.0,"Position":140.0,"HyperDash":false}]},{"StartTime":10208.0,"Objects":[{"StartTime":10208.0,"Position":324.0,"HyperDash":false}]},{"StartTime":10571.0,"Objects":[{"StartTime":10571.0,"Position":136.0,"HyperDash":false},{"StartTime":10643.0,"Position":164.812149,"HyperDash":false},{"StartTime":10752.0,"Position":221.0,"HyperDash":false}]},{"StartTime":10935.0,"Objects":[{"StartTime":10935.0,"Position":128.0,"HyperDash":false},{"StartTime":11007.0,"Position":165.812149,"HyperDash":false},{"StartTime":11116.0,"Position":213.0,"HyperDash":false}]},{"StartTime":11299.0,"Objects":[{"StartTime":11299.0,"Position":384.0,"HyperDash":false}]},{"StartTime":11480.0,"Objects":[{"StartTime":11480.0,"Position":292.0,"HyperDash":false}]},{"StartTime":11662.0,"Objects":[{"StartTime":11662.0,"Position":200.0,"HyperDash":false}]},{"StartTime":12026.0,"Objects":[{"StartTime":12026.0,"Position":488.0,"HyperDash":false},{"StartTime":12116.0,"Position":473.0,"HyperDash":false},{"StartTime":12207.0,"Position":487.234161,"HyperDash":false},{"StartTime":12280.0,"Position":452.046844,"HyperDash":false},{"StartTime":12389.0,"Position":402.0,"HyperDash":false}]},{"StartTime":12571.0,"Objects":[{"StartTime":12571.0,"Position":316.0,"HyperDash":false}]},{"StartTime":12753.0,"Objects":[{"StartTime":12753.0,"Position":144.0,"HyperDash":false},{"StartTime":12843.0,"Position":158.0,"HyperDash":false},{"StartTime":12934.0,"Position":144.0,"HyperDash":false},{"StartTime":13007.0,"Position":125.0,"HyperDash":false},{"StartTime":13116.0,"Position":144.0,"HyperDash":false}]},{"StartTime":13480.0,"Objects":[{"StartTime":13480.0,"Position":314.0,"HyperDash":false},{"StartTime":13570.0,"Position":255.851257,"HyperDash":false},{"StartTime":13661.0,"Position":229.234161,"HyperDash":false},{"StartTime":13734.0,"Position":212.046844,"HyperDash":false},{"StartTime":13843.0,"Position":144.0,"HyperDash":false}]},{"StartTime":14026.0,"Objects":[{"StartTime":14026.0,"Position":144.0,"HyperDash":false}]},{"StartTime":14208.0,"Objects":[{"StartTime":14208.0,"Position":314.0,"HyperDash":false},{"StartTime":14280.0,"Position":346.812164,"HyperDash":false},{"StartTime":14389.0,"Position":399.0,"HyperDash":false}]},{"StartTime":14571.0,"Objects":[{"StartTime":14571.0,"Position":304.0,"HyperDash":false},{"StartTime":14643.0,"Position":297.0,"HyperDash":false},{"StartTime":14752.0,"Position":304.0,"HyperDash":false}]},{"StartTime":14935.0,"Objects":[{"StartTime":14935.0,"Position":132.0,"HyperDash":false},{"StartTime":15025.0,"Position":88.85124,"HyperDash":false},{"StartTime":15116.0,"Position":48.0,"HyperDash":false},{"StartTime":15189.0,"Position":42.0,"HyperDash":false},{"StartTime":15298.0,"Position":48.0,"HyperDash":false}]},{"StartTime":15480.0,"Objects":[{"StartTime":15480.0,"Position":132.0,"HyperDash":false}]},{"StartTime":15662.0,"Objects":[{"StartTime":15662.0,"Position":304.0,"HyperDash":false}]},{"StartTime":16026.0,"Objects":[{"StartTime":16026.0,"Position":132.0,"HyperDash":false}]},{"StartTime":16390.0,"Objects":[{"StartTime":16390.0,"Position":284.0,"HyperDash":false},{"StartTime":16462.0,"Position":289.0,"HyperDash":false},{"StartTime":16571.0,"Position":284.0,"HyperDash":false}]},{"StartTime":16753.0,"Objects":[{"StartTime":16753.0,"Position":192.0,"HyperDash":false}]},{"StartTime":16935.0,"Objects":[{"StartTime":16935.0,"Position":192.0,"HyperDash":false}]},{"StartTime":17117.0,"Objects":[{"StartTime":17117.0,"Position":364.0,"HyperDash":false},{"StartTime":17207.0,"Position":419.148743,"HyperDash":false},{"StartTime":17298.0,"Position":449.0,"HyperDash":false},{"StartTime":17371.0,"Position":432.046844,"HyperDash":false},{"StartTime":17480.0,"Position":364.0,"HyperDash":false}]},{"StartTime":17844.0,"Objects":[{"StartTime":17844.0,"Position":64.0,"HyperDash":false},{"StartTime":17916.0,"Position":81.0,"HyperDash":false},{"StartTime":18025.0,"Position":64.0,"HyperDash":false}]},{"StartTime":18208.0,"Objects":[{"StartTime":18208.0,"Position":148.0,"HyperDash":false},{"StartTime":18280.0,"Position":163.0,"HyperDash":false},{"StartTime":18389.0,"Position":148.0,"HyperDash":false}]},{"StartTime":18571.0,"Objects":[{"StartTime":18571.0,"Position":320.0,"HyperDash":false}]},{"StartTime":18935.0,"Objects":[{"StartTime":18935.0,"Position":132.0,"HyperDash":false}]},{"StartTime":19299.0,"Objects":[{"StartTime":19299.0,"Position":132.0,"HyperDash":false},{"StartTime":19389.0,"Position":191.148758,"HyperDash":false},{"StartTime":19480.0,"Position":216.765839,"HyperDash":false},{"StartTime":19553.0,"Position":233.953156,"HyperDash":false},{"StartTime":19662.0,"Position":302.0,"HyperDash":false}]},{"StartTime":19844.0,"Objects":[{"StartTime":19844.0,"Position":388.0,"HyperDash":false}]},{"StartTime":20026.0,"Objects":[{"StartTime":20026.0,"Position":216.0,"HyperDash":false},{"StartTime":20098.0,"Position":187.187851,"HyperDash":false},{"StartTime":20207.0,"Position":131.0,"HyperDash":false}]},{"StartTime":20390.0,"Objects":[{"StartTime":20390.0,"Position":224.0,"HyperDash":false},{"StartTime":20462.0,"Position":212.0,"HyperDash":false},{"StartTime":20571.0,"Position":224.0,"HyperDash":false}]},{"StartTime":20753.0,"Objects":[{"StartTime":20753.0,"Position":52.0,"HyperDash":false},{"StartTime":20843.0,"Position":37.0,"HyperDash":false},{"StartTime":20934.0,"Position":52.0,"HyperDash":false},{"StartTime":21007.0,"Position":74.95316,"HyperDash":false},{"StartTime":21116.0,"Position":134.0,"HyperDash":false}]},{"StartTime":21299.0,"Objects":[{"StartTime":21299.0,"Position":224.0,"HyperDash":false}]},{"StartTime":21480.0,"Objects":[{"StartTime":21480.0,"Position":396.0,"HyperDash":false}]},{"StartTime":21844.0,"Objects":[{"StartTime":21844.0,"Position":224.0,"HyperDash":false}]},{"StartTime":22026.0,"Objects":[{"StartTime":22026.0,"Position":132.0,"HyperDash":false}]},{"StartTime":22208.0,"Objects":[{"StartTime":22208.0,"Position":224.0,"HyperDash":false}]},{"StartTime":22299.0,"Objects":[{"StartTime":22299.0,"Position":176.0,"HyperDash":false}]},{"StartTime":22390.0,"Objects":[{"StartTime":22390.0,"Position":132.0,"HyperDash":false}]},{"StartTime":22571.0,"Objects":[{"StartTime":22571.0,"Position":232.0,"HyperDash":false}]},{"StartTime":22753.0,"Objects":[{"StartTime":22753.0,"Position":404.0,"HyperDash":false}]},{"StartTime":22935.0,"Objects":[{"StartTime":22935.0,"Position":232.0,"HyperDash":false},{"StartTime":23007.0,"Position":248.0,"HyperDash":false},{"StartTime":23116.0,"Position":232.0,"HyperDash":false}]},{"StartTime":23299.0,"Objects":[{"StartTime":23299.0,"Position":404.0,"HyperDash":false}]},{"StartTime":23389.0,"Objects":[{"StartTime":23389.0,"Position":448.0,"HyperDash":false}]},{"StartTime":23480.0,"Objects":[{"StartTime":23480.0,"Position":492.0,"HyperDash":true}]},{"StartTime":23662.0,"Objects":[{"StartTime":23662.0,"Position":212.0,"HyperDash":false},{"StartTime":23752.0,"Position":164.4215,"HyperDash":false},{"StartTime":23843.0,"Position":110.280991,"HyperDash":false},{"StartTime":23916.0,"Position":70.25621,"HyperDash":false},{"StartTime":24025.0,"Position":8.0,"HyperDash":false}]},{"StartTime":24208.0,"Objects":[{"StartTime":24208.0,"Position":92.0,"HyperDash":false}]},{"StartTime":24390.0,"Objects":[{"StartTime":24390.0,"Position":272.0,"HyperDash":false},{"StartTime":24462.0,"Position":262.0,"HyperDash":false},{"StartTime":24571.0,"Position":272.0,"HyperDash":false}]},{"StartTime":24753.0,"Objects":[{"StartTime":24753.0,"Position":180.0,"HyperDash":false}]},{"StartTime":25117.0,"Objects":[{"StartTime":25117.0,"Position":348.0,"HyperDash":false},{"StartTime":25189.0,"Position":314.187836,"HyperDash":false},{"StartTime":25298.0,"Position":263.0,"HyperDash":false}]},{"StartTime":25480.0,"Objects":[{"StartTime":25480.0,"Position":355.0,"HyperDash":false}]},{"StartTime":25662.0,"Objects":[{"StartTime":25662.0,"Position":179.0,"HyperDash":false}]},{"StartTime":25752.0,"Objects":[{"StartTime":25752.0,"Position":135.0,"HyperDash":false}]},{"StartTime":25843.0,"Objects":[{"StartTime":25843.0,"Position":91.0,"HyperDash":false},{"StartTime":25933.0,"Position":30.8512421,"HyperDash":false},{"StartTime":26024.0,"Position":6.0,"HyperDash":false},{"StartTime":26097.0,"Position":23.9531631,"HyperDash":false},{"StartTime":26206.0,"Position":91.0,"HyperDash":false}]},{"StartTime":26571.0,"Objects":[{"StartTime":26571.0,"Position":383.0,"HyperDash":false}]},{"StartTime":26753.0,"Objects":[{"StartTime":26753.0,"Position":299.0,"HyperDash":false},{"StartTime":26843.0,"Position":264.851257,"HyperDash":false},{"StartTime":26934.0,"Position":215.0,"HyperDash":false},{"StartTime":27007.0,"Position":195.0,"HyperDash":false},{"StartTime":27116.0,"Position":215.0,"HyperDash":false}]},{"StartTime":27299.0,"Objects":[{"StartTime":27299.0,"Position":391.0,"HyperDash":false}]},{"StartTime":27662.0,"Objects":[{"StartTime":27662.0,"Position":239.0,"HyperDash":false},{"StartTime":27734.0,"Position":234.0,"HyperDash":false},{"StartTime":27843.0,"Position":239.0,"HyperDash":false}]},{"StartTime":28026.0,"Objects":[{"StartTime":28026.0,"Position":323.0,"HyperDash":false}]},{"StartTime":28208.0,"Objects":[{"StartTime":28208.0,"Position":231.0,"HyperDash":false}]},{"StartTime":28390.0,"Objects":[{"StartTime":28390.0,"Position":315.0,"HyperDash":false}]},{"StartTime":28571.0,"Objects":[{"StartTime":28571.0,"Position":143.0,"HyperDash":false}]},{"StartTime":28753.0,"Objects":[{"StartTime":28753.0,"Position":315.0,"HyperDash":false}]},{"StartTime":28935.0,"Objects":[{"StartTime":28935.0,"Position":407.0,"HyperDash":false},{"StartTime":29025.0,"Position":446.57605,"HyperDash":false},{"StartTime":29116.0,"Position":508.0,"HyperDash":false},{"StartTime":29189.0,"Position":506.0,"HyperDash":false},{"StartTime":29298.0,"Position":508.0,"HyperDash":true}]},{"StartTime":29480.0,"Objects":[{"StartTime":29480.0,"Position":212.0,"HyperDash":false},{"StartTime":29570.0,"Position":178.4679,"HyperDash":false},{"StartTime":29661.0,"Position":110.374321,"HyperDash":false},{"StartTime":29752.0,"Position":113.0,"HyperDash":false},{"StartTime":29843.0,"Position":108.0,"HyperDash":false},{"StartTime":29916.0,"Position":158.8,"HyperDash":false},{"StartTime":30025.0,"Position":210.0,"HyperDash":false}]},{"StartTime":30208.0,"Objects":[{"StartTime":30208.0,"Position":304.0,"HyperDash":false},{"StartTime":30298.0,"Position":356.148743,"HyperDash":false},{"StartTime":30389.0,"Position":389.0,"HyperDash":false},{"StartTime":30462.0,"Position":359.046844,"HyperDash":false},{"StartTime":30571.0,"Position":304.0,"HyperDash":false}]},{"StartTime":30935.0,"Objects":[{"StartTime":30935.0,"Position":152.0,"HyperDash":false},{"StartTime":31007.0,"Position":159.0,"HyperDash":false},{"StartTime":31116.0,"Position":152.0,"HyperDash":false}]},{"StartTime":31299.0,"Objects":[{"StartTime":31299.0,"Position":236.0,"HyperDash":false},{"StartTime":31371.0,"Position":252.0,"HyperDash":false},{"StartTime":31480.0,"Position":236.0,"HyperDash":false}]},{"StartTime":31662.0,"Objects":[{"StartTime":31662.0,"Position":320.0,"HyperDash":false},{"StartTime":31752.0,"Position":262.851257,"HyperDash":false},{"StartTime":31843.0,"Position":235.0,"HyperDash":false},{"StartTime":31916.0,"Position":265.953156,"HyperDash":false},{"StartTime":32025.0,"Position":320.0,"HyperDash":false}]},{"StartTime":32390.0,"Objects":[{"StartTime":32390.0,"Position":136.0,"HyperDash":false},{"StartTime":32458.0,"Position":135.0,"HyperDash":false},{"StartTime":32526.0,"Position":346.0,"HyperDash":false},{"StartTime":32594.0,"Position":39.0,"HyperDash":false},{"StartTime":32662.0,"Position":300.0,"HyperDash":false},{"StartTime":32730.0,"Position":398.0,"HyperDash":false},{"StartTime":32798.0,"Position":151.0,"HyperDash":false},{"StartTime":32866.0,"Position":73.0,"HyperDash":false},{"StartTime":32935.0,"Position":311.0,"HyperDash":false},{"StartTime":33003.0,"Position":90.0,"HyperDash":false},{"StartTime":33071.0,"Position":264.0,"HyperDash":false},{"StartTime":33139.0,"Position":477.0,"HyperDash":false},{"StartTime":33207.0,"Position":473.0,"HyperDash":false},{"StartTime":33275.0,"Position":120.0,"HyperDash":false},{"StartTime":33343.0,"Position":115.0,"HyperDash":false},{"StartTime":33411.0,"Position":163.0,"HyperDash":false},{"StartTime":33480.0,"Position":447.0,"HyperDash":false}]},{"StartTime":33844.0,"Objects":[{"StartTime":33844.0,"Position":428.0,"HyperDash":false},{"StartTime":33934.0,"Position":428.0,"HyperDash":false},{"StartTime":34025.0,"Position":428.0,"HyperDash":false}]},{"StartTime":34208.0,"Objects":[{"StartTime":34208.0,"Position":256.0,"HyperDash":false},{"StartTime":34280.0,"Position":207.187851,"HyperDash":false},{"StartTime":34389.0,"Position":171.0,"HyperDash":false}]},{"StartTime":34480.0,"Objects":[{"StartTime":34480.0,"Position":216.0,"HyperDash":false}]},{"StartTime":34571.0,"Objects":[{"StartTime":34571.0,"Position":264.0,"HyperDash":false},{"StartTime":34661.0,"Position":306.5,"HyperDash":false},{"StartTime":34752.0,"Position":264.0,"HyperDash":false}]},{"StartTime":34935.0,"Objects":[{"StartTime":34935.0,"Position":92.0,"HyperDash":false},{"StartTime":35007.0,"Position":54.1878471,"HyperDash":false},{"StartTime":35116.0,"Position":7.0,"HyperDash":true}]},{"StartTime":35299.0,"Objects":[{"StartTime":35299.0,"Position":288.0,"HyperDash":false},{"StartTime":35389.0,"Position":341.578522,"HyperDash":false},{"StartTime":35480.0,"Position":389.719,"HyperDash":false},{"StartTime":35553.0,"Position":430.743774,"HyperDash":false},{"StartTime":35662.0,"Position":492.0,"HyperDash":false}]},{"StartTime":35844.0,"Objects":[{"StartTime":35844.0,"Position":400.0,"HyperDash":false}]},{"StartTime":36026.0,"Objects":[{"StartTime":36026.0,"Position":224.0,"HyperDash":false},{"StartTime":36098.0,"Position":203.187851,"HyperDash":false},{"StartTime":36207.0,"Position":139.0,"HyperDash":false}]},{"StartTime":36390.0,"Objects":[{"StartTime":36390.0,"Position":232.0,"HyperDash":false},{"StartTime":36462.0,"Position":229.0,"HyperDash":false},{"StartTime":36571.0,"Position":232.0,"HyperDash":false}]},{"StartTime":36753.0,"Objects":[{"StartTime":36753.0,"Position":56.0,"HyperDash":false},{"StartTime":36825.0,"Position":72.0,"HyperDash":false},{"StartTime":36934.0,"Position":56.0,"HyperDash":false}]},{"StartTime":37026.0,"Objects":[{"StartTime":37026.0,"Position":104.0,"HyperDash":false}]},{"StartTime":37117.0,"Objects":[{"StartTime":37117.0,"Position":152.0,"HyperDash":false}]},{"StartTime":37299.0,"Objects":[{"StartTime":37299.0,"Position":244.0,"HyperDash":false}]},{"StartTime":37480.0,"Objects":[{"StartTime":37480.0,"Position":152.0,"HyperDash":false},{"StartTime":37552.0,"Position":109.187851,"HyperDash":false},{"StartTime":37661.0,"Position":67.0,"HyperDash":false}]},{"StartTime":37844.0,"Objects":[{"StartTime":37844.0,"Position":244.0,"HyperDash":false},{"StartTime":37916.0,"Position":233.0,"HyperDash":false},{"StartTime":38025.0,"Position":244.0,"HyperDash":false}]},{"StartTime":38208.0,"Objects":[{"StartTime":38208.0,"Position":496.0,"HyperDash":false},{"StartTime":38298.0,"Position":482.0,"HyperDash":false},{"StartTime":38389.0,"Position":495.234161,"HyperDash":false},{"StartTime":38462.0,"Position":471.046844,"HyperDash":false},{"StartTime":38571.0,"Position":410.0,"HyperDash":false}]},{"StartTime":38753.0,"Objects":[{"StartTime":38753.0,"Position":504.0,"HyperDash":false},{"StartTime":38843.0,"Position":480.851257,"HyperDash":false},{"StartTime":38934.0,"Position":419.234161,"HyperDash":false},{"StartTime":39007.0,"Position":379.046844,"HyperDash":false},{"StartTime":39116.0,"Position":334.0,"HyperDash":false}]},{"StartTime":39299.0,"Objects":[{"StartTime":39299.0,"Position":156.0,"HyperDash":false},{"StartTime":39371.0,"Position":128.187851,"HyperDash":false},{"StartTime":39480.0,"Position":71.0,"HyperDash":false}]},{"StartTime":39662.0,"Objects":[{"StartTime":39662.0,"Position":252.0,"HyperDash":false},{"StartTime":39752.0,"Position":294.5,"HyperDash":false},{"StartTime":39843.0,"Position":252.0,"HyperDash":false}]},{"StartTime":40026.0,"Objects":[{"StartTime":40026.0,"Position":71.0,"HyperDash":false},{"StartTime":40098.0,"Position":83.0,"HyperDash":false},{"StartTime":40207.0,"Position":71.0,"HyperDash":false}]},{"StartTime":40390.0,"Objects":[{"StartTime":40390.0,"Position":164.0,"HyperDash":false},{"StartTime":40462.0,"Position":117.187851,"HyperDash":false},{"StartTime":40571.0,"Position":79.0,"HyperDash":false}]},{"StartTime":40753.0,"Objects":[{"StartTime":40753.0,"Position":256.0,"HyperDash":false},{"StartTime":40825.0,"Position":275.812164,"HyperDash":false},{"StartTime":40934.0,"Position":341.0,"HyperDash":false}]},{"StartTime":41117.0,"Objects":[{"StartTime":41117.0,"Position":84.0,"HyperDash":false},{"StartTime":41207.0,"Position":107.148758,"HyperDash":false},{"StartTime":41298.0,"Position":168.765839,"HyperDash":false},{"StartTime":41371.0,"Position":188.953156,"HyperDash":false},{"StartTime":41480.0,"Position":254.0,"HyperDash":false}]},{"StartTime":41662.0,"Objects":[{"StartTime":41662.0,"Position":432.0,"HyperDash":false},{"StartTime":41734.0,"Position":438.0,"HyperDash":false},{"StartTime":41843.0,"Position":432.0,"HyperDash":false}]},{"StartTime":42026.0,"Objects":[{"StartTime":42026.0,"Position":348.0,"HyperDash":false}]},{"StartTime":42208.0,"Objects":[{"StartTime":42208.0,"Position":432.0,"HyperDash":false},{"StartTime":42280.0,"Position":411.187836,"HyperDash":false},{"StartTime":42389.0,"Position":347.0,"HyperDash":false}]},{"StartTime":42571.0,"Objects":[{"StartTime":42571.0,"Position":176.0,"HyperDash":false},{"StartTime":42643.0,"Position":132.187851,"HyperDash":false},{"StartTime":42752.0,"Position":91.0,"HyperDash":false}]},{"StartTime":42844.0,"Objects":[{"StartTime":42844.0,"Position":132.0,"HyperDash":false}]},{"StartTime":42935.0,"Objects":[{"StartTime":42935.0,"Position":176.0,"HyperDash":false}]},{"StartTime":43117.0,"Objects":[{"StartTime":43117.0,"Position":260.0,"HyperDash":false},{"StartTime":43207.0,"Position":210.851242,"HyperDash":false},{"StartTime":43298.0,"Position":175.0,"HyperDash":false},{"StartTime":43371.0,"Position":218.953156,"HyperDash":false},{"StartTime":43480.0,"Position":260.0,"HyperDash":false}]},{"StartTime":43662.0,"Objects":[{"StartTime":43662.0,"Position":84.0,"HyperDash":false},{"StartTime":43734.0,"Position":93.0,"HyperDash":false},{"StartTime":43843.0,"Position":84.0,"HyperDash":false}]},{"StartTime":44026.0,"Objects":[{"StartTime":44026.0,"Position":336.0,"HyperDash":false},{"StartTime":44116.0,"Position":393.578522,"HyperDash":false},{"StartTime":44207.0,"Position":436.0,"HyperDash":false},{"StartTime":44280.0,"Position":442.0,"HyperDash":false},{"StartTime":44389.0,"Position":436.0,"HyperDash":false}]},{"StartTime":44571.0,"Objects":[{"StartTime":44571.0,"Position":344.0,"HyperDash":false}]},{"StartTime":44753.0,"Objects":[{"StartTime":44753.0,"Position":252.0,"HyperDash":false},{"StartTime":44825.0,"Position":246.0,"HyperDash":false},{"StartTime":44934.0,"Position":252.0,"HyperDash":false}]},{"StartTime":45117.0,"Objects":[{"StartTime":45117.0,"Position":428.0,"HyperDash":false},{"StartTime":45189.0,"Position":387.187836,"HyperDash":false},{"StartTime":45298.0,"Position":343.0,"HyperDash":false}]},{"StartTime":45480.0,"Objects":[{"StartTime":45480.0,"Position":164.0,"HyperDash":false}]},{"StartTime":45570.0,"Objects":[{"StartTime":45570.0,"Position":121.0,"HyperDash":false}]},{"StartTime":45661.0,"Objects":[{"StartTime":45661.0,"Position":79.0,"HyperDash":false}]},{"StartTime":45844.0,"Objects":[{"StartTime":45844.0,"Position":256.0,"HyperDash":false},{"StartTime":45916.0,"Position":275.0,"HyperDash":false},{"StartTime":46025.0,"Position":256.0,"HyperDash":false}]},{"StartTime":46208.0,"Objects":[{"StartTime":46208.0,"Position":160.0,"HyperDash":false},{"StartTime":46280.0,"Position":188.812149,"HyperDash":false},{"StartTime":46389.0,"Position":245.0,"HyperDash":false}]},{"StartTime":46571.0,"Objects":[{"StartTime":46571.0,"Position":68.0,"HyperDash":false},{"StartTime":46643.0,"Position":68.0,"HyperDash":false},{"StartTime":46752.0,"Position":68.0,"HyperDash":false}]},{"StartTime":46935.0,"Objects":[{"StartTime":46935.0,"Position":324.0,"HyperDash":false},{"StartTime":47025.0,"Position":381.148743,"HyperDash":false},{"StartTime":47116.0,"Position":409.0,"HyperDash":false},{"StartTime":47189.0,"Position":359.046844,"HyperDash":false},{"StartTime":47298.0,"Position":324.0,"HyperDash":false}]},{"StartTime":47480.0,"Objects":[{"StartTime":47480.0,"Position":154.0,"HyperDash":false},{"StartTime":47570.0,"Position":213.148758,"HyperDash":false},{"StartTime":47661.0,"Position":238.765839,"HyperDash":false},{"StartTime":47734.0,"Position":268.953156,"HyperDash":false},{"StartTime":47843.0,"Position":324.0,"HyperDash":false}]},{"StartTime":48026.0,"Objects":[{"StartTime":48026.0,"Position":420.0,"HyperDash":false},{"StartTime":48098.0,"Position":428.0,"HyperDash":false},{"StartTime":48207.0,"Position":420.0,"HyperDash":false}]},{"StartTime":48390.0,"Objects":[{"StartTime":48390.0,"Position":240.0,"HyperDash":false},{"StartTime":48462.0,"Position":205.187851,"HyperDash":false},{"StartTime":48571.0,"Position":155.0,"HyperDash":false}]},{"StartTime":48662.0,"Objects":[{"StartTime":48662.0,"Position":112.0,"HyperDash":false}]},{"StartTime":48753.0,"Objects":[{"StartTime":48753.0,"Position":68.0,"HyperDash":false}]},{"StartTime":48935.0,"Objects":[{"StartTime":48935.0,"Position":160.0,"HyperDash":false},{"StartTime":49025.0,"Position":132.851242,"HyperDash":false},{"StartTime":49116.0,"Position":75.0,"HyperDash":false},{"StartTime":49189.0,"Position":96.95316,"HyperDash":false},{"StartTime":49298.0,"Position":160.0,"HyperDash":false}]},{"StartTime":49480.0,"Objects":[{"StartTime":49480.0,"Position":336.0,"HyperDash":false},{"StartTime":49552.0,"Position":353.812164,"HyperDash":false},{"StartTime":49661.0,"Position":421.0,"HyperDash":false}]},{"StartTime":49844.0,"Objects":[{"StartTime":49844.0,"Position":164.0,"HyperDash":false},{"StartTime":49916.0,"Position":123.187851,"HyperDash":false},{"StartTime":50025.0,"Position":79.0,"HyperDash":false}]},{"StartTime":50117.0,"Objects":[{"StartTime":50117.0,"Position":79.0,"HyperDash":false}]},{"StartTime":50208.0,"Objects":[{"StartTime":50208.0,"Position":79.0,"HyperDash":false}]},{"StartTime":50390.0,"Objects":[{"StartTime":50390.0,"Position":172.0,"HyperDash":false},{"StartTime":50480.0,"Position":196.148758,"HyperDash":false},{"StartTime":50571.0,"Position":256.0,"HyperDash":false},{"StartTime":50644.0,"Position":261.0,"HyperDash":false},{"StartTime":50753.0,"Position":256.0,"HyperDash":false}]},{"StartTime":50935.0,"Objects":[{"StartTime":50935.0,"Position":80.0,"HyperDash":false},{"StartTime":51007.0,"Position":81.0,"HyperDash":false},{"StartTime":51116.0,"Position":80.0,"HyperDash":false}]},{"StartTime":51299.0,"Objects":[{"StartTime":51299.0,"Position":256.0,"HyperDash":false},{"StartTime":51389.0,"Position":296.148743,"HyperDash":false},{"StartTime":51480.0,"Position":340.765839,"HyperDash":false},{"StartTime":51553.0,"Position":371.953156,"HyperDash":false},{"StartTime":51662.0,"Position":426.0,"HyperDash":false}]},{"StartTime":51844.0,"Objects":[{"StartTime":51844.0,"Position":340.0,"HyperDash":false}]},{"StartTime":52026.0,"Objects":[{"StartTime":52026.0,"Position":426.0,"HyperDash":false},{"StartTime":52098.0,"Position":406.187836,"HyperDash":false},{"StartTime":52207.0,"Position":341.0,"HyperDash":false}]},{"StartTime":52390.0,"Objects":[{"StartTime":52390.0,"Position":164.0,"HyperDash":false},{"StartTime":52462.0,"Position":117.187851,"HyperDash":false},{"StartTime":52571.0,"Position":79.0,"HyperDash":true}]},{"StartTime":52753.0,"Objects":[{"StartTime":52753.0,"Position":336.0,"HyperDash":false},{"StartTime":52843.0,"Position":377.148743,"HyperDash":false},{"StartTime":52934.0,"Position":420.765839,"HyperDash":false},{"StartTime":53007.0,"Position":457.953156,"HyperDash":false},{"StartTime":53116.0,"Position":506.0,"HyperDash":false}]},{"StartTime":53299.0,"Objects":[{"StartTime":53299.0,"Position":328.0,"HyperDash":false},{"StartTime":53389.0,"Position":380.148743,"HyperDash":false},{"StartTime":53480.0,"Position":412.765839,"HyperDash":false},{"StartTime":53553.0,"Position":426.953156,"HyperDash":false},{"StartTime":53662.0,"Position":498.0,"HyperDash":false}]},{"StartTime":53844.0,"Objects":[{"StartTime":53844.0,"Position":412.0,"HyperDash":false},{"StartTime":53916.0,"Position":416.0,"HyperDash":false},{"StartTime":54025.0,"Position":412.0,"HyperDash":false}]},{"StartTime":54208.0,"Objects":[{"StartTime":54208.0,"Position":236.0,"HyperDash":false},{"StartTime":54280.0,"Position":207.187851,"HyperDash":false},{"StartTime":54389.0,"Position":151.0,"HyperDash":false}]},{"StartTime":54480.0,"Objects":[{"StartTime":54480.0,"Position":192.0,"HyperDash":false}]},{"StartTime":54571.0,"Objects":[{"StartTime":54571.0,"Position":236.0,"HyperDash":false}]},{"StartTime":54753.0,"Objects":[{"StartTime":54753.0,"Position":320.0,"HyperDash":false}]},{"StartTime":54935.0,"Objects":[{"StartTime":54935.0,"Position":236.0,"HyperDash":false}]},{"StartTime":55117.0,"Objects":[{"StartTime":55117.0,"Position":152.0,"HyperDash":false}]},{"StartTime":55299.0,"Objects":[{"StartTime":55299.0,"Position":328.0,"HyperDash":false},{"StartTime":55371.0,"Position":328.0,"HyperDash":false},{"StartTime":55480.0,"Position":328.0,"HyperDash":false}]},{"StartTime":55662.0,"Objects":[{"StartTime":55662.0,"Position":72.0,"HyperDash":false},{"StartTime":55734.0,"Position":54.0,"HyperDash":false},{"StartTime":55843.0,"Position":72.0,"HyperDash":false}]},{"StartTime":55935.0,"Objects":[{"StartTime":55935.0,"Position":116.0,"HyperDash":false}]},{"StartTime":56026.0,"Objects":[{"StartTime":56026.0,"Position":160.0,"HyperDash":false}]},{"StartTime":56208.0,"Objects":[{"StartTime":56208.0,"Position":244.0,"HyperDash":false},{"StartTime":56298.0,"Position":182.851242,"HyperDash":false},{"StartTime":56389.0,"Position":159.0,"HyperDash":false},{"StartTime":56462.0,"Position":181.953156,"HyperDash":false},{"StartTime":56571.0,"Position":244.0,"HyperDash":false}]},{"StartTime":56753.0,"Objects":[{"StartTime":56753.0,"Position":72.0,"HyperDash":false},{"StartTime":56825.0,"Position":81.0,"HyperDash":false},{"StartTime":56934.0,"Position":72.0,"HyperDash":false}]},{"StartTime":57117.0,"Objects":[{"StartTime":57117.0,"Position":248.0,"HyperDash":false},{"StartTime":57207.0,"Position":290.5,"HyperDash":false},{"StartTime":57298.0,"Position":248.0,"HyperDash":false}]},{"StartTime":57481.0,"Objects":[{"StartTime":57481.0,"Position":78.0,"HyperDash":false},{"StartTime":57553.0,"Position":71.67611,"HyperDash":false},{"StartTime":57662.0,"Position":79.69966,"HyperDash":false}]},{"StartTime":57844.0,"Objects":[{"StartTime":57844.0,"Position":164.0,"HyperDash":false},{"StartTime":57916.0,"Position":146.187851,"HyperDash":false},{"StartTime":58025.0,"Position":79.0,"HyperDash":false}]},{"StartTime":58208.0,"Objects":[{"StartTime":58208.0,"Position":248.0,"HyperDash":false},{"StartTime":58280.0,"Position":228.187851,"HyperDash":false},{"StartTime":58389.0,"Position":163.0,"HyperDash":false}]},{"StartTime":58571.0,"Objects":[{"StartTime":58571.0,"Position":416.0,"HyperDash":false},{"StartTime":58661.0,"Position":451.148743,"HyperDash":false},{"StartTime":58752.0,"Position":499.234161,"HyperDash":false},{"StartTime":58825.0,"Position":447.046844,"HyperDash":false},{"StartTime":58934.0,"Position":414.0,"HyperDash":false}]},{"StartTime":59117.0,"Objects":[{"StartTime":59117.0,"Position":320.0,"HyperDash":false}]},{"StartTime":59299.0,"Objects":[{"StartTime":59299.0,"Position":140.0,"HyperDash":false},{"StartTime":59389.0,"Position":111.851242,"HyperDash":false},{"StartTime":59480.0,"Position":55.0,"HyperDash":false},{"StartTime":59553.0,"Position":89.95316,"HyperDash":false},{"StartTime":59662.0,"Position":140.0,"HyperDash":false}]},{"StartTime":60026.0,"Objects":[{"StartTime":60026.0,"Position":428.0,"HyperDash":false},{"StartTime":60098.0,"Position":432.0,"HyperDash":false},{"StartTime":60207.0,"Position":428.0,"HyperDash":false}]},{"StartTime":60390.0,"Objects":[{"StartTime":60390.0,"Position":332.0,"HyperDash":false},{"StartTime":60462.0,"Position":362.812164,"HyperDash":false},{"StartTime":60571.0,"Position":417.0,"HyperDash":false}]},{"StartTime":60753.0,"Objects":[{"StartTime":60753.0,"Position":324.0,"HyperDash":false}]},{"StartTime":60843.0,"Objects":[{"StartTime":60843.0,"Position":366.0,"HyperDash":false}]},{"StartTime":60934.0,"Objects":[{"StartTime":60934.0,"Position":409.0,"HyperDash":false}]},{"StartTime":61117.0,"Objects":[{"StartTime":61117.0,"Position":228.0,"HyperDash":false},{"StartTime":61189.0,"Position":181.187851,"HyperDash":false},{"StartTime":61298.0,"Position":143.0,"HyperDash":false}]},{"StartTime":61480.0,"Objects":[{"StartTime":61480.0,"Position":324.0,"HyperDash":false},{"StartTime":61570.0,"Position":323.0,"HyperDash":false},{"StartTime":61661.0,"Position":324.0,"HyperDash":false},{"StartTime":61734.0,"Position":306.0,"HyperDash":false},{"StartTime":61843.0,"Position":324.0,"HyperDash":false}]},{"StartTime":62026.0,"Objects":[{"StartTime":62026.0,"Position":228.0,"HyperDash":false}]},{"StartTime":62208.0,"Objects":[{"StartTime":62208.0,"Position":408.0,"HyperDash":false},{"StartTime":62298.0,"Position":361.851257,"HyperDash":false},{"StartTime":62389.0,"Position":323.0,"HyperDash":false},{"StartTime":62462.0,"Position":339.953156,"HyperDash":false},{"StartTime":62571.0,"Position":408.0,"HyperDash":false}]},{"StartTime":62935.0,"Objects":[{"StartTime":62935.0,"Position":120.0,"HyperDash":false},{"StartTime":63025.0,"Position":77.5,"HyperDash":false},{"StartTime":63116.0,"Position":120.0,"HyperDash":false}]},{"StartTime":63299.0,"Objects":[{"StartTime":63299.0,"Position":216.0,"HyperDash":false},{"StartTime":63371.0,"Position":227.0,"HyperDash":false},{"StartTime":63480.0,"Position":216.0,"HyperDash":false}]},{"StartTime":63662.0,"Objects":[{"StartTime":63662.0,"Position":396.0,"HyperDash":false},{"StartTime":63734.0,"Position":343.187836,"HyperDash":false},{"StartTime":63843.0,"Position":311.0,"HyperDash":false}]},{"StartTime":64026.0,"Objects":[{"StartTime":64026.0,"Position":148.0,"HyperDash":false}]},{"StartTime":64208.0,"Objects":[{"StartTime":64208.0,"Position":320.0,"HyperDash":false}]},{"StartTime":64390.0,"Objects":[{"StartTime":64390.0,"Position":140.0,"HyperDash":false},{"StartTime":64480.0,"Position":114.851242,"HyperDash":false},{"StartTime":64571.0,"Position":56.0,"HyperDash":false},{"StartTime":64644.0,"Position":56.0,"HyperDash":false},{"StartTime":64753.0,"Position":56.0,"HyperDash":false}]},{"StartTime":64935.0,"Objects":[{"StartTime":64935.0,"Position":140.0,"HyperDash":false}]},{"StartTime":65117.0,"Objects":[{"StartTime":65117.0,"Position":396.0,"HyperDash":false},{"StartTime":65189.0,"Position":395.0,"HyperDash":false},{"StartTime":65298.0,"Position":396.0,"HyperDash":false}]},{"StartTime":65480.0,"Objects":[{"StartTime":65480.0,"Position":312.0,"HyperDash":false}]},{"StartTime":65662.0,"Objects":[{"StartTime":65662.0,"Position":404.0,"HyperDash":false}]},{"StartTime":65844.0,"Objects":[{"StartTime":65844.0,"Position":300.0,"HyperDash":false},{"StartTime":65916.0,"Position":278.187836,"HyperDash":false},{"StartTime":66025.0,"Position":215.0,"HyperDash":false}]},{"StartTime":66208.0,"Objects":[{"StartTime":66208.0,"Position":392.0,"HyperDash":false},{"StartTime":66280.0,"Position":394.0,"HyperDash":false},{"StartTime":66389.0,"Position":392.0,"HyperDash":false}]},{"StartTime":66571.0,"Objects":[{"StartTime":66571.0,"Position":136.0,"HyperDash":false},{"StartTime":66643.0,"Position":137.0,"HyperDash":false},{"StartTime":66752.0,"Position":136.0,"HyperDash":false}]},{"StartTime":66935.0,"Objects":[{"StartTime":66935.0,"Position":307.0,"HyperDash":false},{"StartTime":67007.0,"Position":327.812164,"HyperDash":false},{"StartTime":67116.0,"Position":392.0,"HyperDash":false}]},{"StartTime":67299.0,"Objects":[{"StartTime":67299.0,"Position":476.0,"HyperDash":false},{"StartTime":67371.0,"Position":479.0,"HyperDash":false},{"StartTime":67480.0,"Position":476.0,"HyperDash":false}]},{"StartTime":67662.0,"Objects":[{"StartTime":67662.0,"Position":307.0,"HyperDash":false},{"StartTime":67734.0,"Position":295.0,"HyperDash":false},{"StartTime":67843.0,"Position":307.0,"HyperDash":true}]},{"StartTime":68026.0,"Objects":[{"StartTime":68026.0,"Position":48.0,"HyperDash":false},{"StartTime":68098.0,"Position":74.81215,"HyperDash":false},{"StartTime":68207.0,"Position":133.0,"HyperDash":false}]},{"StartTime":68390.0,"Objects":[{"StartTime":68390.0,"Position":307.0,"HyperDash":false},{"StartTime":68462.0,"Position":288.0,"HyperDash":false},{"StartTime":68571.0,"Position":307.0,"HyperDash":false}]},{"StartTime":68753.0,"Objects":[{"StartTime":68753.0,"Position":222.0,"HyperDash":false},{"StartTime":68825.0,"Position":257.812134,"HyperDash":false},{"StartTime":68934.0,"Position":307.0,"HyperDash":false}]},{"StartTime":69117.0,"Objects":[{"StartTime":69117.0,"Position":136.0,"HyperDash":false},{"StartTime":69189.0,"Position":131.0,"HyperDash":false},{"StartTime":69298.0,"Position":136.0,"HyperDash":false}]},{"StartTime":69480.0,"Objects":[{"StartTime":69480.0,"Position":228.0,"HyperDash":false},{"StartTime":69552.0,"Position":175.187851,"HyperDash":false},{"StartTime":69661.0,"Position":143.0,"HyperDash":false}]},{"StartTime":69844.0,"Objects":[{"StartTime":69844.0,"Position":236.0,"HyperDash":false},{"StartTime":69916.0,"Position":254.812164,"HyperDash":false},{"StartTime":70025.0,"Position":321.0,"HyperDash":true}]},{"StartTime":70208.0,"Objects":[{"StartTime":70208.0,"Position":60.0,"HyperDash":false},{"StartTime":70298.0,"Position":66.0,"HyperDash":false},{"StartTime":70389.0,"Position":60.76584,"HyperDash":false},{"StartTime":70462.0,"Position":88.95316,"HyperDash":false},{"StartTime":70571.0,"Position":146.0,"HyperDash":false}]},{"StartTime":70753.0,"Objects":[{"StartTime":70753.0,"Position":232.0,"HyperDash":false}]},{"StartTime":70935.0,"Objects":[{"StartTime":70935.0,"Position":412.0,"HyperDash":false},{"StartTime":71025.0,"Position":356.851257,"HyperDash":false},{"StartTime":71116.0,"Position":327.0,"HyperDash":false},{"StartTime":71189.0,"Position":351.953156,"HyperDash":false},{"StartTime":71298.0,"Position":412.0,"HyperDash":false}]},{"StartTime":71662.0,"Objects":[{"StartTime":71662.0,"Position":124.0,"HyperDash":false},{"StartTime":71734.0,"Position":118.0,"HyperDash":false},{"StartTime":71843.0,"Position":124.0,"HyperDash":false}]},{"StartTime":72026.0,"Objects":[{"StartTime":72026.0,"Position":220.0,"HyperDash":false},{"StartTime":72098.0,"Position":242.812149,"HyperDash":false},{"StartTime":72207.0,"Position":305.0,"HyperDash":false}]},{"StartTime":72389.0,"Objects":[{"StartTime":72389.0,"Position":212.0,"HyperDash":false}]},{"StartTime":72571.0,"Objects":[{"StartTime":72571.0,"Position":316.0,"HyperDash":false}]},{"StartTime":72753.0,"Objects":[{"StartTime":72753.0,"Position":136.0,"HyperDash":false},{"StartTime":72825.0,"Position":102.187851,"HyperDash":false},{"StartTime":72934.0,"Position":51.0,"HyperDash":true}]},{"StartTime":73116.0,"Objects":[{"StartTime":73116.0,"Position":316.0,"HyperDash":false},{"StartTime":73206.0,"Position":344.148743,"HyperDash":false},{"StartTime":73297.0,"Position":400.0,"HyperDash":false},{"StartTime":73370.0,"Position":415.0,"HyperDash":false},{"StartTime":73479.0,"Position":400.0,"HyperDash":false}]},{"StartTime":73662.0,"Objects":[{"StartTime":73662.0,"Position":316.0,"HyperDash":false}]},{"StartTime":73844.0,"Objects":[{"StartTime":73844.0,"Position":144.0,"HyperDash":false}]},{"StartTime":74026.0,"Objects":[{"StartTime":74026.0,"Position":236.0,"HyperDash":false}]},{"StartTime":74208.0,"Objects":[{"StartTime":74208.0,"Position":328.0,"HyperDash":false}]},{"StartTime":74571.0,"Objects":[{"StartTime":74571.0,"Position":56.0,"HyperDash":false}]},{"StartTime":74753.0,"Objects":[{"StartTime":74753.0,"Position":228.0,"HyperDash":false}]},{"StartTime":74935.0,"Objects":[{"StartTime":74935.0,"Position":400.0,"HyperDash":false},{"StartTime":75007.0,"Position":389.0,"HyperDash":false},{"StartTime":75116.0,"Position":400.0,"HyperDash":false}]},{"StartTime":75298.0,"Objects":[{"StartTime":75298.0,"Position":308.0,"HyperDash":false},{"StartTime":75370.0,"Position":335.812164,"HyperDash":false},{"StartTime":75479.0,"Position":393.0,"HyperDash":false}]},{"StartTime":75662.0,"Objects":[{"StartTime":75662.0,"Position":232.0,"HyperDash":false}]},{"StartTime":75844.0,"Objects":[{"StartTime":75844.0,"Position":401.0,"HyperDash":false}]},{"StartTime":76026.0,"Objects":[{"StartTime":76026.0,"Position":224.0,"HyperDash":false},{"StartTime":76116.0,"Position":198.851242,"HyperDash":false},{"StartTime":76207.0,"Position":140.765839,"HyperDash":false},{"StartTime":76280.0,"Position":189.953156,"HyperDash":false},{"StartTime":76389.0,"Position":226.0,"HyperDash":false}]},{"StartTime":76571.0,"Objects":[{"StartTime":76571.0,"Position":312.0,"HyperDash":false}]},{"StartTime":76753.0,"Objects":[{"StartTime":76753.0,"Position":56.0,"HyperDash":false},{"StartTime":76825.0,"Position":74.0,"HyperDash":false},{"StartTime":76934.0,"Position":56.0,"HyperDash":false}]},{"StartTime":77116.0,"Objects":[{"StartTime":77116.0,"Position":140.0,"HyperDash":false}]},{"StartTime":77298.0,"Objects":[{"StartTime":77298.0,"Position":48.0,"HyperDash":false}]},{"StartTime":77480.0,"Objects":[{"StartTime":77480.0,"Position":148.0,"HyperDash":false},{"StartTime":77552.0,"Position":164.812149,"HyperDash":false},{"StartTime":77661.0,"Position":233.0,"HyperDash":false}]},{"StartTime":77844.0,"Objects":[{"StartTime":77844.0,"Position":408.0,"HyperDash":false},{"StartTime":77916.0,"Position":392.0,"HyperDash":false},{"StartTime":78025.0,"Position":408.0,"HyperDash":false}]},{"StartTime":78207.0,"Objects":[{"StartTime":78207.0,"Position":236.0,"HyperDash":false},{"StartTime":78279.0,"Position":281.812164,"HyperDash":false},{"StartTime":78388.0,"Position":321.0,"HyperDash":false}]},{"StartTime":78571.0,"Objects":[{"StartTime":78571.0,"Position":493.0,"HyperDash":false},{"StartTime":78643.0,"Position":471.187836,"HyperDash":false},{"StartTime":78752.0,"Position":408.0,"HyperDash":false}]},{"StartTime":78935.0,"Objects":[{"StartTime":78935.0,"Position":504.0,"HyperDash":false}]},{"StartTime":79117.0,"Objects":[{"StartTime":79117.0,"Position":332.0,"HyperDash":false}]},{"StartTime":79208.0,"Objects":[{"StartTime":79208.0,"Position":284.0,"HyperDash":false}]},{"StartTime":79298.0,"Objects":[{"StartTime":79298.0,"Position":236.0,"HyperDash":false},{"StartTime":79370.0,"Position":251.0,"HyperDash":false},{"StartTime":79479.0,"Position":236.0,"HyperDash":false}]},{"StartTime":79662.0,"Objects":[{"StartTime":79662.0,"Position":60.0,"HyperDash":false},{"StartTime":79734.0,"Position":52.0,"HyperDash":false},{"StartTime":79843.0,"Position":60.0,"HyperDash":false}]},{"StartTime":80026.0,"Objects":[{"StartTime":80026.0,"Position":236.0,"HyperDash":false},{"StartTime":80098.0,"Position":255.812164,"HyperDash":false},{"StartTime":80207.0,"Position":321.0,"HyperDash":false}]},{"StartTime":80389.0,"Objects":[{"StartTime":80389.0,"Position":228.0,"HyperDash":false}]},{"StartTime":80479.0,"Objects":[{"StartTime":80479.0,"Position":228.0,"HyperDash":false}]},{"StartTime":80570.0,"Objects":[{"StartTime":80570.0,"Position":228.0,"HyperDash":false}]},{"StartTime":80753.0,"Objects":[{"StartTime":80753.0,"Position":404.0,"HyperDash":false},{"StartTime":80825.0,"Position":400.0,"HyperDash":false},{"StartTime":80934.0,"Position":404.0,"HyperDash":false}]},{"StartTime":81116.0,"Objects":[{"StartTime":81116.0,"Position":227.0,"HyperDash":false},{"StartTime":81188.0,"Position":273.812164,"HyperDash":false},{"StartTime":81297.0,"Position":312.0,"HyperDash":false}]},{"StartTime":81480.0,"Objects":[{"StartTime":81480.0,"Position":404.0,"HyperDash":false},{"StartTime":81552.0,"Position":369.187836,"HyperDash":false},{"StartTime":81661.0,"Position":319.0,"HyperDash":false}]},{"StartTime":81844.0,"Objects":[{"StartTime":81844.0,"Position":133.0,"HyperDash":false},{"StartTime":81934.0,"Position":90.5,"HyperDash":false},{"StartTime":82025.0,"Position":133.0,"HyperDash":false}]},{"StartTime":82208.0,"Objects":[{"StartTime":82208.0,"Position":303.0,"HyperDash":false},{"StartTime":82280.0,"Position":269.187836,"HyperDash":false},{"StartTime":82389.0,"Position":218.0,"HyperDash":false}]},{"StartTime":82480.0,"Objects":[{"StartTime":82480.0,"Position":264.0,"HyperDash":false}]},{"StartTime":82572.0,"Objects":[{"StartTime":82572.0,"Position":313.0,"HyperDash":false},{"StartTime":82644.0,"Position":272.187836,"HyperDash":false},{"StartTime":82753.0,"Position":228.0,"HyperDash":false}]},{"StartTime":82935.0,"Objects":[{"StartTime":82935.0,"Position":48.0,"HyperDash":false},{"StartTime":83007.0,"Position":97.81215,"HyperDash":false},{"StartTime":83116.0,"Position":133.0,"HyperDash":true}]},{"StartTime":83299.0,"Objects":[{"StartTime":83299.0,"Position":392.0,"HyperDash":false},{"StartTime":83389.0,"Position":451.578522,"HyperDash":false},{"StartTime":83480.0,"Position":493.719,"HyperDash":false},{"StartTime":83553.0,"Position":512.0,"HyperDash":false},{"StartTime":83662.0,"Position":496.0,"HyperDash":false}]},{"StartTime":83753.0,"Objects":[{"StartTime":83753.0,"Position":452.0,"HyperDash":false}]},{"StartTime":83844.0,"Objects":[{"StartTime":83844.0,"Position":408.0,"HyperDash":false}]},{"StartTime":84026.0,"Objects":[{"StartTime":84026.0,"Position":324.0,"HyperDash":false},{"StartTime":84098.0,"Position":308.0,"HyperDash":false},{"StartTime":84207.0,"Position":324.0,"HyperDash":false}]},{"StartTime":84390.0,"Objects":[{"StartTime":84390.0,"Position":152.0,"HyperDash":false},{"StartTime":84480.0,"Position":152.0,"HyperDash":false}]},{"StartTime":84662.0,"Objects":[{"StartTime":84662.0,"Position":248.0,"HyperDash":false}]},{"StartTime":84753.0,"Objects":[{"StartTime":84753.0,"Position":248.0,"HyperDash":false},{"StartTime":84825.0,"Position":213.187851,"HyperDash":false},{"StartTime":84934.0,"Position":163.0,"HyperDash":false}]},{"StartTime":85117.0,"Objects":[{"StartTime":85117.0,"Position":332.0,"HyperDash":false},{"StartTime":85207.0,"Position":332.0,"HyperDash":false},{"StartTime":85298.0,"Position":332.0,"HyperDash":false}]},{"StartTime":85480.0,"Objects":[{"StartTime":85480.0,"Position":244.0,"HyperDash":false}]},{"StartTime":85662.0,"Objects":[{"StartTime":85662.0,"Position":332.0,"HyperDash":false}]},{"StartTime":85844.0,"Objects":[{"StartTime":85844.0,"Position":156.0,"HyperDash":false},{"StartTime":85916.0,"Position":105.187851,"HyperDash":false},{"StartTime":86025.0,"Position":71.0,"HyperDash":false}]},{"StartTime":86208.0,"Objects":[{"StartTime":86208.0,"Position":164.0,"HyperDash":false},{"StartTime":86280.0,"Position":185.812149,"HyperDash":false},{"StartTime":86389.0,"Position":249.0,"HyperDash":false}]},{"StartTime":86571.0,"Objects":[{"StartTime":86571.0,"Position":80.0,"HyperDash":false}]},{"StartTime":86661.0,"Objects":[{"StartTime":86661.0,"Position":122.0,"HyperDash":false}]},{"StartTime":86752.0,"Objects":[{"StartTime":86752.0,"Position":165.0,"HyperDash":false}]},{"StartTime":86935.0,"Objects":[{"StartTime":86935.0,"Position":252.0,"HyperDash":false}]},{"StartTime":87117.0,"Objects":[{"StartTime":87117.0,"Position":156.0,"HyperDash":false}]},{"StartTime":87299.0,"Objects":[{"StartTime":87299.0,"Position":328.0,"HyperDash":false},{"StartTime":87389.0,"Position":328.0,"HyperDash":false}]},{"StartTime":87662.0,"Objects":[{"StartTime":87662.0,"Position":152.0,"HyperDash":false},{"StartTime":87752.0,"Position":109.5,"HyperDash":false},{"StartTime":87843.0,"Position":152.0,"HyperDash":false}]},{"StartTime":88026.0,"Objects":[{"StartTime":88026.0,"Position":236.0,"HyperDash":false},{"StartTime":88098.0,"Position":190.187851,"HyperDash":false},{"StartTime":88207.0,"Position":151.0,"HyperDash":false}]},{"StartTime":88390.0,"Objects":[{"StartTime":88390.0,"Position":328.0,"HyperDash":false},{"StartTime":88462.0,"Position":320.0,"HyperDash":false},{"StartTime":88571.0,"Position":328.0,"HyperDash":false}]},{"StartTime":88753.0,"Objects":[{"StartTime":88753.0,"Position":152.0,"HyperDash":false},{"StartTime":88825.0,"Position":120.187851,"HyperDash":false},{"StartTime":88934.0,"Position":67.0,"HyperDash":false}]},{"StartTime":89117.0,"Objects":[{"StartTime":89117.0,"Position":324.0,"HyperDash":false},{"StartTime":89207.0,"Position":355.148743,"HyperDash":false},{"StartTime":89298.0,"Position":408.765839,"HyperDash":false},{"StartTime":89371.0,"Position":451.953156,"HyperDash":false},{"StartTime":89480.0,"Position":494.0,"HyperDash":false}]},{"StartTime":89571.0,"Objects":[{"StartTime":89571.0,"Position":452.0,"HyperDash":false}]},{"StartTime":89662.0,"Objects":[{"StartTime":89662.0,"Position":408.0,"HyperDash":false}]},{"StartTime":89844.0,"Objects":[{"StartTime":89844.0,"Position":324.0,"HyperDash":false},{"StartTime":89916.0,"Position":314.0,"HyperDash":false},{"StartTime":90025.0,"Position":324.0,"HyperDash":false}]},{"StartTime":90208.0,"Objects":[{"StartTime":90208.0,"Position":148.0,"HyperDash":false},{"StartTime":90298.0,"Position":148.0,"HyperDash":false}]},{"StartTime":90480.0,"Objects":[{"StartTime":90480.0,"Position":232.0,"HyperDash":false}]},{"StartTime":90571.0,"Objects":[{"StartTime":90571.0,"Position":284.0,"HyperDash":false},{"StartTime":90643.0,"Position":299.0,"HyperDash":false},{"StartTime":90752.0,"Position":284.0,"HyperDash":false}]},{"StartTime":90844.0,"Objects":[{"StartTime":90844.0,"Position":236.0,"HyperDash":false},{"StartTime":90916.0,"Position":193.187851,"HyperDash":false},{"StartTime":91025.0,"Position":151.0,"HyperDash":false}]},{"StartTime":91117.0,"Objects":[{"StartTime":91117.0,"Position":152.0,"HyperDash":false}]},{"StartTime":91299.0,"Objects":[{"StartTime":91299.0,"Position":236.0,"HyperDash":false}]},{"StartTime":91480.0,"Objects":[{"StartTime":91480.0,"Position":144.0,"HyperDash":false}]},{"StartTime":91662.0,"Objects":[{"StartTime":91662.0,"Position":320.0,"HyperDash":false},{"StartTime":91734.0,"Position":309.0,"HyperDash":false},{"StartTime":91843.0,"Position":320.0,"HyperDash":false}]},{"StartTime":92026.0,"Objects":[{"StartTime":92026.0,"Position":224.0,"HyperDash":false},{"StartTime":92098.0,"Position":177.187851,"HyperDash":false},{"StartTime":92207.0,"Position":139.0,"HyperDash":false}]},{"StartTime":92299.0,"Objects":[{"StartTime":92299.0,"Position":92.0,"HyperDash":false},{"StartTime":92371.0,"Position":115.812149,"HyperDash":false},{"StartTime":92480.0,"Position":177.0,"HyperDash":false}]},{"StartTime":92571.0,"Objects":[{"StartTime":92571.0,"Position":224.0,"HyperDash":false}]},{"StartTime":92753.0,"Objects":[{"StartTime":92753.0,"Position":132.0,"HyperDash":false},{"StartTime":92825.0,"Position":167.812149,"HyperDash":false},{"StartTime":92934.0,"Position":217.0,"HyperDash":false}]},{"StartTime":93117.0,"Objects":[{"StartTime":93117.0,"Position":392.0,"HyperDash":false},{"StartTime":93189.0,"Position":384.0,"HyperDash":false},{"StartTime":93298.0,"Position":392.0,"HyperDash":false}]},{"StartTime":93480.0,"Objects":[{"StartTime":93480.0,"Position":216.0,"HyperDash":false}]},{"StartTime":93570.0,"Objects":[{"StartTime":93570.0,"Position":173.0,"HyperDash":false}]},{"StartTime":93661.0,"Objects":[{"StartTime":93661.0,"Position":131.0,"HyperDash":false}]},{"StartTime":93844.0,"Objects":[{"StartTime":93844.0,"Position":224.0,"HyperDash":false}]},{"StartTime":93934.0,"Objects":[{"StartTime":93934.0,"Position":181.0,"HyperDash":false}]},{"StartTime":94025.0,"Objects":[{"StartTime":94025.0,"Position":139.0,"HyperDash":false}]},{"StartTime":94208.0,"Objects":[{"StartTime":94208.0,"Position":312.0,"HyperDash":false},{"StartTime":94280.0,"Position":363.812164,"HyperDash":false},{"StartTime":94389.0,"Position":397.0,"HyperDash":false}]},{"StartTime":94571.0,"Objects":[{"StartTime":94571.0,"Position":220.0,"HyperDash":false},{"StartTime":94643.0,"Position":192.187851,"HyperDash":false},{"StartTime":94752.0,"Position":135.0,"HyperDash":false}]},{"StartTime":94935.0,"Objects":[{"StartTime":94935.0,"Position":392.0,"HyperDash":false},{"StartTime":95007.0,"Position":440.812164,"HyperDash":false},{"StartTime":95116.0,"Position":477.0,"HyperDash":false}]},{"StartTime":95299.0,"Objects":[{"StartTime":95299.0,"Position":384.0,"HyperDash":false},{"StartTime":95371.0,"Position":389.0,"HyperDash":false},{"StartTime":95480.0,"Position":384.0,"HyperDash":false}]},{"StartTime":95662.0,"Objects":[{"StartTime":95662.0,"Position":212.0,"HyperDash":false}]},{"StartTime":95844.0,"Objects":[{"StartTime":95844.0,"Position":306.0,"HyperDash":false}]},{"StartTime":96026.0,"Objects":[{"StartTime":96026.0,"Position":477.0,"HyperDash":false},{"StartTime":96098.0,"Position":461.0,"HyperDash":false},{"StartTime":96207.0,"Position":477.0,"HyperDash":false}]},{"StartTime":96390.0,"Objects":[{"StartTime":96390.0,"Position":300.0,"HyperDash":false},{"StartTime":96462.0,"Position":249.187836,"HyperDash":false},{"StartTime":96571.0,"Position":215.0,"HyperDash":false}]},{"StartTime":96753.0,"Objects":[{"StartTime":96753.0,"Position":308.0,"HyperDash":false},{"StartTime":96825.0,"Position":320.0,"HyperDash":false},{"StartTime":96934.0,"Position":308.0,"HyperDash":false}]},{"StartTime":97117.0,"Objects":[{"StartTime":97117.0,"Position":136.0,"HyperDash":false}]},{"StartTime":97299.0,"Objects":[{"StartTime":97299.0,"Position":300.0,"HyperDash":false}]},{"StartTime":97480.0,"Objects":[{"StartTime":97480.0,"Position":128.0,"HyperDash":false},{"StartTime":97552.0,"Position":135.0,"HyperDash":false},{"StartTime":97661.0,"Position":128.0,"HyperDash":false}]},{"StartTime":97844.0,"Objects":[{"StartTime":97844.0,"Position":300.0,"HyperDash":false},{"StartTime":97916.0,"Position":248.187836,"HyperDash":false},{"StartTime":98025.0,"Position":215.0,"HyperDash":false}]},{"StartTime":98208.0,"Objects":[{"StartTime":98208.0,"Position":308.0,"HyperDash":false}]},{"StartTime":98298.0,"Objects":[{"StartTime":98298.0,"Position":308.0,"HyperDash":false}]},{"StartTime":98389.0,"Objects":[{"StartTime":98389.0,"Position":308.0,"HyperDash":false}]},{"StartTime":98571.0,"Objects":[{"StartTime":98571.0,"Position":136.0,"HyperDash":false},{"StartTime":98643.0,"Position":173.812149,"HyperDash":false},{"StartTime":98752.0,"Position":221.0,"HyperDash":false}]},{"StartTime":98935.0,"Objects":[{"StartTime":98935.0,"Position":404.0,"HyperDash":false},{"StartTime":99007.0,"Position":405.0,"HyperDash":false},{"StartTime":99116.0,"Position":404.0,"HyperDash":false}]},{"StartTime":99299.0,"Objects":[{"StartTime":99299.0,"Position":224.0,"HyperDash":false},{"StartTime":99371.0,"Position":198.187851,"HyperDash":false},{"StartTime":99480.0,"Position":139.0,"HyperDash":false}]},{"StartTime":99662.0,"Objects":[{"StartTime":99662.0,"Position":312.0,"HyperDash":false},{"StartTime":99734.0,"Position":297.0,"HyperDash":false},{"StartTime":99843.0,"Position":312.0,"HyperDash":false}]},{"StartTime":100026.0,"Objects":[{"StartTime":100026.0,"Position":220.0,"HyperDash":false}]},{"StartTime":100208.0,"Objects":[{"StartTime":100208.0,"Position":312.0,"HyperDash":false}]},{"StartTime":100390.0,"Objects":[{"StartTime":100390.0,"Position":136.0,"HyperDash":false},{"StartTime":100462.0,"Position":98.18785,"HyperDash":false},{"StartTime":100571.0,"Position":51.0,"HyperDash":true}]},{"StartTime":100753.0,"Objects":[{"StartTime":100753.0,"Position":308.0,"HyperDash":false},{"StartTime":100825.0,"Position":340.812164,"HyperDash":false},{"StartTime":100934.0,"Position":393.0,"HyperDash":false}]},{"StartTime":101117.0,"Objects":[{"StartTime":101117.0,"Position":216.0,"HyperDash":false},{"StartTime":101189.0,"Position":223.0,"HyperDash":false},{"StartTime":101298.0,"Position":216.0,"HyperDash":false}]},{"StartTime":101480.0,"Objects":[{"StartTime":101480.0,"Position":300.0,"HyperDash":false}]},{"StartTime":101662.0,"Objects":[{"StartTime":101662.0,"Position":208.0,"HyperDash":false}]},{"StartTime":101844.0,"Objects":[{"StartTime":101844.0,"Position":384.0,"HyperDash":false},{"StartTime":101916.0,"Position":372.0,"HyperDash":false},{"StartTime":102025.0,"Position":384.0,"HyperDash":false}]},{"StartTime":102208.0,"Objects":[{"StartTime":102208.0,"Position":208.0,"HyperDash":false},{"StartTime":102280.0,"Position":181.187851,"HyperDash":false},{"StartTime":102389.0,"Position":123.0,"HyperDash":false}]},{"StartTime":102571.0,"Objects":[{"StartTime":102571.0,"Position":216.0,"HyperDash":false},{"StartTime":102643.0,"Position":214.0,"HyperDash":false},{"StartTime":102752.0,"Position":216.0,"HyperDash":false}]},{"StartTime":102935.0,"Objects":[{"StartTime":102935.0,"Position":52.0,"HyperDash":false}]},{"StartTime":103117.0,"Objects":[{"StartTime":103117.0,"Position":224.0,"HyperDash":false}]},{"StartTime":103299.0,"Objects":[{"StartTime":103299.0,"Position":44.0,"HyperDash":false},{"StartTime":103371.0,"Position":43.0,"HyperDash":false},{"StartTime":103480.0,"Position":44.0,"HyperDash":false}]},{"StartTime":103662.0,"Objects":[{"StartTime":103662.0,"Position":136.0,"HyperDash":false},{"StartTime":103734.0,"Position":162.812149,"HyperDash":false},{"StartTime":103843.0,"Position":221.0,"HyperDash":false}]},{"StartTime":103935.0,"Objects":[{"StartTime":103935.0,"Position":268.0,"HyperDash":false}]},{"StartTime":104026.0,"Objects":[{"StartTime":104026.0,"Position":316.0,"HyperDash":false},{"StartTime":104098.0,"Position":314.0,"HyperDash":false},{"StartTime":104207.0,"Position":316.0,"HyperDash":false}]},{"StartTime":104390.0,"Objects":[{"StartTime":104390.0,"Position":140.0,"HyperDash":false},{"StartTime":104462.0,"Position":188.812149,"HyperDash":false},{"StartTime":104571.0,"Position":225.0,"HyperDash":false}]},{"StartTime":104753.0,"Objects":[{"StartTime":104753.0,"Position":400.0,"HyperDash":false},{"StartTime":104825.0,"Position":417.0,"HyperDash":false},{"StartTime":104934.0,"Position":400.0,"HyperDash":false}]},{"StartTime":105117.0,"Objects":[{"StartTime":105117.0,"Position":224.0,"HyperDash":false}]},{"StartTime":105207.0,"Objects":[{"StartTime":105207.0,"Position":181.0,"HyperDash":false}]},{"StartTime":105298.0,"Objects":[{"StartTime":105298.0,"Position":139.0,"HyperDash":false}]},{"StartTime":105480.0,"Objects":[{"StartTime":105480.0,"Position":309.0,"HyperDash":false},{"StartTime":105552.0,"Position":259.187836,"HyperDash":false},{"StartTime":105661.0,"Position":224.0,"HyperDash":false}]},{"StartTime":105844.0,"Objects":[{"StartTime":105844.0,"Position":128.0,"HyperDash":false}]},{"StartTime":106026.0,"Objects":[{"StartTime":106026.0,"Position":216.0,"HyperDash":false}]},{"StartTime":106208.0,"Objects":[{"StartTime":106208.0,"Position":393.0,"HyperDash":false},{"StartTime":106280.0,"Position":408.812164,"HyperDash":false},{"StartTime":106389.0,"Position":478.0,"HyperDash":true}]},{"StartTime":106571.0,"Objects":[{"StartTime":106571.0,"Position":216.0,"HyperDash":false},{"StartTime":106643.0,"Position":194.187851,"HyperDash":false},{"StartTime":106752.0,"Position":131.0,"HyperDash":false}]},{"StartTime":106844.0,"Objects":[{"StartTime":106844.0,"Position":84.0,"HyperDash":false}]},{"StartTime":106935.0,"Objects":[{"StartTime":106935.0,"Position":131.0,"HyperDash":false},{"StartTime":107007.0,"Position":171.812149,"HyperDash":false},{"StartTime":107116.0,"Position":216.0,"HyperDash":false}]},{"StartTime":107299.0,"Objects":[{"StartTime":107299.0,"Position":312.0,"HyperDash":false}]},{"StartTime":107480.0,"Objects":[{"StartTime":107480.0,"Position":212.0,"HyperDash":false}]},{"StartTime":107662.0,"Objects":[{"StartTime":107662.0,"Position":392.0,"HyperDash":false},{"StartTime":107734.0,"Position":372.0,"HyperDash":false},{"StartTime":107843.0,"Position":392.0,"HyperDash":false}]},{"StartTime":108026.0,"Objects":[{"StartTime":108026.0,"Position":136.0,"HyperDash":false},{"StartTime":108098.0,"Position":101.187851,"HyperDash":false},{"StartTime":108207.0,"Position":51.0,"HyperDash":false}]},{"StartTime":108390.0,"Objects":[{"StartTime":108390.0,"Position":144.0,"HyperDash":false},{"StartTime":108462.0,"Position":129.0,"HyperDash":false},{"StartTime":108571.0,"Position":144.0,"HyperDash":false}]},{"StartTime":108753.0,"Objects":[{"StartTime":108753.0,"Position":304.0,"HyperDash":false}]},{"StartTime":108935.0,"Objects":[{"StartTime":108935.0,"Position":140.0,"HyperDash":false}]},{"StartTime":109117.0,"Objects":[{"StartTime":109117.0,"Position":312.0,"HyperDash":false},{"StartTime":109189.0,"Position":293.0,"HyperDash":false},{"StartTime":109298.0,"Position":312.0,"HyperDash":false}]},{"StartTime":109480.0,"Objects":[{"StartTime":109480.0,"Position":56.0,"HyperDash":false},{"StartTime":109552.0,"Position":60.0,"HyperDash":false},{"StartTime":109661.0,"Position":56.0,"HyperDash":false}]},{"StartTime":109844.0,"Objects":[{"StartTime":109844.0,"Position":140.0,"HyperDash":false}]},{"StartTime":109934.0,"Objects":[{"StartTime":109934.0,"Position":182.0,"HyperDash":false}]},{"StartTime":110025.0,"Objects":[{"StartTime":110025.0,"Position":225.0,"HyperDash":false}]},{"StartTime":110208.0,"Objects":[{"StartTime":110208.0,"Position":56.0,"HyperDash":false}]},{"StartTime":110390.0,"Objects":[{"StartTime":110390.0,"Position":152.0,"HyperDash":false}]},{"StartTime":110571.0,"Objects":[{"StartTime":110571.0,"Position":52.0,"HyperDash":false},{"StartTime":110643.0,"Position":54.0,"HyperDash":false},{"StartTime":110752.0,"Position":52.0,"HyperDash":true}]},{"StartTime":110935.0,"Objects":[{"StartTime":110935.0,"Position":312.0,"HyperDash":false},{"StartTime":111007.0,"Position":362.812164,"HyperDash":false},{"StartTime":111116.0,"Position":397.0,"HyperDash":false}]},{"StartTime":111299.0,"Objects":[{"StartTime":111299.0,"Position":304.0,"HyperDash":false}]},{"StartTime":111480.0,"Objects":[{"StartTime":111480.0,"Position":404.0,"HyperDash":false}]},{"StartTime":111662.0,"Objects":[{"StartTime":111662.0,"Position":312.0,"HyperDash":false}]},{"StartTime":111752.0,"Objects":[{"StartTime":111752.0,"Position":269.0,"HyperDash":false}]},{"StartTime":111843.0,"Objects":[{"StartTime":111843.0,"Position":227.0,"HyperDash":false}]},{"StartTime":112026.0,"Objects":[{"StartTime":112026.0,"Position":328.0,"HyperDash":false},{"StartTime":112098.0,"Position":339.0,"HyperDash":false},{"StartTime":112207.0,"Position":328.0,"HyperDash":true}]},{"StartTime":112390.0,"Objects":[{"StartTime":112390.0,"Position":68.0,"HyperDash":false},{"StartTime":112462.0,"Position":70.0,"HyperDash":false},{"StartTime":112571.0,"Position":68.0,"HyperDash":false}]},{"StartTime":112753.0,"Objects":[{"StartTime":112753.0,"Position":160.0,"HyperDash":false},{"StartTime":112825.0,"Position":201.812149,"HyperDash":false},{"StartTime":112934.0,"Position":245.0,"HyperDash":false}]},{"StartTime":113117.0,"Objects":[{"StartTime":113117.0,"Position":420.0,"HyperDash":false},{"StartTime":113189.0,"Position":413.0,"HyperDash":false},{"StartTime":113298.0,"Position":420.0,"HyperDash":false}]},{"StartTime":113480.0,"Objects":[{"StartTime":113480.0,"Position":328.0,"HyperDash":false}]},{"StartTime":113570.0,"Objects":[{"StartTime":113570.0,"Position":285.0,"HyperDash":false}]},{"StartTime":113661.0,"Objects":[{"StartTime":113661.0,"Position":243.0,"HyperDash":false}]},{"StartTime":113844.0,"Objects":[{"StartTime":113844.0,"Position":492.0,"HyperDash":false},{"StartTime":113916.0,"Position":493.0,"HyperDash":false},{"StartTime":114025.0,"Position":492.0,"HyperDash":false}]},{"StartTime":114208.0,"Objects":[{"StartTime":114208.0,"Position":396.0,"HyperDash":false},{"StartTime":114280.0,"Position":346.187836,"HyperDash":false},{"StartTime":114389.0,"Position":311.0,"HyperDash":false}]},{"StartTime":114571.0,"Objects":[{"StartTime":114571.0,"Position":140.0,"HyperDash":false}]},{"StartTime":114753.0,"Objects":[{"StartTime":114753.0,"Position":311.0,"HyperDash":false}]},{"StartTime":114935.0,"Objects":[{"StartTime":114935.0,"Position":140.0,"HyperDash":false},{"StartTime":115007.0,"Position":121.0,"HyperDash":false},{"StartTime":115116.0,"Position":140.0,"HyperDash":false}]},{"StartTime":115299.0,"Objects":[{"StartTime":115299.0,"Position":396.0,"HyperDash":false},{"StartTime":115371.0,"Position":409.812164,"HyperDash":false},{"StartTime":115480.0,"Position":481.0,"HyperDash":false}]},{"StartTime":115662.0,"Objects":[{"StartTime":115662.0,"Position":308.0,"HyperDash":false},{"StartTime":115734.0,"Position":311.0,"HyperDash":false},{"StartTime":115843.0,"Position":308.0,"HyperDash":false}]},{"StartTime":116026.0,"Objects":[{"StartTime":116026.0,"Position":136.0,"HyperDash":false}]},{"StartTime":116208.0,"Objects":[{"StartTime":116208.0,"Position":228.0,"HyperDash":false}]},{"StartTime":116390.0,"Objects":[{"StartTime":116390.0,"Position":56.0,"HyperDash":false},{"StartTime":116462.0,"Position":56.0,"HyperDash":false},{"StartTime":116571.0,"Position":56.0,"HyperDash":false}]},{"StartTime":116753.0,"Objects":[{"StartTime":116753.0,"Position":312.0,"HyperDash":false},{"StartTime":116825.0,"Position":322.0,"HyperDash":false},{"StartTime":116934.0,"Position":312.0,"HyperDash":false}]},{"StartTime":117117.0,"Objects":[{"StartTime":117117.0,"Position":484.0,"HyperDash":false},{"StartTime":117207.0,"Position":484.0,"HyperDash":false},{"StartTime":117298.0,"Position":484.0,"HyperDash":false}]},{"StartTime":117480.0,"Objects":[{"StartTime":117480.0,"Position":392.0,"HyperDash":false}]},{"StartTime":117662.0,"Objects":[{"StartTime":117662.0,"Position":476.0,"HyperDash":false}]},{"StartTime":117844.0,"Objects":[{"StartTime":117844.0,"Position":304.0,"HyperDash":false}]},{"StartTime":117934.0,"Objects":[{"StartTime":117934.0,"Position":262.0,"HyperDash":false}]},{"StartTime":118025.0,"Objects":[{"StartTime":118025.0,"Position":219.0,"HyperDash":false}]},{"StartTime":118208.0,"Objects":[{"StartTime":118208.0,"Position":476.0,"HyperDash":false}]},{"StartTime":118299.0,"Objects":[{"StartTime":118299.0,"Position":476.0,"HyperDash":false}]},{"StartTime":118390.0,"Objects":[{"StartTime":118390.0,"Position":432.0,"HyperDash":false}]},{"StartTime":118571.0,"Objects":[{"StartTime":118571.0,"Position":260.0,"HyperDash":false}]},{"StartTime":118662.0,"Objects":[{"StartTime":118662.0,"Position":260.0,"HyperDash":false}]},{"StartTime":118753.0,"Objects":[{"StartTime":118753.0,"Position":260.0,"HyperDash":false}]},{"StartTime":118935.0,"Objects":[{"StartTime":118935.0,"Position":88.0,"HyperDash":false}]},{"StartTime":119026.0,"Objects":[{"StartTime":119026.0,"Position":88.0,"HyperDash":false}]},{"StartTime":119117.0,"Objects":[{"StartTime":119117.0,"Position":132.0,"HyperDash":false}]},{"StartTime":119299.0,"Objects":[{"StartTime":119299.0,"Position":304.0,"HyperDash":false},{"StartTime":119371.0,"Position":319.812164,"HyperDash":false},{"StartTime":119480.0,"Position":389.0,"HyperDash":true}]},{"StartTime":119662.0,"Objects":[{"StartTime":119662.0,"Position":112.0,"HyperDash":false}]},{"StartTime":120026.0,"Objects":[{"StartTime":120026.0,"Position":221.0,"HyperDash":false},{"StartTime":120111.0,"Position":407.0,"HyperDash":false},{"StartTime":120196.0,"Position":287.0,"HyperDash":false},{"StartTime":120281.0,"Position":135.0,"HyperDash":false},{"StartTime":120366.0,"Position":437.0,"HyperDash":false},{"StartTime":120452.0,"Position":289.0,"HyperDash":false},{"StartTime":120537.0,"Position":464.0,"HyperDash":false},{"StartTime":120622.0,"Position":36.0,"HyperDash":false},{"StartTime":120707.0,"Position":378.0,"HyperDash":false},{"StartTime":120792.0,"Position":297.0,"HyperDash":false},{"StartTime":120878.0,"Position":418.0,"HyperDash":false},{"StartTime":120963.0,"Position":329.0,"HyperDash":false},{"StartTime":121048.0,"Position":338.0,"HyperDash":false},{"StartTime":121133.0,"Position":394.0,"HyperDash":false},{"StartTime":121219.0,"Position":40.0,"HyperDash":false},{"StartTime":121304.0,"Position":13.0,"HyperDash":false},{"StartTime":121389.0,"Position":80.0,"HyperDash":false},{"StartTime":121474.0,"Position":138.0,"HyperDash":false},{"StartTime":121559.0,"Position":311.0,"HyperDash":false},{"StartTime":121645.0,"Position":216.0,"HyperDash":false},{"StartTime":121730.0,"Position":310.0,"HyperDash":false},{"StartTime":121815.0,"Position":397.0,"HyperDash":false},{"StartTime":121900.0,"Position":214.0,"HyperDash":false},{"StartTime":121986.0,"Position":505.0,"HyperDash":false},{"StartTime":122071.0,"Position":173.0,"HyperDash":false},{"StartTime":122156.0,"Position":295.0,"HyperDash":false},{"StartTime":122241.0,"Position":199.0,"HyperDash":false},{"StartTime":122326.0,"Position":494.0,"HyperDash":false},{"StartTime":122412.0,"Position":293.0,"HyperDash":false},{"StartTime":122497.0,"Position":115.0,"HyperDash":false},{"StartTime":122582.0,"Position":412.0,"HyperDash":false},{"StartTime":122667.0,"Position":506.0,"HyperDash":false},{"StartTime":122753.0,"Position":293.0,"HyperDash":false},{"StartTime":122838.0,"Position":346.0,"HyperDash":false},{"StartTime":122923.0,"Position":117.0,"HyperDash":false},{"StartTime":123008.0,"Position":285.0,"HyperDash":false},{"StartTime":123093.0,"Position":17.0,"HyperDash":false},{"StartTime":123179.0,"Position":238.0,"HyperDash":false},{"StartTime":123264.0,"Position":222.0,"HyperDash":false},{"StartTime":123349.0,"Position":450.0,"HyperDash":false},{"StartTime":123434.0,"Position":67.0,"HyperDash":false},{"StartTime":123519.0,"Position":219.0,"HyperDash":false},{"StartTime":123605.0,"Position":307.0,"HyperDash":false},{"StartTime":123690.0,"Position":367.0,"HyperDash":false},{"StartTime":123775.0,"Position":412.0,"HyperDash":false},{"StartTime":123860.0,"Position":413.0,"HyperDash":false},{"StartTime":123946.0,"Position":143.0,"HyperDash":false},{"StartTime":124031.0,"Position":339.0,"HyperDash":false},{"StartTime":124116.0,"Position":342.0,"HyperDash":false},{"StartTime":124201.0,"Position":249.0,"HyperDash":false},{"StartTime":124286.0,"Position":235.0,"HyperDash":false},{"StartTime":124372.0,"Position":323.0,"HyperDash":false},{"StartTime":124457.0,"Position":365.0,"HyperDash":false},{"StartTime":124542.0,"Position":74.0,"HyperDash":false},{"StartTime":124627.0,"Position":281.0,"HyperDash":false},{"StartTime":124713.0,"Position":398.0,"HyperDash":false},{"StartTime":124798.0,"Position":335.0,"HyperDash":false},{"StartTime":124883.0,"Position":388.0,"HyperDash":false},{"StartTime":124968.0,"Position":228.0,"HyperDash":false},{"StartTime":125053.0,"Position":323.0,"HyperDash":false},{"StartTime":125139.0,"Position":441.0,"HyperDash":false},{"StartTime":125224.0,"Position":442.0,"HyperDash":false},{"StartTime":125309.0,"Position":278.0,"HyperDash":false},{"StartTime":125394.0,"Position":90.0,"HyperDash":false},{"StartTime":125480.0,"Position":409.0,"HyperDash":false}]},{"StartTime":131299.0,"Objects":[{"StartTime":131299.0,"Position":296.0,"HyperDash":false},{"StartTime":131389.0,"Position":305.0,"HyperDash":false},{"StartTime":131480.0,"Position":296.0,"HyperDash":false},{"StartTime":131553.0,"Position":309.0,"HyperDash":false},{"StartTime":131662.0,"Position":296.0,"HyperDash":false}]},{"StartTime":132026.0,"Objects":[{"StartTime":132026.0,"Position":152.0,"HyperDash":false}]},{"StartTime":132208.0,"Objects":[{"StartTime":132208.0,"Position":244.0,"HyperDash":false}]},{"StartTime":132390.0,"Objects":[{"StartTime":132390.0,"Position":336.0,"HyperDash":false}]},{"StartTime":132571.0,"Objects":[{"StartTime":132571.0,"Position":244.0,"HyperDash":false}]},{"StartTime":132753.0,"Objects":[{"StartTime":132753.0,"Position":416.0,"HyperDash":false},{"StartTime":132843.0,"Position":402.0,"HyperDash":false},{"StartTime":132934.0,"Position":416.0,"HyperDash":false},{"StartTime":133025.0,"Position":411.0,"HyperDash":false},{"StartTime":133116.0,"Position":416.0,"HyperDash":false},{"StartTime":133207.0,"Position":416.0,"HyperDash":false},{"StartTime":133298.0,"Position":416.0,"HyperDash":false},{"StartTime":133371.0,"Position":427.0,"HyperDash":false},{"StartTime":133480.0,"Position":416.0,"HyperDash":false}]},{"StartTime":133844.0,"Objects":[{"StartTime":133844.0,"Position":280.0,"HyperDash":false}]},{"StartTime":134026.0,"Objects":[{"StartTime":134026.0,"Position":188.0,"HyperDash":false}]},{"StartTime":134208.0,"Objects":[{"StartTime":134208.0,"Position":16.0,"HyperDash":false},{"StartTime":134298.0,"Position":1.0,"HyperDash":false},{"StartTime":134389.0,"Position":16.0,"HyperDash":false},{"StartTime":134462.0,"Position":9.0,"HyperDash":false},{"StartTime":134571.0,"Position":16.0,"HyperDash":false}]},{"StartTime":134935.0,"Objects":[{"StartTime":134935.0,"Position":176.0,"HyperDash":false}]},{"StartTime":135299.0,"Objects":[{"StartTime":135299.0,"Position":32.0,"HyperDash":false}]},{"StartTime":135662.0,"Objects":[{"StartTime":135662.0,"Position":272.0,"HyperDash":false},{"StartTime":135752.0,"Position":255.0,"HyperDash":false},{"StartTime":135843.0,"Position":272.0,"HyperDash":false},{"StartTime":135916.0,"Position":286.0,"HyperDash":false},{"StartTime":136025.0,"Position":272.0,"HyperDash":false}]},{"StartTime":136390.0,"Objects":[{"StartTime":136390.0,"Position":428.0,"HyperDash":false},{"StartTime":136480.0,"Position":429.0,"HyperDash":false},{"StartTime":136571.0,"Position":428.0,"HyperDash":false},{"StartTime":136644.0,"Position":433.0,"HyperDash":false},{"StartTime":136753.0,"Position":428.0,"HyperDash":false}]},{"StartTime":137117.0,"Objects":[{"StartTime":137117.0,"Position":132.0,"HyperDash":false},{"StartTime":137207.0,"Position":168.09079,"HyperDash":false},{"StartTime":137298.0,"Position":216.649246,"HyperDash":false},{"StartTime":137389.0,"Position":265.2077,"HyperDash":false},{"StartTime":137480.0,"Position":302.0,"HyperDash":false},{"StartTime":137571.0,"Position":256.675385,"HyperDash":false},{"StartTime":137662.0,"Position":217.116913,"HyperDash":false},{"StartTime":137735.0,"Position":187.976624,"HyperDash":false},{"StartTime":137844.0,"Position":132.0,"HyperDash":false}]},{"StartTime":138571.0,"Objects":[{"StartTime":138571.0,"Position":336.0,"HyperDash":false},{"StartTime":138661.0,"Position":321.0,"HyperDash":false},{"StartTime":138752.0,"Position":336.0,"HyperDash":false},{"StartTime":138825.0,"Position":328.0,"HyperDash":false},{"StartTime":138934.0,"Position":336.0,"HyperDash":false}]},{"StartTime":139117.0,"Objects":[{"StartTime":139117.0,"Position":240.0,"HyperDash":false}]},{"StartTime":139299.0,"Objects":[{"StartTime":139299.0,"Position":336.0,"HyperDash":false}]},{"StartTime":139662.0,"Objects":[{"StartTime":139662.0,"Position":480.0,"HyperDash":false}]},{"StartTime":139844.0,"Objects":[{"StartTime":139844.0,"Position":388.0,"HyperDash":false}]},{"StartTime":140026.0,"Objects":[{"StartTime":140026.0,"Position":212.0,"HyperDash":false},{"StartTime":140116.0,"Position":200.0,"HyperDash":false},{"StartTime":140207.0,"Position":212.0,"HyperDash":false},{"StartTime":140298.0,"Position":229.0,"HyperDash":false},{"StartTime":140389.0,"Position":212.0,"HyperDash":false},{"StartTime":140480.0,"Position":203.0,"HyperDash":false},{"StartTime":140571.0,"Position":212.0,"HyperDash":false},{"StartTime":140644.0,"Position":211.0,"HyperDash":false},{"StartTime":140753.0,"Position":212.0,"HyperDash":false}]},{"StartTime":141480.0,"Objects":[{"StartTime":141480.0,"Position":448.0,"HyperDash":false},{"StartTime":141570.0,"Position":415.636353,"HyperDash":false},{"StartTime":141661.0,"Position":354.5,"HyperDash":false},{"StartTime":141734.0,"Position":391.84848,"HyperDash":false},{"StartTime":141843.0,"Position":448.0,"HyperDash":false}]},{"StartTime":142208.0,"Objects":[{"StartTime":142208.0,"Position":244.0,"HyperDash":false}]},{"StartTime":142390.0,"Objects":[{"StartTime":142390.0,"Position":348.0,"HyperDash":false}]},{"StartTime":142571.0,"Objects":[{"StartTime":142571.0,"Position":448.0,"HyperDash":false}]},{"StartTime":142935.0,"Objects":[{"StartTime":142935.0,"Position":152.0,"HyperDash":false},{"StartTime":143025.0,"Position":137.0,"HyperDash":false},{"StartTime":143116.0,"Position":152.0,"HyperDash":false},{"StartTime":143189.0,"Position":168.0,"HyperDash":false},{"StartTime":143298.0,"Position":152.0,"HyperDash":false}]},{"StartTime":143480.0,"Objects":[{"StartTime":143480.0,"Position":236.0,"HyperDash":false}]},{"StartTime":143662.0,"Objects":[{"StartTime":143662.0,"Position":144.0,"HyperDash":false},{"StartTime":143752.0,"Position":93.85124,"HyperDash":false},{"StartTime":143843.0,"Position":59.0,"HyperDash":false},{"StartTime":143916.0,"Position":76.95316,"HyperDash":false},{"StartTime":144025.0,"Position":144.0,"HyperDash":false}]},{"StartTime":144390.0,"Objects":[{"StartTime":144390.0,"Position":316.0,"HyperDash":false}]},{"StartTime":144571.0,"Objects":[{"StartTime":144571.0,"Position":232.0,"HyperDash":false}]},{"StartTime":144753.0,"Objects":[{"StartTime":144753.0,"Position":148.0,"HyperDash":false}]},{"StartTime":145117.0,"Objects":[{"StartTime":145117.0,"Position":316.0,"HyperDash":false},{"StartTime":145207.0,"Position":275.851257,"HyperDash":false},{"StartTime":145298.0,"Position":231.0,"HyperDash":false},{"StartTime":145371.0,"Position":279.953156,"HyperDash":false},{"StartTime":145480.0,"Position":316.0,"HyperDash":false}]},{"StartTime":145844.0,"Objects":[{"StartTime":145844.0,"Position":144.0,"HyperDash":false},{"StartTime":145916.0,"Position":147.0,"HyperDash":false},{"StartTime":146025.0,"Position":144.0,"HyperDash":false}]},{"StartTime":146208.0,"Objects":[{"StartTime":146208.0,"Position":228.0,"HyperDash":false}]},{"StartTime":146571.0,"Objects":[{"StartTime":146571.0,"Position":59.0,"HyperDash":false},{"StartTime":146661.0,"Position":108.148758,"HyperDash":false},{"StartTime":146752.0,"Position":144.0,"HyperDash":false},{"StartTime":146825.0,"Position":95.04684,"HyperDash":false},{"StartTime":146934.0,"Position":59.0,"HyperDash":false}]},{"StartTime":147299.0,"Objects":[{"StartTime":147299.0,"Position":228.0,"HyperDash":false},{"StartTime":147371.0,"Position":264.812164,"HyperDash":false},{"StartTime":147480.0,"Position":313.0,"HyperDash":false}]},{"StartTime":147662.0,"Objects":[{"StartTime":147662.0,"Position":220.0,"HyperDash":false},{"StartTime":147734.0,"Position":215.0,"HyperDash":false},{"StartTime":147843.0,"Position":220.0,"HyperDash":false}]},{"StartTime":148026.0,"Objects":[{"StartTime":148026.0,"Position":313.0,"HyperDash":false},{"StartTime":148098.0,"Position":313.0,"HyperDash":false},{"StartTime":148207.0,"Position":313.0,"HyperDash":false}]},{"StartTime":148390.0,"Objects":[{"StartTime":148390.0,"Position":228.0,"HyperDash":false}]},{"StartTime":148571.0,"Objects":[{"StartTime":148571.0,"Position":320.0,"HyperDash":false}]},{"StartTime":148753.0,"Objects":[{"StartTime":148753.0,"Position":64.0,"HyperDash":false},{"StartTime":148825.0,"Position":82.0,"HyperDash":false},{"StartTime":148934.0,"Position":64.0,"HyperDash":false}]},{"StartTime":149117.0,"Objects":[{"StartTime":149117.0,"Position":152.0,"HyperDash":false},{"StartTime":149189.0,"Position":148.0,"HyperDash":false},{"StartTime":149298.0,"Position":152.0,"HyperDash":false}]},{"StartTime":149480.0,"Objects":[{"StartTime":149480.0,"Position":328.0,"HyperDash":false}]},{"StartTime":149844.0,"Objects":[{"StartTime":149844.0,"Position":184.0,"HyperDash":false},{"StartTime":149916.0,"Position":215.812149,"HyperDash":false},{"StartTime":150025.0,"Position":269.0,"HyperDash":false}]},{"StartTime":150208.0,"Objects":[{"StartTime":150208.0,"Position":356.0,"HyperDash":false}]},{"StartTime":150571.0,"Objects":[{"StartTime":150571.0,"Position":204.0,"HyperDash":false},{"StartTime":150643.0,"Position":221.0,"HyperDash":false},{"StartTime":150752.0,"Position":204.0,"HyperDash":false}]},{"StartTime":150935.0,"Objects":[{"StartTime":150935.0,"Position":28.0,"HyperDash":false}]},{"StartTime":151299.0,"Objects":[{"StartTime":151299.0,"Position":172.0,"HyperDash":false},{"StartTime":151371.0,"Position":221.812149,"HyperDash":false},{"StartTime":151480.0,"Position":257.0,"HyperDash":false}]},{"StartTime":151662.0,"Objects":[{"StartTime":151662.0,"Position":164.0,"HyperDash":false},{"StartTime":151734.0,"Position":168.0,"HyperDash":false},{"StartTime":151843.0,"Position":164.0,"HyperDash":false}]},{"StartTime":152026.0,"Objects":[{"StartTime":152026.0,"Position":257.0,"HyperDash":false},{"StartTime":152098.0,"Position":274.0,"HyperDash":false},{"StartTime":152207.0,"Position":257.0,"HyperDash":false}]},{"StartTime":152390.0,"Objects":[{"StartTime":152390.0,"Position":432.0,"HyperDash":false}]},{"StartTime":152753.0,"Objects":[{"StartTime":152753.0,"Position":288.0,"HyperDash":false},{"StartTime":152825.0,"Position":271.187866,"HyperDash":false},{"StartTime":152934.0,"Position":203.0,"HyperDash":false}]},{"StartTime":153117.0,"Objects":[{"StartTime":153117.0,"Position":380.0,"HyperDash":false},{"StartTime":153189.0,"Position":381.0,"HyperDash":false},{"StartTime":153298.0,"Position":380.0,"HyperDash":false}]},{"StartTime":153480.0,"Objects":[{"StartTime":153480.0,"Position":288.0,"HyperDash":false},{"StartTime":153552.0,"Position":301.0,"HyperDash":false},{"StartTime":153661.0,"Position":288.0,"HyperDash":false}]},{"StartTime":153844.0,"Objects":[{"StartTime":153844.0,"Position":112.0,"HyperDash":false},{"StartTime":153916.0,"Position":121.0,"HyperDash":false},{"StartTime":154025.0,"Position":112.0,"HyperDash":false}]},{"StartTime":154208.0,"Objects":[{"StartTime":154208.0,"Position":203.0,"HyperDash":false},{"StartTime":154280.0,"Position":235.812149,"HyperDash":false},{"StartTime":154389.0,"Position":288.0,"HyperDash":false}]},{"StartTime":154571.0,"Objects":[{"StartTime":154571.0,"Position":32.0,"HyperDash":false},{"StartTime":154661.0,"Position":45.0,"HyperDash":false},{"StartTime":154752.0,"Position":32.0,"HyperDash":false},{"StartTime":154825.0,"Position":23.0,"HyperDash":false},{"StartTime":154934.0,"Position":32.0,"HyperDash":false}]},{"StartTime":155299.0,"Objects":[{"StartTime":155299.0,"Position":216.0,"HyperDash":false}]},{"StartTime":155480.0,"Objects":[{"StartTime":155480.0,"Position":124.0,"HyperDash":false}]},{"StartTime":155662.0,"Objects":[{"StartTime":155662.0,"Position":32.0,"HyperDash":false}]},{"StartTime":156026.0,"Objects":[{"StartTime":156026.0,"Position":216.0,"HyperDash":false},{"StartTime":156098.0,"Position":237.803421,"HyperDash":false},{"StartTime":156207.0,"Position":300.978058,"HyperDash":false}]},{"StartTime":156390.0,"Objects":[{"StartTime":156390.0,"Position":300.0,"HyperDash":false}]},{"StartTime":156753.0,"Objects":[{"StartTime":156753.0,"Position":132.0,"HyperDash":false},{"StartTime":156843.0,"Position":176.148758,"HyperDash":false},{"StartTime":156934.0,"Position":217.0,"HyperDash":false},{"StartTime":157007.0,"Position":167.046844,"HyperDash":false},{"StartTime":157116.0,"Position":132.0,"HyperDash":false}]},{"StartTime":157299.0,"Objects":[{"StartTime":157299.0,"Position":48.0,"HyperDash":false}]},{"StartTime":157480.0,"Objects":[{"StartTime":157480.0,"Position":140.0,"HyperDash":false},{"StartTime":157552.0,"Position":145.0,"HyperDash":false},{"StartTime":157661.0,"Position":140.0,"HyperDash":false}]},{"StartTime":157844.0,"Objects":[{"StartTime":157844.0,"Position":236.0,"HyperDash":false},{"StartTime":157916.0,"Position":252.0,"HyperDash":false},{"StartTime":158025.0,"Position":236.0,"HyperDash":false}]},{"StartTime":158208.0,"Objects":[{"StartTime":158208.0,"Position":412.0,"HyperDash":false},{"StartTime":158298.0,"Position":434.148743,"HyperDash":false},{"StartTime":158389.0,"Position":497.0,"HyperDash":false},{"StartTime":158462.0,"Position":464.046844,"HyperDash":false},{"StartTime":158571.0,"Position":412.0,"HyperDash":false}]},{"StartTime":158935.0,"Objects":[{"StartTime":158935.0,"Position":268.0,"HyperDash":false}]},{"StartTime":159117.0,"Objects":[{"StartTime":159117.0,"Position":344.0,"HyperDash":false}]},{"StartTime":159299.0,"Objects":[{"StartTime":159299.0,"Position":420.0,"HyperDash":false}]},{"StartTime":159480.0,"Objects":[{"StartTime":159480.0,"Position":496.0,"HyperDash":false}]},{"StartTime":159662.0,"Objects":[{"StartTime":159662.0,"Position":412.0,"HyperDash":false},{"StartTime":159734.0,"Position":448.812164,"HyperDash":false},{"StartTime":159843.0,"Position":497.0,"HyperDash":false}]},{"StartTime":160026.0,"Objects":[{"StartTime":160026.0,"Position":324.0,"HyperDash":false},{"StartTime":160098.0,"Position":341.0,"HyperDash":false},{"StartTime":160207.0,"Position":324.0,"HyperDash":false}]},{"StartTime":160390.0,"Objects":[{"StartTime":160390.0,"Position":68.0,"HyperDash":false},{"StartTime":160462.0,"Position":75.0,"HyperDash":false},{"StartTime":160571.0,"Position":68.0,"HyperDash":false}]},{"StartTime":160753.0,"Objects":[{"StartTime":160753.0,"Position":152.0,"HyperDash":false},{"StartTime":160825.0,"Position":187.812149,"HyperDash":false},{"StartTime":160934.0,"Position":237.0,"HyperDash":false}]},{"StartTime":161117.0,"Objects":[{"StartTime":161117.0,"Position":409.0,"HyperDash":false},{"StartTime":161189.0,"Position":409.0,"HyperDash":false},{"StartTime":161298.0,"Position":409.0,"HyperDash":false}]},{"StartTime":161480.0,"Objects":[{"StartTime":161480.0,"Position":324.0,"HyperDash":false},{"StartTime":161552.0,"Position":355.812164,"HyperDash":false},{"StartTime":161661.0,"Position":409.0,"HyperDash":false}]},{"StartTime":161844.0,"Objects":[{"StartTime":161844.0,"Position":313.0,"HyperDash":false},{"StartTime":161916.0,"Position":320.0,"HyperDash":false},{"StartTime":162025.0,"Position":313.0,"HyperDash":false}]},{"StartTime":162208.0,"Objects":[{"StartTime":162208.0,"Position":140.0,"HyperDash":false},{"StartTime":162280.0,"Position":128.0,"HyperDash":false},{"StartTime":162389.0,"Position":140.0,"HyperDash":false}]},{"StartTime":162480.0,"Objects":[{"StartTime":162480.0,"Position":184.0,"HyperDash":false}]},{"StartTime":162571.0,"Objects":[{"StartTime":162571.0,"Position":228.0,"HyperDash":false},{"StartTime":162643.0,"Position":255.812164,"HyperDash":false},{"StartTime":162752.0,"Position":313.0,"HyperDash":false}]},{"StartTime":162935.0,"Objects":[{"StartTime":162935.0,"Position":400.0,"HyperDash":false},{"StartTime":163007.0,"Position":417.0,"HyperDash":false},{"StartTime":163116.0,"Position":400.0,"HyperDash":false}]},{"StartTime":163299.0,"Objects":[{"StartTime":163299.0,"Position":217.0,"HyperDash":false},{"StartTime":163367.0,"Position":455.0,"HyperDash":false},{"StartTime":163435.0,"Position":229.0,"HyperDash":false},{"StartTime":163503.0,"Position":51.0,"HyperDash":false},{"StartTime":163571.0,"Position":199.0,"HyperDash":false},{"StartTime":163639.0,"Position":208.0,"HyperDash":false},{"StartTime":163707.0,"Position":173.0,"HyperDash":false},{"StartTime":163775.0,"Position":367.0,"HyperDash":false},{"StartTime":163844.0,"Position":193.0,"HyperDash":false},{"StartTime":163912.0,"Position":488.0,"HyperDash":false},{"StartTime":163980.0,"Position":314.0,"HyperDash":false},{"StartTime":164048.0,"Position":135.0,"HyperDash":false},{"StartTime":164116.0,"Position":399.0,"HyperDash":false},{"StartTime":164184.0,"Position":404.0,"HyperDash":false},{"StartTime":164252.0,"Position":152.0,"HyperDash":false},{"StartTime":164320.0,"Position":353.0,"HyperDash":false},{"StartTime":164389.0,"Position":358.0,"HyperDash":false}]},{"StartTime":164753.0,"Objects":[{"StartTime":164753.0,"Position":132.0,"HyperDash":false},{"StartTime":164843.0,"Position":132.0,"HyperDash":false},{"StartTime":164934.0,"Position":132.0,"HyperDash":false}]},{"StartTime":165117.0,"Objects":[{"StartTime":165117.0,"Position":304.0,"HyperDash":false}]},{"StartTime":165207.0,"Objects":[{"StartTime":165207.0,"Position":352.0,"HyperDash":false}]},{"StartTime":165298.0,"Objects":[{"StartTime":165298.0,"Position":372.0,"HyperDash":false}]},{"StartTime":165389.0,"Objects":[{"StartTime":165389.0,"Position":351.0,"HyperDash":false}]},{"StartTime":165480.0,"Objects":[{"StartTime":165480.0,"Position":303.0,"HyperDash":false}]},{"StartTime":165662.0,"Objects":[{"StartTime":165662.0,"Position":208.0,"HyperDash":false}]},{"StartTime":165844.0,"Objects":[{"StartTime":165844.0,"Position":388.0,"HyperDash":false},{"StartTime":165916.0,"Position":435.812164,"HyperDash":false},{"StartTime":166025.0,"Position":473.0,"HyperDash":false}]},{"StartTime":166208.0,"Objects":[{"StartTime":166208.0,"Position":216.0,"HyperDash":false},{"StartTime":166298.0,"Position":158.851242,"HyperDash":false},{"StartTime":166389.0,"Position":131.0,"HyperDash":false},{"StartTime":166462.0,"Position":155.953156,"HyperDash":false},{"StartTime":166571.0,"Position":216.0,"HyperDash":false}]},{"StartTime":166753.0,"Objects":[{"StartTime":166753.0,"Position":308.0,"HyperDash":false},{"StartTime":166843.0,"Position":274.851257,"HyperDash":false},{"StartTime":166934.0,"Position":223.234161,"HyperDash":false},{"StartTime":167007.0,"Position":206.046844,"HyperDash":false},{"StartTime":167116.0,"Position":138.0,"HyperDash":false}]},{"StartTime":167299.0,"Objects":[{"StartTime":167299.0,"Position":312.0,"HyperDash":false},{"StartTime":167371.0,"Position":305.0,"HyperDash":false},{"StartTime":167480.0,"Position":312.0,"HyperDash":false}]},{"StartTime":167662.0,"Objects":[{"StartTime":167662.0,"Position":138.0,"HyperDash":false},{"StartTime":167752.0,"Position":192.148758,"HyperDash":false},{"StartTime":167843.0,"Position":222.765839,"HyperDash":false},{"StartTime":167916.0,"Position":254.953156,"HyperDash":false},{"StartTime":168025.0,"Position":308.0,"HyperDash":false}]},{"StartTime":168208.0,"Objects":[{"StartTime":168208.0,"Position":404.0,"HyperDash":false},{"StartTime":168298.0,"Position":395.0,"HyperDash":false},{"StartTime":168389.0,"Position":403.234161,"HyperDash":false},{"StartTime":168462.0,"Position":382.046844,"HyperDash":false},{"StartTime":168571.0,"Position":318.0,"HyperDash":false}]},{"StartTime":168753.0,"Objects":[{"StartTime":168753.0,"Position":140.0,"HyperDash":false},{"StartTime":168825.0,"Position":131.0,"HyperDash":false},{"StartTime":168934.0,"Position":140.0,"HyperDash":false}]},{"StartTime":169117.0,"Objects":[{"StartTime":169117.0,"Position":320.0,"HyperDash":false},{"StartTime":169207.0,"Position":375.148743,"HyperDash":false},{"StartTime":169298.0,"Position":404.0,"HyperDash":false},{"StartTime":169371.0,"Position":419.0,"HyperDash":false},{"StartTime":169480.0,"Position":404.0,"HyperDash":false}]},{"StartTime":169662.0,"Objects":[{"StartTime":169662.0,"Position":232.0,"HyperDash":false},{"StartTime":169752.0,"Position":176.851242,"HyperDash":false},{"StartTime":169843.0,"Position":147.234161,"HyperDash":false},{"StartTime":169916.0,"Position":100.046837,"HyperDash":false},{"StartTime":170025.0,"Position":62.0,"HyperDash":false}]},{"StartTime":170208.0,"Objects":[{"StartTime":170208.0,"Position":232.0,"HyperDash":false},{"StartTime":170280.0,"Position":203.187851,"HyperDash":false},{"StartTime":170389.0,"Position":147.0,"HyperDash":false}]},{"StartTime":170571.0,"Objects":[{"StartTime":170571.0,"Position":52.0,"HyperDash":false},{"StartTime":170661.0,"Position":52.0,"HyperDash":false}]},{"StartTime":170753.0,"Objects":[{"StartTime":170753.0,"Position":100.0,"HyperDash":false}]},{"StartTime":170935.0,"Objects":[{"StartTime":170935.0,"Position":192.0,"HyperDash":false}]},{"StartTime":171117.0,"Objects":[{"StartTime":171117.0,"Position":448.0,"HyperDash":false},{"StartTime":171189.0,"Position":432.0,"HyperDash":false},{"StartTime":171298.0,"Position":448.0,"HyperDash":false}]},{"StartTime":171480.0,"Objects":[{"StartTime":171480.0,"Position":356.0,"HyperDash":false}]},{"StartTime":171662.0,"Objects":[{"StartTime":171662.0,"Position":184.0,"HyperDash":false},{"StartTime":171734.0,"Position":202.812149,"HyperDash":false},{"StartTime":171843.0,"Position":269.0,"HyperDash":false}]},{"StartTime":172026.0,"Objects":[{"StartTime":172026.0,"Position":20.0,"HyperDash":false},{"StartTime":172116.0,"Position":20.0,"HyperDash":false},{"StartTime":172207.0,"Position":20.0,"HyperDash":false}]},{"StartTime":172390.0,"Objects":[{"StartTime":172390.0,"Position":116.0,"HyperDash":false}]},{"StartTime":172571.0,"Objects":[{"StartTime":172571.0,"Position":32.0,"HyperDash":false}]},{"StartTime":172753.0,"Objects":[{"StartTime":172753.0,"Position":208.0,"HyperDash":false},{"StartTime":172825.0,"Position":252.812149,"HyperDash":false},{"StartTime":172934.0,"Position":293.0,"HyperDash":false}]},{"StartTime":173117.0,"Objects":[{"StartTime":173117.0,"Position":200.0,"HyperDash":false},{"StartTime":173189.0,"Position":212.0,"HyperDash":false},{"StartTime":173298.0,"Position":200.0,"HyperDash":false}]},{"StartTime":173480.0,"Objects":[{"StartTime":173480.0,"Position":376.0,"HyperDash":false},{"StartTime":173552.0,"Position":379.0,"HyperDash":false},{"StartTime":173661.0,"Position":376.0,"HyperDash":false}]},{"StartTime":173844.0,"Objects":[{"StartTime":173844.0,"Position":200.0,"HyperDash":false}]},{"StartTime":174026.0,"Objects":[{"StartTime":174026.0,"Position":116.0,"HyperDash":false},{"StartTime":174116.0,"Position":76.2682648,"HyperDash":false},{"StartTime":174207.0,"Position":64.10713,"HyperDash":false},{"StartTime":174280.0,"Position":75.55404,"HyperDash":false},{"StartTime":174389.0,"Position":115.499283,"HyperDash":false}]},{"StartTime":174571.0,"Objects":[{"StartTime":174571.0,"Position":372.0,"HyperDash":false},{"StartTime":174643.0,"Position":412.812164,"HyperDash":false},{"StartTime":174752.0,"Position":457.0,"HyperDash":false}]},{"StartTime":174935.0,"Objects":[{"StartTime":174935.0,"Position":280.0,"HyperDash":false},{"StartTime":175007.0,"Position":297.0,"HyperDash":false},{"StartTime":175116.0,"Position":280.0,"HyperDash":false}]},{"StartTime":175299.0,"Objects":[{"StartTime":175299.0,"Position":368.0,"HyperDash":false}]},{"StartTime":175480.0,"Objects":[{"StartTime":175480.0,"Position":192.0,"HyperDash":false},{"StartTime":175552.0,"Position":197.0,"HyperDash":false},{"StartTime":175661.0,"Position":192.0,"HyperDash":false}]},{"StartTime":175844.0,"Objects":[{"StartTime":175844.0,"Position":280.0,"HyperDash":false}]},{"StartTime":176026.0,"Objects":[{"StartTime":176026.0,"Position":453.0,"HyperDash":false},{"StartTime":176098.0,"Position":425.187836,"HyperDash":false},{"StartTime":176207.0,"Position":368.0,"HyperDash":false}]},{"StartTime":176390.0,"Objects":[{"StartTime":176390.0,"Position":112.0,"HyperDash":false},{"StartTime":176480.0,"Position":69.85124,"HyperDash":false},{"StartTime":176571.0,"Position":27.0,"HyperDash":false},{"StartTime":176644.0,"Position":44.9531631,"HyperDash":false},{"StartTime":176753.0,"Position":112.0,"HyperDash":false}]},{"StartTime":176935.0,"Objects":[{"StartTime":176935.0,"Position":292.0,"HyperDash":false},{"StartTime":177025.0,"Position":231.851242,"HyperDash":false},{"StartTime":177116.0,"Position":207.234161,"HyperDash":false},{"StartTime":177189.0,"Position":180.046844,"HyperDash":false},{"StartTime":177298.0,"Position":122.0,"HyperDash":false}]},{"StartTime":177480.0,"Objects":[{"StartTime":177480.0,"Position":304.0,"HyperDash":false},{"StartTime":177552.0,"Position":349.812164,"HyperDash":false},{"StartTime":177661.0,"Position":389.0,"HyperDash":false}]},{"StartTime":177844.0,"Objects":[{"StartTime":177844.0,"Position":132.0,"HyperDash":false},{"StartTime":177934.0,"Position":67.42149,"HyperDash":false},{"StartTime":178025.0,"Position":32.0,"HyperDash":false},{"StartTime":178098.0,"Position":18.0,"HyperDash":false},{"StartTime":178207.0,"Position":32.0,"HyperDash":false}]},{"StartTime":178390.0,"Objects":[{"StartTime":178390.0,"Position":208.0,"HyperDash":false},{"StartTime":178480.0,"Position":249.148758,"HyperDash":false},{"StartTime":178571.0,"Position":292.765839,"HyperDash":false},{"StartTime":178644.0,"Position":311.953156,"HyperDash":false},{"StartTime":178753.0,"Position":378.0,"HyperDash":false}]},{"StartTime":178935.0,"Objects":[{"StartTime":178935.0,"Position":284.0,"HyperDash":false},{"StartTime":179007.0,"Position":301.0,"HyperDash":false},{"StartTime":179116.0,"Position":284.0,"HyperDash":false}]},{"StartTime":179299.0,"Objects":[{"StartTime":179299.0,"Position":464.0,"HyperDash":false},{"StartTime":179371.0,"Position":479.0,"HyperDash":false},{"StartTime":179480.0,"Position":464.0,"HyperDash":false}]},{"StartTime":179662.0,"Objects":[{"StartTime":179662.0,"Position":380.0,"HyperDash":false}]},{"StartTime":179844.0,"Objects":[{"StartTime":179844.0,"Position":204.0,"HyperDash":false},{"StartTime":179934.0,"Position":249.148758,"HyperDash":false},{"StartTime":180025.0,"Position":288.765839,"HyperDash":false},{"StartTime":180098.0,"Position":306.953156,"HyperDash":false},{"StartTime":180207.0,"Position":374.0,"HyperDash":false}]},{"StartTime":180390.0,"Objects":[{"StartTime":180390.0,"Position":460.0,"HyperDash":false},{"StartTime":180462.0,"Position":450.0,"HyperDash":false},{"StartTime":180571.0,"Position":460.0,"HyperDash":false}]},{"StartTime":180753.0,"Objects":[{"StartTime":180753.0,"Position":284.0,"HyperDash":false},{"StartTime":180843.0,"Position":257.851257,"HyperDash":false},{"StartTime":180934.0,"Position":200.0,"HyperDash":false},{"StartTime":181007.0,"Position":192.0,"HyperDash":false},{"StartTime":181116.0,"Position":200.0,"HyperDash":false}]},{"StartTime":181299.0,"Objects":[{"StartTime":181299.0,"Position":380.0,"HyperDash":false},{"StartTime":181389.0,"Position":345.851257,"HyperDash":false},{"StartTime":181480.0,"Position":295.234161,"HyperDash":false},{"StartTime":181553.0,"Position":258.046844,"HyperDash":false},{"StartTime":181662.0,"Position":210.0,"HyperDash":false}]},{"StartTime":181844.0,"Objects":[{"StartTime":181844.0,"Position":302.0,"HyperDash":false},{"StartTime":181916.0,"Position":255.187836,"HyperDash":false},{"StartTime":182025.0,"Position":217.0,"HyperDash":false}]},{"StartTime":182208.0,"Objects":[{"StartTime":182208.0,"Position":124.0,"HyperDash":false},{"StartTime":182280.0,"Position":131.0,"HyperDash":false},{"StartTime":182389.0,"Position":124.0,"HyperDash":false}]},{"StartTime":182571.0,"Objects":[{"StartTime":182571.0,"Position":302.0,"HyperDash":false},{"StartTime":182643.0,"Position":248.187836,"HyperDash":false},{"StartTime":182752.0,"Position":217.0,"HyperDash":false}]},{"StartTime":182935.0,"Objects":[{"StartTime":182935.0,"Position":312.0,"HyperDash":false},{"StartTime":183025.0,"Position":354.5,"HyperDash":false},{"StartTime":183116.0,"Position":312.0,"HyperDash":false}]},{"StartTime":183299.0,"Objects":[{"StartTime":183299.0,"Position":132.0,"HyperDash":false},{"StartTime":183371.0,"Position":80.18785,"HyperDash":false},{"StartTime":183480.0,"Position":47.0,"HyperDash":true}]},{"StartTime":183662.0,"Objects":[{"StartTime":183662.0,"Position":312.0,"HyperDash":false},{"StartTime":183752.0,"Position":350.73175,"HyperDash":false},{"StartTime":183843.0,"Position":363.892883,"HyperDash":false},{"StartTime":183916.0,"Position":353.445984,"HyperDash":false},{"StartTime":184025.0,"Position":312.500732,"HyperDash":false}]},{"StartTime":184208.0,"Objects":[{"StartTime":184208.0,"Position":220.0,"HyperDash":false}]},{"StartTime":184390.0,"Objects":[{"StartTime":184390.0,"Position":324.0,"HyperDash":false},{"StartTime":184462.0,"Position":310.0,"HyperDash":false},{"StartTime":184571.0,"Position":324.0,"HyperDash":false}]},{"StartTime":184753.0,"Objects":[{"StartTime":184753.0,"Position":144.0,"HyperDash":false},{"StartTime":184825.0,"Position":142.0,"HyperDash":false},{"StartTime":184934.0,"Position":144.0,"HyperDash":false}]},{"StartTime":185117.0,"Objects":[{"StartTime":185117.0,"Position":324.0,"HyperDash":false},{"StartTime":185189.0,"Position":348.812164,"HyperDash":false},{"StartTime":185298.0,"Position":409.0,"HyperDash":false}]},{"StartTime":185480.0,"Objects":[{"StartTime":185480.0,"Position":232.0,"HyperDash":false},{"StartTime":185552.0,"Position":224.0,"HyperDash":false},{"StartTime":185661.0,"Position":232.0,"HyperDash":false}]},{"StartTime":185844.0,"Objects":[{"StartTime":185844.0,"Position":316.0,"HyperDash":false}]},{"StartTime":186026.0,"Objects":[{"StartTime":186026.0,"Position":232.0,"HyperDash":false}]},{"StartTime":186208.0,"Objects":[{"StartTime":186208.0,"Position":408.0,"HyperDash":false},{"StartTime":186280.0,"Position":427.0,"HyperDash":false},{"StartTime":186389.0,"Position":408.0,"HyperDash":false}]},{"StartTime":186571.0,"Objects":[{"StartTime":186571.0,"Position":152.0,"HyperDash":false},{"StartTime":186661.0,"Position":106.851242,"HyperDash":false},{"StartTime":186752.0,"Position":68.76584,"HyperDash":false},{"StartTime":186825.0,"Position":87.95316,"HyperDash":false},{"StartTime":186934.0,"Position":154.0,"HyperDash":false}]},{"StartTime":187117.0,"Objects":[{"StartTime":187117.0,"Position":332.0,"HyperDash":false},{"StartTime":187207.0,"Position":276.851257,"HyperDash":false},{"StartTime":187298.0,"Position":247.234161,"HyperDash":false},{"StartTime":187371.0,"Position":205.046844,"HyperDash":false},{"StartTime":187480.0,"Position":162.0,"HyperDash":false}]},{"StartTime":187662.0,"Objects":[{"StartTime":187662.0,"Position":76.0,"HyperDash":false},{"StartTime":187734.0,"Position":74.0,"HyperDash":false},{"StartTime":187843.0,"Position":76.0,"HyperDash":false}]},{"StartTime":188026.0,"Objects":[{"StartTime":188026.0,"Position":252.0,"HyperDash":false}]},{"StartTime":188116.0,"Objects":[{"StartTime":188116.0,"Position":294.0,"HyperDash":false}]},{"StartTime":188207.0,"Objects":[{"StartTime":188207.0,"Position":337.0,"HyperDash":false}]},{"StartTime":188390.0,"Objects":[{"StartTime":188390.0,"Position":176.0,"HyperDash":false}]},{"StartTime":188571.0,"Objects":[{"StartTime":188571.0,"Position":344.0,"HyperDash":false},{"StartTime":188661.0,"Position":370.214264,"HyperDash":false},{"StartTime":188752.0,"Position":396.42868,"HyperDash":false},{"StartTime":188825.0,"Position":403.238831,"HyperDash":false},{"StartTime":188934.0,"Position":343.061737,"HyperDash":false}]},{"StartTime":189117.0,"Objects":[{"StartTime":189117.0,"Position":168.0,"HyperDash":false},{"StartTime":189189.0,"Position":133.187851,"HyperDash":false},{"StartTime":189298.0,"Position":83.0,"HyperDash":true}]},{"StartTime":189480.0,"Objects":[{"StartTime":189480.0,"Position":344.0,"HyperDash":false},{"StartTime":189570.0,"Position":378.578522,"HyperDash":false},{"StartTime":189661.0,"Position":445.719,"HyperDash":false},{"StartTime":189734.0,"Position":443.0,"HyperDash":false},{"StartTime":189843.0,"Position":448.0,"HyperDash":false}]},{"StartTime":190026.0,"Objects":[{"StartTime":190026.0,"Position":352.0,"HyperDash":false},{"StartTime":190116.0,"Position":300.851257,"HyperDash":false},{"StartTime":190207.0,"Position":267.234161,"HyperDash":false},{"StartTime":190280.0,"Position":224.046844,"HyperDash":false},{"StartTime":190389.0,"Position":182.0,"HyperDash":false}]},{"StartTime":190571.0,"Objects":[{"StartTime":190571.0,"Position":276.0,"HyperDash":false},{"StartTime":190643.0,"Position":262.0,"HyperDash":false},{"StartTime":190752.0,"Position":276.0,"HyperDash":false}]},{"StartTime":190935.0,"Objects":[{"StartTime":190935.0,"Position":96.0,"HyperDash":false},{"StartTime":191007.0,"Position":114.0,"HyperDash":false},{"StartTime":191116.0,"Position":96.0,"HyperDash":false}]},{"StartTime":191299.0,"Objects":[{"StartTime":191299.0,"Position":192.0,"HyperDash":false},{"StartTime":191371.0,"Position":154.187851,"HyperDash":false},{"StartTime":191480.0,"Position":107.0,"HyperDash":false}]},{"StartTime":191662.0,"Objects":[{"StartTime":191662.0,"Position":284.0,"HyperDash":false},{"StartTime":191734.0,"Position":328.812164,"HyperDash":false},{"StartTime":191843.0,"Position":369.0,"HyperDash":false}]},{"StartTime":192026.0,"Objects":[{"StartTime":192026.0,"Position":464.0,"HyperDash":false},{"StartTime":192116.0,"Position":464.0,"HyperDash":false}]},{"StartTime":192208.0,"Objects":[{"StartTime":192208.0,"Position":420.0,"HyperDash":false}]},{"StartTime":192390.0,"Objects":[{"StartTime":192390.0,"Position":240.0,"HyperDash":false},{"StartTime":192480.0,"Position":193.851242,"HyperDash":false},{"StartTime":192571.0,"Position":155.234161,"HyperDash":false},{"StartTime":192644.0,"Position":139.046844,"HyperDash":false},{"StartTime":192753.0,"Position":70.0,"HyperDash":false}]},{"StartTime":192935.0,"Objects":[{"StartTime":192935.0,"Position":156.0,"HyperDash":false}]},{"StartTime":193117.0,"Objects":[{"StartTime":193117.0,"Position":64.0,"HyperDash":false},{"StartTime":193189.0,"Position":49.0,"HyperDash":false},{"StartTime":193298.0,"Position":64.0,"HyperDash":false}]},{"StartTime":193480.0,"Objects":[{"StartTime":193480.0,"Position":156.0,"HyperDash":false},{"StartTime":193552.0,"Position":173.0,"HyperDash":false},{"StartTime":193661.0,"Position":156.0,"HyperDash":false}]},{"StartTime":193844.0,"Objects":[{"StartTime":193844.0,"Position":332.0,"HyperDash":false},{"StartTime":193934.0,"Position":374.5,"HyperDash":false},{"StartTime":194025.0,"Position":332.0,"HyperDash":false}]},{"StartTime":194208.0,"Objects":[{"StartTime":194208.0,"Position":156.0,"HyperDash":false},{"StartTime":194280.0,"Position":194.812149,"HyperDash":false},{"StartTime":194389.0,"Position":241.0,"HyperDash":false}]},{"StartTime":194571.0,"Objects":[{"StartTime":194571.0,"Position":328.0,"HyperDash":false}]},{"StartTime":194753.0,"Objects":[{"StartTime":194753.0,"Position":236.0,"HyperDash":false}]},{"StartTime":194935.0,"Objects":[{"StartTime":194935.0,"Position":416.0,"HyperDash":false},{"StartTime":195007.0,"Position":430.0,"HyperDash":false},{"StartTime":195116.0,"Position":416.0,"HyperDash":false}]},{"StartTime":195299.0,"Objects":[{"StartTime":195299.0,"Position":160.0,"HyperDash":false},{"StartTime":195389.0,"Position":112.851242,"HyperDash":false},{"StartTime":195480.0,"Position":76.0,"HyperDash":false},{"StartTime":195553.0,"Position":72.0,"HyperDash":false},{"StartTime":195662.0,"Position":76.0,"HyperDash":false}]},{"StartTime":195844.0,"Objects":[{"StartTime":195844.0,"Position":164.0,"HyperDash":false},{"StartTime":195934.0,"Position":224.148758,"HyperDash":false},{"StartTime":196025.0,"Position":248.765839,"HyperDash":false},{"StartTime":196098.0,"Position":284.953156,"HyperDash":false},{"StartTime":196207.0,"Position":334.0,"HyperDash":false}]},{"StartTime":196389.0,"Objects":[{"StartTime":196389.0,"Position":240.0,"HyperDash":false},{"StartTime":196461.0,"Position":232.0,"HyperDash":false},{"StartTime":196570.0,"Position":240.0,"HyperDash":false}]},{"StartTime":196753.0,"Objects":[{"StartTime":196753.0,"Position":420.0,"HyperDash":false},{"StartTime":196825.0,"Position":435.0,"HyperDash":false},{"StartTime":196934.0,"Position":420.0,"HyperDash":false}]},{"StartTime":197026.0,"Objects":[{"StartTime":197026.0,"Position":372.0,"HyperDash":false}]},{"StartTime":197117.0,"Objects":[{"StartTime":197117.0,"Position":324.0,"HyperDash":false},{"StartTime":197189.0,"Position":282.187836,"HyperDash":false},{"StartTime":197298.0,"Position":239.0,"HyperDash":false}]},{"StartTime":197480.0,"Objects":[{"StartTime":197480.0,"Position":332.0,"HyperDash":false},{"StartTime":197552.0,"Position":346.0,"HyperDash":false},{"StartTime":197661.0,"Position":332.0,"HyperDash":false}]},{"StartTime":197844.0,"Objects":[{"StartTime":197844.0,"Position":152.0,"HyperDash":false},{"StartTime":197934.0,"Position":109.5,"HyperDash":false},{"StartTime":198025.0,"Position":152.0,"HyperDash":false}]},{"StartTime":198208.0,"Objects":[{"StartTime":198208.0,"Position":328.0,"HyperDash":false},{"StartTime":198298.0,"Position":387.148743,"HyperDash":false},{"StartTime":198389.0,"Position":412.765839,"HyperDash":false},{"StartTime":198462.0,"Position":458.953156,"HyperDash":false},{"StartTime":198571.0,"Position":498.0,"HyperDash":false}]},{"StartTime":198753.0,"Objects":[{"StartTime":198753.0,"Position":412.0,"HyperDash":false}]},{"StartTime":198935.0,"Objects":[{"StartTime":198935.0,"Position":236.0,"HyperDash":false},{"StartTime":199007.0,"Position":253.0,"HyperDash":false},{"StartTime":199116.0,"Position":236.0,"HyperDash":false}]},{"StartTime":199298.0,"Objects":[{"StartTime":199298.0,"Position":328.0,"HyperDash":false},{"StartTime":199370.0,"Position":276.187836,"HyperDash":false},{"StartTime":199479.0,"Position":243.0,"HyperDash":false}]},{"StartTime":199662.0,"Objects":[{"StartTime":199662.0,"Position":64.0,"HyperDash":false},{"StartTime":199734.0,"Position":66.0,"HyperDash":false},{"StartTime":199843.0,"Position":64.0,"HyperDash":false}]},{"StartTime":200026.0,"Objects":[{"StartTime":200026.0,"Position":160.0,"HyperDash":false}]},{"StartTime":200116.0,"Objects":[{"StartTime":200116.0,"Position":112.0,"HyperDash":false}]},{"StartTime":200207.0,"Objects":[{"StartTime":200207.0,"Position":64.0,"HyperDash":false}]},{"StartTime":200390.0,"Objects":[{"StartTime":200390.0,"Position":240.0,"HyperDash":false},{"StartTime":200462.0,"Position":232.0,"HyperDash":false},{"StartTime":200571.0,"Position":240.0,"HyperDash":false}]},{"StartTime":200753.0,"Objects":[{"StartTime":200753.0,"Position":416.0,"HyperDash":false},{"StartTime":200825.0,"Position":438.812164,"HyperDash":false},{"StartTime":200934.0,"Position":501.0,"HyperDash":true}]},{"StartTime":201117.0,"Objects":[{"StartTime":201117.0,"Position":240.0,"HyperDash":false},{"StartTime":201207.0,"Position":198.4215,"HyperDash":false},{"StartTime":201298.0,"Position":138.280991,"HyperDash":false},{"StartTime":201371.0,"Position":113.25621,"HyperDash":false},{"StartTime":201480.0,"Position":36.0,"HyperDash":false}]},{"StartTime":201662.0,"Objects":[{"StartTime":201662.0,"Position":128.0,"HyperDash":false},{"StartTime":201752.0,"Position":185.148758,"HyperDash":false},{"StartTime":201843.0,"Position":212.765839,"HyperDash":false},{"StartTime":201916.0,"Position":198.0,"HyperDash":false},{"StartTime":202025.0,"Position":216.0,"HyperDash":false}]},{"StartTime":202208.0,"Objects":[{"StartTime":202208.0,"Position":40.0,"HyperDash":false},{"StartTime":202280.0,"Position":56.0,"HyperDash":false},{"StartTime":202389.0,"Position":40.0,"HyperDash":false}]},{"StartTime":202571.0,"Objects":[{"StartTime":202571.0,"Position":216.0,"HyperDash":false},{"StartTime":202643.0,"Position":263.812134,"HyperDash":false},{"StartTime":202752.0,"Position":301.0,"HyperDash":false}]},{"StartTime":202844.0,"Objects":[{"StartTime":202844.0,"Position":348.0,"HyperDash":false}]},{"StartTime":202935.0,"Objects":[{"StartTime":202935.0,"Position":396.0,"HyperDash":false},{"StartTime":203007.0,"Position":411.0,"HyperDash":false},{"StartTime":203116.0,"Position":396.0,"HyperDash":false}]},{"StartTime":203299.0,"Objects":[{"StartTime":203299.0,"Position":492.0,"HyperDash":false},{"StartTime":203371.0,"Position":454.187836,"HyperDash":false},{"StartTime":203480.0,"Position":407.0,"HyperDash":false}]},{"StartTime":203662.0,"Objects":[{"StartTime":203662.0,"Position":232.0,"HyperDash":false},{"StartTime":203734.0,"Position":231.0,"HyperDash":false},{"StartTime":203843.0,"Position":232.0,"HyperDash":false}]},{"StartTime":204026.0,"Objects":[{"StartTime":204026.0,"Position":408.0,"HyperDash":false},{"StartTime":204116.0,"Position":436.148743,"HyperDash":false},{"StartTime":204207.0,"Position":493.0,"HyperDash":false},{"StartTime":204280.0,"Position":447.046844,"HyperDash":false},{"StartTime":204389.0,"Position":408.0,"HyperDash":false}]},{"StartTime":204571.0,"Objects":[{"StartTime":204571.0,"Position":316.0,"HyperDash":false},{"StartTime":204661.0,"Position":377.148743,"HyperDash":false},{"StartTime":204752.0,"Position":400.765839,"HyperDash":false},{"StartTime":204825.0,"Position":421.953156,"HyperDash":false},{"StartTime":204934.0,"Position":486.0,"HyperDash":false}]},{"StartTime":205117.0,"Objects":[{"StartTime":205117.0,"Position":308.0,"HyperDash":false},{"StartTime":205189.0,"Position":279.187836,"HyperDash":false},{"StartTime":205298.0,"Position":223.0,"HyperDash":false}]},{"StartTime":205480.0,"Objects":[{"StartTime":205480.0,"Position":48.0,"HyperDash":false},{"StartTime":205552.0,"Position":51.0,"HyperDash":false},{"StartTime":205661.0,"Position":48.0,"HyperDash":false}]},{"StartTime":205844.0,"Objects":[{"StartTime":205844.0,"Position":224.0,"HyperDash":false},{"StartTime":205916.0,"Position":246.812164,"HyperDash":false},{"StartTime":206025.0,"Position":309.0,"HyperDash":false}]},{"StartTime":206208.0,"Objects":[{"StartTime":206208.0,"Position":216.0,"HyperDash":false}]},{"StartTime":206390.0,"Objects":[{"StartTime":206390.0,"Position":320.0,"HyperDash":false}]},{"StartTime":206571.0,"Objects":[{"StartTime":206571.0,"Position":144.0,"HyperDash":false},{"StartTime":206643.0,"Position":107.187851,"HyperDash":false},{"StartTime":206752.0,"Position":59.0,"HyperDash":true}]},{"StartTime":206935.0,"Objects":[{"StartTime":206935.0,"Position":320.0,"HyperDash":false},{"StartTime":207007.0,"Position":361.812164,"HyperDash":false},{"StartTime":207116.0,"Position":405.0,"HyperDash":false}]},{"StartTime":207208.0,"Objects":[{"StartTime":207208.0,"Position":405.0,"HyperDash":false}]},{"StartTime":207299.0,"Objects":[{"StartTime":207299.0,"Position":405.0,"HyperDash":false}]},{"StartTime":207480.0,"Objects":[{"StartTime":207480.0,"Position":312.0,"HyperDash":false},{"StartTime":207570.0,"Position":265.367828,"HyperDash":false},{"StartTime":207661.0,"Position":263.844818,"HyperDash":false},{"StartTime":207734.0,"Position":266.8324,"HyperDash":false},{"StartTime":207843.0,"Position":312.8251,"HyperDash":false}]},{"StartTime":208026.0,"Objects":[{"StartTime":208026.0,"Position":488.0,"HyperDash":false},{"StartTime":208098.0,"Position":506.0,"HyperDash":false},{"StartTime":208207.0,"Position":488.0,"HyperDash":false}]},{"StartTime":208390.0,"Objects":[{"StartTime":208390.0,"Position":308.0,"HyperDash":false},{"StartTime":208462.0,"Position":292.187836,"HyperDash":false},{"StartTime":208571.0,"Position":223.0,"HyperDash":false}]},{"StartTime":208753.0,"Objects":[{"StartTime":208753.0,"Position":404.0,"HyperDash":false},{"StartTime":208825.0,"Position":411.0,"HyperDash":false},{"StartTime":208934.0,"Position":404.0,"HyperDash":false}]},{"StartTime":209117.0,"Objects":[{"StartTime":209117.0,"Position":308.0,"HyperDash":false}]},{"StartTime":209299.0,"Objects":[{"StartTime":209299.0,"Position":392.0,"HyperDash":false}]},{"StartTime":209480.0,"Objects":[{"StartTime":209480.0,"Position":216.0,"HyperDash":false},{"StartTime":209552.0,"Position":192.187851,"HyperDash":false},{"StartTime":209661.0,"Position":131.0,"HyperDash":false}]},{"StartTime":209844.0,"Objects":[{"StartTime":209844.0,"Position":308.0,"HyperDash":false},{"StartTime":209916.0,"Position":293.0,"HyperDash":false},{"StartTime":210025.0,"Position":308.0,"HyperDash":false}]},{"StartTime":210117.0,"Objects":[{"StartTime":210117.0,"Position":264.0,"HyperDash":false}]},{"StartTime":210208.0,"Objects":[{"StartTime":210208.0,"Position":220.0,"HyperDash":false}]},{"StartTime":210390.0,"Objects":[{"StartTime":210390.0,"Position":308.0,"HyperDash":false},{"StartTime":210480.0,"Position":347.148743,"HyperDash":false},{"StartTime":210571.0,"Position":392.765839,"HyperDash":false},{"StartTime":210644.0,"Position":414.953156,"HyperDash":false},{"StartTime":210753.0,"Position":478.0,"HyperDash":false}]},{"StartTime":210935.0,"Objects":[{"StartTime":210935.0,"Position":296.0,"HyperDash":false},{"StartTime":211007.0,"Position":313.0,"HyperDash":false},{"StartTime":211116.0,"Position":296.0,"HyperDash":false}]},{"StartTime":211299.0,"Objects":[{"StartTime":211299.0,"Position":120.0,"HyperDash":false}]},{"StartTime":211389.0,"Objects":[{"StartTime":211389.0,"Position":120.0,"HyperDash":false}]},{"StartTime":211480.0,"Objects":[{"StartTime":211480.0,"Position":120.0,"HyperDash":false}]},{"StartTime":211662.0,"Objects":[{"StartTime":211662.0,"Position":296.0,"HyperDash":false},{"StartTime":211734.0,"Position":276.187836,"HyperDash":false},{"StartTime":211843.0,"Position":211.0,"HyperDash":false}]},{"StartTime":212026.0,"Objects":[{"StartTime":212026.0,"Position":120.0,"HyperDash":false},{"StartTime":212098.0,"Position":122.0,"HyperDash":false},{"StartTime":212207.0,"Position":120.0,"HyperDash":false}]},{"StartTime":212390.0,"Objects":[{"StartTime":212390.0,"Position":296.0,"HyperDash":false}]},{"StartTime":212571.0,"Objects":[{"StartTime":212571.0,"Position":196.0,"HyperDash":true}]},{"StartTime":212753.0,"Objects":[{"StartTime":212753.0,"Position":456.0,"HyperDash":false},{"StartTime":212825.0,"Position":465.0,"HyperDash":false},{"StartTime":212934.0,"Position":456.0,"HyperDash":false}]},{"StartTime":213117.0,"Objects":[{"StartTime":213117.0,"Position":276.0,"HyperDash":false},{"StartTime":213189.0,"Position":223.187851,"HyperDash":false},{"StartTime":213298.0,"Position":191.0,"HyperDash":false}]},{"StartTime":213480.0,"Objects":[{"StartTime":213480.0,"Position":284.0,"HyperDash":false},{"StartTime":213552.0,"Position":282.0,"HyperDash":false},{"StartTime":213661.0,"Position":284.0,"HyperDash":false}]},{"StartTime":213844.0,"Objects":[{"StartTime":213844.0,"Position":104.0,"HyperDash":false},{"StartTime":213916.0,"Position":147.812149,"HyperDash":false},{"StartTime":214025.0,"Position":189.0,"HyperDash":true}]},{"StartTime":214208.0,"Objects":[{"StartTime":214208.0,"Position":448.0,"HyperDash":false},{"StartTime":214280.0,"Position":454.0,"HyperDash":false},{"StartTime":214389.0,"Position":448.0,"HyperDash":false}]},{"StartTime":214480.0,"Objects":[{"StartTime":214480.0,"Position":400.0,"HyperDash":false}]},{"StartTime":214571.0,"Objects":[{"StartTime":214571.0,"Position":352.0,"HyperDash":false}]},{"StartTime":214753.0,"Objects":[{"StartTime":214753.0,"Position":448.0,"HyperDash":false}]},{"StartTime":214935.0,"Objects":[{"StartTime":214935.0,"Position":272.0,"HyperDash":false},{"StartTime":215007.0,"Position":280.0,"HyperDash":false},{"StartTime":215116.0,"Position":272.0,"HyperDash":false}]},{"StartTime":215299.0,"Objects":[{"StartTime":215299.0,"Position":96.0,"HyperDash":false},{"StartTime":215371.0,"Position":74.18785,"HyperDash":false},{"StartTime":215480.0,"Position":11.0,"HyperDash":true}]},{"StartTime":215662.0,"Objects":[{"StartTime":215662.0,"Position":272.0,"HyperDash":false},{"StartTime":215734.0,"Position":321.812164,"HyperDash":false},{"StartTime":215843.0,"Position":357.0,"HyperDash":false}]},{"StartTime":216026.0,"Objects":[{"StartTime":216026.0,"Position":180.0,"HyperDash":false},{"StartTime":216098.0,"Position":185.0,"HyperDash":false},{"StartTime":216207.0,"Position":180.0,"HyperDash":false}]},{"StartTime":216390.0,"Objects":[{"StartTime":216390.0,"Position":356.0,"HyperDash":false}]},{"StartTime":216571.0,"Objects":[{"StartTime":216571.0,"Position":256.0,"HyperDash":false}]},{"StartTime":216753.0,"Objects":[{"StartTime":216753.0,"Position":436.0,"HyperDash":false},{"StartTime":216825.0,"Position":411.187836,"HyperDash":false},{"StartTime":216934.0,"Position":351.0,"HyperDash":false}]},{"StartTime":217117.0,"Objects":[{"StartTime":217117.0,"Position":96.0,"HyperDash":false},{"StartTime":217207.0,"Position":60.8512421,"HyperDash":false},{"StartTime":217298.0,"Position":12.7658386,"HyperDash":false},{"StartTime":217371.0,"Position":64.95316,"HyperDash":false},{"StartTime":217480.0,"Position":98.0,"HyperDash":false}]},{"StartTime":217662.0,"Objects":[{"StartTime":217662.0,"Position":276.0,"HyperDash":false},{"StartTime":217752.0,"Position":324.148743,"HyperDash":false},{"StartTime":217843.0,"Position":361.0,"HyperDash":false},{"StartTime":217916.0,"Position":327.046844,"HyperDash":false},{"StartTime":218025.0,"Position":276.0,"HyperDash":false}]},{"StartTime":218208.0,"Objects":[{"StartTime":218208.0,"Position":98.0,"HyperDash":false},{"StartTime":218280.0,"Position":87.0,"HyperDash":false},{"StartTime":218389.0,"Position":98.0,"HyperDash":true}]},{"StartTime":218571.0,"Objects":[{"StartTime":218571.0,"Position":360.0,"HyperDash":false},{"StartTime":218661.0,"Position":414.2143,"HyperDash":false},{"StartTime":218752.0,"Position":412.42868,"HyperDash":false},{"StartTime":218825.0,"Position":397.238861,"HyperDash":false},{"StartTime":218934.0,"Position":359.061737,"HyperDash":false}]},{"StartTime":219026.0,"Objects":[{"StartTime":219026.0,"Position":312.0,"HyperDash":false}]},{"StartTime":219117.0,"Objects":[{"StartTime":219117.0,"Position":264.0,"HyperDash":false}]},{"StartTime":219299.0,"Objects":[{"StartTime":219299.0,"Position":88.0,"HyperDash":false},{"StartTime":219371.0,"Position":104.812149,"HyperDash":false},{"StartTime":219480.0,"Position":173.0,"HyperDash":false}]},{"StartTime":219662.0,"Objects":[{"StartTime":219662.0,"Position":268.0,"HyperDash":false},{"StartTime":219734.0,"Position":274.0,"HyperDash":false},{"StartTime":219843.0,"Position":268.0,"HyperDash":false}]},{"StartTime":220026.0,"Objects":[{"StartTime":220026.0,"Position":88.0,"HyperDash":false},{"StartTime":220098.0,"Position":105.0,"HyperDash":false},{"StartTime":220207.0,"Position":88.0,"HyperDash":false}]},{"StartTime":220390.0,"Objects":[{"StartTime":220390.0,"Position":268.0,"HyperDash":false}]},{"StartTime":220571.0,"Objects":[{"StartTime":220571.0,"Position":180.0,"HyperDash":false}]},{"StartTime":220753.0,"Objects":[{"StartTime":220753.0,"Position":436.0,"HyperDash":false},{"StartTime":220825.0,"Position":425.0,"HyperDash":false},{"StartTime":220934.0,"Position":436.0,"HyperDash":false}]},{"StartTime":221117.0,"Objects":[{"StartTime":221117.0,"Position":260.0,"HyperDash":false},{"StartTime":221189.0,"Position":241.187851,"HyperDash":false},{"StartTime":221298.0,"Position":175.0,"HyperDash":true}]},{"StartTime":221480.0,"Objects":[{"StartTime":221480.0,"Position":436.0,"HyperDash":false},{"StartTime":221552.0,"Position":398.187836,"HyperDash":false},{"StartTime":221661.0,"Position":351.0,"HyperDash":false}]},{"StartTime":221753.0,"Objects":[{"StartTime":221753.0,"Position":308.0,"HyperDash":false}]},{"StartTime":221844.0,"Objects":[{"StartTime":221844.0,"Position":264.0,"HyperDash":false}]},{"StartTime":222026.0,"Objects":[{"StartTime":222026.0,"Position":356.0,"HyperDash":false}]},{"StartTime":222208.0,"Objects":[{"StartTime":222208.0,"Position":100.0,"HyperDash":false},{"StartTime":222280.0,"Position":74.18785,"HyperDash":false},{"StartTime":222389.0,"Position":15.0,"HyperDash":false}]},{"StartTime":222571.0,"Objects":[{"StartTime":222571.0,"Position":108.0,"HyperDash":false},{"StartTime":222643.0,"Position":119.0,"HyperDash":false},{"StartTime":222752.0,"Position":108.0,"HyperDash":true}]},{"StartTime":222935.0,"Objects":[{"StartTime":222935.0,"Position":368.0,"HyperDash":false},{"StartTime":223025.0,"Position":410.5,"HyperDash":false},{"StartTime":223116.0,"Position":368.0,"HyperDash":false}]},{"StartTime":223299.0,"Objects":[{"StartTime":223299.0,"Position":188.0,"HyperDash":false}]},{"StartTime":223480.0,"Objects":[{"StartTime":223480.0,"Position":280.0,"HyperDash":false}]},{"StartTime":223571.0,"Objects":[{"StartTime":223571.0,"Position":328.0,"HyperDash":false}]},{"StartTime":223662.0,"Objects":[{"StartTime":223662.0,"Position":376.0,"HyperDash":false},{"StartTime":223734.0,"Position":377.0,"HyperDash":false},{"StartTime":223843.0,"Position":376.0,"HyperDash":false}]},{"StartTime":224026.0,"Objects":[{"StartTime":224026.0,"Position":196.0,"HyperDash":false},{"StartTime":224098.0,"Position":161.187851,"HyperDash":false},{"StartTime":224207.0,"Position":111.0,"HyperDash":true}]},{"StartTime":224390.0,"Objects":[{"StartTime":224390.0,"Position":376.0,"HyperDash":false},{"StartTime":224480.0,"Position":398.812927,"HyperDash":false},{"StartTime":224571.0,"Position":435.886963,"HyperDash":false},{"StartTime":224644.0,"Position":405.66272,"HyperDash":false},{"StartTime":224753.0,"Position":375.3338,"HyperDash":false}]},{"StartTime":225117.0,"Objects":[{"StartTime":225117.0,"Position":96.0,"HyperDash":false},{"StartTime":225189.0,"Position":107.0,"HyperDash":false},{"StartTime":225298.0,"Position":96.0,"HyperDash":false}]},{"StartTime":225480.0,"Objects":[{"StartTime":225480.0,"Position":180.0,"HyperDash":false}]},{"StartTime":225662.0,"Objects":[{"StartTime":225662.0,"Position":356.0,"HyperDash":false}]},{"StartTime":225753.0,"Objects":[{"StartTime":225753.0,"Position":400.0,"HyperDash":false}]},{"StartTime":225844.0,"Objects":[{"StartTime":225844.0,"Position":444.0,"HyperDash":false},{"StartTime":225916.0,"Position":453.0,"HyperDash":false},{"StartTime":226025.0,"Position":444.0,"HyperDash":false}]},{"StartTime":226208.0,"Objects":[{"StartTime":226208.0,"Position":360.0,"HyperDash":false},{"StartTime":226280.0,"Position":323.187836,"HyperDash":false},{"StartTime":226389.0,"Position":275.0,"HyperDash":false}]},{"StartTime":226571.0,"Objects":[{"StartTime":226571.0,"Position":96.0,"HyperDash":false},{"StartTime":226643.0,"Position":104.0,"HyperDash":false},{"StartTime":226752.0,"Position":96.0,"HyperDash":false}]},{"StartTime":226935.0,"Objects":[{"StartTime":226935.0,"Position":181.0,"HyperDash":false},{"StartTime":227007.0,"Position":146.187851,"HyperDash":false},{"StartTime":227116.0,"Position":96.0,"HyperDash":false}]},{"StartTime":227299.0,"Objects":[{"StartTime":227299.0,"Position":276.0,"HyperDash":false},{"StartTime":227389.0,"Position":322.148743,"HyperDash":false},{"StartTime":227480.0,"Position":360.0,"HyperDash":false},{"StartTime":227553.0,"Position":357.0,"HyperDash":false},{"StartTime":227662.0,"Position":360.0,"HyperDash":false}]},{"StartTime":227844.0,"Objects":[{"StartTime":227844.0,"Position":276.0,"HyperDash":false}]},{"StartTime":228026.0,"Objects":[{"StartTime":228026.0,"Position":96.0,"HyperDash":false},{"StartTime":228098.0,"Position":82.0,"HyperDash":false},{"StartTime":228207.0,"Position":96.0,"HyperDash":false}]},{"StartTime":228390.0,"Objects":[{"StartTime":228390.0,"Position":180.0,"HyperDash":false},{"StartTime":228462.0,"Position":193.0,"HyperDash":false},{"StartTime":228571.0,"Position":180.0,"HyperDash":false}]},{"StartTime":228753.0,"Objects":[{"StartTime":228753.0,"Position":356.0,"HyperDash":false}]},{"StartTime":228935.0,"Objects":[{"StartTime":228935.0,"Position":440.0,"HyperDash":false}]},{"StartTime":229117.0,"Objects":[{"StartTime":229117.0,"Position":440.0,"HyperDash":false}]},{"StartTime":229299.0,"Objects":[{"StartTime":229299.0,"Position":356.0,"HyperDash":false}]},{"StartTime":229480.0,"Objects":[{"StartTime":229480.0,"Position":176.0,"HyperDash":false},{"StartTime":229552.0,"Position":177.0,"HyperDash":false},{"StartTime":229661.0,"Position":176.0,"HyperDash":false}]},{"StartTime":229844.0,"Objects":[{"StartTime":229844.0,"Position":264.0,"HyperDash":false}]},{"StartTime":229934.0,"Objects":[{"StartTime":229934.0,"Position":310.0,"HyperDash":false}]},{"StartTime":230025.0,"Objects":[{"StartTime":230025.0,"Position":356.0,"HyperDash":false}]},{"StartTime":230208.0,"Objects":[{"StartTime":230208.0,"Position":176.0,"HyperDash":false},{"StartTime":230298.0,"Position":147.851242,"HyperDash":false},{"StartTime":230389.0,"Position":91.23416,"HyperDash":false},{"StartTime":230462.0,"Position":41.0468369,"HyperDash":false},{"StartTime":230571.0,"Position":6.0,"HyperDash":false}]},{"StartTime":230753.0,"Objects":[{"StartTime":230753.0,"Position":92.0,"HyperDash":false}]},{"StartTime":230935.0,"Objects":[{"StartTime":230935.0,"Position":268.0,"HyperDash":false},{"StartTime":231007.0,"Position":314.812164,"HyperDash":false},{"StartTime":231116.0,"Position":353.0,"HyperDash":false}]},{"StartTime":231299.0,"Objects":[{"StartTime":231299.0,"Position":260.0,"HyperDash":false},{"StartTime":231371.0,"Position":259.0,"HyperDash":false},{"StartTime":231480.0,"Position":260.0,"HyperDash":false}]},{"StartTime":231571.0,"Objects":[{"StartTime":231571.0,"Position":308.0,"HyperDash":false}]},{"StartTime":231662.0,"Objects":[{"StartTime":231662.0,"Position":356.0,"HyperDash":false},{"StartTime":231752.0,"Position":386.148743,"HyperDash":false},{"StartTime":231843.0,"Position":440.0,"HyperDash":false},{"StartTime":231916.0,"Position":455.0,"HyperDash":false},{"StartTime":232025.0,"Position":440.0,"HyperDash":false}]},{"StartTime":232208.0,"Objects":[{"StartTime":232208.0,"Position":356.0,"HyperDash":false}]},{"StartTime":232390.0,"Objects":[{"StartTime":232390.0,"Position":180.0,"HyperDash":false},{"StartTime":232462.0,"Position":176.0,"HyperDash":false},{"StartTime":232571.0,"Position":180.0,"HyperDash":false}]},{"StartTime":232753.0,"Objects":[{"StartTime":232753.0,"Position":272.0,"HyperDash":false},{"StartTime":232843.0,"Position":272.0,"HyperDash":false},{"StartTime":232934.0,"Position":272.0,"HyperDash":false}]},{"StartTime":233117.0,"Objects":[{"StartTime":233117.0,"Position":92.0,"HyperDash":false},{"StartTime":233207.0,"Position":62.53518,"HyperDash":false},{"StartTime":233298.0,"Position":40.0084,"HyperDash":false},{"StartTime":233371.0,"Position":54.41962,"HyperDash":false},{"StartTime":233480.0,"Position":88.82208,"HyperDash":false}]},{"StartTime":233662.0,"Objects":[{"StartTime":233662.0,"Position":172.0,"HyperDash":false}]},{"StartTime":233844.0,"Objects":[{"StartTime":233844.0,"Position":352.0,"HyperDash":false},{"StartTime":233916.0,"Position":341.0,"HyperDash":false},{"StartTime":234025.0,"Position":352.0,"HyperDash":false}]},{"StartTime":234208.0,"Objects":[{"StartTime":234208.0,"Position":268.0,"HyperDash":false}]},{"StartTime":234390.0,"Objects":[{"StartTime":234390.0,"Position":360.0,"HyperDash":false}]},{"StartTime":234571.0,"Objects":[{"StartTime":234571.0,"Position":172.0,"HyperDash":false},{"StartTime":234661.0,"Position":172.0,"HyperDash":false},{"StartTime":234752.0,"Position":172.0,"HyperDash":false}]},{"StartTime":234935.0,"Objects":[{"StartTime":234935.0,"Position":268.0,"HyperDash":false},{"StartTime":235007.0,"Position":228.187851,"HyperDash":false},{"StartTime":235116.0,"Position":183.0,"HyperDash":false}]},{"StartTime":235298.0,"Objects":[{"StartTime":235298.0,"Position":364.0,"HyperDash":false},{"StartTime":235370.0,"Position":353.0,"HyperDash":false},{"StartTime":235479.0,"Position":364.0,"HyperDash":false}]},{"StartTime":235662.0,"Objects":[{"StartTime":235662.0,"Position":183.0,"HyperDash":false}]},{"StartTime":235752.0,"Objects":[{"StartTime":235752.0,"Position":140.0,"HyperDash":false}]},{"StartTime":235843.0,"Objects":[{"StartTime":235843.0,"Position":98.0,"HyperDash":true}]},{"StartTime":236026.0,"Objects":[{"StartTime":236026.0,"Position":376.0,"HyperDash":false}]},{"StartTime":236390.0,"Objects":[{"StartTime":236390.0,"Position":224.0,"HyperDash":false}]},{"StartTime":236753.0,"Objects":[{"StartTime":236753.0,"Position":496.0,"HyperDash":false},{"StartTime":236843.0,"Position":487.0,"HyperDash":false},{"StartTime":236934.0,"Position":496.0,"HyperDash":false},{"StartTime":237007.0,"Position":494.0,"HyperDash":false},{"StartTime":237116.0,"Position":496.0,"HyperDash":false}]},{"StartTime":237480.0,"Objects":[{"StartTime":237480.0,"Position":266.0,"HyperDash":false},{"StartTime":237548.0,"Position":100.0,"HyperDash":false},{"StartTime":237616.0,"Position":57.0,"HyperDash":false},{"StartTime":237684.0,"Position":199.0,"HyperDash":false},{"StartTime":237752.0,"Position":129.0,"HyperDash":false},{"StartTime":237820.0,"Position":232.0,"HyperDash":false},{"StartTime":237889.0,"Position":464.0,"HyperDash":false},{"StartTime":237957.0,"Position":364.0,"HyperDash":false},{"StartTime":238025.0,"Position":170.0,"HyperDash":false},{"StartTime":238093.0,"Position":496.0,"HyperDash":false},{"StartTime":238161.0,"Position":27.0,"HyperDash":false},{"StartTime":238230.0,"Position":477.0,"HyperDash":false},{"StartTime":238298.0,"Position":163.0,"HyperDash":false},{"StartTime":238366.0,"Position":260.0,"HyperDash":false},{"StartTime":238434.0,"Position":253.0,"HyperDash":false},{"StartTime":238502.0,"Position":423.0,"HyperDash":false},{"StartTime":238571.0,"Position":367.0,"HyperDash":false}]},{"StartTime":238935.0,"Objects":[{"StartTime":238935.0,"Position":256.0,"HyperDash":false},{"StartTime":239025.0,"Position":247.0,"HyperDash":false},{"StartTime":239116.0,"Position":256.0,"HyperDash":false},{"StartTime":239189.0,"Position":240.0,"HyperDash":false},{"StartTime":239298.0,"Position":256.0,"HyperDash":false}]},{"StartTime":239662.0,"Objects":[{"StartTime":239662.0,"Position":78.0,"HyperDash":false},{"StartTime":239713.0,"Position":446.0,"HyperDash":false},{"StartTime":239764.0,"Position":99.0,"HyperDash":false},{"StartTime":239815.0,"Position":155.0,"HyperDash":false},{"StartTime":239866.0,"Position":322.0,"HyperDash":false},{"StartTime":239917.0,"Position":261.0,"HyperDash":false},{"StartTime":239968.0,"Position":22.0,"HyperDash":false},{"StartTime":240019.0,"Position":481.0,"HyperDash":false},{"StartTime":240071.0,"Position":103.0,"HyperDash":false},{"StartTime":240122.0,"Position":316.0,"HyperDash":false},{"StartTime":240173.0,"Position":175.0,"HyperDash":false},{"StartTime":240224.0,"Position":48.0,"HyperDash":false},{"StartTime":240275.0,"Position":307.0,"HyperDash":false},{"StartTime":240326.0,"Position":375.0,"HyperDash":false},{"StartTime":240377.0,"Position":149.0,"HyperDash":false},{"StartTime":240429.0,"Position":250.0,"HyperDash":false},{"StartTime":240480.0,"Position":142.0,"HyperDash":false},{"StartTime":240531.0,"Position":170.0,"HyperDash":false},{"StartTime":240582.0,"Position":281.0,"HyperDash":false},{"StartTime":240633.0,"Position":444.0,"HyperDash":false},{"StartTime":240684.0,"Position":414.0,"HyperDash":false},{"StartTime":240735.0,"Position":321.0,"HyperDash":false},{"StartTime":240787.0,"Position":328.0,"HyperDash":false},{"StartTime":240838.0,"Position":32.0,"HyperDash":false},{"StartTime":240889.0,"Position":259.0,"HyperDash":false},{"StartTime":240940.0,"Position":169.0,"HyperDash":false},{"StartTime":240991.0,"Position":207.0,"HyperDash":false},{"StartTime":241042.0,"Position":464.0,"HyperDash":false},{"StartTime":241093.0,"Position":192.0,"HyperDash":false},{"StartTime":241145.0,"Position":317.0,"HyperDash":false},{"StartTime":241196.0,"Position":376.0,"HyperDash":false},{"StartTime":241247.0,"Position":100.0,"HyperDash":false},{"StartTime":241298.0,"Position":70.0,"HyperDash":false},{"StartTime":241349.0,"Position":287.0,"HyperDash":false},{"StartTime":241400.0,"Position":468.0,"HyperDash":false},{"StartTime":241451.0,"Position":58.0,"HyperDash":false},{"StartTime":241503.0,"Position":352.0,"HyperDash":false},{"StartTime":241554.0,"Position":305.0,"HyperDash":false},{"StartTime":241605.0,"Position":177.0,"HyperDash":false},{"StartTime":241656.0,"Position":414.0,"HyperDash":false},{"StartTime":241707.0,"Position":182.0,"HyperDash":false},{"StartTime":241758.0,"Position":174.0,"HyperDash":false},{"StartTime":241809.0,"Position":89.0,"HyperDash":false},{"StartTime":241861.0,"Position":254.0,"HyperDash":false},{"StartTime":241912.0,"Position":320.0,"HyperDash":false},{"StartTime":241963.0,"Position":406.0,"HyperDash":false},{"StartTime":242014.0,"Position":182.0,"HyperDash":false},{"StartTime":242065.0,"Position":301.0,"HyperDash":false},{"StartTime":242116.0,"Position":169.0,"HyperDash":false},{"StartTime":242167.0,"Position":470.0,"HyperDash":false},{"StartTime":242219.0,"Position":278.0,"HyperDash":false},{"StartTime":242270.0,"Position":146.0,"HyperDash":false},{"StartTime":242321.0,"Position":480.0,"HyperDash":false},{"StartTime":242372.0,"Position":41.0,"HyperDash":false},{"StartTime":242423.0,"Position":51.0,"HyperDash":false},{"StartTime":242474.0,"Position":295.0,"HyperDash":false},{"StartTime":242525.0,"Position":145.0,"HyperDash":false},{"StartTime":242577.0,"Position":237.0,"HyperDash":false},{"StartTime":242628.0,"Position":152.0,"HyperDash":false},{"StartTime":242679.0,"Position":500.0,"HyperDash":false},{"StartTime":242730.0,"Position":278.0,"HyperDash":false},{"StartTime":242781.0,"Position":174.0,"HyperDash":false},{"StartTime":242832.0,"Position":92.0,"HyperDash":false},{"StartTime":242883.0,"Position":248.0,"HyperDash":false},{"StartTime":242935.0,"Position":284.0,"HyperDash":false},{"StartTime":242986.0,"Position":296.0,"HyperDash":false},{"StartTime":243037.0,"Position":325.0,"HyperDash":false},{"StartTime":243088.0,"Position":116.0,"HyperDash":false},{"StartTime":243139.0,"Position":293.0,"HyperDash":false},{"StartTime":243190.0,"Position":511.0,"HyperDash":false},{"StartTime":243241.0,"Position":17.0,"HyperDash":false},{"StartTime":243292.0,"Position":64.0,"HyperDash":false},{"StartTime":243344.0,"Position":486.0,"HyperDash":false},{"StartTime":243395.0,"Position":209.0,"HyperDash":false},{"StartTime":243446.0,"Position":264.0,"HyperDash":false},{"StartTime":243497.0,"Position":47.0,"HyperDash":false},{"StartTime":243548.0,"Position":206.0,"HyperDash":false},{"StartTime":243599.0,"Position":353.0,"HyperDash":false},{"StartTime":243650.0,"Position":244.0,"HyperDash":false},{"StartTime":243702.0,"Position":157.0,"HyperDash":false},{"StartTime":243753.0,"Position":227.0,"HyperDash":false},{"StartTime":243804.0,"Position":167.0,"HyperDash":false},{"StartTime":243855.0,"Position":420.0,"HyperDash":false},{"StartTime":243906.0,"Position":103.0,"HyperDash":false},{"StartTime":243957.0,"Position":188.0,"HyperDash":false},{"StartTime":244008.0,"Position":300.0,"HyperDash":false},{"StartTime":244060.0,"Position":60.0,"HyperDash":false},{"StartTime":244111.0,"Position":120.0,"HyperDash":false},{"StartTime":244162.0,"Position":501.0,"HyperDash":false},{"StartTime":244213.0,"Position":341.0,"HyperDash":false},{"StartTime":244264.0,"Position":181.0,"HyperDash":false},{"StartTime":244315.0,"Position":337.0,"HyperDash":false},{"StartTime":244366.0,"Position":269.0,"HyperDash":false},{"StartTime":244418.0,"Position":398.0,"HyperDash":false},{"StartTime":244469.0,"Position":308.0,"HyperDash":false},{"StartTime":244520.0,"Position":323.0,"HyperDash":false},{"StartTime":244571.0,"Position":201.0,"HyperDash":false},{"StartTime":244622.0,"Position":204.0,"HyperDash":false},{"StartTime":244673.0,"Position":44.0,"HyperDash":false},{"StartTime":244724.0,"Position":217.0,"HyperDash":false},{"StartTime":244776.0,"Position":510.0,"HyperDash":false},{"StartTime":244827.0,"Position":324.0,"HyperDash":false},{"StartTime":244878.0,"Position":131.0,"HyperDash":false},{"StartTime":244929.0,"Position":13.0,"HyperDash":false},{"StartTime":244980.0,"Position":360.0,"HyperDash":false},{"StartTime":245031.0,"Position":510.0,"HyperDash":false},{"StartTime":245082.0,"Position":203.0,"HyperDash":false},{"StartTime":245134.0,"Position":416.0,"HyperDash":false},{"StartTime":245185.0,"Position":162.0,"HyperDash":false},{"StartTime":245236.0,"Position":277.0,"HyperDash":false},{"StartTime":245287.0,"Position":329.0,"HyperDash":false},{"StartTime":245338.0,"Position":357.0,"HyperDash":false},{"StartTime":245389.0,"Position":388.0,"HyperDash":false},{"StartTime":245440.0,"Position":87.0,"HyperDash":false},{"StartTime":245492.0,"Position":462.0,"HyperDash":false},{"StartTime":245543.0,"Position":357.0,"HyperDash":false},{"StartTime":245594.0,"Position":343.0,"HyperDash":false},{"StartTime":245645.0,"Position":248.0,"HyperDash":false},{"StartTime":245696.0,"Position":174.0,"HyperDash":false},{"StartTime":245747.0,"Position":112.0,"HyperDash":false},{"StartTime":245798.0,"Position":420.0,"HyperDash":false},{"StartTime":245850.0,"Position":229.0,"HyperDash":false},{"StartTime":245901.0,"Position":270.0,"HyperDash":false},{"StartTime":245952.0,"Position":3.0,"HyperDash":false},{"StartTime":246003.0,"Position":446.0,"HyperDash":false},{"StartTime":246054.0,"Position":78.0,"HyperDash":false},{"StartTime":246105.0,"Position":157.0,"HyperDash":false},{"StartTime":246156.0,"Position":344.0,"HyperDash":false},{"StartTime":246208.0,"Position":72.0,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3689906.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3689906.osu new file mode 100644 index 0000000000..070143fcf1 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3689906.osu @@ -0,0 +1,942 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 +Mode: 2 + +[Difficulty] +HPDrainRate:4 +CircleSize:3.2 +OverallDifficulty:8 +ApproachRate:8 +SliderMultiplier:1.7 +SliderTickRate:2 + +[Events] +//Background and Video events +//Break Periods +2,125844,129844 +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Layer 4 (Overlay) +//Storyboard Sound Samples + +[TimingPoints] +390,363.636363636364,4,2,1,60,1,0 +3480,-100,4,2,2,70,0,0 +3662,-100,4,2,1,60,0,0 +4753,-100,4,2,2,50,0,0 +4935,-100,4,2,1,60,0,0 +6208,-100,4,2,3,60,0,0 +6390,-100,4,2,1,60,0,0 +9299,-100,4,2,2,70,0,0 +9480,-100,4,2,1,60,0,0 +12026,-100,4,2,3,70,0,0 +12208,-100,4,2,1,70,0,0 +23662,-83.3333333333333,4,2,3,70,0,0 +24026,-100,4,2,1,80,0,0 +26753,-100,4,2,2,80,0,0 +26935,-100,4,2,1,80,0,0 +28935,-83.3333333333333,4,2,1,80,0,0 +29480,-83.3333333333333,4,2,3,70,0,0 +30026,-100,4,2,1,70,0,0 +30935,-100,4,2,1,30,0,0 +31662,-100,4,2,1,40,0,0 +32390,-100,4,2,1,30,0,0 +32753,-100,4,2,1,40,0,0 +33117,-100,4,2,1,50,0,0 +33480,-100,4,2,1,60,0,0 +33844,-100,4,2,1,70,0,0 +34117,-100,4,2,1,40,0,0 +34208,-100,4,2,1,70,0,0 +34299,-100,4,2,1,40,0,0 +34480,-100,4,2,1,70,0,0 +34662,-100,4,2,1,40,0,0 +34753,-100,4,2,1,70,0,0 +34935,-100,4,2,77,80,0,0 +35299,-83.3333333333333,4,2,3,80,0,0 +35662,-100,4,2,1,80,0,0 +38753,-100,4,2,1,80,0,0 +39117,-100,4,2,1,80,0,0 +44026,-83.3333333333333,4,2,1,80,0,0 +44390,-100,4,2,1,80,0,0 +46208,-100,4,2,1,80,0,0 +46571,-100,4,2,77,90,0,0 +46753,-100,4,2,1,80,0,0 +46935,-100,4,2,3,80,0,0 +47117,-100,4,2,1,80,0,0 +52390,-100,4,2,1,80,0,0 +52753,-100,4,2,1,80,0,0 +55662,-100,4,2,1,85,0,0 +57117,-100,4,2,1,90,0,0 +58208,-100,4,2,77,90,0,0 +58390,-100,4,2,1,80,0,0 +58571,-100,4,2,3,90,0,1 +58753,-100,4,2,1,90,0,1 +69844,-100,4,2,1,90,0,0 +70208,-100,4,2,3,90,0,1 +70390,-100,4,2,1,90,0,1 +82935,-100,4,2,77,90,0,1 +83299,-83.3333333333333,4,2,3,80,0,0 +83662,-100,4,2,1,80,0,0 +88753,-100,4,2,1,80,0,0 +89117,-100,4,2,1,80,0,0 +94571,-100,4,2,77,80,0,0 +94753,-100,4,2,1,80,0,0 +94935,-100,4,2,3,80,0,0 +95117,-100,4,2,1,80,0,0 +106208,-100,4,2,77,80,0,0 +106571,-100,4,2,1,80,0,0 +112390,-100,4,2,77,90,0,0 +112571,-100,4,2,1,80,0,0 +117117,-100,4,2,1,40,0,0 +117480,-100,4,2,1,50,0,0 +117844,-100,4,2,1,60,0,0 +118208,-100,4,2,1,55,0,0 +118571,-100,4,2,1,65,0,0 +118935,-100,4,2,1,75,0,0 +119299,-100,4,2,1,85,0,0 +119662,-100,4,2,3,100,0,0 +120026,-100,4,2,1,30,0,0 +125480,-100,4,2,1,5,0,0 +131299,-100,4,2,4,60,0,0 +136390,-100,4,2,4,60,0,0 +137117,-100,4,2,4,70,0,0 +137480,-100,4,2,4,50,0,0 +138571,-100,4,2,4,60,0,0 +141480,-90.9090909090909,4,2,4,50,0,0 +141844,-100,4,2,4,50,0,0 +142662,-100,4,2,78,50,0,0 +143117,-100,4,2,4,60,0,0 +148753,-100,4,2,78,50,0,0 +148935,-100,4,2,4,60,0,0 +153117,-100,4,2,4,50,0,0 +154571,-100,4,2,78,50,0,0 +154753,-100,4,2,4,60,0,0 +160390,-100,4,2,3,60,0,0 +160571,-100,4,2,4,60,0,0 +163299,-100,4,2,1,30,0,0 +163480,-100,4,2,5,30,0,0 +163571,-100,4,2,1,30,0,0 +163662,-100,4,2,1,40,0,0 +163844,-100,4,2,5,40,0,0 +163935,-100,4,2,1,40,0,0 +164026,-100,4,2,1,50,0,0 +164208,-100,4,2,5,50,0,0 +164299,-100,4,2,1,50,0,0 +164390,-100,4,2,1,60,0,0 +164571,-100,4,2,5,60,0,0 +164662,-100,4,2,1,60,0,0 +164753,-100,4,2,1,70,0,0 +165117,-100,4,2,1,70,0,0 +165480,-100,4,2,1,70,0,0 +165844,-100,4,2,77,80,0,0 +166208,-100,4,2,1,80,0,0 +174571,-100,4,2,1,80,0,0 +174935,-100,4,2,1,80,0,0 +177844,-83.3333333333333,4,2,1,80,0,0 +178208,-100,4,2,1,80,0,0 +186571,-100,4,2,1,80,0,0 +187117,-100,4,2,1,80,0,0 +187480,-100,4,2,1,80,0,0 +188571,-100,4,2,1,80,0,0 +188934,-100,4,2,1,80,0,0 +189480,-83.3333333333333,4,2,3,90,0,1 +189844,-100,4,2,1,90,0,1 +194208,-100,4,2,1,90,0,1 +194571,-100,4,2,1,90,0,1 +195844,-100,4,2,1,90,0,1 +196208,-100,4,2,1,90,0,1 +200753,-100,4,2,1,90,0,1 +200935,-100,4,2,1,90,0,0 +201117,-83.3333333333333,4,2,1,90,0,1 +201480,-100,4,2,1,90,0,1 +212026,-100,4,2,77,90,0,1 +212571,-100,4,2,1,90,0,0 +212753,-100,4,2,3,90,0,1 +212935,-100,4,2,1,90,0,1 +214026,-100,4,2,5,60,0,1 +214117,-100,4,2,1,90,0,1 +215480,-100,4,2,5,60,0,1 +215571,-100,4,2,1,90,0,1 +216935,-100,4,2,5,60,0,1 +217026,-100,4,2,1,90,0,1 +218390,-100,4,2,5,60,0,1 +218481,-100,4,2,1,90,0,1 +219844,-100,4,2,5,60,0,1 +219935,-100,4,2,1,90,0,1 +221299,-100,4,2,5,60,0,1 +221390,-100,4,2,1,90,0,1 +222753,-100,4,2,5,60,0,1 +222844,-100,4,2,1,90,0,1 +224208,-100,4,2,77,90,0,1 +224390,-83.3333333333333,4,2,3,80,0,0 +224753,-100,4,2,1,80,0,0 +225662,-100,4,2,5,60,0,0 +225753,-100,4,2,1,90,0,0 +226026,-100,4,2,5,60,0,0 +226117,-100,4,2,1,90,0,0 +227844,-100,4,2,5,60,0,0 +227935,-100,4,2,1,90,0,0 +228571,-100,4,2,5,60,0,0 +228662,-100,4,2,1,90,0,0 +230026,-100,4,2,5,60,0,0 +230117,-100,4,2,1,90,0,0 +230753,-100,4,2,5,60,0,0 +230844,-100,4,2,1,90,0,0 +231480,-100,4,2,5,60,0,0 +231571,-100,4,2,1,90,0,0 +232208,-100,4,2,5,60,0,0 +232299,-100,4,2,1,90,0,0 +233117,-100,4,2,1,35,0,0 +233480,-100,4,2,1,45,0,0 +233844,-100,4,2,1,55,0,0 +234208,-100,4,2,1,75,0,0 +234299,-100,4,2,1,65,0,0 +234390,-100,4,2,1,75,0,0 +234480,-100,4,2,1,65,0,0 +234571,-100,4,2,1,75,0,0 +234935,-100,4,2,1,85,0,0 +235299,-100,4,2,1,95,0,0 +235662,-100,4,2,1,85,0,0 +236026,-100,4,2,3,80,0,0 +236390,-100,4,2,4,70,0,0 +236753,-100,4,2,78,70,0,0 +237480,-100,4,2,0,50,0,0 +237844,-100,4,2,0,40,0,0 +238208,-100,4,2,0,30,0,0 +238571,-100,4,2,0,20,0,0 +238935,-100,4,2,78,50,0,0 +239662,-100,4,2,0,50,0,0 +240390,-100,4,2,0,45,0,0 +241117,-100,4,2,0,40,0,0 +241844,-100,4,2,0,35,0,0 +242571,-100,4,2,0,30,0,0 +243299,-100,4,2,0,25,0,0 +244026,-100,4,2,0,20,0,0 +244753,-100,4,2,0,15,0,0 +245480,-100,4,2,0,10,0,0 +246208,-100,4,2,0,5,0,0 + +[HitObjects] +124,320,390,6,0,L|124:128,1,170,4|0,0:0|0:0,0:0:0:0: +208,148,935,1,0,0:0:0:0: +380,192,1117,2,0,L|380:16,1,170,8|2,0:0|0:0,0:0:0:0: +208,24,1844,5,0,0:0:0:0: +360,24,2208,1,2,0:0:0:0: +188,24,2390,1,2,0:0:0:0: +152,24,2480,1,2,0:0:0:0: +112,24,2571,2,0,L|112:128,1,85,8|2,0:0|0:0,0:0:0:0: +196,108,2935,1,0,0:0:0:0: +280,108,3117,1,0,0:0:0:0: +196,108,3299,5,2,0:0:0:0: +288,108,3480,2,0,L|288:292,1,170,2|0,0:0|0:0,0:0:0:0: +116,312,4026,1,8,0:0:0:0: +300,280,4390,1,2,0:0:0:0: +28,192,4753,6,0,L|28:100,1,85,4|2,0:0|0:0,0:0:0:0: +112,108,5117,1,0,0:0:0:0: +20,108,5299,1,2,0:0:0:0: +192,108,5480,2,0,L|280:108,2,85,8|2|0,0:0|0:0|0:0,0:0:0:0: +484,364,6208,6,0,L|484:172,1,170,14|0,0:0|0:0,0:0:0:0: +400,192,6753,1,0,0:0:0:0: +228,236,6935,2,0,L|228:60,1,170,8|2,0:0|0:0,0:0:0:0: +396,64,7662,5,0,0:0:0:0: +244,64,8026,1,2,0:0:0:0: +416,64,8208,1,2,0:0:0:0: +452,64,8298,1,2,0:0:0:0: +492,64,8389,2,0,L|492:168,1,85,8|2,0:0|0:0,0:0:0:0: +396,148,8753,1,0,0:0:0:0: +304,148,8935,1,0,0:0:0:0: +212,148,9117,5,2,0:0:0:0: +312,148,9298,2,0,L|312:332,1,170,2|0,0:0|0:0,0:0:0:0: +140,352,9844,1,8,0:0:0:0: +324,320,10208,1,2,0:0:0:0: +136,192,10571,6,0,L|232:192,1,85,2|2,0:0|0:0,0:0:0:0: +128,192,10935,2,0,L|216:192,1,85,0|2,0:0|0:0,0:0:0:0: +384,192,11299,1,8,0:0:0:0: +292,192,11480,1,2,0:0:0:0: +200,192,11662,1,0,0:0:0:0: +488,192,12026,6,0,B|488:108|488:108|400:108,1,170,10|0,0:0|0:0,0:0:0:0: +316,108,12571,1,0,0:0:0:0: +144,108,12753,2,0,L|144:296,1,170,8|2,0:0|0:0,0:0:0:0: +314,278,13480,6,0,L|134:278,1,170,0|2,0:0|0:0,0:0:0:0: +144,278,14026,1,2,0:0:0:0: +314,278,14208,2,0,L|406:278,1,85,8|2,0:0|0:0,0:0:0:0: +304,276,14571,2,0,L|304:172,1,85,0|0,0:0|0:0,0:0:0:0: +132,192,14935,6,0,B|48:192|48:192|48:104,1,170,2|0,0:0|0:0,0:0:0:0: +132,104,15480,1,0,0:0:0:0: +304,48,15662,1,8,0:0:0:0: +132,104,16026,1,2,0:0:0:0: +284,104,16390,6,0,L|284:188,1,85,0|0,0:0|0:0,0:0:0:0: +192,192,16753,1,2,0:0:0:0: +192,192,16935,1,2,0:0:0:0: +364,192,17117,2,0,L|456:192,2,85,8|2|0,0:0|0:0|0:0,0:0:0:0: +64,192,17844,6,0,L|64:292,1,85,2|0,0:0|0:0,0:0:0:0: +148,192,18208,2,0,L|148:288,1,85,0|0,0:0|0:0,0:0:0:0: +320,192,18571,1,8,0:0:0:0: +132,192,18935,1,2,0:0:0:0: +132,192,19299,6,0,L|304:192,1,170,0|2,0:0|0:0,0:0:0:0: +388,192,19844,1,2,0:0:0:0: +216,192,20026,2,0,L|124:192,1,85,8|2,0:0|0:0,0:0:0:0: +224,192,20390,2,0,L|224:100,1,85,0|0,0:0|0:0,0:0:0:0: +52,20,20753,6,0,B|52:108|52:108|140:108,1,170,2|0,0:0|0:0,0:0:0:0: +224,107,21299,1,0,0:0:0:0: +396,192,21480,1,8,0:0:0:0: +224,192,21844,1,2,0:0:0:0: +132,192,22026,1,2,0:0:0:0: +224,192,22208,5,0,0:0:0:0: +176,192,22299,1,2,0:0:0:0: +132,192,22390,1,2,0:0:0:0: +232,192,22571,1,2,0:0:0:0: +404,192,22753,1,8,0:0:0:0: +232,192,22935,2,0,L|232:288,1,85,8|2,0:0|0:0,0:0:0:0: +404,277,23299,1,2,0:0:0:0: +448,276,23389,1,2,0:0:0:0: +492,276,23480,1,2,0:0:0:0: +212,192,23662,6,0,L|8:192,1,203.999993774414,10|0,0:0|0:0,0:0:0:0: +92,192,24208,1,0,0:0:0:0: +272,192,24390,2,0,L|272:96,1,85,8|0,0:0|0:0,0:0:0:0: +180,108,24753,1,2,0:0:0:0: +348,104,25117,6,0,L|252:104,1,85,0|0,0:0|0:0,0:0:0:0: +355,105,25480,1,2,0:0:0:0: +179,105,25662,1,2,0:0:0:0: +135,105,25752,1,2,0:0:0:0: +91,105,25843,2,0,L|7:105,2,85,8|2|0,0:0|0:0|0:0,0:0:0:0: +383,105,26571,5,2,0:0:0:0: +299,105,26753,2,0,B|215:105|215:105|215:193,1,170,2|0,0:0|0:0,0:0:0:0: +391,105,27299,1,8,0:0:0:0: +239,193,27662,2,0,L|239:281,1,85,2|0,0:0|0:0,0:0:0:0: +323,277,28026,5,0,0:0:0:0: +231,277,28208,1,2,0:0:0:0: +315,277,28390,1,0,0:0:0:0: +143,277,28571,1,2,0:0:0:0: +315,277,28753,1,8,0:0:0:0: +407,277,28935,2,0,B|508:276|508:276|508:168,1,203.999993774414,2|0,0:0|0:0,0:0:0:0: +212,192,29480,6,0,B|108:192|108:192|108:92|108:92|212:92,1,305.999990661621,6|0,0:0|0:0,0:0:0:0: +304,92,30208,2,0,L|392:92,2,85,2|0|2,0:0|0:0|0:0,0:0:0:0: +152,96,30935,6,0,L|152:180,1,85,2|2,0:0|0:0,0:0:0:0: +236,192,31299,2,0,L|236:296,1,85,2|2,0:0|0:0,0:0:0:0: +320,276,31662,2,0,L|232:276,2,85,2|2|2,0:0|0:0|0:0,0:0:0:0: +256,192,32390,12,8,33480,0:0:0:0: +428,192,33844,6,0,L|428:132,2,42.5,2|2|2,0:0|0:0|0:0,0:0:0:0: +256,192,34208,2,0,L|160:192,1,85,2|8,0:0|0:0,0:0:0:0: +216,192,34480,1,2,0:0:0:0: +264,192,34571,2,0,L|316:192,2,42.5,2|8|2,0:0|0:0|0:0,0:0:0:0: +92,192,34935,2,0,L|8:192,1,85,12|8,0:0|0:0,0:0:0:0: +288,192,35299,6,0,L|492:192,1,203.999993774414,10|8,3:2|3:2,3:3:0:0: +400,192,35844,1,2,3:2:0:0: +224,192,36026,2,0,L|136:192,1,85,0|2,3:2|3:2,0:0:0:0: +232,192,36390,2,0,L|232:104,1,85,8|0,3:2|3:2,3:3:0:0: +56,32,36753,6,0,L|56:116,1,85,6|0,3:2|3:2,3:3:0:0: +104,120,37026,1,0,3:2:0:0: +152,124,37117,1,8,3:2:0:0: +244,124,37299,1,2,3:2:0:0: +152,124,37480,2,0,L|64:124,1,85,0|2,3:2|3:2,0:0:0:0: +244,124,37844,2,0,L|244:216,1,85,8|0,3:2|3:2,3:3:0:0: +496,296,38208,6,0,B|496:212|496:212|408:212,1,170,6|8,3:2|3:2,3:3:0:0: +504,212,38753,2,0,L|324:212,1,170,2|2,3:2|3:2,3:3:0:0: +156,192,39299,2,0,L|60:192,1,85,8|0,3:2|3:2,3:3:0:0: +252,192,39662,6,0,L|312:192,2,42.5,6|2|2,3:2|3:2|3:2,3:3:0:0: +71,192,40026,2,0,L|71:92,1,85,8|2,3:2|3:2,3:3:0:0: +164,108,40390,2,0,L|80:108,1,85,0|2,3:2|3:2,0:0:0:0: +256,108,40753,2,0,L|340:108,1,85,8|0,3:2|3:2,3:3:0:0: +84,192,41117,6,0,L|276:192,1,170,6|8,3:2|3:2,3:3:0:0: +432,192,41662,2,0,L|432:104,1,85,2|0,3:2|3:2,3:3:0:0: +348,108,42026,1,2,3:2:0:0: +432,192,42208,2,0,L|348:192,1,85,8|0,3:2|3:2,3:3:0:0: +176,192,42571,6,0,L|84:192,1,85,6|0,3:2|3:2,3:3:0:0: +132,192,42844,1,0,3:2:0:0: +176,192,42935,1,8,3:2:0:0: +260,192,43117,2,0,L|176:192,2,85,2|0|2,3:2|3:2|3:2,3:3:0:0: +84,192,43662,2,0,L|84:288,1,85,8|0,3:2|3:2,3:3:0:0: +336,192,44026,6,0,B|436:192|436:192|436:296,1,203.999993774414,6|8,3:2|3:2,3:3:0:0: +344,296,44571,1,2,3:2:0:0: +252,296,44753,2,0,L|252:212,1,85,0|2,3:2|3:2,3:3:0:0: +428,192,45117,2,0,L|340:192,1,85,8|0,3:2|3:2,3:3:0:0: +164,192,45480,5,6,3:2:0:0: +121,192,45570,1,2,3:2:0:0: +79,192,45661,1,2,3:2:0:0: +256,192,45844,2,0,L|256:104,1,85,8|2,3:2|3:2,3:3:0:0: +160,104,46208,2,0,L|244:104,1,85,2|2,3:2|3:2,3:3:0:0: +68,32,46571,2,0,L|68:120,1,85,12|2,3:2|3:2,3:3:0:0: +324,192,46935,6,0,L|408:192,2,85,10|0|8,3:2|3:2|3:2,3:3:0:0: +154,192,47480,2,0,L|338:192,1,170,2|2,3:2|3:2,3:3:0:0: +420,192,48026,2,0,L|420:280,1,85,8|0,3:2|3:2,3:3:0:0: +240,328,48390,6,0,B|156:328,1,85,6|0,3:2|3:2,3:3:0:0: +112,328,48662,1,0,3:2:0:0: +68,328,48753,1,8,3:2:0:0: +160,244,48935,2,0,L|72:244,2,85,2|0|2,3:2|3:2|3:2,0:0:0:0: +336,244,49480,2,0,L|420:244,1,85,8|0,3:2|3:2,3:3:0:0: +164,116,49844,6,0,B|80:116,1,85,6|0,3:2|3:2,3:3:0:0: +79,116,50117,1,0,3:2:0:0: +79,116,50208,1,8,3:2:0:0: +172,116,50390,2,0,B|256:116|256:116|256:28,1,170,2|2,3:2|3:2,3:3:0:0: +80,30,50935,2,0,L|80:126,1,85,8|0,3:2|3:2,3:3:0:0: +256,192,51299,6,0,L|436:192,1,170,6|8,3:2|3:2,3:3:0:0: +340,192,51844,1,2,3:2:0:0: +426,192,52026,2,0,L|338:192,1,85,0|2,3:2|3:2,3:3:0:0: +164,192,52390,2,0,L|64:192,1,85,8|0,3:2|0:0,3:3:0:0: +336,72,52753,6,0,L|508:72,1,170,6|8,3:2|3:2,3:3:0:0: +328,160,53299,2,0,L|500:160,1,170,2|2,3:2|3:2,3:3:0:0: +412,160,53844,2,0,L|412:260,1,85,8|0,3:2|3:2,3:3:0:0: +236,192,54208,6,0,L|144:192,1,85,6|0,3:2|3:2,3:3:0:0: +192,192,54480,1,0,3:2:0:0: +236,192,54571,1,8,3:2:0:0: +320,192,54753,1,2,3:2:0:0: +236,192,54935,1,0,3:2:0:0: +152,192,55117,1,2,3:2:0:0: +328,192,55299,2,0,L|328:280,1,85,8|0,3:2|3:2,3:3:0:0: +72,192,55662,6,0,L|72:100,1,85,6|0,3:2|3:2,3:3:0:0: +116,104,55935,1,0,3:2:0:0: +160,100,56026,1,8,3:2:0:0: +244,100,56208,2,0,L|156:100,2,85,2|0|2,3:2|3:2|3:2,3:3:0:0: +72,107,56753,2,0,L|72:19,1,85,8|0,3:2|3:2,0:0:0:0: +248,192,57117,6,0,L|292:192,2,42.5,2|2|2,3:2|3:2|3:2,0:0:0:0: +78,192,57481,2,0,L|80:92,1,85,8|2,3:2|3:2,0:0:0:0: +164,107,57844,2,0,L|64:107,1,85,8|2,3:2|3:2,3:3:0:0: +248,192,58208,2,0,L|164:192,1,85,12|2,3:2|3:2,3:3:0:0: +416,192,58571,6,0,B|500:192|500:192|412:192,1,170,10|8,3:2|3:2,3:3:0:0: +320,192,59117,1,2,3:2:0:0: +140,192,59299,2,0,L|56:192,2,85,0|2|8,3:2|3:2|3:2,0:0:0:0: +428,192,60026,6,0,L|428:104,1,85,2|0,3:2|3:2,3:3:0:0: +332,108,60390,2,0,L|420:108,1,85,8|2,3:2|3:2,3:3:0:0: +324,108,60753,1,2,3:2:0:0: +366,108,60843,1,2,3:2:0:0: +409,108,60934,1,2,3:2:0:0: +228,108,61117,2,0,L|140:108,1,85,8|0,3:2|3:2,3:3:0:0: +324,108,61480,6,0,L|324:280,1,170,2|8,3:2|3:2,3:3:0:0: +228,280,62026,1,2,3:2:0:0: +408,192,62208,2,0,L|312:192,2,85,0|2|8,3:2|3:2|3:2,3:3:0:0: +120,192,62935,6,0,L|72:192,2,42.5,2|2|2,3:2|3:2|3:2,0:0:0:0: +216,192,63299,2,0,L|216:96,1,85,8|0,3:2|3:2,3:3:0:0: +396,60,63662,2,0,L|312:60,1,85,2|0,3:2|3:2,3:3:0:0: +148,192,64026,1,8,3:2:0:0: +320,60,64208,1,2,3:2:0:0: +140,192,64390,6,0,B|56:192|56:192|56:104,1,170,2|8,3:2|3:2,0:0:0:0: +140,104,64935,1,2,3:2:0:0: +396,145,65117,2,0,L|396:57,1,85,0|2,3:2|3:2,0:0:0:0: +312,61,65480,1,8,3:2:0:0: +404,61,65662,1,0,3:2:0:0: +300,60,65844,6,0,L|212:60,1,85,2|0,3:2|3:2,3:3:0:0: +392,60,66208,2,0,L|392:160,1,85,8|2,3:2|3:2,3:3:0:0: +136,192,66571,2,0,L|136:104,1,85,2|2,3:2|3:2,3:3:0:0: +307,145,66935,2,0,L|395:145,1,85,8|0,3:2|3:2,3:3:0:0: +476,144,67299,6,0,L|476:244,1,85,2|0,3:2|3:2,3:3:0:0: +307,145,67662,2,0,L|307:45,1,85,8|2,3:2|3:2,3:3:0:0: +48,192,68026,2,0,L|140:192,1,85,0|2,3:2|3:2,3:3:0:0: +307,145,68390,2,0,L|307:233,1,85,8|0,3:2|3:2,3:3:0:0: +222,230,68753,6,0,L|326:230,1,85,2|2,3:2|3:2,0:0:0:0: +136,228,69117,2,0,L|136:324,1,85,8|2,3:2|3:2,3:3:0:0: +228,312,69480,2,0,L|132:312,1,85,2|2,3:2|3:2,3:3:0:0: +236,312,69844,2,0,L|327:312,1,85,8|0,3:2|3:2,3:3:0:0: +60,312,70208,6,0,B|60:228|60:228|148:228,1,170,10|8,3:2|3:2,3:3:0:0: +232,228,70753,1,2,3:2:0:0: +412,192,70935,2,0,L|320:192,2,85,0|2|8,3:2|3:2|3:2,0:0:0:0: +124,192,71662,6,0,L|124:104,1,85,2|0,3:2|3:2,3:3:0:0: +220,108,72026,2,0,L|320:108,1,85,8|2,3:2|3:2,3:3:0:0: +212,108,72389,1,2,3:2:0:0: +316,108,72571,1,2,3:2:0:0: +136,108,72753,2,0,L|48:108,1,85,8|0,3:2|3:2,3:3:0:0: +316,108,73116,6,0,B|400:108|400:108|400:200,1,170,2|8,3:2|3:2,3:3:0:0: +316,192,73662,1,2,3:2:0:0: +144,192,73844,1,2,3:2:0:0: +236,192,74026,1,2,3:2:0:0: +328,192,74208,1,8,3:2:0:0: +56,192,74571,5,2,3:2:0:0: +228,192,74753,1,2,3:2:0:0: +400,192,74935,2,0,L|400:96,1,85,8|0,3:2|3:2,3:3:0:0: +308,108,75298,2,0,L|392:108,1,85,2|2,3:2|3:2,3:3:0:0: +232,192,75662,1,8,3:2:0:0: +401,107,75844,1,2,3:2:0:0: +224,192,76026,6,0,B|140:192|140:192|228:192,1,170,2|8,3:2|3:2,0:0:0:0: +312,192,76571,1,2,3:2:0:0: +56,192,76753,2,0,L|56:104,1,85,0|2,3:2|3:2,0:0:0:0: +140,108,77116,1,8,3:2:0:0: +48,108,77298,1,0,3:2:0:0: +148,107,77480,6,0,L|236:107,1,85,2|0,3:2|3:2,3:3:0:0: +408,108,77844,2,0,L|408:208,1,85,8|2,3:2|3:2,3:3:0:0: +236,192,78207,2,0,L|320:192,1,85,0|2,3:2|3:2,3:3:0:0: +493,193,78571,2,0,L|409:193,1,85,8|0,3:2|3:2,3:3:0:0: +504,192,78935,5,2,3:2:0:0: +332,192,79117,1,2,3:2:0:0: +284,192,79208,1,0,0:0:0:0: +236,192,79298,2,0,L|236:92,1,85,8|0,3:2|3:2,3:3:0:0: +60,28,79662,2,0,L|60:119,1,85,0|2,3:2|3:2,3:3:0:0: +236,107,80026,2,0,L|328:107,1,85,8|2,3:2|3:2,3:3:0:0: +228,108,80389,5,2,3:2:0:0: +228,150,80479,1,2,3:2:0:0: +228,193,80570,1,2,3:2:0:0: +404,192,80753,2,0,L|404:288,1,85,8|2,3:2|3:2,3:3:0:0: +227,280,81116,2,0,L|323:280,1,85,0|2,3:2|3:2,3:3:0:0: +404,277,81480,2,0,L|313:277,1,85,8|2,3:2|3:2,3:3:0:0: +133,193,81844,6,0,L|89:193,2,42.5,2|2|2,3:2|3:2|3:2,0:0:0:0: +303,193,82208,2,0,L|217:193,1,85,8|0,3:2|3:2,3:3:0:0: +264,192,82480,1,2,3:2:0:0: +313,193,82572,2,0,L|229:193,1,85,8|2,3:2|3:2,3:3:0:0: +48,193,82935,2,0,L|132:193,1,85,12|0,3:2|3:2,3:3:0:0: +392,192,83299,6,0,B|496:192|496:192|496:88,1,203.999993774414,10|8,3:2|3:2,0:0:0:0: +452,92,83753,1,0,3:2:0:0: +408,92,83844,1,2,3:2:0:0: +324,92,84026,2,0,L|324:-8,1,85,0|2,3:2|3:2,3:3:0:0: +152,8,84390,2,0,L|152:56,1,42.5,8|2,3:2|3:2,3:3:0:0: +248,92,84662,1,2,3:2:0:0: +248,92,84753,6,0,L|156:92,1,85,2|0,3:2|3:2,3:3:0:0: +332,92,85117,2,0,L|332:152,2,42.5,8|0|2,3:2|3:2|3:2,3:3:0:0: +244,192,85480,1,0,3:2:0:0: +332,92,85662,1,2,3:2:0:0: +156,192,85844,2,0,L|68:192,1,85,8|2,3:2|3:2,3:3:0:0: +164,192,86208,6,0,L|256:192,1,85,2|0,3:2|3:2,3:3:0:0: +80,296,86571,1,8,3:2:0:0: +122,296,86661,1,0,3:2:0:0: +165,296,86752,1,2,3:2:0:0: +252,296,86935,1,0,3:2:0:0: +156,296,87117,1,2,3:2:0:0: +328,296,87299,2,0,L|328:232,1,42.5,8|2,3:2|3:2,3:3:0:0: +152,192,87662,6,0,L|104:192,2,42.5,2|0|2,3:2|3:2|3:2,0:0:0:0: +236,192,88026,2,0,L|144:192,1,85,8|2,3:2|3:2,3:3:0:0: +328,192,88390,2,0,L|328:104,1,85,2|2,3:2|3:2,3:3:0:0: +152,32,88753,2,0,L|64:32,1,85,8|0,3:2|3:2,3:3:0:0: +324,32,89117,6,0,L|496:32,1,170,2|8,3:2|3:2,3:3:0:0: +452,32,89571,1,0,3:2:0:0: +408,32,89662,1,0,3:2:0:0: +324,32,89844,2,0,L|324:128,1,85,2|2,3:2|3:2,3:3:0:0: +148,192,90208,2,0,L|148:244,1,42.5,8|2,3:2|3:2,3:3:0:0: +232,192,90480,1,2,3:2:0:0: +284,192,90571,6,0,L|284:280,1,85,2|0,3:2|3:2,3:3:0:0: +236,316,90844,2,0,L|144:316,1,85,2|0,3:2|3:2,3:3:0:0: +152,316,91117,1,2,3:2:0:0: +236,316,91299,1,2,3:2:0:0: +144,316,91480,1,2,3:2:0:0: +320,316,91662,2,0,L|320:216,1,85,8|2,3:2|3:2,3:3:0:0: +224,192,92026,6,0,L|136:192,1,85,2|0,3:2|3:2,3:3:0:0: +92,192,92299,2,0,L|184:192,1,85,2|0,3:2|3:2,3:3:0:0: +224,192,92571,1,2,3:2:0:0: +132,192,92753,2,0,L|216:192,1,85,2|2,3:2|3:2,3:3:0:0: +392,192,93117,2,0,L|392:104,1,85,8|2,3:2|3:2,0:0:0:0: +216,44,93480,5,2,3:2:0:0: +173,44,93570,1,2,3:2:0:0: +131,44,93661,1,2,3:2:0:0: +224,128,93844,1,8,3:2:0:0: +181,128,93934,1,0,3:2:0:0: +139,128,94025,1,2,3:2:0:0: +312,128,94208,2,0,L|396:128,1,85,8|2,3:2|3:2,3:3:0:0: +220,224,94571,2,0,L|136:224,1,85,12|2,3:2|3:2,3:3:0:0: +392,224,94935,6,0,L|484:224,1,85,10|0,3:2|3:2,3:3:0:0: +384,224,95299,2,0,L|384:128,1,85,8|0,3:2|3:2,3:3:0:0: +212,224,95662,1,2,3:2:0:0: +306,224,95844,1,2,3:2:0:0: +477,224,96026,2,0,L|477:136,1,85,8|0,3:2|3:2,3:3:0:0: +300,136,96390,6,0,L|212:136,1,85,6|0,3:2|3:2,3:3:0:0: +308,136,96753,2,0,L|308:44,1,85,8|2,3:2|3:2,3:2:0:0: +136,136,97117,1,2,3:2:0:0: +300,136,97299,1,2,3:2:0:0: +128,136,97480,2,0,L|128:40,1,85,8|0,3:2|3:2,3:3:0:0: +300,136,97844,6,0,L|212:136,1,85,6|0,3:2|3:2,3:3:0:0: +308,136,98208,1,8,3:2:0:0: +308,93,98298,1,0,0:0:0:0: +308,51,98389,1,0,3:2:0:0: +136,40,98571,2,0,L|224:40,1,85,2|2,3:2|3:2,3:3:0:0: +404,140,98935,2,0,L|404:240,1,85,8|0,3:2|3:2,0:0:0:0: +224,288,99299,6,0,L|136:288,1,85,2|2,3:2|3:2,3:3:0:0: +312,288,99662,2,0,L|312:196,1,85,8|2,3:2|3:2,3:3:0:0: +220,192,100026,1,0,3:2:0:0: +312,288,100208,1,2,3:2:0:0: +136,192,100390,2,0,L|52:192,1,85,8|0,3:2|3:2,3:3:0:0: +308,192,100753,6,0,B|392:192,1,85,6|0,3:2|3:2,3:3:0:0: +216,192,101117,2,0,L|216:104,1,85,8|2,3:2|3:2,3:3:0:0: +300,108,101480,1,0,3:2:0:0: +208,108,101662,1,2,3:2:0:0: +384,108,101844,2,0,L|384:12,1,85,8|0,3:2|3:2,3:3:0:0: +208,108,102208,6,0,L|104:108,1,85,6|0,3:2|3:2,3:3:0:0: +216,108,102571,2,0,L|216:192,1,85,8|2,3:2|3:2,0:0:0:0: +52,108,102935,1,2,3:2:0:0: +224,192,103117,1,2,3:2:0:0: +44,108,103299,2,0,L|44:204,1,85,8|2,3:2|3:2,3:3:0:0: +136,192,103662,6,0,L|224:192,1,85,6|0,3:2|3:2,3:3:0:0: +268,192,103935,1,0,3:2:0:0: +316,192,104026,2,0,L|316:96,1,85,8|2,3:2|3:2,3:3:0:0: +140,36,104390,2,0,L|228:36,1,85,2|2,3:2|3:2,0:0:0:0: +400,36,104753,2,0,L|400:136,1,85,8|0,3:2|3:2,3:3:0:0: +224,192,105117,5,2,3:2:0:0: +181,192,105207,1,2,3:2:0:0: +139,192,105298,1,2,3:2:0:0: +309,192,105480,2,0,L|221:192,1,85,8|2,3:2|3:2,3:3:0:0: +128,192,105844,1,0,3:2:0:0: +216,192,106026,1,2,3:2:0:0: +393,192,106208,2,0,L|493:192,1,85,12|0,3:2|0:0,3:3:0:0: +216,276,106571,6,0,L|128:276,1,85,6|0,3:2|3:2,3:3:0:0: +84,276,106844,1,0,3:2:0:0: +131,276,106935,2,0,L|216:276,1,85,8|2,3:2|3:2,3:3:0:0: +312,276,107299,1,0,3:2:0:0: +212,276,107480,1,2,3:2:0:0: +392,276,107662,2,0,L|392:176,1,85,8|2,3:2|3:2,3:3:0:0: +136,192,108026,6,0,B|44:192,1,85,6|0,3:2|3:2,3:3:0:0: +144,192,108390,2,0,L|144:104,1,85,8|0,3:2|3:2,0:0:0:0: +304,68,108753,1,2,3:2:0:0: +140,192,108935,1,2,3:2:0:0: +312,68,109117,2,0,L|312:168,1,85,8|2,3:2|3:2,3:3:0:0: +56,192,109480,6,0,L|56:284,1,85,6|0,3:2|3:2,3:3:0:0: +140,280,109844,1,8,3:2:0:0: +182,280,109934,1,0,3:2:0:0: +225,280,110025,1,2,3:2:0:0: +56,277,110208,1,2,3:2:0:0: +152,280,110390,1,2,3:2:0:0: +52,277,110571,2,0,L|52:189,1,85,8|0,3:2|0:0,3:3:0:0: +312,192,110935,6,0,L|396:192,1,85,2|2,3:2|3:2,3:3:0:0: +304,192,111299,1,8,3:2:0:0: +404,192,111480,1,2,3:2:0:0: +312,192,111662,1,0,3:2:0:0: +269,192,111752,1,0,3:2:0:0: +227,192,111843,1,2,3:2:0:0: +328,192,112026,2,0,L|328:96,1,85,8|0,3:2|3:2,3:3:0:0: +68,192,112390,6,0,L|68:104,1,85,6|0,3:2|3:2,3:3:0:0: +160,108,112753,2,0,L|248:108,1,85,8|2,3:2|3:2,0:0:0:0: +420,108,113117,2,0,L|420:196,1,85,0|2,3:2|3:2,0:0:0:0: +328,192,113480,1,8,3:2:0:0: +285,192,113570,1,0,0:0:0:0: +243,192,113661,1,0,3:2:0:0: +492,192,113844,6,4,L|492:292,1,85,6|4,3:2|3:2,3:3:0:0: +396,276,114208,2,0,L|304:276,1,85,8|2,3:2|3:2,3:3:0:0: +140,276,114571,1,2,3:2:0:0: +311,276,114753,1,2,3:2:0:0: +140,276,114935,2,0,L|140:192,1,85,8|0,3:2|3:2,3:3:0:0: +396,192,115299,6,0,L|492:192,1,85,6|0,3:2|3:2,3:3:0:0: +308,192,115662,2,0,L|308:96,1,85,8|2,3:2|3:2,0:0:0:0: +136,192,116026,1,2,3:2:0:0: +228,192,116208,1,2,3:2:0:0: +56,192,116390,2,0,L|56:96,1,85,8|2,3:2|3:2,0:0:0:0: +312,192,116753,6,0,L|312:96,1,85,10|2,3:2|3:2,3:3:0:0: +484,28,117117,2,0,L|484:84,2,42.5,8|2|2,3:2|3:2|3:2,3:3:0:0: +392,28,117480,1,8,3:2:0:0: +476,28,117662,1,2,3:2:0:0: +304,28,117844,1,8,3:2:0:0: +262,28,117934,1,2,3:2:0:0: +219,28,118025,1,2,3:2:0:0: +476,28,118208,5,0,0:0:0:0: +476,28,118299,1,0,0:0:0:0: +432,28,118390,1,0,0:0:0:0: +260,132,118571,1,0,0:0:0:0: +260,132,118662,1,0,0:0:0:0: +260,132,118753,1,0,0:0:0:0: +88,236,118935,1,8,0:0:0:0: +88,236,119026,1,2,0:0:0:0: +132,236,119117,1,2,0:0:0:0: +304,288,119299,2,0,L|392:288,1,85,8|8,0:0|0:0,0:0:0:0: +112,236,119662,5,10,0:0:0:0: +256,192,120026,12,0,125480,0:0:0:0: +296,284,131299,6,0,L|296:108,1,170,4|0,0:0|0:0,0:0:0:0: +152,192,132026,1,2,0:0:0:0: +244,192,132208,1,0,0:0:0:0: +336,192,132390,1,0,0:0:0:0: +244,192,132571,1,0,0:0:0:0: +416,192,132753,6,0,L|416:20,2,170,2|0|0,0:0|0:0|0:0,0:0:0:0: +280,192,133844,1,0,0:0:0:0: +188,192,134026,1,0,0:0:0:0: +16,192,134208,6,0,L|16:16,1,170,2|0,0:0|0:0,0:0:0:0: +176,20,134935,1,2,0:0:0:0: +32,24,135299,1,0,0:0:0:0: +272,16,135662,6,0,L|272:192,1,170,2|0,0:0|0:0,0:0:0:0: +428,80,136390,2,0,L|428:272,1,170,2|0,0:0|0:0,0:0:0:0: +132,52,137117,6,0,B|304:52,2,170,4|8|8,0:0|0:0|0:0,0:0:0:0: +336,52,138571,6,0,L|336:224,1,170,2|0,0:0|0:0,0:0:0:0: +240,224,139117,1,0,0:0:0:0: +336,222,139299,1,2,0:0:0:0: +480,192,139662,1,2,0:0:0:0: +388,192,139844,1,0,0:0:0:0: +212,192,140026,6,0,L|212:364,2,170,2|0|2,0:0|0:0|0:0,0:0:0:0: +448,192,141480,6,0,L|344:192,2,93.5000028533936,8|8|8,0:0|0:0|0:0,0:0:0:0: +244,192,142208,1,8,0:0:0:0: +348,192,142390,1,8,0:0:0:0: +448,192,142571,1,8,0:0:0:0: +152,192,142935,6,0,L|152:12,1,170,4|0,0:0|0:0,0:0:0:0: +236,20,143480,1,0,0:0:0:0: +144,20,143662,2,0,L|60:20,2,85,2|0|0,0:0|0:0|0:0,0:0:0:0: +316,136,144390,5,2,0:0:0:0: +232,136,144571,1,0,0:0:0:0: +148,136,144753,1,0,0:0:0:0: +316,136,145117,2,0,L|232:136,2,85,2|0|0,0:0|0:0|0:0,0:0:0:0: +144,136,145844,6,0,L|144:224,1,85,2|0,0:0|0:0,0:0:0:0: +228,220,146208,1,2,0:0:0:0: +59,221,146571,2,0,L|159:221,2,85,2|0|0,0:0|0:0|0:0,0:0:0:0: +228,224,147299,6,0,L|312:224,1,85,2|0,0:0|0:0,0:0:0:0: +220,224,147662,2,0,L|220:320,1,85,0|0,0:0|0:0,0:0:0:0: +313,309,148026,2,0,L|313:225,1,85,2|0,0:0|0:0,0:0:0:0: +228,224,148390,1,0,0:0:0:0: +320,224,148571,1,0,0:0:0:0: +64,276,148753,6,0,L|64:192,1,85,4|0,0:0|0:0,0:0:0:0: +152,192,149117,2,0,L|152:104,1,85,2|0,0:0|0:0,0:0:0:0: +328,108,149480,1,2,0:0:0:0: +184,108,149844,2,0,L|268:108,1,85,2|0,0:0|0:0,0:0:0:0: +356,108,150208,5,2,0:0:0:0: +204,108,150571,2,0,L|204:208,1,85,2|0,0:0|0:0,0:0:0:0: +28,192,150935,1,2,0:0:0:0: +172,192,151299,2,0,L|256:192,1,85,2|0,0:0|0:0,0:0:0:0: +164,192,151662,6,0,L|164:292,1,85,2|0,0:0|0:0,0:0:0:0: +257,277,152026,2,0,L|257:193,1,85,2|0,0:0|0:0,0:0:0:0: +432,192,152390,1,2,0:0:0:0: +288,192,152753,2,0,L|200:192,1,85,2|0,0:0|0:0,0:0:0:0: +380,192,153117,6,0,L|380:104,1,85,8|8,0:0|0:0,0:0:0:0: +288,108,153480,2,0,L|288:20,1,85,8|0,0:0|0:0,0:0:0:0: +112,24,153844,2,0,L|112:108,1,85,8|8,0:0|0:0,0:0:0:0: +203,108,154208,2,0,L|291:108,1,85,8|0,0:0|0:0,0:0:0:0: +32,108,154571,6,0,L|32:288,1,170,4|0,0:0|0:0,0:0:0:0: +216,278,155299,1,2,0:0:0:0: +124,278,155480,1,0,0:0:0:0: +32,278,155662,1,0,0:0:0:0: +216,278,156026,6,0,L|304:280,1,85,8|0,0:0|0:0,0:0:0:0: +300,279,156390,1,0,0:0:0:0: +132,192,156753,2,0,L|220:192,2,85,2|0|0,0:0|0:0|0:0,0:0:0:0: +48,192,157299,1,0,0:0:0:0: +140,192,157480,6,0,L|140:104,1,85,8|0,0:0|0:0,0:0:0:0: +236,108,157844,2,0,L|236:20,1,85,0|0,0:0|0:0,0:0:0:0: +412,48,158208,2,0,L|496:48,2,85,2|0|0,0:0|0:0|0:0,0:0:0:0: +268,192,158935,5,8,0:0:0:0: +344,192,159117,1,8,0:0:0:0: +420,192,159299,1,8,0:0:0:0: +496,192,159480,1,8,0:0:0:0: +412,192,159662,2,0,L|496:192,1,85,2|0,0:0|0:0,0:0:0:0: +324,192,160026,2,0,L|324:104,1,85,2|0,0:0|0:0,0:0:0:0: +68,192,160390,6,0,L|68:108,1,85,10|0,0:0|0:0,0:0:0:0: +152,108,160753,2,0,L|240:108,1,85,8|0,0:0|0:0,0:0:0:0: +409,107,161117,2,0,L|409:191,1,85,2|2,0:0|0:0,0:0:0:0: +324,192,161480,2,0,L|412:192,1,85,8|0,0:0|0:0,0:0:0:0: +313,191,161844,6,0,L|313:299,1,85,2|0,0:0|0:0,0:0:0:0: +140,192,162208,2,0,L|140:284,1,85,8|0,0:0|0:0,0:0:0:0: +184,276,162480,1,0,0:0:0:0: +228,276,162571,2,0,L|312:276,1,85,2|2,0:0|0:0,0:0:0:0: +400,276,162935,2,0,L|400:192,1,85,8|8,0:0|0:0,0:0:0:0: +256,192,163299,12,8,164389,0:0:0:0: +132,192,164753,6,0,L|132:132,2,42.5,8|2|2,0:0|0:0|0:0,0:0:0:0: +304,192,165117,1,8,0:0:0:0: +352,173,165207,1,2,0:0:0:0: +372,125,165298,1,2,0:0:0:0: +351,78,165389,1,2,0:0:0:0: +303,59,165480,1,8,0:0:0:0: +208,60,165662,1,2,0:0:0:0: +388,8,165844,2,0,L|472:8,1,85,12|0,0:0|0:0,0:0:0:0: +216,192,166208,6,0,L|120:192,2,85,6|0|8,3:2|3:2|3:2,3:3:0:0: +308,192,166753,2,0,L|136:192,1,170,6|2,3:2|3:2,3:3:0:0: +312,192,167299,2,0,L|312:296,1,85,8|0,3:2|3:2,3:3:0:0: +138,192,167662,6,0,L|310:192,1,170,6|8,3:2|3:2,3:3:0:0: +404,192,168208,2,0,B|404:276|404:276|316:276,1,170,2|2,3:2|3:2,3:3:0:0: +140,336,168753,2,0,L|140:248,1,85,8|0,3:2|3:2,3:3:0:0: +320,192,169117,6,0,B|404:192|404:192|404:104,1,170,2|8,3:2|3:2,3:3:0:0: +232,32,169662,2,0,L|52:32,1,170,2|2,3:2|3:2,3:3:0:0: +232,32,170208,2,0,L|128:32,1,85,8|0,3:2|3:2,3:3:0:0: +52,32,170571,6,0,L|52:88,1,42.5,2|2,3:2|3:2,3:3:0:0: +100,76,170753,1,2,3:2:0:0: +192,76,170935,1,8,3:2:0:0: +448,192,171117,2,0,L|448:104,1,85,2|0,3:2|3:2,0:0:0:0: +356,104,171480,1,0,3:2:0:0: +184,192,171662,2,0,L|268:192,1,85,8|0,3:2|3:2,3:3:0:0: +20,192,172026,6,0,L|20:144,2,42.5,6|0|0,3:2|3:2|3:2,3:3:0:0: +116,192,172390,1,8,3:2:0:0: +32,192,172571,1,2,3:2:0:0: +208,192,172753,2,0,L|312:192,1,85,0|2,3:2|3:2,3:3:0:0: +200,192,173117,2,0,L|200:280,1,85,8|0,3:2|3:2,3:3:0:0: +376,192,173480,6,0,L|376:108,1,85,6|0,3:2|3:2,3:3:0:0: +200,192,173844,1,8,3:2:0:0: +116,192,174026,2,0,P|64:132|116:76,1,170,2|2,3:2|3:2,3:3:0:0: +372,76,174571,2,0,L|460:76,1,85,8|0,3:2|3:2,3:3:0:0: +280,76,174935,6,0,L|280:172,1,85,2|2,3:2|3:2,3:3:0:0: +368,192,175299,1,8,3:2:0:0: +192,192,175480,2,0,L|192:288,1,85,2|0,3:2|3:2,3:3:0:0: +280,308,175844,1,2,3:2:0:0: +453,192,176026,2,0,L|365:192,1,85,8|2,3:2|3:2,0:0:0:0: +112,192,176390,6,0,L|20:192,2,85,8|2|8,3:2|3:2|3:2,3:3:0:0: +292,192,176935,2,0,L|116:192,1,170,2|2,3:2|3:2,3:3:0:0: +304,192,177480,2,0,L|402:192,1,85,8|0,3:2|3:2,3:3:0:0: +132,192,177844,6,0,B|32:192|32:192|32:88,1,203.999993774414,6|8,3:2|3:2,3:3:0:0: +208,44,178390,2,0,L|380:44,1,170,6|2,3:2|3:2,3:3:0:0: +284,44,178935,2,0,L|284:140,1,85,8|0,3:2|3:2,3:3:0:0: +464,136,179299,6,0,L|464:232,1,85,2|0,3:2|3:2,3:3:0:0: +380,220,179662,1,8,3:2:0:0: +204,192,179844,2,0,L|376:192,1,170,2|2,3:2|1:3,3:3:0:0: +460,192,180390,2,0,L|460:92,1,85,8|0,3:2|3:2,3:3:0:0: +284,16,180753,6,0,B|200:16|200:16|200:104,1,170,2|8,3:2|3:2,3:3:0:0: +380,192,181299,2,0,L|204:192,1,170,2|2,3:2|3:2,3:3:0:0: +302,193,181844,2,0,L|210:193,1,85,8|0,3:2|3:2,3:3:0:0: +124,192,182208,6,0,L|124:288,1,85,2|2,3:2|3:2,3:3:0:0: +302,193,182571,2,0,L|210:193,1,85,8|2,3:2|3:2,0:0:0:0: +312,192,182935,2,0,L|360:192,2,42.5,0|0|2,3:2|3:2|3:2,3:3:0:0: +132,192,183299,2,0,L|32:192,1,85,8|0,3:2|3:2,3:3:0:0: +312,192,183662,6,0,P|364:248|312:308,1,170,6|8,3:2|3:2,3:3:0:0: +220,308,184208,1,2,3:2:0:0: +324,308,184390,2,0,L|324:216,1,85,0|2,3:2|3:2,3:3:0:0: +144,192,184753,2,0,L|144:280,1,85,8|0,3:2|3:2,3:3:0:0: +324,224,185117,6,0,L|408:224,1,85,2|2,3:2|3:2,3:3:0:0: +232,192,185480,2,0,L|232:96,1,85,8|2,3:2|3:2,3:3:0:0: +316,108,185844,1,0,3:2:0:0: +232,108,186026,1,2,3:2:0:0: +408,108,186208,2,0,L|408:16,1,85,8|0,3:2|3:2,3:3:0:0: +152,20,186571,6,0,B|68:20|68:20|156:20,1,170,6|0,3:2|3:2,3:3:0:0: +332,132,187117,2,0,L|152:132,1,170,6|2,3:2|3:2,3:3:0:0: +76,132,187662,2,0,L|76:216,1,85,8|0,3:2|3:2,3:3:0:0: +252,280,188026,5,2,3:2:0:0: +294,280,188116,1,2,3:2:0:0: +337,280,188207,1,2,3:2:0:0: +176,280,188390,1,8,3:2:0:0: +344,280,188571,2,0,P|396:232|344:168,1,170,6|2,3:2|3:2,3:3:0:0: +168,192,189117,2,0,L|80:192,1,85,8|0,3:2|3:2,3:3:0:0: +344,168,189480,6,0,B|448:168|448:168|448:64,1,203.999993774414,10|8,3:2|3:2,3:3:0:0: +352,68,190026,2,0,L|172:68,1,170,0|2,3:2|3:2,0:0:0:0: +276,68,190571,2,0,L|276:164,1,85,8|0,3:2|3:2,3:3:0:0: +96,192,190935,6,0,L|96:96,1,85,2|0,3:2|3:2,3:3:0:0: +192,104,191299,2,0,L|100:104,1,85,8|2,3:2|3:2,3:3:0:0: +284,192,191662,2,0,L|372:192,1,85,0|2,3:2|3:2,3:3:0:0: +464,192,192026,2,0,L|464:148,1,42.5,8|0,3:2|0:0,3:3:0:0: +420,132,192208,1,0,3:2:0:0: +240,192,192390,6,0,L|64:192,1,170,2|8,3:2|3:2,3:3:0:0: +156,192,192935,1,2,3:2:0:0: +64,192,193117,2,0,L|64:100,1,85,2|2,3:2|3:2,3:3:0:0: +156,192,193480,2,0,L|156:108,1,85,8|0,3:2|3:2,3:3:0:0: +332,192,193844,6,0,L|376:192,2,42.5,2|2|2,3:2|3:2|3:2,0:0:0:0: +156,192,194208,2,0,L|244:192,1,85,8|0,3:2|3:2,3:3:0:0: +328,192,194571,1,2,3:2:0:0: +236,192,194753,1,2,3:2:0:0: +416,192,194935,2,0,L|416:284,1,85,8|0,3:2|3:2,3:3:0:0: +160,336,195299,6,0,B|76:336|76:336|76:244,1,170,6|8,3:2|3:2,3:3:0:0: +164,192,195844,2,0,L|344:192,1,170,6|2,3:2|3:2,3:3:0:0: +240,192,196389,2,0,L|240:96,1,85,8|0,3:2|3:2,3:3:0:0: +420,68,196753,6,0,L|420:164,1,85,6|2,3:2|3:2,3:3:0:0: +372,156,197026,1,2,3:2:0:0: +324,156,197117,2,0,L|240:156,1,85,8|2,3:2|3:2,3:3:0:0: +332,156,197480,2,0,L|332:72,1,85,0|2,3:2|3:2,3:3:0:0: +152,20,197844,2,0,L|108:20,2,42.5,8|0|0,3:2|3:2|3:2,0:0:0:0: +328,192,198208,6,0,L|504:192,1,170,6|8,3:2|3:2,3:3:0:0: +412,192,198753,1,2,3:2:0:0: +236,192,198935,2,0,L|236:100,1,85,2|2,3:2|3:2,3:3:0:0: +328,192,199298,2,0,L|240:192,1,85,8|2,3:2|3:2,0:0:0:0: +64,192,199662,6,0,L|64:280,1,85,6|2,3:2|3:2,3:3:0:0: +160,276,200026,1,8,3:2:0:0: +112,276,200116,1,2,3:2:0:0: +64,277,200207,1,8,3:2:0:0: +240,192,200390,2,0,L|240:280,1,85,8|2,3:2|3:2,3:3:0:0: +416,192,200753,2,0,L|508:192,1,85,8|2,3:2|3:2,3:3:0:0: +240,192,201117,6,0,L|36:192,1,203.999993774414,6|8,3:2|3:2,3:3:0:0: +128,192,201662,2,0,B|216:192|216:192|216:104,1,170,2|2,3:2|3:2,0:0:0:0: +40,16,202208,2,0,L|40:104,1,85,8|0,3:2|3:2,3:3:0:0: +216,110,202571,6,0,L|308:110,1,85,6|2,3:2|3:2,3:3:0:0: +348,112,202844,1,2,3:2:0:0: +396,112,202935,2,0,L|396:24,1,85,8|2,3:2|3:2,3:3:0:0: +492,28,203299,2,0,L|404:28,1,85,4|2,3:2|3:2,3:3:0:0: +232,32,203662,2,0,L|232:116,1,85,8|0,3:2|3:2,0:0:0:0: +408,192,204026,6,0,L|500:192,2,85,6|2|8,3:2|3:2|3:2,3:3:0:0: +316,192,204571,2,0,L|492:192,1,170,2|2,3:2|3:2,0:0:0:0: +308,192,205117,2,0,L|220:192,1,85,8|0,3:2|3:2,3:3:0:0: +48,192,205480,6,0,L|48:284,1,85,2|2,3:2|3:2,3:3:0:0: +224,192,205844,2,0,L|312:192,1,85,8|2,3:2|3:2,0:0:0:0: +216,192,206208,1,2,3:2:0:0: +320,192,206390,1,2,3:2:0:0: +144,192,206571,2,0,L|60:192,1,85,8|2,3:2|3:2,3:3:0:0: +320,192,206935,6,0,L|408:192,1,85,6|2,3:2|3:2,3:3:0:0: +405,192,207208,1,2,3:2:0:0: +405,192,207299,1,8,3:2:0:0: +312,192,207480,2,0,P|264:136|312:68,1,170,2|0,3:2|3:2,0:0:0:0: +488,68,208026,2,0,L|488:152,1,85,8|2,3:2|3:2,3:3:0:0: +308,192,208390,6,0,L|220:192,1,85,6|2,3:2|3:2,3:3:0:0: +404,192,208753,2,0,L|404:280,1,85,8|2,3:2|3:2,3:3:0:0: +308,276,209117,1,4,3:2:0:0: +392,276,209299,1,2,3:2:0:0: +216,276,209480,2,0,L|120:276,1,85,8|2,3:2|3:2,3:3:0:0: +308,276,209844,6,0,L|308:192,1,85,6|2,3:2|3:2,3:3:0:0: +264,192,210117,1,2,3:2:0:0: +220,192,210208,1,8,3:2:0:0: +308,192,210390,2,0,L|480:192,1,170,6|2,3:2|3:2,3:3:0:0: +296,192,210935,2,0,L|296:100,1,85,8|2,3:2|3:2,3:3:0:0: +120,28,211299,5,2,3:2:0:0: +120,70,211389,1,2,3:2:0:0: +120,113,211480,1,2,3:2:0:0: +296,192,211662,2,0,L|200:192,1,85,8|8,3:2|3:2,3:3:0:0: +120,113,212026,2,0,L|120:200,1,85,12|0,3:2|3:2,3:3:0:0: +296,192,212390,1,12,3:2:0:0: +196,192,212571,1,2,3:2:0:0: +456,192,212753,6,0,L|456:280,1,85,10|0,3:2|3:2,3:3:0:0: +276,336,213117,2,0,L|180:336,1,85,8|2,3:2|3:2,0:0:0:0: +284,336,213480,2,0,L|284:240,1,85,2|2,3:2|3:2,0:0:0:0: +104,192,213844,2,0,L|188:192,1,85,8|2,3:2|3:2,0:0:0:0: +448,192,214208,6,0,L|448:100,1,85,2|2,3:2|3:2,3:3:0:0: +400,108,214480,1,2,3:2:0:0: +352,108,214571,1,8,3:2:0:0: +448,192,214753,1,2,3:2:0:0: +272,192,214935,2,0,L|272:108,1,85,0|2,3:2|3:2,3:3:0:0: +96,192,215299,2,0,L|8:192,1,85,8|2,3:2|3:2,0:0:0:0: +272,192,215662,6,0,L|360:192,1,85,6|2,3:2|3:2,3:3:0:0: +180,192,216026,2,0,L|180:104,1,85,8|2,3:2|3:2,3:3:0:0: +356,192,216390,1,2,3:2:0:0: +256,192,216571,1,2,3:2:0:0: +436,192,216753,2,0,L|332:192,1,85,8|2,3:2|3:2,3:3:0:0: +96,192,217117,6,0,B|12:192|12:192|100:192,1,170,2|8,3:2|3:2,3:3:0:0: +276,192,217662,2,0,L|364:192,2,85,2|2|2,3:2|3:2|3:2,0:0:0:0: +98,192,218208,2,0,L|98:104,1,85,8|2,3:2|3:2,3:3:0:0: +360,192,218571,6,0,P|412:128|360:80,1,170,6|8,3:2|3:2,3:3:0:0: +312,80,219026,1,2,3:2:0:0: +264,80,219117,1,2,3:2:0:0: +88,80,219299,2,0,L|172:80,1,85,4|2,3:2|3:2,3:3:0:0: +268,80,219662,2,0,L|268:168,1,85,8|2,3:2|3:2,3:3:0:0: +88,192,220026,6,0,L|88:280,1,85,6|2,3:2|3:2,3:3:0:0: +268,164,220390,1,8,3:2:0:0: +180,192,220571,1,2,3:2:0:0: +436,192,220753,2,0,L|436:96,1,85,0|2,0:0|3:2,0:0:0:0: +260,44,221117,2,0,L|168:44,1,85,8|2,3:2|3:2,3:3:0:0: +436,192,221480,6,0,L|352:192,1,85,6|2,3:2|3:2,3:3:0:0: +308,192,221753,1,2,3:2:0:0: +264,192,221844,1,8,3:2:0:0: +356,192,222026,1,2,3:2:0:0: +100,192,222208,2,0,L|16:192,1,85,4|2,3:2|3:2,3:3:0:0: +108,192,222571,2,0,L|108:104,1,85,8|2,3:2|3:2,3:3:0:0: +368,192,222935,6,0,L|416:192,2,42.5,2|2|2,3:2|3:2|3:2,3:3:0:0: +188,192,223299,1,8,3:2:0:0: +280,192,223480,1,0,3:2:0:0: +328,192,223571,1,2,3:2:0:0: +376,192,223662,2,0,L|376:104,1,85,8|2,3:2|3:2,0:0:0:0: +196,48,224026,2,0,L|104:48,1,85,8|0,3:2|0:0,0:0:0:0: +376,24,224390,6,0,P|436:96|376:168,1,203.999993774414,14|2,0:0|0:0,0:0:0:0: +96,192,225117,2,0,L|96:280,1,85,8|0,0:0|0:0,0:0:0:0: +180,276,225480,1,2,0:0:0:0: +356,192,225662,1,2,0:0:0:0: +400,192,225753,1,0,0:0:0:0: +444,192,225844,6,0,L|444:280,1,85,0|0,0:0|0:0,0:0:0:0: +360,276,226208,2,0,L|276:276,1,85,2|2,0:0|0:0,0:0:0:0: +96,192,226571,2,0,L|96:276,1,85,8|0,0:0|0:0,0:0:0:0: +181,277,226935,2,0,L|97:277,1,85,2|0,0:0|0:0,0:0:0:0: +276,192,227299,6,0,B|360:192|360:192|360:104,1,170,2|2,0:0|0:0,0:0:0:0: +276,104,227844,1,2,0:0:0:0: +96,104,228026,2,0,L|96:188,1,85,8|0,0:0|0:0,0:0:0:0: +180,192,228390,2,0,L|180:104,1,85,2|2,0:0|0:0,0:0:0:0: +356,192,228753,5,2,0:0:0:0: +440,192,228935,1,2,0:0:0:0: +440,108,229117,1,0,0:0:0:0: +356,108,229299,1,2,0:0:0:0: +176,108,229480,2,0,L|176:192,1,85,8|0,0:0|0:0,0:0:0:0: +264,192,229844,1,2,0:0:0:0: +310,192,229934,1,0,0:0:0:0: +356,192,230025,1,2,0:0:0:0: +176,192,230208,6,0,L|4:192,1,170,6|2,0:0|0:0,0:0:0:0: +92,192,230753,1,2,0:0:0:0: +268,192,230935,2,0,L|356:192,1,85,8|0,0:0|0:0,0:0:0:0: +260,192,231299,2,0,L|260:108,1,85,2|2,0:0|0:0,0:0:0:0: +308,104,231571,1,0,0:0:0:0: +356,104,231662,6,0,B|440:104|440:104|440:192,1,170,2|2,0:0|0:0,0:0:0:0: +356,192,232208,1,2,0:0:0:0: +180,192,232390,2,0,L|180:304,1,85,8|0,0:0|0:0,0:0:0:0: +272,280,232753,2,0,L|272:232,2,42.5,2|0|0,0:0|0:0|0:0,0:0:0:0: +92,280,233117,6,0,P|40:224|92:160,1,170,8|8,0:0|0:0,0:0:0:0: +172,160,233662,1,8,0:0:0:0: +352,160,233844,2,0,L|352:68,1,85,8|8,0:0|0:0,0:0:0:0: +268,76,234208,1,2,0:0:0:0: +360,76,234390,1,2,0:0:0:0: +172,160,234571,6,0,L|172:100,2,42.5,2|2|2,0:0|0:0|0:0,0:0:0:0: +268,192,234935,2,0,L|172:192,1,85,8|2,0:0|0:0,0:0:0:0: +364,192,235298,2,0,L|364:280,1,85,8|2,0:0|0:0,0:0:0:0: +183,192,235662,1,2,0:0:0:0: +140,192,235752,1,2,0:0:0:0: +98,192,235843,1,2,0:0:0:0: +376,192,236026,5,6,0:0:0:0: +224,192,236390,1,2,0:0:0:0: +496,192,236753,6,0,L|496:20,1,170,4|0,0:0|0:0,0:0:0:0: +256,192,237480,12,0,238571,0:0:0:0: +256,192,238935,6,0,L|256:368,1,170,4|0,0:0|0:0,0:0:0:0: +256,192,239662,12,0,246208,0:0:0:0: diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/37902-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/37902-expected-conversion.json new file mode 100644 index 0000000000..efc1144d05 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/37902-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":12017.0,"Objects":[{"StartTime":12017.0,"Position":48.0,"HyperDash":false},{"StartTime":12091.0,"Position":44.67537,"HyperDash":false},{"StartTime":12166.0,"Position":74.08286,"HyperDash":false},{"StartTime":12241.0,"Position":88.0374,"HyperDash":false},{"StartTime":12316.0,"Position":110.33316,"HyperDash":false},{"StartTime":12391.0,"Position":148.554672,"HyperDash":false},{"StartTime":12466.0,"Position":154.24501,"HyperDash":false},{"StartTime":12541.0,"Position":176.957489,"HyperDash":false},{"StartTime":12616.0,"Position":202.32959,"HyperDash":false},{"StartTime":12673.0,"Position":213.120667,"HyperDash":false},{"StartTime":12766.0,"Position":252.041336,"HyperDash":false}]},{"StartTime":13067.0,"Objects":[{"StartTime":13067.0,"Position":320.0,"HyperDash":false}]},{"StartTime":13367.0,"Objects":[{"StartTime":13367.0,"Position":464.0,"HyperDash":false}]},{"StartTime":13667.0,"Objects":[{"StartTime":13667.0,"Position":484.0,"HyperDash":false}]},{"StartTime":13966.0,"Objects":[{"StartTime":13966.0,"Position":444.0,"HyperDash":false}]},{"StartTime":14116.0,"Objects":[{"StartTime":14116.0,"Position":444.0,"HyperDash":false}]},{"StartTime":14416.0,"Objects":[{"StartTime":14416.0,"Position":464.0,"HyperDash":false},{"StartTime":14490.0,"Position":453.158569,"HyperDash":false},{"StartTime":14565.0,"Position":455.987671,"HyperDash":false},{"StartTime":14640.0,"Position":425.82608,"HyperDash":false},{"StartTime":14715.0,"Position":428.8319,"HyperDash":false},{"StartTime":14790.0,"Position":427.066162,"HyperDash":false},{"StartTime":14865.0,"Position":386.833649,"HyperDash":false},{"StartTime":14940.0,"Position":376.186218,"HyperDash":false},{"StartTime":15015.0,"Position":338.7702,"HyperDash":false},{"StartTime":15072.0,"Position":302.942566,"HyperDash":false},{"StartTime":15165.0,"Position":288.993134,"HyperDash":false}]},{"StartTime":15466.0,"Objects":[{"StartTime":15466.0,"Position":216.0,"HyperDash":false}]},{"StartTime":15766.0,"Objects":[{"StartTime":15766.0,"Position":72.0,"HyperDash":false}]},{"StartTime":16066.0,"Objects":[{"StartTime":16066.0,"Position":92.0,"HyperDash":false}]},{"StartTime":16366.0,"Objects":[{"StartTime":16366.0,"Position":52.0,"HyperDash":false}]},{"StartTime":16815.0,"Objects":[{"StartTime":16815.0,"Position":72.0,"HyperDash":false},{"StartTime":16889.0,"Position":79.642746,"HyperDash":false},{"StartTime":16964.0,"Position":89.107,"HyperDash":false},{"StartTime":17039.0,"Position":108.5208,"HyperDash":false},{"StartTime":17114.0,"Position":136.488754,"HyperDash":false},{"StartTime":17189.0,"Position":172.402725,"HyperDash":false},{"StartTime":17264.0,"Position":179.293137,"HyperDash":false},{"StartTime":17339.0,"Position":180.858765,"HyperDash":false},{"StartTime":17414.0,"Position":220.396072,"HyperDash":false},{"StartTime":17471.0,"Position":249.039856,"HyperDash":false},{"StartTime":17564.0,"Position":261.951355,"HyperDash":false}]},{"StartTime":17865.0,"Objects":[{"StartTime":17865.0,"Position":320.0,"HyperDash":false}]},{"StartTime":18165.0,"Objects":[{"StartTime":18165.0,"Position":432.0,"HyperDash":false}]},{"StartTime":18465.0,"Objects":[{"StartTime":18465.0,"Position":448.0,"HyperDash":false}]},{"StartTime":18765.0,"Objects":[{"StartTime":18765.0,"Position":504.0,"HyperDash":false}]},{"StartTime":18915.0,"Objects":[{"StartTime":18915.0,"Position":484.0,"HyperDash":false}]},{"StartTime":19215.0,"Objects":[{"StartTime":19215.0,"Position":504.0,"HyperDash":false},{"StartTime":19289.0,"Position":501.08197,"HyperDash":false},{"StartTime":19364.0,"Position":495.2131,"HyperDash":false},{"StartTime":19439.0,"Position":481.1128,"HyperDash":false},{"StartTime":19514.0,"Position":463.28656,"HyperDash":false},{"StartTime":19589.0,"Position":434.907227,"HyperDash":false},{"StartTime":19664.0,"Position":416.885864,"HyperDash":false},{"StartTime":19739.0,"Position":405.201477,"HyperDash":false},{"StartTime":19814.0,"Position":367.272461,"HyperDash":false},{"StartTime":19871.0,"Position":365.267731,"HyperDash":false},{"StartTime":19964.0,"Position":317.231384,"HyperDash":false}]},{"StartTime":20264.0,"Objects":[{"StartTime":20264.0,"Position":248.0,"HyperDash":false}]},{"StartTime":20564.0,"Objects":[{"StartTime":20564.0,"Position":268.0,"HyperDash":false}]},{"StartTime":20864.0,"Objects":[{"StartTime":20864.0,"Position":104.0,"HyperDash":false}]},{"StartTime":21164.0,"Objects":[{"StartTime":21164.0,"Position":248.0,"HyperDash":false}]},{"StartTime":21614.0,"Objects":[{"StartTime":21614.0,"Position":72.0,"HyperDash":false},{"StartTime":21688.0,"Position":89.44662,"HyperDash":false},{"StartTime":21763.0,"Position":74.09614,"HyperDash":false},{"StartTime":21838.0,"Position":60.5660629,"HyperDash":false},{"StartTime":21913.0,"Position":83.94954,"HyperDash":false},{"StartTime":21988.0,"Position":82.8251,"HyperDash":false},{"StartTime":22063.0,"Position":111.00235,"HyperDash":false},{"StartTime":22138.0,"Position":149.062637,"HyperDash":false},{"StartTime":22213.0,"Position":152.832413,"HyperDash":false},{"StartTime":22270.0,"Position":185.730072,"HyperDash":false},{"StartTime":22363.0,"Position":197.239868,"HyperDash":false}]},{"StartTime":22663.0,"Objects":[{"StartTime":22663.0,"Position":264.0,"HyperDash":false},{"StartTime":22737.0,"Position":291.67392,"HyperDash":false},{"StartTime":22812.0,"Position":313.532043,"HyperDash":false},{"StartTime":22887.0,"Position":338.985229,"HyperDash":false},{"StartTime":22962.0,"Position":361.614532,"HyperDash":false},{"StartTime":23037.0,"Position":383.778625,"HyperDash":false},{"StartTime":23112.0,"Position":403.659546,"HyperDash":false},{"StartTime":23187.0,"Position":404.466278,"HyperDash":false},{"StartTime":23262.0,"Position":433.744751,"HyperDash":false},{"StartTime":23337.0,"Position":430.5013,"HyperDash":false},{"StartTime":23412.0,"Position":450.112335,"HyperDash":false},{"StartTime":23469.0,"Position":448.32254,"HyperDash":false},{"StartTime":23562.0,"Position":455.8164,"HyperDash":false}]},{"StartTime":23863.0,"Objects":[{"StartTime":23863.0,"Position":456.0,"HyperDash":false},{"StartTime":23937.0,"Position":420.344849,"HyperDash":false},{"StartTime":24012.0,"Position":406.676758,"HyperDash":false},{"StartTime":24087.0,"Position":381.029877,"HyperDash":false},{"StartTime":24162.0,"Position":361.682678,"HyperDash":false},{"StartTime":24237.0,"Position":326.453217,"HyperDash":false},{"StartTime":24312.0,"Position":325.5777,"HyperDash":false},{"StartTime":24387.0,"Position":323.0864,"HyperDash":false},{"StartTime":24462.0,"Position":280.111542,"HyperDash":false},{"StartTime":24537.0,"Position":265.3847,"HyperDash":false},{"StartTime":24612.0,"Position":230.444534,"HyperDash":false},{"StartTime":24669.0,"Position":218.443909,"HyperDash":false},{"StartTime":24762.0,"Position":180.416458,"HyperDash":false}]},{"StartTime":25063.0,"Objects":[{"StartTime":25063.0,"Position":184.0,"HyperDash":false}]},{"StartTime":25662.0,"Objects":[{"StartTime":25662.0,"Position":204.0,"HyperDash":false}]},{"StartTime":26262.0,"Objects":[{"StartTime":26262.0,"Position":320.0,"HyperDash":false}]},{"StartTime":26862.0,"Objects":[{"StartTime":26862.0,"Position":300.0,"HyperDash":false}]},{"StartTime":27612.0,"Objects":[{"StartTime":27612.0,"Position":96.0,"HyperDash":false},{"StartTime":27686.0,"Position":93.6587143,"HyperDash":false},{"StartTime":27761.0,"Position":98.89105,"HyperDash":false},{"StartTime":27836.0,"Position":108.2196,"HyperDash":false},{"StartTime":27911.0,"Position":110.334862,"HyperDash":false},{"StartTime":27986.0,"Position":125.092537,"HyperDash":false},{"StartTime":28061.0,"Position":136.262375,"HyperDash":false},{"StartTime":28136.0,"Position":145.71701,"HyperDash":false},{"StartTime":28211.0,"Position":178.315811,"HyperDash":false},{"StartTime":28268.0,"Position":210.647934,"HyperDash":false},{"StartTime":28361.0,"Position":227.43338,"HyperDash":false}]},{"StartTime":28661.0,"Objects":[{"StartTime":28661.0,"Position":296.0,"HyperDash":false},{"StartTime":28735.0,"Position":302.1624,"HyperDash":false},{"StartTime":28810.0,"Position":292.488281,"HyperDash":false},{"StartTime":28885.0,"Position":289.777161,"HyperDash":false},{"StartTime":28960.0,"Position":280.749847,"HyperDash":false},{"StartTime":29035.0,"Position":254.2413,"HyperDash":false},{"StartTime":29110.0,"Position":259.131836,"HyperDash":false},{"StartTime":29185.0,"Position":252.401169,"HyperDash":false},{"StartTime":29260.0,"Position":227.176636,"HyperDash":false},{"StartTime":29335.0,"Position":210.735916,"HyperDash":false},{"StartTime":29410.0,"Position":186.45578,"HyperDash":false},{"StartTime":29467.0,"Position":186.31813,"HyperDash":false},{"StartTime":29560.0,"Position":140.066452,"HyperDash":false}]},{"StartTime":29861.0,"Objects":[{"StartTime":29861.0,"Position":72.0,"HyperDash":false},{"StartTime":29935.0,"Position":47.3521729,"HyperDash":false},{"StartTime":30010.0,"Position":57.88796,"HyperDash":false},{"StartTime":30085.0,"Position":33.71809,"HyperDash":false},{"StartTime":30160.0,"Position":48.9088,"HyperDash":false},{"StartTime":30235.0,"Position":57.53698,"HyperDash":false},{"StartTime":30310.0,"Position":45.7225456,"HyperDash":false},{"StartTime":30385.0,"Position":39.74385,"HyperDash":false},{"StartTime":30460.0,"Position":49.84991,"HyperDash":false},{"StartTime":30535.0,"Position":61.4995155,"HyperDash":false},{"StartTime":30610.0,"Position":64.31644,"HyperDash":false},{"StartTime":30667.0,"Position":81.55949,"HyperDash":false},{"StartTime":30760.0,"Position":93.98463,"HyperDash":false}]},{"StartTime":31060.0,"Objects":[{"StartTime":31060.0,"Position":160.0,"HyperDash":false}]},{"StartTime":31660.0,"Objects":[{"StartTime":31660.0,"Position":432.0,"HyperDash":false}]},{"StartTime":32260.0,"Objects":[{"StartTime":32260.0,"Position":412.0,"HyperDash":false}]},{"StartTime":32860.0,"Objects":[{"StartTime":32860.0,"Position":432.0,"HyperDash":false}]},{"StartTime":33610.0,"Objects":[{"StartTime":33610.0,"Position":256.0,"HyperDash":false},{"StartTime":33684.0,"Position":223.29216,"HyperDash":false},{"StartTime":33759.0,"Position":206.250412,"HyperDash":false},{"StartTime":33834.0,"Position":193.208679,"HyperDash":false},{"StartTime":33909.0,"Position":156.0,"HyperDash":false},{"StartTime":33984.0,"Position":175.874786,"HyperDash":false},{"StartTime":34059.0,"Position":205.916534,"HyperDash":false},{"StartTime":34116.0,"Position":211.948242,"HyperDash":false},{"StartTime":34209.0,"Position":256.0,"HyperDash":false}]},{"StartTime":34359.0,"Objects":[{"StartTime":34359.0,"Position":376.0,"HyperDash":false}]},{"StartTime":34659.0,"Objects":[{"StartTime":34659.0,"Position":256.0,"HyperDash":false}]},{"StartTime":34809.0,"Objects":[{"StartTime":34809.0,"Position":256.0,"HyperDash":false},{"StartTime":34883.0,"Position":283.707855,"HyperDash":false},{"StartTime":34958.0,"Position":305.749573,"HyperDash":false},{"StartTime":35033.0,"Position":327.791321,"HyperDash":false},{"StartTime":35108.0,"Position":356.0,"HyperDash":false},{"StartTime":35183.0,"Position":331.1252,"HyperDash":false},{"StartTime":35258.0,"Position":306.083466,"HyperDash":false},{"StartTime":35315.0,"Position":273.051758,"HyperDash":false},{"StartTime":35408.0,"Position":256.0,"HyperDash":false}]},{"StartTime":35559.0,"Objects":[{"StartTime":35559.0,"Position":128.0,"HyperDash":false}]},{"StartTime":35859.0,"Objects":[{"StartTime":35859.0,"Position":256.0,"HyperDash":false}]},{"StartTime":36009.0,"Objects":[{"StartTime":36009.0,"Position":256.0,"HyperDash":false},{"StartTime":36083.0,"Position":230.29216,"HyperDash":false},{"StartTime":36158.0,"Position":206.250412,"HyperDash":false},{"StartTime":36233.0,"Position":193.208679,"HyperDash":false},{"StartTime":36308.0,"Position":156.0,"HyperDash":false},{"StartTime":36383.0,"Position":191.874786,"HyperDash":false},{"StartTime":36458.0,"Position":205.916534,"HyperDash":false},{"StartTime":36515.0,"Position":241.948242,"HyperDash":false},{"StartTime":36608.0,"Position":256.0,"HyperDash":false}]},{"StartTime":36758.0,"Objects":[{"StartTime":36758.0,"Position":376.0,"HyperDash":false}]},{"StartTime":37058.0,"Objects":[{"StartTime":37058.0,"Position":328.0,"HyperDash":false},{"StartTime":37132.0,"Position":343.611969,"HyperDash":false},{"StartTime":37207.0,"Position":376.99472,"HyperDash":false},{"StartTime":37282.0,"Position":386.735321,"HyperDash":false},{"StartTime":37357.0,"Position":419.270874,"HyperDash":false},{"StartTime":37432.0,"Position":438.334564,"HyperDash":false},{"StartTime":37507.0,"Position":444.7913,"HyperDash":false},{"StartTime":37582.0,"Position":467.3238,"HyperDash":false},{"StartTime":37657.0,"Position":454.839142,"HyperDash":false},{"StartTime":37732.0,"Position":439.412842,"HyperDash":false},{"StartTime":37807.0,"Position":444.935333,"HyperDash":false},{"StartTime":37882.0,"Position":421.561951,"HyperDash":false},{"StartTime":37957.0,"Position":419.5829,"HyperDash":false},{"StartTime":38032.0,"Position":392.116547,"HyperDash":false},{"StartTime":38107.0,"Position":377.418579,"HyperDash":false},{"StartTime":38182.0,"Position":348.0527,"HyperDash":false},{"StartTime":38257.0,"Position":328.0,"HyperDash":false},{"StartTime":38332.0,"Position":347.832336,"HyperDash":false},{"StartTime":38407.0,"Position":377.206635,"HyperDash":false},{"StartTime":38482.0,"Position":402.925934,"HyperDash":false},{"StartTime":38557.0,"Position":419.42688,"HyperDash":false},{"StartTime":38632.0,"Position":422.448273,"HyperDash":false},{"StartTime":38707.0,"Position":444.8633,"HyperDash":false},{"StartTime":38764.0,"Position":446.1127,"HyperDash":false},{"StartTime":38857.0,"Position":454.839142,"HyperDash":false}]},{"StartTime":39607.0,"Objects":[{"StartTime":39607.0,"Position":440.0,"HyperDash":false}]},{"StartTime":39907.0,"Objects":[{"StartTime":39907.0,"Position":296.0,"HyperDash":false}]},{"StartTime":40207.0,"Objects":[{"StartTime":40207.0,"Position":316.0,"HyperDash":false}]},{"StartTime":40357.0,"Objects":[{"StartTime":40357.0,"Position":256.0,"HyperDash":false},{"StartTime":40431.0,"Position":212.250839,"HyperDash":false},{"StartTime":40506.0,"Position":206.167221,"HyperDash":false},{"StartTime":40563.0,"Position":200.103668,"HyperDash":false},{"StartTime":40656.0,"Position":156.0,"HyperDash":false}]},{"StartTime":41107.0,"Objects":[{"StartTime":41107.0,"Position":64.0,"HyperDash":false}]},{"StartTime":41407.0,"Objects":[{"StartTime":41407.0,"Position":256.0,"HyperDash":false}]},{"StartTime":41557.0,"Objects":[{"StartTime":41557.0,"Position":192.0,"HyperDash":false},{"StartTime":41631.0,"Position":213.749161,"HyperDash":false},{"StartTime":41706.0,"Position":241.832779,"HyperDash":false},{"StartTime":41763.0,"Position":251.896332,"HyperDash":false},{"StartTime":41856.0,"Position":292.0,"HyperDash":false}]},{"StartTime":42307.0,"Objects":[{"StartTime":42307.0,"Position":392.0,"HyperDash":false}]},{"StartTime":42606.0,"Objects":[{"StartTime":42606.0,"Position":288.0,"HyperDash":false}]},{"StartTime":42756.0,"Objects":[{"StartTime":42756.0,"Position":256.0,"HyperDash":false},{"StartTime":42830.0,"Position":220.250839,"HyperDash":false},{"StartTime":42905.0,"Position":206.167221,"HyperDash":false},{"StartTime":42962.0,"Position":205.103668,"HyperDash":false},{"StartTime":43055.0,"Position":156.0,"HyperDash":false}]},{"StartTime":43356.0,"Objects":[{"StartTime":43356.0,"Position":172.0,"HyperDash":false}]},{"StartTime":43506.0,"Objects":[{"StartTime":43506.0,"Position":144.0,"HyperDash":false}]},{"StartTime":43656.0,"Objects":[{"StartTime":43656.0,"Position":172.0,"HyperDash":false}]},{"StartTime":43956.0,"Objects":[{"StartTime":43956.0,"Position":288.0,"HyperDash":false}]},{"StartTime":44106.0,"Objects":[{"StartTime":44106.0,"Position":230.0,"HyperDash":false}]},{"StartTime":44256.0,"Objects":[{"StartTime":44256.0,"Position":250.0,"HyperDash":false}]},{"StartTime":44556.0,"Objects":[{"StartTime":44556.0,"Position":374.0,"HyperDash":false}]},{"StartTime":44706.0,"Objects":[{"StartTime":44706.0,"Position":302.0,"HyperDash":false}]},{"StartTime":44856.0,"Objects":[{"StartTime":44856.0,"Position":282.0,"HyperDash":false}]},{"StartTime":45605.0,"Objects":[{"StartTime":45605.0,"Position":256.0,"HyperDash":false},{"StartTime":45679.0,"Position":263.6996,"HyperDash":false},{"StartTime":45754.0,"Position":306.0,"HyperDash":false},{"StartTime":45829.0,"Position":275.233643,"HyperDash":false},{"StartTime":45904.0,"Position":256.0,"HyperDash":false},{"StartTime":45979.0,"Position":286.8331,"HyperDash":false},{"StartTime":46054.0,"Position":306.0,"HyperDash":false},{"StartTime":46129.0,"Position":293.100128,"HyperDash":false},{"StartTime":46204.0,"Position":256.0,"HyperDash":false},{"StartTime":46261.0,"Position":261.958618,"HyperDash":false},{"StartTime":46354.0,"Position":306.0,"HyperDash":false}]},{"StartTime":46655.0,"Objects":[{"StartTime":46655.0,"Position":376.0,"HyperDash":false}]},{"StartTime":46955.0,"Objects":[{"StartTime":46955.0,"Position":448.0,"HyperDash":false}]},{"StartTime":47255.0,"Objects":[{"StartTime":47255.0,"Position":459.0,"HyperDash":false}]},{"StartTime":47555.0,"Objects":[{"StartTime":47555.0,"Position":304.0,"HyperDash":false}]},{"StartTime":47705.0,"Objects":[{"StartTime":47705.0,"Position":376.0,"HyperDash":false}]},{"StartTime":48005.0,"Objects":[{"StartTime":48005.0,"Position":376.0,"HyperDash":false},{"StartTime":48079.0,"Position":381.749176,"HyperDash":false},{"StartTime":48154.0,"Position":426.0,"HyperDash":false},{"StartTime":48211.0,"Position":410.103668,"HyperDash":false},{"StartTime":48304.0,"Position":376.0,"HyperDash":false}]},{"StartTime":48454.0,"Objects":[{"StartTime":48454.0,"Position":232.0,"HyperDash":false}]},{"StartTime":48604.0,"Objects":[{"StartTime":48604.0,"Position":304.0,"HyperDash":false}]},{"StartTime":48754.0,"Objects":[{"StartTime":48754.0,"Position":224.0,"HyperDash":false}]},{"StartTime":49054.0,"Objects":[{"StartTime":49054.0,"Position":160.0,"HyperDash":false}]},{"StartTime":49354.0,"Objects":[{"StartTime":49354.0,"Position":80.0,"HyperDash":false}]},{"StartTime":49654.0,"Objects":[{"StartTime":49654.0,"Position":16.0,"HyperDash":false}]},{"StartTime":49954.0,"Objects":[{"StartTime":49954.0,"Position":80.0,"HyperDash":false}]},{"StartTime":50404.0,"Objects":[{"StartTime":50404.0,"Position":48.0,"HyperDash":false},{"StartTime":50460.0,"Position":52.7919464,"HyperDash":false},{"StartTime":50553.0,"Position":98.0,"HyperDash":false}]},{"StartTime":50704.0,"Objects":[{"StartTime":50704.0,"Position":136.0,"HyperDash":false},{"StartTime":50760.0,"Position":160.791946,"HyperDash":false},{"StartTime":50853.0,"Position":186.0,"HyperDash":false}]},{"StartTime":51003.0,"Objects":[{"StartTime":51003.0,"Position":224.0,"HyperDash":false},{"StartTime":51059.0,"Position":255.791946,"HyperDash":false},{"StartTime":51152.0,"Position":274.0,"HyperDash":false}]},{"StartTime":51453.0,"Objects":[{"StartTime":51453.0,"Position":400.0,"HyperDash":false}]},{"StartTime":51753.0,"Objects":[{"StartTime":51753.0,"Position":432.0,"HyperDash":false}]},{"StartTime":52053.0,"Objects":[{"StartTime":52053.0,"Position":488.0,"HyperDash":false}]},{"StartTime":52353.0,"Objects":[{"StartTime":52353.0,"Position":507.0,"HyperDash":false}]},{"StartTime":52503.0,"Objects":[{"StartTime":52503.0,"Position":508.0,"HyperDash":false}]},{"StartTime":52803.0,"Objects":[{"StartTime":52803.0,"Position":488.0,"HyperDash":false},{"StartTime":52877.0,"Position":473.278381,"HyperDash":false},{"StartTime":52952.0,"Position":438.0,"HyperDash":false},{"StartTime":53027.0,"Position":471.832977,"HyperDash":false},{"StartTime":53102.0,"Position":488.0,"HyperDash":false},{"StartTime":53159.0,"Position":476.069031,"HyperDash":false},{"StartTime":53252.0,"Position":438.0,"HyperDash":false}]},{"StartTime":53403.0,"Objects":[{"StartTime":53403.0,"Position":368.0,"HyperDash":false}]},{"StartTime":53553.0,"Objects":[{"StartTime":53553.0,"Position":368.0,"HyperDash":false},{"StartTime":53627.0,"Position":341.4955,"HyperDash":false},{"StartTime":53702.0,"Position":320.428864,"HyperDash":false},{"StartTime":53777.0,"Position":295.8305,"HyperDash":false},{"StartTime":53852.0,"Position":289.023224,"HyperDash":false},{"StartTime":53927.0,"Position":294.625671,"HyperDash":false},{"StartTime":54002.0,"Position":320.1455,"HyperDash":false},{"StartTime":54059.0,"Position":332.386719,"HyperDash":false},{"StartTime":54152.0,"Position":368.0,"HyperDash":false}]},{"StartTime":54452.0,"Objects":[{"StartTime":54452.0,"Position":368.0,"HyperDash":false},{"StartTime":54526.0,"Position":361.6083,"HyperDash":false},{"StartTime":54601.0,"Position":346.408356,"HyperDash":false},{"StartTime":54676.0,"Position":309.5409,"HyperDash":false},{"StartTime":54751.0,"Position":302.165344,"HyperDash":false},{"StartTime":54826.0,"Position":280.769684,"HyperDash":false},{"StartTime":54901.0,"Position":252.958511,"HyperDash":false},{"StartTime":54976.0,"Position":235.981033,"HyperDash":false},{"StartTime":55051.0,"Position":202.952667,"HyperDash":false},{"StartTime":55108.0,"Position":198.931656,"HyperDash":false},{"StartTime":55201.0,"Position":152.9338,"HyperDash":false}]},{"StartTime":60000.0,"Objects":[{"StartTime":60000.0,"Position":256.0,"HyperDash":false},{"StartTime":60074.0,"Position":256.3498,"HyperDash":false},{"StartTime":60149.0,"Position":264.8665,"HyperDash":false},{"StartTime":60224.0,"Position":286.383179,"HyperDash":false},{"StartTime":60299.0,"Position":305.899872,"HyperDash":false},{"StartTime":60374.0,"Position":314.416565,"HyperDash":false},{"StartTime":60449.0,"Position":335.933228,"HyperDash":false},{"StartTime":60524.0,"Position":344.449921,"HyperDash":false},{"StartTime":60599.0,"Position":355.9666,"HyperDash":false},{"StartTime":60656.0,"Position":381.4793,"HyperDash":false},{"StartTime":60749.0,"Position":381.0,"HyperDash":false}]},{"StartTime":61050.0,"Objects":[{"StartTime":61050.0,"Position":416.0,"HyperDash":false},{"StartTime":61124.0,"Position":413.0,"HyperDash":false},{"StartTime":61199.0,"Position":430.0,"HyperDash":false},{"StartTime":61274.0,"Position":403.0,"HyperDash":false},{"StartTime":61349.0,"Position":416.0,"HyperDash":false},{"StartTime":61424.0,"Position":404.0,"HyperDash":false},{"StartTime":61499.0,"Position":419.0,"HyperDash":false},{"StartTime":61574.0,"Position":426.0,"HyperDash":false},{"StartTime":61649.0,"Position":416.0,"HyperDash":false},{"StartTime":61715.0,"Position":396.0,"HyperDash":false},{"StartTime":61781.0,"Position":420.0,"HyperDash":false},{"StartTime":61847.0,"Position":421.0,"HyperDash":false},{"StartTime":61949.0,"Position":416.0,"HyperDash":false}]},{"StartTime":62250.0,"Objects":[{"StartTime":62250.0,"Position":416.0,"HyperDash":false},{"StartTime":62324.0,"Position":403.652954,"HyperDash":false},{"StartTime":62399.0,"Position":373.139038,"HyperDash":false},{"StartTime":62474.0,"Position":359.625122,"HyperDash":false},{"StartTime":62549.0,"Position":366.111237,"HyperDash":false},{"StartTime":62624.0,"Position":362.597321,"HyperDash":false},{"StartTime":62699.0,"Position":334.083435,"HyperDash":false},{"StartTime":62774.0,"Position":344.569519,"HyperDash":false},{"StartTime":62849.0,"Position":316.0556,"HyperDash":false},{"StartTime":62915.0,"Position":298.0434,"HyperDash":false},{"StartTime":62981.0,"Position":275.031158,"HyperDash":false},{"StartTime":63047.0,"Position":265.018921,"HyperDash":false},{"StartTime":63149.0,"Position":266.0,"HyperDash":false}]},{"StartTime":63449.0,"Objects":[{"StartTime":63449.0,"Position":232.0,"HyperDash":false},{"StartTime":63523.0,"Position":246.0,"HyperDash":false},{"StartTime":63598.0,"Position":233.0,"HyperDash":false},{"StartTime":63673.0,"Position":236.0,"HyperDash":false},{"StartTime":63748.0,"Position":232.0,"HyperDash":false},{"StartTime":63823.0,"Position":219.0,"HyperDash":false},{"StartTime":63898.0,"Position":231.0,"HyperDash":false},{"StartTime":63973.0,"Position":242.0,"HyperDash":false},{"StartTime":64048.0,"Position":232.0,"HyperDash":false},{"StartTime":64123.0,"Position":228.0,"HyperDash":false},{"StartTime":64198.0,"Position":215.0,"HyperDash":false},{"StartTime":64273.0,"Position":243.0,"HyperDash":false},{"StartTime":64348.0,"Position":232.0,"HyperDash":false},{"StartTime":64405.0,"Position":249.0,"HyperDash":false},{"StartTime":64498.0,"Position":232.0,"HyperDash":false}]},{"StartTime":64799.0,"Objects":[{"StartTime":64799.0,"Position":160.0,"HyperDash":false},{"StartTime":64873.0,"Position":144.3059,"HyperDash":false},{"StartTime":64948.0,"Position":110.278084,"HyperDash":false},{"StartTime":65023.0,"Position":84.25028,"HyperDash":false},{"StartTime":65098.0,"Position":60.0,"HyperDash":false},{"StartTime":65173.0,"Position":96.80534,"HyperDash":false},{"StartTime":65248.0,"Position":109.833145,"HyperDash":false},{"StartTime":65323.0,"Position":135.860962,"HyperDash":false},{"StartTime":65398.0,"Position":160.0,"HyperDash":false},{"StartTime":65473.0,"Position":122.08342,"HyperDash":false},{"StartTime":65548.0,"Position":110.055618,"HyperDash":false},{"StartTime":65605.0,"Position":79.03449,"HyperDash":false},{"StartTime":65698.0,"Position":60.0,"HyperDash":false}]},{"StartTime":65998.0,"Objects":[{"StartTime":65998.0,"Position":56.0,"HyperDash":false}]},{"StartTime":66298.0,"Objects":[{"StartTime":66298.0,"Position":36.0,"HyperDash":false}]},{"StartTime":66598.0,"Objects":[{"StartTime":66598.0,"Position":63.0,"HyperDash":false}]},{"StartTime":66898.0,"Objects":[{"StartTime":66898.0,"Position":200.0,"HyperDash":false}]},{"StartTime":67198.0,"Objects":[{"StartTime":67198.0,"Position":287.0,"HyperDash":false},{"StartTime":67272.0,"Position":341.0,"HyperDash":false},{"StartTime":67347.0,"Position":145.0,"HyperDash":false},{"StartTime":67422.0,"Position":84.0,"HyperDash":false},{"StartTime":67497.0,"Position":189.0,"HyperDash":false},{"StartTime":67572.0,"Position":498.0,"HyperDash":false},{"StartTime":67647.0,"Position":416.0,"HyperDash":false},{"StartTime":67722.0,"Position":211.0,"HyperDash":false},{"StartTime":67797.0,"Position":167.0,"HyperDash":false},{"StartTime":67872.0,"Position":466.0,"HyperDash":false},{"StartTime":67947.0,"Position":114.0,"HyperDash":false},{"StartTime":68022.0,"Position":125.0,"HyperDash":false},{"StartTime":68097.0,"Position":457.0,"HyperDash":false},{"StartTime":68172.0,"Position":131.0,"HyperDash":false},{"StartTime":68247.0,"Position":337.0,"HyperDash":false},{"StartTime":68322.0,"Position":39.0,"HyperDash":false},{"StartTime":68397.0,"Position":311.0,"HyperDash":false},{"StartTime":68472.0,"Position":208.0,"HyperDash":false},{"StartTime":68547.0,"Position":357.0,"HyperDash":false},{"StartTime":68622.0,"Position":240.0,"HyperDash":false},{"StartTime":68697.0,"Position":35.0,"HyperDash":false},{"StartTime":68772.0,"Position":254.0,"HyperDash":false},{"StartTime":68847.0,"Position":292.0,"HyperDash":false},{"StartTime":68922.0,"Position":369.0,"HyperDash":false},{"StartTime":68997.0,"Position":14.0,"HyperDash":false},{"StartTime":69072.0,"Position":390.0,"HyperDash":false},{"StartTime":69147.0,"Position":286.0,"HyperDash":false},{"StartTime":69222.0,"Position":92.0,"HyperDash":false},{"StartTime":69297.0,"Position":170.0,"HyperDash":false},{"StartTime":69372.0,"Position":93.0,"HyperDash":false},{"StartTime":69447.0,"Position":139.0,"HyperDash":false},{"StartTime":69522.0,"Position":301.0,"HyperDash":false},{"StartTime":69597.0,"Position":137.0,"HyperDash":false}]},{"StartTime":69897.0,"Objects":[{"StartTime":69897.0,"Position":256.0,"HyperDash":false}]},{"StartTime":70047.0,"Objects":[{"StartTime":70047.0,"Position":320.0,"HyperDash":false}]},{"StartTime":70197.0,"Objects":[{"StartTime":70197.0,"Position":340.0,"HyperDash":false}]},{"StartTime":70497.0,"Objects":[{"StartTime":70497.0,"Position":340.0,"HyperDash":false}]},{"StartTime":70797.0,"Objects":[{"StartTime":70797.0,"Position":300.0,"HyperDash":false}]},{"StartTime":71096.0,"Objects":[{"StartTime":71096.0,"Position":248.0,"HyperDash":false}]},{"StartTime":71246.0,"Objects":[{"StartTime":71246.0,"Position":168.0,"HyperDash":false}]},{"StartTime":71396.0,"Objects":[{"StartTime":71396.0,"Position":184.0,"HyperDash":false}]},{"StartTime":71696.0,"Objects":[{"StartTime":71696.0,"Position":24.0,"HyperDash":false}]},{"StartTime":71996.0,"Objects":[{"StartTime":71996.0,"Position":104.0,"HyperDash":false},{"StartTime":72070.0,"Position":79.25084,"HyperDash":false},{"StartTime":72145.0,"Position":54.0,"HyperDash":false},{"StartTime":72202.0,"Position":54.8963242,"HyperDash":false},{"StartTime":72295.0,"Position":104.0,"HyperDash":false}]},{"StartTime":72446.0,"Objects":[{"StartTime":72446.0,"Position":192.0,"HyperDash":false}]},{"StartTime":72746.0,"Objects":[{"StartTime":72746.0,"Position":16.0,"HyperDash":false}]},{"StartTime":73046.0,"Objects":[{"StartTime":73046.0,"Position":104.0,"HyperDash":false},{"StartTime":73120.0,"Position":123.738281,"HyperDash":false},{"StartTime":73195.0,"Position":123.252632,"HyperDash":false},{"StartTime":73270.0,"Position":144.834549,"HyperDash":false},{"StartTime":73345.0,"Position":166.952621,"HyperDash":false},{"StartTime":73420.0,"Position":208.089325,"HyperDash":false},{"StartTime":73495.0,"Position":215.686081,"HyperDash":false},{"StartTime":73570.0,"Position":248.499512,"HyperDash":false},{"StartTime":73645.0,"Position":265.421631,"HyperDash":false},{"StartTime":73720.0,"Position":275.398376,"HyperDash":false},{"StartTime":73795.0,"Position":315.4022,"HyperDash":false},{"StartTime":73870.0,"Position":351.418854,"HyperDash":false},{"StartTime":73945.0,"Position":365.440521,"HyperDash":false},{"StartTime":74002.0,"Position":402.458252,"HyperDash":false},{"StartTime":74095.0,"Position":415.4878,"HyperDash":false}]},{"StartTime":74395.0,"Objects":[{"StartTime":74395.0,"Position":416.0,"HyperDash":false},{"StartTime":74469.0,"Position":417.7021,"HyperDash":false},{"StartTime":74544.0,"Position":397.104645,"HyperDash":false},{"StartTime":74619.0,"Position":387.306122,"HyperDash":false},{"StartTime":74694.0,"Position":350.779144,"HyperDash":false},{"StartTime":74769.0,"Position":366.889374,"HyperDash":false},{"StartTime":74844.0,"Position":396.758,"HyperDash":false},{"StartTime":74919.0,"Position":408.543182,"HyperDash":false},{"StartTime":74994.0,"Position":416.0,"HyperDash":false},{"StartTime":75069.0,"Position":394.62265,"HyperDash":false},{"StartTime":75144.0,"Position":396.931335,"HyperDash":false},{"StartTime":75201.0,"Position":363.6833,"HyperDash":false},{"StartTime":75294.0,"Position":350.779144,"HyperDash":false}]},{"StartTime":75595.0,"Objects":[{"StartTime":75595.0,"Position":280.0,"HyperDash":false}]},{"StartTime":75895.0,"Objects":[{"StartTime":75895.0,"Position":136.0,"HyperDash":false}]},{"StartTime":76195.0,"Objects":[{"StartTime":76195.0,"Position":280.0,"HyperDash":false}]},{"StartTime":76345.0,"Objects":[{"StartTime":76345.0,"Position":208.0,"HyperDash":false}]},{"StartTime":76495.0,"Objects":[{"StartTime":76495.0,"Position":228.0,"HyperDash":false}]},{"StartTime":76794.0,"Objects":[{"StartTime":76794.0,"Position":21.0,"HyperDash":false},{"StartTime":76859.0,"Position":193.0,"HyperDash":false},{"StartTime":76925.0,"Position":52.0,"HyperDash":false},{"StartTime":76990.0,"Position":466.0,"HyperDash":false},{"StartTime":77056.0,"Position":135.0,"HyperDash":false},{"StartTime":77121.0,"Position":121.0,"HyperDash":false},{"StartTime":77187.0,"Position":427.0,"HyperDash":false},{"StartTime":77253.0,"Position":176.0,"HyperDash":false},{"StartTime":77318.0,"Position":96.0,"HyperDash":false},{"StartTime":77384.0,"Position":345.0,"HyperDash":false},{"StartTime":77449.0,"Position":11.0,"HyperDash":false},{"StartTime":77515.0,"Position":393.0,"HyperDash":false},{"StartTime":77581.0,"Position":440.0,"HyperDash":false},{"StartTime":77646.0,"Position":179.0,"HyperDash":false},{"StartTime":77712.0,"Position":470.0,"HyperDash":false},{"StartTime":77777.0,"Position":89.0,"HyperDash":false},{"StartTime":77843.0,"Position":408.0,"HyperDash":false},{"StartTime":77909.0,"Position":243.0,"HyperDash":false},{"StartTime":77974.0,"Position":78.0,"HyperDash":false},{"StartTime":78040.0,"Position":172.0,"HyperDash":false},{"StartTime":78105.0,"Position":450.0,"HyperDash":false},{"StartTime":78171.0,"Position":231.0,"HyperDash":false},{"StartTime":78237.0,"Position":118.0,"HyperDash":false},{"StartTime":78302.0,"Position":511.0,"HyperDash":false},{"StartTime":78368.0,"Position":333.0,"HyperDash":false},{"StartTime":78433.0,"Position":234.0,"HyperDash":false},{"StartTime":78499.0,"Position":228.0,"HyperDash":false},{"StartTime":78565.0,"Position":302.0,"HyperDash":false},{"StartTime":78630.0,"Position":390.0,"HyperDash":false},{"StartTime":78696.0,"Position":75.0,"HyperDash":false},{"StartTime":78761.0,"Position":506.0,"HyperDash":false},{"StartTime":78827.0,"Position":3.0,"HyperDash":false},{"StartTime":78893.0,"Position":289.0,"HyperDash":false}]},{"StartTime":79194.0,"Objects":[{"StartTime":79194.0,"Position":256.0,"HyperDash":false},{"StartTime":79268.0,"Position":249.6807,"HyperDash":false},{"StartTime":79343.0,"Position":245.6988,"HyperDash":false},{"StartTime":79418.0,"Position":237.299881,"HyperDash":false},{"StartTime":79493.0,"Position":211.7363,"HyperDash":false},{"StartTime":79550.0,"Position":208.713608,"HyperDash":false},{"StartTime":79643.0,"Position":165.0138,"HyperDash":false}]},{"StartTime":79793.0,"Objects":[{"StartTime":79793.0,"Position":128.0,"HyperDash":false},{"StartTime":79867.0,"Position":121.464394,"HyperDash":false},{"StartTime":79942.0,"Position":81.6096039,"HyperDash":false},{"StartTime":80017.0,"Position":52.0348129,"HyperDash":false},{"StartTime":80092.0,"Position":60.8326073,"HyperDash":false},{"StartTime":80149.0,"Position":53.9088326,"HyperDash":false},{"StartTime":80242.0,"Position":56.0562,"HyperDash":false}]},{"StartTime":80543.0,"Objects":[{"StartTime":80543.0,"Position":76.0,"HyperDash":false}]},{"StartTime":80843.0,"Objects":[{"StartTime":80843.0,"Position":56.0,"HyperDash":false}]},{"StartTime":81143.0,"Objects":[{"StartTime":81143.0,"Position":200.0,"HyperDash":false}]},{"StartTime":81443.0,"Objects":[{"StartTime":81443.0,"Position":180.0,"HyperDash":false}]},{"StartTime":81593.0,"Objects":[{"StartTime":81593.0,"Position":200.0,"HyperDash":false},{"StartTime":81667.0,"Position":218.643234,"HyperDash":false},{"StartTime":81742.0,"Position":249.328659,"HyperDash":false},{"StartTime":81817.0,"Position":278.3164,"HyperDash":false},{"StartTime":81892.0,"Position":296.1659,"HyperDash":false},{"StartTime":81967.0,"Position":318.451416,"HyperDash":false},{"StartTime":82042.0,"Position":336.820862,"HyperDash":false},{"StartTime":82117.0,"Position":362.178284,"HyperDash":false},{"StartTime":82192.0,"Position":369.602051,"HyperDash":false},{"StartTime":82267.0,"Position":338.393555,"HyperDash":false},{"StartTime":82342.0,"Position":337.067169,"HyperDash":false},{"StartTime":82417.0,"Position":321.719727,"HyperDash":false},{"StartTime":82492.0,"Position":296.459869,"HyperDash":false},{"StartTime":82567.0,"Position":256.630157,"HyperDash":false},{"StartTime":82642.0,"Position":249.6552,"HyperDash":false},{"StartTime":82699.0,"Position":228.9382,"HyperDash":false},{"StartTime":82792.0,"Position":200.0,"HyperDash":false}]},{"StartTime":82942.0,"Objects":[{"StartTime":82942.0,"Position":200.0,"HyperDash":false}]},{"StartTime":83242.0,"Objects":[{"StartTime":83242.0,"Position":180.0,"HyperDash":false}]},{"StartTime":83542.0,"Objects":[{"StartTime":83542.0,"Position":180.0,"HyperDash":false}]},{"StartTime":83692.0,"Objects":[{"StartTime":83692.0,"Position":220.0,"HyperDash":false}]},{"StartTime":83842.0,"Objects":[{"StartTime":83842.0,"Position":220.0,"HyperDash":false}]},{"StartTime":83992.0,"Objects":[{"StartTime":83992.0,"Position":200.0,"HyperDash":false},{"StartTime":84066.0,"Position":214.895981,"HyperDash":false},{"StartTime":84141.0,"Position":217.903488,"HyperDash":false},{"StartTime":84216.0,"Position":225.305542,"HyperDash":false},{"StartTime":84291.0,"Position":263.285431,"HyperDash":false},{"StartTime":84348.0,"Position":288.04718,"HyperDash":false},{"StartTime":84441.0,"Position":312.975067,"HyperDash":false}]},{"StartTime":84592.0,"Objects":[{"StartTime":84592.0,"Position":344.0,"HyperDash":false},{"StartTime":84666.0,"Position":386.711,"HyperDash":false},{"StartTime":84741.0,"Position":393.655243,"HyperDash":false},{"StartTime":84816.0,"Position":433.20578,"HyperDash":false},{"StartTime":84891.0,"Position":441.4496,"HyperDash":false},{"StartTime":84948.0,"Position":466.8295,"HyperDash":false},{"StartTime":85041.0,"Position":473.5803,"HyperDash":false}]},{"StartTime":85341.0,"Objects":[{"StartTime":85341.0,"Position":464.0,"HyperDash":false}]},{"StartTime":85641.0,"Objects":[{"StartTime":85641.0,"Position":480.0,"HyperDash":false}]},{"StartTime":85941.0,"Objects":[{"StartTime":85941.0,"Position":464.0,"HyperDash":false}]},{"StartTime":86241.0,"Objects":[{"StartTime":86241.0,"Position":336.0,"HyperDash":false}]},{"StartTime":86391.0,"Objects":[{"StartTime":86391.0,"Position":400.0,"HyperDash":false},{"StartTime":86465.0,"Position":384.340973,"HyperDash":false},{"StartTime":86540.0,"Position":350.5981,"HyperDash":false},{"StartTime":86615.0,"Position":323.677429,"HyperDash":false},{"StartTime":86690.0,"Position":304.500153,"HyperDash":false},{"StartTime":86765.0,"Position":291.3181,"HyperDash":false},{"StartTime":86840.0,"Position":264.219,"HyperDash":false},{"StartTime":86915.0,"Position":246.938583,"HyperDash":false},{"StartTime":86990.0,"Position":217.532028,"HyperDash":false},{"StartTime":87065.0,"Position":225.6241,"HyperDash":false},{"StartTime":87140.0,"Position":263.9408,"HyperDash":false},{"StartTime":87215.0,"Position":291.0559,"HyperDash":false},{"StartTime":87290.0,"Position":304.220367,"HyperDash":false},{"StartTime":87365.0,"Position":325.3685,"HyperDash":false},{"StartTime":87440.0,"Position":350.271576,"HyperDash":false},{"StartTime":87497.0,"Position":364.034241,"HyperDash":false},{"StartTime":87590.0,"Position":400.0,"HyperDash":false}]},{"StartTime":87741.0,"Objects":[{"StartTime":87741.0,"Position":400.0,"HyperDash":false}]},{"StartTime":88041.0,"Objects":[{"StartTime":88041.0,"Position":420.0,"HyperDash":false}]},{"StartTime":88340.0,"Objects":[{"StartTime":88340.0,"Position":380.0,"HyperDash":false}]},{"StartTime":88490.0,"Objects":[{"StartTime":88490.0,"Position":320.0,"HyperDash":false}]},{"StartTime":88640.0,"Objects":[{"StartTime":88640.0,"Position":314.0,"HyperDash":false}]},{"StartTime":88940.0,"Objects":[{"StartTime":88940.0,"Position":0.0,"HyperDash":false},{"StartTime":89033.0,"Position":111.0,"HyperDash":false},{"StartTime":89127.0,"Position":358.0,"HyperDash":false},{"StartTime":89221.0,"Position":476.0,"HyperDash":false},{"StartTime":89315.0,"Position":87.0,"HyperDash":false},{"StartTime":89408.0,"Position":33.0,"HyperDash":false},{"StartTime":89502.0,"Position":166.0,"HyperDash":false},{"StartTime":89596.0,"Position":275.0,"HyperDash":false},{"StartTime":89690.0,"Position":119.0,"HyperDash":false}]},{"StartTime":89990.0,"Objects":[{"StartTime":89990.0,"Position":56.0,"HyperDash":false}]},{"StartTime":90140.0,"Objects":[{"StartTime":90140.0,"Position":76.0,"HyperDash":false}]},{"StartTime":90290.0,"Objects":[{"StartTime":90290.0,"Position":36.0,"HyperDash":false}]},{"StartTime":90590.0,"Objects":[{"StartTime":90590.0,"Position":200.0,"HyperDash":false}]},{"StartTime":90740.0,"Objects":[{"StartTime":90740.0,"Position":160.0,"HyperDash":false},{"StartTime":90814.0,"Position":176.321808,"HyperDash":false},{"StartTime":90889.0,"Position":206.339,"HyperDash":false},{"StartTime":90964.0,"Position":213.574585,"HyperDash":false},{"StartTime":91039.0,"Position":236.2215,"HyperDash":false},{"StartTime":91114.0,"Position":240.839,"HyperDash":false},{"StartTime":91189.0,"Position":206.688522,"HyperDash":false},{"StartTime":91264.0,"Position":182.7456,"HyperDash":false},{"StartTime":91339.0,"Position":160.0,"HyperDash":false},{"StartTime":91414.0,"Position":183.5337,"HyperDash":false},{"StartTime":91489.0,"Position":206.513763,"HyperDash":false},{"StartTime":91546.0,"Position":220.042145,"HyperDash":false},{"StartTime":91639.0,"Position":236.2215,"HyperDash":false}]},{"StartTime":91939.0,"Objects":[{"StartTime":91939.0,"Position":264.0,"HyperDash":false}]},{"StartTime":92089.0,"Objects":[{"StartTime":92089.0,"Position":259.0,"HyperDash":false}]},{"StartTime":92389.0,"Objects":[{"StartTime":92389.0,"Position":408.0,"HyperDash":false}]},{"StartTime":92539.0,"Objects":[{"StartTime":92539.0,"Position":328.0,"HyperDash":false}]},{"StartTime":92689.0,"Objects":[{"StartTime":92689.0,"Position":400.0,"HyperDash":false}]},{"StartTime":92839.0,"Objects":[{"StartTime":92839.0,"Position":464.0,"HyperDash":false}]},{"StartTime":92989.0,"Objects":[{"StartTime":92989.0,"Position":484.0,"HyperDash":false}]},{"StartTime":93139.0,"Objects":[{"StartTime":93139.0,"Position":496.0,"HyperDash":false}]},{"StartTime":93439.0,"Objects":[{"StartTime":93439.0,"Position":496.0,"HyperDash":false},{"StartTime":93513.0,"Position":508.470551,"HyperDash":false},{"StartTime":93588.0,"Position":481.299042,"HyperDash":false},{"StartTime":93663.0,"Position":447.88858,"HyperDash":false},{"StartTime":93738.0,"Position":442.8401,"HyperDash":false},{"StartTime":93813.0,"Position":434.920868,"HyperDash":false},{"StartTime":93888.0,"Position":396.039459,"HyperDash":false},{"StartTime":93963.0,"Position":378.642273,"HyperDash":false},{"StartTime":94038.0,"Position":346.954773,"HyperDash":false},{"StartTime":94113.0,"Position":325.103058,"HyperDash":false},{"StartTime":94188.0,"Position":297.157654,"HyperDash":false},{"StartTime":94245.0,"Position":278.1643,"HyperDash":false},{"StartTime":94338.0,"Position":247.142532,"HyperDash":false}]},{"StartTime":94788.0,"Objects":[{"StartTime":94788.0,"Position":160.0,"HyperDash":false},{"StartTime":94862.0,"Position":178.0,"HyperDash":false},{"StartTime":94937.0,"Position":160.0,"HyperDash":false},{"StartTime":94994.0,"Position":156.0,"HyperDash":false},{"StartTime":95087.0,"Position":160.0,"HyperDash":false}]},{"StartTime":95238.0,"Objects":[{"StartTime":95238.0,"Position":180.0,"HyperDash":false}]},{"StartTime":95388.0,"Objects":[{"StartTime":95388.0,"Position":140.0,"HyperDash":false}]},{"StartTime":95538.0,"Objects":[{"StartTime":95538.0,"Position":160.0,"HyperDash":false},{"StartTime":95612.0,"Position":178.7418,"HyperDash":false},{"StartTime":95687.0,"Position":173.08049,"HyperDash":false},{"StartTime":95744.0,"Position":203.788971,"HyperDash":false},{"StartTime":95837.0,"Position":215.526108,"HyperDash":false}]},{"StartTime":96138.0,"Objects":[{"StartTime":96138.0,"Position":296.0,"HyperDash":false},{"StartTime":96212.0,"Position":337.7064,"HyperDash":false},{"StartTime":96287.0,"Position":345.412964,"HyperDash":false},{"StartTime":96344.0,"Position":376.616638,"HyperDash":false},{"StartTime":96437.0,"Position":391.160645,"HyperDash":false}]},{"StartTime":96737.0,"Objects":[{"StartTime":96737.0,"Position":464.0,"HyperDash":false}]},{"StartTime":96887.0,"Objects":[{"StartTime":96887.0,"Position":416.0,"HyperDash":false}]},{"StartTime":97187.0,"Objects":[{"StartTime":97187.0,"Position":440.0,"HyperDash":false},{"StartTime":97261.0,"Position":447.4056,"HyperDash":false},{"StartTime":97336.0,"Position":432.9317,"HyperDash":false},{"StartTime":97411.0,"Position":435.742554,"HyperDash":false},{"StartTime":97486.0,"Position":407.575775,"HyperDash":false},{"StartTime":97561.0,"Position":379.243927,"HyperDash":false},{"StartTime":97636.0,"Position":366.1177,"HyperDash":false},{"StartTime":97711.0,"Position":344.4165,"HyperDash":false},{"StartTime":97786.0,"Position":317.914917,"HyperDash":false},{"StartTime":97843.0,"Position":304.0325,"HyperDash":false},{"StartTime":97936.0,"Position":268.035461,"HyperDash":false}]},{"StartTime":98237.0,"Objects":[{"StartTime":98237.0,"Position":200.0,"HyperDash":false}]},{"StartTime":98537.0,"Objects":[{"StartTime":98537.0,"Position":56.0,"HyperDash":false}]},{"StartTime":98837.0,"Objects":[{"StartTime":98837.0,"Position":76.0,"HyperDash":false}]},{"StartTime":99137.0,"Objects":[{"StartTime":99137.0,"Position":56.0,"HyperDash":false},{"StartTime":99211.0,"Position":60.74916,"HyperDash":false},{"StartTime":99286.0,"Position":106.0,"HyperDash":false},{"StartTime":99343.0,"Position":88.1036758,"HyperDash":false},{"StartTime":99436.0,"Position":56.0,"HyperDash":false}]},{"StartTime":99586.0,"Objects":[{"StartTime":99586.0,"Position":56.0,"HyperDash":false},{"StartTime":99660.0,"Position":61.48453,"HyperDash":false},{"StartTime":99735.0,"Position":67.69644,"HyperDash":false},{"StartTime":99810.0,"Position":92.0094,"HyperDash":false},{"StartTime":99885.0,"Position":107.968033,"HyperDash":false},{"StartTime":99960.0,"Position":87.38017,"HyperDash":false},{"StartTime":100035.0,"Position":67.9301,"HyperDash":false},{"StartTime":100110.0,"Position":56.5835648,"HyperDash":false},{"StartTime":100185.0,"Position":56.0,"HyperDash":false},{"StartTime":100260.0,"Position":65.53404,"HyperDash":false},{"StartTime":100335.0,"Position":67.81326,"HyperDash":false},{"StartTime":100392.0,"Position":74.3646545,"HyperDash":false},{"StartTime":100485.0,"Position":107.968033,"HyperDash":false}]},{"StartTime":100636.0,"Objects":[{"StartTime":100636.0,"Position":144.0,"HyperDash":false}]},{"StartTime":100936.0,"Objects":[{"StartTime":100936.0,"Position":288.0,"HyperDash":false}]},{"StartTime":101236.0,"Objects":[{"StartTime":101236.0,"Position":268.0,"HyperDash":false}]},{"StartTime":101536.0,"Objects":[{"StartTime":101536.0,"Position":360.0,"HyperDash":false},{"StartTime":101610.0,"Position":381.602356,"HyperDash":false},{"StartTime":101685.0,"Position":408.7352,"HyperDash":false},{"StartTime":101760.0,"Position":420.921082,"HyperDash":false},{"StartTime":101835.0,"Position":450.0819,"HyperDash":false},{"StartTime":101910.0,"Position":480.7513,"HyperDash":false},{"StartTime":101985.0,"Position":478.132416,"HyperDash":false},{"StartTime":102042.0,"Position":481.652863,"HyperDash":false},{"StartTime":102135.0,"Position":495.055,"HyperDash":false}]},{"StartTime":102435.0,"Objects":[{"StartTime":102435.0,"Position":496.0,"HyperDash":false},{"StartTime":102509.0,"Position":478.3312,"HyperDash":false},{"StartTime":102584.0,"Position":446.623962,"HyperDash":false},{"StartTime":102659.0,"Position":441.667145,"HyperDash":false},{"StartTime":102734.0,"Position":400.097137,"HyperDash":false},{"StartTime":102809.0,"Position":373.617828,"HyperDash":false},{"StartTime":102884.0,"Position":361.791168,"HyperDash":false},{"StartTime":102941.0,"Position":366.174866,"HyperDash":false},{"StartTime":103034.0,"Position":334.736969,"HyperDash":false}]},{"StartTime":103335.0,"Objects":[{"StartTime":103335.0,"Position":288.0,"HyperDash":false}]},{"StartTime":103635.0,"Objects":[{"StartTime":103635.0,"Position":272.0,"HyperDash":false}]},{"StartTime":103935.0,"Objects":[{"StartTime":103935.0,"Position":176.0,"HyperDash":false}]},{"StartTime":104385.0,"Objects":[{"StartTime":104385.0,"Position":64.0,"HyperDash":false}]},{"StartTime":104535.0,"Objects":[{"StartTime":104535.0,"Position":120.0,"HyperDash":false}]},{"StartTime":104685.0,"Objects":[{"StartTime":104685.0,"Position":104.0,"HyperDash":false}]},{"StartTime":104835.0,"Objects":[{"StartTime":104835.0,"Position":140.0,"HyperDash":false}]},{"StartTime":104985.0,"Objects":[{"StartTime":104985.0,"Position":140.0,"HyperDash":false}]},{"StartTime":105135.0,"Objects":[{"StartTime":105135.0,"Position":120.0,"HyperDash":false},{"StartTime":105209.0,"Position":126.278061,"HyperDash":false},{"StartTime":105284.0,"Position":134.685547,"HyperDash":false},{"StartTime":105359.0,"Position":146.535583,"HyperDash":false},{"StartTime":105434.0,"Position":176.619583,"HyperDash":false},{"StartTime":105509.0,"Position":149.913956,"HyperDash":false},{"StartTime":105584.0,"Position":134.963058,"HyperDash":false},{"StartTime":105659.0,"Position":122.398125,"HyperDash":false},{"StartTime":105734.0,"Position":120.0,"HyperDash":false},{"StartTime":105809.0,"Position":138.336151,"HyperDash":false},{"StartTime":105884.0,"Position":134.8243,"HyperDash":false},{"StartTime":105941.0,"Position":165.701263,"HyperDash":false},{"StartTime":106034.0,"Position":176.619583,"HyperDash":false}]},{"StartTime":106334.0,"Objects":[{"StartTime":106334.0,"Position":248.0,"HyperDash":false},{"StartTime":106408.0,"Position":283.699738,"HyperDash":false},{"StartTime":106483.0,"Position":297.674133,"HyperDash":false},{"StartTime":106558.0,"Position":330.484833,"HyperDash":false},{"StartTime":106633.0,"Position":346.951965,"HyperDash":false},{"StartTime":106708.0,"Position":384.7983,"HyperDash":false},{"StartTime":106783.0,"Position":393.6557,"HyperDash":false},{"StartTime":106840.0,"Position":405.136719,"HyperDash":false},{"StartTime":106933.0,"Position":435.388184,"HyperDash":false}]},{"StartTime":107234.0,"Objects":[{"StartTime":107234.0,"Position":464.0,"HyperDash":false},{"StartTime":107308.0,"Position":456.621124,"HyperDash":false},{"StartTime":107383.0,"Position":471.782776,"HyperDash":false},{"StartTime":107458.0,"Position":457.492584,"HyperDash":false},{"StartTime":107533.0,"Position":461.751678,"HyperDash":false},{"StartTime":107608.0,"Position":448.0888,"HyperDash":false},{"StartTime":107683.0,"Position":429.117279,"HyperDash":false},{"StartTime":107740.0,"Position":423.223846,"HyperDash":false},{"StartTime":107833.0,"Position":382.2534,"HyperDash":false}]},{"StartTime":108134.0,"Objects":[{"StartTime":108134.0,"Position":24.0,"HyperDash":false}]},{"StartTime":108433.0,"Objects":[{"StartTime":108433.0,"Position":88.0,"HyperDash":false}]},{"StartTime":108733.0,"Objects":[{"StartTime":108733.0,"Position":200.0,"HyperDash":false}]},{"StartTime":108883.0,"Objects":[{"StartTime":108883.0,"Position":220.0,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/37902.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/37902.osu new file mode 100644 index 0000000000..04942acb1e --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/37902.osu @@ -0,0 +1,230 @@ +osu file format v5 + +[General] +StackLeniency: 0.7 +Mode: 0 + +[Difficulty] +HPDrainRate:5 +CircleSize:3 +OverallDifficulty:5 +SliderMultiplier:1 +SliderTickRate:2 + +[Events] +//Break Periods +2,55404,58804 +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples +//Background Colour Transformations +3,100,163,162,255 + +[TimingPoints] +2421,299.895036737142,4,1,0,100,1,0 +27079,-100,4,2,0,100,0,0 +27529,-100,4,1,0,100,0,0 +33077,-100,4,2,0,100,0,0 +33527,-100,4,1,0,100,0,0 +39075,-100,4,2,0,100,0,0 +39525,-100,4,1,0,100,0,0 +45073,-100,4,2,0,100,0,0 +53696,-100,4,1,0,100,0,0 +60000,-200,4,1,0,100,0,0 +64799,-100,4,1,0,100,0,0 + +[HitObjects] +48,192,12017,2,0,B|104:312|272:312,1,250,0|2 +320,312,13067,1,0 +392,312,13367,1,2 +464,312,13667,1,0 +464,240,13966,1,4 +464,240,14116,1,4 +464,168,14416,6,0,B|464:80|400:32|272:32,1,250,0|2 +216,32,15466,1,0 +144,32,15766,1,2 +72,32,16066,1,0 +72,104,16366,1,4 +72,208,16815,6,0,B|72:288|152:288|152:208|248:208|248:160|296:160,1,250,0|2 +320,128,17865,1,0 +376,88,18165,1,2 +440,64,18465,1,0 +504,48,18765,1,4 +504,48,18915,1,4 +504,120,19215,6,0,B|504:232|400:232|296:232,1,250,0|2 +248,232,20264,1,0 +248,160,20564,1,2 +176,160,20864,1,0 +176,232,21164,1,4 +72,232,21614,6,0,B|72:88|112:88|200:40,1,250,0|2 +264,32,22663,2,0,B|456:32|456:224,1,300,0|2 +456,280,23863,2,0,B|360:280|336:320|336:352|320:368|168:368,1,300,0|2 +184,296,25063,5,4 +184,152,25662,1,4 +320,152,26262,1,4 +320,296,26862,1,4 +96,296,27612,6,0,B|96:168|144:120|240:120,1,250,0|2 +296,120,28661,2,0,B|296:248|232:328|128:352,1,300,0|2 +72,352,29861,2,0,B|32:240|32:96|112:56,1,300,0|2 +160,64,31060,5,4 +296,64,31660,1,4 +432,64,32260,1,4 +432,200,32860,1,4 +256,192,33610,6,0,B|136:192,2,100 +256,192,34359,1,2 +256,264,34659,1,2 +256,264,34809,2,0,B|384:264,2,100 +256,264,35559,1,2 +256,336,35859,1,2 +256,336,36009,2,0,B|136:336,2,100 +256,336,36758,1,2 +328,336,37058,2,0,B|456:336|456:184,3,200,4|4|4|4 +440,40,39607,5,0 +368,40,39907,1,0 +296,40,40207,1,0 +256,40,40357,2,2,B|112:40,1,100 +88,120,41107,1,0 +160,120,41407,1,0 +192,120,41557,2,2,B|328:120,1,100 +360,192,42307,1,0 +288,192,42606,1,0 +256,192,42756,2,2,B|144:192,1,100,2|4 +158,262,43356,5,0 +158,262,43506,1,0 +158,262,43656,1,4 +230,262,43956,1,0 +230,262,44106,1,0 +230,262,44256,1,4 +302,262,44556,1,0 +302,262,44706,1,0 +302,262,44856,1,4 +256,88,45605,6,2,B|328:88,5,50,2|2|2|2|0|2 +376,88,46655,1,0 +448,88,46955,1,2 +448,160,47255,1,0 +376,160,47555,1,0 +376,160,47705,1,4 +376,232,48005,6,2,B|440:232,2,50 +336,232,48454,1,2 +304,232,48604,1,0 +264,232,48754,1,2 +192,232,49054,1,0 +120,232,49354,1,2 +48,232,49654,1,0 +48,160,49954,1,4 +48,56,50404,6,2,B|112:56,1,50 +136,56,50704,2,2,B|208:56,1,50 +224,56,51003,2,2,B|288:56,1,50,0|2 +344,56,51453,1,0 +416,56,51753,1,2 +488,56,52053,1,0 +488,128,52353,1,0 +488,128,52503,1,4 +488,200,52803,6,2,B|432:200,3,50 +400,200,53403,1,0 +368,200,53553,2,0,B|296:200|280:120,2,100,2|4|4 +368,272,54452,2,4,B|360:368|120:344,1,250,4|4 +256,288,60000,6,0,B|400:288,1,125 +416,288,61050,2,0,B|416:128,1,150 +416,104,62250,2,0,B|240:104,1,150,0|0 +232,104,63449,2,0,B|232:296,1,175,0|4 +160,280,64799,6,0,B|48:280,3,100,0|8|0|8 +56,208,65998,1,0 +56,136,66298,1,8 +56,64,66598,1,0 +128,64,66898,1,8 +256,192,67198,12,0,69597 +256,192,69897,5,8 +288,192,70047,1,0 +320,192,70197,1,0 +320,120,70497,1,8 +320,48,70797,1,0 +248,48,71096,1,8 +208,48,71246,1,0 +176,48,71396,1,0 +104,48,71696,1,8 +104,120,71996,6,0,B|16:120,2,50,0|0|8 +104,120,72446,1,2 +104,192,72746,1,2 +104,264,73046,2,2,B|104:352|264:352|424:352,1,350,2|4 +416,280,74395,6,0,B|416:216|320:216,3,100,0|8|0|8 +280,216,75595,1,0 +208,216,75895,1,8 +208,144,76195,1,0 +208,112,76345,1,0 +208,80,76495,1,8 +256,192,76794,12,0,78893 +256,192,79194,6,2,B|256:104|152:88,1,150,6|0 +128,88,79793,2,2,B|56:72|56:200,1,150,2|0 +56,264,80543,1,2 +56,336,80843,1,0 +128,336,81143,1,2 +200,336,81443,1,4 +200,336,81593,6,2,B|320:336|384:224,2,200,6|2|0 +200,336,82942,1,2 +200,264,83242,1,2 +200,192,83542,1,4 +200,160,83692,1,4 +200,128,83842,1,4 +200,96,83992,6,2,B|200:40|248:24|360:24,1,150,6|0 +344,24,84592,2,2,B|440:24|480:48|480:120,1,150,2|0 +472,144,85341,1,2 +472,216,85641,1,0 +472,288,85941,1,2 +400,288,86241,1,4 +400,288,86391,6,2,B|272:288|296:216|192:216,2,200,6|2|0 +400,288,87741,5,2 +400,216,88041,1,2 +400,144,88340,1,4 +360,144,88490,1,4 +320,144,88640,1,4 +256,192,88940,12,0,89690 +56,192,89990,5,0 +56,192,90140,1,0 +56,192,90290,1,8 +128,192,90590,1,0 +160,192,90740,2,2,B|224:192|248:104,3,100 +264,72,91939,1,4 +264,72,92089,1,4 +336,72,92389,5,0 +368,72,92539,1,0 +400,72,92689,1,8 +432,72,92839,1,0 +464,72,92989,1,0 +496,72,93139,1,2 +496,144,93439,2,2,B|496:256|232:256,1,300,2|4 +160,192,94788,6,0,B|160:136,2,50,0|0|8 +160,224,95238,1,0 +160,256,95388,1,0 +160,288,95538,2,2,B|160:360|238:362,1,100 +296,360,96138,2,2,B|376:360|416:312,1,100 +440,288,96737,1,4 +440,288,96887,1,4 +440,216,97187,6,0,B|440:80|264:80,1,250,0|2 +200,80,98237,1,2 +128,80,98537,1,2 +56,80,98837,1,4 +56,152,99137,6,0,B|136:152,2,50 +56,184,99586,2,0,B|56:264|144:264,3,100,8|8|8|8 +144,264,100636,1,4 +216,264,100936,1,4 +288,264,101236,1,4 +360,264,101536,2,0,B|464:264|496:136,1,200,4|0 +496,72,102435,6,0,B|360:72|320:208,1,200 +304,232,103335,1,2 +280,296,103635,1,0 +224,344,103935,1,4 +120,296,104385,5,8 +120,264,104535,1,0 +120,232,104685,1,8 +120,200,104835,1,0 +120,168,104985,1,8 +120,136,105135,2,4,B|120:64|216:56,3,100,0|4|4|4 +248,48,106334,2,0,B|376:48|416:88|464:128,1,200,4|0 +464,168,107234,2,0,B|488:248|456:312|376:320,1,200,0|4 +200,320,108134,5,4 +56,192,108433,1,4 +200,64,108733,1,4 +200,64,108883,1,4 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/39206-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/39206-expected-conversion.json new file mode 100644 index 0000000000..35fcd88d4e --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/39206-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":678.0,"Objects":[{"StartTime":678.0,"Position":256.0,"HyperDash":false}]},{"StartTime":1021.0,"Objects":[{"StartTime":1021.0,"Position":456.0,"HyperDash":false}]},{"StartTime":1193.0,"Objects":[{"StartTime":1193.0,"Position":456.0,"HyperDash":false}]},{"StartTime":1364.0,"Objects":[{"StartTime":1364.0,"Position":456.0,"HyperDash":false}]},{"StartTime":1707.0,"Objects":[{"StartTime":1707.0,"Position":312.0,"HyperDash":false}]},{"StartTime":1878.0,"Objects":[{"StartTime":1878.0,"Position":312.0,"HyperDash":false}]},{"StartTime":2050.0,"Objects":[{"StartTime":2050.0,"Position":312.0,"HyperDash":false}]},{"StartTime":2393.0,"Objects":[{"StartTime":2393.0,"Position":168.0,"HyperDash":false}]},{"StartTime":2564.0,"Objects":[{"StartTime":2564.0,"Position":168.0,"HyperDash":false}]},{"StartTime":2736.0,"Objects":[{"StartTime":2736.0,"Position":168.0,"HyperDash":false}]},{"StartTime":3078.0,"Objects":[{"StartTime":3078.0,"Position":24.0,"HyperDash":false}]},{"StartTime":3250.0,"Objects":[{"StartTime":3250.0,"Position":24.0,"HyperDash":false}]},{"StartTime":3421.0,"Objects":[{"StartTime":3421.0,"Position":24.0,"HyperDash":false}]},{"StartTime":3764.0,"Objects":[{"StartTime":3764.0,"Position":56.0,"HyperDash":false}]},{"StartTime":3936.0,"Objects":[{"StartTime":3936.0,"Position":136.0,"HyperDash":false}]},{"StartTime":4107.0,"Objects":[{"StartTime":4107.0,"Position":216.0,"HyperDash":false}]},{"StartTime":4450.0,"Objects":[{"StartTime":4450.0,"Position":296.0,"HyperDash":false}]},{"StartTime":4621.0,"Objects":[{"StartTime":4621.0,"Position":376.0,"HyperDash":false}]},{"StartTime":4793.0,"Objects":[{"StartTime":4793.0,"Position":456.0,"HyperDash":false}]},{"StartTime":5135.0,"Objects":[{"StartTime":5135.0,"Position":456.0,"HyperDash":false}]},{"StartTime":5307.0,"Objects":[{"StartTime":5307.0,"Position":376.0,"HyperDash":false}]},{"StartTime":5478.0,"Objects":[{"StartTime":5478.0,"Position":296.0,"HyperDash":false}]},{"StartTime":5821.0,"Objects":[{"StartTime":5821.0,"Position":216.0,"HyperDash":false}]},{"StartTime":5993.0,"Objects":[{"StartTime":5993.0,"Position":136.0,"HyperDash":false}]},{"StartTime":6164.0,"Objects":[{"StartTime":6164.0,"Position":56.0,"HyperDash":false}]},{"StartTime":6507.0,"Objects":[{"StartTime":6507.0,"Position":24.0,"HyperDash":false},{"StartTime":6583.0,"Position":9.0,"HyperDash":false},{"StartTime":6660.0,"Position":13.0,"HyperDash":false},{"StartTime":6736.0,"Position":21.0,"HyperDash":false},{"StartTime":6849.0,"Position":24.0,"HyperDash":false}]},{"StartTime":7193.0,"Objects":[{"StartTime":7193.0,"Position":144.0,"HyperDash":false},{"StartTime":7269.0,"Position":159.0,"HyperDash":false},{"StartTime":7346.0,"Position":161.0,"HyperDash":false},{"StartTime":7422.0,"Position":145.0,"HyperDash":false},{"StartTime":7535.0,"Position":144.0,"HyperDash":false}]},{"StartTime":7878.0,"Objects":[{"StartTime":7878.0,"Position":256.0,"HyperDash":false},{"StartTime":7954.0,"Position":255.0,"HyperDash":false},{"StartTime":8031.0,"Position":241.0,"HyperDash":false},{"StartTime":8107.0,"Position":248.0,"HyperDash":false},{"StartTime":8220.0,"Position":256.0,"HyperDash":false}]},{"StartTime":8564.0,"Objects":[{"StartTime":8564.0,"Position":376.0,"HyperDash":false},{"StartTime":8640.0,"Position":364.0,"HyperDash":false},{"StartTime":8717.0,"Position":372.0,"HyperDash":false},{"StartTime":8793.0,"Position":390.0,"HyperDash":false},{"StartTime":8906.0,"Position":376.0,"HyperDash":false}]},{"StartTime":9250.0,"Objects":[{"StartTime":9250.0,"Position":488.0,"HyperDash":false},{"StartTime":9326.0,"Position":492.0,"HyperDash":false},{"StartTime":9403.0,"Position":479.0,"HyperDash":false},{"StartTime":9479.0,"Position":493.0,"HyperDash":false},{"StartTime":9592.0,"Position":488.0,"HyperDash":false}]},{"StartTime":9935.0,"Objects":[{"StartTime":9935.0,"Position":17.0,"HyperDash":false},{"StartTime":10004.0,"Position":433.0,"HyperDash":false},{"StartTime":10074.0,"Position":201.0,"HyperDash":false},{"StartTime":10144.0,"Position":244.0,"HyperDash":false},{"StartTime":10213.0,"Position":55.0,"HyperDash":false},{"StartTime":10283.0,"Position":166.0,"HyperDash":false},{"StartTime":10353.0,"Position":332.0,"HyperDash":false},{"StartTime":10422.0,"Position":460.0,"HyperDash":false},{"StartTime":10492.0,"Position":329.0,"HyperDash":false},{"StartTime":10562.0,"Position":156.0,"HyperDash":false},{"StartTime":10631.0,"Position":273.0,"HyperDash":false},{"StartTime":10701.0,"Position":57.0,"HyperDash":false},{"StartTime":10771.0,"Position":199.0,"HyperDash":false},{"StartTime":10840.0,"Position":485.0,"HyperDash":false},{"StartTime":10910.0,"Position":388.0,"HyperDash":false},{"StartTime":10980.0,"Position":470.0,"HyperDash":false},{"StartTime":11050.0,"Position":326.0,"HyperDash":false}]},{"StartTime":11307.0,"Objects":[{"StartTime":11307.0,"Position":40.0,"HyperDash":false}]},{"StartTime":11393.0,"Objects":[{"StartTime":11393.0,"Position":56.0,"HyperDash":false}]},{"StartTime":11478.0,"Objects":[{"StartTime":11478.0,"Position":80.0,"HyperDash":false}]},{"StartTime":11564.0,"Objects":[{"StartTime":11564.0,"Position":104.0,"HyperDash":false}]},{"StartTime":11650.0,"Objects":[{"StartTime":11650.0,"Position":128.0,"HyperDash":false},{"StartTime":11726.0,"Position":139.513672,"HyperDash":false},{"StartTime":11803.0,"Position":178.88179,"HyperDash":false},{"StartTime":11879.0,"Position":208.079636,"HyperDash":false},{"StartTime":11992.0,"Position":226.574265,"HyperDash":false}]},{"StartTime":12336.0,"Objects":[{"StartTime":12336.0,"Position":288.0,"HyperDash":false},{"StartTime":12412.0,"Position":273.486328,"HyperDash":false},{"StartTime":12489.0,"Position":256.118225,"HyperDash":false},{"StartTime":12565.0,"Position":223.920364,"HyperDash":false},{"StartTime":12678.0,"Position":189.425735,"HyperDash":false}]},{"StartTime":13021.0,"Objects":[{"StartTime":13021.0,"Position":344.0,"HyperDash":false},{"StartTime":13097.0,"Position":346.513672,"HyperDash":false},{"StartTime":13174.0,"Position":370.8818,"HyperDash":false},{"StartTime":13250.0,"Position":413.079651,"HyperDash":false},{"StartTime":13363.0,"Position":442.574249,"HyperDash":false}]},{"StartTime":13707.0,"Objects":[{"StartTime":13707.0,"Position":504.0,"HyperDash":false},{"StartTime":13783.0,"Position":490.486328,"HyperDash":false},{"StartTime":13860.0,"Position":453.1182,"HyperDash":false},{"StartTime":13936.0,"Position":440.920349,"HyperDash":false},{"StartTime":14049.0,"Position":405.425751,"HyperDash":false}]},{"StartTime":14221.0,"Objects":[{"StartTime":14221.0,"Position":328.0,"HyperDash":false}]},{"StartTime":14307.0,"Objects":[{"StartTime":14307.0,"Position":312.0,"HyperDash":false}]},{"StartTime":14393.0,"Objects":[{"StartTime":14393.0,"Position":296.0,"HyperDash":false},{"StartTime":14469.0,"Position":285.453,"HyperDash":false},{"StartTime":14546.0,"Position":295.793518,"HyperDash":false},{"StartTime":14622.0,"Position":253.246521,"HyperDash":false},{"StartTime":14735.0,"Position":257.538452,"HyperDash":false}]},{"StartTime":15078.0,"Objects":[{"StartTime":15078.0,"Position":160.0,"HyperDash":false},{"StartTime":15154.0,"Position":179.547012,"HyperDash":false},{"StartTime":15231.0,"Position":158.206482,"HyperDash":false},{"StartTime":15307.0,"Position":192.7535,"HyperDash":false},{"StartTime":15420.0,"Position":198.461548,"HyperDash":false}]},{"StartTime":15764.0,"Objects":[{"StartTime":15764.0,"Position":296.0,"HyperDash":false},{"StartTime":15840.0,"Position":298.453,"HyperDash":false},{"StartTime":15917.0,"Position":269.793518,"HyperDash":false},{"StartTime":15993.0,"Position":263.246521,"HyperDash":false},{"StartTime":16106.0,"Position":257.538452,"HyperDash":false}]},{"StartTime":16450.0,"Objects":[{"StartTime":16450.0,"Position":160.0,"HyperDash":false},{"StartTime":16526.0,"Position":168.547012,"HyperDash":false},{"StartTime":16603.0,"Position":183.206482,"HyperDash":false},{"StartTime":16679.0,"Position":170.7535,"HyperDash":false},{"StartTime":16792.0,"Position":198.461548,"HyperDash":false}]},{"StartTime":16964.0,"Objects":[{"StartTime":16964.0,"Position":112.0,"HyperDash":false}]},{"StartTime":17050.0,"Objects":[{"StartTime":17050.0,"Position":96.0,"HyperDash":false}]},{"StartTime":17136.0,"Objects":[{"StartTime":17136.0,"Position":88.0,"HyperDash":false},{"StartTime":17221.0,"Position":108.141563,"HyperDash":false},{"StartTime":17307.0,"Position":125.724724,"HyperDash":false},{"StartTime":17392.0,"Position":123.658127,"HyperDash":false},{"StartTime":17478.0,"Position":151.393967,"HyperDash":false},{"StartTime":17564.0,"Position":185.463791,"HyperDash":false},{"StartTime":17650.0,"Position":197.255447,"HyperDash":false},{"StartTime":17735.0,"Position":168.730637,"HyperDash":false},{"StartTime":17821.0,"Position":151.639252,"HyperDash":false},{"StartTime":17897.0,"Position":121.04126,"HyperDash":false},{"StartTime":17974.0,"Position":121.285477,"HyperDash":false},{"StartTime":18051.0,"Position":123.615044,"HyperDash":false},{"StartTime":18164.0,"Position":88.0,"HyperDash":false}]},{"StartTime":18507.0,"Objects":[{"StartTime":18507.0,"Position":424.0,"HyperDash":false},{"StartTime":18592.0,"Position":408.858429,"HyperDash":false},{"StartTime":18678.0,"Position":397.275269,"HyperDash":false},{"StartTime":18763.0,"Position":362.3419,"HyperDash":false},{"StartTime":18849.0,"Position":360.606018,"HyperDash":false},{"StartTime":18935.0,"Position":337.536224,"HyperDash":false},{"StartTime":19021.0,"Position":314.744537,"HyperDash":false},{"StartTime":19106.0,"Position":355.269379,"HyperDash":false},{"StartTime":19192.0,"Position":360.360748,"HyperDash":false},{"StartTime":19268.0,"Position":388.95874,"HyperDash":false},{"StartTime":19345.0,"Position":391.7145,"HyperDash":false},{"StartTime":19422.0,"Position":424.384949,"HyperDash":false},{"StartTime":19535.0,"Position":424.0,"HyperDash":false}]},{"StartTime":19707.0,"Objects":[{"StartTime":19707.0,"Position":368.0,"HyperDash":false}]},{"StartTime":19793.0,"Objects":[{"StartTime":19793.0,"Position":352.0,"HyperDash":false}]},{"StartTime":19878.0,"Objects":[{"StartTime":19878.0,"Position":336.0,"HyperDash":false},{"StartTime":19954.0,"Position":304.777771,"HyperDash":false},{"StartTime":20031.0,"Position":306.263153,"HyperDash":false},{"StartTime":20107.0,"Position":256.040924,"HyperDash":false},{"StartTime":20220.0,"Position":236.0,"HyperDash":false}]},{"StartTime":20564.0,"Objects":[{"StartTime":20564.0,"Position":136.0,"HyperDash":false},{"StartTime":20640.0,"Position":147.222229,"HyperDash":false},{"StartTime":20717.0,"Position":184.736847,"HyperDash":false},{"StartTime":20793.0,"Position":190.959076,"HyperDash":false},{"StartTime":20906.0,"Position":236.0,"HyperDash":false}]},{"StartTime":21250.0,"Objects":[{"StartTime":21250.0,"Position":392.0,"HyperDash":false},{"StartTime":21335.0,"Position":420.1406,"HyperDash":false},{"StartTime":21421.0,"Position":400.481,"HyperDash":false},{"StartTime":21506.0,"Position":414.916046,"HyperDash":false},{"StartTime":21592.0,"Position":414.21582,"HyperDash":false},{"StartTime":21660.0,"Position":403.507965,"HyperDash":false},{"StartTime":21764.0,"Position":397.683655,"HyperDash":true}]},{"StartTime":21936.0,"Objects":[{"StartTime":21936.0,"Position":120.0,"HyperDash":false},{"StartTime":22021.0,"Position":99.85941,"HyperDash":false},{"StartTime":22107.0,"Position":90.5190048,"HyperDash":false},{"StartTime":22192.0,"Position":91.0839539,"HyperDash":false},{"StartTime":22278.0,"Position":97.78417,"HyperDash":false},{"StartTime":22346.0,"Position":116.49202,"HyperDash":false},{"StartTime":22450.0,"Position":114.31633,"HyperDash":false}]},{"StartTime":22621.0,"Objects":[{"StartTime":22621.0,"Position":176.0,"HyperDash":false},{"StartTime":22706.0,"Position":203.4664,"HyperDash":false},{"StartTime":22792.0,"Position":212.3834,"HyperDash":false},{"StartTime":22877.0,"Position":234.448669,"HyperDash":false},{"StartTime":22963.0,"Position":266.38324,"HyperDash":false},{"StartTime":23031.0,"Position":276.057281,"HyperDash":false},{"StartTime":23135.0,"Position":297.221375,"HyperDash":false}]},{"StartTime":23307.0,"Objects":[{"StartTime":23307.0,"Position":297.0,"HyperDash":false}]},{"StartTime":23821.0,"Objects":[{"StartTime":23821.0,"Position":448.0,"HyperDash":false}]},{"StartTime":23993.0,"Objects":[{"StartTime":23993.0,"Position":352.0,"HyperDash":false},{"StartTime":24069.0,"Position":334.661774,"HyperDash":false},{"StartTime":24146.0,"Position":291.266022,"HyperDash":false},{"StartTime":24222.0,"Position":288.570435,"HyperDash":false},{"StartTime":24335.0,"Position":255.710861,"HyperDash":false}]},{"StartTime":24507.0,"Objects":[{"StartTime":24507.0,"Position":160.0,"HyperDash":false}]},{"StartTime":24593.0,"Objects":[{"StartTime":24593.0,"Position":160.0,"HyperDash":false}]},{"StartTime":24678.0,"Objects":[{"StartTime":24678.0,"Position":160.0,"HyperDash":false}]},{"StartTime":25021.0,"Objects":[{"StartTime":25021.0,"Position":88.0,"HyperDash":false}]},{"StartTime":25193.0,"Objects":[{"StartTime":25193.0,"Position":176.0,"HyperDash":false}]},{"StartTime":25364.0,"Objects":[{"StartTime":25364.0,"Position":256.0,"HyperDash":false}]},{"StartTime":25707.0,"Objects":[{"StartTime":25707.0,"Position":424.0,"HyperDash":false}]},{"StartTime":25878.0,"Objects":[{"StartTime":25878.0,"Position":448.0,"HyperDash":false}]},{"StartTime":26050.0,"Objects":[{"StartTime":26050.0,"Position":472.0,"HyperDash":false},{"StartTime":26135.0,"Position":467.1815,"HyperDash":false},{"StartTime":26221.0,"Position":430.508972,"HyperDash":false},{"StartTime":26306.0,"Position":426.762726,"HyperDash":false},{"StartTime":26392.0,"Position":386.21756,"HyperDash":false},{"StartTime":26460.0,"Position":355.4007,"HyperDash":false},{"StartTime":26564.0,"Position":336.352875,"HyperDash":false}]},{"StartTime":26736.0,"Objects":[{"StartTime":26736.0,"Position":304.0,"HyperDash":false},{"StartTime":26812.0,"Position":269.465637,"HyperDash":false},{"StartTime":26889.0,"Position":283.52124,"HyperDash":false},{"StartTime":26965.0,"Position":262.29248,"HyperDash":false},{"StartTime":27078.0,"Position":241.4827,"HyperDash":false}]},{"StartTime":27250.0,"Objects":[{"StartTime":27250.0,"Position":508.0,"HyperDash":false},{"StartTime":27303.0,"Position":417.0,"HyperDash":false},{"StartTime":27357.0,"Position":302.0,"HyperDash":false},{"StartTime":27410.0,"Position":132.0,"HyperDash":false},{"StartTime":27464.0,"Position":352.0,"HyperDash":false},{"StartTime":27517.0,"Position":174.0,"HyperDash":false},{"StartTime":27571.0,"Position":453.0,"HyperDash":false},{"StartTime":27624.0,"Position":205.0,"HyperDash":false},{"StartTime":27678.0,"Position":105.0,"HyperDash":false},{"StartTime":27732.0,"Position":213.0,"HyperDash":false},{"StartTime":27785.0,"Position":472.0,"HyperDash":false},{"StartTime":27839.0,"Position":251.0,"HyperDash":false},{"StartTime":27892.0,"Position":208.0,"HyperDash":false},{"StartTime":27946.0,"Position":261.0,"HyperDash":false},{"StartTime":27999.0,"Position":382.0,"HyperDash":false},{"StartTime":28053.0,"Position":170.0,"HyperDash":false},{"StartTime":28107.0,"Position":269.0,"HyperDash":false}]},{"StartTime":28621.0,"Objects":[{"StartTime":28621.0,"Position":32.0,"HyperDash":false}]},{"StartTime":28793.0,"Objects":[{"StartTime":28793.0,"Position":80.0,"HyperDash":false}]},{"StartTime":29307.0,"Objects":[{"StartTime":29307.0,"Position":352.0,"HyperDash":false}]},{"StartTime":29478.0,"Objects":[{"StartTime":29478.0,"Position":424.0,"HyperDash":false}]},{"StartTime":29650.0,"Objects":[{"StartTime":29650.0,"Position":472.0,"HyperDash":false}]},{"StartTime":29821.0,"Objects":[{"StartTime":29821.0,"Position":432.0,"HyperDash":false}]},{"StartTime":29993.0,"Objects":[{"StartTime":29993.0,"Position":360.0,"HyperDash":false}]},{"StartTime":30078.0,"Objects":[{"StartTime":30078.0,"Position":360.0,"HyperDash":false}]},{"StartTime":30164.0,"Objects":[{"StartTime":30164.0,"Position":360.0,"HyperDash":false}]},{"StartTime":30507.0,"Objects":[{"StartTime":30507.0,"Position":184.0,"HyperDash":false},{"StartTime":30592.0,"Position":194.11496,"HyperDash":false},{"StartTime":30678.0,"Position":206.360687,"HyperDash":false},{"StartTime":30745.0,"Position":207.599487,"HyperDash":false},{"StartTime":30849.0,"Position":184.0,"HyperDash":false}]},{"StartTime":31193.0,"Objects":[{"StartTime":31193.0,"Position":64.0,"HyperDash":false},{"StartTime":31278.0,"Position":59.6773758,"HyperDash":false},{"StartTime":31364.0,"Position":91.51566,"HyperDash":false},{"StartTime":31431.0,"Position":76.73467,"HyperDash":false},{"StartTime":31535.0,"Position":64.0,"HyperDash":false}]},{"StartTime":31878.0,"Objects":[{"StartTime":31878.0,"Position":352.0,"HyperDash":false},{"StartTime":31963.0,"Position":310.184479,"HyperDash":false},{"StartTime":32049.0,"Position":302.077,"HyperDash":false},{"StartTime":32116.0,"Position":332.637482,"HyperDash":false},{"StartTime":32220.0,"Position":352.0,"HyperDash":false}]},{"StartTime":32393.0,"Objects":[{"StartTime":32393.0,"Position":320.0,"HyperDash":false},{"StartTime":32435.0,"Position":297.345428,"HyperDash":false},{"StartTime":32478.0,"Position":320.0,"HyperDash":false},{"StartTime":32521.0,"Position":297.345428,"HyperDash":false},{"StartTime":32564.0,"Position":320.0,"HyperDash":false},{"StartTime":32607.0,"Position":297.345428,"HyperDash":false}]},{"StartTime":32736.0,"Objects":[{"StartTime":32736.0,"Position":342.0,"HyperDash":false},{"StartTime":32778.0,"Position":319.345428,"HyperDash":false},{"StartTime":32821.0,"Position":342.0,"HyperDash":false},{"StartTime":32864.0,"Position":319.345428,"HyperDash":false},{"StartTime":32907.0,"Position":342.0,"HyperDash":false},{"StartTime":32950.0,"Position":319.345428,"HyperDash":false}]},{"StartTime":33078.0,"Objects":[{"StartTime":33078.0,"Position":399.0,"HyperDash":false},{"StartTime":33120.0,"Position":376.345428,"HyperDash":false},{"StartTime":33163.0,"Position":399.0,"HyperDash":false},{"StartTime":33206.0,"Position":376.345428,"HyperDash":false},{"StartTime":33249.0,"Position":399.0,"HyperDash":false},{"StartTime":33292.0,"Position":376.345428,"HyperDash":false}]},{"StartTime":33421.0,"Objects":[{"StartTime":33421.0,"Position":422.0,"HyperDash":false},{"StartTime":33463.0,"Position":399.345428,"HyperDash":false},{"StartTime":33506.0,"Position":422.0,"HyperDash":false},{"StartTime":33549.0,"Position":399.345428,"HyperDash":false},{"StartTime":33592.0,"Position":422.0,"HyperDash":false},{"StartTime":33635.0,"Position":399.345428,"HyperDash":false},{"StartTime":33678.0,"Position":422.0,"HyperDash":false}]},{"StartTime":34107.0,"Objects":[{"StartTime":34107.0,"Position":368.0,"HyperDash":false}]},{"StartTime":34278.0,"Objects":[{"StartTime":34278.0,"Position":280.0,"HyperDash":false}]},{"StartTime":34793.0,"Objects":[{"StartTime":34793.0,"Position":280.0,"HyperDash":false}]},{"StartTime":34964.0,"Objects":[{"StartTime":34964.0,"Position":184.0,"HyperDash":false}]},{"StartTime":35136.0,"Objects":[{"StartTime":35136.0,"Position":112.0,"HyperDash":false}]},{"StartTime":35307.0,"Objects":[{"StartTime":35307.0,"Position":64.0,"HyperDash":false}]},{"StartTime":35478.0,"Objects":[{"StartTime":35478.0,"Position":32.0,"HyperDash":false}]},{"StartTime":35564.0,"Objects":[{"StartTime":35564.0,"Position":32.0,"HyperDash":false}]},{"StartTime":35650.0,"Objects":[{"StartTime":35650.0,"Position":32.0,"HyperDash":false}]},{"StartTime":35993.0,"Objects":[{"StartTime":35993.0,"Position":232.0,"HyperDash":false}]},{"StartTime":36164.0,"Objects":[{"StartTime":36164.0,"Position":328.0,"HyperDash":false}]},{"StartTime":36336.0,"Objects":[{"StartTime":36336.0,"Position":408.0,"HyperDash":false}]},{"StartTime":36507.0,"Objects":[{"StartTime":36507.0,"Position":464.0,"HyperDash":false}]},{"StartTime":36678.0,"Objects":[{"StartTime":36678.0,"Position":408.0,"HyperDash":false}]},{"StartTime":36850.0,"Objects":[{"StartTime":36850.0,"Position":328.0,"HyperDash":false}]},{"StartTime":37021.0,"Objects":[{"StartTime":37021.0,"Position":232.0,"HyperDash":false}]},{"StartTime":37535.0,"Objects":[{"StartTime":37535.0,"Position":72.0,"HyperDash":false}]},{"StartTime":37707.0,"Objects":[{"StartTime":37707.0,"Position":112.0,"HyperDash":false}]},{"StartTime":37878.0,"Objects":[{"StartTime":37878.0,"Position":144.0,"HyperDash":false},{"StartTime":37920.0,"Position":119.0,"HyperDash":false},{"StartTime":37963.0,"Position":144.0,"HyperDash":false},{"StartTime":38006.0,"Position":119.0,"HyperDash":false},{"StartTime":38049.0,"Position":144.0,"HyperDash":false}]},{"StartTime":38221.0,"Objects":[{"StartTime":38221.0,"Position":232.0,"HyperDash":false},{"StartTime":38263.0,"Position":207.0,"HyperDash":false},{"StartTime":38306.0,"Position":232.0,"HyperDash":false},{"StartTime":38349.0,"Position":207.0,"HyperDash":false},{"StartTime":38392.0,"Position":232.0,"HyperDash":false}]},{"StartTime":38564.0,"Objects":[{"StartTime":38564.0,"Position":320.0,"HyperDash":false},{"StartTime":38606.0,"Position":295.0,"HyperDash":false},{"StartTime":38649.0,"Position":320.0,"HyperDash":false},{"StartTime":38692.0,"Position":295.0,"HyperDash":false},{"StartTime":38735.0,"Position":320.0,"HyperDash":false}]},{"StartTime":38907.0,"Objects":[{"StartTime":38907.0,"Position":408.0,"HyperDash":false},{"StartTime":38949.0,"Position":383.0,"HyperDash":false},{"StartTime":38992.0,"Position":408.0,"HyperDash":false},{"StartTime":39035.0,"Position":383.0,"HyperDash":false},{"StartTime":39078.0,"Position":408.0,"HyperDash":false}]},{"StartTime":39593.0,"Objects":[{"StartTime":39593.0,"Position":304.0,"HyperDash":false}]},{"StartTime":39764.0,"Objects":[{"StartTime":39764.0,"Position":208.0,"HyperDash":false}]},{"StartTime":40278.0,"Objects":[{"StartTime":40278.0,"Position":40.0,"HyperDash":false}]},{"StartTime":40450.0,"Objects":[{"StartTime":40450.0,"Position":112.0,"HyperDash":false}]},{"StartTime":40621.0,"Objects":[{"StartTime":40621.0,"Position":200.0,"HyperDash":false}]},{"StartTime":40793.0,"Objects":[{"StartTime":40793.0,"Position":264.0,"HyperDash":false}]},{"StartTime":40964.0,"Objects":[{"StartTime":40964.0,"Position":352.0,"HyperDash":false}]},{"StartTime":41050.0,"Objects":[{"StartTime":41050.0,"Position":352.0,"HyperDash":false}]},{"StartTime":41135.0,"Objects":[{"StartTime":41135.0,"Position":352.0,"HyperDash":false}]},{"StartTime":41478.0,"Objects":[{"StartTime":41478.0,"Position":480.0,"HyperDash":false}]},{"StartTime":41650.0,"Objects":[{"StartTime":41650.0,"Position":422.0,"HyperDash":false}]},{"StartTime":41821.0,"Objects":[{"StartTime":41821.0,"Position":364.0,"HyperDash":false}]},{"StartTime":41993.0,"Objects":[{"StartTime":41993.0,"Position":422.0,"HyperDash":false}]},{"StartTime":42164.0,"Objects":[{"StartTime":42164.0,"Position":327.0,"HyperDash":false}]},{"StartTime":42335.0,"Objects":[{"StartTime":42335.0,"Position":226.0,"HyperDash":false}]},{"StartTime":42507.0,"Objects":[{"StartTime":42507.0,"Position":327.0,"HyperDash":false}]},{"StartTime":42678.0,"Objects":[{"StartTime":42678.0,"Position":381.0,"HyperDash":false}]},{"StartTime":42850.0,"Objects":[{"StartTime":42850.0,"Position":437.0,"HyperDash":false}]},{"StartTime":43021.0,"Objects":[{"StartTime":43021.0,"Position":381.0,"HyperDash":false}]},{"StartTime":43193.0,"Objects":[{"StartTime":43193.0,"Position":327.0,"HyperDash":false}]},{"StartTime":43278.0,"Objects":[{"StartTime":43278.0,"Position":16.0,"HyperDash":false},{"StartTime":43374.0,"Position":248.0,"HyperDash":false},{"StartTime":43471.0,"Position":100.0,"HyperDash":false},{"StartTime":43567.0,"Position":24.0,"HyperDash":false},{"StartTime":43664.0,"Position":66.0,"HyperDash":false},{"StartTime":43760.0,"Position":97.0,"HyperDash":false},{"StartTime":43857.0,"Position":267.0,"HyperDash":false},{"StartTime":43953.0,"Position":116.0,"HyperDash":false},{"StartTime":44050.0,"Position":451.0,"HyperDash":false}]},{"StartTime":44221.0,"Objects":[{"StartTime":44221.0,"Position":328.0,"HyperDash":false},{"StartTime":44297.0,"Position":357.352631,"HyperDash":false},{"StartTime":44374.0,"Position":374.9336,"HyperDash":false},{"StartTime":44450.0,"Position":395.286255,"HyperDash":false},{"StartTime":44563.0,"Position":406.086884,"HyperDash":false}]},{"StartTime":44907.0,"Objects":[{"StartTime":44907.0,"Position":184.0,"HyperDash":false},{"StartTime":44983.0,"Position":158.647354,"HyperDash":false},{"StartTime":45060.0,"Position":135.0664,"HyperDash":false},{"StartTime":45136.0,"Position":127.713745,"HyperDash":false},{"StartTime":45249.0,"Position":105.913116,"HyperDash":false}]},{"StartTime":45421.0,"Objects":[{"StartTime":45421.0,"Position":192.0,"HyperDash":false}]},{"StartTime":45507.0,"Objects":[{"StartTime":45507.0,"Position":192.0,"HyperDash":false}]},{"StartTime":45593.0,"Objects":[{"StartTime":45593.0,"Position":192.0,"HyperDash":false}]},{"StartTime":45764.0,"Objects":[{"StartTime":45764.0,"Position":106.0,"HyperDash":false}]},{"StartTime":45850.0,"Objects":[{"StartTime":45850.0,"Position":106.0,"HyperDash":false}]},{"StartTime":45935.0,"Objects":[{"StartTime":45935.0,"Position":106.0,"HyperDash":false}]},{"StartTime":46107.0,"Objects":[{"StartTime":46107.0,"Position":154.0,"HyperDash":false}]},{"StartTime":46278.0,"Objects":[{"StartTime":46278.0,"Position":237.0,"HyperDash":false}]},{"StartTime":46364.0,"Objects":[{"StartTime":46364.0,"Position":237.0,"HyperDash":false}]},{"StartTime":46450.0,"Objects":[{"StartTime":46450.0,"Position":237.0,"HyperDash":false}]},{"StartTime":46535.0,"Objects":[{"StartTime":46535.0,"Position":237.0,"HyperDash":false}]},{"StartTime":46621.0,"Objects":[{"StartTime":46621.0,"Position":237.0,"HyperDash":false}]},{"StartTime":46964.0,"Objects":[{"StartTime":46964.0,"Position":410.0,"HyperDash":false}]},{"StartTime":47135.0,"Objects":[{"StartTime":47135.0,"Position":410.0,"HyperDash":false}]},{"StartTime":47307.0,"Objects":[{"StartTime":47307.0,"Position":462.0,"HyperDash":false}]},{"StartTime":47478.0,"Objects":[{"StartTime":47478.0,"Position":462.0,"HyperDash":false}]},{"StartTime":47650.0,"Objects":[{"StartTime":47650.0,"Position":379.0,"HyperDash":false}]},{"StartTime":47821.0,"Objects":[{"StartTime":47821.0,"Position":379.0,"HyperDash":false}]},{"StartTime":47993.0,"Objects":[{"StartTime":47993.0,"Position":328.0,"HyperDash":false}]},{"StartTime":48164.0,"Objects":[{"StartTime":48164.0,"Position":328.0,"HyperDash":false}]},{"StartTime":48335.0,"Objects":[{"StartTime":48335.0,"Position":237.0,"HyperDash":false}]},{"StartTime":48507.0,"Objects":[{"StartTime":48507.0,"Position":328.0,"HyperDash":false}]},{"StartTime":48678.0,"Objects":[{"StartTime":48678.0,"Position":410.0,"HyperDash":false}]},{"StartTime":48935.0,"Objects":[{"StartTime":48935.0,"Position":264.0,"HyperDash":false}]},{"StartTime":49021.0,"Objects":[{"StartTime":49021.0,"Position":264.0,"HyperDash":false}]},{"StartTime":49193.0,"Objects":[{"StartTime":49193.0,"Position":304.0,"HyperDash":false}]},{"StartTime":49364.0,"Objects":[{"StartTime":49364.0,"Position":368.0,"HyperDash":false}]},{"StartTime":49707.0,"Objects":[{"StartTime":49707.0,"Position":368.0,"HyperDash":false},{"StartTime":49783.0,"Position":403.222229,"HyperDash":false},{"StartTime":49860.0,"Position":411.736847,"HyperDash":false},{"StartTime":49936.0,"Position":434.959076,"HyperDash":false},{"StartTime":50049.0,"Position":468.0,"HyperDash":false}]},{"StartTime":50393.0,"Objects":[{"StartTime":50393.0,"Position":280.0,"HyperDash":false},{"StartTime":50469.0,"Position":295.222229,"HyperDash":false},{"StartTime":50546.0,"Position":340.736847,"HyperDash":false},{"StartTime":50622.0,"Position":355.959076,"HyperDash":false},{"StartTime":50735.0,"Position":380.0,"HyperDash":false}]},{"StartTime":51250.0,"Objects":[{"StartTime":51250.0,"Position":88.0,"HyperDash":false},{"StartTime":51326.0,"Position":103.222221,"HyperDash":false},{"StartTime":51403.0,"Position":148.736847,"HyperDash":false},{"StartTime":51479.0,"Position":138.959076,"HyperDash":false},{"StartTime":51592.0,"Position":188.0,"HyperDash":false}]},{"StartTime":51764.0,"Objects":[{"StartTime":51764.0,"Position":264.0,"HyperDash":false}]},{"StartTime":51850.0,"Objects":[{"StartTime":51850.0,"Position":280.0,"HyperDash":false}]},{"StartTime":51935.0,"Objects":[{"StartTime":51935.0,"Position":296.0,"HyperDash":false}]},{"StartTime":52021.0,"Objects":[{"StartTime":52021.0,"Position":312.0,"HyperDash":false}]},{"StartTime":52107.0,"Objects":[{"StartTime":52107.0,"Position":328.0,"HyperDash":false}]},{"StartTime":52450.0,"Objects":[{"StartTime":52450.0,"Position":208.0,"HyperDash":false}]},{"StartTime":52621.0,"Objects":[{"StartTime":52621.0,"Position":304.0,"HyperDash":false}]},{"StartTime":52793.0,"Objects":[{"StartTime":52793.0,"Position":256.0,"HyperDash":false}]},{"StartTime":53135.0,"Objects":[{"StartTime":53135.0,"Position":208.0,"HyperDash":false}]},{"StartTime":53307.0,"Objects":[{"StartTime":53307.0,"Position":304.0,"HyperDash":false}]},{"StartTime":53478.0,"Objects":[{"StartTime":53478.0,"Position":208.0,"HyperDash":false}]},{"StartTime":53650.0,"Objects":[{"StartTime":53650.0,"Position":304.0,"HyperDash":false}]},{"StartTime":53821.0,"Objects":[{"StartTime":53821.0,"Position":208.0,"HyperDash":false}]},{"StartTime":53993.0,"Objects":[{"StartTime":53993.0,"Position":304.0,"HyperDash":false}]},{"StartTime":54164.0,"Objects":[{"StartTime":54164.0,"Position":247.0,"HyperDash":false},{"StartTime":54217.0,"Position":162.0,"HyperDash":false},{"StartTime":54271.0,"Position":383.0,"HyperDash":false},{"StartTime":54324.0,"Position":127.0,"HyperDash":false},{"StartTime":54378.0,"Position":161.0,"HyperDash":false},{"StartTime":54431.0,"Position":332.0,"HyperDash":false},{"StartTime":54485.0,"Position":356.0,"HyperDash":false},{"StartTime":54538.0,"Position":362.0,"HyperDash":false},{"StartTime":54592.0,"Position":347.0,"HyperDash":false},{"StartTime":54646.0,"Position":252.0,"HyperDash":false},{"StartTime":54699.0,"Position":477.0,"HyperDash":false},{"StartTime":54753.0,"Position":358.0,"HyperDash":false},{"StartTime":54806.0,"Position":17.0,"HyperDash":false},{"StartTime":54860.0,"Position":399.0,"HyperDash":false},{"StartTime":54913.0,"Position":280.0,"HyperDash":false},{"StartTime":54967.0,"Position":304.0,"HyperDash":false},{"StartTime":55021.0,"Position":221.0,"HyperDash":false}]},{"StartTime":55193.0,"Objects":[{"StartTime":55193.0,"Position":256.0,"HyperDash":false},{"StartTime":55269.0,"Position":251.286514,"HyperDash":false},{"StartTime":55346.0,"Position":219.366272,"HyperDash":false},{"StartTime":55422.0,"Position":195.652786,"HyperDash":false},{"StartTime":55535.0,"Position":185.289337,"HyperDash":false}]},{"StartTime":55878.0,"Objects":[{"StartTime":55878.0,"Position":256.0,"HyperDash":false},{"StartTime":55954.0,"Position":280.71347,"HyperDash":false},{"StartTime":56031.0,"Position":289.633728,"HyperDash":false},{"StartTime":56107.0,"Position":299.3472,"HyperDash":false},{"StartTime":56220.0,"Position":326.710663,"HyperDash":false}]},{"StartTime":56393.0,"Objects":[{"StartTime":56393.0,"Position":256.0,"HyperDash":false}]},{"StartTime":56564.0,"Objects":[{"StartTime":56564.0,"Position":160.0,"HyperDash":false}]},{"StartTime":56650.0,"Objects":[{"StartTime":56650.0,"Position":160.0,"HyperDash":false}]},{"StartTime":56735.0,"Objects":[{"StartTime":56735.0,"Position":160.0,"HyperDash":false}]},{"StartTime":56907.0,"Objects":[{"StartTime":56907.0,"Position":160.0,"HyperDash":false}]},{"StartTime":57078.0,"Objects":[{"StartTime":57078.0,"Position":256.0,"HyperDash":false}]},{"StartTime":57250.0,"Objects":[{"StartTime":57250.0,"Position":352.0,"HyperDash":false}]},{"StartTime":57335.0,"Objects":[{"StartTime":57335.0,"Position":360.0,"HyperDash":false}]},{"StartTime":57421.0,"Objects":[{"StartTime":57421.0,"Position":368.0,"HyperDash":false}]},{"StartTime":57507.0,"Objects":[{"StartTime":57507.0,"Position":376.0,"HyperDash":false}]},{"StartTime":57593.0,"Objects":[{"StartTime":57593.0,"Position":384.0,"HyperDash":false}]},{"StartTime":57935.0,"Objects":[{"StartTime":57935.0,"Position":472.0,"HyperDash":false}]},{"StartTime":58107.0,"Objects":[{"StartTime":58107.0,"Position":387.0,"HyperDash":false}]},{"StartTime":58278.0,"Objects":[{"StartTime":58278.0,"Position":284.0,"HyperDash":false}]},{"StartTime":58450.0,"Objects":[{"StartTime":58450.0,"Position":193.0,"HyperDash":false}]},{"StartTime":58621.0,"Objects":[{"StartTime":58621.0,"Position":139.0,"HyperDash":false}]},{"StartTime":58793.0,"Objects":[{"StartTime":58793.0,"Position":132.0,"HyperDash":false}]},{"StartTime":58964.0,"Objects":[{"StartTime":58964.0,"Position":174.0,"HyperDash":false}]},{"StartTime":59307.0,"Objects":[{"StartTime":59307.0,"Position":256.0,"HyperDash":false}]},{"StartTime":59478.0,"Objects":[{"StartTime":59478.0,"Position":208.0,"HyperDash":false}]},{"StartTime":59650.0,"Objects":[{"StartTime":59650.0,"Position":304.0,"HyperDash":false}]},{"StartTime":59821.0,"Objects":[{"StartTime":59821.0,"Position":344.0,"HyperDash":false}]},{"StartTime":59907.0,"Objects":[{"StartTime":59907.0,"Position":312.0,"HyperDash":false}]},{"StartTime":59993.0,"Objects":[{"StartTime":59993.0,"Position":280.0,"HyperDash":false}]},{"StartTime":60164.0,"Objects":[{"StartTime":60164.0,"Position":208.0,"HyperDash":false}]},{"StartTime":60335.0,"Objects":[{"StartTime":60335.0,"Position":304.0,"HyperDash":false}]},{"StartTime":60678.0,"Objects":[{"StartTime":60678.0,"Position":200.0,"HyperDash":false},{"StartTime":60754.0,"Position":175.647354,"HyperDash":false},{"StartTime":60831.0,"Position":176.0664,"HyperDash":false},{"StartTime":60907.0,"Position":137.713745,"HyperDash":false},{"StartTime":61020.0,"Position":121.913116,"HyperDash":false}]},{"StartTime":61364.0,"Objects":[{"StartTime":61364.0,"Position":312.0,"HyperDash":false},{"StartTime":61440.0,"Position":348.352631,"HyperDash":false},{"StartTime":61517.0,"Position":333.9336,"HyperDash":false},{"StartTime":61593.0,"Position":362.286255,"HyperDash":false},{"StartTime":61706.0,"Position":390.086884,"HyperDash":false}]},{"StartTime":62050.0,"Objects":[{"StartTime":62050.0,"Position":390.0,"HyperDash":false}]},{"StartTime":62393.0,"Objects":[{"StartTime":62393.0,"Position":121.0,"HyperDash":false}]},{"StartTime":62735.0,"Objects":[{"StartTime":62735.0,"Position":256.0,"HyperDash":false}]},{"StartTime":62821.0,"Objects":[{"StartTime":62821.0,"Position":256.0,"HyperDash":false}]},{"StartTime":62907.0,"Objects":[{"StartTime":62907.0,"Position":256.0,"HyperDash":false}]},{"StartTime":62993.0,"Objects":[{"StartTime":62993.0,"Position":256.0,"HyperDash":false}]},{"StartTime":63078.0,"Objects":[{"StartTime":63078.0,"Position":256.0,"HyperDash":false}]},{"StartTime":63421.0,"Objects":[{"StartTime":63421.0,"Position":432.0,"HyperDash":false}]},{"StartTime":63593.0,"Objects":[{"StartTime":63593.0,"Position":496.0,"HyperDash":false}]},{"StartTime":63764.0,"Objects":[{"StartTime":63764.0,"Position":496.0,"HyperDash":false}]},{"StartTime":63935.0,"Objects":[{"StartTime":63935.0,"Position":440.0,"HyperDash":false}]},{"StartTime":64107.0,"Objects":[{"StartTime":64107.0,"Position":352.0,"HyperDash":false}]},{"StartTime":64278.0,"Objects":[{"StartTime":64278.0,"Position":256.0,"HyperDash":false}]},{"StartTime":64450.0,"Objects":[{"StartTime":64450.0,"Position":160.0,"HyperDash":false}]},{"StartTime":64621.0,"Objects":[{"StartTime":64621.0,"Position":72.0,"HyperDash":false}]},{"StartTime":64793.0,"Objects":[{"StartTime":64793.0,"Position":8.0,"HyperDash":false}]},{"StartTime":64964.0,"Objects":[{"StartTime":64964.0,"Position":8.0,"HyperDash":false}]},{"StartTime":65135.0,"Objects":[{"StartTime":65135.0,"Position":56.0,"HyperDash":false}]},{"StartTime":65221.0,"Objects":[{"StartTime":65221.0,"Position":437.0,"HyperDash":false},{"StartTime":65317.0,"Position":289.0,"HyperDash":false},{"StartTime":65414.0,"Position":464.0,"HyperDash":false},{"StartTime":65510.0,"Position":36.0,"HyperDash":false},{"StartTime":65607.0,"Position":378.0,"HyperDash":false},{"StartTime":65703.0,"Position":297.0,"HyperDash":false},{"StartTime":65800.0,"Position":418.0,"HyperDash":false},{"StartTime":65896.0,"Position":329.0,"HyperDash":false},{"StartTime":65993.0,"Position":338.0,"HyperDash":false}]},{"StartTime":66164.0,"Objects":[{"StartTime":66164.0,"Position":296.0,"HyperDash":false},{"StartTime":66240.0,"Position":317.930573,"HyperDash":false},{"StartTime":66317.0,"Position":317.018127,"HyperDash":false},{"StartTime":66393.0,"Position":314.9487,"HyperDash":false},{"StartTime":66506.0,"Position":349.687561,"HyperDash":false}]},{"StartTime":66850.0,"Objects":[{"StartTime":66850.0,"Position":216.0,"HyperDash":false},{"StartTime":66926.0,"Position":184.069427,"HyperDash":false},{"StartTime":67003.0,"Position":174.981888,"HyperDash":false},{"StartTime":67079.0,"Position":196.051315,"HyperDash":false},{"StartTime":67192.0,"Position":162.312454,"HyperDash":false}]},{"StartTime":67535.0,"Objects":[{"StartTime":67535.0,"Position":296.0,"HyperDash":false},{"StartTime":67611.0,"Position":288.930573,"HyperDash":false},{"StartTime":67688.0,"Position":339.018127,"HyperDash":false},{"StartTime":67764.0,"Position":312.9487,"HyperDash":false},{"StartTime":67877.0,"Position":349.687561,"HyperDash":false}]},{"StartTime":67964.0,"Objects":[{"StartTime":67964.0,"Position":267.0,"HyperDash":false},{"StartTime":68060.0,"Position":477.0,"HyperDash":false},{"StartTime":68156.0,"Position":282.0,"HyperDash":false},{"StartTime":68253.0,"Position":216.0,"HyperDash":false},{"StartTime":68349.0,"Position":106.0,"HyperDash":false},{"StartTime":68445.0,"Position":353.0,"HyperDash":false},{"StartTime":68542.0,"Position":162.0,"HyperDash":false},{"StartTime":68638.0,"Position":473.0,"HyperDash":false},{"StartTime":68735.0,"Position":260.0,"HyperDash":false}]},{"StartTime":68907.0,"Objects":[{"StartTime":68907.0,"Position":304.0,"HyperDash":false},{"StartTime":68983.0,"Position":334.222229,"HyperDash":false},{"StartTime":69060.0,"Position":328.736847,"HyperDash":false},{"StartTime":69136.0,"Position":355.959076,"HyperDash":false},{"StartTime":69249.0,"Position":404.0,"HyperDash":false}]},{"StartTime":69593.0,"Objects":[{"StartTime":69593.0,"Position":208.0,"HyperDash":false},{"StartTime":69669.0,"Position":188.777771,"HyperDash":false},{"StartTime":69746.0,"Position":175.263153,"HyperDash":false},{"StartTime":69822.0,"Position":151.040924,"HyperDash":false},{"StartTime":69935.0,"Position":108.0,"HyperDash":false}]},{"StartTime":70278.0,"Objects":[{"StartTime":70278.0,"Position":304.0,"HyperDash":false},{"StartTime":70354.0,"Position":332.222229,"HyperDash":false},{"StartTime":70431.0,"Position":343.736847,"HyperDash":false},{"StartTime":70507.0,"Position":361.959076,"HyperDash":false},{"StartTime":70620.0,"Position":404.0,"HyperDash":false}]},{"StartTime":71307.0,"Objects":[{"StartTime":71307.0,"Position":56.0,"HyperDash":false},{"StartTime":71392.0,"Position":57.8317223,"HyperDash":false},{"StartTime":71478.0,"Position":43.0449677,"HyperDash":false},{"StartTime":71563.0,"Position":62.4877777,"HyperDash":false},{"StartTime":71649.0,"Position":54.65224,"HyperDash":false},{"StartTime":71725.0,"Position":50.5213776,"HyperDash":false},{"StartTime":71802.0,"Position":33.41652,"HyperDash":false},{"StartTime":71879.0,"Position":58.5728569,"HyperDash":false},{"StartTime":71992.0,"Position":56.0,"HyperDash":false}]},{"StartTime":72335.0,"Objects":[{"StartTime":72335.0,"Position":256.0,"HyperDash":false}]},{"StartTime":72507.0,"Objects":[{"StartTime":72507.0,"Position":328.0,"HyperDash":false}]},{"StartTime":72678.0,"Objects":[{"StartTime":72678.0,"Position":400.0,"HyperDash":false}]},{"StartTime":73021.0,"Objects":[{"StartTime":73021.0,"Position":400.0,"HyperDash":false},{"StartTime":73097.0,"Position":415.600647,"HyperDash":false},{"StartTime":73174.0,"Position":409.291138,"HyperDash":false},{"StartTime":73250.0,"Position":437.285278,"HyperDash":false},{"StartTime":73363.0,"Position":410.36676,"HyperDash":false}]},{"StartTime":73707.0,"Objects":[{"StartTime":73707.0,"Position":112.0,"HyperDash":false},{"StartTime":73783.0,"Position":117.399353,"HyperDash":false},{"StartTime":73860.0,"Position":79.7088547,"HyperDash":false},{"StartTime":73936.0,"Position":102.714714,"HyperDash":false},{"StartTime":74049.0,"Position":101.633247,"HyperDash":false}]},{"StartTime":74393.0,"Objects":[{"StartTime":74393.0,"Position":304.0,"HyperDash":false},{"StartTime":74469.0,"Position":301.197144,"HyperDash":false},{"StartTime":74546.0,"Position":342.5416,"HyperDash":false},{"StartTime":74622.0,"Position":349.738739,"HyperDash":false},{"StartTime":74735.0,"Position":354.387115,"HyperDash":false}]},{"StartTime":75078.0,"Objects":[{"StartTime":75078.0,"Position":304.0,"HyperDash":false},{"StartTime":75154.0,"Position":303.197144,"HyperDash":false},{"StartTime":75231.0,"Position":337.5416,"HyperDash":false},{"StartTime":75307.0,"Position":353.738739,"HyperDash":false},{"StartTime":75420.0,"Position":354.387115,"HyperDash":false}]},{"StartTime":75764.0,"Objects":[{"StartTime":75764.0,"Position":464.0,"HyperDash":false}]},{"StartTime":75935.0,"Objects":[{"StartTime":75935.0,"Position":384.0,"HyperDash":false}]},{"StartTime":76107.0,"Objects":[{"StartTime":76107.0,"Position":304.0,"HyperDash":false}]},{"StartTime":76278.0,"Objects":[{"StartTime":76278.0,"Position":232.0,"HyperDash":false}]},{"StartTime":76450.0,"Objects":[{"StartTime":76450.0,"Position":160.0,"HyperDash":false},{"StartTime":76535.0,"Position":135.0,"HyperDash":false},{"StartTime":76621.0,"Position":160.0,"HyperDash":false}]},{"StartTime":76793.0,"Objects":[{"StartTime":76793.0,"Position":80.0,"HyperDash":false}]},{"StartTime":77135.0,"Objects":[{"StartTime":77135.0,"Position":120.0,"HyperDash":false},{"StartTime":77211.0,"Position":119.7057,"HyperDash":false},{"StartTime":77288.0,"Position":91.15754,"HyperDash":false},{"StartTime":77364.0,"Position":60.8632431,"HyperDash":false},{"StartTime":77477.0,"Position":33.1756821,"HyperDash":false}]},{"StartTime":77821.0,"Objects":[{"StartTime":77821.0,"Position":232.0,"HyperDash":false},{"StartTime":77897.0,"Position":234.148621,"HyperDash":false},{"StartTime":77974.0,"Position":272.5492,"HyperDash":false},{"StartTime":78050.0,"Position":284.6978,"HyperDash":false},{"StartTime":78163.0,"Position":318.1688,"HyperDash":false}]},{"StartTime":78507.0,"Objects":[{"StartTime":78507.0,"Position":176.0,"HyperDash":false},{"StartTime":78583.0,"Position":142.7057,"HyperDash":false},{"StartTime":78660.0,"Position":129.157532,"HyperDash":false},{"StartTime":78736.0,"Position":124.863243,"HyperDash":false},{"StartTime":78849.0,"Position":89.17568,"HyperDash":false}]},{"StartTime":79193.0,"Objects":[{"StartTime":79193.0,"Position":288.0,"HyperDash":false},{"StartTime":79269.0,"Position":321.241455,"HyperDash":false},{"StartTime":79346.0,"Position":319.736084,"HyperDash":false},{"StartTime":79422.0,"Position":360.9775,"HyperDash":false},{"StartTime":79535.0,"Position":374.586517,"HyperDash":false}]},{"StartTime":79878.0,"Objects":[{"StartTime":79878.0,"Position":240.0,"HyperDash":false},{"StartTime":79954.0,"Position":209.7057,"HyperDash":false},{"StartTime":80031.0,"Position":189.157532,"HyperDash":false},{"StartTime":80107.0,"Position":196.863251,"HyperDash":false},{"StartTime":80220.0,"Position":153.17569,"HyperDash":false}]},{"StartTime":80564.0,"Objects":[{"StartTime":80564.0,"Position":32.0,"HyperDash":false}]},{"StartTime":80735.0,"Objects":[{"StartTime":80735.0,"Position":66.0,"HyperDash":false}]},{"StartTime":80907.0,"Objects":[{"StartTime":80907.0,"Position":161.0,"HyperDash":false}]},{"StartTime":81078.0,"Objects":[{"StartTime":81078.0,"Position":190.0,"HyperDash":false}]},{"StartTime":81250.0,"Objects":[{"StartTime":81250.0,"Position":285.0,"HyperDash":false}]},{"StartTime":81593.0,"Objects":[{"StartTime":81593.0,"Position":384.0,"HyperDash":false},{"StartTime":81652.0,"Position":401.608948,"HyperDash":false},{"StartTime":81712.0,"Position":403.3638,"HyperDash":false},{"StartTime":81772.0,"Position":419.118683,"HyperDash":false},{"StartTime":81832.0,"Position":416.873535,"HyperDash":false},{"StartTime":81891.0,"Position":412.482483,"HyperDash":false},{"StartTime":81951.0,"Position":417.237366,"HyperDash":false},{"StartTime":82011.0,"Position":431.992218,"HyperDash":false},{"StartTime":82107.0,"Position":459.0,"HyperDash":false}]},{"StartTime":82278.0,"Objects":[{"StartTime":82278.0,"Position":440.0,"HyperDash":false},{"StartTime":82345.0,"Position":418.133057,"HyperDash":false},{"StartTime":82449.0,"Position":412.264984,"HyperDash":false}]},{"StartTime":82621.0,"Objects":[{"StartTime":82621.0,"Position":320.0,"HyperDash":false},{"StartTime":82688.0,"Position":303.133057,"HyperDash":false},{"StartTime":82792.0,"Position":292.264984,"HyperDash":false}]},{"StartTime":82964.0,"Objects":[{"StartTime":82964.0,"Position":200.0,"HyperDash":false},{"StartTime":83031.0,"Position":187.133057,"HyperDash":false},{"StartTime":83135.0,"Position":172.264984,"HyperDash":false}]},{"StartTime":83307.0,"Objects":[{"StartTime":83307.0,"Position":248.0,"HyperDash":false}]},{"StartTime":83478.0,"Objects":[{"StartTime":83478.0,"Position":344.0,"HyperDash":false}]},{"StartTime":83650.0,"Objects":[{"StartTime":83650.0,"Position":448.0,"HyperDash":false}]},{"StartTime":83993.0,"Objects":[{"StartTime":83993.0,"Position":400.0,"HyperDash":false},{"StartTime":84060.0,"Position":372.409363,"HyperDash":false},{"StartTime":84164.0,"Position":350.0,"HyperDash":false}]},{"StartTime":84335.0,"Objects":[{"StartTime":84335.0,"Position":400.0,"HyperDash":false},{"StartTime":84402.0,"Position":438.590637,"HyperDash":false},{"StartTime":84506.0,"Position":450.0,"HyperDash":false}]},{"StartTime":84678.0,"Objects":[{"StartTime":84678.0,"Position":408.0,"HyperDash":false}]},{"StartTime":84850.0,"Objects":[{"StartTime":84850.0,"Position":304.0,"HyperDash":false}]},{"StartTime":85021.0,"Objects":[{"StartTime":85021.0,"Position":208.0,"HyperDash":false}]},{"StartTime":85364.0,"Objects":[{"StartTime":85364.0,"Position":208.0,"HyperDash":false},{"StartTime":85440.0,"Position":179.777771,"HyperDash":false},{"StartTime":85517.0,"Position":160.263153,"HyperDash":false},{"StartTime":85593.0,"Position":156.040924,"HyperDash":false},{"StartTime":85706.0,"Position":108.0,"HyperDash":false}]},{"StartTime":86050.0,"Objects":[{"StartTime":86050.0,"Position":304.0,"HyperDash":false},{"StartTime":86126.0,"Position":332.222229,"HyperDash":false},{"StartTime":86203.0,"Position":364.736847,"HyperDash":false},{"StartTime":86279.0,"Position":385.959076,"HyperDash":false},{"StartTime":86392.0,"Position":404.0,"HyperDash":false}]},{"StartTime":86735.0,"Objects":[{"StartTime":86735.0,"Position":480.0,"HyperDash":false}]},{"StartTime":86907.0,"Objects":[{"StartTime":86907.0,"Position":376.0,"HyperDash":false}]},{"StartTime":87078.0,"Objects":[{"StartTime":87078.0,"Position":272.0,"HyperDash":false}]},{"StartTime":87250.0,"Objects":[{"StartTime":87250.0,"Position":168.0,"HyperDash":false}]},{"StartTime":87421.0,"Objects":[{"StartTime":87421.0,"Position":64.0,"HyperDash":false},{"StartTime":87506.0,"Position":39.0,"HyperDash":false},{"StartTime":87592.0,"Position":64.0,"HyperDash":false}]},{"StartTime":87764.0,"Objects":[{"StartTime":87764.0,"Position":64.0,"HyperDash":false}]},{"StartTime":88107.0,"Objects":[{"StartTime":88107.0,"Position":208.0,"HyperDash":false},{"StartTime":88183.0,"Position":190.802856,"HyperDash":false},{"StartTime":88260.0,"Position":183.4584,"HyperDash":false},{"StartTime":88336.0,"Position":187.261261,"HyperDash":false},{"StartTime":88449.0,"Position":157.612885,"HyperDash":false}]},{"StartTime":88793.0,"Objects":[{"StartTime":88793.0,"Position":304.0,"HyperDash":false},{"StartTime":88869.0,"Position":300.197144,"HyperDash":false},{"StartTime":88946.0,"Position":313.5416,"HyperDash":false},{"StartTime":89022.0,"Position":334.738739,"HyperDash":false},{"StartTime":89135.0,"Position":354.387115,"HyperDash":false}]},{"StartTime":89478.0,"Objects":[{"StartTime":89478.0,"Position":208.0,"HyperDash":false},{"StartTime":89554.0,"Position":197.802872,"HyperDash":false},{"StartTime":89631.0,"Position":182.4584,"HyperDash":false},{"StartTime":89707.0,"Position":169.261261,"HyperDash":false},{"StartTime":89820.0,"Position":157.612885,"HyperDash":false}]},{"StartTime":90164.0,"Objects":[{"StartTime":90164.0,"Position":304.0,"HyperDash":false},{"StartTime":90249.0,"Position":316.8624,"HyperDash":false},{"StartTime":90335.0,"Position":304.0,"HyperDash":false}]},{"StartTime":90507.0,"Objects":[{"StartTime":90507.0,"Position":208.0,"HyperDash":false}]},{"StartTime":90850.0,"Objects":[{"StartTime":90850.0,"Position":56.0,"HyperDash":false}]},{"StartTime":91021.0,"Objects":[{"StartTime":91021.0,"Position":56.0,"HyperDash":false}]},{"StartTime":91193.0,"Objects":[{"StartTime":91193.0,"Position":144.0,"HyperDash":false}]},{"StartTime":91536.0,"Objects":[{"StartTime":91536.0,"Position":344.0,"HyperDash":false}]},{"StartTime":91707.0,"Objects":[{"StartTime":91707.0,"Position":424.0,"HyperDash":false}]},{"StartTime":91878.0,"Objects":[{"StartTime":91878.0,"Position":424.0,"HyperDash":false}]},{"StartTime":92050.0,"Objects":[{"StartTime":92050.0,"Position":344.0,"HyperDash":false}]},{"StartTime":92221.0,"Objects":[{"StartTime":92221.0,"Position":256.0,"HyperDash":false}]},{"StartTime":92564.0,"Objects":[{"StartTime":92564.0,"Position":160.0,"HyperDash":false},{"StartTime":92649.0,"Position":142.69455,"HyperDash":false},{"StartTime":92735.0,"Position":131.871,"HyperDash":false},{"StartTime":92820.0,"Position":111.443245,"HyperDash":false},{"StartTime":92906.0,"Position":100.496979,"HyperDash":false},{"StartTime":92974.0,"Position":108.492638,"HyperDash":false},{"StartTime":93078.0,"Position":101.630577,"HyperDash":true}]},{"StartTime":93250.0,"Objects":[{"StartTime":93250.0,"Position":352.0,"HyperDash":false},{"StartTime":93335.0,"Position":389.30545,"HyperDash":false},{"StartTime":93421.0,"Position":398.129,"HyperDash":false},{"StartTime":93506.0,"Position":421.556763,"HyperDash":false},{"StartTime":93592.0,"Position":411.503021,"HyperDash":false},{"StartTime":93660.0,"Position":408.507355,"HyperDash":false},{"StartTime":93764.0,"Position":410.369415,"HyperDash":false}]},{"StartTime":93936.0,"Objects":[{"StartTime":93936.0,"Position":256.0,"HyperDash":false}]},{"StartTime":94021.0,"Objects":[{"StartTime":94021.0,"Position":256.0,"HyperDash":false}]},{"StartTime":94107.0,"Objects":[{"StartTime":94107.0,"Position":256.0,"HyperDash":false}]},{"StartTime":94193.0,"Objects":[{"StartTime":94193.0,"Position":256.0,"HyperDash":false}]},{"StartTime":94278.0,"Objects":[{"StartTime":94278.0,"Position":256.0,"HyperDash":false}]},{"StartTime":94364.0,"Objects":[{"StartTime":94364.0,"Position":256.0,"HyperDash":false}]},{"StartTime":94450.0,"Objects":[{"StartTime":94450.0,"Position":256.0,"HyperDash":false}]},{"StartTime":94536.0,"Objects":[{"StartTime":94536.0,"Position":256.0,"HyperDash":false}]},{"StartTime":94621.0,"Objects":[{"StartTime":94621.0,"Position":256.0,"HyperDash":false},{"StartTime":94706.0,"Position":317.7076,"HyperDash":false},{"StartTime":94792.0,"Position":356.0,"HyperDash":false},{"StartTime":94859.0,"Position":307.8187,"HyperDash":false},{"StartTime":94963.0,"Position":256.0,"HyperDash":false}]},{"StartTime":95136.0,"Objects":[{"StartTime":95136.0,"Position":448.0,"HyperDash":false},{"StartTime":95203.0,"Position":427.818726,"HyperDash":false},{"StartTime":95307.0,"Position":348.0,"HyperDash":false}]},{"StartTime":95650.0,"Objects":[{"StartTime":95650.0,"Position":40.0,"HyperDash":false},{"StartTime":95735.0,"Position":86.81512,"HyperDash":false},{"StartTime":95821.0,"Position":118.086884,"HyperDash":false},{"StartTime":95888.0,"Position":133.0,"HyperDash":false},{"StartTime":95992.0,"Position":120.0,"HyperDash":false}]},{"StartTime":96336.0,"Objects":[{"StartTime":96336.0,"Position":480.0,"HyperDash":false},{"StartTime":96403.0,"Position":422.173248,"HyperDash":false},{"StartTime":96507.0,"Position":383.4571,"HyperDash":false}]},{"StartTime":96678.0,"Objects":[{"StartTime":96678.0,"Position":176.0,"HyperDash":false},{"StartTime":96745.0,"Position":193.826752,"HyperDash":false},{"StartTime":96849.0,"Position":272.5429,"HyperDash":false}]},{"StartTime":97021.0,"Objects":[{"StartTime":97021.0,"Position":440.0,"HyperDash":false},{"StartTime":97106.0,"Position":383.010834,"HyperDash":false},{"StartTime":97192.0,"Position":343.4571,"HyperDash":false},{"StartTime":97259.0,"Position":370.283844,"HyperDash":false},{"StartTime":97363.0,"Position":440.0,"HyperDash":false}]},{"StartTime":97707.0,"Objects":[{"StartTime":97707.0,"Position":40.0,"HyperDash":false},{"StartTime":97792.0,"Position":41.5126953,"HyperDash":false},{"StartTime":97878.0,"Position":39.0196533,"HyperDash":false},{"StartTime":97945.0,"Position":73.99493,"HyperDash":false},{"StartTime":98049.0,"Position":115.429176,"HyperDash":false}]},{"StartTime":98393.0,"Objects":[{"StartTime":98393.0,"Position":440.0,"HyperDash":false},{"StartTime":98478.0,"Position":400.2924,"HyperDash":false},{"StartTime":98564.0,"Position":340.0,"HyperDash":false},{"StartTime":98631.0,"Position":332.355255,"HyperDash":false},{"StartTime":98735.0,"Position":276.290741,"HyperDash":false}]},{"StartTime":99078.0,"Objects":[{"StartTime":99078.0,"Position":32.0,"HyperDash":false},{"StartTime":99145.0,"Position":65.70535,"HyperDash":false},{"StartTime":99249.0,"Position":102.710678,"HyperDash":false}]},{"StartTime":99421.0,"Objects":[{"StartTime":99421.0,"Position":296.0,"HyperDash":false},{"StartTime":99488.0,"Position":284.294647,"HyperDash":false},{"StartTime":99592.0,"Position":225.289322,"HyperDash":false}]},{"StartTime":99764.0,"Objects":[{"StartTime":99764.0,"Position":408.0,"HyperDash":false},{"StartTime":99849.0,"Position":457.1486,"HyperDash":false},{"StartTime":99935.0,"Position":478.7107,"HyperDash":false},{"StartTime":100002.0,"Position":469.0053,"HyperDash":false},{"StartTime":100106.0,"Position":408.0,"HyperDash":false}]},{"StartTime":100450.0,"Objects":[{"StartTime":100450.0,"Position":32.0,"HyperDash":false},{"StartTime":100535.0,"Position":80.7076,"HyperDash":false},{"StartTime":100621.0,"Position":132.0,"HyperDash":false},{"StartTime":100688.0,"Position":176.18129,"HyperDash":false},{"StartTime":100792.0,"Position":232.0,"HyperDash":false}]},{"StartTime":101136.0,"Objects":[{"StartTime":101136.0,"Position":480.0,"HyperDash":false},{"StartTime":101221.0,"Position":426.2924,"HyperDash":false},{"StartTime":101307.0,"Position":380.0,"HyperDash":false},{"StartTime":101374.0,"Position":346.818726,"HyperDash":false},{"StartTime":101478.0,"Position":280.0,"HyperDash":false}]},{"StartTime":101821.0,"Objects":[{"StartTime":101821.0,"Position":256.0,"HyperDash":false},{"StartTime":101906.0,"Position":250.0,"HyperDash":false},{"StartTime":101992.0,"Position":256.0,"HyperDash":false},{"StartTime":102059.0,"Position":256.0,"HyperDash":false},{"StartTime":102163.0,"Position":256.0,"HyperDash":false}]},{"StartTime":102336.0,"Objects":[{"StartTime":102336.0,"Position":256.0,"HyperDash":false}]},{"StartTime":102507.0,"Objects":[{"StartTime":102507.0,"Position":256.0,"HyperDash":false},{"StartTime":102592.0,"Position":261.0,"HyperDash":false},{"StartTime":102678.0,"Position":256.0,"HyperDash":false},{"StartTime":102745.0,"Position":274.0,"HyperDash":false},{"StartTime":102849.0,"Position":256.0,"HyperDash":false}]},{"StartTime":103193.0,"Objects":[{"StartTime":103193.0,"Position":432.0,"HyperDash":false}]},{"StartTime":103364.0,"Objects":[{"StartTime":103364.0,"Position":256.0,"HyperDash":false}]},{"StartTime":103536.0,"Objects":[{"StartTime":103536.0,"Position":80.0,"HyperDash":true}]},{"StartTime":103878.0,"Objects":[{"StartTime":103878.0,"Position":480.0,"HyperDash":false}]},{"StartTime":104050.0,"Objects":[{"StartTime":104050.0,"Position":408.0,"HyperDash":false}]},{"StartTime":104221.0,"Objects":[{"StartTime":104221.0,"Position":336.0,"HyperDash":false}]},{"StartTime":104393.0,"Objects":[{"StartTime":104393.0,"Position":264.0,"HyperDash":false}]},{"StartTime":104564.0,"Objects":[{"StartTime":104564.0,"Position":184.0,"HyperDash":false}]},{"StartTime":104736.0,"Objects":[{"StartTime":104736.0,"Position":104.0,"HyperDash":false}]},{"StartTime":104907.0,"Objects":[{"StartTime":104907.0,"Position":32.0,"HyperDash":false}]},{"StartTime":105593.0,"Objects":[{"StartTime":105593.0,"Position":376.0,"HyperDash":false},{"StartTime":105678.0,"Position":422.963257,"HyperDash":false}]},{"StartTime":105764.0,"Objects":[{"StartTime":105764.0,"Position":411.0,"HyperDash":false},{"StartTime":105849.0,"Position":461.0,"HyperDash":false}]},{"StartTime":105936.0,"Objects":[{"StartTime":105936.0,"Position":438.0,"HyperDash":false},{"StartTime":106021.0,"Position":486.1759,"HyperDash":false}]},{"StartTime":106107.0,"Objects":[{"StartTime":106107.0,"Position":447.0,"HyperDash":false},{"StartTime":106192.0,"Position":492.579346,"HyperDash":false}]},{"StartTime":106278.0,"Objects":[{"StartTime":106278.0,"Position":492.0,"HyperDash":false}]},{"StartTime":106621.0,"Objects":[{"StartTime":106621.0,"Position":120.0,"HyperDash":false},{"StartTime":106706.0,"Position":80.04692,"HyperDash":false},{"StartTime":106792.0,"Position":49.0288429,"HyperDash":false},{"StartTime":106859.0,"Position":45.9070129,"HyperDash":false},{"StartTime":106963.0,"Position":40.4979248,"HyperDash":false}]},{"StartTime":107307.0,"Objects":[{"StartTime":107307.0,"Position":400.0,"HyperDash":false},{"StartTime":107392.0,"Position":359.373077,"HyperDash":false}]},{"StartTime":107478.0,"Objects":[{"StartTime":107478.0,"Position":422.0,"HyperDash":false},{"StartTime":107563.0,"Position":374.933044,"HyperDash":false}]},{"StartTime":107650.0,"Objects":[{"StartTime":107650.0,"Position":436.0,"HyperDash":false},{"StartTime":107735.0,"Position":386.191254,"HyperDash":false}]},{"StartTime":107821.0,"Objects":[{"StartTime":107821.0,"Position":430.0,"HyperDash":false},{"StartTime":107906.0,"Position":380.633484,"HyperDash":false}]},{"StartTime":107993.0,"Objects":[{"StartTime":107993.0,"Position":410.0,"HyperDash":false},{"StartTime":108078.0,"Position":364.420654,"HyperDash":false}]},{"StartTime":108164.0,"Objects":[{"StartTime":108164.0,"Position":377.0,"HyperDash":false},{"StartTime":108249.0,"Position":339.099457,"HyperDash":false}]},{"StartTime":108336.0,"Objects":[{"StartTime":108336.0,"Position":343.0,"HyperDash":false}]},{"StartTime":108678.0,"Objects":[{"StartTime":108678.0,"Position":48.0,"HyperDash":false},{"StartTime":108763.0,"Position":72.7952957,"HyperDash":false},{"StartTime":108849.0,"Position":118.469154,"HyperDash":false},{"StartTime":108916.0,"Position":133.274063,"HyperDash":false},{"StartTime":109020.0,"Position":126.387558,"HyperDash":false}]},{"StartTime":109364.0,"Objects":[{"StartTime":109364.0,"Position":464.0,"HyperDash":false},{"StartTime":109449.0,"Position":407.88382,"HyperDash":false},{"StartTime":109535.0,"Position":392.819672,"HyperDash":false},{"StartTime":109602.0,"Position":400.819,"HyperDash":false},{"StartTime":109706.0,"Position":384.6482,"HyperDash":false}]},{"StartTime":110050.0,"Objects":[{"StartTime":110050.0,"Position":32.0,"HyperDash":false},{"StartTime":110135.0,"Position":80.1758957,"HyperDash":false}]},{"StartTime":110221.0,"Objects":[{"StartTime":110221.0,"Position":16.0,"HyperDash":false},{"StartTime":110306.0,"Position":59.1889458,"HyperDash":false}]},{"StartTime":110393.0,"Objects":[{"StartTime":110393.0,"Position":27.0,"HyperDash":false},{"StartTime":110478.0,"Position":62.3553429,"HyperDash":false}]},{"StartTime":110564.0,"Objects":[{"StartTime":110564.0,"Position":42.0,"HyperDash":false},{"StartTime":110649.0,"Position":66.13023,"HyperDash":false}]},{"StartTime":110736.0,"Objects":[{"StartTime":110736.0,"Position":76.0,"HyperDash":false},{"StartTime":110821.0,"Position":88.54811,"HyperDash":false}]},{"StartTime":110907.0,"Objects":[{"StartTime":110907.0,"Position":134.0,"HyperDash":false},{"StartTime":110992.0,"Position":133.107285,"HyperDash":false}]},{"StartTime":111078.0,"Objects":[{"StartTime":111078.0,"Position":134.0,"HyperDash":false}]},{"StartTime":111421.0,"Objects":[{"StartTime":111421.0,"Position":456.0,"HyperDash":false},{"StartTime":111506.0,"Position":404.2924,"HyperDash":false},{"StartTime":111592.0,"Position":356.0,"HyperDash":false},{"StartTime":111659.0,"Position":397.1813,"HyperDash":false},{"StartTime":111763.0,"Position":456.0,"HyperDash":false}]},{"StartTime":112107.0,"Objects":[{"StartTime":112107.0,"Position":56.0,"HyperDash":false},{"StartTime":112192.0,"Position":97.7076,"HyperDash":false},{"StartTime":112278.0,"Position":156.0,"HyperDash":false},{"StartTime":112345.0,"Position":112.81871,"HyperDash":false},{"StartTime":112449.0,"Position":56.0,"HyperDash":false}]},{"StartTime":112793.0,"Objects":[{"StartTime":112793.0,"Position":16.0,"HyperDash":false}]},{"StartTime":112964.0,"Objects":[{"StartTime":112964.0,"Position":96.0,"HyperDash":false}]},{"StartTime":113136.0,"Objects":[{"StartTime":113136.0,"Position":176.0,"HyperDash":false}]},{"StartTime":113307.0,"Objects":[{"StartTime":113307.0,"Position":256.0,"HyperDash":false}]},{"StartTime":113478.0,"Objects":[{"StartTime":113478.0,"Position":336.0,"HyperDash":false}]},{"StartTime":113650.0,"Objects":[{"StartTime":113650.0,"Position":416.0,"HyperDash":false}]},{"StartTime":113821.0,"Objects":[{"StartTime":113821.0,"Position":496.0,"HyperDash":false}]},{"StartTime":114164.0,"Objects":[{"StartTime":114164.0,"Position":312.0,"HyperDash":false}]},{"StartTime":114336.0,"Objects":[{"StartTime":114336.0,"Position":256.0,"HyperDash":false}]},{"StartTime":114507.0,"Objects":[{"StartTime":114507.0,"Position":192.0,"HyperDash":false}]},{"StartTime":114850.0,"Objects":[{"StartTime":114850.0,"Position":256.0,"HyperDash":false}]},{"StartTime":115021.0,"Objects":[{"StartTime":115021.0,"Position":344.0,"HyperDash":false}]},{"StartTime":115193.0,"Objects":[{"StartTime":115193.0,"Position":312.0,"HyperDash":false}]},{"StartTime":115364.0,"Objects":[{"StartTime":115364.0,"Position":208.0,"HyperDash":false}]},{"StartTime":115536.0,"Objects":[{"StartTime":115536.0,"Position":176.0,"HyperDash":false}]},{"StartTime":115707.0,"Objects":[{"StartTime":115707.0,"Position":256.0,"HyperDash":false}]},{"StartTime":115878.0,"Objects":[{"StartTime":115878.0,"Position":256.0,"HyperDash":false}]},{"StartTime":116564.0,"Objects":[{"StartTime":116564.0,"Position":120.0,"HyperDash":false},{"StartTime":116640.0,"Position":111.647354,"HyperDash":false},{"StartTime":116717.0,"Position":96.06639,"HyperDash":false},{"StartTime":116793.0,"Position":53.7137451,"HyperDash":false},{"StartTime":116906.0,"Position":41.9131165,"HyperDash":false}]},{"StartTime":117250.0,"Objects":[{"StartTime":117250.0,"Position":368.0,"HyperDash":false},{"StartTime":117326.0,"Position":376.156769,"HyperDash":false},{"StartTime":117403.0,"Position":397.605072,"HyperDash":false},{"StartTime":117479.0,"Position":440.761841,"HyperDash":false},{"StartTime":117592.0,"Position":467.705444,"HyperDash":false}]},{"StartTime":117936.0,"Objects":[{"StartTime":117936.0,"Position":72.0,"HyperDash":false},{"StartTime":118012.0,"Position":82.97272,"HyperDash":false},{"StartTime":118089.0,"Position":63.8529663,"HyperDash":false},{"StartTime":118165.0,"Position":65.82568,"HyperDash":false},{"StartTime":118278.0,"Position":40.377224,"HyperDash":false}]},{"StartTime":118621.0,"Objects":[{"StartTime":118621.0,"Position":368.0,"HyperDash":false},{"StartTime":118706.0,"Position":353.255585,"HyperDash":false},{"StartTime":118792.0,"Position":328.220032,"HyperDash":false},{"StartTime":118877.0,"Position":302.475647,"HyperDash":false},{"StartTime":118963.0,"Position":268.294556,"HyperDash":false},{"StartTime":119039.0,"Position":291.273438,"HyperDash":false},{"StartTime":119116.0,"Position":309.688965,"HyperDash":false},{"StartTime":119193.0,"Position":348.1045,"HyperDash":false},{"StartTime":119306.0,"Position":368.0,"HyperDash":false}]},{"StartTime":119650.0,"Objects":[{"StartTime":119650.0,"Position":392.0,"HyperDash":false}]},{"StartTime":119821.0,"Objects":[{"StartTime":119821.0,"Position":392.0,"HyperDash":false}]},{"StartTime":119993.0,"Objects":[{"StartTime":119993.0,"Position":448.0,"HyperDash":false}]},{"StartTime":120164.0,"Objects":[{"StartTime":120164.0,"Position":448.0,"HyperDash":false}]},{"StartTime":120336.0,"Objects":[{"StartTime":120336.0,"Position":480.0,"HyperDash":false}]},{"StartTime":120507.0,"Objects":[{"StartTime":120507.0,"Position":480.0,"HyperDash":false}]},{"StartTime":120678.0,"Objects":[{"StartTime":120678.0,"Position":480.0,"HyperDash":false}]},{"StartTime":121021.0,"Objects":[{"StartTime":121021.0,"Position":448.0,"HyperDash":false}]},{"StartTime":121107.0,"Objects":[{"StartTime":121107.0,"Position":440.0,"HyperDash":false}]},{"StartTime":121193.0,"Objects":[{"StartTime":121193.0,"Position":432.0,"HyperDash":false}]},{"StartTime":121278.0,"Objects":[{"StartTime":121278.0,"Position":424.0,"HyperDash":false}]},{"StartTime":121364.0,"Objects":[{"StartTime":121364.0,"Position":416.0,"HyperDash":false}]},{"StartTime":121621.0,"Objects":[{"StartTime":121621.0,"Position":312.0,"HyperDash":false}]},{"StartTime":121707.0,"Objects":[{"StartTime":121707.0,"Position":312.0,"HyperDash":false}]},{"StartTime":121878.0,"Objects":[{"StartTime":121878.0,"Position":232.0,"HyperDash":false}]},{"StartTime":122050.0,"Objects":[{"StartTime":122050.0,"Position":168.0,"HyperDash":false}]},{"StartTime":122393.0,"Objects":[{"StartTime":122393.0,"Position":352.0,"HyperDash":false}]},{"StartTime":122564.0,"Objects":[{"StartTime":122564.0,"Position":376.0,"HyperDash":false}]},{"StartTime":122736.0,"Objects":[{"StartTime":122736.0,"Position":352.0,"HyperDash":false}]},{"StartTime":123078.0,"Objects":[{"StartTime":123078.0,"Position":168.0,"HyperDash":false}]},{"StartTime":123250.0,"Objects":[{"StartTime":123250.0,"Position":144.0,"HyperDash":false}]},{"StartTime":123421.0,"Objects":[{"StartTime":123421.0,"Position":168.0,"HyperDash":false}]},{"StartTime":123936.0,"Objects":[{"StartTime":123936.0,"Position":400.0,"HyperDash":false}]},{"StartTime":124107.0,"Objects":[{"StartTime":124107.0,"Position":467.0,"HyperDash":false}]},{"StartTime":124278.0,"Objects":[{"StartTime":124278.0,"Position":400.0,"HyperDash":false}]},{"StartTime":124450.0,"Objects":[{"StartTime":124450.0,"Position":326.0,"HyperDash":false}]},{"StartTime":124536.0,"Objects":[{"StartTime":124536.0,"Position":320.0,"HyperDash":false}]},{"StartTime":124621.0,"Objects":[{"StartTime":124621.0,"Position":315.0,"HyperDash":false}]},{"StartTime":124707.0,"Objects":[{"StartTime":124707.0,"Position":309.0,"HyperDash":false}]},{"StartTime":124793.0,"Objects":[{"StartTime":124793.0,"Position":303.0,"HyperDash":false}]},{"StartTime":125136.0,"Objects":[{"StartTime":125136.0,"Position":112.0,"HyperDash":false}]},{"StartTime":125307.0,"Objects":[{"StartTime":125307.0,"Position":112.0,"HyperDash":false}]},{"StartTime":125478.0,"Objects":[{"StartTime":125478.0,"Position":44.0,"HyperDash":false}]},{"StartTime":125650.0,"Objects":[{"StartTime":125650.0,"Position":44.0,"HyperDash":false}]},{"StartTime":125821.0,"Objects":[{"StartTime":125821.0,"Position":112.0,"HyperDash":false}]},{"StartTime":125993.0,"Objects":[{"StartTime":125993.0,"Position":112.0,"HyperDash":false}]},{"StartTime":126164.0,"Objects":[{"StartTime":126164.0,"Position":184.0,"HyperDash":false}]},{"StartTime":126250.0,"Objects":[{"StartTime":126250.0,"Position":189.0,"HyperDash":false}]},{"StartTime":126336.0,"Objects":[{"StartTime":126336.0,"Position":195.0,"HyperDash":false}]},{"StartTime":126421.0,"Objects":[{"StartTime":126421.0,"Position":200.0,"HyperDash":false}]},{"StartTime":126507.0,"Objects":[{"StartTime":126507.0,"Position":206.0,"HyperDash":false}]},{"StartTime":126593.0,"Objects":[{"StartTime":126593.0,"Position":212.0,"HyperDash":false}]},{"StartTime":126678.0,"Objects":[{"StartTime":126678.0,"Position":217.0,"HyperDash":false}]},{"StartTime":126764.0,"Objects":[{"StartTime":126764.0,"Position":223.0,"HyperDash":false}]},{"StartTime":126850.0,"Objects":[{"StartTime":126850.0,"Position":229.0,"HyperDash":false}]},{"StartTime":127536.0,"Objects":[{"StartTime":127536.0,"Position":72.0,"HyperDash":false}]},{"StartTime":127707.0,"Objects":[{"StartTime":127707.0,"Position":72.0,"HyperDash":false}]},{"StartTime":127878.0,"Objects":[{"StartTime":127878.0,"Position":112.0,"HyperDash":false}]},{"StartTime":128050.0,"Objects":[{"StartTime":128050.0,"Position":112.0,"HyperDash":false}]},{"StartTime":128221.0,"Objects":[{"StartTime":128221.0,"Position":152.0,"HyperDash":false}]},{"StartTime":128393.0,"Objects":[{"StartTime":128393.0,"Position":152.0,"HyperDash":false}]},{"StartTime":128564.0,"Objects":[{"StartTime":128564.0,"Position":192.0,"HyperDash":false}]},{"StartTime":128736.0,"Objects":[{"StartTime":128736.0,"Position":192.0,"HyperDash":false}]},{"StartTime":128907.0,"Objects":[{"StartTime":128907.0,"Position":280.0,"HyperDash":false}]},{"StartTime":129250.0,"Objects":[{"StartTime":129250.0,"Position":296.0,"HyperDash":false}]},{"StartTime":129421.0,"Objects":[{"StartTime":129421.0,"Position":395.0,"HyperDash":false}]},{"StartTime":129593.0,"Objects":[{"StartTime":129593.0,"Position":395.0,"HyperDash":false}]},{"StartTime":129764.0,"Objects":[{"StartTime":129764.0,"Position":295.0,"HyperDash":false}]},{"StartTime":129936.0,"Objects":[{"StartTime":129936.0,"Position":295.0,"HyperDash":false}]},{"StartTime":130107.0,"Objects":[{"StartTime":130107.0,"Position":391.0,"HyperDash":false}]},{"StartTime":130278.0,"Objects":[{"StartTime":130278.0,"Position":391.0,"HyperDash":false}]},{"StartTime":130621.0,"Objects":[{"StartTime":130621.0,"Position":256.0,"HyperDash":false}]},{"StartTime":130793.0,"Objects":[{"StartTime":130793.0,"Position":168.0,"HyperDash":false}]},{"StartTime":130964.0,"Objects":[{"StartTime":130964.0,"Position":256.0,"HyperDash":false}]},{"StartTime":131136.0,"Objects":[{"StartTime":131136.0,"Position":344.0,"HyperDash":false}]},{"StartTime":131307.0,"Objects":[{"StartTime":131307.0,"Position":344.0,"HyperDash":false}]},{"StartTime":131478.0,"Objects":[{"StartTime":131478.0,"Position":344.0,"HyperDash":false}]},{"StartTime":131650.0,"Objects":[{"StartTime":131650.0,"Position":344.0,"HyperDash":false}]},{"StartTime":131993.0,"Objects":[{"StartTime":131993.0,"Position":168.0,"HyperDash":false}]},{"StartTime":132164.0,"Objects":[{"StartTime":132164.0,"Position":168.0,"HyperDash":false}]},{"StartTime":132336.0,"Objects":[{"StartTime":132336.0,"Position":168.0,"HyperDash":false}]},{"StartTime":132593.0,"Objects":[{"StartTime":132593.0,"Position":272.0,"HyperDash":false}]},{"StartTime":132678.0,"Objects":[{"StartTime":132678.0,"Position":272.0,"HyperDash":false}]},{"StartTime":132850.0,"Objects":[{"StartTime":132850.0,"Position":168.0,"HyperDash":false}]},{"StartTime":133021.0,"Objects":[{"StartTime":133021.0,"Position":168.0,"HyperDash":false}]},{"StartTime":133364.0,"Objects":[{"StartTime":133364.0,"Position":40.0,"HyperDash":false},{"StartTime":133440.0,"Position":47.0,"HyperDash":false},{"StartTime":133517.0,"Position":46.0,"HyperDash":false},{"StartTime":133593.0,"Position":45.0,"HyperDash":false},{"StartTime":133706.0,"Position":40.0,"HyperDash":false}]},{"StartTime":134050.0,"Objects":[{"StartTime":134050.0,"Position":208.0,"HyperDash":false},{"StartTime":134126.0,"Position":192.0,"HyperDash":false},{"StartTime":134203.0,"Position":205.0,"HyperDash":false},{"StartTime":134279.0,"Position":215.0,"HyperDash":false},{"StartTime":134392.0,"Position":208.0,"HyperDash":false}]},{"StartTime":134736.0,"Objects":[{"StartTime":134736.0,"Position":208.0,"HyperDash":false}]},{"StartTime":134907.0,"Objects":[{"StartTime":134907.0,"Position":208.0,"HyperDash":false}]},{"StartTime":135078.0,"Objects":[{"StartTime":135078.0,"Position":304.0,"HyperDash":false}]},{"StartTime":135250.0,"Objects":[{"StartTime":135250.0,"Position":304.0,"HyperDash":false}]},{"StartTime":135421.0,"Objects":[{"StartTime":135421.0,"Position":400.0,"HyperDash":false}]},{"StartTime":135593.0,"Objects":[{"StartTime":135593.0,"Position":400.0,"HyperDash":false}]},{"StartTime":135764.0,"Objects":[{"StartTime":135764.0,"Position":496.0,"HyperDash":false}]},{"StartTime":136107.0,"Objects":[{"StartTime":136107.0,"Position":296.0,"HyperDash":false}]},{"StartTime":136278.0,"Objects":[{"StartTime":136278.0,"Position":216.0,"HyperDash":false}]},{"StartTime":136450.0,"Objects":[{"StartTime":136450.0,"Position":296.0,"HyperDash":false}]},{"StartTime":136621.0,"Objects":[{"StartTime":136621.0,"Position":216.0,"HyperDash":false}]},{"StartTime":136793.0,"Objects":[{"StartTime":136793.0,"Position":296.0,"HyperDash":false}]},{"StartTime":136964.0,"Objects":[{"StartTime":136964.0,"Position":292.0,"HyperDash":false}]},{"StartTime":137050.0,"Objects":[{"StartTime":137050.0,"Position":300.0,"HyperDash":false}]},{"StartTime":137136.0,"Objects":[{"StartTime":137136.0,"Position":308.0,"HyperDash":false}]},{"StartTime":137307.0,"Objects":[{"StartTime":137307.0,"Position":220.0,"HyperDash":false}]},{"StartTime":137393.0,"Objects":[{"StartTime":137393.0,"Position":212.0,"HyperDash":false}]},{"StartTime":137478.0,"Objects":[{"StartTime":137478.0,"Position":204.0,"HyperDash":false}]},{"StartTime":137650.0,"Objects":[{"StartTime":137650.0,"Position":260.0,"HyperDash":false}]},{"StartTime":137736.0,"Objects":[{"StartTime":137736.0,"Position":260.0,"HyperDash":false}]},{"StartTime":137821.0,"Objects":[{"StartTime":137821.0,"Position":260.0,"HyperDash":false}]},{"StartTime":137993.0,"Objects":[{"StartTime":137993.0,"Position":441.0,"HyperDash":false},{"StartTime":138057.0,"Position":442.0,"HyperDash":false},{"StartTime":138121.0,"Position":278.0,"HyperDash":false},{"StartTime":138185.0,"Position":90.0,"HyperDash":false},{"StartTime":138250.0,"Position":409.0,"HyperDash":false},{"StartTime":138314.0,"Position":377.0,"HyperDash":false},{"StartTime":138378.0,"Position":457.0,"HyperDash":false},{"StartTime":138442.0,"Position":409.0,"HyperDash":false},{"StartTime":138507.0,"Position":43.0,"HyperDash":false},{"StartTime":138571.0,"Position":162.0,"HyperDash":false},{"StartTime":138635.0,"Position":341.0,"HyperDash":false},{"StartTime":138699.0,"Position":72.0,"HyperDash":false},{"StartTime":138764.0,"Position":135.0,"HyperDash":false},{"StartTime":138828.0,"Position":252.0,"HyperDash":false},{"StartTime":138892.0,"Position":446.0,"HyperDash":false},{"StartTime":138956.0,"Position":284.0,"HyperDash":false},{"StartTime":139021.0,"Position":70.0,"HyperDash":false}]},{"StartTime":139193.0,"Objects":[{"StartTime":139193.0,"Position":256.0,"HyperDash":false}]},{"StartTime":139536.0,"Objects":[{"StartTime":139536.0,"Position":256.0,"HyperDash":false},{"StartTime":139612.0,"Position":285.1111,"HyperDash":false},{"StartTime":139689.0,"Position":274.3684,"HyperDash":false},{"StartTime":139765.0,"Position":287.479523,"HyperDash":false},{"StartTime":139878.0,"Position":306.0,"HyperDash":false}]},{"StartTime":140221.0,"Objects":[{"StartTime":140221.0,"Position":256.0,"HyperDash":false},{"StartTime":140297.0,"Position":261.8889,"HyperDash":false},{"StartTime":140374.0,"Position":249.631577,"HyperDash":false},{"StartTime":140450.0,"Position":226.520462,"HyperDash":false},{"StartTime":140563.0,"Position":206.0,"HyperDash":false}]},{"StartTime":140907.0,"Objects":[{"StartTime":140907.0,"Position":256.0,"HyperDash":false},{"StartTime":140983.0,"Position":284.1111,"HyperDash":false},{"StartTime":141060.0,"Position":295.3684,"HyperDash":false},{"StartTime":141136.0,"Position":290.479523,"HyperDash":false},{"StartTime":141249.0,"Position":306.0,"HyperDash":false}]},{"StartTime":141593.0,"Objects":[{"StartTime":141593.0,"Position":256.0,"HyperDash":false},{"StartTime":141669.0,"Position":257.8889,"HyperDash":false},{"StartTime":141746.0,"Position":242.631577,"HyperDash":false},{"StartTime":141822.0,"Position":221.520462,"HyperDash":false},{"StartTime":141935.0,"Position":206.0,"HyperDash":false}]},{"StartTime":142278.0,"Objects":[{"StartTime":142278.0,"Position":425.0,"HyperDash":false},{"StartTime":142363.0,"Position":281.0,"HyperDash":false},{"StartTime":142449.0,"Position":3.0,"HyperDash":false},{"StartTime":142535.0,"Position":346.0,"HyperDash":false},{"StartTime":142620.0,"Position":350.0,"HyperDash":false},{"StartTime":142706.0,"Position":217.0,"HyperDash":false},{"StartTime":142792.0,"Position":455.0,"HyperDash":false},{"StartTime":142878.0,"Position":229.0,"HyperDash":false},{"StartTime":142963.0,"Position":51.0,"HyperDash":false},{"StartTime":143049.0,"Position":199.0,"HyperDash":false},{"StartTime":143135.0,"Position":208.0,"HyperDash":false},{"StartTime":143220.0,"Position":173.0,"HyperDash":false},{"StartTime":143306.0,"Position":367.0,"HyperDash":false},{"StartTime":143392.0,"Position":193.0,"HyperDash":false},{"StartTime":143478.0,"Position":488.0,"HyperDash":false},{"StartTime":143563.0,"Position":314.0,"HyperDash":false},{"StartTime":143649.0,"Position":135.0,"HyperDash":false},{"StartTime":143735.0,"Position":399.0,"HyperDash":false},{"StartTime":143820.0,"Position":404.0,"HyperDash":false},{"StartTime":143906.0,"Position":152.0,"HyperDash":false},{"StartTime":143992.0,"Position":353.0,"HyperDash":false},{"StartTime":144078.0,"Position":358.0,"HyperDash":false},{"StartTime":144163.0,"Position":447.0,"HyperDash":false},{"StartTime":144249.0,"Position":222.0,"HyperDash":false},{"StartTime":144335.0,"Position":382.0,"HyperDash":false},{"StartTime":144420.0,"Position":433.0,"HyperDash":false},{"StartTime":144506.0,"Position":450.0,"HyperDash":false},{"StartTime":144592.0,"Position":326.0,"HyperDash":false},{"StartTime":144678.0,"Position":414.0,"HyperDash":false},{"StartTime":144763.0,"Position":285.0,"HyperDash":false},{"StartTime":144849.0,"Position":336.0,"HyperDash":false},{"StartTime":144935.0,"Position":509.0,"HyperDash":false},{"StartTime":145021.0,"Position":334.0,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/39206.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/39206.osu new file mode 100644 index 0000000000..3aeb80e9d5 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/39206.osu @@ -0,0 +1,524 @@ +osu file format v7 + +[General] +StackLeniency: 0.7 +Mode: 0 + +[Difficulty] +HPDrainRate:5 +CircleSize:4 +OverallDifficulty:8 +SliderMultiplier:1 +SliderTickRate:1 + +[Events] +//Break Periods +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples +//Background Colour Transformations +3,100,163,162,255 + +[TimingPoints] +336,342.857142857143,4,1,0,100,1,0 +1020,-100,4,2,0,100,0,0 +21250,-100,4,1,0,100,0,0 +23131,-100,4,1,0,100,0,0 +26731,-100,4,1,0,100,0,0 +27931,-50,4,1,0,100,0,0 +28616,-100,4,1,0,100,0,0 +32388,-50,4,1,0,100,0,0 +34102,-100,4,1,0,100,0,0 +37874,-50,4,1,0,100,0,0 +39588,-100,4,1,0,100,0,0 +51759,-100,4,2,0,100,0,0 +52445,-100,4,1,0,100,0,0 +62730,-100,4,2,0,100,0,0 +63416,-100,4,2,0,100,0,0 +66159,-100,4,2,0,100,0,0 +81588,-200,4,2,0,100,0,0 +82278,-100,4,2,0,100,0,0 +85359,-100,4,2,0,100,0,0 +92564,-100,4,1,0,100,0,0 +94616,-50,4,1,0,100,0,0 +116559,-100,4,1,0,100,0,0 +139188,-200,4,2,0,100,0,0 + +[HitObjects] +256,192,678,1,0 +456,216,1021,5,2 +456,264,1193,1,2 +456,312,1364,1,2 +312,168,1707,1,2 +312,120,1878,1,2 +312,72,2050,1,2 +168,216,2393,1,2 +168,264,2564,1,2 +168,312,2736,1,2 +24,168,3078,1,2 +24,120,3250,1,2 +24,72,3421,1,2 +56,272,3764,5,2 +136,336,3936,1,2 +216,272,4107,1,2 +296,88,4450,1,2 +376,24,4621,1,2 +456,88,4793,1,2 +456,288,5135,1,2 +376,352,5307,1,2 +296,288,5478,1,2 +216,104,5821,1,2 +136,40,5993,1,2 +56,104,6164,1,2 +24,304,6507,6,2,B|24:200,1,100 +144,40,7193,2,2,B|144:144,1,100 +256,304,7878,2,2,B|256:200,1,100 +376,40,8564,2,2,B|376:144,1,100 +488,304,9250,2,2,B|488:200,1,100 +256,208,9935,12,0,11050 +40,104,11307,5,2 +56,88,11393,1,2 +80,72,11478,1,2 +104,64,11564,1,2 +128,56,11650,2,2,B|176:40|232:56,1,100 +288,248,12336,2,2,B|240:264|184:248,1,100 +344,120,13021,2,2,B|392:104|448:120,1,100 +504,312,13707,2,2,B|456:328|400:312,1,100 +328,264,14221,5,2 +312,264,14307,1,2 +296,264,14393,2,2,B|256:360,1,100 +160,184,15078,2,2,B|200:280,1,100,2|2 +296,104,15764,2,2,B|256:200,1,100 +160,24,16450,2,2,B|200:120,1,100 +112,160,16964,5,2 +96,176,17050,1,2 +88,200,17136,2,2,B|128:280|200:296,2,150 +424,184,18507,2,2,B|384:104|312:88,2,150 +368,256,19707,1,2 +352,256,19793,1,2 +336,256,19878,2,2,B|232:256,1,100 +136,80,20564,2,2,B|240:80,1,100 +392,208,21250,6,0,B|440:280|392:360,1,150 +120,176,21936,2,0,B|72:104|120:24,1,150 +176,112,22621,6,0,B|269:103|307:15,1,150 +297,35,23307,1,0 +448,296,23821,1,0 +352,328,23993,2,0,B|304:352|248:352,1,100 +160,352,24507,1,0 +160,352,24593,1,0 +160,352,24678,1,0 +88,168,25021,5,0 +176,112,25193,1,0 +256,56,25364,1,0 +424,160,25707,1,0 +448,256,25878,1,0 +472,352,26050,2,0,B|414:287|325:312,1,150,0|0 +304,216,26736,2,0,B|248:232|240:296,1,100 +256,208,27250,12,0,28107 +32,248,28621,5,0 +80,160,28793,1,0 +352,32,29307,1,0 +424,104,29478,1,0 +472,192,29650,1,0 +432,280,29821,1,0 +360,352,29993,1,0 +360,352,30078,1,0 +360,352,30164,1,0 +184,256,30507,6,0,B|208:208,2,50 +64,56,31193,2,0,B|93:100,2,50,0|0|0 +352,40,31878,2,0,B|298:43,2,50,0|0|0 +320,136,32393,6,0,B|290:122,5,25 +342,181,32736,2,0,B|312:167,5,25,0|0|0|0|0|0 +399,173,33078,2,0,B|369:159,5,25 +422,219,33421,2,0,B|392:205,6,25 +368,104,34107,5,0 +280,48,34278,1,0 +280,344,34793,1,0 +184,320,34964,1,0 +112,248,35136,1,0 +64,160,35307,1,0 +32,64,35478,1,0 +32,64,35564,1,0 +32,64,35650,1,0 +232,32,35993,5,0 +328,56,36164,1,0 +408,120,36336,1,0 +464,200,36507,1,0 +408,120,36678,1,0 +328,56,36850,1,0 +232,32,37021,1,0 +72,288,37535,5,0 +112,192,37707,1,0 +144,96,37878,6,0,B|112:96,4,25 +232,144,38221,2,0,B|200:144,4,25,0|0|0|0|0 +320,96,38564,2,0,B|288:96,4,25 +408,144,38907,2,0,B|376:144,4,25 +304,248,39593,5,0 +208,280,39764,1,0 +40,48,40278,1,0 +112,120,40450,1,0 +200,72,40621,1,0 +264,152,40793,1,0 +352,104,40964,1,0 +352,104,41050,1,0 +352,104,41135,1,0 +480,256,41478,5,0 +422,179,41650,1,0 +364,102,41821,1,0 +422,179,41993,1,0 +327,199,42164,1,0 +226,220,42335,1,0 +327,199,42507,1,0 +381,118,42678,1,0 +437,32,42850,1,0 +381,118,43021,1,0 +327,199,43193,1,0 +256,208,43278,12,0,44050 +328,184,44221,6,0,B|408:248,1,100 +184,200,44907,2,0,B|104:136,1,100 +192,88,45421,5,0 +192,88,45507,1,0 +192,88,45593,1,0 +106,135,45764,1,0 +106,135,45850,1,0 +106,135,45935,1,0 +154,219,46107,1,0 +237,170,46278,1,0 +237,170,46364,1,0 +237,170,46450,1,0 +237,170,46535,1,0 +237,170,46621,1,0 +410,70,46964,5,0 +410,70,47135,1,0 +462,160,47307,1,0 +462,160,47478,1,0 +379,209,47650,1,0 +379,209,47821,1,0 +328,119,47993,1,0 +328,119,48164,1,0 +237,170,48335,1,0 +328,119,48507,1,0 +410,71,48678,1,0 +264,88,48935,5,0 +264,88,49021,1,0 +304,184,49193,1,0 +368,256,49364,1,0 +368,256,49707,6,0,B|472:256,1,100,0|0 +280,184,50393,2,0,B|392:184,1,100 +88,248,51250,2,0,B|200:248,1,100 +264,312,51764,1,4 +280,312,51850,1,4 +296,312,51935,1,4 +312,312,52021,1,4 +328,312,52107,1,4 +208,152,52450,5,0 +304,152,52621,1,0 +256,64,52793,1,0 +208,256,53135,1,0 +304,256,53307,1,0 +208,216,53478,1,0 +304,216,53650,1,0 +208,176,53821,1,0 +304,176,53993,1,0 +256,208,54164,12,0,55021 +256,320,55193,6,0,B|184:248,1,100 +256,64,55878,2,0,B|328:136,1,100 +256,192,56393,5,4 +160,192,56564,1,4 +160,192,56650,1,4 +160,192,56735,1,4 +160,88,56907,1,4 +256,88,57078,1,4 +352,88,57250,1,0 +360,88,57335,1,0 +368,88,57421,1,0 +376,88,57507,1,0 +384,88,57593,1,0 +472,264,57935,5,0 +387,318,58107,1,0 +284,325,58278,1,0 +193,291,58450,1,0 +139,207,58621,1,0 +132,103,58793,1,0 +174,12,58964,1,0 +256,200,59307,5,0 +208,288,59478,1,0 +304,288,59650,1,0 +344,200,59821,1,0 +312,160,59907,1,0 +280,120,59993,1,0 +208,56,60164,1,0 +304,56,60335,1,0 +200,224,60678,6,0,B|120:288,1,100 +312,224,61364,2,0,B|392:288,1,100 +390,286,62050,1,0 +121,286,62393,1,0 +256,224,62735,1,4 +256,232,62821,1,4 +256,240,62907,1,4 +256,248,62993,1,4 +256,256,63078,1,4 +432,352,63421,5,2 +496,272,63593,1,2 +496,168,63764,1,2 +440,88,63935,1,2 +352,32,64107,1,2 +256,8,64278,1,2 +160,32,64450,1,2 +72,88,64621,1,2 +8,168,64793,1,2 +8,264,64964,1,2 +56,352,65135,1,2 +256,208,65221,12,4,65993 +296,232,66164,6,2,B|352:320,1,100 +216,160,66850,2,2,B|160:248,1,100 +296,88,67535,2,2,B|352:176,1,100 +256,208,67964,12,4,68735 +304,136,68907,6,2,B|408:136,1,100 +208,192,69593,2,2,B|104:192,1,100 +304,248,70278,2,2,B|408:248,1,100 +56,48,71307,6,0,B|24:88|56:144,2,100 +256,48,72335,1,2 +328,120,72507,1,2 +400,48,72678,1,2 +400,48,73021,2,4,B|440:88|408:144,1,100,4|0 +112,336,73707,2,4,B|72:296|104:240,1,100,4|0 +304,264,74393,6,2,B|360:360,1,100 +304,120,75078,2,2,B|360:24,1,100 +464,200,75764,1,2 +384,264,75935,1,2 +304,200,76107,1,2 +232,264,76278,1,2 +160,200,76450,2,4,B|120:200,2,25 +80,264,76793,1,4 +120,72,77135,6,2,B|29:124,1,100,2|2 +232,96,77821,2,2,B|322:43,1,100 +176,184,78507,2,2,B|85:236,1,100 +288,208,79193,2,2,B|378:156,1,100,2|2 +240,304,79878,2,2,B|149:356,1,100,2|2 +32,192,80564,5,2 +66,95,80735,1,2 +161,131,80907,1,2 +190,38,81078,1,2 +285,73,81250,1,2 +384,72,81593,2,12,B|464:72,1,75,4|4 +440,176,82278,6,0,B|408:224,1,50 +320,176,82621,2,0,B|288:128,1,50 +200,176,82964,2,0,B|168:224,1,50 +248,280,83307,1,2 +344,280,83478,1,2 +448,280,83650,1,2 +400,80,83993,2,0,L|344:80,1,50 +400,80,84335,2,0,L|456:80,1,50 +408,168,84678,1,2 +304,168,84850,1,2 +208,168,85021,1,2 +208,168,85364,6,0,B|104:168,1,100,0|4 +304,216,86050,2,0,B|408:216,1,100,0|4 +480,32,86735,1,2 +376,32,86907,1,2 +272,32,87078,1,2 +168,32,87250,1,2 +64,32,87421,2,2,B|16:32,2,25 +64,32,87764,1,2 +208,168,88107,6,0,B|152:72,1,100,0|2 +304,224,88793,2,0,B|360:128,1,100,0|4 +208,272,89478,2,0,B|152:176,1,100,0|4 +304,328,90164,2,2,B|328:288,2,25 +208,368,90507,1,2 +56,232,90850,5,2 +56,128,91021,1,2 +144,80,91193,1,2 +344,80,91536,5,2 +424,136,91707,1,2 +424,232,91878,1,2 +344,288,92050,1,2 +256,248,92221,1,2 +160,56,92564,6,0,B|80:104|104:192,1,150 +352,328,93250,2,0,B|432:280|408:192,1,150 +256,192,93936,1,0 +256,200,94021,1,0 +256,208,94107,1,0 +256,216,94193,1,0 +256,224,94278,1,0 +256,232,94364,1,0 +256,240,94450,1,0 +256,248,94536,1,4 +256,256,94621,6,0,B|360:256,2,100 +448,328,95136,2,0,B|344:328,1,100 +40,72,95650,2,0,L|120:136|120:240,1,200 +480,64,96336,2,0,B|380:37,1,100 +176,48,96678,2,0,B|276:75,1,100 +440,184,97021,2,0,B|340:157,2,100 +40,176,97707,6,0,L|39:278|120:343,1,200,0|0 +440,112,98393,2,0,L|337:112|272:31,1,200,0|0 +32,344,99078,2,0,B|111:265,1,100,0|0 +296,200,99421,2,0,B|217:279,1,100,0|0 +408,184,99764,2,0,B|487:105,2,100 +32,32,100450,6,0,L|232:32,1,200 +480,352,101136,2,0,L|280:352,1,200 +256,192,101821,2,0,B|256:296,2,100,0|0|0 +256,192,102336,1,0 +256,192,102507,2,0,B|256:88,2,100,0|0|0 +432,344,103193,5,0 +256,248,103364,1,0 +80,344,103536,1,0 +480,256,103878,5,0 +408,72,104050,1,0 +336,256,104221,1,0 +264,72,104393,1,0 +184,256,104564,1,0 +104,72,104736,1,0 +32,256,104907,1,8 +376,48,105593,6,0,B|428:29,1,50,0|0 +411,78,105764,2,0,B|467:78,1,50,0|0 +438,127,105936,2,0,B|492:142,1,50,0|0 +447,176,106107,2,0,B|498:199,1,50,0|0 +492,196,106278,1,8 +120,344,106621,2,0,B|13:289|43:167,1,200 +400,352,107307,6,0,B|354:319,1,50,0|0 +422,286,107478,2,0,B|369:267,1,50,0|0 +436,219,107650,2,0,B|379:214,1,50,0|0 +430,152,107821,2,0,B|374:161,1,50,0|0 +410,89,107993,2,0,B|359:112,1,50,0|0 +377,34,108164,2,0,B|334:71,1,50,0|0 +343,68,108336,1,8 +48,344,108678,6,0,B|154:289|124:167,1,200 +464,40,109364,2,0,B|357:94|387:216,1,200 +32,32,110050,6,0,B|86:17,1,50 +16,94,110221,2,0,B|64:66,1,50 +27,165,110393,2,0,B|67:125,1,50 +42,226,110564,2,0,B|69:177,1,50 +76,282,110736,2,0,B|90:228,1,50 +134,324,110907,2,0,B|133:268,1,50 +134,274,111078,1,8 +456,40,111421,6,0,B|352:40,2,100,0|0|8 +56,40,112107,2,0,B|160:40,2,100,0|0|8 +16,192,112793,5,0 +96,192,112964,1,0 +176,192,113136,1,0 +256,192,113307,1,0 +336,192,113478,1,0 +416,192,113650,1,0 +496,192,113821,1,8 +312,112,114164,5,0 +256,192,114336,1,0 +192,112,114507,1,0 +256,304,114850,5,0 +344,256,115021,1,0 +312,160,115193,1,0 +208,160,115364,1,0 +176,256,115536,1,0 +256,304,115707,1,0 +256,304,115878,1,2 +120,160,116564,6,0,B|40:96,1,100,0|0 +368,336,117250,2,0,B|472:328,1,100,0|0 +72,248,117936,2,0,B|40:344,1,100 +368,112,118621,2,0,B|264:104,2,100 +392,312,119650,5,0 +392,312,119821,1,0 +448,264,119993,1,0 +448,264,120164,1,0 +480,200,120336,1,0 +480,200,120507,1,0 +480,200,120678,1,0 +448,48,121021,5,0 +440,48,121107,1,0 +432,48,121193,1,0 +424,48,121278,1,0 +416,48,121364,1,0 +312,96,121621,1,0 +312,96,121707,1,0 +232,104,121878,1,0 +168,144,122050,1,0 +352,232,122393,5,0 +376,192,122564,1,0 +352,144,122736,1,0 +168,144,123078,1,0 +144,184,123250,1,0 +168,232,123421,1,0 +400,48,123936,5,0 +467,115,124107,1,0 +400,183,124278,1,0 +326,110,124450,1,0 +320,104,124536,1,0 +315,98,124621,1,0 +309,93,124707,1,0 +303,87,124793,1,0 +112,336,125136,5,0 +112,336,125307,1,0 +44,268,125478,1,0 +44,268,125650,1,0 +112,200,125821,1,0 +112,200,125993,1,0 +184,264,126164,1,0 +189,258,126250,1,0 +195,252,126336,1,0 +200,247,126421,1,0 +206,241,126507,1,0 +212,235,126593,1,0 +217,230,126678,1,0 +223,224,126764,1,0 +229,218,126850,1,0 +72,96,127536,5,0 +72,192,127707,1,0 +112,96,127878,1,0 +112,192,128050,1,0 +152,96,128221,1,0 +152,192,128393,1,0 +192,96,128564,1,0 +192,192,128736,1,0 +280,144,128907,1,0 +296,344,129250,5,0 +395,344,129421,1,0 +395,243,129593,1,0 +295,241,129764,1,0 +295,137,129936,1,0 +391,137,130107,1,0 +391,33,130278,1,0 +256,104,130621,5,0 +168,192,130793,1,0 +256,280,130964,1,0 +344,192,131136,1,0 +344,192,131307,1,0 +344,288,131478,1,0 +344,96,131650,1,0 +168,184,131993,5,0 +168,184,132164,1,0 +168,184,132336,1,0 +272,80,132593,1,0 +272,80,132678,1,0 +168,80,132850,1,0 +168,80,133021,1,0 +40,240,133364,6,0,B|40:344,1,100 +208,224,134050,2,0,B|208:120,1,100 +208,328,134736,1,0 +208,224,134907,1,0 +304,224,135078,1,0 +304,120,135250,1,0 +400,120,135421,1,0 +400,16,135593,1,0 +496,16,135764,1,0 +296,56,136107,5,0 +216,112,136278,1,0 +296,168,136450,1,0 +216,232,136621,1,0 +296,288,136793,1,0 +292,188,136964,5,4 +300,188,137050,1,4 +308,188,137136,1,4 +220,188,137307,1,4 +212,188,137393,1,4 +204,188,137478,1,4 +260,268,137650,1,4 +260,276,137736,1,4 +260,284,137821,1,4 +256,208,137993,12,4,139021 +256,16,139193,5,2 +256,112,139536,2,2,B|312:112,1,50,2|2 +256,200,140221,2,2,B|200:200,1,50,2|2 +256,288,140907,2,2,B|312:288,1,50,2|2 +256,376,141593,2,2,B|200:376,1,50 +256,208,142278,12,4,145021 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3949367-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3949367-expected-conversion.json new file mode 100644 index 0000000000..da0e4e120a --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3949367-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":6003.0,"Objects":[{"StartTime":6003.0,"Position":64.0,"HyperDash":false}]},{"StartTime":6366.0,"Objects":[{"StartTime":6366.0,"Position":192.0,"HyperDash":false}]},{"StartTime":6730.0,"Objects":[{"StartTime":6730.0,"Position":64.0,"HyperDash":false}]},{"StartTime":7094.0,"Objects":[{"StartTime":7094.0,"Position":192.0,"HyperDash":false}]},{"StartTime":7457.0,"Objects":[{"StartTime":7457.0,"Position":320.0,"HyperDash":false}]},{"StartTime":7821.0,"Objects":[{"StartTime":7821.0,"Position":192.0,"HyperDash":false}]},{"StartTime":8185.0,"Objects":[{"StartTime":8185.0,"Position":320.0,"HyperDash":false}]},{"StartTime":8548.0,"Objects":[{"StartTime":8548.0,"Position":192.0,"HyperDash":false}]},{"StartTime":8912.0,"Objects":[{"StartTime":8912.0,"Position":320.0,"HyperDash":false}]},{"StartTime":9275.0,"Objects":[{"StartTime":9275.0,"Position":448.0,"HyperDash":false}]},{"StartTime":9639.0,"Objects":[{"StartTime":9639.0,"Position":320.0,"HyperDash":false}]},{"StartTime":10003.0,"Objects":[{"StartTime":10003.0,"Position":448.0,"HyperDash":false}]},{"StartTime":10366.0,"Objects":[{"StartTime":10366.0,"Position":65.0,"HyperDash":false},{"StartTime":10434.0,"Position":482.0,"HyperDash":false},{"StartTime":10502.0,"Position":164.0,"HyperDash":false},{"StartTime":10570.0,"Position":315.0,"HyperDash":false},{"StartTime":10638.0,"Position":145.0,"HyperDash":false},{"StartTime":10706.0,"Position":159.0,"HyperDash":false},{"StartTime":10775.0,"Position":310.0,"HyperDash":false},{"StartTime":10843.0,"Position":441.0,"HyperDash":false},{"StartTime":10911.0,"Position":428.0,"HyperDash":false},{"StartTime":10979.0,"Position":243.0,"HyperDash":false},{"StartTime":11047.0,"Position":422.0,"HyperDash":false},{"StartTime":11116.0,"Position":481.0,"HyperDash":false},{"StartTime":11184.0,"Position":104.0,"HyperDash":false},{"StartTime":11252.0,"Position":473.0,"HyperDash":false},{"StartTime":11320.0,"Position":135.0,"HyperDash":false},{"StartTime":11388.0,"Position":360.0,"HyperDash":false},{"StartTime":11457.0,"Position":123.0,"HyperDash":false}]},{"StartTime":11821.0,"Objects":[{"StartTime":11821.0,"Position":96.0,"HyperDash":false}]},{"StartTime":12003.0,"Objects":[{"StartTime":12003.0,"Position":176.0,"HyperDash":false},{"StartTime":12093.0,"Position":204.284271,"HyperDash":false},{"StartTime":12184.0,"Position":176.0,"HyperDash":false}]},{"StartTime":12366.0,"Objects":[{"StartTime":12366.0,"Position":64.0,"HyperDash":false}]},{"StartTime":12730.0,"Objects":[{"StartTime":12730.0,"Position":224.0,"HyperDash":false},{"StartTime":12820.0,"Position":252.284271,"HyperDash":false},{"StartTime":12911.0,"Position":224.0,"HyperDash":false}]},{"StartTime":13094.0,"Objects":[{"StartTime":13094.0,"Position":144.0,"HyperDash":false}]},{"StartTime":13275.0,"Objects":[{"StartTime":13275.0,"Position":320.0,"HyperDash":false}]},{"StartTime":13366.0,"Objects":[{"StartTime":13366.0,"Position":368.0,"HyperDash":false}]},{"StartTime":13457.0,"Objects":[{"StartTime":13457.0,"Position":320.0,"HyperDash":false}]},{"StartTime":13639.0,"Objects":[{"StartTime":13639.0,"Position":208.0,"HyperDash":false}]},{"StartTime":13730.0,"Objects":[{"StartTime":13730.0,"Position":160.0,"HyperDash":false},{"StartTime":13820.0,"Position":160.0,"HyperDash":false}]},{"StartTime":14003.0,"Objects":[{"StartTime":14003.0,"Position":240.0,"HyperDash":false}]},{"StartTime":14185.0,"Objects":[{"StartTime":14185.0,"Position":128.0,"HyperDash":false}]},{"StartTime":14366.0,"Objects":[{"StartTime":14366.0,"Position":208.0,"HyperDash":false},{"StartTime":14456.0,"Position":208.0,"HyperDash":false}]},{"StartTime":14548.0,"Objects":[{"StartTime":14548.0,"Position":160.0,"HyperDash":false}]},{"StartTime":14730.0,"Objects":[{"StartTime":14730.0,"Position":336.0,"HyperDash":false}]},{"StartTime":14912.0,"Objects":[{"StartTime":14912.0,"Position":256.0,"HyperDash":false},{"StartTime":15002.0,"Position":227.715729,"HyperDash":false},{"StartTime":15093.0,"Position":256.0,"HyperDash":false}]},{"StartTime":15275.0,"Objects":[{"StartTime":15275.0,"Position":368.0,"HyperDash":false}]},{"StartTime":15639.0,"Objects":[{"StartTime":15639.0,"Position":208.0,"HyperDash":false},{"StartTime":15729.0,"Position":179.715729,"HyperDash":false},{"StartTime":15820.0,"Position":208.0,"HyperDash":false}]},{"StartTime":16003.0,"Objects":[{"StartTime":16003.0,"Position":288.0,"HyperDash":false}]},{"StartTime":16185.0,"Objects":[{"StartTime":16185.0,"Position":112.0,"HyperDash":false}]},{"StartTime":16275.0,"Objects":[{"StartTime":16275.0,"Position":64.0,"HyperDash":false}]},{"StartTime":16366.0,"Objects":[{"StartTime":16366.0,"Position":112.0,"HyperDash":false}]},{"StartTime":16548.0,"Objects":[{"StartTime":16548.0,"Position":224.0,"HyperDash":false}]},{"StartTime":16639.0,"Objects":[{"StartTime":16639.0,"Position":272.0,"HyperDash":false},{"StartTime":16729.0,"Position":272.0,"HyperDash":false}]},{"StartTime":16912.0,"Objects":[{"StartTime":16912.0,"Position":160.0,"HyperDash":false}]},{"StartTime":17003.0,"Objects":[{"StartTime":17003.0,"Position":208.0,"HyperDash":false}]},{"StartTime":17094.0,"Objects":[{"StartTime":17094.0,"Position":256.0,"HyperDash":false}]},{"StartTime":17275.0,"Objects":[{"StartTime":17275.0,"Position":144.0,"HyperDash":false}]},{"StartTime":17366.0,"Objects":[{"StartTime":17366.0,"Position":80.0,"HyperDash":false}]},{"StartTime":17457.0,"Objects":[{"StartTime":17457.0,"Position":144.0,"HyperDash":false}]},{"StartTime":17639.0,"Objects":[{"StartTime":17639.0,"Position":320.0,"HyperDash":false}]},{"StartTime":17821.0,"Objects":[{"StartTime":17821.0,"Position":400.0,"HyperDash":false}]},{"StartTime":17912.0,"Objects":[{"StartTime":17912.0,"Position":352.0,"HyperDash":false}]},{"StartTime":18003.0,"Objects":[{"StartTime":18003.0,"Position":304.0,"HyperDash":false}]},{"StartTime":18185.0,"Objects":[{"StartTime":18185.0,"Position":416.0,"HyperDash":false},{"StartTime":18275.0,"Position":406.779816,"HyperDash":false},{"StartTime":18366.0,"Position":431.646057,"HyperDash":false},{"StartTime":18439.0,"Position":420.6284,"HyperDash":false},{"StartTime":18548.0,"Position":353.58432,"HyperDash":false}]},{"StartTime":18639.0,"Objects":[{"StartTime":18639.0,"Position":400.0,"HyperDash":false}]},{"StartTime":18730.0,"Objects":[{"StartTime":18730.0,"Position":448.0,"HyperDash":false}]},{"StartTime":18912.0,"Objects":[{"StartTime":18912.0,"Position":368.0,"HyperDash":false}]},{"StartTime":19094.0,"Objects":[{"StartTime":19094.0,"Position":192.0,"HyperDash":false}]},{"StartTime":19185.0,"Objects":[{"StartTime":19185.0,"Position":144.0,"HyperDash":false}]},{"StartTime":19275.0,"Objects":[{"StartTime":19275.0,"Position":192.0,"HyperDash":false}]},{"StartTime":19457.0,"Objects":[{"StartTime":19457.0,"Position":304.0,"HyperDash":false}]},{"StartTime":19548.0,"Objects":[{"StartTime":19548.0,"Position":352.0,"HyperDash":false},{"StartTime":19638.0,"Position":352.0,"HyperDash":false}]},{"StartTime":19821.0,"Objects":[{"StartTime":19821.0,"Position":272.0,"HyperDash":false}]},{"StartTime":20003.0,"Objects":[{"StartTime":20003.0,"Position":384.0,"HyperDash":false}]},{"StartTime":20185.0,"Objects":[{"StartTime":20185.0,"Position":304.0,"HyperDash":false},{"StartTime":20275.0,"Position":304.0,"HyperDash":false}]},{"StartTime":20366.0,"Objects":[{"StartTime":20366.0,"Position":352.0,"HyperDash":false}]},{"StartTime":20548.0,"Objects":[{"StartTime":20548.0,"Position":176.0,"HyperDash":false}]},{"StartTime":20730.0,"Objects":[{"StartTime":20730.0,"Position":96.0,"HyperDash":false}]},{"StartTime":20821.0,"Objects":[{"StartTime":20821.0,"Position":144.0,"HyperDash":false}]},{"StartTime":20912.0,"Objects":[{"StartTime":20912.0,"Position":192.0,"HyperDash":false}]},{"StartTime":21094.0,"Objects":[{"StartTime":21094.0,"Position":80.0,"HyperDash":false},{"StartTime":21184.0,"Position":82.2201843,"HyperDash":false},{"StartTime":21275.0,"Position":64.35393,"HyperDash":false},{"StartTime":21348.0,"Position":98.3716049,"HyperDash":false},{"StartTime":21457.0,"Position":142.41568,"HyperDash":false}]},{"StartTime":21548.0,"Objects":[{"StartTime":21548.0,"Position":96.0,"HyperDash":false}]},{"StartTime":21639.0,"Objects":[{"StartTime":21639.0,"Position":48.0,"HyperDash":false}]},{"StartTime":21821.0,"Objects":[{"StartTime":21821.0,"Position":128.0,"HyperDash":false}]},{"StartTime":22003.0,"Objects":[{"StartTime":22003.0,"Position":304.0,"HyperDash":false}]},{"StartTime":22094.0,"Objects":[{"StartTime":22094.0,"Position":352.0,"HyperDash":false}]},{"StartTime":22185.0,"Objects":[{"StartTime":22185.0,"Position":304.0,"HyperDash":false}]},{"StartTime":22366.0,"Objects":[{"StartTime":22366.0,"Position":192.0,"HyperDash":false}]},{"StartTime":22457.0,"Objects":[{"StartTime":22457.0,"Position":144.0,"HyperDash":false},{"StartTime":22547.0,"Position":144.0,"HyperDash":false}]},{"StartTime":22730.0,"Objects":[{"StartTime":22730.0,"Position":224.0,"HyperDash":false},{"StartTime":22820.0,"Position":191.366974,"HyperDash":false},{"StartTime":22911.0,"Position":144.293579,"HyperDash":false},{"StartTime":23002.0,"Position":168.779816,"HyperDash":false},{"StartTime":23093.0,"Position":223.85321,"HyperDash":false},{"StartTime":23166.0,"Position":182.0,"HyperDash":false},{"StartTime":23275.0,"Position":144.0,"HyperDash":true}]},{"StartTime":23457.0,"Objects":[{"StartTime":23457.0,"Position":400.0,"HyperDash":false}]},{"StartTime":23639.0,"Objects":[{"StartTime":23639.0,"Position":480.0,"HyperDash":false},{"StartTime":23729.0,"Position":480.0,"HyperDash":false}]},{"StartTime":23821.0,"Objects":[{"StartTime":23821.0,"Position":432.0,"HyperDash":false}]},{"StartTime":24003.0,"Objects":[{"StartTime":24003.0,"Position":320.0,"HyperDash":true}]},{"StartTime":24185.0,"Objects":[{"StartTime":24185.0,"Position":64.0,"HyperDash":false},{"StartTime":24257.0,"Position":62.7589569,"HyperDash":false},{"StartTime":24366.0,"Position":48.3107071,"HyperDash":false}]},{"StartTime":24457.0,"Objects":[{"StartTime":24457.0,"Position":96.0,"HyperDash":false}]},{"StartTime":24548.0,"Objects":[{"StartTime":24548.0,"Position":144.0,"HyperDash":false}]},{"StartTime":24730.0,"Objects":[{"StartTime":24730.0,"Position":64.0,"HyperDash":false}]},{"StartTime":24912.0,"Objects":[{"StartTime":24912.0,"Position":240.0,"HyperDash":false}]},{"StartTime":25094.0,"Objects":[{"StartTime":25094.0,"Position":320.0,"HyperDash":false},{"StartTime":25184.0,"Position":360.0,"HyperDash":false},{"StartTime":25275.0,"Position":320.0,"HyperDash":false}]},{"StartTime":25457.0,"Objects":[{"StartTime":25457.0,"Position":208.0,"HyperDash":true}]},{"StartTime":25639.0,"Objects":[{"StartTime":25639.0,"Position":464.0,"HyperDash":false},{"StartTime":25711.0,"Position":466.758942,"HyperDash":false},{"StartTime":25820.0,"Position":448.3107,"HyperDash":false}]},{"StartTime":26003.0,"Objects":[{"StartTime":26003.0,"Position":336.0,"HyperDash":false},{"StartTime":26075.0,"Position":333.758942,"HyperDash":false},{"StartTime":26184.0,"Position":320.3107,"HyperDash":false}]},{"StartTime":26366.0,"Objects":[{"StartTime":26366.0,"Position":496.0,"HyperDash":false}]},{"StartTime":26548.0,"Objects":[{"StartTime":26548.0,"Position":416.0,"HyperDash":false},{"StartTime":26638.0,"Position":416.0,"HyperDash":false}]},{"StartTime":26730.0,"Objects":[{"StartTime":26730.0,"Position":464.0,"HyperDash":false}]},{"StartTime":26912.0,"Objects":[{"StartTime":26912.0,"Position":352.0,"HyperDash":true}]},{"StartTime":27094.0,"Objects":[{"StartTime":27094.0,"Position":96.0,"HyperDash":false},{"StartTime":27166.0,"Position":79.75896,"HyperDash":false},{"StartTime":27275.0,"Position":80.31071,"HyperDash":false}]},{"StartTime":27457.0,"Objects":[{"StartTime":27457.0,"Position":192.0,"HyperDash":false}]},{"StartTime":27548.0,"Objects":[{"StartTime":27548.0,"Position":240.0,"HyperDash":false},{"StartTime":27638.0,"Position":240.0,"HyperDash":false}]},{"StartTime":27821.0,"Objects":[{"StartTime":27821.0,"Position":64.0,"HyperDash":false}]},{"StartTime":28003.0,"Objects":[{"StartTime":28003.0,"Position":176.0,"HyperDash":false}]},{"StartTime":28185.0,"Objects":[{"StartTime":28185.0,"Position":64.0,"HyperDash":false}]},{"StartTime":28275.0,"Objects":[{"StartTime":28275.0,"Position":16.0,"HyperDash":false},{"StartTime":28365.0,"Position":16.0,"HyperDash":false}]},{"StartTime":28548.0,"Objects":[{"StartTime":28548.0,"Position":272.0,"HyperDash":false},{"StartTime":28620.0,"Position":293.8232,"HyperDash":false},{"StartTime":28729.0,"Position":352.0,"HyperDash":false}]},{"StartTime":28912.0,"Objects":[{"StartTime":28912.0,"Position":240.0,"HyperDash":false},{"StartTime":28984.0,"Position":215.176788,"HyperDash":false},{"StartTime":29093.0,"Position":160.0,"HyperDash":true}]},{"StartTime":29275.0,"Objects":[{"StartTime":29275.0,"Position":416.0,"HyperDash":false}]},{"StartTime":29457.0,"Objects":[{"StartTime":29457.0,"Position":496.0,"HyperDash":false},{"StartTime":29547.0,"Position":496.0,"HyperDash":false}]},{"StartTime":29639.0,"Objects":[{"StartTime":29639.0,"Position":448.0,"HyperDash":false}]},{"StartTime":29821.0,"Objects":[{"StartTime":29821.0,"Position":336.0,"HyperDash":true}]},{"StartTime":30003.0,"Objects":[{"StartTime":30003.0,"Position":80.0,"HyperDash":false},{"StartTime":30075.0,"Position":74.90608,"HyperDash":false},{"StartTime":30184.0,"Position":32.0,"HyperDash":false}]},{"StartTime":30275.0,"Objects":[{"StartTime":30275.0,"Position":32.0,"HyperDash":false}]},{"StartTime":30366.0,"Objects":[{"StartTime":30366.0,"Position":64.0,"HyperDash":false}]},{"StartTime":30548.0,"Objects":[{"StartTime":30548.0,"Position":144.0,"HyperDash":false}]},{"StartTime":30730.0,"Objects":[{"StartTime":30730.0,"Position":320.0,"HyperDash":false}]},{"StartTime":30912.0,"Objects":[{"StartTime":30912.0,"Position":240.0,"HyperDash":false},{"StartTime":31002.0,"Position":200.0,"HyperDash":false},{"StartTime":31093.0,"Position":240.0,"HyperDash":false}]},{"StartTime":31275.0,"Objects":[{"StartTime":31275.0,"Position":352.0,"HyperDash":true}]},{"StartTime":31457.0,"Objects":[{"StartTime":31457.0,"Position":96.0,"HyperDash":false},{"StartTime":31529.0,"Position":96.75896,"HyperDash":false},{"StartTime":31638.0,"Position":80.31071,"HyperDash":false}]},{"StartTime":31821.0,"Objects":[{"StartTime":31821.0,"Position":192.0,"HyperDash":false}]},{"StartTime":32003.0,"Objects":[{"StartTime":32003.0,"Position":80.0,"HyperDash":false}]},{"StartTime":32185.0,"Objects":[{"StartTime":32185.0,"Position":256.0,"HyperDash":false}]},{"StartTime":32366.0,"Objects":[{"StartTime":32366.0,"Position":336.0,"HyperDash":false},{"StartTime":32456.0,"Position":336.0,"HyperDash":false}]},{"StartTime":32548.0,"Objects":[{"StartTime":32548.0,"Position":288.0,"HyperDash":false}]},{"StartTime":32730.0,"Objects":[{"StartTime":32730.0,"Position":400.0,"HyperDash":true}]},{"StartTime":32912.0,"Objects":[{"StartTime":32912.0,"Position":144.0,"HyperDash":false},{"StartTime":32984.0,"Position":149.758957,"HyperDash":false},{"StartTime":33093.0,"Position":128.310715,"HyperDash":false}]},{"StartTime":33275.0,"Objects":[{"StartTime":33275.0,"Position":240.0,"HyperDash":false}]},{"StartTime":33366.0,"Objects":[{"StartTime":33366.0,"Position":288.0,"HyperDash":false}]},{"StartTime":33457.0,"Objects":[{"StartTime":33457.0,"Position":240.0,"HyperDash":false}]},{"StartTime":33639.0,"Objects":[{"StartTime":33639.0,"Position":128.0,"HyperDash":false}]},{"StartTime":33821.0,"Objects":[{"StartTime":33821.0,"Position":240.0,"HyperDash":false}]},{"StartTime":34003.0,"Objects":[{"StartTime":34003.0,"Position":128.0,"HyperDash":false}]},{"StartTime":34094.0,"Objects":[{"StartTime":34094.0,"Position":80.0,"HyperDash":false},{"StartTime":34184.0,"Position":80.0,"HyperDash":true}]},{"StartTime":34366.0,"Objects":[{"StartTime":34366.0,"Position":336.0,"HyperDash":false},{"StartTime":34438.0,"Position":369.8232,"HyperDash":false},{"StartTime":34547.0,"Position":416.0,"HyperDash":false}]},{"StartTime":34730.0,"Objects":[{"StartTime":34730.0,"Position":240.0,"HyperDash":false},{"StartTime":34802.0,"Position":189.176788,"HyperDash":false},{"StartTime":34911.0,"Position":160.0,"HyperDash":true}]},{"StartTime":35094.0,"Objects":[{"StartTime":35094.0,"Position":432.0,"HyperDash":false},{"StartTime":35184.0,"Position":432.0,"HyperDash":false}]},{"StartTime":35275.0,"Objects":[{"StartTime":35275.0,"Position":384.0,"HyperDash":false}]},{"StartTime":35457.0,"Objects":[{"StartTime":35457.0,"Position":208.0,"HyperDash":false},{"StartTime":35529.0,"Position":159.176788,"HyperDash":false},{"StartTime":35638.0,"Position":128.0,"HyperDash":false}]},{"StartTime":35821.0,"Objects":[{"StartTime":35821.0,"Position":384.0,"HyperDash":false}]},{"StartTime":36003.0,"Objects":[{"StartTime":36003.0,"Position":464.0,"HyperDash":false}]},{"StartTime":36094.0,"Objects":[{"StartTime":36094.0,"Position":384.0,"HyperDash":false}]},{"StartTime":36185.0,"Objects":[{"StartTime":36185.0,"Position":336.0,"HyperDash":false}]},{"StartTime":36366.0,"Objects":[{"StartTime":36366.0,"Position":448.0,"HyperDash":true}]},{"StartTime":36548.0,"Objects":[{"StartTime":36548.0,"Position":192.0,"HyperDash":false},{"StartTime":36638.0,"Position":152.0,"HyperDash":false},{"StartTime":36729.0,"Position":192.0,"HyperDash":false}]},{"StartTime":36912.0,"Objects":[{"StartTime":36912.0,"Position":368.0,"HyperDash":false}]},{"StartTime":37003.0,"Objects":[{"StartTime":37003.0,"Position":416.0,"HyperDash":false},{"StartTime":37093.0,"Position":416.0,"HyperDash":true}]},{"StartTime":37275.0,"Objects":[{"StartTime":37275.0,"Position":160.0,"HyperDash":false},{"StartTime":37347.0,"Position":156.758957,"HyperDash":false},{"StartTime":37456.0,"Position":144.310715,"HyperDash":false}]},{"StartTime":37548.0,"Objects":[{"StartTime":37548.0,"Position":192.0,"HyperDash":false}]},{"StartTime":37639.0,"Objects":[{"StartTime":37639.0,"Position":272.0,"HyperDash":false}]},{"StartTime":37821.0,"Objects":[{"StartTime":37821.0,"Position":160.0,"HyperDash":true}]},{"StartTime":38003.0,"Objects":[{"StartTime":38003.0,"Position":416.0,"HyperDash":false}]},{"StartTime":38185.0,"Objects":[{"StartTime":38185.0,"Position":496.0,"HyperDash":false},{"StartTime":38275.0,"Position":496.0,"HyperDash":false}]},{"StartTime":38366.0,"Objects":[{"StartTime":38366.0,"Position":416.0,"HyperDash":false}]},{"StartTime":38548.0,"Objects":[{"StartTime":38548.0,"Position":496.0,"HyperDash":true}]},{"StartTime":38730.0,"Objects":[{"StartTime":38730.0,"Position":240.0,"HyperDash":false}]},{"StartTime":38821.0,"Objects":[{"StartTime":38821.0,"Position":192.0,"HyperDash":false}]},{"StartTime":38912.0,"Objects":[{"StartTime":38912.0,"Position":240.0,"HyperDash":false}]},{"StartTime":39094.0,"Objects":[{"StartTime":39094.0,"Position":352.0,"HyperDash":false},{"StartTime":39166.0,"Position":328.1768,"HyperDash":false},{"StartTime":39275.0,"Position":272.0,"HyperDash":true}]},{"StartTime":39457.0,"Objects":[{"StartTime":39457.0,"Position":16.0,"HyperDash":false},{"StartTime":39547.0,"Position":16.0,"HyperDash":false}]},{"StartTime":39639.0,"Objects":[{"StartTime":39639.0,"Position":64.0,"HyperDash":false}]},{"StartTime":39821.0,"Objects":[{"StartTime":39821.0,"Position":240.0,"HyperDash":false},{"StartTime":39911.0,"Position":211.715729,"HyperDash":false}]},{"StartTime":40003.0,"Objects":[{"StartTime":40003.0,"Position":160.0,"HyperDash":true}]},{"StartTime":40185.0,"Objects":[{"StartTime":40185.0,"Position":416.0,"HyperDash":false}]},{"StartTime":40275.0,"Objects":[{"StartTime":40275.0,"Position":464.0,"HyperDash":false}]},{"StartTime":40366.0,"Objects":[{"StartTime":40366.0,"Position":416.0,"HyperDash":false}]},{"StartTime":40548.0,"Objects":[{"StartTime":40548.0,"Position":240.0,"HyperDash":false}]},{"StartTime":40639.0,"Objects":[{"StartTime":40639.0,"Position":288.0,"HyperDash":false}]},{"StartTime":40730.0,"Objects":[{"StartTime":40730.0,"Position":336.0,"HyperDash":true}]},{"StartTime":40912.0,"Objects":[{"StartTime":40912.0,"Position":64.0,"HyperDash":false},{"StartTime":41002.0,"Position":64.0,"HyperDash":false}]},{"StartTime":41094.0,"Objects":[{"StartTime":41094.0,"Position":112.0,"HyperDash":false}]},{"StartTime":41275.0,"Objects":[{"StartTime":41275.0,"Position":288.0,"HyperDash":false},{"StartTime":41347.0,"Position":312.8232,"HyperDash":false},{"StartTime":41456.0,"Position":368.0,"HyperDash":false}]},{"StartTime":41639.0,"Objects":[{"StartTime":41639.0,"Position":112.0,"HyperDash":false}]},{"StartTime":41821.0,"Objects":[{"StartTime":41821.0,"Position":32.0,"HyperDash":false}]},{"StartTime":41912.0,"Objects":[{"StartTime":41912.0,"Position":112.0,"HyperDash":false}]},{"StartTime":42003.0,"Objects":[{"StartTime":42003.0,"Position":160.0,"HyperDash":false}]},{"StartTime":42185.0,"Objects":[{"StartTime":42185.0,"Position":48.0,"HyperDash":true}]},{"StartTime":42366.0,"Objects":[{"StartTime":42366.0,"Position":304.0,"HyperDash":false},{"StartTime":42438.0,"Position":338.8232,"HyperDash":false},{"StartTime":42547.0,"Position":384.0,"HyperDash":false}]},{"StartTime":42730.0,"Objects":[{"StartTime":42730.0,"Position":208.0,"HyperDash":false},{"StartTime":42802.0,"Position":174.176788,"HyperDash":false},{"StartTime":42911.0,"Position":128.0,"HyperDash":false}]},{"StartTime":43094.0,"Objects":[{"StartTime":43094.0,"Position":384.0,"HyperDash":false},{"StartTime":43166.0,"Position":407.241058,"HyperDash":false},{"StartTime":43275.0,"Position":399.6893,"HyperDash":false}]},{"StartTime":43366.0,"Objects":[{"StartTime":43366.0,"Position":352.0,"HyperDash":false}]},{"StartTime":43457.0,"Objects":[{"StartTime":43457.0,"Position":272.0,"HyperDash":false}]},{"StartTime":43639.0,"Objects":[{"StartTime":43639.0,"Position":384.0,"HyperDash":true}]},{"StartTime":43821.0,"Objects":[{"StartTime":43821.0,"Position":128.0,"HyperDash":false}]},{"StartTime":44003.0,"Objects":[{"StartTime":44003.0,"Position":48.0,"HyperDash":false},{"StartTime":44093.0,"Position":48.0,"HyperDash":false}]},{"StartTime":44185.0,"Objects":[{"StartTime":44185.0,"Position":128.0,"HyperDash":false}]},{"StartTime":44366.0,"Objects":[{"StartTime":44366.0,"Position":48.0,"HyperDash":true}]},{"StartTime":44548.0,"Objects":[{"StartTime":44548.0,"Position":304.0,"HyperDash":false}]},{"StartTime":44730.0,"Objects":[{"StartTime":44730.0,"Position":384.0,"HyperDash":false}]},{"StartTime":44821.0,"Objects":[{"StartTime":44821.0,"Position":336.0,"HyperDash":false}]},{"StartTime":44912.0,"Objects":[{"StartTime":44912.0,"Position":256.0,"HyperDash":false}]},{"StartTime":45094.0,"Objects":[{"StartTime":45094.0,"Position":368.0,"HyperDash":true}]},{"StartTime":45275.0,"Objects":[{"StartTime":45275.0,"Position":112.0,"HyperDash":false}]},{"StartTime":45366.0,"Objects":[{"StartTime":45366.0,"Position":64.0,"HyperDash":false}]},{"StartTime":45457.0,"Objects":[{"StartTime":45457.0,"Position":112.0,"HyperDash":false}]},{"StartTime":45639.0,"Objects":[{"StartTime":45639.0,"Position":288.0,"HyperDash":false}]},{"StartTime":45730.0,"Objects":[{"StartTime":45730.0,"Position":336.0,"HyperDash":false},{"StartTime":45820.0,"Position":336.0,"HyperDash":false}]},{"StartTime":46003.0,"Objects":[{"StartTime":46003.0,"Position":80.0,"HyperDash":false},{"StartTime":46093.0,"Position":80.0,"HyperDash":false}]},{"StartTime":46185.0,"Objects":[{"StartTime":46185.0,"Position":128.0,"HyperDash":false}]},{"StartTime":46366.0,"Objects":[{"StartTime":46366.0,"Position":304.0,"HyperDash":false}]},{"StartTime":46457.0,"Objects":[{"StartTime":46457.0,"Position":256.0,"HyperDash":false}]},{"StartTime":46548.0,"Objects":[{"StartTime":46548.0,"Position":208.0,"HyperDash":true}]},{"StartTime":46730.0,"Objects":[{"StartTime":46730.0,"Position":464.0,"HyperDash":false}]},{"StartTime":46912.0,"Objects":[{"StartTime":46912.0,"Position":45.0,"HyperDash":false},{"StartTime":46997.0,"Position":397.0,"HyperDash":false},{"StartTime":47082.0,"Position":342.0,"HyperDash":false},{"StartTime":47167.0,"Position":163.0,"HyperDash":false},{"StartTime":47252.0,"Position":278.0,"HyperDash":false},{"StartTime":47338.0,"Position":220.0,"HyperDash":false},{"StartTime":47423.0,"Position":253.0,"HyperDash":false},{"StartTime":47508.0,"Position":233.0,"HyperDash":false},{"StartTime":47593.0,"Position":97.0,"HyperDash":false},{"StartTime":47678.0,"Position":473.0,"HyperDash":false},{"StartTime":47764.0,"Position":189.0,"HyperDash":false},{"StartTime":47849.0,"Position":194.0,"HyperDash":false},{"StartTime":47934.0,"Position":107.0,"HyperDash":false},{"StartTime":48019.0,"Position":21.0,"HyperDash":false},{"StartTime":48105.0,"Position":461.0,"HyperDash":false},{"StartTime":48190.0,"Position":498.0,"HyperDash":false},{"StartTime":48275.0,"Position":184.0,"HyperDash":false},{"StartTime":48360.0,"Position":78.0,"HyperDash":false},{"StartTime":48445.0,"Position":338.0,"HyperDash":false},{"StartTime":48531.0,"Position":392.0,"HyperDash":false},{"StartTime":48616.0,"Position":335.0,"HyperDash":false},{"StartTime":48701.0,"Position":193.0,"HyperDash":false},{"StartTime":48786.0,"Position":478.0,"HyperDash":false},{"StartTime":48872.0,"Position":255.0,"HyperDash":false},{"StartTime":48957.0,"Position":175.0,"HyperDash":false},{"StartTime":49042.0,"Position":274.0,"HyperDash":false},{"StartTime":49127.0,"Position":442.0,"HyperDash":false},{"StartTime":49212.0,"Position":295.0,"HyperDash":false},{"StartTime":49298.0,"Position":311.0,"HyperDash":false},{"StartTime":49383.0,"Position":17.0,"HyperDash":false},{"StartTime":49468.0,"Position":467.0,"HyperDash":false},{"StartTime":49553.0,"Position":30.0,"HyperDash":false},{"StartTime":49639.0,"Position":218.0,"HyperDash":false}]},{"StartTime":52548.0,"Objects":[{"StartTime":52548.0,"Position":200.0,"HyperDash":false},{"StartTime":52620.0,"Position":175.758957,"HyperDash":false},{"StartTime":52729.0,"Position":184.310715,"HyperDash":false}]},{"StartTime":52912.0,"Objects":[{"StartTime":52912.0,"Position":280.0,"HyperDash":false},{"StartTime":52984.0,"Position":269.758942,"HyperDash":false},{"StartTime":53093.0,"Position":264.3107,"HyperDash":false}]},{"StartTime":53457.0,"Objects":[{"StartTime":53457.0,"Position":104.0,"HyperDash":false}]},{"StartTime":53639.0,"Objects":[{"StartTime":53639.0,"Position":184.0,"HyperDash":false}]},{"StartTime":54003.0,"Objects":[{"StartTime":54003.0,"Position":344.0,"HyperDash":false},{"StartTime":54075.0,"Position":333.241058,"HyperDash":false},{"StartTime":54184.0,"Position":359.6893,"HyperDash":false}]},{"StartTime":54366.0,"Objects":[{"StartTime":54366.0,"Position":256.0,"HyperDash":false},{"StartTime":54438.0,"Position":273.241058,"HyperDash":false},{"StartTime":54547.0,"Position":271.6893,"HyperDash":false}]},{"StartTime":54912.0,"Objects":[{"StartTime":54912.0,"Position":448.0,"HyperDash":false}]},{"StartTime":55094.0,"Objects":[{"StartTime":55094.0,"Position":360.0,"HyperDash":false}]},{"StartTime":55457.0,"Objects":[{"StartTime":55457.0,"Position":176.0,"HyperDash":false},{"StartTime":55529.0,"Position":150.758957,"HyperDash":false},{"StartTime":55638.0,"Position":160.310715,"HyperDash":false}]},{"StartTime":55821.0,"Objects":[{"StartTime":55821.0,"Position":272.0,"HyperDash":false},{"StartTime":55893.0,"Position":282.758942,"HyperDash":false},{"StartTime":56002.0,"Position":256.3107,"HyperDash":false}]},{"StartTime":56366.0,"Objects":[{"StartTime":56366.0,"Position":64.0,"HyperDash":false}]},{"StartTime":56548.0,"Objects":[{"StartTime":56548.0,"Position":160.0,"HyperDash":false}]},{"StartTime":56912.0,"Objects":[{"StartTime":56912.0,"Position":368.0,"HyperDash":false},{"StartTime":56984.0,"Position":383.241058,"HyperDash":false},{"StartTime":57093.0,"Position":383.6893,"HyperDash":false}]},{"StartTime":57275.0,"Objects":[{"StartTime":57275.0,"Position":264.0,"HyperDash":false},{"StartTime":57347.0,"Position":270.241058,"HyperDash":false},{"StartTime":57456.0,"Position":279.6893,"HyperDash":false}]},{"StartTime":57639.0,"Objects":[{"StartTime":57639.0,"Position":400.0,"HyperDash":false},{"StartTime":57711.0,"Position":367.1768,"HyperDash":false},{"StartTime":57820.0,"Position":320.0,"HyperDash":false}]},{"StartTime":58003.0,"Objects":[{"StartTime":58003.0,"Position":464.0,"HyperDash":false}]},{"StartTime":58185.0,"Objects":[{"StartTime":58185.0,"Position":320.0,"HyperDash":false}]},{"StartTime":58366.0,"Objects":[{"StartTime":58366.0,"Position":144.0,"HyperDash":false}]},{"StartTime":58548.0,"Objects":[{"StartTime":58548.0,"Position":224.0,"HyperDash":false},{"StartTime":58638.0,"Position":264.0,"HyperDash":false},{"StartTime":58729.0,"Position":224.0,"HyperDash":false}]},{"StartTime":58912.0,"Objects":[{"StartTime":58912.0,"Position":144.0,"HyperDash":false}]},{"StartTime":59094.0,"Objects":[{"StartTime":59094.0,"Position":16.0,"HyperDash":false},{"StartTime":59184.0,"Position":16.0,"HyperDash":false}]},{"StartTime":59275.0,"Objects":[{"StartTime":59275.0,"Position":64.0,"HyperDash":false}]},{"StartTime":59457.0,"Objects":[{"StartTime":59457.0,"Position":144.0,"HyperDash":false}]},{"StartTime":59639.0,"Objects":[{"StartTime":59639.0,"Position":64.0,"HyperDash":false}]},{"StartTime":59821.0,"Objects":[{"StartTime":59821.0,"Position":240.0,"HyperDash":false},{"StartTime":59911.0,"Position":240.0,"HyperDash":false}]},{"StartTime":60003.0,"Objects":[{"StartTime":60003.0,"Position":192.0,"HyperDash":false}]},{"StartTime":60185.0,"Objects":[{"StartTime":60185.0,"Position":80.0,"HyperDash":false}]},{"StartTime":60275.0,"Objects":[{"StartTime":60275.0,"Position":128.0,"HyperDash":false}]},{"StartTime":60366.0,"Objects":[{"StartTime":60366.0,"Position":176.0,"HyperDash":false}]},{"StartTime":60548.0,"Objects":[{"StartTime":60548.0,"Position":64.0,"HyperDash":false},{"StartTime":60638.0,"Position":64.0,"HyperDash":false}]},{"StartTime":60730.0,"Objects":[{"StartTime":60730.0,"Position":112.0,"HyperDash":false}]},{"StartTime":60912.0,"Objects":[{"StartTime":60912.0,"Position":224.0,"HyperDash":false},{"StartTime":60984.0,"Position":204.176788,"HyperDash":false},{"StartTime":61093.0,"Position":144.0,"HyperDash":false}]},{"StartTime":61275.0,"Objects":[{"StartTime":61275.0,"Position":320.0,"HyperDash":false}]},{"StartTime":61457.0,"Objects":[{"StartTime":61457.0,"Position":400.0,"HyperDash":false},{"StartTime":61547.0,"Position":400.0,"HyperDash":false}]},{"StartTime":61639.0,"Objects":[{"StartTime":61639.0,"Position":352.0,"HyperDash":false}]},{"StartTime":61821.0,"Objects":[{"StartTime":61821.0,"Position":432.0,"HyperDash":false}]},{"StartTime":62003.0,"Objects":[{"StartTime":62003.0,"Position":320.0,"HyperDash":false},{"StartTime":62093.0,"Position":320.0,"HyperDash":false}]},{"StartTime":62185.0,"Objects":[{"StartTime":62185.0,"Position":368.0,"HyperDash":false}]},{"StartTime":62366.0,"Objects":[{"StartTime":62366.0,"Position":448.0,"HyperDash":false}]},{"StartTime":62548.0,"Objects":[{"StartTime":62548.0,"Position":368.0,"HyperDash":false}]},{"StartTime":62730.0,"Objects":[{"StartTime":62730.0,"Position":192.0,"HyperDash":false}]},{"StartTime":62912.0,"Objects":[{"StartTime":62912.0,"Position":272.0,"HyperDash":false}]},{"StartTime":63094.0,"Objects":[{"StartTime":63094.0,"Position":192.0,"HyperDash":false},{"StartTime":63184.0,"Position":152.0,"HyperDash":false},{"StartTime":63275.0,"Position":192.0,"HyperDash":false}]},{"StartTime":63457.0,"Objects":[{"StartTime":63457.0,"Position":304.0,"HyperDash":false},{"StartTime":63529.0,"Position":274.1768,"HyperDash":false},{"StartTime":63638.0,"Position":224.0,"HyperDash":false}]},{"StartTime":63821.0,"Objects":[{"StartTime":63821.0,"Position":112.0,"HyperDash":false},{"StartTime":63893.0,"Position":144.823212,"HyperDash":false},{"StartTime":64002.0,"Position":192.0,"HyperDash":false}]},{"StartTime":64185.0,"Objects":[{"StartTime":64185.0,"Position":368.0,"HyperDash":false}]},{"StartTime":64366.0,"Objects":[{"StartTime":64366.0,"Position":288.0,"HyperDash":false},{"StartTime":64456.0,"Position":248.0,"HyperDash":false},{"StartTime":64547.0,"Position":288.0,"HyperDash":false}]},{"StartTime":64730.0,"Objects":[{"StartTime":64730.0,"Position":368.0,"HyperDash":false}]},{"StartTime":64912.0,"Objects":[{"StartTime":64912.0,"Position":448.0,"HyperDash":false}]},{"StartTime":65094.0,"Objects":[{"StartTime":65094.0,"Position":368.0,"HyperDash":false},{"StartTime":65184.0,"Position":328.0,"HyperDash":false},{"StartTime":65275.0,"Position":368.0,"HyperDash":false}]},{"StartTime":65457.0,"Objects":[{"StartTime":65457.0,"Position":448.0,"HyperDash":false}]},{"StartTime":65639.0,"Objects":[{"StartTime":65639.0,"Position":272.0,"HyperDash":false},{"StartTime":65729.0,"Position":272.0,"HyperDash":false}]},{"StartTime":65821.0,"Objects":[{"StartTime":65821.0,"Position":320.0,"HyperDash":false}]},{"StartTime":66003.0,"Objects":[{"StartTime":66003.0,"Position":432.0,"HyperDash":false}]},{"StartTime":66094.0,"Objects":[{"StartTime":66094.0,"Position":384.0,"HyperDash":false}]},{"StartTime":66185.0,"Objects":[{"StartTime":66185.0,"Position":336.0,"HyperDash":false}]},{"StartTime":66366.0,"Objects":[{"StartTime":66366.0,"Position":224.0,"HyperDash":false}]},{"StartTime":66457.0,"Objects":[{"StartTime":66457.0,"Position":272.0,"HyperDash":false}]},{"StartTime":66548.0,"Objects":[{"StartTime":66548.0,"Position":320.0,"HyperDash":false}]},{"StartTime":66730.0,"Objects":[{"StartTime":66730.0,"Position":432.0,"HyperDash":false}]},{"StartTime":66912.0,"Objects":[{"StartTime":66912.0,"Position":320.0,"HyperDash":false}]},{"StartTime":67094.0,"Objects":[{"StartTime":67094.0,"Position":144.0,"HyperDash":false}]},{"StartTime":67275.0,"Objects":[{"StartTime":67275.0,"Position":64.0,"HyperDash":false},{"StartTime":67365.0,"Position":64.0,"HyperDash":false}]},{"StartTime":67457.0,"Objects":[{"StartTime":67457.0,"Position":112.0,"HyperDash":false}]},{"StartTime":67639.0,"Objects":[{"StartTime":67639.0,"Position":192.0,"HyperDash":false}]},{"StartTime":67821.0,"Objects":[{"StartTime":67821.0,"Position":80.0,"HyperDash":false},{"StartTime":67911.0,"Position":92.64911,"HyperDash":false}]},{"StartTime":68003.0,"Objects":[{"StartTime":68003.0,"Position":144.0,"HyperDash":false}]},{"StartTime":68185.0,"Objects":[{"StartTime":68185.0,"Position":224.0,"HyperDash":false}]},{"StartTime":68366.0,"Objects":[{"StartTime":68366.0,"Position":144.0,"HyperDash":false}]},{"StartTime":68548.0,"Objects":[{"StartTime":68548.0,"Position":320.0,"HyperDash":false}]},{"StartTime":68730.0,"Objects":[{"StartTime":68730.0,"Position":400.0,"HyperDash":false},{"StartTime":68820.0,"Position":407.844635,"HyperDash":false}]},{"StartTime":69003.0,"Objects":[{"StartTime":69003.0,"Position":296.0,"HyperDash":false},{"StartTime":69093.0,"Position":256.0,"HyperDash":false}]},{"StartTime":69275.0,"Objects":[{"StartTime":69275.0,"Position":368.0,"HyperDash":false}]},{"StartTime":69457.0,"Objects":[{"StartTime":69457.0,"Position":256.0,"HyperDash":false}]},{"StartTime":69639.0,"Objects":[{"StartTime":69639.0,"Position":80.0,"HyperDash":false}]},{"StartTime":69821.0,"Objects":[{"StartTime":69821.0,"Position":192.0,"HyperDash":true}]},{"StartTime":70003.0,"Objects":[{"StartTime":70003.0,"Position":448.0,"HyperDash":false}]},{"StartTime":75821.0,"Objects":[{"StartTime":75821.0,"Position":160.0,"HyperDash":false},{"StartTime":75893.0,"Position":115.176788,"HyperDash":false},{"StartTime":76002.0,"Position":80.0,"HyperDash":false}]},{"StartTime":76185.0,"Objects":[{"StartTime":76185.0,"Position":160.0,"HyperDash":false},{"StartTime":76257.0,"Position":131.176788,"HyperDash":false},{"StartTime":76366.0,"Position":80.0,"HyperDash":false}]},{"StartTime":76548.0,"Objects":[{"StartTime":76548.0,"Position":160.0,"HyperDash":false}]},{"StartTime":76730.0,"Objects":[{"StartTime":76730.0,"Position":240.0,"HyperDash":false}]},{"StartTime":76912.0,"Objects":[{"StartTime":76912.0,"Position":240.0,"HyperDash":false}]},{"StartTime":77094.0,"Objects":[{"StartTime":77094.0,"Position":240.0,"HyperDash":false}]},{"StartTime":77275.0,"Objects":[{"StartTime":77275.0,"Position":368.0,"HyperDash":false},{"StartTime":77347.0,"Position":387.8232,"HyperDash":false},{"StartTime":77456.0,"Position":448.0,"HyperDash":false}]},{"StartTime":77639.0,"Objects":[{"StartTime":77639.0,"Position":368.0,"HyperDash":false},{"StartTime":77711.0,"Position":392.8232,"HyperDash":false},{"StartTime":77820.0,"Position":448.0,"HyperDash":false}]},{"StartTime":78003.0,"Objects":[{"StartTime":78003.0,"Position":352.0,"HyperDash":false}]},{"StartTime":78185.0,"Objects":[{"StartTime":78185.0,"Position":256.0,"HyperDash":false}]},{"StartTime":78366.0,"Objects":[{"StartTime":78366.0,"Position":256.0,"HyperDash":false}]},{"StartTime":78548.0,"Objects":[{"StartTime":78548.0,"Position":352.0,"HyperDash":false}]},{"StartTime":78730.0,"Objects":[{"StartTime":78730.0,"Position":176.0,"HyperDash":false},{"StartTime":78802.0,"Position":125.176788,"HyperDash":false},{"StartTime":78911.0,"Position":96.0,"HyperDash":false}]},{"StartTime":79094.0,"Objects":[{"StartTime":79094.0,"Position":176.0,"HyperDash":false},{"StartTime":79166.0,"Position":146.176788,"HyperDash":false},{"StartTime":79275.0,"Position":96.0,"HyperDash":false}]},{"StartTime":79457.0,"Objects":[{"StartTime":79457.0,"Position":192.0,"HyperDash":false}]},{"StartTime":79639.0,"Objects":[{"StartTime":79639.0,"Position":288.0,"HyperDash":false}]},{"StartTime":79821.0,"Objects":[{"StartTime":79821.0,"Position":192.0,"HyperDash":false}]},{"StartTime":80003.0,"Objects":[{"StartTime":80003.0,"Position":288.0,"HyperDash":false}]},{"StartTime":80185.0,"Objects":[{"StartTime":80185.0,"Position":194.0,"HyperDash":false},{"StartTime":80253.0,"Position":234.0,"HyperDash":false},{"StartTime":80321.0,"Position":179.0,"HyperDash":false},{"StartTime":80389.0,"Position":278.0,"HyperDash":false},{"StartTime":80457.0,"Position":474.0,"HyperDash":false},{"StartTime":80525.0,"Position":50.0,"HyperDash":false},{"StartTime":80593.0,"Position":458.0,"HyperDash":false},{"StartTime":80661.0,"Position":425.0,"HyperDash":false},{"StartTime":80730.0,"Position":466.0,"HyperDash":false},{"StartTime":80798.0,"Position":56.0,"HyperDash":false},{"StartTime":80866.0,"Position":109.0,"HyperDash":false},{"StartTime":80934.0,"Position":482.0,"HyperDash":false},{"StartTime":81002.0,"Position":147.0,"HyperDash":false},{"StartTime":81070.0,"Position":285.0,"HyperDash":false},{"StartTime":81138.0,"Position":452.0,"HyperDash":false},{"StartTime":81206.0,"Position":419.0,"HyperDash":false},{"StartTime":81275.0,"Position":269.0,"HyperDash":false}]},{"StartTime":81639.0,"Objects":[{"StartTime":81639.0,"Position":416.0,"HyperDash":false}]},{"StartTime":81821.0,"Objects":[{"StartTime":81821.0,"Position":336.0,"HyperDash":false},{"StartTime":81911.0,"Position":307.715729,"HyperDash":false},{"StartTime":82002.0,"Position":336.0,"HyperDash":false}]},{"StartTime":82185.0,"Objects":[{"StartTime":82185.0,"Position":448.0,"HyperDash":false}]},{"StartTime":82548.0,"Objects":[{"StartTime":82548.0,"Position":288.0,"HyperDash":false},{"StartTime":82638.0,"Position":259.715729,"HyperDash":false},{"StartTime":82729.0,"Position":288.0,"HyperDash":false}]},{"StartTime":82912.0,"Objects":[{"StartTime":82912.0,"Position":368.0,"HyperDash":false}]},{"StartTime":83094.0,"Objects":[{"StartTime":83094.0,"Position":192.0,"HyperDash":false}]},{"StartTime":83185.0,"Objects":[{"StartTime":83185.0,"Position":144.0,"HyperDash":false}]},{"StartTime":83275.0,"Objects":[{"StartTime":83275.0,"Position":192.0,"HyperDash":false}]},{"StartTime":83457.0,"Objects":[{"StartTime":83457.0,"Position":304.0,"HyperDash":false}]},{"StartTime":83548.0,"Objects":[{"StartTime":83548.0,"Position":352.0,"HyperDash":false},{"StartTime":83638.0,"Position":352.0,"HyperDash":false}]},{"StartTime":83821.0,"Objects":[{"StartTime":83821.0,"Position":272.0,"HyperDash":false}]},{"StartTime":84003.0,"Objects":[{"StartTime":84003.0,"Position":384.0,"HyperDash":false}]},{"StartTime":84185.0,"Objects":[{"StartTime":84185.0,"Position":304.0,"HyperDash":false},{"StartTime":84275.0,"Position":304.0,"HyperDash":false}]},{"StartTime":84366.0,"Objects":[{"StartTime":84366.0,"Position":352.0,"HyperDash":false}]},{"StartTime":84548.0,"Objects":[{"StartTime":84548.0,"Position":176.0,"HyperDash":false}]},{"StartTime":84730.0,"Objects":[{"StartTime":84730.0,"Position":256.0,"HyperDash":false},{"StartTime":84820.0,"Position":284.284271,"HyperDash":false},{"StartTime":84911.0,"Position":256.0,"HyperDash":false}]},{"StartTime":85094.0,"Objects":[{"StartTime":85094.0,"Position":144.0,"HyperDash":false}]},{"StartTime":85457.0,"Objects":[{"StartTime":85457.0,"Position":304.0,"HyperDash":false},{"StartTime":85547.0,"Position":332.284271,"HyperDash":false},{"StartTime":85638.0,"Position":304.0,"HyperDash":false}]},{"StartTime":85821.0,"Objects":[{"StartTime":85821.0,"Position":224.0,"HyperDash":false}]},{"StartTime":86003.0,"Objects":[{"StartTime":86003.0,"Position":400.0,"HyperDash":false}]},{"StartTime":86094.0,"Objects":[{"StartTime":86094.0,"Position":448.0,"HyperDash":false}]},{"StartTime":86185.0,"Objects":[{"StartTime":86185.0,"Position":400.0,"HyperDash":false}]},{"StartTime":86366.0,"Objects":[{"StartTime":86366.0,"Position":288.0,"HyperDash":false}]},{"StartTime":86457.0,"Objects":[{"StartTime":86457.0,"Position":240.0,"HyperDash":false},{"StartTime":86547.0,"Position":240.0,"HyperDash":false}]},{"StartTime":86730.0,"Objects":[{"StartTime":86730.0,"Position":352.0,"HyperDash":false}]},{"StartTime":86821.0,"Objects":[{"StartTime":86821.0,"Position":304.0,"HyperDash":false}]},{"StartTime":86912.0,"Objects":[{"StartTime":86912.0,"Position":256.0,"HyperDash":false}]},{"StartTime":87094.0,"Objects":[{"StartTime":87094.0,"Position":368.0,"HyperDash":false}]},{"StartTime":87185.0,"Objects":[{"StartTime":87185.0,"Position":432.0,"HyperDash":false}]},{"StartTime":87275.0,"Objects":[{"StartTime":87275.0,"Position":368.0,"HyperDash":false}]},{"StartTime":87457.0,"Objects":[{"StartTime":87457.0,"Position":192.0,"HyperDash":false}]},{"StartTime":87639.0,"Objects":[{"StartTime":87639.0,"Position":112.0,"HyperDash":false}]},{"StartTime":87730.0,"Objects":[{"StartTime":87730.0,"Position":160.0,"HyperDash":false}]},{"StartTime":87821.0,"Objects":[{"StartTime":87821.0,"Position":208.0,"HyperDash":false}]},{"StartTime":88003.0,"Objects":[{"StartTime":88003.0,"Position":96.0,"HyperDash":false},{"StartTime":88093.0,"Position":87.2201843,"HyperDash":false},{"StartTime":88184.0,"Position":80.35393,"HyperDash":false},{"StartTime":88257.0,"Position":98.3716049,"HyperDash":false},{"StartTime":88366.0,"Position":158.41568,"HyperDash":false}]},{"StartTime":88457.0,"Objects":[{"StartTime":88457.0,"Position":112.0,"HyperDash":false}]},{"StartTime":88548.0,"Objects":[{"StartTime":88548.0,"Position":64.0,"HyperDash":false}]},{"StartTime":88730.0,"Objects":[{"StartTime":88730.0,"Position":144.0,"HyperDash":false}]},{"StartTime":88912.0,"Objects":[{"StartTime":88912.0,"Position":320.0,"HyperDash":false}]},{"StartTime":89003.0,"Objects":[{"StartTime":89003.0,"Position":368.0,"HyperDash":false}]},{"StartTime":89094.0,"Objects":[{"StartTime":89094.0,"Position":320.0,"HyperDash":false}]},{"StartTime":89275.0,"Objects":[{"StartTime":89275.0,"Position":208.0,"HyperDash":false}]},{"StartTime":89366.0,"Objects":[{"StartTime":89366.0,"Position":160.0,"HyperDash":false},{"StartTime":89456.0,"Position":160.0,"HyperDash":false}]},{"StartTime":89639.0,"Objects":[{"StartTime":89639.0,"Position":240.0,"HyperDash":false}]},{"StartTime":89821.0,"Objects":[{"StartTime":89821.0,"Position":128.0,"HyperDash":false}]},{"StartTime":90003.0,"Objects":[{"StartTime":90003.0,"Position":208.0,"HyperDash":false},{"StartTime":90093.0,"Position":208.0,"HyperDash":false}]},{"StartTime":90185.0,"Objects":[{"StartTime":90185.0,"Position":160.0,"HyperDash":false}]},{"StartTime":90366.0,"Objects":[{"StartTime":90366.0,"Position":336.0,"HyperDash":false}]},{"StartTime":90548.0,"Objects":[{"StartTime":90548.0,"Position":416.0,"HyperDash":false}]},{"StartTime":90639.0,"Objects":[{"StartTime":90639.0,"Position":368.0,"HyperDash":false}]},{"StartTime":90730.0,"Objects":[{"StartTime":90730.0,"Position":320.0,"HyperDash":false}]},{"StartTime":90912.0,"Objects":[{"StartTime":90912.0,"Position":432.0,"HyperDash":false},{"StartTime":91002.0,"Position":446.779816,"HyperDash":false},{"StartTime":91093.0,"Position":447.646057,"HyperDash":false},{"StartTime":91166.0,"Position":416.6284,"HyperDash":false},{"StartTime":91275.0,"Position":369.58432,"HyperDash":false}]},{"StartTime":91366.0,"Objects":[{"StartTime":91366.0,"Position":416.0,"HyperDash":false}]},{"StartTime":91457.0,"Objects":[{"StartTime":91457.0,"Position":464.0,"HyperDash":false}]},{"StartTime":91639.0,"Objects":[{"StartTime":91639.0,"Position":384.0,"HyperDash":false}]},{"StartTime":91821.0,"Objects":[{"StartTime":91821.0,"Position":208.0,"HyperDash":false}]},{"StartTime":91912.0,"Objects":[{"StartTime":91912.0,"Position":160.0,"HyperDash":false}]},{"StartTime":92003.0,"Objects":[{"StartTime":92003.0,"Position":208.0,"HyperDash":false}]},{"StartTime":92185.0,"Objects":[{"StartTime":92185.0,"Position":320.0,"HyperDash":false}]},{"StartTime":92275.0,"Objects":[{"StartTime":92275.0,"Position":368.0,"HyperDash":false},{"StartTime":92365.0,"Position":368.0,"HyperDash":false}]},{"StartTime":92548.0,"Objects":[{"StartTime":92548.0,"Position":288.0,"HyperDash":false},{"StartTime":92638.0,"Position":241.366974,"HyperDash":false},{"StartTime":92729.0,"Position":208.293579,"HyperDash":false},{"StartTime":92820.0,"Position":262.779816,"HyperDash":false},{"StartTime":92911.0,"Position":287.8532,"HyperDash":false},{"StartTime":92984.0,"Position":304.0,"HyperDash":false},{"StartTime":93093.0,"Position":368.0,"HyperDash":true}]},{"StartTime":93275.0,"Objects":[{"StartTime":93275.0,"Position":112.0,"HyperDash":false}]},{"StartTime":93457.0,"Objects":[{"StartTime":93457.0,"Position":32.0,"HyperDash":false},{"StartTime":93547.0,"Position":32.0,"HyperDash":false}]},{"StartTime":93639.0,"Objects":[{"StartTime":93639.0,"Position":80.0,"HyperDash":false}]},{"StartTime":93821.0,"Objects":[{"StartTime":93821.0,"Position":192.0,"HyperDash":true}]},{"StartTime":94003.0,"Objects":[{"StartTime":94003.0,"Position":448.0,"HyperDash":false},{"StartTime":94075.0,"Position":436.241058,"HyperDash":false},{"StartTime":94184.0,"Position":463.6893,"HyperDash":false}]},{"StartTime":94275.0,"Objects":[{"StartTime":94275.0,"Position":416.0,"HyperDash":false}]},{"StartTime":94366.0,"Objects":[{"StartTime":94366.0,"Position":368.0,"HyperDash":false}]},{"StartTime":94548.0,"Objects":[{"StartTime":94548.0,"Position":448.0,"HyperDash":false}]},{"StartTime":94730.0,"Objects":[{"StartTime":94730.0,"Position":272.0,"HyperDash":false}]},{"StartTime":94912.0,"Objects":[{"StartTime":94912.0,"Position":192.0,"HyperDash":false},{"StartTime":95002.0,"Position":152.0,"HyperDash":false},{"StartTime":95093.0,"Position":192.0,"HyperDash":false}]},{"StartTime":95275.0,"Objects":[{"StartTime":95275.0,"Position":304.0,"HyperDash":true}]},{"StartTime":95457.0,"Objects":[{"StartTime":95457.0,"Position":48.0,"HyperDash":false},{"StartTime":95529.0,"Position":66.24104,"HyperDash":false},{"StartTime":95638.0,"Position":63.6892929,"HyperDash":false}]},{"StartTime":95821.0,"Objects":[{"StartTime":95821.0,"Position":176.0,"HyperDash":false},{"StartTime":95893.0,"Position":189.241043,"HyperDash":false},{"StartTime":96002.0,"Position":191.689285,"HyperDash":false}]},{"StartTime":96185.0,"Objects":[{"StartTime":96185.0,"Position":16.0,"HyperDash":false}]},{"StartTime":96366.0,"Objects":[{"StartTime":96366.0,"Position":96.0,"HyperDash":false},{"StartTime":96456.0,"Position":96.0,"HyperDash":false}]},{"StartTime":96548.0,"Objects":[{"StartTime":96548.0,"Position":48.0,"HyperDash":false}]},{"StartTime":96730.0,"Objects":[{"StartTime":96730.0,"Position":160.0,"HyperDash":true}]},{"StartTime":96912.0,"Objects":[{"StartTime":96912.0,"Position":416.0,"HyperDash":false},{"StartTime":96984.0,"Position":402.241058,"HyperDash":false},{"StartTime":97093.0,"Position":431.6893,"HyperDash":false}]},{"StartTime":97275.0,"Objects":[{"StartTime":97275.0,"Position":320.0,"HyperDash":false}]},{"StartTime":97366.0,"Objects":[{"StartTime":97366.0,"Position":272.0,"HyperDash":false},{"StartTime":97456.0,"Position":272.0,"HyperDash":false}]},{"StartTime":97639.0,"Objects":[{"StartTime":97639.0,"Position":448.0,"HyperDash":false}]},{"StartTime":97821.0,"Objects":[{"StartTime":97821.0,"Position":336.0,"HyperDash":false}]},{"StartTime":98003.0,"Objects":[{"StartTime":98003.0,"Position":448.0,"HyperDash":false}]},{"StartTime":98094.0,"Objects":[{"StartTime":98094.0,"Position":496.0,"HyperDash":false},{"StartTime":98184.0,"Position":496.0,"HyperDash":true}]},{"StartTime":98366.0,"Objects":[{"StartTime":98366.0,"Position":240.0,"HyperDash":false},{"StartTime":98438.0,"Position":199.221,"HyperDash":false},{"StartTime":98547.0,"Position":140.0,"HyperDash":false}]},{"StartTime":98730.0,"Objects":[{"StartTime":98730.0,"Position":240.0,"HyperDash":false},{"StartTime":98802.0,"Position":264.779,"HyperDash":false},{"StartTime":98911.0,"Position":340.0,"HyperDash":false}]},{"StartTime":99094.0,"Objects":[{"StartTime":99094.0,"Position":96.0,"HyperDash":false}]},{"StartTime":99275.0,"Objects":[{"StartTime":99275.0,"Position":16.0,"HyperDash":false},{"StartTime":99365.0,"Position":16.0,"HyperDash":false}]},{"StartTime":99457.0,"Objects":[{"StartTime":99457.0,"Position":64.0,"HyperDash":false}]},{"StartTime":99639.0,"Objects":[{"StartTime":99639.0,"Position":176.0,"HyperDash":true}]},{"StartTime":99821.0,"Objects":[{"StartTime":99821.0,"Position":432.0,"HyperDash":false},{"StartTime":99893.0,"Position":432.093933,"HyperDash":false},{"StartTime":100002.0,"Position":480.0,"HyperDash":false}]},{"StartTime":100094.0,"Objects":[{"StartTime":100094.0,"Position":480.0,"HyperDash":false}]},{"StartTime":100185.0,"Objects":[{"StartTime":100185.0,"Position":448.0,"HyperDash":false}]},{"StartTime":100366.0,"Objects":[{"StartTime":100366.0,"Position":368.0,"HyperDash":false}]},{"StartTime":100548.0,"Objects":[{"StartTime":100548.0,"Position":192.0,"HyperDash":false}]},{"StartTime":100730.0,"Objects":[{"StartTime":100730.0,"Position":272.0,"HyperDash":false},{"StartTime":100820.0,"Position":312.0,"HyperDash":false},{"StartTime":100911.0,"Position":272.0,"HyperDash":false}]},{"StartTime":101094.0,"Objects":[{"StartTime":101094.0,"Position":160.0,"HyperDash":true}]},{"StartTime":101275.0,"Objects":[{"StartTime":101275.0,"Position":416.0,"HyperDash":false},{"StartTime":101347.0,"Position":426.241058,"HyperDash":false},{"StartTime":101456.0,"Position":431.6893,"HyperDash":false}]},{"StartTime":101639.0,"Objects":[{"StartTime":101639.0,"Position":320.0,"HyperDash":false}]},{"StartTime":101821.0,"Objects":[{"StartTime":101821.0,"Position":432.0,"HyperDash":false}]},{"StartTime":102003.0,"Objects":[{"StartTime":102003.0,"Position":256.0,"HyperDash":false}]},{"StartTime":102185.0,"Objects":[{"StartTime":102185.0,"Position":176.0,"HyperDash":false},{"StartTime":102275.0,"Position":176.0,"HyperDash":false}]},{"StartTime":102366.0,"Objects":[{"StartTime":102366.0,"Position":224.0,"HyperDash":false}]},{"StartTime":102548.0,"Objects":[{"StartTime":102548.0,"Position":112.0,"HyperDash":true}]},{"StartTime":102730.0,"Objects":[{"StartTime":102730.0,"Position":368.0,"HyperDash":false},{"StartTime":102802.0,"Position":376.241058,"HyperDash":false},{"StartTime":102911.0,"Position":383.6893,"HyperDash":false}]},{"StartTime":103094.0,"Objects":[{"StartTime":103094.0,"Position":272.0,"HyperDash":false}]},{"StartTime":103185.0,"Objects":[{"StartTime":103185.0,"Position":224.0,"HyperDash":false}]},{"StartTime":103275.0,"Objects":[{"StartTime":103275.0,"Position":272.0,"HyperDash":false}]},{"StartTime":103457.0,"Objects":[{"StartTime":103457.0,"Position":384.0,"HyperDash":false}]},{"StartTime":103639.0,"Objects":[{"StartTime":103639.0,"Position":272.0,"HyperDash":false}]},{"StartTime":103821.0,"Objects":[{"StartTime":103821.0,"Position":384.0,"HyperDash":false}]},{"StartTime":103912.0,"Objects":[{"StartTime":103912.0,"Position":432.0,"HyperDash":false},{"StartTime":104002.0,"Position":432.0,"HyperDash":false}]},{"StartTime":104185.0,"Objects":[{"StartTime":104185.0,"Position":176.0,"HyperDash":false},{"StartTime":104257.0,"Position":161.176788,"HyperDash":false},{"StartTime":104366.0,"Position":96.0,"HyperDash":false}]},{"StartTime":104548.0,"Objects":[{"StartTime":104548.0,"Position":272.0,"HyperDash":false},{"StartTime":104620.0,"Position":314.8232,"HyperDash":false},{"StartTime":104729.0,"Position":352.0,"HyperDash":true}]},{"StartTime":104912.0,"Objects":[{"StartTime":104912.0,"Position":80.0,"HyperDash":false},{"StartTime":105002.0,"Position":80.0,"HyperDash":false}]},{"StartTime":105094.0,"Objects":[{"StartTime":105094.0,"Position":128.0,"HyperDash":false}]},{"StartTime":105275.0,"Objects":[{"StartTime":105275.0,"Position":304.0,"HyperDash":false},{"StartTime":105347.0,"Position":337.8232,"HyperDash":false},{"StartTime":105456.0,"Position":384.0,"HyperDash":false}]},{"StartTime":105639.0,"Objects":[{"StartTime":105639.0,"Position":128.0,"HyperDash":false}]},{"StartTime":105821.0,"Objects":[{"StartTime":105821.0,"Position":48.0,"HyperDash":false}]},{"StartTime":105912.0,"Objects":[{"StartTime":105912.0,"Position":128.0,"HyperDash":false}]},{"StartTime":106003.0,"Objects":[{"StartTime":106003.0,"Position":176.0,"HyperDash":false}]},{"StartTime":106185.0,"Objects":[{"StartTime":106185.0,"Position":64.0,"HyperDash":true}]},{"StartTime":106366.0,"Objects":[{"StartTime":106366.0,"Position":320.0,"HyperDash":false},{"StartTime":106456.0,"Position":360.0,"HyperDash":false},{"StartTime":106547.0,"Position":320.0,"HyperDash":false}]},{"StartTime":106730.0,"Objects":[{"StartTime":106730.0,"Position":144.0,"HyperDash":false}]},{"StartTime":106821.0,"Objects":[{"StartTime":106821.0,"Position":96.0,"HyperDash":false},{"StartTime":106911.0,"Position":96.0,"HyperDash":false}]},{"StartTime":107094.0,"Objects":[{"StartTime":107094.0,"Position":352.0,"HyperDash":false},{"StartTime":107166.0,"Position":366.241058,"HyperDash":false},{"StartTime":107275.0,"Position":367.6893,"HyperDash":false}]},{"StartTime":107366.0,"Objects":[{"StartTime":107366.0,"Position":320.0,"HyperDash":false}]},{"StartTime":107457.0,"Objects":[{"StartTime":107457.0,"Position":240.0,"HyperDash":false}]},{"StartTime":107639.0,"Objects":[{"StartTime":107639.0,"Position":352.0,"HyperDash":true}]},{"StartTime":107821.0,"Objects":[{"StartTime":107821.0,"Position":96.0,"HyperDash":false}]},{"StartTime":108003.0,"Objects":[{"StartTime":108003.0,"Position":16.0,"HyperDash":false},{"StartTime":108093.0,"Position":16.0,"HyperDash":false}]},{"StartTime":108185.0,"Objects":[{"StartTime":108185.0,"Position":96.0,"HyperDash":false}]},{"StartTime":108366.0,"Objects":[{"StartTime":108366.0,"Position":16.0,"HyperDash":true}]},{"StartTime":108548.0,"Objects":[{"StartTime":108548.0,"Position":272.0,"HyperDash":false}]},{"StartTime":108639.0,"Objects":[{"StartTime":108639.0,"Position":320.0,"HyperDash":false}]},{"StartTime":108730.0,"Objects":[{"StartTime":108730.0,"Position":272.0,"HyperDash":false}]},{"StartTime":108912.0,"Objects":[{"StartTime":108912.0,"Position":160.0,"HyperDash":false},{"StartTime":108984.0,"Position":184.823212,"HyperDash":false},{"StartTime":109093.0,"Position":240.0,"HyperDash":true}]},{"StartTime":109275.0,"Objects":[{"StartTime":109275.0,"Position":496.0,"HyperDash":false},{"StartTime":109365.0,"Position":496.0,"HyperDash":false}]},{"StartTime":109457.0,"Objects":[{"StartTime":109457.0,"Position":448.0,"HyperDash":false}]},{"StartTime":109639.0,"Objects":[{"StartTime":109639.0,"Position":272.0,"HyperDash":false},{"StartTime":109729.0,"Position":300.284271,"HyperDash":false}]},{"StartTime":109821.0,"Objects":[{"StartTime":109821.0,"Position":352.0,"HyperDash":true}]},{"StartTime":110003.0,"Objects":[{"StartTime":110003.0,"Position":96.0,"HyperDash":false}]},{"StartTime":110094.0,"Objects":[{"StartTime":110094.0,"Position":48.0,"HyperDash":false}]},{"StartTime":110185.0,"Objects":[{"StartTime":110185.0,"Position":96.0,"HyperDash":false}]},{"StartTime":110366.0,"Objects":[{"StartTime":110366.0,"Position":272.0,"HyperDash":false}]},{"StartTime":110457.0,"Objects":[{"StartTime":110457.0,"Position":224.0,"HyperDash":false}]},{"StartTime":110548.0,"Objects":[{"StartTime":110548.0,"Position":176.0,"HyperDash":true}]},{"StartTime":110730.0,"Objects":[{"StartTime":110730.0,"Position":448.0,"HyperDash":false},{"StartTime":110820.0,"Position":448.0,"HyperDash":false}]},{"StartTime":110912.0,"Objects":[{"StartTime":110912.0,"Position":400.0,"HyperDash":false}]},{"StartTime":111094.0,"Objects":[{"StartTime":111094.0,"Position":224.0,"HyperDash":false},{"StartTime":111166.0,"Position":193.176788,"HyperDash":false},{"StartTime":111275.0,"Position":144.0,"HyperDash":true}]},{"StartTime":111457.0,"Objects":[{"StartTime":111457.0,"Position":400.0,"HyperDash":false}]},{"StartTime":111639.0,"Objects":[{"StartTime":111639.0,"Position":480.0,"HyperDash":false}]},{"StartTime":111730.0,"Objects":[{"StartTime":111730.0,"Position":400.0,"HyperDash":false}]},{"StartTime":111821.0,"Objects":[{"StartTime":111821.0,"Position":352.0,"HyperDash":false}]},{"StartTime":112003.0,"Objects":[{"StartTime":112003.0,"Position":464.0,"HyperDash":true}]},{"StartTime":112185.0,"Objects":[{"StartTime":112185.0,"Position":208.0,"HyperDash":false},{"StartTime":112257.0,"Position":160.176788,"HyperDash":false},{"StartTime":112366.0,"Position":128.0,"HyperDash":false}]},{"StartTime":112548.0,"Objects":[{"StartTime":112548.0,"Position":304.0,"HyperDash":false},{"StartTime":112620.0,"Position":316.8232,"HyperDash":false},{"StartTime":112729.0,"Position":384.0,"HyperDash":false}]},{"StartTime":112912.0,"Objects":[{"StartTime":112912.0,"Position":128.0,"HyperDash":false},{"StartTime":112984.0,"Position":101.758957,"HyperDash":false},{"StartTime":113093.0,"Position":112.310707,"HyperDash":false}]},{"StartTime":113185.0,"Objects":[{"StartTime":113185.0,"Position":160.0,"HyperDash":false}]},{"StartTime":113275.0,"Objects":[{"StartTime":113275.0,"Position":240.0,"HyperDash":false}]},{"StartTime":113457.0,"Objects":[{"StartTime":113457.0,"Position":128.0,"HyperDash":true}]},{"StartTime":113639.0,"Objects":[{"StartTime":113639.0,"Position":384.0,"HyperDash":false}]},{"StartTime":113821.0,"Objects":[{"StartTime":113821.0,"Position":464.0,"HyperDash":false},{"StartTime":113911.0,"Position":464.0,"HyperDash":false}]},{"StartTime":114003.0,"Objects":[{"StartTime":114003.0,"Position":384.0,"HyperDash":false}]},{"StartTime":114185.0,"Objects":[{"StartTime":114185.0,"Position":464.0,"HyperDash":true}]},{"StartTime":114366.0,"Objects":[{"StartTime":114366.0,"Position":208.0,"HyperDash":false}]},{"StartTime":114548.0,"Objects":[{"StartTime":114548.0,"Position":128.0,"HyperDash":false}]},{"StartTime":114639.0,"Objects":[{"StartTime":114639.0,"Position":176.0,"HyperDash":false}]},{"StartTime":114730.0,"Objects":[{"StartTime":114730.0,"Position":256.0,"HyperDash":false}]},{"StartTime":114912.0,"Objects":[{"StartTime":114912.0,"Position":144.0,"HyperDash":true}]},{"StartTime":115094.0,"Objects":[{"StartTime":115094.0,"Position":400.0,"HyperDash":false}]},{"StartTime":115185.0,"Objects":[{"StartTime":115185.0,"Position":448.0,"HyperDash":false}]},{"StartTime":115275.0,"Objects":[{"StartTime":115275.0,"Position":400.0,"HyperDash":false}]},{"StartTime":115457.0,"Objects":[{"StartTime":115457.0,"Position":224.0,"HyperDash":false}]},{"StartTime":115548.0,"Objects":[{"StartTime":115548.0,"Position":176.0,"HyperDash":false},{"StartTime":115638.0,"Position":176.0,"HyperDash":false}]},{"StartTime":115821.0,"Objects":[{"StartTime":115821.0,"Position":432.0,"HyperDash":false},{"StartTime":115911.0,"Position":432.0,"HyperDash":false}]},{"StartTime":116003.0,"Objects":[{"StartTime":116003.0,"Position":384.0,"HyperDash":false}]},{"StartTime":116185.0,"Objects":[{"StartTime":116185.0,"Position":208.0,"HyperDash":false}]},{"StartTime":116275.0,"Objects":[{"StartTime":116275.0,"Position":256.0,"HyperDash":false}]},{"StartTime":116366.0,"Objects":[{"StartTime":116366.0,"Position":304.0,"HyperDash":true}]},{"StartTime":116548.0,"Objects":[{"StartTime":116548.0,"Position":48.0,"HyperDash":false}]},{"StartTime":116730.0,"Objects":[{"StartTime":116730.0,"Position":304.0,"HyperDash":false},{"StartTime":116815.0,"Position":221.0,"HyperDash":false},{"StartTime":116900.0,"Position":407.0,"HyperDash":false},{"StartTime":116985.0,"Position":287.0,"HyperDash":false},{"StartTime":117070.0,"Position":135.0,"HyperDash":false},{"StartTime":117156.0,"Position":437.0,"HyperDash":false},{"StartTime":117241.0,"Position":289.0,"HyperDash":false},{"StartTime":117326.0,"Position":464.0,"HyperDash":false},{"StartTime":117411.0,"Position":36.0,"HyperDash":false},{"StartTime":117496.0,"Position":378.0,"HyperDash":false},{"StartTime":117582.0,"Position":297.0,"HyperDash":false},{"StartTime":117667.0,"Position":418.0,"HyperDash":false},{"StartTime":117752.0,"Position":329.0,"HyperDash":false},{"StartTime":117837.0,"Position":338.0,"HyperDash":false},{"StartTime":117923.0,"Position":394.0,"HyperDash":false},{"StartTime":118008.0,"Position":40.0,"HyperDash":false},{"StartTime":118093.0,"Position":13.0,"HyperDash":false},{"StartTime":118178.0,"Position":80.0,"HyperDash":false},{"StartTime":118263.0,"Position":138.0,"HyperDash":false},{"StartTime":118349.0,"Position":311.0,"HyperDash":false},{"StartTime":118434.0,"Position":216.0,"HyperDash":false},{"StartTime":118519.0,"Position":310.0,"HyperDash":false},{"StartTime":118604.0,"Position":397.0,"HyperDash":false},{"StartTime":118690.0,"Position":214.0,"HyperDash":false},{"StartTime":118775.0,"Position":505.0,"HyperDash":false},{"StartTime":118860.0,"Position":173.0,"HyperDash":false},{"StartTime":118945.0,"Position":295.0,"HyperDash":false},{"StartTime":119030.0,"Position":199.0,"HyperDash":false},{"StartTime":119116.0,"Position":494.0,"HyperDash":false},{"StartTime":119201.0,"Position":293.0,"HyperDash":false},{"StartTime":119286.0,"Position":115.0,"HyperDash":false},{"StartTime":119371.0,"Position":412.0,"HyperDash":false},{"StartTime":119457.0,"Position":506.0,"HyperDash":false}]},{"StartTime":122366.0,"Objects":[{"StartTime":122366.0,"Position":312.0,"HyperDash":false},{"StartTime":122438.0,"Position":320.241058,"HyperDash":false},{"StartTime":122547.0,"Position":327.6893,"HyperDash":false}]},{"StartTime":122730.0,"Objects":[{"StartTime":122730.0,"Position":232.0,"HyperDash":false},{"StartTime":122802.0,"Position":233.241043,"HyperDash":false},{"StartTime":122911.0,"Position":247.689285,"HyperDash":false}]},{"StartTime":123275.0,"Objects":[{"StartTime":123275.0,"Position":408.0,"HyperDash":false}]},{"StartTime":123457.0,"Objects":[{"StartTime":123457.0,"Position":328.0,"HyperDash":false}]},{"StartTime":123821.0,"Objects":[{"StartTime":123821.0,"Position":168.0,"HyperDash":false},{"StartTime":123893.0,"Position":147.758957,"HyperDash":false},{"StartTime":124002.0,"Position":152.310715,"HyperDash":false}]},{"StartTime":124185.0,"Objects":[{"StartTime":124185.0,"Position":256.0,"HyperDash":false},{"StartTime":124257.0,"Position":241.758957,"HyperDash":false},{"StartTime":124366.0,"Position":240.310715,"HyperDash":false}]},{"StartTime":124730.0,"Objects":[{"StartTime":124730.0,"Position":64.0,"HyperDash":false}]},{"StartTime":124912.0,"Objects":[{"StartTime":124912.0,"Position":152.0,"HyperDash":false}]},{"StartTime":125275.0,"Objects":[{"StartTime":125275.0,"Position":336.0,"HyperDash":false},{"StartTime":125347.0,"Position":349.241058,"HyperDash":false},{"StartTime":125456.0,"Position":351.6893,"HyperDash":false}]},{"StartTime":125639.0,"Objects":[{"StartTime":125639.0,"Position":240.0,"HyperDash":false},{"StartTime":125711.0,"Position":260.241028,"HyperDash":false},{"StartTime":125820.0,"Position":255.689285,"HyperDash":false}]},{"StartTime":126185.0,"Objects":[{"StartTime":126185.0,"Position":448.0,"HyperDash":false}]},{"StartTime":126366.0,"Objects":[{"StartTime":126366.0,"Position":352.0,"HyperDash":false}]},{"StartTime":126730.0,"Objects":[{"StartTime":126730.0,"Position":144.0,"HyperDash":false},{"StartTime":126802.0,"Position":130.758957,"HyperDash":false},{"StartTime":126911.0,"Position":128.310715,"HyperDash":false}]},{"StartTime":127094.0,"Objects":[{"StartTime":127094.0,"Position":248.0,"HyperDash":false},{"StartTime":127166.0,"Position":256.758972,"HyperDash":false},{"StartTime":127275.0,"Position":232.310715,"HyperDash":false}]},{"StartTime":127457.0,"Objects":[{"StartTime":127457.0,"Position":112.0,"HyperDash":false},{"StartTime":127529.0,"Position":132.823212,"HyperDash":false},{"StartTime":127638.0,"Position":192.0,"HyperDash":false}]},{"StartTime":127821.0,"Objects":[{"StartTime":127821.0,"Position":48.0,"HyperDash":false}]},{"StartTime":128003.0,"Objects":[{"StartTime":128003.0,"Position":192.0,"HyperDash":false}]},{"StartTime":128185.0,"Objects":[{"StartTime":128185.0,"Position":368.0,"HyperDash":false}]},{"StartTime":128366.0,"Objects":[{"StartTime":128366.0,"Position":288.0,"HyperDash":false},{"StartTime":128456.0,"Position":248.0,"HyperDash":false},{"StartTime":128547.0,"Position":288.0,"HyperDash":false}]},{"StartTime":128730.0,"Objects":[{"StartTime":128730.0,"Position":368.0,"HyperDash":false}]},{"StartTime":128912.0,"Objects":[{"StartTime":128912.0,"Position":496.0,"HyperDash":false},{"StartTime":129002.0,"Position":496.0,"HyperDash":false}]},{"StartTime":129094.0,"Objects":[{"StartTime":129094.0,"Position":448.0,"HyperDash":false}]},{"StartTime":129275.0,"Objects":[{"StartTime":129275.0,"Position":368.0,"HyperDash":false}]},{"StartTime":129457.0,"Objects":[{"StartTime":129457.0,"Position":448.0,"HyperDash":false}]},{"StartTime":129639.0,"Objects":[{"StartTime":129639.0,"Position":272.0,"HyperDash":false},{"StartTime":129729.0,"Position":272.0,"HyperDash":false}]},{"StartTime":129821.0,"Objects":[{"StartTime":129821.0,"Position":320.0,"HyperDash":false}]},{"StartTime":130003.0,"Objects":[{"StartTime":130003.0,"Position":432.0,"HyperDash":false}]},{"StartTime":130094.0,"Objects":[{"StartTime":130094.0,"Position":384.0,"HyperDash":false}]},{"StartTime":130185.0,"Objects":[{"StartTime":130185.0,"Position":336.0,"HyperDash":false}]},{"StartTime":130366.0,"Objects":[{"StartTime":130366.0,"Position":448.0,"HyperDash":false},{"StartTime":130456.0,"Position":448.0,"HyperDash":false}]},{"StartTime":130548.0,"Objects":[{"StartTime":130548.0,"Position":400.0,"HyperDash":false}]},{"StartTime":130730.0,"Objects":[{"StartTime":130730.0,"Position":288.0,"HyperDash":false},{"StartTime":130802.0,"Position":307.8232,"HyperDash":false},{"StartTime":130911.0,"Position":368.0,"HyperDash":false}]},{"StartTime":131094.0,"Objects":[{"StartTime":131094.0,"Position":192.0,"HyperDash":false}]},{"StartTime":131275.0,"Objects":[{"StartTime":131275.0,"Position":112.0,"HyperDash":false},{"StartTime":131365.0,"Position":112.0,"HyperDash":false}]},{"StartTime":131457.0,"Objects":[{"StartTime":131457.0,"Position":160.0,"HyperDash":false}]},{"StartTime":131639.0,"Objects":[{"StartTime":131639.0,"Position":80.0,"HyperDash":false}]},{"StartTime":131821.0,"Objects":[{"StartTime":131821.0,"Position":192.0,"HyperDash":false},{"StartTime":131911.0,"Position":192.0,"HyperDash":false}]},{"StartTime":132003.0,"Objects":[{"StartTime":132003.0,"Position":144.0,"HyperDash":false}]},{"StartTime":132185.0,"Objects":[{"StartTime":132185.0,"Position":64.0,"HyperDash":false}]},{"StartTime":132366.0,"Objects":[{"StartTime":132366.0,"Position":144.0,"HyperDash":false}]},{"StartTime":132548.0,"Objects":[{"StartTime":132548.0,"Position":320.0,"HyperDash":false}]},{"StartTime":132730.0,"Objects":[{"StartTime":132730.0,"Position":240.0,"HyperDash":false}]},{"StartTime":132912.0,"Objects":[{"StartTime":132912.0,"Position":320.0,"HyperDash":false},{"StartTime":133002.0,"Position":360.0,"HyperDash":false},{"StartTime":133093.0,"Position":320.0,"HyperDash":false}]},{"StartTime":133275.0,"Objects":[{"StartTime":133275.0,"Position":208.0,"HyperDash":false},{"StartTime":133347.0,"Position":254.823212,"HyperDash":false},{"StartTime":133456.0,"Position":288.0,"HyperDash":false}]},{"StartTime":133639.0,"Objects":[{"StartTime":133639.0,"Position":400.0,"HyperDash":false},{"StartTime":133711.0,"Position":377.1768,"HyperDash":false},{"StartTime":133820.0,"Position":320.0,"HyperDash":false}]},{"StartTime":134003.0,"Objects":[{"StartTime":134003.0,"Position":144.0,"HyperDash":false}]},{"StartTime":134185.0,"Objects":[{"StartTime":134185.0,"Position":224.0,"HyperDash":false},{"StartTime":134275.0,"Position":264.0,"HyperDash":false},{"StartTime":134366.0,"Position":224.0,"HyperDash":false}]},{"StartTime":134548.0,"Objects":[{"StartTime":134548.0,"Position":144.0,"HyperDash":false}]},{"StartTime":134730.0,"Objects":[{"StartTime":134730.0,"Position":64.0,"HyperDash":false}]},{"StartTime":134912.0,"Objects":[{"StartTime":134912.0,"Position":144.0,"HyperDash":false},{"StartTime":135002.0,"Position":184.0,"HyperDash":false},{"StartTime":135093.0,"Position":144.0,"HyperDash":false}]},{"StartTime":135275.0,"Objects":[{"StartTime":135275.0,"Position":64.0,"HyperDash":false}]},{"StartTime":135457.0,"Objects":[{"StartTime":135457.0,"Position":240.0,"HyperDash":false},{"StartTime":135547.0,"Position":240.0,"HyperDash":false}]},{"StartTime":135639.0,"Objects":[{"StartTime":135639.0,"Position":192.0,"HyperDash":false}]},{"StartTime":135821.0,"Objects":[{"StartTime":135821.0,"Position":80.0,"HyperDash":false}]},{"StartTime":135912.0,"Objects":[{"StartTime":135912.0,"Position":128.0,"HyperDash":false}]},{"StartTime":136003.0,"Objects":[{"StartTime":136003.0,"Position":176.0,"HyperDash":false}]},{"StartTime":136185.0,"Objects":[{"StartTime":136185.0,"Position":288.0,"HyperDash":false}]},{"StartTime":136275.0,"Objects":[{"StartTime":136275.0,"Position":240.0,"HyperDash":false}]},{"StartTime":136366.0,"Objects":[{"StartTime":136366.0,"Position":192.0,"HyperDash":false}]},{"StartTime":136548.0,"Objects":[{"StartTime":136548.0,"Position":80.0,"HyperDash":false}]},{"StartTime":136730.0,"Objects":[{"StartTime":136730.0,"Position":192.0,"HyperDash":false}]},{"StartTime":136912.0,"Objects":[{"StartTime":136912.0,"Position":368.0,"HyperDash":false}]},{"StartTime":137094.0,"Objects":[{"StartTime":137094.0,"Position":448.0,"HyperDash":false},{"StartTime":137184.0,"Position":448.0,"HyperDash":false}]},{"StartTime":137275.0,"Objects":[{"StartTime":137275.0,"Position":400.0,"HyperDash":false}]},{"StartTime":137457.0,"Objects":[{"StartTime":137457.0,"Position":320.0,"HyperDash":false}]},{"StartTime":137639.0,"Objects":[{"StartTime":137639.0,"Position":432.0,"HyperDash":false},{"StartTime":137729.0,"Position":419.3509,"HyperDash":false}]},{"StartTime":137821.0,"Objects":[{"StartTime":137821.0,"Position":368.0,"HyperDash":false}]},{"StartTime":138003.0,"Objects":[{"StartTime":138003.0,"Position":288.0,"HyperDash":false}]},{"StartTime":138185.0,"Objects":[{"StartTime":138185.0,"Position":368.0,"HyperDash":false}]},{"StartTime":138366.0,"Objects":[{"StartTime":138366.0,"Position":192.0,"HyperDash":false}]},{"StartTime":138548.0,"Objects":[{"StartTime":138548.0,"Position":112.0,"HyperDash":false},{"StartTime":138638.0,"Position":104.155357,"HyperDash":false}]},{"StartTime":138821.0,"Objects":[{"StartTime":138821.0,"Position":216.0,"HyperDash":false},{"StartTime":138911.0,"Position":256.0,"HyperDash":false}]},{"StartTime":139094.0,"Objects":[{"StartTime":139094.0,"Position":144.0,"HyperDash":false}]},{"StartTime":139275.0,"Objects":[{"StartTime":139275.0,"Position":224.0,"HyperDash":false}]},{"StartTime":139457.0,"Objects":[{"StartTime":139457.0,"Position":48.0,"HyperDash":false}]},{"StartTime":139639.0,"Objects":[{"StartTime":139639.0,"Position":160.0,"HyperDash":true}]},{"StartTime":139821.0,"Objects":[{"StartTime":139821.0,"Position":416.0,"HyperDash":false}]},{"StartTime":140003.0,"Objects":[{"StartTime":140003.0,"Position":285.0,"HyperDash":false},{"StartTime":140056.0,"Position":17.0,"HyperDash":false},{"StartTime":140110.0,"Position":238.0,"HyperDash":false},{"StartTime":140164.0,"Position":222.0,"HyperDash":false},{"StartTime":140218.0,"Position":450.0,"HyperDash":false},{"StartTime":140272.0,"Position":67.0,"HyperDash":false},{"StartTime":140326.0,"Position":219.0,"HyperDash":false},{"StartTime":140380.0,"Position":307.0,"HyperDash":false},{"StartTime":140434.0,"Position":367.0,"HyperDash":false},{"StartTime":140488.0,"Position":412.0,"HyperDash":false},{"StartTime":140542.0,"Position":413.0,"HyperDash":false},{"StartTime":140596.0,"Position":143.0,"HyperDash":false},{"StartTime":140650.0,"Position":339.0,"HyperDash":false},{"StartTime":140704.0,"Position":342.0,"HyperDash":false},{"StartTime":140758.0,"Position":249.0,"HyperDash":false},{"StartTime":140812.0,"Position":235.0,"HyperDash":false},{"StartTime":140866.0,"Position":323.0,"HyperDash":false},{"StartTime":140920.0,"Position":365.0,"HyperDash":false},{"StartTime":140974.0,"Position":74.0,"HyperDash":false},{"StartTime":141028.0,"Position":281.0,"HyperDash":false},{"StartTime":141082.0,"Position":398.0,"HyperDash":false},{"StartTime":141136.0,"Position":335.0,"HyperDash":false},{"StartTime":141190.0,"Position":388.0,"HyperDash":false},{"StartTime":141244.0,"Position":228.0,"HyperDash":false},{"StartTime":141298.0,"Position":323.0,"HyperDash":false},{"StartTime":141352.0,"Position":441.0,"HyperDash":false},{"StartTime":141406.0,"Position":442.0,"HyperDash":false},{"StartTime":141460.0,"Position":278.0,"HyperDash":false},{"StartTime":141514.0,"Position":90.0,"HyperDash":false},{"StartTime":141568.0,"Position":409.0,"HyperDash":false},{"StartTime":141622.0,"Position":377.0,"HyperDash":false},{"StartTime":141676.0,"Position":457.0,"HyperDash":false},{"StartTime":141730.0,"Position":409.0,"HyperDash":false},{"StartTime":141783.0,"Position":43.0,"HyperDash":false},{"StartTime":141837.0,"Position":162.0,"HyperDash":false},{"StartTime":141891.0,"Position":341.0,"HyperDash":false},{"StartTime":141945.0,"Position":72.0,"HyperDash":false},{"StartTime":141999.0,"Position":135.0,"HyperDash":false},{"StartTime":142053.0,"Position":252.0,"HyperDash":false},{"StartTime":142107.0,"Position":446.0,"HyperDash":false},{"StartTime":142161.0,"Position":284.0,"HyperDash":false},{"StartTime":142215.0,"Position":70.0,"HyperDash":false},{"StartTime":142269.0,"Position":494.0,"HyperDash":false},{"StartTime":142323.0,"Position":463.0,"HyperDash":false},{"StartTime":142377.0,"Position":277.0,"HyperDash":false},{"StartTime":142431.0,"Position":425.0,"HyperDash":false},{"StartTime":142485.0,"Position":281.0,"HyperDash":false},{"StartTime":142539.0,"Position":3.0,"HyperDash":false},{"StartTime":142593.0,"Position":346.0,"HyperDash":false},{"StartTime":142647.0,"Position":350.0,"HyperDash":false},{"StartTime":142701.0,"Position":217.0,"HyperDash":false},{"StartTime":142755.0,"Position":455.0,"HyperDash":false},{"StartTime":142809.0,"Position":229.0,"HyperDash":false},{"StartTime":142863.0,"Position":51.0,"HyperDash":false},{"StartTime":142917.0,"Position":199.0,"HyperDash":false},{"StartTime":142971.0,"Position":208.0,"HyperDash":false},{"StartTime":143025.0,"Position":173.0,"HyperDash":false},{"StartTime":143079.0,"Position":367.0,"HyperDash":false},{"StartTime":143133.0,"Position":193.0,"HyperDash":false},{"StartTime":143187.0,"Position":488.0,"HyperDash":false},{"StartTime":143241.0,"Position":314.0,"HyperDash":false},{"StartTime":143295.0,"Position":135.0,"HyperDash":false},{"StartTime":143349.0,"Position":399.0,"HyperDash":false},{"StartTime":143403.0,"Position":404.0,"HyperDash":false},{"StartTime":143457.0,"Position":152.0,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3949367.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3949367.osu new file mode 100644 index 0000000000..19fab1c61c --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/3949367.osu @@ -0,0 +1,832 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 +Mode: 2 + +[Difficulty] +HPDrainRate:4 +CircleSize:3.5 +OverallDifficulty:8 +ApproachRate:8 +SliderMultiplier:1.6 +SliderTickRate:2 + +[Events] +//Background and Video events +//Break Periods +2,49839,51798 +2,70203,75071 +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Layer 4 (Overlay) +//Storyboard Sound Samples + +[TimingPoints] +185,363.636363636364,4,2,0,50,1,0 +7457,-100,4,2,0,55,0,0 +8912,-100,4,2,0,60,0,0 +10366,-100,4,2,0,65,0,0 +10730,-100,4,2,0,70,0,0 +11094,-100,4,2,0,75,0,0 +11457,-100,4,2,0,80,0,0 +11821,-100,4,1,1,80,0,0 +17821,-100,4,1,0,70,0,0 +17912,-100,4,1,1,80,0,0 +18003,-100,4,1,0,70,0,0 +18094,-100,4,1,1,80,0,0 +18366,-100,4,1,0,70,0,0 +18457,-100,4,1,1,80,0,0 +19275,-100,4,1,0,70,0,0 +19366,-100,4,1,1,80,0,0 +19457,-100,4,1,0,70,0,0 +19548,-100,4,1,1,80,0,0 +19821,-100,4,1,0,70,0,0 +19912,-100,4,1,1,80,0,0 +20639,-100,4,1,0,70,0,0 +21094,-100,4,1,1,80,0,0 +21185,-100,4,1,0,70,0,0 +21457,-100,4,1,1,80,0,0 +21548,-100,4,1,0,70,0,0 +21639,-100,4,1,1,80,0,0 +22094,-100,4,1,0,70,0,0 +22548,-100,4,1,1,80,0,0 +22639,-100,4,1,0,70,0,0 +22730,-100,4,2,0,60,0,0 +22912,-100,4,2,0,30,0,0 +23094,-100,4,2,0,60,0,0 +23276,-100,4,2,0,30,0,0 +23457,-100,4,3,1,80,0,0 +24185,-100,4,3,2,80,0,0 +24275,-100,4,3,1,80,0,0 +25275,-100,4,3,1,80,0,0 +25639,-100,4,3,2,80,0,0 +25730,-100,4,3,1,80,0,0 +26003,-100,4,3,1,80,0,0 +27094,-100,4,3,2,80,0,0 +27184,-100,4,3,1,80,0,0 +28548,-100,4,3,2,80,0,0 +28638,-100,4,3,1,80,0,0 +30003,-100,4,3,2,80,0,0 +30093,-100,4,3,1,80,0,0 +31094,-100,4,3,1,80,0,0 +31457,-100,4,3,2,80,0,0 +31548,-100,4,3,1,80,0,0 +31821,-100,4,3,1,80,0,0 +32185,-100,4,3,1,80,0,0 +32912,-100,4,3,2,80,0,0 +33002,-100,4,3,1,80,0,0 +34366,-100,4,3,2,80,0,0 +34456,-100,4,3,1,80,0,0 +35094,-100,4,3,3,90,0,1 +35821,-100,4,1,0,80,0,1 +35912,-100,4,3,3,90,0,1 +37275,-100,4,1,0,80,0,1 +37366,-100,4,3,3,90,0,1 +38730,-100,4,1,0,80,0,1 +38821,-100,4,3,3,90,0,1 +40185,-100,4,1,0,80,0,1 +40366,-100,4,3,3,90,0,1 +40457,-100,4,1,0,80,0,1 +40548,-100,4,3,3,90,0,1 +40639,-100,4,1,0,80,0,1 +40730,-100,4,3,3,90,0,1 +40821,-100,4,1,0,80,0,1 +40912,-100,4,3,3,90,0,1 +41639,-100,4,1,0,80,0,1 +41730,-100,4,3,3,90,0,1 +43093,-100,4,1,0,80,0,1 +43184,-100,4,3,3,90,0,1 +43457,-100,4,3,3,90,0,1 +43639,-100,4,3,3,90,0,1 +44548,-100,4,1,0,80,0,1 +44639,-100,4,3,3,90,0,1 +46003,-100,4,1,0,80,0,1 +46184,-100,4,3,3,90,0,1 +46275,-100,4,1,0,80,0,1 +46366,-100,4,3,3,90,0,1 +46457,-100,4,1,0,80,0,1 +46548,-100,4,3,3,90,0,1 +46639,-100,4,1,0,80,0,1 +46730,-100,4,1,2,80,0,0 +46912,-100,4,2,0,50,0,0 +51094,-100,4,2,0,55,0,0 +52548,-100,4,2,0,60,0,0 +54003,-100,4,2,0,65,0,0 +55457,-100,4,2,0,70,0,0 +56912,-100,4,2,0,75,0,0 +57639,-100,4,2,0,80,0,0 +58003,-100,4,2,0,85,0,0 +58366,-100,4,1,1,80,0,0 +58548,-100,4,2,1,60,0,0 +69275,-100,4,2,1,70,0,0 +69639,-100,4,2,1,80,0,0 +70003,-100,4,1,1,80,0,0 +70185,-100,4,2,1,50,0,0 +70548,-100,4,3,0,50,0,0 +71457,-100,4,2,1,50,0,0 +72003,-100,4,3,0,50,0,0 +72912,-100,4,2,1,60,0,0 +73457,-100,4,3,0,50,0,0 +74366,-100,4,2,1,60,0,0 +74912,-100,4,3,0,50,0,0 +75094,-100,4,2,1,60,0,0 +75275,-100,4,3,0,50,0,0 +75457,-100,4,2,1,60,0,0 +75639,-100,4,3,0,50,0,0 +75821,-100,4,2,1,70,0,0 +76003,-100,4,3,0,50,0,0 +76185,-100,4,2,1,70,0,0 +76366,-100,4,3,0,50,0,0 +76548,-100,4,2,1,70,0,0 +76730,-100,4,3,0,50,0,0 +76912,-100,4,2,1,70,0,0 +77094,-100,4,3,0,50,0,0 +77275,-100,4,2,1,70,0,0 +77457,-100,4,3,0,50,0,0 +77639,-100,4,2,1,70,0,0 +77820,-100,4,3,0,50,0,0 +78002,-100,4,2,1,70,0,0 +78184,-100,4,3,0,50,0,0 +78366,-100,4,2,1,70,0,0 +78548,-100,4,3,0,50,0,0 +78730,-100,4,2,1,75,0,0 +78912,-100,4,3,0,50,0,0 +79094,-100,4,2,1,75,0,0 +79275,-100,4,3,0,50,0,0 +79457,-100,4,2,1,75,0,0 +79639,-100,4,3,0,50,0,0 +79821,-100,4,2,1,75,0,0 +80003,-100,4,3,0,50,0,0 +80185,-100,4,2,1,75,0,0 +80367,-100,4,3,0,50,0,0 +80549,-100,4,2,1,75,0,0 +80730,-100,4,3,0,50,0,0 +80912,-100,4,2,1,80,0,0 +81094,-100,4,3,0,50,0,0 +81276,-100,4,2,1,80,0,0 +81458,-100,4,3,0,50,0,0 +81639,-100,4,1,1,80,0,0 +87639,-100,4,1,0,70,0,0 +87730,-100,4,1,1,80,0,0 +87821,-100,4,1,0,70,0,0 +87912,-100,4,1,1,80,0,0 +88184,-100,4,1,0,70,0,0 +88275,-100,4,1,1,80,0,0 +89093,-100,4,1,0,70,0,0 +89184,-100,4,1,1,80,0,0 +89275,-100,4,1,0,70,0,0 +89366,-100,4,1,1,80,0,0 +89639,-100,4,1,0,70,0,0 +89730,-100,4,1,1,80,0,0 +90457,-100,4,1,0,70,0,0 +90912,-100,4,1,1,80,0,0 +91003,-100,4,1,0,70,0,0 +91275,-100,4,1,1,80,0,0 +91366,-100,4,1,0,70,0,0 +91457,-100,4,1,1,80,0,0 +91912,-100,4,1,0,70,0,0 +92366,-100,4,1,1,80,0,0 +92457,-100,4,1,0,70,0,0 +92548,-100,4,2,0,60,0,0 +92594,-100,4,2,0,30,0,0 +92730,-100,4,2,0,60,0,0 +92776,-100,4,2,0,30,0,0 +92912,-100,4,2,0,60,0,0 +92958,-100,4,2,0,30,0,0 +93094,-100,4,2,0,60,0,0 +93140,-100,4,2,0,30,0,0 +93275,-100,4,3,1,80,0,0 +94003,-100,4,3,2,80,0,0 +94093,-100,4,3,1,80,0,0 +95094,-100,4,3,1,80,0,0 +95457,-100,4,3,2,80,0,0 +95548,-100,4,3,1,80,0,0 +95821,-100,4,3,1,80,0,0 +96912,-100,4,3,2,80,0,0 +97002,-100,4,3,1,80,0,0 +98366,-80,4,3,2,80,0,0 +98456,-80,4,3,1,80,0,0 +99094,-100,4,3,1,80,0,0 +99821,-100,4,3,2,80,0,0 +99911,-100,4,3,1,80,0,0 +100912,-100,4,3,1,80,0,0 +101275,-100,4,3,2,80,0,0 +101366,-100,4,3,1,80,0,0 +101639,-100,4,3,1,80,0,0 +102003,-100,4,3,1,80,0,0 +102730,-100,4,3,2,80,0,0 +102820,-100,4,3,1,80,0,0 +104184,-100,4,3,2,80,0,0 +104274,-100,4,3,1,80,0,0 +104912,-100,4,3,3,90,0,1 +105639,-100,4,1,0,80,0,1 +105730,-100,4,3,3,90,0,1 +107093,-100,4,1,0,80,0,1 +107184,-100,4,3,3,90,0,1 +108548,-100,4,1,0,80,0,1 +108639,-100,4,3,3,90,0,1 +110003,-100,4,1,0,80,0,1 +110184,-100,4,3,3,90,0,1 +110275,-100,4,1,0,80,0,1 +110366,-100,4,3,3,90,0,1 +110457,-100,4,1,0,80,0,1 +110548,-100,4,3,3,90,0,1 +110639,-100,4,1,0,80,0,1 +110730,-100,4,3,3,90,0,1 +111457,-100,4,1,0,80,0,1 +111548,-100,4,3,3,90,0,1 +112911,-100,4,1,0,80,0,1 +113002,-100,4,3,3,90,0,1 +113275,-100,4,3,3,90,0,1 +113457,-100,4,3,3,90,0,1 +114366,-100,4,1,0,80,0,1 +114457,-100,4,3,3,90,0,1 +115821,-100,4,1,0,80,0,1 +116002,-100,4,3,3,90,0,1 +116093,-100,4,1,0,80,0,1 +116184,-100,4,3,3,90,0,1 +116275,-100,4,1,0,80,0,1 +116366,-100,4,3,3,90,0,1 +116457,-100,4,1,0,80,0,1 +116548,-100,4,1,2,80,0,0 +116730,-100,4,2,0,50,0,0 +120912,-100,4,2,0,55,0,0 +122366,-100,4,2,0,60,0,0 +123821,-100,4,2,0,65,0,0 +125275,-100,4,2,0,70,0,0 +126730,-100,4,2,0,75,0,0 +127457,-100,4,2,0,80,0,0 +127821,-100,4,2,0,85,0,0 +128184,-100,4,1,1,80,0,0 +128366,-100,4,2,1,60,0,0 +139093,-100,4,2,1,70,0,0 +139457,-100,4,2,1,80,0,0 +139821,-100,4,1,1,80,0,0 +140003,-100,4,2,1,50,0,0 +140548,-100,4,2,1,45,0,0 +140912,-100,4,2,1,40,0,0 +141275,-100,4,2,1,35,0,0 +141639,-100,4,2,1,30,0,0 +142003,-100,4,2,1,25,0,0 +142366,-100,4,2,1,20,0,0 +142730,-100,4,2,1,15,0,0 +143094,-100,4,2,1,10,0,0 +143457,-100,4,2,1,5,0,0 + +[HitObjects] +64,192,6003,5,2,0:0:0:0: +192,192,6366,1,0,0:0:0:0: +64,192,6730,1,2,0:0:0:0: +192,192,7094,1,0,0:0:0:0: +320,192,7457,5,2,0:0:0:0: +192,192,7821,1,0,0:0:0:0: +320,192,8185,1,2,0:0:0:0: +192,192,8548,1,0,0:0:0:0: +320,192,8912,5,2,0:0:0:0: +448,192,9275,1,0,0:0:0:0: +320,192,9639,1,2,0:0:0:0: +448,192,10003,1,0,0:0:0:0: +256,192,10366,12,2,11457,0:0:0:0: +96,192,11821,5,14,0:0:0:0: +176,192,12003,2,0,L|208:160,2,40 +64,192,12366,1,2,0:0:0:0: +224,192,12730,2,0,L|252:220,2,40,2|0|2,0:0|0:0|0:0,0:0:0:0: +144,192,13094,1,2,0:0:0:0: +320,192,13275,5,10,0:0:0:0: +368,192,13366,1,0,0:0:0:0: +320,192,13457,1,0,0:0:0:0: +208,192,13639,1,0,0:0:0:0: +160,192,13730,2,0,L|160:144,1,40,0|2,0:0|0:0,0:0:0:0: +240,112,14003,1,0,0:0:0:0: +128,112,14185,1,2,0:0:0:0: +208,112,14366,2,0,L|208:160,1,40,2|0,0:0|0:0,0:0:0:0: +160,192,14548,1,2,0:0:0:0: +336,192,14730,5,10,0:0:0:0: +256,192,14912,2,0,L|224:224,2,40,0|0|0,0:0|0:0|0:0,0:0:0:0: +368,192,15275,1,2,0:0:0:0: +208,192,15639,2,0,L|180:164,2,40,2|0|2,0:0|0:0|0:0,0:0:0:0: +288,192,16003,1,2,0:0:0:0: +112,192,16185,5,10,0:0:0:0: +64,192,16275,1,0,0:0:0:0: +112,192,16366,1,0,0:0:0:0: +224,192,16548,1,0,0:0:0:0: +272,192,16639,2,0,L|272:240,1,40,0|2,0:0|0:0,0:0:0:0: +160,192,16912,1,0,0:0:0:0: +208,192,17003,1,0,0:0:0:0: +256,192,17094,1,2,0:0:0:0: +144,112,17275,1,2,0:0:0:0: +80,112,17366,1,0,0:0:0:0: +144,112,17457,1,2,0:0:0:0: +320,112,17639,5,10,0:0:0:0: +400,112,17821,1,0,0:0:0:0: +352,112,17912,1,0,0:0:0:0: +304,112,18003,1,0,0:0:0:0: +416,112,18185,2,0,B|432:192|432:192|352:192,1,160,2|2,0:0|0:0,0:0:0:0: +400,192,18639,1,0,0:0:0:0: +448,192,18730,1,2,0:0:0:0: +368,192,18912,1,2,0:0:0:0: +192,192,19094,5,10,0:0:0:0: +144,192,19185,1,0,0:0:0:0: +192,192,19275,1,0,0:0:0:0: +304,192,19457,1,0,0:0:0:0: +352,192,19548,2,0,L|352:240,1,40,0|2,0:0|0:0,0:0:0:0: +272,272,19821,1,0,0:0:0:0: +384,272,20003,1,2,0:0:0:0: +304,272,20185,2,0,L|304:224,1,40,2|0,0:0|0:0,0:0:0:0: +352,192,20366,1,2,0:0:0:0: +176,272,20548,5,10,0:0:0:0: +96,272,20730,1,0,0:0:0:0: +144,272,20821,1,0,0:0:0:0: +192,272,20912,1,0,0:0:0:0: +80,272,21094,2,0,B|64:192|64:192|144:192,1,160,2|2,0:0|0:0,0:0:0:0: +96,192,21548,1,0,0:0:0:0: +48,192,21639,1,2,0:0:0:0: +128,192,21821,1,2,0:0:0:0: +304,192,22003,5,10,0:0:0:0: +352,192,22094,1,0,0:0:0:0: +304,192,22185,1,0,0:0:0:0: +192,192,22366,1,0,0:0:0:0: +144,192,22457,2,0,L|144:144,1,40,0|2,0:0|0:0,0:0:0:0: +224,80,22730,6,0,B|144:80|144:80|224:80|224:80|144:80,1,240,2|2,0:0|0:0,0:0:0:0: +400,80,23457,5,12,0:0:0:0: +480,80,23639,2,0,L|480:128,1,40,2|0,0:0|0:0,0:0:0:0: +432,144,23821,1,2,0:0:0:0: +320,144,24003,1,8,0:0:0:0: +64,192,24185,2,0,L|48:112,1,80,10|2,0:0|0:0,0:0:0:0: +96,96,24457,1,0,0:0:0:0: +144,80,24548,1,0,0:0:0:0: +64,80,24730,1,2,0:0:0:0: +240,80,24912,5,8,0:0:0:0: +320,80,25094,2,0,L|368:80,2,40,2|0|2,0:0|0:0|0:0,0:0:0:0: +208,80,25457,1,8,0:0:0:0: +464,192,25639,2,0,L|448:112,1,80,8|2,0:0|2:0,0:0:0:0: +336,192,26003,2,0,L|320:112,1,80,2|2,0:0|2:0,0:0:0:0: +496,48,26366,5,8,0:0:0:0: +416,48,26548,2,0,L|416:96,1,40,2|0,0:0|0:0,0:0:0:0: +464,128,26730,1,2,0:0:0:0: +352,128,26912,1,8,0:0:0:0: +96,128,27094,2,0,L|80:48,1,80,10|2,0:0|0:0,0:0:0:0: +192,48,27457,1,0,0:0:0:0: +240,48,27548,2,0,L|240:96,1,40,0|2,0:0|0:0,0:0:0:0: +64,192,27821,5,8,0:0:0:0: +176,192,28003,1,2,0:0:0:0: +64,192,28185,1,2,0:0:0:0: +16,192,28275,2,0,L|16:144,1,40,0|8,0:0|0:0,0:0:0:0: +272,192,28548,2,0,L|352:192,1,80,10|10,0:0|0:0,0:0:0:0: +240,128,28912,2,0,L|160:128,1,80,8|10,0:0|0:0,0:0:0:0: +416,128,29275,5,12,0:0:0:0: +496,128,29457,2,0,L|496:80,1,40,2|0,0:0|0:0,0:0:0:0: +448,64,29639,1,2,0:0:0:0: +336,64,29821,1,8,0:0:0:0: +80,192,30003,2,0,L|32:128,1,80,10|2,0:0|0:0,0:0:0:0: +32,80,30275,1,0,0:0:0:0: +64,80,30366,1,0,0:0:0:0: +144,80,30548,1,2,0:0:0:0: +320,80,30730,5,8,0:0:0:0: +240,80,30912,2,0,L|192:80,2,40,2|0|2,0:0|0:0|0:0,0:0:0:0: +352,80,31275,1,8,0:0:0:0: +96,192,31457,2,0,L|80:112,1,80,8|2,0:0|2:0,0:0:0:0: +192,112,31821,1,2,0:0:0:0: +80,112,32003,1,2,2:0:0:0: +256,112,32185,5,8,0:0:0:0: +336,112,32366,2,0,L|336:160,1,40,2|0,0:0|0:0,0:0:0:0: +288,192,32548,1,2,0:0:0:0: +400,192,32730,1,8,0:0:0:0: +144,192,32912,2,0,L|128:112,1,80,10|2,0:0|0:0,0:0:0:0: +240,112,33275,1,0,0:0:0:0: +288,112,33366,1,0,0:0:0:0: +240,112,33457,1,2,0:0:0:0: +128,192,33639,5,8,0:0:0:0: +240,192,33821,1,2,0:0:0:0: +128,192,34003,1,2,0:0:0:0: +80,192,34094,2,0,L|80:144,1,40,0|8,0:0|0:0,0:0:0:0: +336,192,34366,2,0,L|416:192,1,80,10|0,0:0|0:0,0:0:0:0: +240,128,34730,2,0,L|160:128,1,80,10|0,0:0|0:0,0:0:0:0: +432,128,35094,6,0,L|432:80,1,40,12|0,0:0|0:0,0:0:0:0: +384,64,35275,1,2,0:0:0:0: +208,64,35457,2,0,L|128:64,1,80,8|0,0:0|0:0,0:0:0:0: +384,192,35821,1,8,0:0:0:0: +464,128,36003,1,2,0:0:0:0: +384,128,36094,1,2,0:0:0:0: +336,128,36185,1,8,0:0:0:0: +448,128,36366,1,2,0:0:0:0: +192,128,36548,6,0,L|144:128,2,40,8|0|2,0:0|0:0|0:0,0:0:0:0: +368,128,36912,1,8,0:0:0:0: +416,144,37003,2,0,L|416:192,1,40,0|2,0:0|0:0,0:0:0:0: +160,192,37275,2,0,L|144:112,1,80,8|2,0:0|0:0,0:0:0:0: +192,80,37548,1,2,0:0:0:0: +272,80,37639,1,8,0:0:0:0: +160,80,37821,1,2,0:0:0:0: +416,192,38003,5,8,0:0:0:0: +496,192,38185,2,0,L|496:144,1,40,2|0,0:0|0:0,0:0:0:0: +416,144,38366,1,8,0:0:0:0: +496,144,38548,1,0,0:0:0:0: +240,144,38730,1,8,0:0:0:0: +192,144,38821,1,0,0:0:0:0: +240,144,38912,1,2,0:0:0:0: +352,144,39094,2,0,L|272:144,1,80,8|2,0:0|0:0,0:0:0:0: +16,192,39457,6,0,L|16:144,1,40,8|0,0:0|0:0,0:0:0:0: +64,128,39639,1,2,0:0:0:0: +240,176,39821,2,0,L|208:144,1,40,8|0,0:0|0:0,0:0:0:0: +160,128,40003,1,2,0:0:0:0: +416,128,40185,1,8,0:0:0:0: +464,128,40275,1,0,0:0:0:0: +416,128,40366,1,2,0:0:0:0: +240,128,40548,1,8,0:0:0:0: +288,128,40639,1,0,0:0:0:0: +336,128,40730,1,2,0:0:0:0: +64,128,40912,6,0,L|64:80,1,40,12|0,0:0|0:0,0:0:0:0: +112,64,41094,1,2,0:0:0:0: +288,64,41275,2,0,L|368:64,1,80,8|0,0:0|0:0,0:0:0:0: +112,192,41639,1,8,0:0:0:0: +32,128,41821,1,2,0:0:0:0: +112,128,41912,1,2,0:0:0:0: +160,128,42003,1,8,0:0:0:0: +48,128,42185,1,2,0:0:0:0: +304,192,42366,6,0,L|384:192,1,80,8|2,0:0|0:0,0:0:0:0: +208,96,42730,2,0,L|128:96,1,80,8|2,0:0|0:0,0:0:0:0: +384,192,43094,2,0,L|400:272,1,80,8|2,0:0|0:0,0:0:0:0: +352,304,43366,1,2,0:0:0:0: +272,304,43457,1,8,0:0:0:0: +384,304,43639,1,2,0:0:0:0: +128,192,43821,5,8,0:0:0:0: +48,192,44003,2,0,L|48:144,1,40,2|0,0:0|0:0,0:0:0:0: +128,144,44185,1,8,0:0:0:0: +48,144,44366,1,2,0:0:0:0: +304,144,44548,1,8,0:0:0:0: +384,144,44730,1,2,0:0:0:0: +336,144,44821,1,0,0:0:0:0: +256,144,44912,1,8,0:0:0:0: +368,144,45094,1,2,0:0:0:0: +112,256,45275,5,8,0:0:0:0: +64,256,45366,1,0,0:0:0:0: +112,256,45457,1,2,0:0:0:0: +288,256,45639,1,8,0:0:0:0: +336,224,45730,2,0,L|336:176,1,40,0|2,0:0|0:0,0:0:0:0: +80,192,46003,2,0,L|80:152,1,40,8|0,0:0|0:0,0:0:0:0: +128,120,46185,1,2,0:0:0:0: +304,128,46366,1,8,0:0:0:0: +256,128,46457,1,0,0:0:0:0: +208,128,46548,1,2,0:0:0:0: +464,192,46730,5,12,0:0:0:0: +256,192,46912,12,2,49639,0:0:0:0: +200,192,52548,6,0,L|184:112,1,80,2|0,0:0|0:0,0:0:0:0: +280,192,52912,2,0,L|264:112,1,80,2|0,0:0|0:0,0:0:0:0: +104,112,53457,1,2,0:0:0:0: +184,113,53639,1,2,0:0:0:0: +344,192,54003,6,0,L|360:112,1,80,2|0,0:0|0:0,0:0:0:0: +256,192,54366,2,0,L|272:112,1,80,2|0,0:0|0:0,0:0:0:0: +448,112,54912,1,0,0:0:0:0: +360,112,55094,1,2,0:0:0:0: +176,192,55457,6,0,L|160:112,1,80,2|0,0:0|0:0,0:0:0:0: +272,192,55821,2,0,L|256:112,1,80,2|0,0:0|0:0,0:0:0:0: +64,112,56366,1,2,0:0:0:0: +160,112,56548,1,2,0:0:0:0: +368,192,56912,6,0,L|384:112,1,80,2|0,0:0|0:0,0:0:0:0: +264,192,57275,2,0,L|280:112,1,80,2|0,0:0|0:0,0:0:0:0: +400,112,57639,2,0,L|320:112,1,80,2|2,0:0|0:0,0:0:0:0: +464,112,58003,1,2,0:0:0:0: +320,112,58185,1,2,0:0:0:0: +144,112,58366,5,12,0:0:0:0: +224,112,58548,2,0,L|272:112,2,40,2|0|8,0:0|0:0|0:0,0:0:0:0: +144,112,58912,1,2,0:0:0:0: +16,192,59094,2,0,L|16:144,1,40,2|0,3:0|0:0,0:0:0:0: +64,112,59275,1,2,0:0:0:0: +144,112,59457,1,8,0:0:0:0: +64,112,59639,1,2,0:0:0:0: +240,192,59821,6,0,L|240:144,1,40,2|0,3:0|0:0,0:0:0:0: +192,128,60003,1,2,0:0:0:0: +80,192,60185,1,8,0:0:0:0: +128,192,60275,1,0,0:0:0:0: +176,192,60366,1,2,0:0:0:0: +64,192,60548,2,0,L|64:144,1,40,2|0,3:0|0:0,0:0:0:0: +112,128,60730,1,2,0:0:0:0: +224,128,60912,2,0,L|144:128,1,80,10|10,0:0|0:0,0:0:0:0: +320,128,61275,5,2,3:0:0:0: +400,128,61457,2,0,L|400:176,1,40,2|0,0:0|0:0,0:0:0:0: +352,192,61639,1,8,0:0:0:0: +432,192,61821,1,2,0:0:0:0: +320,192,62003,2,0,L|320:240,1,40,2|0,3:0|0:0,0:0:0:0: +368,272,62185,1,2,0:0:0:0: +448,272,62366,1,8,0:0:0:0: +368,272,62548,1,2,0:0:0:0: +192,272,62730,5,2,3:0:0:0: +272,272,62912,1,2,0:0:0:0: +192,272,63094,2,0,L|144:272,2,40,8|0|2,0:0|0:0|0:0,0:0:0:0: +304,192,63457,2,0,L|224:192,1,80,2|2,3:0|0:0,0:0:0:0: +112,112,63821,2,0,L|192:112,1,80,10|10,0:0|0:0,0:0:0:0: +368,112,64185,5,2,3:0:0:0: +288,112,64366,2,0,L|240:112,2,40,2|0|8,0:0|0:0|0:0,0:0:0:0: +368,112,64730,1,2,0:0:0:0: +448,192,64912,1,2,3:0:0:0: +368,192,65094,2,0,L|320:192,2,40,2|0|8,0:0|0:0|0:0,0:0:0:0: +448,192,65457,1,2,0:0:0:0: +272,192,65639,6,0,L|272:240,1,40,2|0,3:0|0:0,0:0:0:0: +320,256,65821,1,2,0:0:0:0: +432,192,66003,1,8,0:0:0:0: +384,192,66094,1,0,0:0:0:0: +336,192,66185,1,2,0:0:0:0: +224,112,66366,1,2,3:0:0:0: +272,112,66457,1,0,0:0:0:0: +320,112,66548,1,2,0:0:0:0: +432,112,66730,1,10,0:0:0:0: +320,112,66912,1,10,0:0:0:0: +144,112,67094,5,2,3:0:0:0: +64,112,67275,2,0,L|64:160,1,40,2|0,0:0|0:0,0:0:0:0: +112,176,67457,1,8,0:0:0:0: +192,176,67639,1,2,0:0:0:0: +80,176,67821,2,0,L|96:224,1,40,2|0,3:0|0:0,0:0:0:0: +144,256,68003,1,2,0:0:0:0: +224,256,68185,1,8,0:0:0:0: +144,256,68366,1,2,0:0:0:0: +320,192,68548,5,2,3:0:0:0: +400,112,68730,2,0,L|408:72,1,40,8|2,0:0|0:0,0:0:0:0: +296,64,69003,2,0,L|256:64,1,40,2|10,0:0|0:0,0:0:0:0: +368,192,69275,1,2,3:0:0:0: +256,192,69457,1,10,0:0:0:0: +80,192,69639,1,10,0:0:0:0: +192,192,69821,1,10,0:0:0:0: +448,192,70003,5,12,0:0:0:0: +160,192,75821,6,0,L|80:192,1,80,10|0,0:0|0:0,0:0:0:0: +160,112,76185,2,0,L|80:112,1,80,10|0,0:0|0:0,0:0:0:0: +160,112,76548,1,2,0:0:0:0: +240,112,76730,1,0,0:0:0:0: +240,112,76912,1,2,0:0:0:0: +240,112,77094,1,0,0:0:0:0: +368,192,77275,6,0,L|448:192,1,80,10|0,0:0|0:0,0:0:0:0: +368,112,77639,2,0,L|448:112,1,80,10|0,0:0|0:0,0:0:0:0: +352,112,78003,1,2,0:0:0:0: +256,112,78185,1,0,0:0:0:0: +256,208,78366,1,2,0:0:0:0: +352,208,78548,1,0,0:0:0:0: +176,192,78730,6,0,L|96:192,1,80,10|0,0:0|0:0,0:0:0:0: +176,112,79094,2,0,L|96:112,1,80,10|0,0:0|0:0,0:0:0:0: +192,112,79457,1,2,0:0:0:0: +288,112,79639,1,0,0:0:0:0: +192,208,79821,1,2,0:0:0:0: +288,208,80003,1,0,0:0:0:0: +256,192,80185,12,10,81275,0:0:0:0: +416,192,81639,5,14,0:0:0:0: +336,192,81821,2,0,L|304:160,2,40 +448,192,82185,1,2,0:0:0:0: +288,192,82548,2,0,L|260:220,2,40,2|0|2,0:0|0:0|0:0,0:0:0:0: +368,192,82912,1,2,0:0:0:0: +192,192,83094,5,10,0:0:0:0: +144,192,83185,1,0,0:0:0:0: +192,192,83275,1,0,0:0:0:0: +304,192,83457,1,0,0:0:0:0: +352,192,83548,2,0,L|352:144,1,40,0|2,0:0|0:0,0:0:0:0: +272,112,83821,1,0,0:0:0:0: +384,112,84003,1,2,0:0:0:0: +304,112,84185,2,0,L|304:160,1,40,2|0,0:0|0:0,0:0:0:0: +352,192,84366,1,2,0:0:0:0: +176,192,84548,5,10,0:0:0:0: +256,192,84730,2,0,L|288:224,2,40,0|0|0,0:0|0:0|0:0,0:0:0:0: +144,192,85094,1,2,0:0:0:0: +304,192,85457,2,0,L|332:164,2,40,2|0|2,0:0|0:0|0:0,0:0:0:0: +224,192,85821,1,2,0:0:0:0: +400,192,86003,5,10,0:0:0:0: +448,192,86094,1,0,0:0:0:0: +400,192,86185,1,0,0:0:0:0: +288,192,86366,1,0,0:0:0:0: +240,192,86457,2,0,L|240:240,1,40,0|2,0:0|0:0,0:0:0:0: +352,192,86730,1,0,0:0:0:0: +304,192,86821,1,0,0:0:0:0: +256,192,86912,1,2,0:0:0:0: +368,112,87094,1,2,0:0:0:0: +432,112,87185,1,0,0:0:0:0: +368,112,87275,1,2,0:0:0:0: +192,112,87457,5,10,0:0:0:0: +112,112,87639,1,0,0:0:0:0: +160,112,87730,1,0,0:0:0:0: +208,112,87821,1,0,0:0:0:0: +96,112,88003,2,0,B|80:192|80:192|160:192,1,160,2|2,0:0|0:0,0:0:0:0: +112,192,88457,1,0,0:0:0:0: +64,192,88548,1,2,0:0:0:0: +144,192,88730,1,2,0:0:0:0: +320,192,88912,5,10,0:0:0:0: +368,192,89003,1,0,0:0:0:0: +320,192,89094,1,0,0:0:0:0: +208,192,89275,1,0,0:0:0:0: +160,192,89366,2,0,L|160:240,1,40,0|2,0:0|0:0,0:0:0:0: +240,272,89639,1,0,0:0:0:0: +128,272,89821,1,2,0:0:0:0: +208,272,90003,2,0,L|208:224,1,40,2|0,0:0|0:0,0:0:0:0: +160,192,90185,1,2,0:0:0:0: +336,272,90366,5,10,0:0:0:0: +416,272,90548,1,0,0:0:0:0: +368,272,90639,1,0,0:0:0:0: +320,272,90730,1,0,0:0:0:0: +432,272,90912,2,0,B|448:192|448:192|368:192,1,160,2|2,0:0|0:0,0:0:0:0: +416,192,91366,1,0,0:0:0:0: +464,192,91457,1,2,0:0:0:0: +384,192,91639,1,2,0:0:0:0: +208,192,91821,5,10,0:0:0:0: +160,192,91912,1,0,0:0:0:0: +208,192,92003,1,0,0:0:0:0: +320,192,92185,1,0,0:0:0:0: +368,192,92275,2,0,L|368:144,1,40,0|2,0:0|0:0,0:0:0:0: +288,80,92548,6,0,B|208:80|208:80|288:80|288:80|368:80,1,240,2|2,0:0|0:0,0:0:0:0: +112,80,93275,5,12,0:0:0:0: +32,80,93457,2,0,L|32:128,1,40,2|0,0:0|0:0,0:0:0:0: +80,144,93639,1,2,0:0:0:0: +192,144,93821,1,8,0:0:0:0: +448,192,94003,2,0,L|464:112,1,80,10|2,0:0|0:0,0:0:0:0: +416,96,94275,1,0,0:0:0:0: +368,80,94366,1,0,0:0:0:0: +448,80,94548,1,2,0:0:0:0: +272,80,94730,5,8,0:0:0:0: +192,80,94912,2,0,L|144:80,2,40,2|0|2,0:0|0:0|0:0,0:0:0:0: +304,80,95275,1,8,0:0:0:0: +48,192,95457,2,0,L|64:112,1,80,8|2,0:0|2:0,0:0:0:0: +176,192,95821,2,0,L|192:112,1,80,2|2,0:0|2:0,0:0:0:0: +16,48,96185,5,8,0:0:0:0: +96,48,96366,2,0,L|96:96,1,40,2|0,0:0|0:0,0:0:0:0: +48,128,96548,1,2,0:0:0:0: +160,128,96730,1,8,0:0:0:0: +416,128,96912,2,0,L|432:48,1,80,10|2,0:0|0:0,0:0:0:0: +320,48,97275,1,0,0:0:0:0: +272,48,97366,2,0,L|272:96,1,40,0|2,0:0|0:0,0:0:0:0: +448,192,97639,5,8,0:0:0:0: +336,192,97821,1,2,0:0:0:0: +448,192,98003,1,2,0:0:0:0: +496,192,98094,2,0,L|496:144,1,40,0|8,0:0|0:0,0:0:0:0: +240,192,98366,2,0,L|128:192,1,100,10|10,0:0|0:0,0:0:0:0: +240,128,98730,2,0,L|352:128,1,100,8|10,0:0|0:0,0:0:0:0: +96,128,99094,5,12,0:0:0:0: +16,128,99275,2,0,L|16:80,1,40,2|0,0:0|0:0,0:0:0:0: +64,64,99457,1,2,0:0:0:0: +176,64,99639,1,8,0:0:0:0: +432,192,99821,2,0,L|480:128,1,80,10|2,0:0|0:0,0:0:0:0: +480,80,100094,1,0,0:0:0:0: +448,80,100185,1,0,0:0:0:0: +368,80,100366,1,2,0:0:0:0: +192,80,100548,5,8,0:0:0:0: +272,80,100730,2,0,L|320:80,2,40,2|0|2,0:0|0:0|0:0,0:0:0:0: +160,80,101094,1,8,0:0:0:0: +416,192,101275,2,0,L|432:112,1,80,8|2,0:0|2:0,0:0:0:0: +320,112,101639,1,2,0:0:0:0: +432,112,101821,1,2,2:0:0:0: +256,112,102003,5,8,0:0:0:0: +176,112,102185,2,0,L|176:160,1,40,2|0,0:0|0:0,0:0:0:0: +224,192,102366,1,2,0:0:0:0: +112,192,102548,1,8,0:0:0:0: +368,192,102730,2,0,L|384:112,1,80,10|2,0:0|0:0,0:0:0:0: +272,112,103094,1,0,0:0:0:0: +224,112,103185,1,0,0:0:0:0: +272,112,103275,1,2,0:0:0:0: +384,192,103457,5,8,0:0:0:0: +272,192,103639,1,2,0:0:0:0: +384,192,103821,1,2,0:0:0:0: +432,192,103912,2,0,L|432:144,1,40,0|8,0:0|0:0,0:0:0:0: +176,192,104185,2,0,L|96:192,1,80,10|0,0:0|0:0,0:0:0:0: +272,128,104548,2,0,L|352:128,1,80,10|0,0:0|0:0,0:0:0:0: +80,128,104912,6,0,L|80:80,1,40,12|0,0:0|0:0,0:0:0:0: +128,64,105094,1,2,0:0:0:0: +304,64,105275,2,0,L|384:64,1,80,8|0,0:0|0:0,0:0:0:0: +128,192,105639,1,8,0:0:0:0: +48,128,105821,1,2,0:0:0:0: +128,128,105912,1,2,0:0:0:0: +176,128,106003,1,8,0:0:0:0: +64,128,106185,1,2,0:0:0:0: +320,128,106366,6,0,L|368:128,2,40,8|0|2,0:0|0:0|0:0,0:0:0:0: +144,128,106730,1,8,0:0:0:0: +96,144,106821,2,0,L|96:192,1,40,0|2,0:0|0:0,0:0:0:0: +352,192,107094,2,0,L|368:112,1,80,8|2,0:0|0:0,0:0:0:0: +320,80,107366,1,2,0:0:0:0: +240,80,107457,1,8,0:0:0:0: +352,80,107639,1,2,0:0:0:0: +96,192,107821,5,8,0:0:0:0: +16,192,108003,2,0,L|16:144,1,40,2|0,0:0|0:0,0:0:0:0: +96,144,108185,1,8,0:0:0:0: +16,144,108366,1,0,0:0:0:0: +272,144,108548,1,8,0:0:0:0: +320,144,108639,1,0,0:0:0:0: +272,144,108730,1,2,0:0:0:0: +160,144,108912,2,0,L|240:144,1,80,8|2,0:0|0:0,0:0:0:0: +496,192,109275,6,0,L|496:144,1,40,8|0,0:0|0:0,0:0:0:0: +448,128,109457,1,2,0:0:0:0: +272,176,109639,2,0,L|304:144,1,40,8|0,0:0|0:0,0:0:0:0: +352,128,109821,1,2,0:0:0:0: +96,128,110003,1,8,0:0:0:0: +48,128,110094,1,0,0:0:0:0: +96,128,110185,1,2,0:0:0:0: +272,128,110366,1,8,0:0:0:0: +224,128,110457,1,0,0:0:0:0: +176,128,110548,1,2,0:0:0:0: +448,128,110730,6,0,L|448:80,1,40,12|0,0:0|0:0,0:0:0:0: +400,64,110912,1,2,0:0:0:0: +224,64,111094,2,0,L|144:64,1,80,8|0,0:0|0:0,0:0:0:0: +400,192,111457,1,8,0:0:0:0: +480,128,111639,1,2,0:0:0:0: +400,128,111730,1,2,0:0:0:0: +352,128,111821,1,8,0:0:0:0: +464,128,112003,1,2,0:0:0:0: +208,192,112185,6,0,L|128:192,1,80,8|2,0:0|0:0,0:0:0:0: +304,96,112548,2,0,L|384:96,1,80,8|2,0:0|0:0,0:0:0:0: +128,192,112912,2,0,L|112:272,1,80,8|2,0:0|0:0,0:0:0:0: +160,304,113185,1,2,0:0:0:0: +240,304,113275,1,8,0:0:0:0: +128,304,113457,1,2,0:0:0:0: +384,192,113639,5,8,0:0:0:0: +464,192,113821,2,0,L|464:144,1,40,2|0,0:0|0:0,0:0:0:0: +384,144,114003,1,8,0:0:0:0: +464,144,114185,1,2,0:0:0:0: +208,144,114366,1,8,0:0:0:0: +128,144,114548,1,2,0:0:0:0: +176,144,114639,1,0,0:0:0:0: +256,144,114730,1,8,0:0:0:0: +144,144,114912,1,2,0:0:0:0: +400,256,115094,5,8,0:0:0:0: +448,256,115185,1,0,0:0:0:0: +400,256,115275,1,2,0:0:0:0: +224,256,115457,1,8,0:0:0:0: +176,224,115548,2,0,L|176:176,1,40,0|2,0:0|0:0,0:0:0:0: +432,192,115821,2,0,L|432:152,1,40,8|0,0:0|0:0,0:0:0:0: +384,120,116003,1,2,0:0:0:0: +208,128,116185,1,8,0:0:0:0: +256,128,116275,1,0,0:0:0:0: +304,128,116366,1,2,0:0:0:0: +48,192,116548,5,12,0:0:0:0: +256,192,116730,12,2,119457,0:0:0:0: +312,192,122366,6,0,L|328:112,1,80,2|0,0:0|0:0,0:0:0:0: +232,192,122730,2,0,L|248:112,1,80,2|0,0:0|0:0,0:0:0:0: +408,112,123275,1,2,0:0:0:0: +328,113,123457,1,2,0:0:0:0: +168,192,123821,6,0,L|152:112,1,80,2|0,0:0|0:0,0:0:0:0: +256,192,124185,2,0,L|240:112,1,80,2|0,0:0|0:0,0:0:0:0: +64,112,124730,1,0,0:0:0:0: +152,112,124912,1,2,0:0:0:0: +336,192,125275,6,0,L|352:112,1,80,2|0,0:0|0:0,0:0:0:0: +240,192,125639,2,0,L|256:112,1,80,2|0,0:0|0:0,0:0:0:0: +448,112,126185,1,2,0:0:0:0: +352,112,126366,1,2,0:0:0:0: +144,192,126730,6,0,L|128:112,1,80,2|0,0:0|0:0,0:0:0:0: +248,192,127094,2,0,L|232:112,1,80,2|0,0:0|0:0,0:0:0:0: +112,112,127457,2,0,L|192:112,1,80,2|2,0:0|0:0,0:0:0:0: +48,112,127821,1,2,0:0:0:0: +192,112,128003,1,2,0:0:0:0: +368,112,128185,5,12,0:0:0:0: +288,112,128366,2,0,L|240:112,2,40,2|0|8,0:0|0:0|0:0,0:0:0:0: +368,112,128730,1,2,0:0:0:0: +496,192,128912,2,0,L|496:144,1,40,2|0,3:0|0:0,0:0:0:0: +448,112,129094,1,2,0:0:0:0: +368,112,129275,1,8,0:0:0:0: +448,112,129457,1,2,0:0:0:0: +272,192,129639,6,0,L|272:144,1,40,2|0,3:0|0:0,0:0:0:0: +320,128,129821,1,2,0:0:0:0: +432,192,130003,1,8,0:0:0:0: +384,192,130094,1,0,0:0:0:0: +336,192,130185,1,2,0:0:0:0: +448,192,130366,2,0,L|448:144,1,40,2|0,3:0|0:0,0:0:0:0: +400,128,130548,1,2,0:0:0:0: +288,128,130730,2,0,L|368:128,1,80,10|10,0:0|0:0,0:0:0:0: +192,128,131094,5,2,3:0:0:0: +112,128,131275,2,0,L|112:176,1,40,2|0,0:0|0:0,0:0:0:0: +160,192,131457,1,8,0:0:0:0: +80,192,131639,1,2,0:0:0:0: +192,192,131821,2,0,L|192:240,1,40,2|0,3:0|0:0,0:0:0:0: +144,272,132003,1,2,0:0:0:0: +64,272,132185,1,8,0:0:0:0: +144,272,132366,1,2,0:0:0:0: +320,272,132548,5,2,3:0:0:0: +240,272,132730,1,2,0:0:0:0: +320,272,132912,2,0,L|368:272,2,40,8|0|2,0:0|0:0|0:0,0:0:0:0: +208,192,133275,2,0,L|288:192,1,80,2|2,3:0|0:0,0:0:0:0: +400,112,133639,2,0,L|320:112,1,80,10|10,0:0|0:0,0:0:0:0: +144,112,134003,5,2,3:0:0:0: +224,112,134185,2,0,L|272:112,2,40,2|0|8,0:0|0:0|0:0,0:0:0:0: +144,112,134548,1,2,0:0:0:0: +64,192,134730,1,2,3:0:0:0: +144,192,134912,2,0,L|192:192,2,40,2|0|8,0:0|0:0|0:0,0:0:0:0: +64,192,135275,1,2,0:0:0:0: +240,192,135457,6,0,L|240:240,1,40,2|0,3:0|0:0,0:0:0:0: +192,256,135639,1,2,0:0:0:0: +80,192,135821,1,8,0:0:0:0: +128,192,135912,1,0,0:0:0:0: +176,192,136003,1,2,0:0:0:0: +288,112,136185,1,2,3:0:0:0: +240,112,136275,1,0,0:0:0:0: +192,112,136366,1,2,0:0:0:0: +80,112,136548,1,10,0:0:0:0: +192,112,136730,1,10,0:0:0:0: +368,112,136912,5,2,3:0:0:0: +448,112,137094,2,0,L|448:160,1,40,2|0,0:0|0:0,0:0:0:0: +400,176,137275,1,8,0:0:0:0: +320,176,137457,1,2,0:0:0:0: +432,176,137639,2,0,L|416:224,1,40,2|0,3:0|0:0,0:0:0:0: +368,256,137821,1,2,0:0:0:0: +288,256,138003,1,8,0:0:0:0: +368,256,138185,1,2,0:0:0:0: +192,192,138366,5,2,3:0:0:0: +112,112,138548,2,0,L|104:72,1,40,8|2,0:0|0:0,0:0:0:0: +216,64,138821,2,0,L|256:64,1,40,2|10,0:0|0:0,0:0:0:0: +144,192,139094,1,2,3:0:0:0: +224,192,139275,1,10,0:0:0:0: +48,192,139457,1,10,0:0:0:0: +160,192,139639,1,10,0:0:0:0: +416,192,139821,5,12,0:0:0:0: +256,192,140003,12,0,143457,0:0:0:0: diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/42587-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/42587-expected-conversion.json new file mode 100644 index 0000000000..42df40a57e --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/42587-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":24383.0,"Objects":[{"StartTime":24383.0,"Position":376.0,"HyperDash":false}]},{"StartTime":24478.0,"Objects":[{"StartTime":24478.0,"Position":392.0,"HyperDash":false}]},{"StartTime":24573.0,"Objects":[{"StartTime":24573.0,"Position":408.0,"HyperDash":false}]},{"StartTime":24763.0,"Objects":[{"StartTime":24763.0,"Position":448.0,"HyperDash":false},{"StartTime":24839.0,"Position":396.8095,"HyperDash":false},{"StartTime":24952.0,"Position":358.0,"HyperDash":false}]},{"StartTime":25143.0,"Objects":[{"StartTime":25143.0,"Position":280.0,"HyperDash":false}]},{"StartTime":25333.0,"Objects":[{"StartTime":25333.0,"Position":232.0,"HyperDash":false}]},{"StartTime":25523.0,"Objects":[{"StartTime":25523.0,"Position":152.0,"HyperDash":false},{"StartTime":25599.0,"Position":104.809525,"HyperDash":false},{"StartTime":25712.0,"Position":62.0,"HyperDash":false}]},{"StartTime":25902.0,"Objects":[{"StartTime":25902.0,"Position":32.0,"HyperDash":false}]},{"StartTime":26092.0,"Objects":[{"StartTime":26092.0,"Position":96.0,"HyperDash":false},{"StartTime":26186.0,"Position":119.750992,"HyperDash":false},{"StartTime":26281.0,"Position":168.644272,"HyperDash":false},{"StartTime":26376.0,"Position":215.703568,"HyperDash":false},{"StartTime":26471.0,"Position":244.0,"HyperDash":false},{"StartTime":26557.0,"Position":204.446625,"HyperDash":false},{"StartTime":26643.0,"Position":161.656128,"HyperDash":false},{"StartTime":26729.0,"Position":115.71936,"HyperDash":false},{"StartTime":26851.0,"Position":96.0,"HyperDash":false}]},{"StartTime":27042.0,"Objects":[{"StartTime":27042.0,"Position":96.0,"HyperDash":false}]},{"StartTime":27232.0,"Objects":[{"StartTime":27232.0,"Position":176.0,"HyperDash":false},{"StartTime":27308.0,"Position":204.190475,"HyperDash":false},{"StartTime":27421.0,"Position":266.0,"HyperDash":false}]},{"StartTime":27801.0,"Objects":[{"StartTime":27801.0,"Position":448.0,"HyperDash":false}]},{"StartTime":27991.0,"Objects":[{"StartTime":27991.0,"Position":360.0,"HyperDash":false}]},{"StartTime":28371.0,"Objects":[{"StartTime":28371.0,"Position":192.0,"HyperDash":false}]},{"StartTime":28561.0,"Objects":[{"StartTime":28561.0,"Position":280.0,"HyperDash":false}]},{"StartTime":28751.0,"Objects":[{"StartTime":28751.0,"Position":368.0,"HyperDash":false}]},{"StartTime":28940.0,"Objects":[{"StartTime":28940.0,"Position":456.0,"HyperDash":false}]},{"StartTime":29130.0,"Objects":[{"StartTime":29130.0,"Position":456.0,"HyperDash":false},{"StartTime":29224.0,"Position":417.249023,"HyperDash":false},{"StartTime":29319.0,"Position":394.3557,"HyperDash":false},{"StartTime":29414.0,"Position":367.296448,"HyperDash":false},{"StartTime":29509.0,"Position":308.0,"HyperDash":false},{"StartTime":29595.0,"Position":352.553375,"HyperDash":false},{"StartTime":29681.0,"Position":380.343872,"HyperDash":false},{"StartTime":29767.0,"Position":426.28064,"HyperDash":false},{"StartTime":29889.0,"Position":456.0,"HyperDash":false}]},{"StartTime":30080.0,"Objects":[{"StartTime":30080.0,"Position":456.0,"HyperDash":false}]},{"StartTime":30270.0,"Objects":[{"StartTime":30270.0,"Position":376.0,"HyperDash":false},{"StartTime":30346.0,"Position":320.8095,"HyperDash":false},{"StartTime":30459.0,"Position":286.0,"HyperDash":false}]},{"StartTime":30839.0,"Objects":[{"StartTime":30839.0,"Position":112.0,"HyperDash":false}]},{"StartTime":31029.0,"Objects":[{"StartTime":31029.0,"Position":176.0,"HyperDash":false}]},{"StartTime":31219.0,"Objects":[{"StartTime":31219.0,"Position":112.0,"HyperDash":false}]},{"StartTime":31314.0,"Objects":[{"StartTime":31314.0,"Position":112.0,"HyperDash":false}]},{"StartTime":31409.0,"Objects":[{"StartTime":31409.0,"Position":112.0,"HyperDash":false}]},{"StartTime":31599.0,"Objects":[{"StartTime":31599.0,"Position":176.0,"HyperDash":false}]},{"StartTime":31788.0,"Objects":[{"StartTime":31788.0,"Position":240.0,"HyperDash":false}]},{"StartTime":31978.0,"Objects":[{"StartTime":31978.0,"Position":176.0,"HyperDash":false}]},{"StartTime":32168.0,"Objects":[{"StartTime":32168.0,"Position":240.0,"HyperDash":false},{"StartTime":32262.0,"Position":264.85144,"HyperDash":false},{"StartTime":32357.0,"Position":329.8879,"HyperDash":false},{"StartTime":32452.0,"Position":373.9472,"HyperDash":false},{"StartTime":32547.0,"Position":402.243652,"HyperDash":false},{"StartTime":32633.0,"Position":374.690277,"HyperDash":false},{"StartTime":32719.0,"Position":312.89978,"HyperDash":false},{"StartTime":32805.0,"Position":266.934845,"HyperDash":false},{"StartTime":32927.0,"Position":240.0,"HyperDash":false}]},{"StartTime":33118.0,"Objects":[{"StartTime":33118.0,"Position":240.0,"HyperDash":false}]},{"StartTime":33307.0,"Objects":[{"StartTime":33307.0,"Position":328.0,"HyperDash":false},{"StartTime":33383.0,"Position":341.0,"HyperDash":false},{"StartTime":33496.0,"Position":298.93808,"HyperDash":false}]},{"StartTime":33877.0,"Objects":[{"StartTime":33877.0,"Position":136.0,"HyperDash":false}]},{"StartTime":34067.0,"Objects":[{"StartTime":34067.0,"Position":80.0,"HyperDash":false}]},{"StartTime":34257.0,"Objects":[{"StartTime":34257.0,"Position":24.0,"HyperDash":false}]},{"StartTime":34352.0,"Objects":[{"StartTime":34352.0,"Position":24.0,"HyperDash":false}]},{"StartTime":34447.0,"Objects":[{"StartTime":34447.0,"Position":24.0,"HyperDash":false}]},{"StartTime":34542.0,"Objects":[{"StartTime":34542.0,"Position":40.0,"HyperDash":false}]},{"StartTime":34637.0,"Objects":[{"StartTime":34637.0,"Position":56.0,"HyperDash":false}]},{"StartTime":34826.0,"Objects":[{"StartTime":34826.0,"Position":144.0,"HyperDash":false}]},{"StartTime":35016.0,"Objects":[{"StartTime":35016.0,"Position":232.0,"HyperDash":false}]},{"StartTime":35206.0,"Objects":[{"StartTime":35206.0,"Position":376.0,"HyperDash":false},{"StartTime":35300.0,"Position":413.2887,"HyperDash":false},{"StartTime":35395.0,"Position":455.196533,"HyperDash":false},{"StartTime":35472.0,"Position":429.631622,"HyperDash":false},{"StartTime":35585.0,"Position":376.0,"HyperDash":false}]},{"StartTime":35776.0,"Objects":[{"StartTime":35776.0,"Position":232.0,"HyperDash":false},{"StartTime":35852.0,"Position":181.590073,"HyperDash":false},{"StartTime":35965.0,"Position":152.803467,"HyperDash":false}]},{"StartTime":36156.0,"Objects":[{"StartTime":36156.0,"Position":304.0,"HyperDash":false}]},{"StartTime":36345.0,"Objects":[{"StartTime":36345.0,"Position":304.0,"HyperDash":false},{"StartTime":36421.0,"Position":300.0,"HyperDash":false},{"StartTime":36534.0,"Position":304.0,"HyperDash":false}]},{"StartTime":36725.0,"Objects":[{"StartTime":36725.0,"Position":112.0,"HyperDash":false},{"StartTime":36801.0,"Position":78.63026,"HyperDash":false},{"StartTime":36914.0,"Position":31.5015564,"HyperDash":false}]},{"StartTime":37105.0,"Objects":[{"StartTime":37105.0,"Position":112.0,"HyperDash":false},{"StartTime":37181.0,"Position":93.63026,"HyperDash":false},{"StartTime":37294.0,"Position":31.5015564,"HyperDash":false}]},{"StartTime":37485.0,"Objects":[{"StartTime":37485.0,"Position":112.0,"HyperDash":false}]},{"StartTime":37675.0,"Objects":[{"StartTime":37675.0,"Position":112.0,"HyperDash":false},{"StartTime":37769.0,"Position":57.8074036,"HyperDash":false},{"StartTime":37864.0,"Position":32.9893951,"HyperDash":false},{"StartTime":37941.0,"Position":45.885498,"HyperDash":false},{"StartTime":38054.0,"Position":112.0,"HyperDash":false}]},{"StartTime":38244.0,"Objects":[{"StartTime":38244.0,"Position":112.0,"HyperDash":false}]},{"StartTime":38434.0,"Objects":[{"StartTime":38434.0,"Position":32.0,"HyperDash":false}]},{"StartTime":38624.0,"Objects":[{"StartTime":38624.0,"Position":112.0,"HyperDash":false}]},{"StartTime":38814.0,"Objects":[{"StartTime":38814.0,"Position":32.0,"HyperDash":false}]},{"StartTime":39004.0,"Objects":[{"StartTime":39004.0,"Position":112.0,"HyperDash":false}]},{"StartTime":39194.0,"Objects":[{"StartTime":39194.0,"Position":200.0,"HyperDash":false},{"StartTime":39270.0,"Position":220.190475,"HyperDash":false},{"StartTime":39383.0,"Position":290.0,"HyperDash":false}]},{"StartTime":39573.0,"Objects":[{"StartTime":39573.0,"Position":384.0,"HyperDash":false},{"StartTime":39649.0,"Position":360.8095,"HyperDash":false},{"StartTime":39762.0,"Position":294.0,"HyperDash":false}]},{"StartTime":39953.0,"Objects":[{"StartTime":39953.0,"Position":200.0,"HyperDash":false},{"StartTime":40047.0,"Position":261.604553,"HyperDash":false},{"StartTime":40142.0,"Position":290.0,"HyperDash":false},{"StartTime":40237.0,"Position":244.237259,"HyperDash":false},{"StartTime":40332.0,"Position":200.0,"HyperDash":false},{"StartTime":40409.0,"Position":229.379608,"HyperDash":false},{"StartTime":40522.0,"Position":290.0,"HyperDash":false}]},{"StartTime":40713.0,"Objects":[{"StartTime":40713.0,"Position":408.0,"HyperDash":false}]},{"StartTime":40902.0,"Objects":[{"StartTime":40902.0,"Position":360.0,"HyperDash":false}]},{"StartTime":41092.0,"Objects":[{"StartTime":41092.0,"Position":280.0,"HyperDash":false},{"StartTime":41177.0,"Position":240.0683,"HyperDash":false},{"StartTime":41263.0,"Position":219.442886,"HyperDash":false},{"StartTime":41349.0,"Position":153.743317,"HyperDash":false},{"StartTime":41471.0,"Position":103.528549,"HyperDash":false}]},{"StartTime":41662.0,"Objects":[{"StartTime":41662.0,"Position":168.0,"HyperDash":false},{"StartTime":41756.0,"Position":130.327,"HyperDash":false},{"StartTime":41851.0,"Position":85.20778,"HyperDash":false},{"StartTime":41928.0,"Position":130.082031,"HyperDash":false},{"StartTime":42041.0,"Position":168.0,"HyperDash":false}]},{"StartTime":42232.0,"Objects":[{"StartTime":42232.0,"Position":264.0,"HyperDash":false},{"StartTime":42317.0,"Position":321.196838,"HyperDash":false},{"StartTime":42403.0,"Position":360.600128,"HyperDash":false},{"StartTime":42489.0,"Position":394.9759,"HyperDash":false},{"StartTime":42611.0,"Position":423.349854,"HyperDash":false}]},{"StartTime":42801.0,"Objects":[{"StartTime":42801.0,"Position":320.0,"HyperDash":false},{"StartTime":42877.0,"Position":322.0243,"HyperDash":false},{"StartTime":42990.0,"Position":282.757751,"HyperDash":false}]},{"StartTime":43181.0,"Objects":[{"StartTime":43181.0,"Position":184.0,"HyperDash":false},{"StartTime":43257.0,"Position":210.32988,"HyperDash":false},{"StartTime":43370.0,"Position":227.0967,"HyperDash":false}]},{"StartTime":43561.0,"Objects":[{"StartTime":43561.0,"Position":227.0,"HyperDash":false}]},{"StartTime":43751.0,"Objects":[{"StartTime":43751.0,"Position":192.0,"HyperDash":false},{"StartTime":43845.0,"Position":152.03096,"HyperDash":false},{"StartTime":43940.0,"Position":145.695374,"HyperDash":false},{"StartTime":44017.0,"Position":169.388275,"HyperDash":false},{"StartTime":44130.0,"Position":192.0,"HyperDash":false}]},{"StartTime":44320.0,"Objects":[{"StartTime":44320.0,"Position":128.0,"HyperDash":false},{"StartTime":44405.0,"Position":142.777267,"HyperDash":false},{"StartTime":44491.0,"Position":173.576416,"HyperDash":false},{"StartTime":44577.0,"Position":244.930679,"HyperDash":false},{"StartTime":44699.0,"Position":284.40564,"HyperDash":false}]},{"StartTime":44890.0,"Objects":[{"StartTime":44890.0,"Position":376.0,"HyperDash":false}]},{"StartTime":45270.0,"Objects":[{"StartTime":45270.0,"Position":440.0,"HyperDash":false}]},{"StartTime":45459.0,"Objects":[{"StartTime":45459.0,"Position":384.0,"HyperDash":false}]},{"StartTime":45649.0,"Objects":[{"StartTime":45649.0,"Position":304.0,"HyperDash":false}]},{"StartTime":45839.0,"Objects":[{"StartTime":45839.0,"Position":216.0,"HyperDash":false},{"StartTime":45924.0,"Position":169.327576,"HyperDash":false},{"StartTime":46010.0,"Position":156.035263,"HyperDash":false},{"StartTime":46096.0,"Position":110.220467,"HyperDash":false},{"StartTime":46218.0,"Position":68.60332,"HyperDash":false}]},{"StartTime":46409.0,"Objects":[{"StartTime":46409.0,"Position":56.0,"HyperDash":false}]},{"StartTime":46788.0,"Objects":[{"StartTime":46788.0,"Position":216.0,"HyperDash":false}]},{"StartTime":46978.0,"Objects":[{"StartTime":46978.0,"Position":296.0,"HyperDash":false}]},{"StartTime":47168.0,"Objects":[{"StartTime":47168.0,"Position":216.0,"HyperDash":false}]},{"StartTime":47358.0,"Objects":[{"StartTime":47358.0,"Position":296.0,"HyperDash":false}]},{"StartTime":47738.0,"Objects":[{"StartTime":47738.0,"Position":136.0,"HyperDash":false}]},{"StartTime":48118.0,"Objects":[{"StartTime":48118.0,"Position":376.0,"HyperDash":false}]},{"StartTime":48497.0,"Objects":[{"StartTime":48497.0,"Position":136.0,"HyperDash":false}]},{"StartTime":48877.0,"Objects":[{"StartTime":48877.0,"Position":376.0,"HyperDash":false},{"StartTime":48953.0,"Position":329.8095,"HyperDash":false},{"StartTime":49066.0,"Position":286.0,"HyperDash":false}]},{"StartTime":49257.0,"Objects":[{"StartTime":49257.0,"Position":192.0,"HyperDash":false}]},{"StartTime":49447.0,"Objects":[{"StartTime":49447.0,"Position":128.0,"HyperDash":false}]},{"StartTime":49637.0,"Objects":[{"StartTime":49637.0,"Position":216.0,"HyperDash":false},{"StartTime":49731.0,"Position":261.604553,"HyperDash":false},{"StartTime":49826.0,"Position":306.0,"HyperDash":false},{"StartTime":49921.0,"Position":251.237244,"HyperDash":false},{"StartTime":50016.0,"Position":216.0,"HyperDash":false},{"StartTime":50093.0,"Position":252.379608,"HyperDash":false},{"StartTime":50206.0,"Position":306.0,"HyperDash":false}]},{"StartTime":50396.0,"Objects":[{"StartTime":50396.0,"Position":400.0,"HyperDash":false},{"StartTime":50481.0,"Position":405.0538,"HyperDash":false},{"StartTime":50567.0,"Position":419.1245,"HyperDash":false},{"StartTime":50653.0,"Position":441.139374,"HyperDash":false},{"StartTime":50775.0,"Position":411.5338,"HyperDash":false}]},{"StartTime":50966.0,"Objects":[{"StartTime":50966.0,"Position":336.0,"HyperDash":false},{"StartTime":51042.0,"Position":300.082825,"HyperDash":false},{"StartTime":51155.0,"Position":279.0086,"HyperDash":false}]},{"StartTime":51345.0,"Objects":[{"StartTime":51345.0,"Position":208.0,"HyperDash":false}]},{"StartTime":51535.0,"Objects":[{"StartTime":51535.0,"Position":168.0,"HyperDash":false}]},{"StartTime":51725.0,"Objects":[{"StartTime":51725.0,"Position":120.0,"HyperDash":false}]},{"StartTime":51915.0,"Objects":[{"StartTime":51915.0,"Position":72.0,"HyperDash":false},{"StartTime":52000.0,"Position":46.90651,"HyperDash":false},{"StartTime":52086.0,"Position":66.65258,"HyperDash":false},{"StartTime":52172.0,"Position":76.80537,"HyperDash":false},{"StartTime":52294.0,"Position":126.392281,"HyperDash":false}]},{"StartTime":52485.0,"Objects":[{"StartTime":52485.0,"Position":216.0,"HyperDash":false}]},{"StartTime":52675.0,"Objects":[{"StartTime":52675.0,"Position":304.0,"HyperDash":false}]},{"StartTime":52864.0,"Objects":[{"StartTime":52864.0,"Position":232.0,"HyperDash":false}]},{"StartTime":53054.0,"Objects":[{"StartTime":53054.0,"Position":312.0,"HyperDash":false}]},{"StartTime":53244.0,"Objects":[{"StartTime":53244.0,"Position":288.0,"HyperDash":false},{"StartTime":53329.0,"Position":335.2697,"HyperDash":false},{"StartTime":53415.0,"Position":365.515228,"HyperDash":false},{"StartTime":53501.0,"Position":421.4718,"HyperDash":false},{"StartTime":53623.0,"Position":449.9475,"HyperDash":false}]},{"StartTime":53814.0,"Objects":[{"StartTime":53814.0,"Position":392.0,"HyperDash":false},{"StartTime":53890.0,"Position":357.8421,"HyperDash":false},{"StartTime":54003.0,"Position":349.331024,"HyperDash":false}]},{"StartTime":54194.0,"Objects":[{"StartTime":54194.0,"Position":280.0,"HyperDash":false},{"StartTime":54270.0,"Position":256.0476,"HyperDash":false},{"StartTime":54383.0,"Position":208.0,"HyperDash":false}]},{"StartTime":54573.0,"Objects":[{"StartTime":54573.0,"Position":176.0,"HyperDash":false}]},{"StartTime":54763.0,"Objects":[{"StartTime":54763.0,"Position":104.0,"HyperDash":false},{"StartTime":54829.0,"Position":110.83445,"HyperDash":false},{"StartTime":54896.0,"Position":93.97989,"HyperDash":false},{"StartTime":54962.0,"Position":92.00793,"HyperDash":false},{"StartTime":55029.0,"Position":91.28976,"HyperDash":false},{"StartTime":55096.0,"Position":107.652794,"HyperDash":false},{"StartTime":55162.0,"Position":137.152725,"HyperDash":false},{"StartTime":55229.0,"Position":132.523422,"HyperDash":false},{"StartTime":55332.0,"Position":193.99971,"HyperDash":false}]},{"StartTime":55523.0,"Objects":[{"StartTime":55523.0,"Position":216.0,"HyperDash":false}]},{"StartTime":55713.0,"Objects":[{"StartTime":55713.0,"Position":264.0,"HyperDash":false}]},{"StartTime":55902.0,"Objects":[{"StartTime":55902.0,"Position":352.0,"HyperDash":false}]},{"StartTime":56092.0,"Objects":[{"StartTime":56092.0,"Position":440.0,"HyperDash":false}]},{"StartTime":56282.0,"Objects":[{"StartTime":56282.0,"Position":352.0,"HyperDash":false}]},{"StartTime":56472.0,"Objects":[{"StartTime":56472.0,"Position":264.0,"HyperDash":false},{"StartTime":56538.0,"Position":236.1824,"HyperDash":false},{"StartTime":56605.0,"Position":201.573,"HyperDash":false},{"StartTime":56671.0,"Position":187.143539,"HyperDash":false},{"StartTime":56738.0,"Position":159.746231,"HyperDash":false},{"StartTime":56805.0,"Position":123.075737,"HyperDash":false},{"StartTime":56871.0,"Position":78.82073,"HyperDash":false},{"StartTime":56938.0,"Position":49.782032,"HyperDash":false},{"StartTime":57041.0,"Position":18.8103733,"HyperDash":false}]},{"StartTime":57421.0,"Objects":[{"StartTime":57421.0,"Position":160.0,"HyperDash":false}]},{"StartTime":57611.0,"Objects":[{"StartTime":57611.0,"Position":32.0,"HyperDash":false}]},{"StartTime":57801.0,"Objects":[{"StartTime":57801.0,"Position":160.0,"HyperDash":false}]},{"StartTime":57991.0,"Objects":[{"StartTime":57991.0,"Position":248.0,"HyperDash":false},{"StartTime":58057.0,"Position":279.8176,"HyperDash":false},{"StartTime":58124.0,"Position":314.427,"HyperDash":false},{"StartTime":58190.0,"Position":336.856445,"HyperDash":false},{"StartTime":58257.0,"Position":373.253754,"HyperDash":false},{"StartTime":58324.0,"Position":394.924255,"HyperDash":false},{"StartTime":58390.0,"Position":404.17926,"HyperDash":false},{"StartTime":58457.0,"Position":435.217957,"HyperDash":false},{"StartTime":58560.0,"Position":493.189636,"HyperDash":false}]},{"StartTime":58940.0,"Objects":[{"StartTime":58940.0,"Position":360.0,"HyperDash":false}]},{"StartTime":59130.0,"Objects":[{"StartTime":59130.0,"Position":256.0,"HyperDash":false}]},{"StartTime":59320.0,"Objects":[{"StartTime":59320.0,"Position":152.0,"HyperDash":false}]},{"StartTime":59510.0,"Objects":[{"StartTime":59510.0,"Position":168.0,"HyperDash":false},{"StartTime":59604.0,"Position":213.268082,"HyperDash":false},{"StartTime":59699.0,"Position":263.009766,"HyperDash":false},{"StartTime":59794.0,"Position":292.74585,"HyperDash":false},{"StartTime":59889.0,"Position":343.451233,"HyperDash":false},{"StartTime":59984.0,"Position":303.3675,"HyperDash":false},{"StartTime":60079.0,"Position":253.641876,"HyperDash":false},{"StartTime":60174.0,"Position":227.887085,"HyperDash":false},{"StartTime":60269.0,"Position":168.0,"HyperDash":false},{"StartTime":60355.0,"Position":189.407471,"HyperDash":false},{"StartTime":60441.0,"Position":257.7932,"HyperDash":false},{"StartTime":60527.0,"Position":268.439819,"HyperDash":false},{"StartTime":60649.0,"Position":343.451233,"HyperDash":false}]},{"StartTime":60839.0,"Objects":[{"StartTime":60839.0,"Position":408.0,"HyperDash":false}]},{"StartTime":60934.0,"Objects":[{"StartTime":60934.0,"Position":408.0,"HyperDash":false}]},{"StartTime":61029.0,"Objects":[{"StartTime":61029.0,"Position":408.0,"HyperDash":false},{"StartTime":61114.0,"Position":391.84967,"HyperDash":false},{"StartTime":61200.0,"Position":372.427,"HyperDash":false},{"StartTime":61286.0,"Position":329.0043,"HyperDash":false},{"StartTime":61408.0,"Position":304.776764,"HyperDash":false}]},{"StartTime":61599.0,"Objects":[{"StartTime":61599.0,"Position":304.0,"HyperDash":false}]},{"StartTime":61788.0,"Objects":[{"StartTime":61788.0,"Position":216.0,"HyperDash":false},{"StartTime":61873.0,"Position":231.980789,"HyperDash":false},{"StartTime":61959.0,"Position":262.282928,"HyperDash":false},{"StartTime":62045.0,"Position":291.630219,"HyperDash":false},{"StartTime":62167.0,"Position":318.820038,"HyperDash":false}]},{"StartTime":62358.0,"Objects":[{"StartTime":62358.0,"Position":319.0,"HyperDash":false}]},{"StartTime":62548.0,"Objects":[{"StartTime":62548.0,"Position":240.0,"HyperDash":false},{"StartTime":62642.0,"Position":251.786285,"HyperDash":false},{"StartTime":62737.0,"Position":294.0,"HyperDash":false},{"StartTime":62814.0,"Position":280.200531,"HyperDash":false},{"StartTime":62927.0,"Position":240.0,"HyperDash":false}]},{"StartTime":63118.0,"Objects":[{"StartTime":63118.0,"Position":192.0,"HyperDash":false},{"StartTime":63203.0,"Position":179.84967,"HyperDash":false},{"StartTime":63289.0,"Position":137.426987,"HyperDash":false},{"StartTime":63375.0,"Position":126.0043,"HyperDash":false},{"StartTime":63497.0,"Position":88.77678,"HyperDash":false}]},{"StartTime":63687.0,"Objects":[{"StartTime":63687.0,"Position":176.0,"HyperDash":false}]},{"StartTime":63877.0,"Objects":[{"StartTime":63877.0,"Position":264.0,"HyperDash":false}]},{"StartTime":64067.0,"Objects":[{"StartTime":64067.0,"Position":352.0,"HyperDash":false},{"StartTime":64152.0,"Position":394.99942,"HyperDash":false},{"StartTime":64238.0,"Position":401.898163,"HyperDash":false},{"StartTime":64324.0,"Position":421.559357,"HyperDash":false},{"StartTime":64446.0,"Position":422.749817,"HyperDash":false}]},{"StartTime":64637.0,"Objects":[{"StartTime":64637.0,"Position":352.0,"HyperDash":false}]},{"StartTime":64826.0,"Objects":[{"StartTime":64826.0,"Position":272.0,"HyperDash":false},{"StartTime":64902.0,"Position":280.7143,"HyperDash":false},{"StartTime":65015.0,"Position":326.0,"HyperDash":false}]},{"StartTime":65206.0,"Objects":[{"StartTime":65206.0,"Position":326.0,"HyperDash":false}]},{"StartTime":65396.0,"Objects":[{"StartTime":65396.0,"Position":272.0,"HyperDash":false},{"StartTime":65481.0,"Position":237.667328,"HyperDash":false},{"StartTime":65567.0,"Position":221.486267,"HyperDash":false},{"StartTime":65653.0,"Position":177.3163,"HyperDash":false},{"StartTime":65775.0,"Position":167.831314,"HyperDash":false}]},{"StartTime":65966.0,"Objects":[{"StartTime":65966.0,"Position":104.0,"HyperDash":false}]},{"StartTime":66156.0,"Objects":[{"StartTime":66156.0,"Position":48.0,"HyperDash":false}]},{"StartTime":66345.0,"Objects":[{"StartTime":66345.0,"Position":104.0,"HyperDash":false}]},{"StartTime":66535.0,"Objects":[{"StartTime":66535.0,"Position":56.0,"HyperDash":false}]},{"StartTime":66630.0,"Objects":[{"StartTime":66630.0,"Position":80.0,"HyperDash":false}]},{"StartTime":66725.0,"Objects":[{"StartTime":66725.0,"Position":104.0,"HyperDash":false}]},{"StartTime":66915.0,"Objects":[{"StartTime":66915.0,"Position":192.0,"HyperDash":false}]},{"StartTime":67105.0,"Objects":[{"StartTime":67105.0,"Position":280.0,"HyperDash":false},{"StartTime":67190.0,"Position":314.967377,"HyperDash":false},{"StartTime":67276.0,"Position":332.346161,"HyperDash":false},{"StartTime":67362.0,"Position":371.7249,"HyperDash":false},{"StartTime":67484.0,"Position":435.9134,"HyperDash":false}]},{"StartTime":67675.0,"Objects":[{"StartTime":67675.0,"Position":448.0,"HyperDash":false}]},{"StartTime":67864.0,"Objects":[{"StartTime":67864.0,"Position":456.0,"HyperDash":false},{"StartTime":67949.0,"Position":419.88092,"HyperDash":false},{"StartTime":68035.0,"Position":402.3487,"HyperDash":false},{"StartTime":68121.0,"Position":361.816467,"HyperDash":false},{"StartTime":68243.0,"Position":299.410278,"HyperDash":false}]},{"StartTime":68434.0,"Objects":[{"StartTime":68434.0,"Position":288.0,"HyperDash":false}]},{"StartTime":68624.0,"Objects":[{"StartTime":68624.0,"Position":208.0,"HyperDash":false}]},{"StartTime":68814.0,"Objects":[{"StartTime":68814.0,"Position":128.0,"HyperDash":false}]},{"StartTime":69004.0,"Objects":[{"StartTime":69004.0,"Position":48.0,"HyperDash":false}]},{"StartTime":69194.0,"Objects":[{"StartTime":69194.0,"Position":128.0,"HyperDash":false},{"StartTime":69279.0,"Position":167.7291,"HyperDash":false},{"StartTime":69365.0,"Position":176.461563,"HyperDash":false},{"StartTime":69451.0,"Position":208.0288,"HyperDash":false},{"StartTime":69573.0,"Position":193.863922,"HyperDash":false}]},{"StartTime":69763.0,"Objects":[{"StartTime":69763.0,"Position":256.0,"HyperDash":false}]},{"StartTime":69953.0,"Objects":[{"StartTime":69953.0,"Position":256.0,"HyperDash":false}]},{"StartTime":70143.0,"Objects":[{"StartTime":70143.0,"Position":318.0,"HyperDash":false},{"StartTime":70228.0,"Position":307.361053,"HyperDash":false},{"StartTime":70314.0,"Position":339.541077,"HyperDash":false},{"StartTime":70400.0,"Position":329.926361,"HyperDash":false},{"StartTime":70522.0,"Position":382.366,"HyperDash":false}]},{"StartTime":70902.0,"Objects":[{"StartTime":70902.0,"Position":256.0,"HyperDash":false},{"StartTime":70973.0,"Position":237.22084,"HyperDash":false},{"StartTime":71044.0,"Position":261.5726,"HyperDash":false},{"StartTime":71115.0,"Position":256.317383,"HyperDash":false},{"StartTime":71186.0,"Position":259.616821,"HyperDash":false},{"StartTime":71248.0,"Position":284.489624,"HyperDash":false},{"StartTime":71310.0,"Position":249.320618,"HyperDash":false},{"StartTime":71372.0,"Position":249.26532,"HyperDash":false},{"StartTime":71471.0,"Position":256.0,"HyperDash":false}]},{"StartTime":71662.0,"Objects":[{"StartTime":71662.0,"Position":256.0,"HyperDash":false}]},{"StartTime":72042.0,"Objects":[{"StartTime":72042.0,"Position":256.0,"HyperDash":false}]},{"StartTime":72421.0,"Objects":[{"StartTime":72421.0,"Position":160.0,"HyperDash":false}]},{"StartTime":72611.0,"Objects":[{"StartTime":72611.0,"Position":224.0,"HyperDash":false}]},{"StartTime":72801.0,"Objects":[{"StartTime":72801.0,"Position":288.0,"HyperDash":false}]},{"StartTime":72991.0,"Objects":[{"StartTime":72991.0,"Position":352.0,"HyperDash":false}]},{"StartTime":73181.0,"Objects":[{"StartTime":73181.0,"Position":408.0,"HyperDash":false}]},{"StartTime":73371.0,"Objects":[{"StartTime":73371.0,"Position":304.0,"HyperDash":false}]},{"StartTime":73561.0,"Objects":[{"StartTime":73561.0,"Position":208.0,"HyperDash":false}]},{"StartTime":73751.0,"Objects":[{"StartTime":73751.0,"Position":112.0,"HyperDash":false}]},{"StartTime":73940.0,"Objects":[{"StartTime":73940.0,"Position":160.0,"HyperDash":false}]},{"StartTime":74130.0,"Objects":[{"StartTime":74130.0,"Position":224.0,"HyperDash":false}]},{"StartTime":74225.0,"Objects":[{"StartTime":74225.0,"Position":248.0,"HyperDash":false}]},{"StartTime":74320.0,"Objects":[{"StartTime":74320.0,"Position":272.0,"HyperDash":false}]},{"StartTime":74415.0,"Objects":[{"StartTime":74415.0,"Position":296.0,"HyperDash":false}]},{"StartTime":74510.0,"Objects":[{"StartTime":74510.0,"Position":320.0,"HyperDash":false}]},{"StartTime":74605.0,"Objects":[{"StartTime":74605.0,"Position":344.0,"HyperDash":false}]},{"StartTime":74700.0,"Objects":[{"StartTime":74700.0,"Position":368.0,"HyperDash":false},{"StartTime":74785.0,"Position":391.4436,"HyperDash":false},{"StartTime":74871.0,"Position":429.4646,"HyperDash":false},{"StartTime":74957.0,"Position":450.2139,"HyperDash":false},{"StartTime":75079.0,"Position":476.639343,"HyperDash":false}]},{"StartTime":75270.0,"Objects":[{"StartTime":75270.0,"Position":368.0,"HyperDash":false}]},{"StartTime":75459.0,"Objects":[{"StartTime":75459.0,"Position":296.0,"HyperDash":false},{"StartTime":75535.0,"Position":252.914215,"HyperDash":false},{"StartTime":75648.0,"Position":210.849869,"HyperDash":false}]},{"StartTime":75839.0,"Objects":[{"StartTime":75839.0,"Position":144.0,"HyperDash":false}]},{"StartTime":76029.0,"Objects":[{"StartTime":76029.0,"Position":168.0,"HyperDash":false},{"StartTime":76114.0,"Position":202.25589,"HyperDash":false},{"StartTime":76200.0,"Position":242.877075,"HyperDash":false},{"StartTime":76286.0,"Position":302.62854,"HyperDash":false},{"StartTime":76408.0,"Position":345.765717,"HyperDash":false}]},{"StartTime":76599.0,"Objects":[{"StartTime":76599.0,"Position":344.0,"HyperDash":false},{"StartTime":76684.0,"Position":307.766968,"HyperDash":false},{"StartTime":76770.0,"Position":252.272888,"HyperDash":false},{"StartTime":76856.0,"Position":211.514786,"HyperDash":false},{"StartTime":76978.0,"Position":167.090546,"HyperDash":false}]},{"StartTime":77168.0,"Objects":[{"StartTime":77168.0,"Position":256.0,"HyperDash":false}]},{"StartTime":77358.0,"Objects":[{"StartTime":77358.0,"Position":256.0,"HyperDash":false}]},{"StartTime":77548.0,"Objects":[{"StartTime":77548.0,"Position":424.0,"HyperDash":false},{"StartTime":77633.0,"Position":417.615356,"HyperDash":false},{"StartTime":77719.0,"Position":433.1576,"HyperDash":false},{"StartTime":77805.0,"Position":439.338928,"HyperDash":false},{"StartTime":77927.0,"Position":425.7557,"HyperDash":false}]},{"StartTime":78118.0,"Objects":[{"StartTime":78118.0,"Position":296.0,"HyperDash":false},{"StartTime":78194.0,"Position":289.17218,"HyperDash":false},{"StartTime":78307.0,"Position":326.270264,"HyperDash":false}]},{"StartTime":78497.0,"Objects":[{"StartTime":78497.0,"Position":240.0,"HyperDash":false},{"StartTime":78573.0,"Position":252.172165,"HyperDash":false},{"StartTime":78686.0,"Position":270.270264,"HyperDash":false}]},{"StartTime":78877.0,"Objects":[{"StartTime":78877.0,"Position":168.0,"HyperDash":false},{"StartTime":78953.0,"Position":193.367844,"HyperDash":false},{"StartTime":79066.0,"Position":198.756882,"HyperDash":false}]},{"StartTime":79257.0,"Objects":[{"StartTime":79257.0,"Position":104.0,"HyperDash":false},{"StartTime":79333.0,"Position":113.367844,"HyperDash":false},{"StartTime":79446.0,"Position":134.756882,"HyperDash":false}]},{"StartTime":79637.0,"Objects":[{"StartTime":79637.0,"Position":48.0,"HyperDash":false},{"StartTime":79731.0,"Position":47.97381,"HyperDash":false},{"StartTime":79826.0,"Position":15.6918831,"HyperDash":false},{"StartTime":79903.0,"Position":19.7344723,"HyperDash":false},{"StartTime":80016.0,"Position":48.0,"HyperDash":false}]},{"StartTime":80206.0,"Objects":[{"StartTime":80206.0,"Position":48.0,"HyperDash":false}]},{"StartTime":80396.0,"Objects":[{"StartTime":80396.0,"Position":48.0,"HyperDash":false}]},{"StartTime":80586.0,"Objects":[{"StartTime":80586.0,"Position":48.0,"HyperDash":false},{"StartTime":80671.0,"Position":50.6653442,"HyperDash":false},{"StartTime":80757.0,"Position":72.3398361,"HyperDash":false},{"StartTime":80843.0,"Position":138.798065,"HyperDash":false},{"StartTime":80965.0,"Position":177.234756,"HyperDash":false}]},{"StartTime":81156.0,"Objects":[{"StartTime":81156.0,"Position":334.0,"HyperDash":false},{"StartTime":81241.0,"Position":384.0748,"HyperDash":false},{"StartTime":81327.0,"Position":430.608582,"HyperDash":false},{"StartTime":81413.0,"Position":423.721344,"HyperDash":false},{"StartTime":81535.0,"Position":463.472778,"HyperDash":false}]},{"StartTime":81725.0,"Objects":[{"StartTime":81725.0,"Position":256.0,"HyperDash":false}]},{"StartTime":81915.0,"Objects":[{"StartTime":81915.0,"Position":256.0,"HyperDash":false}]},{"StartTime":82105.0,"Objects":[{"StartTime":82105.0,"Position":48.0,"HyperDash":false},{"StartTime":82190.0,"Position":55.6653442,"HyperDash":false},{"StartTime":82276.0,"Position":92.3398361,"HyperDash":false},{"StartTime":82362.0,"Position":132.798065,"HyperDash":false},{"StartTime":82484.0,"Position":177.234756,"HyperDash":false}]},{"StartTime":82675.0,"Objects":[{"StartTime":82675.0,"Position":334.0,"HyperDash":false},{"StartTime":82760.0,"Position":361.0748,"HyperDash":false},{"StartTime":82846.0,"Position":397.608582,"HyperDash":false},{"StartTime":82932.0,"Position":421.721344,"HyperDash":false},{"StartTime":83054.0,"Position":463.472778,"HyperDash":false}]},{"StartTime":83244.0,"Objects":[{"StartTime":83244.0,"Position":256.0,"HyperDash":false}]},{"StartTime":83434.0,"Objects":[{"StartTime":83434.0,"Position":256.0,"HyperDash":false}]},{"StartTime":83624.0,"Objects":[{"StartTime":83624.0,"Position":177.0,"HyperDash":false},{"StartTime":83709.0,"Position":139.9757,"HyperDash":false},{"StartTime":83795.0,"Position":85.66393,"HyperDash":false},{"StartTime":83881.0,"Position":76.88606,"HyperDash":false},{"StartTime":84003.0,"Position":48.41881,"HyperDash":false}]},{"StartTime":84194.0,"Objects":[{"StartTime":84194.0,"Position":240.0,"HyperDash":false},{"StartTime":84270.0,"Position":217.612869,"HyperDash":false},{"StartTime":84383.0,"Position":151.997787,"HyperDash":false}]},{"StartTime":84573.0,"Objects":[{"StartTime":84573.0,"Position":40.0,"HyperDash":false},{"StartTime":84649.0,"Position":65.48768,"HyperDash":false},{"StartTime":84762.0,"Position":128.252258,"HyperDash":false}]},{"StartTime":84953.0,"Objects":[{"StartTime":84953.0,"Position":280.0,"HyperDash":false},{"StartTime":85029.0,"Position":237.890076,"HyperDash":false},{"StartTime":85142.0,"Position":192.68718,"HyperDash":false}]},{"StartTime":85333.0,"Objects":[{"StartTime":85333.0,"Position":392.0,"HyperDash":false},{"StartTime":85392.0,"Position":335.0,"HyperDash":false},{"StartTime":85451.0,"Position":193.0,"HyperDash":false},{"StartTime":85510.0,"Position":478.0,"HyperDash":false},{"StartTime":85570.0,"Position":255.0,"HyperDash":false},{"StartTime":85629.0,"Position":175.0,"HyperDash":false},{"StartTime":85688.0,"Position":274.0,"HyperDash":false},{"StartTime":85748.0,"Position":442.0,"HyperDash":false},{"StartTime":85807.0,"Position":295.0,"HyperDash":false},{"StartTime":85866.0,"Position":311.0,"HyperDash":false},{"StartTime":85926.0,"Position":17.0,"HyperDash":false},{"StartTime":85985.0,"Position":467.0,"HyperDash":false},{"StartTime":86044.0,"Position":30.0,"HyperDash":false},{"StartTime":86104.0,"Position":218.0,"HyperDash":false},{"StartTime":86163.0,"Position":26.0,"HyperDash":false},{"StartTime":86222.0,"Position":16.0,"HyperDash":false},{"StartTime":86282.0,"Position":248.0,"HyperDash":false}]},{"StartTime":86472.0,"Objects":[{"StartTime":86472.0,"Position":256.0,"HyperDash":false}]},{"StartTime":86662.0,"Objects":[{"StartTime":86662.0,"Position":128.0,"HyperDash":false}]},{"StartTime":86757.0,"Objects":[{"StartTime":86757.0,"Position":152.0,"HyperDash":false}]},{"StartTime":86852.0,"Objects":[{"StartTime":86852.0,"Position":176.0,"HyperDash":false},{"StartTime":86928.0,"Position":199.190475,"HyperDash":false},{"StartTime":87041.0,"Position":266.0,"HyperDash":false}]},{"StartTime":87232.0,"Objects":[{"StartTime":87232.0,"Position":360.0,"HyperDash":false},{"StartTime":87317.0,"Position":331.134338,"HyperDash":false},{"StartTime":87403.0,"Position":283.893768,"HyperDash":false},{"StartTime":87489.0,"Position":250.155975,"HyperDash":false},{"StartTime":87611.0,"Position":199.214035,"HyperDash":false}]},{"StartTime":87801.0,"Objects":[{"StartTime":87801.0,"Position":136.0,"HyperDash":false},{"StartTime":87877.0,"Position":153.190475,"HyperDash":false},{"StartTime":87990.0,"Position":226.0,"HyperDash":false}]},{"StartTime":88181.0,"Objects":[{"StartTime":88181.0,"Position":440.0,"HyperDash":false},{"StartTime":88266.0,"Position":401.6306,"HyperDash":false},{"StartTime":88352.0,"Position":361.417969,"HyperDash":false},{"StartTime":88438.0,"Position":315.9722,"HyperDash":false},{"StartTime":88560.0,"Position":286.761566,"HyperDash":false}]},{"StartTime":88751.0,"Objects":[{"StartTime":88751.0,"Position":72.0,"HyperDash":false},{"StartTime":88836.0,"Position":97.36939,"HyperDash":false},{"StartTime":88922.0,"Position":151.554047,"HyperDash":false},{"StartTime":89008.0,"Position":175.23497,"HyperDash":false},{"StartTime":89130.0,"Position":225.445587,"HyperDash":false}]},{"StartTime":89320.0,"Objects":[{"StartTime":89320.0,"Position":256.0,"HyperDash":false},{"StartTime":89414.0,"Position":256.0,"HyperDash":false},{"StartTime":89509.0,"Position":256.0,"HyperDash":false}]},{"StartTime":89700.0,"Objects":[{"StartTime":89700.0,"Position":488.0,"HyperDash":false},{"StartTime":89785.0,"Position":441.7927,"HyperDash":false},{"StartTime":89871.0,"Position":394.103729,"HyperDash":false},{"StartTime":89957.0,"Position":358.440735,"HyperDash":false},{"StartTime":90079.0,"Position":314.813538,"HyperDash":false}]},{"StartTime":90270.0,"Objects":[{"StartTime":90270.0,"Position":256.0,"HyperDash":false}]},{"StartTime":90459.0,"Objects":[{"StartTime":90459.0,"Position":160.0,"HyperDash":false}]},{"StartTime":90649.0,"Objects":[{"StartTime":90649.0,"Position":64.0,"HyperDash":false}]},{"StartTime":90839.0,"Objects":[{"StartTime":90839.0,"Position":160.0,"HyperDash":false}]},{"StartTime":91029.0,"Objects":[{"StartTime":91029.0,"Position":256.0,"HyperDash":false}]},{"StartTime":91219.0,"Objects":[{"StartTime":91219.0,"Position":352.0,"HyperDash":false}]},{"StartTime":91409.0,"Objects":[{"StartTime":91409.0,"Position":448.0,"HyperDash":false}]},{"StartTime":91599.0,"Objects":[{"StartTime":91599.0,"Position":352.0,"HyperDash":false}]},{"StartTime":91788.0,"Objects":[{"StartTime":91788.0,"Position":256.0,"HyperDash":false}]},{"StartTime":91978.0,"Objects":[{"StartTime":91978.0,"Position":256.0,"HyperDash":false}]},{"StartTime":92168.0,"Objects":[{"StartTime":92168.0,"Position":256.0,"HyperDash":false}]},{"StartTime":92358.0,"Objects":[{"StartTime":92358.0,"Position":256.0,"HyperDash":false},{"StartTime":92434.0,"Position":250.0,"HyperDash":false},{"StartTime":92547.0,"Position":256.0,"HyperDash":false}]},{"StartTime":92738.0,"Objects":[{"StartTime":92738.0,"Position":32.0,"HyperDash":false},{"StartTime":92823.0,"Position":61.3693924,"HyperDash":false},{"StartTime":92909.0,"Position":113.213722,"HyperDash":false},{"StartTime":92995.0,"Position":137.112122,"HyperDash":false},{"StartTime":93117.0,"Position":192.083252,"HyperDash":false}]},{"StartTime":93307.0,"Objects":[{"StartTime":93307.0,"Position":64.0,"HyperDash":false},{"StartTime":93383.0,"Position":90.59053,"HyperDash":false},{"StartTime":93496.0,"Position":127.639618,"HyperDash":false}]},{"StartTime":93687.0,"Objects":[{"StartTime":93687.0,"Position":256.0,"HyperDash":false},{"StartTime":93763.0,"Position":296.590546,"HyperDash":false},{"StartTime":93876.0,"Position":319.639618,"HyperDash":false}]},{"StartTime":94067.0,"Objects":[{"StartTime":94067.0,"Position":424.0,"HyperDash":false}]},{"StartTime":94257.0,"Objects":[{"StartTime":94257.0,"Position":256.0,"HyperDash":false},{"StartTime":94342.0,"Position":210.766815,"HyperDash":false},{"StartTime":94428.0,"Position":186.0,"HyperDash":false},{"StartTime":94514.0,"Position":209.0,"HyperDash":false},{"StartTime":94636.0,"Position":192.0,"HyperDash":false}]},{"StartTime":94826.0,"Objects":[{"StartTime":94826.0,"Position":328.0,"HyperDash":false},{"StartTime":94920.0,"Position":353.6438,"HyperDash":false},{"StartTime":95015.0,"Position":418.0,"HyperDash":false},{"StartTime":95092.0,"Position":396.667542,"HyperDash":false},{"StartTime":95205.0,"Position":328.0,"HyperDash":false}]},{"StartTime":95396.0,"Objects":[{"StartTime":95396.0,"Position":328.0,"HyperDash":false}]},{"StartTime":95586.0,"Objects":[{"StartTime":95586.0,"Position":328.0,"HyperDash":false}]},{"StartTime":95776.0,"Objects":[{"StartTime":95776.0,"Position":192.0,"HyperDash":false},{"StartTime":95861.0,"Position":170.3153,"HyperDash":false},{"StartTime":95947.0,"Position":94.6082153,"HyperDash":false},{"StartTime":96033.0,"Position":54.801857,"HyperDash":false},{"StartTime":96155.0,"Position":13.5809069,"HyperDash":false}]},{"StartTime":96345.0,"Objects":[{"StartTime":96345.0,"Position":56.0,"HyperDash":false},{"StartTime":96421.0,"Position":104.190475,"HyperDash":false},{"StartTime":96534.0,"Position":146.0,"HyperDash":false}]},{"StartTime":96725.0,"Objects":[{"StartTime":96725.0,"Position":232.0,"HyperDash":false}]},{"StartTime":96915.0,"Objects":[{"StartTime":96915.0,"Position":280.0,"HyperDash":false}]},{"StartTime":97105.0,"Objects":[{"StartTime":97105.0,"Position":360.0,"HyperDash":false},{"StartTime":97181.0,"Position":408.1905,"HyperDash":false},{"StartTime":97294.0,"Position":450.0,"HyperDash":false}]},{"StartTime":97485.0,"Objects":[{"StartTime":97485.0,"Position":458.0,"HyperDash":false},{"StartTime":97579.0,"Position":425.0,"HyperDash":false},{"StartTime":97674.0,"Position":466.0,"HyperDash":false},{"StartTime":97769.0,"Position":56.0,"HyperDash":false},{"StartTime":97864.0,"Position":109.0,"HyperDash":false},{"StartTime":97959.0,"Position":482.0,"HyperDash":false},{"StartTime":98054.0,"Position":147.0,"HyperDash":false},{"StartTime":98149.0,"Position":285.0,"HyperDash":false},{"StartTime":98244.0,"Position":452.0,"HyperDash":false},{"StartTime":98339.0,"Position":419.0,"HyperDash":false},{"StartTime":98434.0,"Position":269.0,"HyperDash":false},{"StartTime":98529.0,"Position":249.0,"HyperDash":false},{"StartTime":98624.0,"Position":233.0,"HyperDash":false},{"StartTime":98719.0,"Position":449.0,"HyperDash":false},{"StartTime":98814.0,"Position":411.0,"HyperDash":false},{"StartTime":98909.0,"Position":75.0,"HyperDash":false},{"StartTime":99004.0,"Position":474.0,"HyperDash":false}]},{"StartTime":111156.0,"Objects":[{"StartTime":111156.0,"Position":256.0,"HyperDash":false}]},{"StartTime":111915.0,"Objects":[{"StartTime":111915.0,"Position":256.0,"HyperDash":false}]},{"StartTime":112105.0,"Objects":[{"StartTime":112105.0,"Position":256.0,"HyperDash":false}]},{"StartTime":112295.0,"Objects":[{"StartTime":112295.0,"Position":256.0,"HyperDash":false}]},{"StartTime":112485.0,"Objects":[{"StartTime":112485.0,"Position":256.0,"HyperDash":false}]},{"StartTime":112675.0,"Objects":[{"StartTime":112675.0,"Position":328.0,"HyperDash":false},{"StartTime":112760.0,"Position":361.17868,"HyperDash":false},{"StartTime":112846.0,"Position":407.7525,"HyperDash":false},{"StartTime":112932.0,"Position":421.257233,"HyperDash":false},{"StartTime":113054.0,"Position":455.379,"HyperDash":false}]},{"StartTime":113244.0,"Objects":[{"StartTime":113244.0,"Position":456.0,"HyperDash":false}]},{"StartTime":113434.0,"Objects":[{"StartTime":113434.0,"Position":456.0,"HyperDash":false}]},{"StartTime":113624.0,"Objects":[{"StartTime":113624.0,"Position":368.0,"HyperDash":false},{"StartTime":113718.0,"Position":305.349274,"HyperDash":false},{"StartTime":113813.0,"Position":287.706543,"HyperDash":false},{"StartTime":113890.0,"Position":296.162018,"HyperDash":false},{"StartTime":114003.0,"Position":368.0,"HyperDash":false}]},{"StartTime":114194.0,"Objects":[{"StartTime":114194.0,"Position":456.0,"HyperDash":false},{"StartTime":114279.0,"Position":435.479034,"HyperDash":false},{"StartTime":114365.0,"Position":402.81424,"HyperDash":false},{"StartTime":114451.0,"Position":376.903717,"HyperDash":false},{"StartTime":114573.0,"Position":310.688843,"HyperDash":false}]},{"StartTime":114763.0,"Objects":[{"StartTime":114763.0,"Position":256.0,"HyperDash":false},{"StartTime":114839.0,"Position":203.173843,"HyperDash":false},{"StartTime":114952.0,"Position":176.330536,"HyperDash":false}]},{"StartTime":115143.0,"Objects":[{"StartTime":115143.0,"Position":112.0,"HyperDash":false}]},{"StartTime":115333.0,"Objects":[{"StartTime":115333.0,"Position":176.0,"HyperDash":false}]},{"StartTime":115523.0,"Objects":[{"StartTime":115523.0,"Position":240.0,"HyperDash":false}]},{"StartTime":115713.0,"Objects":[{"StartTime":115713.0,"Position":176.0,"HyperDash":false},{"StartTime":115798.0,"Position":197.9177,"HyperDash":false},{"StartTime":115884.0,"Position":227.720581,"HyperDash":false},{"StartTime":115970.0,"Position":273.524536,"HyperDash":false},{"StartTime":116092.0,"Position":328.682556,"HyperDash":false}]},{"StartTime":116282.0,"Objects":[{"StartTime":116282.0,"Position":296.0,"HyperDash":false}]},{"StartTime":116472.0,"Objects":[{"StartTime":116472.0,"Position":360.0,"HyperDash":false}]},{"StartTime":116662.0,"Objects":[{"StartTime":116662.0,"Position":448.0,"HyperDash":false},{"StartTime":116738.0,"Position":439.409454,"HyperDash":false},{"StartTime":116851.0,"Position":384.360382,"HyperDash":false}]},{"StartTime":117042.0,"Objects":[{"StartTime":117042.0,"Position":384.0,"HyperDash":false},{"StartTime":117127.0,"Position":354.734955,"HyperDash":false},{"StartTime":117213.0,"Position":299.665924,"HyperDash":false},{"StartTime":117299.0,"Position":257.5697,"HyperDash":false},{"StartTime":117421.0,"Position":234.549561,"HyperDash":false}]},{"StartTime":117611.0,"Objects":[{"StartTime":117611.0,"Position":280.0,"HyperDash":false},{"StartTime":117687.0,"Position":309.4148,"HyperDash":false},{"StartTime":117800.0,"Position":286.3127,"HyperDash":false}]},{"StartTime":117991.0,"Objects":[{"StartTime":117991.0,"Position":192.0,"HyperDash":false},{"StartTime":118067.0,"Position":177.6565,"HyperDash":false},{"StartTime":118180.0,"Position":196.931625,"HyperDash":false}]},{"StartTime":118561.0,"Objects":[{"StartTime":118561.0,"Position":248.0,"HyperDash":false}]},{"StartTime":118940.0,"Objects":[{"StartTime":118940.0,"Position":248.0,"HyperDash":false}]},{"StartTime":119320.0,"Objects":[{"StartTime":119320.0,"Position":248.0,"HyperDash":false}]},{"StartTime":119700.0,"Objects":[{"StartTime":119700.0,"Position":448.0,"HyperDash":false}]},{"StartTime":119890.0,"Objects":[{"StartTime":119890.0,"Position":384.0,"HyperDash":false}]},{"StartTime":120080.0,"Objects":[{"StartTime":120080.0,"Position":320.0,"HyperDash":false}]},{"StartTime":120270.0,"Objects":[{"StartTime":120270.0,"Position":256.0,"HyperDash":false},{"StartTime":120355.0,"Position":213.1814,"HyperDash":false},{"StartTime":120441.0,"Position":169.527481,"HyperDash":false},{"StartTime":120527.0,"Position":146.788452,"HyperDash":false},{"StartTime":120649.0,"Position":78.92116,"HyperDash":false}]},{"StartTime":120839.0,"Objects":[{"StartTime":120839.0,"Position":80.0,"HyperDash":false}]},{"StartTime":121219.0,"Objects":[{"StartTime":121219.0,"Position":32.0,"HyperDash":false}]},{"StartTime":121409.0,"Objects":[{"StartTime":121409.0,"Position":120.0,"HyperDash":false}]},{"StartTime":121599.0,"Objects":[{"StartTime":121599.0,"Position":208.0,"HyperDash":false}]},{"StartTime":121788.0,"Objects":[{"StartTime":121788.0,"Position":296.0,"HyperDash":false},{"StartTime":121873.0,"Position":324.8186,"HyperDash":false},{"StartTime":121959.0,"Position":394.4725,"HyperDash":false},{"StartTime":122045.0,"Position":403.211548,"HyperDash":false},{"StartTime":122167.0,"Position":473.078827,"HyperDash":false}]},{"StartTime":122358.0,"Objects":[{"StartTime":122358.0,"Position":472.0,"HyperDash":false}]},{"StartTime":122738.0,"Objects":[{"StartTime":122738.0,"Position":208.0,"HyperDash":false}]},{"StartTime":122928.0,"Objects":[{"StartTime":122928.0,"Position":256.0,"HyperDash":false}]},{"StartTime":123117.0,"Objects":[{"StartTime":123117.0,"Position":304.0,"HyperDash":false}]},{"StartTime":123307.0,"Objects":[{"StartTime":123307.0,"Position":256.0,"HyperDash":false}]},{"StartTime":123687.0,"Objects":[{"StartTime":123687.0,"Position":256.0,"HyperDash":false}]},{"StartTime":124067.0,"Objects":[{"StartTime":124067.0,"Position":256.0,"HyperDash":false}]},{"StartTime":124257.0,"Objects":[{"StartTime":124257.0,"Position":256.0,"HyperDash":false}]},{"StartTime":124447.0,"Objects":[{"StartTime":124447.0,"Position":256.0,"HyperDash":false}]},{"StartTime":124637.0,"Objects":[{"StartTime":124637.0,"Position":256.0,"HyperDash":false}]},{"StartTime":124732.0,"Objects":[{"StartTime":124732.0,"Position":256.0,"HyperDash":false}]},{"StartTime":124826.0,"Objects":[{"StartTime":124826.0,"Position":256.0,"HyperDash":false},{"StartTime":124911.0,"Position":294.15155,"HyperDash":false},{"StartTime":124997.0,"Position":348.2999,"HyperDash":false},{"StartTime":125083.0,"Position":367.688416,"HyperDash":false},{"StartTime":125205.0,"Position":375.982025,"HyperDash":false}]},{"StartTime":125396.0,"Objects":[{"StartTime":125396.0,"Position":456.0,"HyperDash":false}]},{"StartTime":125586.0,"Objects":[{"StartTime":125586.0,"Position":392.0,"HyperDash":false}]},{"StartTime":125776.0,"Objects":[{"StartTime":125776.0,"Position":304.0,"HyperDash":false},{"StartTime":125852.0,"Position":263.17807,"HyperDash":false},{"StartTime":125965.0,"Position":227.350739,"HyperDash":false}]},{"StartTime":126156.0,"Objects":[{"StartTime":126156.0,"Position":192.0,"HyperDash":false}]},{"StartTime":126345.0,"Objects":[{"StartTime":126345.0,"Position":160.0,"HyperDash":false},{"StartTime":126430.0,"Position":126.124176,"HyperDash":false},{"StartTime":126516.0,"Position":85.81763,"HyperDash":false},{"StartTime":126602.0,"Position":37.2244949,"HyperDash":false},{"StartTime":126724.0,"Position":32.57615,"HyperDash":false}]},{"StartTime":126915.0,"Objects":[{"StartTime":126915.0,"Position":120.0,"HyperDash":false},{"StartTime":126991.0,"Position":102.400024,"HyperDash":false},{"StartTime":127104.0,"Position":68.7711,"HyperDash":false}]},{"StartTime":127295.0,"Objects":[{"StartTime":127295.0,"Position":136.0,"HyperDash":false},{"StartTime":127389.0,"Position":114.741783,"HyperDash":false},{"StartTime":127484.0,"Position":83.06455,"HyperDash":false},{"StartTime":127561.0,"Position":120.434265,"HyperDash":false},{"StartTime":127674.0,"Position":136.0,"HyperDash":false}]},{"StartTime":127864.0,"Objects":[{"StartTime":127864.0,"Position":184.0,"HyperDash":false},{"StartTime":127949.0,"Position":200.744141,"HyperDash":false},{"StartTime":128035.0,"Position":221.767609,"HyperDash":false},{"StartTime":128121.0,"Position":262.7911,"HyperDash":false},{"StartTime":128243.0,"Position":289.8709,"HyperDash":false}]},{"StartTime":128434.0,"Objects":[{"StartTime":128434.0,"Position":384.0,"HyperDash":false}]},{"StartTime":128624.0,"Objects":[{"StartTime":128624.0,"Position":448.0,"HyperDash":false}]},{"StartTime":128814.0,"Objects":[{"StartTime":128814.0,"Position":448.0,"HyperDash":false},{"StartTime":128890.0,"Position":398.135345,"HyperDash":false},{"StartTime":129003.0,"Position":368.7576,"HyperDash":false}]},{"StartTime":129194.0,"Objects":[{"StartTime":129194.0,"Position":440.0,"HyperDash":false},{"StartTime":129279.0,"Position":389.377838,"HyperDash":false},{"StartTime":129365.0,"Position":370.438324,"HyperDash":false},{"StartTime":129451.0,"Position":329.820526,"HyperDash":false},{"StartTime":129573.0,"Position":267.138855,"HyperDash":false}]},{"StartTime":129763.0,"Objects":[{"StartTime":129763.0,"Position":208.0,"HyperDash":false}]},{"StartTime":129953.0,"Objects":[{"StartTime":129953.0,"Position":128.0,"HyperDash":false}]},{"StartTime":130143.0,"Objects":[{"StartTime":130143.0,"Position":208.0,"HyperDash":false}]},{"StartTime":130333.0,"Objects":[{"StartTime":130333.0,"Position":288.0,"HyperDash":false},{"StartTime":130409.0,"Position":333.1905,"HyperDash":false},{"StartTime":130522.0,"Position":378.0,"HyperDash":false}]},{"StartTime":130713.0,"Objects":[{"StartTime":130713.0,"Position":448.0,"HyperDash":false},{"StartTime":130789.0,"Position":411.8095,"HyperDash":false},{"StartTime":130902.0,"Position":358.0,"HyperDash":false}]},{"StartTime":131282.0,"Objects":[{"StartTime":131282.0,"Position":176.0,"HyperDash":false}]},{"StartTime":131662.0,"Objects":[{"StartTime":131662.0,"Position":360.0,"HyperDash":false}]},{"StartTime":131852.0,"Objects":[{"StartTime":131852.0,"Position":288.0,"HyperDash":false}]},{"StartTime":132042.0,"Objects":[{"StartTime":132042.0,"Position":200.0,"HyperDash":false}]},{"StartTime":132232.0,"Objects":[{"StartTime":132232.0,"Position":112.0,"HyperDash":false}]},{"StartTime":132421.0,"Objects":[{"StartTime":132421.0,"Position":96.0,"HyperDash":false},{"StartTime":132487.0,"Position":49.5101624,"HyperDash":false},{"StartTime":132554.0,"Position":57.3071823,"HyperDash":false},{"StartTime":132620.0,"Position":26.5927753,"HyperDash":false},{"StartTime":132687.0,"Position":15.30433,"HyperDash":false},{"StartTime":132754.0,"Position":15.7045517,"HyperDash":false},{"StartTime":132820.0,"Position":49.43814,"HyperDash":false},{"StartTime":132887.0,"Position":66.86148,"HyperDash":false},{"StartTime":132990.0,"Position":96.71054,"HyperDash":false}]},{"StartTime":133371.0,"Objects":[{"StartTime":133371.0,"Position":224.0,"HyperDash":false}]},{"StartTime":133561.0,"Objects":[{"StartTime":133561.0,"Position":312.0,"HyperDash":false}]},{"StartTime":133751.0,"Objects":[{"StartTime":133751.0,"Position":400.0,"HyperDash":false}]},{"StartTime":133940.0,"Objects":[{"StartTime":133940.0,"Position":416.0,"HyperDash":false},{"StartTime":134006.0,"Position":452.489838,"HyperDash":false},{"StartTime":134073.0,"Position":482.6928,"HyperDash":false},{"StartTime":134139.0,"Position":473.407227,"HyperDash":false},{"StartTime":134206.0,"Position":502.695679,"HyperDash":false},{"StartTime":134273.0,"Position":505.295471,"HyperDash":false},{"StartTime":134339.0,"Position":462.561859,"HyperDash":false},{"StartTime":134406.0,"Position":475.138519,"HyperDash":false},{"StartTime":134509.0,"Position":415.289459,"HyperDash":false}]},{"StartTime":134890.0,"Objects":[{"StartTime":134890.0,"Position":80.0,"HyperDash":false}]},{"StartTime":135080.0,"Objects":[{"StartTime":135080.0,"Position":160.0,"HyperDash":false}]},{"StartTime":135270.0,"Objects":[{"StartTime":135270.0,"Position":200.0,"HyperDash":false}]},{"StartTime":135459.0,"Objects":[{"StartTime":135459.0,"Position":280.0,"HyperDash":false}]},{"StartTime":135839.0,"Objects":[{"StartTime":135839.0,"Position":464.0,"HyperDash":false}]},{"StartTime":136029.0,"Objects":[{"StartTime":136029.0,"Position":376.0,"HyperDash":false}]},{"StartTime":136219.0,"Objects":[{"StartTime":136219.0,"Position":376.0,"HyperDash":false}]},{"StartTime":136409.0,"Objects":[{"StartTime":136409.0,"Position":280.0,"HyperDash":false}]},{"StartTime":136599.0,"Objects":[{"StartTime":136599.0,"Position":280.0,"HyperDash":false}]},{"StartTime":136978.0,"Objects":[{"StartTime":136978.0,"Position":56.0,"HyperDash":false},{"StartTime":137063.0,"Position":98.33999,"HyperDash":false},{"StartTime":137149.0,"Position":121.429718,"HyperDash":false},{"StartTime":137235.0,"Position":184.982086,"HyperDash":false},{"StartTime":137357.0,"Position":227.214722,"HyperDash":false}]},{"StartTime":137738.0,"Objects":[{"StartTime":137738.0,"Position":456.0,"HyperDash":false},{"StartTime":137823.0,"Position":411.66,"HyperDash":false},{"StartTime":137909.0,"Position":389.570282,"HyperDash":false},{"StartTime":137995.0,"Position":336.0179,"HyperDash":false},{"StartTime":138117.0,"Position":284.785278,"HyperDash":false}]},{"StartTime":138497.0,"Objects":[{"StartTime":138497.0,"Position":256.0,"HyperDash":false}]},{"StartTime":138687.0,"Objects":[{"StartTime":138687.0,"Position":200.0,"HyperDash":false}]},{"StartTime":138877.0,"Objects":[{"StartTime":138877.0,"Position":256.0,"HyperDash":false}]},{"StartTime":139067.0,"Objects":[{"StartTime":139067.0,"Position":312.0,"HyperDash":false},{"StartTime":139143.0,"Position":331.1905,"HyperDash":false},{"StartTime":139256.0,"Position":402.0,"HyperDash":false}]},{"StartTime":139447.0,"Objects":[{"StartTime":139447.0,"Position":400.0,"HyperDash":false},{"StartTime":139541.0,"Position":424.6438,"HyperDash":false},{"StartTime":139636.0,"Position":490.0,"HyperDash":false},{"StartTime":139713.0,"Position":436.667542,"HyperDash":false},{"StartTime":139826.0,"Position":400.0,"HyperDash":false}]},{"StartTime":140016.0,"Objects":[{"StartTime":140016.0,"Position":400.0,"HyperDash":false},{"StartTime":140101.0,"Position":405.018951,"HyperDash":false},{"StartTime":140187.0,"Position":337.8755,"HyperDash":false},{"StartTime":140273.0,"Position":336.351257,"HyperDash":false},{"StartTime":140395.0,"Position":259.5054,"HyperDash":false}]},{"StartTime":140586.0,"Objects":[{"StartTime":140586.0,"Position":224.0,"HyperDash":false}]},{"StartTime":140776.0,"Objects":[{"StartTime":140776.0,"Position":296.0,"HyperDash":false}]},{"StartTime":140966.0,"Objects":[{"StartTime":140966.0,"Position":224.0,"HyperDash":false}]},{"StartTime":141156.0,"Objects":[{"StartTime":141156.0,"Position":296.0,"HyperDash":false}]},{"StartTime":141345.0,"Objects":[{"StartTime":141345.0,"Position":256.0,"HyperDash":false},{"StartTime":141430.0,"Position":196.648087,"HyperDash":false},{"StartTime":141516.0,"Position":175.249878,"HyperDash":false},{"StartTime":141602.0,"Position":133.184525,"HyperDash":false},{"StartTime":141724.0,"Position":114.597687,"HyperDash":false}]},{"StartTime":141915.0,"Objects":[{"StartTime":141915.0,"Position":112.0,"HyperDash":false},{"StartTime":142009.0,"Position":98.0,"HyperDash":false},{"StartTime":142104.0,"Position":112.0,"HyperDash":false},{"StartTime":142181.0,"Position":98.0,"HyperDash":false},{"StartTime":142294.0,"Position":112.0,"HyperDash":false}]},{"StartTime":142485.0,"Objects":[{"StartTime":142485.0,"Position":112.0,"HyperDash":false}]},{"StartTime":142580.0,"Objects":[{"StartTime":142580.0,"Position":112.0,"HyperDash":false}]},{"StartTime":142675.0,"Objects":[{"StartTime":142675.0,"Position":112.0,"HyperDash":false}]},{"StartTime":142864.0,"Objects":[{"StartTime":142864.0,"Position":112.0,"HyperDash":false}]},{"StartTime":143054.0,"Objects":[{"StartTime":143054.0,"Position":232.0,"HyperDash":false},{"StartTime":143139.0,"Position":225.714432,"HyperDash":false},{"StartTime":143225.0,"Position":180.464615,"HyperDash":false},{"StartTime":143311.0,"Position":216.858948,"HyperDash":false},{"StartTime":143433.0,"Position":221.927963,"HyperDash":false}]},{"StartTime":143814.0,"Objects":[{"StartTime":143814.0,"Position":280.0,"HyperDash":false},{"StartTime":143899.0,"Position":293.285583,"HyperDash":false},{"StartTime":143985.0,"Position":317.53537,"HyperDash":false},{"StartTime":144071.0,"Position":329.141052,"HyperDash":false},{"StartTime":144193.0,"Position":290.072052,"HyperDash":false}]},{"StartTime":144573.0,"Objects":[{"StartTime":144573.0,"Position":256.0,"HyperDash":false}]},{"StartTime":144763.0,"Objects":[{"StartTime":144763.0,"Position":344.0,"HyperDash":false}]},{"StartTime":144953.0,"Objects":[{"StartTime":144953.0,"Position":416.0,"HyperDash":false}]},{"StartTime":145143.0,"Objects":[{"StartTime":145143.0,"Position":416.0,"HyperDash":false},{"StartTime":145228.0,"Position":392.6306,"HyperDash":false},{"StartTime":145314.0,"Position":338.7863,"HyperDash":false},{"StartTime":145400.0,"Position":289.941956,"HyperDash":false},{"StartTime":145522.0,"Position":236.0,"HyperDash":false}]},{"StartTime":145713.0,"Objects":[{"StartTime":145713.0,"Position":144.0,"HyperDash":false}]},{"StartTime":145902.0,"Objects":[{"StartTime":145902.0,"Position":80.0,"HyperDash":false}]},{"StartTime":146092.0,"Objects":[{"StartTime":146092.0,"Position":16.0,"HyperDash":false}]},{"StartTime":146472.0,"Objects":[{"StartTime":146472.0,"Position":256.0,"HyperDash":false}]},{"StartTime":146852.0,"Objects":[{"StartTime":146852.0,"Position":496.0,"HyperDash":false}]},{"StartTime":147137.0,"Objects":[{"StartTime":147137.0,"Position":352.0,"HyperDash":false}]},{"StartTime":147421.0,"Objects":[{"StartTime":147421.0,"Position":160.0,"HyperDash":false}]},{"StartTime":147611.0,"Objects":[{"StartTime":147611.0,"Position":256.0,"HyperDash":false}]},{"StartTime":147991.0,"Objects":[{"StartTime":147991.0,"Position":256.0,"HyperDash":false}]},{"StartTime":148371.0,"Objects":[{"StartTime":148371.0,"Position":256.0,"HyperDash":false}]},{"StartTime":148561.0,"Objects":[{"StartTime":148561.0,"Position":368.0,"HyperDash":false}]},{"StartTime":148751.0,"Objects":[{"StartTime":148751.0,"Position":256.0,"HyperDash":false}]},{"StartTime":148940.0,"Objects":[{"StartTime":148940.0,"Position":144.0,"HyperDash":false}]},{"StartTime":149130.0,"Objects":[{"StartTime":149130.0,"Position":288.0,"HyperDash":false}]},{"StartTime":149225.0,"Objects":[{"StartTime":149225.0,"Position":312.0,"HyperDash":false}]},{"StartTime":149320.0,"Objects":[{"StartTime":149320.0,"Position":336.0,"HyperDash":false}]},{"StartTime":149415.0,"Objects":[{"StartTime":149415.0,"Position":312.0,"HyperDash":false}]},{"StartTime":149510.0,"Objects":[{"StartTime":149510.0,"Position":288.0,"HyperDash":false}]},{"StartTime":149700.0,"Objects":[{"StartTime":149700.0,"Position":224.0,"HyperDash":false}]},{"StartTime":149795.0,"Objects":[{"StartTime":149795.0,"Position":200.0,"HyperDash":false}]},{"StartTime":149890.0,"Objects":[{"StartTime":149890.0,"Position":176.0,"HyperDash":false}]},{"StartTime":149985.0,"Objects":[{"StartTime":149985.0,"Position":200.0,"HyperDash":false}]},{"StartTime":150080.0,"Objects":[{"StartTime":150080.0,"Position":224.0,"HyperDash":false}]},{"StartTime":150175.0,"Objects":[{"StartTime":150175.0,"Position":256.0,"HyperDash":false}]},{"StartTime":150270.0,"Objects":[{"StartTime":150270.0,"Position":256.0,"HyperDash":false}]},{"StartTime":150649.0,"Objects":[{"StartTime":150649.0,"Position":168.0,"HyperDash":false},{"StartTime":150725.0,"Position":142.229309,"HyperDash":false},{"StartTime":150838.0,"Position":168.0,"HyperDash":false}]},{"StartTime":151029.0,"Objects":[{"StartTime":151029.0,"Position":344.0,"HyperDash":false},{"StartTime":151105.0,"Position":368.7707,"HyperDash":false},{"StartTime":151218.0,"Position":344.0,"HyperDash":false}]},{"StartTime":151409.0,"Objects":[{"StartTime":151409.0,"Position":256.0,"HyperDash":false}]},{"StartTime":151599.0,"Objects":[{"StartTime":151599.0,"Position":256.0,"HyperDash":false}]},{"StartTime":151694.0,"Objects":[{"StartTime":151694.0,"Position":256.0,"HyperDash":false}]},{"StartTime":151788.0,"Objects":[{"StartTime":151788.0,"Position":256.0,"HyperDash":false}]},{"StartTime":151978.0,"Objects":[{"StartTime":151978.0,"Position":464.0,"HyperDash":false},{"StartTime":152063.0,"Position":422.517944,"HyperDash":false},{"StartTime":152149.0,"Position":426.162018,"HyperDash":false},{"StartTime":152235.0,"Position":393.104584,"HyperDash":false},{"StartTime":152357.0,"Position":346.162628,"HyperDash":true}]},{"StartTime":152548.0,"Objects":[{"StartTime":152548.0,"Position":48.0,"HyperDash":false},{"StartTime":152633.0,"Position":100.48204,"HyperDash":false},{"StartTime":152719.0,"Position":77.83798,"HyperDash":false},{"StartTime":152805.0,"Position":114.895416,"HyperDash":false},{"StartTime":152927.0,"Position":165.837372,"HyperDash":false}]},{"StartTime":153118.0,"Objects":[{"StartTime":153118.0,"Position":256.0,"HyperDash":false}]},{"StartTime":153213.0,"Objects":[{"StartTime":153213.0,"Position":256.0,"HyperDash":false}]},{"StartTime":153307.0,"Objects":[{"StartTime":153307.0,"Position":256.0,"HyperDash":false}]},{"StartTime":153497.0,"Objects":[{"StartTime":153497.0,"Position":168.0,"HyperDash":false},{"StartTime":153582.0,"Position":217.34,"HyperDash":false},{"StartTime":153668.0,"Position":235.429718,"HyperDash":false},{"StartTime":153754.0,"Position":295.9821,"HyperDash":false},{"StartTime":153876.0,"Position":339.214722,"HyperDash":false}]},{"StartTime":154067.0,"Objects":[{"StartTime":154067.0,"Position":168.0,"HyperDash":false},{"StartTime":154143.0,"Position":134.40947,"HyperDash":false},{"StartTime":154256.0,"Position":104.3604,"HyperDash":false}]},{"StartTime":154447.0,"Objects":[{"StartTime":154447.0,"Position":344.0,"HyperDash":false},{"StartTime":154523.0,"Position":362.5905,"HyperDash":false},{"StartTime":154636.0,"Position":407.6396,"HyperDash":true}]},{"StartTime":154826.0,"Objects":[{"StartTime":154826.0,"Position":168.0,"HyperDash":false},{"StartTime":154902.0,"Position":150.40947,"HyperDash":false},{"StartTime":155015.0,"Position":104.3604,"HyperDash":false}]},{"StartTime":155206.0,"Objects":[{"StartTime":155206.0,"Position":344.0,"HyperDash":false},{"StartTime":155282.0,"Position":365.5905,"HyperDash":false},{"StartTime":155395.0,"Position":407.6396,"HyperDash":false}]},{"StartTime":155586.0,"Objects":[{"StartTime":155586.0,"Position":256.0,"HyperDash":false},{"StartTime":155680.0,"Position":270.830933,"HyperDash":false},{"StartTime":155775.0,"Position":254.810913,"HyperDash":false},{"StartTime":155852.0,"Position":238.329559,"HyperDash":false},{"StartTime":155965.0,"Position":256.0,"HyperDash":false}]},{"StartTime":156156.0,"Objects":[{"StartTime":156156.0,"Position":256.0,"HyperDash":false}]},{"StartTime":156345.0,"Objects":[{"StartTime":156345.0,"Position":256.0,"HyperDash":false}]},{"StartTime":156535.0,"Objects":[{"StartTime":156535.0,"Position":96.0,"HyperDash":false},{"StartTime":156620.0,"Position":138.369385,"HyperDash":false},{"StartTime":156706.0,"Position":196.213715,"HyperDash":false},{"StartTime":156792.0,"Position":213.399918,"HyperDash":false},{"StartTime":156914.0,"Position":244.507538,"HyperDash":false}]},{"StartTime":157105.0,"Objects":[{"StartTime":157105.0,"Position":152.0,"HyperDash":false},{"StartTime":157181.0,"Position":158.0,"HyperDash":false},{"StartTime":157294.0,"Position":122.301514,"HyperDash":false}]},{"StartTime":157485.0,"Objects":[{"StartTime":157485.0,"Position":32.0,"HyperDash":false},{"StartTime":157561.0,"Position":15.0,"HyperDash":false},{"StartTime":157674.0,"Position":61.6984863,"HyperDash":false}]},{"StartTime":157864.0,"Objects":[{"StartTime":157864.0,"Position":152.0,"HyperDash":true}]},{"StartTime":158054.0,"Objects":[{"StartTime":158054.0,"Position":416.0,"HyperDash":false},{"StartTime":158139.0,"Position":368.6306,"HyperDash":false},{"StartTime":158225.0,"Position":342.7863,"HyperDash":false},{"StartTime":158311.0,"Position":278.600067,"HyperDash":false},{"StartTime":158433.0,"Position":267.492462,"HyperDash":false}]},{"StartTime":158624.0,"Objects":[{"StartTime":158624.0,"Position":360.0,"HyperDash":false},{"StartTime":158700.0,"Position":345.0,"HyperDash":false},{"StartTime":158813.0,"Position":389.6985,"HyperDash":false}]},{"StartTime":159004.0,"Objects":[{"StartTime":159004.0,"Position":480.0,"HyperDash":false},{"StartTime":159080.0,"Position":483.0,"HyperDash":false},{"StartTime":159193.0,"Position":450.3015,"HyperDash":false}]},{"StartTime":159383.0,"Objects":[{"StartTime":159383.0,"Position":360.0,"HyperDash":false}]},{"StartTime":159573.0,"Objects":[{"StartTime":159573.0,"Position":255.0,"HyperDash":false},{"StartTime":159658.0,"Position":267.0,"HyperDash":false},{"StartTime":159744.0,"Position":265.0,"HyperDash":false},{"StartTime":159830.0,"Position":261.0,"HyperDash":false},{"StartTime":159952.0,"Position":255.0,"HyperDash":false}]},{"StartTime":160143.0,"Objects":[{"StartTime":160143.0,"Position":256.0,"HyperDash":false}]},{"StartTime":160333.0,"Objects":[{"StartTime":160333.0,"Position":376.0,"HyperDash":false}]},{"StartTime":160523.0,"Objects":[{"StartTime":160523.0,"Position":376.0,"HyperDash":false}]},{"StartTime":160713.0,"Objects":[{"StartTime":160713.0,"Position":256.0,"HyperDash":false}]},{"StartTime":160902.0,"Objects":[{"StartTime":160902.0,"Position":136.0,"HyperDash":false}]},{"StartTime":161092.0,"Objects":[{"StartTime":161092.0,"Position":136.0,"HyperDash":false}]},{"StartTime":161282.0,"Objects":[{"StartTime":161282.0,"Position":199.0,"HyperDash":false},{"StartTime":161341.0,"Position":494.0,"HyperDash":false},{"StartTime":161400.0,"Position":293.0,"HyperDash":false},{"StartTime":161460.0,"Position":115.0,"HyperDash":false},{"StartTime":161519.0,"Position":412.0,"HyperDash":false},{"StartTime":161578.0,"Position":506.0,"HyperDash":false},{"StartTime":161638.0,"Position":293.0,"HyperDash":false},{"StartTime":161697.0,"Position":346.0,"HyperDash":false},{"StartTime":161757.0,"Position":117.0,"HyperDash":false},{"StartTime":161816.0,"Position":285.0,"HyperDash":false},{"StartTime":161875.0,"Position":17.0,"HyperDash":false},{"StartTime":161935.0,"Position":238.0,"HyperDash":false},{"StartTime":161994.0,"Position":222.0,"HyperDash":false},{"StartTime":162053.0,"Position":450.0,"HyperDash":false},{"StartTime":162113.0,"Position":67.0,"HyperDash":false},{"StartTime":162172.0,"Position":219.0,"HyperDash":false},{"StartTime":162232.0,"Position":307.0,"HyperDash":false}]},{"StartTime":162421.0,"Objects":[{"StartTime":162421.0,"Position":256.0,"HyperDash":false}]},{"StartTime":162611.0,"Objects":[{"StartTime":162611.0,"Position":168.0,"HyperDash":false}]},{"StartTime":162706.0,"Objects":[{"StartTime":162706.0,"Position":152.0,"HyperDash":false}]},{"StartTime":162801.0,"Objects":[{"StartTime":162801.0,"Position":136.0,"HyperDash":false},{"StartTime":162886.0,"Position":184.369385,"HyperDash":false},{"StartTime":162972.0,"Position":235.213715,"HyperDash":false},{"StartTime":163058.0,"Position":243.058044,"HyperDash":false},{"StartTime":163180.0,"Position":306.314148,"HyperDash":false}]},{"StartTime":163371.0,"Objects":[{"StartTime":163371.0,"Position":392.0,"HyperDash":false},{"StartTime":163447.0,"Position":387.0,"HyperDash":false},{"StartTime":163560.0,"Position":392.0,"HyperDash":false}]},{"StartTime":163751.0,"Objects":[{"StartTime":163751.0,"Position":440.0,"HyperDash":false}]},{"StartTime":163940.0,"Objects":[{"StartTime":163940.0,"Position":344.0,"HyperDash":false}]},{"StartTime":164130.0,"Objects":[{"StartTime":164130.0,"Position":120.0,"HyperDash":false},{"StartTime":164215.0,"Position":96.0444,"HyperDash":false},{"StartTime":164301.0,"Position":55.6488266,"HyperDash":false},{"StartTime":164387.0,"Position":88.2046661,"HyperDash":false},{"StartTime":164509.0,"Position":93.82585,"HyperDash":false}]},{"StartTime":164700.0,"Objects":[{"StartTime":164700.0,"Position":232.0,"HyperDash":false},{"StartTime":164785.0,"Position":275.9556,"HyperDash":false},{"StartTime":164871.0,"Position":299.351166,"HyperDash":false},{"StartTime":164957.0,"Position":295.795349,"HyperDash":false},{"StartTime":165079.0,"Position":258.174164,"HyperDash":false}]},{"StartTime":165270.0,"Objects":[{"StartTime":165270.0,"Position":160.0,"HyperDash":false}]},{"StartTime":165459.0,"Objects":[{"StartTime":165459.0,"Position":160.0,"HyperDash":false}]},{"StartTime":165649.0,"Objects":[{"StartTime":165649.0,"Position":304.0,"HyperDash":false},{"StartTime":165734.0,"Position":324.3694,"HyperDash":false},{"StartTime":165820.0,"Position":364.7582,"HyperDash":false},{"StartTime":165906.0,"Position":401.273468,"HyperDash":false},{"StartTime":166028.0,"Position":446.4695,"HyperDash":false}]},{"StartTime":166219.0,"Objects":[{"StartTime":166219.0,"Position":320.0,"HyperDash":false},{"StartTime":166295.0,"Position":331.608,"HyperDash":false},{"StartTime":166408.0,"Position":376.222565,"HyperDash":false}]},{"StartTime":166599.0,"Objects":[{"StartTime":166599.0,"Position":456.0,"HyperDash":false},{"StartTime":166693.0,"Position":485.888763,"HyperDash":false},{"StartTime":166788.0,"Position":512.0,"HyperDash":false},{"StartTime":166865.0,"Position":508.525848,"HyperDash":false},{"StartTime":166978.0,"Position":456.0,"HyperDash":false}]},{"StartTime":167168.0,"Objects":[{"StartTime":167168.0,"Position":376.0,"HyperDash":false}]},{"StartTime":167358.0,"Objects":[{"StartTime":167358.0,"Position":376.0,"HyperDash":false},{"StartTime":167434.0,"Position":359.082825,"HyperDash":false},{"StartTime":167547.0,"Position":319.0086,"HyperDash":false}]},{"StartTime":167738.0,"Objects":[{"StartTime":167738.0,"Position":240.0,"HyperDash":false},{"StartTime":167814.0,"Position":227.391983,"HyperDash":false},{"StartTime":167927.0,"Position":183.777435,"HyperDash":false}]},{"StartTime":168118.0,"Objects":[{"StartTime":168118.0,"Position":112.0,"HyperDash":false},{"StartTime":168203.0,"Position":82.78144,"HyperDash":false},{"StartTime":168289.0,"Position":79.26619,"HyperDash":false},{"StartTime":168375.0,"Position":41.750946,"HyperDash":false},{"StartTime":168497.0,"Position":0.0,"HyperDash":true}]},{"StartTime":168687.0,"Objects":[{"StartTime":168687.0,"Position":256.0,"HyperDash":false},{"StartTime":168772.0,"Position":272.0,"HyperDash":false},{"StartTime":168858.0,"Position":270.0,"HyperDash":false},{"StartTime":168944.0,"Position":274.0,"HyperDash":false},{"StartTime":169066.0,"Position":256.0,"HyperDash":false}]},{"StartTime":169257.0,"Objects":[{"StartTime":169257.0,"Position":328.0,"HyperDash":false}]},{"StartTime":169447.0,"Objects":[{"StartTime":169447.0,"Position":256.0,"HyperDash":false}]},{"StartTime":169637.0,"Objects":[{"StartTime":169637.0,"Position":184.0,"HyperDash":false}]},{"StartTime":169827.0,"Objects":[{"StartTime":169827.0,"Position":256.0,"HyperDash":false}]},{"StartTime":170016.0,"Objects":[{"StartTime":170016.0,"Position":328.0,"HyperDash":true}]},{"StartTime":170206.0,"Objects":[{"StartTime":170206.0,"Position":32.0,"HyperDash":false},{"StartTime":170291.0,"Position":69.44879,"HyperDash":false},{"StartTime":170377.0,"Position":93.3499146,"HyperDash":false},{"StartTime":170463.0,"Position":153.251038,"HyperDash":false},{"StartTime":170585.0,"Position":203.43634,"HyperDash":true}]},{"StartTime":170776.0,"Objects":[{"StartTime":170776.0,"Position":480.0,"HyperDash":false},{"StartTime":170861.0,"Position":437.5512,"HyperDash":false},{"StartTime":170947.0,"Position":400.6501,"HyperDash":false},{"StartTime":171033.0,"Position":369.748962,"HyperDash":false},{"StartTime":171155.0,"Position":308.56366,"HyperDash":false}]},{"StartTime":171345.0,"Objects":[{"StartTime":171345.0,"Position":328.0,"HyperDash":false}]},{"StartTime":171535.0,"Objects":[{"StartTime":171535.0,"Position":184.0,"HyperDash":true}]},{"StartTime":171725.0,"Objects":[{"StartTime":171725.0,"Position":440.0,"HyperDash":false},{"StartTime":171810.0,"Position":393.6306,"HyperDash":false},{"StartTime":171896.0,"Position":358.7863,"HyperDash":false},{"StartTime":171982.0,"Position":322.941956,"HyperDash":false},{"StartTime":172104.0,"Position":260.0,"HyperDash":false}]},{"StartTime":172295.0,"Objects":[{"StartTime":172295.0,"Position":152.0,"HyperDash":false}]},{"StartTime":172485.0,"Objects":[{"StartTime":172485.0,"Position":192.0,"HyperDash":false}]},{"StartTime":172675.0,"Objects":[{"StartTime":172675.0,"Position":320.0,"HyperDash":false}]},{"StartTime":172864.0,"Objects":[{"StartTime":172864.0,"Position":360.0,"HyperDash":false}]},{"StartTime":173054.0,"Objects":[{"StartTime":173054.0,"Position":320.0,"HyperDash":false}]},{"StartTime":173244.0,"Objects":[{"StartTime":173244.0,"Position":192.0,"HyperDash":false}]},{"StartTime":173434.0,"Objects":[{"StartTime":173434.0,"Position":487.0,"HyperDash":false},{"StartTime":173528.0,"Position":53.0,"HyperDash":false},{"StartTime":173623.0,"Position":40.0,"HyperDash":false},{"StartTime":173718.0,"Position":153.0,"HyperDash":false},{"StartTime":173813.0,"Position":79.0,"HyperDash":false},{"StartTime":173908.0,"Position":488.0,"HyperDash":false},{"StartTime":174003.0,"Position":396.0,"HyperDash":false},{"StartTime":174098.0,"Position":428.0,"HyperDash":false},{"StartTime":174193.0,"Position":59.0,"HyperDash":false},{"StartTime":174288.0,"Position":255.0,"HyperDash":false},{"StartTime":174383.0,"Position":294.0,"HyperDash":false},{"StartTime":174478.0,"Position":354.0,"HyperDash":false},{"StartTime":174573.0,"Position":270.0,"HyperDash":false},{"StartTime":174668.0,"Position":362.0,"HyperDash":false},{"StartTime":174763.0,"Position":255.0,"HyperDash":false},{"StartTime":174858.0,"Position":203.0,"HyperDash":false},{"StartTime":174953.0,"Position":67.0,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/42587.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/42587.osu new file mode 100644 index 0000000000..41366eab43 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/42587.osu @@ -0,0 +1,528 @@ +osu file format v6 + +[General] +StackLeniency: 0.7 +Mode: 0 + +[Difficulty] +HPDrainRate:8 +CircleSize:5 +OverallDifficulty:8 +SliderMultiplier:1.8 +SliderTickRate:0.5 + +[Events] +//Break Periods +2,99204,110406 +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples +//Background Colour Transformations +3,100,0,0,0 + +[TimingPoints] +270,379.746835443038,4,2,1,85,1,0 +48782,-100,4,2,0,50,0,0 +48972,-100,4,2,0,85,0,0 +60744,-100,4,2,1,85,0,0 +60982,-100,4,2,1,65,0,0 +61171,-100,4,2,1,85,0,0 +71092,-100,4,2,1,40,0,0 +71282,-100,4,2,1,85,0,0 +71567,-100,4,2,0,60,0,0 +71852,-100,4,2,1,85,0,0 +72232,-100,4,2,1,85,0,0 +73086,-100,4,1,0,60,0,0 +74035,-100,4,1,1,50,0,0 +74890,-100,4,2,1,85,0,0 +81061,-100,4,2,1,60,0,0 +81251,-100,4,2,1,85,0,0 +82580,-100,4,2,1,60,0,0 +82770,-100,4,2,1,85,0,0 +86804,-100,4,2,1,60,0,0 +86947,-100,4,2,1,85,0,0 +87137,-100,4,2,1,60,0,0 +87326,-100,4,2,1,85,0,0 +88656,-100,4,2,1,60,0,0 +88845,-100,4,2,1,85,0,0 +92643,-100,4,2,1,60,0,0 +92833,-100,4,2,1,85,0,0 +93592,-100,4,2,1,60,0,0 +93782,-100,4,2,1,86,0,0 +94162,-100,4,2,1,60,0,0 +94352,-100,4,2,1,85,0,0 +95111,-100,4,2,1,60,0,0 +95301,-100,4,2,1,85,0,0 +98624,-100,4,1,1,70,0,0 +110966,-100,4,1,0,60,0,0 +112390,-100,4,2,1,85,0,0 +118371,-100,4,2,1,75,0,0 +118751,-100,4,2,1,65,0,0 +119130,-100,4,2,1,55,0,0 +119510,-100,4,2,1,85,0,0 +135934,-100,4,1,1,80,0,0 +136314,-100,4,2,1,85,0,0 +136883,-100,4,2,1,65,0,0 +137073,-100,4,2,1,85,0,0 +147516,-100,4,2,0,60,0,0 +147801,-100,4,2,1,85,0,0 +149035,-100,4,1,1,65,0,0 +149605,-100,4,1,1,75,0,0 +150459,-100,4,2,1,70,0,0 +150744,-100,4,2,1,85,0,0 +150934,-100,4,2,1,70,0,0 +151124,-100,4,2,1,85,0,0 +157010,-100,4,2,1,70,0,0 +157200,-100,4,2,1,85,0,0 +158529,-100,4,2,1,70,0,0 +158719,-100,4,2,1,85,0,0 +162754,-100,4,2,1,70,0,0 +162896,-100,4,2,1,85,0,0 +163466,-100,4,2,1,70,0,0 +163656,-100,4,2,1,85,0,0 +164035,-100,4,2,1,70,0,0 +164225,-100,4,2,1,85,0,0 +164605,-100,4,2,1,70,0,0 +164795,-100,4,2,1,85,0,0 +168592,-100,4,2,1,70,0,0 +168782,-100,4,2,1,85,0,0 +169542,-100,4,2,1,70,0,0 +169732,-100,4,2,1,85,0,0 +170111,-100,4,2,1,70,0,0 +170301,-100,4,2,1,85,0,0 +170681,-100,4,2,1,70,0,0 +170871,-100,4,2,1,85,0,0 +173339,-100,4,1,0,51,0,0 + +[HitObjects] +376,288,24383,1,0 +392,264,24478,1,0 +408,240,24573,1,8 +448,160,24763,2,0,B|344:160,1,90,0|0 +280,120,25143,1,0 +232,200,25333,1,0 +152,160,25523,2,0,B|56:160,1,90,0|0 +32,248,25902,1,0 +96,312,26092,6,0,L|144:376|264:376,2,180 +96,224,27042,1,0 +176,264,27232,2,0,B|288:264,1,90 +448,264,27801,1,0 +360,264,27991,1,0 +192,192,28371,1,0 +280,192,28561,1,0 +368,192,28751,1,0 +456,192,28940,1,0 +456,192,29130,6,0,L|408:128|288:128,2,180 +456,280,30080,1,0 +376,240,30270,2,0,B|256:240,1,90 +112,280,30839,1,0 +176,216,31029,1,0 +112,152,31219,1,0 +112,152,31314,1,0 +112,152,31409,1,0 +176,88,31599,1,0 +240,152,31788,1,0 +176,216,31978,1,0 +240,280,32168,6,0,L|296:328|416:328,2,180 +240,192,33118,1,0 +328,192,33307,2,0,L|328:152|288:96,1,90 +136,32,33877,1,0 +80,104,34067,1,0 +24,176,34257,1,0 +24,200,34352,1,0 +24,224,34447,1,0 +40,240,34542,1,0 +56,248,34637,1,0 +144,248,34826,1,0 +232,248,35016,1,0 +376,248,35206,6,0,L|408:248|464:288,2,90,0|0|0 +232,248,35776,2,0,L|200:248|144:288,1,90 +304,352,36156,1,0 +304,248,36345,2,0,B|304:152,1,90 +112,80,36725,6,0,B|16:128,1,90,8|0 +112,160,37105,2,0,B|16:208,1,90 +112,240,37485,1,0 +112,328,37675,6,0,B|24:376,2,90 +112,240,38244,1,0 +32,200,38434,1,0 +112,160,38624,1,0 +32,120,38814,1,0 +112,80,39004,1,0 +200,80,39194,6,0,B|304:80,1,90 +384,168,39573,2,0,B|272:168,1,90 +200,256,39953,2,0,B|296:256,3,90 +408,200,40713,5,0 +360,112,40902,1,0 +280,56,41092,2,0,B|192:24|88:64,1,180 +168,128,41662,2,0,B|120:136|64:176,2,90 +264,128,42232,2,0,B|384:128|448:232,1,180 +320,224,42801,2,0,B|280:312,1,90,0|0 +184,336,43181,2,0,B|232:248,1,90 +227,256,43561,1,0 +192,176,43751,6,0,B|144:256,2,90,0|0|0 +128,112,44320,2,0,B|184:32|304:40,1,180 +376,40,44890,1,0 +440,208,45270,5,0 +384,280,45459,1,0 +304,312,45649,1,0 +216,328,45839,2,0,B|141:308|112:292|56:216,1,180 +56,144,46409,1,0 +216,64,46788,5,0 +296,96,46978,1,0 +216,144,47168,1,0 +296,176,47358,1,0 +136,232,47738,1,0 +376,296,48118,1,0 +136,360,48497,1,0 +376,184,48877,6,0,B|256:184,1,90,4|0 +192,184,49257,1,0 +128,120,49447,1,0 +216,120,49637,2,0,B|328:120,3,90 +400,120,50396,2,0,B|472:200|400:304,1,180 +336,232,50966,2,0,B|264:320,1,90 +208,360,51345,5,0 +168,280,51535,1,0 +120,360,51725,1,0 +72,280,51915,2,0,B|48:176|136:112,1,180 +216,88,52485,1,0 +304,112,52675,1,0 +232,168,52864,5,0 +312,200,53054,1,0 +288,288,53244,2,0,B|368:320|456:232,1,180 +392,176,53814,2,0,B|336:72,1,90 +280,152,54194,2,0,B|184:80,1,90,0|0 +176,192,54573,1,0 +104,136,54763,2,0,B|48:248|104:336|208:344,1,270 +216,256,55523,1,0 +264,184,55713,1,0 +352,184,55902,5,0 +440,136,56092,1,0 +352,88,56282,1,0 +264,88,56472,2,0,B|144:-16|8:96,1,270 +160,152,57421,5,0 +32,216,57611,1,0 +160,280,57801,1,0 +248,312,57991,2,0,B|368:416|504:304,1,270 +360,192,58940,5,0 +256,192,59130,1,0 +152,192,59320,1,0 +168,96,59510,2,0,B|256:56|368:104,3,180 +408,136,60839,1,4 +408,136,60934,1,4 +408,136,61029,6,0,B|352:216|296:296,1,180,8|4 +304,283,61599,1,0 +216,280,61788,2,0,B|263:212|319:132,1,180,0|4 +319,132,62358,1,0 +240,96,62548,6,0,B|312:0,2,90,0|0|4 +192,168,63118,2,0,B|136:248|80:328,1,180,0|0 +176,312,63687,1,4 +264,312,63877,1,0 +352,312,64067,6,0,B|448:248|416:120,1,180,0|4 +352,208,64637,1,0 +272,168,64826,2,0,B|344:72,1,90 +326,96,65206,1,4 +272,24,65396,2,0,B|160:56|168:184,1,180 +104,96,65966,1,4 +48,168,66156,1,0 +104,232,66345,1,0 +56,312,66535,1,0 +80,328,66630,1,0 +104,344,66725,1,4 +192,312,66915,1,0 +280,344,67105,6,0,B|436:254,1,180,0|4 +448,168,67675,1,0 +456,80,67864,2,0,B|299:169,1,180,0|4 +288,256,68434,1,0 +208,296,68624,5,0 +128,256,68814,1,0 +48,296,69004,1,4 +128,256,69194,2,0,B|208:192|192:80,1,180 +256,32,69763,1,4 +256,32,69953,1,0 +318,96,70143,6,0,B|304:192|384:256,1,180,0|4 +256,120,70902,2,0,B|224:184|304:200|248:264,2,135 +256,32,71662,5,4 +256,32,72042,1,4 +160,144,72421,5,2 +224,144,72611,1,2 +288,144,72801,1,2 +352,144,72991,1,2 +408,216,73181,5,0 +304,216,73371,1,0 +208,216,73561,1,0 +112,216,73751,1,0 +160,288,73940,5,0 +224,288,74130,1,8 +248,288,74225,1,8 +272,288,74320,1,8 +296,288,74415,1,0 +320,288,74510,1,0 +344,288,74605,1,4 +368,288,74700,6,0,B|464:256|480:136,1,180,4|4 +368,64,75270,1,0 +296,176,75459,2,0,B|240:208|184:152,1,90 +144,64,75839,1,4 +168,328,76029,6,0,B|224:344|262:347|352:328,1,180,0|0 +344,192,76599,2,0,B|282:175|232:167|144:200,1,180,4|0 +256,256,77168,1,0 +256,256,77358,1,4 +424,256,77548,6,0,B|444:180|440:128|424:72,1,180 +296,32,78118,2,0,B|336:144,1,90,4|0 +240,264,78497,2,0,B|280:152,1,90 +168,32,78877,2,0,B|200:120,1,90,4|0 +104,264,79257,2,0,B|136:176,1,90 +48,120,79637,2,0,B|8:16,2,90,4|0|0 +48,120,80206,1,4 +48,120,80396,1,4 +48,256,80586,6,0,B|72:360|192:360,1,180,0|0 +334,359,81156,2,0,B|440:360|464:256,1,180,12|0 +256,192,81725,1,0 +256,192,81915,1,4 +48,128,82105,6,0,B|72:24|192:24,1,180,0|0 +334,25,82675,2,0,B|440:24|464:128,1,180,12|0 +256,192,83244,1,0 +256,192,83434,1,4 +177,24,83624,6,0,B|72:24|48:128,1,180 +240,96,84194,2,0,B|128:120,1,90,4|0 +40,208,84573,2,0,B|160:184,1,90,0|0 +280,216,84953,2,0,B|184:240,1,90,4|0 +256,208,85333,12,4,86282 +256,192,86472,5,4 +128,80,86662,5,4 +152,64,86757,1,4 +176,48,86852,2,0,B|288:48,1,90,12|0 +360,56,87232,2,0,L|288:112|176:112,1,180,12|0 +136,176,87801,2,0,B|240:176,1,90,0|4 +440,352,88181,6,0,L|389:352|344:312|272:360,1,180,0|0 +72,352,88751,2,0,L|122:352|168:312|240:360,1,180,12|0 +256,192,89320,2,0,B|256:240,2,45,0|0|4 +488,48,89700,6,0,B|389:33|304:88,1,180 +256,192,90270,1,4 +160,280,90459,1,0 +64,192,90649,1,0 +160,104,90839,1,0 +256,192,91029,1,4 +352,280,91219,1,0 +448,192,91409,1,0 +352,104,91599,1,0 +256,192,91788,1,4 +256,64,91978,1,0 +256,192,92168,1,0 +256,192,92358,2,0,B|256:304,1,90,4|4 +32,32,92738,6,0,L|144:32|200:88,1,180,8|0 +64,128,93307,2,0,B|127:191,1,90,4|0 +256,152,93687,2,0,B|319:215,1,90,8|0 +424,304,94067,1,4 +256,368,94257,6,0,L|192:328|192:216,1,180,8|0 +328,224,94826,2,0,B|440:224,2,90,4|0|8 +328,88,95396,1,0 +328,88,95586,1,4 +192,88,95776,6,0,B|104:67|12:88,1,180,0|2 +56,192,96345,2,0,B|176:192,1,90,6|2 +232,232,96725,1,2 +280,152,96915,1,2 +360,192,97105,2,12,B|472:192,1,90,6|2 +256,208,97485,12,4,99004 +256,352,111156,5,4 +256,192,111915,1,0 +256,192,112105,1,0 +256,192,112295,1,0 +256,104,112485,1,0 +328,48,112675,6,0,B|416:48|456:88|456:160,1,180 +456,232,113244,1,0 +456,320,113434,1,0 +368,336,113624,2,0,B|304:336|272:392,2,90 +456,320,114194,2,0,B|416:256|376:232|288:224,1,180 +256,296,114763,2,0,B|200:288|160:240,1,90 +112,192,115143,5,0 +176,256,115333,1,0 +240,192,115523,1,0 +176,128,115713,2,0,B|224:48|344:48,1,180 +296,128,116282,1,0 +360,192,116472,1,0 +448,192,116662,6,0,B|368:272,1,90 +384,352,117042,2,0,B|264:360|216:232,1,180 +280,192,117611,2,0,B|323:159|280:104,1,90 +192,112,117991,2,0,B|155:158|198:191,1,90 +248,360,118561,1,0 +248,296,118940,1,0 +248,232,119320,1,0 +448,240,119700,5,0 +384,304,119890,1,0 +320,240,120080,1,0 +256,304,120270,2,0,B|176:336|48:296,1,180 +80,304,120839,1,0 +32,32,121219,5,0 +120,136,121409,1,0 +208,32,121599,1,0 +296,136,121788,2,0,B|376:104|504:144,1,180 +472,136,122358,1,0 +208,192,122738,5,0 +256,112,122928,1,0 +304,192,123117,1,0 +256,272,123307,1,0 +256,48,123687,1,0 +256,336,124067,1,0 +256,248,124257,5,4 +256,160,124447,1,4 +256,72,124637,1,4 +256,72,124732,1,4 +256,72,124826,2,4,B|376:72|376:176,1,180,0|4 +456,224,125396,1,0 +392,288,125586,1,0 +304,288,125776,6,0,B|200:352,1,90,0|4 +192,248,126156,1,0 +160,336,126345,2,0,B|48:336|24:192,1,180,0|4 +120,224,126915,2,0,B|48:120,1,90 +136,96,127295,2,0,B|72:8,2,90,0|4|0 +184,168,127864,2,0,B|312:344,1,180,0|4 +384,312,128434,1,0 +448,256,128624,1,0 +448,168,128814,6,0,B|344:112,1,90,0|4 +440,72,129194,2,0,B|368:40|328:40|248:80,1,180 +208,136,129763,1,4 +128,184,129953,1,0 +208,232,130143,1,0 +288,184,130333,2,0,B|400:184,1,90,0|4 +448,248,130713,2,0,B|352:248,1,90 +176,248,131282,1,4 +360,248,131662,1,0 +288,192,131852,5,0 +200,192,132042,1,4 +112,192,132232,1,0 +96,288,132421,2,0,B|0:256|-32:144|112:88,1,270 +224,192,133371,5,0 +312,192,133561,1,4 +400,192,133751,1,0 +416,288,133940,2,0,B|512:256|544:144|400:88,1,270 +80,192,134890,5,0 +160,152,135080,1,4 +200,232,135270,1,0 +280,192,135459,1,0 +464,192,135839,1,4 +376,192,136029,1,0 +376,192,136219,1,0 +280,192,136409,1,4 +280,192,136599,1,4 +56,216,136978,6,0,B|144:272|256:200,1,180,8|4 +456,168,137738,2,0,B|368:112|256:184,1,180,0|4 +256,32,138497,5,0 +200,104,138687,1,0 +256,176,138877,1,4 +312,104,139067,2,0,B|424:104,1,90 +400,192,139447,2,0,B|504:192,2,90,0|4|0 +400,280,140016,6,0,B|400:368|232:352,1,180,0|4 +224,272,140586,1,0 +296,216,140776,1,0 +224,168,140966,1,0 +296,112,141156,1,4 +256,32,141345,2,0,B|107:25|115:113,1,180,0|0 +112,200,141915,2,0,B|112:312,2,90,4|0|0 +112,112,142485,1,0 +112,112,142580,1,0 +112,112,142675,1,4 +112,24,142864,1,0 +232,8,143054,6,0,B|152:96|248:208,1,180,0|4 +280,376,143814,2,0,B|360:288|264:176,1,180,0|4 +256,32,144573,5,0 +344,32,144763,1,0 +416,88,144953,1,4 +416,176,145143,2,0,B|232:176,1,180 +144,176,145713,1,4 +80,112,145902,1,0 +16,176,146092,5,0 +256,304,146472,1,4 +496,176,146852,1,0 +352,32,147137,1,0 +160,32,147421,1,0 +256,160,147611,5,4 +256,224,147991,1,4 +256,96,148371,5,2 +368,192,148561,1,2 +256,288,148751,1,2 +144,192,148940,1,2 +288,144,149130,5,0 +312,168,149225,1,8 +336,192,149320,1,0 +312,216,149415,1,8 +288,240,149510,1,0 +224,144,149700,5,0 +200,168,149795,1,8 +176,192,149890,1,0 +200,216,149985,1,8 +224,240,150080,1,0 +256,256,150175,1,8 +256,288,150270,1,0 +168,24,150649,6,4,L|152:56|168:80|168:128,1,90,8|0 +344,24,151029,2,0,L|360:56|344:80|344:128,1,90,12|0 +256,264,151409,1,8 +256,80,151599,1,0 +256,80,151694,1,0 +256,80,151788,1,4 +464,224,151978,6,0,L|424:240|424:240|440:280|328:280,1,180,0|0 +48,280,152548,2,0,L|88:264|88:264|72:224|184:224,1,180,4|0 +256,80,153118,1,0 +256,80,153213,1,0 +256,80,153307,1,4 +168,312,153497,6,0,B|256:368|368:296,1,180 +168,248,154067,2,0,B|96:176,1,90,4|0 +344,248,154447,2,0,B|416:176,1,90 +168,160,154826,2,0,B|96:88,1,90,4|0 +344,160,155206,2,0,B|416:88,1,90 +256,352,155586,2,0,B|280:312|216:296|272:248,2,90,4|0|0 +256,352,156156,1,4 +256,352,156345,1,4 +96,32,156535,6,0,L|208:32|264:120,1,180,0|0 +152,96,157105,2,0,L|152:144|112:184,1,90,12|0 +32,176,157485,2,0,L|32:224|64:256,1,90 +152,256,157864,1,4 +416,352,158054,6,0,L|304:352|248:264,1,180 +360,288,158624,2,0,L|360:240|400:200,1,90,12|0 +480,208,159004,2,0,L|480:160|448:128,1,90 +360,128,159383,1,4 +255,236,159573,6,0,B|255:52,1,180 +256,56,160143,1,4 +376,120,160333,1,0 +376,264,160523,1,0 +256,328,160713,1,0 +136,264,160902,1,4 +136,120,161092,1,0 +256,208,161282,12,4,162232 +256,192,162421,5,4 +168,320,162611,5,4 +152,336,162706,1,4 +136,352,162801,2,0,L|264:352|320:312,1,180,12|4 +392,352,163371,2,0,B|392:248,1,90,0|8 +440,184,163751,1,0 +344,184,163940,1,4 +120,32,164130,6,0,B|8:64|120:216,1,180,8|0 +232,136,164700,2,0,B|344:168|232:320,1,180,8|0 +160,360,165270,1,0 +160,360,165459,1,4 +304,360,165649,6,0,L|384:360|448:280,1,180 +320,288,166219,2,0,B|384:208,1,90,4|0 +456,120,166599,2,0,B|512:50,2,90,0|0|4 +376,216,167168,1,0 +376,88,167358,2,0,B|304:176,1,90 +240,120,167738,2,0,B|176:200,1,90,4|0 +112,144,168118,2,0,B|-16:304,1,180,0|4 +256,360,168687,6,0,B|256:168,1,180,8|0 +328,96,169257,1,4 +256,16,169447,1,0 +184,96,169637,1,8 +256,176,169827,1,0 +328,96,170016,1,4 +32,304,170206,6,0,B|232:240,1,180,8|0 +480,80,170776,2,0,B|280:144,1,180,8|0 +328,280,171345,1,0 +184,104,171535,1,4 +440,192,171725,6,4,B|248:192,1,180,0|2 +152,192,172295,1,2 +192,72,172485,1,2 +320,72,172675,1,2 +360,192,172864,1,2 +320,312,173054,1,2 +192,312,173244,1,2 +256,208,173434,12,4,174953 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/50859-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/50859-expected-conversion.json new file mode 100644 index 0000000000..ee89090492 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/50859-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":179.0,"Objects":[{"StartTime":179.0,"Position":120.0,"HyperDash":false}]},{"StartTime":786.0,"Objects":[{"StartTime":786.0,"Position":311.0,"HyperDash":false},{"StartTime":852.0,"Position":322.1386,"HyperDash":false},{"StartTime":919.0,"Position":352.673279,"HyperDash":false},{"StartTime":986.0,"Position":387.207916,"HyperDash":false},{"StartTime":1089.0,"Position":431.0,"HyperDash":false}]},{"StartTime":1392.0,"Objects":[{"StartTime":1392.0,"Position":431.0,"HyperDash":false},{"StartTime":1458.0,"Position":419.8614,"HyperDash":false},{"StartTime":1525.0,"Position":395.326721,"HyperDash":false},{"StartTime":1592.0,"Position":352.792084,"HyperDash":false},{"StartTime":1695.0,"Position":311.0,"HyperDash":false}]},{"StartTime":1998.0,"Objects":[{"StartTime":1998.0,"Position":215.0,"HyperDash":false}]},{"StartTime":2301.0,"Objects":[{"StartTime":2301.0,"Position":119.0,"HyperDash":false},{"StartTime":2376.0,"Position":147.702972,"HyperDash":false},{"StartTime":2452.0,"Position":163.801971,"HyperDash":false},{"StartTime":2528.0,"Position":200.900986,"HyperDash":false},{"StartTime":2604.0,"Position":239.0,"HyperDash":false},{"StartTime":2679.0,"Position":264.702972,"HyperDash":false},{"StartTime":2755.0,"Position":312.801971,"HyperDash":false},{"StartTime":2831.0,"Position":332.901,"HyperDash":false},{"StartTime":2907.0,"Position":359.0,"HyperDash":false},{"StartTime":2973.0,"Position":390.1386,"HyperDash":false},{"StartTime":3040.0,"Position":392.673279,"HyperDash":false},{"StartTime":3107.0,"Position":429.207916,"HyperDash":false},{"StartTime":3210.0,"Position":479.0,"HyperDash":false}]},{"StartTime":3513.0,"Objects":[{"StartTime":3513.0,"Position":478.0,"HyperDash":false}]},{"StartTime":3816.0,"Objects":[{"StartTime":3816.0,"Position":382.0,"HyperDash":false},{"StartTime":3882.0,"Position":373.8614,"HyperDash":false},{"StartTime":3949.0,"Position":346.326721,"HyperDash":false},{"StartTime":4016.0,"Position":315.792084,"HyperDash":false},{"StartTime":4119.0,"Position":262.0,"HyperDash":false}]},{"StartTime":4422.0,"Objects":[{"StartTime":4422.0,"Position":166.0,"HyperDash":false},{"StartTime":4488.0,"Position":158.0,"HyperDash":false},{"StartTime":4555.0,"Position":149.0,"HyperDash":false},{"StartTime":4622.0,"Position":179.0,"HyperDash":false},{"StartTime":4725.0,"Position":166.0,"HyperDash":false}]},{"StartTime":5331.0,"Objects":[{"StartTime":5331.0,"Position":166.0,"HyperDash":false}]},{"StartTime":5634.0,"Objects":[{"StartTime":5634.0,"Position":261.0,"HyperDash":false},{"StartTime":5691.0,"Position":278.649017,"HyperDash":false},{"StartTime":5785.0,"Position":321.0,"HyperDash":false}]},{"StartTime":6089.0,"Objects":[{"StartTime":6089.0,"Position":321.0,"HyperDash":false},{"StartTime":6146.0,"Position":325.0,"HyperDash":false},{"StartTime":6240.0,"Position":321.0,"HyperDash":false}]},{"StartTime":6543.0,"Objects":[{"StartTime":6543.0,"Position":321.0,"HyperDash":false}]},{"StartTime":6998.0,"Objects":[{"StartTime":6998.0,"Position":465.0,"HyperDash":false},{"StartTime":7055.0,"Position":450.0,"HyperDash":false},{"StartTime":7149.0,"Position":465.0,"HyperDash":false}]},{"StartTime":7452.0,"Objects":[{"StartTime":7452.0,"Position":369.0,"HyperDash":false},{"StartTime":7518.0,"Position":365.0,"HyperDash":false},{"StartTime":7585.0,"Position":368.0,"HyperDash":false},{"StartTime":7652.0,"Position":383.0,"HyperDash":false},{"StartTime":7755.0,"Position":369.0,"HyperDash":false}]},{"StartTime":8058.0,"Objects":[{"StartTime":8058.0,"Position":464.0,"HyperDash":false}]},{"StartTime":8361.0,"Objects":[{"StartTime":8361.0,"Position":464.0,"HyperDash":false},{"StartTime":8427.0,"Position":422.8614,"HyperDash":false},{"StartTime":8494.0,"Position":392.326721,"HyperDash":false},{"StartTime":8561.0,"Position":368.792084,"HyperDash":false},{"StartTime":8664.0,"Position":344.0,"HyperDash":false}]},{"StartTime":8967.0,"Objects":[{"StartTime":8967.0,"Position":248.0,"HyperDash":false}]},{"StartTime":9270.0,"Objects":[{"StartTime":9270.0,"Position":200.0,"HyperDash":false}]},{"StartTime":9573.0,"Objects":[{"StartTime":9573.0,"Position":296.0,"HyperDash":false}]},{"StartTime":10180.0,"Objects":[{"StartTime":10180.0,"Position":275.0,"HyperDash":false}]},{"StartTime":10483.0,"Objects":[{"StartTime":10483.0,"Position":179.0,"HyperDash":false}]},{"StartTime":10786.0,"Objects":[{"StartTime":10786.0,"Position":179.0,"HyperDash":false},{"StartTime":10852.0,"Position":218.138611,"HyperDash":false},{"StartTime":10919.0,"Position":248.673264,"HyperDash":false},{"StartTime":10986.0,"Position":257.207916,"HyperDash":false},{"StartTime":11089.0,"Position":299.0,"HyperDash":false}]},{"StartTime":11392.0,"Objects":[{"StartTime":11392.0,"Position":299.0,"HyperDash":false}]},{"StartTime":11695.0,"Objects":[{"StartTime":11695.0,"Position":203.0,"HyperDash":false},{"StartTime":11752.0,"Position":173.351,"HyperDash":false},{"StartTime":11846.0,"Position":143.0,"HyperDash":false}]},{"StartTime":11998.0,"Objects":[{"StartTime":11998.0,"Position":94.0,"HyperDash":false}]},{"StartTime":12301.0,"Objects":[{"StartTime":12301.0,"Position":94.0,"HyperDash":false}]},{"StartTime":12604.0,"Objects":[{"StartTime":12604.0,"Position":189.0,"HyperDash":false}]},{"StartTime":13513.0,"Objects":[{"StartTime":13513.0,"Position":476.0,"HyperDash":false}]},{"StartTime":13816.0,"Objects":[{"StartTime":13816.0,"Position":380.0,"HyperDash":false}]},{"StartTime":14725.0,"Objects":[{"StartTime":14725.0,"Position":272.0,"HyperDash":false},{"StartTime":14782.0,"Position":248.351,"HyperDash":false},{"StartTime":14876.0,"Position":212.0,"HyperDash":false}]},{"StartTime":15028.0,"Objects":[{"StartTime":15028.0,"Position":177.0,"HyperDash":false},{"StartTime":15085.0,"Position":196.0,"HyperDash":false},{"StartTime":15179.0,"Position":177.0,"HyperDash":false}]},{"StartTime":15331.0,"Objects":[{"StartTime":15331.0,"Position":225.0,"HyperDash":false}]},{"StartTime":15483.0,"Objects":[{"StartTime":15483.0,"Position":273.0,"HyperDash":false}]},{"StartTime":15786.0,"Objects":[{"StartTime":15786.0,"Position":273.0,"HyperDash":false}]},{"StartTime":16089.0,"Objects":[{"StartTime":16089.0,"Position":273.0,"HyperDash":false}]},{"StartTime":16846.0,"Objects":[{"StartTime":16846.0,"Position":33.0,"HyperDash":false},{"StartTime":16903.0,"Position":27.0,"HyperDash":false},{"StartTime":16997.0,"Position":33.0,"HyperDash":false}]},{"StartTime":17149.0,"Objects":[{"StartTime":17149.0,"Position":33.0,"HyperDash":false}]},{"StartTime":17755.0,"Objects":[{"StartTime":17755.0,"Position":224.0,"HyperDash":false}]},{"StartTime":18967.0,"Objects":[{"StartTime":18967.0,"Position":277.0,"HyperDash":false}]},{"StartTime":19119.0,"Objects":[{"StartTime":19119.0,"Position":228.0,"HyperDash":false}]},{"StartTime":19270.0,"Objects":[{"StartTime":19270.0,"Position":181.0,"HyperDash":false}]},{"StartTime":19573.0,"Objects":[{"StartTime":19573.0,"Position":181.0,"HyperDash":false}]},{"StartTime":19876.0,"Objects":[{"StartTime":19876.0,"Position":181.0,"HyperDash":false}]},{"StartTime":20786.0,"Objects":[{"StartTime":20786.0,"Position":469.0,"HyperDash":false}]},{"StartTime":21089.0,"Objects":[{"StartTime":21089.0,"Position":373.0,"HyperDash":false}]},{"StartTime":21392.0,"Objects":[{"StartTime":21392.0,"Position":277.0,"HyperDash":false}]},{"StartTime":21998.0,"Objects":[{"StartTime":21998.0,"Position":243.0,"HyperDash":false}]},{"StartTime":22149.0,"Objects":[{"StartTime":22149.0,"Position":243.0,"HyperDash":false}]},{"StartTime":22301.0,"Objects":[{"StartTime":22301.0,"Position":243.0,"HyperDash":false}]},{"StartTime":22452.0,"Objects":[{"StartTime":22452.0,"Position":290.0,"HyperDash":false},{"StartTime":22509.0,"Position":295.0,"HyperDash":false},{"StartTime":22603.0,"Position":290.0,"HyperDash":false}]},{"StartTime":22755.0,"Objects":[{"StartTime":22755.0,"Position":290.0,"HyperDash":false}]},{"StartTime":23058.0,"Objects":[{"StartTime":23058.0,"Position":385.0,"HyperDash":false}]},{"StartTime":23361.0,"Objects":[{"StartTime":23361.0,"Position":385.0,"HyperDash":false}]},{"StartTime":24119.0,"Objects":[{"StartTime":24119.0,"Position":213.0,"HyperDash":false},{"StartTime":24176.0,"Position":203.351,"HyperDash":false},{"StartTime":24270.0,"Position":153.0,"HyperDash":false}]},{"StartTime":24422.0,"Objects":[{"StartTime":24422.0,"Position":104.0,"HyperDash":false}]},{"StartTime":25028.0,"Objects":[{"StartTime":25028.0,"Position":295.0,"HyperDash":false}]},{"StartTime":26240.0,"Objects":[{"StartTime":26240.0,"Position":56.0,"HyperDash":false}]},{"StartTime":26392.0,"Objects":[{"StartTime":26392.0,"Position":56.0,"HyperDash":false}]},{"StartTime":26543.0,"Objects":[{"StartTime":26543.0,"Position":56.0,"HyperDash":false}]},{"StartTime":26846.0,"Objects":[{"StartTime":26846.0,"Position":56.0,"HyperDash":false}]},{"StartTime":27149.0,"Objects":[{"StartTime":27149.0,"Position":151.0,"HyperDash":false}]},{"StartTime":28058.0,"Objects":[{"StartTime":28058.0,"Position":438.0,"HyperDash":false},{"StartTime":28124.0,"Position":455.0,"HyperDash":false},{"StartTime":28191.0,"Position":455.0,"HyperDash":false},{"StartTime":28258.0,"Position":453.0,"HyperDash":false},{"StartTime":28361.0,"Position":438.0,"HyperDash":false}]},{"StartTime":29270.0,"Objects":[{"StartTime":29270.0,"Position":184.0,"HyperDash":false},{"StartTime":29336.0,"Position":227.138611,"HyperDash":false},{"StartTime":29403.0,"Position":245.673264,"HyperDash":false},{"StartTime":29470.0,"Position":246.207916,"HyperDash":false},{"StartTime":29573.0,"Position":304.0,"HyperDash":false}]},{"StartTime":29876.0,"Objects":[{"StartTime":29876.0,"Position":399.0,"HyperDash":false}]},{"StartTime":30180.0,"Objects":[{"StartTime":30180.0,"Position":399.0,"HyperDash":false}]},{"StartTime":30483.0,"Objects":[{"StartTime":30483.0,"Position":303.0,"HyperDash":false},{"StartTime":30549.0,"Position":281.8614,"HyperDash":false},{"StartTime":30616.0,"Position":238.326736,"HyperDash":false},{"StartTime":30683.0,"Position":208.792084,"HyperDash":false},{"StartTime":30786.0,"Position":183.0,"HyperDash":false}]},{"StartTime":31089.0,"Objects":[{"StartTime":31089.0,"Position":115.0,"HyperDash":false},{"StartTime":31155.0,"Position":159.138611,"HyperDash":false},{"StartTime":31222.0,"Position":159.673264,"HyperDash":false},{"StartTime":31289.0,"Position":210.207916,"HyperDash":false},{"StartTime":31392.0,"Position":235.0,"HyperDash":false}]},{"StartTime":31695.0,"Objects":[{"StartTime":31695.0,"Position":330.0,"HyperDash":false}]},{"StartTime":31998.0,"Objects":[{"StartTime":31998.0,"Position":425.0,"HyperDash":false}]},{"StartTime":32301.0,"Objects":[{"StartTime":32301.0,"Position":425.0,"HyperDash":false},{"StartTime":32367.0,"Position":401.8614,"HyperDash":false},{"StartTime":32434.0,"Position":362.326721,"HyperDash":false},{"StartTime":32501.0,"Position":346.792084,"HyperDash":false},{"StartTime":32604.0,"Position":305.0,"HyperDash":false}]},{"StartTime":32907.0,"Objects":[{"StartTime":32907.0,"Position":209.0,"HyperDash":false},{"StartTime":32973.0,"Position":172.861389,"HyperDash":false},{"StartTime":33040.0,"Position":156.326736,"HyperDash":false},{"StartTime":33107.0,"Position":111.792084,"HyperDash":false},{"StartTime":33210.0,"Position":89.0,"HyperDash":false}]},{"StartTime":33513.0,"Objects":[{"StartTime":33513.0,"Position":89.0,"HyperDash":false}]},{"StartTime":33816.0,"Objects":[{"StartTime":33816.0,"Position":184.0,"HyperDash":false}]},{"StartTime":34119.0,"Objects":[{"StartTime":34119.0,"Position":279.0,"HyperDash":false}]},{"StartTime":34422.0,"Objects":[{"StartTime":34422.0,"Position":374.0,"HyperDash":false}]},{"StartTime":34725.0,"Objects":[{"StartTime":34725.0,"Position":469.0,"HyperDash":false},{"StartTime":34791.0,"Position":453.0,"HyperDash":false},{"StartTime":34858.0,"Position":477.0,"HyperDash":false},{"StartTime":34925.0,"Position":456.0,"HyperDash":false},{"StartTime":35028.0,"Position":469.0,"HyperDash":false}]},{"StartTime":35331.0,"Objects":[{"StartTime":35331.0,"Position":373.0,"HyperDash":false},{"StartTime":35397.0,"Position":326.8614,"HyperDash":false},{"StartTime":35464.0,"Position":315.326721,"HyperDash":false},{"StartTime":35531.0,"Position":282.792084,"HyperDash":false},{"StartTime":35634.0,"Position":253.0,"HyperDash":false}]},{"StartTime":35937.0,"Objects":[{"StartTime":35937.0,"Position":157.0,"HyperDash":false}]},{"StartTime":36240.0,"Objects":[{"StartTime":36240.0,"Position":157.0,"HyperDash":false}]},{"StartTime":36392.0,"Objects":[{"StartTime":36392.0,"Position":157.0,"HyperDash":false}]},{"StartTime":36543.0,"Objects":[{"StartTime":36543.0,"Position":204.0,"HyperDash":false},{"StartTime":36618.0,"Position":241.702972,"HyperDash":false},{"StartTime":36694.0,"Position":264.0,"HyperDash":false},{"StartTime":36752.0,"Position":239.227722,"HyperDash":false},{"StartTime":36846.0,"Position":204.0,"HyperDash":false}]},{"StartTime":36998.0,"Objects":[{"StartTime":36998.0,"Position":204.0,"HyperDash":false},{"StartTime":37055.0,"Position":221.0,"HyperDash":false},{"StartTime":37149.0,"Position":204.0,"HyperDash":false}]},{"StartTime":37301.0,"Objects":[{"StartTime":37301.0,"Position":205.0,"HyperDash":false}]},{"StartTime":37604.0,"Objects":[{"StartTime":37604.0,"Position":300.0,"HyperDash":false}]},{"StartTime":37907.0,"Objects":[{"StartTime":37907.0,"Position":300.0,"HyperDash":false}]},{"StartTime":38967.0,"Objects":[{"StartTime":38967.0,"Position":32.0,"HyperDash":false}]},{"StartTime":39573.0,"Objects":[{"StartTime":39573.0,"Position":32.0,"HyperDash":false}]},{"StartTime":40786.0,"Objects":[{"StartTime":40786.0,"Position":416.0,"HyperDash":false}]},{"StartTime":40937.0,"Objects":[{"StartTime":40937.0,"Position":416.0,"HyperDash":false}]},{"StartTime":41089.0,"Objects":[{"StartTime":41089.0,"Position":416.0,"HyperDash":false}]},{"StartTime":41392.0,"Objects":[{"StartTime":41392.0,"Position":320.0,"HyperDash":false}]},{"StartTime":41695.0,"Objects":[{"StartTime":41695.0,"Position":320.0,"HyperDash":false}]},{"StartTime":42604.0,"Objects":[{"StartTime":42604.0,"Position":48.0,"HyperDash":false},{"StartTime":42670.0,"Position":57.13861,"HyperDash":false},{"StartTime":42737.0,"Position":105.673264,"HyperDash":false},{"StartTime":42804.0,"Position":146.207916,"HyperDash":false},{"StartTime":42907.0,"Position":168.0,"HyperDash":false}]},{"StartTime":43210.0,"Objects":[{"StartTime":43210.0,"Position":263.0,"HyperDash":false}]},{"StartTime":43816.0,"Objects":[{"StartTime":43816.0,"Position":376.0,"HyperDash":false},{"StartTime":43891.0,"Position":326.594055,"HyperDash":false},{"StartTime":43967.0,"Position":256.396027,"HyperDash":false},{"StartTime":44043.0,"Position":200.198029,"HyperDash":false},{"StartTime":44119.0,"Position":136.0,"HyperDash":false},{"StartTime":44194.0,"Position":202.405945,"HyperDash":false},{"StartTime":44270.0,"Position":255.603943,"HyperDash":false},{"StartTime":44346.0,"Position":300.802,"HyperDash":false},{"StartTime":44422.0,"Position":376.0,"HyperDash":false},{"StartTime":44497.0,"Position":313.594055,"HyperDash":false},{"StartTime":44573.0,"Position":256.396027,"HyperDash":false},{"StartTime":44649.0,"Position":201.198044,"HyperDash":false},{"StartTime":44725.0,"Position":136.0,"HyperDash":false},{"StartTime":44800.0,"Position":204.405945,"HyperDash":false},{"StartTime":44876.0,"Position":255.603973,"HyperDash":false},{"StartTime":44952.0,"Position":305.801971,"HyperDash":false},{"StartTime":45028.0,"Position":376.0,"HyperDash":false},{"StartTime":45103.0,"Position":306.594055,"HyperDash":false},{"StartTime":45179.0,"Position":256.3961,"HyperDash":false},{"StartTime":45237.0,"Position":224.45549,"HyperDash":false},{"StartTime":45331.0,"Position":136.0,"HyperDash":false}]},{"StartTime":45634.0,"Objects":[{"StartTime":45634.0,"Position":376.0,"HyperDash":false},{"StartTime":45709.0,"Position":323.594055,"HyperDash":false},{"StartTime":45785.0,"Position":256.396027,"HyperDash":false},{"StartTime":45861.0,"Position":198.198029,"HyperDash":false},{"StartTime":45937.0,"Position":136.0,"HyperDash":false},{"StartTime":46012.0,"Position":176.405945,"HyperDash":false},{"StartTime":46088.0,"Position":255.603943,"HyperDash":false},{"StartTime":46164.0,"Position":318.802,"HyperDash":false},{"StartTime":46240.0,"Position":376.0,"HyperDash":false},{"StartTime":46315.0,"Position":324.594055,"HyperDash":false},{"StartTime":46391.0,"Position":256.396027,"HyperDash":false},{"StartTime":46467.0,"Position":199.198044,"HyperDash":false},{"StartTime":46543.0,"Position":136.0,"HyperDash":false},{"StartTime":46618.0,"Position":193.405945,"HyperDash":false},{"StartTime":46694.0,"Position":255.603973,"HyperDash":false},{"StartTime":46770.0,"Position":298.801971,"HyperDash":false},{"StartTime":46846.0,"Position":376.0,"HyperDash":false},{"StartTime":46921.0,"Position":327.594055,"HyperDash":false},{"StartTime":46997.0,"Position":256.3961,"HyperDash":false},{"StartTime":47055.0,"Position":217.45549,"HyperDash":false},{"StartTime":47149.0,"Position":136.0,"HyperDash":false}]},{"StartTime":47452.0,"Objects":[{"StartTime":47452.0,"Position":376.0,"HyperDash":false},{"StartTime":47527.0,"Position":327.594055,"HyperDash":false},{"StartTime":47603.0,"Position":256.396027,"HyperDash":false},{"StartTime":47679.0,"Position":189.198029,"HyperDash":false},{"StartTime":47755.0,"Position":136.0,"HyperDash":false},{"StartTime":47830.0,"Position":195.405945,"HyperDash":false},{"StartTime":47906.0,"Position":255.603943,"HyperDash":false},{"StartTime":47982.0,"Position":300.802,"HyperDash":false},{"StartTime":48058.0,"Position":376.0,"HyperDash":false},{"StartTime":48133.0,"Position":324.594055,"HyperDash":false},{"StartTime":48209.0,"Position":256.396027,"HyperDash":false},{"StartTime":48285.0,"Position":188.198044,"HyperDash":false},{"StartTime":48361.0,"Position":136.0,"HyperDash":false},{"StartTime":48436.0,"Position":199.405945,"HyperDash":false},{"StartTime":48512.0,"Position":255.603973,"HyperDash":false},{"StartTime":48588.0,"Position":310.801971,"HyperDash":false},{"StartTime":48664.0,"Position":376.0,"HyperDash":false},{"StartTime":48739.0,"Position":317.594055,"HyperDash":false},{"StartTime":48815.0,"Position":256.3961,"HyperDash":false},{"StartTime":48873.0,"Position":213.45549,"HyperDash":false},{"StartTime":48967.0,"Position":136.0,"HyperDash":false}]},{"StartTime":49270.0,"Objects":[{"StartTime":49270.0,"Position":376.0,"HyperDash":false},{"StartTime":49345.0,"Position":335.594482,"HyperDash":false},{"StartTime":49421.0,"Position":256.396881,"HyperDash":false},{"StartTime":49479.0,"Position":210.456619,"HyperDash":false},{"StartTime":49573.0,"Position":136.001678,"HyperDash":false}]},{"StartTime":49876.0,"Objects":[{"StartTime":49876.0,"Position":136.0,"HyperDash":false}]},{"StartTime":50180.0,"Objects":[{"StartTime":50180.0,"Position":328.0,"HyperDash":false}]},{"StartTime":50483.0,"Objects":[{"StartTime":50483.0,"Position":329.0,"HyperDash":false}]},{"StartTime":50786.0,"Objects":[{"StartTime":50786.0,"Position":136.0,"HyperDash":false}]},{"StartTime":50937.0,"Objects":[{"StartTime":50937.0,"Position":138.0,"HyperDash":false}]},{"StartTime":51089.0,"Objects":[{"StartTime":51089.0,"Position":138.0,"HyperDash":false},{"StartTime":51146.0,"Position":142.649,"HyperDash":false},{"StartTime":51240.0,"Position":198.0,"HyperDash":false}]},{"StartTime":51392.0,"Objects":[{"StartTime":51392.0,"Position":198.0,"HyperDash":false}]},{"StartTime":51543.0,"Objects":[{"StartTime":51543.0,"Position":246.0,"HyperDash":false}]},{"StartTime":51695.0,"Objects":[{"StartTime":51695.0,"Position":295.0,"HyperDash":false},{"StartTime":51752.0,"Position":303.649017,"HyperDash":false},{"StartTime":51846.0,"Position":355.0,"HyperDash":false}]},{"StartTime":52149.0,"Objects":[{"StartTime":52149.0,"Position":355.0,"HyperDash":false}]},{"StartTime":52452.0,"Objects":[{"StartTime":52452.0,"Position":260.0,"HyperDash":false}]},{"StartTime":53513.0,"Objects":[{"StartTime":53513.0,"Position":40.0,"HyperDash":false},{"StartTime":53588.0,"Position":68.70297,"HyperDash":false},{"StartTime":53664.0,"Position":116.801987,"HyperDash":false},{"StartTime":53740.0,"Position":141.900986,"HyperDash":false},{"StartTime":53816.0,"Position":160.0,"HyperDash":false},{"StartTime":53882.0,"Position":184.138626,"HyperDash":false},{"StartTime":53949.0,"Position":229.673264,"HyperDash":false},{"StartTime":54016.0,"Position":230.207916,"HyperDash":false},{"StartTime":54119.0,"Position":280.0,"HyperDash":false}]},{"StartTime":55331.0,"Objects":[{"StartTime":55331.0,"Position":40.0,"HyperDash":false},{"StartTime":55406.0,"Position":84.70297,"HyperDash":false},{"StartTime":55482.0,"Position":100.0,"HyperDash":false},{"StartTime":55540.0,"Position":64.22772,"HyperDash":false},{"StartTime":55634.0,"Position":40.0,"HyperDash":false}]},{"StartTime":55937.0,"Objects":[{"StartTime":55937.0,"Position":40.0,"HyperDash":false},{"StartTime":56003.0,"Position":29.0,"HyperDash":false},{"StartTime":56070.0,"Position":44.0,"HyperDash":false},{"StartTime":56137.0,"Position":28.0,"HyperDash":false},{"StartTime":56240.0,"Position":40.0,"HyperDash":false}]},{"StartTime":57149.0,"Objects":[{"StartTime":57149.0,"Position":300.0,"HyperDash":false},{"StartTime":57215.0,"Position":316.0,"HyperDash":false},{"StartTime":57282.0,"Position":288.0,"HyperDash":false},{"StartTime":57349.0,"Position":299.0,"HyperDash":false},{"StartTime":57452.0,"Position":300.0,"HyperDash":false}]},{"StartTime":58361.0,"Objects":[{"StartTime":58361.0,"Position":256.0,"HyperDash":false},{"StartTime":58418.0,"Position":265.649017,"HyperDash":false},{"StartTime":58512.0,"Position":316.0,"HyperDash":false}]},{"StartTime":58664.0,"Objects":[{"StartTime":58664.0,"Position":364.0,"HyperDash":false},{"StartTime":58721.0,"Position":358.0,"HyperDash":false},{"StartTime":58815.0,"Position":364.0,"HyperDash":false}]},{"StartTime":58967.0,"Objects":[{"StartTime":58967.0,"Position":329.0,"HyperDash":false}]},{"StartTime":59119.0,"Objects":[{"StartTime":59119.0,"Position":280.0,"HyperDash":false},{"StartTime":59176.0,"Position":249.350983,"HyperDash":false},{"StartTime":59270.0,"Position":220.0,"HyperDash":false}]},{"StartTime":59422.0,"Objects":[{"StartTime":59422.0,"Position":185.0,"HyperDash":false},{"StartTime":59479.0,"Position":176.0,"HyperDash":false},{"StartTime":59573.0,"Position":185.0,"HyperDash":false}]},{"StartTime":59876.0,"Objects":[{"StartTime":59876.0,"Position":185.0,"HyperDash":false}]},{"StartTime":60180.0,"Objects":[{"StartTime":60180.0,"Position":253.0,"HyperDash":false}]},{"StartTime":60331.0,"Objects":[{"StartTime":60331.0,"Position":253.0,"HyperDash":false}]},{"StartTime":60483.0,"Objects":[{"StartTime":60483.0,"Position":253.0,"HyperDash":false}]},{"StartTime":60634.0,"Objects":[{"StartTime":60634.0,"Position":253.0,"HyperDash":false}]},{"StartTime":60786.0,"Objects":[{"StartTime":60786.0,"Position":253.0,"HyperDash":false},{"StartTime":60861.0,"Position":218.297028,"HyperDash":false},{"StartTime":60937.0,"Position":193.0,"HyperDash":false},{"StartTime":61013.0,"Position":217.900986,"HyperDash":false},{"StartTime":61089.0,"Position":253.0,"HyperDash":false},{"StartTime":61164.0,"Position":237.297028,"HyperDash":false},{"StartTime":61240.0,"Position":193.0,"HyperDash":false},{"StartTime":61298.0,"Position":218.772278,"HyperDash":false},{"StartTime":61392.0,"Position":253.0,"HyperDash":false}]},{"StartTime":61695.0,"Objects":[{"StartTime":61695.0,"Position":253.0,"HyperDash":false}]},{"StartTime":61998.0,"Objects":[{"StartTime":61998.0,"Position":348.0,"HyperDash":false},{"StartTime":62073.0,"Position":336.0,"HyperDash":false},{"StartTime":62149.0,"Position":348.0,"HyperDash":false},{"StartTime":62225.0,"Position":336.0,"HyperDash":false},{"StartTime":62301.0,"Position":348.0,"HyperDash":false},{"StartTime":62376.0,"Position":333.0,"HyperDash":false},{"StartTime":62452.0,"Position":348.0,"HyperDash":false},{"StartTime":62510.0,"Position":344.0,"HyperDash":false},{"StartTime":62604.0,"Position":348.0,"HyperDash":false}]},{"StartTime":62755.0,"Objects":[{"StartTime":62755.0,"Position":348.0,"HyperDash":false}]},{"StartTime":62907.0,"Objects":[{"StartTime":62907.0,"Position":348.0,"HyperDash":false}]},{"StartTime":63058.0,"Objects":[{"StartTime":63058.0,"Position":348.0,"HyperDash":false}]},{"StartTime":63210.0,"Objects":[{"StartTime":63210.0,"Position":348.0,"HyperDash":false}]},{"StartTime":63513.0,"Objects":[{"StartTime":63513.0,"Position":252.0,"HyperDash":false}]},{"StartTime":63816.0,"Objects":[{"StartTime":63816.0,"Position":252.0,"HyperDash":false}]},{"StartTime":63967.0,"Objects":[{"StartTime":63967.0,"Position":252.0,"HyperDash":false},{"StartTime":64042.0,"Position":225.264313,"HyperDash":false},{"StartTime":64118.0,"Position":192.0,"HyperDash":false},{"StartTime":64194.0,"Position":203.0,"HyperDash":false},{"StartTime":64270.0,"Position":252.0,"HyperDash":false},{"StartTime":64327.0,"Position":229.268738,"HyperDash":false},{"StartTime":64421.0,"Position":192.0,"HyperDash":false}]},{"StartTime":64725.0,"Objects":[{"StartTime":64725.0,"Position":288.0,"HyperDash":false}]},{"StartTime":65028.0,"Objects":[{"StartTime":65028.0,"Position":383.0,"HyperDash":false}]},{"StartTime":65331.0,"Objects":[{"StartTime":65331.0,"Position":383.0,"HyperDash":false}]},{"StartTime":65634.0,"Objects":[{"StartTime":65634.0,"Position":287.0,"HyperDash":false},{"StartTime":65691.0,"Position":277.350983,"HyperDash":false},{"StartTime":65785.0,"Position":227.0,"HyperDash":false}]},{"StartTime":65937.0,"Objects":[{"StartTime":65937.0,"Position":178.0,"HyperDash":false}]},{"StartTime":66089.0,"Objects":[{"StartTime":66089.0,"Position":129.0,"HyperDash":false}]},{"StartTime":66240.0,"Objects":[{"StartTime":66240.0,"Position":81.0,"HyperDash":false}]},{"StartTime":66392.0,"Objects":[{"StartTime":66392.0,"Position":81.0,"HyperDash":false}]},{"StartTime":66543.0,"Objects":[{"StartTime":66543.0,"Position":81.0,"HyperDash":false},{"StartTime":66600.0,"Position":100.64901,"HyperDash":false},{"StartTime":66694.0,"Position":141.0,"HyperDash":false}]},{"StartTime":66846.0,"Objects":[{"StartTime":66846.0,"Position":189.0,"HyperDash":false},{"StartTime":66903.0,"Position":227.649,"HyperDash":false},{"StartTime":66997.0,"Position":249.0,"HyperDash":false}]},{"StartTime":67755.0,"Objects":[{"StartTime":67755.0,"Position":192.0,"HyperDash":false},{"StartTime":67812.0,"Position":205.649,"HyperDash":false},{"StartTime":67906.0,"Position":252.0,"HyperDash":false}]},{"StartTime":68058.0,"Objects":[{"StartTime":68058.0,"Position":300.0,"HyperDash":false}]},{"StartTime":68664.0,"Objects":[{"StartTime":68664.0,"Position":300.0,"HyperDash":false}]},{"StartTime":68967.0,"Objects":[{"StartTime":68967.0,"Position":300.0,"HyperDash":false}]},{"StartTime":69270.0,"Objects":[{"StartTime":69270.0,"Position":204.0,"HyperDash":false}]},{"StartTime":69876.0,"Objects":[{"StartTime":69876.0,"Position":395.0,"HyperDash":false},{"StartTime":69933.0,"Position":384.272858,"HyperDash":false},{"StartTime":70027.0,"Position":395.722839,"HyperDash":false}]},{"StartTime":70180.0,"Objects":[{"StartTime":70180.0,"Position":395.0,"HyperDash":false}]},{"StartTime":70483.0,"Objects":[{"StartTime":70483.0,"Position":296.0,"HyperDash":false}]},{"StartTime":70786.0,"Objects":[{"StartTime":70786.0,"Position":200.0,"HyperDash":false}]},{"StartTime":71695.0,"Objects":[{"StartTime":71695.0,"Position":200.0,"HyperDash":false}]},{"StartTime":71998.0,"Objects":[{"StartTime":71998.0,"Position":295.0,"HyperDash":false}]},{"StartTime":72907.0,"Objects":[{"StartTime":72907.0,"Position":91.0,"HyperDash":false}]},{"StartTime":73058.0,"Objects":[{"StartTime":73058.0,"Position":138.0,"HyperDash":false}]},{"StartTime":73210.0,"Objects":[{"StartTime":73210.0,"Position":186.0,"HyperDash":false}]},{"StartTime":73361.0,"Objects":[{"StartTime":73361.0,"Position":186.0,"HyperDash":false},{"StartTime":73418.0,"Position":194.649,"HyperDash":false},{"StartTime":73512.0,"Position":246.0,"HyperDash":false}]},{"StartTime":73664.0,"Objects":[{"StartTime":73664.0,"Position":294.0,"HyperDash":false},{"StartTime":73721.0,"Position":334.649017,"HyperDash":false},{"StartTime":73815.0,"Position":354.0,"HyperDash":false}]},{"StartTime":73967.0,"Objects":[{"StartTime":73967.0,"Position":354.0,"HyperDash":false}]},{"StartTime":74270.0,"Objects":[{"StartTime":74270.0,"Position":354.0,"HyperDash":false}]},{"StartTime":75331.0,"Objects":[{"StartTime":75331.0,"Position":40.0,"HyperDash":false}]},{"StartTime":75937.0,"Objects":[{"StartTime":75937.0,"Position":160.0,"HyperDash":false}]},{"StartTime":76089.0,"Objects":[{"StartTime":76089.0,"Position":160.0,"HyperDash":false}]},{"StartTime":76543.0,"Objects":[{"StartTime":76543.0,"Position":303.0,"HyperDash":false}]},{"StartTime":77149.0,"Objects":[{"StartTime":77149.0,"Position":160.0,"HyperDash":false},{"StartTime":77206.0,"Position":192.649,"HyperDash":false},{"StartTime":77300.0,"Position":220.0,"HyperDash":false}]},{"StartTime":77452.0,"Objects":[{"StartTime":77452.0,"Position":268.0,"HyperDash":false}]},{"StartTime":77755.0,"Objects":[{"StartTime":77755.0,"Position":268.0,"HyperDash":false}]},{"StartTime":78058.0,"Objects":[{"StartTime":78058.0,"Position":268.0,"HyperDash":false}]},{"StartTime":78361.0,"Objects":[{"StartTime":78361.0,"Position":363.0,"HyperDash":false},{"StartTime":78418.0,"Position":382.0,"HyperDash":false},{"StartTime":78512.0,"Position":363.0,"HyperDash":false}]},{"StartTime":78967.0,"Objects":[{"StartTime":78967.0,"Position":363.0,"HyperDash":false}]},{"StartTime":79270.0,"Objects":[{"StartTime":79270.0,"Position":267.0,"HyperDash":false},{"StartTime":79336.0,"Position":223.861389,"HyperDash":false},{"StartTime":79403.0,"Position":208.326736,"HyperDash":false},{"StartTime":79470.0,"Position":193.792084,"HyperDash":false},{"StartTime":79573.0,"Position":147.0,"HyperDash":false}]},{"StartTime":80180.0,"Objects":[{"StartTime":80180.0,"Position":96.0,"HyperDash":false},{"StartTime":80255.0,"Position":108.0,"HyperDash":false},{"StartTime":80331.0,"Position":83.0,"HyperDash":false},{"StartTime":80407.0,"Position":82.0,"HyperDash":false},{"StartTime":80483.0,"Position":96.0,"HyperDash":false},{"StartTime":80558.0,"Position":99.0,"HyperDash":false},{"StartTime":80634.0,"Position":82.0,"HyperDash":false},{"StartTime":80710.0,"Position":102.0,"HyperDash":false},{"StartTime":80786.0,"Position":96.0,"HyperDash":false},{"StartTime":80861.0,"Position":86.0,"HyperDash":false},{"StartTime":80937.0,"Position":89.0,"HyperDash":false},{"StartTime":81013.0,"Position":123.90097,"HyperDash":false},{"StartTime":81089.0,"Position":144.0,"HyperDash":false},{"StartTime":81155.0,"Position":177.138611,"HyperDash":false},{"StartTime":81222.0,"Position":194.673279,"HyperDash":false},{"StartTime":81289.0,"Position":229.207916,"HyperDash":false},{"StartTime":81392.0,"Position":264.0,"HyperDash":false}]},{"StartTime":81695.0,"Objects":[{"StartTime":81695.0,"Position":360.0,"HyperDash":false}]},{"StartTime":81998.0,"Objects":[{"StartTime":81998.0,"Position":455.0,"HyperDash":false},{"StartTime":82073.0,"Position":449.0,"HyperDash":false},{"StartTime":82149.0,"Position":448.0,"HyperDash":false},{"StartTime":82225.0,"Position":474.0,"HyperDash":false},{"StartTime":82301.0,"Position":455.0,"HyperDash":false},{"StartTime":82376.0,"Position":455.0,"HyperDash":false},{"StartTime":82452.0,"Position":470.0,"HyperDash":false},{"StartTime":82528.0,"Position":439.0,"HyperDash":false},{"StartTime":82604.0,"Position":455.0,"HyperDash":false},{"StartTime":82679.0,"Position":458.0,"HyperDash":false},{"StartTime":82755.0,"Position":451.0,"HyperDash":false},{"StartTime":82831.0,"Position":445.09903,"HyperDash":false},{"StartTime":82907.0,"Position":407.0,"HyperDash":false},{"StartTime":82982.0,"Position":392.297028,"HyperDash":false},{"StartTime":83058.0,"Position":335.198029,"HyperDash":false},{"StartTime":83134.0,"Position":301.09903,"HyperDash":false},{"StartTime":83210.0,"Position":287.0,"HyperDash":false},{"StartTime":83276.0,"Position":242.861389,"HyperDash":false},{"StartTime":83343.0,"Position":230.326721,"HyperDash":false},{"StartTime":83410.0,"Position":212.792053,"HyperDash":false},{"StartTime":83513.0,"Position":167.0,"HyperDash":false}]},{"StartTime":83816.0,"Objects":[{"StartTime":83816.0,"Position":124.0,"HyperDash":false},{"StartTime":83891.0,"Position":154.702972,"HyperDash":false},{"StartTime":83967.0,"Position":181.801987,"HyperDash":false},{"StartTime":84043.0,"Position":229.900986,"HyperDash":false},{"StartTime":84119.0,"Position":244.0,"HyperDash":false},{"StartTime":84194.0,"Position":287.702972,"HyperDash":false},{"StartTime":84270.0,"Position":290.801971,"HyperDash":false},{"StartTime":84346.0,"Position":332.901,"HyperDash":false},{"StartTime":84422.0,"Position":364.0,"HyperDash":false},{"StartTime":84497.0,"Position":367.0,"HyperDash":false},{"StartTime":84573.0,"Position":374.0,"HyperDash":false},{"StartTime":84649.0,"Position":360.0,"HyperDash":false},{"StartTime":84725.0,"Position":364.0,"HyperDash":false},{"StartTime":84791.0,"Position":368.0,"HyperDash":false},{"StartTime":84858.0,"Position":369.0,"HyperDash":false},{"StartTime":84925.0,"Position":364.0,"HyperDash":false},{"StartTime":85028.0,"Position":364.0,"HyperDash":false}]},{"StartTime":85331.0,"Objects":[{"StartTime":85331.0,"Position":268.0,"HyperDash":false}]},{"StartTime":85634.0,"Objects":[{"StartTime":85634.0,"Position":172.0,"HyperDash":false},{"StartTime":85709.0,"Position":124.288116,"HyperDash":false},{"StartTime":85785.0,"Position":93.18007,"HyperDash":false},{"StartTime":85861.0,"Position":71.07203,"HyperDash":false},{"StartTime":85937.0,"Position":52.0,"HyperDash":false},{"StartTime":86012.0,"Position":45.0,"HyperDash":false},{"StartTime":86088.0,"Position":68.0,"HyperDash":false},{"StartTime":86164.0,"Position":66.0,"HyperDash":false},{"StartTime":86240.0,"Position":52.0,"HyperDash":false},{"StartTime":86315.0,"Position":33.0,"HyperDash":false},{"StartTime":86391.0,"Position":34.0,"HyperDash":false},{"StartTime":86467.0,"Position":66.0,"HyperDash":false},{"StartTime":86543.0,"Position":76.10803,"HyperDash":false},{"StartTime":86618.0,"Position":109.819916,"HyperDash":false},{"StartTime":86694.0,"Position":132.927948,"HyperDash":false},{"StartTime":86770.0,"Position":153.036011,"HyperDash":false},{"StartTime":86846.0,"Position":196.144073,"HyperDash":false},{"StartTime":86921.0,"Position":235.855927,"HyperDash":false},{"StartTime":86997.0,"Position":237.963989,"HyperDash":false},{"StartTime":87073.0,"Position":282.072021,"HyperDash":false},{"StartTime":87149.0,"Position":316.0,"HyperDash":false},{"StartTime":87206.0,"Position":327.0,"HyperDash":false},{"StartTime":87300.0,"Position":316.0,"HyperDash":false}]},{"StartTime":87452.0,"Objects":[{"StartTime":87452.0,"Position":316.0,"HyperDash":false},{"StartTime":87518.0,"Position":297.0,"HyperDash":false},{"StartTime":87585.0,"Position":333.0,"HyperDash":false},{"StartTime":87652.0,"Position":325.0,"HyperDash":false},{"StartTime":87755.0,"Position":316.0,"HyperDash":false}]},{"StartTime":88058.0,"Objects":[{"StartTime":88058.0,"Position":411.0,"HyperDash":false},{"StartTime":88133.0,"Position":411.0,"HyperDash":false},{"StartTime":88209.0,"Position":410.0,"HyperDash":false},{"StartTime":88285.0,"Position":423.0,"HyperDash":false},{"StartTime":88361.0,"Position":411.0,"HyperDash":false},{"StartTime":88436.0,"Position":412.0,"HyperDash":false},{"StartTime":88512.0,"Position":398.0,"HyperDash":false},{"StartTime":88588.0,"Position":414.0,"HyperDash":false},{"StartTime":88664.0,"Position":411.0,"HyperDash":false},{"StartTime":88739.0,"Position":382.297028,"HyperDash":false},{"StartTime":88815.0,"Position":340.198,"HyperDash":false},{"StartTime":88891.0,"Position":331.09903,"HyperDash":false},{"StartTime":88967.0,"Position":299.0,"HyperDash":false},{"StartTime":89033.0,"Position":253.861389,"HyperDash":false},{"StartTime":89100.0,"Position":231.326721,"HyperDash":false},{"StartTime":89167.0,"Position":225.792084,"HyperDash":false},{"StartTime":89270.0,"Position":179.0,"HyperDash":false}]},{"StartTime":89876.0,"Objects":[{"StartTime":89876.0,"Position":176.0,"HyperDash":false},{"StartTime":89951.0,"Position":144.297028,"HyperDash":false},{"StartTime":90027.0,"Position":110.198013,"HyperDash":false},{"StartTime":90103.0,"Position":73.0990143,"HyperDash":false},{"StartTime":90179.0,"Position":56.0,"HyperDash":false},{"StartTime":90245.0,"Position":34.0,"HyperDash":false},{"StartTime":90312.0,"Position":29.0,"HyperDash":false},{"StartTime":90379.0,"Position":40.0,"HyperDash":false},{"StartTime":90482.0,"Position":40.0,"HyperDash":false}]},{"StartTime":91089.0,"Objects":[{"StartTime":91089.0,"Position":232.0,"HyperDash":false}]},{"StartTime":91695.0,"Objects":[{"StartTime":91695.0,"Position":423.0,"HyperDash":false},{"StartTime":91770.0,"Position":409.0,"HyperDash":false},{"StartTime":91846.0,"Position":424.0,"HyperDash":false},{"StartTime":91922.0,"Position":438.0,"HyperDash":false},{"StartTime":91998.0,"Position":423.0,"HyperDash":false},{"StartTime":92073.0,"Position":417.0,"HyperDash":false},{"StartTime":92149.0,"Position":420.198029,"HyperDash":false},{"StartTime":92225.0,"Position":354.099,"HyperDash":false},{"StartTime":92301.0,"Position":343.0,"HyperDash":false},{"StartTime":92376.0,"Position":331.297028,"HyperDash":false},{"StartTime":92452.0,"Position":266.198,"HyperDash":false},{"StartTime":92528.0,"Position":237.09903,"HyperDash":false},{"StartTime":92604.0,"Position":223.0,"HyperDash":false},{"StartTime":92670.0,"Position":208.861389,"HyperDash":false},{"StartTime":92737.0,"Position":185.326721,"HyperDash":false},{"StartTime":92804.0,"Position":135.792084,"HyperDash":false},{"StartTime":92907.0,"Position":103.0,"HyperDash":false}]},{"StartTime":93513.0,"Objects":[{"StartTime":93513.0,"Position":112.0,"HyperDash":false}]},{"StartTime":94119.0,"Objects":[{"StartTime":94119.0,"Position":303.0,"HyperDash":false}]},{"StartTime":94725.0,"Objects":[{"StartTime":94725.0,"Position":440.0,"HyperDash":false},{"StartTime":94800.0,"Position":426.0,"HyperDash":false},{"StartTime":94876.0,"Position":436.0,"HyperDash":false},{"StartTime":94952.0,"Position":453.0,"HyperDash":false},{"StartTime":95028.0,"Position":440.0,"HyperDash":false},{"StartTime":95103.0,"Position":440.0,"HyperDash":false},{"StartTime":95179.0,"Position":433.0,"HyperDash":false},{"StartTime":95255.0,"Position":456.0,"HyperDash":false},{"StartTime":95331.0,"Position":440.0,"HyperDash":false},{"StartTime":95406.0,"Position":449.0,"HyperDash":false},{"StartTime":95482.0,"Position":433.0,"HyperDash":false},{"StartTime":95558.0,"Position":456.0,"HyperDash":false},{"StartTime":95634.0,"Position":440.0,"HyperDash":false},{"StartTime":95700.0,"Position":439.0,"HyperDash":false},{"StartTime":95767.0,"Position":423.0,"HyperDash":false},{"StartTime":95834.0,"Position":428.0,"HyperDash":false},{"StartTime":95937.0,"Position":440.0,"HyperDash":false}]},{"StartTime":96543.0,"Objects":[{"StartTime":96543.0,"Position":216.0,"HyperDash":false},{"StartTime":96618.0,"Position":204.0,"HyperDash":false},{"StartTime":96694.0,"Position":208.0,"HyperDash":false},{"StartTime":96770.0,"Position":218.0,"HyperDash":false},{"StartTime":96846.0,"Position":216.0,"HyperDash":false},{"StartTime":96912.0,"Position":233.0,"HyperDash":false},{"StartTime":96979.0,"Position":225.0,"HyperDash":false},{"StartTime":97046.0,"Position":206.0,"HyperDash":false},{"StartTime":97149.0,"Position":216.0,"HyperDash":false}]},{"StartTime":97755.0,"Objects":[{"StartTime":97755.0,"Position":48.0,"HyperDash":false}]},{"StartTime":98361.0,"Objects":[{"StartTime":98361.0,"Position":216.0,"HyperDash":false}]},{"StartTime":98967.0,"Objects":[{"StartTime":98967.0,"Position":216.0,"HyperDash":false},{"StartTime":99042.0,"Position":231.0,"HyperDash":false},{"StartTime":99118.0,"Position":207.0,"HyperDash":false},{"StartTime":99194.0,"Position":205.0,"HyperDash":false},{"StartTime":99270.0,"Position":216.0,"HyperDash":false},{"StartTime":99345.0,"Position":206.0,"HyperDash":false},{"StartTime":99421.0,"Position":218.0,"HyperDash":false},{"StartTime":99497.0,"Position":208.0,"HyperDash":false},{"StartTime":99573.0,"Position":216.0,"HyperDash":false},{"StartTime":99648.0,"Position":234.0,"HyperDash":false},{"StartTime":99724.0,"Position":222.0,"HyperDash":false},{"StartTime":99800.0,"Position":231.0,"HyperDash":false},{"StartTime":99876.0,"Position":216.0,"HyperDash":false},{"StartTime":99942.0,"Position":200.0,"HyperDash":false},{"StartTime":100009.0,"Position":199.0,"HyperDash":false},{"StartTime":100076.0,"Position":228.0,"HyperDash":false},{"StartTime":100179.0,"Position":216.0,"HyperDash":false}]},{"StartTime":100786.0,"Objects":[{"StartTime":100786.0,"Position":216.0,"HyperDash":false}]},{"StartTime":101392.0,"Objects":[{"StartTime":101392.0,"Position":216.0,"HyperDash":false}]},{"StartTime":101998.0,"Objects":[{"StartTime":101998.0,"Position":356.0,"HyperDash":false},{"StartTime":102054.0,"Position":362.0,"HyperDash":false},{"StartTime":102111.0,"Position":347.0,"HyperDash":false},{"StartTime":102168.0,"Position":252.0,"HyperDash":false},{"StartTime":102225.0,"Position":477.0,"HyperDash":false},{"StartTime":102282.0,"Position":358.0,"HyperDash":false},{"StartTime":102338.0,"Position":17.0,"HyperDash":false},{"StartTime":102395.0,"Position":399.0,"HyperDash":false},{"StartTime":102452.0,"Position":280.0,"HyperDash":false},{"StartTime":102509.0,"Position":304.0,"HyperDash":false},{"StartTime":102566.0,"Position":221.0,"HyperDash":false},{"StartTime":102622.0,"Position":407.0,"HyperDash":false},{"StartTime":102679.0,"Position":287.0,"HyperDash":false},{"StartTime":102736.0,"Position":135.0,"HyperDash":false},{"StartTime":102793.0,"Position":437.0,"HyperDash":false},{"StartTime":102850.0,"Position":289.0,"HyperDash":false},{"StartTime":102907.0,"Position":464.0,"HyperDash":false},{"StartTime":102963.0,"Position":36.0,"HyperDash":false},{"StartTime":103020.0,"Position":378.0,"HyperDash":false},{"StartTime":103077.0,"Position":297.0,"HyperDash":false},{"StartTime":103134.0,"Position":418.0,"HyperDash":false},{"StartTime":103191.0,"Position":329.0,"HyperDash":false},{"StartTime":103247.0,"Position":338.0,"HyperDash":false},{"StartTime":103304.0,"Position":394.0,"HyperDash":false},{"StartTime":103361.0,"Position":40.0,"HyperDash":false},{"StartTime":103418.0,"Position":13.0,"HyperDash":false},{"StartTime":103475.0,"Position":80.0,"HyperDash":false},{"StartTime":103531.0,"Position":138.0,"HyperDash":false},{"StartTime":103588.0,"Position":311.0,"HyperDash":false},{"StartTime":103645.0,"Position":216.0,"HyperDash":false},{"StartTime":103702.0,"Position":310.0,"HyperDash":false},{"StartTime":103759.0,"Position":397.0,"HyperDash":false},{"StartTime":103816.0,"Position":214.0,"HyperDash":false},{"StartTime":103872.0,"Position":505.0,"HyperDash":false},{"StartTime":103929.0,"Position":173.0,"HyperDash":false},{"StartTime":103986.0,"Position":295.0,"HyperDash":false},{"StartTime":104043.0,"Position":199.0,"HyperDash":false},{"StartTime":104100.0,"Position":494.0,"HyperDash":false},{"StartTime":104156.0,"Position":293.0,"HyperDash":false},{"StartTime":104213.0,"Position":115.0,"HyperDash":false},{"StartTime":104270.0,"Position":412.0,"HyperDash":false},{"StartTime":104327.0,"Position":506.0,"HyperDash":false},{"StartTime":104384.0,"Position":293.0,"HyperDash":false},{"StartTime":104440.0,"Position":346.0,"HyperDash":false},{"StartTime":104497.0,"Position":117.0,"HyperDash":false},{"StartTime":104554.0,"Position":285.0,"HyperDash":false},{"StartTime":104611.0,"Position":17.0,"HyperDash":false},{"StartTime":104668.0,"Position":238.0,"HyperDash":false},{"StartTime":104725.0,"Position":222.0,"HyperDash":false},{"StartTime":104781.0,"Position":450.0,"HyperDash":false},{"StartTime":104838.0,"Position":67.0,"HyperDash":false},{"StartTime":104895.0,"Position":219.0,"HyperDash":false},{"StartTime":104952.0,"Position":307.0,"HyperDash":false},{"StartTime":105009.0,"Position":367.0,"HyperDash":false},{"StartTime":105065.0,"Position":412.0,"HyperDash":false},{"StartTime":105122.0,"Position":413.0,"HyperDash":false},{"StartTime":105179.0,"Position":143.0,"HyperDash":false},{"StartTime":105236.0,"Position":339.0,"HyperDash":false},{"StartTime":105293.0,"Position":342.0,"HyperDash":false},{"StartTime":105349.0,"Position":249.0,"HyperDash":false},{"StartTime":105406.0,"Position":235.0,"HyperDash":false},{"StartTime":105463.0,"Position":323.0,"HyperDash":false},{"StartTime":105520.0,"Position":365.0,"HyperDash":false},{"StartTime":105577.0,"Position":74.0,"HyperDash":false},{"StartTime":105634.0,"Position":281.0,"HyperDash":false},{"StartTime":105690.0,"Position":398.0,"HyperDash":false},{"StartTime":105747.0,"Position":335.0,"HyperDash":false},{"StartTime":105804.0,"Position":388.0,"HyperDash":false},{"StartTime":105861.0,"Position":228.0,"HyperDash":false},{"StartTime":105918.0,"Position":323.0,"HyperDash":false},{"StartTime":105974.0,"Position":441.0,"HyperDash":false},{"StartTime":106031.0,"Position":442.0,"HyperDash":false},{"StartTime":106088.0,"Position":278.0,"HyperDash":false},{"StartTime":106145.0,"Position":90.0,"HyperDash":false},{"StartTime":106202.0,"Position":409.0,"HyperDash":false},{"StartTime":106258.0,"Position":377.0,"HyperDash":false},{"StartTime":106315.0,"Position":457.0,"HyperDash":false},{"StartTime":106372.0,"Position":409.0,"HyperDash":false},{"StartTime":106429.0,"Position":43.0,"HyperDash":false},{"StartTime":106486.0,"Position":162.0,"HyperDash":false},{"StartTime":106543.0,"Position":341.0,"HyperDash":false},{"StartTime":106599.0,"Position":72.0,"HyperDash":false},{"StartTime":106656.0,"Position":135.0,"HyperDash":false},{"StartTime":106713.0,"Position":252.0,"HyperDash":false},{"StartTime":106770.0,"Position":446.0,"HyperDash":false},{"StartTime":106827.0,"Position":284.0,"HyperDash":false},{"StartTime":106883.0,"Position":70.0,"HyperDash":false},{"StartTime":106940.0,"Position":494.0,"HyperDash":false},{"StartTime":106997.0,"Position":463.0,"HyperDash":false},{"StartTime":107054.0,"Position":277.0,"HyperDash":false},{"StartTime":107111.0,"Position":425.0,"HyperDash":false},{"StartTime":107167.0,"Position":281.0,"HyperDash":false},{"StartTime":107224.0,"Position":3.0,"HyperDash":false},{"StartTime":107281.0,"Position":346.0,"HyperDash":false},{"StartTime":107338.0,"Position":350.0,"HyperDash":false},{"StartTime":107395.0,"Position":217.0,"HyperDash":false},{"StartTime":107452.0,"Position":455.0,"HyperDash":false},{"StartTime":107508.0,"Position":229.0,"HyperDash":false},{"StartTime":107565.0,"Position":51.0,"HyperDash":false},{"StartTime":107622.0,"Position":199.0,"HyperDash":false},{"StartTime":107679.0,"Position":208.0,"HyperDash":false},{"StartTime":107736.0,"Position":173.0,"HyperDash":false},{"StartTime":107792.0,"Position":367.0,"HyperDash":false},{"StartTime":107849.0,"Position":193.0,"HyperDash":false},{"StartTime":107906.0,"Position":488.0,"HyperDash":false},{"StartTime":107963.0,"Position":314.0,"HyperDash":false},{"StartTime":108020.0,"Position":135.0,"HyperDash":false},{"StartTime":108076.0,"Position":399.0,"HyperDash":false},{"StartTime":108133.0,"Position":404.0,"HyperDash":false},{"StartTime":108190.0,"Position":152.0,"HyperDash":false},{"StartTime":108247.0,"Position":353.0,"HyperDash":false},{"StartTime":108304.0,"Position":358.0,"HyperDash":false},{"StartTime":108361.0,"Position":447.0,"HyperDash":false},{"StartTime":108417.0,"Position":222.0,"HyperDash":false},{"StartTime":108474.0,"Position":382.0,"HyperDash":false},{"StartTime":108531.0,"Position":433.0,"HyperDash":false},{"StartTime":108588.0,"Position":450.0,"HyperDash":false},{"StartTime":108645.0,"Position":326.0,"HyperDash":false},{"StartTime":108701.0,"Position":414.0,"HyperDash":false},{"StartTime":108758.0,"Position":285.0,"HyperDash":false},{"StartTime":108815.0,"Position":336.0,"HyperDash":false},{"StartTime":108872.0,"Position":509.0,"HyperDash":false},{"StartTime":108929.0,"Position":334.0,"HyperDash":false},{"StartTime":108985.0,"Position":72.0,"HyperDash":false},{"StartTime":109042.0,"Position":425.0,"HyperDash":false},{"StartTime":109099.0,"Position":451.0,"HyperDash":false},{"StartTime":109156.0,"Position":220.0,"HyperDash":false},{"StartTime":109213.0,"Position":25.0,"HyperDash":false},{"StartTime":109270.0,"Position":77.0,"HyperDash":false}]},{"StartTime":111392.0,"Objects":[{"StartTime":111392.0,"Position":48.0,"HyperDash":false},{"StartTime":111449.0,"Position":89.64901,"HyperDash":false},{"StartTime":111543.0,"Position":108.0,"HyperDash":false}]},{"StartTime":111695.0,"Objects":[{"StartTime":111695.0,"Position":156.0,"HyperDash":false}]},{"StartTime":112301.0,"Objects":[{"StartTime":112301.0,"Position":347.0,"HyperDash":false},{"StartTime":112358.0,"Position":344.0,"HyperDash":false},{"StartTime":112452.0,"Position":347.0,"HyperDash":false}]},{"StartTime":112604.0,"Objects":[{"StartTime":112604.0,"Position":347.0,"HyperDash":false},{"StartTime":112661.0,"Position":343.0,"HyperDash":false},{"StartTime":112755.0,"Position":347.0,"HyperDash":false}]},{"StartTime":112907.0,"Objects":[{"StartTime":112907.0,"Position":347.0,"HyperDash":false}]},{"StartTime":113513.0,"Objects":[{"StartTime":113513.0,"Position":155.0,"HyperDash":false}]},{"StartTime":113664.0,"Objects":[{"StartTime":113664.0,"Position":155.0,"HyperDash":false}]},{"StartTime":113816.0,"Objects":[{"StartTime":113816.0,"Position":155.0,"HyperDash":false},{"StartTime":113891.0,"Position":169.702972,"HyperDash":false},{"StartTime":113967.0,"Position":201.801987,"HyperDash":false},{"StartTime":114043.0,"Position":248.900986,"HyperDash":false},{"StartTime":114119.0,"Position":275.0,"HyperDash":false},{"StartTime":114185.0,"Position":240.861389,"HyperDash":false},{"StartTime":114252.0,"Position":220.326736,"HyperDash":false},{"StartTime":114319.0,"Position":184.792084,"HyperDash":false},{"StartTime":114422.0,"Position":155.0,"HyperDash":false}]},{"StartTime":114725.0,"Objects":[{"StartTime":114725.0,"Position":155.0,"HyperDash":false},{"StartTime":114782.0,"Position":174.649,"HyperDash":false},{"StartTime":114876.0,"Position":215.0,"HyperDash":false}]},{"StartTime":115331.0,"Objects":[{"StartTime":115331.0,"Position":359.0,"HyperDash":false}]},{"StartTime":115634.0,"Objects":[{"StartTime":115634.0,"Position":359.0,"HyperDash":false},{"StartTime":115700.0,"Position":376.0,"HyperDash":false},{"StartTime":115767.0,"Position":358.0,"HyperDash":false},{"StartTime":115834.0,"Position":343.0,"HyperDash":false},{"StartTime":115937.0,"Position":359.0,"HyperDash":false}]},{"StartTime":116543.0,"Objects":[{"StartTime":116543.0,"Position":167.0,"HyperDash":false},{"StartTime":116600.0,"Position":186.0,"HyperDash":false},{"StartTime":116694.0,"Position":167.0,"HyperDash":false}]},{"StartTime":116846.0,"Objects":[{"StartTime":116846.0,"Position":167.0,"HyperDash":false}]},{"StartTime":116998.0,"Objects":[{"StartTime":116998.0,"Position":215.0,"HyperDash":false},{"StartTime":117055.0,"Position":232.649,"HyperDash":false},{"StartTime":117149.0,"Position":275.0,"HyperDash":false}]},{"StartTime":117301.0,"Objects":[{"StartTime":117301.0,"Position":323.0,"HyperDash":false}]},{"StartTime":117604.0,"Objects":[{"StartTime":117604.0,"Position":323.0,"HyperDash":false}]},{"StartTime":117907.0,"Objects":[{"StartTime":117907.0,"Position":227.0,"HyperDash":false}]},{"StartTime":118967.0,"Objects":[{"StartTime":118967.0,"Position":40.0,"HyperDash":false}]},{"StartTime":119573.0,"Objects":[{"StartTime":119573.0,"Position":231.0,"HyperDash":false}]},{"StartTime":120180.0,"Objects":[{"StartTime":120180.0,"Position":422.0,"HyperDash":false},{"StartTime":120255.0,"Position":413.0,"HyperDash":false},{"StartTime":120331.0,"Position":402.0,"HyperDash":false},{"StartTime":120407.0,"Position":413.0,"HyperDash":false},{"StartTime":120483.0,"Position":422.0,"HyperDash":false},{"StartTime":120549.0,"Position":440.0,"HyperDash":false},{"StartTime":120616.0,"Position":418.0,"HyperDash":false},{"StartTime":120683.0,"Position":433.0,"HyperDash":false},{"StartTime":120786.0,"Position":422.0,"HyperDash":false}]},{"StartTime":120937.0,"Objects":[{"StartTime":120937.0,"Position":373.0,"HyperDash":false}]},{"StartTime":121089.0,"Objects":[{"StartTime":121089.0,"Position":324.0,"HyperDash":false},{"StartTime":121155.0,"Position":293.8614,"HyperDash":false},{"StartTime":121222.0,"Position":274.326721,"HyperDash":false},{"StartTime":121289.0,"Position":262.792084,"HyperDash":false},{"StartTime":121392.0,"Position":204.0,"HyperDash":false}]},{"StartTime":121695.0,"Objects":[{"StartTime":121695.0,"Position":204.0,"HyperDash":false}]},{"StartTime":122604.0,"Objects":[{"StartTime":122604.0,"Position":40.0,"HyperDash":false}]},{"StartTime":122907.0,"Objects":[{"StartTime":122907.0,"Position":256.0,"HyperDash":false}]},{"StartTime":123210.0,"Objects":[{"StartTime":123210.0,"Position":472.0,"HyperDash":false}]},{"StartTime":123816.0,"Objects":[{"StartTime":123816.0,"Position":472.0,"HyperDash":false},{"StartTime":123891.0,"Position":427.297028,"HyperDash":false},{"StartTime":123967.0,"Position":429.198029,"HyperDash":false},{"StartTime":124043.0,"Position":387.099,"HyperDash":false},{"StartTime":124119.0,"Position":352.0,"HyperDash":false},{"StartTime":124194.0,"Position":317.297028,"HyperDash":false},{"StartTime":124270.0,"Position":277.198029,"HyperDash":false},{"StartTime":124346.0,"Position":258.099,"HyperDash":false},{"StartTime":124422.0,"Position":232.0,"HyperDash":false},{"StartTime":124497.0,"Position":217.297028,"HyperDash":false},{"StartTime":124573.0,"Position":174.198029,"HyperDash":false},{"StartTime":124649.0,"Position":134.09903,"HyperDash":false},{"StartTime":124725.0,"Position":112.0,"HyperDash":false},{"StartTime":124800.0,"Position":74.29706,"HyperDash":false},{"StartTime":124876.0,"Position":66.19803,"HyperDash":false},{"StartTime":124952.0,"Position":49.0,"HyperDash":false},{"StartTime":125028.0,"Position":32.0,"HyperDash":false},{"StartTime":125103.0,"Position":44.0,"HyperDash":false},{"StartTime":125179.0,"Position":49.0,"HyperDash":false},{"StartTime":125255.0,"Position":39.901,"HyperDash":false},{"StartTime":125331.0,"Position":88.0,"HyperDash":false},{"StartTime":125397.0,"Position":106.138611,"HyperDash":false},{"StartTime":125464.0,"Position":129.673279,"HyperDash":false},{"StartTime":125531.0,"Position":176.207947,"HyperDash":false},{"StartTime":125634.0,"Position":208.0,"HyperDash":false}]},{"StartTime":126240.0,"Objects":[{"StartTime":126240.0,"Position":399.0,"HyperDash":false}]},{"StartTime":126846.0,"Objects":[{"StartTime":126846.0,"Position":399.0,"HyperDash":false}]},{"StartTime":127452.0,"Objects":[{"StartTime":127452.0,"Position":315.0,"HyperDash":false},{"StartTime":127508.0,"Position":35.0,"HyperDash":false},{"StartTime":127565.0,"Position":208.0,"HyperDash":false},{"StartTime":127622.0,"Position":504.0,"HyperDash":false},{"StartTime":127679.0,"Position":296.0,"HyperDash":false},{"StartTime":127736.0,"Position":105.0,"HyperDash":false},{"StartTime":127792.0,"Position":488.0,"HyperDash":false},{"StartTime":127849.0,"Position":230.0,"HyperDash":false},{"StartTime":127906.0,"Position":446.0,"HyperDash":false},{"StartTime":127963.0,"Position":241.0,"HyperDash":false},{"StartTime":128020.0,"Position":413.0,"HyperDash":false},{"StartTime":128076.0,"Position":357.0,"HyperDash":false},{"StartTime":128133.0,"Position":256.0,"HyperDash":false},{"StartTime":128190.0,"Position":192.0,"HyperDash":false},{"StartTime":128247.0,"Position":116.0,"HyperDash":false},{"StartTime":128304.0,"Position":397.0,"HyperDash":false},{"StartTime":128361.0,"Position":422.0,"HyperDash":false},{"StartTime":128417.0,"Position":230.0,"HyperDash":false},{"StartTime":128474.0,"Position":479.0,"HyperDash":false},{"StartTime":128531.0,"Position":276.0,"HyperDash":false},{"StartTime":128588.0,"Position":423.0,"HyperDash":false},{"StartTime":128645.0,"Position":450.0,"HyperDash":false},{"StartTime":128701.0,"Position":336.0,"HyperDash":false},{"StartTime":128758.0,"Position":145.0,"HyperDash":false},{"StartTime":128815.0,"Position":30.0,"HyperDash":false},{"StartTime":128872.0,"Position":426.0,"HyperDash":false},{"StartTime":128929.0,"Position":394.0,"HyperDash":false},{"StartTime":128985.0,"Position":274.0,"HyperDash":false},{"StartTime":129042.0,"Position":44.0,"HyperDash":false},{"StartTime":129099.0,"Position":32.0,"HyperDash":false},{"StartTime":129156.0,"Position":10.0,"HyperDash":false},{"StartTime":129213.0,"Position":505.0,"HyperDash":false},{"StartTime":129270.0,"Position":321.0,"HyperDash":false}]},{"StartTime":129876.0,"Objects":[{"StartTime":129876.0,"Position":48.0,"HyperDash":false}]},{"StartTime":130483.0,"Objects":[{"StartTime":130483.0,"Position":144.0,"HyperDash":false}]},{"StartTime":131089.0,"Objects":[{"StartTime":131089.0,"Position":240.0,"HyperDash":false},{"StartTime":131164.0,"Position":239.851486,"HyperDash":false},{"StartTime":131240.0,"Position":286.901,"HyperDash":false},{"StartTime":131316.0,"Position":289.9505,"HyperDash":false},{"StartTime":131392.0,"Position":282.0,"HyperDash":false},{"StartTime":131467.0,"Position":312.8515,"HyperDash":false},{"StartTime":131543.0,"Position":328.901,"HyperDash":false},{"StartTime":131619.0,"Position":352.9505,"HyperDash":false},{"StartTime":131695.0,"Position":360.0,"HyperDash":false},{"StartTime":131770.0,"Position":349.0,"HyperDash":false},{"StartTime":131846.0,"Position":359.0,"HyperDash":false},{"StartTime":131922.0,"Position":362.0,"HyperDash":false},{"StartTime":131998.0,"Position":352.0,"HyperDash":false},{"StartTime":132073.0,"Position":356.0,"HyperDash":false},{"StartTime":132149.0,"Position":371.0,"HyperDash":false},{"StartTime":132225.0,"Position":346.0,"HyperDash":false},{"StartTime":132301.0,"Position":360.0,"HyperDash":false},{"StartTime":132372.0,"Position":328.9406,"HyperDash":false},{"StartTime":132443.0,"Position":335.8812,"HyperDash":false},{"StartTime":132514.0,"Position":328.821777,"HyperDash":false},{"StartTime":132586.0,"Position":302.564362,"HyperDash":false},{"StartTime":132657.0,"Position":285.504944,"HyperDash":false},{"StartTime":132728.0,"Position":274.445557,"HyperDash":false},{"StartTime":132799.0,"Position":246.386139,"HyperDash":false},{"StartTime":132907.0,"Position":240.0,"HyperDash":false}]},{"StartTime":133513.0,"Objects":[{"StartTime":133513.0,"Position":144.0,"HyperDash":false}]},{"StartTime":134119.0,"Objects":[{"StartTime":134119.0,"Position":144.0,"HyperDash":false}]},{"StartTime":134725.0,"Objects":[{"StartTime":134725.0,"Position":423.0,"HyperDash":false},{"StartTime":134781.0,"Position":367.0,"HyperDash":false},{"StartTime":134838.0,"Position":146.0,"HyperDash":false},{"StartTime":134895.0,"Position":322.0,"HyperDash":false},{"StartTime":134952.0,"Position":169.0,"HyperDash":false},{"StartTime":135009.0,"Position":159.0,"HyperDash":false},{"StartTime":135065.0,"Position":388.0,"HyperDash":false},{"StartTime":135122.0,"Position":67.0,"HyperDash":false},{"StartTime":135179.0,"Position":176.0,"HyperDash":false},{"StartTime":135236.0,"Position":371.0,"HyperDash":false},{"StartTime":135293.0,"Position":365.0,"HyperDash":false},{"StartTime":135349.0,"Position":104.0,"HyperDash":false},{"StartTime":135406.0,"Position":363.0,"HyperDash":false},{"StartTime":135463.0,"Position":75.0,"HyperDash":false},{"StartTime":135520.0,"Position":158.0,"HyperDash":false},{"StartTime":135577.0,"Position":98.0,"HyperDash":false},{"StartTime":135634.0,"Position":30.0,"HyperDash":false},{"StartTime":135690.0,"Position":164.0,"HyperDash":false},{"StartTime":135747.0,"Position":341.0,"HyperDash":false},{"StartTime":135804.0,"Position":18.0,"HyperDash":false},{"StartTime":135861.0,"Position":210.0,"HyperDash":false},{"StartTime":135918.0,"Position":420.0,"HyperDash":false},{"StartTime":135974.0,"Position":447.0,"HyperDash":false},{"StartTime":136031.0,"Position":78.0,"HyperDash":false},{"StartTime":136088.0,"Position":177.0,"HyperDash":false},{"StartTime":136145.0,"Position":305.0,"HyperDash":false},{"StartTime":136202.0,"Position":400.0,"HyperDash":false},{"StartTime":136258.0,"Position":462.0,"HyperDash":false},{"StartTime":136315.0,"Position":64.0,"HyperDash":false},{"StartTime":136372.0,"Position":458.0,"HyperDash":false},{"StartTime":136429.0,"Position":380.0,"HyperDash":false},{"StartTime":136486.0,"Position":65.0,"HyperDash":false},{"StartTime":136543.0,"Position":23.0,"HyperDash":false},{"StartTime":136599.0,"Position":379.0,"HyperDash":false},{"StartTime":136656.0,"Position":44.0,"HyperDash":false},{"StartTime":136713.0,"Position":485.0,"HyperDash":false},{"StartTime":136770.0,"Position":269.0,"HyperDash":false},{"StartTime":136827.0,"Position":155.0,"HyperDash":false},{"StartTime":136883.0,"Position":324.0,"HyperDash":false},{"StartTime":136940.0,"Position":149.0,"HyperDash":false},{"StartTime":136997.0,"Position":351.0,"HyperDash":false},{"StartTime":137054.0,"Position":385.0,"HyperDash":false},{"StartTime":137111.0,"Position":338.0,"HyperDash":false},{"StartTime":137167.0,"Position":322.0,"HyperDash":false},{"StartTime":137224.0,"Position":84.0,"HyperDash":false},{"StartTime":137281.0,"Position":342.0,"HyperDash":false},{"StartTime":137338.0,"Position":395.0,"HyperDash":false},{"StartTime":137395.0,"Position":72.0,"HyperDash":false},{"StartTime":137452.0,"Position":324.0,"HyperDash":false},{"StartTime":137508.0,"Position":67.0,"HyperDash":false},{"StartTime":137565.0,"Position":371.0,"HyperDash":false},{"StartTime":137622.0,"Position":446.0,"HyperDash":false},{"StartTime":137679.0,"Position":29.0,"HyperDash":false},{"StartTime":137736.0,"Position":22.0,"HyperDash":false},{"StartTime":137792.0,"Position":432.0,"HyperDash":false},{"StartTime":137849.0,"Position":12.0,"HyperDash":false},{"StartTime":137906.0,"Position":330.0,"HyperDash":false},{"StartTime":137963.0,"Position":419.0,"HyperDash":false},{"StartTime":138020.0,"Position":278.0,"HyperDash":false},{"StartTime":138076.0,"Position":202.0,"HyperDash":false},{"StartTime":138133.0,"Position":208.0,"HyperDash":false},{"StartTime":138190.0,"Position":21.0,"HyperDash":false},{"StartTime":138247.0,"Position":437.0,"HyperDash":false},{"StartTime":138304.0,"Position":312.0,"HyperDash":false},{"StartTime":138361.0,"Position":508.0,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/50859.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/50859.osu new file mode 100644 index 0000000000..8272b8b1db --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/50859.osu @@ -0,0 +1,290 @@ +osu file format v7 + +[General] +StackLeniency: 0.5 +Mode: 0 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:7 +SliderMultiplier:2.4 +SliderTickRate:2 + +[Events] +//Break Periods +2,109470,110492 +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples +//Background Colour Transformations +3,100,163,162,255 + +[TimingPoints] +180,606.060606060606,3,2,1,20,1,0 +11528,-100,4,2,1,50,0,0 +28952,-100,4,2,1,20,0,0 +36452,-100,4,2,1,50,0,0 +43523,-50,4,2,2,20,0,0 +50921,-100,4,2,1,40,0,0 +51073,-100,4,2,1,60,0,0 +53371,-100,4,2,2,60,0,0 +54280,-100,4,2,1,60,0,0 +58195,-100,4,2,1,40,0,0 +65468,-100,4,2,1,60,0,0 +68801,-100,4,1,0,30,0,0 +69129,-100,4,1,1,30,0,0 +69407,-100,4,2,1,60,0,0 +75644,-100,4,1,0,35,0,0 +76680,-100,4,2,1,60,0,0 +78195,-100,4,2,1,40,0,0 +78649,-100,4,2,1,60,0,0 +87386,-100,4,1,2,30,0,0 +101856,-100,4,1,1,40,0,0 +109583,-100,4,2,2,60,0,0 +112008,-100,4,1,2,20,0,0 +113068,-100,4,2,2,60,0,0 +114583,-100,4,2,2,40,0,0 +115038,-100,4,2,2,60,0,0 +123523,-100,4,1,2,30,0,0 +124280,-100,4,1,0,30,0,0 +124583,-100,4,1,2,30,0,0 +124886,-100,4,1,0,30,0,0 +125189,-100,4,1,2,30,0,0 +125644,-100,4,1,1,30,0,0 +125947,-100,4,1,2,20,0,0 +127159,-100,4,1,1,60,0,0 +129583,-200,4,1,0,20,0,0 +134583,-200,4,1,1,0,0,0 + +[HitObjects] +120,72,179,1,0 +311,72,786,2,0,B|448:72,1,120,2|0 +431,167,1392,2,0,B|303:167,1,120,2|2 +215,167,1998,1,0 +119,167,2301,6,0,B|487:167,1,360,2|2 +478,261,3513,1,0 +382,261,3816,6,0,B|254:261,1,120 +166,261,4422,2,0,B|166:138,1,120,2|2 +166,332,5331,1,0 +261,332,5634,2,0,L|327:332,1,60,0|2 +321,235,6089,2,0,L|321:175,1,60,0|2 +321,79,6543,1,0 +465,79,6998,2,0,L|465:143,1,60,0|2 +369,139,7452,6,0,B|369:278,1,120 +464,259,8058,1,2 +464,163,8361,2,0,B|288:163,1,120,0|2 +248,163,8967,1,0 +200,243,9270,1,0 +296,243,9573,1,2 +275,37,10180,5,0 +179,37,10483,1,2 +179,132,10786,2,0,B|307:132,1,120,2|0 +299,227,11392,1,0 +203,227,11695,6,0,L|142:227,1,60 +94,227,11998,1,2 +94,131,12301,1,2 +189,131,12604,1,0 +476,131,13513,1,0 +380,131,13816,1,2 +272,23,14725,6,0,L|208:23,1,60,2|0 +177,57,15028,2,0,L|177:129,1,60 +225,117,15331,1,0 +273,117,15483,1,2 +273,211,15786,1,2 +273,306,16089,1,2 +33,306,16846,6,0,L|33:242,1,60 +33,197,17149,1,2 +224,197,17755,1,0 +277,50,18967,5,0 +228,50,19119,1,0 +181,50,19270,1,2 +181,145,19573,1,2 +181,240,19876,1,0 +469,240,20786,5,0 +373,240,21089,1,2 +277,240,21392,1,0 +243,350,21998,5,2 +243,302,22149,1,0 +243,254,22301,1,2 +290,254,22452,2,0,L|290:193,1,60 +290,146,22755,1,2 +385,146,23058,1,2 +385,241,23361,1,2 +213,68,24119,6,0,L|149:68,1,60,0|0 +104,68,24422,1,2 +295,68,25028,1,0 +56,64,26240,5,0 +56,64,26392,1,0 +56,64,26543,1,2 +56,159,26846,1,2 +151,159,27149,1,0 +438,159,28058,6,0,B|438:303,1,120,0|2 +184,192,29270,6,0,B|312:192,1,120,6|0 +399,192,29876,1,2 +399,95,30180,1,0 +303,95,30483,2,0,B|129:95,1,120,2|0 +115,162,31089,6,0,B|243:162,1,120,2|0 +330,162,31695,1,2 +425,162,31998,1,0 +425,257,32301,2,0,B|265:257,1,120,2|0 +209,257,32907,6,0,B|65:257,1,120,6|0 +89,160,33513,1,2 +184,160,33816,1,0 +279,160,34119,1,2 +374,160,34422,1,0 +469,160,34725,6,0,B|469:304,1,120,2|0 +373,280,35331,2,0,B|216:280,1,120,2|0 +157,280,35937,1,2 +157,184,36240,1,0 +157,135,36392,1,0 +204,135,36543,6,0,B|268:135,2,60,2|0|2 +204,183,36998,2,0,B|204:255,1,60 +205,291,37301,1,2 +300,291,37604,1,2 +300,195,37907,1,2 +32,32,38967,5,2 +32,223,39573,1,0 +416,223,40786,5,0 +416,176,40937,1,0 +416,128,41089,1,2 +320,128,41392,1,2 +320,224,41695,1,0 +48,128,42604,6,0,B|192:128,1,120,0|2 +263,128,43210,1,0 +376,192,43816,6,0,B|136:192,5,240,6|0|2|0|2|0 +376,248,45634,6,0,B|136:248,5,240,2|0|2|0|2|0 +376,184,47452,6,0,B|136:184,5,240,6|0|2|0|2|0 +376,248,49270,6,0,B|109:247,1,240,2|0 +136,136,49876,1,2 +328,136,50180,1,0 +329,326,50483,1,2 +136,328,50786,1,0 +138,278,50937,1,0 +138,229,51089,6,0,B|255:229,1,60,6|0 +198,180,51392,1,2 +246,180,51543,1,0 +295,180,51695,2,0,B|365:180,1,60,0|2 +355,84,52149,1,2 +260,84,52452,1,2 +40,344,53513,6,0,L|280:344,1,240,2|0 +40,40,55331,6,0,L|120:40,2,60,0|0|2 +40,135,55937,2,0,L|40:262,1,120,2|0 +300,132,57149,6,0,L|300:272,1,120,0|2 +256,192,58361,6,0,L|336:192,1,60,2|0 +364,192,58664,2,0,L|364:256,1,60,2|0 +329,286,58967,1,0 +280,286,59119,2,0,L|208:286,1,60,2|0 +185,251,59422,2,0,L|185:179,1,60,2|0 +185,95,59876,1,4 +253,163,60180,5,2 +253,163,60331,1,2 +253,163,60483,1,2 +253,163,60634,1,0 +253,211,60786,2,0,L|192:211,4,60,2|0|2|0|2 +253,115,61695,1,4 +348,115,61998,6,0,L|348:51,4,60,2|0|2|0|2 +348,162,62755,1,2 +348,210,62907,1,2 +348,257,63058,1,0 +348,257,63210,1,2 +252,257,63513,1,4 +252,161,63816,5,0 +252,113,63967,2,0,L|169:113,3,60,2|0|2|0 +288,113,64725,1,4 +383,113,65028,1,4 +383,208,65331,1,0 +287,208,65634,6,0,L|195:208,1,60,2|0 +178,208,65937,1,2 +129,208,66089,1,0 +81,208,66240,1,0 +81,256,66392,1,2 +81,303,66543,2,0,L|145:303,1,60,0|2 +189,303,66846,2,0,L|253:303,1,60,0|2 +192,48,67755,6,0,L|304:48,1,60 +300,48,68058,1,2 +300,239,68664,1,0 +300,143,68967,5,0 +204,143,69270,1,4 +395,143,69876,6,0,L|396:226,1,60,0|0 +395,251,70180,1,2 +296,248,70483,1,2 +200,248,70786,1,0 +200,40,71695,1,0 +295,40,71998,1,2 +91,243,72907,5,2 +138,243,73058,1,0 +186,243,73210,1,0 +186,290,73361,2,0,L|254:290,1,60,2|0 +294,290,73664,2,0,L|371:290,1,60,2|0 +354,241,73967,1,2 +354,145,74270,1,2 +40,40,75331,5,2 +160,208,75937,1,0 +160,208,76089,1,0 +303,208,76543,1,4 +160,80,77149,6,0,L|232:80,1,60,0|0 +268,80,77452,1,2 +268,175,77755,1,2 +268,270,78058,1,0 +363,270,78361,6,4,L|363:187,1,60 +363,65,78967,5,0 +267,65,79270,2,0,L|126:65,1,120,2|0 +96,32,80180,6,0,L|96:344|296:344,1,480 +360,344,81695,1,0 +455,344,81998,2,0,L|455:32|159:32,1,600,2|0 +124,99,83816,6,0,L|364:99|364:347,1,480 +268,339,85331,1,4 +172,339,85634,2,0,L|52:339|52:235|52:123|156:123|316:123|316:219,1,660 +316,231,87452,6,0,L|316:354,1,120 +411,351,88058,2,0,L|411:103|147:103,1,480,4|0 +176,296,89876,2,0,L|40:296|40:152,1,240 +232,191,91089,5,4 +423,191,91695,2,0,L|423:351|71:351,1,480,4|4 +112,167,93513,1,0 +303,167,94119,1,0 +440,280,94725,6,0,L|440:35,2,240,4|4|0 +216,280,96543,2,0,L|216:32,1,240,4|0 +48,160,97755,1,0 +216,40,98361,5,4 +216,352,98967,2,0,L|216:104,2,240,4|0|4 +216,32,100786,1,4 +216,352,101392,1,0 +256,192,101998,12,0,109270 +48,48,111392,6,0,L|128:48,1,60 +156,48,111695,1,2 +347,48,112301,2,0,L|347:112,1,60 +347,156,112604,2,0,L|347:220,1,60 +347,264,112907,1,4 +155,264,113513,5,0 +155,216,113664,1,0 +155,167,113816,2,0,L|275:167,2,120,2|2|0 +155,71,114725,6,4,L|217:71,1,60 +359,71,115331,5,0 +359,166,115634,2,0,L|359:296,1,120,2|0 +167,286,116543,6,0,L|167:205,1,60,2|0 +167,177,116846,1,0 +215,177,116998,2,0,L|281:177,1,60,2|0 +323,177,117301,5,2 +323,81,117604,1,2 +227,81,117907,1,2 +40,344,118967,5,2 +231,344,119573,1,0 +422,344,120180,6,0,L|422:50,1,240 +373,104,120937,1,0 +324,104,121089,2,0,L|204:104,1,120,2|2 +204,199,121695,1,0 +40,40,122604,5,0 +256,40,122907,1,2 +472,40,123210,1,0 +472,232,123816,6,2,L|32:232|32:336|240:336,1,720,2|2 +399,336,126240,1,8 +399,144,126846,1,8 +256,192,127452,12,0,129270 +48,192,129876,5,8 +144,192,130483,1,8 +240,192,131089,2,2,L|360:192|360:72|240:72,1,360,2|2 +144,72,133513,1,8 +144,167,134119,1,8 +256,192,134725,12,0,138361 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/75858-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/75858-expected-conversion.json new file mode 100644 index 0000000000..d5db48bc8c --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/75858-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":1173.0,"Objects":[{"StartTime":1173.0,"Position":94.0,"HyperDash":false},{"StartTime":1251.0,"Position":94.284874,"HyperDash":false},{"StartTime":1330.0,"Position":115.604385,"HyperDash":false},{"StartTime":1409.0,"Position":141.8706,"HyperDash":false},{"StartTime":1488.0,"Position":178.597519,"HyperDash":false},{"StartTime":1566.0,"Position":199.289474,"HyperDash":false},{"StartTime":1645.0,"Position":202.258377,"HyperDash":false},{"StartTime":1724.0,"Position":219.14473,"HyperDash":false},{"StartTime":1839.0,"Position":247.271439,"HyperDash":false}]},{"StartTime":2506.0,"Objects":[{"StartTime":2506.0,"Position":398.0,"HyperDash":false}]},{"StartTime":3172.0,"Objects":[{"StartTime":3172.0,"Position":471.0,"HyperDash":false}]},{"StartTime":3839.0,"Objects":[{"StartTime":3839.0,"Position":320.0,"HyperDash":false},{"StartTime":3917.0,"Position":287.205841,"HyperDash":false},{"StartTime":3996.0,"Position":275.6191,"HyperDash":false},{"StartTime":4075.0,"Position":252.736725,"HyperDash":false},{"StartTime":4154.0,"Position":241.768585,"HyperDash":false},{"StartTime":4232.0,"Position":241.149734,"HyperDash":false},{"StartTime":4311.0,"Position":212.634354,"HyperDash":false},{"StartTime":4390.0,"Position":181.770065,"HyperDash":false},{"StartTime":4505.0,"Position":166.756821,"HyperDash":false}]},{"StartTime":5173.0,"Objects":[{"StartTime":5173.0,"Position":65.0,"HyperDash":false}]},{"StartTime":5839.0,"Objects":[{"StartTime":5839.0,"Position":233.0,"HyperDash":false}]},{"StartTime":6506.0,"Objects":[{"StartTime":6506.0,"Position":239.0,"HyperDash":false},{"StartTime":6584.0,"Position":262.183746,"HyperDash":false},{"StartTime":6663.0,"Position":257.084351,"HyperDash":false},{"StartTime":6742.0,"Position":286.044983,"HyperDash":false},{"StartTime":6821.0,"Position":331.921021,"HyperDash":false},{"StartTime":6899.0,"Position":349.51355,"HyperDash":false},{"StartTime":6978.0,"Position":364.384766,"HyperDash":false},{"StartTime":7057.0,"Position":362.333984,"HyperDash":false},{"StartTime":7172.0,"Position":397.8736,"HyperDash":false}]},{"StartTime":7839.0,"Objects":[{"StartTime":7839.0,"Position":493.0,"HyperDash":false}]},{"StartTime":8506.0,"Objects":[{"StartTime":8506.0,"Position":175.0,"HyperDash":false}]},{"StartTime":9173.0,"Objects":[{"StartTime":9173.0,"Position":223.0,"HyperDash":false}]},{"StartTime":9839.0,"Objects":[{"StartTime":9839.0,"Position":119.0,"HyperDash":false}]},{"StartTime":10506.0,"Objects":[{"StartTime":10506.0,"Position":423.0,"HyperDash":false}]},{"StartTime":10839.0,"Objects":[{"StartTime":10839.0,"Position":199.0,"HyperDash":false},{"StartTime":10917.0,"Position":173.819885,"HyperDash":false},{"StartTime":10996.0,"Position":195.248886,"HyperDash":false},{"StartTime":11075.0,"Position":168.416779,"HyperDash":false},{"StartTime":11154.0,"Position":168.359177,"HyperDash":false},{"StartTime":11232.0,"Position":140.23172,"HyperDash":false},{"StartTime":11311.0,"Position":141.862854,"HyperDash":false},{"StartTime":11390.0,"Position":135.414291,"HyperDash":false},{"StartTime":11505.0,"Position":122.606651,"HyperDash":false}]},{"StartTime":11839.0,"Objects":[{"StartTime":11839.0,"Position":42.0,"HyperDash":false}]},{"StartTime":12506.0,"Objects":[{"StartTime":12506.0,"Position":178.0,"HyperDash":false}]},{"StartTime":13172.0,"Objects":[{"StartTime":13172.0,"Position":263.0,"HyperDash":false},{"StartTime":13250.0,"Position":294.723358,"HyperDash":false},{"StartTime":13329.0,"Position":284.270325,"HyperDash":false},{"StartTime":13408.0,"Position":299.0951,"HyperDash":false},{"StartTime":13487.0,"Position":321.0376,"HyperDash":false},{"StartTime":13565.0,"Position":368.773682,"HyperDash":false},{"StartTime":13644.0,"Position":391.718231,"HyperDash":false},{"StartTime":13723.0,"Position":392.566528,"HyperDash":false},{"StartTime":13838.0,"Position":420.607758,"HyperDash":false}]},{"StartTime":14506.0,"Objects":[{"StartTime":14506.0,"Position":293.0,"HyperDash":false},{"StartTime":14584.0,"Position":277.467133,"HyperDash":false},{"StartTime":14663.0,"Position":271.341,"HyperDash":false},{"StartTime":14742.0,"Position":276.740753,"HyperDash":false},{"StartTime":14821.0,"Position":235.4871,"HyperDash":false},{"StartTime":14899.0,"Position":229.366821,"HyperDash":false},{"StartTime":14978.0,"Position":219.329987,"HyperDash":false},{"StartTime":15057.0,"Position":204.814072,"HyperDash":false},{"StartTime":15172.0,"Position":160.38443,"HyperDash":false}]},{"StartTime":15839.0,"Objects":[{"StartTime":15839.0,"Position":282.0,"HyperDash":false},{"StartTime":15917.0,"Position":307.532867,"HyperDash":false},{"StartTime":15996.0,"Position":317.659,"HyperDash":false},{"StartTime":16075.0,"Position":334.259247,"HyperDash":false},{"StartTime":16154.0,"Position":342.512878,"HyperDash":false},{"StartTime":16232.0,"Position":333.633179,"HyperDash":false},{"StartTime":16311.0,"Position":373.67,"HyperDash":false},{"StartTime":16390.0,"Position":375.1859,"HyperDash":false},{"StartTime":16505.0,"Position":414.61557,"HyperDash":false}]},{"StartTime":17172.0,"Objects":[{"StartTime":17172.0,"Position":416.0,"HyperDash":false}]},{"StartTime":17839.0,"Objects":[{"StartTime":17839.0,"Position":256.0,"HyperDash":false},{"StartTime":17920.0,"Position":221.750565,"HyperDash":false},{"StartTime":18001.0,"Position":235.501129,"HyperDash":false},{"StartTime":18082.0,"Position":190.251709,"HyperDash":false},{"StartTime":18163.0,"Position":195.002274,"HyperDash":false},{"StartTime":18244.0,"Position":183.643,"HyperDash":false},{"StartTime":18325.0,"Position":181.956619,"HyperDash":false},{"StartTime":18406.0,"Position":200.650589,"HyperDash":false},{"StartTime":18487.0,"Position":193.194916,"HyperDash":false},{"StartTime":18568.0,"Position":202.48082,"HyperDash":false},{"StartTime":18649.0,"Position":179.827667,"HyperDash":false},{"StartTime":18730.0,"Position":173.703339,"HyperDash":false},{"StartTime":18811.0,"Position":186.446991,"HyperDash":false},{"StartTime":18892.0,"Position":151.2917,"HyperDash":false},{"StartTime":18973.0,"Position":126.879227,"HyperDash":false},{"StartTime":19054.0,"Position":122.770569,"HyperDash":false},{"StartTime":19172.0,"Position":99.93324,"HyperDash":false}]},{"StartTime":19839.0,"Objects":[{"StartTime":19839.0,"Position":256.0,"HyperDash":false}]},{"StartTime":20173.0,"Objects":[{"StartTime":20173.0,"Position":123.0,"HyperDash":false},{"StartTime":20245.0,"Position":42.0,"HyperDash":false},{"StartTime":20318.0,"Position":393.0,"HyperDash":false},{"StartTime":20391.0,"Position":75.0,"HyperDash":false},{"StartTime":20464.0,"Position":377.0,"HyperDash":false},{"StartTime":20537.0,"Position":354.0,"HyperDash":false},{"StartTime":20610.0,"Position":287.0,"HyperDash":false},{"StartTime":20683.0,"Position":361.0,"HyperDash":false},{"StartTime":20756.0,"Position":479.0,"HyperDash":false},{"StartTime":20829.0,"Position":346.0,"HyperDash":false},{"StartTime":20902.0,"Position":266.0,"HyperDash":false},{"StartTime":20974.0,"Position":400.0,"HyperDash":false},{"StartTime":21047.0,"Position":202.0,"HyperDash":false},{"StartTime":21120.0,"Position":500.0,"HyperDash":false},{"StartTime":21193.0,"Position":80.0,"HyperDash":false},{"StartTime":21266.0,"Position":399.0,"HyperDash":false},{"StartTime":21339.0,"Position":455.0,"HyperDash":false},{"StartTime":21412.0,"Position":105.0,"HyperDash":false},{"StartTime":21485.0,"Position":100.0,"HyperDash":false},{"StartTime":21558.0,"Position":195.0,"HyperDash":false},{"StartTime":21631.0,"Position":106.0,"HyperDash":false},{"StartTime":21704.0,"Position":305.0,"HyperDash":false},{"StartTime":21776.0,"Position":225.0,"HyperDash":false},{"StartTime":21849.0,"Position":79.0,"HyperDash":false},{"StartTime":21922.0,"Position":38.0,"HyperDash":false},{"StartTime":21995.0,"Position":99.0,"HyperDash":false},{"StartTime":22068.0,"Position":79.0,"HyperDash":false},{"StartTime":22141.0,"Position":169.0,"HyperDash":false},{"StartTime":22214.0,"Position":238.0,"HyperDash":false},{"StartTime":22287.0,"Position":511.0,"HyperDash":false},{"StartTime":22360.0,"Position":58.0,"HyperDash":false},{"StartTime":22433.0,"Position":368.0,"HyperDash":false},{"StartTime":22506.0,"Position":52.0,"HyperDash":false}]},{"StartTime":22961.0,"Objects":[{"StartTime":22961.0,"Position":256.0,"HyperDash":false}]},{"StartTime":23415.0,"Objects":[{"StartTime":23415.0,"Position":236.0,"HyperDash":false}]},{"StartTime":23870.0,"Objects":[{"StartTime":23870.0,"Position":104.0,"HyperDash":false},{"StartTime":23926.0,"Position":115.299927,"HyperDash":false},{"StartTime":23983.0,"Position":150.004791,"HyperDash":false},{"StartTime":24040.0,"Position":149.768875,"HyperDash":false},{"StartTime":24097.0,"Position":172.51532,"HyperDash":false},{"StartTime":24192.0,"Position":154.09137,"HyperDash":false},{"StartTime":24324.0,"Position":104.0,"HyperDash":false}]},{"StartTime":24779.0,"Objects":[{"StartTime":24779.0,"Position":256.0,"HyperDash":false},{"StartTime":24835.0,"Position":262.6648,"HyperDash":false},{"StartTime":24892.0,"Position":294.163452,"HyperDash":false},{"StartTime":24949.0,"Position":301.429565,"HyperDash":false},{"StartTime":25006.0,"Position":329.7457,"HyperDash":false},{"StartTime":25101.0,"Position":304.427277,"HyperDash":false},{"StartTime":25233.0,"Position":256.0,"HyperDash":false}]},{"StartTime":25688.0,"Objects":[{"StartTime":25688.0,"Position":118.0,"HyperDash":false},{"StartTime":25783.0,"Position":160.2344,"HyperDash":false},{"StartTime":25915.0,"Position":196.5579,"HyperDash":false}]},{"StartTime":26142.0,"Objects":[{"StartTime":26142.0,"Position":321.0,"HyperDash":false}]},{"StartTime":26597.0,"Objects":[{"StartTime":26597.0,"Position":419.0,"HyperDash":false},{"StartTime":26692.0,"Position":383.782776,"HyperDash":false},{"StartTime":26824.0,"Position":341.8768,"HyperDash":false}]},{"StartTime":27052.0,"Objects":[{"StartTime":27052.0,"Position":185.0,"HyperDash":false}]},{"StartTime":27506.0,"Objects":[{"StartTime":27506.0,"Position":71.0,"HyperDash":false}]},{"StartTime":27733.0,"Objects":[{"StartTime":27733.0,"Position":97.0,"HyperDash":false},{"StartTime":27828.0,"Position":69.73373,"HyperDash":false},{"StartTime":27960.0,"Position":95.43024,"HyperDash":false}]},{"StartTime":28415.0,"Objects":[{"StartTime":28415.0,"Position":376.0,"HyperDash":false}]},{"StartTime":28642.0,"Objects":[{"StartTime":28642.0,"Position":313.0,"HyperDash":false},{"StartTime":28737.0,"Position":349.615631,"HyperDash":false},{"StartTime":28869.0,"Position":392.036163,"HyperDash":false}]},{"StartTime":29324.0,"Objects":[{"StartTime":29324.0,"Position":501.0,"HyperDash":false}]},{"StartTime":29552.0,"Objects":[{"StartTime":29552.0,"Position":411.0,"HyperDash":false}]},{"StartTime":29779.0,"Objects":[{"StartTime":29779.0,"Position":501.0,"HyperDash":false}]},{"StartTime":30233.0,"Objects":[{"StartTime":30233.0,"Position":311.0,"HyperDash":false}]},{"StartTime":30461.0,"Objects":[{"StartTime":30461.0,"Position":231.0,"HyperDash":false}]},{"StartTime":30688.0,"Objects":[{"StartTime":30688.0,"Position":151.0,"HyperDash":false},{"StartTime":30744.0,"Position":136.485382,"HyperDash":false},{"StartTime":30801.0,"Position":111.448036,"HyperDash":false},{"StartTime":30915.0,"Position":151.0,"HyperDash":false}]},{"StartTime":31142.0,"Objects":[{"StartTime":31142.0,"Position":364.0,"HyperDash":false}]},{"StartTime":31370.0,"Objects":[{"StartTime":31370.0,"Position":202.0,"HyperDash":false}]},{"StartTime":31597.0,"Objects":[{"StartTime":31597.0,"Position":194.0,"HyperDash":false},{"StartTime":31649.0,"Position":193.329712,"HyperDash":false},{"StartTime":31701.0,"Position":177.29129,"HyperDash":false},{"StartTime":31753.0,"Position":180.897339,"HyperDash":false},{"StartTime":31806.0,"Position":196.245209,"HyperDash":false},{"StartTime":31858.0,"Position":225.942978,"HyperDash":false},{"StartTime":31910.0,"Position":221.896729,"HyperDash":false},{"StartTime":31962.0,"Position":258.838379,"HyperDash":false},{"StartTime":32051.0,"Position":270.298431,"HyperDash":false}]},{"StartTime":32279.0,"Objects":[{"StartTime":32279.0,"Position":316.0,"HyperDash":false}]},{"StartTime":32506.0,"Objects":[{"StartTime":32506.0,"Position":273.0,"HyperDash":false},{"StartTime":32558.0,"Position":268.772461,"HyperDash":false},{"StartTime":32610.0,"Position":229.84964,"HyperDash":false},{"StartTime":32662.0,"Position":200.402817,"HyperDash":false},{"StartTime":32715.0,"Position":184.266861,"HyperDash":false},{"StartTime":32767.0,"Position":200.3539,"HyperDash":false},{"StartTime":32819.0,"Position":172.707291,"HyperDash":false},{"StartTime":32871.0,"Position":162.9561,"HyperDash":false},{"StartTime":32960.0,"Position":144.968948,"HyperDash":false}]},{"StartTime":33188.0,"Objects":[{"StartTime":33188.0,"Position":294.0,"HyperDash":false}]},{"StartTime":33415.0,"Objects":[{"StartTime":33415.0,"Position":295.0,"HyperDash":false},{"StartTime":33467.0,"Position":281.203522,"HyperDash":false},{"StartTime":33519.0,"Position":263.38385,"HyperDash":false},{"StartTime":33571.0,"Position":270.625458,"HyperDash":false},{"StartTime":33624.0,"Position":279.906525,"HyperDash":false},{"StartTime":33676.0,"Position":253.1824,"HyperDash":false},{"StartTime":33728.0,"Position":271.372864,"HyperDash":false},{"StartTime":33780.0,"Position":265.406738,"HyperDash":false},{"StartTime":33869.0,"Position":300.795166,"HyperDash":false}]},{"StartTime":34097.0,"Objects":[{"StartTime":34097.0,"Position":406.0,"HyperDash":false}]},{"StartTime":34324.0,"Objects":[{"StartTime":34324.0,"Position":372.0,"HyperDash":false},{"StartTime":34376.0,"Position":366.75238,"HyperDash":false},{"StartTime":34428.0,"Position":319.542755,"HyperDash":false},{"StartTime":34480.0,"Position":337.707428,"HyperDash":false},{"StartTime":34533.0,"Position":311.1586,"HyperDash":false},{"StartTime":34585.0,"Position":283.844269,"HyperDash":false},{"StartTime":34637.0,"Position":264.676025,"HyperDash":false},{"StartTime":34689.0,"Position":259.952332,"HyperDash":false},{"StartTime":34778.0,"Position":219.572815,"HyperDash":false}]},{"StartTime":35006.0,"Objects":[{"StartTime":35006.0,"Position":117.0,"HyperDash":false}]},{"StartTime":35233.0,"Objects":[{"StartTime":35233.0,"Position":107.0,"HyperDash":false},{"StartTime":35285.0,"Position":123.10994,"HyperDash":false},{"StartTime":35337.0,"Position":138.9144,"HyperDash":false},{"StartTime":35389.0,"Position":142.755829,"HyperDash":false},{"StartTime":35442.0,"Position":177.3442,"HyperDash":false},{"StartTime":35494.0,"Position":180.653748,"HyperDash":false},{"StartTime":35546.0,"Position":203.782745,"HyperDash":false},{"StartTime":35598.0,"Position":209.428528,"HyperDash":false},{"StartTime":35687.0,"Position":255.847168,"HyperDash":false}]},{"StartTime":35915.0,"Objects":[{"StartTime":35915.0,"Position":370.0,"HyperDash":false}]},{"StartTime":36142.0,"Objects":[{"StartTime":36142.0,"Position":330.0,"HyperDash":false}]},{"StartTime":36597.0,"Objects":[{"StartTime":36597.0,"Position":370.0,"HyperDash":false}]},{"StartTime":36824.0,"Objects":[{"StartTime":36824.0,"Position":416.0,"HyperDash":false}]},{"StartTime":37051.0,"Objects":[{"StartTime":37051.0,"Position":406.0,"HyperDash":false},{"StartTime":37103.0,"Position":403.974335,"HyperDash":false},{"StartTime":37155.0,"Position":389.16626,"HyperDash":false},{"StartTime":37207.0,"Position":364.729828,"HyperDash":false},{"StartTime":37260.0,"Position":356.0597,"HyperDash":false},{"StartTime":37312.0,"Position":363.056549,"HyperDash":false},{"StartTime":37364.0,"Position":339.779724,"HyperDash":false},{"StartTime":37416.0,"Position":319.443939,"HyperDash":false},{"StartTime":37505.0,"Position":295.632843,"HyperDash":false}]},{"StartTime":37733.0,"Objects":[{"StartTime":37733.0,"Position":161.0,"HyperDash":false}]},{"StartTime":37961.0,"Objects":[{"StartTime":37961.0,"Position":147.0,"HyperDash":false}]},{"StartTime":38074.0,"Objects":[{"StartTime":38074.0,"Position":161.0,"HyperDash":false}]},{"StartTime":38188.0,"Objects":[{"StartTime":38188.0,"Position":147.0,"HyperDash":false}]},{"StartTime":46142.0,"Objects":[{"StartTime":46142.0,"Position":105.0,"HyperDash":false},{"StartTime":46194.0,"Position":101.3565,"HyperDash":false},{"StartTime":46246.0,"Position":117.818565,"HyperDash":false},{"StartTime":46298.0,"Position":135.426117,"HyperDash":false},{"StartTime":46351.0,"Position":146.825043,"HyperDash":false},{"StartTime":46403.0,"Position":174.897232,"HyperDash":false},{"StartTime":46455.0,"Position":178.608673,"HyperDash":false},{"StartTime":46507.0,"Position":211.715851,"HyperDash":false},{"StartTime":46596.0,"Position":242.038391,"HyperDash":false}]},{"StartTime":47051.0,"Objects":[{"StartTime":47051.0,"Position":399.0,"HyperDash":false},{"StartTime":47107.0,"Position":433.4483,"HyperDash":false},{"StartTime":47164.0,"Position":427.2428,"HyperDash":false},{"StartTime":47221.0,"Position":452.0353,"HyperDash":false},{"StartTime":47278.0,"Position":477.822449,"HyperDash":false},{"StartTime":47373.0,"Position":461.8406,"HyperDash":false},{"StartTime":47505.0,"Position":399.0,"HyperDash":false}]},{"StartTime":47961.0,"Objects":[{"StartTime":47961.0,"Position":422.0,"HyperDash":false},{"StartTime":48013.0,"Position":393.6435,"HyperDash":false},{"StartTime":48065.0,"Position":415.181427,"HyperDash":false},{"StartTime":48117.0,"Position":403.573883,"HyperDash":false},{"StartTime":48170.0,"Position":352.174957,"HyperDash":false},{"StartTime":48222.0,"Position":336.102783,"HyperDash":false},{"StartTime":48274.0,"Position":346.391327,"HyperDash":false},{"StartTime":48326.0,"Position":328.284149,"HyperDash":false},{"StartTime":48415.0,"Position":284.9616,"HyperDash":false}]},{"StartTime":48870.0,"Objects":[{"StartTime":48870.0,"Position":128.0,"HyperDash":false},{"StartTime":48926.0,"Position":123.551682,"HyperDash":false},{"StartTime":48983.0,"Position":80.7571945,"HyperDash":false},{"StartTime":49040.0,"Position":54.96469,"HyperDash":false},{"StartTime":49097.0,"Position":49.17756,"HyperDash":false},{"StartTime":49192.0,"Position":78.1594,"HyperDash":false},{"StartTime":49324.0,"Position":128.0,"HyperDash":false}]},{"StartTime":49779.0,"Objects":[{"StartTime":49779.0,"Position":252.0,"HyperDash":false},{"StartTime":49831.0,"Position":281.5043,"HyperDash":false},{"StartTime":49883.0,"Position":284.787231,"HyperDash":false},{"StartTime":49935.0,"Position":303.602631,"HyperDash":false},{"StartTime":49988.0,"Position":315.098541,"HyperDash":false},{"StartTime":50040.0,"Position":356.3944,"HyperDash":false},{"StartTime":50092.0,"Position":367.7095,"HyperDash":false},{"StartTime":50144.0,"Position":369.952545,"HyperDash":false},{"StartTime":50233.0,"Position":407.8509,"HyperDash":false}]},{"StartTime":50688.0,"Objects":[{"StartTime":50688.0,"Position":248.0,"HyperDash":false}]},{"StartTime":50915.0,"Objects":[{"StartTime":50915.0,"Position":377.0,"HyperDash":false},{"StartTime":51010.0,"Position":359.866516,"HyperDash":false},{"StartTime":51142.0,"Position":298.613647,"HyperDash":false}]},{"StartTime":51370.0,"Objects":[{"StartTime":51370.0,"Position":161.0,"HyperDash":false}]},{"StartTime":51597.0,"Objects":[{"StartTime":51597.0,"Position":159.0,"HyperDash":false},{"StartTime":51692.0,"Position":111.3809,"HyperDash":false},{"StartTime":51824.0,"Position":81.1563339,"HyperDash":false}]},{"StartTime":52051.0,"Objects":[{"StartTime":52051.0,"Position":107.0,"HyperDash":false},{"StartTime":52146.0,"Position":72.66428,"HyperDash":false},{"StartTime":52278.0,"Position":28.8123531,"HyperDash":false}]},{"StartTime":52506.0,"Objects":[{"StartTime":52506.0,"Position":75.0,"HyperDash":false},{"StartTime":52558.0,"Position":76.23376,"HyperDash":false},{"StartTime":52610.0,"Position":99.55078,"HyperDash":false},{"StartTime":52662.0,"Position":117.824188,"HyperDash":false},{"StartTime":52715.0,"Position":140.248856,"HyperDash":false},{"StartTime":52767.0,"Position":167.9607,"HyperDash":false},{"StartTime":52819.0,"Position":172.073,"HyperDash":false},{"StartTime":52871.0,"Position":216.350311,"HyperDash":false},{"StartTime":52960.0,"Position":224.446579,"HyperDash":false}]},{"StartTime":53415.0,"Objects":[{"StartTime":53415.0,"Position":413.0,"HyperDash":false}]},{"StartTime":53642.0,"Objects":[{"StartTime":53642.0,"Position":321.0,"HyperDash":false}]},{"StartTime":53870.0,"Objects":[{"StartTime":53870.0,"Position":321.0,"HyperDash":false},{"StartTime":53922.0,"Position":347.217651,"HyperDash":false},{"StartTime":53974.0,"Position":342.931427,"HyperDash":false},{"StartTime":54026.0,"Position":379.435669,"HyperDash":false},{"StartTime":54079.0,"Position":363.721619,"HyperDash":false},{"StartTime":54131.0,"Position":366.5289,"HyperDash":false},{"StartTime":54183.0,"Position":366.0941,"HyperDash":false},{"StartTime":54235.0,"Position":367.430542,"HyperDash":false},{"StartTime":54324.0,"Position":367.2075,"HyperDash":false}]},{"StartTime":54551.0,"Objects":[{"StartTime":54551.0,"Position":310.0,"HyperDash":false}]},{"StartTime":54779.0,"Objects":[{"StartTime":54779.0,"Position":222.0,"HyperDash":false}]},{"StartTime":55233.0,"Objects":[{"StartTime":55233.0,"Position":310.0,"HyperDash":false}]},{"StartTime":55461.0,"Objects":[{"StartTime":55461.0,"Position":222.0,"HyperDash":false}]},{"StartTime":55688.0,"Objects":[{"StartTime":55688.0,"Position":266.0,"HyperDash":false},{"StartTime":55740.0,"Position":250.312454,"HyperDash":false},{"StartTime":55792.0,"Position":222.151321,"HyperDash":false},{"StartTime":55844.0,"Position":229.8381,"HyperDash":false},{"StartTime":55897.0,"Position":199.311859,"HyperDash":false},{"StartTime":55949.0,"Position":190.573822,"HyperDash":false},{"StartTime":56001.0,"Position":163.5982,"HyperDash":false},{"StartTime":56053.0,"Position":126.797623,"HyperDash":false},{"StartTime":56142.0,"Position":119.985596,"HyperDash":false}]},{"StartTime":56370.0,"Objects":[{"StartTime":56370.0,"Position":70.0,"HyperDash":false}]},{"StartTime":56597.0,"Objects":[{"StartTime":56597.0,"Position":128.0,"HyperDash":false}]},{"StartTime":57051.0,"Objects":[{"StartTime":57051.0,"Position":70.0,"HyperDash":false}]},{"StartTime":57279.0,"Objects":[{"StartTime":57279.0,"Position":128.0,"HyperDash":false}]},{"StartTime":57506.0,"Objects":[{"StartTime":57506.0,"Position":99.0,"HyperDash":false},{"StartTime":57558.0,"Position":95.98298,"HyperDash":false},{"StartTime":57610.0,"Position":141.1198,"HyperDash":false},{"StartTime":57662.0,"Position":153.374634,"HyperDash":false},{"StartTime":57715.0,"Position":146.589783,"HyperDash":false},{"StartTime":57767.0,"Position":186.773819,"HyperDash":false},{"StartTime":57819.0,"Position":202.087418,"HyperDash":false},{"StartTime":57871.0,"Position":227.361313,"HyperDash":false},{"StartTime":57960.0,"Position":249.971191,"HyperDash":false}]},{"StartTime":58188.0,"Objects":[{"StartTime":58188.0,"Position":398.0,"HyperDash":false}]},{"StartTime":58415.0,"Objects":[{"StartTime":58415.0,"Position":366.0,"HyperDash":false}]},{"StartTime":58642.0,"Objects":[{"StartTime":58642.0,"Position":401.0,"HyperDash":false},{"StartTime":58737.0,"Position":372.52832,"HyperDash":false},{"StartTime":58869.0,"Position":348.93866,"HyperDash":false}]},{"StartTime":59097.0,"Objects":[{"StartTime":59097.0,"Position":203.0,"HyperDash":false}]},{"StartTime":59324.0,"Objects":[{"StartTime":59324.0,"Position":337.0,"HyperDash":false},{"StartTime":59419.0,"Position":354.740051,"HyperDash":false},{"StartTime":59551.0,"Position":364.726837,"HyperDash":false}]},{"StartTime":59779.0,"Objects":[{"StartTime":59779.0,"Position":284.0,"HyperDash":false},{"StartTime":59831.0,"Position":267.281219,"HyperDash":false},{"StartTime":59883.0,"Position":257.357849,"HyperDash":false},{"StartTime":59935.0,"Position":220.054123,"HyperDash":false},{"StartTime":59988.0,"Position":220.521576,"HyperDash":false},{"StartTime":60040.0,"Position":193.393219,"HyperDash":false},{"StartTime":60092.0,"Position":176.168411,"HyperDash":false},{"StartTime":60144.0,"Position":151.876328,"HyperDash":false},{"StartTime":60233.0,"Position":130.344528,"HyperDash":false}]},{"StartTime":60688.0,"Objects":[{"StartTime":60688.0,"Position":41.0,"HyperDash":false}]},{"StartTime":61142.0,"Objects":[{"StartTime":61142.0,"Position":191.0,"HyperDash":false},{"StartTime":61237.0,"Position":220.210571,"HyperDash":false},{"StartTime":61369.0,"Position":265.576843,"HyperDash":false}]},{"StartTime":61597.0,"Objects":[{"StartTime":61597.0,"Position":254.0,"HyperDash":false},{"StartTime":61692.0,"Position":300.210571,"HyperDash":false},{"StartTime":61824.0,"Position":328.576843,"HyperDash":false}]},{"StartTime":62051.0,"Objects":[{"StartTime":62051.0,"Position":299.0,"HyperDash":false}]},{"StartTime":62279.0,"Objects":[{"StartTime":62279.0,"Position":319.0,"HyperDash":false},{"StartTime":62374.0,"Position":304.789429,"HyperDash":false},{"StartTime":62506.0,"Position":244.423172,"HyperDash":false}]},{"StartTime":62733.0,"Objects":[{"StartTime":62733.0,"Position":102.0,"HyperDash":false}]},{"StartTime":62961.0,"Objects":[{"StartTime":62961.0,"Position":80.0,"HyperDash":false}]},{"StartTime":63188.0,"Objects":[{"StartTime":63188.0,"Position":31.0,"HyperDash":false}]},{"StartTime":63415.0,"Objects":[{"StartTime":63415.0,"Position":31.0,"HyperDash":false},{"StartTime":63471.0,"Position":15.0,"HyperDash":false},{"StartTime":63528.0,"Position":13.0,"HyperDash":false},{"StartTime":63585.0,"Position":43.0,"HyperDash":false},{"StartTime":63642.0,"Position":31.0,"HyperDash":false},{"StartTime":63737.0,"Position":38.0,"HyperDash":false},{"StartTime":63869.0,"Position":31.0,"HyperDash":false}]},{"StartTime":64324.0,"Objects":[{"StartTime":64324.0,"Position":331.0,"HyperDash":false}]},{"StartTime":64779.0,"Objects":[{"StartTime":64779.0,"Position":335.0,"HyperDash":false},{"StartTime":64874.0,"Position":315.0,"HyperDash":false},{"StartTime":65006.0,"Position":335.0,"HyperDash":false}]},{"StartTime":65233.0,"Objects":[{"StartTime":65233.0,"Position":405.0,"HyperDash":false},{"StartTime":65328.0,"Position":404.0,"HyperDash":false},{"StartTime":65460.0,"Position":405.0,"HyperDash":false}]},{"StartTime":65688.0,"Objects":[{"StartTime":65688.0,"Position":475.0,"HyperDash":false}]},{"StartTime":65915.0,"Objects":[{"StartTime":65915.0,"Position":475.0,"HyperDash":false},{"StartTime":66010.0,"Position":460.0,"HyperDash":false},{"StartTime":66142.0,"Position":475.0,"HyperDash":false}]},{"StartTime":66370.0,"Objects":[{"StartTime":66370.0,"Position":335.0,"HyperDash":false}]},{"StartTime":66597.0,"Objects":[{"StartTime":66597.0,"Position":315.0,"HyperDash":false}]},{"StartTime":66824.0,"Objects":[{"StartTime":66824.0,"Position":189.0,"HyperDash":false}]},{"StartTime":67051.0,"Objects":[{"StartTime":67051.0,"Position":219.0,"HyperDash":false}]},{"StartTime":67279.0,"Objects":[{"StartTime":67279.0,"Position":159.0,"HyperDash":false}]},{"StartTime":67506.0,"Objects":[{"StartTime":67506.0,"Position":245.0,"HyperDash":false}]},{"StartTime":67733.0,"Objects":[{"StartTime":67733.0,"Position":255.0,"HyperDash":false}]},{"StartTime":67961.0,"Objects":[{"StartTime":67961.0,"Position":329.0,"HyperDash":false},{"StartTime":68056.0,"Position":343.033264,"HyperDash":false},{"StartTime":68188.0,"Position":407.932129,"HyperDash":false}]},{"StartTime":68415.0,"Objects":[{"StartTime":68415.0,"Position":427.0,"HyperDash":false},{"StartTime":68510.0,"Position":397.966736,"HyperDash":false},{"StartTime":68642.0,"Position":348.067871,"HyperDash":false}]},{"StartTime":68870.0,"Objects":[{"StartTime":68870.0,"Position":303.0,"HyperDash":false},{"StartTime":68965.0,"Position":338.033264,"HyperDash":false},{"StartTime":69097.0,"Position":381.932129,"HyperDash":false}]},{"StartTime":69324.0,"Objects":[{"StartTime":69324.0,"Position":401.0,"HyperDash":false},{"StartTime":69419.0,"Position":384.966736,"HyperDash":false},{"StartTime":69551.0,"Position":322.067871,"HyperDash":false}]},{"StartTime":69779.0,"Objects":[{"StartTime":69779.0,"Position":186.0,"HyperDash":false}]},{"StartTime":70006.0,"Objects":[{"StartTime":70006.0,"Position":298.0,"HyperDash":false}]},{"StartTime":70233.0,"Objects":[{"StartTime":70233.0,"Position":163.0,"HyperDash":false}]},{"StartTime":70461.0,"Objects":[{"StartTime":70461.0,"Position":143.0,"HyperDash":false}]},{"StartTime":70688.0,"Objects":[{"StartTime":70688.0,"Position":84.0,"HyperDash":false},{"StartTime":70744.0,"Position":78.72694,"HyperDash":false},{"StartTime":70801.0,"Position":82.25869,"HyperDash":false},{"StartTime":70858.0,"Position":70.21074,"HyperDash":false},{"StartTime":70915.0,"Position":84.71703,"HyperDash":false},{"StartTime":70971.0,"Position":78.31016,"HyperDash":false},{"StartTime":71028.0,"Position":58.2654724,"HyperDash":false},{"StartTime":71085.0,"Position":57.63716,"HyperDash":false},{"StartTime":71142.0,"Position":84.0,"HyperDash":false},{"StartTime":71237.0,"Position":54.4390259,"HyperDash":false},{"StartTime":71369.0,"Position":84.71703,"HyperDash":false}]},{"StartTime":71597.0,"Objects":[{"StartTime":71597.0,"Position":148.0,"HyperDash":false},{"StartTime":71649.0,"Position":165.786362,"HyperDash":false},{"StartTime":71701.0,"Position":179.3862,"HyperDash":false},{"StartTime":71753.0,"Position":206.911774,"HyperDash":false},{"StartTime":71806.0,"Position":197.271149,"HyperDash":false},{"StartTime":71858.0,"Position":230.514236,"HyperDash":false},{"StartTime":71910.0,"Position":245.834518,"HyperDash":false},{"StartTime":71962.0,"Position":272.100525,"HyperDash":false},{"StartTime":72051.0,"Position":300.802856,"HyperDash":false}]},{"StartTime":72506.0,"Objects":[{"StartTime":72506.0,"Position":374.0,"HyperDash":false},{"StartTime":72558.0,"Position":376.213623,"HyperDash":false},{"StartTime":72610.0,"Position":353.6138,"HyperDash":false},{"StartTime":72662.0,"Position":320.088226,"HyperDash":false},{"StartTime":72715.0,"Position":293.728851,"HyperDash":false},{"StartTime":72767.0,"Position":297.485779,"HyperDash":false},{"StartTime":72819.0,"Position":272.165466,"HyperDash":false},{"StartTime":72871.0,"Position":247.899475,"HyperDash":false},{"StartTime":72960.0,"Position":221.197159,"HyperDash":false}]},{"StartTime":73188.0,"Objects":[{"StartTime":73188.0,"Position":77.0,"HyperDash":false}]},{"StartTime":73415.0,"Objects":[{"StartTime":73415.0,"Position":213.0,"HyperDash":false},{"StartTime":73510.0,"Position":233.974548,"HyperDash":false},{"StartTime":73642.0,"Position":279.844421,"HyperDash":false}]},{"StartTime":73870.0,"Objects":[{"StartTime":73870.0,"Position":346.0,"HyperDash":false},{"StartTime":73965.0,"Position":336.709564,"HyperDash":false},{"StartTime":74097.0,"Position":297.516541,"HyperDash":false}]},{"StartTime":74324.0,"Objects":[{"StartTime":74324.0,"Position":222.0,"HyperDash":false}]},{"StartTime":74551.0,"Objects":[{"StartTime":74551.0,"Position":282.0,"HyperDash":false}]},{"StartTime":74779.0,"Objects":[{"StartTime":74779.0,"Position":252.0,"HyperDash":false},{"StartTime":74835.0,"Position":222.93634,"HyperDash":false},{"StartTime":74892.0,"Position":231.971436,"HyperDash":false},{"StartTime":74949.0,"Position":179.985031,"HyperDash":false},{"StartTime":75006.0,"Position":173.674133,"HyperDash":false},{"StartTime":75101.0,"Position":204.278076,"HyperDash":false},{"StartTime":75233.0,"Position":252.0,"HyperDash":false}]},{"StartTime":75688.0,"Objects":[{"StartTime":75688.0,"Position":194.0,"HyperDash":false},{"StartTime":75744.0,"Position":188.93634,"HyperDash":false},{"StartTime":75801.0,"Position":159.971436,"HyperDash":false},{"StartTime":75858.0,"Position":124.985031,"HyperDash":false},{"StartTime":75915.0,"Position":115.674141,"HyperDash":false},{"StartTime":76010.0,"Position":154.278076,"HyperDash":false},{"StartTime":76142.0,"Position":194.0,"HyperDash":false}]},{"StartTime":76597.0,"Objects":[{"StartTime":76597.0,"Position":347.0,"HyperDash":false}]},{"StartTime":76824.0,"Objects":[{"StartTime":76824.0,"Position":327.0,"HyperDash":false}]},{"StartTime":77051.0,"Objects":[{"StartTime":77051.0,"Position":351.0,"HyperDash":false}]},{"StartTime":77506.0,"Objects":[{"StartTime":77506.0,"Position":448.0,"HyperDash":false}]},{"StartTime":77733.0,"Objects":[{"StartTime":77733.0,"Position":368.0,"HyperDash":false}]},{"StartTime":77961.0,"Objects":[{"StartTime":77961.0,"Position":242.0,"HyperDash":false}]},{"StartTime":78415.0,"Objects":[{"StartTime":78415.0,"Position":50.0,"HyperDash":false}]},{"StartTime":78642.0,"Objects":[{"StartTime":78642.0,"Position":118.0,"HyperDash":false},{"StartTime":78737.0,"Position":102.772095,"HyperDash":false},{"StartTime":78869.0,"Position":62.0753326,"HyperDash":false}]},{"StartTime":79324.0,"Objects":[{"StartTime":79324.0,"Position":218.0,"HyperDash":false},{"StartTime":79419.0,"Position":253.274826,"HyperDash":false},{"StartTime":79551.0,"Position":294.3989,"HyperDash":false}]},{"StartTime":79779.0,"Objects":[{"StartTime":79779.0,"Position":443.0,"HyperDash":false}]},{"StartTime":80233.0,"Objects":[{"StartTime":80233.0,"Position":286.0,"HyperDash":false},{"StartTime":80289.0,"Position":301.1139,"HyperDash":false},{"StartTime":80346.0,"Position":277.211975,"HyperDash":false},{"StartTime":80403.0,"Position":273.310028,"HyperDash":false},{"StartTime":80460.0,"Position":282.4081,"HyperDash":false},{"StartTime":80555.0,"Position":290.911316,"HyperDash":false},{"StartTime":80687.0,"Position":286.0,"HyperDash":false}]},{"StartTime":81142.0,"Objects":[{"StartTime":81142.0,"Position":427.0,"HyperDash":false}]},{"StartTime":81370.0,"Objects":[{"StartTime":81370.0,"Position":423.0,"HyperDash":false}]},{"StartTime":81597.0,"Objects":[{"StartTime":81597.0,"Position":427.0,"HyperDash":false},{"StartTime":81653.0,"Position":415.357849,"HyperDash":false},{"StartTime":81710.0,"Position":429.752075,"HyperDash":false},{"StartTime":81824.0,"Position":427.0,"HyperDash":false}]},{"StartTime":82051.0,"Objects":[{"StartTime":82051.0,"Position":411.0,"HyperDash":false}]},{"StartTime":82279.0,"Objects":[{"StartTime":82279.0,"Position":301.0,"HyperDash":false}]},{"StartTime":82506.0,"Objects":[{"StartTime":82506.0,"Position":285.0,"HyperDash":false},{"StartTime":82558.0,"Position":275.960876,"HyperDash":false},{"StartTime":82610.0,"Position":258.1504,"HyperDash":false},{"StartTime":82662.0,"Position":240.960434,"HyperDash":false},{"StartTime":82715.0,"Position":213.29361,"HyperDash":false},{"StartTime":82767.0,"Position":177.039841,"HyperDash":false},{"StartTime":82819.0,"Position":192.027191,"HyperDash":false},{"StartTime":82871.0,"Position":162.507034,"HyperDash":false},{"StartTime":82960.0,"Position":132.24411,"HyperDash":false}]},{"StartTime":83188.0,"Objects":[{"StartTime":83188.0,"Position":246.0,"HyperDash":false}]},{"StartTime":83415.0,"Objects":[{"StartTime":83415.0,"Position":267.0,"HyperDash":false},{"StartTime":83467.0,"Position":277.3109,"HyperDash":false},{"StartTime":83519.0,"Position":280.6682,"HyperDash":false},{"StartTime":83571.0,"Position":302.941681,"HyperDash":false},{"StartTime":83624.0,"Position":298.1128,"HyperDash":false},{"StartTime":83676.0,"Position":291.2743,"HyperDash":false},{"StartTime":83728.0,"Position":290.638519,"HyperDash":false},{"StartTime":83780.0,"Position":254.4002,"HyperDash":false},{"StartTime":83869.0,"Position":250.128326,"HyperDash":false}]},{"StartTime":84097.0,"Objects":[{"StartTime":84097.0,"Position":161.0,"HyperDash":false}]},{"StartTime":84324.0,"Objects":[{"StartTime":84324.0,"Position":188.0,"HyperDash":false},{"StartTime":84376.0,"Position":211.223328,"HyperDash":false},{"StartTime":84428.0,"Position":235.527512,"HyperDash":false},{"StartTime":84480.0,"Position":225.64093,"HyperDash":false},{"StartTime":84533.0,"Position":278.681366,"HyperDash":false},{"StartTime":84585.0,"Position":283.741333,"HyperDash":false},{"StartTime":84637.0,"Position":282.889923,"HyperDash":false},{"StartTime":84689.0,"Position":320.765961,"HyperDash":false},{"StartTime":84778.0,"Position":329.839569,"HyperDash":false}]},{"StartTime":85006.0,"Objects":[{"StartTime":85006.0,"Position":177.0,"HyperDash":false}]},{"StartTime":85233.0,"Objects":[{"StartTime":85233.0,"Position":177.0,"HyperDash":false},{"StartTime":85285.0,"Position":165.201,"HyperDash":false},{"StartTime":85337.0,"Position":182.838409,"HyperDash":false},{"StartTime":85389.0,"Position":172.951111,"HyperDash":false},{"StartTime":85442.0,"Position":165.6638,"HyperDash":false},{"StartTime":85494.0,"Position":172.542755,"HyperDash":false},{"StartTime":85546.0,"Position":188.28334,"HyperDash":false},{"StartTime":85598.0,"Position":236.294235,"HyperDash":false},{"StartTime":85687.0,"Position":249.329514,"HyperDash":false}]},{"StartTime":85915.0,"Objects":[{"StartTime":85915.0,"Position":368.0,"HyperDash":false}]},{"StartTime":86142.0,"Objects":[{"StartTime":86142.0,"Position":404.0,"HyperDash":false},{"StartTime":86194.0,"Position":401.8277,"HyperDash":false},{"StartTime":86246.0,"Position":454.150146,"HyperDash":false},{"StartTime":86298.0,"Position":426.349945,"HyperDash":false},{"StartTime":86351.0,"Position":450.306671,"HyperDash":false},{"StartTime":86403.0,"Position":444.439728,"HyperDash":false},{"StartTime":86455.0,"Position":429.120422,"HyperDash":false},{"StartTime":86507.0,"Position":418.0135,"HyperDash":false},{"StartTime":86596.0,"Position":405.457642,"HyperDash":false}]},{"StartTime":86824.0,"Objects":[{"StartTime":86824.0,"Position":272.0,"HyperDash":false}]},{"StartTime":87051.0,"Objects":[{"StartTime":87051.0,"Position":220.0,"HyperDash":false}]},{"StartTime":87506.0,"Objects":[{"StartTime":87506.0,"Position":272.0,"HyperDash":false}]},{"StartTime":87733.0,"Objects":[{"StartTime":87733.0,"Position":192.0,"HyperDash":false}]},{"StartTime":87961.0,"Objects":[{"StartTime":87961.0,"Position":168.0,"HyperDash":false},{"StartTime":88013.0,"Position":180.07048,"HyperDash":false},{"StartTime":88065.0,"Position":145.3697,"HyperDash":false},{"StartTime":88117.0,"Position":181.001678,"HyperDash":false},{"StartTime":88170.0,"Position":158.001877,"HyperDash":false},{"StartTime":88222.0,"Position":177.868271,"HyperDash":false},{"StartTime":88274.0,"Position":202.302979,"HyperDash":false},{"StartTime":88326.0,"Position":215.876221,"HyperDash":false},{"StartTime":88415.0,"Position":224.187881,"HyperDash":false}]},{"StartTime":88642.0,"Objects":[{"StartTime":88642.0,"Position":363.0,"HyperDash":false}]},{"StartTime":88870.0,"Objects":[{"StartTime":88870.0,"Position":393.0,"HyperDash":false}]},{"StartTime":88983.0,"Objects":[{"StartTime":88983.0,"Position":363.0,"HyperDash":false}]},{"StartTime":89097.0,"Objects":[{"StartTime":89097.0,"Position":393.0,"HyperDash":false}]},{"StartTime":93415.0,"Objects":[{"StartTime":93415.0,"Position":330.0,"HyperDash":false},{"StartTime":93500.0,"Position":362.93335,"HyperDash":false},{"StartTime":93585.0,"Position":384.5453,"HyperDash":false},{"StartTime":93670.0,"Position":408.46228,"HyperDash":false},{"StartTime":93755.0,"Position":448.525055,"HyperDash":false},{"StartTime":93831.0,"Position":430.9859,"HyperDash":false},{"StartTime":93907.0,"Position":391.21936,"HyperDash":false},{"StartTime":93983.0,"Position":356.6329,"HyperDash":false},{"StartTime":94096.0,"Position":330.0,"HyperDash":false}]},{"StartTime":94324.0,"Objects":[{"StartTime":94324.0,"Position":55.0,"HyperDash":false}]},{"StartTime":94552.0,"Objects":[{"StartTime":94552.0,"Position":181.0,"HyperDash":false},{"StartTime":94665.0,"Position":145.222916,"HyperDash":false}]},{"StartTime":95233.0,"Objects":[{"StartTime":95233.0,"Position":181.0,"HyperDash":false},{"StartTime":95318.0,"Position":141.066635,"HyperDash":false},{"StartTime":95403.0,"Position":137.4547,"HyperDash":false},{"StartTime":95488.0,"Position":96.53772,"HyperDash":false},{"StartTime":95573.0,"Position":62.47494,"HyperDash":false},{"StartTime":95649.0,"Position":96.0141144,"HyperDash":false},{"StartTime":95725.0,"Position":128.78064,"HyperDash":false},{"StartTime":95801.0,"Position":133.367081,"HyperDash":false},{"StartTime":95914.0,"Position":181.0,"HyperDash":false}]},{"StartTime":96142.0,"Objects":[{"StartTime":96142.0,"Position":456.0,"HyperDash":false}]},{"StartTime":96370.0,"Objects":[{"StartTime":96370.0,"Position":330.0,"HyperDash":false},{"StartTime":96483.0,"Position":365.7771,"HyperDash":false}]},{"StartTime":97052.0,"Objects":[{"StartTime":97052.0,"Position":330.0,"HyperDash":false},{"StartTime":97137.0,"Position":369.93335,"HyperDash":false},{"StartTime":97222.0,"Position":380.5453,"HyperDash":false},{"StartTime":97307.0,"Position":411.46228,"HyperDash":false},{"StartTime":97392.0,"Position":448.525055,"HyperDash":false},{"StartTime":97468.0,"Position":429.9859,"HyperDash":false},{"StartTime":97544.0,"Position":391.21936,"HyperDash":false},{"StartTime":97620.0,"Position":384.6329,"HyperDash":false},{"StartTime":97733.0,"Position":330.0,"HyperDash":false}]},{"StartTime":97961.0,"Objects":[{"StartTime":97961.0,"Position":55.0,"HyperDash":false}]},{"StartTime":98188.0,"Objects":[{"StartTime":98188.0,"Position":181.0,"HyperDash":false},{"StartTime":98301.0,"Position":145.222916,"HyperDash":false}]},{"StartTime":98870.0,"Objects":[{"StartTime":98870.0,"Position":181.0,"HyperDash":false},{"StartTime":98955.0,"Position":139.066635,"HyperDash":false},{"StartTime":99040.0,"Position":124.4547,"HyperDash":false},{"StartTime":99125.0,"Position":111.53772,"HyperDash":false},{"StartTime":99210.0,"Position":62.47494,"HyperDash":false},{"StartTime":99286.0,"Position":89.0141144,"HyperDash":false},{"StartTime":99362.0,"Position":121.780647,"HyperDash":false},{"StartTime":99438.0,"Position":125.367081,"HyperDash":false},{"StartTime":99551.0,"Position":181.0,"HyperDash":false}]},{"StartTime":99779.0,"Objects":[{"StartTime":99779.0,"Position":456.0,"HyperDash":false}]},{"StartTime":100006.0,"Objects":[{"StartTime":100006.0,"Position":330.0,"HyperDash":false},{"StartTime":100119.0,"Position":365.7771,"HyperDash":false}]},{"StartTime":100688.0,"Objects":[{"StartTime":100688.0,"Position":454.0,"HyperDash":false},{"StartTime":100801.0,"Position":414.608643,"HyperDash":false}]},{"StartTime":101029.0,"Objects":[{"StartTime":101029.0,"Position":335.0,"HyperDash":false},{"StartTime":101142.0,"Position":295.465118,"HyperDash":false}]},{"StartTime":101370.0,"Objects":[{"StartTime":101370.0,"Position":162.0,"HyperDash":false}]},{"StartTime":101597.0,"Objects":[{"StartTime":101597.0,"Position":137.0,"HyperDash":false},{"StartTime":101692.0,"Position":96.96697,"HyperDash":false},{"StartTime":101824.0,"Position":57.5450439,"HyperDash":false}]},{"StartTime":101938.0,"Objects":[{"StartTime":101938.0,"Position":84.0,"HyperDash":false}]},{"StartTime":102506.0,"Objects":[{"StartTime":102506.0,"Position":57.0,"HyperDash":false},{"StartTime":102619.0,"Position":96.39134,"HyperDash":false}]},{"StartTime":102847.0,"Objects":[{"StartTime":102847.0,"Position":176.0,"HyperDash":false},{"StartTime":102960.0,"Position":215.520477,"HyperDash":false}]},{"StartTime":103188.0,"Objects":[{"StartTime":103188.0,"Position":350.0,"HyperDash":false}]},{"StartTime":103415.0,"Objects":[{"StartTime":103415.0,"Position":374.0,"HyperDash":false},{"StartTime":103510.0,"Position":415.03302,"HyperDash":false},{"StartTime":103642.0,"Position":453.454956,"HyperDash":false}]},{"StartTime":103756.0,"Objects":[{"StartTime":103756.0,"Position":427.0,"HyperDash":false}]},{"StartTime":104324.0,"Objects":[{"StartTime":104324.0,"Position":454.0,"HyperDash":false},{"StartTime":104437.0,"Position":414.608643,"HyperDash":false}]},{"StartTime":104665.0,"Objects":[{"StartTime":104665.0,"Position":335.0,"HyperDash":false},{"StartTime":104778.0,"Position":295.465118,"HyperDash":false}]},{"StartTime":105006.0,"Objects":[{"StartTime":105006.0,"Position":162.0,"HyperDash":false}]},{"StartTime":105120.0,"Objects":[{"StartTime":105120.0,"Position":190.0,"HyperDash":false}]},{"StartTime":105233.0,"Objects":[{"StartTime":105233.0,"Position":137.0,"HyperDash":false},{"StartTime":105328.0,"Position":83.96697,"HyperDash":false},{"StartTime":105460.0,"Position":57.5450439,"HyperDash":false}]},{"StartTime":105574.0,"Objects":[{"StartTime":105574.0,"Position":84.0,"HyperDash":false}]},{"StartTime":106142.0,"Objects":[{"StartTime":106142.0,"Position":57.0,"HyperDash":false},{"StartTime":106255.0,"Position":96.39134,"HyperDash":false}]},{"StartTime":106483.0,"Objects":[{"StartTime":106483.0,"Position":176.0,"HyperDash":false},{"StartTime":106596.0,"Position":215.520477,"HyperDash":false}]},{"StartTime":106824.0,"Objects":[{"StartTime":106824.0,"Position":295.0,"HyperDash":false},{"StartTime":106904.0,"Position":306.2746,"HyperDash":false},{"StartTime":106985.0,"Position":352.66098,"HyperDash":false},{"StartTime":107065.0,"Position":389.650146,"HyperDash":false},{"StartTime":107146.0,"Position":414.777618,"HyperDash":false},{"StartTime":107227.0,"Position":392.217163,"HyperDash":false},{"StartTime":107307.0,"Position":354.003235,"HyperDash":false},{"StartTime":107388.0,"Position":321.594,"HyperDash":false},{"StartTime":107505.0,"Position":294.390747,"HyperDash":false}]},{"StartTime":115233.0,"Objects":[{"StartTime":115233.0,"Position":114.0,"HyperDash":false},{"StartTime":115285.0,"Position":143.939957,"HyperDash":false},{"StartTime":115337.0,"Position":150.324554,"HyperDash":false},{"StartTime":115389.0,"Position":183.259644,"HyperDash":false},{"StartTime":115442.0,"Position":188.794647,"HyperDash":false},{"StartTime":115494.0,"Position":195.08873,"HyperDash":false},{"StartTime":115546.0,"Position":237.411819,"HyperDash":false},{"StartTime":115598.0,"Position":240.698227,"HyperDash":false},{"StartTime":115687.0,"Position":269.692047,"HyperDash":false}]},{"StartTime":115915.0,"Objects":[{"StartTime":115915.0,"Position":413.0,"HyperDash":false}]},{"StartTime":116142.0,"Objects":[{"StartTime":116142.0,"Position":419.0,"HyperDash":false},{"StartTime":116198.0,"Position":419.6598,"HyperDash":false},{"StartTime":116255.0,"Position":457.3915,"HyperDash":false},{"StartTime":116312.0,"Position":466.4778,"HyperDash":false},{"StartTime":116369.0,"Position":449.986969,"HyperDash":false},{"StartTime":116464.0,"Position":432.831818,"HyperDash":false},{"StartTime":116596.0,"Position":419.0,"HyperDash":false}]},{"StartTime":117052.0,"Objects":[{"StartTime":117052.0,"Position":366.0,"HyperDash":false},{"StartTime":117147.0,"Position":351.721741,"HyperDash":false},{"StartTime":117279.0,"Position":295.245026,"HyperDash":false}]},{"StartTime":117506.0,"Objects":[{"StartTime":117506.0,"Position":157.0,"HyperDash":false}]},{"StartTime":117733.0,"Objects":[{"StartTime":117733.0,"Position":141.0,"HyperDash":false}]},{"StartTime":117961.0,"Objects":[{"StartTime":117961.0,"Position":84.0,"HyperDash":false},{"StartTime":118017.0,"Position":70.0,"HyperDash":false},{"StartTime":118074.0,"Position":100.0,"HyperDash":false},{"StartTime":118131.0,"Position":96.0,"HyperDash":false},{"StartTime":118188.0,"Position":84.0,"HyperDash":false},{"StartTime":118244.0,"Position":72.0,"HyperDash":false},{"StartTime":118301.0,"Position":95.0,"HyperDash":false},{"StartTime":118358.0,"Position":100.0,"HyperDash":false},{"StartTime":118415.0,"Position":84.0,"HyperDash":false},{"StartTime":118510.0,"Position":103.0,"HyperDash":false},{"StartTime":118642.0,"Position":84.0,"HyperDash":false}]},{"StartTime":118870.0,"Objects":[{"StartTime":118870.0,"Position":86.0,"HyperDash":false}]},{"StartTime":119097.0,"Objects":[{"StartTime":119097.0,"Position":224.0,"HyperDash":false}]},{"StartTime":119324.0,"Objects":[{"StartTime":119324.0,"Position":226.0,"HyperDash":false}]},{"StartTime":119552.0,"Objects":[{"StartTime":119552.0,"Position":366.0,"HyperDash":false}]},{"StartTime":119779.0,"Objects":[{"StartTime":119779.0,"Position":368.0,"HyperDash":false},{"StartTime":119835.0,"Position":397.255524,"HyperDash":false},{"StartTime":119892.0,"Position":406.27597,"HyperDash":false},{"StartTime":119949.0,"Position":410.2929,"HyperDash":false},{"StartTime":120006.0,"Position":446.9768,"HyperDash":false},{"StartTime":120101.0,"Position":415.96817,"HyperDash":false},{"StartTime":120233.0,"Position":368.0,"HyperDash":false}]},{"StartTime":120688.0,"Objects":[{"StartTime":120688.0,"Position":407.0,"HyperDash":false}]},{"StartTime":120915.0,"Objects":[{"StartTime":120915.0,"Position":321.0,"HyperDash":false}]},{"StartTime":121142.0,"Objects":[{"StartTime":121142.0,"Position":286.0,"HyperDash":false},{"StartTime":121194.0,"Position":262.810974,"HyperDash":false},{"StartTime":121246.0,"Position":235.4965,"HyperDash":false},{"StartTime":121298.0,"Position":223.241028,"HyperDash":false},{"StartTime":121351.0,"Position":219.863861,"HyperDash":false},{"StartTime":121403.0,"Position":209.2498,"HyperDash":false},{"StartTime":121455.0,"Position":171.249588,"HyperDash":false},{"StartTime":121507.0,"Position":177.110733,"HyperDash":false},{"StartTime":121596.0,"Position":137.418732,"HyperDash":false}]},{"StartTime":121824.0,"Objects":[{"StartTime":121824.0,"Position":78.0,"HyperDash":false}]},{"StartTime":122052.0,"Objects":[{"StartTime":122052.0,"Position":102.0,"HyperDash":false},{"StartTime":122147.0,"Position":99.41432,"HyperDash":false},{"StartTime":122279.0,"Position":141.1235,"HyperDash":false}]},{"StartTime":122506.0,"Objects":[{"StartTime":122506.0,"Position":187.0,"HyperDash":false},{"StartTime":122558.0,"Position":192.496933,"HyperDash":false},{"StartTime":122610.0,"Position":237.792938,"HyperDash":false},{"StartTime":122662.0,"Position":249.932373,"HyperDash":false},{"StartTime":122715.0,"Position":261.228668,"HyperDash":false},{"StartTime":122767.0,"Position":286.120331,"HyperDash":false},{"StartTime":122819.0,"Position":293.076569,"HyperDash":false},{"StartTime":122871.0,"Position":298.186584,"HyperDash":false},{"StartTime":122960.0,"Position":344.480072,"HyperDash":false}]},{"StartTime":123188.0,"Objects":[{"StartTime":123188.0,"Position":450.0,"HyperDash":false}]},{"StartTime":123415.0,"Objects":[{"StartTime":123415.0,"Position":342.0,"HyperDash":false},{"StartTime":123467.0,"Position":304.888275,"HyperDash":false},{"StartTime":123519.0,"Position":292.63266,"HyperDash":false},{"StartTime":123571.0,"Position":276.625946,"HyperDash":false},{"StartTime":123624.0,"Position":263.464172,"HyperDash":false},{"StartTime":123676.0,"Position":249.683212,"HyperDash":false},{"StartTime":123728.0,"Position":225.779449,"HyperDash":false},{"StartTime":123780.0,"Position":234.624741,"HyperDash":false},{"StartTime":123869.0,"Position":184.843155,"HyperDash":false}]},{"StartTime":124097.0,"Objects":[{"StartTime":124097.0,"Position":52.0,"HyperDash":false}]},{"StartTime":124324.0,"Objects":[{"StartTime":124324.0,"Position":184.0,"HyperDash":false},{"StartTime":124376.0,"Position":195.443817,"HyperDash":false},{"StartTime":124428.0,"Position":216.739166,"HyperDash":false},{"StartTime":124480.0,"Position":252.924118,"HyperDash":false},{"StartTime":124533.0,"Position":262.274628,"HyperDash":false},{"StartTime":124585.0,"Position":290.18573,"HyperDash":false},{"StartTime":124637.0,"Position":307.118835,"HyperDash":false},{"StartTime":124689.0,"Position":304.171661,"HyperDash":false},{"StartTime":124778.0,"Position":341.434662,"HyperDash":false}]},{"StartTime":125006.0,"Objects":[{"StartTime":125006.0,"Position":437.0,"HyperDash":false}]},{"StartTime":125233.0,"Objects":[{"StartTime":125233.0,"Position":474.0,"HyperDash":false},{"StartTime":125328.0,"Position":482.109436,"HyperDash":false},{"StartTime":125460.0,"Position":475.3147,"HyperDash":false}]},{"StartTime":125688.0,"Objects":[{"StartTime":125688.0,"Position":437.0,"HyperDash":false},{"StartTime":125783.0,"Position":440.578949,"HyperDash":false},{"StartTime":125915.0,"Position":435.0608,"HyperDash":false}]},{"StartTime":126142.0,"Objects":[{"StartTime":126142.0,"Position":506.0,"HyperDash":false},{"StartTime":126194.0,"Position":472.674347,"HyperDash":false},{"StartTime":126246.0,"Position":456.3487,"HyperDash":false},{"StartTime":126298.0,"Position":448.023041,"HyperDash":false},{"StartTime":126351.0,"Position":433.344971,"HyperDash":false},{"StartTime":126403.0,"Position":411.019348,"HyperDash":false},{"StartTime":126455.0,"Position":390.693665,"HyperDash":false},{"StartTime":126507.0,"Position":380.368042,"HyperDash":false},{"StartTime":126596.0,"Position":346.003,"HyperDash":false}]},{"StartTime":127052.0,"Objects":[{"StartTime":127052.0,"Position":28.0,"HyperDash":false},{"StartTime":127104.0,"Position":56.3256531,"HyperDash":false},{"StartTime":127156.0,"Position":67.6513062,"HyperDash":false},{"StartTime":127208.0,"Position":71.97695,"HyperDash":false},{"StartTime":127261.0,"Position":111.655014,"HyperDash":false},{"StartTime":127313.0,"Position":136.980667,"HyperDash":false},{"StartTime":127365.0,"Position":146.30632,"HyperDash":false},{"StartTime":127417.0,"Position":174.631958,"HyperDash":false},{"StartTime":127506.0,"Position":187.997025,"HyperDash":false}]},{"StartTime":127733.0,"Objects":[{"StartTime":127733.0,"Position":342.0,"HyperDash":false}]},{"StartTime":127961.0,"Objects":[{"StartTime":127961.0,"Position":226.0,"HyperDash":false},{"StartTime":128017.0,"Position":203.38623,"HyperDash":false},{"StartTime":128074.0,"Position":210.367325,"HyperDash":false},{"StartTime":128131.0,"Position":229.320847,"HyperDash":false},{"StartTime":128188.0,"Position":223.423782,"HyperDash":false},{"StartTime":128283.0,"Position":206.484131,"HyperDash":false},{"StartTime":128415.0,"Position":226.0,"HyperDash":false}]},{"StartTime":128642.0,"Objects":[{"StartTime":128642.0,"Position":302.0,"HyperDash":false}]},{"StartTime":128870.0,"Objects":[{"StartTime":128870.0,"Position":314.0,"HyperDash":false}]},{"StartTime":129097.0,"Objects":[{"StartTime":129097.0,"Position":302.0,"HyperDash":false}]},{"StartTime":129324.0,"Objects":[{"StartTime":129324.0,"Position":314.0,"HyperDash":false}]},{"StartTime":129779.0,"Objects":[{"StartTime":129779.0,"Position":308.0,"HyperDash":false},{"StartTime":129835.0,"Position":334.61377,"HyperDash":false},{"StartTime":129892.0,"Position":326.6327,"HyperDash":false},{"StartTime":129949.0,"Position":328.679138,"HyperDash":false},{"StartTime":130006.0,"Position":310.576233,"HyperDash":false},{"StartTime":130101.0,"Position":331.515869,"HyperDash":false},{"StartTime":130233.0,"Position":308.0,"HyperDash":false}]},{"StartTime":130461.0,"Objects":[{"StartTime":130461.0,"Position":232.0,"HyperDash":false}]},{"StartTime":130688.0,"Objects":[{"StartTime":130688.0,"Position":220.0,"HyperDash":false}]},{"StartTime":130915.0,"Objects":[{"StartTime":130915.0,"Position":232.0,"HyperDash":false}]},{"StartTime":131142.0,"Objects":[{"StartTime":131142.0,"Position":220.0,"HyperDash":false}]},{"StartTime":131597.0,"Objects":[{"StartTime":131597.0,"Position":18.0,"HyperDash":false}]},{"StartTime":132052.0,"Objects":[{"StartTime":132052.0,"Position":278.0,"HyperDash":false}]},{"StartTime":132506.0,"Objects":[{"StartTime":132506.0,"Position":326.0,"HyperDash":false}]},{"StartTime":132961.0,"Objects":[{"StartTime":132961.0,"Position":430.0,"HyperDash":false}]},{"StartTime":133415.0,"Objects":[{"StartTime":133415.0,"Position":358.0,"HyperDash":false}]},{"StartTime":133870.0,"Objects":[{"StartTime":133870.0,"Position":122.0,"HyperDash":false}]},{"StartTime":134324.0,"Objects":[{"StartTime":134324.0,"Position":119.0,"HyperDash":false},{"StartTime":134419.0,"Position":68.98176,"HyperDash":false},{"StartTime":134551.0,"Position":42.8232956,"HyperDash":false}]},{"StartTime":134779.0,"Objects":[{"StartTime":134779.0,"Position":113.0,"HyperDash":false}]},{"StartTime":135233.0,"Objects":[{"StartTime":135233.0,"Position":243.0,"HyperDash":false}]},{"StartTime":135688.0,"Objects":[{"StartTime":135688.0,"Position":251.0,"HyperDash":false}]},{"StartTime":136142.0,"Objects":[{"StartTime":136142.0,"Position":406.0,"HyperDash":false}]},{"StartTime":136597.0,"Objects":[{"StartTime":136597.0,"Position":484.0,"HyperDash":false}]},{"StartTime":137052.0,"Objects":[{"StartTime":137052.0,"Position":352.0,"HyperDash":false}]},{"StartTime":137506.0,"Objects":[{"StartTime":137506.0,"Position":164.0,"HyperDash":false}]},{"StartTime":137961.0,"Objects":[{"StartTime":137961.0,"Position":178.0,"HyperDash":false},{"StartTime":138056.0,"Position":131.390686,"HyperDash":false},{"StartTime":138188.0,"Position":107.408012,"HyperDash":false}]},{"StartTime":138415.0,"Objects":[{"StartTime":138415.0,"Position":129.0,"HyperDash":false}]},{"StartTime":138870.0,"Objects":[{"StartTime":138870.0,"Position":247.0,"HyperDash":false},{"StartTime":138965.0,"Position":268.5533,"HyperDash":false},{"StartTime":139097.0,"Position":323.543732,"HyperDash":false}]},{"StartTime":139324.0,"Objects":[{"StartTime":139324.0,"Position":469.0,"HyperDash":false}]},{"StartTime":139779.0,"Objects":[{"StartTime":139779.0,"Position":309.0,"HyperDash":false},{"StartTime":139874.0,"Position":267.4467,"HyperDash":false},{"StartTime":140006.0,"Position":232.456268,"HyperDash":false}]},{"StartTime":140233.0,"Objects":[{"StartTime":140233.0,"Position":87.0,"HyperDash":false}]},{"StartTime":140688.0,"Objects":[{"StartTime":140688.0,"Position":109.0,"HyperDash":false}]},{"StartTime":140915.0,"Objects":[{"StartTime":140915.0,"Position":241.0,"HyperDash":false}]},{"StartTime":141142.0,"Objects":[{"StartTime":141142.0,"Position":243.0,"HyperDash":false}]},{"StartTime":141370.0,"Objects":[{"StartTime":141370.0,"Position":305.0,"HyperDash":false}]},{"StartTime":141597.0,"Objects":[{"StartTime":141597.0,"Position":349.0,"HyperDash":false}]},{"StartTime":141824.0,"Objects":[{"StartTime":141824.0,"Position":449.0,"HyperDash":false}]},{"StartTime":142052.0,"Objects":[{"StartTime":142052.0,"Position":493.0,"HyperDash":false}]},{"StartTime":142506.0,"Objects":[{"StartTime":142506.0,"Position":401.0,"HyperDash":false},{"StartTime":142562.0,"Position":403.0,"HyperDash":false},{"StartTime":142619.0,"Position":420.0,"HyperDash":false},{"StartTime":142676.0,"Position":407.0,"HyperDash":false},{"StartTime":142733.0,"Position":401.0,"HyperDash":false},{"StartTime":142828.0,"Position":411.0,"HyperDash":false},{"StartTime":142960.0,"Position":401.0,"HyperDash":false}]},{"StartTime":143415.0,"Objects":[{"StartTime":143415.0,"Position":246.0,"HyperDash":false},{"StartTime":143471.0,"Position":242.0,"HyperDash":false},{"StartTime":143528.0,"Position":264.0,"HyperDash":false},{"StartTime":143585.0,"Position":252.0,"HyperDash":false},{"StartTime":143642.0,"Position":246.0,"HyperDash":false},{"StartTime":143737.0,"Position":262.0,"HyperDash":false},{"StartTime":143869.0,"Position":246.0,"HyperDash":false}]},{"StartTime":144324.0,"Objects":[{"StartTime":144324.0,"Position":91.0,"HyperDash":false}]},{"StartTime":144552.0,"Objects":[{"StartTime":144552.0,"Position":45.0,"HyperDash":false}]},{"StartTime":144779.0,"Objects":[{"StartTime":144779.0,"Position":135.0,"HyperDash":false}]},{"StartTime":145006.0,"Objects":[{"StartTime":145006.0,"Position":45.0,"HyperDash":false}]},{"StartTime":145233.0,"Objects":[{"StartTime":145233.0,"Position":133.0,"HyperDash":false}]},{"StartTime":145688.0,"Objects":[{"StartTime":145688.0,"Position":337.0,"HyperDash":false}]},{"StartTime":145915.0,"Objects":[{"StartTime":145915.0,"Position":277.0,"HyperDash":false}]},{"StartTime":146142.0,"Objects":[{"StartTime":146142.0,"Position":386.0,"HyperDash":false}]},{"StartTime":146597.0,"Objects":[{"StartTime":146597.0,"Position":406.0,"HyperDash":false}]},{"StartTime":146824.0,"Objects":[{"StartTime":146824.0,"Position":320.0,"HyperDash":false}]},{"StartTime":147051.0,"Objects":[{"StartTime":147051.0,"Position":378.0,"HyperDash":false}]},{"StartTime":147506.0,"Objects":[{"StartTime":147506.0,"Position":320.0,"HyperDash":false}]},{"StartTime":147733.0,"Objects":[{"StartTime":147733.0,"Position":282.0,"HyperDash":false},{"StartTime":147828.0,"Position":269.560242,"HyperDash":false},{"StartTime":147960.0,"Position":205.662415,"HyperDash":false}]},{"StartTime":148415.0,"Objects":[{"StartTime":148415.0,"Position":234.0,"HyperDash":false},{"StartTime":148510.0,"Position":236.789261,"HyperDash":false},{"StartTime":148642.0,"Position":226.947067,"HyperDash":false}]},{"StartTime":148870.0,"Objects":[{"StartTime":148870.0,"Position":194.0,"HyperDash":false}]},{"StartTime":149324.0,"Objects":[{"StartTime":149324.0,"Position":88.0,"HyperDash":false},{"StartTime":149380.0,"Position":61.61062,"HyperDash":false},{"StartTime":149437.0,"Position":75.7172852,"HyperDash":false},{"StartTime":149494.0,"Position":64.72825,"HyperDash":false},{"StartTime":149551.0,"Position":71.9050446,"HyperDash":false},{"StartTime":149646.0,"Position":67.47814,"HyperDash":false},{"StartTime":149778.0,"Position":88.0,"HyperDash":false}]},{"StartTime":150233.0,"Objects":[{"StartTime":150233.0,"Position":120.0,"HyperDash":false},{"StartTime":150289.0,"Position":137.763626,"HyperDash":false},{"StartTime":150346.0,"Position":141.788849,"HyperDash":false},{"StartTime":150403.0,"Position":166.251251,"HyperDash":false},{"StartTime":150460.0,"Position":185.8204,"HyperDash":false},{"StartTime":150555.0,"Position":158.755432,"HyperDash":false},{"StartTime":150687.0,"Position":120.0,"HyperDash":false}]},{"StartTime":151142.0,"Objects":[{"StartTime":151142.0,"Position":276.0,"HyperDash":false},{"StartTime":151198.0,"Position":313.273468,"HyperDash":false},{"StartTime":151255.0,"Position":314.899536,"HyperDash":false},{"StartTime":151312.0,"Position":331.123352,"HyperDash":false},{"StartTime":151369.0,"Position":346.809448,"HyperDash":false},{"StartTime":151464.0,"Position":327.8075,"HyperDash":false},{"StartTime":151596.0,"Position":276.0,"HyperDash":false}]},{"StartTime":152051.0,"Objects":[{"StartTime":152051.0,"Position":384.0,"HyperDash":false},{"StartTime":152146.0,"Position":373.33017,"HyperDash":false},{"StartTime":152278.0,"Position":375.168457,"HyperDash":false}]},{"StartTime":152506.0,"Objects":[{"StartTime":152506.0,"Position":256.0,"HyperDash":false}]},{"StartTime":152733.0,"Objects":[{"StartTime":152733.0,"Position":218.0,"HyperDash":false}]},{"StartTime":152961.0,"Objects":[{"StartTime":152961.0,"Position":100.0,"HyperDash":false}]},{"StartTime":153188.0,"Objects":[{"StartTime":153188.0,"Position":104.0,"HyperDash":false}]},{"StartTime":153415.0,"Objects":[{"StartTime":153415.0,"Position":60.0,"HyperDash":false}]},{"StartTime":153870.0,"Objects":[{"StartTime":153870.0,"Position":241.0,"HyperDash":false},{"StartTime":153965.0,"Position":262.296783,"HyperDash":false},{"StartTime":154097.0,"Position":297.158661,"HyperDash":false}]},{"StartTime":154324.0,"Objects":[{"StartTime":154324.0,"Position":311.0,"HyperDash":false}]},{"StartTime":154779.0,"Objects":[{"StartTime":154779.0,"Position":365.0,"HyperDash":false},{"StartTime":154835.0,"Position":380.953857,"HyperDash":false},{"StartTime":154892.0,"Position":377.488251,"HyperDash":false},{"StartTime":154949.0,"Position":393.8036,"HyperDash":false},{"StartTime":155006.0,"Position":430.5609,"HyperDash":false},{"StartTime":155101.0,"Position":417.3473,"HyperDash":false},{"StartTime":155233.0,"Position":365.0,"HyperDash":false}]},{"StartTime":155688.0,"Objects":[{"StartTime":155688.0,"Position":179.0,"HyperDash":false}]},{"StartTime":155915.0,"Objects":[{"StartTime":155915.0,"Position":285.0,"HyperDash":false}]},{"StartTime":156142.0,"Objects":[{"StartTime":156142.0,"Position":154.0,"HyperDash":false}]},{"StartTime":156597.0,"Objects":[{"StartTime":156597.0,"Position":26.0,"HyperDash":false}]},{"StartTime":156824.0,"Objects":[{"StartTime":156824.0,"Position":166.0,"HyperDash":false},{"StartTime":156919.0,"Position":196.995117,"HyperDash":false},{"StartTime":157051.0,"Position":244.69249,"HyperDash":false}]},{"StartTime":157506.0,"Objects":[{"StartTime":157506.0,"Position":305.0,"HyperDash":false},{"StartTime":157601.0,"Position":339.2251,"HyperDash":false},{"StartTime":157733.0,"Position":383.441528,"HyperDash":false}]},{"StartTime":157961.0,"Objects":[{"StartTime":157961.0,"Position":461.0,"HyperDash":false}]},{"StartTime":158415.0,"Objects":[{"StartTime":158415.0,"Position":279.0,"HyperDash":false}]},{"StartTime":158642.0,"Objects":[{"StartTime":158642.0,"Position":370.0,"HyperDash":false}]},{"StartTime":158870.0,"Objects":[{"StartTime":158870.0,"Position":353.0,"HyperDash":false}]},{"StartTime":159324.0,"Objects":[{"StartTime":159324.0,"Position":140.0,"HyperDash":false}]},{"StartTime":159551.0,"Objects":[{"StartTime":159551.0,"Position":320.0,"HyperDash":false}]},{"StartTime":159779.0,"Objects":[{"StartTime":159779.0,"Position":399.0,"HyperDash":false}]},{"StartTime":160006.0,"Objects":[{"StartTime":160006.0,"Position":320.0,"HyperDash":false}]},{"StartTime":160233.0,"Objects":[{"StartTime":160233.0,"Position":255.0,"HyperDash":false},{"StartTime":160328.0,"Position":225.620453,"HyperDash":false},{"StartTime":160460.0,"Position":209.024933,"HyperDash":false}]},{"StartTime":160688.0,"Objects":[{"StartTime":160688.0,"Position":187.0,"HyperDash":false}]},{"StartTime":161142.0,"Objects":[{"StartTime":161142.0,"Position":354.0,"HyperDash":false},{"StartTime":161237.0,"Position":355.953247,"HyperDash":false},{"StartTime":161369.0,"Position":320.988251,"HyperDash":false}]},{"StartTime":161597.0,"Objects":[{"StartTime":161597.0,"Position":207.0,"HyperDash":false}]},{"StartTime":162051.0,"Objects":[{"StartTime":162051.0,"Position":43.0,"HyperDash":false}]},{"StartTime":162279.0,"Objects":[{"StartTime":162279.0,"Position":119.0,"HyperDash":false},{"StartTime":162374.0,"Position":150.19606,"HyperDash":false},{"StartTime":162506.0,"Position":180.9159,"HyperDash":false}]},{"StartTime":162961.0,"Objects":[{"StartTime":162961.0,"Position":195.0,"HyperDash":false},{"StartTime":163056.0,"Position":148.134537,"HyperDash":false},{"StartTime":163188.0,"Position":125.699371,"HyperDash":false}]},{"StartTime":163415.0,"Objects":[{"StartTime":163415.0,"Position":266.0,"HyperDash":false}]},{"StartTime":163870.0,"Objects":[{"StartTime":163870.0,"Position":337.0,"HyperDash":false},{"StartTime":163926.0,"Position":340.576416,"HyperDash":false},{"StartTime":163983.0,"Position":358.8032,"HyperDash":false},{"StartTime":164040.0,"Position":399.717,"HyperDash":false},{"StartTime":164097.0,"Position":413.786346,"HyperDash":false},{"StartTime":164192.0,"Position":398.392517,"HyperDash":false},{"StartTime":164324.0,"Position":337.0,"HyperDash":false}]},{"StartTime":164779.0,"Objects":[{"StartTime":164779.0,"Position":365.0,"HyperDash":false},{"StartTime":164874.0,"Position":341.216339,"HyperDash":false},{"StartTime":165006.0,"Position":289.0602,"HyperDash":false}]},{"StartTime":165233.0,"Objects":[{"StartTime":165233.0,"Position":164.0,"HyperDash":false}]},{"StartTime":165688.0,"Objects":[{"StartTime":165688.0,"Position":420.0,"HyperDash":false}]},{"StartTime":165915.0,"Objects":[{"StartTime":165915.0,"Position":347.0,"HyperDash":false},{"StartTime":166010.0,"Position":378.42804,"HyperDash":false},{"StartTime":166142.0,"Position":365.11972,"HyperDash":false}]},{"StartTime":166597.0,"Objects":[{"StartTime":166597.0,"Position":86.0,"HyperDash":false}]},{"StartTime":166824.0,"Objects":[{"StartTime":166824.0,"Position":212.0,"HyperDash":false}]},{"StartTime":167051.0,"Objects":[{"StartTime":167051.0,"Position":74.0,"HyperDash":false},{"StartTime":167107.0,"Position":65.55724,"HyperDash":false},{"StartTime":167164.0,"Position":36.62049,"HyperDash":false},{"StartTime":167278.0,"Position":74.0,"HyperDash":false}]},{"StartTime":167506.0,"Objects":[{"StartTime":167506.0,"Position":244.0,"HyperDash":false}]},{"StartTime":167733.0,"Objects":[{"StartTime":167733.0,"Position":166.0,"HyperDash":false}]},{"StartTime":167961.0,"Objects":[{"StartTime":167961.0,"Position":274.0,"HyperDash":false},{"StartTime":168013.0,"Position":301.951935,"HyperDash":false},{"StartTime":168065.0,"Position":319.251465,"HyperDash":false},{"StartTime":168117.0,"Position":329.4265,"HyperDash":false},{"StartTime":168170.0,"Position":343.4541,"HyperDash":false},{"StartTime":168222.0,"Position":376.318848,"HyperDash":false},{"StartTime":168274.0,"Position":385.979645,"HyperDash":false},{"StartTime":168326.0,"Position":398.919922,"HyperDash":false},{"StartTime":168415.0,"Position":410.559265,"HyperDash":false}]},{"StartTime":168642.0,"Objects":[{"StartTime":168642.0,"Position":266.0,"HyperDash":false}]},{"StartTime":168870.0,"Objects":[{"StartTime":168870.0,"Position":262.0,"HyperDash":false},{"StartTime":168922.0,"Position":264.549957,"HyperDash":false},{"StartTime":168974.0,"Position":244.610046,"HyperDash":false},{"StartTime":169026.0,"Position":261.6293,"HyperDash":false},{"StartTime":169079.0,"Position":278.3733,"HyperDash":false},{"StartTime":169131.0,"Position":294.3729,"HyperDash":false},{"StartTime":169183.0,"Position":290.6487,"HyperDash":false},{"StartTime":169235.0,"Position":313.168427,"HyperDash":false},{"StartTime":169324.0,"Position":333.1015,"HyperDash":false}]},{"StartTime":169551.0,"Objects":[{"StartTime":169551.0,"Position":391.0,"HyperDash":false}]},{"StartTime":169779.0,"Objects":[{"StartTime":169779.0,"Position":340.0,"HyperDash":false},{"StartTime":169831.0,"Position":319.4877,"HyperDash":false},{"StartTime":169883.0,"Position":321.416565,"HyperDash":false},{"StartTime":169935.0,"Position":292.492767,"HyperDash":false},{"StartTime":169988.0,"Position":257.8616,"HyperDash":false},{"StartTime":170040.0,"Position":232.613708,"HyperDash":false},{"StartTime":170092.0,"Position":235.74971,"HyperDash":false},{"StartTime":170144.0,"Position":224.646179,"HyperDash":false},{"StartTime":170233.0,"Position":191.472458,"HyperDash":false}]},{"StartTime":170461.0,"Objects":[{"StartTime":170461.0,"Position":300.0,"HyperDash":false}]},{"StartTime":170688.0,"Objects":[{"StartTime":170688.0,"Position":319.0,"HyperDash":false},{"StartTime":170740.0,"Position":317.817749,"HyperDash":false},{"StartTime":170792.0,"Position":324.806122,"HyperDash":false},{"StartTime":170844.0,"Position":327.30014,"HyperDash":false},{"StartTime":170897.0,"Position":341.6977,"HyperDash":false},{"StartTime":170949.0,"Position":331.214264,"HyperDash":false},{"StartTime":171001.0,"Position":340.934967,"HyperDash":false},{"StartTime":171053.0,"Position":335.773926,"HyperDash":false},{"StartTime":171142.0,"Position":303.6745,"HyperDash":false}]},{"StartTime":171370.0,"Objects":[{"StartTime":171370.0,"Position":157.0,"HyperDash":false}]},{"StartTime":171597.0,"Objects":[{"StartTime":171597.0,"Position":184.0,"HyperDash":false},{"StartTime":171649.0,"Position":177.816864,"HyperDash":false},{"StartTime":171701.0,"Position":167.695633,"HyperDash":false},{"StartTime":171753.0,"Position":168.327789,"HyperDash":false},{"StartTime":171806.0,"Position":156.99791,"HyperDash":false},{"StartTime":171858.0,"Position":147.82634,"HyperDash":false},{"StartTime":171910.0,"Position":166.402451,"HyperDash":false},{"StartTime":171962.0,"Position":147.244476,"HyperDash":false},{"StartTime":172051.0,"Position":180.821411,"HyperDash":false}]},{"StartTime":172279.0,"Objects":[{"StartTime":172279.0,"Position":296.0,"HyperDash":false}]},{"StartTime":172506.0,"Objects":[{"StartTime":172506.0,"Position":366.0,"HyperDash":false}]},{"StartTime":172961.0,"Objects":[{"StartTime":172961.0,"Position":296.0,"HyperDash":false}]},{"StartTime":173188.0,"Objects":[{"StartTime":173188.0,"Position":272.0,"HyperDash":false}]},{"StartTime":173415.0,"Objects":[{"StartTime":173415.0,"Position":216.0,"HyperDash":false},{"StartTime":173467.0,"Position":213.8114,"HyperDash":false},{"StartTime":173519.0,"Position":176.041458,"HyperDash":false},{"StartTime":173571.0,"Position":162.964,"HyperDash":false},{"StartTime":173624.0,"Position":126.355881,"HyperDash":false},{"StartTime":173676.0,"Position":137.035782,"HyperDash":false},{"StartTime":173728.0,"Position":92.75827,"HyperDash":false},{"StartTime":173780.0,"Position":98.66459,"HyperDash":false},{"StartTime":173869.0,"Position":60.0903053,"HyperDash":false}]},{"StartTime":174097.0,"Objects":[{"StartTime":174097.0,"Position":156.0,"HyperDash":false}]},{"StartTime":174324.0,"Objects":[{"StartTime":174324.0,"Position":150.0,"HyperDash":false}]},{"StartTime":174438.0,"Objects":[{"StartTime":174438.0,"Position":156.0,"HyperDash":false}]},{"StartTime":174551.0,"Objects":[{"StartTime":174551.0,"Position":150.0,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/75858.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/75858.osu new file mode 100644 index 0000000000..637273efad --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/75858.osu @@ -0,0 +1,417 @@ +osu file format v8 + +[General] +StackLeniency: 0.6 +Mode: 0 + +[Difficulty] +HPDrainRate:5 +CircleSize:4 +OverallDifficulty:7 +ApproachRate:7 +SliderMultiplier:1.6 +SliderTickRate:0.5 + +[Events] +//Background and Video events +//Break Periods +2,38388,45242 +2,89297,92515 +2,107705,114333 +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples +//Background Colour Transformations +3,100,163,162,255 + +[TimingPoints] +1173,666.666666666667,4,2,1,60,1,0 +2173,-100,4,2,1,50,0,0 +2839,-100,4,2,1,60,0,0 +4839,-100,4,2,1,50,0,0 +5506,-100,4,2,1,60,0,0 +7506,-100,4,2,1,50,0,0 +8173,-100,4,2,1,60,0,0 +10673,-200,4,2,1,60,0,0 +11173,-200,4,2,1,10,0,0 +11673,-100,4,2,1,60,0,0 +12839,-100,4,2,1,50,0,0 +13506,-100,4,2,1,60,0,0 +15506,-100,4,2,1,50,0,0 +16173,-100,4,2,1,60,0,0 +16839,-100,4,2,1,50,0,0 +17506,-100,4,2,1,60,0,0 +19506,-100,4,2,1,70,0,0 +20006,-100,4,2,1,30,0,0 +22052,454.545454545455,4,2,1,40,1,0 +23642,-100,4,2,2,70,0,0 +23870,-100,4,2,2,70,0,1 +38415,-100,4,2,2,70,0,0 +45915,-100,4,2,2,60,0,0 +52733,-100,4,2,2,25,0,0 +53188,-100,4,2,2,60,0,0 +60006,-100,4,2,2,25,0,0 +60460,-100,4,2,1,45,0,0 +67620,-100,4,2,1,50,0,0 +71483,-100,4,2,1,55,0,0 +74267,-100,4,2,1,70,0,0 +74665,-100,4,2,2,80,0,0 +74779,-100,4,2,2,80,0,1 +89324,-100,4,2,2,80,0,0 +92961,-100,4,2,2,65,0,0 +107279,-100,4,2,1,40,0,0 +107733,-100,4,2,1,45,0,0 +126029,-100,4,2,1,50,0,0 +128813,-100,4,2,1,60,0,0 +129211,-100,4,2,1,70,0,0 +129438,-100,4,2,1,55,0,0 +130631,-100,4,2,1,65,0,0 +131029,-100,4,2,1,75,0,0 +131370,-100,4,2,2,65,0,0 +145461,-100,4,2,2,75,0,0 +145688,-100,4,2,2,75,0,1 +160120,-100,4,2,2,80,0,0 +160233,-100,4,2,2,80,0,1 +174779,-100,4,2,2,80,0,0 + +[HitObjects] +94,279,1173,2,0,B|125:307|190:315|253:298,1,160,8|2 +398,247,2506,1,0 +471,104,3172,1,2 +320,51,3839,6,0,B|275:33|209:34|165:67,1,160,8|2 +65,190,5173,1,0 +149,325,5839,1,2 +239,192,6506,6,0,B|295:173|352:207|417:188,1,160,4|2 +493,320,7839,1,0 +334,340,8506,1,2 +199,253,9173,5,0 +171,95,9839,1,2 +271,219,10506,1,0 +199,253,10839,2,0,B|161:276|115:272,1,80,2|0 +42,266,11839,5,4 +110,121,12506,1,2 +263,168,13172,2,0,B|305:186|378:185|423:172,1,160,0|2 +293,75,14506,6,0,B|276:121|216:147|156:149,1,160,8|2 +282,251,15839,2,0,B|299:297|359:323|419:325,1,160,0|2 +416,164,17172,1,4 +256,148,17839,2,0,B|172:136|172:136|230:189|198:266|154:289|154:289|85:276,1,320,2|2 +256,148,19839,1,4 +256,192,20173,12,8,22506 +256,192,22961,5,8 +256,192,23415,1,8 +104,245,23870,6,0,B|132:284|196:282,2,80,2|0|2 +256,192,24779,2,0,B|314:192|348:238,2,80,2|0|2 +118,111,25688,2,0,B|165:91|229:112,1,80,0|2 +275,113,26142,1,0 +419,185,26597,2,0,B|383:204|338:204|337:204,1,80,2|0 +261,196,27052,1,2 +128,285,27506,5,0 +97,211,27733,2,0,B|82:168|96:131,1,80,2|0 +236,56,28415,1,0 +313,77,28642,2,0,B|356:91|411:84,1,80,0|2 +456,232,29324,1,0 +456,232,29552,1,2 +456,232,29779,1,0 +311,299,30233,1,0 +231,312,30461,1,2 +151,300,30688,2,0,B|98:292,2,40,0|0|0 +231,312,31142,5,2 +202,236,31370,1,0 +194,156,31597,2,0,B|188:101|218:47|274:27,1,160,2|2 +295,104,32279,1,0 +273,181,32506,2,0,B|218:179|150:144|144:96,1,160,2|0 +219,72,33188,1,2 +295,104,33415,2,0,B|252:152|265:239|328:269,1,160 +367,205,34097,1,0 +372,125,34324,2,0,B|323:95|242:92|191:148,1,160,2|0 +154,170,35006,1,2 +107,234,35233,6,0,B|134:267|226:268|262:230,1,160,0|2 +316,183,35915,1,0 +350,111,36142,1,2 +350,111,36597,1,0 +393,178,36824,1,2 +406,257,37051,2,0,B|402:303|344:360|271:363,1,160,0|2 +216,350,37733,1,0 +154,298,37961,5,2 +154,298,38074,1,2 +154,298,38188,1,2 +105,136,46142,6,0,B|125:91|191:64|257:74,1,160,0|2 +399,102,47051,2,0,B|486:117|485:117,2,80,0|2|0 +422,260,47961,2,0,B|402:305|336:332|270:322,1,160,0|2 +128,294,48870,2,0,B|41:279|42:279,2,80,0|2|0 +252,193,49779,6,0,B|297:168|358:163|436:186,1,160,0|2 +342,324,50688,1,0 +377,252,50915,2,0,B|335:241|293:260,1,80,2|0 +227,293,51370,1,0 +159,335,51597,2,0,B|118:354|78:347,1,80,2|0 +107,271,52051,2,0,B|56:280|16:255,1,80,2|0 +75,196,52506,2,0,B|132:204|191:190|229:151,1,160,4|0 +321,27,53415,5,2 +321,27,53642,1,0 +321,27,53870,2,0,B|376:37|403:124|352:180,1,160 +331,230,54551,1,2 +266,276,54779,1,0 +266,276,55233,5,2 +266,276,55461,1,0 +266,276,55688,2,0,B|208:296|133:275|108:219,1,160 +89,164,56370,1,2 +99,84,56597,1,0 +99,84,57051,5,2 +99,84,57279,1,0 +99,84,57506,2,0,B|128:116|201:127|254:108,1,160 +326,84,58188,1,2 +382,27,58415,1,0 +401,104,58642,2,0,B|392:148|345:160,1,80,0|2 +274,188,59097,1,0 +337,236,59324,2,0,B|374:265|364:310,1,80,2|0 +284,298,59779,2,0,B|243:334|169:279|128:318,1,160,4|0 +41,182,60688,5,0 +191,127,61142,2,0,B|276:94,1,80,2|0 +254,177,61597,2,0,B|339:144,1,80 +319,227,62051,1,2 +319,227,62279,2,0,B|234:260,1,80,0|8 +168,281,62733,1,0 +91,305,62961,1,2 +31,252,63188,1,0 +31,172,63415,2,0,B|31:88,2,80,0|2|2 +181,116,64324,5,8 +335,74,64779,2,0,B|335:162,1,80,2|0 +405,116,65233,2,0,B|405:198,1,80 +475,157,65688,1,2 +475,157,65915,2,0,B|475:69,1,80,0|8 +405,37,66370,1,0 +325,26,66597,1,2 +252,60,66824,1,8 +204,124,67051,1,0 +189,202,67279,1,2 +202,280,67506,1,10 +250,343,67733,1,0 +329,332,67961,6,0,B|432:315,1,80,0|8 +427,241,68415,2,0,B|324:258,1,80,2|8 +303,187,68870,2,0,B|406:170,1,80,0|8 +401,96,69324,2,0,B|298:113,1,80,2|8 +242,122,69779,5,0 +242,122,70006,1,8 +163,135,70233,1,2 +163,135,70461,1,8 +84,150,70688,2,0,B|60:195|95:243,3,80,0|2|2|0 +148,275,71597,6,0,B|180:305|252:312|305:295,1,160,4|10 +374,86,72506,2,0,B|342:56|270:49|217:66,1,160,4|10 +147,97,73188,1,0 +213,141,73415,2,0,B|286:189,1,80,8|2 +346,229,73870,2,0,B|282:313,1,80,8|2 +252,358,74324,1,10 +252,358,74551,1,10 +252,358,74779,6,0,B|208:373|169:356,2,80,0|0|2 +194,208,75688,2,0,B|150:193|111:210,2,80,2|0|2 +347,252,76597,1,0 +347,252,76824,1,2 +347,252,77051,1,0 +448,128,77506,1,0 +368,117,77733,1,0 +305,67,77961,1,2 +146,87,78415,5,0 +118,161,78642,2,0,B|99:205|41:224,1,80,2|0 +218,249,79324,2,0,B|252:272|301:266,1,80 +372,247,79779,1,2 +286,112,80233,2,0,B|282:23,2,80,0|2|0 +427,186,81142,1,0 +427,186,81370,1,2 +427,186,81597,2,0,B|431:244,2,40 +421,105,82051,5,0 +356,152,82279,1,0 +285,188,82506,2,0,B|236:212|160:202|130:174,1,160,2|2 +188,119,83188,1,0 +267,110,83415,2,0,B|303:160|289:236|225:276,1,160,2|0 +193,198,84097,1,2 +188,119,84324,2,0,B|240:128|312:104|337:51,1,160 +257,29,85006,1,0 +177,39,85233,2,0,B|160:93|191:163|284:166,1,160,2|0 +326,183,85915,1,2 +404,197,86142,6,0,B|455:212|468:261|448:314|380:320,1,160 +326,330,86824,1,0 +246,322,87051,1,2 +246,322,87506,1,0 +192,262,87733,1,2 +168,185,87961,2,0,B|148:132|174:73|235:44,1,160,0|2 +299,23,88642,1,0 +378,36,88870,5,2 +378,36,88983,1,2 +378,36,89097,1,2 +330,47,93415,6,0,B|388:28|453:36,2,120,2|0|2 +254,74,94324,1,0 +181,108,94552,2,0,B|129:134,1,40 +181,107,95233,6,0,B|123:88|58:96,2,120,2|0|2 +257,134,96142,1,0 +330,168,96370,2,0,B|382:194,1,40 +330,168,97052,6,0,B|388:149|453:157,2,120,2|0|2 +254,195,97961,1,0 +181,229,98188,2,0,B|129:255,1,40 +181,228,98870,6,0,B|123:209|58:217,2,120,2|0|2 +257,255,99779,1,0 +330,289,100006,2,0,B|382:315,1,40 +454,74,100688,6,0,B|403:83,1,40,2|0 +335,95,101029,2,0,B|270:105,1,40,2|0 +216,114,101370,1,0 +137,127,101597,2,0,B|89:139|30:126,1,80,2|0 +57,130,101938,1,0 +57,130,102506,6,0,B|108:139,1,40,2|0 +176,151,102847,2,0,B|240:161,1,40,2|0 +295,170,103188,1,0 +374,183,103415,2,0,B|422:195|481:182,1,80,2|0 +454,187,103756,1,0 +454,187,104324,6,0,B|403:196,1,40,2|0 +335,208,104665,2,0,B|270:218,1,40,2|0 +216,227,105006,1,0 +176,234,105120,1,0 +137,240,105233,2,0,B|89:252|30:239,1,80,2|0 +57,244,105574,1,0 +57,244,106142,6,0,B|108:253,1,40,2|0 +176,265,106483,2,0,B|240:275,1,40,2|0 +295,284,106824,2,0,B|320:300|372:301|408:283|408:283|371:256|318:256|291:287,1,240,0|12 +114,269,115233,6,0,B|145:293|228:297|281:282,1,160,0|2 +347,264,115915,1,0 +419,230,116142,2,0,B|452:197|450:147,2,80,0|0|2 +366,78,117052,2,0,B|330:116|275:110,1,80,8|0 +216,99,117506,1,2 +149,54,117733,1,0 +84,102,117961,2,0,B|84:216,3,80,0|2|2|0 +85,262,118870,5,8 +155,299,119097,1,0 +225,261,119324,1,2 +296,297,119552,1,0 +368,263,119779,2,0,B|411:250|461:267,2,80,0|0|2 +434,117,120688,1,0 +364,77,120915,1,8 +286,58,121142,2,0,B|229:48|161:67|126:113,1,160,2|0 +102,172,121824,1,10 +102,252,122052,2,0,B|104:301|150:325|152:324,1,80,2|0 +187,253,122506,6,0,B|228:231|312:284|368:259,1,160,8|2 +409,217,123188,1,0 +342,172,123415,2,0,B|297:185|225:140|184:159,1,160,0|2 +118,114,124097,1,0 +184,70,124324,2,0,B|226:47|319:101|365:76,1,160,8|2 +401,29,125006,1,0 +474,59,125233,2,0,B|496:100|472:142,1,80,0|2 +437,206,125688,2,0,B|415:251|442:297,1,80,2|0 +506,246,126142,6,0,B|342:247,1,160,0|10 +28,229,127052,2,0,B|192:228,1,160,0|10 +267,228,127733,1,0 +226,297,127961,2,0,B|202:340|232:391,2,80,8|0|10 +267,228,128642,1,0 +308,159,128870,1,8 +308,159,129097,1,10 +308,159,129324,1,4 +308,159,129779,6,0,B|332:202|302:253,2,80,10|0|10 +267,90,130461,1,0 +226,21,130688,1,10 +226,21,130915,1,10 +226,21,131142,1,6 +119,140,131597,5,2 +148,297,132052,1,0 +302,338,132506,1,2 +430,242,132961,1,0 +394,86,133415,1,2 +240,40,133870,1,0 +119,140,134324,2,0,B|81:168|17:153,1,80,2|0 +65,80,134779,1,0 +178,192,135233,5,2 +247,336,135688,1,0 +406,343,136142,1,2 +484,203,136597,1,0 +418,57,137052,1,2 +258,52,137506,1,0 +178,192,137961,2,0,B|141:228|91:227,1,80,2|2 +110,146,138415,1,0 +247,228,138870,6,0,B|282:250|337:247,1,80,2|0 +403,246,139324,1,0 +309,115,139779,2,0,B|274:93|219:96,1,80,2|0 +153,97,140233,1,0 +98,247,140688,1,2 +175,265,140915,1,0 +242,221,141142,1,2 +274,147,141370,1,0 +327,87,141597,1,2 +399,52,141824,1,0 +471,86,142052,1,0 +401,230,142506,6,0,B|401:323,2,80,2|0|0 +246,272,143415,2,0,B|246:365,2,80,2|0|0 +91,314,144324,1,2 +45,247,144552,1,0 +90,181,144779,1,2 +45,114,145006,1,0 +89,47,145233,1,2 +235,112,145688,5,0 +307,146,145915,1,0 +386,139,146142,1,2 +386,139,146597,1,2 +353,211,146824,1,0 +349,291,147051,1,2 +349,291,147506,1,0 +282,246,147733,2,0,B|245:222|179:226,1,80,2|0 +234,70,148415,2,0,B|247:122|216:167,1,80,2|0 +205,225,148870,1,2 +88,116,149324,6,0,B|56:159|77:205,2,80,0|2|0 +120,272,150233,2,0,B|139:307|193:313,2,80,2|0|2 +276,304,151142,2,0,B|324:298|364:252,2,80,0|2|0 +384,185,152051,2,0,B|399:140|372:104,1,80,0|2 +314,56,152506,1,0 +237,34,152733,1,0 +159,54,152961,5,2 +102,110,153188,1,0 +82,187,153415,1,2 +241,172,153870,2,0,B|296:192|303:250,1,80 +307,304,154324,1,2 +365,155,154779,2,0,B|389:116|435:115,2,80,0|2|0 +307,304,155688,1,2 +232,334,155915,1,0 +154,315,156142,1,2 +90,167,156597,5,0 +166,189,156824,2,0,B|211:202|257:182,1,80,2|0 +305,38,157506,2,0,B|345:21|392:34,1,80,2|0 +461,50,157961,1,2 +370,181,158415,1,0 +370,181,158642,1,2 +370,181,158870,1,0 +255,292,159324,1,0 +320,337,159551,1,2 +399,341,159779,5,2 +320,337,160006,1,0 +255,292,160233,2,0,B|209:264|205:203,1,80,2|0 +196,149,160688,1,2 +354,171,161142,2,0,B|352:219|305:256,1,80 +256,290,161597,1,2 +125,197,162051,1,0 +119,117,162279,2,0,B|138:78|187:70,1,80,2|0 +195,230,162961,2,0,B|143:232|114:179,1,80,2|0 +190,150,163415,1,2 +337,86,163870,6,0,B|372:64|421:70,2,80,0|2|0 +365,243,164779,2,0,B|328:272|260:256,1,80,2|0 +212,239,165233,1,2 +292,111,165688,1,0 +347,168,165915,2,0,B|377:201|362:257,1,80,2|0 +224,320,166597,1,0 +149,292,166824,1,2 +74,261,167051,2,0,B|32:245,2,40 +138,213,167506,5,2 +205,169,167733,1,0 +274,129,167961,2,0,B|328:113|400:144|414:196,1,160,2|0 +340,224,168642,1,0 +262,204,168870,2,0,B|249:152|288:80|343:74,1,160,2|0 +367,148,169551,1,2 +340,224,169779,2,0,B|298:191|219:196|180:244,1,160,0|2 +240,295,170461,1,0 +319,301,170688,2,0,B|355:264|345:184|301:156,1,160,2|0 +229,127,171370,1,2 +184,60,171597,6,0,B|131:94|134:176|208:218,1,160 +252,234,172279,1,0 +331,241,172506,1,2 +331,241,172961,1,0 +284,306,173188,1,2 +216,348,173415,2,0,B|171:368|94:370|56:347,1,160,0|2 +106,283,174097,1,0 +153,218,174324,5,2 +153,218,174438,1,2 +153,218,174551,1,2 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/871815-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/871815-expected-conversion.json new file mode 100644 index 0000000000..c8ebf04ca4 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/871815-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":1459.0,"Objects":[{"StartTime":1459.0,"Position":150.0,"HyperDash":false}]},{"StartTime":1809.0,"Objects":[{"StartTime":1809.0,"Position":150.0,"HyperDash":false},{"StartTime":1896.0,"Position":172.185562,"HyperDash":false},{"StartTime":1984.0,"Position":224.798538,"HyperDash":false},{"StartTime":2072.0,"Position":262.41153,"HyperDash":false}]},{"StartTime":2160.0,"Objects":[{"StartTime":2160.0,"Position":281.0,"HyperDash":false},{"StartTime":2229.0,"Position":285.0371,"HyperDash":false},{"StartTime":2335.0,"Position":285.094574,"HyperDash":false}]},{"StartTime":2511.0,"Objects":[{"StartTime":2511.0,"Position":272.0,"HyperDash":false}]},{"StartTime":2687.0,"Objects":[{"StartTime":2687.0,"Position":367.0,"HyperDash":false},{"StartTime":2774.0,"Position":395.071045,"HyperDash":false},{"StartTime":2862.0,"Position":385.323761,"HyperDash":false},{"StartTime":2931.0,"Position":384.634644,"HyperDash":false},{"StartTime":3037.0,"Position":371.162445,"HyperDash":false}]},{"StartTime":3213.0,"Objects":[{"StartTime":3213.0,"Position":278.0,"HyperDash":false}]},{"StartTime":3388.0,"Objects":[{"StartTime":3388.0,"Position":113.0,"HyperDash":false}]},{"StartTime":3476.0,"Objects":[{"StartTime":3476.0,"Position":116.0,"HyperDash":false}]},{"StartTime":3564.0,"Objects":[{"StartTime":3564.0,"Position":121.0,"HyperDash":false},{"StartTime":3607.0,"Position":121.720619,"HyperDash":false},{"StartTime":3651.0,"Position":121.0,"HyperDash":false},{"StartTime":3695.0,"Position":121.720619,"HyperDash":false},{"StartTime":3739.0,"Position":121.0,"HyperDash":false},{"StartTime":3783.0,"Position":121.720619,"HyperDash":false},{"StartTime":3827.0,"Position":121.0,"HyperDash":false},{"StartTime":3871.0,"Position":121.720619,"HyperDash":false},{"StartTime":3914.0,"Position":121.0,"HyperDash":false}]},{"StartTime":4266.0,"Objects":[{"StartTime":4266.0,"Position":368.0,"HyperDash":false},{"StartTime":4353.0,"Position":394.044952,"HyperDash":false},{"StartTime":4441.0,"Position":430.602478,"HyperDash":false},{"StartTime":4510.0,"Position":451.933838,"HyperDash":false},{"StartTime":4616.0,"Position":498.848267,"HyperDash":false}]},{"StartTime":4792.0,"Objects":[{"StartTime":4792.0,"Position":440.0,"HyperDash":false}]},{"StartTime":4967.0,"Objects":[{"StartTime":4967.0,"Position":289.0,"HyperDash":false},{"StartTime":5036.0,"Position":249.229553,"HyperDash":false},{"StartTime":5142.0,"Position":216.522751,"HyperDash":false}]},{"StartTime":5318.0,"Objects":[{"StartTime":5318.0,"Position":105.0,"HyperDash":false}]},{"StartTime":5494.0,"Objects":[{"StartTime":5494.0,"Position":119.0,"HyperDash":false},{"StartTime":5581.0,"Position":96.97381,"HyperDash":false},{"StartTime":5669.0,"Position":97.10305,"HyperDash":false},{"StartTime":5738.0,"Position":108.834061,"HyperDash":false},{"StartTime":5844.0,"Position":132.852325,"HyperDash":false}]},{"StartTime":6020.0,"Objects":[{"StartTime":6020.0,"Position":192.0,"HyperDash":true}]},{"StartTime":6195.0,"Objects":[{"StartTime":6195.0,"Position":451.0,"HyperDash":false},{"StartTime":6282.0,"Position":417.845947,"HyperDash":false},{"StartTime":6370.0,"Position":392.282257,"HyperDash":false},{"StartTime":6439.0,"Position":343.915466,"HyperDash":false},{"StartTime":6545.0,"Position":323.918671,"HyperDash":false}]},{"StartTime":6722.0,"Objects":[{"StartTime":6722.0,"Position":380.0,"HyperDash":false}]},{"StartTime":6897.0,"Objects":[{"StartTime":6897.0,"Position":334.0,"HyperDash":false}]},{"StartTime":6985.0,"Objects":[{"StartTime":6985.0,"Position":334.0,"HyperDash":false}]},{"StartTime":7073.0,"Objects":[{"StartTime":7073.0,"Position":334.0,"HyperDash":false},{"StartTime":7160.0,"Position":336.334045,"HyperDash":false},{"StartTime":7248.0,"Position":347.94342,"HyperDash":false},{"StartTime":7317.0,"Position":360.6226,"HyperDash":false},{"StartTime":7423.0,"Position":326.399445,"HyperDash":false}]},{"StartTime":7599.0,"Objects":[{"StartTime":7599.0,"Position":281.0,"HyperDash":false}]},{"StartTime":7774.0,"Objects":[{"StartTime":7774.0,"Position":140.0,"HyperDash":false}]},{"StartTime":7950.0,"Objects":[{"StartTime":7950.0,"Position":274.0,"HyperDash":false}]},{"StartTime":8125.0,"Objects":[{"StartTime":8125.0,"Position":138.0,"HyperDash":false}]},{"StartTime":8301.0,"Objects":[{"StartTime":8301.0,"Position":266.0,"HyperDash":false},{"StartTime":8388.0,"Position":316.25592,"HyperDash":false},{"StartTime":8476.0,"Position":340.940063,"HyperDash":false},{"StartTime":8545.0,"Position":353.487854,"HyperDash":false},{"StartTime":8651.0,"Position":415.880127,"HyperDash":false}]},{"StartTime":8827.0,"Objects":[{"StartTime":8827.0,"Position":512.0,"HyperDash":false}]},{"StartTime":9002.0,"Objects":[{"StartTime":9002.0,"Position":490.0,"HyperDash":false},{"StartTime":9089.0,"Position":482.986267,"HyperDash":false},{"StartTime":9177.0,"Position":439.110077,"HyperDash":false},{"StartTime":9246.0,"Position":414.503052,"HyperDash":false},{"StartTime":9352.0,"Position":366.139923,"HyperDash":false}]},{"StartTime":9529.0,"Objects":[{"StartTime":9529.0,"Position":260.0,"HyperDash":false},{"StartTime":9598.0,"Position":239.881287,"HyperDash":false},{"StartTime":9704.0,"Position":261.6383,"HyperDash":false}]},{"StartTime":9792.0,"Objects":[{"StartTime":9792.0,"Position":267.0,"HyperDash":false}]},{"StartTime":9880.0,"Objects":[{"StartTime":9880.0,"Position":267.0,"HyperDash":false},{"StartTime":9967.0,"Position":234.350662,"HyperDash":false},{"StartTime":10055.0,"Position":203.079025,"HyperDash":false},{"StartTime":10124.0,"Position":187.624725,"HyperDash":false},{"StartTime":10230.0,"Position":130.615417,"HyperDash":false}]},{"StartTime":10406.0,"Objects":[{"StartTime":10406.0,"Position":185.0,"HyperDash":false}]},{"StartTime":10581.0,"Objects":[{"StartTime":10581.0,"Position":177.0,"HyperDash":false},{"StartTime":10650.0,"Position":191.354156,"HyperDash":false},{"StartTime":10756.0,"Position":249.516769,"HyperDash":false}]},{"StartTime":10932.0,"Objects":[{"StartTime":10932.0,"Position":352.0,"HyperDash":false}]},{"StartTime":11108.0,"Objects":[{"StartTime":11108.0,"Position":436.0,"HyperDash":false},{"StartTime":11177.0,"Position":446.0463,"HyperDash":false},{"StartTime":11283.0,"Position":509.668152,"HyperDash":false}]},{"StartTime":11458.0,"Objects":[{"StartTime":11458.0,"Position":368.0,"HyperDash":false},{"StartTime":11527.0,"Position":322.9537,"HyperDash":false},{"StartTime":11633.0,"Position":294.331848,"HyperDash":false}]},{"StartTime":11809.0,"Objects":[{"StartTime":11809.0,"Position":181.0,"HyperDash":false},{"StartTime":11878.0,"Position":187.9666,"HyperDash":false},{"StartTime":11984.0,"Position":184.937943,"HyperDash":false}]},{"StartTime":12160.0,"Objects":[{"StartTime":12160.0,"Position":221.0,"HyperDash":false}]},{"StartTime":12248.0,"Objects":[{"StartTime":12248.0,"Position":221.0,"HyperDash":false}]},{"StartTime":12336.0,"Objects":[{"StartTime":12336.0,"Position":221.0,"HyperDash":false},{"StartTime":12405.0,"Position":266.95636,"HyperDash":false},{"StartTime":12511.0,"Position":293.24704,"HyperDash":false}]},{"StartTime":12687.0,"Objects":[{"StartTime":12687.0,"Position":440.0,"HyperDash":false},{"StartTime":12774.0,"Position":402.903931,"HyperDash":false},{"StartTime":12862.0,"Position":366.3639,"HyperDash":false},{"StartTime":12931.0,"Position":335.8872,"HyperDash":false},{"StartTime":13037.0,"Position":292.814026,"HyperDash":false}]},{"StartTime":13213.0,"Objects":[{"StartTime":13213.0,"Position":330.0,"HyperDash":false}]},{"StartTime":13301.0,"Objects":[{"StartTime":13301.0,"Position":330.0,"HyperDash":false}]},{"StartTime":13388.0,"Objects":[{"StartTime":13388.0,"Position":330.0,"HyperDash":false},{"StartTime":13457.0,"Position":378.510529,"HyperDash":false},{"StartTime":13563.0,"Position":404.689636,"HyperDash":false}]},{"StartTime":13739.0,"Objects":[{"StartTime":13739.0,"Position":494.0,"HyperDash":false}]},{"StartTime":13915.0,"Objects":[{"StartTime":13915.0,"Position":321.0,"HyperDash":false}]},{"StartTime":14002.0,"Objects":[{"StartTime":14002.0,"Position":321.0,"HyperDash":false}]},{"StartTime":14090.0,"Objects":[{"StartTime":14090.0,"Position":321.0,"HyperDash":false},{"StartTime":14159.0,"Position":343.727631,"HyperDash":false},{"StartTime":14265.0,"Position":391.072754,"HyperDash":false}]},{"StartTime":14441.0,"Objects":[{"StartTime":14441.0,"Position":231.0,"HyperDash":false}]},{"StartTime":14616.0,"Objects":[{"StartTime":14616.0,"Position":188.0,"HyperDash":false},{"StartTime":14703.0,"Position":182.992142,"HyperDash":false},{"StartTime":14791.0,"Position":176.795868,"HyperDash":false},{"StartTime":14860.0,"Position":189.954117,"HyperDash":false},{"StartTime":14966.0,"Position":188.0,"HyperDash":false}]},{"StartTime":15143.0,"Objects":[{"StartTime":15143.0,"Position":125.0,"HyperDash":false},{"StartTime":15230.0,"Position":105.420952,"HyperDash":false},{"StartTime":15318.0,"Position":59.72222,"HyperDash":false},{"StartTime":15406.0,"Position":22.9492321,"HyperDash":false}]},{"StartTime":15494.0,"Objects":[{"StartTime":15494.0,"Position":17.0,"HyperDash":false},{"StartTime":15563.0,"Position":33.37393,"HyperDash":false},{"StartTime":15669.0,"Position":20.4846058,"HyperDash":false}]},{"StartTime":15844.0,"Objects":[{"StartTime":15844.0,"Position":29.0,"HyperDash":false}]},{"StartTime":16020.0,"Objects":[{"StartTime":16020.0,"Position":130.0,"HyperDash":false}]},{"StartTime":16108.0,"Objects":[{"StartTime":16108.0,"Position":130.0,"HyperDash":false}]},{"StartTime":16195.0,"Objects":[{"StartTime":16195.0,"Position":130.0,"HyperDash":false},{"StartTime":16264.0,"Position":176.33783,"HyperDash":false},{"StartTime":16370.0,"Position":203.709747,"HyperDash":false}]},{"StartTime":16546.0,"Objects":[{"StartTime":16546.0,"Position":287.0,"HyperDash":false}]},{"StartTime":16722.0,"Objects":[{"StartTime":16722.0,"Position":402.0,"HyperDash":false},{"StartTime":16791.0,"Position":440.382324,"HyperDash":false},{"StartTime":16897.0,"Position":476.5204,"HyperDash":false}]},{"StartTime":17073.0,"Objects":[{"StartTime":17073.0,"Position":326.0,"HyperDash":false},{"StartTime":17142.0,"Position":279.617676,"HyperDash":false},{"StartTime":17248.0,"Position":251.479614,"HyperDash":false}]},{"StartTime":17423.0,"Objects":[{"StartTime":17423.0,"Position":125.0,"HyperDash":false},{"StartTime":17492.0,"Position":122.322762,"HyperDash":false},{"StartTime":17598.0,"Position":119.049225,"HyperDash":false}]},{"StartTime":17774.0,"Objects":[{"StartTime":17774.0,"Position":125.0,"HyperDash":false}]},{"StartTime":17862.0,"Objects":[{"StartTime":17862.0,"Position":125.0,"HyperDash":false}]},{"StartTime":17950.0,"Objects":[{"StartTime":17950.0,"Position":125.0,"HyperDash":false},{"StartTime":18019.0,"Position":142.158081,"HyperDash":false},{"StartTime":18125.0,"Position":198.3747,"HyperDash":false}]},{"StartTime":18301.0,"Objects":[{"StartTime":18301.0,"Position":245.0,"HyperDash":false},{"StartTime":18388.0,"Position":193.484589,"HyperDash":false},{"StartTime":18476.0,"Position":170.83812,"HyperDash":false},{"StartTime":18545.0,"Position":133.486023,"HyperDash":false},{"StartTime":18651.0,"Position":97.91507,"HyperDash":false}]},{"StartTime":18827.0,"Objects":[{"StartTime":18827.0,"Position":15.0,"HyperDash":false}]},{"StartTime":18915.0,"Objects":[{"StartTime":18915.0,"Position":15.0,"HyperDash":false}]},{"StartTime":19002.0,"Objects":[{"StartTime":19002.0,"Position":15.0,"HyperDash":false},{"StartTime":19071.0,"Position":21.7103615,"HyperDash":false},{"StartTime":19177.0,"Position":4.26349068,"HyperDash":false}]},{"StartTime":19353.0,"Objects":[{"StartTime":19353.0,"Position":0.0,"HyperDash":false}]},{"StartTime":19529.0,"Objects":[{"StartTime":19529.0,"Position":137.0,"HyperDash":false},{"StartTime":19598.0,"Position":169.483047,"HyperDash":false},{"StartTime":19704.0,"Position":210.398544,"HyperDash":false}]},{"StartTime":19880.0,"Objects":[{"StartTime":19880.0,"Position":328.0,"HyperDash":false},{"StartTime":19949.0,"Position":319.217133,"HyperDash":false},{"StartTime":20055.0,"Position":318.546051,"HyperDash":false}]},{"StartTime":20230.0,"Objects":[{"StartTime":20230.0,"Position":264.0,"HyperDash":false}]},{"StartTime":20318.0,"Objects":[{"StartTime":20318.0,"Position":264.0,"HyperDash":false}]},{"StartTime":20406.0,"Objects":[{"StartTime":20406.0,"Position":264.0,"HyperDash":false},{"StartTime":20493.0,"Position":295.147522,"HyperDash":false},{"StartTime":20581.0,"Position":330.866455,"HyperDash":false},{"StartTime":20650.0,"Position":359.710419,"HyperDash":false},{"StartTime":20756.0,"Position":396.14447,"HyperDash":false}]},{"StartTime":21108.0,"Objects":[{"StartTime":21108.0,"Position":412.0,"HyperDash":false},{"StartTime":21195.0,"Position":395.0836,"HyperDash":false},{"StartTime":21283.0,"Position":414.179626,"HyperDash":false},{"StartTime":21370.0,"Position":423.2632,"HyperDash":false},{"StartTime":21458.0,"Position":416.359283,"HyperDash":false},{"StartTime":21528.0,"Position":397.23114,"HyperDash":false},{"StartTime":21634.0,"Position":418.551361,"HyperDash":false}]},{"StartTime":21809.0,"Objects":[{"StartTime":21809.0,"Position":496.0,"HyperDash":false},{"StartTime":21896.0,"Position":491.214264,"HyperDash":false},{"StartTime":21984.0,"Position":496.431,"HyperDash":false},{"StartTime":22053.0,"Position":504.600922,"HyperDash":false},{"StartTime":22159.0,"Position":496.862,"HyperDash":false}]},{"StartTime":22336.0,"Objects":[{"StartTime":22336.0,"Position":499.0,"HyperDash":false}]},{"StartTime":22511.0,"Objects":[{"StartTime":22511.0,"Position":379.0,"HyperDash":false},{"StartTime":22598.0,"Position":360.8092,"HyperDash":false},{"StartTime":22686.0,"Position":345.0908,"HyperDash":false},{"StartTime":22773.0,"Position":309.7649,"HyperDash":false},{"StartTime":22861.0,"Position":307.9739,"HyperDash":false},{"StartTime":22931.0,"Position":312.241852,"HyperDash":false},{"StartTime":23037.0,"Position":271.985718,"HyperDash":false}]},{"StartTime":23213.0,"Objects":[{"StartTime":23213.0,"Position":322.0,"HyperDash":false},{"StartTime":23300.0,"Position":336.858,"HyperDash":false},{"StartTime":23388.0,"Position":327.7828,"HyperDash":false},{"StartTime":23457.0,"Position":329.661743,"HyperDash":false},{"StartTime":23563.0,"Position":317.734131,"HyperDash":false}]},{"StartTime":23739.0,"Objects":[{"StartTime":23739.0,"Position":240.0,"HyperDash":false}]},{"StartTime":23915.0,"Objects":[{"StartTime":23915.0,"Position":345.0,"HyperDash":false},{"StartTime":23984.0,"Position":381.55426,"HyperDash":false},{"StartTime":24090.0,"Position":419.956451,"HyperDash":false}]},{"StartTime":24266.0,"Objects":[{"StartTime":24266.0,"Position":283.0,"HyperDash":false}]},{"StartTime":24441.0,"Objects":[{"StartTime":24441.0,"Position":111.0,"HyperDash":false},{"StartTime":24510.0,"Position":97.44574,"HyperDash":false},{"StartTime":24616.0,"Position":36.04355,"HyperDash":false}]},{"StartTime":24792.0,"Objects":[{"StartTime":24792.0,"Position":173.0,"HyperDash":false}]},{"StartTime":24967.0,"Objects":[{"StartTime":24967.0,"Position":263.0,"HyperDash":false}]},{"StartTime":25055.0,"Objects":[{"StartTime":25055.0,"Position":280.0,"HyperDash":false}]},{"StartTime":25143.0,"Objects":[{"StartTime":25143.0,"Position":297.0,"HyperDash":false}]},{"StartTime":25230.0,"Objects":[{"StartTime":25230.0,"Position":314.0,"HyperDash":false}]},{"StartTime":25318.0,"Objects":[{"StartTime":25318.0,"Position":337.0,"HyperDash":false},{"StartTime":25376.0,"Position":334.666473,"HyperDash":false},{"StartTime":25434.0,"Position":337.0,"HyperDash":false},{"StartTime":25493.0,"Position":334.666473,"HyperDash":false},{"StartTime":25551.0,"Position":337.0,"HyperDash":false},{"StartTime":25610.0,"Position":334.666473,"HyperDash":false},{"StartTime":25668.0,"Position":337.0,"HyperDash":false}]},{"StartTime":25844.0,"Objects":[{"StartTime":25844.0,"Position":447.0,"HyperDash":false}]},{"StartTime":26020.0,"Objects":[{"StartTime":26020.0,"Position":436.0,"HyperDash":false}]},{"StartTime":26195.0,"Objects":[{"StartTime":26195.0,"Position":297.0,"HyperDash":false}]},{"StartTime":26546.0,"Objects":[{"StartTime":26546.0,"Position":297.0,"HyperDash":false},{"StartTime":26633.0,"Position":249.353119,"HyperDash":false},{"StartTime":26721.0,"Position":227.527557,"HyperDash":false},{"StartTime":26790.0,"Position":188.1133,"HyperDash":false},{"StartTime":26896.0,"Position":156.074387,"HyperDash":false}]},{"StartTime":27072.0,"Objects":[{"StartTime":27072.0,"Position":51.0,"HyperDash":false}]},{"StartTime":27247.0,"Objects":[{"StartTime":27247.0,"Position":185.0,"HyperDash":false},{"StartTime":27316.0,"Position":218.59346,"HyperDash":false},{"StartTime":27422.0,"Position":258.538177,"HyperDash":false}]},{"StartTime":27598.0,"Objects":[{"StartTime":27598.0,"Position":436.0,"HyperDash":false},{"StartTime":27667.0,"Position":416.406555,"HyperDash":false},{"StartTime":27773.0,"Position":362.461823,"HyperDash":false}]},{"StartTime":27949.0,"Objects":[{"StartTime":27949.0,"Position":151.0,"HyperDash":false},{"StartTime":28036.0,"Position":189.972488,"HyperDash":false},{"StartTime":28124.0,"Position":223.203812,"HyperDash":false},{"StartTime":28193.0,"Position":242.7229,"HyperDash":false},{"StartTime":28299.0,"Position":296.7707,"HyperDash":false}]},{"StartTime":28475.0,"Objects":[{"StartTime":28475.0,"Position":223.0,"HyperDash":false}]},{"StartTime":28651.0,"Objects":[{"StartTime":28651.0,"Position":296.0,"HyperDash":false},{"StartTime":28738.0,"Position":337.803925,"HyperDash":false},{"StartTime":28826.0,"Position":368.138336,"HyperDash":false},{"StartTime":28895.0,"Position":404.540863,"HyperDash":false},{"StartTime":29001.0,"Position":440.327179,"HyperDash":false}]},{"StartTime":29177.0,"Objects":[{"StartTime":29177.0,"Position":486.0,"HyperDash":false}]},{"StartTime":29353.0,"Objects":[{"StartTime":29353.0,"Position":366.0,"HyperDash":false},{"StartTime":29422.0,"Position":350.499329,"HyperDash":false},{"StartTime":29528.0,"Position":293.446533,"HyperDash":false}]},{"StartTime":29703.0,"Objects":[{"StartTime":29703.0,"Position":169.0,"HyperDash":false}]},{"StartTime":29879.0,"Objects":[{"StartTime":29879.0,"Position":245.0,"HyperDash":false}]},{"StartTime":30054.0,"Objects":[{"StartTime":30054.0,"Position":126.0,"HyperDash":false},{"StartTime":30123.0,"Position":155.500671,"HyperDash":false},{"StartTime":30229.0,"Position":198.553482,"HyperDash":false}]},{"StartTime":30404.0,"Objects":[{"StartTime":30404.0,"Position":323.0,"HyperDash":false}]},{"StartTime":30580.0,"Objects":[{"StartTime":30580.0,"Position":247.0,"HyperDash":false}]},{"StartTime":30756.0,"Objects":[{"StartTime":30756.0,"Position":349.0,"HyperDash":false},{"StartTime":30843.0,"Position":365.629761,"HyperDash":false},{"StartTime":30931.0,"Position":422.0551,"HyperDash":false},{"StartTime":31000.0,"Position":454.551147,"HyperDash":false},{"StartTime":31106.0,"Position":495.5697,"HyperDash":false}]},{"StartTime":31282.0,"Objects":[{"StartTime":31282.0,"Position":423.0,"HyperDash":false}]},{"StartTime":31458.0,"Objects":[{"StartTime":31458.0,"Position":323.0,"HyperDash":false},{"StartTime":31545.0,"Position":295.370239,"HyperDash":false},{"StartTime":31633.0,"Position":249.944885,"HyperDash":false},{"StartTime":31702.0,"Position":223.448853,"HyperDash":false},{"StartTime":31808.0,"Position":176.4303,"HyperDash":false}]},{"StartTime":31984.0,"Objects":[{"StartTime":31984.0,"Position":247.0,"HyperDash":false}]},{"StartTime":32160.0,"Objects":[{"StartTime":32160.0,"Position":99.0,"HyperDash":false},{"StartTime":32247.0,"Position":84.41518,"HyperDash":false},{"StartTime":32335.0,"Position":83.6537247,"HyperDash":false},{"StartTime":32404.0,"Position":71.69304,"HyperDash":false},{"StartTime":32510.0,"Position":108.235535,"HyperDash":false}]},{"StartTime":32686.0,"Objects":[{"StartTime":32686.0,"Position":164.0,"HyperDash":false}]},{"StartTime":32861.0,"Objects":[{"StartTime":32861.0,"Position":323.0,"HyperDash":false},{"StartTime":32930.0,"Position":362.799,"HyperDash":false},{"StartTime":33036.0,"Position":396.638153,"HyperDash":false}]},{"StartTime":33212.0,"Objects":[{"StartTime":33212.0,"Position":164.0,"HyperDash":false},{"StartTime":33281.0,"Position":116.200989,"HyperDash":false},{"StartTime":33387.0,"Position":90.36186,"HyperDash":false}]},{"StartTime":33563.0,"Objects":[{"StartTime":33563.0,"Position":323.0,"HyperDash":false},{"StartTime":33632.0,"Position":336.507568,"HyperDash":false},{"StartTime":33738.0,"Position":323.911469,"HyperDash":true}]},{"StartTime":33914.0,"Objects":[{"StartTime":33914.0,"Position":78.0,"HyperDash":false},{"StartTime":33983.0,"Position":82.492424,"HyperDash":false},{"StartTime":34089.0,"Position":77.08854,"HyperDash":false}]},{"StartTime":34265.0,"Objects":[{"StartTime":34265.0,"Position":234.0,"HyperDash":false},{"StartTime":34352.0,"Position":191.5233,"HyperDash":false},{"StartTime":34440.0,"Position":164.09671,"HyperDash":false},{"StartTime":34509.0,"Position":134.647873,"HyperDash":false},{"StartTime":34615.0,"Position":89.6628342,"HyperDash":false}]},{"StartTime":34791.0,"Objects":[{"StartTime":34791.0,"Position":148.0,"HyperDash":false}]},{"StartTime":34967.0,"Objects":[{"StartTime":34967.0,"Position":175.0,"HyperDash":false},{"StartTime":35054.0,"Position":199.913467,"HyperDash":false},{"StartTime":35142.0,"Position":201.876785,"HyperDash":false},{"StartTime":35211.0,"Position":207.491714,"HyperDash":false},{"StartTime":35317.0,"Position":181.816238,"HyperDash":false}]},{"StartTime":35493.0,"Objects":[{"StartTime":35493.0,"Position":94.0,"HyperDash":false}]},{"StartTime":35668.0,"Objects":[{"StartTime":35668.0,"Position":95.0,"HyperDash":false},{"StartTime":35755.0,"Position":137.9405,"HyperDash":false},{"StartTime":35843.0,"Position":163.627121,"HyperDash":false},{"StartTime":35912.0,"Position":197.017715,"HyperDash":false},{"StartTime":36018.0,"Position":234.539215,"HyperDash":false}]},{"StartTime":36195.0,"Objects":[{"StartTime":36195.0,"Position":319.0,"HyperDash":false}]},{"StartTime":36370.0,"Objects":[{"StartTime":36370.0,"Position":251.0,"HyperDash":false},{"StartTime":36457.0,"Position":231.0595,"HyperDash":false},{"StartTime":36545.0,"Position":182.372879,"HyperDash":false},{"StartTime":36614.0,"Position":153.982285,"HyperDash":false},{"StartTime":36720.0,"Position":111.460777,"HyperDash":false}]},{"StartTime":36896.0,"Objects":[{"StartTime":36896.0,"Position":175.0,"HyperDash":false}]},{"StartTime":37072.0,"Objects":[{"StartTime":37072.0,"Position":229.0,"HyperDash":false}]},{"StartTime":37160.0,"Objects":[{"StartTime":37160.0,"Position":245.0,"HyperDash":false}]},{"StartTime":37247.0,"Objects":[{"StartTime":37247.0,"Position":261.0,"HyperDash":false}]},{"StartTime":37335.0,"Objects":[{"StartTime":37335.0,"Position":277.0,"HyperDash":false}]},{"StartTime":37423.0,"Objects":[{"StartTime":37423.0,"Position":292.0,"HyperDash":false},{"StartTime":37492.0,"Position":308.471649,"HyperDash":false},{"StartTime":37598.0,"Position":366.746948,"HyperDash":false}]},{"StartTime":37774.0,"Objects":[{"StartTime":37774.0,"Position":491.0,"HyperDash":false}]},{"StartTime":38124.0,"Objects":[{"StartTime":38124.0,"Position":491.0,"HyperDash":false}]},{"StartTime":38300.0,"Objects":[{"StartTime":38300.0,"Position":422.0,"HyperDash":false}]},{"StartTime":38475.0,"Objects":[{"StartTime":38475.0,"Position":388.0,"HyperDash":false}]},{"StartTime":38826.0,"Objects":[{"StartTime":38826.0,"Position":388.0,"HyperDash":false}]},{"StartTime":39002.0,"Objects":[{"StartTime":39002.0,"Position":270.0,"HyperDash":false}]},{"StartTime":39177.0,"Objects":[{"StartTime":39177.0,"Position":305.0,"HyperDash":false},{"StartTime":39264.0,"Position":31.0,"HyperDash":false},{"StartTime":39352.0,"Position":421.0,"HyperDash":false},{"StartTime":39440.0,"Position":145.0,"HyperDash":false},{"StartTime":39528.0,"Position":318.0,"HyperDash":false},{"StartTime":39615.0,"Position":249.0,"HyperDash":false},{"StartTime":39703.0,"Position":147.0,"HyperDash":false},{"StartTime":39791.0,"Position":302.0,"HyperDash":false},{"StartTime":39879.0,"Position":212.0,"HyperDash":false},{"StartTime":39966.0,"Position":427.0,"HyperDash":false},{"StartTime":40054.0,"Position":116.0,"HyperDash":false},{"StartTime":40142.0,"Position":508.0,"HyperDash":false},{"StartTime":40230.0,"Position":417.0,"HyperDash":false},{"StartTime":40317.0,"Position":302.0,"HyperDash":false},{"StartTime":40405.0,"Position":132.0,"HyperDash":false},{"StartTime":40493.0,"Position":352.0,"HyperDash":false},{"StartTime":40581.0,"Position":174.0,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/871815.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/871815.osu new file mode 100644 index 0000000000..668c12fc0c --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/871815.osu @@ -0,0 +1,165 @@ +osu file format v14 + +[General] +StackLeniency: 0.3 +Mode: 0 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:6 +ApproachRate:8.3 +SliderMultiplier:1.5 +SliderTickRate:2 + +[Events] +//Background and Video events +//Break Periods +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples + +[TimingPoints] +1459,350.877192982456,4,2,1,45,1,0 +21108,-200,4,2,1,45,0,0 +23915,-100,4,2,1,45,0,0 +26546,350.877192982456,4,2,1,65,1,1 +40581,-100,4,2,1,45,0,0 + +[HitObjects] +150,114,1459,5,2,0:0:0:0: +150,114,1809,2,0,L|276:109,1,112.5,2|0,0:0|0:0,0:0:0:0: +281,109,2160,2,0,P|289:151|282:193,1,75,0|0,0:0|0:0,0:0:0:0: +272,298,2511,1,0,0:0:0:0: +367,70,2687,6,0,P|384:125|366:226,1,150,2|0,0:0|0:0,0:0:0:0: +278,289,3213,1,0,0:0:0:0: +113,221,3388,1,0,1:0:0:0: +116,235,3476,1,0,1:0:0:0: +121,248,3564,2,0,L|122:274,8,18.75,0|0|0|0|2|0|0|0|2,1:0|0:0|0:0|0:0|0:0|0:0|0:0|0:0|1:0,0:0:0:0: +368,70,4266,6,0,P|412:37|501:60,1,150,4|8,1:0|1:0,0:0:0:0: +440,119,4792,1,0,0:0:0:0: +289,84,4967,2,0,P|250:95|210:87,1,75,0|0,0:0|1:0,0:0:0:0: +105,24,5318,1,0,1:0:0:0: +119,191,5494,6,0,P|98:235|145:332,1,150,4|0,1:0|1:0,0:0:0:0: +192,253,6020,1,0,1:0:0:0: +451,314,6195,2,0,P|393:272|315:306,1,150,4|0,1:0|0:0,0:0:0:0: +380,360,6722,1,0,1:0:0:0: +334,189,6897,1,2,0:0:0:0: +334,189,6985,1,0,0:0:0:0: +334,189,7073,6,0,P|348:132|320:35,1,150,4|0,1:0|1:0,0:0:0:0: +281,256,7599,1,0,0:0:0:0: +140,171,7774,1,0,0:0:0:0: +274,290,7950,1,2,0:0:0:0: +138,135,8125,1,0,1:0:0:0: +266,321,8301,6,0,L|416:315,1,150,0|0,1:0|0:0,0:0:0:0: +512,307,8827,1,0,1:0:0:0: +490,150,9002,2,0,P|435:96|347:109,1,150,2|2,0:0|0:0,0:0:0:0: +260,59,9529,2,0,P|255:102|268:147,1,75,8|0,1:0|1:0,0:0:0:0: +267,164,9792,1,0,0:0:0:0: +267,164,9880,6,0,P|217:197|121:182,1,150,4|0,1:0|1:0,0:0:0:0: +185,106,10406,1,0,0:0:0:0: +177,283,10581,2,0,P|219:284|260:266,1,75,0|0,1:0|0:0,0:0:0:0: +352,225,10932,1,0,1:0:0:0: +436,132,11108,6,0,L|525:149,1,75,2|0,1:2|0:0,0:0:0:0: +368,30,11458,2,0,L|279:47,1,75,2|0,0:2|1:0,0:0:0:0: +181,124,11809,2,0,P|175:162|190:205,1,75,2|0,0:0|1:0,0:0:0:0: +221,325,12160,1,0,0:0:0:0: +221,325,12248,1,0,0:0:0:0: +221,325,12336,2,0,P|257:330|294:318,1,75,0|0,1:0|0:0,0:0:0:0: +440,318,12687,6,0,P|378:306|272:327,1,150,2|0,1:2|1:0,0:0:0:0: +330,209,13213,1,0,0:0:0:0: +330,209,13301,1,0,0:0:0:0: +330,209,13388,2,0,P|388:209|417:204,1,75,0|0,0:0|0:0,0:0:0:0: +494,149,13739,1,0,1:0:0:0: +321,99,13915,1,2,1:2:0:0: +321,99,14002,1,0,0:0:0:0: +321,99,14090,6,0,P|364:87|392:73,1,75,0|0,0:0|0:0,0:0:0:0: +231,160,14441,1,0,1:0:0:0: +188,259,14616,2,0,P|177:302|177:335,2,75,2|0|0,1:2|0:0|0:0,0:0:0:0: +125,87,15143,6,0,B|99:97|70:83|70:83|73:86|73:86|35:72|7:97,1,112.5,8|0,1:0|0:0,0:0:0:0: +17,99,15494,6,0,L|21:185,1,75,4|0,1:0|0:0,0:0:0:0: +29,282,15844,1,0,1:0:0:0: +130,334,16020,1,0,0:0:0:0: +130,334,16108,1,0,0:0:0:0: +130,334,16195,2,0,P|165:337|208:327,1,75,0|0,1:0|0:0,0:0:0:0: +287,251,16546,1,0,1:0:0:0: +402,165,16722,6,0,L|490:155,1,75,2|0,1:2|0:0,0:0:0:0: +326,67,17073,2,0,L|238:57,1,75,2|0,0:2|1:0,0:0:0:0: +125,41,17423,2,0,P|116:84|124:131,1,75,2|0,0:0|1:0,0:0:0:0: +125,238,17774,1,2,0:0:0:0: +125,238,17862,1,0,0:0:0:0: +125,238,17950,2,0,P|165:242|204:231,1,75,0|0,1:0|0:0,0:0:0:0: +245,344,18301,6,0,P|162:336|85:357,1,150,2|0,1:2|1:0,0:0:0:0: +15,271,18827,1,0,0:0:0:0: +15,271,18915,1,0,0:0:0:0: +15,271,19002,2,0,P|3:222|7:184,1,75,0|2,0:0|0:0,0:0:0:0: +0,85,19353,1,0,1:0:0:0: +137,68,19529,6,0,P|170:69|214:57,1,75,4|0,1:2|1:0,0:0:0:0: +328,191,19880,2,0,P|329:158|317:114,1,75,0|2,0:0|1:0,0:0:0:0: +264,261,20230,1,0,1:0:0:0: +264,261,20318,1,0,0:0:0:0: +264,261,20406,2,0,P|318:289|401:251,1,150,0|8,0:0|1:0,0:0:0:0: +412,245,21108,6,0,L|419:365,1,112.5,4|0,1:0|0:0,0:0:0:0: +496,259,21809,2,0,L|497:172,1,75,0|0,1:0|0:0,0:0:0:0: +499,82,22336,1,0,0:0:0:0: +379,42,22511,6,0,P|338:25|265:38,1,112.5,2|2,0:0|0:0,0:0:0:0: +322,179,23213,2,0,P|328:145|318:107,1,75,0|0,0:0|0:0,0:0:0:0: +240,150,23739,1,0,0:0:0:0: +345,271,23915,6,0,L|433:274,1,75,4|0,1:0|0:0,0:0:0:0: +283,331,24266,1,0,0:0:0:0: +111,275,24441,6,0,L|23:272,1,75,4|0,1:0|0:0,0:0:0:0: +173,215,24792,1,0,0:0:0:0: +263,127,24967,5,0,1:0:0:0: +280,119,25055,1,0,0:0:0:0: +297,112,25143,1,0,1:0:0:0: +314,105,25230,1,0,0:0:0:0: +337,95,25318,6,0,L|334:127,6,25,0|0|0|0|0|0|0,1:0|0:0|0:0|1:0|0:0|0:0|1:0,0:0:0:0: +447,46,25844,1,0,1:0:0:0: +436,197,26020,1,0,0:0:0:0: +297,263,26195,1,2,1:0:0:0: +297,263,26546,6,0,P|230:288|143:260,1,150,4|0,1:0|0:0,0:0:0:0: +51,182,27072,1,2,1:0:0:0: +185,111,27247,2,0,P|224:103|271:112,1,75,0|0,0:0|0:0,0:0:0:0: +436,197,27598,2,0,P|397:205|350:196,1,75,0|2,0:0|1:0,0:0:0:0: +151,269,27949,6,0,P|208:252|320:273,1,150,0|0,1:0|0:0,0:0:0:0: +223,342,28475,1,0,0:0:0:0: +296,262,28651,2,0,P|353:279|456:253,1,150,0|0,1:0|0:0,0:0:0:0: +486,133,29177,1,2,1:0:0:0: +366,52,29353,6,0,P|324:39|288:42,1,75,2|0,0:0|0:0,0:0:0:0: +169,61,29703,1,0,0:0:0:0: +245,149,29879,1,2,1:0:0:0: +126,258,30054,6,0,P|168:271|204:268,1,75,2|0,0:0|0:0,0:0:0:0: +323,249,30404,1,0,0:0:0:0: +247,161,30580,1,0,0:0:0:0: +349,54,30756,6,0,P|397:41|502:54,1,150,2|0,0:0|0:0,0:0:0:0: +423,138,31282,1,0,1:0:0:0: +323,249,31458,2,0,P|275:262|170:249,1,150,0|0,1:0|0:0,0:0:0:0: +247,161,31984,1,2,1:0:0:0: +99,42,32160,6,0,P|85:127|121:200,1,150,4|0,1:0|0:0,0:0:0:0: +164,309,32686,1,2,1:0:0:0: +323,249,32861,2,0,P|376:243|401:249,1,75,0|0,0:0|0:0,0:0:0:0: +164,309,33212,2,0,P|111:315|86:309,1,75,0|2,0:0|1:0,0:0:0:0: +323,249,33563,6,0,P|330:211|316:158,1,75,0|0,1:0|0:0,0:0:0:0: +78,57,33914,2,0,P|71:95|85:148,1,75,0|0,0:0|0:0,0:0:0:0: +234,300,34265,2,0,P|174:276|80:280,1,150,0|0,1:0|0:0,0:0:0:0: +148,364,34791,1,2,1:0:0:0: +175,186,34967,6,0,P|199:138|172:34,1,150,4|0,1:0|0:0,0:0:0:0: +94,115,35493,1,2,1:0:0:0: +95,260,35668,2,0,P|143:284|247:257,1,150,4|0,1:0|0:0,0:0:0:0: +319,199,36195,1,0,0:0:0:0: +251,89,36370,6,0,P|203:65|99:92,1,150,0|0,1:0|0:0,0:0:0:0: +175,186,36896,1,0,0:0:0:0: +229,329,37072,1,0,1:0:0:0: +245,337,37160,1,0,1:0:0:0: +261,345,37247,1,0,1:0:0:0: +277,353,37335,1,0,0:0:0:0: +292,361,37423,2,0,L|377:368,1,75,0|0,1:0|1:0,0:0:0:0: +491,315,37774,5,4,1:0:0:0: +491,315,38124,1,0,1:0:0:0: +422,209,38300,1,0,1:0:0:0: +388,68,38475,1,4,1:0:0:0: +388,68,38826,1,0,1:0:0:0: +270,153,39002,1,0,1:0:0:0: +256,192,39177,12,4,40581,1:0:0:0: diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/basic-expected-conversion.json similarity index 100% rename from osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json rename to osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/basic-expected-conversion.json diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-hyperdash-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/basic-hyperdash-expected-conversion.json similarity index 100% rename from osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-hyperdash-expected-conversion.json rename to osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/basic-hyperdash-expected-conversion.json diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-hyperdash.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/basic-hyperdash.osu similarity index 100% rename from osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-hyperdash.osu rename to osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/basic-hyperdash.osu diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/basic.osu similarity index 96% rename from osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic.osu rename to osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/basic.osu index 40b4409760..abd2ff2ee6 100644 --- a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic.osu +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/basic.osu @@ -1,27 +1,27 @@ -osu file format v14 - -[Difficulty] -HPDrainRate:6 -CircleSize:4 -OverallDifficulty:7 -ApproachRate:8.3 -SliderMultiplier:1.6 -SliderTickRate:1 - -[TimingPoints] -500,500,4,2,1,50,1,0 -13426,-100,4,3,1,45,0,0 -14884,-100,4,2,1,50,0,0 - -[HitObjects] -96,192,500,6,0,L|416:192,2,320 -256,192,3000,12,0,4000,0:0:0:0: -256,192,4500,12,0,5500,0:0:0:0: -256,192,6000,12,0,6500,0:0:0:0: -256,128,7000,6,0,L|352:128,4,80 -32,192,8500,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800 -256,192,11500,12,0,12000,0:0:0:0: -512,320,12500,6,0,B|0:256|0:256|512:96|512:96|256:32,1,1280 -256,256,17000,6,0,L|160:256,4,80 -256,192,18500,12,0,19450,0:0:0:0: -216,231,19875,6,0,B|216:135|280:135|344:135|344:199|344:263|248:327|248:327|120:327|120:327|56:39|408:39|408:39|472:150|408:342,1,1280 +osu file format v14 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:7 +ApproachRate:8.3 +SliderMultiplier:1.6 +SliderTickRate:1 + +[TimingPoints] +500,500,4,2,1,50,1,0 +13426,-100,4,3,1,45,0,0 +14884,-100,4,2,1,50,0,0 + +[HitObjects] +96,192,500,6,0,L|416:192,2,320 +256,192,3000,12,0,4000,0:0:0:0: +256,192,4500,12,0,5500,0:0:0:0: +256,192,6000,12,0,6500,0:0:0:0: +256,128,7000,6,0,L|352:128,4,80 +32,192,8500,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800 +256,192,11500,12,0,12000,0:0:0:0: +512,320,12500,6,0,B|0:256|0:256|512:96|512:96|256:32,1,1280 +256,256,17000,6,0,L|160:256,4,80 +256,192,18500,12,0,19450,0:0:0:0: +216,231,19875,6,0,B|216:135|280:135|344:135|344:199|344:263|248:327|248:327|120:327|120:327|56:39|408:39|408:39|472:150|408:342,1,1280 diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/diffcalc-test.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/diffcalc-test.osu similarity index 100% rename from osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/diffcalc-test.osu rename to osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/diffcalc-test.osu diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/hardrock-repeat-slider-expected-conversion.json similarity index 100% rename from osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider-expected-conversion.json rename to osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/hardrock-repeat-slider-expected-conversion.json diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/hardrock-repeat-slider.osu similarity index 100% rename from osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider.osu rename to osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/hardrock-repeat-slider.osu diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/hardrock-spinner-expected-conversion.json similarity index 100% rename from osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner-expected-conversion.json rename to osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/hardrock-spinner-expected-conversion.json diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/hardrock-spinner.osu similarity index 100% rename from osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner.osu rename to osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/hardrock-spinner.osu diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/hardrock-stream-expected-conversion.json similarity index 100% rename from osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream-expected-conversion.json rename to osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/hardrock-stream-expected-conversion.json diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/hardrock-stream.osu similarity index 100% rename from osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream.osu rename to osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/hardrock-stream.osu diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/pixel-jump-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/pixel-jump-expected-conversion.json new file mode 100644 index 0000000000..c9fbaf92a3 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/pixel-jump-expected-conversion.json @@ -0,0 +1,34 @@ +{ + "Mappings": [ + { + "StartTime": 23143.0, + "Objects": [ + { + "StartTime": 23143.0, + "Position": 307.0, + "HyperDash": false + }, + { + "StartTime": 23226.0, + "Position": 354.644958, + "HyperDash": false + } + ] + }, + { + "StartTime": 23310.0, + "Objects": [ + { + "StartTime": 23310.0, + "Position": 214.0, + "HyperDash": false + }, + { + "StartTime": 23393.0, + "Position": 154.841156, + "HyperDash": false + } + ] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/pixel-jump.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/pixel-jump.osu new file mode 100644 index 0000000000..6f470e77e5 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/pixel-jump.osu @@ -0,0 +1,23 @@ +osu file format v14 + +[General] +StackLeniency: 0.2 +Mode: 0 + +[Difficulty] +HPDrainRate:5.5 +CircleSize:4 +OverallDifficulty:8.6 +ApproachRate:9.4 +SliderMultiplier:2 +SliderTickRate:1 + +[TimingPoints] +310,333.333333333333,4,2,1,45,1,0 +23142,-83.3333333333333,4,2,1,70,0,0 +23225,-83.3333333333333,4,2,1,5,0,0 +23309,-83.3333333333333,4,2,1,75,0,0 + +[HitObjects] +307,184,23143,2,0,P|330:160|366:150,1,59.9999981689454,2|0,0:1|0:0,0:0:0:0: +214,335,23310,2,0,L|149:324,1,59.9999981689454,10|0,0:0|0:0,0:0:0:0: \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json similarity index 100% rename from osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json rename to osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/right-bound-hr-offset.osu similarity index 100% rename from osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset.osu rename to osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/right-bound-hr-offset.osu diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/slider-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/slider-expected-conversion.json similarity index 100% rename from osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/slider-expected-conversion.json rename to osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/slider-expected-conversion.json diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/slider.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/slider.osu similarity index 100% rename from osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/slider.osu rename to osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/slider.osu diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-and-circles-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/spinner-and-circles-expected-conversion.json similarity index 100% rename from osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-and-circles-expected-conversion.json rename to osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/spinner-and-circles-expected-conversion.json diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-and-circles.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/spinner-and-circles.osu similarity index 100% rename from osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-and-circles.osu rename to osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/spinner-and-circles.osu diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/spinner-expected-conversion.json similarity index 100% rename from osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner-expected-conversion.json rename to osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/spinner-expected-conversion.json diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/spinner-precision-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/spinner-precision-expected-conversion.json new file mode 100644 index 0000000000..95a0c8b34e --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/spinner-precision-expected-conversion.json @@ -0,0 +1,649 @@ +{ + "Mappings": [ + { + "StartTime": 276419.0, + "Objects": [ + { + "StartTime": 276419.0, + "Position": 65.0, + "HyperDash": false + }, + { + "StartTime": 276494.0, + "Position": 482.0, + "HyperDash": false + }, + { + "StartTime": 276569.0, + "Position": 164.0, + "HyperDash": false + }, + { + "StartTime": 276645.0, + "Position": 315.0, + "HyperDash": false + }, + { + "StartTime": 276720.0, + "Position": 145.0, + "HyperDash": false + }, + { + "StartTime": 276795.0, + "Position": 159.0, + "HyperDash": false + }, + { + "StartTime": 276871.0, + "Position": 310.0, + "HyperDash": false + }, + { + "StartTime": 276946.0, + "Position": 441.0, + "HyperDash": false + }, + { + "StartTime": 277021.0, + "Position": 428.0, + "HyperDash": false + }, + { + "StartTime": 277097.0, + "Position": 243.0, + "HyperDash": false + }, + { + "StartTime": 277172.0, + "Position": 422.0, + "HyperDash": false + }, + { + "StartTime": 277247.0, + "Position": 481.0, + "HyperDash": false + }, + { + "StartTime": 277323.0, + "Position": 104.0, + "HyperDash": false + }, + { + "StartTime": 277398.0, + "Position": 473.0, + "HyperDash": false + }, + { + "StartTime": 277473.0, + "Position": 135.0, + "HyperDash": false + }, + { + "StartTime": 277549.0, + "Position": 360.0, + "HyperDash": false + }, + { + "StartTime": 277624.0, + "Position": 123.0, + "HyperDash": false + }, + { + "StartTime": 277699.0, + "Position": 42.0, + "HyperDash": false + }, + { + "StartTime": 277775.0, + "Position": 393.0, + "HyperDash": false + }, + { + "StartTime": 277850.0, + "Position": 75.0, + "HyperDash": false + }, + { + "StartTime": 277925.0, + "Position": 377.0, + "HyperDash": false + }, + { + "StartTime": 278001.0, + "Position": 354.0, + "HyperDash": false + }, + { + "StartTime": 278076.0, + "Position": 287.0, + "HyperDash": false + }, + { + "StartTime": 278151.0, + "Position": 361.0, + "HyperDash": false + }, + { + "StartTime": 278227.0, + "Position": 479.0, + "HyperDash": false + }, + { + "StartTime": 278302.0, + "Position": 346.0, + "HyperDash": false + }, + { + "StartTime": 278377.0, + "Position": 266.0, + "HyperDash": false + }, + { + "StartTime": 278453.0, + "Position": 400.0, + "HyperDash": false + }, + { + "StartTime": 278528.0, + "Position": 202.0, + "HyperDash": false + }, + { + "StartTime": 278603.0, + "Position": 500.0, + "HyperDash": false + }, + { + "StartTime": 278679.0, + "Position": 80.0, + "HyperDash": false + }, + { + "StartTime": 278754.0, + "Position": 399.0, + "HyperDash": false + }, + { + "StartTime": 278830.0, + "Position": 455.0, + "HyperDash": false + }, + { + "StartTime": 278905.0, + "Position": 105.0, + "HyperDash": false + }, + { + "StartTime": 278980.0, + "Position": 100.0, + "HyperDash": false + }, + { + "StartTime": 279056.0, + "Position": 195.0, + "HyperDash": false + }, + { + "StartTime": 279131.0, + "Position": 106.0, + "HyperDash": false + }, + { + "StartTime": 279206.0, + "Position": 305.0, + "HyperDash": false + }, + { + "StartTime": 279282.0, + "Position": 225.0, + "HyperDash": false + }, + { + "StartTime": 279357.0, + "Position": 79.0, + "HyperDash": false + }, + { + "StartTime": 279432.0, + "Position": 38.0, + "HyperDash": false + }, + { + "StartTime": 279508.0, + "Position": 99.0, + "HyperDash": false + }, + { + "StartTime": 279583.0, + "Position": 79.0, + "HyperDash": false + }, + { + "StartTime": 279658.0, + "Position": 169.0, + "HyperDash": false + }, + { + "StartTime": 279734.0, + "Position": 238.0, + "HyperDash": false + }, + { + "StartTime": 279809.0, + "Position": 511.0, + "HyperDash": false + }, + { + "StartTime": 279884.0, + "Position": 58.0, + "HyperDash": false + }, + { + "StartTime": 279960.0, + "Position": 368.0, + "HyperDash": false + }, + { + "StartTime": 280035.0, + "Position": 52.0, + "HyperDash": false + }, + { + "StartTime": 280110.0, + "Position": 327.0, + "HyperDash": false + }, + { + "StartTime": 280186.0, + "Position": 226.0, + "HyperDash": false + }, + { + "StartTime": 280261.0, + "Position": 110.0, + "HyperDash": false + }, + { + "StartTime": 280336.0, + "Position": 3.0, + "HyperDash": false + }, + { + "StartTime": 280412.0, + "Position": 26.0, + "HyperDash": false + }, + { + "StartTime": 280487.0, + "Position": 173.0, + "HyperDash": false + }, + { + "StartTime": 280562.0, + "Position": 18.0, + "HyperDash": false + }, + { + "StartTime": 280638.0, + "Position": 310.0, + "HyperDash": false + }, + { + "StartTime": 280713.0, + "Position": 394.0, + "HyperDash": false + }, + { + "StartTime": 280788.0, + "Position": 406.0, + "HyperDash": false + }, + { + "StartTime": 280864.0, + "Position": 262.0, + "HyperDash": false + }, + { + "StartTime": 280939.0, + "Position": 278.0, + "HyperDash": false + }, + { + "StartTime": 281014.0, + "Position": 171.0, + "HyperDash": false + }, + { + "StartTime": 281090.0, + "Position": 22.0, + "HyperDash": false + }, + { + "StartTime": 281165.0, + "Position": 187.0, + "HyperDash": false + }, + { + "StartTime": 281241.0, + "Position": 124.0, + "HyperDash": false + }, + { + "StartTime": 281316.0, + "Position": 454.0, + "HyperDash": false + }, + { + "StartTime": 281391.0, + "Position": 16.0, + "HyperDash": false + }, + { + "StartTime": 281467.0, + "Position": 61.0, + "HyperDash": false + }, + { + "StartTime": 281542.0, + "Position": 161.0, + "HyperDash": false + }, + { + "StartTime": 281617.0, + "Position": 243.0, + "HyperDash": false + }, + { + "StartTime": 281693.0, + "Position": 375.0, + "HyperDash": false + }, + { + "StartTime": 281768.0, + "Position": 247.0, + "HyperDash": false + }, + { + "StartTime": 281843.0, + "Position": 162.0, + "HyperDash": false + }, + { + "StartTime": 281919.0, + "Position": 383.0, + "HyperDash": false + }, + { + "StartTime": 281994.0, + "Position": 127.0, + "HyperDash": false + }, + { + "StartTime": 282069.0, + "Position": 161.0, + "HyperDash": false + }, + { + "StartTime": 282145.0, + "Position": 332.0, + "HyperDash": false + }, + { + "StartTime": 282220.0, + "Position": 356.0, + "HyperDash": false + }, + { + "StartTime": 282295.0, + "Position": 362.0, + "HyperDash": false + }, + { + "StartTime": 282371.0, + "Position": 347.0, + "HyperDash": false + }, + { + "StartTime": 282446.0, + "Position": 252.0, + "HyperDash": false + }, + { + "StartTime": 282521.0, + "Position": 477.0, + "HyperDash": false + }, + { + "StartTime": 282597.0, + "Position": 358.0, + "HyperDash": false + }, + { + "StartTime": 282672.0, + "Position": 17.0, + "HyperDash": false + }, + { + "StartTime": 282747.0, + "Position": 399.0, + "HyperDash": false + }, + { + "StartTime": 282823.0, + "Position": 280.0, + "HyperDash": false + }, + { + "StartTime": 282898.0, + "Position": 304.0, + "HyperDash": false + }, + { + "StartTime": 282973.0, + "Position": 221.0, + "HyperDash": false + }, + { + "StartTime": 283049.0, + "Position": 407.0, + "HyperDash": false + }, + { + "StartTime": 283124.0, + "Position": 287.0, + "HyperDash": false + }, + { + "StartTime": 283199.0, + "Position": 135.0, + "HyperDash": false + }, + { + "StartTime": 283275.0, + "Position": 437.0, + "HyperDash": false + }, + { + "StartTime": 283350.0, + "Position": 289.0, + "HyperDash": false + }, + { + "StartTime": 283425.0, + "Position": 464.0, + "HyperDash": false + }, + { + "StartTime": 283501.0, + "Position": 36.0, + "HyperDash": false + }, + { + "StartTime": 283576.0, + "Position": 378.0, + "HyperDash": false + }, + { + "StartTime": 283652.0, + "Position": 297.0, + "HyperDash": false + }, + { + "StartTime": 283727.0, + "Position": 418.0, + "HyperDash": false + }, + { + "StartTime": 283802.0, + "Position": 329.0, + "HyperDash": false + }, + { + "StartTime": 283878.0, + "Position": 338.0, + "HyperDash": false + }, + { + "StartTime": 283953.0, + "Position": 394.0, + "HyperDash": false + }, + { + "StartTime": 284028.0, + "Position": 40.0, + "HyperDash": false + }, + { + "StartTime": 284104.0, + "Position": 13.0, + "HyperDash": false + }, + { + "StartTime": 284179.0, + "Position": 80.0, + "HyperDash": false + }, + { + "StartTime": 284254.0, + "Position": 138.0, + "HyperDash": false + }, + { + "StartTime": 284330.0, + "Position": 311.0, + "HyperDash": false + }, + { + "StartTime": 284405.0, + "Position": 216.0, + "HyperDash": false + }, + { + "StartTime": 284480.0, + "Position": 310.0, + "HyperDash": false + }, + { + "StartTime": 284556.0, + "Position": 397.0, + "HyperDash": false + }, + { + "StartTime": 284631.0, + "Position": 214.0, + "HyperDash": false + }, + { + "StartTime": 284706.0, + "Position": 505.0, + "HyperDash": false + }, + { + "StartTime": 284782.0, + "Position": 173.0, + "HyperDash": false + }, + { + "StartTime": 284857.0, + "Position": 295.0, + "HyperDash": false + }, + { + "StartTime": 284932.0, + "Position": 199.0, + "HyperDash": false + }, + { + "StartTime": 285008.0, + "Position": 494.0, + "HyperDash": false + }, + { + "StartTime": 285083.0, + "Position": 293.0, + "HyperDash": false + }, + { + "StartTime": 285158.0, + "Position": 115.0, + "HyperDash": false + }, + { + "StartTime": 285234.0, + "Position": 412.0, + "HyperDash": false + }, + { + "StartTime": 285309.0, + "Position": 506.0, + "HyperDash": false + }, + { + "StartTime": 285384.0, + "Position": 293.0, + "HyperDash": false + }, + { + "StartTime": 285460.0, + "Position": 346.0, + "HyperDash": false + }, + { + "StartTime": 285535.0, + "Position": 117.0, + "HyperDash": false + }, + { + "StartTime": 285610.0, + "Position": 285.0, + "HyperDash": false + }, + { + "StartTime": 285686.0, + "Position": 17.0, + "HyperDash": false + }, + { + "StartTime": 285761.0, + "Position": 238.0, + "HyperDash": false + }, + { + "StartTime": 285836.0, + "Position": 222.0, + "HyperDash": false + }, + { + "StartTime": 285912.0, + "Position": 450.0, + "HyperDash": false + }, + { + "StartTime": 285987.0, + "Position": 67.0, + "HyperDash": false + } + ] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/spinner-precision.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/spinner-precision.osu new file mode 100644 index 0000000000..2ba1fea357 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/spinner-precision.osu @@ -0,0 +1,20 @@ +osu file format v14 + +[General] +StackLeniency: 0.8 +Mode: 0 + +[Difficulty] +HPDrainRate:5 +CircleSize:3 +OverallDifficulty:8 +ApproachRate:9.2 +SliderMultiplier:1.7 +SliderTickRate:1 + +[TimingPoints] +276254,995.850622406639,4,2,1,70,1,0 +276254,-100,4,2,1,70,0,0 + +[HitObjects] +256,192,276419,12,4,286062,2:3:0:0: diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/spinner.osu similarity index 100% rename from osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/spinner.osu rename to osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/spinner.osu diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/tiny-ticks-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/tiny-ticks-expected-conversion.json new file mode 100644 index 0000000000..7a9e848a7b --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/tiny-ticks-expected-conversion.json @@ -0,0 +1,44 @@ +{ + "Mappings": [ + { + "StartTime": 27002.0, + "Objects": [ + { + "StartTime": 27002.0, + "Position": 326.0, + "HyperDash": false + }, + { + "StartTime": 27102.0, + "Position": 267.416656, + "HyperDash": false + }, + { + "StartTime": 27238.0, + "Position": 217.484329, + "HyperDash": false + } + ] + }, + { + "StartTime": 27318.0, + "Objects": [ + { + "StartTime": 27318.0, + "Position": 215.0, + "HyperDash": false + }, + { + "StartTime": 27418.0, + "Position": 251.682343, + "HyperDash": false + }, + { + "StartTime": 27554.0, + "Position": 323.347046, + "HyperDash": false + } + ] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/tiny-ticks.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/tiny-ticks.osu new file mode 100644 index 0000000000..7808bd0764 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/tiny-ticks.osu @@ -0,0 +1,21 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 +Mode: 0 + +[Difficulty] +HPDrainRate:4.7 +CircleSize:3.7 +OverallDifficulty:8.4 +ApproachRate:9 +SliderMultiplier:1.57 +SliderTickRate:1 + +[TimingPoints] +476,315.789473684211,4,2,1,50,1,0 +18160,-103.092783505155,4,2,1,70,0,0 + +[HitObjects] +326,119,27002,6,0,P|266:96|196:111,1,114.217502701372,14|0,0:2|0:0,0:0:0:0: +215,85,27318,2,0,P|271:80|323:102,1,114.217502701372,8|0,0:2|0:0,0:0:0:0: \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/v8-tick-distance-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/v8-tick-distance-expected-conversion.json new file mode 100644 index 0000000000..82167f37dd --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/v8-tick-distance-expected-conversion.json @@ -0,0 +1,54 @@ +{ + "Mappings": [ + { + "StartTime": 81593.0, + "Objects": [ + { + "StartTime": 81593.0, + "Position": 384.0, + "HyperDash": false + }, + { + "StartTime": 81652.0, + "Position": 377.608948, + "HyperDash": false + }, + { + "StartTime": 81712.0, + "Position": 390.3638, + "HyperDash": false + }, + { + "StartTime": 81772.0, + "Position": 407.118683, + "HyperDash": false + }, + { + "StartTime": 81832.0, + "Position": 433.873535, + "HyperDash": false + }, + { + "StartTime": 81891.0, + "Position": 444.482483, + "HyperDash": false + }, + { + "StartTime": 81951.0, + "Position": 437.237366, + "HyperDash": false + }, + { + "StartTime": 82011.0, + "Position": 443.992218, + "HyperDash": false + }, + { + "StartTime": 82107.0, + "Position": 459.0, + "HyperDash": false + } + ] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/v8-tick-distance.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/v8-tick-distance.osu new file mode 100644 index 0000000000..9fdba9dc0b --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/v8-tick-distance.osu @@ -0,0 +1,19 @@ +osu file format v7 + +[General] +StackLeniency: 0.7 +Mode: 0 + +[Difficulty] +HPDrainRate:5 +CircleSize:4 +OverallDifficulty:8 +SliderMultiplier:1 +SliderTickRate:1 + +[TimingPoints] +336,342.857142857143,4,1,0,100,1,0 +81588,-200,4,2,0,100,0,0 + +[HitObjects] +384,72,81593,2,12,B|464:72,1,75,4|4 \ No newline at end of file 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 3261fb656e..202f010680 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Tests beatmap.HitObjects.Add(new JuiceStream { X = CatchPlayfield.CENTER_X - width / 2, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(width, 0) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs index a44575a46e..825e8c697c 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Tests new JuiceStream { StartTime = 1000, - Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(0, -192) }), + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(0, -192) }), X = CatchPlayfield.WIDTH / 2 } } @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Catch.Tests Mod = new CatchModHidden(), PassCondition = () => Player.Results.Count > 0 && Player.ChildrenOfType().Single().Alpha > 0 - && Player.ChildrenOfType().Last().Alpha > 0 + && Player.ChildrenOfType().First().Alpha > 0 }); } 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/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs index 11d6419507..9c5cd68201 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Catch.Tests { X = xCoords, StartTime = playfieldTime + 1000, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(0, 200) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs index de3d9d6530..8e7f77285c 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -26,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(); }); } @@ -44,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; @@ -54,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); @@ -64,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/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 3c222662f5..3f26647f86 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Tests AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash); - for (int i = 0; i < 9; i++) + for (int i = 0; i < 11; i++) { int count = i + 1; AddUntilStep($"wait for hyperdash #{count}", () => hyperDashCount >= count); @@ -63,7 +63,11 @@ namespace osu.Game.Rulesets.Catch.Tests BeatmapInfo = { Ruleset = ruleset, - Difficulty = new BeatmapDifficulty { CircleSize = 3.6f } + Difficulty = new BeatmapDifficulty + { + CircleSize = 3.6f, + SliderMultiplier = 1, + }, } }; @@ -100,12 +104,22 @@ namespace osu.Game.Rulesets.Catch.Tests }) }, 1); + createObjects(() => new Fruit { X = right_x }, count: 2, spacing: 0, spacingAfterGroup: 400); + createObjects(() => new TestJuiceStream(left_x) + { + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero), + new PathControlPoint(new Vector2(0, 300)) + }) + }, count: 1, spacingAfterGroup: 150); + createObjects(() => new Fruit { X = left_x }, count: 1, spacing: 0, spacingAfterGroup: 400); + createObjects(() => new Fruit { X = right_x }, count: 2, spacing: 0); + return beatmap; - void createObjects(Func createObject, int count = 3) + void createObjects(Func createObject, int count = 3, float spacing = 140, float spacingAfterGroup = 700) { - const float spacing = 140; - for (int i = 0; i < count; i++) { var hitObject = createObject(); @@ -113,7 +127,7 @@ namespace osu.Game.Rulesets.Catch.Tests beatmap.HitObjects.Add(hitObject); } - startTime += 700; + startTime += spacingAfterGroup; } } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs index c31a7ca99f..9a923adaab 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Tests new JuiceStream { X = CatchPlayfield.CENTER_X, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(0, 100) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneOutOfBoundsObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneOutOfBoundsObjects.cs new file mode 100644 index 0000000000..951f5d1ca1 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneOutOfBoundsObjects.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.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public partial class TestSceneOutOfBoundsObjects : TestSceneCatchPlayer + { + protected override bool Autoplay => true; + + [Test] + public void TestNoOutOfBoundsObjects() + { + bool anyObjectOutOfBounds = false; + + AddStep("reset flag", () => anyObjectOutOfBounds = false); + + AddUntilStep("check for out-of-bounds objects", + () => + { + anyObjectOutOfBounds |= Player.ChildrenOfType().Any(dho => dho.X < 0 || dho.X > CatchPlayfield.WIDTH); + return Player.ScoreProcessor.HasCompleted.Value; + }); + + AddAssert("no out of bound objects found", () => !anyObjectOutOfBounds); + } + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + Ruleset = ruleset, + }, + HitObjects = new List + { + new Fruit { StartTime = 1000, X = -50 }, + new Fruit { StartTime = 1200, X = CatchPlayfield.WIDTH + 50 }, + new JuiceStream + { + StartTime = 1500, + X = 10, + Path = new SliderPath(PathType.LINEAR, new[] + { + Vector2.Zero, + new Vector2(-200, 0) + }) + }, + new JuiceStream + { + StartTime = 3000, + X = CatchPlayfield.WIDTH - 10, + Path = new SliderPath(PathType.LINEAR, new[] + { + Vector2.Zero, + new Vector2(200, 0) + }) + }, + } + }; + } +} diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs new file mode 100644 index 0000000000..9f667358db --- /dev/null +++ b/osu.Game.Rulesets.Catch.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 System.Collections.Generic; +using System.Linq; +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.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; +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(IReadOnlyList selectedMods) + => new ScoreV1(selectedMods) { ScoreMultiplier = { BindTarget = scoreMultiplier } }; + + protected override IScoringAlgorithm CreateScoreV2(int maxCombo, IReadOnlyList selectedMods) + => new ScoreV2(maxCombo, selectedMods); + + protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode, IReadOnlyList selectedMods) + => new CatchProcessorBasedScoringAlgorithm(beatmap, mode, selectedMods); + + [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 readonly double modMultiplier; + + public BindableDouble ScoreMultiplier { get; } = new BindableDouble(); + + private int currentCombo; + + public ScoreV1(IReadOnlyList selectedMods) + { + var ruleset = new CatchRuleset(); + modMultiplier = ruleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(selectedMods, new LegacyBeatmapConversionDifficultyInfo + { + SourceRuleset = ruleset.RulesetInfo + }); + } + + 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 * modMultiplier))); + + currentCombo++; + } + + public long TotalScore { get; private set; } + } + + private class ScoreV2 : IScoringAlgorithm + { + private int currentCombo; + private double comboPortion; + + private readonly double modMultiplier; + + private readonly double comboPortionMax; + + private const double combo_base = 4; + private const int combo_cap = 200; + + public ScoreV2(int maxCombo, IReadOnlyList selectedMods) + { + var ruleset = new CatchRuleset(); + modMultiplier = ruleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier( + selectedMods.Append(new ModScoreV2()).ToList(), + new LegacyBeatmapConversionDifficultyInfo + { + SourceRuleset = ruleset.RulesetInfo + }); + + 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) * modMultiplier); // vast simplification, as we're not doing ticks here. + } + + private class CatchProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm + { + public CatchProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode, IReadOnlyList selectedMods) + : base(beatmap, mode, selectedMods) + { + } + + 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..0a77845343 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/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs index f009c10a9c..1f05d66b86 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Catch.Beatmaps { @@ -38,5 +39,25 @@ namespace osu.Game.Rulesets.Catch.Beatmaps } }; } + + /// + /// Enumerate all s, sorted by their start times. + /// + /// + /// If multiple objects have the same start time, the ordering is preserved (it is a stable sorting). + /// + public static IEnumerable GetPalpableObjects(IEnumerable hitObjects) + { + return hitObjects.SelectMany(selectPalpableObjects).OrderBy(h => h.StartTime); + + IEnumerable selectPalpableObjects(HitObject h) + { + if (h is PalpableCatchHitObject palpable) + yield return palpable; + + foreach (var nested in h.NestedHitObjects.OfType()) + yield return nested; + } + } } } diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 2c8ef9eae0..8c460586b0 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -9,6 +9,7 @@ using System.Threading; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Beatmaps.Legacy; namespace osu.Game.Rulesets.Catch.Beatmaps { @@ -41,9 +42,11 @@ 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 + // 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 ()) + { + if (obj is not BananaShower && (lastObj == null || lastObj is BananaShower)) + obj.NewCombo = true; + lastObj = obj; + } + + base.PreProcess(); + } + public override void PostProcess() { base.PostProcess(); @@ -192,24 +207,9 @@ namespace osu.Game.Rulesets.Catch.Beatmaps private static void initialiseHyperDash(IBeatmap beatmap) { - List palpableObjects = new List(); - - foreach (var currentObject in beatmap.HitObjects) - { - if (currentObject is Fruit fruitObject) - palpableObjects.Add(fruitObject); - - if (currentObject is JuiceStream) - { - foreach (var juice in currentObject.NestedHitObjects) - { - if (juice is PalpableCatchHitObject palpableObject && !(juice is TinyDroplet)) - palpableObjects.Add(palpableObject); - } - } - } - - palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); + var palpableObjects = CatchBeatmap.GetPalpableObjects(beatmap.HitObjects) + .Where(h => h is Fruit || (h is Droplet && h is not TinyDroplet)) + .ToArray(); double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) / 2; @@ -221,7 +221,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps int lastDirection = 0; double lastExcess = halfCatcherWidth; - for (int i = 0; i < palpableObjects.Count - 1; i++) + for (int i = 0; i < palpableObjects.Length - 1; i++) { var currentObject = palpableObjects[i]; var nextObject = palpableObjects[i + 1]; @@ -231,7 +231,9 @@ namespace osu.Game.Rulesets.Catch.Beatmaps currentObject.DistanceToHyperDash = 0; int thisDirection = nextObject.EffectiveX > currentObject.EffectiveX ? 1 : -1; - double timeToNext = nextObject.StartTime - currentObject.StartTime - 1000f / 60f / 4; // 1/4th of a frame of grace time, taken from osu-stable + + // Int truncation added to match osu!stable. + double timeToNext = (int)nextObject.StartTime - (int)currentObject.StartTime - 1000f / 60f / 4; // 1/4th of a frame of grace time, taken from osu-stable double distanceToNext = Math.Abs(nextObject.EffectiveX - currentObject.EffectiveX) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth); float distanceToHyper = (float)(timeToNext * Catcher.BASE_DASH_SPEED - distanceToNext); diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index bd6b857fe8..72d1a161dd 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty; using osu.Game.Rulesets.Catch.Edit; using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Catch.Scoring; using osu.Game.Rulesets.Catch.Skinning.Argon; @@ -25,6 +26,7 @@ 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; @@ -38,6 +40,8 @@ namespace osu.Game.Rulesets.Catch public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(); + public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new CatchHealthProcessor(drainStartTime); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap, this); public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new CatchBeatmapProcessor(beatmap); @@ -53,7 +57,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) @@ -232,5 +236,17 @@ namespace osu.Game.Rulesets.Catch }), }; } + + /// + public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate) + { + BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty); + + double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN); + preempt /= rate; + adjustedDifficulty.ApproachRate = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN); + + return adjustedDifficulty; + } } } diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 0b56405299..f12c41a415 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; using osu.Game.Rulesets.Catch.Difficulty.Skills; using osu.Game.Rulesets.Catch.Mods; @@ -25,12 +26,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty public override int Version => 20220701; - private readonly IWorkingBeatmap workingBeatmap; - public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { - workingBeatmap = beatmap; } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) @@ -49,15 +47,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)), }; - if (ComputeLegacyScoringValues) - { - CatchLegacyScoreSimulator sv1Simulator = new CatchLegacyScoreSimulator(); - sv1Simulator.Simulate(workingBeatmap, beatmap, mods); - attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore; - attributes.LegacyComboScore = sv1Simulator.ComboScore; - attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio; - } - return attributes; } @@ -68,13 +57,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty List objects = new List(); // In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream. - foreach (var hitObject in beatmap.HitObjects - .SelectMany(obj => obj is JuiceStream stream ? stream.NestedHitObjects.AsEnumerable() : new[] { obj }) - .Cast() - .OrderBy(x => x.StartTime)) + foreach (var hitObject in CatchBeatmap.GetPalpableObjects(beatmap.HitObjects)) { // We want to only consider fruits that contribute to the combo. - if (hitObject is BananaShower || hitObject is TinyDroplet) + if (hitObject is Banana || hitObject is TinyDroplet) continue; if (lastObject != null) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs index c79fd36d96..f931795ff2 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs @@ -5,30 +5,29 @@ 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.Catch.Scoring; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Legacy; 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 { - public int AccuracyScore { get; private set; } - - public int ComboScore { get; private set; } - - public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore; + private readonly ScoreProcessor scoreProcessor = new CatchScoreProcessor(); private int legacyBonusScore; - private int modernBonusScore; + private int standardisedBonusScore; private int combo; private double scoreMultiplier; - public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap) { IBeatmap baseBeatmap = workingBeatmap.Beatmap; @@ -64,19 +63,21 @@ namespace osu.Game.Rulesets.Catch.Difficulty 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 = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength); - scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + LegacyScoreAttributes attributes = new LegacyScoreAttributes(); foreach (var obj in playableBeatmap.HitObjects) - simulateHit(obj); + simulateHit(obj, ref attributes); + + attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore; + attributes.BonusScore = legacyBonusScore; + attributes.MaxCombo = combo; + + return attributes; } - private void simulateHit(HitObject hitObject) + private void simulateHit(HitObject hitObject, ref LegacyScoreAttributes attributes) { bool increaseCombo = true; bool addScoreComboMultiplier = false; @@ -112,31 +113,79 @@ namespace osu.Game.Rulesets.Catch.Difficulty case JuiceStream: foreach (var nested in hitObject.NestedHitObjects) - simulateHit(nested); + simulateHit(nested, ref attributes); return; case BananaShower: foreach (var nested in hitObject.NestedHitObjects) - simulateHit(nested); + simulateHit(nested, ref attributes); return; } if (addScoreComboMultiplier) { // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) - ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier)); + attributes.ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier)); } if (isBonus) { legacyBonusScore += scoreIncrease; - modernBonusScore += Judgement.ToNumericResult(bonusResult); + standardisedBonusScore += scoreProcessor.GetBaseScoreForResult(bonusResult); } else - AccuracyScore += scoreIncrease; + 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/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs index 1e63d32c41..6902f78172 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; @@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints private double placementStartTime; private double placementEndTime; - protected override bool IsValidForPlacement => HitObject.Duration > 0; + protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0); public BananaShowerPlacementBlueprint() { diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs index 7a577f8a83..86f92d16ca 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs @@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components path.ConvertFromSliderPath(sliderPath, hitObject.Velocity); // If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices. - if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear)) + if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.LINEAR)) { path.ResampleVertices(hitObject.NestedHitObjects .Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used. @@ -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/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs index 9e50b5a80f..c8c8db1ebd 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; @@ -24,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints private InputManager inputManager = null!; - protected override bool IsValidForPlacement => HitObject.Duration > 0; + protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0); public JuiceStreamPlacementBlueprint() { 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 dd6757eac9..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.PlayfieldAdjustmentContainer.Anchor = Anchor.Centre; - drawableRuleset.PlayfieldAdjustmentContainer.Origin = Anchor.Centre; - 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/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs index b05c8e5f77..328cc2b52a 100644 --- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs @@ -23,29 +23,30 @@ namespace osu.Game.Rulesets.Catch.Objects private void createBananas(CancellationToken cancellationToken) { - double spacing = Duration; + // Int truncation added to match osu!stable. + int startTime = (int)StartTime; + int endTime = (int)EndTime; + float spacing = (float)(EndTime - StartTime); while (spacing > 100) spacing /= 2; if (spacing <= 0) return; - double time = StartTime; - int i = 0; + int count = 0; - while (time <= EndTime) + for (float time = startTime; time <= endTime; time += spacing) { cancellationToken.ThrowIfCancellationRequested(); AddNested(new Banana { StartTime = time, - BananaIndex = i, + BananaIndex = count, Samples = new List { new Banana.BananaHitSampleInfo(CreateHitSampleInfo().Volume) } }); - time += spacing; - i++; + count++; } } diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index f4bd515995..52c42dfddb 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; @@ -149,9 +150,36 @@ namespace osu.Game.Rulesets.Catch.Objects { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); + TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN); - Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2; + Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize); + } + + public void UpdateComboInformation(IHasComboInformation? lastObj) + { + // Note that this implementation is shared with the osu! ruleset's implementation. + // If a change is made here, OsuHitObject.cs should also be updated. + ComboIndex = lastObj?.ComboIndex ?? 0; + ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; + IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; + + if (this is BananaShower) + { + // For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. + return; + } + + // At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo, + // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. + if (NewCombo || lastObj == null || lastObj is BananaShower) + { + IndexInCurrentCombo = 0; + ComboIndex++; + ComboIndexWithOffsets += ComboOffset + 1; + + if (lastObj != null) + lastObj.LastInCombo = true; + } } protected override HitWindows CreateHitWindows() => HitWindows.Empty; @@ -161,6 +189,21 @@ namespace osu.Game.Rulesets.Catch.Objects // The half of the height of the osu! playfield. public const float DEFAULT_LEGACY_CONVERT_Y = 192; + /// + /// Minimum preempt time at AR=10. + /// + public const double PREEMPT_MIN = 450; + + /// + /// Median preempt time at AR=5. + /// + public const double PREEMPT_MID = 1200; + + /// + /// Maximum preempt time at AR=0. + /// + public const double PREEMPT_MAX = 1800; + /// /// The Y position of the hit object is not used in the normal osu!catch gameplay. /// It is preserved to maximize the backward compatibility with the legacy editor, in which the mappers use the Y position to organize the patterns. diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs index 03adbce885..9ee4a15182 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables RelativeSizeAxes = Axes.X; Origin = Anchor.BottomLeft; - AddInternal(bananaContainer = new Container { RelativeSizeAxes = Axes.Both }); + AddInternal(bananaContainer = new NestedFruitContainer { RelativeSizeAxes = Axes.Both }); } protected override void AddNestedHitObject(DrawableHitObject hitObject) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs index 41ecf59276..677b61df47 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables RelativeSizeAxes = Axes.X; Origin = Anchor.BottomLeft; - AddInternal(dropletContainer = new Container { RelativeSizeAxes = Axes.Both, }); + AddInternal(dropletContainer = new NestedFruitContainer { RelativeSizeAxes = Axes.Both, }); } protected override void AddNestedHitObject(DrawableHitObject hitObject) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index 4a9661f108..ade00918ab 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -1,10 +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; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Catch.UI; using osuTK; using osuTK.Graphics; @@ -70,7 +72,10 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables private void updateXPosition(ValueChangedEvent _) { - X = OriginalXBindable.Value + XOffsetBindable.Value; + // same as `CatchHitObject.EffectiveX`. + // not using that property directly to support scenarios where `HitObject` may not necessarily be present + // for this pooled drawable. + X = Math.Clamp(OriginalXBindable.Value + XOffsetBindable.Value, 0, CatchPlayfield.WIDTH); } protected override void OnApply() diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/NestedFruitContainer.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/NestedFruitContainer.cs new file mode 100644 index 0000000000..90bdb0237e --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/NestedFruitContainer.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 osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables +{ + public partial class NestedFruitContainer : Container + { + /// + /// This comparison logic is a copy of comparison logic, + /// which can't be easily extracted to a more common place. + /// + /// + protected override int Compare(Drawable x, Drawable y) + { + if (x is not DrawableCatchHitObject xObj || y is not DrawableCatchHitObject yObj) + return base.Compare(x, y); + + int result = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime); + return result == 0 ? CompareReverseChildID(x, y) : result; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index d9bbbedfcf..cb9e694b8d 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -10,7 +10,6 @@ using osu.Framework.Bindables; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -28,19 +27,25 @@ 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; } + /// + /// An extra multiplier that affects the number of s generated by this . + /// An increase in this value increases , which reduces the number of ticks generated. + /// + public double TickDistanceMultiplier = 1; + [JsonIgnore] private double velocityFactor; @@ -48,10 +53,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 * TickDistanceMultiplier; /// /// The length of one span of this . @@ -79,12 +84,12 @@ 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) { - double sinceLastTick = e.Time - lastEvent.Value.Time; + double sinceLastTick = (int)e.Time - (int)lastEvent.Value.Time; if (sinceLastTick > 80) { @@ -99,15 +104,14 @@ namespace osu.Game.Rulesets.Catch.Objects AddNested(new TinyDroplet { StartTime = t + lastEvent.Value.Time, - X = ClampToPlayfield(EffectiveX + Path.PositionAt( - lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X), + X = EffectiveX + Path.PositionAt(lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X, }); } } } - // 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) @@ -117,7 +121,7 @@ namespace osu.Game.Rulesets.Catch.Objects { Samples = dropletSamples, StartTime = e.Time, - X = ClampToPlayfield(EffectiveX + Path.PositionAt(e.PathProgress).X), + X = EffectiveX + Path.PositionAt(e.PathProgress).X, }); break; @@ -128,16 +132,14 @@ namespace osu.Game.Rulesets.Catch.Objects { Samples = this.GetNodeSamples(nodeIndex++), StartTime = e.Time, - X = ClampToPlayfield(EffectiveX + Path.PositionAt(e.PathProgress).X), + X = EffectiveX + Path.PositionAt(e.PathProgress).X, }); break; } } } - public float EndX => ClampToPlayfield(EffectiveX + this.CurvePositionAt(1).X); - - public float ClampToPlayfield(float value) => Math.Clamp(value, 0, CatchPlayfield.WIDTH); + public float EndX => EffectiveX + this.CurvePositionAt(1).X; [JsonIgnore] public double Duration @@ -164,7 +166,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/Objects/JuiceStreamPath.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs index 0633151ddd..57acf7cee2 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs @@ -236,7 +236,7 @@ namespace osu.Game.Rulesets.Catch.Objects for (int i = 1; i < vertices.Count; i++) { - sliderPath.ControlPoints[^1].Type = PathType.Linear; + sliderPath.ControlPoints[^1].Type = PathType.LINEAR; float deltaX = vertices[i].X - lastPosition.X; double length = (vertices[i].Time - currentTime) * velocity; diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs new file mode 100644 index 0000000000..c3cc488941 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs @@ -0,0 +1,57 @@ +// 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.Catch.Objects; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Catch.Scoring +{ + public partial class CatchHealthProcessor : LegacyDrainingHealthProcessor + { + public CatchHealthProcessor(double drainStartTime) + : base(drainStartTime) + { + } + + protected override IEnumerable EnumerateTopLevelHitObjects() => EnumerateHitObjects(Beatmap).Where(h => h is Fruit || h is Droplet || h is Banana); + + protected override IEnumerable EnumerateNestedHitObjects(HitObject hitObject) => Enumerable.Empty(); + + protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result) + { + double increase = 0; + + switch (result) + { + case HitResult.SmallTickMiss: + return 0; + + case HitResult.LargeTickMiss: + case HitResult.Miss: + return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.03, -0.125, -0.2); + + case HitResult.SmallTickHit: + increase = 0.0015; + break; + + case HitResult.LargeTickHit: + increase = 0.015; + break; + + case HitResult.Great: + increase = 0.03; + break; + + case HitResult.LargeBonus: + increase = 0.0025; + break; + } + + return HpMultiplierNormal * increase; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 9323296b7f..12a4182bf1 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -2,29 +2,136 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Catch.Scoring { public partial class CatchScoreProcessor : ScoreProcessor { + private const double accuracy_cutoff_x = 1; + private const double accuracy_cutoff_s = 0.98; + private const double accuracy_cutoff_a = 0.94; + private const double accuracy_cutoff_b = 0.9; + private const double accuracy_cutoff_c = 0.85; + private const double accuracy_cutoff_d = 0; + private const int combo_cap = 200; private const double combo_base = 4; + private double fruitTinyScale; + public CatchScoreProcessor() : base(new CatchRuleset()) { } + protected override void Reset(bool storeResults) + { + base.Reset(storeResults); + + // large ticks are *purposefully* not counted to match stable + int fruitTinyScaleDivisor = MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) + MaximumResultCounts.GetValueOrDefault(HitResult.Great); + fruitTinyScale = fruitTinyScaleDivisor == 0 + ? 0 + : (double)MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) / fruitTinyScaleDivisor; + } + protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) { - return 600000 * comboProgress - + 400000 * Accuracy.Value * accuracyProgress + const int max_tiny_droplets_portion = 400000; + + double comboPortion = 1000000 - max_tiny_droplets_portion + max_tiny_droplets_portion * (1 - fruitTinyScale); + double dropletsPortion = max_tiny_droplets_portion * fruitTinyScale; + double dropletsHit = MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) == 0 + ? 0 + : (double)ScoreResultCounts.GetValueOrDefault(HitResult.SmallTickHit) / MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit); + + return comboPortion * comboProgress + + dropletsPortion * dropletsHit + bonusPortion; } + public override int GetBaseScoreForResult(HitResult result) + { + switch (result) + { + // dirty hack to emulate accuracy on stable weighting every object equally in accuracy portion + case HitResult.Great: + case HitResult.LargeTickHit: + case HitResult.SmallTickHit: + return 300; + + case HitResult.LargeBonus: + return 200; + } + + return base.GetBaseScoreForResult(result); + } + protected override double GetComboScoreChange(JudgementResult result) - => Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base)); + { + double baseIncrease = 0; + + switch (result.Type) + { + case HitResult.Great: + baseIncrease = 300; + break; + + case HitResult.LargeTickHit: + baseIncrease = 100; + break; + } + + return baseIncrease * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base)); + } + + public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary results) + { + if (accuracy == accuracy_cutoff_x) + return ScoreRank.X; + if (accuracy >= accuracy_cutoff_s) + return ScoreRank.S; + if (accuracy >= accuracy_cutoff_a) + return ScoreRank.A; + if (accuracy >= accuracy_cutoff_b) + return ScoreRank.B; + if (accuracy >= accuracy_cutoff_c) + return ScoreRank.C; + + return ScoreRank.D; + } + + public override double AccuracyCutoffFromRank(ScoreRank rank) + { + switch (rank) + { + case ScoreRank.X: + case ScoreRank.XH: + return accuracy_cutoff_x; + + case ScoreRank.S: + case ScoreRank.SH: + return accuracy_cutoff_s; + + case ScoreRank.A: + return accuracy_cutoff_a; + + case ScoreRank.B: + return accuracy_cutoff_b; + + case ScoreRank.C: + return accuracy_cutoff_c; + + case ScoreRank.D: + return accuracy_cutoff_d; + + default: + throw new ArgumentOutOfRangeException(nameof(rank), rank, null); + } + } } } 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/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..147850a9b7 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; /// @@ -118,6 +126,7 @@ namespace osu.Game.Rulesets.Catch.UI private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR; + private double? lastHyperDashStartTime; private double hyperDashModifier = 1; private int hyperDashDirection; private float hyperDashTargetPosition; @@ -175,11 +184,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. /// @@ -230,16 +234,23 @@ namespace osu.Game.Rulesets.Catch.UI // droplet doesn't affect the catcher state if (hitObject is TinyDroplet) return; - if (result.IsHit && hitObject.HyperDashTarget is CatchHitObject target) + // if a hyper fruit was already handled this frame, just go where it says to go. + // this special-cases some aspire maps that have doubled-up objects (one hyper, one not) at the same time instant. + // handling this "properly" elsewhere is impossible as there is no feasible way to ensure + // that the hyperfruit gets judged second (especially if it coincides with a last fruit in a juice stream). + if (lastHyperDashStartTime != Time.Current) { - double timeDifference = target.StartTime - hitObject.StartTime; - double positionDifference = target.EffectiveX - X; - double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0); + if (result.IsHit && hitObject.HyperDashTarget is CatchHitObject target) + { + double timeDifference = target.StartTime - hitObject.StartTime; + double positionDifference = target.EffectiveX - X; + double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0); - SetHyperDashState(Math.Abs(velocity) / BASE_DASH_SPEED, target.EffectiveX); + SetHyperDashState(Math.Abs(velocity) / BASE_DASH_SPEED, target.EffectiveX); + } + else + SetHyperDashState(); } - else - SetHyperDashState(); if (result.IsHit) CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle; @@ -289,6 +300,8 @@ namespace osu.Game.Rulesets.Catch.UI if (wasHyperDashing) runHyperDashStateTransition(false); + + lastHyperDashStartTime = null; } else { @@ -298,6 +311,8 @@ namespace osu.Game.Rulesets.Catch.UI if (!wasHyperDashing) runHyperDashStateTransition(true); + + lastHyperDashStartTime = Time.Current; } } @@ -464,6 +479,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/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/Editor/TestSceneOpenEditorTimestampInMania.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneOpenEditorTimestampInMania.cs new file mode 100644 index 0000000000..05c881d284 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneOpenEditorTimestampInMania.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 NUnit.Framework; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests.Editor +{ + public partial class TestSceneOpenEditorTimestampInMania : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new ManiaRuleset(); + + [Test] + public void TestNormalSelection() + { + addStepClickLink("00:05:920 (5920|3,6623|3,6857|2,7326|1)"); + AddAssert("selected group", () => checkSnapAndSelectColumn(5_920, new List<(int, int)> + { (5_920, 3), (6_623, 3), (6_857, 2), (7_326, 1) } + )); + + addReset(); + addStepClickLink("00:42:716 (42716|3,43420|2,44123|0,44357|1,45295|1)"); + AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(42_716, new List<(int, int)> + { (42_716, 3), (43_420, 2), (44_123, 0), (44_357, 1), (45_295, 1) } + )); + + addReset(); + AddStep("add notes to row", () => + { + if (EditorBeatmap.HitObjects.Any(x => x is ManiaHitObject m && m.StartTime == 11_545 && m.Column is 1 or 2 or 3)) + return; + + ManiaHitObject first = (ManiaHitObject)EditorBeatmap.HitObjects.First(x => x is ManiaHitObject m && m.StartTime == 11_545 && m.Column == 0); + ManiaHitObject second = new Note { Column = 1, StartTime = first.StartTime }; + ManiaHitObject third = new Note { Column = 2, StartTime = first.StartTime }; + ManiaHitObject forth = new Note { Column = 3, StartTime = first.StartTime }; + EditorBeatmap.AddRange(new[] { second, third, forth }); + }); + addStepClickLink("00:11:545 (11545|0,11545|1,11545|2,11545|3)"); + AddAssert("selected in row", () => checkSnapAndSelectColumn(11_545, new List<(int, int)> + { (11_545, 0), (11_545, 1), (11_545, 2), (11_545, 3) } + )); + + addReset(); + addStepClickLink("01:36:623 (96623|1,97560|1,97677|1,97795|1,98966|1)"); + AddAssert("selected in column", () => checkSnapAndSelectColumn(96_623, new List<(int, int)> + { (96_623, 1), (97_560, 1), (97_677, 1), (97_795, 1), (98_966, 1) } + )); + } + + [Test] + public void TestUnusualSelection() + { + addStepClickLink("00:00:000 (0|1)", "wrong offset"); + AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170)); + + addReset(); + addStepClickLink("00:00:000 (2)", "std link"); + AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170)); + + addReset(); + addStepClickLink("00:00:000 (1,2)", "std link"); + AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170)); + } + + private void addStepClickLink(string timestamp, string step = "", bool displayTimestamp = true) + { + AddStep(displayTimestamp ? $"{step} {timestamp}" : step, () => Editor.HandleTimestamp(timestamp)); + AddUntilStep("wait for seek", () => EditorClock.SeekingOrStopped.Value); + } + + private void addReset() => addStepClickLink("00:00:000", "reset", false); + + private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(int, int)>? columnPairs = null) + { + bool checkColumns = columnPairs != null + ? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2))) + : !EditorBeatmap.SelectedHitObjects.Any(); + + return EditorClock.CurrentTime == startTime + && EditorBeatmap.SelectedHitObjects.Count == (columnPairs?.Count ?? 0) + && checkColumns; + } + + private bool isNoteAt(HitObject hitObject, double time, int column) => + hitObject is ManiaHitObject maniaHitObject + && maniaHitObject.StartTime == time + && maniaHitObject.Column == column; + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs index ef6dca620a..609c2e8953 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs @@ -18,10 +18,13 @@ namespace osu.Game.Rulesets.Mania.Tests [TestFixture] public class ManiaBeatmapConversionTest : BeatmapConversionTest { - protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; + protected override string ResourceAssembly => "osu.Game.Rulesets.Mania.Tests"; [TestCase("basic")] [TestCase("zero-length-slider")] + [TestCase("20544")] + [TestCase("100374")] + [TestCase("1450162")] public void Test(string name) => base.Test(name); protected override IEnumerable CreateConvertValue(HitObject hitObject) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs index 51f35d3c3d..99598557a6 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Tests [TestFixture] public class ManiaBeatmapSampleConversionTest : BeatmapConversionTest, SampleConvertValue> { - protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; + protected override string ResourceAssembly => "osu.Game.Rulesets.Mania.Tests"; [TestCase("convert-samples")] [TestCase("mania-samples")] diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs index 7b0171a9ee..229df4b67b 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mania.Tests { public class ManiaDifficultyCalculatorTest : DifficultyCalculatorTest { - protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; + protected override string ResourceAssembly => "osu.Game.Rulesets.Mania.Tests"; [TestCase(2.3493769750220914d, 242, "diffcalc-test")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs new file mode 100644 index 0000000000..315849f7de --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.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 NUnit.Framework; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Scoring; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [TestFixture] + public class ManiaHealthProcessorTest + { + [Test] + public void TestNoDrain() + { + var processor = new ManiaHealthProcessor(0); + processor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4)) + { + HitObjects = + { + new Note { StartTime = 0 }, + new Note { StartTime = 1000 }, + } + }); + + // No matter what, mania doesn't have passive HP drain. + Assert.That(processor.DrainRate, Is.Zero); + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModAutoplay.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModAutoplay.cs new file mode 100644 index 0000000000..f653f209c1 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModAutoplay.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 System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests.Mods +{ + public partial class TestSceneManiaModAutoplay : ModTestScene + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + + [Test] + public void TestPerfectScoreOnShortHoldNote() + { + CreateModTest(new ModTestData + { + Autoplay = true, + Beatmap = new ManiaBeatmap(new StageDefinition(1)) + { + HitObjects = new List + { + new HoldNote + { + StartTime = 100, + EndTime = 100, + }, + new HoldNote + { + StartTime = 100.1, + EndTime = 150, + }, + } + }, + PassCondition = () => Player.ScoreProcessor.Combo.Value == 4 + }); + } + } +} 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..975e43ec08 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModDoubleTime.cs @@ -0,0 +1,75 @@ +// 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.Utils; +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 + && Precision.AlmostEquals(Player.ScoreProcessor.Accuracy.Value, 0.9836, 0.01) + && Player.ScoreProcessor.TotalScore.Value == 946_049, + 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_000 * 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/Mods/TestSceneManiaModPerfect.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs index 97a6ee28f4..51730e2b43 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.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. +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 TestSceneManiaModPerfect : ModPerfectTestScene + public partial class TestSceneManiaModPerfect : ModFailConditionTestScene { protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); @@ -24,5 +29,52 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods [TestCase(false)] [TestCase(true)] public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss); + + [Test] + public void TestGreatHit() => CreateModTest(new ModTestData + { + Mod = new ManiaModPerfect(), + PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false), + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Note + { + StartTime = 1000, + } + }, + }, + ReplayFrames = new List + { + new ManiaReplayFrame(1020, ManiaAction.Key1), + new ManiaReplayFrame(2000) + } + }); + + [Test] + public void TestBreakOnHoldNote() => CreateModTest(new ModTestData + { + Mod = new ManiaModPerfect(), + PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true) && Player.Results.Count == 2, + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new HoldNote + { + StartTime = 1000, + EndTime = 3000, + }, + }, + }, + ReplayFrames = new List + { + new ManiaReplayFrame(1000, ManiaAction.Key1), + new ManiaReplayFrame(2000) + } + }); } } diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModSuddenDeath.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModSuddenDeath.cs new file mode 100644 index 0000000000..619816a815 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModSuddenDeath.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.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 TestSceneManiaModSuddenDeath : ModFailConditionTestScene + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + + public TestSceneManiaModSuddenDeath() + : base(new ManiaModSuddenDeath()) + { + } + + [Test] + public void TestGreatHit() => CreateModTest(new ModTestData + { + Mod = new ManiaModSuddenDeath(), + PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false), + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Note + { + StartTime = 1000, + } + }, + }, + ReplayFrames = new List + { + new ManiaReplayFrame(1020, ManiaAction.Key1), + new ManiaReplayFrame(2000) + } + }); + + [Test] + public void TestBreakOnHoldNote() => CreateModTest(new ModTestData + { + Mod = new ManiaModSuddenDeath(), + PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true) && Player.Results.Count == 2, + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new HoldNote + { + StartTime = 1000, + EndTime = 3000, + }, + }, + }, + ReplayFrames = new List + { + new ManiaReplayFrame(1000, ManiaAction.Key1), + new ManiaReplayFrame(2000) + } + }); + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/100374-expected-conversion.json b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/100374-expected-conversion.json new file mode 100644 index 0000000000..59cf6d2672 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/100374-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"RandomW":273084013,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":15562.0,"Objects":[{"StartTime":15562.0,"EndTime":17155.0,"Column":0}]},{"RandomW":2659258901,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273084013,"StartTime":17686.0,"Objects":[{"StartTime":17686.0,"EndTime":17686.0,"Column":0},{"StartTime":17686.0,"EndTime":17686.0,"Column":1}]},{"RandomW":3083655709,"RandomX":273326509,"RandomY":273084013,"RandomZ":2659258901,"StartTime":17951.0,"Objects":[{"StartTime":17951.0,"EndTime":17951.0,"Column":1}]},{"RandomW":3588026162,"RandomX":2659258901,"RandomY":3083655709,"RandomZ":4073603712,"StartTime":18217.0,"Objects":[{"StartTime":18217.0,"EndTime":18217.0,"Column":2},{"StartTime":18217.0,"EndTime":18217.0,"Column":4}]},{"RandomW":1130061350,"RandomX":3083655709,"RandomY":4073603712,"RandomZ":3588026162,"StartTime":18482.0,"Objects":[{"StartTime":18482.0,"EndTime":18482.0,"Column":2}]},{"RandomW":315421426,"RandomX":3588026162,"RandomY":1130061350,"RandomZ":2459334754,"StartTime":18748.0,"Objects":[{"StartTime":18748.0,"EndTime":19013.0,"Column":0}]},{"RandomW":3110660773,"RandomX":2459334754,"RandomY":315421426,"RandomZ":542845670,"StartTime":19279.0,"Objects":[{"StartTime":19279.0,"EndTime":19809.0,"Column":3},{"StartTime":19544.0,"EndTime":19544.0,"Column":1},{"StartTime":19809.0,"EndTime":19809.0,"Column":1}]},{"RandomW":3110660773,"RandomX":2459334754,"RandomY":315421426,"RandomZ":542845670,"StartTime":20075.0,"Objects":[{"StartTime":20075.0,"EndTime":20075.0,"Column":4},{"StartTime":20075.0,"EndTime":20075.0,"Column":2}]},{"RandomW":2552021122,"RandomX":315421426,"RandomY":542845670,"RandomZ":3110660773,"StartTime":20341.0,"Objects":[{"StartTime":20341.0,"EndTime":20341.0,"Column":3}]},{"RandomW":3979536913,"RandomX":542845670,"RandomY":3110660773,"RandomZ":2552021122,"StartTime":20606.0,"Objects":[{"StartTime":20606.0,"EndTime":20606.0,"Column":2},{"StartTime":20606.0,"EndTime":20606.0,"Column":3}]},{"RandomW":3926138036,"RandomX":2552021122,"RandomY":3979536913,"RandomZ":348643659,"StartTime":20871.0,"Objects":[{"StartTime":20871.0,"EndTime":21401.0,"Column":4}]},{"RandomW":4001028953,"RandomX":348643659,"RandomY":3926138036,"RandomZ":2489502118,"StartTime":21933.0,"Objects":[{"StartTime":21933.0,"EndTime":22198.0,"Column":5}]},{"RandomW":263714783,"RandomX":2489502118,"RandomY":4001028953,"RandomZ":3315380836,"StartTime":22464.0,"Objects":[{"StartTime":22464.0,"EndTime":22729.0,"Column":0}]},{"RandomW":3045229215,"RandomX":3315380836,"RandomY":263714783,"RandomZ":2367299702,"StartTime":22995.0,"Objects":[{"StartTime":22995.0,"EndTime":23791.0,"Column":2}]},{"RandomW":622075324,"RandomX":2367299702,"RandomY":3045229215,"RandomZ":2511145433,"StartTime":24057.0,"Objects":[{"StartTime":24057.0,"EndTime":24322.0,"Column":1}]},{"RandomW":1428674661,"RandomX":3630592823,"RandomY":628640291,"RandomZ":2684635853,"StartTime":24588.0,"Objects":[{"StartTime":24588.0,"EndTime":24853.0,"Column":4},{"StartTime":24588.0,"EndTime":24853.0,"Column":3}]},{"RandomW":2963472042,"RandomX":3191072317,"RandomY":1509788298,"RandomZ":3677221210,"StartTime":25119.0,"Objects":[{"StartTime":25119.0,"EndTime":25649.0,"Column":2}]},{"RandomW":2441208973,"RandomX":1509788298,"RandomY":3677221210,"RandomZ":2963472042,"StartTime":26181.0,"Objects":[{"StartTime":26181.0,"EndTime":26181.0,"Column":2},{"StartTime":26181.0,"EndTime":26181.0,"Column":3}]},{"RandomW":614303213,"RandomX":3677221210,"RandomY":2963472042,"RandomZ":2441208973,"StartTime":26447.0,"Objects":[{"StartTime":26447.0,"EndTime":26447.0,"Column":3}]},{"RandomW":931064848,"RandomX":2441208973,"RandomY":614303213,"RandomZ":2425227013,"StartTime":26712.0,"Objects":[{"StartTime":26712.0,"EndTime":26977.0,"Column":2}]},{"RandomW":1631554006,"RandomX":2425227013,"RandomY":931064848,"RandomZ":2839921662,"StartTime":27243.0,"Objects":[{"StartTime":27243.0,"EndTime":27508.0,"Column":4}]},{"RandomW":1102544522,"RandomX":2839921662,"RandomY":1631554006,"RandomZ":2171149531,"StartTime":27774.0,"Objects":[{"StartTime":27774.0,"EndTime":28039.0,"Column":3}]},{"RandomW":1535528787,"RandomX":2171149531,"RandomY":1102544522,"RandomZ":3328843633,"StartTime":28305.0,"Objects":[{"StartTime":28305.0,"EndTime":28835.0,"Column":4},{"StartTime":28305.0,"EndTime":28305.0,"Column":3},{"StartTime":28570.0,"EndTime":28570.0,"Column":3},{"StartTime":28835.0,"EndTime":28835.0,"Column":3}]},{"RandomW":2462060348,"RandomX":1102544522,"RandomY":3328843633,"RandomZ":1535528787,"StartTime":29102.0,"Objects":[{"StartTime":29102.0,"EndTime":29102.0,"Column":3}]},{"RandomW":2548780898,"RandomX":2462060348,"RandomY":1752789184,"RandomZ":4269701929,"StartTime":29367.0,"Objects":[{"StartTime":29367.0,"EndTime":29897.0,"Column":5},{"StartTime":29367.0,"EndTime":29897.0,"Column":1}]},{"RandomW":2872444045,"RandomX":2548780898,"RandomY":96471884,"RandomZ":2795275332,"StartTime":30429.0,"Objects":[{"StartTime":30429.0,"EndTime":30694.0,"Column":2}]},{"RandomW":554186146,"RandomX":2872444045,"RandomY":1718345430,"RandomZ":1676944188,"StartTime":30960.0,"Objects":[{"StartTime":30960.0,"EndTime":31225.0,"Column":4},{"StartTime":30960.0,"EndTime":31225.0,"Column":1}]},{"RandomW":44350362,"RandomX":1676944188,"RandomY":554186146,"RandomZ":973164386,"StartTime":31491.0,"Objects":[{"StartTime":31491.0,"EndTime":32287.0,"Column":0}]},{"RandomW":2689469863,"RandomX":973164386,"RandomY":44350362,"RandomZ":3230373169,"StartTime":32553.0,"Objects":[{"StartTime":32553.0,"EndTime":32818.0,"Column":1}]},{"RandomW":3076210018,"RandomX":3230373169,"RandomY":2689469863,"RandomZ":2416196755,"StartTime":33084.0,"Objects":[{"StartTime":33084.0,"EndTime":33349.0,"Column":2}]},{"RandomW":4212524875,"RandomX":2416196755,"RandomY":3076210018,"RandomZ":736433317,"StartTime":33615.0,"Objects":[{"StartTime":33615.0,"EndTime":34145.0,"Column":5}]},{"RandomW":668643347,"RandomX":4212524875,"RandomY":1246190622,"RandomZ":614058009,"StartTime":34677.0,"Objects":[{"StartTime":34677.0,"EndTime":34677.0,"Column":0},{"StartTime":34677.0,"EndTime":34677.0,"Column":5}]},{"RandomW":4133034829,"RandomX":668643347,"RandomY":1824376828,"RandomZ":476758489,"StartTime":34942.0,"Objects":[{"StartTime":34942.0,"EndTime":34942.0,"Column":1},{"StartTime":34942.0,"EndTime":34942.0,"Column":5}]},{"RandomW":82933693,"RandomX":1824376828,"RandomY":476758489,"RandomZ":4133034829,"StartTime":35208.0,"Objects":[{"StartTime":35208.0,"EndTime":35208.0,"Column":0},{"StartTime":35208.0,"EndTime":35208.0,"Column":1}]},{"RandomW":2263995128,"RandomX":476758489,"RandomY":4133034829,"RandomZ":82933693,"StartTime":35473.0,"Objects":[{"StartTime":35473.0,"EndTime":35473.0,"Column":1}]},{"RandomW":3437211638,"RandomX":4133034829,"RandomY":82933693,"RandomZ":2263995128,"StartTime":35739.0,"Objects":[{"StartTime":35739.0,"EndTime":35739.0,"Column":2}]},{"RandomW":2107738941,"RandomX":2263995128,"RandomY":3437211638,"RandomZ":4066526803,"StartTime":36004.0,"Objects":[{"StartTime":36004.0,"EndTime":36004.0,"Column":2},{"StartTime":36004.0,"EndTime":36004.0,"Column":5}]},{"RandomW":1976561763,"RandomX":3437211638,"RandomY":4066526803,"RandomZ":2107738941,"StartTime":36270.0,"Objects":[{"StartTime":36270.0,"EndTime":36270.0,"Column":3},{"StartTime":36270.0,"EndTime":36270.0,"Column":4}]},{"RandomW":1147027763,"RandomX":4066526803,"RandomY":2107738941,"RandomZ":1976561763,"StartTime":36535.0,"Objects":[{"StartTime":36535.0,"EndTime":36535.0,"Column":3}]},{"RandomW":3580315894,"RandomX":1976561763,"RandomY":1147027763,"RandomZ":2767111989,"StartTime":36801.0,"Objects":[{"StartTime":36801.0,"EndTime":37331.0,"Column":4}]},{"RandomW":3743545041,"RandomX":1147027763,"RandomY":2767111989,"RandomZ":3580315894,"StartTime":37597.0,"Objects":[{"StartTime":37597.0,"EndTime":37597.0,"Column":1}]},{"RandomW":1409948107,"RandomX":3743545041,"RandomY":1774216159,"RandomZ":3150304957,"StartTime":37863.0,"Objects":[{"StartTime":37863.0,"EndTime":38393.0,"Column":2},{"StartTime":37863.0,"EndTime":38393.0,"Column":3}]},{"RandomW":4009340712,"RandomX":3150304957,"RandomY":1409948107,"RandomZ":2219703013,"StartTime":38925.0,"Objects":[{"StartTime":38925.0,"EndTime":39190.0,"Column":5}]},{"RandomW":3071167491,"RandomX":2065497204,"RandomY":2145154717,"RandomZ":2494378321,"StartTime":39456.0,"Objects":[{"StartTime":39456.0,"EndTime":39721.0,"Column":0},{"StartTime":39456.0,"EndTime":39721.0,"Column":2}]},{"RandomW":1245938367,"RandomX":3071167491,"RandomY":728627658,"RandomZ":3080260260,"StartTime":39987.0,"Objects":[{"StartTime":39987.0,"EndTime":40783.0,"Column":3}]},{"RandomW":3032241617,"RandomX":1245938367,"RandomY":2414391712,"RandomZ":3406801470,"StartTime":41048.0,"Objects":[{"StartTime":41048.0,"EndTime":41313.0,"Column":2}]},{"RandomW":3367991920,"RandomX":3804000131,"RandomY":672376773,"RandomZ":2667292323,"StartTime":41579.0,"Objects":[{"StartTime":41579.0,"EndTime":41844.0,"Column":1},{"StartTime":41579.0,"EndTime":41844.0,"Column":3}]},{"RandomW":2095476726,"RandomX":2667292323,"RandomY":3367991920,"RandomZ":3380532371,"StartTime":42110.0,"Objects":[{"StartTime":42110.0,"EndTime":42640.0,"Column":5}]},{"RandomW":869340745,"RandomX":2095476726,"RandomY":1063981175,"RandomZ":204767504,"StartTime":43172.0,"Objects":[{"StartTime":43172.0,"EndTime":43172.0,"Column":1},{"StartTime":43172.0,"EndTime":43172.0,"Column":4}]},{"RandomW":461904197,"RandomX":204767504,"RandomY":869340745,"RandomZ":2080855578,"StartTime":43438.0,"Objects":[{"StartTime":43438.0,"EndTime":43438.0,"Column":2},{"StartTime":43438.0,"EndTime":43438.0,"Column":1}]},{"RandomW":3004966693,"RandomX":869340745,"RandomY":2080855578,"RandomZ":461904197,"StartTime":43703.0,"Objects":[{"StartTime":43703.0,"EndTime":43703.0,"Column":3},{"StartTime":43703.0,"EndTime":43703.0,"Column":4}]},{"RandomW":147065937,"RandomX":2080855578,"RandomY":461904197,"RandomZ":3004966693,"StartTime":43969.0,"Objects":[{"StartTime":43969.0,"EndTime":43969.0,"Column":4}]},{"RandomW":1312111829,"RandomX":461904197,"RandomY":3004966693,"RandomZ":147065937,"StartTime":44234.0,"Objects":[{"StartTime":44234.0,"EndTime":44234.0,"Column":4}]},{"RandomW":355223143,"RandomX":3004966693,"RandomY":147065937,"RandomZ":1312111829,"StartTime":44500.0,"Objects":[{"StartTime":44500.0,"EndTime":44500.0,"Column":3}]},{"RandomW":1197174504,"RandomX":147065937,"RandomY":1312111829,"RandomZ":355223143,"StartTime":44765.0,"Objects":[{"StartTime":44765.0,"EndTime":44765.0,"Column":2},{"StartTime":44765.0,"EndTime":44765.0,"Column":3}]},{"RandomW":2296450669,"RandomX":355223143,"RandomY":1197174504,"RandomZ":1876247766,"StartTime":45031.0,"Objects":[{"StartTime":45031.0,"EndTime":45031.0,"Column":1},{"StartTime":45031.0,"EndTime":45031.0,"Column":0}]},{"RandomW":1664705375,"RandomX":1876247766,"RandomY":2296450669,"RandomZ":4287200872,"StartTime":45296.0,"Objects":[{"StartTime":45296.0,"EndTime":45296.0,"Column":0},{"StartTime":45296.0,"EndTime":45296.0,"Column":4}]},{"RandomW":2786027546,"RandomX":2296450669,"RandomY":4287200872,"RandomZ":1664705375,"StartTime":45562.0,"Objects":[{"StartTime":45562.0,"EndTime":45562.0,"Column":1}]},{"RandomW":639469776,"RandomX":4287200872,"RandomY":1664705375,"RandomZ":2786027546,"StartTime":45827.0,"Objects":[{"StartTime":45827.0,"EndTime":45827.0,"Column":3},{"StartTime":45827.0,"EndTime":45827.0,"Column":4}]},{"RandomW":2463352901,"RandomX":1664705375,"RandomY":2786027546,"RandomZ":639469776,"StartTime":46093.0,"Objects":[{"StartTime":46093.0,"EndTime":46093.0,"Column":4}]},{"RandomW":760995091,"RandomX":2463352901,"RandomY":978871003,"RandomZ":3888812594,"StartTime":46358.0,"Objects":[{"StartTime":46358.0,"EndTime":46888.0,"Column":2}]},{"RandomW":3631307076,"RandomX":3888812594,"RandomY":760995091,"RandomZ":566667549,"StartTime":47420.0,"Objects":[{"StartTime":47420.0,"EndTime":47685.0,"Column":4}]},{"RandomW":2353216536,"RandomX":3631307076,"RandomY":1805196154,"RandomZ":2564415583,"StartTime":47951.0,"Objects":[{"StartTime":47951.0,"EndTime":48216.0,"Column":1},{"StartTime":47951.0,"EndTime":48216.0,"Column":0}]},{"RandomW":717730087,"RandomX":2353216536,"RandomY":3735744429,"RandomZ":2102099401,"StartTime":48482.0,"Objects":[{"StartTime":48482.0,"EndTime":49278.0,"Column":5},{"StartTime":48482.0,"EndTime":49278.0,"Column":2}]},{"RandomW":271333990,"RandomX":717730087,"RandomY":3220302747,"RandomZ":917482575,"StartTime":49544.0,"Objects":[{"StartTime":49544.0,"EndTime":49809.0,"Column":0}]},{"RandomW":937976203,"RandomX":917482575,"RandomY":271333990,"RandomZ":125173709,"StartTime":50075.0,"Objects":[{"StartTime":50075.0,"EndTime":50340.0,"Column":2}]},{"RandomW":2781059562,"RandomX":937976203,"RandomY":2087616237,"RandomZ":232817676,"StartTime":50606.0,"Objects":[{"StartTime":50606.0,"EndTime":51667.0,"Column":0},{"StartTime":50606.0,"EndTime":51667.0,"Column":1}]},{"RandomW":3511898336,"RandomX":2087616237,"RandomY":232817676,"RandomZ":2781059562,"StartTime":52730.0,"Objects":[{"StartTime":52730.0,"EndTime":52730.0,"Column":4}]},{"RandomW":623291556,"RandomX":3737503025,"RandomY":3607951873,"RandomZ":1857627587,"StartTime":53792.0,"Objects":[{"StartTime":53792.0,"EndTime":54322.0,"Column":5},{"StartTime":53792.0,"EndTime":54322.0,"Column":1}]},{"RandomW":3577350524,"RandomX":3607951873,"RandomY":1857627587,"RandomZ":623291556,"StartTime":54588.0,"Objects":[{"StartTime":54588.0,"EndTime":54588.0,"Column":2}]},{"RandomW":3611414219,"RandomX":1700150568,"RandomY":3261504380,"RandomZ":3526708248,"StartTime":54854.0,"Objects":[{"StartTime":54854.0,"EndTime":55384.0,"Column":3},{"StartTime":54854.0,"EndTime":55384.0,"Column":4}]},{"RandomW":4116828180,"RandomX":3526708248,"RandomY":3611414219,"RandomZ":53089910,"StartTime":55916.0,"Objects":[{"StartTime":55916.0,"EndTime":56446.0,"Column":5}]},{"RandomW":1419945944,"RandomX":53089910,"RandomY":4116828180,"RandomZ":2370574124,"StartTime":56978.0,"Objects":[{"StartTime":56978.0,"EndTime":57549.0,"Column":3}]},{"RandomW":4235330325,"RandomX":2370574124,"RandomY":1419945944,"RandomZ":124293788,"StartTime":58120.0,"Objects":[{"StartTime":58120.0,"EndTime":58405.0,"Column":5}]},{"RandomW":1354196818,"RandomX":124293788,"RandomY":4235330325,"RandomZ":292200128,"StartTime":58692.0,"Objects":[{"StartTime":58692.0,"EndTime":58973.0,"Column":3}]},{"RandomW":2131632245,"RandomX":292200128,"RandomY":1354196818,"RandomZ":319349674,"StartTime":59325.0,"Objects":[{"StartTime":59325.0,"EndTime":60170.0,"Column":5}]},{"RandomW":987180490,"RandomX":1354196818,"RandomY":319349674,"RandomZ":2131632245,"StartTime":60513.0,"Objects":[{"StartTime":60513.0,"EndTime":60513.0,"Column":3}]},{"RandomW":2247158810,"RandomX":2131632245,"RandomY":987180490,"RandomZ":3518058549,"StartTime":60778.0,"Objects":[{"StartTime":60778.0,"EndTime":61043.0,"Column":0}]},{"RandomW":2347989337,"RandomX":987180490,"RandomY":3518058549,"RandomZ":2247158810,"StartTime":61309.0,"Objects":[{"StartTime":61309.0,"EndTime":61309.0,"Column":3}]},{"RandomW":82954311,"RandomX":1403151684,"RandomY":1362150166,"RandomZ":1092174296,"StartTime":61840.0,"Objects":[{"StartTime":61840.0,"EndTime":62105.0,"Column":0}]},{"RandomW":408605211,"RandomX":82954311,"RandomY":1144587736,"RandomZ":2479248954,"StartTime":62371.0,"Objects":[{"StartTime":62371.0,"EndTime":62901.0,"Column":1}]},{"RandomW":2455999143,"RandomX":1144587736,"RandomY":2479248954,"RandomZ":408605211,"StartTime":63168.0,"Objects":[{"StartTime":63168.0,"EndTime":63168.0,"Column":2}]},{"RandomW":1898608481,"RandomX":2455999143,"RandomY":519590646,"RandomZ":3207504021,"StartTime":63433.0,"Objects":[{"StartTime":63433.0,"EndTime":63963.0,"Column":5}]},{"RandomW":601995191,"RandomX":3207504021,"RandomY":1898608481,"RandomZ":4283573577,"StartTime":64230.0,"Objects":[{"StartTime":64230.0,"EndTime":64230.0,"Column":5},{"StartTime":64230.0,"EndTime":64230.0,"Column":1}]},{"RandomW":3909194070,"RandomX":1898608481,"RandomY":4283573577,"RandomZ":601995191,"StartTime":64495.0,"Objects":[{"StartTime":64495.0,"EndTime":64495.0,"Column":3},{"StartTime":64495.0,"EndTime":64495.0,"Column":4}]},{"RandomW":3417465448,"RandomX":4283573577,"RandomY":601995191,"RandomZ":3909194070,"StartTime":64761.0,"Objects":[{"StartTime":64761.0,"EndTime":64761.0,"Column":4}]},{"RandomW":2779016762,"RandomX":601995191,"RandomY":3909194070,"RandomZ":3417465448,"StartTime":65026.0,"Objects":[{"StartTime":65026.0,"EndTime":65026.0,"Column":4},{"StartTime":65026.0,"EndTime":65026.0,"Column":5}]},{"RandomW":2346068278,"RandomX":3909194070,"RandomY":3417465448,"RandomZ":2779016762,"StartTime":65292.0,"Objects":[{"StartTime":65292.0,"EndTime":65292.0,"Column":3}]},{"RandomW":1857589819,"RandomX":3417465448,"RandomY":2779016762,"RandomZ":2346068278,"StartTime":65557.0,"Objects":[{"StartTime":65557.0,"EndTime":65557.0,"Column":4},{"StartTime":65557.0,"EndTime":65557.0,"Column":5}]},{"RandomW":910236838,"RandomX":2779016762,"RandomY":2346068278,"RandomZ":1857589819,"StartTime":66088.0,"Objects":[{"StartTime":66088.0,"EndTime":66088.0,"Column":3},{"StartTime":66088.0,"EndTime":66088.0,"Column":4}]},{"RandomW":910236838,"RandomX":2779016762,"RandomY":2346068278,"RandomZ":1857589819,"StartTime":66354.0,"Objects":[{"StartTime":66354.0,"EndTime":66354.0,"Column":2},{"StartTime":66354.0,"EndTime":66354.0,"Column":1}]},{"RandomW":2327273799,"RandomX":1857589819,"RandomY":910236838,"RandomZ":2953998826,"StartTime":66619.0,"Objects":[{"StartTime":66619.0,"EndTime":67149.0,"Column":0}]},{"RandomW":540283744,"RandomX":910236838,"RandomY":2953998826,"RandomZ":2327273799,"StartTime":67416.0,"Objects":[{"StartTime":67416.0,"EndTime":67416.0,"Column":0}]},{"RandomW":1024467186,"RandomX":2327273799,"RandomY":540283744,"RandomZ":514760684,"StartTime":67681.0,"Objects":[{"StartTime":67681.0,"EndTime":68211.0,"Column":2}]},{"RandomW":211600206,"RandomX":540283744,"RandomY":514760684,"RandomZ":1024467186,"StartTime":68478.0,"Objects":[{"StartTime":68478.0,"EndTime":68478.0,"Column":3}]},{"RandomW":2360573614,"RandomX":514760684,"RandomY":1024467186,"RandomZ":211600206,"StartTime":68743.0,"Objects":[{"StartTime":68743.0,"EndTime":68743.0,"Column":4},{"StartTime":68743.0,"EndTime":68743.0,"Column":5}]},{"RandomW":3867722027,"RandomX":1024467186,"RandomY":211600206,"RandomZ":2360573614,"StartTime":69009.0,"Objects":[{"StartTime":69009.0,"EndTime":69009.0,"Column":3}]},{"RandomW":1512274616,"RandomX":211600206,"RandomY":2360573614,"RandomZ":3867722027,"StartTime":69274.0,"Objects":[{"StartTime":69274.0,"EndTime":69274.0,"Column":4},{"StartTime":69274.0,"EndTime":69274.0,"Column":5}]},{"RandomW":2957984769,"RandomX":2360573614,"RandomY":3867722027,"RandomZ":1512274616,"StartTime":69540.0,"Objects":[{"StartTime":69540.0,"EndTime":69540.0,"Column":3}]},{"RandomW":2803767976,"RandomX":3867722027,"RandomY":1512274616,"RandomZ":2957984769,"StartTime":69805.0,"Objects":[{"StartTime":69805.0,"EndTime":69805.0,"Column":4},{"StartTime":69805.0,"EndTime":69805.0,"Column":5}]},{"RandomW":1183341084,"RandomX":2957984769,"RandomY":2803767976,"RandomZ":121575161,"StartTime":70336.0,"Objects":[{"StartTime":70336.0,"EndTime":70601.0,"Column":3}]},{"RandomW":3685872119,"RandomX":121575161,"RandomY":1183341084,"RandomZ":2351788416,"StartTime":70867.0,"Objects":[{"StartTime":70867.0,"EndTime":71397.0,"Column":4}]},{"RandomW":617004198,"RandomX":1183341084,"RandomY":2351788416,"RandomZ":3685872119,"StartTime":71663.0,"Objects":[{"StartTime":71663.0,"EndTime":71663.0,"Column":3}]},{"RandomW":2478235967,"RandomX":617004198,"RandomY":546986648,"RandomZ":3353120378,"StartTime":71929.0,"Objects":[{"StartTime":71929.0,"EndTime":72459.0,"Column":0}]},{"RandomW":2189712483,"RandomX":546986648,"RandomY":3353120378,"RandomZ":2478235967,"StartTime":72725.0,"Objects":[{"StartTime":72725.0,"EndTime":72725.0,"Column":2}]},{"RandomW":1882757169,"RandomX":3353120378,"RandomY":2478235967,"RandomZ":2189712483,"StartTime":72991.0,"Objects":[{"StartTime":72991.0,"EndTime":72991.0,"Column":3},{"StartTime":72991.0,"EndTime":72991.0,"Column":4}]},{"RandomW":1404331794,"RandomX":2478235967,"RandomY":2189712483,"RandomZ":1882757169,"StartTime":73256.0,"Objects":[{"StartTime":73256.0,"EndTime":73256.0,"Column":1}]},{"RandomW":1999620930,"RandomX":2189712483,"RandomY":1882757169,"RandomZ":1404331794,"StartTime":73522.0,"Objects":[{"StartTime":73522.0,"EndTime":73522.0,"Column":3},{"StartTime":73522.0,"EndTime":73522.0,"Column":4}]},{"RandomW":3622364800,"RandomX":1882757169,"RandomY":1404331794,"RandomZ":1999620930,"StartTime":73787.0,"Objects":[{"StartTime":73787.0,"EndTime":73787.0,"Column":2}]},{"RandomW":1671763292,"RandomX":1404331794,"RandomY":1999620930,"RandomZ":3622364800,"StartTime":74053.0,"Objects":[{"StartTime":74053.0,"EndTime":74053.0,"Column":3},{"StartTime":74053.0,"EndTime":74053.0,"Column":4}]},{"RandomW":2594561583,"RandomX":3622364800,"RandomY":1671763292,"RandomZ":2480497357,"StartTime":74584.0,"Objects":[{"StartTime":74584.0,"EndTime":74849.0,"Column":1}]},{"RandomW":1101860073,"RandomX":2480497357,"RandomY":2594561583,"RandomZ":183105309,"StartTime":75115.0,"Objects":[{"StartTime":75115.0,"EndTime":75645.0,"Column":3}]},{"RandomW":423280923,"RandomX":2594561583,"RandomY":183105309,"RandomZ":1101860073,"StartTime":75911.0,"Objects":[{"StartTime":75911.0,"EndTime":75911.0,"Column":2}]},{"RandomW":3905841932,"RandomX":1101860073,"RandomY":423280923,"RandomZ":2916757685,"StartTime":76177.0,"Objects":[{"StartTime":76177.0,"EndTime":76707.0,"Column":4}]},{"RandomW":3241015480,"RandomX":423280923,"RandomY":2916757685,"RandomZ":3905841932,"StartTime":76973.0,"Objects":[{"StartTime":76973.0,"EndTime":76973.0,"Column":3}]},{"RandomW":1928531304,"RandomX":3905841932,"RandomY":3241015480,"RandomZ":248564639,"StartTime":77239.0,"Objects":[{"StartTime":77239.0,"EndTime":77504.0,"Column":5}]},{"RandomW":634267655,"RandomX":3925777969,"RandomY":1203262350,"RandomZ":3485263061,"StartTime":77770.0,"Objects":[{"StartTime":77770.0,"EndTime":78035.0,"Column":3},{"StartTime":77770.0,"EndTime":78035.0,"Column":1}]},{"RandomW":953955737,"RandomX":1203262350,"RandomY":3485263061,"RandomZ":634267655,"StartTime":78301.0,"Objects":[{"StartTime":78301.0,"EndTime":78301.0,"Column":3}]},{"RandomW":3179099439,"RandomX":3485263061,"RandomY":634267655,"RandomZ":953955737,"StartTime":78566.0,"Objects":[{"StartTime":78566.0,"EndTime":78566.0,"Column":2},{"StartTime":78566.0,"EndTime":78566.0,"Column":3}]},{"RandomW":2513433625,"RandomX":634267655,"RandomY":953955737,"RandomZ":3179099439,"StartTime":78832.0,"Objects":[{"StartTime":78832.0,"EndTime":78832.0,"Column":3},{"StartTime":78832.0,"EndTime":78832.0,"Column":4}]},{"RandomW":3239409847,"RandomX":953955737,"RandomY":3179099439,"RandomZ":2513433625,"StartTime":79097.0,"Objects":[{"StartTime":79097.0,"EndTime":79097.0,"Column":5},{"StartTime":79097.0,"EndTime":79097.0,"Column":0}]},{"RandomW":1279031172,"RandomX":2513433625,"RandomY":3239409847,"RandomZ":415034865,"StartTime":79363.0,"Objects":[{"StartTime":79363.0,"EndTime":79893.0,"Column":3}]},{"RandomW":2797153574,"RandomX":3239409847,"RandomY":415034865,"RandomZ":1279031172,"StartTime":80159.0,"Objects":[{"StartTime":80159.0,"EndTime":80159.0,"Column":3}]},{"RandomW":858752658,"RandomX":1279031172,"RandomY":2797153574,"RandomZ":3422759302,"StartTime":80424.0,"Objects":[{"StartTime":80424.0,"EndTime":80954.0,"Column":2}]},{"RandomW":2617268004,"RandomX":2797153574,"RandomY":3422759302,"RandomZ":858752658,"StartTime":81221.0,"Objects":[{"StartTime":81221.0,"EndTime":81221.0,"Column":4}]},{"RandomW":4089416095,"RandomX":3422759302,"RandomY":858752658,"RandomZ":2617268004,"StartTime":81486.0,"Objects":[{"StartTime":81486.0,"EndTime":81486.0,"Column":4},{"StartTime":81486.0,"EndTime":81486.0,"Column":5}]},{"RandomW":640008567,"RandomX":858752658,"RandomY":2617268004,"RandomZ":4089416095,"StartTime":81752.0,"Objects":[{"StartTime":81752.0,"EndTime":81752.0,"Column":4}]},{"RandomW":1769064503,"RandomX":2617268004,"RandomY":4089416095,"RandomZ":640008567,"StartTime":82017.0,"Objects":[{"StartTime":82017.0,"EndTime":82017.0,"Column":5},{"StartTime":82017.0,"EndTime":82017.0,"Column":0}]},{"RandomW":4171929422,"RandomX":640008567,"RandomY":1769064503,"RandomZ":4149611338,"StartTime":82283.0,"Objects":[{"StartTime":82283.0,"EndTime":82283.0,"Column":3},{"StartTime":82283.0,"EndTime":82283.0,"Column":5}]},{"RandomW":4035764053,"RandomX":1769064503,"RandomY":4149611338,"RandomZ":4171929422,"StartTime":82548.0,"Objects":[{"StartTime":82548.0,"EndTime":82548.0,"Column":5},{"StartTime":82548.0,"EndTime":82548.0,"Column":0}]},{"RandomW":391872771,"RandomX":4149611338,"RandomY":4171929422,"RandomZ":4035764053,"StartTime":83079.0,"Objects":[{"StartTime":83079.0,"EndTime":83079.0,"Column":3},{"StartTime":83079.0,"EndTime":83079.0,"Column":4}]},{"RandomW":391872771,"RandomX":4149611338,"RandomY":4171929422,"RandomZ":4035764053,"StartTime":83345.0,"Objects":[{"StartTime":83345.0,"EndTime":83345.0,"Column":2},{"StartTime":83345.0,"EndTime":83345.0,"Column":1}]},{"RandomW":4239141202,"RandomX":4035764053,"RandomY":391872771,"RandomZ":1343280377,"StartTime":83610.0,"Objects":[{"StartTime":83610.0,"EndTime":84140.0,"Column":5}]},{"RandomW":2008371177,"RandomX":4239141202,"RandomY":1783379941,"RandomZ":2715086902,"StartTime":84407.0,"Objects":[{"StartTime":84407.0,"EndTime":84407.0,"Column":1},{"StartTime":84407.0,"EndTime":84407.0,"Column":5}]},{"RandomW":980563717,"RandomX":3939376884,"RandomY":3778473815,"RandomZ":3882214919,"StartTime":84672.0,"Objects":[{"StartTime":84672.0,"EndTime":85202.0,"Column":4},{"StartTime":84672.0,"EndTime":85202.0,"Column":2}]},{"RandomW":2698098433,"RandomX":3778473815,"RandomY":3882214919,"RandomZ":980563717,"StartTime":85469.0,"Objects":[{"StartTime":85469.0,"EndTime":85469.0,"Column":1}]},{"RandomW":4140546075,"RandomX":3882214919,"RandomY":980563717,"RandomZ":2698098433,"StartTime":85734.0,"Objects":[{"StartTime":85734.0,"EndTime":85734.0,"Column":3},{"StartTime":85734.0,"EndTime":85734.0,"Column":4}]},{"RandomW":1045835035,"RandomX":980563717,"RandomY":2698098433,"RandomZ":4140546075,"StartTime":86000.0,"Objects":[{"StartTime":86000.0,"EndTime":86000.0,"Column":1}]},{"RandomW":2503475147,"RandomX":2698098433,"RandomY":4140546075,"RandomZ":1045835035,"StartTime":86265.0,"Objects":[{"StartTime":86265.0,"EndTime":86265.0,"Column":1},{"StartTime":86265.0,"EndTime":86265.0,"Column":2}]},{"RandomW":3094559699,"RandomX":4140546075,"RandomY":1045835035,"RandomZ":2503475147,"StartTime":86531.0,"Objects":[{"StartTime":86531.0,"EndTime":86531.0,"Column":3}]},{"RandomW":332613542,"RandomX":1045835035,"RandomY":2503475147,"RandomZ":3094559699,"StartTime":86796.0,"Objects":[{"StartTime":86796.0,"EndTime":86796.0,"Column":2},{"StartTime":86796.0,"EndTime":86796.0,"Column":3}]},{"RandomW":2534271858,"RandomX":332613542,"RandomY":2623704626,"RandomZ":3061969874,"StartTime":87327.0,"Objects":[{"StartTime":87327.0,"EndTime":87592.0,"Column":1}]},{"RandomW":794230988,"RandomX":2534271858,"RandomY":510287938,"RandomZ":2532404899,"StartTime":87858.0,"Objects":[{"StartTime":87858.0,"EndTime":88388.0,"Column":2}]},{"RandomW":3623430191,"RandomX":510287938,"RandomY":2532404899,"RandomZ":794230988,"StartTime":88655.0,"Objects":[{"StartTime":88655.0,"EndTime":88655.0,"Column":2}]},{"RandomW":2269498220,"RandomX":794230988,"RandomY":3623430191,"RandomZ":2598120162,"StartTime":88920.0,"Objects":[{"StartTime":88920.0,"EndTime":89450.0,"Column":0}]},{"RandomW":277080616,"RandomX":3623430191,"RandomY":2598120162,"RandomZ":2269498220,"StartTime":89717.0,"Objects":[{"StartTime":89717.0,"EndTime":89717.0,"Column":2}]},{"RandomW":237305927,"RandomX":2598120162,"RandomY":2269498220,"RandomZ":277080616,"StartTime":89982.0,"Objects":[{"StartTime":89982.0,"EndTime":89982.0,"Column":1},{"StartTime":89982.0,"EndTime":89982.0,"Column":2}]},{"RandomW":3697412902,"RandomX":277080616,"RandomY":237305927,"RandomZ":1976938587,"StartTime":90247.0,"Objects":[{"StartTime":90247.0,"EndTime":90247.0,"Column":1},{"StartTime":90247.0,"EndTime":90247.0,"Column":4}]},{"RandomW":3552536616,"RandomX":237305927,"RandomY":1976938587,"RandomZ":3697412902,"StartTime":90513.0,"Objects":[{"StartTime":90513.0,"EndTime":90513.0,"Column":2},{"StartTime":90513.0,"EndTime":90513.0,"Column":3}]},{"RandomW":758205604,"RandomX":3697412902,"RandomY":3552536616,"RandomZ":4122897696,"StartTime":90778.0,"Objects":[{"StartTime":90778.0,"EndTime":90778.0,"Column":1},{"StartTime":90778.0,"EndTime":90778.0,"Column":2}]},{"RandomW":3787868447,"RandomX":3552536616,"RandomY":4122897696,"RandomZ":758205604,"StartTime":91044.0,"Objects":[{"StartTime":91044.0,"EndTime":91044.0,"Column":2},{"StartTime":91044.0,"EndTime":91044.0,"Column":3}]},{"RandomW":1748107640,"RandomX":3787868447,"RandomY":3373302567,"RandomZ":3485540424,"StartTime":91575.0,"Objects":[{"StartTime":91575.0,"EndTime":91840.0,"Column":4}]},{"RandomW":4130051617,"RandomX":3485540424,"RandomY":1748107640,"RandomZ":3144627152,"StartTime":92106.0,"Objects":[{"StartTime":92106.0,"EndTime":92636.0,"Column":5}]},{"RandomW":808332236,"RandomX":1748107640,"RandomY":3144627152,"RandomZ":4130051617,"StartTime":92902.0,"Objects":[{"StartTime":92902.0,"EndTime":92902.0,"Column":3}]},{"RandomW":182226446,"RandomX":4130051617,"RandomY":808332236,"RandomZ":3371160944,"StartTime":93168.0,"Objects":[{"StartTime":93168.0,"EndTime":93698.0,"Column":0}]},{"RandomW":2699856874,"RandomX":808332236,"RandomY":3371160944,"RandomZ":182226446,"StartTime":93964.0,"Objects":[{"StartTime":93964.0,"EndTime":93964.0,"Column":1}]},{"RandomW":3110990203,"RandomX":2699856874,"RandomY":3789399152,"RandomZ":1462741358,"StartTime":94230.0,"Objects":[{"StartTime":94230.0,"EndTime":94495.0,"Column":4},{"StartTime":94230.0,"EndTime":94495.0,"Column":2}]},{"RandomW":2375429180,"RandomX":2098892391,"RandomY":1911053200,"RandomZ":1537665050,"StartTime":94761.0,"Objects":[{"StartTime":94761.0,"EndTime":95026.0,"Column":5},{"StartTime":94761.0,"EndTime":95026.0,"Column":0}]},{"RandomW":391186846,"RandomX":1537665050,"RandomY":2375429180,"RandomZ":609673823,"StartTime":95292.0,"Objects":[{"StartTime":95292.0,"EndTime":96353.0,"Column":1}]},{"RandomW":2078004566,"RandomX":2375429180,"RandomY":609673823,"RandomZ":391186846,"StartTime":96486.0,"Objects":[{"StartTime":96486.0,"EndTime":98478.0,"Column":5}]},{"RandomW":2078004566,"RandomX":2375429180,"RandomY":609673823,"RandomZ":391186846,"StartTime":113345.0,"Objects":[{"StartTime":113345.0,"EndTime":113345.0,"Column":4}]},{"RandomW":2078004566,"RandomX":2375429180,"RandomY":609673823,"RandomZ":391186846,"StartTime":113876.0,"Objects":[{"StartTime":113876.0,"EndTime":113876.0,"Column":1}]},{"RandomW":2078004566,"RandomX":2375429180,"RandomY":609673823,"RandomZ":391186846,"StartTime":114407.0,"Objects":[{"StartTime":114407.0,"EndTime":114407.0,"Column":4}]},{"RandomW":1192288733,"RandomX":609673823,"RandomY":391186846,"RandomZ":2078004566,"StartTime":114672.0,"Objects":[{"StartTime":114672.0,"EndTime":114672.0,"Column":2},{"StartTime":114672.0,"EndTime":114672.0,"Column":3}]},{"RandomW":3569858426,"RandomX":391186846,"RandomY":2078004566,"RandomZ":1192288733,"StartTime":114938.0,"Objects":[{"StartTime":114938.0,"EndTime":114938.0,"Column":2}]},{"RandomW":1262832005,"RandomX":2078004566,"RandomY":1192288733,"RandomZ":3569858426,"StartTime":115203.0,"Objects":[{"StartTime":115203.0,"EndTime":115203.0,"Column":3},{"StartTime":115203.0,"EndTime":115203.0,"Column":4}]},{"RandomW":4002501854,"RandomX":1192288733,"RandomY":3569858426,"RandomZ":1262832005,"StartTime":115469.0,"Objects":[{"StartTime":115469.0,"EndTime":115469.0,"Column":3},{"StartTime":115469.0,"EndTime":115469.0,"Column":4}]},{"RandomW":776953560,"RandomX":3569858426,"RandomY":1262832005,"RandomZ":4002501854,"StartTime":116000.0,"Objects":[{"StartTime":116000.0,"EndTime":116000.0,"Column":3},{"StartTime":116000.0,"EndTime":116000.0,"Column":4}]},{"RandomW":776953560,"RandomX":3569858426,"RandomY":1262832005,"RandomZ":4002501854,"StartTime":116531.0,"Objects":[{"StartTime":116531.0,"EndTime":116531.0,"Column":2},{"StartTime":116531.0,"EndTime":116531.0,"Column":1}]},{"RandomW":3352969228,"RandomX":1262832005,"RandomY":4002501854,"RandomZ":776953560,"StartTime":117062.0,"Objects":[{"StartTime":117062.0,"EndTime":117062.0,"Column":3},{"StartTime":117062.0,"EndTime":117062.0,"Column":4}]},{"RandomW":2796695571,"RandomX":4002501854,"RandomY":776953560,"RandomZ":3352969228,"StartTime":117327.0,"Objects":[{"StartTime":117327.0,"EndTime":117327.0,"Column":2}]},{"RandomW":3269572543,"RandomX":776953560,"RandomY":3352969228,"RandomZ":2796695571,"StartTime":117593.0,"Objects":[{"StartTime":117593.0,"EndTime":117593.0,"Column":4},{"StartTime":117593.0,"EndTime":117593.0,"Column":5}]},{"RandomW":3269572543,"RandomX":776953560,"RandomY":3352969228,"RandomZ":2796695571,"StartTime":118124.0,"Objects":[{"StartTime":118124.0,"EndTime":118124.0,"Column":1},{"StartTime":118124.0,"EndTime":118124.0,"Column":0}]},{"RandomW":3269572543,"RandomX":776953560,"RandomY":3352969228,"RandomZ":2796695571,"StartTime":118655.0,"Objects":[{"StartTime":118655.0,"EndTime":118655.0,"Column":5},{"StartTime":118655.0,"EndTime":118655.0,"Column":4}]},{"RandomW":2517403813,"RandomX":3352969228,"RandomY":2796695571,"RandomZ":3269572543,"StartTime":118920.0,"Objects":[{"StartTime":118920.0,"EndTime":118920.0,"Column":2},{"StartTime":118920.0,"EndTime":118920.0,"Column":3}]},{"RandomW":2210619464,"RandomX":2796695571,"RandomY":3269572543,"RandomZ":2517403813,"StartTime":119186.0,"Objects":[{"StartTime":119186.0,"EndTime":119186.0,"Column":4}]},{"RandomW":3032935051,"RandomX":3269572543,"RandomY":2517403813,"RandomZ":2210619464,"StartTime":119451.0,"Objects":[{"StartTime":119451.0,"EndTime":119451.0,"Column":5},{"StartTime":119451.0,"EndTime":119451.0,"Column":0}]},{"RandomW":2069229539,"RandomX":2517403813,"RandomY":2210619464,"RandomZ":3032935051,"StartTime":119717.0,"Objects":[{"StartTime":119717.0,"EndTime":119717.0,"Column":4},{"StartTime":119717.0,"EndTime":119717.0,"Column":5}]},{"RandomW":2069229539,"RandomX":2517403813,"RandomY":2210619464,"RandomZ":3032935051,"StartTime":120247.0,"Objects":[{"StartTime":120247.0,"EndTime":120247.0,"Column":1},{"StartTime":120247.0,"EndTime":120247.0,"Column":0}]},{"RandomW":2069229539,"RandomX":2517403813,"RandomY":2210619464,"RandomZ":3032935051,"StartTime":120778.0,"Objects":[{"StartTime":120778.0,"EndTime":120778.0,"Column":5},{"StartTime":120778.0,"EndTime":120778.0,"Column":4}]},{"RandomW":2069229539,"RandomX":2517403813,"RandomY":2210619464,"RandomZ":3032935051,"StartTime":121309.0,"Objects":[{"StartTime":121309.0,"EndTime":121309.0,"Column":1},{"StartTime":121309.0,"EndTime":121309.0,"Column":0}]},{"RandomW":2314078604,"RandomX":2210619464,"RandomY":3032935051,"RandomZ":2069229539,"StartTime":121575.0,"Objects":[{"StartTime":121575.0,"EndTime":121575.0,"Column":3}]},{"RandomW":297269721,"RandomX":3032935051,"RandomY":2069229539,"RandomZ":2314078604,"StartTime":121840.0,"Objects":[{"StartTime":121840.0,"EndTime":121840.0,"Column":3},{"StartTime":121840.0,"EndTime":121840.0,"Column":4}]},{"RandomW":297269721,"RandomX":3032935051,"RandomY":2069229539,"RandomZ":2314078604,"StartTime":122371.0,"Objects":[{"StartTime":122371.0,"EndTime":122371.0,"Column":2},{"StartTime":122371.0,"EndTime":122371.0,"Column":1}]},{"RandomW":297269721,"RandomX":3032935051,"RandomY":2069229539,"RandomZ":2314078604,"StartTime":122902.0,"Objects":[{"StartTime":122902.0,"EndTime":122902.0,"Column":4},{"StartTime":122902.0,"EndTime":122902.0,"Column":3}]},{"RandomW":297269721,"RandomX":3032935051,"RandomY":2069229539,"RandomZ":2314078604,"StartTime":123433.0,"Objects":[{"StartTime":123433.0,"EndTime":123433.0,"Column":2},{"StartTime":123433.0,"EndTime":123433.0,"Column":1}]},{"RandomW":2460408790,"RandomX":2069229539,"RandomY":2314078604,"RandomZ":297269721,"StartTime":123699.0,"Objects":[{"StartTime":123699.0,"EndTime":123699.0,"Column":1}]},{"RandomW":1180177558,"RandomX":2314078604,"RandomY":297269721,"RandomZ":2460408790,"StartTime":123964.0,"Objects":[{"StartTime":123964.0,"EndTime":123964.0,"Column":3},{"StartTime":123964.0,"EndTime":123964.0,"Column":4}]},{"RandomW":1180177558,"RandomX":2314078604,"RandomY":297269721,"RandomZ":2460408790,"StartTime":124495.0,"Objects":[{"StartTime":124495.0,"EndTime":124495.0,"Column":2},{"StartTime":124495.0,"EndTime":124495.0,"Column":1}]},{"RandomW":1180177558,"RandomX":2314078604,"RandomY":297269721,"RandomZ":2460408790,"StartTime":125026.0,"Objects":[{"StartTime":125026.0,"EndTime":125026.0,"Column":4},{"StartTime":125026.0,"EndTime":125026.0,"Column":3}]},{"RandomW":1180177558,"RandomX":2314078604,"RandomY":297269721,"RandomZ":2460408790,"StartTime":125557.0,"Objects":[{"StartTime":125557.0,"EndTime":125557.0,"Column":2},{"StartTime":125557.0,"EndTime":125557.0,"Column":1}]},{"RandomW":3204700088,"RandomX":297269721,"RandomY":2460408790,"RandomZ":1180177558,"StartTime":125823.0,"Objects":[{"StartTime":125823.0,"EndTime":125823.0,"Column":2}]},{"RandomW":299141296,"RandomX":2460408790,"RandomY":1180177558,"RandomZ":3204700088,"StartTime":126088.0,"Objects":[{"StartTime":126088.0,"EndTime":126088.0,"Column":3},{"StartTime":126088.0,"EndTime":126088.0,"Column":4}]},{"RandomW":299141296,"RandomX":2460408790,"RandomY":1180177558,"RandomZ":3204700088,"StartTime":126619.0,"Objects":[{"StartTime":126619.0,"EndTime":126619.0,"Column":2},{"StartTime":126619.0,"EndTime":126619.0,"Column":1}]},{"RandomW":299141296,"RandomX":2460408790,"RandomY":1180177558,"RandomZ":3204700088,"StartTime":127150.0,"Objects":[{"StartTime":127150.0,"EndTime":127150.0,"Column":4},{"StartTime":127150.0,"EndTime":127150.0,"Column":3}]},{"RandomW":3037239607,"RandomX":1180177558,"RandomY":3204700088,"RandomZ":299141296,"StartTime":127416.0,"Objects":[{"StartTime":127416.0,"EndTime":127416.0,"Column":4},{"StartTime":127416.0,"EndTime":127416.0,"Column":5}]},{"RandomW":863164324,"RandomX":3204700088,"RandomY":299141296,"RandomZ":3037239607,"StartTime":127681.0,"Objects":[{"StartTime":127681.0,"EndTime":127681.0,"Column":5}]},{"RandomW":2456647781,"RandomX":299141296,"RandomY":3037239607,"RandomZ":863164324,"StartTime":127947.0,"Objects":[{"StartTime":127947.0,"EndTime":127947.0,"Column":4},{"StartTime":127947.0,"EndTime":127947.0,"Column":5}]},{"RandomW":659157904,"RandomX":3037239607,"RandomY":863164324,"RandomZ":2456647781,"StartTime":128212.0,"Objects":[{"StartTime":128212.0,"EndTime":128212.0,"Column":3},{"StartTime":128212.0,"EndTime":128212.0,"Column":4}]},{"RandomW":659157904,"RandomX":3037239607,"RandomY":863164324,"RandomZ":2456647781,"StartTime":128743.0,"Objects":[{"StartTime":128743.0,"EndTime":128743.0,"Column":2},{"StartTime":128743.0,"EndTime":128743.0,"Column":1}]},{"RandomW":659157904,"RandomX":3037239607,"RandomY":863164324,"RandomZ":2456647781,"StartTime":129274.0,"Objects":[{"StartTime":129274.0,"EndTime":129274.0,"Column":4},{"StartTime":129274.0,"EndTime":129274.0,"Column":3}]},{"RandomW":3598260079,"RandomX":863164324,"RandomY":2456647781,"RandomZ":659157904,"StartTime":129540.0,"Objects":[{"StartTime":129540.0,"EndTime":129540.0,"Column":3},{"StartTime":129540.0,"EndTime":129540.0,"Column":4}]},{"RandomW":1930638835,"RandomX":2456647781,"RandomY":659157904,"RandomZ":3598260079,"StartTime":129805.0,"Objects":[{"StartTime":129805.0,"EndTime":129805.0,"Column":1},{"StartTime":129805.0,"EndTime":129805.0,"Column":2}]},{"RandomW":4230333264,"RandomX":1930638835,"RandomY":2319762852,"RandomZ":3807998479,"StartTime":130071.0,"Objects":[{"StartTime":130071.0,"EndTime":130071.0,"Column":2},{"StartTime":130071.0,"EndTime":130071.0,"Column":3}]},{"RandomW":2482386774,"RandomX":4230333264,"RandomY":376688010,"RandomZ":3132506885,"StartTime":132460.0,"Objects":[{"StartTime":132460.0,"EndTime":132990.0,"Column":0}]},{"RandomW":3381449487,"RandomX":3132506885,"RandomY":2482386774,"RandomZ":1092311355,"StartTime":133522.0,"Objects":[{"StartTime":133522.0,"EndTime":134052.0,"Column":3}]},{"RandomW":3812940964,"RandomX":1092311355,"RandomY":3381449487,"RandomZ":3240759120,"StartTime":134318.0,"Objects":[{"StartTime":134318.0,"EndTime":134848.0,"Column":4}]},{"RandomW":2199106412,"RandomX":2014155638,"RandomY":3619038163,"RandomZ":1182263034,"StartTime":135115.0,"Objects":[{"StartTime":135115.0,"EndTime":135380.0,"Column":3},{"StartTime":135115.0,"EndTime":135380.0,"Column":0}]},{"RandomW":4049541057,"RandomX":1182263034,"RandomY":2199106412,"RandomZ":2542868059,"StartTime":135646.0,"Objects":[{"StartTime":135646.0,"EndTime":136176.0,"Column":5}]},{"RandomW":376448389,"RandomX":2542868059,"RandomY":4049541057,"RandomZ":149323558,"StartTime":136708.0,"Objects":[{"StartTime":136708.0,"EndTime":136973.0,"Column":1}]},{"RandomW":10761513,"RandomX":149323558,"RandomY":376448389,"RandomZ":156027614,"StartTime":137239.0,"Objects":[{"StartTime":137239.0,"EndTime":137504.0,"Column":0}]},{"RandomW":2890609580,"RandomX":156027614,"RandomY":10761513,"RandomZ":998270292,"StartTime":137770.0,"Objects":[{"StartTime":137770.0,"EndTime":138566.0,"Column":2}]},{"RandomW":3792858866,"RandomX":998270292,"RandomY":2890609580,"RandomZ":3275622081,"StartTime":138832.0,"Objects":[{"StartTime":138832.0,"EndTime":139097.0,"Column":4}]},{"RandomW":479756469,"RandomX":3792858866,"RandomY":3665829153,"RandomZ":799245198,"StartTime":139363.0,"Objects":[{"StartTime":139363.0,"EndTime":139628.0,"Column":2},{"StartTime":139363.0,"EndTime":139628.0,"Column":1}]},{"RandomW":1559664190,"RandomX":1837897770,"RandomY":3074386351,"RandomZ":2226336565,"StartTime":139894.0,"Objects":[{"StartTime":139894.0,"EndTime":140690.0,"Column":0},{"StartTime":139894.0,"EndTime":140690.0,"Column":4}]},{"RandomW":1370921154,"RandomX":3074386351,"RandomY":2226336565,"RandomZ":1559664190,"StartTime":140955.0,"Objects":[{"StartTime":140955.0,"EndTime":140955.0,"Column":4}]},{"RandomW":12534613,"RandomX":1559664190,"RandomY":1370921154,"RandomZ":495513930,"StartTime":141221.0,"Objects":[{"StartTime":141221.0,"EndTime":141751.0,"Column":3},{"StartTime":141486.0,"EndTime":141486.0,"Column":1},{"StartTime":141751.0,"EndTime":141751.0,"Column":1}]},{"RandomW":1474110729,"RandomX":12534613,"RandomY":3893387802,"RandomZ":226854738,"StartTime":142017.0,"Objects":[{"StartTime":142017.0,"EndTime":142017.0,"Column":2},{"StartTime":142017.0,"EndTime":142017.0,"Column":3}]},{"RandomW":3883366092,"RandomX":1474110729,"RandomY":2911002956,"RandomZ":3337209428,"StartTime":142283.0,"Objects":[{"StartTime":142283.0,"EndTime":142548.0,"Column":4}]},{"RandomW":1868157439,"RandomX":3883366092,"RandomY":1497166406,"RandomZ":3876220972,"StartTime":142814.0,"Objects":[{"StartTime":142814.0,"EndTime":143079.0,"Column":5}]},{"RandomW":868486094,"RandomX":1497166406,"RandomY":3876220972,"RandomZ":1868157439,"StartTime":143345.0,"Objects":[{"StartTime":143345.0,"EndTime":143345.0,"Column":2}]},{"RandomW":2379505970,"RandomX":3876220972,"RandomY":1868157439,"RandomZ":868486094,"StartTime":143610.0,"Objects":[{"StartTime":143610.0,"EndTime":143610.0,"Column":2}]},{"RandomW":971762612,"RandomX":1868157439,"RandomY":868486094,"RandomZ":2379505970,"StartTime":143876.0,"Objects":[{"StartTime":143876.0,"EndTime":143876.0,"Column":4}]},{"RandomW":2333467129,"RandomX":2379505970,"RandomY":971762612,"RandomZ":2560365407,"StartTime":144141.0,"Objects":[{"StartTime":144141.0,"EndTime":144671.0,"Column":0}]},{"RandomW":3275109659,"RandomX":2560365407,"RandomY":2333467129,"RandomZ":2783370328,"StartTime":145203.0,"Objects":[{"StartTime":145203.0,"EndTime":145468.0,"Column":3}]},{"RandomW":2675369072,"RandomX":2783370328,"RandomY":3275109659,"RandomZ":3142107337,"StartTime":145734.0,"Objects":[{"StartTime":145734.0,"EndTime":145999.0,"Column":1}]},{"RandomW":2114821552,"RandomX":3142107337,"RandomY":2675369072,"RandomZ":216133594,"StartTime":146265.0,"Objects":[{"StartTime":146265.0,"EndTime":146795.0,"Column":5}]},{"RandomW":2210288688,"RandomX":2675369072,"RandomY":216133594,"RandomZ":2114821552,"StartTime":147062.0,"Objects":[{"StartTime":147062.0,"EndTime":147062.0,"Column":3}]},{"RandomW":2824847566,"RandomX":2114821552,"RandomY":2210288688,"RandomZ":2881713491,"StartTime":147327.0,"Objects":[{"StartTime":147327.0,"EndTime":147592.0,"Column":1}]},{"RandomW":3418617049,"RandomX":2881713491,"RandomY":2824847566,"RandomZ":3131910248,"StartTime":147858.0,"Objects":[{"StartTime":147858.0,"EndTime":148123.0,"Column":3}]},{"RandomW":4264037536,"RandomX":3418617049,"RandomY":2065328415,"RandomZ":756387586,"StartTime":148389.0,"Objects":[{"StartTime":148389.0,"EndTime":149450.0,"Column":2},{"StartTime":148389.0,"EndTime":149450.0,"Column":5}]},{"RandomW":714689152,"RandomX":2065328415,"RandomY":756387586,"RandomZ":4264037536,"StartTime":149717.0,"Objects":[{"StartTime":149717.0,"EndTime":149717.0,"Column":2}]},{"RandomW":2187562077,"RandomX":756387586,"RandomY":4264037536,"RandomZ":714689152,"StartTime":149982.0,"Objects":[{"StartTime":149982.0,"EndTime":149982.0,"Column":1},{"StartTime":149982.0,"EndTime":149982.0,"Column":2}]},{"RandomW":59731596,"RandomX":4264037536,"RandomY":714689152,"RandomZ":2187562077,"StartTime":150247.0,"Objects":[{"StartTime":150247.0,"EndTime":150247.0,"Column":0}]},{"RandomW":3179032401,"RandomX":714689152,"RandomY":2187562077,"RandomZ":59731596,"StartTime":150513.0,"Objects":[{"StartTime":150513.0,"EndTime":150513.0,"Column":1}]},{"RandomW":1565638452,"RandomX":2187562077,"RandomY":59731596,"RandomZ":3179032401,"StartTime":150778.0,"Objects":[{"StartTime":150778.0,"EndTime":150778.0,"Column":2}]},{"RandomW":3285111207,"RandomX":59731596,"RandomY":3179032401,"RandomZ":1565638452,"StartTime":151044.0,"Objects":[{"StartTime":151044.0,"EndTime":151044.0,"Column":3},{"StartTime":151044.0,"EndTime":151044.0,"Column":4}]},{"RandomW":3142401116,"RandomX":3179032401,"RandomY":1565638452,"RandomZ":3285111207,"StartTime":151309.0,"Objects":[{"StartTime":151309.0,"EndTime":151309.0,"Column":4}]},{"RandomW":2191101353,"RandomX":3142401116,"RandomY":3877079747,"RandomZ":930029834,"StartTime":151575.0,"Objects":[{"StartTime":151575.0,"EndTime":152105.0,"Column":2},{"StartTime":151575.0,"EndTime":152105.0,"Column":0}]},{"RandomW":1171726387,"RandomX":2191101353,"RandomY":1357180538,"RandomZ":201209655,"StartTime":152637.0,"Objects":[{"StartTime":152637.0,"EndTime":152902.0,"Column":3}]},{"RandomW":2089660876,"RandomX":201209655,"RandomY":1171726387,"RandomZ":191699429,"StartTime":153168.0,"Objects":[{"StartTime":153168.0,"EndTime":153698.0,"Column":5}]},{"RandomW":2251323109,"RandomX":1171726387,"RandomY":191699429,"RandomZ":2089660876,"StartTime":153964.0,"Objects":[{"StartTime":153964.0,"EndTime":153964.0,"Column":3}]},{"RandomW":147408153,"RandomX":2251323109,"RandomY":2048526504,"RandomZ":433820735,"StartTime":154230.0,"Objects":[{"StartTime":154230.0,"EndTime":154230.0,"Column":0},{"StartTime":154230.0,"EndTime":154230.0,"Column":5}]},{"RandomW":223059387,"RandomX":2048526504,"RandomY":433820735,"RandomZ":147408153,"StartTime":154495.0,"Objects":[{"StartTime":154495.0,"EndTime":154495.0,"Column":3}]},{"RandomW":1644267862,"RandomX":147408153,"RandomY":223059387,"RandomZ":2814282738,"StartTime":154761.0,"Objects":[{"StartTime":154761.0,"EndTime":155026.0,"Column":4}]},{"RandomW":585628331,"RandomX":1644267862,"RandomY":547547522,"RandomZ":1901399656,"StartTime":155292.0,"Objects":[{"StartTime":155292.0,"EndTime":155292.0,"Column":0},{"StartTime":155292.0,"EndTime":155292.0,"Column":5}]},{"RandomW":1287818392,"RandomX":547547522,"RandomY":1901399656,"RandomZ":585628331,"StartTime":155557.0,"Objects":[{"StartTime":155557.0,"EndTime":155557.0,"Column":1}]},{"RandomW":3879046214,"RandomX":2065404539,"RandomY":2732913982,"RandomZ":3217781099,"StartTime":155823.0,"Objects":[{"StartTime":155823.0,"EndTime":156088.0,"Column":2},{"StartTime":155823.0,"EndTime":156088.0,"Column":4}]},{"RandomW":3318878889,"RandomX":3217781099,"RandomY":3879046214,"RandomZ":1075466897,"StartTime":156354.0,"Objects":[{"StartTime":156354.0,"EndTime":156619.0,"Column":3}]},{"RandomW":1785367685,"RandomX":1075466897,"RandomY":3318878889,"RandomZ":561406801,"StartTime":156885.0,"Objects":[{"StartTime":156885.0,"EndTime":157415.0,"Column":4}]},{"RandomW":2909067134,"RandomX":561406801,"RandomY":1785367685,"RandomZ":4168537475,"StartTime":157947.0,"Objects":[{"StartTime":157947.0,"EndTime":157947.0,"Column":5},{"StartTime":157947.0,"EndTime":157947.0,"Column":2}]},{"RandomW":1067074920,"RandomX":1785367685,"RandomY":4168537475,"RandomZ":2909067134,"StartTime":158212.0,"Objects":[{"StartTime":158212.0,"EndTime":158212.0,"Column":4}]},{"RandomW":27977914,"RandomX":4168537475,"RandomY":2909067134,"RandomZ":1067074920,"StartTime":158478.0,"Objects":[{"StartTime":158478.0,"EndTime":158478.0,"Column":5},{"StartTime":158478.0,"EndTime":158478.0,"Column":0}]},{"RandomW":1329528769,"RandomX":2909067134,"RandomY":1067074920,"RandomZ":27977914,"StartTime":158743.0,"Objects":[{"StartTime":158743.0,"EndTime":158743.0,"Column":4}]},{"RandomW":3295284863,"RandomX":1067074920,"RandomY":27977914,"RandomZ":1329528769,"StartTime":159009.0,"Objects":[{"StartTime":159009.0,"EndTime":159009.0,"Column":5}]},{"RandomW":691446431,"RandomX":27977914,"RandomY":1329528769,"RandomZ":3295284863,"StartTime":159540.0,"Objects":[{"StartTime":159540.0,"EndTime":159540.0,"Column":3},{"StartTime":159540.0,"EndTime":159540.0,"Column":4}]},{"RandomW":3354872060,"RandomX":3295284863,"RandomY":691446431,"RandomZ":2140106811,"StartTime":159805.0,"Objects":[{"StartTime":159805.0,"EndTime":159805.0,"Column":2},{"StartTime":159805.0,"EndTime":159805.0,"Column":3}]},{"RandomW":1400553355,"RandomX":691446431,"RandomY":2140106811,"RandomZ":3354872060,"StartTime":160071.0,"Objects":[{"StartTime":160071.0,"EndTime":160071.0,"Column":2}]},{"RandomW":1400553355,"RandomX":691446431,"RandomY":2140106811,"RandomZ":3354872060,"StartTime":160601.0,"Objects":[{"StartTime":160601.0,"EndTime":160601.0,"Column":3}]},{"RandomW":3485781281,"RandomX":2140106811,"RandomY":3354872060,"RandomZ":1400553355,"StartTime":160867.0,"Objects":[{"StartTime":160867.0,"EndTime":160867.0,"Column":3}]},{"RandomW":3053679463,"RandomX":1400553355,"RandomY":3485781281,"RandomZ":3419304522,"StartTime":161132.0,"Objects":[{"StartTime":161132.0,"EndTime":161397.0,"Column":2}]},{"RandomW":3645336111,"RandomX":3419304522,"RandomY":3053679463,"RandomZ":805504203,"StartTime":161663.0,"Objects":[{"StartTime":161663.0,"EndTime":162193.0,"Column":4}]},{"RandomW":1638076271,"RandomX":3053679463,"RandomY":805504203,"RandomZ":3645336111,"StartTime":162460.0,"Objects":[{"StartTime":162460.0,"EndTime":162460.0,"Column":3}]},{"RandomW":107981020,"RandomX":1638076271,"RandomY":3432435831,"RandomZ":3835408498,"StartTime":162725.0,"Objects":[{"StartTime":162725.0,"EndTime":162725.0,"Column":0},{"StartTime":162725.0,"EndTime":162725.0,"Column":5}]},{"RandomW":94467567,"RandomX":3835408498,"RandomY":107981020,"RandomZ":2144208649,"StartTime":163256.0,"Objects":[{"StartTime":163256.0,"EndTime":163256.0,"Column":4},{"StartTime":163256.0,"EndTime":163256.0,"Column":0}]},{"RandomW":1015041289,"RandomX":107981020,"RandomY":2144208649,"RandomZ":94467567,"StartTime":163522.0,"Objects":[{"StartTime":163522.0,"EndTime":163522.0,"Column":3}]},{"RandomW":2029876639,"RandomX":1204955917,"RandomY":1210817201,"RandomZ":1177260118,"StartTime":163787.0,"Objects":[{"StartTime":163787.0,"EndTime":164052.0,"Column":5}]},{"RandomW":3125496505,"RandomX":1177260118,"RandomY":2029876639,"RandomZ":2929832910,"StartTime":164318.0,"Objects":[{"StartTime":164318.0,"EndTime":164583.0,"Column":2}]},{"RandomW":2426857185,"RandomX":3125496505,"RandomY":2700661894,"RandomZ":859446411,"StartTime":164849.0,"Objects":[{"StartTime":164849.0,"EndTime":165114.0,"Column":0}]},{"RandomW":4116661924,"RandomX":2426857185,"RandomY":1884842190,"RandomZ":375578279,"StartTime":165380.0,"Objects":[{"StartTime":165380.0,"EndTime":165910.0,"Column":1},{"StartTime":165380.0,"EndTime":165910.0,"Column":5}]},{"RandomW":3787729819,"RandomX":375578279,"RandomY":4116661924,"RandomZ":1382622976,"StartTime":166442.0,"Objects":[{"StartTime":166442.0,"EndTime":166972.0,"Column":4}]},{"RandomW":3780331234,"RandomX":4116661924,"RandomY":1382622976,"RandomZ":3787729819,"StartTime":167239.0,"Objects":[{"StartTime":167239.0,"EndTime":167239.0,"Column":3}]},{"RandomW":891570220,"RandomX":3780331234,"RandomY":3996538378,"RandomZ":4118560235,"StartTime":167504.0,"Objects":[{"StartTime":167504.0,"EndTime":168034.0,"Column":5},{"StartTime":167504.0,"EndTime":168034.0,"Column":2}]},{"RandomW":1312521276,"RandomX":3996538378,"RandomY":4118560235,"RandomZ":891570220,"StartTime":168301.0,"Objects":[{"StartTime":168301.0,"EndTime":168301.0,"Column":0}]},{"RandomW":316798455,"RandomX":4118560235,"RandomY":891570220,"RandomZ":1312521276,"StartTime":168566.0,"Objects":[{"StartTime":168566.0,"EndTime":168566.0,"Column":2},{"StartTime":168566.0,"EndTime":168566.0,"Column":3}]},{"RandomW":107348261,"RandomX":891570220,"RandomY":1312521276,"RandomZ":316798455,"StartTime":168832.0,"Objects":[{"StartTime":168832.0,"EndTime":168832.0,"Column":1}]},{"RandomW":286543085,"RandomX":1312521276,"RandomY":316798455,"RandomZ":107348261,"StartTime":169097.0,"Objects":[{"StartTime":169097.0,"EndTime":169097.0,"Column":1},{"StartTime":169097.0,"EndTime":169097.0,"Column":2}]},{"RandomW":2220558447,"RandomX":316798455,"RandomY":107348261,"RandomZ":286543085,"StartTime":169363.0,"Objects":[{"StartTime":169363.0,"EndTime":169363.0,"Column":2}]},{"RandomW":2567445342,"RandomX":107348261,"RandomY":286543085,"RandomZ":2220558447,"StartTime":169628.0,"Objects":[{"StartTime":169628.0,"EndTime":169628.0,"Column":1},{"StartTime":169628.0,"EndTime":169628.0,"Column":2}]},{"RandomW":2941341299,"RandomX":286543085,"RandomY":2220558447,"RandomZ":2567445342,"StartTime":170159.0,"Objects":[{"StartTime":170159.0,"EndTime":170159.0,"Column":3},{"StartTime":170159.0,"EndTime":170159.0,"Column":4}]},{"RandomW":2941341299,"RandomX":286543085,"RandomY":2220558447,"RandomZ":2567445342,"StartTime":170424.0,"Objects":[{"StartTime":170424.0,"EndTime":170424.0,"Column":2},{"StartTime":170424.0,"EndTime":170424.0,"Column":1}]},{"RandomW":1087727581,"RandomX":2567445342,"RandomY":2941341299,"RandomZ":479267920,"StartTime":170690.0,"Objects":[{"StartTime":170690.0,"EndTime":171220.0,"Column":3}]},{"RandomW":2581485170,"RandomX":2941341299,"RandomY":479267920,"RandomZ":1087727581,"StartTime":171486.0,"Objects":[{"StartTime":171486.0,"EndTime":171486.0,"Column":5}]},{"RandomW":683596203,"RandomX":1087727581,"RandomY":2581485170,"RandomZ":3168383468,"StartTime":171752.0,"Objects":[{"StartTime":171752.0,"EndTime":172282.0,"Column":1}]},{"RandomW":3284056302,"RandomX":2581485170,"RandomY":3168383468,"RandomZ":683596203,"StartTime":172548.0,"Objects":[{"StartTime":172548.0,"EndTime":172548.0,"Column":2}]},{"RandomW":2830633773,"RandomX":3168383468,"RandomY":683596203,"RandomZ":3284056302,"StartTime":172814.0,"Objects":[{"StartTime":172814.0,"EndTime":172814.0,"Column":3},{"StartTime":172814.0,"EndTime":172814.0,"Column":4}]},{"RandomW":3651115271,"RandomX":683596203,"RandomY":3284056302,"RandomZ":2830633773,"StartTime":173079.0,"Objects":[{"StartTime":173079.0,"EndTime":173079.0,"Column":3}]},{"RandomW":120746014,"RandomX":3284056302,"RandomY":2830633773,"RandomZ":3651115271,"StartTime":173345.0,"Objects":[{"StartTime":173345.0,"EndTime":173345.0,"Column":3},{"StartTime":173345.0,"EndTime":173345.0,"Column":4}]},{"RandomW":830325214,"RandomX":2830633773,"RandomY":3651115271,"RandomZ":120746014,"StartTime":173610.0,"Objects":[{"StartTime":173610.0,"EndTime":173610.0,"Column":4}]},{"RandomW":1509180863,"RandomX":3651115271,"RandomY":120746014,"RandomZ":830325214,"StartTime":173876.0,"Objects":[{"StartTime":173876.0,"EndTime":173876.0,"Column":3},{"StartTime":173876.0,"EndTime":173876.0,"Column":4}]},{"RandomW":2233493011,"RandomX":3902833961,"RandomY":923589330,"RandomZ":3425613873,"StartTime":174407.0,"Objects":[{"StartTime":174407.0,"EndTime":174672.0,"Column":2},{"StartTime":174407.0,"EndTime":174672.0,"Column":0}]},{"RandomW":2517643905,"RandomX":1207989122,"RandomY":993303558,"RandomZ":3011821377,"StartTime":174938.0,"Objects":[{"StartTime":174938.0,"EndTime":175468.0,"Column":3},{"StartTime":174938.0,"EndTime":175468.0,"Column":1}]},{"RandomW":3720863650,"RandomX":993303558,"RandomY":3011821377,"RandomZ":2517643905,"StartTime":175734.0,"Objects":[{"StartTime":175734.0,"EndTime":175734.0,"Column":2}]},{"RandomW":3563355415,"RandomX":2517643905,"RandomY":3720863650,"RandomZ":1116519600,"StartTime":176000.0,"Objects":[{"StartTime":176000.0,"EndTime":176530.0,"Column":3}]},{"RandomW":3287800096,"RandomX":3720863650,"RandomY":1116519600,"RandomZ":3563355415,"StartTime":176796.0,"Objects":[{"StartTime":176796.0,"EndTime":176796.0,"Column":3}]},{"RandomW":539898931,"RandomX":1116519600,"RandomY":3563355415,"RandomZ":3287800096,"StartTime":177062.0,"Objects":[{"StartTime":177062.0,"EndTime":177062.0,"Column":2},{"StartTime":177062.0,"EndTime":177062.0,"Column":3}]},{"RandomW":123758010,"RandomX":3563355415,"RandomY":3287800096,"RandomZ":539898931,"StartTime":177327.0,"Objects":[{"StartTime":177327.0,"EndTime":177327.0,"Column":4}]},{"RandomW":4028312708,"RandomX":3287800096,"RandomY":539898931,"RandomZ":123758010,"StartTime":177593.0,"Objects":[{"StartTime":177593.0,"EndTime":177593.0,"Column":2},{"StartTime":177593.0,"EndTime":177593.0,"Column":3}]},{"RandomW":2371409278,"RandomX":539898931,"RandomY":123758010,"RandomZ":4028312708,"StartTime":177858.0,"Objects":[{"StartTime":177858.0,"EndTime":177858.0,"Column":3}]},{"RandomW":3699828554,"RandomX":123758010,"RandomY":4028312708,"RandomZ":2371409278,"StartTime":178124.0,"Objects":[{"StartTime":178124.0,"EndTime":178124.0,"Column":2},{"StartTime":178124.0,"EndTime":178124.0,"Column":3}]},{"RandomW":4053363780,"RandomX":2371409278,"RandomY":3699828554,"RandomZ":3637445845,"StartTime":178655.0,"Objects":[{"StartTime":178655.0,"EndTime":178920.0,"Column":5}]},{"RandomW":1366734997,"RandomX":3637445845,"RandomY":4053363780,"RandomZ":3122766892,"StartTime":179186.0,"Objects":[{"StartTime":179186.0,"EndTime":179716.0,"Column":3}]},{"RandomW":2085192570,"RandomX":1366734997,"RandomY":4047501250,"RandomZ":3422445293,"StartTime":179982.0,"Objects":[{"StartTime":179982.0,"EndTime":179982.0,"Column":3},{"StartTime":179982.0,"EndTime":179982.0,"Column":5}]},{"RandomW":2526042960,"RandomX":3422445293,"RandomY":2085192570,"RandomZ":2552180342,"StartTime":180247.0,"Objects":[{"StartTime":180247.0,"EndTime":180777.0,"Column":1}]},{"RandomW":2946528857,"RandomX":2085192570,"RandomY":2552180342,"RandomZ":2526042960,"StartTime":181044.0,"Objects":[{"StartTime":181044.0,"EndTime":181044.0,"Column":2}]},{"RandomW":4275012500,"RandomX":2526042960,"RandomY":2946528857,"RandomZ":2680316548,"StartTime":181309.0,"Objects":[{"StartTime":181309.0,"EndTime":181574.0,"Column":5}]},{"RandomW":716767862,"RandomX":1177533555,"RandomY":3396673648,"RandomZ":1210370441,"StartTime":181840.0,"Objects":[{"StartTime":181840.0,"EndTime":182105.0,"Column":3},{"StartTime":181840.0,"EndTime":182105.0,"Column":2}]},{"RandomW":1918581647,"RandomX":1210370441,"RandomY":716767862,"RandomZ":290385782,"StartTime":182371.0,"Objects":[{"StartTime":182371.0,"EndTime":182636.0,"Column":5}]},{"RandomW":2554770024,"RandomX":1918581647,"RandomY":475913420,"RandomZ":4262840195,"StartTime":182902.0,"Objects":[{"StartTime":182902.0,"EndTime":183432.0,"Column":1}]},{"RandomW":862610860,"RandomX":475913420,"RandomY":4262840195,"RandomZ":2554770024,"StartTime":183699.0,"Objects":[{"StartTime":183699.0,"EndTime":185557.0,"Column":2}]},{"RandomW":3240322225,"RandomX":4262840195,"RandomY":2554770024,"RandomZ":862610860,"StartTime":202017.0,"Objects":[{"StartTime":202017.0,"EndTime":202017.0,"Column":0}]},{"RandomW":2438630089,"RandomX":2554770024,"RandomY":862610860,"RandomZ":3240322225,"StartTime":202283.0,"Objects":[{"StartTime":202283.0,"EndTime":202283.0,"Column":1}]},{"RandomW":1543895637,"RandomX":3240322225,"RandomY":2438630089,"RandomZ":1008910200,"StartTime":202548.0,"Objects":[{"StartTime":202548.0,"EndTime":203078.0,"Column":4}]},{"RandomW":2262375304,"RandomX":2438630089,"RandomY":1008910200,"RandomZ":1543895637,"StartTime":203345.0,"Objects":[{"StartTime":203345.0,"EndTime":203345.0,"Column":2}]},{"RandomW":3932191533,"RandomX":1543895637,"RandomY":2262375304,"RandomZ":3281044824,"StartTime":203610.0,"Objects":[{"StartTime":203610.0,"EndTime":203875.0,"Column":4}]},{"RandomW":2456816417,"RandomX":3932191533,"RandomY":2579817318,"RandomZ":3616517773,"StartTime":204141.0,"Objects":[{"StartTime":204141.0,"EndTime":204406.0,"Column":0}]},{"RandomW":1863357795,"RandomX":2456816417,"RandomY":2065740625,"RandomZ":3309416576,"StartTime":204672.0,"Objects":[{"StartTime":204672.0,"EndTime":205202.0,"Column":3},{"StartTime":204672.0,"EndTime":205202.0,"Column":5}]},{"RandomW":66010220,"RandomX":3309416576,"RandomY":1863357795,"RandomZ":2100015779,"StartTime":205469.0,"Objects":[{"StartTime":205469.0,"EndTime":205469.0,"Column":4},{"StartTime":205469.0,"EndTime":205469.0,"Column":0}]},{"RandomW":548562611,"RandomX":2100015779,"RandomY":66010220,"RandomZ":3420604705,"StartTime":205734.0,"Objects":[{"StartTime":205734.0,"EndTime":205999.0,"Column":1}]},{"RandomW":2052728473,"RandomX":3420604705,"RandomY":548562611,"RandomZ":2913964,"StartTime":206265.0,"Objects":[{"StartTime":206265.0,"EndTime":206530.0,"Column":5}]},{"RandomW":1944462115,"RandomX":2052728473,"RandomY":2737357746,"RandomZ":270315162,"StartTime":206796.0,"Objects":[{"StartTime":206796.0,"EndTime":206796.0,"Column":2},{"StartTime":206796.0,"EndTime":206796.0,"Column":3}]},{"RandomW":3626216744,"RandomX":2737357746,"RandomY":270315162,"RandomZ":1944462115,"StartTime":207062.0,"Objects":[{"StartTime":207062.0,"EndTime":207062.0,"Column":5}]},{"RandomW":1039388877,"RandomX":270315162,"RandomY":1944462115,"RandomZ":3626216744,"StartTime":207327.0,"Objects":[{"StartTime":207327.0,"EndTime":207327.0,"Column":4}]},{"RandomW":3362701719,"RandomX":1944462115,"RandomY":3626216744,"RandomZ":1039388877,"StartTime":207593.0,"Objects":[{"StartTime":207593.0,"EndTime":207593.0,"Column":3}]},{"RandomW":3968495235,"RandomX":3362701719,"RandomY":2329091202,"RandomZ":1331472925,"StartTime":207858.0,"Objects":[{"StartTime":207858.0,"EndTime":208388.0,"Column":5}]},{"RandomW":1381394684,"RandomX":2329091202,"RandomY":1331472925,"RandomZ":3968495235,"StartTime":208655.0,"Objects":[{"StartTime":208655.0,"EndTime":208655.0,"Column":5}]},{"RandomW":1435798214,"RandomX":1381394684,"RandomY":1081301304,"RandomZ":3939835753,"StartTime":208920.0,"Objects":[{"StartTime":208920.0,"EndTime":209450.0,"Column":4}]},{"RandomW":3026458880,"RandomX":1081301304,"RandomY":3939835753,"RandomZ":1435798214,"StartTime":209717.0,"Objects":[{"StartTime":209717.0,"EndTime":209717.0,"Column":5}]},{"RandomW":3713738018,"RandomX":3026458880,"RandomY":1845767213,"RandomZ":745035987,"StartTime":209982.0,"Objects":[{"StartTime":209982.0,"EndTime":210512.0,"Column":2},{"StartTime":209982.0,"EndTime":210512.0,"Column":4}]},{"RandomW":1231260560,"RandomX":1845767213,"RandomY":745035987,"RandomZ":3713738018,"StartTime":210778.0,"Objects":[{"StartTime":210778.0,"EndTime":210778.0,"Column":4}]},{"RandomW":105489365,"RandomX":745035987,"RandomY":3713738018,"RandomZ":1231260560,"StartTime":211044.0,"Objects":[{"StartTime":211044.0,"EndTime":211044.0,"Column":4}]},{"RandomW":1753861391,"RandomX":3713738018,"RandomY":1231260560,"RandomZ":105489365,"StartTime":211309.0,"Objects":[{"StartTime":211309.0,"EndTime":211309.0,"Column":2}]},{"RandomW":966114829,"RandomX":105489365,"RandomY":1753861391,"RandomZ":1828685577,"StartTime":211575.0,"Objects":[{"StartTime":211575.0,"EndTime":211575.0,"Column":3},{"StartTime":211575.0,"EndTime":211575.0,"Column":2}]},{"RandomW":1431749195,"RandomX":1836275468,"RandomY":1290011463,"RandomZ":1159621643,"StartTime":211840.0,"Objects":[{"StartTime":211840.0,"EndTime":212370.0,"Column":5},{"StartTime":211840.0,"EndTime":212370.0,"Column":4}]},{"RandomW":3472418283,"RandomX":1159621643,"RandomY":1431749195,"RandomZ":2724869338,"StartTime":212637.0,"Objects":[{"StartTime":212637.0,"EndTime":212902.0,"Column":3}]},{"RandomW":1755864208,"RandomX":3472418283,"RandomY":2016458251,"RandomZ":2610391004,"StartTime":213168.0,"Objects":[{"StartTime":213168.0,"EndTime":213698.0,"Column":1},{"StartTime":213168.0,"EndTime":213698.0,"Column":4}]},{"RandomW":1635138515,"RandomX":2016458251,"RandomY":2610391004,"RandomZ":1755864208,"StartTime":213964.0,"Objects":[{"StartTime":213964.0,"EndTime":213964.0,"Column":3}]},{"RandomW":3162662082,"RandomX":1755864208,"RandomY":1635138515,"RandomZ":2617989400,"StartTime":214230.0,"Objects":[{"StartTime":214230.0,"EndTime":214495.0,"Column":2}]},{"RandomW":1184692914,"RandomX":2617989400,"RandomY":3162662082,"RandomZ":2531582750,"StartTime":214761.0,"Objects":[{"StartTime":214761.0,"EndTime":215026.0,"Column":3}]},{"RandomW":798124101,"RandomX":2531582750,"RandomY":1184692914,"RandomZ":2157553888,"StartTime":215292.0,"Objects":[{"StartTime":215292.0,"EndTime":215557.0,"Column":2}]},{"RandomW":1923400471,"RandomX":798124101,"RandomY":2665448122,"RandomZ":1060614841,"StartTime":215823.0,"Objects":[{"StartTime":215823.0,"EndTime":216088.0,"Column":5}]},{"RandomW":775950648,"RandomX":1923400471,"RandomY":3469237574,"RandomZ":2892029047,"StartTime":216354.0,"Objects":[{"StartTime":216354.0,"EndTime":216354.0,"Column":1},{"StartTime":216354.0,"EndTime":216354.0,"Column":4}]},{"RandomW":1321234603,"RandomX":4127626210,"RandomY":1546611249,"RandomZ":1925740893,"StartTime":216885.0,"Objects":[{"StartTime":216885.0,"EndTime":217150.0,"Column":5},{"StartTime":216885.0,"EndTime":217150.0,"Column":3}]},{"RandomW":2881678930,"RandomX":1925740893,"RandomY":1321234603,"RandomZ":2358993682,"StartTime":217416.0,"Objects":[{"StartTime":217416.0,"EndTime":217946.0,"Column":2}]},{"RandomW":2599512294,"RandomX":1321234603,"RandomY":2358993682,"RandomZ":2881678930,"StartTime":218212.0,"Objects":[{"StartTime":218212.0,"EndTime":218212.0,"Column":1}]},{"RandomW":2150464549,"RandomX":2881678930,"RandomY":2599512294,"RandomZ":3623425595,"StartTime":218478.0,"Objects":[{"StartTime":218478.0,"EndTime":219008.0,"Column":0}]},{"RandomW":763775798,"RandomX":3623425595,"RandomY":2150464549,"RandomZ":1008837132,"StartTime":219274.0,"Objects":[{"StartTime":219274.0,"EndTime":221132.0,"Column":2}]},{"RandomW":3656799832,"RandomX":1008837132,"RandomY":763775798,"RandomZ":852609139,"StartTime":221663.0,"Objects":[{"StartTime":221663.0,"EndTime":222193.0,"Column":4}]},{"RandomW":4147545979,"RandomX":852609139,"RandomY":3656799832,"RandomZ":3908484776,"StartTime":222460.0,"Objects":[{"StartTime":222460.0,"EndTime":222460.0,"Column":2},{"StartTime":222460.0,"EndTime":222460.0,"Column":5}]},{"RandomW":540508179,"RandomX":3908484776,"RandomY":4147545979,"RandomZ":1259887550,"StartTime":222725.0,"Objects":[{"StartTime":222725.0,"EndTime":223255.0,"Column":1}]},{"RandomW":1042752714,"RandomX":1259887550,"RandomY":540508179,"RandomZ":2104064323,"StartTime":223522.0,"Objects":[{"StartTime":223522.0,"EndTime":223522.0,"Column":5},{"StartTime":223522.0,"EndTime":223522.0,"Column":2}]},{"RandomW":3077262619,"RandomX":540508179,"RandomY":2104064323,"RandomZ":1042752714,"StartTime":223787.0,"Objects":[{"StartTime":223787.0,"EndTime":223787.0,"Column":3},{"StartTime":223787.0,"EndTime":223787.0,"Column":4}]},{"RandomW":734033149,"RandomX":2104064323,"RandomY":1042752714,"RandomZ":3077262619,"StartTime":224053.0,"Objects":[{"StartTime":224053.0,"EndTime":224053.0,"Column":4}]},{"RandomW":492155815,"RandomX":1042752714,"RandomY":3077262619,"RandomZ":734033149,"StartTime":224318.0,"Objects":[{"StartTime":224318.0,"EndTime":224318.0,"Column":4},{"StartTime":224318.0,"EndTime":224318.0,"Column":5}]},{"RandomW":441697715,"RandomX":3077262619,"RandomY":734033149,"RandomZ":492155815,"StartTime":224584.0,"Objects":[{"StartTime":224584.0,"EndTime":224584.0,"Column":3}]},{"RandomW":4156379255,"RandomX":734033149,"RandomY":492155815,"RandomZ":441697715,"StartTime":224849.0,"Objects":[{"StartTime":224849.0,"EndTime":224849.0,"Column":4},{"StartTime":224849.0,"EndTime":224849.0,"Column":5}]},{"RandomW":3757225441,"RandomX":492155815,"RandomY":441697715,"RandomZ":4156379255,"StartTime":225380.0,"Objects":[{"StartTime":225380.0,"EndTime":225380.0,"Column":2},{"StartTime":225380.0,"EndTime":225380.0,"Column":3}]},{"RandomW":3757225441,"RandomX":492155815,"RandomY":441697715,"RandomZ":4156379255,"StartTime":225646.0,"Objects":[{"StartTime":225646.0,"EndTime":225646.0,"Column":3},{"StartTime":225646.0,"EndTime":225646.0,"Column":2}]},{"RandomW":2225043333,"RandomX":3950035756,"RandomY":4132636893,"RandomZ":3158636107,"StartTime":225911.0,"Objects":[{"StartTime":225911.0,"EndTime":226441.0,"Column":5},{"StartTime":225911.0,"EndTime":226441.0,"Column":0}]},{"RandomW":479006094,"RandomX":2225043333,"RandomY":3919293849,"RandomZ":2279622039,"StartTime":226708.0,"Objects":[{"StartTime":226708.0,"EndTime":226708.0,"Column":0},{"StartTime":226708.0,"EndTime":226708.0,"Column":1}]},{"RandomW":3529234379,"RandomX":479006094,"RandomY":1674670789,"RandomZ":1460857923,"StartTime":226973.0,"Objects":[{"StartTime":226973.0,"EndTime":227503.0,"Column":4},{"StartTime":226973.0,"EndTime":227503.0,"Column":3}]},{"RandomW":2798539123,"RandomX":1674670789,"RandomY":1460857923,"RandomZ":3529234379,"StartTime":227770.0,"Objects":[{"StartTime":227770.0,"EndTime":227770.0,"Column":3}]},{"RandomW":1315002421,"RandomX":1460857923,"RandomY":3529234379,"RandomZ":2798539123,"StartTime":228035.0,"Objects":[{"StartTime":228035.0,"EndTime":228035.0,"Column":2},{"StartTime":228035.0,"EndTime":228035.0,"Column":3}]},{"RandomW":2396116302,"RandomX":3529234379,"RandomY":2798539123,"RandomZ":1315002421,"StartTime":228301.0,"Objects":[{"StartTime":228301.0,"EndTime":228301.0,"Column":1}]},{"RandomW":2184752848,"RandomX":2798539123,"RandomY":1315002421,"RandomZ":2396116302,"StartTime":228566.0,"Objects":[{"StartTime":228566.0,"EndTime":228566.0,"Column":2},{"StartTime":228566.0,"EndTime":228566.0,"Column":3}]},{"RandomW":1453929005,"RandomX":1315002421,"RandomY":2396116302,"RandomZ":2184752848,"StartTime":228832.0,"Objects":[{"StartTime":228832.0,"EndTime":228832.0,"Column":1}]},{"RandomW":307062845,"RandomX":2396116302,"RandomY":2184752848,"RandomZ":1453929005,"StartTime":229097.0,"Objects":[{"StartTime":229097.0,"EndTime":229097.0,"Column":2},{"StartTime":229097.0,"EndTime":229097.0,"Column":3}]},{"RandomW":2488853431,"RandomX":1430246951,"RandomY":1243135735,"RandomZ":862796553,"StartTime":229628.0,"Objects":[{"StartTime":229628.0,"EndTime":229893.0,"Column":0}]},{"RandomW":2954723307,"RandomX":862796553,"RandomY":2488853431,"RandomZ":1065193973,"StartTime":230159.0,"Objects":[{"StartTime":230159.0,"EndTime":230689.0,"Column":2}]},{"RandomW":3118771232,"RandomX":1065193973,"RandomY":2954723307,"RandomZ":3941773202,"StartTime":230955.0,"Objects":[{"StartTime":230955.0,"EndTime":230955.0,"Column":3},{"StartTime":230955.0,"EndTime":230955.0,"Column":2}]},{"RandomW":1630107201,"RandomX":3532926875,"RandomY":2476115689,"RandomZ":1207743047,"StartTime":231221.0,"Objects":[{"StartTime":231221.0,"EndTime":231751.0,"Column":0},{"StartTime":231221.0,"EndTime":231751.0,"Column":4}]},{"RandomW":313681160,"RandomX":2476115689,"RandomY":1207743047,"RandomZ":1630107201,"StartTime":232017.0,"Objects":[{"StartTime":232017.0,"EndTime":232017.0,"Column":2}]},{"RandomW":892602489,"RandomX":1207743047,"RandomY":1630107201,"RandomZ":313681160,"StartTime":232283.0,"Objects":[{"StartTime":232283.0,"EndTime":232283.0,"Column":3},{"StartTime":232283.0,"EndTime":232283.0,"Column":4}]},{"RandomW":2549672466,"RandomX":1630107201,"RandomY":313681160,"RandomZ":892602489,"StartTime":232548.0,"Objects":[{"StartTime":232548.0,"EndTime":232548.0,"Column":1}]},{"RandomW":3175685586,"RandomX":313681160,"RandomY":892602489,"RandomZ":2549672466,"StartTime":232814.0,"Objects":[{"StartTime":232814.0,"EndTime":232814.0,"Column":3},{"StartTime":232814.0,"EndTime":232814.0,"Column":4}]},{"RandomW":1012053334,"RandomX":892602489,"RandomY":2549672466,"RandomZ":3175685586,"StartTime":233079.0,"Objects":[{"StartTime":233079.0,"EndTime":233079.0,"Column":2}]},{"RandomW":2846885221,"RandomX":2549672466,"RandomY":3175685586,"RandomZ":1012053334,"StartTime":233345.0,"Objects":[{"StartTime":233345.0,"EndTime":233345.0,"Column":3},{"StartTime":233345.0,"EndTime":233345.0,"Column":4}]},{"RandomW":2773158813,"RandomX":2846885221,"RandomY":4182295099,"RandomZ":203093837,"StartTime":233876.0,"Objects":[{"StartTime":233876.0,"EndTime":234141.0,"Column":0},{"StartTime":233876.0,"EndTime":234141.0,"Column":1}]},{"RandomW":857734082,"RandomX":203093837,"RandomY":2773158813,"RandomZ":2365172092,"StartTime":234407.0,"Objects":[{"StartTime":234407.0,"EndTime":234937.0,"Column":2}]},{"RandomW":3898917491,"RandomX":2773158813,"RandomY":2365172092,"RandomZ":857734082,"StartTime":235203.0,"Objects":[{"StartTime":235203.0,"EndTime":235203.0,"Column":2}]},{"RandomW":1417532037,"RandomX":857734082,"RandomY":3898917491,"RandomZ":361638657,"StartTime":235469.0,"Objects":[{"StartTime":235469.0,"EndTime":235999.0,"Column":3}]},{"RandomW":2557538851,"RandomX":3898917491,"RandomY":361638657,"RandomZ":1417532037,"StartTime":236265.0,"Objects":[{"StartTime":236265.0,"EndTime":236265.0,"Column":3}]},{"RandomW":846935039,"RandomX":1417532037,"RandomY":2557538851,"RandomZ":1456065540,"StartTime":236531.0,"Objects":[{"StartTime":236531.0,"EndTime":236796.0,"Column":2}]},{"RandomW":2547399683,"RandomX":1456065540,"RandomY":846935039,"RandomZ":2284332751,"StartTime":237062.0,"Objects":[{"StartTime":237062.0,"EndTime":237327.0,"Column":1}]},{"RandomW":2405919505,"RandomX":846935039,"RandomY":2284332751,"RandomZ":2547399683,"StartTime":237593.0,"Objects":[{"StartTime":237593.0,"EndTime":237593.0,"Column":3},{"StartTime":237593.0,"EndTime":237593.0,"Column":4}]},{"RandomW":1684559305,"RandomX":2284332751,"RandomY":2547399683,"RandomZ":2405919505,"StartTime":237858.0,"Objects":[{"StartTime":237858.0,"EndTime":237858.0,"Column":5},{"StartTime":237858.0,"EndTime":237858.0,"Column":0}]},{"RandomW":2914982357,"RandomX":2547399683,"RandomY":2405919505,"RandomZ":1684559305,"StartTime":238124.0,"Objects":[{"StartTime":238124.0,"EndTime":238124.0,"Column":3},{"StartTime":238124.0,"EndTime":238124.0,"Column":4}]},{"RandomW":2343509573,"RandomX":2405919505,"RandomY":1684559305,"RandomZ":2914982357,"StartTime":238389.0,"Objects":[{"StartTime":238389.0,"EndTime":238389.0,"Column":5}]},{"RandomW":1059378114,"RandomX":1684559305,"RandomY":2914982357,"RandomZ":2343509573,"StartTime":238655.0,"Objects":[{"StartTime":238655.0,"EndTime":240778.0,"Column":2}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/100374.osu b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/100374.osu new file mode 100644 index 0000000000..50f943b9e6 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/100374.osu @@ -0,0 +1,449 @@ +osu file format v9 + +[General] +StackLeniency: 0.4 +Mode: 0 + +[Difficulty] +HPDrainRate:5 +CircleSize:4 +OverallDifficulty:5 +ApproachRate:6 +SliderMultiplier:1.7 +SliderTickRate:2 + +[Events] +//Background and Video events +//Break Periods +2,98678,112295 +2,185757,200967 +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples +//Background Colour Transformations +3,100,163,162,255 + +[TimingPoints] +695,530.973451327434,4,2,1,20,1,0 +33457,-100,4,2,1,25,0,0 +33988,-100,4,2,1,30,0,0 +34386,-100,4,1,0,30,0,0 +38649,-100,4,1,1,30,0,0 +42897,-100,4,1,0,30,0,0 +47144,-100,4,1,1,30,0,0 +51530,-100,4,2,1,20,0,0 +56978,571.428571428571,4,2,1,20,1,0 +58692,845.070422535211,4,2,1,20,1,0 +60248,530.973451327434,4,2,1,20,1,0 +60740,-100,4,1,1,30,0,0 +61555,-66.6666666666667,4,1,1,30,0,0 +62219,-100,4,1,0,40,0,0 +78148,-100,4,1,0,30,0,0 +78413,-100,4,1,0,35,0,0 +78679,-100,4,1,0,40,0,0 +78944,-100,4,1,0,45,0,0 +79210,-100,4,1,0,40,0,0 +96466,-100,4,2,1,30,0,0 +132285,-100,4,2,1,20,0,0 +149453,-100,4,1,1,35,0,0 +153790,-100,4,2,1,40,0,0 +157639,-100,4,1,1,35,0,0 +162020,-100,4,2,1,40,0,0 +166158,-100,4,1,0,40,0,0 +201733,-100,4,2,1,20,0,0 +219099,-133.333333333333,4,2,1,20,0,0 +221024,-100,4,1,1,30,0,0 +221290,-100,4,1,0,30,0,0 + +[HitObjects] +256,192,15562,12,0,17155 +72,120,17686,5,8 +128,224,17951,1,0 +185,119,18217,1,0 +246,220,18482,1,0 +128,224,18748,2,0,B|161:262|208:264,1,85,4|0 +309,213,19279,2,0,B|297:169|325:120,2,85,0|0|8 +309,213,20075,5,0 +309,332,20341,1,0 +206,272,20606,1,8 +309,213,20871,2,0,B|336:117|261:56,1,170,4|0 +205,272,21933,6,0,B|183:307|125:328,1,85,8|0 +149,256,22464,2,0,B|114:281|45:280,1,85,0|0 +101,216,22995,2,0,B|16:264|-56:176|16:72|104:128,1,255,4|0 +149,136,24057,6,0,B|170:100|229:80,1,85,8|0 +205,149,24588,2,0,B|239:123|309:125,1,85,0|8 +253,189,25119,2,0,B|349:144|413:221,1,170,4|8 +240,336,26181,5,8 +288,264,26447,1,0 +344,328,26712,2,0,B|391:339|440:328,1,85,0|0 +488,270,27243,2,0,B|424:256|392:200,1,85,4|0 +329,230,27774,2,0,B|328:176|386:142,1,85,0|0 +363,69,28305,2,0,B|328:40|280:56,2,85,8|0|0 +312,136,29102,1,0 +224,120,29367,2,0,B|192:168|256:240|224:296,1,170,4|8 +96,240,30429,6,0,B|83:195|56:160,1,85,8|0 +96,88,30960,2,0,B|83:132|56:168,1,85,0|0 +59,164,31491,2,0,B|129:182|187:167|254:149|323:168,1,255,4|0 +312,165,32553,6,0,B|302:210|256:237,1,85,8|0 +312,166,33084,2,0,B|321:120|368:94,1,85,8|0 +312,166,33615,2,0,B|318:204|374:193|426:183|450:247,1,170,8|8 +200,232,34677,5,4 +119,169,34942,1,0 +57,248,35208,1,8 +137,311,35473,1,0 +200,232,35739,5,0 +248,302,36004,1,0 +318,254,36270,1,8 +270,183,36535,1,0 +200,232,36801,6,0,B|120:272|120:272|40:224,1,170,0|8 +130,183,37597,1,0 +200,232,37863,2,0,B|280:192|280:192|368:240,1,170,0|8 +167,111,38925,6,0,B|134:71|98:65,1,85,8|0 +167,112,39456,2,0,B|115:116|90:142,1,85,4|0 +167,112,39987,2,0,B|120:192|176:248|240:312|152:368,1,255,8|0 +173,351,41048,6,0,B|142:305|80:288,1,85,8|0 +173,351,41579,2,0,B|194:299|175:238,1,85,4|0 +173,351,42110,2,0,B|237:351|253:303|269:255|341:263,1,170,8|8 +128,144,43172,5,4 +208,176,43438,1,0 +288,144,43703,1,8 +368,176,43969,1,0 +408,272,44234,5,0 +312,312,44500,1,0 +216,272,44765,1,8 +120,312,45031,1,0 +48,240,45296,5,0 +160,272,45562,1,0 +272,240,45827,1,8 +384,280,46093,1,0 +496,240,46358,2,0,B|448:208|448:208|496:176|504:128|442:127,1,170,0|8 +152,128,47420,6,0,B|122:167|120:224,1,85,8|0 +88,128,47951,2,0,B|95:177|133:218,1,85,4|0 +121,204,48482,2,0,B|140:296|264:280|308:368,1,255,8|0 +308,368,49544,6,0,B|293:318|324:264,1,85,8|0 +368,348,50075,2,0,B|322:323|305:263,1,85,4|0 +324,200,50606,2,0,B|274:214|203:224|142:108|131:56|243:32|243:120|211:160|107:136,1,340,8|2 +369,216,52730,5,2 +176,312,53792,2,0,B|166:217|64:144,1,170,0|0 +179,150,54588,1,0 +120,88,54854,2,0,B|107:176|38:232,1,170,2|0 +464,320,55916,6,0,B|392:252|288:280,1,170,0|0 +280,104,56978,6,0,B|312:192|416:208,1,170,2|0 +192,160,58120,2,0,B|182:224|112:240,1,85,2|0 +24,240,58692,6,0,B|72:240|88:272,1,56.6666666666667,6|0 +224,296,59325,2,0,B|240:200|200:120,1,170 +316,136,60513,5,0 +400,156,60778,2,0,B|408:100|364:56,1,85,10|0 +320,16,61309,1,2 +160,112,61840,6,0,B|95:104|28:135,1,127.499996200204,8|0 +160,112,62371,6,0,B|80:168|96:296,1,170,4|8 +176,280,63168,1,0 +224,208,63433,2,0,B|280:288|392:264,1,170,0|8 +456,184,64230,1,0 +328,144,64495,1,8 +416,248,64761,1,0 +408,112,65026,1,8 +336,232,65292,1,0 +388,182,65557,1,8 +256,288,66088,5,8 +256,288,66354,1,0 +256,288,66619,2,0,B|200:360|72:368,1,170,0|8 +44,308,67416,1,0 +87,234,67681,2,0,B|163:279|207:386,1,170,0|8 +256,288,68478,1,0 +400,120,68743,5,8 +328,256,69009,1,0 +400,120,69274,1,8 +264,184,69540,1,0 +400,120,69805,1,8 +400,120,70336,6,0,B|395:173|368:200,1,85,8|0 +213,255,70867,2,0,B|279:198|383:198,1,170,4|8 +329,125,71663,1,0 +248,104,71929,2,0,B|184:168|80:152,1,170,0|8 +200,224,72725,1,0 +272,339,72991,5,8 +151,276,73256,1,0 +267,204,73522,1,8 +204,322,73787,1,0 +287,272,74053,1,8 +287,272,74584,6,0,B|336:256|368:208,1,85,8|0 +372,140,75115,2,0,B|323:206|324:308,1,170,0|8 +240,288,75911,1,0 +160,248,76177,2,0,B|216:176|320:216,1,170,0|8 +272,136,76973,1,0 +200,88,77239,6,0,B|216:136|192:176,1,85,8|0 +160,248,77770,2,0,B|160:296|208:320,1,85,8|0 +328,232,78301,5,0 +233,133,78566,1,8 +297,15,78832,1,8 +432,40,79097,1,8 +453,176,79363,6,0,B|448:240|384:272|328:232,1,170,4|8 +286,306,80159,1,0 +203,288,80424,2,0,B|208:224|272:192|328:232,1,170,0|8 +404,231,81221,1,0 +408,160,81486,5,8 +360,288,81752,1,0 +472,216,82017,1,8 +336,208,82283,1,0 +440,296,82548,1,8 +288,320,83079,5,8 +288,320,83345,1,0 +288,320,83610,2,0,B|200:314|128:248,1,170,0|8 +88,320,84407,1,0 +56,240,84672,2,0,B|133:287|176:392,1,170,0|8 +163,274,85469,1,0 +296,216,85734,5,8 +165,75,86000,1,0 +99,178,86265,1,8 +282,97,86531,1,0 +184,264,86796,1,8 +184,264,87327,6,0,B|159:295|110:299,1,85,8|0 +23,247,87858,2,0,B|91:300|192:261,1,170,4|8 +245,326,88655,1,0 +293,254,88920,2,0,B|213:198|109:246,1,170,0|8 +181,302,89717,1,0 +165,166,89982,5,8 +141,302,90247,1,0 +205,182,90513,1,8 +109,278,90778,1,0 +229,214,91044,1,8 +376,132,91575,6,0,B|424:140|464:100,1,85,8|0 +464,192,92106,2,0,B|456:280|352:320,1,170,0|8 +300,256,92902,1,0 +228,212,93168,2,0,B|268:116|164:60,1,170,0|8 +100,32,93964,1,0 +84,116,94230,2,0,B|116:156|108:212,1,85,8|0 +188,160,94761,2,0,B|188:208|232:244,1,85,8|0 +296,196,95292,2,0,B|320:236|349:239|399:242|379:198|379:198|334:185|358:245|368:276|440:260|480:316|416:356,1,340,8|4 +256,192,96486,12,8,98478 +264,192,113345,5,8 +264,192,113876,1,8 +264,192,114407,5,0 +172,236,114672,1,8 +184,336,114938,1,0 +284,356,115203,1,8 +340,268,115469,1,8 +304,100,116000,1,8 +304,100,116531,1,0 +272,336,117062,5,8 +248,200,117327,1,0 +376,152,117593,1,8 +376,152,118124,1,8 +376,152,118655,5,0 +240,128,118920,1,8 +376,192,119186,1,0 +496,152,119451,1,8 +376,224,119717,1,8 +376,224,120247,1,8 +376,224,120778,1,0 +376,224,121309,5,8 +264,296,121575,1,0 +256,160,121840,1,8 +256,160,122371,1,8 +256,160,122902,1,0 +256,160,123433,5,8 +168,264,123699,1,0 +312,280,123964,1,8 +312,280,124495,1,8 +312,280,125026,1,0 +312,280,125557,5,8 +200,200,125823,1,0 +312,280,126088,1,8 +312,280,126619,1,8 +312,280,127150,5,0 +416,200,127416,1,8 +432,336,127681,1,0 +416,200,127947,1,8 +312,280,128212,1,8 +312,280,128743,1,8 +312,280,129274,5,8 +264,152,129540,1,8 +136,192,129805,1,8 +184,320,130071,1,12 +88,120,132460,6,0,B|127:224|104:304,1,170,2|0 +424,264,133522,2,0,B|384:159|408:80,1,170 +448,168,134318,2,0,B|369:240|297:240,1,170,4|0 +301,158,135115,2,0,B|277:206|309:262,1,85 +395,295,135646,2,0,B|323:263|227:287,1,170,0|2 +176,88,136708,6,0,B|134:57|80:64,1,85 +176,88,137239,2,0,B|221:64|264:64,1,85,8|0 +176,88,137770,2,0,B|137:175|196:220|272:272|208:344,1,255,4|0 +136,328,138832,6,0,B|83:306|40:328,1,85 +136,328,139363,2,0,B|184:312|224:328,1,85,2|0 +300,296,139894,2,0,B|300:198|388:200|468:200|452:104,1,255,4|0 +372,100,140955,1,0 +292,72,141221,6,0,B|250:102|244:152,2,85,0|8|0 +332,148,142017,1,4 +388,212,142283,2,0,B|414:243|465:241,1,85 +440,148,142814,2,0,B|400:172|388:213,1,85 +236,232,143345,1,0 +204,84,143610,1,0 +356,64,143876,1,0 +388,212,144141,2,0,B|350:295|228:308,1,170,4|0 +96,304,145203,6,0,B|96:208,1,85 +144,203,145734,2,0,B|144:288,1,85,8|0 +192,272,146265,2,0,B|192:176|192:176|192:120|256:112,1,170,4|0 +312,56,147062,1,0 +392,120,147327,6,0,B|392:208,1,85 +336,221,147858,2,0,B|336:136,1,85,8|0 +280,152,148389,2,0,B|280:256|280:256|264:272|280:288|280:288|296:304|280:320|280:320|248:336|280:352|280:352|312:368|312:368|280:376|224:384,1,340,4|4 +172,322,149717,5,0 +136,248,149982,1,8 +64,208,150247,1,0 +147,112,150513,5,0 +224,80,150778,1,0 +304,112,151044,1,8 +384,88,151309,1,0 +336,192,151575,6,0,B|280:272|176:264,1,170,0|8 +408,216,152637,2,0,B|429:173|464:152,1,85,0|0 +360,80,153168,2,0,B|376:168|304:264,1,170,8|0 +256,288,153964,5,2 +192,240,154230,1,4 +272,208,154495,1,0 +229,134,154761,2,0,B|276:214,1,85,0|2 +160,248,155292,1,4 +120,136,155557,1,0 +229,134,155823,6,0,B|331:134,1,85,0|2 +408,208,156354,2,0,B|312:208,1,85,4|0 +216,256,156885,2,0,B|272:280|264:352|208:344|192:296|256:272|328:312,1,170,0|4 +456,224,157947,5,0 +400,136,158212,1,0 +456,224,158478,1,8 +392,304,158743,1,0 +456,224,159009,1,0 +288,232,159540,5,8 +200,283,159805,1,0 +176,184,160071,1,0 +176,184,160601,5,8 +278,184,160867,1,0 +176,184,161132,2,0,B|88:184,1,85 +24,88,161663,2,0,B|192:88,1,170,8|0 +280,88,162460,1,2 +240,168,162725,1,4 +360,48,163256,5,0 +280,88,163522,1,2 +240,168,163787,2,0,B|344:168,1,85,4|0 +376,240,164318,2,0,B|320:312,1,85,2|0 +248,304,164849,2,0,B|200:232,1,85,6|0 +288,240,165380,2,0,B|288:136|288:136|286:82|344:72,1,170,6|8 +480,104,166442,6,0,B|416:168|416:296,1,170,4|8 +336,280,167239,1,0 +288,208,167504,2,0,B|232:288|120:264,1,170,0|8 +56,184,168301,1,0 +184,144,168566,1,8 +96,248,168832,1,0 +104,112,169097,1,8 +176,232,169363,1,0 +124,182,169628,1,8 +272,256,170159,5,8 +272,256,170424,1,0 +272,256,170690,2,0,B|310:339|428:329,1,170,0|8 +487,259,171486,1,0 +423,179,171752,2,0,B|340:241|340:329,1,170,0|8 +251,346,172548,1,0 +260,193,172814,5,8 +340,321,173079,1,0 +260,193,173345,1,8 +404,249,173610,1,0 +260,193,173876,1,8 +112,120,174407,6,0,B|117:173|144:200,1,85,8|0 +309,191,174938,2,0,B|225:225|117:191,1,170,0|8 +184,128,175734,1,0 +264,104,176000,2,0,B|328:168|432:152,1,170,0|8 +312,224,176796,1,0 +240,339,177062,5,8 +361,276,177327,1,0 +245,204,177593,1,8 +308,322,177858,1,0 +225,270,178124,1,8 +225,270,178655,6,0,B|176:256|144:208,1,85,8|0 +32,256,179186,2,0,B|120:256|192:312,1,170,0|8 +272,288,179982,1,0 +352,248,180247,2,0,B|296:176|192:216,1,170,0|8 +240,136,181044,1,0 +325,129,181309,6,0,B|322:176|285:217,1,85,8|0 +167,291,181840,2,0,B|170:244|207:203,1,85,8|0 +327,289,182371,2,0,B|280:286|239:249,1,85,8|0 +160,120,182902,2,0,B|216:112|248:152|272:192|336:192,1,170,8|4 +256,192,183699,12,4,185557 +80,104,202017,5,2 +152,219,202283,1,0 +16,224,202548,2,0,B|88:208|158:111,1,170,8|0 +226,87,203345,1,0 +304,120,203610,2,0,B|352:120|400:104,1,85,2|0 +304,120,204141,2,0,B|336:88|344:32,1,85,0|0 +341,45,204672,6,0,B|429:77|450:203,1,170,8|0 +360,184,205469,1,0 +304,120,205734,2,0,B|264:96|240:48,1,85,2|0 +304,120,206265,2,0,B|311:76|344:32,1,85,0|0 +408,88,206796,5,4 +472,168,207062,1,0 +392,224,207327,1,0 +304,280,207593,1,0 +224,208,207858,2,0,B|309:237|393:224,1,170 +472,168,208655,1,0 +408,88,208920,6,0,B|368:166|402:252,1,170,8|0 +504,280,209717,1,0 +403,319,209982,2,0,B|459:276|475:151,1,170,4|0 +408,88,210778,1,0 +384,200,211044,5,2 +240,160,211309,1,0 +264,304,211575,1,0 +296,224,211840,2,0,B|336:137|464:136,1,170,2|0 +296,224,212637,6,0,B|243:220|208:161,1,85,2|0 +163,324,213168,2,0,B|244:308|308:204,1,170,8|0 +296,136,213964,1,0 +264,56,214230,2,0,B|232:96|192:136,1,85,4|0 +208,120,214761,2,0,B|200:72|168:32,1,85 +175,42,215292,2,0,B|155:86|98:112,1,85,2|0 +50,53,215823,2,0,B|98:69|122:109,1,85,0|0 +117,102,216354,1,4 +168,344,216885,6,0,B|167:287|131:246,1,85 +88,160,217416,2,0,B|48:248|96:328,1,170,8|0 +144,264,218212,1,0 +224,296,218478,2,0,B|328:312|368:216,1,170,6|0 +363,110,219274,2,0,B|259:246|139:206|147:94|275:70|355:198|130:268,1,446.249986700714,2|8 +160,112,221663,6,0,B|80:168|96:296,1,170,4|8 +176,280,222460,1,0 +224,208,222725,2,0,B|280:288|392:264,1,170,0|8 +456,184,223522,1,0 +328,144,223787,5,8 +416,248,224053,1,0 +408,112,224318,1,8 +336,232,224584,1,0 +388,182,224849,1,8 +240,256,225380,5,8 +240,256,225646,1,0 +240,256,225911,2,0,B|184:328|76:314,1,170,0|8 +3,315,226708,1,0 +89,315,226973,2,0,B|184:302|240:374,1,170,0|8 +314,332,227770,1,0 +252,194,228035,5,8 +116,130,228301,1,0 +252,194,228566,1,8 +140,298,228832,1,0 +252,194,229097,1,8 +400,120,229628,6,0,B|352:112|288:144,1,85,8|0 +203,191,230159,2,0,B|287:225|395:191,1,170,0|8 +330,124,230955,1,0 +248,104,231221,2,0,B|152:96|80:152,1,170,0|8 +200,224,232017,1,0 +272,339,232283,5,8 +151,276,232548,1,0 +267,204,232814,1,8 +204,322,233079,1,0 +287,270,233345,1,8 +287,270,233876,6,0,B|335:254|367:206,1,85,8|0 +464,288,234407,2,0,B|368:272|304:344,1,170,0|8 +226,317,235203,1,0 +165,256,235469,2,0,B|224:192|336:208,1,170,0|8 +272,136,236265,1,0 +199,63,236531,2,0,B|152:80|120:128,1,85,8|0 +203,184,237062,2,0,B|167:218|165:267,1,85,8|0 +312,264,237593,5,8 +440,264,237858,1,8 +256,144,238124,1,8 +496,144,238389,1,0 +256,192,238655,12,4,240778 diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/1450162-expected-conversion.json b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/1450162-expected-conversion.json new file mode 100644 index 0000000000..4981951267 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/1450162-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"RandomW":2659430625,"RandomX":3579807591,"RandomY":273326509,"RandomZ":272911513,"StartTime":1107.0,"Objects":[{"StartTime":1107.0,"EndTime":1838.0,"Column":1}]},{"RandomW":4073513076,"RandomX":272911513,"RandomY":2659430625,"RandomZ":3083761897,"StartTime":2570.0,"Objects":[{"StartTime":2570.0,"EndTime":2935.0,"Column":6}]},{"RandomW":1129971314,"RandomX":3083761897,"RandomY":4073513076,"RandomZ":3235797552,"StartTime":3302.0,"Objects":[{"StartTime":3302.0,"EndTime":3667.0,"Column":3}]},{"RandomW":315510790,"RandomX":3235797552,"RandomY":1129971314,"RandomZ":2274676672,"StartTime":4033.0,"Objects":[{"StartTime":4033.0,"EndTime":4764.0,"Column":1}]},{"RandomW":2899658679,"RandomX":2274676672,"RandomY":315510790,"RandomZ":552830901,"StartTime":5497.0,"Objects":[{"StartTime":5497.0,"EndTime":5862.0,"Column":2}]},{"RandomW":3979364583,"RandomX":552830901,"RandomY":2899658679,"RandomZ":2367584034,"StartTime":6228.0,"Objects":[{"StartTime":6228.0,"EndTime":6593.0,"Column":5}]},{"RandomW":1470933435,"RandomX":2367584034,"RandomY":3979364583,"RandomZ":1363326171,"StartTime":6960.0,"Objects":[{"StartTime":6960.0,"EndTime":7142.0,"Column":4}]},{"RandomW":695558923,"RandomX":3979364583,"RandomY":1363326171,"RandomZ":1470933435,"StartTime":7326.0,"Objects":[{"StartTime":7326.0,"EndTime":7326.0,"Column":2},{"StartTime":7326.0,"EndTime":7326.0,"Column":3}]},{"RandomW":47047112,"RandomX":1470933435,"RandomY":695558923,"RandomZ":1181573554,"StartTime":7509.0,"Objects":[{"StartTime":7509.0,"EndTime":7691.0,"Column":0}]},{"RandomW":807301467,"RandomX":695558923,"RandomY":1181573554,"RandomZ":47047112,"StartTime":7875.0,"Objects":[{"StartTime":7875.0,"EndTime":7875.0,"Column":5}]},{"RandomW":2679940725,"RandomX":47047112,"RandomY":807301467,"RandomZ":3002147176,"StartTime":8058.0,"Objects":[{"StartTime":8058.0,"EndTime":8240.0,"Column":1}]},{"RandomW":176449914,"RandomX":2679940725,"RandomY":4061321195,"RandomZ":826668123,"StartTime":8424.0,"Objects":[{"StartTime":8424.0,"EndTime":8789.0,"Column":2},{"StartTime":8424.0,"EndTime":8789.0,"Column":0}]},{"RandomW":3697485076,"RandomX":347653435,"RandomY":172035291,"RandomZ":598178640,"StartTime":8972.0,"Objects":[{"StartTime":8972.0,"EndTime":9154.0,"Column":1},{"StartTime":8972.0,"EndTime":9154.0,"Column":5}]},{"RandomW":237023934,"RandomX":172035291,"RandomY":598178640,"RandomZ":3697485076,"StartTime":9338.0,"Objects":[{"StartTime":9338.0,"EndTime":9338.0,"Column":4},{"StartTime":9338.0,"EndTime":9338.0,"Column":5}]},{"RandomW":201670773,"RandomX":598178640,"RandomY":3697485076,"RandomZ":237023934,"StartTime":9521.0,"Objects":[{"StartTime":9521.0,"EndTime":9521.0,"Column":3}]},{"RandomW":3522038595,"RandomX":237023934,"RandomY":201670773,"RandomZ":341886814,"StartTime":9887.0,"Objects":[{"StartTime":9887.0,"EndTime":10069.0,"Column":4}]},{"RandomW":3662734978,"RandomX":201670773,"RandomY":341886814,"RandomZ":3522038595,"StartTime":10253.0,"Objects":[{"StartTime":10253.0,"EndTime":10253.0,"Column":3},{"StartTime":10253.0,"EndTime":10253.0,"Column":4}]},{"RandomW":4235203413,"RandomX":341886814,"RandomY":3522038595,"RandomZ":3662734978,"StartTime":10436.0,"Objects":[{"StartTime":10436.0,"EndTime":10436.0,"Column":2},{"StartTime":10436.0,"EndTime":10436.0,"Column":3}]},{"RandomW":3996672434,"RandomX":3522038595,"RandomY":3662734978,"RandomZ":4235203413,"StartTime":10619.0,"Objects":[{"StartTime":10619.0,"EndTime":10619.0,"Column":1},{"StartTime":10619.0,"EndTime":10619.0,"Column":2}]},{"RandomW":1328405285,"RandomX":3662734978,"RandomY":4235203413,"RandomZ":3996672434,"StartTime":10802.0,"Objects":[{"StartTime":10802.0,"EndTime":10802.0,"Column":0},{"StartTime":10802.0,"EndTime":10802.0,"Column":1}]},{"RandomW":303317172,"RandomX":4235203413,"RandomY":3996672434,"RandomZ":1328405285,"StartTime":10985.0,"Objects":[{"StartTime":10985.0,"EndTime":10985.0,"Column":1},{"StartTime":10985.0,"EndTime":10985.0,"Column":2}]},{"RandomW":1854018328,"RandomX":3996672434,"RandomY":1328405285,"RandomZ":303317172,"StartTime":11167.0,"Objects":[{"StartTime":11167.0,"EndTime":11167.0,"Column":2},{"StartTime":11167.0,"EndTime":11167.0,"Column":3}]},{"RandomW":1134221963,"RandomX":1328405285,"RandomY":303317172,"RandomZ":1854018328,"StartTime":12814.0,"Objects":[{"StartTime":12814.0,"EndTime":12814.0,"Column":1}]},{"RandomW":2894789541,"RandomX":1134221963,"RandomY":1649399086,"RandomZ":3538823219,"StartTime":13180.0,"Objects":[{"StartTime":13180.0,"EndTime":13362.0,"Column":4},{"StartTime":13180.0,"EndTime":13362.0,"Column":2}]},{"RandomW":2259123626,"RandomX":2894789541,"RandomY":961618493,"RandomZ":631989916,"StartTime":13546.0,"Objects":[{"StartTime":13546.0,"EndTime":13728.0,"Column":0}]},{"RandomW":3004853499,"RandomX":2259123626,"RandomY":2097932552,"RandomZ":3455806558,"StartTime":13911.0,"Objects":[{"StartTime":13911.0,"EndTime":14093.0,"Column":4},{"StartTime":13911.0,"EndTime":14093.0,"Column":2}]},{"RandomW":1511929919,"RandomX":250420511,"RandomY":747435619,"RandomZ":973338160,"StartTime":14277.0,"Objects":[{"StartTime":14277.0,"EndTime":14277.0,"Column":5},{"StartTime":14277.0,"EndTime":14277.0,"Column":6},{"StartTime":14459.0,"EndTime":14459.0,"Column":2},{"StartTime":14459.0,"EndTime":14459.0,"Column":3},{"StartTime":14641.0,"EndTime":14641.0,"Column":3},{"StartTime":14641.0,"EndTime":14641.0,"Column":4}]},{"RandomW":1997079940,"RandomX":973338160,"RandomY":1511929919,"RandomZ":1014879110,"StartTime":14826.0,"Objects":[{"StartTime":14826.0,"EndTime":15191.0,"Column":6}]},{"RandomW":735692759,"RandomX":1997079940,"RandomY":1386139427,"RandomZ":4192918159,"StartTime":15375.0,"Objects":[{"StartTime":15375.0,"EndTime":15557.0,"Column":2}]},{"RandomW":348373517,"RandomX":1386139427,"RandomY":4192918159,"RandomZ":735692759,"StartTime":15741.0,"Objects":[{"StartTime":15741.0,"EndTime":15741.0,"Column":5},{"StartTime":15741.0,"EndTime":15741.0,"Column":6}]},{"RandomW":521239132,"RandomX":735692759,"RandomY":348373517,"RandomZ":2961240161,"StartTime":16106.0,"Objects":[{"StartTime":16106.0,"EndTime":16288.0,"Column":1}]},{"RandomW":1199465075,"RandomX":521239132,"RandomY":4195606806,"RandomZ":4039804915,"StartTime":16472.0,"Objects":[{"StartTime":16472.0,"EndTime":16654.0,"Column":6},{"StartTime":16472.0,"EndTime":16654.0,"Column":3}]},{"RandomW":3059180408,"RandomX":4039804915,"RandomY":1199465075,"RandomZ":3542692698,"StartTime":16838.0,"Objects":[{"StartTime":16838.0,"EndTime":17020.0,"Column":2}]},{"RandomW":834119344,"RandomX":302423902,"RandomY":2799635095,"RandomZ":1022775029,"StartTime":17204.0,"Objects":[{"StartTime":17204.0,"EndTime":17204.0,"Column":4},{"StartTime":17204.0,"EndTime":17204.0,"Column":5},{"StartTime":17386.0,"EndTime":17386.0,"Column":2},{"StartTime":17386.0,"EndTime":17386.0,"Column":3},{"StartTime":17568.0,"EndTime":17568.0,"Column":3},{"StartTime":17568.0,"EndTime":17568.0,"Column":4}]},{"RandomW":1236797567,"RandomX":1022775029,"RandomY":834119344,"RandomZ":393032631,"StartTime":17753.0,"Objects":[{"StartTime":17753.0,"EndTime":18118.0,"Column":4}]},{"RandomW":892840048,"RandomX":1236797567,"RandomY":3350685275,"RandomZ":1270471227,"StartTime":18302.0,"Objects":[{"StartTime":18302.0,"EndTime":18484.0,"Column":2}]},{"RandomW":3233581364,"RandomX":1270471227,"RandomY":892840048,"RandomZ":3158680921,"StartTime":18667.0,"Objects":[{"StartTime":18667.0,"EndTime":19032.0,"Column":3}]},{"RandomW":1163000602,"RandomX":892840048,"RandomY":3158680921,"RandomZ":3233581364,"StartTime":19216.0,"Objects":[{"StartTime":19216.0,"EndTime":19216.0,"Column":2}]},{"RandomW":1548989545,"RandomX":3233581364,"RandomY":1163000602,"RandomZ":3450712040,"StartTime":19399.0,"Objects":[{"StartTime":19399.0,"EndTime":19581.0,"Column":5}]},{"RandomW":313779584,"RandomX":1548989545,"RandomY":2021811198,"RandomZ":2999045855,"StartTime":19765.0,"Objects":[{"StartTime":19765.0,"EndTime":19947.0,"Column":2},{"StartTime":19765.0,"EndTime":19947.0,"Column":1}]},{"RandomW":3548572483,"RandomX":2021811198,"RandomY":2999045855,"RandomZ":313779584,"StartTime":20131.0,"Objects":[{"StartTime":20131.0,"EndTime":20131.0,"Column":6}]},{"RandomW":75459001,"RandomX":313779584,"RandomY":3548572483,"RandomZ":3094675294,"StartTime":20314.0,"Objects":[{"StartTime":20314.0,"EndTime":20496.0,"Column":0}]},{"RandomW":1299261902,"RandomX":3094675294,"RandomY":75459001,"RandomZ":2305626963,"StartTime":20680.0,"Objects":[{"StartTime":20680.0,"EndTime":21045.0,"Column":4}]},{"RandomW":2905421941,"RandomX":2305626963,"RandomY":1299261902,"RandomZ":1390453041,"StartTime":21228.0,"Objects":[{"StartTime":21228.0,"EndTime":21410.0,"Column":2}]},{"RandomW":2294300184,"RandomX":1390453041,"RandomY":2905421941,"RandomZ":1278955784,"StartTime":21594.0,"Objects":[{"StartTime":21594.0,"EndTime":22325.0,"Column":6},{"StartTime":21594.0,"EndTime":21594.0,"Column":4},{"StartTime":21959.0,"EndTime":21959.0,"Column":4},{"StartTime":22324.0,"EndTime":22324.0,"Column":4}]},{"RandomW":3749637912,"RandomX":2905421941,"RandomY":1278955784,"RandomZ":2294300184,"StartTime":22509.0,"Objects":[{"StartTime":22509.0,"EndTime":22509.0,"Column":6},{"StartTime":22509.0,"EndTime":22509.0,"Column":0}]},{"RandomW":753327495,"RandomX":458525202,"RandomY":2373004129,"RandomZ":80278569,"StartTime":22692.0,"Objects":[{"StartTime":22692.0,"EndTime":22874.0,"Column":2}]},{"RandomW":3562609217,"RandomX":753327495,"RandomY":2472396307,"RandomZ":2540952890,"StartTime":23058.0,"Objects":[{"StartTime":23058.0,"EndTime":23058.0,"Column":1},{"StartTime":23058.0,"EndTime":23058.0,"Column":5}]},{"RandomW":3562609217,"RandomX":753327495,"RandomY":2472396307,"RandomZ":2540952890,"StartTime":23241.0,"Objects":[{"StartTime":23241.0,"EndTime":23241.0,"Column":5},{"StartTime":23241.0,"EndTime":23241.0,"Column":1}]},{"RandomW":3009004844,"RandomX":2540952890,"RandomY":3562609217,"RandomZ":3460951976,"StartTime":23606.0,"Objects":[{"StartTime":23606.0,"EndTime":23971.0,"Column":2}]},{"RandomW":1524995266,"RandomX":3133817968,"RandomY":2791164538,"RandomZ":669533622,"StartTime":24155.0,"Objects":[{"StartTime":24155.0,"EndTime":24337.0,"Column":4}]},{"RandomW":2667749121,"RandomX":1524995266,"RandomY":3001332266,"RandomZ":4204965910,"StartTime":24521.0,"Objects":[{"StartTime":24521.0,"EndTime":24521.0,"Column":0},{"StartTime":24521.0,"EndTime":24521.0,"Column":6}]},{"RandomW":1230014889,"RandomX":2127937865,"RandomY":2434329733,"RandomZ":443126576,"StartTime":24704.0,"Objects":[{"StartTime":24704.0,"EndTime":25069.0,"Column":1},{"StartTime":24704.0,"EndTime":25069.0,"Column":4}]},{"RandomW":1409501366,"RandomX":2573194819,"RandomY":3480583465,"RandomZ":2580776932,"StartTime":25253.0,"Objects":[{"StartTime":25253.0,"EndTime":25253.0,"Column":0},{"StartTime":25253.0,"EndTime":25253.0,"Column":1},{"StartTime":25435.0,"EndTime":25435.0,"Column":4},{"StartTime":25435.0,"EndTime":25435.0,"Column":5},{"StartTime":25617.0,"EndTime":25617.0,"Column":1},{"StartTime":25617.0,"EndTime":25617.0,"Column":2}]},{"RandomW":864641467,"RandomX":3480583465,"RandomY":2580776932,"RandomZ":1409501366,"StartTime":25802.0,"Objects":[{"StartTime":25802.0,"EndTime":25802.0,"Column":1}]},{"RandomW":1467076310,"RandomX":2580776932,"RandomY":1409501366,"RandomZ":864641467,"StartTime":25985.0,"Objects":[{"StartTime":25985.0,"EndTime":25985.0,"Column":2}]},{"RandomW":479214438,"RandomX":864641467,"RandomY":1467076310,"RandomZ":1385729915,"StartTime":26167.0,"Objects":[{"StartTime":26167.0,"EndTime":26532.0,"Column":1}]},{"RandomW":3054605916,"RandomX":1684801014,"RandomY":3182588115,"RandomZ":734516041,"StartTime":26716.0,"Objects":[{"StartTime":26716.0,"EndTime":26716.0,"Column":4},{"StartTime":26716.0,"EndTime":26716.0,"Column":2},{"StartTime":26898.0,"EndTime":26898.0,"Column":3},{"StartTime":26898.0,"EndTime":26898.0,"Column":1},{"StartTime":27080.0,"EndTime":27080.0,"Column":2},{"StartTime":27080.0,"EndTime":27080.0,"Column":6}]},{"RandomW":2992010973,"RandomX":3182588115,"RandomY":734516041,"RandomZ":3054605916,"StartTime":27265.0,"Objects":[{"StartTime":27265.0,"EndTime":27265.0,"Column":2}]},{"RandomW":2622274732,"RandomX":734516041,"RandomY":3054605916,"RandomZ":2992010973,"StartTime":27448.0,"Objects":[{"StartTime":27448.0,"EndTime":27448.0,"Column":3}]},{"RandomW":3013455357,"RandomX":2992010973,"RandomY":2622274732,"RandomZ":2298767863,"StartTime":27631.0,"Objects":[{"StartTime":27631.0,"EndTime":27996.0,"Column":2}]},{"RandomW":2994521549,"RandomX":2622274732,"RandomY":2298767863,"RandomZ":3013455357,"StartTime":28180.0,"Objects":[{"StartTime":28180.0,"EndTime":28180.0,"Column":5}]},{"RandomW":3949426364,"RandomX":1261217522,"RandomY":3788322225,"RandomZ":3210845744,"StartTime":28363.0,"Objects":[{"StartTime":28363.0,"EndTime":28363.0,"Column":5},{"StartTime":28363.0,"EndTime":28363.0,"Column":2},{"StartTime":28545.0,"EndTime":28545.0,"Column":5},{"StartTime":28545.0,"EndTime":28545.0,"Column":2},{"StartTime":28727.0,"EndTime":28727.0,"Column":3},{"StartTime":28727.0,"EndTime":28727.0,"Column":6}]},{"RandomW":1304069042,"RandomX":3210845744,"RandomY":3949426364,"RandomZ":3310503444,"StartTime":28911.0,"Objects":[{"StartTime":28911.0,"EndTime":29276.0,"Column":4}]},{"RandomW":781934546,"RandomX":3310503444,"RandomY":1304069042,"RandomZ":4271440939,"StartTime":29460.0,"Objects":[{"StartTime":29460.0,"EndTime":29460.0,"Column":6},{"StartTime":29460.0,"EndTime":29460.0,"Column":2}]},{"RandomW":3592330498,"RandomX":781934546,"RandomY":2041503475,"RandomZ":3767559527,"StartTime":29643.0,"Objects":[{"StartTime":29643.0,"EndTime":30008.0,"Column":5},{"StartTime":29643.0,"EndTime":30008.0,"Column":4}]},{"RandomW":579808732,"RandomX":2041503475,"RandomY":3767559527,"RandomZ":3592330498,"StartTime":30192.0,"Objects":[{"StartTime":30192.0,"EndTime":30192.0,"Column":4}]},{"RandomW":769209912,"RandomX":3767559527,"RandomY":3592330498,"RandomZ":579808732,"StartTime":30375.0,"Objects":[{"StartTime":30375.0,"EndTime":30375.0,"Column":4}]},{"RandomW":1825941494,"RandomX":579808732,"RandomY":769209912,"RandomZ":1308743097,"StartTime":30558.0,"Objects":[{"StartTime":30558.0,"EndTime":30923.0,"Column":5}]},{"RandomW":1378114054,"RandomX":930055805,"RandomY":3554877022,"RandomZ":2467280262,"StartTime":31106.0,"Objects":[{"StartTime":31106.0,"EndTime":31106.0,"Column":2},{"StartTime":31106.0,"EndTime":31106.0,"Column":5},{"StartTime":31288.0,"EndTime":31288.0,"Column":4},{"StartTime":31288.0,"EndTime":31288.0,"Column":1},{"StartTime":31470.0,"EndTime":31470.0,"Column":1},{"StartTime":31470.0,"EndTime":31470.0,"Column":4}]},{"RandomW":422797905,"RandomX":3554877022,"RandomY":2467280262,"RandomZ":1378114054,"StartTime":31655.0,"Objects":[{"StartTime":31655.0,"EndTime":31655.0,"Column":1}]},{"RandomW":3538525895,"RandomX":2467280262,"RandomY":1378114054,"RandomZ":422797905,"StartTime":31838.0,"Objects":[{"StartTime":31838.0,"EndTime":31838.0,"Column":0}]},{"RandomW":1277180769,"RandomX":422797905,"RandomY":3538525895,"RandomZ":1017422489,"StartTime":32021.0,"Objects":[{"StartTime":32021.0,"EndTime":32386.0,"Column":4}]},{"RandomW":69027963,"RandomX":3464755550,"RandomY":1342331375,"RandomZ":1235978524,"StartTime":32570.0,"Objects":[{"StartTime":32570.0,"EndTime":32752.0,"Column":0}]},{"RandomW":3582265519,"RandomX":1342331375,"RandomY":1235978524,"RandomZ":69027963,"StartTime":32936.0,"Objects":[{"StartTime":32936.0,"EndTime":32936.0,"Column":2},{"StartTime":32936.0,"EndTime":32936.0,"Column":3}]},{"RandomW":2197579333,"RandomX":69027963,"RandomY":3582265519,"RandomZ":2534080209,"StartTime":33302.0,"Objects":[{"StartTime":33302.0,"EndTime":33667.0,"Column":0}]},{"RandomW":820123404,"RandomX":1816967409,"RandomY":2440103335,"RandomZ":1364041006,"StartTime":33850.0,"Objects":[{"StartTime":33850.0,"EndTime":34215.0,"Column":4},{"StartTime":33850.0,"EndTime":34215.0,"Column":2}]},{"RandomW":962636497,"RandomX":2440103335,"RandomY":1364041006,"RandomZ":820123404,"StartTime":34399.0,"Objects":[{"StartTime":34399.0,"EndTime":34399.0,"Column":3},{"StartTime":34399.0,"EndTime":34399.0,"Column":4}]},{"RandomW":539348071,"RandomX":1364041006,"RandomY":820123404,"RandomZ":962636497,"StartTime":34582.0,"Objects":[{"StartTime":34582.0,"EndTime":34582.0,"Column":4}]},{"RandomW":1036431212,"RandomX":962636497,"RandomY":539348071,"RandomZ":498893216,"StartTime":34765.0,"Objects":[{"StartTime":34765.0,"EndTime":34947.0,"Column":3}]},{"RandomW":30194727,"RandomX":539348071,"RandomY":498893216,"RandomZ":1036431212,"StartTime":35131.0,"Objects":[{"StartTime":35131.0,"EndTime":35131.0,"Column":4},{"StartTime":35131.0,"EndTime":35131.0,"Column":5}]},{"RandomW":4140580700,"RandomX":1036431212,"RandomY":30194727,"RandomZ":260312717,"StartTime":35314.0,"Objects":[{"StartTime":35314.0,"EndTime":35496.0,"Column":6}]},{"RandomW":4269364006,"RandomX":30194727,"RandomY":260312717,"RandomZ":4140580700,"StartTime":35680.0,"Objects":[{"StartTime":35680.0,"EndTime":35680.0,"Column":5},{"StartTime":35680.0,"EndTime":35680.0,"Column":6}]},{"RandomW":3052364007,"RandomX":4140580700,"RandomY":4269364006,"RandomZ":2586895690,"StartTime":35863.0,"Objects":[{"StartTime":35863.0,"EndTime":36045.0,"Column":2}]},{"RandomW":575578073,"RandomX":4269364006,"RandomY":2586895690,"RandomZ":3052364007,"StartTime":36228.0,"Objects":[{"StartTime":36228.0,"EndTime":36228.0,"Column":4}]},{"RandomW":379197653,"RandomX":2586895690,"RandomY":3052364007,"RandomZ":575578073,"StartTime":36411.0,"Objects":[{"StartTime":36411.0,"EndTime":36411.0,"Column":3}]},{"RandomW":2472409868,"RandomX":379197653,"RandomY":194885113,"RandomZ":3317367861,"StartTime":36594.0,"Objects":[{"StartTime":36594.0,"EndTime":36776.0,"Column":1}]},{"RandomW":3530386304,"RandomX":1439106306,"RandomY":3004383294,"RandomZ":2928959685,"StartTime":36960.0,"Objects":[{"StartTime":36960.0,"EndTime":36960.0,"Column":1},{"StartTime":36960.0,"EndTime":36960.0,"Column":5},{"StartTime":37142.0,"EndTime":37142.0,"Column":2},{"StartTime":37142.0,"EndTime":37142.0,"Column":6},{"StartTime":37324.0,"EndTime":37324.0,"Column":2},{"StartTime":37324.0,"EndTime":37324.0,"Column":6}]},{"RandomW":3220147162,"RandomX":3004383294,"RandomY":2928959685,"RandomZ":3530386304,"StartTime":37509.0,"Objects":[{"StartTime":37509.0,"EndTime":37509.0,"Column":2}]},{"RandomW":2530492073,"RandomX":2928959685,"RandomY":3530386304,"RandomZ":3220147162,"StartTime":37692.0,"Objects":[{"StartTime":37692.0,"EndTime":37692.0,"Column":1}]},{"RandomW":2605446910,"RandomX":3530386304,"RandomY":3220147162,"RandomZ":2530492073,"StartTime":37875.0,"Objects":[{"StartTime":37875.0,"EndTime":37875.0,"Column":2}]},{"RandomW":3786494373,"RandomX":2530492073,"RandomY":2605446910,"RandomZ":583253884,"StartTime":38058.0,"Objects":[{"StartTime":38058.0,"EndTime":38240.0,"Column":5}]},{"RandomW":1188028287,"RandomX":3601275468,"RandomY":312474208,"RandomZ":764976912,"StartTime":38424.0,"Objects":[{"StartTime":38424.0,"EndTime":38424.0,"Column":4},{"StartTime":38424.0,"EndTime":38424.0,"Column":2},{"StartTime":38606.0,"EndTime":38606.0,"Column":1},{"StartTime":38606.0,"EndTime":38606.0,"Column":5},{"StartTime":38788.0,"EndTime":38788.0,"Column":2},{"StartTime":38788.0,"EndTime":38788.0,"Column":6}]},{"RandomW":2824132752,"RandomX":312474208,"RandomY":764976912,"RandomZ":1188028287,"StartTime":38972.0,"Objects":[{"StartTime":38972.0,"EndTime":38972.0,"Column":3}]},{"RandomW":1173715712,"RandomX":764976912,"RandomY":1188028287,"RandomZ":2824132752,"StartTime":39155.0,"Objects":[{"StartTime":39155.0,"EndTime":39155.0,"Column":4}]},{"RandomW":2490370662,"RandomX":2824132752,"RandomY":1173715712,"RandomZ":2893810865,"StartTime":39338.0,"Objects":[{"StartTime":39338.0,"EndTime":39520.0,"Column":1}]},{"RandomW":1949144326,"RandomX":2893810865,"RandomY":2490370662,"RandomZ":2599342112,"StartTime":39704.0,"Objects":[{"StartTime":39704.0,"EndTime":40069.0,"Column":6}]},{"RandomW":743381221,"RandomX":2599342112,"RandomY":1949144326,"RandomZ":947390134,"StartTime":40253.0,"Objects":[{"StartTime":40253.0,"EndTime":40435.0,"Column":2}]},{"RandomW":3629226534,"RandomX":947390134,"RandomY":743381221,"RandomZ":3234636444,"StartTime":40619.0,"Objects":[{"StartTime":40619.0,"EndTime":40984.0,"Column":4}]},{"RandomW":551844396,"RandomX":743381221,"RandomY":3234636444,"RandomZ":3629226534,"StartTime":41167.0,"Objects":[{"StartTime":41167.0,"EndTime":41167.0,"Column":3}]},{"RandomW":2240897560,"RandomX":551844396,"RandomY":1949877989,"RandomZ":3510981308,"StartTime":41350.0,"Objects":[{"StartTime":41350.0,"EndTime":41532.0,"Column":4},{"StartTime":41350.0,"EndTime":41532.0,"Column":0}]},{"RandomW":874163267,"RandomX":3510981308,"RandomY":2240897560,"RandomZ":2259115420,"StartTime":41716.0,"Objects":[{"StartTime":41716.0,"EndTime":41898.0,"Column":2}]},{"RandomW":3476146382,"RandomX":2240897560,"RandomY":2259115420,"RandomZ":874163267,"StartTime":42082.0,"Objects":[{"StartTime":42082.0,"EndTime":42082.0,"Column":4}]},{"RandomW":2101943428,"RandomX":874163267,"RandomY":3476146382,"RandomZ":3250516626,"StartTime":42265.0,"Objects":[{"StartTime":42265.0,"EndTime":42447.0,"Column":6}]},{"RandomW":2630934490,"RandomX":3476146382,"RandomY":3250516626,"RandomZ":2101943428,"StartTime":42631.0,"Objects":[{"StartTime":42631.0,"EndTime":42631.0,"Column":4}]},{"RandomW":3294029476,"RandomX":3722838838,"RandomY":3959050362,"RandomZ":3731020989,"StartTime":42814.0,"Objects":[{"StartTime":42814.0,"EndTime":42814.0,"Column":6},{"StartTime":42814.0,"EndTime":42814.0,"Column":4},{"StartTime":42996.0,"EndTime":42996.0,"Column":5},{"StartTime":42996.0,"EndTime":42996.0,"Column":3},{"StartTime":43178.0,"EndTime":43178.0,"Column":5},{"StartTime":43178.0,"EndTime":43178.0,"Column":3}]},{"RandomW":692368043,"RandomX":3959050362,"RandomY":3731020989,"RandomZ":3294029476,"StartTime":43363.0,"Objects":[{"StartTime":43363.0,"EndTime":43363.0,"Column":5}]},{"RandomW":268717689,"RandomX":3731020989,"RandomY":3294029476,"RandomZ":692368043,"StartTime":43546.0,"Objects":[{"StartTime":43546.0,"EndTime":43546.0,"Column":4}]},{"RandomW":3628859376,"RandomX":3294029476,"RandomY":692368043,"RandomZ":268717689,"StartTime":43728.0,"Objects":[{"StartTime":43728.0,"EndTime":43728.0,"Column":3}]},{"RandomW":2810605489,"RandomX":268717689,"RandomY":3628859376,"RandomZ":2874884507,"StartTime":43911.0,"Objects":[{"StartTime":43911.0,"EndTime":44093.0,"Column":2}]},{"RandomW":317739913,"RandomX":2874884507,"RandomY":2810605489,"RandomZ":2512620222,"StartTime":44277.0,"Objects":[{"StartTime":44277.0,"EndTime":44459.0,"Column":1}]},{"RandomW":967116709,"RandomX":4156133369,"RandomY":2124840394,"RandomZ":3998877068,"StartTime":44643.0,"Objects":[{"StartTime":44643.0,"EndTime":44825.0,"Column":6},{"StartTime":44643.0,"EndTime":44825.0,"Column":3}]},{"RandomW":1331553411,"RandomX":3998877068,"RandomY":967116709,"RandomZ":39354671,"StartTime":45009.0,"Objects":[{"StartTime":45009.0,"EndTime":45374.0,"Column":4}]},{"RandomW":2785797100,"RandomX":1331553411,"RandomY":1897266817,"RandomZ":1620854569,"StartTime":45558.0,"Objects":[{"StartTime":45558.0,"EndTime":45923.0,"Column":5},{"StartTime":45558.0,"EndTime":45923.0,"Column":2}]},{"RandomW":114455122,"RandomX":1897266817,"RandomY":1620854569,"RandomZ":2785797100,"StartTime":46106.0,"Objects":[{"StartTime":46106.0,"EndTime":46106.0,"Column":3},{"StartTime":46106.0,"EndTime":46106.0,"Column":4}]},{"RandomW":3639436799,"RandomX":1620854569,"RandomY":2785797100,"RandomZ":114455122,"StartTime":46289.0,"Objects":[{"StartTime":46289.0,"EndTime":46289.0,"Column":4}]},{"RandomW":2239997850,"RandomX":1523242180,"RandomY":2737260786,"RandomZ":921894438,"StartTime":46472.0,"Objects":[{"StartTime":46472.0,"EndTime":46472.0,"Column":5},{"StartTime":46472.0,"EndTime":46472.0,"Column":3},{"StartTime":46654.0,"EndTime":46654.0,"Column":1},{"StartTime":46654.0,"EndTime":46654.0,"Column":5},{"StartTime":46836.0,"EndTime":46836.0,"Column":3},{"StartTime":46836.0,"EndTime":46836.0,"Column":1}]},{"RandomW":270173708,"RandomX":921894438,"RandomY":2239997850,"RandomZ":2313367322,"StartTime":47021.0,"Objects":[{"StartTime":47021.0,"EndTime":47386.0,"Column":0}]},{"RandomW":2981644775,"RandomX":2239997850,"RandomY":2313367322,"RandomZ":270173708,"StartTime":47570.0,"Objects":[{"StartTime":47570.0,"EndTime":47570.0,"Column":5}]},{"RandomW":698324797,"RandomX":2313367322,"RandomY":270173708,"RandomZ":2981644775,"StartTime":47936.0,"Objects":[{"StartTime":47936.0,"EndTime":47936.0,"Column":2}]},{"RandomW":2105158963,"RandomX":2981644775,"RandomY":698324797,"RandomZ":3113547499,"StartTime":48119.0,"Objects":[{"StartTime":48119.0,"EndTime":48667.0,"Column":6}]},{"RandomW":3675126935,"RandomX":3113547499,"RandomY":2105158963,"RandomZ":251569162,"StartTime":48850.0,"Objects":[{"StartTime":48850.0,"EndTime":49398.0,"Column":4}]},{"RandomW":1771033747,"RandomX":251569162,"RandomY":3675126935,"RandomZ":3308284595,"StartTime":49582.0,"Objects":[{"StartTime":49582.0,"EndTime":50130.0,"Column":5}]},{"RandomW":653741274,"RandomX":3308284595,"RandomY":1771033747,"RandomZ":2460676956,"StartTime":50314.0,"Objects":[{"StartTime":50314.0,"EndTime":50862.0,"Column":2}]},{"RandomW":3908591175,"RandomX":2011739264,"RandomY":2988284210,"RandomZ":772833847,"StartTime":51046.0,"Objects":[{"StartTime":51046.0,"EndTime":51594.0,"Column":6},{"StartTime":51046.0,"EndTime":51594.0,"Column":5}]},{"RandomW":782718603,"RandomX":3908591175,"RandomY":3666262892,"RandomZ":2215410951,"StartTime":51777.0,"Objects":[{"StartTime":51777.0,"EndTime":51959.0,"Column":0},{"StartTime":51777.0,"EndTime":51959.0,"Column":2}]},{"RandomW":3946166617,"RandomX":2215410951,"RandomY":782718603,"RandomZ":75972478,"StartTime":52143.0,"Objects":[{"StartTime":52143.0,"EndTime":52508.0,"Column":5}]},{"RandomW":204866941,"RandomX":782718603,"RandomY":75972478,"RandomZ":3946166617,"StartTime":52692.0,"Objects":[{"StartTime":52692.0,"EndTime":52692.0,"Column":3},{"StartTime":52692.0,"EndTime":52692.0,"Column":4}]},{"RandomW":628140489,"RandomX":3946166617,"RandomY":204866941,"RandomZ":405870974,"StartTime":52875.0,"Objects":[{"StartTime":52875.0,"EndTime":53240.0,"Column":2}]},{"RandomW":1325586396,"RandomX":628140489,"RandomY":1674126159,"RandomZ":3748192166,"StartTime":53424.0,"Objects":[{"StartTime":53424.0,"EndTime":53606.0,"Column":5},{"StartTime":53424.0,"EndTime":53606.0,"Column":4}]},{"RandomW":3311768819,"RandomX":3748192166,"RandomY":1325586396,"RandomZ":4019978516,"StartTime":53789.0,"Objects":[{"StartTime":53789.0,"EndTime":53789.0,"Column":4},{"StartTime":53789.0,"EndTime":53789.0,"Column":3}]},{"RandomW":1550448150,"RandomX":1325586396,"RandomY":4019978516,"RandomZ":3311768819,"StartTime":53972.0,"Objects":[{"StartTime":53972.0,"EndTime":53972.0,"Column":5}]},{"RandomW":169296756,"RandomX":3311768819,"RandomY":1550448150,"RandomZ":93091440,"StartTime":54155.0,"Objects":[{"StartTime":54155.0,"EndTime":54337.0,"Column":0}]},{"RandomW":2528106598,"RandomX":169296756,"RandomY":3812396233,"RandomZ":4042657790,"StartTime":54521.0,"Objects":[{"StartTime":54521.0,"EndTime":54703.0,"Column":6},{"StartTime":54521.0,"EndTime":54703.0,"Column":1}]},{"RandomW":1636289987,"RandomX":2528106598,"RandomY":638788900,"RandomZ":558809067,"StartTime":54887.0,"Objects":[{"StartTime":54887.0,"EndTime":55069.0,"Column":5}]},{"RandomW":914779004,"RandomX":558809067,"RandomY":1636289987,"RandomZ":2298692989,"StartTime":55253.0,"Objects":[{"StartTime":55253.0,"EndTime":55618.0,"Column":2}]},{"RandomW":1650670496,"RandomX":1636289987,"RandomY":2298692989,"RandomZ":914779004,"StartTime":55802.0,"Objects":[{"StartTime":55802.0,"EndTime":55802.0,"Column":2}]},{"RandomW":3497220679,"RandomX":1037372410,"RandomY":2926479760,"RandomZ":2880883370,"StartTime":55985.0,"Objects":[{"StartTime":55985.0,"EndTime":56350.0,"Column":4}]},{"RandomW":1164710248,"RandomX":2926479760,"RandomY":2880883370,"RandomZ":3497220679,"StartTime":56533.0,"Objects":[{"StartTime":56533.0,"EndTime":56533.0,"Column":5}]},{"RandomW":2188007582,"RandomX":3497220679,"RandomY":1164710248,"RandomZ":2677289564,"StartTime":56716.0,"Objects":[{"StartTime":56716.0,"EndTime":57081.0,"Column":0}]},{"RandomW":3363933174,"RandomX":1164710248,"RandomY":2677289564,"RandomZ":2188007582,"StartTime":57265.0,"Objects":[{"StartTime":57265.0,"EndTime":57265.0,"Column":3}]},{"RandomW":50721184,"RandomX":3363933174,"RandomY":3980600543,"RandomZ":3548114425,"StartTime":57448.0,"Objects":[{"StartTime":57448.0,"EndTime":57630.0,"Column":4},{"StartTime":57448.0,"EndTime":57630.0,"Column":0}]},{"RandomW":864990701,"RandomX":3548114425,"RandomY":50721184,"RandomZ":3340702733,"StartTime":57814.0,"Objects":[{"StartTime":57814.0,"EndTime":57996.0,"Column":2}]},{"RandomW":322108643,"RandomX":3340702733,"RandomY":864990701,"RandomZ":1066828352,"StartTime":58180.0,"Objects":[{"StartTime":58180.0,"EndTime":58362.0,"Column":1}]},{"RandomW":1792394322,"RandomX":1066828352,"RandomY":322108643,"RandomZ":749878772,"StartTime":58546.0,"Objects":[{"StartTime":58546.0,"EndTime":58728.0,"Column":5}]},{"RandomW":475567653,"RandomX":3789213642,"RandomY":1703666422,"RandomZ":3630902830,"StartTime":58911.0,"Objects":[{"StartTime":58911.0,"EndTime":59093.0,"Column":4},{"StartTime":58911.0,"EndTime":59093.0,"Column":1}]},{"RandomW":292381990,"RandomX":3630902830,"RandomY":475567653,"RandomZ":734768891,"StartTime":59277.0,"Objects":[{"StartTime":59277.0,"EndTime":59459.0,"Column":0}]},{"RandomW":1221027582,"RandomX":734768891,"RandomY":292381990,"RandomZ":2432050043,"StartTime":59643.0,"Objects":[{"StartTime":59643.0,"EndTime":60008.0,"Column":3}]},{"RandomW":1041081707,"RandomX":292381990,"RandomY":2432050043,"RandomZ":1221027582,"StartTime":60192.0,"Objects":[{"StartTime":60192.0,"EndTime":60192.0,"Column":0}]},{"RandomW":1144239065,"RandomX":2432050043,"RandomY":1221027582,"RandomZ":1041081707,"StartTime":60375.0,"Objects":[{"StartTime":60375.0,"EndTime":60375.0,"Column":1}]},{"RandomW":1711255007,"RandomX":1221027582,"RandomY":1041081707,"RandomZ":1144239065,"StartTime":60558.0,"Objects":[{"StartTime":60558.0,"EndTime":60558.0,"Column":2},{"StartTime":60558.0,"EndTime":60558.0,"Column":3}]},{"RandomW":377276168,"RandomX":1041081707,"RandomY":1144239065,"RandomZ":1711255007,"StartTime":60649.0,"Objects":[{"StartTime":60649.0,"EndTime":60649.0,"Column":1}]},{"RandomW":377276168,"RandomX":1041081707,"RandomY":1144239065,"RandomZ":1711255007,"StartTime":60741.0,"Objects":[{"StartTime":60741.0,"EndTime":60741.0,"Column":2}]},{"RandomW":1158225489,"RandomX":1144239065,"RandomY":1711255007,"RandomZ":377276168,"StartTime":60924.0,"Objects":[{"StartTime":60924.0,"EndTime":60924.0,"Column":3}]},{"RandomW":74717015,"RandomX":377276168,"RandomY":1158225489,"RandomZ":2625486930,"StartTime":61106.0,"Objects":[{"StartTime":61106.0,"EndTime":61288.0,"Column":0}]},{"RandomW":4106277974,"RandomX":1158225489,"RandomY":2625486930,"RandomZ":74717015,"StartTime":61472.0,"Objects":[{"StartTime":61472.0,"EndTime":61472.0,"Column":2},{"StartTime":61472.0,"EndTime":61472.0,"Column":3}]},{"RandomW":3720471658,"RandomX":4181108489,"RandomY":2335938349,"RandomZ":793896882,"StartTime":61655.0,"Objects":[{"StartTime":61655.0,"EndTime":61655.0,"Column":6},{"StartTime":61746.0,"EndTime":61746.0,"Column":0},{"StartTime":61837.0,"EndTime":61837.0,"Column":2}]},{"RandomW":3031050452,"RandomX":2441289268,"RandomY":3327554006,"RandomZ":1721397977,"StartTime":62021.0,"Objects":[{"StartTime":62021.0,"EndTime":62021.0,"Column":0},{"StartTime":62112.0,"EndTime":62112.0,"Column":3},{"StartTime":62203.0,"EndTime":62203.0,"Column":5}]},{"RandomW":1028780747,"RandomX":3327554006,"RandomY":1721397977,"RandomZ":3031050452,"StartTime":62387.0,"Objects":[{"StartTime":62387.0,"EndTime":62387.0,"Column":1}]},{"RandomW":4249178890,"RandomX":3031050452,"RandomY":1028780747,"RandomZ":1224535158,"StartTime":62570.0,"Objects":[{"StartTime":62570.0,"EndTime":62935.0,"Column":6}]},{"RandomW":407644414,"RandomX":1028780747,"RandomY":1224535158,"RandomZ":4249178890,"StartTime":63119.0,"Objects":[{"StartTime":63119.0,"EndTime":63119.0,"Column":4}]},{"RandomW":84513019,"RandomX":4249178890,"RandomY":407644414,"RandomZ":2855880342,"StartTime":63302.0,"Objects":[{"StartTime":63302.0,"EndTime":63667.0,"Column":0}]},{"RandomW":2876344117,"RandomX":2855880342,"RandomY":84513019,"RandomZ":3523432019,"StartTime":63850.0,"Objects":[{"StartTime":63850.0,"EndTime":63850.0,"Column":5},{"StartTime":63850.0,"EndTime":63850.0,"Column":2}]},{"RandomW":1247936821,"RandomX":2876344117,"RandomY":3407636795,"RandomZ":2195437291,"StartTime":64033.0,"Objects":[{"StartTime":64033.0,"EndTime":64033.0,"Column":1},{"StartTime":64033.0,"EndTime":64033.0,"Column":5}]},{"RandomW":1165002312,"RandomX":2195437291,"RandomY":1247936821,"RandomZ":1829597027,"StartTime":64216.0,"Objects":[{"StartTime":64216.0,"EndTime":64216.0,"Column":6},{"StartTime":64216.0,"EndTime":64216.0,"Column":3}]},{"RandomW":440601827,"RandomX":1247936821,"RandomY":1829597027,"RandomZ":1165002312,"StartTime":64399.0,"Objects":[{"StartTime":64399.0,"EndTime":64399.0,"Column":6},{"StartTime":64399.0,"EndTime":64399.0,"Column":0}]},{"RandomW":1174586413,"RandomX":1165002312,"RandomY":440601827,"RandomZ":1081265463,"StartTime":64582.0,"Objects":[{"StartTime":64582.0,"EndTime":64947.0,"Column":3}]},{"RandomW":1399461522,"RandomX":1174586413,"RandomY":2273396835,"RandomZ":2242340964,"StartTime":65131.0,"Objects":[{"StartTime":65131.0,"EndTime":65313.0,"Column":0},{"StartTime":65131.0,"EndTime":65313.0,"Column":4}]},{"RandomW":806141128,"RandomX":1399461522,"RandomY":52007806,"RandomZ":2388001070,"StartTime":65497.0,"Objects":[{"StartTime":65497.0,"EndTime":65862.0,"Column":2}]},{"RandomW":869393117,"RandomX":52007806,"RandomY":2388001070,"RandomZ":806141128,"StartTime":66046.0,"Objects":[{"StartTime":66046.0,"EndTime":66046.0,"Column":2}]},{"RandomW":2114055664,"RandomX":932480042,"RandomY":484530218,"RandomZ":2599754617,"StartTime":66228.0,"Objects":[{"StartTime":66228.0,"EndTime":66593.0,"Column":3},{"StartTime":66228.0,"EndTime":66593.0,"Column":1},{"StartTime":66228.0,"EndTime":66593.0,"Column":6}]},{"RandomW":4212241992,"RandomX":2599754617,"RandomY":2114055664,"RandomZ":3978789838,"StartTime":66777.0,"Objects":[{"StartTime":66777.0,"EndTime":66777.0,"Column":0},{"StartTime":66777.0,"EndTime":66777.0,"Column":6}]},{"RandomW":1778029315,"RandomX":4212241992,"RandomY":3373094016,"RandomZ":3088207420,"StartTime":66960.0,"Objects":[{"StartTime":66960.0,"EndTime":67142.0,"Column":3},{"StartTime":66960.0,"EndTime":67142.0,"Column":5}]},{"RandomW":523225986,"RandomX":3373094016,"RandomY":3088207420,"RandomZ":1778029315,"StartTime":67326.0,"Objects":[{"StartTime":67326.0,"EndTime":67326.0,"Column":2},{"StartTime":67326.0,"EndTime":67326.0,"Column":3}]},{"RandomW":2523721637,"RandomX":1778029315,"RandomY":523225986,"RandomZ":3156555187,"StartTime":67509.0,"Objects":[{"StartTime":67509.0,"EndTime":67874.0,"Column":1}]},{"RandomW":3753678213,"RandomX":2523721637,"RandomY":733156576,"RandomZ":1252112847,"StartTime":68058.0,"Objects":[{"StartTime":68058.0,"EndTime":68240.0,"Column":4},{"StartTime":68058.0,"EndTime":68240.0,"Column":5}]},{"RandomW":765988363,"RandomX":2650496303,"RandomY":3671318686,"RandomZ":3791148796,"StartTime":68424.0,"Objects":[{"StartTime":68424.0,"EndTime":68789.0,"Column":1},{"StartTime":68424.0,"EndTime":68789.0,"Column":2}]},{"RandomW":1639351583,"RandomX":1794981044,"RandomY":795866725,"RandomZ":201525954,"StartTime":68972.0,"Objects":[{"StartTime":68972.0,"EndTime":69337.0,"Column":0},{"StartTime":68972.0,"EndTime":69337.0,"Column":5}]},{"RandomW":3794603265,"RandomX":795866725,"RandomY":201525954,"RandomZ":1639351583,"StartTime":69521.0,"Objects":[{"StartTime":69521.0,"EndTime":69521.0,"Column":4},{"StartTime":69521.0,"EndTime":69521.0,"Column":5}]},{"RandomW":2799716979,"RandomX":1639351583,"RandomY":3794603265,"RandomZ":2996900863,"StartTime":69704.0,"Objects":[{"StartTime":69704.0,"EndTime":69886.0,"Column":2}]},{"RandomW":1138768260,"RandomX":2799716979,"RandomY":1940635085,"RandomZ":4184142780,"StartTime":70070.0,"Objects":[{"StartTime":70070.0,"EndTime":70252.0,"Column":6},{"StartTime":70070.0,"EndTime":70252.0,"Column":3}]},{"RandomW":3382500543,"RandomX":4184142780,"RandomY":1138768260,"RandomZ":3891744857,"StartTime":70436.0,"Objects":[{"StartTime":70436.0,"EndTime":70436.0,"Column":3},{"StartTime":70436.0,"EndTime":70436.0,"Column":4}]},{"RandomW":665559990,"RandomX":398143267,"RandomY":1440028745,"RandomZ":150863666,"StartTime":70619.0,"Objects":[{"StartTime":70619.0,"EndTime":70984.0,"Column":0},{"StartTime":70619.0,"EndTime":70984.0,"Column":2}]},{"RandomW":340592762,"RandomX":150863666,"RandomY":665559990,"RandomZ":3920056919,"StartTime":71167.0,"Objects":[{"StartTime":71167.0,"EndTime":71167.0,"Column":5},{"StartTime":71167.0,"EndTime":71167.0,"Column":1}]},{"RandomW":1518605551,"RandomX":340592762,"RandomY":4088291758,"RandomZ":2304957054,"StartTime":71350.0,"Objects":[{"StartTime":71350.0,"EndTime":71532.0,"Column":0},{"StartTime":71350.0,"EndTime":71532.0,"Column":4}]},{"RandomW":972812530,"RandomX":1518605551,"RandomY":653707549,"RandomZ":2799009660,"StartTime":71716.0,"Objects":[{"StartTime":71716.0,"EndTime":71898.0,"Column":2},{"StartTime":71716.0,"EndTime":71898.0,"Column":3}]},{"RandomW":3736044692,"RandomX":972812530,"RandomY":1134737486,"RandomZ":3549179654,"StartTime":72082.0,"Objects":[{"StartTime":72082.0,"EndTime":72264.0,"Column":4},{"StartTime":72082.0,"EndTime":72264.0,"Column":5}]},{"RandomW":2646968586,"RandomX":3695561354,"RandomY":2121039538,"RandomZ":3939713463,"StartTime":72448.0,"Objects":[{"StartTime":72448.0,"EndTime":72630.0,"Column":6},{"StartTime":72448.0,"EndTime":72630.0,"Column":1}]},{"RandomW":34357760,"RandomX":2646968586,"RandomY":1864765858,"RandomZ":1923246874,"StartTime":72814.0,"Objects":[{"StartTime":72814.0,"EndTime":72814.0,"Column":0},{"StartTime":72814.0,"EndTime":72814.0,"Column":6}]},{"RandomW":34357760,"RandomX":2646968586,"RandomY":1864765858,"RandomZ":1923246874,"StartTime":72997.0,"Objects":[{"StartTime":72997.0,"EndTime":72997.0,"Column":6},{"StartTime":72997.0,"EndTime":72997.0,"Column":0}]},{"RandomW":3006273170,"RandomX":1864765858,"RandomY":1923246874,"RandomZ":34357760,"StartTime":73363.0,"Objects":[{"StartTime":73363.0,"EndTime":73363.0,"Column":4},{"StartTime":73363.0,"EndTime":73363.0,"Column":5}]},{"RandomW":3978541447,"RandomX":3006273170,"RandomY":3972311639,"RandomZ":2371876462,"StartTime":73728.0,"Objects":[{"StartTime":73728.0,"EndTime":73728.0,"Column":2},{"StartTime":73819.0,"EndTime":73819.0,"Column":5},{"StartTime":73910.0,"EndTime":73910.0,"Column":0}]},{"RandomW":399194528,"RandomX":2371876462,"RandomY":3978541447,"RandomZ":3734283831,"StartTime":74094.0,"Objects":[{"StartTime":74094.0,"EndTime":74094.0,"Column":3},{"StartTime":74094.0,"EndTime":74094.0,"Column":1}]},{"RandomW":2883430810,"RandomX":4002716317,"RandomY":2698819798,"RandomZ":1875619237,"StartTime":74277.0,"Objects":[{"StartTime":74277.0,"EndTime":74642.0,"Column":6},{"StartTime":74277.0,"EndTime":74642.0,"Column":2}]},{"RandomW":3502984571,"RandomX":3789000206,"RandomY":2760409322,"RandomZ":2518464347,"StartTime":74826.0,"Objects":[{"StartTime":74826.0,"EndTime":74826.0,"Column":1},{"StartTime":74826.0,"EndTime":74826.0,"Column":4}]},{"RandomW":2447473462,"RandomX":1834893326,"RandomY":512459921,"RandomZ":2493625006,"StartTime":75009.0,"Objects":[{"StartTime":75009.0,"EndTime":75374.0,"Column":5},{"StartTime":75009.0,"EndTime":75374.0,"Column":0}]},{"RandomW":236980020,"RandomX":512459921,"RandomY":2493625006,"RandomZ":2447473462,"StartTime":75558.0,"Objects":[{"StartTime":75558.0,"EndTime":75558.0,"Column":4}]},{"RandomW":1338160073,"RandomX":236980020,"RandomY":1288545645,"RandomZ":3579861656,"StartTime":75741.0,"Objects":[{"StartTime":75741.0,"EndTime":75741.0,"Column":1},{"StartTime":75741.0,"EndTime":75741.0,"Column":5}]},{"RandomW":1104479394,"RandomX":1288545645,"RandomY":3579861656,"RandomZ":1338160073,"StartTime":75924.0,"Objects":[{"StartTime":75924.0,"EndTime":75924.0,"Column":6}]},{"RandomW":1611802424,"RandomX":3579861656,"RandomY":1338160073,"RandomZ":1104479394,"StartTime":76106.0,"Objects":[{"StartTime":76106.0,"EndTime":76106.0,"Column":5},{"StartTime":76106.0,"EndTime":76106.0,"Column":6}]},{"RandomW":74337788,"RandomX":1611802424,"RandomY":3077637432,"RandomZ":3984045284,"StartTime":76289.0,"Objects":[{"StartTime":76289.0,"EndTime":76654.0,"Column":0}]},{"RandomW":2589155279,"RandomX":74337788,"RandomY":4122247598,"RandomZ":3402826469,"StartTime":76838.0,"Objects":[{"StartTime":76838.0,"EndTime":77020.0,"Column":4},{"StartTime":76838.0,"EndTime":77020.0,"Column":1}]},{"RandomW":4015672441,"RandomX":2589155279,"RandomY":3961839828,"RandomZ":3184309519,"StartTime":77204.0,"Objects":[{"StartTime":77204.0,"EndTime":77569.0,"Column":3},{"StartTime":77204.0,"EndTime":77569.0,"Column":6}]},{"RandomW":605987856,"RandomX":3184309519,"RandomY":4015672441,"RandomZ":4025998202,"StartTime":77753.0,"Objects":[{"StartTime":77753.0,"EndTime":77753.0,"Column":0},{"StartTime":77753.0,"EndTime":77753.0,"Column":1}]},{"RandomW":1497070673,"RandomX":2430309501,"RandomY":1093966930,"RandomZ":2905669028,"StartTime":77936.0,"Objects":[{"StartTime":77936.0,"EndTime":78301.0,"Column":3},{"StartTime":77936.0,"EndTime":78301.0,"Column":2},{"StartTime":77936.0,"EndTime":78301.0,"Column":4}]},{"RandomW":353334135,"RandomX":1093966930,"RandomY":2905669028,"RandomZ":1497070673,"StartTime":78485.0,"Objects":[{"StartTime":78485.0,"EndTime":78485.0,"Column":1}]},{"RandomW":912971684,"RandomX":4030507912,"RandomY":3670783478,"RandomZ":1485865738,"StartTime":78667.0,"Objects":[{"StartTime":78667.0,"EndTime":78849.0,"Column":4},{"StartTime":78667.0,"EndTime":78849.0,"Column":2}]},{"RandomW":589257226,"RandomX":3670783478,"RandomY":1485865738,"RandomZ":912971684,"StartTime":79033.0,"Objects":[{"StartTime":79033.0,"EndTime":79033.0,"Column":3},{"StartTime":79033.0,"EndTime":79033.0,"Column":4}]},{"RandomW":2024304860,"RandomX":912971684,"RandomY":589257226,"RandomZ":2767994778,"StartTime":79216.0,"Objects":[{"StartTime":79216.0,"EndTime":79581.0,"Column":6}]},{"RandomW":2219601613,"RandomX":2024304860,"RandomY":404709274,"RandomZ":3238631833,"StartTime":79765.0,"Objects":[{"StartTime":79765.0,"EndTime":79947.0,"Column":3},{"StartTime":79765.0,"EndTime":79947.0,"Column":0}]},{"RandomW":3490718869,"RandomX":2219601613,"RandomY":3210330120,"RandomZ":1566096374,"StartTime":80131.0,"Objects":[{"StartTime":80131.0,"EndTime":80496.0,"Column":5},{"StartTime":80131.0,"EndTime":80496.0,"Column":4}]},{"RandomW":1189469485,"RandomX":1566096374,"RandomY":3490718869,"RandomZ":936182364,"StartTime":80680.0,"Objects":[{"StartTime":80680.0,"EndTime":81045.0,"Column":3}]},{"RandomW":3740948748,"RandomX":3490718869,"RandomY":936182364,"RandomZ":1189469485,"StartTime":81228.0,"Objects":[{"StartTime":81228.0,"EndTime":81228.0,"Column":5},{"StartTime":81228.0,"EndTime":81228.0,"Column":6}]},{"RandomW":3491747463,"RandomX":1189469485,"RandomY":3740948748,"RandomZ":2409626314,"StartTime":81411.0,"Objects":[{"StartTime":81411.0,"EndTime":81776.0,"Column":4}]},{"RandomW":3095098652,"RandomX":3740948748,"RandomY":2409626314,"RandomZ":3491747463,"StartTime":81960.0,"Objects":[{"StartTime":81960.0,"EndTime":81960.0,"Column":3},{"StartTime":81960.0,"EndTime":81960.0,"Column":4}]},{"RandomW":3024447782,"RandomX":2409626314,"RandomY":3491747463,"RandomZ":3095098652,"StartTime":82143.0,"Objects":[{"StartTime":82143.0,"EndTime":82143.0,"Column":2},{"StartTime":82143.0,"EndTime":82143.0,"Column":3}]},{"RandomW":3942236456,"RandomX":3095098652,"RandomY":3024447782,"RandomZ":3296500942,"StartTime":82326.0,"Objects":[{"StartTime":82326.0,"EndTime":82508.0,"Column":5}]},{"RandomW":912304721,"RandomX":3942236456,"RandomY":2303302398,"RandomZ":383442600,"StartTime":82692.0,"Objects":[{"StartTime":82692.0,"EndTime":82874.0,"Column":1},{"StartTime":82692.0,"EndTime":82874.0,"Column":2}]},{"RandomW":2431170151,"RandomX":3622775798,"RandomY":385908797,"RandomZ":604082862,"StartTime":83058.0,"Objects":[{"StartTime":83058.0,"EndTime":83240.0,"Column":4},{"StartTime":83058.0,"EndTime":83240.0,"Column":0}]},{"RandomW":4088921973,"RandomX":1523770388,"RandomY":1345324755,"RandomZ":2436511051,"StartTime":83424.0,"Objects":[{"StartTime":83424.0,"EndTime":83606.0,"Column":2},{"StartTime":83424.0,"EndTime":83606.0,"Column":6}]},{"RandomW":2663434012,"RandomX":3999189199,"RandomY":2928551970,"RandomZ":3800966865,"StartTime":83789.0,"Objects":[{"StartTime":83789.0,"EndTime":83971.0,"Column":5},{"StartTime":83789.0,"EndTime":83971.0,"Column":1}]},{"RandomW":183339481,"RandomX":3405481532,"RandomY":1385906264,"RandomZ":3611020052,"StartTime":84155.0,"Objects":[{"StartTime":84155.0,"EndTime":84337.0,"Column":4},{"StartTime":84155.0,"EndTime":84337.0,"Column":0}]},{"RandomW":472982750,"RandomX":1385906264,"RandomY":3611020052,"RandomZ":183339481,"StartTime":84521.0,"Objects":[{"StartTime":84521.0,"EndTime":84521.0,"Column":4}]},{"RandomW":2485141120,"RandomX":3611020052,"RandomY":183339481,"RandomZ":472982750,"StartTime":84704.0,"Objects":[{"StartTime":84704.0,"EndTime":84704.0,"Column":5}]},{"RandomW":2638881915,"RandomX":183339481,"RandomY":472982750,"RandomZ":2485141120,"StartTime":84887.0,"Objects":[{"StartTime":84887.0,"EndTime":84887.0,"Column":6},{"StartTime":84887.0,"EndTime":84887.0,"Column":0}]},{"RandomW":2991178386,"RandomX":1846348081,"RandomY":4216122958,"RandomZ":938042528,"StartTime":85070.0,"Objects":[{"StartTime":85070.0,"EndTime":85070.0,"Column":5},{"StartTime":85161.0,"EndTime":85161.0,"Column":6},{"StartTime":85252.0,"EndTime":85252.0,"Column":3}]},{"RandomW":2830634920,"RandomX":3020624235,"RandomY":682207034,"RandomZ":1410927339,"StartTime":85436.0,"Objects":[{"StartTime":85436.0,"EndTime":85436.0,"Column":4},{"StartTime":85527.0,"EndTime":85527.0,"Column":2},{"StartTime":85618.0,"EndTime":85618.0,"Column":4}]},{"RandomW":1154798493,"RandomX":682207034,"RandomY":1410927339,"RandomZ":2830634920,"StartTime":85802.0,"Objects":[{"StartTime":85802.0,"EndTime":85802.0,"Column":3}]},{"RandomW":3579754894,"RandomX":1154798493,"RandomY":555826250,"RandomZ":3186828503,"StartTime":85985.0,"Objects":[{"StartTime":85985.0,"EndTime":86167.0,"Column":4}]},{"RandomW":522156379,"RandomX":3186828503,"RandomY":3579754894,"RandomZ":938791043,"StartTime":86350.0,"Objects":[{"StartTime":86350.0,"EndTime":86532.0,"Column":1}]},{"RandomW":2327696617,"RandomX":522156379,"RandomY":1005466611,"RandomZ":459042761,"StartTime":86716.0,"Objects":[{"StartTime":86716.0,"EndTime":86898.0,"Column":0}]},{"RandomW":3698157493,"RandomX":2327696617,"RandomY":1854714180,"RandomZ":615999181,"StartTime":87082.0,"Objects":[{"StartTime":87082.0,"EndTime":87264.0,"Column":2},{"StartTime":87082.0,"EndTime":87264.0,"Column":5}]},{"RandomW":2615638464,"RandomX":3088317005,"RandomY":3005119130,"RandomZ":738255674,"StartTime":87448.0,"Objects":[{"StartTime":87448.0,"EndTime":87448.0,"Column":4},{"StartTime":87448.0,"EndTime":87448.0,"Column":1},{"StartTime":87630.0,"EndTime":87630.0,"Column":2},{"StartTime":87630.0,"EndTime":87630.0,"Column":5},{"StartTime":87812.0,"EndTime":87812.0,"Column":2},{"StartTime":87812.0,"EndTime":87812.0,"Column":5}]},{"RandomW":4236988115,"RandomX":738255674,"RandomY":2615638464,"RandomZ":3154196835,"StartTime":87997.0,"Objects":[{"StartTime":87997.0,"EndTime":88362.0,"Column":6}]},{"RandomW":3260011681,"RandomX":4236988115,"RandomY":3619257163,"RandomZ":1999646981,"StartTime":88546.0,"Objects":[{"StartTime":88546.0,"EndTime":88728.0,"Column":3}]},{"RandomW":1679091693,"RandomX":3619257163,"RandomY":1999646981,"RandomZ":3260011681,"StartTime":88911.0,"Objects":[{"StartTime":88911.0,"EndTime":88911.0,"Column":1},{"StartTime":88911.0,"EndTime":88911.0,"Column":2}]},{"RandomW":4053500035,"RandomX":2020322055,"RandomY":2384790806,"RandomZ":846406319,"StartTime":89277.0,"Objects":[{"StartTime":89277.0,"EndTime":89459.0,"Column":0},{"StartTime":89277.0,"EndTime":89459.0,"Column":6}]},{"RandomW":3656101543,"RandomX":4053500035,"RandomY":3566026276,"RandomZ":1915132950,"StartTime":89643.0,"Objects":[{"StartTime":89643.0,"EndTime":89825.0,"Column":4}]},{"RandomW":3002483376,"RandomX":1234751024,"RandomY":253242681,"RandomZ":2332173547,"StartTime":90009.0,"Objects":[{"StartTime":90009.0,"EndTime":90191.0,"Column":0},{"StartTime":90009.0,"EndTime":90191.0,"Column":2}]},{"RandomW":1769147212,"RandomX":1032909712,"RandomY":4079968510,"RandomZ":1771054860,"StartTime":90375.0,"Objects":[{"StartTime":90375.0,"EndTime":90375.0,"Column":3},{"StartTime":90375.0,"EndTime":90375.0,"Column":6},{"StartTime":90557.0,"EndTime":90557.0,"Column":6},{"StartTime":90557.0,"EndTime":90557.0,"Column":3},{"StartTime":90739.0,"EndTime":90739.0,"Column":5},{"StartTime":90739.0,"EndTime":90739.0,"Column":2}]},{"RandomW":1533402007,"RandomX":1771054860,"RandomY":1769147212,"RandomZ":3552934273,"StartTime":90924.0,"Objects":[{"StartTime":90924.0,"EndTime":91289.0,"Column":4}]},{"RandomW":1123904499,"RandomX":3552934273,"RandomY":1533402007,"RandomZ":3005562800,"StartTime":91472.0,"Objects":[{"StartTime":91472.0,"EndTime":91654.0,"Column":3}]},{"RandomW":3485521641,"RandomX":3005562800,"RandomY":1123904499,"RandomZ":3121355612,"StartTime":91838.0,"Objects":[{"StartTime":91838.0,"EndTime":92203.0,"Column":4}]},{"RandomW":1434626078,"RandomX":1123904499,"RandomY":3121355612,"RandomZ":3485521641,"StartTime":92387.0,"Objects":[{"StartTime":92387.0,"EndTime":92387.0,"Column":4}]},{"RandomW":4013632575,"RandomX":1434626078,"RandomY":4236899246,"RandomZ":646300056,"StartTime":92570.0,"Objects":[{"StartTime":92570.0,"EndTime":92752.0,"Column":2},{"StartTime":92570.0,"EndTime":92752.0,"Column":6}]},{"RandomW":471738692,"RandomX":646300056,"RandomY":4013632575,"RandomZ":2948180894,"StartTime":92936.0,"Objects":[{"StartTime":92936.0,"EndTime":93118.0,"Column":1}]},{"RandomW":1081382077,"RandomX":471738692,"RandomY":346006110,"RandomZ":586362406,"StartTime":93302.0,"Objects":[{"StartTime":93302.0,"EndTime":93302.0,"Column":1},{"StartTime":93302.0,"EndTime":93302.0,"Column":5}]},{"RandomW":1151929163,"RandomX":586362406,"RandomY":1081382077,"RandomZ":2915942910,"StartTime":93485.0,"Objects":[{"StartTime":93485.0,"EndTime":93667.0,"Column":3}]},{"RandomW":3634683246,"RandomX":1151929163,"RandomY":4287668198,"RandomZ":463810005,"StartTime":93850.0,"Objects":[{"StartTime":93850.0,"EndTime":94215.0,"Column":1},{"StartTime":93850.0,"EndTime":94215.0,"Column":4}]},{"RandomW":2941238432,"RandomX":463810005,"RandomY":3634683246,"RandomZ":3562759778,"StartTime":94399.0,"Objects":[{"StartTime":94399.0,"EndTime":94581.0,"Column":2}]},{"RandomW":1661408876,"RandomX":3562759778,"RandomY":2941238432,"RandomZ":2646009625,"StartTime":94765.0,"Objects":[{"StartTime":94765.0,"EndTime":95130.0,"Column":5}]},{"RandomW":3189251976,"RandomX":2646009625,"RandomY":1661408876,"RandomZ":1818231832,"StartTime":95314.0,"Objects":[{"StartTime":95314.0,"EndTime":95314.0,"Column":5},{"StartTime":95314.0,"EndTime":95314.0,"Column":3}]},{"RandomW":2743067846,"RandomX":3189251976,"RandomY":2495392125,"RandomZ":3478354416,"StartTime":95497.0,"Objects":[{"StartTime":95497.0,"EndTime":95497.0,"Column":0},{"StartTime":95497.0,"EndTime":95497.0,"Column":6}]},{"RandomW":2762867836,"RandomX":3722791806,"RandomY":2892228350,"RandomZ":4171994747,"StartTime":95680.0,"Objects":[{"StartTime":95680.0,"EndTime":95680.0,"Column":5},{"StartTime":95680.0,"EndTime":95680.0,"Column":3},{"StartTime":95862.0,"EndTime":95862.0,"Column":2},{"StartTime":95862.0,"EndTime":95862.0,"Column":6},{"StartTime":96044.0,"EndTime":96044.0,"Column":6},{"StartTime":96044.0,"EndTime":96044.0,"Column":4}]},{"RandomW":1153177485,"RandomX":2762867836,"RandomY":1407653164,"RandomZ":3758120376,"StartTime":96228.0,"Objects":[{"StartTime":96228.0,"EndTime":96228.0,"Column":1},{"StartTime":96228.0,"EndTime":96228.0,"Column":5}]},{"RandomW":1153177485,"RandomX":2762867836,"RandomY":1407653164,"RandomZ":3758120376,"StartTime":96411.0,"Objects":[{"StartTime":96411.0,"EndTime":96411.0,"Column":5},{"StartTime":96411.0,"EndTime":96411.0,"Column":1}]},{"RandomW":2430957186,"RandomX":1407653164,"RandomY":3758120376,"RandomZ":1153177485,"StartTime":96777.0,"Objects":[{"StartTime":96777.0,"EndTime":96777.0,"Column":3},{"StartTime":96777.0,"EndTime":96777.0,"Column":4}]},{"RandomW":4223688647,"RandomX":3758120376,"RandomY":1153177485,"RandomZ":2430957186,"StartTime":97143.0,"Objects":[{"StartTime":97143.0,"EndTime":97143.0,"Column":4},{"StartTime":97143.0,"EndTime":97143.0,"Column":5}]},{"RandomW":433008794,"RandomX":1153177485,"RandomY":2430957186,"RandomZ":4223688647,"StartTime":97509.0,"Objects":[{"StartTime":97509.0,"EndTime":97509.0,"Column":5},{"StartTime":97509.0,"EndTime":97509.0,"Column":6}]},{"RandomW":3177925713,"RandomX":2430957186,"RandomY":4223688647,"RandomZ":433008794,"StartTime":97692.0,"Objects":[{"StartTime":97692.0,"EndTime":100619.0,"Column":3}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/1450162.osu b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/1450162.osu new file mode 100644 index 0000000000..42669b1516 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/1450162.osu @@ -0,0 +1,297 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 +Mode: 0 + +[Difficulty] +HPDrainRate:5 +CircleSize:4 +OverallDifficulty:7 +ApproachRate:7.5 +SliderMultiplier:1.4 +SliderTickRate:1 + +[Events] +//Background and Video events +//Break Periods +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples + +[TimingPoints] +1107,365.853658536585,4,2,1,50,1,0 +1107,-166.666666666667,4,2,1,50,0,0 +6960,-111.111111111111,4,2,1,50,0,0 +8424,-100,4,2,1,50,0,0 +48119,-125,4,2,1,50,0,0 +52143,-100,4,2,1,50,0,0 +62570,-100,4,2,1,60,0,1 +85985,-100,4,2,1,50,0,0 +97692,-100,4,2,1,30,0,0 +99155,-100,4,2,1,20,0,0 +100619,-100,4,2,1,5,0,0 + +[HitObjects] +38,247,1107,6,0,P|96:269|170:192,1,167.999994873047,2|0,0:0|0:0,0:0:0:0: +201,128,2570,6,0,L|205:221,1,83.9999974365235,2|0,0:0|0:0,0:0:0:0: +242,230,3302,2,0,L|234:324,1,83.9999974365235,2|0,0:0|0:0,0:0:0:0: +205,343,4033,6,0,P|246:296|351:314,1,167.999994873047,2|0,0:0|0:0,0:0:0:0: +400,368,5497,6,0,L|412:269,1,83.9999974365235,6|0,0:0|0:0,0:0:0:0: +436,251,6228,2,0,P|425:203|408:153,1,83.9999974365235,2|0,0:0|0:0,0:0:0:0: +304,200,6960,6,0,P|262:186|234:181,1,62.9999980773926,6|0,0:0|0:0,0:0:0:0: +202,179,7326,1,8,0:0:0:0: +276,94,7509,2,0,P|313:92|353:87,1,62.9999980773926,2|0,0:0|0:0,0:0:0:0: +398,31,7875,1,2,0:0:0:0: +464,81,8058,2,0,L|450:150,1,62.9999980773926,2|0,0:0|0:0,0:0:0:0: +449,230,8424,6,0,P|347:206|306:217,1,140,2|8,0:0|0:0,0:0:0:0: +229,273,8972,2,0,P|225:339|235:361,1,70,2|0,0:0|0:0,0:0:0:0: +304,313,9338,1,8,0:0:0:0: +224,190,9521,1,2,0:0:0:0: +296,45,9887,6,0,P|297:97|288:125,1,70,6|0,0:0|0:0,0:0:0:0: +224,190,10253,1,8,0:0:0:0: +167,118,10436,1,8,0:0:0:0: +76,126,10619,1,8,0:0:0:0: +39,209,10802,1,8,0:0:0:0: +93,282,10985,1,10,0:0:0:0: +184,280,11167,1,10,0:0:0:0: +102,136,12814,5,2,0:0:0:0: +102,136,13180,2,0,L|199:130,1,70,8|0,0:0|0:0,0:0:0:0: +256,167,13546,2,0,L|339:161,1,70,8|2,0:0|0:0,0:0:0:0: +408,201,13911,2,0,P|454:176|471:143,1,70,8|2,0:0|0:0,0:0:0:0: +373,54,14277,6,0,L|396:137,2,70,6|0|8,0:0|0:0|0:0,0:0:0:0: +305,111,14826,2,0,L|287:274,1,140,0|2,0:0|0:0,0:0:0:0: +262,337,15375,2,0,L|349:327,1,70,8|2,0:0|0:0,0:0:0:0: +419,354,15741,1,8,0:0:0:0: +477,197,16106,6,0,P|423:197|385:209,1,70,8|0,0:0|0:0,0:0:0:0: +321,170,16472,2,0,P|278:190|253:219,1,70,8|2,0:0|0:0,0:0:0:0: +171,213,16838,2,0,P|152:259|158:304,1,70,8|2,0:0|0:0,0:0:0:0: +305,294,17204,6,0,L|224:278,2,70,6|0|8,0:0|0:0|0:0,0:0:0:0: +310,202,17753,2,0,L|149:214,1,140,0|2,0:0|0:0,0:0:0:0: +84,244,18302,2,0,L|92:152,1,70,8|2,0:0|0:0,0:0:0:0: +47,93,18667,6,0,P|78:53|176:80,1,140,6|8,0:0|0:0,0:0:0:0: +218,130,19216,1,0,0:0:0:0: +299,88,19399,2,0,L|387:91,1,70,8|0,0:0|0:0,0:0:0:0: +458,106,19765,2,0,P|447:139|444:205,1,70,8|0,0:0|0:0,0:0:0:0: +455,274,20131,5,2,0:0:0:0: +366,292,20314,2,0,L|353:211,1,70,0|8,0:0|0:0,0:0:0:0: +277,173,20680,2,0,L|253:342,1,140,0|2,0:0|0:0,0:0:0:0: +322,376,21228,2,0,P|368:368|416:370,1,70,8|2,0:0|0:0,0:0:0:0: +500,287,21594,6,0,P|427:273|362:293,2,140,6|8|8,0:0|0:0|0:0,0:0:0:0: +496,111,22509,1,8,0:0:0:0: +499,189,22692,2,0,L|418:191,1,70,8|2,0:0|0:0,0:0:0:0: +344,164,23058,5,6,0:0:0:0: +344,164,23241,1,12,0:0:0:0: +261,326,23606,2,0,L|246:178,1,140,8|2,0:0|0:0,0:0:0:0: +277,100,24155,2,0,P|225:99|196:109,1,70,8|2,0:0|0:0,0:0:0:0: +165,273,24521,5,6,0:0:0:0: +83,235,24704,2,0,L|93:81,1,140,0|0,0:0|0:0,0:0:0:0: +21,37,25253,2,0,L|1:120,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0: +110,17,25802,1,0,0:0:0:0: +172,83,25985,5,2,0:0:0:0: +236,19,26167,2,0,P|223:70|227:170,1,140,0|0,0:0|0:0,0:0:0:0: +293,216,26716,2,0,P|316:165|314:134,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0: +206,245,27265,1,0,0:0:0:0: +274,305,27448,5,2,0:0:0:0: +194,348,27631,2,0,L|363:332,1,140,0|0,0:0|0:0,0:0:0:0: +424,336,28180,1,2,0:0:0:0: +431,245,28363,2,0,P|381:252|354:276,2,70,0|8|0,0:0|0:0|0:0,0:0:0:0: +509,291,28911,6,0,L|496:128,1,140,2|8,0:0|0:0,0:0:0:0: +504,60,29460,1,0,0:0:0:0: +417,34,29643,2,0,L|402:183,1,140,2|8,0:0|0:0,0:0:0:0: +365,262,30192,1,0,0:0:0:0: +295,202,30375,5,2,0:0:0:0: +309,112,30558,2,0,P|282:172|196:176,1,140,0|0,0:0|0:0,0:0:0:0: +148,120,31106,2,0,P|189:99|225:99,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0: +129,209,31655,1,0,0:0:0:0: +63,146,31838,5,2,0:0:0:0: +16,67,32021,2,0,L|27:220,1,140,0|0,0:0|0:0,0:0:0:0: +23,297,32570,2,0,P|81:286|111:290,1,70,2|0,0:0|0:0,0:0:0:0: +173,327,32936,1,8,0:0:0:0: +338,251,33302,6,0,P|268:254|227:199,1,140,2|8,0:0|0:0,0:0:0:0: +203,114,33850,2,0,L|185:262,1,140,0|0,0:0|0:0,0:0:0:0: +244,323,34399,1,8,0:0:0:0: +334,335,34582,1,0,0:0:0:0: +419,219,34765,6,0,L|410:304,1,70,2|0,0:0|0:0,0:0:0:0: +338,251,35131,1,8,0:0:0:0: +301,111,35314,2,0,L|301:190,1,70,6|0,0:0|0:0,0:0:0:0: +383,141,35680,1,8,0:0:0:0: +462,97,35863,2,0,P|427:64|393:54,1,70,2|0,0:0|0:0,0:0:0:0: +321,23,36228,5,2,0:0:0:0: +237,60,36411,1,0,0:0:0:0: +148,38,36594,2,0,P|107:33|56:43,1,70,8|0,0:0|0:0,0:0:0:0: +86,125,36960,2,0,P|51:125|17:117,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0: +175,123,37509,1,0,0:0:0:0: +129,201,37692,5,2,0:0:0:0: +198,259,37875,1,0,0:0:0:0: +205,349,38058,2,0,P|251:330|284:326,1,70,8|0,0:0|0:0,0:0:0:0: +352,285,38424,2,0,P|361:318|357:353,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0: +282,239,38972,1,0,0:0:0:0: +362,195,39155,5,2,0:0:0:0: +436,142,39338,2,0,P|398:115|354:112,1,70,0|8,0:0|0:0,0:0:0:0: +286,92,39704,2,0,L|451:74,1,140,0|0,0:0|0:0,0:0:0:0: +512,118,40253,2,0,L|494:198,1,70,8|0,0:0|0:0,0:0:0:0: +430,297,40619,6,0,P|423:236|336:195,1,140,2|8,0:0|0:0,0:0:0:0: +282,239,41167,1,0,0:0:0:0: +209,184,41350,2,0,L|222:112,1,70,2|2,0:0|0:0,0:0:0:0: +177,34,41716,2,0,P|230:26|269:38,1,70,8|0,0:0|0:0,0:0:0:0: +307,95,42082,5,2,0:0:0:0: +363,23,42265,2,0,L|359:114,1,70,0|8,0:0|0:0,0:0:0:0: +360,184,42631,1,0,0:0:0:0: +450,191,42814,2,0,P|443:145|424:119,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0: +393,263,43363,1,0,0:0:0:0: +304,242,43546,5,2,0:0:0:0: +241,308,43728,1,0,0:0:0:0: +167,256,43911,2,0,P|205:228|245:226,1,70,8|0,0:0|0:0,0:0:0:0: +166,341,44277,2,0,P|118:325|90:289,1,70,2|0,0:0|0:0,0:0:0:0: +125,177,44643,2,0,P|168:152|201:153,1,70,8|0,0:0|0:0,0:0:0:0: +276,132,45009,6,0,L|119:105,1,140,2|8,0:0|0:0,0:0:0:0: +52,74,45558,2,0,L|210:57,1,140,2|0,0:0|0:0,0:0:0:0: +277,28,46106,1,8,0:0:0:0: +349,82,46289,1,0,0:0:0:0: +425,32,46472,6,0,L|451:110,2,70,6|2|8,0:0|0:0|0:0,0:0:0:0: +349,82,47021,2,0,L|344:235,1,140,2|8,0:0|0:0,0:0:0:0: +372,308,47570,1,2,0:0:0:0: +170,324,47936,5,2,0:0:0:0: +99,286,48119,2,0,L|112:112,1,168,2|2,0:0|0:0,0:0:0:0: +64,48,48850,2,0,P|125:36|195:111,1,168,2|2,0:0|0:0,0:0:0:0: +199,189,49582,6,0,L|369:166,1,168,2|2,0:0|0:0,0:0:0:0: +413,97,50314,2,0,P|390:180|377:274,1,168,2|2,0:0|0:0,0:0:0:0: +347,339,51046,6,0,P|424:333|463:251,1,168,2|2,0:0|0:0,0:0:0:0: +473,175,51777,2,0,L|477:105,1,56,2|2,0:0|0:0,0:0:0:0: +446,24,52143,6,0,P|363:22|308:82,1,140,12|2,0:0|0:0,0:0:0:0: +282,138,52692,1,8,0:0:0:0: +193,118,52875,2,0,L|213:281,1,140,2|8,0:0|0:0,0:0:0:0: +225,347,53424,2,0,P|268:328|286:301,1,70,2|0,0:0|0:0,0:0:0:0: +304,222,53789,5,2,0:0:0:0: +385,263,53972,1,0,0:0:0:0: +462,214,54155,2,0,P|421:185|383:179,1,70,8|0,0:0|0:0,0:0:0:0: +322,136,54521,2,0,P|360:105|400:93,1,70,2|0,0:0|0:0,0:0:0:0: +469,107,54887,2,0,L|483:24,1,70,8|0,0:0|0:0,0:0:0:0: +390,22,55253,6,0,L|223:30,1,140,2|8,0:0|0:0,0:0:0:0: +180,87,55802,1,0,0:0:0:0: +230,162,55985,2,0,L|391:154,1,140,2|8,0:0|0:0,0:0:0:0: +430,223,56533,1,0,0:0:0:0: +407,311,56716,6,0,P|356:347|285:307,1,140,2|8,0:0|0:0,0:0:0:0: +236,245,57265,1,0,0:0:0:0: +145,237,57448,2,0,L|162:316,1,70,2|0,0:0|0:0,0:0:0:0: +233,360,57814,6,0,P|185:349|142:350,1,70,8|0,0:0|0:0,0:0:0:0: +11,311,58180,2,0,P|64:302|104:306,1,70,2|0,0:0|0:0,0:0:0:0: +213,248,58546,2,0,P|162:237|130:237,1,70,8|0,0:0|0:0,0:0:0:0: +1,194,58911,2,0,P|47:183|74:185,1,70,2|0,0:0|0:0,0:0:0:0: +234,142,59277,2,0,P|175:129|152:128,1,70,8|0,0:0|0:0,0:0:0:0: +12,26,59643,6,0,P|66:38|71:140,1,140,2|8,0:0|0:0,0:0:0:0: +1,194,60192,1,0,0:0:0:0: +84,230,60375,1,2,0:0:0:0: +173,216,60558,1,8,0:0:0:0: +173,216,60649,1,8,0:0:0:0: +173,216,60741,1,8,0:0:0:0: +263,213,60924,1,2,0:0:0:0: +345,174,61106,6,0,P|320:144|286:130,1,70,2|0,0:0|0:0,0:0:0:0: +200,134,61472,1,8,0:0:0:0: +249,57,61655,2,0,L|263:12,2,35,12|8|8,0:0|0:0|0:0,0:0:0:0: +157,64,62021,2,0,L|153:13,2,35,12|8|8,0:0|0:0|0:0,0:0:0:0: +118,150,62387,1,2,0:0:0:0: +101,260,62570,6,0,P|207:236|257:243,1,140,2|8,0:0|0:0,0:0:0:0: +328,304,63119,1,0,0:0:0:0: +434,156,63302,2,0,P|373:157|329:217,1,140,2|8,0:0|0:0,0:0:0:0: +408,230,63850,1,2,0:0:0:0: +483,215,64033,5,6,0:0:0:0: +508,142,64216,1,0,0:0:0:0: +482,69,64399,1,8,0:0:0:0: +413,34,64582,2,0,P|336:30|256:49,1,140,0|2,0:0|0:0,0:0:0:0: +150,97,65131,2,0,P|190:97|243:107,1,70,8|2,0:0|0:0,0:0:0:0: +257,168,65497,6,0,L|225:323,1,140,2|8,0:0|0:0,0:0:0:0: +155,329,66046,1,0,0:0:0:0: +20,204,66228,2,0,P|92:202|133:271,1,140,8|8,0:0|0:0,0:0:0:0: +56,274,66777,1,2,0:0:0:0: +18,125,66960,6,0,L|93:119,1,70,6|0,0:0|0:0,0:0:0:0: +162,156,67326,1,8,0:0:0:0: +223,52,67509,2,0,L|227:219,1,140,0|2,0:0|0:0,0:0:0:0: +266,263,68058,2,0,P|300:229|308:199,1,70,8|2,0:0|0:0,0:0:0:0: +298,95,68424,6,0,L|458:75,1,140,6|8,0:0|0:0,0:0:0:0: +512,164,68972,2,0,L|358:154,1,140,0|2,0:0|0:0,0:0:0:0: +306,209,69521,1,8,0:0:0:0: +342,334,69704,6,0,P|361:289|369:244,1,70,2|6,0:0|0:0,0:0:0:0: +250,277,70070,2,0,P|223:228|219:186,1,70,0|8,0:0|0:0,0:0:0:0: +272,128,70436,1,0,0:0:0:0: +172,111,70619,2,0,L|343:97,1,140,8|8,0:0|0:0,0:0:0:0: +385,128,71167,1,2,0:0:0:0: +494,63,71350,6,0,L|413:54,1,70,6|0,0:0|0:0,0:0:0:0: +385,128,71716,2,0,L|475:140,1,70,8|0,0:0|0:0,0:0:0:0: +467,217,72082,2,0,L|386:208,1,70,8|2,0:0|0:0,0:0:0:0: +358,282,72448,2,0,L|448:294,1,70,8|2,0:0|0:0,0:0:0:0: +498,339,72814,5,12,0:0:0:0: +498,339,72997,1,12,0:0:0:0: +301,343,73363,1,8,0:0:0:0: +211,173,73728,2,0,L|221:216,2,35,2|2|8,0:0|0:0|0:0,0:0:0:0: +250,100,74094,1,2,0:0:0:0: +123,92,74277,6,0,P|129:156|129:236,1,140,2|8,0:0|0:0,0:0:0:0: +109,321,74826,1,0,0:0:0:0: +211,173,75009,2,0,P|266:165|333:237,1,140,8|8,0:0|0:0,0:0:0:0: +341,302,75558,1,2,0:0:0:0: +418,272,75741,5,6,0:0:0:0: +484,322,75924,1,0,0:0:0:0: +407,352,76106,1,8,0:0:0:0: +341,302,76289,2,0,L|364:147,1,140,0|2,0:0|0:0,0:0:0:0: +269,60,76838,2,0,P|315:69|349:94,1,70,8|0,0:0|0:0,0:0:0:0: +269,150,77204,6,0,P|228:160|114:139,1,140,2|8,0:0|0:0,0:0:0:0: +49,80,77753,1,0,0:0:0:0: +39,235,77936,2,0,P|103:222|160:277,1,140,8|8,0:0|0:0,0:0:0:0: +82,297,78485,1,2,0:0:0:0: +227,326,78667,6,0,L|233:241,1,70,4|0,0:0|0:0,0:0:0:0: +269,150,79033,1,8,0:0:0:0: +408,194,79216,2,0,P|359:172|271:187,1,140,0|2,0:0|0:0,0:0:0:0: +409,281,79765,2,0,P|447:272|478:250,1,70,8|2,0:0|0:0,0:0:0:0: +497,168,80131,6,0,L|481:332,1,140,6|8,0:0|0:0,0:0:0:0: +389,365,80680,2,0,L|376:198,1,140,0|2,0:0|0:0,0:0:0:0: +414,157,81228,1,8,0:0:0:0: +229,89,81411,6,0,P|304:91|338:167,1,140,2|0,0:0|0:0,0:0:0:0: +290,222,81960,1,8,0:0:0:0: +211,214,82143,1,8,0:0:0:0: +93,155,82326,2,0,P|137:143|172:150,1,70,2|2,0:0|0:0,0:0:0:0: +235,301,82692,2,0,P|177:296|141:279,1,70,8|2,0:0|0:0,0:0:0:0: +68,244,83058,6,0,L|72:328,1,70,6|0,0:0|0:0,0:0:0:0: +166,292,83424,2,0,L|157:372,1,70,8|0,0:0|0:0,0:0:0:0: +254,227,83789,2,0,L|258:310,1,70,8|2,0:0|0:0,0:0:0:0: +345,265,84155,2,0,L|336:349,1,70,8|0,0:0|0:0,0:0:0:0: +331,175,84521,5,2,0:0:0:0: +416,205,84704,1,2,0:0:0:0: +481,141,84887,1,8,0:0:0:0: +431,64,85070,2,0,L|444:26,2,35,8|8|2,0:0|0:0|0:0,0:0:0:0: +339,79,85436,2,0,L|341:39,2,35,8|8|8,0:0|0:0|0:0,0:0:0:0: +256,109,85802,1,2,0:0:0:0: +165,97,85985,6,0,P|167:150|164:187,1,70,2|0,0:0|0:0,0:0:0:0: +117,244,86350,2,0,P|163:241|204:235,1,70,8|0,0:0|0:0,0:0:0:0: +229,317,86716,2,0,P|273:305|300:294,1,70,8|2,0:0|0:0,0:0:0:0: +365,354,87082,2,0,P|404:334|430:310,1,70,8|0,0:0|0:0,0:0:0:0: +352,230,87448,6,0,L|271:216,2,70,6|0|8,0:0|0:0|0:0,0:0:0:0: +378,142,87997,2,0,L|222:144,1,140,0|2,0:0|0:0,0:0:0:0: +152,112,88546,2,0,L|166:214,1,70,8|2,0:0|0:0,0:0:0:0: +139,270,88911,5,8,0:0:0:0: +12,138,89277,2,0,L|29:55,1,70,8|0,0:0|0:0,0:0:0:0: +91,5,89643,2,0,L|104:97,1,70,8|2,0:0|0:0,0:0:0:0: +153,149,90009,2,0,L|175:78,1,70,8|0,0:0|0:0,0:0:0:0: +279,36,90375,6,0,L|357:27,2,70,6|0|8,0:0|0:0|0:0,0:0:0:0: +248,122,90924,2,0,L|398:125,1,140,0|2,0:0|0:0,0:0:0:0: +479,123,91472,2,0,P|468:170|445:195,1,70,8|2,0:0|0:0,0:0:0:0: +365,204,91838,6,0,P|414:220|409:320,1,140,6|8,0:0|0:0,0:0:0:0: +354,354,92387,1,0,0:0:0:0: +262,353,92570,2,0,L|271:273,1,70,8|2,0:0|0:0,0:0:0:0: +297,196,92936,2,0,P|243:198|216:215,1,70,8|0,0:0|0:0,0:0:0:0: +172,276,93302,5,6,0:0:0:0: +137,360,93485,2,0,L|127:265,1,70,0|8,0:0|0:0,0:0:0:0: +81,212,93850,2,0,P|93:138|118:67,1,140,0|2,0:0|0:0,0:0:0:0: +170,4,94399,2,0,P|195:37|204:74,1,70,8|2,0:0|0:0,0:0:0:0: +186,153,94765,6,0,L|340:139,1,140,6|8,0:0|0:0,0:0:0:0: +408,101,95314,1,2,0:0:0:0: +443,184,95497,1,6,0:0:0:0: +369,237,95680,2,0,L|300:224,2,70,8|8|2,0:0|0:0|0:0,0:0:0:0: +448,282,96228,5,12,0:0:0:0: +448,282,96411,1,12,0:0:0:0: +270,320,96777,1,8,0:0:0:0: +313,143,97143,1,8,0:0:0:0: +377,314,97509,1,8,0:0:0:0: +256,192,97692,12,0,100619,0:0:0:0: diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/20544-expected-conversion.json b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/20544-expected-conversion.json new file mode 100644 index 0000000000..ed4b550f01 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/20544-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"RandomW":273523780,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":7693.0,"Objects":[{"StartTime":7693.0,"EndTime":7693.0,"Column":0}]},{"RandomW":2659866685,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273523780,"StartTime":8043.0,"Objects":[{"StartTime":8043.0,"EndTime":8043.0,"Column":1}]},{"RandomW":3083309108,"RandomX":273326509,"RandomY":273523780,"RandomZ":2659866685,"StartTime":8393.0,"Objects":[{"StartTime":8393.0,"EndTime":8393.0,"Column":2}]},{"RandomW":2413296944,"RandomX":2659866685,"RandomY":3083309108,"RandomZ":4072999080,"StartTime":8626.0,"Objects":[{"StartTime":8626.0,"EndTime":8626.0,"Column":2},{"StartTime":8626.0,"EndTime":8626.0,"Column":0}]},{"RandomW":1129322311,"RandomX":3083309108,"RandomY":4072999080,"RandomZ":2413296944,"StartTime":8860.0,"Objects":[{"StartTime":8860.0,"EndTime":8860.0,"Column":3}]},{"RandomW":3365759273,"RandomX":4072999080,"RandomY":2413296944,"RandomZ":1129322311,"StartTime":9326.0,"Objects":[{"StartTime":9326.0,"EndTime":9326.0,"Column":3}]},{"RandomW":315078874,"RandomX":2413296944,"RandomY":1129322311,"RandomZ":3365759273,"StartTime":9560.0,"Objects":[{"StartTime":9560.0,"EndTime":9560.0,"Column":3}]},{"RandomW":583662031,"RandomX":1129322311,"RandomY":3365759273,"RandomZ":315078874,"StartTime":9793.0,"Objects":[{"StartTime":9793.0,"EndTime":9793.0,"Column":3}]},{"RandomW":3789568254,"RandomX":3365759273,"RandomY":315078874,"RandomZ":583662031,"StartTime":10260.0,"Objects":[{"StartTime":10260.0,"EndTime":10260.0,"Column":2}]},{"RandomW":3256340938,"RandomX":315078874,"RandomY":583662031,"RandomZ":3789568254,"StartTime":10493.0,"Objects":[{"StartTime":10493.0,"EndTime":10493.0,"Column":2}]},{"RandomW":2152938451,"RandomX":3789568254,"RandomY":3256340938,"RandomZ":3979976762,"StartTime":10727.0,"Objects":[{"StartTime":10727.0,"EndTime":10727.0,"Column":1},{"StartTime":10727.0,"EndTime":10727.0,"Column":0}]},{"RandomW":1620362479,"RandomX":3256340938,"RandomY":3979976762,"RandomZ":2152938451,"StartTime":11427.0,"Objects":[{"StartTime":11427.0,"EndTime":11427.0,"Column":1}]},{"RandomW":477221046,"RandomX":3979976762,"RandomY":2152938451,"RandomZ":1620362479,"StartTime":11777.0,"Objects":[{"StartTime":11777.0,"EndTime":11777.0,"Column":1}]},{"RandomW":1013554034,"RandomX":2152938451,"RandomY":1620362479,"RandomZ":477221046,"StartTime":12127.0,"Objects":[{"StartTime":12127.0,"EndTime":12127.0,"Column":2}]},{"RandomW":637383311,"RandomX":1620362479,"RandomY":477221046,"RandomZ":1013554034,"StartTime":12360.0,"Objects":[{"StartTime":12360.0,"EndTime":12360.0,"Column":2}]},{"RandomW":3817388387,"RandomX":477221046,"RandomY":1013554034,"RandomZ":637383311,"StartTime":12594.0,"Objects":[{"StartTime":12594.0,"EndTime":12594.0,"Column":3}]},{"RandomW":19695232,"RandomX":637383311,"RandomY":3817388387,"RandomZ":1911435716,"StartTime":13060.0,"Objects":[{"StartTime":13060.0,"EndTime":13060.0,"Column":3},{"StartTime":13060.0,"EndTime":13060.0,"Column":0}]},{"RandomW":3381470688,"RandomX":3817388387,"RandomY":1911435716,"RandomZ":19695232,"StartTime":13294.0,"Objects":[{"StartTime":13294.0,"EndTime":13294.0,"Column":3}]},{"RandomW":1862836779,"RandomX":19695232,"RandomY":3381470688,"RandomZ":1869143571,"StartTime":13527.0,"Objects":[{"StartTime":13527.0,"EndTime":13527.0,"Column":3},{"StartTime":13527.0,"EndTime":13527.0,"Column":5}]},{"RandomW":175452620,"RandomX":3381470688,"RandomY":1869143571,"RandomZ":1862836779,"StartTime":13994.0,"Objects":[{"StartTime":13994.0,"EndTime":13994.0,"Column":4}]},{"RandomW":2859972423,"RandomX":1869143571,"RandomY":1862836779,"RandomZ":175452620,"StartTime":14227.0,"Objects":[{"StartTime":14227.0,"EndTime":14227.0,"Column":4}]},{"RandomW":2210823260,"RandomX":1862836779,"RandomY":175452620,"RandomZ":2859972423,"StartTime":14461.0,"Objects":[{"StartTime":14461.0,"EndTime":14461.0,"Column":5}]},{"RandomW":2851442677,"RandomX":175452620,"RandomY":2859972423,"RandomZ":2210823260,"StartTime":14927.0,"Objects":[{"StartTime":14927.0,"EndTime":16561.0,"Column":1}]},{"RandomW":179122262,"RandomX":2859972423,"RandomY":2210823260,"RandomZ":2851442677,"StartTime":16794.0,"Objects":[{"StartTime":16794.0,"EndTime":18078.0,"Column":0}]},{"RandomW":2917386405,"RandomX":2851442677,"RandomY":179122262,"RandomZ":494367691,"StartTime":18661.0,"Objects":[{"StartTime":18661.0,"EndTime":19127.0,"Column":2}]},{"RandomW":3407923728,"RandomX":494367691,"RandomY":2917386405,"RandomZ":2825679051,"StartTime":19595.0,"Objects":[{"StartTime":19595.0,"EndTime":20061.0,"Column":3}]},{"RandomW":358318928,"RandomX":3407923728,"RandomY":1835995540,"RandomZ":3732560508,"StartTime":20528.0,"Objects":[{"StartTime":20528.0,"EndTime":20994.0,"Column":4},{"StartTime":20528.0,"EndTime":20994.0,"Column":1}]},{"RandomW":3440439960,"RandomX":3732560508,"RandomY":358318928,"RandomZ":3638999969,"StartTime":21462.0,"Objects":[{"StartTime":21462.0,"EndTime":21928.0,"Column":3}]},{"RandomW":3249928444,"RandomX":358318928,"RandomY":3638999969,"RandomZ":3440439960,"StartTime":22395.0,"Objects":[{"StartTime":22395.0,"EndTime":22395.0,"Column":1}]},{"RandomW":3857394572,"RandomX":3440439960,"RandomY":3249928444,"RandomZ":138257049,"StartTime":22628.0,"Objects":[{"StartTime":22628.0,"EndTime":24028.0,"Column":4}]},{"RandomW":2938470811,"RandomX":3249928444,"RandomY":138257049,"RandomZ":3857394572,"StartTime":24262.0,"Objects":[{"StartTime":24262.0,"EndTime":24262.0,"Column":3}]},{"RandomW":3241803419,"RandomX":138257049,"RandomY":3857394572,"RandomZ":2938470811,"StartTime":24495.0,"Objects":[{"StartTime":24495.0,"EndTime":24495.0,"Column":4}]},{"RandomW":620078415,"RandomX":3857394572,"RandomY":2938470811,"RandomZ":3241803419,"StartTime":25195.0,"Objects":[{"StartTime":25195.0,"EndTime":25195.0,"Column":4}]},{"RandomW":2566806806,"RandomX":2938470811,"RandomY":3241803419,"RandomZ":620078415,"StartTime":25429.0,"Objects":[{"StartTime":25429.0,"EndTime":25429.0,"Column":4}]},{"RandomW":458505931,"RandomX":3241803419,"RandomY":620078415,"RandomZ":2566806806,"StartTime":26129.0,"Objects":[{"StartTime":26129.0,"EndTime":26129.0,"Column":3}]},{"RandomW":2629948988,"RandomX":2566806806,"RandomY":458505931,"RandomZ":362272284,"StartTime":26362.0,"Objects":[{"StartTime":26362.0,"EndTime":27762.0,"Column":1}]},{"RandomW":1285940261,"RandomX":362272284,"RandomY":2629948988,"RandomZ":4139597407,"StartTime":27996.0,"Objects":[{"StartTime":27996.0,"EndTime":27996.0,"Column":1},{"StartTime":27996.0,"EndTime":27996.0,"Column":3}]},{"RandomW":3878288539,"RandomX":2629948988,"RandomY":4139597407,"RandomZ":1285940261,"StartTime":28229.0,"Objects":[{"StartTime":28229.0,"EndTime":28229.0,"Column":1}]},{"RandomW":1788551508,"RandomX":1285940261,"RandomY":3878288539,"RandomZ":1976280692,"StartTime":28929.0,"Objects":[{"StartTime":28929.0,"EndTime":28929.0,"Column":1},{"StartTime":28929.0,"EndTime":28929.0,"Column":4}]},{"RandomW":159147246,"RandomX":3878288539,"RandomY":1976280692,"RandomZ":1788551508,"StartTime":29163.0,"Objects":[{"StartTime":29163.0,"EndTime":29163.0,"Column":1}]},{"RandomW":2702806142,"RandomX":1976280692,"RandomY":1788551508,"RandomZ":159147246,"StartTime":29863.0,"Objects":[{"StartTime":29863.0,"EndTime":29863.0,"Column":3}]},{"RandomW":2311677487,"RandomX":1788551508,"RandomY":159147246,"RandomZ":2702806142,"StartTime":30213.0,"Objects":[{"StartTime":30213.0,"EndTime":30213.0,"Column":3}]},{"RandomW":3175953261,"RandomX":2311677487,"RandomY":988506051,"RandomZ":3495571300,"StartTime":30446.0,"Objects":[{"StartTime":30446.0,"EndTime":31146.0,"Column":2}]},{"RandomW":516122535,"RandomX":3495571300,"RandomY":3175953261,"RandomZ":2138555125,"StartTime":31730.0,"Objects":[{"StartTime":31730.0,"EndTime":31730.0,"Column":2},{"StartTime":31730.0,"EndTime":31730.0,"Column":1}]},{"RandomW":534989332,"RandomX":3175953261,"RandomY":2138555125,"RandomZ":516122535,"StartTime":32080.0,"Objects":[{"StartTime":32080.0,"EndTime":32080.0,"Column":2}]},{"RandomW":3420570846,"RandomX":2138555125,"RandomY":516122535,"RandomZ":534989332,"StartTime":32430.0,"Objects":[{"StartTime":32430.0,"EndTime":32430.0,"Column":2}]},{"RandomW":172021565,"RandomX":516122535,"RandomY":534989332,"RandomZ":3420570846,"StartTime":32663.0,"Objects":[{"StartTime":32663.0,"EndTime":32663.0,"Column":2}]},{"RandomW":168636292,"RandomX":3420570846,"RandomY":172021565,"RandomZ":263944077,"StartTime":32780.0,"Objects":[{"StartTime":32780.0,"EndTime":32780.0,"Column":0}]},{"RandomW":3473923375,"RandomX":172021565,"RandomY":263944077,"RandomZ":168636292,"StartTime":33597.0,"Objects":[{"StartTime":33597.0,"EndTime":33597.0,"Column":1}]},{"RandomW":3287941836,"RandomX":263944077,"RandomY":168636292,"RandomZ":3473923375,"StartTime":33947.0,"Objects":[{"StartTime":33947.0,"EndTime":33947.0,"Column":1}]},{"RandomW":1950056015,"RandomX":3473923375,"RandomY":3287941836,"RandomZ":388563489,"StartTime":34180.0,"Objects":[{"StartTime":34180.0,"EndTime":35230.0,"Column":5}]},{"RandomW":3600000321,"RandomX":388563489,"RandomY":1950056015,"RandomZ":3312202562,"StartTime":35464.0,"Objects":[{"StartTime":35464.0,"EndTime":36164.0,"Column":4}]},{"RandomW":647123919,"RandomX":3312202562,"RandomY":3600000321,"RandomZ":2314505656,"StartTime":36397.0,"Objects":[{"StartTime":36397.0,"EndTime":37097.0,"Column":1}]},{"RandomW":3375531720,"RandomX":2314505656,"RandomY":647123919,"RandomZ":2193654396,"StartTime":37564.0,"Objects":[{"StartTime":37564.0,"EndTime":37914.0,"Column":3}]},{"RandomW":2335314869,"RandomX":3834006299,"RandomY":1346269295,"RandomZ":3597388662,"StartTime":38264.0,"Objects":[{"StartTime":38264.0,"EndTime":38264.0,"Column":4},{"StartTime":38380.0,"EndTime":38380.0,"Column":3},{"StartTime":38496.0,"EndTime":38496.0,"Column":4}]},{"RandomW":1564102491,"RandomX":1346269295,"RandomY":3597388662,"RandomZ":2335314869,"StartTime":39197.0,"Objects":[{"StartTime":39197.0,"EndTime":39197.0,"Column":2}]},{"RandomW":1989977426,"RandomX":2335314869,"RandomY":1564102491,"RandomZ":4263834011,"StartTime":39431.0,"Objects":[{"StartTime":39431.0,"EndTime":39431.0,"Column":2},{"StartTime":39431.0,"EndTime":39431.0,"Column":5}]},{"RandomW":3806815718,"RandomX":4263834011,"RandomY":1989977426,"RandomZ":1831387023,"StartTime":39664.0,"Objects":[{"StartTime":39664.0,"EndTime":39664.0,"Column":1},{"StartTime":39664.0,"EndTime":39664.0,"Column":4}]},{"RandomW":999749640,"RandomX":1989977426,"RandomY":1831387023,"RandomZ":3806815718,"StartTime":39898.0,"Objects":[{"StartTime":39898.0,"EndTime":40831.0,"Column":1}]},{"RandomW":2830335005,"RandomX":1831387023,"RandomY":3806815718,"RandomZ":999749640,"StartTime":41298.0,"Objects":[{"StartTime":41298.0,"EndTime":41298.0,"Column":1}]},{"RandomW":2152692291,"RandomX":3806815718,"RandomY":999749640,"RandomZ":2830335005,"StartTime":41648.0,"Objects":[{"StartTime":41648.0,"EndTime":41648.0,"Column":1}]},{"RandomW":1499396089,"RandomX":999749640,"RandomY":2830335005,"RandomZ":2152692291,"StartTime":41998.0,"Objects":[{"StartTime":41998.0,"EndTime":41998.0,"Column":2}]},{"RandomW":3582202466,"RandomX":2830335005,"RandomY":2152692291,"RandomZ":1499396089,"StartTime":42231.0,"Objects":[{"StartTime":42231.0,"EndTime":42231.0,"Column":2}]},{"RandomW":3873754971,"RandomX":2152692291,"RandomY":1499396089,"RandomZ":3582202466,"StartTime":42931.0,"Objects":[{"StartTime":42931.0,"EndTime":42931.0,"Column":4}]},{"RandomW":495070374,"RandomX":1499396089,"RandomY":3582202466,"RandomZ":3873754971,"StartTime":43165.0,"Objects":[{"StartTime":43165.0,"EndTime":43165.0,"Column":4}]},{"RandomW":3016618448,"RandomX":3582202466,"RandomY":3873754971,"RandomZ":495070374,"StartTime":43398.0,"Objects":[{"StartTime":43398.0,"EndTime":43398.0,"Column":4}]},{"RandomW":1177547465,"RandomX":3873754971,"RandomY":495070374,"RandomZ":3016618448,"StartTime":43631.0,"Objects":[{"StartTime":43631.0,"EndTime":43631.0,"Column":3}]},{"RandomW":2255582016,"RandomX":495070374,"RandomY":3016618448,"RandomZ":1177547465,"StartTime":43865.0,"Objects":[{"StartTime":43865.0,"EndTime":43865.0,"Column":3}]},{"RandomW":2325387316,"RandomX":3016618448,"RandomY":1177547465,"RandomZ":2255582016,"StartTime":44098.0,"Objects":[{"StartTime":44098.0,"EndTime":44098.0,"Column":2}]},{"RandomW":1443216326,"RandomX":1177547465,"RandomY":2255582016,"RandomZ":2325387316,"StartTime":44332.0,"Objects":[{"StartTime":44332.0,"EndTime":44332.0,"Column":2}]},{"RandomW":1650665398,"RandomX":2325387316,"RandomY":1443216326,"RandomZ":1871032949,"StartTime":44565.0,"Objects":[{"StartTime":44565.0,"EndTime":44565.0,"Column":1},{"StartTime":44565.0,"EndTime":44565.0,"Column":4}]},{"RandomW":1204166455,"RandomX":1871032949,"RandomY":1650665398,"RandomZ":1013336310,"StartTime":44798.0,"Objects":[{"StartTime":44798.0,"EndTime":45498.0,"Column":3}]},{"RandomW":2125976115,"RandomX":1013336310,"RandomY":1204166455,"RandomZ":93461408,"StartTime":45732.0,"Objects":[{"StartTime":45732.0,"EndTime":46432.0,"Column":5}]},{"RandomW":1391245329,"RandomX":1889010923,"RandomY":131109480,"RandomZ":2450179625,"StartTime":46665.0,"Objects":[{"StartTime":46665.0,"EndTime":47365.0,"Column":0},{"StartTime":46665.0,"EndTime":47365.0,"Column":3}]},{"RandomW":1629740061,"RandomX":2450179625,"RandomY":1391245329,"RandomZ":3806548475,"StartTime":47599.0,"Objects":[{"StartTime":47599.0,"EndTime":47949.0,"Column":4}]},{"RandomW":2462543108,"RandomX":3806548475,"RandomY":1629740061,"RandomZ":2782684574,"StartTime":48532.0,"Objects":[{"StartTime":48532.0,"EndTime":49232.0,"Column":0}]},{"RandomW":1398343675,"RandomX":2462543108,"RandomY":1783863854,"RandomZ":368009293,"StartTime":49466.0,"Objects":[{"StartTime":49466.0,"EndTime":50166.0,"Column":1},{"StartTime":49466.0,"EndTime":50166.0,"Column":3}]},{"RandomW":1655209110,"RandomX":1398343675,"RandomY":4200591321,"RandomZ":204183638,"StartTime":50399.0,"Objects":[{"StartTime":50399.0,"EndTime":51099.0,"Column":0},{"StartTime":50399.0,"EndTime":51099.0,"Column":4}]},{"RandomW":2898792131,"RandomX":1655209110,"RandomY":4183149031,"RandomZ":4235317299,"StartTime":51333.0,"Objects":[{"StartTime":51333.0,"EndTime":52033.0,"Column":5},{"StartTime":51333.0,"EndTime":52033.0,"Column":2}]},{"RandomW":2376440576,"RandomX":4183149031,"RandomY":4235317299,"RandomZ":2898792131,"StartTime":52266.0,"Objects":[{"StartTime":52266.0,"EndTime":52266.0,"Column":0}]},{"RandomW":3672662434,"RandomX":4235317299,"RandomY":2898792131,"RandomZ":2376440576,"StartTime":52499.0,"Objects":[{"StartTime":52499.0,"EndTime":52499.0,"Column":1}]},{"RandomW":1144553308,"RandomX":2376440576,"RandomY":3672662434,"RandomZ":2825568900,"StartTime":52849.0,"Objects":[{"StartTime":52849.0,"EndTime":53199.0,"Column":3}]},{"RandomW":3856961856,"RandomX":3672662434,"RandomY":2825568900,"RandomZ":1144553308,"StartTime":54133.0,"Objects":[{"StartTime":54133.0,"EndTime":54133.0,"Column":3}]},{"RandomW":3856961856,"RandomX":3672662434,"RandomY":2825568900,"RandomZ":1144553308,"StartTime":54366.0,"Objects":[{"StartTime":54366.0,"EndTime":54366.0,"Column":2}]},{"RandomW":3856961856,"RandomX":3672662434,"RandomY":2825568900,"RandomZ":1144553308,"StartTime":54600.0,"Objects":[{"StartTime":54600.0,"EndTime":54600.0,"Column":3}]},{"RandomW":2182646490,"RandomX":1144553308,"RandomY":3856961856,"RandomZ":2090342703,"StartTime":55066.0,"Objects":[{"StartTime":55066.0,"EndTime":55066.0,"Column":2},{"StartTime":55066.0,"EndTime":55066.0,"Column":0}]},{"RandomW":2182646490,"RandomX":1144553308,"RandomY":3856961856,"RandomZ":2090342703,"StartTime":55300.0,"Objects":[{"StartTime":55300.0,"EndTime":55300.0,"Column":5},{"StartTime":55300.0,"EndTime":55300.0,"Column":3}]},{"RandomW":2182646490,"RandomX":1144553308,"RandomY":3856961856,"RandomZ":2090342703,"StartTime":55533.0,"Objects":[{"StartTime":55533.0,"EndTime":55533.0,"Column":2},{"StartTime":55533.0,"EndTime":55533.0,"Column":0}]},{"RandomW":3304208416,"RandomX":2090342703,"RandomY":2182646490,"RandomZ":90031962,"StartTime":56000.0,"Objects":[{"StartTime":56000.0,"EndTime":56233.0,"Column":3}]},{"RandomW":1041697651,"RandomX":90031962,"RandomY":3304208416,"RandomZ":2015301872,"StartTime":56583.0,"Objects":[{"StartTime":56583.0,"EndTime":56583.0,"Column":1},{"StartTime":56583.0,"EndTime":56583.0,"Column":2}]},{"RandomW":3818981880,"RandomX":15037736,"RandomY":2251270868,"RandomZ":2287819377,"StartTime":56700.0,"Objects":[{"StartTime":56700.0,"EndTime":56700.0,"Column":0},{"StartTime":56700.0,"EndTime":56700.0,"Column":4}]},{"RandomW":3368447121,"RandomX":2251270868,"RandomY":2287819377,"RandomZ":3818981880,"StartTime":56933.0,"Objects":[{"StartTime":56933.0,"EndTime":56933.0,"Column":1}]},{"RandomW":860096087,"RandomX":2287819377,"RandomY":3818981880,"RandomZ":3368447121,"StartTime":57867.0,"Objects":[{"StartTime":57867.0,"EndTime":57867.0,"Column":3}]},{"RandomW":860096087,"RandomX":2287819377,"RandomY":3818981880,"RandomZ":3368447121,"StartTime":58100.0,"Objects":[{"StartTime":58100.0,"EndTime":58100.0,"Column":2}]},{"RandomW":860096087,"RandomX":2287819377,"RandomY":3818981880,"RandomZ":3368447121,"StartTime":58334.0,"Objects":[{"StartTime":58334.0,"EndTime":58334.0,"Column":3}]},{"RandomW":1369988252,"RandomX":3818981880,"RandomY":3368447121,"RandomZ":860096087,"StartTime":58800.0,"Objects":[{"StartTime":58800.0,"EndTime":58800.0,"Column":4}]},{"RandomW":1369988252,"RandomX":3818981880,"RandomY":3368447121,"RandomZ":860096087,"StartTime":59034.0,"Objects":[{"StartTime":59034.0,"EndTime":59034.0,"Column":1}]},{"RandomW":1369988252,"RandomX":3818981880,"RandomY":3368447121,"RandomZ":860096087,"StartTime":59267.0,"Objects":[{"StartTime":59267.0,"EndTime":59267.0,"Column":4}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/20544.osu b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/20544.osu new file mode 100644 index 0000000000..237a13ecd2 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/20544.osu @@ -0,0 +1,126 @@ +osu file format v5 + +[General] +StackLeniency: 0.7 +Mode: 0 + +[Difficulty] +HPDrainRate:2 +CircleSize:5 +OverallDifficulty:2 +SliderMultiplier:1 +SliderTickRate:2 + +[Events] +//Background and Video events +//Break Periods +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Failing) +//Storyboard Layer 2 (Passing) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples +//Background Colour Transformations +3,100,163,162,255 + +[TimingPoints] +7460,466.735154027506,4,1,0,100 + +[HitObjects] +80,56,7693,1,0 +120,96,8043,1,0 +176,104,8393,1,0 +216,104,8626,1,0 +256,104,8860,1,0 +296,168,9326,5,0 +296,208,9560,1,0 +296,248,9793,1,0 +216,256,10260,1,0 +176,256,10493,1,0 +136,256,10727,1,0 +136,136,11427,5,0 +136,72,11777,1,0 +192,72,12127,1,0 +232,72,12360,1,0 +272,72,12594,1,0 +280,152,13060,5,0 +280,192,13294,1,0 +280,232,13527,1,0 +360,240,13994,1,0 +400,240,14227,1,0 +440,240,14461,1,0 +256,192,14927,12,0,16561 +256,192,16794,12,0,18078 +192,96,18661,6,0,B|312:96,1,100 +288,176,19595,2,0,B|168:176,1,100 +192,256,20528,2,0,B|312:256,1,100 +304,176,21462,2,0,B|240:176|248:88,1,100 +168,104,22395,5,0 +128,104,22628,2,0,B|296:368,1,300 +328,352,24262,5,0 +368,352,24495,1,0 +368,232,25195,1,0 +368,192,25429,1,0 +280,104,26129,5,0 +240,104,26362,2,0,B|40:352,1,300 +88,336,27996,5,0 +128,336,28229,1,0 +136,216,28929,1,0 +136,176,29163,1,0 +256,176,29863,5,0 +312,176,30213,1,0 +352,176,30446,2,0,B|360:264|360:280|360:272|272:272,1,150 +208,232,31730,5,0 +208,168,32080,1,0 +208,104,32430,1,0 +248,104,32663,1,0 +248,104,32780,1,0 +120,160,33597,5,0 +120,216,33947,1,0 +120,256,34180,2,0,B|352:256,1,225 +344,216,35464,6,0,B|200:128,1,150 +176,136,36397,2,0,B|176:288,1,150 +296,288,37564,6,0,B|296:208,1,75 +296,152,38264,2,0,B|296:104,2,25 +248,32,39197,1,0 +208,32,39431,1,0 +168,32,39664,1,0 +168,72,39898,2,0,B|168:136,4,50 +104,128,41298,5,0 +168,136,41648,1,0 +208,184,41998,1,0 +232,216,42231,1,0 +344,248,42931,5,0 +344,208,43165,1,0 +344,168,43398,1,0 +304,168,43631,1,0 +264,168,43865,1,0 +224,168,44098,1,0 +184,168,44332,1,0 +144,168,44565,1,0 +104,176,44798,6,0,B|32:240|160:272,1,150 +192,272,45732,2,0,B|280:272|320:200,1,150 +320,160,46665,2,0,B|248:96|176:136,1,150 +144,144,47599,2,0,B|48:168,1,75 +112,256,48532,6,0,B|256:336,1,150 +280,320,49466,2,0,B|416:240,1,150 +408,200,50399,2,0,B|256:136,1,150 +232,144,51333,2,0,B|80:208,1,150 +56,216,52266,5,0 +96,216,52499,1,0 +152,216,52849,2,0,B|248:216,1,75 +328,88,54133,5,0 +328,88,54366,1,0 +328,88,54600,1,0 +248,88,55066,5,0 +248,88,55300,1,0 +248,88,55533,1,0 +256,168,56000,6,0,B|184:168,1,50 +144,168,56583,1,0 +144,168,56700,1,0 +104,168,56933,1,0 +264,168,57867,5,0 +264,168,58100,1,0 +264,168,58334,1,0 +344,168,58800,5,0 +344,168,59034,1,0 +344,168,59267,1,0 diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/basic-expected-conversion.json b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/basic-expected-conversion.json new file mode 100644 index 0000000000..a25c8a12ab --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/basic-expected-conversion.json @@ -0,0 +1,168 @@ +{ + "Mappings": [ + { + "RandomW": 2659373485, + "RandomX": 3579807591, + "RandomY": 273326509, + "RandomZ": 272969173, + "StartTime": 500.0, + "Objects": [ + { + "StartTime": 500.0, + "EndTime": 2500.0, + "Column": 0 + }, + { + "StartTime": 1500.0, + "EndTime": 2500.0, + "Column": 1 + } + ] + }, + { + "RandomW": 3083803045, + "RandomX": 273326509, + "RandomY": 272969173, + "RandomZ": 2659373485, + "StartTime": 3000.0, + "Objects": [ + { + "StartTime": 3000.0, + "EndTime": 4000.0, + "Column": 2 + } + ] + }, + { + "RandomW": 4073554232, + "RandomX": 272969173, + "RandomY": 2659373485, + "RandomZ": 3083803045, + "StartTime": 4500.0, + "Objects": [ + { + "StartTime": 4500.0, + "EndTime": 5500.0, + "Column": 4 + } + ] + }, + { + "RandomW": 3420401969, + "RandomX": 2659373485, + "RandomY": 3083803045, + "RandomZ": 4073554232, + "StartTime": 6000.0, + "Objects": [ + { + "StartTime": 6000.0, + "EndTime": 6500.0, + "Column": 2 + } + ] + }, + { + "RandomW": 1129881182, + "RandomX": 3083803045, + "RandomY": 4073554232, + "RandomZ": 3420401969, + "StartTime": 7000.0, + "Objects": [ + { + "StartTime": 7000.0, + "EndTime": 8000.0, + "Column": 2 + } + ] + }, + { + "RandomW": 315568458, + "RandomX": 3420401969, + "RandomY": 1129881182, + "RandomZ": 2358617505, + "StartTime": 8500.0, + "Objects": [ + { + "StartTime": 8500.0, + "EndTime": 11000.0, + "Column": 0 + } + ] + }, + { + "RandomW": 548134043, + "RandomX": 1129881182, + "RandomY": 2358617505, + "RandomZ": 315568458, + "StartTime": 11500.0, + "Objects": [ + { + "StartTime": 11500.0, + "EndTime": 12000.0, + "Column": 1 + } + ] + }, + { + "RandomW": 3979422122, + "RandomX": 548134043, + "RandomY": 2810584254, + "RandomZ": 2250186050, + "StartTime": 12500.0, + "Objects": [ + { + "StartTime": 12500.0, + "EndTime": 16500.0, + "Column": 4 + } + ] + }, + { + "RandomW": 2466283411, + "RandomX": 2810584254, + "RandomY": 2250186050, + "RandomZ": 3979422122, + "StartTime": 17000.0, + "Objects": [ + { + "StartTime": 17000.0, + "EndTime": 18000.0, + "Column": 2 + } + ] + }, + { + "RandomW": 83157665, + "RandomX": 2250186050, + "RandomY": 3979422122, + "RandomZ": 2466283411, + "StartTime": 18500.0, + "Objects": [ + { + "StartTime": 18500.0, + "EndTime": 19450.0, + "Column": 0 + } + ] + }, + { + "RandomW": 2383087700, + "RandomX": 83157665, + "RandomY": 2055150192, + "RandomZ": 510071020, + "StartTime": 19875.0, + "Objects": [ + { + "StartTime": 19875.0, + "EndTime": 23875.0, + "Column": 1 + }, + { + "StartTime": 19875.0, + "EndTime": 23875.0, + "Column": 0 + } + ] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic.osu b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/basic.osu similarity index 96% rename from osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic.osu rename to osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/basic.osu index 40b4409760..abd2ff2ee6 100644 --- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic.osu +++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/basic.osu @@ -1,27 +1,27 @@ -osu file format v14 - -[Difficulty] -HPDrainRate:6 -CircleSize:4 -OverallDifficulty:7 -ApproachRate:8.3 -SliderMultiplier:1.6 -SliderTickRate:1 - -[TimingPoints] -500,500,4,2,1,50,1,0 -13426,-100,4,3,1,45,0,0 -14884,-100,4,2,1,50,0,0 - -[HitObjects] -96,192,500,6,0,L|416:192,2,320 -256,192,3000,12,0,4000,0:0:0:0: -256,192,4500,12,0,5500,0:0:0:0: -256,192,6000,12,0,6500,0:0:0:0: -256,128,7000,6,0,L|352:128,4,80 -32,192,8500,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800 -256,192,11500,12,0,12000,0:0:0:0: -512,320,12500,6,0,B|0:256|0:256|512:96|512:96|256:32,1,1280 -256,256,17000,6,0,L|160:256,4,80 -256,192,18500,12,0,19450,0:0:0:0: -216,231,19875,6,0,B|216:135|280:135|344:135|344:199|344:263|248:327|248:327|120:327|120:327|56:39|408:39|408:39|472:150|408:342,1,1280 +osu file format v14 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:7 +ApproachRate:8.3 +SliderMultiplier:1.6 +SliderTickRate:1 + +[TimingPoints] +500,500,4,2,1,50,1,0 +13426,-100,4,3,1,45,0,0 +14884,-100,4,2,1,50,0,0 + +[HitObjects] +96,192,500,6,0,L|416:192,2,320 +256,192,3000,12,0,4000,0:0:0:0: +256,192,4500,12,0,5500,0:0:0:0: +256,192,6000,12,0,6500,0:0:0:0: +256,128,7000,6,0,L|352:128,4,80 +32,192,8500,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800 +256,192,11500,12,0,12000,0:0:0:0: +512,320,12500,6,0,B|0:256|0:256|512:96|512:96|256:32,1,1280 +256,256,17000,6,0,L|160:256,4,80 +256,192,18500,12,0,19450,0:0:0:0: +216,231,19875,6,0,B|216:135|280:135|344:135|344:199|344:263|248:327|248:327|120:327|120:327|56:39|408:39|408:39|472:150|408:342,1,1280 diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json similarity index 100% rename from osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json rename to osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/convert-samples.osu similarity index 100% rename from osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu rename to osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/convert-samples.osu diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/diffcalc-test.osu b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/diffcalc-test.osu similarity index 100% rename from osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/diffcalc-test.osu rename to osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/diffcalc-test.osu diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json similarity index 100% rename from osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json rename to osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-samples.osu similarity index 100% rename from osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu rename to osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-samples.osu diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json similarity index 100% rename from osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json rename to osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples.osu b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/slider-convert-samples.osu similarity index 100% rename from osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples.osu rename to osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/slider-convert-samples.osu diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/zero-length-slider-expected-conversion.json b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/zero-length-slider-expected-conversion.json new file mode 100644 index 0000000000..400ce9cc1c --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/zero-length-slider-expected-conversion.json @@ -0,0 +1,18 @@ +{ + "Mappings": [ + { + "RandomW": 3083084786, + "RandomX": 273326509, + "RandomY": 273553282, + "RandomZ": 2659838971, + "StartTime": 4836.0, + "Objects": [ + { + "StartTime": 4836.0, + "EndTime": 4836.0, + "Column": 0 + } + ] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider.osu b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/zero-length-slider.osu similarity index 100% rename from osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider.osu rename to osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/zero-length-slider.osu 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/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs index ba0c97a121..abf01aa4a4 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs @@ -58,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/TestSceneBarLine.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs index ab9f57ecc3..a5c18babe2 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs @@ -1,10 +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 NUnit.Framework; -using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; @@ -16,37 +14,35 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [Test] public void TestMinor() { - AddStep("Create barlines", () => recreate()); + AddStep("Create barlines", recreate); } - private void recreate(Func>? createBarLines = null) + private void recreate() { var stageDefinitions = new List { new StageDefinition(4), }; - SetContents(_ => new ManiaPlayfield(stageDefinitions).With(s => + SetContents(_ => { - if (createBarLines != null) + var maniaPlayfield = new ManiaPlayfield(stageDefinitions); + + // Must be scheduled so the pool is loaded before we try and retrieve from it. + Schedule(() => { - var barLines = createBarLines(); - - foreach (var b in barLines) - s.Add(b); - - return; - } - - for (int i = 0; i < 64; i++) - { - s.Add(new BarLine + for (int i = 0; i < 64; i++) { - StartTime = Time.Current + i * 500, - Major = i % 4 == 0, - }); - } - })); + maniaPlayfield.Add(new BarLine + { + StartTime = Time.Current + i * 500, + Major = i % 4 == 0, + }); + } + }); + + return maniaPlayfield; + }); } } } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs index 483c468c1e..a0833ff91f 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs @@ -39,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/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 77db1b0bd8..5f299f419d 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,31 @@ 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); + assertComboAtJudgement(0, 1); + assertTailJudgement(HitResult.Meh); + assertComboAtJudgement(1, 0); + assertComboAtJudgement(3, 1); + } + /// /// -----[ ]----- /// xo x o @@ -208,7 +222,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Miss); } @@ -228,7 +241,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Meh); } @@ -246,7 +258,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Miss); } @@ -264,7 +275,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Meh); } @@ -358,7 +368,6 @@ namespace osu.Game.Rulesets.Mania.Tests }, beatmap); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); assertHitObjectJudgement(note, HitResult.Good); @@ -371,7 +380,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 +393,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 +409,6 @@ namespace osu.Game.Rulesets.Mania.Tests }, beatmap); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); assertHitObjectJudgement(note, HitResult.Great); @@ -425,7 +428,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Meh); } @@ -476,42 +478,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 +517,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/TestSceneMaximumScore.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs index 3d0abaceb5..ee6d999932 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.Tests AddAssert("all objects perfectly judged", () => judgementResults.Select(result => result.Type), () => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult))); - AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000)); + AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000)); } [Test] @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Mania.Tests AddAssert("all objects perfectly judged", () => judgementResults.Select(result => result.Type), () => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult))); - AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000)); + AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000)); } private void performTest(List hitObjects, List frames) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneScoring.cs new file mode 100644 index 0000000000..19d90e0551 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneScoring.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.Collections.Generic; +using System.Linq; +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.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; +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(IReadOnlyList selectedMods) => new ScoreV1(MaxCombo.Value, selectedMods); + protected override IScoringAlgorithm CreateScoreV2(int maxCombo, IReadOnlyList selectedMods) => new ScoreV2(maxCombo, selectedMods); + + protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode, IReadOnlyList selectedMods) + => new ManiaProcessorBasedScoringAlgorithm(beatmap, mode, selectedMods); + + [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, IReadOnlyList selectedMods) + { + var ruleset = new ManiaRuleset(); + + scoreMultiplier = 500000d / maxCombo * ruleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(selectedMods, new LegacyBeatmapConversionDifficultyInfo + { + SourceRuleset = ruleset.RulesetInfo + }); + } + + 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 readonly double modMultiplier; + + private const double combo_base = 4; + + public ScoreV2(int maxCombo, IReadOnlyList selectedMods) + { + this.maxCombo = maxCombo; + + var ruleset = new ManiaRuleset(); + modMultiplier = new ManiaRuleset().CreateLegacyScoreSimulator().GetLegacyScoreMultiplier( + selectedMods.Append(new ModScoreV2()).ToArray(), + new LegacyBeatmapConversionDifficultyInfo + { + SourceRuleset = ruleset.RulesetInfo + }); + + 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) + ) * modMultiplier); + } + } + } + + private class ManiaProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm + { + public ManiaProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode, IReadOnlyList selectedMods) + : base(beatmap, mode, selectedMods) + { + } + + 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..877b9c3ea4 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/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index bdc5a00583..def22608d6 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,40 +44,43 @@ 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); - return (int)Math.Max(1, roundedCircleSize); + double roundedCircleSize = Math.Round(difficulty.CircleSize); + + if (difficulty.SourceRuleset.ShortName == ManiaRuleset.SHORT_NAME) + return (int)Math.Max(1, roundedCircleSize); + + double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty); + + if (difficulty.TotalObjectCount > 0 && difficulty.EndTimeObjectCount >= 0) + { + int countSliderOrSpinner = difficulty.EndTimeObjectCount; + + // In osu!stable, this division appears as if it happens on floats, but due to release-mode + // optimisations, it actually ends up happening on doubles. + double percentSpecialObjects = (double)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 override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition); @@ -171,9 +175,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps switch (original) { - case IHasDistance: + case IHasPath: { - var generator = new DistanceObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap); + var generator = new PathObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap); conversion = generator; var positionData = original as IHasPosition; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs similarity index 95% rename from osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs rename to osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs index 91b7be6e8f..4922915c7d 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.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 @@ -21,13 +22,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// /// A pattern generator for IHasDistance hit objects. /// - internal class DistanceObjectPatternGenerator : PatternGenerator + internal class PathObjectPatternGenerator : PatternGenerator { - /// - /// Base osu! slider scoring distance. - /// - private const float osu_base_scoring_distance = 100; - public readonly int StartTime; public readonly int EndTime; public readonly int SegmentDuration; @@ -35,33 +31,34 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy private PatternType convertType; - public DistanceObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) + public PathObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) : base(random, hitObject, beatmap, previousPattern, originalBeatmap) { convertType = PatternType.None; if (!Beatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode) convertType = PatternType.LowProbability; - var distanceData = hitObject as IHasDistance; + var pathData = hitObject as IHasPath; var repeatsData = hitObject as IHasRepeats; - Debug.Assert(distanceData != null); + Debug.Assert(pathData != null); 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; SpanCount = repeatsData?.SpanCount() ?? 1; StartTime = (int)Math.Round(hitObject.StartTime); + double distance = pathData.Path.ExpectedDistance.Value ?? 0; + // This matches stable's calculation. - EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.Difficulty.SliderMultiplier); + EndTime = (int)Math.Floor(StartTime + distance * beatLength * SpanCount * 0.01 / beatmap.Difficulty.SliderMultiplier); SegmentDuration = (EndTime - StartTime) / SpanCount; } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index de9f0d91ae..6bb6879052 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -31,13 +31,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty public override int Version => 20220902; - private readonly IWorkingBeatmap workingBeatmap; - public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { - workingBeatmap = beatmap; - isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset); originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty; } @@ -60,15 +56,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty MaxCombo = beatmap.HitObjects.Sum(maxComboForObject), }; - if (ComputeLegacyScoringValues) - { - ManiaLegacyScoreSimulator sv1Simulator = new ManiaLegacyScoreSimulator(); - sv1Simulator.Simulate(workingBeatmap, beatmap, mods); - attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore; - attributes.LegacyComboScore = sv1Simulator.ComboScore; - attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio; - } - return attributes; } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs index e544428979..d9fd96ac6a 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs @@ -4,25 +4,67 @@ 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; +using osu.Game.Rulesets.Scoring.Legacy; namespace osu.Game.Rulesets.Mania.Difficulty { internal class ManiaLegacyScoreSimulator : ILegacyScoreSimulator { - public int AccuracyScore => 0; - public int ComboScore { get; private set; } - public double BonusScoreRatio => 0; - - public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap) { - double multiplier = mods.Where(m => m is not (ModHidden or ModHardRock or ModDoubleTime or ModFlashlight or ManiaModFadeIn)) - .Select(m => m.ScoreMultiplier) - .Aggregate(1.0, (c, n) => c * n); + return new LegacyScoreAttributes + { + ComboScore = 1000000, + MaxCombo = 0 // Max combo is mod-dependent, so any value here is insufficient. + }; + } - ComboScore = (int)(1000000 * multiplier); + 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/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index 06c825e37d..a24fcaad8d 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -14,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; @@ -50,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])); @@ -70,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/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index cb275a9aa4..991b7f476c 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; @@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints [Resolved] private IScrollingInfo scrollingInfo { get; set; } = null!; - protected override bool IsValidForPlacement => HitObject.Duration > 0; + protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0); public HoldNotePlacementBlueprint() : base(new HoldNote()) @@ -44,8 +45,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 4c356367d3..1ae65dd8c0 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs @@ -37,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/DrawableManiaEditorRuleset.cs b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs index f480fa516b..8d34373f82 100644 --- a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs +++ b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs @@ -2,18 +2,22 @@ // See the LICENCE file in the repository root for full licence text. 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) @@ -21,6 +25,13 @@ namespace osu.Game.Rulesets.Mania.Edit { } + 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/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 9cc84450cc..61c730912f 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -1,204 +1,23 @@ // 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.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; } = null!; - - [Resolved] - private OsuColour colours { get; set; } = null!; - - [Resolved] - private BindableBeatDivisor beatDivisor { get; set; } = null!; - - 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; } = null!; - - 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/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 5e577a2964..967cdb0e54 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -5,15 +5,13 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Input; +using System.Text.RegularExpressions; 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 +19,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,49 +35,53 @@ 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}")); + + // 123|0,456|1,789|2 ... + private static readonly Regex selection_regex = new Regex(@"^\d+\|\d+(,\d+\|\d+)*$", RegexOptions.Compiled); + + public override void SelectFromTimestamp(double timestamp, string objectDescription) + { + if (!selection_regex.IsMatch(objectDescription)) + return; + + List remainingHitObjects = EditorBeatmap.HitObjects.Cast().Where(h => h.StartTime >= timestamp).ToList(); + string[] objectDescriptions = objectDescription.Split(',').ToArray(); + + for (int i = 0; i < objectDescriptions.Length; i++) + { + string[] split = objectDescriptions[i].Split('|').ToArray(); + if (split.Length != 2) + continue; + + if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column)) + continue; + + ManiaHitObject current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column); + + if (current == null) + continue; + + EditorBeatmap.SelectedHitObjects.Add(current); + + if (i < objectDescriptions.Length - 1) + remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList(); + } + } } } diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteBodyJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteBodyJudgement.cs new file mode 100644 index 0000000000..6719665cbe --- /dev/null +++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteBodyJudgement.cs @@ -0,0 +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.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mania.Judgements +{ + public class HoldNoteBodyJudgement : ManiaJudgement + { + public override HitResult MaxResult => HitResult.IgnoreHit; + public override HitResult MinResult => HitResult.ComboBreak; + } +} diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs deleted file mode 100644 index ae9e8bd287..0000000000 --- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs +++ /dev/null @@ -1,12 +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.Game.Rulesets.Scoring; - -namespace osu.Game.Rulesets.Mania.Judgements -{ - public class HoldNoteTickJudgement : ManiaJudgement - { - public override HitResult MaxResult => HitResult.LargeTickHit; - } -} diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs index d28b7bdf58..2b75bcdc3d 100644 --- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs @@ -12,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..930ca217cd 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -4,6 +4,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Filter; using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; @@ -15,7 +16,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 || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo))); } public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 4507015169..c38d6519bd 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; @@ -254,16 +255,6 @@ namespace osu.Game.Rulesets.Mania case ModType.Conversion: return new Mod[] { - new MultiMod(new ManiaModKey4(), - new ManiaModKey5(), - new ManiaModKey6(), - new ManiaModKey7(), - new ManiaModKey8(), - new ManiaModKey9(), - new ManiaModKey10(), - new ManiaModKey1(), - new ManiaModKey2(), - new ManiaModKey3()), new ManiaModRandom(), new ManiaModDualStages(), new ManiaModMirror(), @@ -271,7 +262,19 @@ namespace osu.Game.Rulesets.Mania new ManiaModClassic(), new ManiaModInvert(), new ManiaModConstantSpeed(), - new ManiaModHoldOff() + new ManiaModHoldOff(), + new MultiMod( + new ManiaModKey1(), + new ManiaModKey2(), + new ManiaModKey3(), + new ManiaModKey4(), + new ManiaModKey5(), + new ManiaModKey6(), + new ManiaModKey7(), + new ManiaModKey8(), + new ManiaModKey9(), + new ManiaModKey10() + ), }; case ModType.Automation: @@ -385,21 +388,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) @@ -427,6 +420,9 @@ namespace osu.Game.Rulesets.Mania public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection(); public override DifficultySection CreateEditorDifficultySection() => new ManiaDifficultySection(); + + public int GetKeyCount(IBeatmapInfo beatmapInfo) + => ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo)); } 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..bea1a14110 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs @@ -1,11 +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.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(); + + // For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always + // make the map harder and is more of a personal preference. + // In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency. + public override double ScoreMultiplier => 1; } } 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..7e5e80db6c 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs @@ -2,11 +2,19 @@ // 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(); + + // For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always + // make the map any harder and is more of a personal preference. + // In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency. + public override double ScoreMultiplier => 1; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs index 2e22e23dbd..b02a18c9f4 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs @@ -1,11 +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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModPerfect : ModPerfect { + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) + { + if (!isRelevantResult(result.Judgement.MinResult) && !isRelevantResult(result.Judgement.MaxResult) && !isRelevantResult(result.Type)) + return false; + + // Mania allows imperfect "Great" hits without failing. + if (result.Judgement.MaxResult == HitResult.Perfect) + return result.Type < HitResult.Great; + + return result.Type != result.Judgement.MaxResult; + } + + private bool isRelevantResult(HitResult result) => result.AffectsAccuracy() || result.AffectsCombo(); } } 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 c3fec92b92..3490d50871 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -35,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; @@ -60,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; @@ -103,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, @@ -110,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(), }); } @@ -136,7 +135,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables sizingContainer.Size = Vector2.One; HoldStartTime = null; - HoldBrokenTime = null; releaseTime = null; } @@ -154,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; } } @@ -165,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) @@ -178,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); @@ -266,20 +264,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { 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() @@ -333,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 ((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..680009bc4c 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -90,6 +90,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (!HitObject.HitWindows.CanBeHit(timeOffset)) ApplyResult(r => r.Type = r.Judgement.MinResult); + return; } @@ -97,9 +98,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (result == HitResult.None) return; + result = GetCappedResult(result); + ApplyResult(r => r.Type = result); } + /// + /// 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) 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/HoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs deleted file mode 100644 index e5c5260a49..0000000000 --- a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.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.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 override Judgement CreateJudgement() => new HoldNoteTickJudgement(); - - protected override HitWindows CreateHitWindows() => HitWindows.Empty; - } -} diff --git a/osu.Game.Rulesets.Mania/Objects/TailNote.cs b/osu.Game.Rulesets.Mania/Objects/TailNote.cs index 71a594c6ce..def32880f1 100644 --- a/osu.Game.Rulesets.Mania/Objects/TailNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/TailNote.cs @@ -6,6 +6,9 @@ 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/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs index 7c8afdff12..dd3208bd89 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs @@ -87,15 +87,22 @@ namespace osu.Game.Rulesets.Mania.Replays private double calculateReleaseTime(HitObject currentObject, HitObject? nextObject) { double endTime = currentObject.GetEndTime(); + double releaseDelay = RELEASE_DELAY; - if (currentObject is HoldNote) - // hold note releases must be timed exactly. - return endTime; + if (currentObject is HoldNote hold) + { + if (hold.Duration > 0) + // hold note releases must be timed exactly. + return endTime; + + // Special case for super short hold notes + releaseDelay = 1; + } bool canDelayKeyUpFully = nextObject == null || - nextObject.StartTime > endTime + RELEASE_DELAY; + nextObject.StartTime > endTime + releaseDelay; - return endTime + (canDelayKeyUpFully ? RELEASE_DELAY : (nextObject.AsNonNull().StartTime - endTime) * 0.9); + return endTime + (canDelayKeyUpFully ? releaseDelay : (nextObject.AsNonNull().StartTime - endTime) * 0.9); } protected override HitObject? GetNextObject(int currentIndex) diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic-expected-conversion.json deleted file mode 100644 index 753db99856..0000000000 --- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic-expected-conversion.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "Mappings": [{ - "RandomW": 2659373485, - "RandomX": 3579807591, - "RandomY": 273326509, - "RandomZ": 272969173, - "StartTime": 500.0, - "Objects": [{ - "StartTime": 500.0, - "EndTime": 2500.0, - "Column": 0 - }, { - "StartTime": 1500.0, - "EndTime": 2500.0, - "Column": 1 - }] - }, { - "RandomW": 3083803045, - "RandomX": 273326509, - "RandomY": 272969173, - "RandomZ": 2659373485, - "StartTime": 3000.0, - "Objects": [{ - "StartTime": 3000.0, - "EndTime": 4000.0, - "Column": 2 - }] - }, { - "RandomW": 4073554232, - "RandomX": 272969173, - "RandomY": 2659373485, - "RandomZ": 3083803045, - "StartTime": 4500.0, - "Objects": [{ - "StartTime": 4500.0, - "EndTime": 5500.0, - "Column": 4 - }] - }, { - "RandomW": 3420401969, - "RandomX": 2659373485, - "RandomY": 3083803045, - "RandomZ": 4073554232, - "StartTime": 6000.0, - "Objects": [{ - "StartTime": 6000.0, - "EndTime": 6500.0, - "Column": 2 - }] - }, { - "RandomW": 1129881182, - "RandomX": 3083803045, - "RandomY": 4073554232, - "RandomZ": 3420401969, - "StartTime": 7000.0, - "Objects": [{ - "StartTime": 7000.0, - "EndTime": 8000.0, - "Column": 2 - }] - }, { - "RandomW": 315568458, - "RandomX": 3420401969, - "RandomY": 1129881182, - "RandomZ": 2358617505, - "StartTime": 8500.0, - "Objects": [{ - "StartTime": 8500.0, - "EndTime": 11000.0, - "Column": 0 - }] - }, { - "RandomW": 548134043, - "RandomX": 1129881182, - "RandomY": 2358617505, - "RandomZ": 315568458, - "StartTime": 11500.0, - "Objects": [{ - "StartTime": 11500.0, - "EndTime": 12000.0, - "Column": 1 - }] - }, { - "RandomW": 3979422122, - "RandomX": 548134043, - "RandomY": 2810584254, - "RandomZ": 2250186050, - "StartTime": 12500.0, - "Objects": [{ - "StartTime": 12500.0, - "EndTime": 16500.0, - "Column": 4 - }] - }, { - "RandomW": 2466283411, - "RandomX": 2810584254, - "RandomY": 2250186050, - "RandomZ": 3979422122, - "StartTime": 17000.0, - "Objects": [{ - "StartTime": 17000.0, - "EndTime": 18000.0, - "Column": 2 - }] - }, { - "RandomW": 83157665, - "RandomX": 2250186050, - "RandomY": 3979422122, - "RandomZ": 2466283411, - "StartTime": 18500.0, - "Objects": [{ - "StartTime": 18500.0, - "EndTime": 19450.0, - "Column": 0 - }] - }, { - "RandomW": 2383087700, - "RandomX": 83157665, - "RandomY": 2055150192, - "RandomZ": 510071020, - "StartTime": 19875.0, - "Objects": [{ - "StartTime": 19875.0, - "EndTime": 23875.0, - "Column": 1 - }, { - "StartTime": 19875.0, - "EndTime": 23875.0, - "Column": 0 - }] - }] -} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider-expected-conversion.json deleted file mode 100644 index 229760cd1c..0000000000 --- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider-expected-conversion.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "Mappings": [{ - "RandomW": 3083084786, - "RandomX": 273326509, - "RandomY": 273553282, - "RandomZ": 2659838971, - "StartTime": 4836, - "Objects": [{ - "StartTime": 4836, - "EndTime": 4836, - "Column": 0 - }] - }] -} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs index e63a037ca9..a33eac83c2 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs @@ -1,23 +1,70 @@ // 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 System.Collections.Generic; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Scoring { - public partial class ManiaHealthProcessor : DrainingHealthProcessor + public partial class ManiaHealthProcessor : LegacyDrainingHealthProcessor { - /// public ManiaHealthProcessor(double drainStartTime) - : base(drainStartTime, 1.0) + : base(drainStartTime) { } - protected override HitResult GetSimulatedHitResult(Judgement judgement) + protected override double ComputeDrainRate() { - // Users are not expected to attain perfect judgements for all notes due to the tighter hit window. - return judgement.MaxResult == HitResult.Perfect ? HitResult.Great : judgement.MaxResult; + // Base call is run only to compute HP recovery (namely, `HpMultiplierNormal`). + // This closely mirrors (broken) behaviour of stable and as such is preserved unchanged. + base.ComputeDrainRate(); + + return 0; + } + + protected override IEnumerable EnumerateTopLevelHitObjects() => Beatmap.HitObjects; + + protected override IEnumerable EnumerateNestedHitObjects(HitObject hitObject) => hitObject.NestedHitObjects; + + protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result) + { + double increase = 0; + + switch (result) + { + case HitResult.Miss: + switch (hitObject) + { + case HeadNote: + case TailNote: + return -(Beatmap.Difficulty.DrainRate + 1) * 0.00375; + + default: + return -(Beatmap.Difficulty.DrainRate + 1) * 0.0075; + } + + case HitResult.Meh: + return -(Beatmap.Difficulty.DrainRate + 1) * 0.0016; + + case HitResult.Ok: + return 0; + + case HitResult.Good: + increase = 0.004 - Beatmap.Difficulty.DrainRate * 0.0004; + break; + + case HitResult.Great: + increase = 0.005 - Beatmap.Difficulty.DrainRate * 0.0005; + break; + + case HitResult.Perfect: + increase = 0.0055 - Beatmap.Difficulty.DrainRate * 0.0005; + break; + } + + return HpMultiplierNormal * increase; } } } diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs index 289f8a00ef..627f48f391 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs @@ -1,12 +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.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) @@ -22,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 c53f3c3e07..1947d86a97 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -26,13 +26,37 @@ namespace osu.Game.Rulesets.Mania.Scoring protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) { - return 10000 * comboProgress - + 990000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress + return 150000 * comboProgress + + 850000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress + bonusPortion; } 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)); + { + return getBaseComboScoreForResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base)); + } + + public override int GetBaseScoreForResult(HitResult result) + { + switch (result) + { + case HitResult.Perfect: + return 305; + } + + return base.GetBaseScoreForResult(result); + } + + private int getBaseComboScoreForResult(HitResult result) + { + switch (result) + { + case HitResult.Perfect: + return 300; + } + + return GetBaseScoreForResult(result); + } private class JudgementOrderComparer : IComparer { diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs index 4ce3c50f7c..a191dee1ca 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs @@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon { public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement { + private const float judgement_y_position = 160; + private RingExplosion? ringExplosion; [Resolved] @@ -30,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon AutoSizeAxes = Axes.Both; Origin = Anchor.Centre; - Y = 160; + Y = judgement_y_position; } [BackgroundDependencyLoader] @@ -76,7 +78,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon this.ScaleTo(1.6f); this.ScaleTo(1, 100, Easing.In); - this.MoveTo(Vector2.Zero); + this.MoveToY(judgement_y_position); this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); this.RotateTo(0); diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index ddd6365c25..7f6540e7b5 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -99,9 +99,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon return SkinUtils.As(new Bindable(30)); case LegacyManiaSkinConfigurationLookups.ColumnWidth: - return SkinUtils.As(new Bindable( - stage.IsSpecialColumn(columnIndex) ? 120 : 60 - )); + bool isSpecialColumn = stage.IsSpecialColumn(columnIndex); + + float width = 60 * (isSpecialColumn ? 2 : 1); + + return SkinUtils.As(new Bindable(width)); case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour: 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/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index ef4810c40d..07045b76ca 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); @@ -223,7 +234,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy break; default: - // this is where things get fucked up. + // this is where things get a bit messed up. // honestly there's three modes to handle here but they seem really pointless? // let's wait to see if anyone actually uses them in skins. if (bodySprite != null) 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/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs index 7e3fb0438c..3a69142b3c 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs @@ -34,7 +34,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy FallbackColumnIndex = "S"; else { - int distanceToEdge = Math.Min(Column.Index, (stage.Columns - 1) - Column.Index); + // Account for cases like dual-stage (assume that all stages have the same column count for now). + int columnInStage = Column.Index % stage.Columns; + int distanceToEdge = Math.Min(columnInStage, (stage.Columns - 1) - columnInStage); FallbackColumnIndex = distanceToEdge % 2 == 0 ? "1" : "2"; } } 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..6cd55bb099 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Mania.UI 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/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index 0bc0bf4caf..8734f8ac8a 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -3,14 +3,17 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Mania.UI { @@ -60,6 +63,12 @@ namespace osu.Game.Rulesets.Mania.UI onSkinChanged(); } + protected override void LoadComplete() + { + base.LoadComplete(); + updateMobileSizing(); + } + private void onSkinChanged() { for (int i = 0; i < stageDefinition.Columns; i++) @@ -77,12 +86,15 @@ namespace osu.Game.Rulesets.Mania.UI new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i)) ?.Value; - if (width == null) - // only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration) - columns[i].Width = stageDefinition.IsSpecialColumn(i) ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH; - else - columns[i].Width = width.Value; + bool isSpecialColumn = stageDefinition.IsSpecialColumn(i); + + // only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration) + width ??= isSpecialColumn ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH; + + columns[i].Width = width.Value; } + + updateMobileSizing(); } /// @@ -92,10 +104,29 @@ namespace osu.Game.Rulesets.Mania.UI /// The content. public void SetContentForColumn(int column, TContent content) => columns[column].Child = content; - public new MarginPadding Padding + private void updateMobileSizing() { - get => base.Padding; - set => base.Padding = value; + if (!IsLoaded || !RuntimeInfo.IsMobile) + return; + + // GridContainer+CellContainer containing this stage (gets split up for dual stages). + Vector2? containingCell = this.FindClosestParent()?.Parent?.DrawSize; + + // Will be null in tests. + if (containingCell == null) + return; + + float aspectRatio = containingCell.Value.X / containingCell.Value.Y; + + // 2.83 is a mostly arbitrary scale-up (170 / 60, based on original implementation for argon) + float mobileAdjust = 2.83f * Math.Min(1, 7f / stageDefinition.Columns); + // 1.92 is a "reference" mobile screen aspect ratio for phones. + // We should scale it back for cases like tablets which aren't so extreme. + mobileAdjust *= aspectRatio / 1.92f; + + // Best effort until we have better mobile support. + for (int i = 0; i < stageDefinition.Columns; i++) + columns[i].Width *= mobileAdjust; } protected override void Dispose(bool isDisposing) 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..bea536e4af 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,19 +13,20 @@ 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; using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Scoring; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI { @@ -52,22 +52,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; @@ -75,6 +59,9 @@ namespace osu.Game.Rulesets.Mania.UI // Stores the current speed adjustment active in gameplay. private readonly Track speedAdjustmentTrack = new TrackVirtual(0); + [Resolved] + private ISkinSource skin { get; set; } + public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) : base(ruleset, beatmap, mods) { @@ -122,7 +109,20 @@ namespace osu.Game.Rulesets.Mania.UI updateTimeRange(); } - private void updateTimeRange() => TimeRange.Value = smoothTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value; + private void updateTimeRange() + { + float hitPosition = skin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value + ?? Stage.HIT_TARGET_POSITION; + + const float length_to_default_hit_position = 768 - LegacyManiaSkinConfiguration.DEFAULT_HIT_POSITION; + float lengthToHitPosition = 768 - hitPosition; + + // This scaling factor preserves the scroll speed as the scroll length varies from changes to the hit position. + float scale = lengthToHitPosition / length_to_default_hit_position; + + TimeRange.Value = smoothTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale; + } /// /// Computes a scroll time (in milliseconds) from a scroll speed in the range of 1-40. 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/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index 879c704450..36286940a8 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -1,22 +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.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; @@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.UI private readonly ColumnFlow columnFlow; private readonly JudgementContainer judgements; - private readonly DrawablePool judgementPool; + private readonly JudgementPooler judgementPooler; private readonly Drawable barLineContainer; @@ -48,6 +49,8 @@ namespace osu.Game.Rulesets.Mania.UI private readonly int firstColumnIndex; + private ISkinSource currentSkin = null!; + public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction) { this.firstColumnIndex = firstColumnIndex; @@ -65,7 +68,6 @@ namespace osu.Game.Rulesets.Mania.UI InternalChildren = new Drawable[] { - judgementPool = new DrawablePool(2), new Container { Anchor = Anchor.TopCentre, @@ -104,7 +106,7 @@ namespace osu.Game.Rulesets.Mania.UI { RelativeSizeAxes = Axes.Y, }, - new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground), _ => null) + new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground)) { RelativeSizeAxes = Axes.Both }, @@ -136,9 +138,13 @@ namespace osu.Game.Rulesets.Mania.UI columnFlow.SetContentForColumn(i, column); AddNested(column); } - } - private ISkinSource currentSkin; + var hitWindows = new ManiaHitWindows(); + + AddInternal(judgementPooler = new JudgementPooler(Enum.GetValues().Where(r => hitWindows.IsHitResultAllowed(r)))); + + RegisterPool(50, 200); + } [BackgroundDependencyLoader] private void load(ISkinSource skin) @@ -168,7 +174,7 @@ namespace osu.Game.Rulesets.Mania.UI base.Dispose(isDisposing); - if (currentSkin != null) + if (currentSkin.IsNotNull()) currentSkin.SourceChanged -= onSkinChanged; } @@ -186,25 +192,21 @@ 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 => + judgements.Add(judgementPooler.Get(result.Type, j => { j.Apply(result, judgedObject); j.Anchor = Anchor.Centre; j.Origin = Anchor.Centre; - })); + })!); } protected override void Update() 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/Editor/Checks/CheckOffscreenObjectsTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOffscreenObjectsTest.cs index a72aaa966c..5db6dc6cdd 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOffscreenObjectsTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOffscreenObjectsTest.cs @@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks Position = new Vector2(420, 240), Path = new SliderPath(new[] { - new PathControlPoint(new Vector2(0, 0), PathType.Linear), + new PathControlPoint(new Vector2(0, 0), PathType.LINEAR), new PathControlPoint(new Vector2(-100, 0)) }), } @@ -128,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks Position = playfield_centre, Path = new SliderPath(new[] { - new PathControlPoint(new Vector2(0, 0), PathType.Linear), + new PathControlPoint(new Vector2(0, 0), PathType.LINEAR), new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5)) }), } @@ -149,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks Position = playfield_centre, Path = new SliderPath(new[] { - new PathControlPoint(new Vector2(0, 0), PathType.Linear), + new PathControlPoint(new Vector2(0, 0), PathType.LINEAR), new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5)) }), StackHeight = 5 @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks Position = new Vector2(0, 0), Path = new SliderPath(new[] { - new PathControlPoint(new Vector2(0, 0), PathType.Linear), + new PathControlPoint(new Vector2(0, 0), PathType.LINEAR), new PathControlPoint(playfield_centre) }), } @@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks Position = playfield_centre, Path = new SliderPath(new[] { - new PathControlPoint(new Vector2(0, 0), PathType.Linear), + new PathControlPoint(new Vector2(0, 0), PathType.LINEAR), new PathControlPoint(-playfield_centre) }), } @@ -214,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks Path = new SliderPath(new[] { // Circular arc shoots over the top of the screen. - new PathControlPoint(new Vector2(0, 0), PathType.PerfectCurve), + new PathControlPoint(new Vector2(0, 0), PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(-100, -200)), new PathControlPoint(new Vector2(100, -200)) }), diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index b05c755bfd..3d35ab79f7 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -34,12 +34,12 @@ 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(); AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor( - (pos: circle1.Position, pathType: PathType.Linear), + (pos: circle1.Position, pathType: PathType.LINEAR), (pos: circle2.Position, pathType: null))); AddStep("undo", () => Editor.Undo()); @@ -73,11 +73,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor var controlPoints = slider.Path.ControlPoints; (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints.Count + 2]; - args[0] = (circle1.Position, PathType.Linear); + args[0] = (circle1.Position, PathType.LINEAR); for (int i = 0; i < controlPoints.Count; i++) { - args[i + 1] = (controlPoints[i].Position + slider.Position, i == controlPoints.Count - 1 ? PathType.Linear : controlPoints[i].Type); + args[i + 1] = (controlPoints[i].Position + slider.Position, i == controlPoints.Count - 1 ? PathType.LINEAR : controlPoints[i].Type); } args[^1] = (circle2.Position, null); @@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor mergeSelection(); AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor( - (pos: circle1.Position, pathType: PathType.Linear), + (pos: circle1.Position, pathType: PathType.LINEAR), (pos: circle2.Position, pathType: null))); AddAssert("samples exist", sliderSampleExist); @@ -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,12 +222,12 @@ 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(); AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor( - (pos: circle1.Position, pathType: PathType.Linear), + (pos: circle1.Position, pathType: PathType.LINEAR), (pos: circle2.Position, pathType: null))); } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOpenEditorTimestampInOsu.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOpenEditorTimestampInOsu.cs new file mode 100644 index 0000000000..943858652c --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOpenEditorTimestampInOsu.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 System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + public partial class TestSceneOpenEditorTimestampInOsu : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + [Test] + public void TestNormalSelection() + { + addStepClickLink("00:02:170 (1,2,3)"); + checkSelection(() => 2_170, 1, 2, 3); + + addReset(); + addStepClickLink("00:04:748 (2,3,4,1,2)"); + checkSelection(() => 4_748, 2, 3, 4, 1, 2); + + addReset(); + addStepClickLink("00:02:170 (1,1,1)"); + checkSelection(() => 2_170, 1, 1, 1); + + addReset(); + addStepClickLink("00:02:873 (2,2,2,2)"); + checkSelection(() => 2_873, 2, 2, 2, 2); + } + + [Test] + public void TestUnusualSelection() + { + HitObject firstObject = null!; + + AddStep("retrieve first object", () => firstObject = EditorBeatmap.HitObjects.First()); + + addStepClickLink("00:00:000 (0)", "invalid combo"); + checkSelection(() => firstObject.StartTime); + + addReset(); + addStepClickLink("00:00:000 (1)", "wrong offset"); + checkSelection(() => firstObject.StartTime, 1); + + addReset(); + addStepClickLink("00:00:956 (2,3,4)", "wrong offset"); + checkSelection(() => firstObject.StartTime, 2, 3, 4); + + addReset(); + addStepClickLink("00:00:956 (956|1,956|2)", "mania link"); + checkSelection(() => firstObject.StartTime); + } + + private void addReset() => addStepClickLink("00:00:000", "reset", false); + + private void addStepClickLink(string timestamp, string step = "", bool displayTimestamp = true) + { + AddStep(displayTimestamp ? $"{step} {timestamp}" : step, () => Editor.HandleTimestamp(timestamp)); + AddUntilStep("wait for seek", () => EditorClock.SeekingOrStopped.Value); + } + + private void checkSelection(Func startTime, params int[] comboNumbers) + => AddUntilStep($"seeked & selected {(comboNumbers.Any() ? string.Join(",", comboNumbers) : "nothing")}", () => + { + bool checkCombos = comboNumbers.Any() + ? hasCombosInOrder(EditorBeatmap.SelectedHitObjects, comboNumbers) + : !EditorBeatmap.SelectedHitObjects.Any(); + + return EditorClock.CurrentTime == startTime() + && EditorBeatmap.SelectedHitObjects.Count == comboNumbers.Length + && checkCombos; + }); + + private bool hasCombosInOrder(IEnumerable selected, params int[] comboNumbers) + { + List hitObjects = selected.ToList(); + if (hitObjects.Count != comboNumbers.Length) + return false; + + return !hitObjects.Select(x => (OsuHitObject)x) + .Where((x, i) => x.IndexInCurrentCombo + 1 != comboNumbers[i]) + .Any(); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index 0c064ecfa6..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); @@ -265,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/TestSceneOsuEditorSelectInvalidPath.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs index 37a109de18..43dae38004 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor PathControlPoint[] points = { - new PathControlPoint(new Vector2(0), PathType.PerfectCurve), + new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(-100, 0)), new PathControlPoint(new Vector2(100, 20)) }; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 0d6841017e..8234381283 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { createVisualiser(true); - addControlPointStep(new Vector2(200), PathType.Bezier); + addControlPointStep(new Vector2(200), PathType.BEZIER); addControlPointStep(new Vector2(300)); addControlPointStep(new Vector2(500, 300)); addControlPointStep(new Vector2(700, 200)); @@ -63,9 +63,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true); addContextMenuItemStep("Perfect curve"); - assertControlPointPathType(0, PathType.Bezier); - assertControlPointPathType(1, PathType.PerfectCurve); - assertControlPointPathType(3, PathType.Bezier); + assertControlPointPathType(0, PathType.BEZIER); + assertControlPointPathType(1, PathType.PERFECT_CURVE); + assertControlPointPathType(3, PathType.BEZIER); } [Test] @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { createVisualiser(true); - addControlPointStep(new Vector2(200), PathType.Bezier); + addControlPointStep(new Vector2(200), PathType.BEZIER); addControlPointStep(new Vector2(300)); addControlPointStep(new Vector2(500, 300)); addControlPointStep(new Vector2(700, 200)); @@ -83,8 +83,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("select control point", () => visualiser.Pieces[2].IsSelected.Value = true); addContextMenuItemStep("Perfect curve"); - assertControlPointPathType(0, PathType.Bezier); - assertControlPointPathType(2, PathType.PerfectCurve); + assertControlPointPathType(0, PathType.BEZIER); + assertControlPointPathType(2, PathType.PERFECT_CURVE); assertControlPointPathType(4, null); } @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { createVisualiser(true); - addControlPointStep(new Vector2(200), PathType.Bezier); + addControlPointStep(new Vector2(200), PathType.BEZIER); addControlPointStep(new Vector2(300)); addControlPointStep(new Vector2(500, 300)); addControlPointStep(new Vector2(700, 200)); @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true); addContextMenuItemStep("Perfect curve"); - assertControlPointPathType(0, PathType.Bezier); + assertControlPointPathType(0, PathType.BEZIER); AddAssert("point 3 is not inherited", () => slider.Path.ControlPoints[3].Type != null); } @@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { createVisualiser(true); - addControlPointStep(new Vector2(200), PathType.Linear); + addControlPointStep(new Vector2(200), PathType.LINEAR); addControlPointStep(new Vector2(300)); addControlPointStep(new Vector2(500, 300)); addControlPointStep(new Vector2(700, 200)); @@ -123,9 +123,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true); addContextMenuItemStep("Perfect curve"); - assertControlPointPathType(0, PathType.Linear); - assertControlPointPathType(1, PathType.PerfectCurve); - assertControlPointPathType(3, PathType.Linear); + assertControlPointPathType(0, PathType.LINEAR); + assertControlPointPathType(1, PathType.PERFECT_CURVE); + assertControlPointPathType(3, PathType.LINEAR); } [Test] @@ -133,21 +133,45 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { createVisualiser(true); - addControlPointStep(new Vector2(200), PathType.Bezier); - addControlPointStep(new Vector2(300), PathType.PerfectCurve); + addControlPointStep(new Vector2(200), PathType.BEZIER); + addControlPointStep(new Vector2(300), PathType.PERFECT_CURVE); addControlPointStep(new Vector2(500, 300)); - addControlPointStep(new Vector2(700, 200), PathType.Bezier); + addControlPointStep(new Vector2(700, 200), PathType.BEZIER); addControlPointStep(new Vector2(500, 100)); moveMouseToControlPoint(3); AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true); addContextMenuItemStep("Inherit"); - assertControlPointPathType(0, PathType.Bezier); - assertControlPointPathType(1, PathType.Bezier); + assertControlPointPathType(0, PathType.BEZIER); + assertControlPointPathType(1, PathType.BEZIER); assertControlPointPathType(3, null); } + [Test] + public void TestCatmullAvailableIffSelectionContainsCatmull() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200), PathType.CATMULL); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(500, 300)); + addControlPointStep(new Vector2(700, 200)); + addControlPointStep(new Vector2(500, 100)); + + moveMouseToControlPoint(2); + AddStep("select first and third control point", () => + { + visualiser.Pieces[0].IsSelected.Value = true; + visualiser.Pieces[2].IsSelected.Value = true; + }); + addContextMenuItemStep("Catmull"); + + assertControlPointPathType(0, PathType.CATMULL); + assertControlPointPathType(2, PathType.CATMULL); + assertControlPointPathType(4, null); + } + private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection) { Anchor = Anchor.Centre, @@ -158,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private void addControlPointStep(Vector2 position, PathType? type) { - AddStep($"add {type} control point at {position}", () => + AddStep($"add {type?.Type} control point at {position}", () => { slider.Path.ControlPoints.Add(new PathControlPoint(position, type)); }); @@ -169,7 +193,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)); }); } 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..99ced30ffe 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs @@ -38,9 +38,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor Position = new Vector2(256, 192), Path = new SliderPath(new[] { - new PathControlPoint(Vector2.Zero, PathType.PerfectCurve), + new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(150, 150)), - new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve), + new PathControlPoint(new Vector2(300, 0), PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(400, 0)), new PathControlPoint(new Vector2(400, 150)) }) @@ -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] @@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); assertControlPointPosition(1, new Vector2(150, 50)); - assertControlPointType(0, PathType.PerfectCurve); + assertControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -210,7 +210,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("three control point pieces selected", () => this.ChildrenOfType>().Count(piece => piece.IsSelected.Value) == 3); assertControlPointPosition(2, new Vector2(450, 50)); - assertControlPointType(2, PathType.PerfectCurve); + assertControlPointType(2, PathType.PERFECT_CURVE); assertControlPointPosition(3, new Vector2(550, 50)); @@ -249,7 +249,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider moved", () => Precision.AlmostEquals(slider.Position, new Vector2(256, 192) + new Vector2(150, 50))); assertControlPointPosition(0, Vector2.Zero); - assertControlPointType(0, PathType.PerfectCurve); + assertControlPointType(0, PathType.PERFECT_CURVE); assertControlPointPosition(1, new Vector2(0, 100)); @@ -272,7 +272,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); assertControlPointPosition(1, new Vector2(400, 0.01f)); - assertControlPointType(0, PathType.Bezier); + assertControlPointType(0, PathType.BEZIER); } [Test] @@ -282,13 +282,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); addMovementStep(new Vector2(400, 0.01f)); - assertControlPointType(0, PathType.Bezier); + assertControlPointType(0, PathType.BEZIER); addMovementStep(new Vector2(150, 50)); AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); assertControlPointPosition(1, new Vector2(150, 50)); - assertControlPointType(0, PathType.PerfectCurve); + assertControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -298,32 +298,32 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); addMovementStep(new Vector2(350, 0.01f)); - assertControlPointType(2, PathType.Bezier); + assertControlPointType(2, PathType.BEZIER); addMovementStep(new Vector2(150, 150)); AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); assertControlPointPosition(4, new Vector2(150, 150)); - assertControlPointType(2, PathType.PerfectCurve); + assertControlPointType(2, PathType.PERFECT_CURVE); } [Test] public void TestDragControlPointPathAfterChangingType() { - AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type = PathType.Bezier); + AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type = PathType.BEZIER); AddStep("add point", () => slider.Path.ControlPoints.Add(new PathControlPoint(new Vector2(500, 10)))); - AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type = PathType.PerfectCurve); + AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type = PathType.PERFECT_CURVE); moveMouseToControlPoint(4); AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); - assertControlPointType(3, PathType.PerfectCurve); + assertControlPointType(3, PathType.PERFECT_CURVE); addMovementStep(new Vector2(350, 0.01f)); AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); assertControlPointPosition(4, new Vector2(350, 0.01f)); - assertControlPointType(3, PathType.Bezier); + assertControlPointType(3, PathType.BEZIER); } private void addMovementStep(Vector2 relativePosition) @@ -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/TestSceneSliderLengthValidity.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderLengthValidity.cs index 77e828e80a..931c8c9e63 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderLengthValidity.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderLengthValidity.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor PathControlPoint[] points = { - new PathControlPoint(new Vector2(0), PathType.Linear), + new PathControlPoint(new Vector2(0), PathType.LINEAR), new PathControlPoint(new Vector2(100, 0)), }; @@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor PathControlPoint[] points = { - new PathControlPoint(new Vector2(0), PathType.Linear), + new PathControlPoint(new Vector2(0), PathType.LINEAR), new PathControlPoint(new Vector2(100, 0)), }; @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor PathControlPoint[] points = { - new PathControlPoint(new Vector2(0), PathType.PerfectCurve), + new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(100, 0)), new PathControlPoint(new Vector2(0, 10)) }; @@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor PathControlPoint[] points = { - new PathControlPoint(new Vector2(0), PathType.Linear), + new PathControlPoint(new Vector2(0), PathType.LINEAR), new PathControlPoint(new Vector2(0, 50)), new PathControlPoint(new Vector2(0, 100)) }; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 7d29670daa..bbded55732 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.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 NUnit.Framework; using osu.Framework.Utils; using osu.Game.Rulesets.Edit; @@ -58,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertLength(200); assertControlPointCount(2); - assertControlPointType(0, PathType.Linear); + assertControlPointType(0, PathType.LINEAR); } [Test] @@ -72,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(2); - assertControlPointType(0, PathType.Linear); + assertControlPointType(0, PathType.LINEAR); } [Test] @@ -90,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); assertControlPointPosition(1, new Vector2(100, 0)); - assertControlPointType(0, PathType.PerfectCurve); + assertControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -112,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointCount(4); assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(2, new Vector2(100, 100)); - assertControlPointType(0, PathType.Bezier); + assertControlPointType(0, PathType.BEZIER); } [Test] @@ -131,8 +130,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); assertControlPointPosition(1, new Vector2(100, 0)); - assertControlPointType(0, PathType.Linear); - assertControlPointType(1, PathType.Linear); + assertControlPointType(0, PathType.LINEAR); + assertControlPointType(1, PathType.LINEAR); } [Test] @@ -150,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(2); - assertControlPointType(0, PathType.Linear); + assertControlPointType(0, PathType.LINEAR); assertLength(100); } @@ -172,7 +171,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PerfectCurve); + assertControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -196,7 +195,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(4); - assertControlPointType(0, PathType.Bezier); + assertControlPointType(0, PathType.BEZIER); } [Test] @@ -216,8 +215,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointCount(3); assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(2, new Vector2(100)); - assertControlPointType(0, PathType.Linear); - assertControlPointType(1, PathType.Linear); + assertControlPointType(0, PathType.LINEAR); + assertControlPointType(1, PathType.LINEAR); } [Test] @@ -240,8 +239,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointCount(4); assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(2, new Vector2(100)); - assertControlPointType(0, PathType.Linear); - assertControlPointType(1, PathType.PerfectCurve); + assertControlPointType(0, PathType.LINEAR); + assertControlPointType(1, PathType.PERFECT_CURVE); } [Test] @@ -269,25 +268,79 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointPosition(2, new Vector2(100)); assertControlPointPosition(3, new Vector2(200, 100)); assertControlPointPosition(4, new Vector2(200)); - assertControlPointType(0, PathType.PerfectCurve); - assertControlPointType(2, PathType.PerfectCurve); + assertControlPointType(0, PathType.PERFECT_CURVE); + assertControlPointType(2, PathType.PERFECT_CURVE); } [Test] - public void TestBeginPlacementWithoutReleasingMouse() + public void TestSliderDrawingDoesntActivateAfterNormalPlacement() + { + Vector2 startPoint = new Vector2(200); + + addMovementStep(startPoint); + addClickStep(MouseButton.Left); + + for (int i = 0; i < 20; i++) + { + if (i == 5) + AddStep("press left button", () => InputManager.PressButton(MouseButton.Left)); + addMovementStep(startPoint + new Vector2(i * 40, MathF.Sin(i * MathF.PI / 5) * 50)); + } + + AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left)); + assertPlaced(false); + + addClickStep(MouseButton.Right); + assertPlaced(true); + + assertControlPointType(0, PathType.BEZIER); + } + + [Test] + public void TestSliderDrawingCurve() + { + Vector2 startPoint = new Vector2(200); + + addMovementStep(startPoint); + AddStep("press left button", () => InputManager.PressButton(MouseButton.Left)); + + for (int i = 0; i < 20; i++) + addMovementStep(startPoint + new Vector2(i * 40, MathF.Sin(i * MathF.PI / 5) * 50)); + + AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left)); + + assertPlaced(true); + assertLength(808, tolerance: 10); + assertControlPointCount(5); + assertControlPointType(0, PathType.BSpline(4)); + assertControlPointType(1, null); + assertControlPointType(2, null); + assertControlPointType(3, null); + assertControlPointType(4, null); + } + + [Test] + public void TestSliderDrawingLinear() { addMovementStep(new Vector2(200)); AddStep("press left button", () => InputManager.PressButton(MouseButton.Left)); + addMovementStep(new Vector2(300, 200)); addMovementStep(new Vector2(400, 200)); + addMovementStep(new Vector2(400, 300)); + addMovementStep(new Vector2(400)); + addMovementStep(new Vector2(300, 400)); + addMovementStep(new Vector2(200, 400)); + AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left)); - addClickStep(MouseButton.Right); - assertPlaced(true); - assertLength(200); - assertControlPointCount(2); - assertControlPointType(0, PathType.Linear); + assertLength(600, tolerance: 10); + assertControlPointCount(4); + assertControlPointType(0, PathType.BSpline(4)); + assertControlPointType(1, PathType.BSpline(4)); + assertControlPointType(2, PathType.BSpline(4)); + assertControlPointType(3, null); } [Test] @@ -306,7 +359,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.Bezier); + assertControlPointType(0, PathType.BEZIER); } [Test] @@ -326,7 +379,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PerfectCurve); + assertControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -347,7 +400,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PerfectCurve); + assertControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -368,7 +421,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.Bezier); + assertControlPointType(0, PathType.BEZIER); } [Test] @@ -385,7 +438,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PerfectCurve); + assertControlPointType(0, PathType.PERFECT_CURVE); } private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position))); @@ -397,16 +450,16 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private void assertPlaced(bool expected) => AddAssert($"slider {(expected ? "placed" : "not placed")}", () => (getSlider() != null) == expected); - private void assertLength(double expected) => AddAssert($"slider length is {expected}", () => Precision.AlmostEquals(expected, getSlider().Distance, 1)); + private void assertLength(double expected, double tolerance = 1) => AddAssert($"slider length is {expected}±{tolerance}", () => getSlider()!.Distance, () => Is.EqualTo(expected).Within(tolerance)); - private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider().Path.ControlPoints.Count == expected); + private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider()!.Path.ControlPoints.Count, () => Is.EqualTo(expected)); - private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => getSlider().Path.ControlPoints[index].Type == type); + private void assertControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type)); private void assertControlPointPosition(int index, Vector2 position) => - AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider().Path.ControlPoints[index].Position, 1)); + AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider()!.Path.ControlPoints[index].Position, 1)); - private Slider getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null; + private Slider? getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null; protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject); protected override PlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint(); 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..a44c16a2e0 --- /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.PERFECT_CURVE, + 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..d4d99e1019 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor slider = new Slider { Position = new Vector2(256, 192), - Path = new SliderPath(PathType.Bezier, new[] + Path = new SliderPath(PathType.BEZIER, new[] { Vector2.Zero, new Vector2(150, 150), @@ -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..541fefb3a6 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { ControlPoints = { - new PathControlPoint(Vector2.Zero, PathType.PerfectCurve), + new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(136, 205)), new PathControlPoint(new Vector2(-4, 226)) } @@ -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", () => { @@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { OsuSelectionHandler selectionHandler; - AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve); + AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE); AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); AddStep("rotate 90 degrees ccw", () => @@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor selectionHandler.HandleRotation(-90); }); - AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve); + AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE); } [Test] @@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { OsuSelectionHandler selectionHandler; - AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve); + AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE); AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); AddStep("flip slider horizontally", () => @@ -232,7 +232,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor selectionHandler.OnPressed(new KeyBindingPressEvent(InputManager.CurrentState, GlobalAction.EditorFlipVertically)); }); - AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve); + AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE); } [Test] diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs index 8ba97892fe..6c7733e68a 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs @@ -45,9 +45,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor Position = new Vector2(0, 50), Path = new SliderPath(new[] { - new PathControlPoint(Vector2.Zero, PathType.PerfectCurve), + new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(150, 150)), - new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve), + new PathControlPoint(new Vector2(300, 0), PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(400, 0)), new PathControlPoint(new Vector2(400, 150)) }) @@ -73,20 +73,20 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 2 && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap, - (new Vector2(0, 50), PathType.PerfectCurve), + (new Vector2(0, 50), PathType.PERFECT_CURVE), (new Vector2(150, 200), null), (new Vector2(300, 50), null) ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], slider.StartTime, endTime + split_gap, - (new Vector2(300, 50), PathType.PerfectCurve), + (new Vector2(300, 50), PathType.PERFECT_CURVE), (new Vector2(400, 50), null), (new Vector2(400, 200), null) )); AddStep("undo", () => Editor.Undo()); AddAssert("original slider restored", () => EditorBeatmap.HitObjects.Count == 1 && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, endTime, - (new Vector2(0, 50), PathType.PerfectCurve), + (new Vector2(0, 50), PathType.PERFECT_CURVE), (new Vector2(150, 200), null), - (new Vector2(300, 50), PathType.PerfectCurve), + (new Vector2(300, 50), PathType.PERFECT_CURVE), (new Vector2(400, 50), null), (new Vector2(400, 200), null) )); @@ -104,11 +104,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor Position = new Vector2(0, 50), Path = new SliderPath(new[] { - new PathControlPoint(Vector2.Zero, PathType.PerfectCurve), + new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(150, 150)), - new PathControlPoint(new Vector2(300, 0), PathType.Bezier), + new PathControlPoint(new Vector2(300, 0), PathType.BEZIER), new PathControlPoint(new Vector2(400, 0)), - new PathControlPoint(new Vector2(400, 150), PathType.Catmull), + new PathControlPoint(new Vector2(400, 150), PathType.CATMULL), new PathControlPoint(new Vector2(300, 200)), new PathControlPoint(new Vector2(400, 250)) }) @@ -139,15 +139,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 3 && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap, - (new Vector2(0, 50), PathType.PerfectCurve), + (new Vector2(0, 50), PathType.PERFECT_CURVE), (new Vector2(150, 200), null), (new Vector2(300, 50), null) ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], EditorBeatmap.HitObjects[0].GetEndTime() + split_gap, slider.StartTime - split_gap, - (new Vector2(300, 50), PathType.Bezier), + (new Vector2(300, 50), PathType.BEZIER), (new Vector2(400, 50), null), (new Vector2(400, 200), null) ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[2], EditorBeatmap.HitObjects[1].GetEndTime() + split_gap, endTime + split_gap * 2, - (new Vector2(400, 200), PathType.Catmull), + (new Vector2(400, 200), PathType.CATMULL), (new Vector2(300, 250), null), (new Vector2(400, 300), null) )); @@ -163,12 +163,11 @@ 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), + new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(150, 150)), - new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve), + new PathControlPoint(new Vector2(300, 0), PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(400, 0)), new PathControlPoint(new Vector2(400, 150)) }) @@ -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,7 +241,7 @@ 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?.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/TestSliderScaling.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs index 64d23090d0..021fdba225 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor PathControlPoint[] points = { - new PathControlPoint(new Vector2(0), PathType.Linear), + new PathControlPoint(new Vector2(0), PathType.LINEAR), new PathControlPoint(new Vector2(100, 0)), }; 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 32028823ae..ace7f23989 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs @@ -51,8 +51,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods FinalRate = { Value = 1.3 } }); - [Test] - public void TestPerfectScoreOnShortSliderWithRepeat() + [TestCase(6.25f)] + [TestCase(20)] + public void TestPerfectScoreOnShortSliderWithRepeat(float pathLength) { AddStep("set score to standardised", () => LocalConfig.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised)); @@ -70,10 +71,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Path = new SliderPath(new[] { new PathControlPoint(), - new PathControlPoint(new Vector2(0, 6.25f)) + new PathControlPoint(new Vector2(0, pathLength)) }), RepeatCount = 1, - SliderVelocity = 10 + SliderVelocityMultiplier = 10 } } }, diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs index a353914cd5..776d5854d1 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs @@ -1,8 +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 System.Collections.Generic; +using System.Linq; using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods { @@ -21,5 +31,129 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods [Test] public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true }); + + [Test] + public void TestSliderDimsOnlyAfterStartTime() + { + bool sliderDimmedBeforeStartTime = false; + + CreateModTest(new ModTestData + { + Mod = new OsuModFlashlight(), + PassCondition = () => + { + sliderDimmedBeforeStartTime |= + Player.GameplayClockContainer.CurrentTime < 1000 && Player.ChildrenOfType.Flashlight>().Single().FlashlightDim > 0; + return Player.GameplayState.HasPassed && !sliderDimmedBeforeStartTime; + }, + Beatmap = new OsuBeatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, }, + new Slider + { + StartTime = 1000, + Path = new SliderPath(new[] + { + new PathControlPoint(), + new PathControlPoint(new Vector2(100)) + }) + } + }, + BeatmapInfo = + { + StackLeniency = 0, + } + }, + ReplayFrames = new List + { + new OsuReplayFrame(0, new Vector2(), OsuAction.LeftButton), + new OsuReplayFrame(990, new Vector2()), + new OsuReplayFrame(1000, new Vector2(), OsuAction.LeftButton), + new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton), + new OsuReplayFrame(2001, new Vector2(100)), + }, + Autoplay = false, + }); + } + + [Test] + public void TestSliderDoesDimAfterStartTimeIfHitEarly() + { + bool sliderDimmed = false; + + CreateModTest(new ModTestData + { + Mod = new OsuModFlashlight(), + PassCondition = () => + { + sliderDimmed |= + Player.GameplayClockContainer.CurrentTime >= 1000 && Player.ChildrenOfType.Flashlight>().Single().FlashlightDim > 0; + return Player.GameplayState.HasPassed && sliderDimmed; + }, + Beatmap = new OsuBeatmap + { + HitObjects = new List + { + new Slider + { + StartTime = 1000, + Path = new SliderPath(new[] + { + new PathControlPoint(), + new PathControlPoint(new Vector2(100)) + }) + } + }, + }, + ReplayFrames = new List + { + new OsuReplayFrame(990, new Vector2(), OsuAction.LeftButton), + new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton), + new OsuReplayFrame(2001, new Vector2(100)), + }, + Autoplay = false, + }); + } + + [Test] + public void TestSliderDoesDimAfterStartTimeIfHitLate() + { + bool sliderDimmed = false; + + CreateModTest(new ModTestData + { + Mod = new OsuModFlashlight(), + PassCondition = () => + { + sliderDimmed |= + Player.GameplayClockContainer.CurrentTime >= 1000 && Player.ChildrenOfType.Flashlight>().Single().FlashlightDim > 0; + return Player.GameplayState.HasPassed && sliderDimmed; + }, + Beatmap = new OsuBeatmap + { + HitObjects = new List + { + new Slider + { + StartTime = 1000, + Path = new SliderPath(new[] + { + new PathControlPoint(), + new PathControlPoint(new Vector2(100)) + }) + } + }, + }, + ReplayFrames = new List + { + new OsuReplayFrame(1100, new Vector2(), OsuAction.LeftButton), + new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton), + new OsuReplayFrame(2001, new Vector2(100)), + }, + Autoplay = false, + }); + } } } diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs index 3f84ac6935..58bdd805c1 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs @@ -81,12 +81,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods new Slider { StartTime = 3200, - Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), }) }, new Slider { StartTime = 5200, - Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), }) } } }, @@ -105,12 +105,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods new Slider { StartTime = 1000, - Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), }) }, new Slider { StartTime = 4000, - Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), }) }, } }, @@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { StartTime = 3000, Position = new Vector2(156, 242), - Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(200, 0), }) + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(200, 0), }) }, new Spinner { diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs index f0496efc19..b01bbbfca1 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs @@ -1,17 +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 System.Collections.Generic; using NUnit.Framework; +using osu.Game.Beatmaps; 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.Tests.Visual; using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods { - public partial class TestSceneOsuModPerfect : ModPerfectTestScene + public partial class TestSceneOsuModPerfect : ModFailConditionTestScene { protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); @@ -31,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods var slider = new Slider { StartTime = 1000, - Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), }) }; CreateHitObjectTest(new HitObjectTestData(slider), shouldMiss); @@ -50,5 +54,30 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods CreateHitObjectTest(new HitObjectTestData(spinner), shouldMiss); } + + [Test] + public void TestMissSliderTail() => CreateModTest(new ModTestData + { + Mod = new OsuModPerfect(), + PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true), + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Slider + { + Position = new Vector2(256, 192), + StartTime = 1000, + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), }) + }, + }, + }, + ReplayFrames = new List + { + new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton), + new OsuReplayFrame(1001, new Vector2(256, 192)), + } + }); } } diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModStrictTracking.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModStrictTracking.cs new file mode 100644 index 0000000000..726b415977 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModStrictTracking.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 System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public partial class TestSceneOsuModStrictTracking : OsuModTestScene + { + [Test] + public void TestSliderInput() => CreateModTest(new ModTestData + { + Mod = new OsuModStrictTracking(), + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Slider + { + StartTime = 1000, + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint(), + new PathControlPoint(new Vector2(0, 100)) + } + } + } + } + }, + ReplayFrames = new List + { + new OsuReplayFrame(0, new Vector2(), OsuAction.LeftButton), + new OsuReplayFrame(500, new Vector2(200, 0), OsuAction.LeftButton), + new OsuReplayFrame(501, new Vector2(200, 0)), + new OsuReplayFrame(1000, new Vector2(), OsuAction.LeftButton), + new OsuReplayFrame(1750, new Vector2(0, 100), OsuAction.LeftButton), + new OsuReplayFrame(1751, new Vector2(0, 100)), + }, + PassCondition = () => Player.ScoreProcessor.Combo.Value == 2 + }); + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSuddenDeath.cs new file mode 100644 index 0000000000..ea048aaa6e --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSuddenDeath.cs @@ -0,0 +1,77 @@ +// 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.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.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public partial class TestSceneOsuModSuddenDeath : ModFailConditionTestScene + { + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + + public TestSceneOsuModSuddenDeath() + : base(new OsuModSuddenDeath()) + { + } + + [Test] + public void TestMissTail() => CreateModTest(new ModTestData + { + Mod = new OsuModSuddenDeath(), + PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false), + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Slider + { + Position = new Vector2(256, 192), + StartTime = 1000, + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), }) + }, + }, + }, + ReplayFrames = new List + { + new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton), + new OsuReplayFrame(1001, new Vector2(256, 192)), + } + }); + + [Test] + public void TestMissTick() => CreateModTest(new ModTestData + { + Mod = new OsuModSuddenDeath(), + PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true), + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Slider + { + Position = new Vector2(256, 192), + StartTime = 1000, + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(200, 0), }) + }, + }, + }, + ReplayFrames = new List + { + new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton), + new OsuReplayFrame(1001, new Vector2(256, 192)), + } + }); + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs new file mode 100644 index 0000000000..fdb1cac3e5 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs @@ -0,0 +1,211 @@ +// 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.Input; +using osu.Framework.Screens; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Configuration; +using osu.Game.Input; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; +using osu.Game.Storyboards; +using osu.Game.Tests.Visual; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public partial class TestSceneOsuModTouchDevice : RateAdjustedBeatmapTestScene + { + [Resolved] + private SessionStatics statics { get; set; } = null!; + + private ScoreAccessibleSoloPlayer currentPlayer = null!; + private readonly ManualClock manualClock = new ManualClock { Rate = 0 }; + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) + => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(manualClock), Audio); + + [BackgroundDependencyLoader] + private void load() + { + Add(new TouchInputInterceptor()); + } + + public override void SetUpSteps() + { + AddStep("reset static", () => statics.SetValue(Static.TouchInputActive, false)); + base.SetUpSteps(); + } + + [Test] + public void TestUserAlreadyHasTouchDeviceActive() + { + loadPlayer(); + AddStep("set up touchscreen user", () => + { + currentPlayer.Score.ScoreInfo.Mods = currentPlayer.Score.ScoreInfo.Mods.Append(new OsuModTouchDevice()).ToArray(); + statics.SetValue(Static.TouchInputActive, true); + }); + + AddStep("seek to 0", () => currentPlayer.GameplayClockContainer.Seek(0)); + AddUntilStep("wait until 0", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0)); + AddStep("touch circle", () => + { + var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + InputManager.EndTouch(touch); + }); + AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); + } + + [Test] + public void TestTouchActivePriorToPlayerLoad() + { + AddStep("set touch input active", () => statics.SetValue(Static.TouchInputActive, true)); + loadPlayer(); + AddUntilStep("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); + } + + [Test] + public void TestTouchDuringBreak() + { + loadPlayer(); + AddStep("seek to 2000", () => currentPlayer.GameplayClockContainer.Seek(2000)); + AddUntilStep("wait until 2000", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(2000)); + AddUntilStep("wait until break entered", () => currentPlayer.IsBreakTime.Value); + AddStep("touch playfield", () => + { + var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + InputManager.EndTouch(touch); + }); + AddAssert("touch device mod not activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.None.InstanceOf()); + } + + [Test] + public void TestTouchMiss() + { + loadPlayer(); + // ensure mouse is active (and that it's not suppressed due to touches in previous tests) + AddStep("click mouse", () => InputManager.Click(MouseButton.Left)); + + AddStep("seek to 200", () => currentPlayer.GameplayClockContainer.Seek(200)); + AddUntilStep("wait until 200", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(200)); + AddStep("touch playfield", () => + { + var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + InputManager.EndTouch(touch); + }); + AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); + } + + [Test] + public void TestIncompatibleModActive() + { + loadPlayer(); + // this is only a veneer of enabling autopilot as having it actually active from the start is annoying to make happen + // given the tests' structure. + AddStep("enable autopilot", () => currentPlayer.Score.ScoreInfo.Mods = new Mod[] { new OsuModAutopilot() }); + + AddStep("seek to 0", () => currentPlayer.GameplayClockContainer.Seek(0)); + AddUntilStep("wait until 0", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0)); + AddStep("touch playfield", () => + { + var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + InputManager.EndTouch(touch); + }); + AddAssert("touch device mod not activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.None.InstanceOf()); + } + + [Test] + public void TestSecondObjectTouched() + { + loadPlayer(); + // ensure mouse is active (and that it's not suppressed due to touches in previous tests) + AddStep("click mouse", () => InputManager.Click(MouseButton.Left)); + + AddStep("seek to 0", () => currentPlayer.GameplayClockContainer.Seek(0)); + AddUntilStep("wait until 0", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0)); + AddStep("click circle", () => + { + InputManager.MoveMouseTo(currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); + InputManager.Click(MouseButton.Left); + }); + AddAssert("touch device mod not activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.None.InstanceOf()); + + AddStep("seek to 5000", () => currentPlayer.GameplayClockContainer.Seek(5000)); + AddUntilStep("wait until 5000", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(5000)); + AddStep("touch playfield", () => + { + var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + InputManager.EndTouch(touch); + }); + AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); + } + + private void loadPlayer() + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new OsuBeatmap + { + HitObjects = + { + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE / 2, + StartTime = 0, + }, + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE / 2, + StartTime = 5000, + }, + }, + Breaks = + { + new BreakPeriod(2000, 3000) + } + }); + + var p = new ScoreAccessibleSoloPlayer(); + + LoadScreen(currentPlayer = p); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + } + + private partial class ScoreAccessibleSoloPlayer : SoloPlayer + { + public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; + + public new DrawableRuleset DrawableRuleset => base.DrawableRuleset; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleSoloPlayer() + : base(new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs index b94e9f38c6..838bd35dd4 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs @@ -14,16 +14,19 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public class OsuBeatmapConversionTest : BeatmapConversionTest { - protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; + protected override string ResourceAssembly => "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")] + [TestCase("1124896")] public void Test(string name) => base.Test(name); protected override IEnumerable CreateConvertValue(HitObject hitObject) @@ -32,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Tests { case Slider slider: foreach (var nested in slider.NestedHitObjects) - yield return createConvertValue((OsuHitObject)nested); + yield return createConvertValue((OsuHitObject)nested, slider); break; @@ -42,13 +45,28 @@ 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(); + + // 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 9c6449cfa9..e35cf10d95 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -13,21 +13,24 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public class OsuDifficultyCalculatorTest : DifficultyCalculatorTest { - protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; + protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.7115569159190587d, 206, "diffcalc-test")] - [TestCase(1.4391311903612753d, 45, "zero-length-sliders")] - [TestCase(0.14102693012101306d, 1, "nan-slider")] + [TestCase(6.710442985146793d, 239, "diffcalc-test")] + [TestCase(1.4386882251130073d, 54, "zero-length-sliders")] + [TestCase(0.42506480230838789d, 4, "very-fast-slider")] + [TestCase(0.14102693012101306d, 2, "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, 239, "diffcalc-test")] + [TestCase(1.743180218215227d, 54, "zero-length-sliders")] + [TestCase(0.55071082800473514d, 4, "very-fast-slider")] 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(1.4386882251130073d, 54, "zero-length-sliders")] + [TestCase(0.42506480230838789d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); diff --git a/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs index daa914cac2..d78c32aa6a 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs @@ -26,9 +26,9 @@ namespace osu.Game.Rulesets.Osu.Tests { ControlPoints = { - new PathControlPoint(new Vector2(), PathType.Linear), - new PathControlPoint(new Vector2(-64, -128), PathType.Linear), // absolute position: (64, 0) - new PathControlPoint(new Vector2(-128, 0), PathType.Linear) // absolute position: (0, 128) + new PathControlPoint(new Vector2(), PathType.LINEAR), + new PathControlPoint(new Vector2(-64, -128), PathType.LINEAR), // absolute position: (64, 0) + new PathControlPoint(new Vector2(-128, 0), PathType.LINEAR) // absolute position: (0, 128) } }, RepeatCount = 1 diff --git a/osu.Game.Rulesets.Osu.Tests/OsuRateAdjustedDisplayDifficultyTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuRateAdjustedDisplayDifficultyTest.cs new file mode 100644 index 0000000000..aa903205c8 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/OsuRateAdjustedDisplayDifficultyTest.cs @@ -0,0 +1,65 @@ +// 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; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public class OsuRateAdjustedDisplayDifficultyTest + { + private static IEnumerable difficultyValuesToTest() + { + for (float i = 0; i <= 10; i += 0.5f) + yield return i; + } + + [TestCaseSource(nameof(difficultyValuesToTest))] + public void TestApproachRateIsUnchangedWithRateEqualToOne(float originalApproachRate) + { + var ruleset = new OsuRuleset(); + var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate }; + + var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1); + + Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate)); + } + + [TestCaseSource(nameof(difficultyValuesToTest))] + public void TestOverallDifficultyIsUnchangedWithRateEqualToOne(float originalOverallDifficulty) + { + var ruleset = new OsuRuleset(); + var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty }; + + var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1); + + Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty)); + } + + [Test] + public void TestRateBelowOne() + { + var ruleset = new OsuRuleset(); + var difficulty = new BeatmapDifficulty(); + + var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75); + + Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01)); + Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(2.22).Within(0.01)); + } + + [Test] + public void TestRateAboveOne() + { + var ruleset = new OsuRuleset(); + var difficulty = new BeatmapDifficulty(); + + var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5); + + Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01)); + Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(7.77).Within(0.01)); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/1124896-expected-conversion.json b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/1124896-expected-conversion.json new file mode 100644 index 0000000000..68551d5d10 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/1124896-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":633.0,"Objects":[{"StartTime":633.0,"EndTime":633.0,"X":84.5217361,"Y":88.5217361}]},{"StartTime":844.0,"Objects":[{"StartTime":844.0,"EndTime":844.0,"X":88.2608643,"Y":92.2608643}]},{"StartTime":1055.0,"Objects":[{"StartTime":1055.0,"EndTime":1055.0,"X":92.0,"Y":96.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":1230.0,"EndTime":1230.0,"X":76.53984,"Y":161.705658,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":1477.0,"Objects":[{"StartTime":1477.0,"EndTime":1477.0,"X":200.0,"Y":100.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":1652.0,"EndTime":1652.0,"X":184.097,"Y":34.400116,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":1900.0,"Objects":[{"StartTime":1900.0,"EndTime":1900.0,"X":164.0,"Y":228.0}]},{"StartTime":2111.0,"Objects":[{"StartTime":2111.0,"EndTime":2111.0,"X":256.0,"Y":240.0}]},{"StartTime":2322.0,"Objects":[{"StartTime":2322.0,"EndTime":2322.0,"X":340.0,"Y":192.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":2497.0,"EndTime":2497.0,"X":350.197235,"Y":127.18325,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":2745.0,"Objects":[{"StartTime":2745.0,"EndTime":2745.0,"X":440.0,"Y":200.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":2920.0,"EndTime":2920.0,"X":450.363068,"Y":264.618042,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":3167.0,"Objects":[{"StartTime":3167.0,"EndTime":3167.0,"X":324.521729,"Y":308.521729}]},{"StartTime":3378.0,"Objects":[{"StartTime":3378.0,"EndTime":3378.0,"X":328.260864,"Y":312.260864,"StackOffset":{"X":-3.73913574,"Y":-3.73913574}},{"StartTime":3764.0,"EndTime":3764.0,"X":241.358566,"Y":327.7687,"StackOffset":{"X":-3.73913574,"Y":-3.73913574}}]},{"StartTime":4012.0,"Objects":[{"StartTime":4012.0,"EndTime":4012.0,"X":332.0,"Y":316.0}]},{"StartTime":4224.0,"Objects":[{"StartTime":4224.0,"EndTime":4224.0,"X":312.0,"Y":224.0}]},{"StartTime":4435.0,"Objects":[{"StartTime":4435.0,"EndTime":4435.0,"X":284.0,"Y":132.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":4610.0,"EndTime":4610.0,"X":218.719162,"Y":130.832062,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":4857.0,"Objects":[{"StartTime":4857.0,"EndTime":4857.0,"X":400.0,"Y":192.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":5032.0,"EndTime":5032.0,"X":465.280823,"Y":193.167923,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":5280.0,"Objects":[{"StartTime":5280.0,"EndTime":5280.0,"X":312.0,"Y":224.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":5455.0,"EndTime":5455.0,"X":310.832062,"Y":289.280823,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":5702.0,"Objects":[{"StartTime":5702.0,"EndTime":5702.0,"X":372.260864,"Y":104.260864}]},{"StartTime":5914.0,"Objects":[{"StartTime":5914.0,"EndTime":5914.0,"X":376.0,"Y":108.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":6300.0,"EndTime":6300.0,"X":249.910217,"Y":112.133125,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":6547.0,"Objects":[{"StartTime":6547.0,"EndTime":6547.0,"X":154.0,"Y":122.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":6722.0,"EndTime":6722.0,"X":171.671921,"Y":58.8828773,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":6970.0,"Objects":[{"StartTime":6970.0,"EndTime":6970.0,"X":107.0,"Y":195.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":7181.0,"EndTime":7181.0,"X":68.5987,"Y":143.051712,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":7356.0,"EndTime":7356.0,"X":107.0,"Y":195.0,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":7604.0,"Objects":[{"StartTime":7604.0,"EndTime":7604.0,"X":216.0,"Y":232.0}]},{"StartTime":7815.0,"Objects":[{"StartTime":7815.0,"EndTime":7815.0,"X":116.0,"Y":280.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":7990.0,"EndTime":7990.0,"X":53.6959572,"Y":265.658173,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":8238.0,"Objects":[{"StartTime":8238.0,"EndTime":8238.0,"X":176.0,"Y":160.0}]},{"StartTime":8449.0,"Objects":[{"StartTime":8449.0,"EndTime":8449.0,"X":248.0,"Y":291.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":8729.0,"EndTime":8729.0,"X":333.029968,"Y":327.610535,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":8871.0,"Objects":[{"StartTime":8871.0,"EndTime":8871.0,"X":334.0,"Y":328.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":9257.0,"EndTime":9257.0,"X":318.562378,"Y":193.885574,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":9505.0,"Objects":[{"StartTime":9505.0,"EndTime":9505.0,"X":428.0,"Y":184.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":9680.0,"EndTime":9680.0,"X":436.122375,"Y":251.009521,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":9928.0,"Objects":[{"StartTime":9928.0,"EndTime":9928.0,"X":328.0,"Y":128.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":10103.0,"EndTime":10103.0,"X":318.879852,"Y":194.881042,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":10350.0,"Objects":[{"StartTime":10350.0,"EndTime":10350.0,"X":320.0,"Y":108.0}]},{"StartTime":10773.0,"Objects":[{"StartTime":10773.0,"EndTime":10773.0,"X":308.0,"Y":88.0}]},{"StartTime":11195.0,"Objects":[{"StartTime":11195.0,"EndTime":11195.0,"X":296.0,"Y":68.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":11370.0,"EndTime":11370.0,"X":228.5764,"Y":64.78935,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":11618.0,"Objects":[{"StartTime":11618.0,"EndTime":11618.0,"X":318.0,"Y":194.0}]},{"StartTime":11829.0,"Objects":[{"StartTime":11829.0,"EndTime":11829.0,"X":288.0,"Y":52.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":12004.0,"EndTime":12004.0,"X":220.5764,"Y":48.7893524,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":12252.0,"Objects":[{"StartTime":12252.0,"EndTime":12252.0,"X":236.0,"Y":248.0}]},{"StartTime":12463.0,"Objects":[{"StartTime":12463.0,"EndTime":12463.0,"X":299.0,"Y":170.0}]},{"StartTime":12674.0,"Objects":[{"StartTime":12674.0,"EndTime":12674.0,"X":300.0,"Y":300.0}]},{"StartTime":12885.0,"Objects":[{"StartTime":12885.0,"EndTime":12885.0,"X":168.0,"Y":204.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":13096.0,"EndTime":13096.0,"X":100.5764,"Y":200.789352,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":13271.0,"EndTime":13271.0,"X":168.0,"Y":204.0,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":13519.0,"Objects":[{"StartTime":13519.0,"EndTime":13519.0,"X":227.0,"Y":332.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":13694.0,"EndTime":13694.0,"X":159.619965,"Y":336.022675,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":13942.0,"Objects":[{"StartTime":13942.0,"EndTime":13942.0,"X":299.260864,"Y":362.260864}]},{"StartTime":14153.0,"Objects":[{"StartTime":14153.0,"EndTime":14153.0,"X":302.0,"Y":365.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":14328.0,"EndTime":14328.0,"X":299.3276,"Y":299.703552,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":14576.0,"Objects":[{"StartTime":14576.0,"EndTime":14576.0,"X":469.0,"Y":258.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":14751.0,"EndTime":14751.0,"X":452.420563,"Y":331.144531,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":14998.0,"Objects":[{"StartTime":14998.0,"EndTime":14998.0,"X":376.0,"Y":256.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":15173.0,"EndTime":15173.0,"X":359.2077,"Y":182.904053,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":15421.0,"Objects":[{"StartTime":15421.0,"EndTime":15421.0,"X":384.0,"Y":80.0}]},{"StartTime":15632.0,"Objects":[{"StartTime":15632.0,"EndTime":15632.0,"X":282.0,"Y":102.0}]},{"StartTime":15843.0,"Objects":[{"StartTime":15843.0,"EndTime":15843.0,"X":436.0,"Y":148.0}]},{"StartTime":16055.0,"Objects":[{"StartTime":16055.0,"EndTime":16055.0,"X":266.521729,"Y":178.521729}]},{"StartTime":16160.0,"Objects":[{"StartTime":16160.0,"EndTime":16160.0,"X":270.260864,"Y":182.260864}]},{"StartTime":16266.0,"Objects":[{"StartTime":16266.0,"EndTime":16266.0,"X":274.0,"Y":186.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":16441.0,"EndTime":16441.0,"X":257.420563,"Y":259.144531,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":16688.0,"Objects":[{"StartTime":16688.0,"EndTime":16688.0,"X":160.0,"Y":202.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":16863.0,"EndTime":16863.0,"X":143.207687,"Y":128.904053,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":17111.0,"Objects":[{"StartTime":17111.0,"EndTime":17111.0,"X":79.0,"Y":35.0}]},{"StartTime":17322.0,"Objects":[{"StartTime":17322.0,"EndTime":17322.0,"X":23.0,"Y":123.0}]},{"StartTime":17533.0,"Objects":[{"StartTime":17533.0,"EndTime":17533.0,"X":161.0,"Y":42.0}]},{"StartTime":17745.0,"Objects":[{"StartTime":17745.0,"EndTime":17745.0,"X":76.0,"Y":188.0}]},{"StartTime":17956.0,"Objects":[{"StartTime":17956.0,"EndTime":17956.0,"X":79.0,"Y":35.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":18131.0,"EndTime":18131.0,"X":99.60409,"Y":107.114296,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":18378.0,"Objects":[{"StartTime":18378.0,"EndTime":18378.0,"X":211.0,"Y":104.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":18553.0,"EndTime":18553.0,"X":231.60408,"Y":176.114288,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":18801.0,"Objects":[{"StartTime":18801.0,"EndTime":18801.0,"X":344.0,"Y":170.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":18976.0,"EndTime":18976.0,"X":364.6041,"Y":242.114288,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":19224.0,"Objects":[{"StartTime":19224.0,"EndTime":19224.0,"X":433.0,"Y":132.0}]},{"StartTime":19435.0,"Objects":[{"StartTime":19435.0,"EndTime":19435.0,"X":364.521729,"Y":241.521729}]},{"StartTime":19540.0,"Objects":[{"StartTime":19540.0,"EndTime":19540.0,"X":368.260864,"Y":245.260864}]},{"StartTime":19646.0,"Objects":[{"StartTime":19646.0,"EndTime":19646.0,"X":372.0,"Y":249.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":19821.0,"EndTime":19821.0,"X":444.6992,"Y":253.148651,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":20069.0,"Objects":[{"StartTime":20069.0,"EndTime":20069.0,"X":468.0,"Y":104.0}]},{"StartTime":20280.0,"Objects":[{"StartTime":20280.0,"EndTime":20280.0,"X":413.0,"Y":180.0}]},{"StartTime":20491.0,"Objects":[{"StartTime":20491.0,"EndTime":20491.0,"X":324.0,"Y":58.0}]},{"StartTime":20702.0,"Objects":[{"StartTime":20702.0,"EndTime":20702.0,"X":414.0,"Y":31.0}]},{"StartTime":20914.0,"Objects":[{"StartTime":20914.0,"EndTime":20914.0,"X":324.0,"Y":151.0}]},{"StartTime":21125.0,"Objects":[{"StartTime":21125.0,"EndTime":21125.0,"X":244.0,"Y":40.0}]},{"StartTime":21336.0,"Objects":[{"StartTime":21336.0,"EndTime":21336.0,"X":301.0,"Y":186.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":21616.0,"EndTime":21616.0,"X":197.183792,"Y":187.195663,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":21759.0,"Objects":[{"StartTime":21759.0,"EndTime":21759.0,"X":197.0,"Y":187.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":21934.0,"EndTime":21934.0,"X":197.444717,"Y":260.028961,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":22181.0,"Objects":[{"StartTime":22181.0,"EndTime":22181.0,"X":287.0,"Y":362.0}]},{"StartTime":22393.0,"Objects":[{"StartTime":22393.0,"EndTime":22393.0,"X":330.0,"Y":234.0}]},{"StartTime":22604.0,"Objects":[{"StartTime":22604.0,"EndTime":22604.0,"X":197.0,"Y":260.0}]},{"StartTime":22815.0,"Objects":[{"StartTime":22815.0,"EndTime":22815.0,"X":356.260864,"Y":315.260864}]},{"StartTime":23026.0,"Objects":[{"StartTime":23026.0,"EndTime":23026.0,"X":360.0,"Y":319.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":23306.0,"EndTime":23306.0,"X":465.503235,"Y":323.503082,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":23449.0,"Objects":[{"StartTime":23449.0,"EndTime":23449.0,"X":468.739136,"Y":326.739136}]},{"StartTime":23660.0,"Objects":[{"StartTime":23660.0,"EndTime":23660.0,"X":398.260864,"Y":176.260864}]},{"StartTime":23871.0,"Objects":[{"StartTime":23871.0,"EndTime":23871.0,"X":402.0,"Y":180.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":24046.0,"EndTime":24046.0,"X":415.0339,"Y":253.858765,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":24294.0,"Objects":[{"StartTime":24294.0,"EndTime":24294.0,"X":314.0,"Y":145.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":24469.0,"EndTime":24469.0,"X":326.976959,"Y":71.13121,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":24716.0,"Objects":[{"StartTime":24716.0,"EndTime":24716.0,"X":472.0,"Y":72.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":24891.0,"EndTime":24891.0,"X":485.1493,"Y":145.838318,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":25139.0,"Objects":[{"StartTime":25139.0,"EndTime":25139.0,"X":320.0,"Y":222.0}]},{"StartTime":25350.0,"Objects":[{"StartTime":25350.0,"EndTime":25350.0,"X":235.0,"Y":116.0}]},{"StartTime":25562.0,"Objects":[{"StartTime":25562.0,"EndTime":25562.0,"X":276.0,"Y":295.0}]},{"StartTime":25667.0,"Objects":[{"StartTime":25667.0,"EndTime":25667.0,"X":304.0,"Y":305.0}]},{"StartTime":25773.0,"Objects":[{"StartTime":25773.0,"EndTime":25773.0,"X":333.0,"Y":306.0}]},{"StartTime":25878.0,"Objects":[{"StartTime":25878.0,"EndTime":25878.0,"X":362.0,"Y":299.0}]},{"StartTime":25984.0,"Objects":[{"StartTime":25984.0,"EndTime":25984.0,"X":392.0,"Y":280.0}]},{"StartTime":26090.0,"Objects":[{"StartTime":26090.0,"EndTime":26090.0,"X":425.0,"Y":239.0}]},{"StartTime":26195.0,"Objects":[{"StartTime":26195.0,"EndTime":26195.0,"X":447.0,"Y":193.0}]},{"StartTime":26301.0,"Objects":[{"StartTime":26301.0,"EndTime":26301.0,"X":454.0,"Y":143.0}]},{"StartTime":26407.0,"Objects":[{"StartTime":26407.0,"EndTime":26407.0,"X":452.0,"Y":88.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":26829.0,"EndTime":26829.0,"X":419.177216,"Y":32.9294777,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":27216.0,"EndTime":27216.0,"X":378.111816,"Y":82.11954,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":27463.0,"Objects":[{"StartTime":27463.0,"EndTime":27463.0,"X":368.0,"Y":160.0}]},{"StartTime":27674.0,"Objects":[{"StartTime":27674.0,"EndTime":27674.0,"X":487.0,"Y":58.0}]},{"StartTime":28097.0,"Objects":[{"StartTime":28097.0,"EndTime":28097.0,"X":300.0,"Y":200.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":28272.0,"EndTime":28272.0,"X":296.528,"Y":128.962769,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":28519.0,"Objects":[{"StartTime":28519.0,"EndTime":28519.0,"X":377.0,"Y":238.0}]},{"StartTime":28731.0,"Objects":[{"StartTime":28731.0,"EndTime":28731.0,"X":222.0,"Y":217.0}]},{"StartTime":28942.0,"Objects":[{"StartTime":28942.0,"EndTime":28942.0,"X":369.0,"Y":92.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":29117.0,"EndTime":29117.0,"X":365.6939,"Y":163.550735,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":29364.0,"Objects":[{"StartTime":29364.0,"EndTime":29364.0,"X":223.0,"Y":136.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":29539.0,"EndTime":29539.0,"X":224.683,"Y":64.56601,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":29787.0,"Objects":[{"StartTime":29787.0,"EndTime":29787.0,"X":251.0,"Y":276.0}]},{"StartTime":29998.0,"Objects":[{"StartTime":29998.0,"EndTime":29998.0,"X":135.0,"Y":240.0}]},{"StartTime":30209.0,"Objects":[{"StartTime":30209.0,"EndTime":30209.0,"X":244.0,"Y":356.0}]},{"StartTime":30421.0,"Objects":[{"StartTime":30421.0,"EndTime":30421.0,"X":137.0,"Y":161.0}]},{"StartTime":30632.0,"Objects":[{"StartTime":30632.0,"EndTime":30632.0,"X":166.0,"Y":327.0}]},{"StartTime":30843.0,"Objects":[{"StartTime":30843.0,"EndTime":30843.0,"X":219.0,"Y":187.0}]},{"StartTime":31055.0,"Objects":[{"StartTime":31055.0,"EndTime":31055.0,"X":68.0,"Y":322.0}]},{"StartTime":31266.0,"Objects":[{"StartTime":31266.0,"EndTime":31266.0,"X":311.0,"Y":192.0}]},{"StartTime":31477.0,"Objects":[{"StartTime":31477.0,"EndTime":31477.0,"X":140.0,"Y":89.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":31652.0,"EndTime":31652.0,"X":136.569946,"Y":160.058075,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":31899.0,"Objects":[{"StartTime":31899.0,"EndTime":31899.0,"X":217.0,"Y":51.0}]},{"StartTime":32111.0,"Objects":[{"StartTime":32111.0,"EndTime":32111.0,"X":62.0,"Y":72.0}]},{"StartTime":32322.0,"Objects":[{"StartTime":32322.0,"EndTime":32322.0,"X":209.0,"Y":197.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":32497.0,"EndTime":32497.0,"X":206.163559,"Y":125.298256,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":32744.0,"Objects":[{"StartTime":32744.0,"EndTime":32744.0,"X":64.0,"Y":168.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":32919.0,"EndTime":32919.0,"X":66.155014,"Y":239.272888,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":33167.0,"Objects":[{"StartTime":33167.0,"EndTime":33167.0,"X":209.0,"Y":197.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":33342.0,"EndTime":33342.0,"X":137.56601,"Y":198.683,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":33589.0,"Objects":[{"StartTime":33589.0,"EndTime":33589.0,"X":136.0,"Y":340.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":33764.0,"EndTime":33764.0,"X":207.453568,"Y":342.3376,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":34012.0,"Objects":[{"StartTime":34012.0,"EndTime":34012.0,"X":285.0,"Y":167.0}]},{"StartTime":34224.0,"Objects":[{"StartTime":34224.0,"EndTime":34224.0,"X":308.0,"Y":326.0}]},{"StartTime":34435.0,"Objects":[{"StartTime":34435.0,"EndTime":34435.0,"X":176.0,"Y":276.0}]},{"StartTime":34646.0,"Objects":[{"StartTime":34646.0,"EndTime":34646.0,"X":362.0,"Y":263.0}]},{"StartTime":34857.0,"Objects":[{"StartTime":34857.0,"EndTime":34857.0,"X":184.0,"Y":201.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":35032.0,"EndTime":35032.0,"X":175.4032,"Y":275.505676,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":35280.0,"Objects":[{"StartTime":35280.0,"EndTime":35280.0,"X":118.0,"Y":138.0}]},{"StartTime":35491.0,"Objects":[{"StartTime":35491.0,"EndTime":35491.0,"X":272.0,"Y":162.0}]},{"StartTime":35702.0,"Objects":[{"StartTime":35702.0,"EndTime":35702.0,"X":120.0,"Y":57.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":35877.0,"EndTime":35877.0,"X":164.450928,"Y":3.121443,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":36125.0,"Objects":[{"StartTime":36125.0,"EndTime":36125.0,"X":294.0,"Y":133.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":36300.0,"EndTime":36300.0,"X":247.996475,"Y":185.8328,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":36547.0,"Objects":[{"StartTime":36547.0,"EndTime":36547.0,"X":243.0,"Y":11.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":36722.0,"EndTime":36722.0,"X":296.045258,"Y":56.4152451,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":36970.0,"Objects":[{"StartTime":36970.0,"EndTime":36970.0,"X":171.0,"Y":183.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":37145.0,"EndTime":37145.0,"X":117.339569,"Y":137.949753,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":37393.0,"Objects":[{"StartTime":37393.0,"EndTime":37393.0,"X":368.0,"Y":94.0}]},{"StartTime":37604.0,"Objects":[{"StartTime":37604.0,"EndTime":37604.0,"X":228.0,"Y":243.0}]},{"StartTime":37815.0,"Objects":[{"StartTime":37815.0,"EndTime":37815.0,"X":222.0,"Y":94.0}]},{"StartTime":38026.0,"Objects":[{"StartTime":38026.0,"EndTime":38026.0,"X":374.0,"Y":238.0}]},{"StartTime":38238.0,"Objects":[{"StartTime":38238.0,"EndTime":38238.0,"X":368.0,"Y":94.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":38413.0,"EndTime":38413.0,"X":441.399017,"Y":109.413795,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":38660.0,"Objects":[{"StartTime":38660.0,"EndTime":38660.0,"X":240.0,"Y":170.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":38835.0,"EndTime":38835.0,"X":313.399017,"Y":185.413788,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":39083.0,"Objects":[{"StartTime":39083.0,"EndTime":39083.0,"X":110.0,"Y":240.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":39258.0,"EndTime":39258.0,"X":183.399017,"Y":255.413788,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":39505.0,"Objects":[{"StartTime":39505.0,"EndTime":39505.0,"X":106.0,"Y":321.0}]},{"StartTime":39716.0,"Objects":[{"StartTime":39716.0,"EndTime":39716.0,"X":148.0,"Y":159.0}]},{"StartTime":39928.0,"Objects":[{"StartTime":39928.0,"EndTime":39928.0,"X":35.0,"Y":279.0}]},{"StartTime":40139.0,"Objects":[{"StartTime":40139.0,"EndTime":40139.0,"X":213.0,"Y":325.0}]},{"StartTime":40350.0,"Objects":[{"StartTime":40350.0,"EndTime":40350.0,"X":61.0,"Y":312.0}]},{"StartTime":40561.0,"Objects":[{"StartTime":40561.0,"EndTime":40561.0,"X":237.0,"Y":299.0}]},{"StartTime":40773.0,"Objects":[{"StartTime":40773.0,"EndTime":40773.0,"X":120.0,"Y":92.0}]},{"StartTime":40878.0,"Objects":[{"StartTime":40878.0,"EndTime":40878.0,"X":124.0,"Y":129.0}]},{"StartTime":40984.0,"Objects":[{"StartTime":40984.0,"EndTime":40984.0,"X":128.0,"Y":166.0}]},{"StartTime":41089.0,"Objects":[{"StartTime":41089.0,"EndTime":41089.0,"X":132.0,"Y":203.0}]},{"StartTime":41195.0,"Objects":[{"StartTime":41195.0,"EndTime":41195.0,"X":136.0,"Y":241.0}]},{"StartTime":41407.0,"Objects":[{"StartTime":41407.0,"EndTime":41407.0,"X":273.521729,"Y":106.521736}]},{"StartTime":41512.0,"Objects":[{"StartTime":41512.0,"EndTime":41512.0,"X":277.260864,"Y":110.260864}]},{"StartTime":41618.0,"Objects":[{"StartTime":41618.0,"EndTime":41618.0,"X":281.0,"Y":114.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":41793.0,"EndTime":41793.0,"X":355.8014,"Y":108.545731,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":42040.0,"Objects":[{"StartTime":42040.0,"EndTime":42040.0,"X":292.0,"Y":34.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":42215.0,"EndTime":42215.0,"X":366.8014,"Y":28.54573,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":42463.0,"Objects":[{"StartTime":42463.0,"EndTime":42463.0,"X":400.0,"Y":177.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":42638.0,"EndTime":42638.0,"X":405.454285,"Y":251.8014,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":42885.0,"Objects":[{"StartTime":42885.0,"EndTime":42885.0,"X":480.0,"Y":188.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":43060.0,"EndTime":43060.0,"X":485.454285,"Y":262.8014,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":43308.0,"Objects":[{"StartTime":43308.0,"EndTime":43308.0,"X":330.0,"Y":317.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":43483.0,"EndTime":43483.0,"X":255.1986,"Y":311.545715,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":43730.0,"Objects":[{"StartTime":43730.0,"EndTime":43730.0,"X":319.0,"Y":237.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":43905.0,"EndTime":43905.0,"X":244.1986,"Y":231.545731,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":44153.0,"Objects":[{"StartTime":44153.0,"EndTime":44153.0,"X":129.0,"Y":357.0}]},{"StartTime":44364.0,"Objects":[{"StartTime":44364.0,"EndTime":44364.0,"X":43.0,"Y":239.0}]},{"StartTime":44576.0,"Objects":[{"StartTime":44576.0,"EndTime":44576.0,"X":181.0,"Y":284.0}]},{"StartTime":44787.0,"Objects":[{"StartTime":44787.0,"EndTime":44787.0,"X":43.0,"Y":329.0}]},{"StartTime":44998.0,"Objects":[{"StartTime":44998.0,"EndTime":44998.0,"X":129.0,"Y":211.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":45173.0,"EndTime":45173.0,"X":134.815765,"Y":136.22583,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":45421.0,"Objects":[{"StartTime":45421.0,"EndTime":45421.0,"X":224.0,"Y":157.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":45596.0,"EndTime":45596.0,"X":218.184235,"Y":82.22582,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":45843.0,"Objects":[{"StartTime":45843.0,"EndTime":45843.0,"X":312.0,"Y":60.0}]},{"StartTime":46055.0,"Objects":[{"StartTime":46055.0,"EndTime":46055.0,"X":414.0,"Y":106.0}]},{"StartTime":46266.0,"Objects":[{"StartTime":46266.0,"EndTime":46266.0,"X":401.0,"Y":1.0}]},{"StartTime":46477.0,"Objects":[{"StartTime":46477.0,"EndTime":46477.0,"X":302.521729,"Y":134.521729}]},{"StartTime":46583.0,"Objects":[{"StartTime":46583.0,"EndTime":46583.0,"X":306.260864,"Y":138.260864}]},{"StartTime":46688.0,"Objects":[{"StartTime":46688.0,"EndTime":46688.0,"X":310.0,"Y":142.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":46863.0,"EndTime":46863.0,"X":315.815765,"Y":216.77417,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":47111.0,"Objects":[{"StartTime":47111.0,"EndTime":47111.0,"X":405.0,"Y":196.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":47286.0,"EndTime":47286.0,"X":399.184235,"Y":270.77417,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":47533.0,"Objects":[{"StartTime":47533.0,"EndTime":47533.0,"X":280.0,"Y":288.0}]},{"StartTime":47745.0,"Objects":[{"StartTime":47745.0,"EndTime":47745.0,"X":388.0,"Y":352.0}]},{"StartTime":47956.0,"Objects":[{"StartTime":47956.0,"EndTime":47956.0,"X":492.0,"Y":176.0}]},{"StartTime":48167.0,"Objects":[{"StartTime":48167.0,"EndTime":48167.0,"X":465.0,"Y":312.0}]},{"StartTime":48378.0,"Objects":[{"StartTime":48378.0,"EndTime":48378.0,"X":315.0,"Y":216.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":48553.0,"EndTime":48553.0,"X":243.195923,"Y":215.908646,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":48801.0,"Objects":[{"StartTime":48801.0,"EndTime":48801.0,"X":280.0,"Y":288.0}]},{"StartTime":49012.0,"Objects":[{"StartTime":49012.0,"EndTime":49012.0,"X":392.0,"Y":188.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":49187.0,"EndTime":49187.0,"X":341.5537,"Y":136.966446,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":49435.0,"Objects":[{"StartTime":49435.0,"EndTime":49435.0,"X":472.0,"Y":212.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":49610.0,"EndTime":49610.0,"X":458.927246,"Y":141.03653,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":49857.0,"Objects":[{"StartTime":49857.0,"EndTime":49857.0,"X":399.0,"Y":270.0}]},{"StartTime":50069.0,"Objects":[{"StartTime":50069.0,"EndTime":50069.0,"X":341.0,"Y":136.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":50244.0,"EndTime":50244.0,"X":352.818542,"Y":61.9370422,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":50491.0,"Objects":[{"StartTime":50491.0,"EndTime":50491.0,"X":430.0,"Y":31.0}]},{"StartTime":50702.0,"Objects":[{"StartTime":50702.0,"EndTime":50702.0,"X":274.0,"Y":83.0}]},{"StartTime":50914.0,"Objects":[{"StartTime":50914.0,"EndTime":50914.0,"X":423.0,"Y":111.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":51089.0,"EndTime":51089.0,"X":497.184875,"Y":122.027481,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":51336.0,"Objects":[{"StartTime":51336.0,"EndTime":51336.0,"X":338.0,"Y":215.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":51511.0,"EndTime":51511.0,"X":407.975128,"Y":188.0096,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":51759.0,"Objects":[{"StartTime":51759.0,"EndTime":51759.0,"X":282.0,"Y":268.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":51934.0,"EndTime":51934.0,"X":262.7776,"Y":198.471313,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":52181.0,"Objects":[{"StartTime":52181.0,"EndTime":52181.0,"X":358.0,"Y":289.0}]},{"StartTime":52393.0,"Objects":[{"StartTime":52393.0,"EndTime":52393.0,"X":184.0,"Y":202.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":52568.0,"EndTime":52568.0,"X":218.515137,"Y":138.736755,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":52815.0,"Objects":[{"StartTime":52815.0,"EndTime":52815.0,"X":190.0,"Y":281.0}]},{"StartTime":53026.0,"Objects":[{"StartTime":53026.0,"EndTime":53026.0,"X":119.0,"Y":158.0}]},{"StartTime":53238.0,"Objects":[{"StartTime":53238.0,"EndTime":53238.0,"X":262.0,"Y":200.0}]},{"StartTime":53449.0,"Objects":[{"StartTime":53449.0,"EndTime":53449.0,"X":99.0,"Y":230.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":53624.0,"EndTime":53624.0,"X":118.7338,"Y":157.642715,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":53871.0,"Objects":[{"StartTime":53871.0,"EndTime":53871.0,"X":31.0,"Y":295.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":54046.0,"EndTime":54046.0,"X":11.2661953,"Y":222.642715,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":54294.0,"Objects":[{"StartTime":54294.0,"EndTime":54294.0,"X":131.0,"Y":316.0}]},{"StartTime":54505.0,"Objects":[{"StartTime":54505.0,"EndTime":54505.0,"X":222.0,"Y":242.0}]},{"StartTime":54716.0,"Objects":[{"StartTime":54716.0,"EndTime":54716.0,"X":110.521736,"Y":149.521729}]},{"StartTime":54822.0,"Objects":[{"StartTime":54822.0,"EndTime":54822.0,"X":114.260864,"Y":153.260864}]},{"StartTime":54928.0,"Objects":[{"StartTime":54928.0,"EndTime":54928.0,"X":118.0,"Y":157.0}]},{"StartTime":55139.0,"Objects":[{"StartTime":55139.0,"EndTime":55139.0,"X":226.0,"Y":332.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":55419.0,"EndTime":55419.0,"X":332.02774,"Y":333.580322,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":55562.0,"Objects":[{"StartTime":55562.0,"EndTime":55562.0,"X":332.0,"Y":333.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":55737.0,"EndTime":55737.0,"X":347.450775,"Y":259.608765,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":55984.0,"Objects":[{"StartTime":55984.0,"EndTime":55984.0,"X":289.0,"Y":191.0}]},{"StartTime":56195.0,"Objects":[{"StartTime":56195.0,"EndTime":56195.0,"X":338.0,"Y":116.0}]},{"StartTime":56407.0,"Objects":[{"StartTime":56407.0,"EndTime":56407.0,"X":427.0,"Y":103.0}]},{"StartTime":56618.0,"Objects":[{"StartTime":56618.0,"EndTime":56618.0,"X":502.0,"Y":151.0}]},{"StartTime":56829.0,"Objects":[{"StartTime":56829.0,"EndTime":56829.0,"X":371.0,"Y":38.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":57109.0,"EndTime":57109.0,"X":264.9723,"Y":36.41969,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":57252.0,"Objects":[{"StartTime":57252.0,"EndTime":57252.0,"X":265.0,"Y":37.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":57427.0,"EndTime":57427.0,"X":249.54921,"Y":110.391235,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":57674.0,"Objects":[{"StartTime":57674.0,"EndTime":57674.0,"X":132.0,"Y":25.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":57990.0,"EndTime":57990.0,"X":155.7147,"Y":134.790283,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":58271.0,"EndTime":58271.0,"X":132.0,"Y":25.0,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":58519.0,"Objects":[{"StartTime":58519.0,"EndTime":58519.0,"X":79.0,"Y":150.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":58799.0,"EndTime":58799.0,"X":158.959457,"Y":212.030838,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":58942.0,"Objects":[{"StartTime":58942.0,"EndTime":58942.0,"X":158.0,"Y":212.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":59117.0,"EndTime":59117.0,"X":231.232117,"Y":195.811844,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":59364.0,"Objects":[{"StartTime":59364.0,"EndTime":59364.0,"X":249.0,"Y":110.0}]},{"StartTime":59575.0,"Objects":[{"StartTime":59575.0,"EndTime":59575.0,"X":324.0,"Y":159.0}]},{"StartTime":59787.0,"Objects":[{"StartTime":59787.0,"EndTime":59787.0,"X":337.0,"Y":248.0}]},{"StartTime":59998.0,"Objects":[{"StartTime":59998.0,"EndTime":59998.0,"X":289.0,"Y":323.0}]},{"StartTime":60209.0,"Objects":[{"StartTime":60209.0,"EndTime":60209.0,"X":406.0,"Y":192.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":60489.0,"EndTime":60489.0,"X":468.030823,"Y":271.959473,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":60632.0,"Objects":[{"StartTime":60632.0,"EndTime":60632.0,"X":469.0,"Y":272.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":60807.0,"EndTime":60807.0,"X":451.908661,"Y":345.0266,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":61055.0,"Objects":[{"StartTime":61055.0,"EndTime":61055.0,"X":337.0,"Y":248.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":61371.0,"EndTime":61371.0,"X":359.946777,"Y":357.953369,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":61652.0,"EndTime":61652.0,"X":337.0,"Y":248.0,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":61900.0,"Objects":[{"StartTime":61900.0,"EndTime":61900.0,"X":232.0,"Y":195.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":62075.0,"EndTime":62075.0,"X":214.908661,"Y":268.0266,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":62322.0,"Objects":[{"StartTime":62322.0,"EndTime":62322.0,"X":129.0,"Y":122.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":62497.0,"EndTime":62497.0,"X":145.792313,"Y":195.095947,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":62745.0,"Objects":[{"StartTime":62745.0,"EndTime":62745.0,"X":177.0,"Y":358.0}]},{"StartTime":62956.0,"Objects":[{"StartTime":62956.0,"EndTime":62956.0,"X":108.0,"Y":282.0}]},{"StartTime":63167.0,"Objects":[{"StartTime":63167.0,"EndTime":63167.0,"X":286.0,"Y":341.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":63342.0,"EndTime":63342.0,"X":359.260956,"Y":357.0572,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":63590.0,"Objects":[{"StartTime":63590.0,"EndTime":63590.0,"X":410.0,"Y":231.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":63765.0,"EndTime":63765.0,"X":336.693939,"Y":246.84996,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":64012.0,"Objects":[{"StartTime":64012.0,"EndTime":64012.0,"X":465.0,"Y":158.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":64187.0,"EndTime":64187.0,"X":391.904053,"Y":141.207687,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":64435.0,"Objects":[{"StartTime":64435.0,"EndTime":64435.0,"X":226.0,"Y":111.0}]},{"StartTime":64646.0,"Objects":[{"StartTime":64646.0,"EndTime":64646.0,"X":320.0,"Y":175.0}]},{"StartTime":64857.0,"Objects":[{"StartTime":64857.0,"EndTime":64857.0,"X":222.0,"Y":34.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":65032.0,"EndTime":65032.0,"X":162.249863,"Y":68.4071,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":65280.0,"Objects":[{"StartTime":65280.0,"EndTime":65280.0,"X":218.0,"Y":189.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":65455.0,"EndTime":65455.0,"X":158.249863,"Y":154.592911,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":65702.0,"Objects":[{"StartTime":65702.0,"EndTime":65702.0,"X":296.0,"Y":70.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":65877.0,"EndTime":65877.0,"X":276.006042,"Y":142.285828,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":66125.0,"Objects":[{"StartTime":66125.0,"EndTime":66125.0,"X":236.0,"Y":337.0}]},{"StartTime":66336.0,"Objects":[{"StartTime":66336.0,"EndTime":66336.0,"X":325.0,"Y":219.0}]},{"StartTime":66547.0,"Objects":[{"StartTime":66547.0,"EndTime":66547.0,"X":152.0,"Y":247.0}]},{"StartTime":66758.0,"Objects":[{"StartTime":66758.0,"EndTime":66758.0,"X":316.0,"Y":312.0}]},{"StartTime":66970.0,"Objects":[{"StartTime":66970.0,"EndTime":66970.0,"X":88.0,"Y":184.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":67145.0,"EndTime":67145.0,"X":28.2498646,"Y":218.4071,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":67392.0,"Objects":[{"StartTime":67392.0,"EndTime":67392.0,"X":172.0,"Y":320.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":67567.0,"EndTime":67567.0,"X":152.006042,"Y":247.714172,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":67815.0,"Objects":[{"StartTime":67815.0,"EndTime":67815.0,"X":194.0,"Y":118.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":67990.0,"EndTime":67990.0,"X":127.445862,"Y":99.08952,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":68238.0,"Objects":[{"StartTime":68238.0,"EndTime":68238.0,"X":297.0,"Y":315.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":68413.0,"EndTime":68413.0,"X":277.006042,"Y":242.714172,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":68660.0,"Objects":[{"StartTime":68660.0,"EndTime":68660.0,"X":300.0,"Y":75.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":68835.0,"EndTime":68835.0,"X":277.048523,"Y":162.0243,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":68977.0,"Objects":[{"StartTime":68977.0,"EndTime":68977.0,"X":337.0,"Y":56.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":69152.0,"EndTime":69152.0,"X":314.048523,"Y":143.0243,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":69294.0,"Objects":[{"StartTime":69294.0,"EndTime":69294.0,"X":374.0,"Y":43.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":69363.0,"EndTime":69363.0,"X":353.9267,"Y":115.263847,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":69505.0,"Objects":[{"StartTime":69505.0,"EndTime":69505.0,"X":385.0,"Y":192.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":69680.0,"EndTime":69680.0,"X":470.1033,"Y":203.038986,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":69822.0,"Objects":[{"StartTime":69822.0,"EndTime":69822.0,"X":360.0,"Y":235.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":69997.0,"EndTime":69997.0,"X":444.7288,"Y":245.275024,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":70139.0,"Objects":[{"StartTime":70139.0,"EndTime":70139.0,"X":341.0,"Y":274.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":70208.0,"EndTime":70208.0,"X":412.045074,"Y":278.015778,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":70350.0,"Objects":[{"StartTime":70350.0,"EndTime":70350.0,"X":245.0,"Y":332.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":70525.0,"EndTime":70525.0,"X":238.370941,"Y":249.928986,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":70667.0,"Objects":[{"StartTime":70667.0,"EndTime":70667.0,"X":185.0,"Y":311.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":70842.0,"EndTime":70842.0,"X":238.16449,"Y":248.16507,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":70984.0,"Objects":[{"StartTime":70984.0,"EndTime":70984.0,"X":169.0,"Y":248.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":71053.0,"EndTime":71053.0,"X":237.883636,"Y":247.620834,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":71195.0,"Objects":[{"StartTime":71195.0,"EndTime":71195.0,"X":78.0,"Y":207.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":71370.0,"EndTime":71370.0,"X":63.43404,"Y":122.660629,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":71512.0,"Objects":[{"StartTime":71512.0,"EndTime":71512.0,"X":108.0,"Y":176.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":71687.0,"EndTime":71687.0,"X":93.43404,"Y":91.66063,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":71829.0,"Objects":[{"StartTime":71829.0,"EndTime":71829.0,"X":143.0,"Y":143.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":71898.0,"EndTime":71898.0,"X":131.188721,"Y":73.56615,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":72040.0,"Objects":[{"StartTime":72040.0,"EndTime":72040.0,"X":307.0,"Y":58.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":72215.0,"EndTime":72215.0,"X":225.182,"Y":43.19644,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":72357.0,"Objects":[{"StartTime":72357.0,"EndTime":72357.0,"X":388.0,"Y":72.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":72532.0,"EndTime":72532.0,"X":306.182,"Y":57.1964378,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":72674.0,"Objects":[{"StartTime":72674.0,"EndTime":72674.0,"X":454.0,"Y":91.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":72743.0,"EndTime":72743.0,"X":387.1621,"Y":71.76814,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":72885.0,"Objects":[{"StartTime":72885.0,"EndTime":72885.0,"X":338.0,"Y":180.0}]},{"StartTime":73097.0,"Objects":[{"StartTime":73097.0,"EndTime":73097.0,"X":269.0,"Y":308.0}]},{"StartTime":73202.0,"Objects":[{"StartTime":73202.0,"EndTime":73202.0,"X":304.0,"Y":334.0}]},{"StartTime":73308.0,"Objects":[{"StartTime":73308.0,"EndTime":73308.0,"X":348.0,"Y":344.0}]},{"StartTime":73414.0,"Objects":[{"StartTime":73414.0,"EndTime":73414.0,"X":391.0,"Y":335.0}]},{"StartTime":73519.0,"Objects":[{"StartTime":73519.0,"EndTime":73519.0,"X":428.0,"Y":309.0}]},{"StartTime":73625.0,"Objects":[{"StartTime":73625.0,"EndTime":73625.0,"X":450.0,"Y":271.0}]},{"StartTime":73730.0,"Objects":[{"StartTime":73730.0,"EndTime":73730.0,"X":453.0,"Y":227.0}]},{"StartTime":74576.0,"Objects":[{"StartTime":74576.0,"EndTime":74576.0,"X":453.0,"Y":227.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":74611.0,"EndTime":74611.0,"X":475.4206,"Y":227.605957,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":74646.0,"EndTime":74646.0,"X":453.142365,"Y":227.003845,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":74681.0,"EndTime":74681.0,"X":475.278259,"Y":227.602112,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":74716.0,"EndTime":74716.0,"X":453.2847,"Y":227.00769,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":74752.0,"EndTime":74752.0,"X":475.2071,"Y":227.600189,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":74787.0,"EndTime":74787.0,"X":453.213531,"Y":227.005768,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":74822.0,"EndTime":74822.0,"X":475.349426,"Y":227.604034,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":74857.0,"EndTime":74857.0,"X":453.071167,"Y":227.001923,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":74856.0,"EndTime":74856.0,"X":475.4918,"Y":227.60788,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":74998.0,"Objects":[{"StartTime":74998.0,"EndTime":74998.0,"X":506.0,"Y":152.0}]},{"StartTime":75421.0,"Objects":[{"StartTime":75421.0,"EndTime":75421.0,"X":222.0,"Y":89.0}]},{"StartTime":75632.0,"Objects":[{"StartTime":75632.0,"EndTime":75632.0,"X":194.0,"Y":259.0}]},{"StartTime":75843.0,"Objects":[{"StartTime":75843.0,"EndTime":75843.0,"X":320.0,"Y":218.0}]},{"StartTime":76054.0,"Objects":[{"StartTime":76054.0,"EndTime":76054.0,"X":150.0,"Y":190.0}]},{"StartTime":76266.0,"Objects":[{"StartTime":76266.0,"EndTime":76266.0,"X":339.0,"Y":335.0}]},{"StartTime":76477.0,"Objects":[{"StartTime":76477.0,"EndTime":76477.0,"X":372.0,"Y":130.0}]},{"StartTime":76688.0,"Objects":[{"StartTime":76688.0,"EndTime":76688.0,"X":221.0,"Y":180.0}]},{"StartTime":76899.0,"Objects":[{"StartTime":76899.0,"EndTime":76899.0,"X":425.0,"Y":212.0}]},{"StartTime":77111.0,"Objects":[{"StartTime":77111.0,"EndTime":77111.0,"X":285.0,"Y":121.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":77286.0,"EndTime":77286.0,"X":371.8806,"Y":129.901413,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":77533.0,"Objects":[{"StartTime":77533.0,"EndTime":77533.0,"X":194.0,"Y":259.0}]},{"StartTime":77745.0,"Objects":[{"StartTime":77745.0,"EndTime":77745.0,"X":323.0,"Y":182.0}]},{"StartTime":77956.0,"Objects":[{"StartTime":77956.0,"EndTime":77956.0,"X":244.0,"Y":316.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":78131.0,"EndTime":78131.0,"X":154.157745,"Y":324.1849,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":78378.0,"Objects":[{"StartTime":78378.0,"EndTime":78378.0,"X":245.0,"Y":179.0}]},{"StartTime":78590.0,"Objects":[{"StartTime":78590.0,"EndTime":78590.0,"X":350.0,"Y":277.0}]},{"StartTime":78801.0,"Objects":[{"StartTime":78801.0,"EndTime":78801.0,"X":160.0,"Y":228.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":79081.0,"EndTime":79081.0,"X":163.6551,"Y":81.7956848,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":79224.0,"Objects":[{"StartTime":79224.0,"EndTime":79224.0,"X":194.0,"Y":90.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":79399.0,"EndTime":79399.0,"X":283.264221,"Y":89.8079147,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":79646.0,"Objects":[{"StartTime":79646.0,"EndTime":79646.0,"X":129.0,"Y":0.0}]},{"StartTime":79857.0,"Objects":[{"StartTime":79857.0,"EndTime":79857.0,"X":22.0,"Y":146.0}]},{"StartTime":80069.0,"Objects":[{"StartTime":80069.0,"EndTime":80069.0,"X":194.0,"Y":90.0}]},{"StartTime":80280.0,"Objects":[{"StartTime":80280.0,"EndTime":80280.0,"X":22.0,"Y":33.0}]},{"StartTime":80491.0,"Objects":[{"StartTime":80491.0,"EndTime":80491.0,"X":129.0,"Y":180.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":80666.0,"EndTime":80666.0,"X":219.221863,"Y":178.1168,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":80913.0,"Objects":[{"StartTime":80913.0,"EndTime":80913.0,"X":308.0,"Y":80.0}]},{"StartTime":81125.0,"Objects":[{"StartTime":81125.0,"EndTime":81125.0,"X":280.0,"Y":252.0}]},{"StartTime":81336.0,"Objects":[{"StartTime":81336.0,"EndTime":81336.0,"X":446.0,"Y":206.0}]},{"StartTime":81547.0,"Objects":[{"StartTime":81547.0,"EndTime":81547.0,"X":339.0,"Y":60.0}]},{"StartTime":81759.0,"Objects":[{"StartTime":81759.0,"EndTime":81759.0,"X":511.0,"Y":116.0}]},{"StartTime":81970.0,"Objects":[{"StartTime":81970.0,"EndTime":81970.0,"X":339.0,"Y":173.0}]},{"StartTime":82181.0,"Objects":[{"StartTime":82181.0,"EndTime":82181.0,"X":446.0,"Y":26.0}]},{"StartTime":82393.0,"Objects":[{"StartTime":82393.0,"EndTime":82393.0,"X":280.0,"Y":118.0}]},{"StartTime":82604.0,"Objects":[{"StartTime":82604.0,"EndTime":82604.0,"X":435.0,"Y":118.0}]},{"StartTime":82816.0,"Objects":[{"StartTime":82816.0,"EndTime":82816.0,"X":259.0,"Y":26.0}]},{"StartTime":83026.0,"Objects":[{"StartTime":83026.0,"EndTime":83026.0,"X":339.0,"Y":173.0}]},{"StartTime":83238.0,"Objects":[{"StartTime":83238.0,"EndTime":83238.0,"X":154.0,"Y":128.0}]},{"StartTime":83449.0,"Objects":[{"StartTime":83449.0,"EndTime":83449.0,"X":304.0,"Y":88.0}]},{"StartTime":83661.0,"Objects":[{"StartTime":83661.0,"EndTime":83661.0,"X":157.0,"Y":222.0}]},{"StartTime":83871.0,"Objects":[{"StartTime":83871.0,"EndTime":83871.0,"X":352.0,"Y":280.0}]},{"StartTime":84083.0,"Objects":[{"StartTime":84083.0,"EndTime":84083.0,"X":160.0,"Y":173.0}]},{"StartTime":84294.0,"Objects":[{"StartTime":84294.0,"EndTime":84294.0,"X":339.0,"Y":173.0}]},{"StartTime":84506.0,"Objects":[{"StartTime":84506.0,"EndTime":84506.0,"X":135.0,"Y":280.0}]},{"StartTime":84716.0,"Objects":[{"StartTime":84716.0,"EndTime":84716.0,"X":259.0,"Y":130.0}]},{"StartTime":84928.0,"Objects":[{"StartTime":84928.0,"EndTime":84928.0,"X":65.0,"Y":235.0}]},{"StartTime":85139.0,"Objects":[{"StartTime":85139.0,"EndTime":85139.0,"X":244.0,"Y":235.0}]},{"StartTime":85351.0,"Objects":[{"StartTime":85351.0,"EndTime":85351.0,"X":40.0,"Y":129.0}]},{"StartTime":85562.0,"Objects":[{"StartTime":85562.0,"EndTime":85562.0,"X":300.0,"Y":92.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":85737.0,"EndTime":85737.0,"X":277.179749,"Y":186.7918,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":85984.0,"Objects":[{"StartTime":85984.0,"EndTime":85984.0,"X":192.0,"Y":43.0}]},{"StartTime":86195.0,"Objects":[{"StartTime":86195.0,"EndTime":86195.0,"X":361.0,"Y":34.0}]},{"StartTime":86407.0,"Objects":[{"StartTime":86407.0,"EndTime":86407.0,"X":327.0,"Y":233.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":86582.0,"EndTime":86582.0,"X":232.2082,"Y":210.179749,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":86829.0,"Objects":[{"StartTime":86829.0,"EndTime":86829.0,"X":376.0,"Y":125.0}]},{"StartTime":87040.0,"Objects":[{"StartTime":87040.0,"EndTime":87040.0,"X":385.0,"Y":294.0}]},{"StartTime":87252.0,"Objects":[{"StartTime":87252.0,"EndTime":87252.0,"X":195.0,"Y":265.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":87427.0,"EndTime":87427.0,"X":217.820251,"Y":170.2082,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":87674.0,"Objects":[{"StartTime":87674.0,"EndTime":87674.0,"X":303.0,"Y":314.0}]},{"StartTime":87885.0,"Objects":[{"StartTime":87885.0,"EndTime":87885.0,"X":134.0,"Y":323.0}]},{"StartTime":88097.0,"Objects":[{"StartTime":88097.0,"EndTime":88097.0,"X":177.0,"Y":108.0}]},{"StartTime":88202.0,"Objects":[{"StartTime":88202.0,"EndTime":88202.0,"X":223.0,"Y":95.0}]},{"StartTime":88308.0,"Objects":[{"StartTime":88308.0,"EndTime":88308.0,"X":267.0,"Y":114.0}]},{"StartTime":88413.0,"Objects":[{"StartTime":88413.0,"EndTime":88413.0,"X":291.0,"Y":155.0}]},{"StartTime":88519.0,"Objects":[{"StartTime":88519.0,"EndTime":88519.0,"X":284.0,"Y":203.0}]},{"StartTime":88731.0,"Objects":[{"StartTime":88731.0,"EndTime":88731.0,"X":102.0,"Y":204.0}]},{"StartTime":88942.0,"Objects":[{"StartTime":88942.0,"EndTime":88942.0,"X":224.0,"Y":16.0}]},{"StartTime":89153.0,"Objects":[{"StartTime":89153.0,"EndTime":89153.0,"X":207.0,"Y":200.0}]},{"StartTime":89364.0,"Objects":[{"StartTime":89364.0,"EndTime":89364.0,"X":96.0,"Y":112.0}]},{"StartTime":89575.0,"Objects":[{"StartTime":89575.0,"EndTime":89575.0,"X":113.0,"Y":296.0}]},{"StartTime":89787.0,"Objects":[{"StartTime":89787.0,"EndTime":89787.0,"X":0.0,"Y":152.0}]},{"StartTime":89998.0,"Objects":[{"StartTime":89998.0,"EndTime":89998.0,"X":184.0,"Y":169.0}]},{"StartTime":90209.0,"Objects":[{"StartTime":90209.0,"EndTime":90209.0,"X":16.0,"Y":296.0}]},{"StartTime":90420.0,"Objects":[{"StartTime":90420.0,"EndTime":90420.0,"X":211.0,"Y":242.0}]},{"StartTime":90632.0,"Objects":[{"StartTime":90632.0,"EndTime":90632.0,"X":88.0,"Y":52.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":90807.0,"EndTime":90807.0,"X":78.2983856,"Y":149.016129,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":91055.0,"Objects":[{"StartTime":91055.0,"EndTime":91055.0,"X":231.0,"Y":2.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":91230.0,"EndTime":91230.0,"X":173.4124,"Y":80.6760254,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":91477.0,"Objects":[{"StartTime":91477.0,"EndTime":91477.0,"X":383.0,"Y":22.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":91652.0,"EndTime":91652.0,"X":293.9368,"Y":61.67361,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":91900.0,"Objects":[{"StartTime":91900.0,"EndTime":91900.0,"X":491.0,"Y":110.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":92075.0,"EndTime":92075.0,"X":393.715942,"Y":103.5144,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":92322.0,"Objects":[{"StartTime":92322.0,"EndTime":92322.0,"X":436.0,"Y":284.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":92497.0,"EndTime":92497.0,"X":441.562347,"Y":186.658783,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":92745.0,"Objects":[{"StartTime":92745.0,"EndTime":92745.0,"X":300.260864,"Y":155.260864}]},{"StartTime":92956.0,"Objects":[{"StartTime":92956.0,"EndTime":92956.0,"X":304.0,"Y":159.0}]},{"StartTime":93167.0,"Objects":[{"StartTime":93167.0,"EndTime":93167.0,"X":412.0,"Y":328.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":93342.0,"EndTime":93342.0,"X":417.562347,"Y":230.658783,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":93590.0,"Objects":[{"StartTime":93590.0,"EndTime":93590.0,"X":288.260864,"Y":172.260864}]},{"StartTime":93801.0,"Objects":[{"StartTime":93801.0,"EndTime":93801.0,"X":292.0,"Y":176.0}]},{"StartTime":94012.0,"Objects":[{"StartTime":94012.0,"EndTime":94012.0,"X":392.0,"Y":364.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":94187.0,"EndTime":94187.0,"X":397.562347,"Y":266.658783,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":94435.0,"Objects":[{"StartTime":94435.0,"EndTime":94435.0,"X":276.260864,"Y":192.260864}]},{"StartTime":94646.0,"Objects":[{"StartTime":94646.0,"EndTime":94646.0,"X":280.0,"Y":196.0}]},{"StartTime":94857.0,"Objects":[{"StartTime":94857.0,"EndTime":94857.0,"X":160.0,"Y":155.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":95032.0,"EndTime":95032.0,"X":167.9152,"Y":243.954712,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":95280.0,"Objects":[{"StartTime":95280.0,"EndTime":95280.0,"X":424.0,"Y":112.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":95455.0,"EndTime":95455.0,"X":416.084778,"Y":23.0452919,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":95702.0,"Objects":[{"StartTime":95702.0,"EndTime":95702.0,"X":224.0,"Y":192.0}]},{"StartTime":95913.0,"Objects":[{"StartTime":95913.0,"EndTime":95913.0,"X":421.0,"Y":192.0}]},{"StartTime":96125.0,"Objects":[{"StartTime":96125.0,"EndTime":96125.0,"X":280.0,"Y":56.0}]},{"StartTime":96336.0,"Objects":[{"StartTime":96336.0,"EndTime":96336.0,"X":280.0,"Y":253.0}]},{"StartTime":96547.0,"Objects":[{"StartTime":96547.0,"EndTime":96547.0,"X":431.0,"Y":112.0}]},{"StartTime":96758.0,"Objects":[{"StartTime":96758.0,"EndTime":96758.0,"X":195.0,"Y":112.0}]},{"StartTime":96970.0,"Objects":[{"StartTime":96970.0,"EndTime":96970.0,"X":364.0,"Y":268.0}]},{"StartTime":97181.0,"Objects":[{"StartTime":97181.0,"EndTime":97181.0,"X":364.0,"Y":32.0}]},{"StartTime":97393.0,"Objects":[{"StartTime":97393.0,"EndTime":97393.0,"X":176.0,"Y":264.0}]},{"StartTime":97604.0,"Objects":[{"StartTime":97604.0,"EndTime":97604.0,"X":426.0,"Y":108.0}]},{"StartTime":97815.0,"Objects":[{"StartTime":97815.0,"EndTime":97815.0,"X":200.0,"Y":184.0}]},{"StartTime":98026.0,"Objects":[{"StartTime":98026.0,"EndTime":98026.0,"X":459.0,"Y":264.0}]},{"StartTime":98238.0,"Objects":[{"StartTime":98238.0,"EndTime":98238.0,"X":200.0,"Y":108.0}]},{"StartTime":98449.0,"Objects":[{"StartTime":98449.0,"EndTime":98449.0,"X":426.0,"Y":184.0}]},{"StartTime":98660.0,"Objects":[{"StartTime":98660.0,"EndTime":98660.0,"X":164.0,"Y":32.0}]},{"StartTime":98871.0,"Objects":[{"StartTime":98871.0,"EndTime":98871.0,"X":447.0,"Y":32.0}]},{"StartTime":99083.0,"Objects":[{"StartTime":99083.0,"EndTime":99083.0,"X":312.0,"Y":264.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":99258.0,"EndTime":99258.0,"X":305.2918,"Y":166.731049,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":99505.0,"Objects":[{"StartTime":99505.0,"EndTime":99505.0,"X":412.0,"Y":236.0}]},{"StartTime":99716.0,"Objects":[{"StartTime":99716.0,"EndTime":99716.0,"X":224.0,"Y":224.0}]},{"StartTime":99928.0,"Objects":[{"StartTime":99928.0,"EndTime":99928.0,"X":420.0,"Y":144.0}]},{"StartTime":100139.0,"Objects":[{"StartTime":100139.0,"EndTime":100139.0,"X":408.0,"Y":332.0}]},{"StartTime":100350.0,"Objects":[{"StartTime":100350.0,"EndTime":100350.0,"X":252.0,"Y":136.0}]},{"StartTime":100561.0,"Objects":[{"StartTime":100561.0,"EndTime":100561.0,"X":191.0,"Y":314.0}]},{"StartTime":100773.0,"Objects":[{"StartTime":100773.0,"EndTime":100773.0,"X":412.0,"Y":236.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":100948.0,"EndTime":100948.0,"X":487.0,"Y":236.0,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":101195.0,"Objects":[{"StartTime":101195.0,"EndTime":101195.0,"X":348.0,"Y":288.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":101370.0,"EndTime":101370.0,"X":273.0,"Y":288.0,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":101618.0,"Objects":[{"StartTime":101618.0,"EndTime":101618.0,"X":415.0,"Y":339.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":101898.0,"EndTime":101898.0,"X":411.2817,"Y":235.5634,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":102040.0,"Objects":[{"StartTime":102040.0,"EndTime":102040.0,"X":414.739136,"Y":238.739136}]},{"StartTime":102252.0,"Objects":[{"StartTime":102252.0,"EndTime":102252.0,"X":339.521729,"Y":119.521736}]},{"StartTime":102357.0,"Objects":[{"StartTime":102357.0,"EndTime":102357.0,"X":343.260864,"Y":123.260864}]},{"StartTime":102463.0,"Objects":[{"StartTime":102463.0,"EndTime":102463.0,"X":347.0,"Y":127.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":102638.0,"EndTime":102638.0,"X":432.363373,"Y":134.772491,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":102885.0,"Objects":[{"StartTime":102885.0,"EndTime":102885.0,"X":444.0,"Y":20.0}]},{"StartTime":103097.0,"Objects":[{"StartTime":103097.0,"EndTime":103097.0,"X":280.0,"Y":60.0}]},{"StartTime":103308.0,"Objects":[{"StartTime":103308.0,"EndTime":103308.0,"X":433.0,"Y":135.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":103483.0,"EndTime":103483.0,"X":423.061157,"Y":224.449539,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":103731.0,"Objects":[{"StartTime":103731.0,"EndTime":103731.0,"X":232.0,"Y":120.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":103906.0,"EndTime":103906.0,"X":222.061157,"Y":30.55046,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":104153.0,"Objects":[{"StartTime":104153.0,"EndTime":104153.0,"X":92.0,"Y":254.0}]},{"StartTime":104364.0,"Objects":[{"StartTime":104364.0,"EndTime":104364.0,"X":139.0,"Y":123.0}]},{"StartTime":104575.0,"Objects":[{"StartTime":104575.0,"EndTime":104575.0,"X":0.0,"Y":157.0}]},{"StartTime":104787.0,"Objects":[{"StartTime":104787.0,"EndTime":104787.0,"X":158.0,"Y":201.0}]},{"StartTime":104998.0,"Objects":[{"StartTime":104998.0,"EndTime":104998.0,"X":204.0,"Y":26.0}]},{"StartTime":105209.0,"Objects":[{"StartTime":105209.0,"EndTime":105209.0,"X":34.0,"Y":71.0}]},{"StartTime":105421.0,"Objects":[{"StartTime":105421.0,"EndTime":105421.0,"X":267.0,"Y":106.0}]},{"StartTime":105632.0,"Objects":[{"StartTime":105632.0,"EndTime":105632.0,"X":30.0,"Y":179.0}]},{"StartTime":105843.0,"Objects":[{"StartTime":105843.0,"EndTime":105843.0,"X":163.0,"Y":290.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":106018.0,"EndTime":106018.0,"X":157.2056,"Y":200.186722,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":106266.0,"Objects":[{"StartTime":106266.0,"EndTime":106266.0,"X":273.0,"Y":144.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":106441.0,"EndTime":106441.0,"X":354.2163,"Y":157.9499,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":106688.0,"Objects":[{"StartTime":106688.0,"EndTime":106688.0,"X":512.0,"Y":116.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":106863.0,"EndTime":106863.0,"X":430.2963,"Y":129.688965,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":107111.0,"Objects":[{"StartTime":107111.0,"EndTime":107111.0,"X":384.0,"Y":4.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":107286.0,"EndTime":107286.0,"X":368.694946,"Y":84.79979,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":107533.0,"Objects":[{"StartTime":107533.0,"EndTime":107533.0,"X":396.0,"Y":288.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":107708.0,"EndTime":107708.0,"X":410.385376,"Y":206.609482,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":107956.0,"Objects":[{"StartTime":107956.0,"EndTime":107956.0,"X":408.0,"Y":368.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":108131.0,"EndTime":108131.0,"X":475.4191,"Y":320.030762,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":108378.0,"Objects":[{"StartTime":108378.0,"EndTime":108378.0,"X":332.0,"Y":336.0}]},{"StartTime":108590.0,"Objects":[{"StartTime":108590.0,"EndTime":108590.0,"X":480.0,"Y":244.0}]},{"StartTime":108801.0,"Objects":[{"StartTime":108801.0,"EndTime":108801.0,"X":332.0,"Y":336.0}]},{"StartTime":109013.0,"Objects":[{"StartTime":109013.0,"EndTime":109013.0,"X":372.0,"Y":168.0}]},{"StartTime":109224.0,"Objects":[{"StartTime":109224.0,"EndTime":109224.0,"X":247.0,"Y":313.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":109399.0,"EndTime":109399.0,"X":267.7445,"Y":230.566544,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":109646.0,"Objects":[{"StartTime":109646.0,"EndTime":109646.0,"X":96.0,"Y":136.0}]},{"StartTime":109858.0,"Objects":[{"StartTime":109858.0,"EndTime":109858.0,"X":196.0,"Y":252.0}]},{"StartTime":110069.0,"Objects":[{"StartTime":110069.0,"EndTime":110069.0,"X":260.0,"Y":120.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":110244.0,"EndTime":110244.0,"X":170.550461,"Y":129.938843,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":110491.0,"Objects":[{"StartTime":110491.0,"EndTime":110491.0,"X":28.0,"Y":236.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":110666.0,"EndTime":110666.0,"X":117.449539,"Y":245.938843,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":110914.0,"Objects":[{"StartTime":110914.0,"EndTime":110914.0,"X":86.0,"Y":46.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":111089.0,"EndTime":111089.0,"X":95.05495,"Y":135.543335,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":111337.0,"Objects":[{"StartTime":111337.0,"EndTime":111337.0,"X":186.0,"Y":341.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":111512.0,"EndTime":111512.0,"X":195.938843,"Y":251.550461,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":111759.0,"Objects":[{"StartTime":111759.0,"EndTime":111759.0,"X":216.0,"Y":88.0}]},{"StartTime":111970.0,"Objects":[{"StartTime":111970.0,"EndTime":111970.0,"X":95.0,"Y":135.0}]},{"StartTime":112181.0,"Objects":[{"StartTime":112181.0,"EndTime":112181.0,"X":264.0,"Y":168.0}]},{"StartTime":112393.0,"Objects":[{"StartTime":112393.0,"EndTime":112393.0,"X":191.0,"Y":8.0}]},{"StartTime":112604.0,"Objects":[{"StartTime":112604.0,"EndTime":112604.0,"X":142.0,"Y":221.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":112779.0,"EndTime":112779.0,"X":132.061157,"Y":310.449524,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":113026.0,"Objects":[{"StartTime":113026.0,"EndTime":113026.0,"X":264.0,"Y":168.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":113201.0,"EndTime":113201.0,"X":254.061157,"Y":257.449524,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":113449.0,"Objects":[{"StartTime":113449.0,"EndTime":113449.0,"X":396.0,"Y":112.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":113624.0,"EndTime":113624.0,"X":386.061157,"Y":201.449539,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":113871.0,"Objects":[{"StartTime":113871.0,"EndTime":113871.0,"X":312.0,"Y":104.0}]},{"StartTime":114083.0,"Objects":[{"StartTime":114083.0,"EndTime":114083.0,"X":456.0,"Y":240.0}]},{"StartTime":114294.0,"Objects":[{"StartTime":114294.0,"EndTime":114294.0,"X":442.0,"Y":48.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":114469.0,"EndTime":114469.0,"X":360.0754,"Y":43.94542,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":114716.0,"Objects":[{"StartTime":114716.0,"EndTime":114716.0,"X":303.0,"Y":196.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":114891.0,"EndTime":114891.0,"X":386.2208,"Y":200.863846,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":115139.0,"Objects":[{"StartTime":115139.0,"EndTime":115139.0,"X":208.0,"Y":80.0}]},{"StartTime":115244.0,"Objects":[{"StartTime":115244.0,"EndTime":115244.0,"X":213.0,"Y":124.0}]},{"StartTime":115350.0,"Objects":[{"StartTime":115350.0,"EndTime":115350.0,"X":218.0,"Y":169.0}]},{"StartTime":115455.0,"Objects":[{"StartTime":115455.0,"EndTime":115455.0,"X":224.0,"Y":214.0}]},{"StartTime":115561.0,"Objects":[{"StartTime":115561.0,"EndTime":115561.0,"X":229.0,"Y":258.0}]},{"StartTime":115773.0,"Objects":[{"StartTime":115773.0,"EndTime":115773.0,"X":128.521729,"Y":184.521729}]},{"StartTime":115878.0,"Objects":[{"StartTime":115878.0,"EndTime":115878.0,"X":132.260864,"Y":188.260864}]},{"StartTime":115984.0,"Objects":[{"StartTime":115984.0,"EndTime":115984.0,"X":136.0,"Y":192.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":116159.0,"EndTime":116159.0,"X":61.1985931,"Y":186.545731,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":116407.0,"Objects":[{"StartTime":116407.0,"EndTime":116407.0,"X":60.0,"Y":104.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":116582.0,"EndTime":116582.0,"X":134.853943,"Y":108.678375,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":116829.0,"Objects":[{"StartTime":116829.0,"EndTime":116829.0,"X":202.0,"Y":5.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":117004.0,"EndTime":117004.0,"X":207.454269,"Y":79.80141,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":117251.0,"Objects":[{"StartTime":117251.0,"EndTime":117251.0,"X":288.0,"Y":104.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":117426.0,"EndTime":117426.0,"X":292.988922,"Y":29.1661148,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":117674.0,"Objects":[{"StartTime":117674.0,"EndTime":117674.0,"X":336.0,"Y":184.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":117849.0,"EndTime":117849.0,"X":261.1986,"Y":178.545731,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":118096.0,"Objects":[{"StartTime":118096.0,"EndTime":118096.0,"X":340.0,"Y":264.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":118271.0,"EndTime":118271.0,"X":414.754669,"Y":257.9388,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":118519.0,"Objects":[{"StartTime":118519.0,"EndTime":118519.0,"X":414.0,"Y":112.0}]},{"StartTime":118730.0,"Objects":[{"StartTime":118730.0,"EndTime":118730.0,"X":500.0,"Y":230.0}]},{"StartTime":118942.0,"Objects":[{"StartTime":118942.0,"EndTime":118942.0,"X":362.0,"Y":185.0}]},{"StartTime":119153.0,"Objects":[{"StartTime":119153.0,"EndTime":119153.0,"X":500.0,"Y":140.0}]},{"StartTime":119364.0,"Objects":[{"StartTime":119364.0,"EndTime":119364.0,"X":414.0,"Y":258.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":119539.0,"EndTime":119539.0,"X":339.245331,"Y":264.0612,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":119787.0,"Objects":[{"StartTime":119787.0,"EndTime":119787.0,"X":186.0,"Y":173.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":119962.0,"EndTime":119962.0,"X":260.829376,"Y":178.056046,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":120209.0,"Objects":[{"StartTime":120209.0,"EndTime":120209.0,"X":260.0,"Y":292.0}]},{"StartTime":120421.0,"Objects":[{"StartTime":120421.0,"EndTime":120421.0,"X":169.0,"Y":344.0}]},{"StartTime":120632.0,"Objects":[{"StartTime":120632.0,"EndTime":120632.0,"X":182.0,"Y":239.0}]},{"StartTime":120843.0,"Objects":[{"StartTime":120843.0,"EndTime":120843.0,"X":244.0,"Y":372.0}]},{"StartTime":121054.0,"Objects":[{"StartTime":121054.0,"EndTime":121054.0,"X":104.0,"Y":296.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":121229.0,"EndTime":121229.0,"X":29.2258224,"Y":301.815765,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":121477.0,"Objects":[{"StartTime":121477.0,"EndTime":121477.0,"X":186.0,"Y":173.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":121652.0,"EndTime":121652.0,"X":260.829376,"Y":178.056046,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":121899.0,"Objects":[{"StartTime":121899.0,"EndTime":121899.0,"X":104.0,"Y":208.0}]},{"StartTime":122111.0,"Objects":[{"StartTime":122111.0,"EndTime":122111.0,"X":78.0,"Y":106.0}]},{"StartTime":122322.0,"Objects":[{"StartTime":122322.0,"EndTime":122322.0,"X":104.0,"Y":248.0}]},{"StartTime":122534.0,"Objects":[{"StartTime":122534.0,"EndTime":122534.0,"X":177.0,"Y":144.0}]},{"StartTime":122744.0,"Objects":[{"StartTime":122744.0,"EndTime":122744.0,"X":288.0,"Y":256.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":122919.0,"EndTime":122919.0,"X":216.195923,"Y":256.09137,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":123167.0,"Objects":[{"StartTime":123167.0,"EndTime":123167.0,"X":216.0,"Y":144.0}]},{"StartTime":123378.0,"Objects":[{"StartTime":123378.0,"EndTime":123378.0,"X":367.0,"Y":280.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":123553.0,"EndTime":123553.0,"X":316.5537,"Y":331.033569,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":123801.0,"Objects":[{"StartTime":123801.0,"EndTime":123801.0,"X":450.0,"Y":260.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":123976.0,"EndTime":123976.0,"X":431.362823,"Y":329.464874,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":124223.0,"Objects":[{"StartTime":124223.0,"EndTime":124223.0,"X":277.0,"Y":260.0}]},{"StartTime":124435.0,"Objects":[{"StartTime":124435.0,"EndTime":124435.0,"X":332.0,"Y":128.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":124610.0,"EndTime":124610.0,"X":402.4845,"Y":153.630737,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":124857.0,"Objects":[{"StartTime":124857.0,"EndTime":124857.0,"X":367.0,"Y":280.0}]},{"StartTime":125069.0,"Objects":[{"StartTime":125069.0,"EndTime":125069.0,"X":272.0,"Y":180.0}]},{"StartTime":125280.0,"Objects":[{"StartTime":125280.0,"EndTime":125280.0,"X":470.0,"Y":129.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":125455.0,"EndTime":125455.0,"X":460.233978,"Y":199.678162,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":125702.0,"Objects":[{"StartTime":125702.0,"EndTime":125702.0,"X":356.0,"Y":52.0}]},{"StartTime":125914.0,"Objects":[{"StartTime":125914.0,"EndTime":125914.0,"X":402.0,"Y":153.0}]},{"StartTime":126125.0,"Objects":[{"StartTime":126125.0,"EndTime":126125.0,"X":232.0,"Y":72.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":126300.0,"EndTime":126300.0,"X":212.777573,"Y":141.528687,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":126547.0,"Objects":[{"StartTime":126547.0,"EndTime":126547.0,"X":288.0,"Y":124.0}]},{"StartTime":126759.0,"Objects":[{"StartTime":126759.0,"EndTime":126759.0,"X":134.0,"Y":138.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":126934.0,"EndTime":126934.0,"X":168.515137,"Y":201.263245,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":127181.0,"Objects":[{"StartTime":127181.0,"EndTime":127181.0,"X":335.0,"Y":212.0}]},{"StartTime":127393.0,"Objects":[{"StartTime":127393.0,"EndTime":127393.0,"X":212.0,"Y":141.0}]},{"StartTime":127604.0,"Objects":[{"StartTime":127604.0,"EndTime":127604.0,"X":254.0,"Y":284.0}]},{"StartTime":127815.0,"Objects":[{"StartTime":127815.0,"EndTime":127815.0,"X":286.0,"Y":130.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":127990.0,"EndTime":127990.0,"X":211.678345,"Y":140.064392,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":128237.0,"Objects":[{"StartTime":128237.0,"EndTime":128237.0,"X":384.0,"Y":51.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":128412.0,"EndTime":128412.0,"X":311.6427,"Y":31.2661953,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":128660.0,"Objects":[{"StartTime":128660.0,"EndTime":128660.0,"X":480.0,"Y":108.0}]},{"StartTime":128871.0,"Objects":[{"StartTime":128871.0,"EndTime":128871.0,"X":396.0,"Y":232.0}]},{"StartTime":129082.0,"Objects":[{"StartTime":129082.0,"EndTime":129082.0,"X":233.521729,"Y":217.521729}]},{"StartTime":129188.0,"Objects":[{"StartTime":129188.0,"EndTime":129188.0,"X":237.260864,"Y":221.260864}]},{"StartTime":129294.0,"Objects":[{"StartTime":129294.0,"EndTime":129294.0,"X":241.0,"Y":225.0}]},{"StartTime":129505.0,"Objects":[{"StartTime":129505.0,"EndTime":129505.0,"X":295.0,"Y":288.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":129785.0,"EndTime":129785.0,"X":191.701752,"Y":291.7883,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":129928.0,"Objects":[{"StartTime":129928.0,"EndTime":129928.0,"X":192.0,"Y":292.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":130103.0,"EndTime":130103.0,"X":175.94281,"Y":365.260956,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":130350.0,"Objects":[{"StartTime":130350.0,"EndTime":130350.0,"X":148.0,"Y":220.0}]},{"StartTime":130561.0,"Objects":[{"StartTime":130561.0,"EndTime":130561.0,"X":68.0,"Y":187.0}]},{"StartTime":130772.0,"Objects":[{"StartTime":130772.0,"EndTime":130772.0,"X":36.0,"Y":267.0}]},{"StartTime":130983.0,"Objects":[{"StartTime":130983.0,"EndTime":130983.0,"X":115.0,"Y":300.0}]},{"StartTime":131195.0,"Objects":[{"StartTime":131195.0,"EndTime":131195.0,"X":16.0,"Y":127.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":131475.0,"EndTime":131475.0,"X":119.044754,"Y":123.706215,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":131618.0,"Objects":[{"StartTime":131618.0,"EndTime":131618.0,"X":119.0,"Y":124.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":131793.0,"EndTime":131793.0,"X":192.260956,"Y":107.94281,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":132040.0,"Objects":[{"StartTime":132040.0,"EndTime":132040.0,"X":280.0,"Y":44.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":132356.0,"EndTime":132356.0,"X":170.209717,"Y":20.2853,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":132637.0,"EndTime":132637.0,"X":280.0,"Y":44.0,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":132885.0,"Objects":[{"StartTime":132885.0,"EndTime":132885.0,"X":96.0,"Y":56.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":133165.0,"EndTime":133165.0,"X":90.74685,"Y":156.698685,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":133308.0,"Objects":[{"StartTime":133308.0,"EndTime":133308.0,"X":91.0,"Y":157.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":133483.0,"EndTime":133483.0,"X":164.045471,"Y":139.98941,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":133731.0,"Objects":[{"StartTime":133731.0,"EndTime":133731.0,"X":44.0,"Y":216.0}]},{"StartTime":133942.0,"Objects":[{"StartTime":133942.0,"EndTime":133942.0,"X":123.0,"Y":249.0}]},{"StartTime":134153.0,"Objects":[{"StartTime":134153.0,"EndTime":134153.0,"X":91.0,"Y":329.0}]},{"StartTime":134364.0,"Objects":[{"StartTime":134364.0,"EndTime":134364.0,"X":11.0,"Y":296.0}]},{"StartTime":134576.0,"Objects":[{"StartTime":134576.0,"EndTime":134576.0,"X":200.0,"Y":268.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":134856.0,"EndTime":134856.0,"X":304.8808,"Y":260.356873,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":134998.0,"Objects":[{"StartTime":134998.0,"EndTime":134998.0,"X":304.0,"Y":260.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":135173.0,"EndTime":135173.0,"X":286.908661,"Y":333.0266,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":135421.0,"Objects":[{"StartTime":135421.0,"EndTime":135421.0,"X":436.0,"Y":348.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":135737.0,"EndTime":135737.0,"X":413.2101,"Y":238.014038,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":136018.0,"EndTime":136018.0,"X":436.0,"Y":348.0,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":136266.0,"Objects":[{"StartTime":136266.0,"EndTime":136266.0,"X":448.0,"Y":168.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":136441.0,"EndTime":136441.0,"X":377.865,"Y":166.693008,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":136688.0,"Objects":[{"StartTime":136688.0,"EndTime":136688.0,"X":232.0,"Y":260.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":136863.0,"EndTime":136863.0,"X":302.135,"Y":261.306976,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":137111.0,"Objects":[{"StartTime":137111.0,"EndTime":137111.0,"X":340.0,"Y":100.0}]},{"StartTime":137322.0,"Objects":[{"StartTime":137322.0,"EndTime":137322.0,"X":268.0,"Y":196.0}]},{"StartTime":137533.0,"Objects":[{"StartTime":137533.0,"EndTime":137533.0,"X":240.0,"Y":48.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":137708.0,"EndTime":137708.0,"X":250.133484,"Y":122.312263,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":137956.0,"Objects":[{"StartTime":137956.0,"EndTime":137956.0,"X":92.0,"Y":44.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":138131.0,"EndTime":138131.0,"X":163.568558,"Y":39.28212,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":138378.0,"Objects":[{"StartTime":138378.0,"EndTime":138378.0,"X":168.0,"Y":180.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":138553.0,"EndTime":138553.0,"X":98.2096,"Y":180.324524,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":138801.0,"Objects":[{"StartTime":138801.0,"EndTime":138801.0,"X":12.0,"Y":56.0}]},{"StartTime":139012.0,"Objects":[{"StartTime":139012.0,"EndTime":139012.0,"X":132.0,"Y":112.0}]},{"StartTime":139223.0,"Objects":[{"StartTime":139223.0,"EndTime":139223.0,"X":44.0,"Y":236.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":139398.0,"EndTime":139398.0,"X":19.9848156,"Y":171.056885,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":139646.0,"Objects":[{"StartTime":139646.0,"EndTime":139646.0,"X":244.0,"Y":172.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":139821.0,"EndTime":139821.0,"X":219.45665,"Y":236.357651,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":140069.0,"Objects":[{"StartTime":140069.0,"EndTime":140069.0,"X":216.0,"Y":104.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":140244.0,"EndTime":140244.0,"X":238.580536,"Y":39.2729034,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":140491.0,"Objects":[{"StartTime":140491.0,"EndTime":140491.0,"X":436.0,"Y":68.0}]},{"StartTime":140702.0,"Objects":[{"StartTime":140702.0,"EndTime":140702.0,"X":289.0,"Y":88.0}]},{"StartTime":140913.0,"Objects":[{"StartTime":140913.0,"EndTime":140913.0,"X":459.0,"Y":156.0}]},{"StartTime":141124.0,"Objects":[{"StartTime":141124.0,"EndTime":141124.0,"X":317.0,"Y":50.0}]},{"StartTime":141336.0,"Objects":[{"StartTime":141336.0,"EndTime":141336.0,"X":336.0,"Y":232.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":141511.0,"EndTime":141511.0,"X":325.956146,"Y":306.324432,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":141759.0,"Objects":[{"StartTime":141759.0,"EndTime":141759.0,"X":468.0,"Y":230.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":141934.0,"EndTime":141934.0,"X":458.0877,"Y":155.6579,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":142181.0,"Objects":[{"StartTime":142181.0,"EndTime":142181.0,"X":436.0,"Y":324.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":142356.0,"EndTime":142356.0,"X":510.4514,"Y":333.0549,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":142604.0,"Objects":[{"StartTime":142604.0,"EndTime":142604.0,"X":336.0,"Y":124.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":142779.0,"EndTime":142779.0,"X":261.534241,"Y":132.9359,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":143026.0,"Objects":[{"StartTime":143026.0,"EndTime":143026.0,"X":210.0,"Y":89.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":143201.0,"EndTime":143201.0,"X":184.922729,"Y":169.4724,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":143343.0,"Objects":[{"StartTime":143343.0,"EndTime":143343.0,"X":261.0,"Y":132.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":143518.0,"EndTime":143518.0,"X":185.715179,"Y":170.3263,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":143660.0,"Objects":[{"StartTime":143660.0,"EndTime":143660.0,"X":256.0,"Y":184.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":143729.0,"EndTime":143729.0,"X":184.960236,"Y":170.093552,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":143871.0,"Objects":[{"StartTime":143871.0,"EndTime":143871.0,"X":124.0,"Y":70.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":144046.0,"EndTime":144046.0,"X":110.185104,"Y":158.9334,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":144188.0,"Objects":[{"StartTime":144188.0,"EndTime":144188.0,"X":96.0,"Y":247.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":144363.0,"EndTime":144363.0,"X":109.814896,"Y":158.0666,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":144505.0,"Objects":[{"StartTime":144505.0,"EndTime":144505.0,"X":184.0,"Y":170.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":144574.0,"EndTime":144574.0,"X":109.964081,"Y":158.013229,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":144716.0,"Objects":[{"StartTime":144716.0,"EndTime":144716.0,"X":261.0,"Y":132.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":144891.0,"EndTime":144891.0,"X":349.75293,"Y":146.9304,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":145033.0,"Objects":[{"StartTime":145033.0,"EndTime":145033.0,"X":336.0,"Y":84.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":145208.0,"EndTime":145208.0,"X":387.835815,"Y":157.573425,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":145350.0,"Objects":[{"StartTime":145350.0,"EndTime":145350.0,"X":428.0,"Y":96.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":145419.0,"EndTime":145419.0,"X":415.2836,"Y":169.9141,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":145562.0,"Objects":[{"StartTime":145562.0,"EndTime":145562.0,"X":411.0,"Y":278.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":145737.0,"EndTime":145737.0,"X":491.462463,"Y":247.365463,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":145878.0,"Objects":[{"StartTime":145878.0,"EndTime":145878.0,"X":324.0,"Y":276.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":146053.0,"EndTime":146053.0,"X":409.8932,"Y":277.2359,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":146195.0,"Objects":[{"StartTime":146195.0,"EndTime":146195.0,"X":252.0,"Y":272.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":146264.0,"EndTime":146264.0,"X":324.1942,"Y":274.656555,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":146407.0,"Objects":[{"StartTime":146407.0,"EndTime":146407.0,"X":317.0,"Y":119.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":146582.0,"EndTime":146582.0,"X":292.912048,"Y":205.716614,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":146724.0,"Objects":[{"StartTime":146724.0,"EndTime":146724.0,"X":240.0,"Y":74.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":146899.0,"EndTime":146899.0,"X":262.5866,"Y":161.11972,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":147040.0,"Objects":[{"StartTime":147040.0,"EndTime":147040.0,"X":166.0,"Y":90.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":147109.0,"EndTime":147109.0,"X":219.407776,"Y":142.655563,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":147252.0,"Objects":[{"StartTime":147252.0,"EndTime":147252.0,"X":170.0,"Y":152.0}]},{"StartTime":147464.0,"Objects":[{"StartTime":147464.0,"EndTime":147464.0,"X":38.0,"Y":120.0}]},{"StartTime":147569.0,"Objects":[{"StartTime":147569.0,"EndTime":147569.0,"X":12.0,"Y":155.0}]},{"StartTime":147675.0,"Objects":[{"StartTime":147675.0,"EndTime":147675.0,"X":2.0,"Y":199.0}]},{"StartTime":147781.0,"Objects":[{"StartTime":147781.0,"EndTime":147781.0,"X":11.0,"Y":242.0}]},{"StartTime":147886.0,"Objects":[{"StartTime":147886.0,"EndTime":147886.0,"X":37.0,"Y":279.0}]},{"StartTime":147992.0,"Objects":[{"StartTime":147992.0,"EndTime":147992.0,"X":75.0,"Y":301.0}]},{"StartTime":148097.0,"Objects":[{"StartTime":148097.0,"EndTime":148097.0,"X":119.0,"Y":304.0}]},{"StartTime":148942.0,"Objects":[{"StartTime":148942.0,"EndTime":148942.0,"X":245.0,"Y":208.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":148977.0,"EndTime":148977.0,"X":264.88504,"Y":197.6252,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":149012.0,"EndTime":149012.0,"X":245.126251,"Y":207.934128,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":149047.0,"EndTime":149047.0,"X":264.7588,"Y":197.691071,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":149082.0,"EndTime":149082.0,"X":245.2525,"Y":207.868256,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":149118.0,"EndTime":149118.0,"X":264.695648,"Y":197.724014,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":149153.0,"EndTime":149153.0,"X":245.189377,"Y":207.901184,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":149188.0,"EndTime":149188.0,"X":264.8219,"Y":197.658142,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":149223.0,"EndTime":149223.0,"X":245.063126,"Y":207.967072,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":149222.0,"EndTime":149222.0,"X":264.948151,"Y":197.59227,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":149364.0,"Objects":[{"StartTime":149364.0,"EndTime":149364.0,"X":232.0,"Y":288.0}]},{"StartTime":149787.0,"Objects":[{"StartTime":149787.0,"EndTime":149787.0,"X":217.0,"Y":38.0}]},{"StartTime":149998.0,"Objects":[{"StartTime":149998.0,"EndTime":149998.0,"X":56.0,"Y":98.0}]},{"StartTime":150209.0,"Objects":[{"StartTime":150209.0,"EndTime":150209.0,"X":155.0,"Y":187.0}]},{"StartTime":150420.0,"Objects":[{"StartTime":150420.0,"EndTime":150420.0,"X":94.0,"Y":26.0}]},{"StartTime":150632.0,"Objects":[{"StartTime":150632.0,"EndTime":150632.0,"X":63.0,"Y":262.0}]},{"StartTime":150843.0,"Objects":[{"StartTime":150843.0,"EndTime":150843.0,"X":257.0,"Y":188.0}]},{"StartTime":151054.0,"Objects":[{"StartTime":151054.0,"EndTime":151054.0,"X":138.0,"Y":82.0}]},{"StartTime":151265.0,"Objects":[{"StartTime":151265.0,"EndTime":151265.0,"X":212.0,"Y":275.0}]},{"StartTime":151477.0,"Objects":[{"StartTime":151477.0,"EndTime":151477.0,"X":288.0,"Y":60.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":151652.0,"EndTime":151652.0,"X":266.524567,"Y":155.1055,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":151899.0,"Objects":[{"StartTime":151899.0,"EndTime":151899.0,"X":204.0,"Y":48.0}]},{"StartTime":152111.0,"Objects":[{"StartTime":152111.0,"EndTime":152111.0,"X":346.0,"Y":175.0}]},{"StartTime":152322.0,"Objects":[{"StartTime":152322.0,"EndTime":152322.0,"X":130.0,"Y":263.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":152497.0,"EndTime":152497.0,"X":151.311874,"Y":167.857727,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":152744.0,"Objects":[{"StartTime":152744.0,"EndTime":152744.0,"X":232.0,"Y":244.0}]},{"StartTime":152956.0,"Objects":[{"StartTime":152956.0,"EndTime":152956.0,"X":56.0,"Y":170.0}]},{"StartTime":153167.0,"Objects":[{"StartTime":153167.0,"EndTime":153167.0,"X":64.0,"Y":352.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":153447.0,"EndTime":153447.0,"X":194.861862,"Y":335.192657,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":153590.0,"Objects":[{"StartTime":153590.0,"EndTime":153590.0,"X":224.0,"Y":348.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":153765.0,"EndTime":153765.0,"X":313.264221,"Y":347.8079,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":154012.0,"Objects":[{"StartTime":154012.0,"EndTime":154012.0,"X":376.0,"Y":140.0}]},{"StartTime":154223.0,"Objects":[{"StartTime":154223.0,"EndTime":154223.0,"X":269.0,"Y":286.0}]},{"StartTime":154435.0,"Objects":[{"StartTime":154435.0,"EndTime":154435.0,"X":441.0,"Y":230.0}]},{"StartTime":154646.0,"Objects":[{"StartTime":154646.0,"EndTime":154646.0,"X":269.0,"Y":173.0}]},{"StartTime":154857.0,"Objects":[{"StartTime":154857.0,"EndTime":154857.0,"X":376.0,"Y":320.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":155032.0,"EndTime":155032.0,"X":465.264221,"Y":319.8079,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":155280.0,"Objects":[{"StartTime":155280.0,"EndTime":155280.0,"X":496.0,"Y":136.0}]},{"StartTime":155491.0,"Objects":[{"StartTime":155491.0,"EndTime":155491.0,"X":420.0,"Y":256.0}]},{"StartTime":155702.0,"Objects":[{"StartTime":155702.0,"EndTime":155702.0,"X":330.0,"Y":80.0}]},{"StartTime":155913.0,"Objects":[{"StartTime":155913.0,"EndTime":155913.0,"X":223.0,"Y":226.0}]},{"StartTime":156125.0,"Objects":[{"StartTime":156125.0,"EndTime":156125.0,"X":395.0,"Y":170.0}]},{"StartTime":156336.0,"Objects":[{"StartTime":156336.0,"EndTime":156336.0,"X":223.0,"Y":113.0}]},{"StartTime":156547.0,"Objects":[{"StartTime":156547.0,"EndTime":156547.0,"X":330.0,"Y":260.0}]},{"StartTime":156759.0,"Objects":[{"StartTime":156759.0,"EndTime":156759.0,"X":408.0,"Y":92.0}]},{"StartTime":156970.0,"Objects":[{"StartTime":156970.0,"EndTime":156970.0,"X":168.0,"Y":168.0}]},{"StartTime":157182.0,"Objects":[{"StartTime":157182.0,"EndTime":157182.0,"X":408.0,"Y":244.0}]},{"StartTime":157392.0,"Objects":[{"StartTime":157392.0,"EndTime":157392.0,"X":256.0,"Y":44.0}]},{"StartTime":157604.0,"Objects":[{"StartTime":157604.0,"EndTime":157604.0,"X":264.0,"Y":296.0}]},{"StartTime":157815.0,"Objects":[{"StartTime":157815.0,"EndTime":157815.0,"X":436.0,"Y":168.0}]},{"StartTime":158027.0,"Objects":[{"StartTime":158027.0,"EndTime":158027.0,"X":188.0,"Y":92.0}]},{"StartTime":158238.0,"Objects":[{"StartTime":158238.0,"EndTime":158238.0,"X":212.0,"Y":336.0}]},{"StartTime":158450.0,"Objects":[{"StartTime":158450.0,"EndTime":158450.0,"X":290.0,"Y":168.0}]},{"StartTime":158661.0,"Objects":[{"StartTime":158661.0,"EndTime":158661.0,"X":50.0,"Y":244.0}]},{"StartTime":158871.0,"Objects":[{"StartTime":158871.0,"EndTime":158871.0,"X":290.0,"Y":320.0}]},{"StartTime":159083.0,"Objects":[{"StartTime":159083.0,"EndTime":159083.0,"X":138.0,"Y":120.0}]},{"StartTime":159295.0,"Objects":[{"StartTime":159295.0,"EndTime":159295.0,"X":146.0,"Y":372.0}]},{"StartTime":159506.0,"Objects":[{"StartTime":159506.0,"EndTime":159506.0,"X":318.0,"Y":244.0}]},{"StartTime":159716.0,"Objects":[{"StartTime":159716.0,"EndTime":159716.0,"X":70.0,"Y":168.0}]},{"StartTime":159928.0,"Objects":[{"StartTime":159928.0,"EndTime":159928.0,"X":324.0,"Y":164.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":160103.0,"EndTime":160103.0,"X":396.4909,"Y":220.798523,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":160350.0,"Objects":[{"StartTime":160350.0,"EndTime":160350.0,"X":291.0,"Y":354.0}]},{"StartTime":160562.0,"Objects":[{"StartTime":160562.0,"EndTime":160562.0,"X":209.0,"Y":190.0}]},{"StartTime":160773.0,"Objects":[{"StartTime":160773.0,"EndTime":160773.0,"X":377.0,"Y":321.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":160948.0,"EndTime":160948.0,"X":290.7343,"Y":353.17215,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":161195.0,"Objects":[{"StartTime":161195.0,"EndTime":161195.0,"X":209.0,"Y":190.0}]},{"StartTime":161407.0,"Objects":[{"StartTime":161407.0,"EndTime":161407.0,"X":396.0,"Y":220.0}]},{"StartTime":161618.0,"Objects":[{"StartTime":161618.0,"EndTime":161618.0,"X":200.0,"Y":283.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":161793.0,"EndTime":161793.0,"X":209.6018,"Y":190.27742,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":162040.0,"Objects":[{"StartTime":162040.0,"EndTime":162040.0,"X":396.0,"Y":221.0}]},{"StartTime":162251.0,"Objects":[{"StartTime":162251.0,"EndTime":162251.0,"X":290.0,"Y":353.0}]},{"StartTime":162463.0,"Objects":[{"StartTime":162463.0,"EndTime":162463.0,"X":264.0,"Y":56.0}]},{"StartTime":162568.0,"Objects":[{"StartTime":162568.0,"EndTime":162568.0,"X":277.0,"Y":102.0}]},{"StartTime":162674.0,"Objects":[{"StartTime":162674.0,"EndTime":162674.0,"X":290.0,"Y":149.0}]},{"StartTime":162779.0,"Objects":[{"StartTime":162779.0,"EndTime":162779.0,"X":304.0,"Y":196.0}]},{"StartTime":162885.0,"Objects":[{"StartTime":162885.0,"EndTime":162885.0,"X":317.0,"Y":243.0}]},{"StartTime":163097.0,"Objects":[{"StartTime":163097.0,"EndTime":163097.0,"X":172.0,"Y":164.0}]},{"StartTime":163308.0,"Objects":[{"StartTime":163308.0,"EndTime":163308.0,"X":416.0,"Y":108.0}]},{"StartTime":163519.0,"Objects":[{"StartTime":163519.0,"EndTime":163519.0,"X":232.0,"Y":91.0}]},{"StartTime":163730.0,"Objects":[{"StartTime":163730.0,"EndTime":163730.0,"X":400.0,"Y":12.0}]},{"StartTime":163941.0,"Objects":[{"StartTime":163941.0,"EndTime":163941.0,"X":383.0,"Y":196.0}]},{"StartTime":164153.0,"Objects":[{"StartTime":164153.0,"EndTime":164153.0,"X":217.0,"Y":0.0}]},{"StartTime":164364.0,"Objects":[{"StartTime":164364.0,"EndTime":164364.0,"X":200.0,"Y":184.0}]},{"StartTime":164575.0,"Objects":[{"StartTime":164575.0,"EndTime":164575.0,"X":313.0,"Y":16.0}]},{"StartTime":164786.0,"Objects":[{"StartTime":164786.0,"EndTime":164786.0,"X":112.0,"Y":32.0}]},{"StartTime":164998.0,"Objects":[{"StartTime":164998.0,"EndTime":164998.0,"X":200.0,"Y":184.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":165173.0,"EndTime":165173.0,"X":205.788208,"Y":91.45287,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":165421.0,"Objects":[{"StartTime":165421.0,"EndTime":165421.0,"X":112.0,"Y":256.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":165596.0,"EndTime":165596.0,"X":106.211784,"Y":348.547119,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":165843.0,"Objects":[{"StartTime":165843.0,"EndTime":165843.0,"X":116.0,"Y":176.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":166018.0,"EndTime":166018.0,"X":23.4528751,"Y":170.211777,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":166266.0,"Objects":[{"StartTime":166266.0,"EndTime":166266.0,"X":196.0,"Y":264.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":166441.0,"EndTime":166441.0,"X":288.547119,"Y":269.7882,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":166688.0,"Objects":[{"StartTime":166688.0,"EndTime":166688.0,"X":248.0,"Y":60.0}]},{"StartTime":166899.0,"Objects":[{"StartTime":166899.0,"EndTime":166899.0,"X":248.0,"Y":201.0}]},{"StartTime":167111.0,"Objects":[{"StartTime":167111.0,"EndTime":167111.0,"X":333.0,"Y":55.0}]},{"StartTime":167322.0,"Objects":[{"StartTime":167322.0,"EndTime":167322.0,"X":248.0,"Y":201.0}]},{"StartTime":167533.0,"Objects":[{"StartTime":167533.0,"EndTime":167533.0,"X":424.0,"Y":101.0}]},{"StartTime":167744.0,"Objects":[{"StartTime":167744.0,"EndTime":167744.0,"X":248.0,"Y":201.0}]},{"StartTime":167956.0,"Objects":[{"StartTime":167956.0,"EndTime":167956.0,"X":468.0,"Y":224.0}]},{"StartTime":168167.0,"Objects":[{"StartTime":168167.0,"EndTime":168167.0,"X":292.0,"Y":124.0}]},{"StartTime":168378.0,"Objects":[{"StartTime":168378.0,"EndTime":168378.0,"X":364.0,"Y":328.0}]},{"StartTime":168589.0,"Objects":[{"StartTime":168589.0,"EndTime":168589.0,"X":364.0,"Y":158.0}]},{"StartTime":168801.0,"Objects":[{"StartTime":168801.0,"EndTime":168801.0,"X":244.0,"Y":304.0}]},{"StartTime":169013.0,"Objects":[{"StartTime":169013.0,"EndTime":169013.0,"X":464.0,"Y":327.0}]},{"StartTime":169224.0,"Objects":[{"StartTime":169224.0,"EndTime":169224.0,"X":192.0,"Y":248.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":169399.0,"EndTime":169399.0,"X":184.99115,"Y":345.247742,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":169646.0,"Objects":[{"StartTime":169646.0,"EndTime":169646.0,"X":508.0,"Y":272.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":169821.0,"EndTime":169821.0,"X":500.99115,"Y":174.752258,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":170068.0,"Objects":[{"StartTime":170068.0,"EndTime":170068.0,"X":268.0,"Y":60.0}]},{"StartTime":170279.0,"Objects":[{"StartTime":170279.0,"EndTime":170279.0,"X":268.0,"Y":257.0}]},{"StartTime":170491.0,"Objects":[{"StartTime":170491.0,"EndTime":170491.0,"X":404.0,"Y":116.0}]},{"StartTime":170702.0,"Objects":[{"StartTime":170702.0,"EndTime":170702.0,"X":207.0,"Y":116.0}]},{"StartTime":170913.0,"Objects":[{"StartTime":170913.0,"EndTime":170913.0,"X":348.0,"Y":267.0}]},{"StartTime":171124.0,"Objects":[{"StartTime":171124.0,"EndTime":171124.0,"X":348.0,"Y":31.0}]},{"StartTime":171336.0,"Objects":[{"StartTime":171336.0,"EndTime":171336.0,"X":192.0,"Y":200.0}]},{"StartTime":171547.0,"Objects":[{"StartTime":171547.0,"EndTime":171547.0,"X":428.0,"Y":200.0}]},{"StartTime":171759.0,"Objects":[{"StartTime":171759.0,"EndTime":171759.0,"X":268.0,"Y":60.0}]},{"StartTime":171970.0,"Objects":[{"StartTime":171970.0,"EndTime":171970.0,"X":386.0,"Y":236.0}]},{"StartTime":172181.0,"Objects":[{"StartTime":172181.0,"EndTime":172181.0,"X":386.0,"Y":11.0}]},{"StartTime":172393.0,"Objects":[{"StartTime":172393.0,"EndTime":172393.0,"X":268.0,"Y":187.0}]},{"StartTime":172604.0,"Objects":[{"StartTime":172604.0,"EndTime":172604.0,"X":149.0,"Y":55.0}]},{"StartTime":172815.0,"Objects":[{"StartTime":172815.0,"EndTime":172815.0,"X":30.0,"Y":231.0}]},{"StartTime":173026.0,"Objects":[{"StartTime":173026.0,"EndTime":173026.0,"X":30.0,"Y":7.0}]},{"StartTime":173238.0,"Objects":[{"StartTime":173238.0,"EndTime":173238.0,"X":149.0,"Y":183.0}]},{"StartTime":173449.0,"Objects":[{"StartTime":173449.0,"EndTime":173449.0,"X":30.0,"Y":7.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":173624.0,"EndTime":173624.0,"X":52.15489,"Y":101.949524,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":173871.0,"Objects":[{"StartTime":173871.0,"EndTime":173871.0,"X":240.0,"Y":64.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":174046.0,"EndTime":174046.0,"X":146.743469,"Y":35.54885,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":174294.0,"Objects":[{"StartTime":174294.0,"EndTime":174294.0,"X":80.0,"Y":216.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":174469.0,"EndTime":174469.0,"X":150.509186,"Y":148.659775,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":174716.0,"Objects":[{"StartTime":174716.0,"EndTime":174716.0,"X":124.0,"Y":280.0}]},{"StartTime":174928.0,"Objects":[{"StartTime":174928.0,"EndTime":174928.0,"X":56.0,"Y":128.0}]},{"StartTime":175139.0,"Objects":[{"StartTime":175139.0,"EndTime":175139.0,"X":216.0,"Y":212.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":175314.0,"EndTime":175314.0,"X":204.150711,"Y":286.058044,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":175562.0,"Objects":[{"StartTime":175562.0,"EndTime":175562.0,"X":296.0,"Y":216.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":175737.0,"EndTime":175737.0,"X":280.708374,"Y":304.6914,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":175984.0,"Objects":[{"StartTime":175984.0,"EndTime":175984.0,"X":376.0,"Y":208.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":176264.0,"EndTime":176264.0,"X":353.806122,"Y":341.1632,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":176406.0,"Objects":[{"StartTime":176406.0,"EndTime":176406.0,"X":356.739136,"Y":344.739136}]},{"StartTime":176618.0,"Objects":[{"StartTime":176618.0,"EndTime":176618.0,"X":320.521729,"Y":136.521729}]},{"StartTime":176723.0,"Objects":[{"StartTime":176723.0,"EndTime":176723.0,"X":324.260864,"Y":140.260864}]},{"StartTime":176829.0,"Objects":[{"StartTime":176829.0,"EndTime":176829.0,"X":328.0,"Y":144.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":177004.0,"EndTime":177004.0,"X":411.899,"Y":139.8616,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":177252.0,"Objects":[{"StartTime":177252.0,"EndTime":177252.0,"X":248.0,"Y":152.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":177427.0,"EndTime":177427.0,"X":164.101013,"Y":156.138382,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":177674.0,"Objects":[{"StartTime":177674.0,"EndTime":177674.0,"X":344.0,"Y":120.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":177849.0,"EndTime":177849.0,"X":427.899,"Y":115.8616,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":178097.0,"Objects":[{"StartTime":178097.0,"EndTime":178097.0,"X":236.0,"Y":168.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":178272.0,"EndTime":178272.0,"X":152.101013,"Y":172.138382,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":178519.0,"Objects":[{"StartTime":178519.0,"EndTime":178519.0,"X":192.0,"Y":272.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":178694.0,"EndTime":178694.0,"X":196.1384,"Y":355.899,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":178942.0,"Objects":[{"StartTime":178942.0,"EndTime":178942.0,"X":152.0,"Y":172.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":179117.0,"EndTime":179117.0,"X":147.8616,"Y":88.10101,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":179364.0,"Objects":[{"StartTime":179364.0,"EndTime":179364.0,"X":228.0,"Y":284.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":179539.0,"EndTime":179539.0,"X":232.1384,"Y":367.899,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":179787.0,"Objects":[{"StartTime":179787.0,"EndTime":179787.0,"X":116.0,"Y":152.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":179962.0,"EndTime":179962.0,"X":111.86161,"Y":68.10102,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":180209.0,"Objects":[{"StartTime":180209.0,"EndTime":180209.0,"X":100.0,"Y":256.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":180384.0,"EndTime":180384.0,"X":16.1010227,"Y":260.1384,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":180632.0,"Objects":[{"StartTime":180632.0,"EndTime":180632.0,"X":240.0,"Y":184.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":180807.0,"EndTime":180807.0,"X":323.899,"Y":179.8616,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":181055.0,"Objects":[{"StartTime":181055.0,"EndTime":181055.0,"X":288.0,"Y":336.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":181230.0,"EndTime":181230.0,"X":284.541016,"Y":246.0665,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":181477.0,"Objects":[{"StartTime":181477.0,"EndTime":181477.0,"X":432.0,"Y":84.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":181652.0,"EndTime":181652.0,"X":423.044678,"Y":173.553345,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":181900.0,"Objects":[{"StartTime":181900.0,"EndTime":181900.0,"X":368.0,"Y":352.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":182075.0,"EndTime":182075.0,"X":364.541016,"Y":262.0665,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":182322.0,"Objects":[{"StartTime":182322.0,"EndTime":182322.0,"X":512.0,"Y":100.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":182497.0,"EndTime":182497.0,"X":503.044678,"Y":189.553345,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":182745.0,"Objects":[{"StartTime":182745.0,"EndTime":182745.0,"X":272.0,"Y":104.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":182920.0,"EndTime":182920.0,"X":361.553345,"Y":112.955338,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":183062.0,"Objects":[{"StartTime":183062.0,"EndTime":183062.0,"X":356.0,"Y":132.0}]},{"StartTime":183167.0,"Objects":[{"StartTime":183167.0,"EndTime":183167.0,"X":352.0,"Y":156.0}]},{"StartTime":183378.0,"Objects":[{"StartTime":183378.0,"EndTime":183378.0,"X":276.0,"Y":20.0}]},{"StartTime":183590.0,"Objects":[{"StartTime":183590.0,"EndTime":183590.0,"X":304.0,"Y":240.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":183765.0,"EndTime":183765.0,"X":220.5027,"Y":243.341385,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":184012.0,"Objects":[{"StartTime":184012.0,"EndTime":184012.0,"X":392.0,"Y":272.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":184187.0,"EndTime":184187.0,"X":436.5039,"Y":342.962158,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":184435.0,"Objects":[{"StartTime":184435.0,"EndTime":184435.0,"X":376.0,"Y":184.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":184610.0,"EndTime":184610.0,"X":413.9991,"Y":109.324722,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":184857.0,"Objects":[{"StartTime":184857.0,"EndTime":184857.0,"X":320.0,"Y":336.0}]},{"StartTime":185069.0,"Objects":[{"StartTime":185069.0,"EndTime":185069.0,"X":260.0,"Y":180.0}]},{"StartTime":185280.0,"Objects":[{"StartTime":185280.0,"EndTime":185280.0,"X":176.0,"Y":304.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":185455.0,"EndTime":185455.0,"X":146.285233,"Y":347.999146,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":185702.0,"Objects":[{"StartTime":185702.0,"EndTime":185702.0,"X":207.0,"Y":176.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":185877.0,"EndTime":185877.0,"X":258.989227,"Y":179.51886,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":186125.0,"Objects":[{"StartTime":186125.0,"EndTime":186125.0,"X":84.0,"Y":224.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":186300.0,"EndTime":186300.0,"X":60.46429,"Y":176.0,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":186547.0,"Objects":[{"StartTime":186547.0,"EndTime":186547.0,"X":244.0,"Y":260.0}]},{"StartTime":186759.0,"Objects":[{"StartTime":186759.0,"EndTime":186759.0,"X":88.0,"Y":300.0}]},{"StartTime":186970.0,"Objects":[{"StartTime":186970.0,"EndTime":186970.0,"X":128.0,"Y":44.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":187145.0,"EndTime":187145.0,"X":133.824356,"Y":148.838348,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":187393.0,"Objects":[{"StartTime":187393.0,"EndTime":187393.0,"X":340.0,"Y":208.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":187568.0,"EndTime":187568.0,"X":345.824341,"Y":103.161659,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":187815.0,"Objects":[{"StartTime":187815.0,"EndTime":187815.0,"X":244.0,"Y":260.0}]},{"StartTime":188026.0,"Objects":[{"StartTime":188026.0,"EndTime":188026.0,"X":424.0,"Y":240.0}]},{"StartTime":188238.0,"Objects":[{"StartTime":188238.0,"EndTime":188238.0,"X":211.0,"Y":244.0}]},{"StartTime":188449.0,"Objects":[{"StartTime":188449.0,"EndTime":188449.0,"X":377.0,"Y":317.0}]},{"StartTime":188660.0,"Objects":[{"StartTime":188660.0,"EndTime":188660.0,"X":196.0,"Y":336.0}]},{"StartTime":188871.0,"Objects":[{"StartTime":188871.0,"EndTime":188871.0,"X":224.0,"Y":154.0}]},{"StartTime":189083.0,"Objects":[{"StartTime":189083.0,"EndTime":189083.0,"X":367.0,"Y":270.0}]},{"StartTime":189294.0,"Objects":[{"StartTime":189294.0,"EndTime":189294.0,"X":132.0,"Y":216.0}]},{"StartTime":189505.0,"Objects":[{"StartTime":189505.0,"EndTime":189505.0,"X":338.0,"Y":135.0}]},{"StartTime":189610.0,"Objects":[{"StartTime":189610.0,"EndTime":189610.0,"X":330.0,"Y":186.0}]},{"StartTime":189716.0,"Objects":[{"StartTime":189716.0,"EndTime":189716.0,"X":322.0,"Y":238.0}]},{"StartTime":189821.0,"Objects":[{"StartTime":189821.0,"EndTime":189821.0,"X":314.0,"Y":290.0}]},{"StartTime":189927.0,"Objects":[{"StartTime":189927.0,"EndTime":189927.0,"X":306.0,"Y":342.0}]},{"StartTime":190139.0,"Objects":[{"StartTime":190139.0,"EndTime":190139.0,"X":228.0,"Y":252.0}]},{"StartTime":190350.0,"Objects":[{"StartTime":190350.0,"EndTime":190350.0,"X":420.0,"Y":216.0}]},{"StartTime":190562.0,"Objects":[{"StartTime":190562.0,"EndTime":190562.0,"X":247.0,"Y":160.0}]},{"StartTime":190773.0,"Objects":[{"StartTime":190773.0,"EndTime":190773.0,"X":406.0,"Y":252.0}]},{"StartTime":190985.0,"Objects":[{"StartTime":190985.0,"EndTime":190985.0,"X":368.0,"Y":74.0}]},{"StartTime":191195.0,"Objects":[{"StartTime":191195.0,"EndTime":191195.0,"X":373.0,"Y":269.0}]},{"StartTime":191407.0,"Objects":[{"StartTime":191407.0,"EndTime":191407.0,"X":507.0,"Y":146.0}]},{"StartTime":191618.0,"Objects":[{"StartTime":191618.0,"EndTime":191618.0,"X":335.0,"Y":271.0}]},{"StartTime":191830.0,"Objects":[{"StartTime":191830.0,"EndTime":191830.0,"X":508.0,"Y":325.0}]},{"StartTime":192040.0,"Objects":[{"StartTime":192040.0,"EndTime":192040.0,"X":219.0,"Y":271.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":192215.0,"EndTime":192215.0,"X":205.632385,"Y":186.8185,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":192463.0,"Objects":[{"StartTime":192463.0,"EndTime":192463.0,"X":279.0,"Y":327.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":192638.0,"EndTime":192638.0,"X":197.296051,"Y":348.666077,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":192885.0,"Objects":[{"StartTime":192885.0,"EndTime":192885.0,"X":335.0,"Y":271.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":193060.0,"EndTime":193060.0,"X":356.590668,"Y":352.7418,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":193308.0,"Objects":[{"StartTime":193308.0,"EndTime":193308.0,"X":279.0,"Y":219.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":193483.0,"EndTime":193483.0,"X":360.7418,"Y":197.409332,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":193731.0,"Objects":[{"StartTime":193731.0,"EndTime":193731.0,"X":108.0,"Y":296.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":194011.0,"EndTime":194011.0,"X":111.138687,"Y":161.0365,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":194153.0,"Objects":[{"StartTime":194153.0,"EndTime":194153.0,"X":72.0,"Y":100.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":194328.0,"EndTime":194328.0,"X":155.1787,"Y":102.726517,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":194576.0,"Objects":[{"StartTime":194576.0,"EndTime":194576.0,"X":24.0,"Y":24.0}]},{"StartTime":194787.0,"Objects":[{"StartTime":194787.0,"EndTime":194787.0,"X":36.0,"Y":168.0}]},{"StartTime":194998.0,"Objects":[{"StartTime":194998.0,"EndTime":194998.0,"X":116.0,"Y":40.0}]},{"StartTime":195209.0,"Objects":[{"StartTime":195209.0,"EndTime":195209.0,"X":184.0,"Y":184.0}]},{"StartTime":195421.0,"Objects":[{"StartTime":195421.0,"EndTime":195421.0,"X":256.0,"Y":56.0}]},{"StartTime":195632.0,"Objects":[{"StartTime":195632.0,"EndTime":195632.0,"X":112.0,"Y":155.0}]},{"StartTime":195843.0,"Objects":[{"StartTime":195843.0,"EndTime":195843.0,"X":276.0,"Y":224.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":196018.0,"EndTime":196018.0,"X":268.203339,"Y":134.338348,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":196266.0,"Objects":[{"StartTime":196266.0,"EndTime":196266.0,"X":160.0,"Y":72.0}]},{"StartTime":196477.0,"Objects":[{"StartTime":196477.0,"EndTime":196477.0,"X":16.0,"Y":171.0}]},{"StartTime":196688.0,"Objects":[{"StartTime":196688.0,"EndTime":196688.0,"X":180.0,"Y":240.0}]},{"StartTime":196899.0,"Objects":[{"StartTime":196899.0,"EndTime":196899.0,"X":72.0,"Y":108.0}]},{"StartTime":197111.0,"Objects":[{"StartTime":197111.0,"EndTime":197111.0,"X":76.0,"Y":328.0}]},{"StartTime":197323.0,"Objects":[{"StartTime":197323.0,"EndTime":197323.0,"X":249.0,"Y":274.0}]},{"StartTime":197534.0,"Objects":[{"StartTime":197534.0,"EndTime":197534.0,"X":83.0,"Y":171.0}]},{"StartTime":197745.0,"Objects":[{"StartTime":197745.0,"EndTime":197745.0,"X":217.0,"Y":295.0}]},{"StartTime":197956.0,"Objects":[{"StartTime":197956.0,"EndTime":197956.0,"X":218.0,"Y":119.0}]},{"StartTime":198168.0,"Objects":[{"StartTime":198168.0,"EndTime":198168.0,"X":179.0,"Y":297.0}]},{"StartTime":198379.0,"Objects":[{"StartTime":198379.0,"EndTime":198379.0,"X":317.0,"Y":223.0}]},{"StartTime":198591.0,"Objects":[{"StartTime":198591.0,"EndTime":198591.0,"X":144.0,"Y":279.0}]},{"StartTime":198801.0,"Objects":[{"StartTime":198801.0,"EndTime":198801.0,"X":295.0,"Y":284.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":198976.0,"EndTime":198976.0,"X":277.349548,"Y":195.747742,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":199224.0,"Objects":[{"StartTime":199224.0,"EndTime":199224.0,"X":489.0,"Y":254.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":199399.0,"EndTime":199399.0,"X":471.349548,"Y":342.252258,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":199646.0,"Objects":[{"StartTime":199646.0,"EndTime":199646.0,"X":277.0,"Y":195.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":199821.0,"EndTime":199821.0,"X":259.349548,"Y":106.747734,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":200069.0,"Objects":[{"StartTime":200069.0,"EndTime":200069.0,"X":506.0,"Y":165.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":200244.0,"EndTime":200244.0,"X":488.349548,"Y":253.252258,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":200491.0,"Objects":[{"StartTime":200491.0,"EndTime":200491.0,"X":301.0,"Y":42.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":200771.0,"EndTime":200771.0,"X":419.8098,"Y":32.4704971,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":200914.0,"Objects":[{"StartTime":200914.0,"EndTime":200914.0,"X":432.0,"Y":52.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":201089.0,"EndTime":201089.0,"X":422.412018,"Y":141.487823,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":201336.0,"Objects":[{"StartTime":201336.0,"EndTime":201336.0,"X":262.0,"Y":226.0}]},{"StartTime":201547.0,"Objects":[{"StartTime":201547.0,"EndTime":201547.0,"X":352.0,"Y":103.0}]},{"StartTime":201759.0,"Objects":[{"StartTime":201759.0,"EndTime":201759.0,"X":352.0,"Y":256.0}]},{"StartTime":201970.0,"Objects":[{"StartTime":201970.0,"EndTime":201970.0,"X":262.0,"Y":132.0}]},{"StartTime":202181.0,"Objects":[{"StartTime":202181.0,"EndTime":202181.0,"X":407.0,"Y":179.0}]},{"StartTime":202393.0,"Objects":[{"StartTime":202393.0,"EndTime":202393.0,"X":240.0,"Y":253.0}]},{"StartTime":202604.0,"Objects":[{"StartTime":202604.0,"EndTime":202604.0,"X":418.0,"Y":291.0}]},{"StartTime":202815.0,"Objects":[{"StartTime":202815.0,"EndTime":202815.0,"X":296.0,"Y":155.0}]},{"StartTime":203026.0,"Objects":[{"StartTime":203026.0,"EndTime":203026.0,"X":315.0,"Y":338.0}]},{"StartTime":203131.0,"Objects":[{"StartTime":203131.0,"EndTime":203131.0,"X":281.0,"Y":308.0}]},{"StartTime":203237.0,"Objects":[{"StartTime":203237.0,"EndTime":203237.0,"X":239.0,"Y":292.0}]},{"StartTime":203342.0,"Objects":[{"StartTime":203342.0,"EndTime":203342.0,"X":195.0,"Y":291.0}]},{"StartTime":203448.0,"Objects":[{"StartTime":203448.0,"EndTime":203448.0,"X":152.0,"Y":306.0}]},{"StartTime":203660.0,"Objects":[{"StartTime":203660.0,"EndTime":203660.0,"X":328.0,"Y":380.0}]},{"StartTime":203871.0,"Objects":[{"StartTime":203871.0,"EndTime":203871.0,"X":312.0,"Y":204.0}]},{"StartTime":204083.0,"Objects":[{"StartTime":204083.0,"EndTime":204083.0,"X":120.0,"Y":266.0}]},{"StartTime":204294.0,"Objects":[{"StartTime":204294.0,"EndTime":204294.0,"X":284.0,"Y":136.0}]},{"StartTime":204506.0,"Objects":[{"StartTime":204506.0,"EndTime":204506.0,"X":241.0,"Y":334.0}]},{"StartTime":204716.0,"Objects":[{"StartTime":204716.0,"EndTime":204716.0,"X":210.0,"Y":130.0}]},{"StartTime":204928.0,"Objects":[{"StartTime":204928.0,"EndTime":204928.0,"X":359.0,"Y":267.0}]},{"StartTime":205139.0,"Objects":[{"StartTime":205139.0,"EndTime":205139.0,"X":152.0,"Y":180.0}]},{"StartTime":205351.0,"Objects":[{"StartTime":205351.0,"EndTime":205351.0,"X":345.0,"Y":120.0}]},{"StartTime":205562.0,"Objects":[{"StartTime":205562.0,"EndTime":205562.0,"X":84.0,"Y":136.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":205737.0,"EndTime":205737.0,"X":83.80006,"Y":221.6485,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":205984.0,"Objects":[{"StartTime":205984.0,"EndTime":205984.0,"X":284.0,"Y":136.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":206159.0,"EndTime":206159.0,"X":284.199921,"Y":50.3514977,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":206407.0,"Objects":[{"StartTime":206407.0,"EndTime":206407.0,"X":184.0,"Y":248.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":206582.0,"EndTime":206582.0,"X":269.6485,"Y":248.199936,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":206829.0,"Objects":[{"StartTime":206829.0,"EndTime":206829.0,"X":180.0,"Y":28.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":207004.0,"EndTime":207004.0,"X":94.3514938,"Y":27.80006,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":207252.0,"Objects":[{"StartTime":207252.0,"EndTime":207252.0,"X":153.0,"Y":305.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":207532.0,"EndTime":207532.0,"X":151.988937,"Y":179.081238,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":207674.0,"Objects":[{"StartTime":207674.0,"EndTime":207674.0,"X":140.0,"Y":160.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":207849.0,"EndTime":207849.0,"X":54.3514977,"Y":159.800079,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":208097.0,"Objects":[{"StartTime":208097.0,"EndTime":208097.0,"X":72.0,"Y":336.0}]},{"StartTime":208308.0,"Objects":[{"StartTime":208308.0,"EndTime":208308.0,"X":256.0,"Y":292.0}]},{"StartTime":208519.0,"Objects":[{"StartTime":208519.0,"EndTime":208519.0,"X":100.0,"Y":224.0}]},{"StartTime":208730.0,"Objects":[{"StartTime":208730.0,"EndTime":208730.0,"X":204.0,"Y":381.0}]},{"StartTime":208942.0,"Objects":[{"StartTime":208942.0,"EndTime":208942.0,"X":351.0,"Y":209.0}]},{"StartTime":209153.0,"Objects":[{"StartTime":209153.0,"EndTime":209153.0,"X":178.0,"Y":305.0}]},{"StartTime":209364.0,"Objects":[{"StartTime":209364.0,"EndTime":209364.0,"X":312.0,"Y":344.0}]},{"StartTime":209576.0,"Objects":[{"StartTime":209576.0,"EndTime":209576.0,"X":217.0,"Y":171.0}]},{"StartTime":209787.0,"Objects":[{"StartTime":209787.0,"EndTime":209787.0,"X":472.0,"Y":144.0}]},{"StartTime":209998.0,"Objects":[{"StartTime":209998.0,"EndTime":209998.0,"X":264.0,"Y":259.0}]},{"StartTime":210209.0,"Objects":[{"StartTime":210209.0,"EndTime":210209.0,"X":425.0,"Y":306.0}]},{"StartTime":210421.0,"Objects":[{"StartTime":210421.0,"EndTime":210421.0,"X":311.0,"Y":98.0}]},{"StartTime":210632.0,"Objects":[{"StartTime":210632.0,"EndTime":210632.0,"X":332.0,"Y":312.0}]},{"StartTime":210843.0,"Objects":[{"StartTime":210843.0,"EndTime":210843.0,"X":396.0,"Y":100.0}]},{"StartTime":211055.0,"Objects":[{"StartTime":211055.0,"EndTime":211055.0,"X":192.0,"Y":160.0}]},{"StartTime":211266.0,"Objects":[{"StartTime":211266.0,"EndTime":211266.0,"X":403.0,"Y":224.0}]},{"StartTime":211477.0,"Objects":[{"StartTime":211477.0,"EndTime":211477.0,"X":328.0,"Y":24.0}]},{"StartTime":211688.0,"Objects":[{"StartTime":211688.0,"EndTime":211688.0,"X":255.0,"Y":267.0}]},{"StartTime":211900.0,"Objects":[{"StartTime":211900.0,"EndTime":211900.0,"X":488.0,"Y":198.0}]},{"StartTime":212111.0,"Objects":[{"StartTime":212111.0,"EndTime":212111.0,"X":247.0,"Y":125.0}]},{"StartTime":212322.0,"Objects":[{"StartTime":212322.0,"EndTime":212322.0,"X":392.0,"Y":312.0}]},{"StartTime":212533.0,"Objects":[{"StartTime":212533.0,"EndTime":212533.0,"X":334.0,"Y":66.0}]},{"StartTime":212745.0,"Objects":[{"StartTime":212745.0,"EndTime":212745.0,"X":342.0,"Y":351.0}]},{"StartTime":212956.0,"Objects":[{"StartTime":212956.0,"EndTime":212956.0,"X":372.0,"Y":100.0}]},{"StartTime":213167.0,"Objects":[{"StartTime":213167.0,"EndTime":213167.0,"X":251.0,"Y":373.0}]},{"StartTime":213378.0,"Objects":[{"StartTime":213378.0,"EndTime":213378.0,"X":402.0,"Y":170.0}]},{"StartTime":213590.0,"Objects":[{"StartTime":213590.0,"EndTime":213590.0,"X":136.0,"Y":327.0}]},{"StartTime":213801.0,"Objects":[{"StartTime":213801.0,"EndTime":213801.0,"X":382.0,"Y":270.0}]},{"StartTime":214012.0,"Objects":[{"StartTime":214012.0,"EndTime":214012.0,"X":212.0,"Y":144.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":214187.0,"EndTime":214187.0,"X":220.116043,"Y":240.231522,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":214435.0,"Objects":[{"StartTime":214435.0,"EndTime":214435.0,"X":152.0,"Y":88.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":214610.0,"EndTime":214610.0,"X":65.05222,"Y":46.2239647,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":214857.0,"Objects":[{"StartTime":214857.0,"EndTime":214857.0,"X":232.0,"Y":64.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":215032.0,"EndTime":215032.0,"X":310.786377,"Y":7.698365,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":215280.0,"Objects":[{"StartTime":215280.0,"EndTime":215280.0,"X":80.0,"Y":120.0}]},{"StartTime":215491.0,"Objects":[{"StartTime":215491.0,"EndTime":215491.0,"X":272.0,"Y":188.0}]},{"StartTime":215702.0,"Objects":[{"StartTime":215702.0,"EndTime":215702.0,"X":192.0,"Y":8.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":215877.0,"EndTime":215877.0,"X":194.429779,"Y":88.99472,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":216125.0,"Objects":[{"StartTime":216125.0,"EndTime":216125.0,"X":384.0,"Y":64.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":216300.0,"EndTime":216300.0,"X":328.026855,"Y":123.368477,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":216547.0,"Objects":[{"StartTime":216547.0,"EndTime":216547.0,"X":432.0,"Y":244.0}]},{"StartTime":216759.0,"Objects":[{"StartTime":216759.0,"EndTime":216759.0,"X":260.0,"Y":264.0}]},{"StartTime":216970.0,"Objects":[{"StartTime":216970.0,"EndTime":216970.0,"X":328.0,"Y":123.0}]},{"StartTime":217075.0,"Objects":[{"StartTime":217075.0,"EndTime":217075.0,"X":333.0,"Y":175.0}]},{"StartTime":217181.0,"Objects":[{"StartTime":217181.0,"EndTime":217181.0,"X":338.0,"Y":227.0}]},{"StartTime":217286.0,"Objects":[{"StartTime":217286.0,"EndTime":217286.0,"X":344.0,"Y":279.0}]},{"StartTime":217392.0,"Objects":[{"StartTime":217392.0,"EndTime":217392.0,"X":349.0,"Y":331.0}]},{"StartTime":218238.0,"Objects":[{"StartTime":218238.0,"EndTime":218238.0,"X":349.0,"Y":331.0}]},{"StartTime":218343.0,"Objects":[{"StartTime":218343.0,"EndTime":218343.0,"X":310.0,"Y":323.0}]},{"StartTime":218449.0,"Objects":[{"StartTime":218449.0,"EndTime":218449.0,"X":273.0,"Y":317.0}]},{"StartTime":218554.0,"Objects":[{"StartTime":218554.0,"EndTime":218554.0,"X":236.0,"Y":312.0}]},{"StartTime":218660.0,"Objects":[{"StartTime":218660.0,"EndTime":218660.0,"X":198.0,"Y":306.0}]},{"StartTime":218765.0,"Objects":[{"StartTime":218765.0,"EndTime":218765.0,"X":253.0,"Y":296.0}]},{"StartTime":218871.0,"Objects":[{"StartTime":218871.0,"EndTime":218871.0,"X":309.0,"Y":287.0}]},{"StartTime":218976.0,"Objects":[{"StartTime":218976.0,"EndTime":218976.0,"X":365.0,"Y":278.0}]},{"StartTime":219082.0,"Objects":[{"StartTime":219082.0,"EndTime":219082.0,"X":421.0,"Y":268.0}]},{"StartTime":219294.0,"Objects":[{"StartTime":219294.0,"EndTime":219294.0,"X":348.0,"Y":92.0}]},{"StartTime":219505.0,"Objects":[{"StartTime":219505.0,"EndTime":219505.0,"X":205.0,"Y":236.0}]},{"StartTime":219717.0,"Objects":[{"StartTime":219717.0,"EndTime":219717.0,"X":381.0,"Y":163.0}]},{"StartTime":219928.0,"Objects":[{"StartTime":219928.0,"EndTime":219928.0,"X":237.0,"Y":24.0}]},{"StartTime":220140.0,"Objects":[{"StartTime":220140.0,"EndTime":220140.0,"X":310.0,"Y":200.0}]},{"StartTime":220350.0,"Objects":[{"StartTime":220350.0,"EndTime":220350.0,"X":449.0,"Y":52.0}]},{"StartTime":220562.0,"Objects":[{"StartTime":220562.0,"EndTime":220562.0,"X":273.0,"Y":125.0}]},{"StartTime":220773.0,"Objects":[{"StartTime":220773.0,"EndTime":220773.0,"X":392.0,"Y":272.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":220948.0,"EndTime":220948.0,"X":493.387451,"Y":282.365265,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":221195.0,"Objects":[{"StartTime":221195.0,"EndTime":221195.0,"X":257.0,"Y":249.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":221370.0,"EndTime":221370.0,"X":168.323166,"Y":298.312439,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":221618.0,"Objects":[{"StartTime":221618.0,"EndTime":221618.0,"X":380.0,"Y":189.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":221793.0,"EndTime":221793.0,"X":421.2337,"Y":95.80929,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":222040.0,"Objects":[{"StartTime":222040.0,"EndTime":222040.0,"X":317.0,"Y":308.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":222215.0,"EndTime":222215.0,"X":392.657227,"Y":376.100861,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":222463.0,"Objects":[{"StartTime":222463.0,"EndTime":222463.0,"X":297.0,"Y":175.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":222743.0,"EndTime":222743.0,"X":252.84137,"Y":29.1527958,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":222885.0,"Objects":[{"StartTime":222885.0,"EndTime":222885.0,"X":253.0,"Y":29.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":223060.0,"EndTime":223060.0,"X":343.9761,"Y":72.90899,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":223308.0,"Objects":[{"StartTime":223308.0,"EndTime":223308.0,"X":168.0,"Y":34.0}]},{"StartTime":223519.0,"Objects":[{"StartTime":223519.0,"EndTime":223519.0,"X":63.0,"Y":216.0}]},{"StartTime":223731.0,"Objects":[{"StartTime":223731.0,"EndTime":223731.0,"X":220.0,"Y":125.0}]},{"StartTime":223942.0,"Objects":[{"StartTime":223942.0,"EndTime":223942.0,"X":10.0,"Y":125.0}]},{"StartTime":224153.0,"Objects":[{"StartTime":224153.0,"EndTime":224153.0,"X":168.0,"Y":216.0}]},{"StartTime":224364.0,"Objects":[{"StartTime":224364.0,"EndTime":224364.0,"X":63.0,"Y":34.0}]},{"StartTime":224576.0,"Objects":[{"StartTime":224576.0,"EndTime":224576.0,"X":0.0,"Y":264.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":224751.0,"EndTime":224751.0,"X":93.40772,"Y":288.831,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":224998.0,"Objects":[{"StartTime":224998.0,"EndTime":224998.0,"X":144.0,"Y":140.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":225067.0,"EndTime":225067.0,"X":149.111465,"Y":87.74942,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":225209.0,"Objects":[{"StartTime":225209.0,"EndTime":225209.0,"X":208.0,"Y":304.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":225278.0,"EndTime":225278.0,"X":201.982239,"Y":356.153961,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":225421.0,"Objects":[{"StartTime":225421.0,"EndTime":225421.0,"X":256.0,"Y":144.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":225490.0,"EndTime":225490.0,"X":261.111481,"Y":91.74942,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":225632.0,"Objects":[{"StartTime":225632.0,"EndTime":225632.0,"X":320.0,"Y":308.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":225701.0,"EndTime":225701.0,"X":313.982239,"Y":360.153961,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":225843.0,"Objects":[{"StartTime":225843.0,"EndTime":225843.0,"X":425.0,"Y":265.0}]},{"StartTime":226055.0,"Objects":[{"StartTime":226055.0,"EndTime":226055.0,"X":256.0,"Y":188.0}]},{"StartTime":226266.0,"Objects":[{"StartTime":226266.0,"EndTime":226266.0,"X":425.0,"Y":102.0}]},{"StartTime":226477.0,"Objects":[{"StartTime":226477.0,"EndTime":226477.0,"X":299.0,"Y":248.0}]},{"StartTime":226688.0,"Objects":[{"StartTime":226688.0,"EndTime":226688.0,"X":271.0,"Y":53.0}]},{"StartTime":226900.0,"Objects":[{"StartTime":226900.0,"EndTime":226900.0,"X":369.0,"Y":225.0}]},{"StartTime":227111.0,"Objects":[{"StartTime":227111.0,"EndTime":227111.0,"X":176.0,"Y":183.0}]},{"StartTime":227322.0,"Objects":[{"StartTime":227322.0,"EndTime":227322.0,"X":369.0,"Y":151.0}]},{"StartTime":227533.0,"Objects":[{"StartTime":227533.0,"EndTime":227533.0,"X":274.0,"Y":339.0}]},{"StartTime":227745.0,"Objects":[{"StartTime":227745.0,"EndTime":227745.0,"X":307.0,"Y":116.0}]},{"StartTime":227956.0,"Objects":[{"StartTime":227956.0,"EndTime":227956.0,"X":458.0,"Y":279.0}]},{"StartTime":228168.0,"Objects":[{"StartTime":228168.0,"EndTime":228168.0,"X":256.0,"Y":187.0}]},{"StartTime":228379.0,"Objects":[{"StartTime":228379.0,"EndTime":228379.0,"X":458.0,"Y":83.0}]},{"StartTime":228590.0,"Objects":[{"StartTime":228590.0,"EndTime":228590.0,"X":308.0,"Y":256.0}]},{"StartTime":228801.0,"Objects":[{"StartTime":228801.0,"EndTime":228801.0,"X":274.0,"Y":25.0}]},{"StartTime":229013.0,"Objects":[{"StartTime":229013.0,"EndTime":229013.0,"X":391.0,"Y":231.0}]},{"StartTime":229224.0,"Objects":[{"StartTime":229224.0,"EndTime":229224.0,"X":160.0,"Y":181.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":229399.0,"EndTime":229399.0,"X":175.200348,"Y":84.64736,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":229646.0,"Objects":[{"StartTime":229646.0,"EndTime":229646.0,"X":257.0,"Y":263.0}]},{"StartTime":229858.0,"Objects":[{"StartTime":229858.0,"EndTime":229858.0,"X":288.0,"Y":39.0}]},{"StartTime":230069.0,"Objects":[{"StartTime":230069.0,"EndTime":230069.0,"X":348.0,"Y":227.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":230244.0,"EndTime":230244.0,"X":257.087128,"Y":263.065033,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":230491.0,"Objects":[{"StartTime":230491.0,"EndTime":230491.0,"X":366.0,"Y":100.0}]},{"StartTime":230703.0,"Objects":[{"StartTime":230703.0,"EndTime":230703.0,"X":160.0,"Y":181.0}]},{"StartTime":230914.0,"Objects":[{"StartTime":230914.0,"EndTime":230914.0,"X":288.0,"Y":39.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":231089.0,"EndTime":231089.0,"X":366.498749,"Y":100.621391,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":231336.0,"Objects":[{"StartTime":231336.0,"EndTime":231336.0,"X":175.0,"Y":84.0}]},{"StartTime":231547.0,"Objects":[{"StartTime":231547.0,"EndTime":231547.0,"X":348.0,"Y":227.0}]},{"StartTime":231759.0,"Objects":[{"StartTime":231759.0,"EndTime":231759.0,"X":184.0,"Y":336.0}]},{"StartTime":231864.0,"Objects":[{"StartTime":231864.0,"EndTime":231864.0,"X":181.0,"Y":283.0}]},{"StartTime":231970.0,"Objects":[{"StartTime":231970.0,"EndTime":231970.0,"X":179.0,"Y":231.0}]},{"StartTime":232075.0,"Objects":[{"StartTime":232075.0,"EndTime":232075.0,"X":176.0,"Y":178.0}]},{"StartTime":232181.0,"Objects":[{"StartTime":232181.0,"EndTime":232181.0,"X":174.0,"Y":126.0}]},{"StartTime":232393.0,"Objects":[{"StartTime":232393.0,"EndTime":232393.0,"X":366.0,"Y":100.0}]},{"StartTime":232604.0,"Objects":[{"StartTime":232604.0,"EndTime":232604.0,"X":268.0,"Y":228.0}]},{"StartTime":232815.0,"Objects":[{"StartTime":232815.0,"EndTime":232815.0,"X":412.0,"Y":280.0}]},{"StartTime":233026.0,"Objects":[{"StartTime":233026.0,"EndTime":233026.0,"X":268.0,"Y":188.0}]},{"StartTime":233237.0,"Objects":[{"StartTime":233237.0,"EndTime":233237.0,"X":451.0,"Y":187.0}]},{"StartTime":233449.0,"Objects":[{"StartTime":233449.0,"EndTime":233449.0,"X":256.0,"Y":152.0}]},{"StartTime":233660.0,"Objects":[{"StartTime":233660.0,"EndTime":233660.0,"X":473.0,"Y":113.0}]},{"StartTime":233871.0,"Objects":[{"StartTime":233871.0,"EndTime":233871.0,"X":328.0,"Y":248.0}]},{"StartTime":234082.0,"Objects":[{"StartTime":234082.0,"EndTime":234082.0,"X":289.0,"Y":31.0}]},{"StartTime":234294.0,"Objects":[{"StartTime":234294.0,"EndTime":234294.0,"X":192.0,"Y":204.0}]},{"StartTime":234505.0,"Objects":[{"StartTime":234505.0,"EndTime":234505.0,"X":410.0,"Y":241.0}]},{"StartTime":234716.0,"Objects":[{"StartTime":234716.0,"EndTime":234716.0,"X":112.0,"Y":188.0}]},{"StartTime":234927.0,"Objects":[{"StartTime":234927.0,"EndTime":234927.0,"X":305.0,"Y":297.0}]},{"StartTime":235139.0,"Objects":[{"StartTime":235139.0,"EndTime":235139.0,"X":36.0,"Y":176.0}]},{"StartTime":235350.0,"Objects":[{"StartTime":235350.0,"EndTime":235350.0,"X":181.0,"Y":344.0}]},{"StartTime":235562.0,"Objects":[{"StartTime":235562.0,"EndTime":235562.0,"X":252.0,"Y":136.0}]},{"StartTime":235773.0,"Objects":[{"StartTime":235773.0,"EndTime":235773.0,"X":84.0,"Y":281.0}]},{"StartTime":235984.0,"Objects":[{"StartTime":235984.0,"EndTime":235984.0,"X":316.0,"Y":188.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":236159.0,"EndTime":236159.0,"X":320.0774,"Y":88.93266,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":236407.0,"Objects":[{"StartTime":236407.0,"EndTime":236407.0,"X":328.0,"Y":268.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":236582.0,"EndTime":236582.0,"X":399.9171,"Y":200.393,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":236829.0,"Objects":[{"StartTime":236829.0,"EndTime":236829.0,"X":276.0,"Y":333.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":237004.0,"EndTime":237004.0,"X":374.878357,"Y":336.5995,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":237252.0,"Objects":[{"StartTime":237252.0,"EndTime":237252.0,"X":316.0,"Y":188.0}]},{"StartTime":237463.0,"Objects":[{"StartTime":237463.0,"EndTime":237463.0,"X":204.0,"Y":296.0}]},{"StartTime":237674.0,"Objects":[{"StartTime":237674.0,"EndTime":237674.0,"X":452.0,"Y":336.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":237849.0,"EndTime":237849.0,"X":469.90686,"Y":232.5382,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":238097.0,"Objects":[{"StartTime":238097.0,"EndTime":238097.0,"X":209.0,"Y":104.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":238272.0,"EndTime":238272.0,"X":227.870361,"Y":207.290421,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":238519.0,"Objects":[{"StartTime":238519.0,"EndTime":238519.0,"X":425.0,"Y":45.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":238588.0,"EndTime":238588.0,"X":477.25058,"Y":50.11147,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":238731.0,"Objects":[{"StartTime":238731.0,"EndTime":238731.0,"X":421.0,"Y":157.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":238800.0,"EndTime":238800.0,"X":473.25058,"Y":162.111465,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":238942.0,"Objects":[{"StartTime":238942.0,"EndTime":238942.0,"X":227.0,"Y":207.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":239011.0,"EndTime":239011.0,"X":174.833221,"Y":201.09433,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":239153.0,"Objects":[{"StartTime":239153.0,"EndTime":239153.0,"X":223.0,"Y":319.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":239222.0,"EndTime":239222.0,"X":170.833221,"Y":313.09433,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":239364.0,"Objects":[{"StartTime":239364.0,"EndTime":239364.0,"X":475.0,"Y":370.0}]},{"StartTime":239576.0,"Objects":[{"StartTime":239576.0,"EndTime":239576.0,"X":496.0,"Y":228.0}]},{"StartTime":239787.0,"Objects":[{"StartTime":239787.0,"EndTime":239787.0,"X":380.0,"Y":344.0}]},{"StartTime":239999.0,"Objects":[{"StartTime":239999.0,"EndTime":239999.0,"X":405.0,"Y":173.0}]},{"StartTime":240209.0,"Objects":[{"StartTime":240209.0,"EndTime":240209.0,"X":272.0,"Y":320.0}]},{"StartTime":240421.0,"Objects":[{"StartTime":240421.0,"EndTime":240421.0,"X":302.0,"Y":114.0}]},{"StartTime":240632.0,"Objects":[{"StartTime":240632.0,"EndTime":240632.0,"X":156.0,"Y":300.0}]},{"StartTime":240844.0,"Objects":[{"StartTime":240844.0,"EndTime":240844.0,"X":192.0,"Y":52.0}]},{"StartTime":241055.0,"Objects":[{"StartTime":241055.0,"EndTime":241055.0,"X":20.0,"Y":164.0}]},{"StartTime":241267.0,"Objects":[{"StartTime":241267.0,"EndTime":241267.0,"X":252.0,"Y":84.0}]},{"StartTime":241477.0,"Objects":[{"StartTime":241477.0,"EndTime":241477.0,"X":40.0,"Y":8.0}]},{"StartTime":241689.0,"Objects":[{"StartTime":241689.0,"EndTime":241689.0,"X":240.0,"Y":164.0}]},{"StartTime":241900.0,"Objects":[{"StartTime":241900.0,"EndTime":241900.0,"X":116.0,"Y":28.0}]},{"StartTime":242111.0,"Objects":[{"StartTime":242111.0,"EndTime":242111.0,"X":80.0,"Y":274.0}]},{"StartTime":242322.0,"Objects":[{"StartTime":242322.0,"EndTime":242322.0,"X":32.0,"Y":88.0}]},{"StartTime":242534.0,"Objects":[{"StartTime":242534.0,"EndTime":242534.0,"X":227.0,"Y":242.0}]},{"StartTime":242745.0,"Objects":[{"StartTime":242745.0,"EndTime":242745.0,"X":218.0,"Y":61.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":242920.0,"EndTime":242920.0,"X":239.304214,"Y":163.81601,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":243167.0,"Objects":[{"StartTime":243167.0,"EndTime":243167.0,"X":131.0,"Y":120.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":243342.0,"EndTime":243342.0,"X":31.3882523,"Y":86.79608,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":243590.0,"Objects":[{"StartTime":243590.0,"EndTime":243590.0,"X":292.0,"Y":32.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":243765.0,"EndTime":243765.0,"X":313.30423,"Y":134.81601,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":244012.0,"Objects":[{"StartTime":244012.0,"EndTime":244012.0,"X":132.0,"Y":204.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":244187.0,"EndTime":244187.0,"X":32.3882523,"Y":170.796082,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":244435.0,"Objects":[{"StartTime":244435.0,"EndTime":244435.0,"X":368.0,"Y":4.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":244821.0,"EndTime":244821.0,"X":393.857056,"Y":151.754578,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":245280.0,"Objects":[{"StartTime":245280.0,"EndTime":245280.0,"X":136.0,"Y":288.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":245560.0,"EndTime":245560.0,"X":28.55529,"Y":254.65509,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":245702.0,"Objects":[{"StartTime":245702.0,"EndTime":245702.0,"X":31.7391319,"Y":257.739136}]},{"StartTime":245914.0,"Objects":[{"StartTime":245914.0,"EndTime":245914.0,"X":196.521729,"Y":236.521729}]},{"StartTime":246020.0,"Objects":[{"StartTime":246020.0,"EndTime":246020.0,"X":200.260864,"Y":240.260864}]},{"StartTime":246125.0,"Objects":[{"StartTime":246125.0,"EndTime":246125.0,"X":204.0,"Y":244.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":246300.0,"EndTime":246300.0,"X":198.885376,"Y":339.263245,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":246547.0,"Objects":[{"StartTime":246547.0,"EndTime":246547.0,"X":100.0,"Y":188.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":246722.0,"EndTime":246722.0,"X":93.48614,"Y":92.7003,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":246970.0,"Objects":[{"StartTime":246970.0,"EndTime":246970.0,"X":120.0,"Y":272.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":247145.0,"EndTime":247145.0,"X":24.73676,"Y":266.885345,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":247393.0,"Objects":[{"StartTime":247393.0,"EndTime":247393.0,"X":176.0,"Y":160.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":247568.0,"EndTime":247568.0,"X":271.263245,"Y":165.114624,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":247815.0,"Objects":[{"StartTime":247815.0,"EndTime":247815.0,"X":277.0,"Y":260.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":247990.0,"EndTime":247990.0,"X":270.486145,"Y":164.7003,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":248238.0,"Objects":[{"StartTime":248238.0,"EndTime":248238.0,"X":357.0,"Y":288.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":248413.0,"EndTime":248413.0,"X":276.222839,"Y":340.022369,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":248660.0,"Objects":[{"StartTime":248660.0,"EndTime":248660.0,"X":341.0,"Y":208.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":248835.0,"EndTime":248835.0,"X":425.827118,"Y":250.5753,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":249083.0,"Objects":[{"StartTime":249083.0,"EndTime":249083.0,"X":276.0,"Y":340.0}]},{"StartTime":249294.0,"Objects":[{"StartTime":249294.0,"EndTime":249294.0,"X":341.0,"Y":208.0}]},{"StartTime":249505.0,"Objects":[{"StartTime":249505.0,"EndTime":249505.0,"X":200.0,"Y":120.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":249680.0,"EndTime":249680.0,"X":101.756859,"Y":119.9113,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":249928.0,"Objects":[{"StartTime":249928.0,"EndTime":249928.0,"X":64.0,"Y":300.0}]},{"StartTime":250139.0,"Objects":[{"StartTime":250139.0,"EndTime":250139.0,"X":152.0,"Y":176.0}]},{"StartTime":250350.0,"Objects":[{"StartTime":250350.0,"EndTime":250350.0,"X":12.0,"Y":196.0}]},{"StartTime":250561.0,"Objects":[{"StartTime":250561.0,"EndTime":250561.0,"X":164.0,"Y":210.0}]},{"StartTime":250773.0,"Objects":[{"StartTime":250773.0,"EndTime":250773.0,"X":32.0,"Y":88.0}]},{"StartTime":250984.0,"Objects":[{"StartTime":250984.0,"EndTime":250984.0,"X":49.0,"Y":269.0}]},{"StartTime":251195.0,"Objects":[{"StartTime":251195.0,"EndTime":251195.0,"X":218.0,"Y":129.0}]},{"StartTime":251406.0,"Objects":[{"StartTime":251406.0,"EndTime":251406.0,"X":293.0,"Y":294.0}]},{"StartTime":251618.0,"Objects":[{"StartTime":251618.0,"EndTime":251618.0,"X":341.0,"Y":84.0}]},{"StartTime":251829.0,"Objects":[{"StartTime":251829.0,"EndTime":251829.0,"X":164.0,"Y":210.0}]},{"StartTime":252040.0,"Objects":[{"StartTime":252040.0,"EndTime":252040.0,"X":400.0,"Y":176.0}]},{"StartTime":252251.0,"Objects":[{"StartTime":252251.0,"EndTime":252251.0,"X":232.0,"Y":80.0}]},{"StartTime":252463.0,"Objects":[{"StartTime":252463.0,"EndTime":252463.0,"X":340.0,"Y":272.0}]},{"StartTime":252674.0,"Objects":[{"StartTime":252674.0,"EndTime":252674.0,"X":456.0,"Y":80.0}]},{"StartTime":252885.0,"Objects":[{"StartTime":252885.0,"EndTime":252885.0,"X":452.0,"Y":316.0}]},{"StartTime":253307.0,"Objects":[{"StartTime":253307.0,"EndTime":253307.0,"X":452.0,"Y":316.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":253482.0,"EndTime":253482.0,"X":474.438171,"Y":213.4255,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":253730.0,"Objects":[{"StartTime":253730.0,"EndTime":253730.0,"X":284.0,"Y":220.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":253905.0,"EndTime":253905.0,"X":306.438171,"Y":117.4255,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":254153.0,"Objects":[{"StartTime":254153.0,"EndTime":254153.0,"X":116.0,"Y":132.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":254328.0,"EndTime":254328.0,"X":138.438171,"Y":29.425499,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":254576.0,"Objects":[{"StartTime":254576.0,"EndTime":254576.0,"X":36.0,"Y":236.0}]},{"StartTime":254998.0,"Objects":[{"StartTime":254998.0,"EndTime":254998.0,"X":36.0,"Y":236.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":255173.0,"EndTime":255173.0,"X":111.50975,"Y":251.103058,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":255421.0,"Objects":[{"StartTime":255421.0,"EndTime":255421.0,"X":204.0,"Y":152.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":255596.0,"EndTime":255596.0,"X":279.509766,"Y":167.103058,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":255843.0,"Objects":[{"StartTime":255843.0,"EndTime":255843.0,"X":356.0,"Y":56.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":256018.0,"EndTime":256018.0,"X":431.509766,"Y":71.10306,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":256266.0,"Objects":[{"StartTime":256266.0,"EndTime":256266.0,"X":356.0,"Y":204.0}]},{"StartTime":256688.0,"Objects":[{"StartTime":256688.0,"EndTime":256688.0,"X":356.0,"Y":204.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":256863.0,"EndTime":256863.0,"X":358.602356,"Y":299.339142,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":257111.0,"Objects":[{"StartTime":257111.0,"EndTime":257111.0,"X":252.0,"Y":184.0}]},{"StartTime":257322.0,"Objects":[{"StartTime":257322.0,"EndTime":257322.0,"X":296.0,"Y":340.0}]},{"StartTime":257533.0,"Objects":[{"StartTime":257533.0,"EndTime":257533.0,"X":192.0,"Y":272.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":257708.0,"EndTime":257708.0,"X":295.660339,"Y":255.2806,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":257956.0,"Objects":[{"StartTime":257956.0,"EndTime":257956.0,"X":117.0,"Y":119.0}]},{"StartTime":258167.0,"Objects":[{"StartTime":258167.0,"EndTime":258167.0,"X":285.0,"Y":31.0}]},{"StartTime":258378.0,"Objects":[{"StartTime":258378.0,"EndTime":258378.0,"X":137.0,"Y":31.0}]},{"StartTime":258589.0,"Objects":[{"StartTime":258589.0,"EndTime":258589.0,"X":305.0,"Y":119.0}]},{"StartTime":258801.0,"Objects":[{"StartTime":258801.0,"EndTime":258801.0,"X":49.0,"Y":55.0}]},{"StartTime":258906.0,"Objects":[{"StartTime":258906.0,"EndTime":258906.0,"X":26.0,"Y":101.0}]},{"StartTime":259012.0,"Objects":[{"StartTime":259012.0,"EndTime":259012.0,"X":32.0,"Y":153.0}]},{"StartTime":259117.0,"Objects":[{"StartTime":259117.0,"EndTime":259117.0,"X":64.0,"Y":194.0}]},{"StartTime":259223.0,"Objects":[{"StartTime":259223.0,"EndTime":259223.0,"X":112.0,"Y":212.0}]},{"StartTime":259435.0,"Objects":[{"StartTime":259435.0,"EndTime":259435.0,"X":255.0,"Y":75.0}]},{"StartTime":259646.0,"Objects":[{"StartTime":259646.0,"EndTime":259646.0,"X":240.0,"Y":252.0}]},{"StartTime":259857.0,"Objects":[{"StartTime":259857.0,"EndTime":259857.0,"X":112.0,"Y":212.0}]},{"StartTime":260068.0,"Objects":[{"StartTime":260068.0,"EndTime":260068.0,"X":236.0,"Y":330.0}]},{"StartTime":260280.0,"Objects":[{"StartTime":260280.0,"EndTime":260280.0,"X":114.0,"Y":133.0}]},{"StartTime":260491.0,"Objects":[{"StartTime":260491.0,"EndTime":260491.0,"X":146.0,"Y":308.0}]},{"StartTime":260702.0,"Objects":[{"StartTime":260702.0,"EndTime":260702.0,"X":204.0,"Y":154.0}]},{"StartTime":260914.0,"Objects":[{"StartTime":260914.0,"EndTime":260914.0,"X":51.0,"Y":304.0}]},{"StartTime":261125.0,"Objects":[{"StartTime":261125.0,"EndTime":261125.0,"X":298.0,"Y":156.0}]},{"StartTime":261336.0,"Objects":[{"StartTime":261336.0,"EndTime":261336.0,"X":28.0,"Y":232.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":261511.0,"EndTime":261511.0,"X":26.3694744,"Y":134.648315,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":261759.0,"Objects":[{"StartTime":261759.0,"EndTime":261759.0,"X":320.0,"Y":228.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":261934.0,"EndTime":261934.0,"X":321.6305,"Y":325.351685,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":262181.0,"Objects":[{"StartTime":262181.0,"EndTime":262181.0,"X":64.0,"Y":208.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":262356.0,"EndTime":262356.0,"X":59.4033928,"Y":109.17572,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":262604.0,"Objects":[{"StartTime":262604.0,"EndTime":262604.0,"X":364.0,"Y":248.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":262779.0,"EndTime":262779.0,"X":367.656372,"Y":346.437134,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":263026.0,"Objects":[{"StartTime":263026.0,"EndTime":263026.0,"X":484.0,"Y":148.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":263306.0,"EndTime":263306.0,"X":348.198273,"Y":146.574341,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":263449.0,"Objects":[{"StartTime":263449.0,"EndTime":263449.0,"X":315.0,"Y":131.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":263624.0,"EndTime":263624.0,"X":216.875748,"Y":124.6084,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":263871.0,"Objects":[{"StartTime":263871.0,"EndTime":263871.0,"X":192.0,"Y":300.0}]},{"StartTime":264083.0,"Objects":[{"StartTime":264083.0,"EndTime":264083.0,"X":264.0,"Y":188.0}]},{"StartTime":264294.0,"Objects":[{"StartTime":264294.0,"EndTime":264294.0,"X":172.0,"Y":208.0}]},{"StartTime":264506.0,"Objects":[{"StartTime":264506.0,"EndTime":264506.0,"X":284.0,"Y":280.0}]},{"StartTime":264716.0,"Objects":[{"StartTime":264716.0,"EndTime":264716.0,"X":160.0,"Y":44.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":264996.0,"EndTime":264996.0,"X":161.425659,"Y":179.801727,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":265139.0,"Objects":[{"StartTime":265139.0,"EndTime":265139.0,"X":172.0,"Y":208.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":265314.0,"EndTime":265314.0,"X":163.511826,"Y":305.6148,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":265562.0,"Objects":[{"StartTime":265562.0,"EndTime":265562.0,"X":104.0,"Y":252.0}]},{"StartTime":265773.0,"Objects":[{"StartTime":265773.0,"EndTime":265773.0,"X":264.0,"Y":352.0}]},{"StartTime":265984.0,"Objects":[{"StartTime":265984.0,"EndTime":265984.0,"X":76.0,"Y":352.0}]},{"StartTime":266195.0,"Objects":[{"StartTime":266195.0,"EndTime":266195.0,"X":248.0,"Y":252.0}]},{"StartTime":266407.0,"Objects":[{"StartTime":266407.0,"EndTime":266407.0,"X":132.0,"Y":112.0}]},{"StartTime":266618.0,"Objects":[{"StartTime":266618.0,"EndTime":266618.0,"X":22.0,"Y":288.0}]},{"StartTime":266829.0,"Objects":[{"StartTime":266829.0,"EndTime":266829.0,"X":22.0,"Y":81.0}]},{"StartTime":267040.0,"Objects":[{"StartTime":267040.0,"EndTime":267040.0,"X":132.0,"Y":270.0}]},{"StartTime":267252.0,"Objects":[{"StartTime":267252.0,"EndTime":267252.0,"X":240.0,"Y":112.0}]},{"StartTime":267463.0,"Objects":[{"StartTime":267463.0,"EndTime":267463.0,"X":350.0,"Y":288.0}]},{"StartTime":267674.0,"Objects":[{"StartTime":267674.0,"EndTime":267674.0,"X":350.0,"Y":81.0}]},{"StartTime":267885.0,"Objects":[{"StartTime":267885.0,"EndTime":267885.0,"X":240.0,"Y":270.0}]},{"StartTime":268097.0,"Objects":[{"StartTime":268097.0,"EndTime":268097.0,"X":512.0,"Y":212.0}]},{"StartTime":268308.0,"Objects":[{"StartTime":268308.0,"EndTime":268308.0,"X":290.0,"Y":94.0}]},{"StartTime":268519.0,"Objects":[{"StartTime":268519.0,"EndTime":268519.0,"X":415.0,"Y":310.0}]},{"StartTime":268730.0,"Objects":[{"StartTime":268730.0,"EndTime":268730.0,"X":417.0,"Y":47.0}]},{"StartTime":268942.0,"Objects":[{"StartTime":268942.0,"EndTime":268942.0,"X":168.0,"Y":180.0}]},{"StartTime":269153.0,"Objects":[{"StartTime":269153.0,"EndTime":269153.0,"X":416.0,"Y":214.0}]},{"StartTime":269364.0,"Objects":[{"StartTime":269364.0,"EndTime":269364.0,"X":225.0,"Y":54.0}]},{"StartTime":269576.0,"Objects":[{"StartTime":269576.0,"EndTime":269576.0,"X":313.0,"Y":302.0}]},{"StartTime":269787.0,"Objects":[{"StartTime":269787.0,"EndTime":269787.0,"X":376.0,"Y":172.0}]},{"StartTime":269998.0,"Objects":[{"StartTime":269998.0,"EndTime":269998.0,"X":177.0,"Y":242.0}]},{"StartTime":270209.0,"Objects":[{"StartTime":270209.0,"EndTime":270209.0,"X":345.0,"Y":147.0}]},{"StartTime":270420.0,"Objects":[{"StartTime":270420.0,"EndTime":270420.0,"X":215.0,"Y":254.0}]},{"StartTime":270632.0,"Objects":[{"StartTime":270632.0,"EndTime":270632.0,"X":325.0,"Y":146.0}]},{"StartTime":270843.0,"Objects":[{"StartTime":270843.0,"EndTime":270843.0,"X":237.0,"Y":249.0}]},{"StartTime":271055.0,"Objects":[{"StartTime":271055.0,"EndTime":271055.0,"X":333.0,"Y":238.0}]},{"StartTime":271266.0,"Objects":[{"StartTime":271266.0,"EndTime":271266.0,"X":230.0,"Y":151.0}]},{"StartTime":271477.0,"Objects":[{"StartTime":271477.0,"EndTime":271477.0,"X":292.0,"Y":312.0}]},{"StartTime":271583.0,"Objects":[{"StartTime":271583.0,"EndTime":272745.0,"X":256.0,"Y":192.0}]},{"StartTime":273167.0,"Objects":[{"StartTime":273167.0,"EndTime":273167.0,"X":163.0,"Y":256.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":273342.0,"EndTime":273342.0,"X":78.18209,"Y":248.905472,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":273590.0,"Objects":[{"StartTime":273590.0,"EndTime":273590.0,"X":68.0,"Y":364.0}]},{"StartTime":273801.0,"Objects":[{"StartTime":273801.0,"EndTime":273801.0,"X":236.0,"Y":324.0}]},{"StartTime":274012.0,"Objects":[{"StartTime":274012.0,"EndTime":274012.0,"X":79.0,"Y":249.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":274187.0,"EndTime":274187.0,"X":88.9388351,"Y":159.550461,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":274435.0,"Objects":[{"StartTime":274435.0,"EndTime":274435.0,"X":280.0,"Y":264.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":274610.0,"EndTime":274610.0,"X":289.938843,"Y":353.449524,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":274857.0,"Objects":[{"StartTime":274857.0,"EndTime":274857.0,"X":420.0,"Y":130.0}]},{"StartTime":275068.0,"Objects":[{"StartTime":275068.0,"EndTime":275068.0,"X":373.0,"Y":261.0}]},{"StartTime":275279.0,"Objects":[{"StartTime":275279.0,"EndTime":275279.0,"X":512.0,"Y":227.0}]},{"StartTime":275491.0,"Objects":[{"StartTime":275491.0,"EndTime":275491.0,"X":354.0,"Y":183.0}]},{"StartTime":275702.0,"Objects":[{"StartTime":275702.0,"EndTime":275702.0,"X":308.0,"Y":358.0}]},{"StartTime":275913.0,"Objects":[{"StartTime":275913.0,"EndTime":275913.0,"X":478.0,"Y":313.0}]},{"StartTime":276125.0,"Objects":[{"StartTime":276125.0,"EndTime":276125.0,"X":245.0,"Y":278.0}]},{"StartTime":276336.0,"Objects":[{"StartTime":276336.0,"EndTime":276336.0,"X":482.0,"Y":205.0}]},{"StartTime":276547.0,"Objects":[{"StartTime":276547.0,"EndTime":276547.0,"X":349.0,"Y":94.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":276722.0,"EndTime":276722.0,"X":354.7944,"Y":183.813278,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":276970.0,"Objects":[{"StartTime":276970.0,"EndTime":276970.0,"X":239.0,"Y":240.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":277145.0,"EndTime":277145.0,"X":157.7837,"Y":226.0501,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":277393.0,"Objects":[{"StartTime":277393.0,"EndTime":277393.0,"X":0.0,"Y":268.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":277568.0,"EndTime":277568.0,"X":81.70373,"Y":254.311035,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":277815.0,"Objects":[{"StartTime":277815.0,"EndTime":277815.0,"X":128.0,"Y":380.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":277990.0,"EndTime":277990.0,"X":143.305069,"Y":299.2002,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":278237.0,"Objects":[{"StartTime":278237.0,"EndTime":278237.0,"X":116.0,"Y":96.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":278412.0,"EndTime":278412.0,"X":101.614624,"Y":177.390518,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":278660.0,"Objects":[{"StartTime":278660.0,"EndTime":278660.0,"X":104.0,"Y":16.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":278835.0,"EndTime":278835.0,"X":36.5809135,"Y":63.969265,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":279082.0,"Objects":[{"StartTime":279082.0,"EndTime":279082.0,"X":180.0,"Y":48.0}]},{"StartTime":279294.0,"Objects":[{"StartTime":279294.0,"EndTime":279294.0,"X":32.0,"Y":140.0}]},{"StartTime":279505.0,"Objects":[{"StartTime":279505.0,"EndTime":279505.0,"X":180.0,"Y":48.0}]},{"StartTime":279717.0,"Objects":[{"StartTime":279717.0,"EndTime":279717.0,"X":140.0,"Y":216.0}]},{"StartTime":279928.0,"Objects":[{"StartTime":279928.0,"EndTime":279928.0,"X":265.0,"Y":71.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":280103.0,"EndTime":280103.0,"X":243.523376,"Y":153.8613,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":280350.0,"Objects":[{"StartTime":280350.0,"EndTime":280350.0,"X":416.0,"Y":248.0}]},{"StartTime":280562.0,"Objects":[{"StartTime":280562.0,"EndTime":280562.0,"X":316.0,"Y":132.0}]},{"StartTime":280773.0,"Objects":[{"StartTime":280773.0,"EndTime":280773.0,"X":252.0,"Y":264.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":280948.0,"EndTime":280948.0,"X":341.449524,"Y":254.061157,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":281196.0,"Objects":[{"StartTime":281196.0,"EndTime":281196.0,"X":484.0,"Y":148.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":281371.0,"EndTime":281371.0,"X":394.550476,"Y":138.061157,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":281618.0,"Objects":[{"StartTime":281618.0,"EndTime":281618.0,"X":426.0,"Y":338.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":281793.0,"EndTime":281793.0,"X":416.945068,"Y":248.456665,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":282041.0,"Objects":[{"StartTime":282041.0,"EndTime":282041.0,"X":326.0,"Y":43.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":282216.0,"EndTime":282216.0,"X":316.061157,"Y":132.449539,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":282463.0,"Objects":[{"StartTime":282463.0,"EndTime":282463.0,"X":296.0,"Y":296.0}]},{"StartTime":282674.0,"Objects":[{"StartTime":282674.0,"EndTime":282674.0,"X":417.0,"Y":249.0}]},{"StartTime":282885.0,"Objects":[{"StartTime":282885.0,"EndTime":282885.0,"X":248.0,"Y":216.0}]},{"StartTime":283097.0,"Objects":[{"StartTime":283097.0,"EndTime":283097.0,"X":321.0,"Y":376.0}]},{"StartTime":283308.0,"Objects":[{"StartTime":283308.0,"EndTime":283308.0,"X":370.0,"Y":163.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":283483.0,"EndTime":283483.0,"X":379.938843,"Y":73.55046,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":283730.0,"Objects":[{"StartTime":283730.0,"EndTime":283730.0,"X":248.0,"Y":216.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":283905.0,"EndTime":283905.0,"X":257.938843,"Y":126.550461,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":284153.0,"Objects":[{"StartTime":284153.0,"EndTime":284153.0,"X":122.0,"Y":266.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":284328.0,"EndTime":284328.0,"X":131.938843,"Y":176.550461,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":284575.0,"Objects":[{"StartTime":284575.0,"EndTime":284575.0,"X":200.0,"Y":280.0}]},{"StartTime":284787.0,"Objects":[{"StartTime":284787.0,"EndTime":284787.0,"X":56.0,"Y":144.0}]},{"StartTime":284998.0,"Objects":[{"StartTime":284998.0,"EndTime":284998.0,"X":69.0,"Y":335.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":285173.0,"EndTime":285173.0,"X":151.50708,"Y":340.3292,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":285420.0,"Objects":[{"StartTime":285420.0,"EndTime":285420.0,"X":213.0,"Y":180.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":285595.0,"EndTime":285595.0,"X":130.326477,"Y":176.450455,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":285843.0,"Objects":[{"StartTime":285843.0,"EndTime":285843.0,"X":304.0,"Y":272.0}]},{"StartTime":285948.0,"Objects":[{"StartTime":285948.0,"EndTime":285948.0,"X":299.0,"Y":228.0}]},{"StartTime":286054.0,"Objects":[{"StartTime":286054.0,"EndTime":286054.0,"X":294.0,"Y":183.0}]},{"StartTime":286159.0,"Objects":[{"StartTime":286159.0,"EndTime":286159.0,"X":288.0,"Y":138.0}]},{"StartTime":286265.0,"Objects":[{"StartTime":286265.0,"EndTime":286265.0,"X":283.0,"Y":94.0}]},{"StartTime":286477.0,"Objects":[{"StartTime":286477.0,"EndTime":286477.0,"X":156.521729,"Y":44.5217361}]},{"StartTime":286583.0,"Objects":[{"StartTime":286583.0,"EndTime":286583.0,"X":160.260864,"Y":48.2608681}]},{"StartTime":286688.0,"Objects":[{"StartTime":286688.0,"EndTime":286688.0,"X":164.0,"Y":52.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":287110.0,"EndTime":287110.0,"X":183.3807,"Y":124.354652,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":287533.0,"EndTime":287533.0,"X":172.208191,"Y":190.150177,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":287955.0,"EndTime":287955.0,"X":124.254967,"Y":247.694046,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":288378.0,"EndTime":288378.0,"X":173.0462,"Y":261.451965,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":288800.0,"EndTime":288800.0,"X":242.3152,"Y":273.1244,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":289223.0,"EndTime":289223.0,"X":282.0523,"Y":336.8299,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":289645.0,"EndTime":289645.0,"X":313.3097,"Y":323.5751,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":290068.0,"EndTime":290068.0,"X":338.3643,"Y":252.795883,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":290490.0,"EndTime":290490.0,"X":410.361755,"Y":235.620316,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":290913.0,"EndTime":290913.0,"X":431.88385,"Y":207.80217,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":291335.0,"EndTime":291335.0,"X":373.0279,"Y":161.46875,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":291758.0,"EndTime":291758.0,"X":367.150818,"Y":92.54223,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":292180.0,"EndTime":292180.0,"X":357.807159,"Y":45.76682,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":292603.0,"EndTime":292603.0,"X":294.6491,"Y":86.36842,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":292990.0,"EndTime":292990.0,"X":228.255249,"Y":76.85775,"StackOffset":{"X":0.0,"Y":0.0}}]},{"StartTime":293238.0,"Objects":[{"StartTime":293238.0,"EndTime":293238.0,"X":231.739136,"Y":79.7391357}]},{"StartTime":293343.0,"Objects":[{"StartTime":293343.0,"EndTime":301900.0,"X":256.0,"Y":192.0}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/1124896.osu b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/1124896.osu new file mode 100644 index 0000000000..8a9b18ae9c --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/1124896.osu @@ -0,0 +1,1122 @@ +osu file format v14 + +[General] +StackLeniency: 0.6 +Mode: 0 + +[Difficulty] +HPDrainRate:6 +CircleSize:3.8 +OverallDifficulty:7.5 +ApproachRate:8.7 +SliderMultiplier:1.5 +SliderTickRate:1 + +[Events] +//Background and Video events +//Break Periods +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples + +[TimingPoints] +1055,422.535211267606,4,2,1,35,1,0 +1055,-111.111111111111,4,2,1,35,0,0 +8660,-111.111111111111,4,2,1,10,0,0 +8871,-111.111111111111,4,2,1,35,0,0 +13942,-111.111111111111,4,2,2,60,0,0 +14470,-111.111111111111,4,2,2,5,0,0 +14576,-100,4,2,2,45,0,0 +25562,-200,4,2,2,40,0,0 +28097,-100,4,2,2,40,0,0 +41618,-100,4,2,2,50,0,0 +55139,-100,4,2,3,45,0,0 +68660,-83.3333333333333,4,2,2,50,0,0 +69294,-50,4,2,2,50,0,0 +69505,-83.3333333333333,4,2,2,50,0,0 +70139,-50,4,2,2,50,0,0 +70350,-83.3333333333333,4,2,2,50,0,0 +70984,-50,4,2,2,50,0,0 +71195,-83.3333333333333,4,2,2,50,0,0 +71829,-50,4,2,2,50,0,0 +72040,-83.3333333333333,4,2,2,50,0,0 +72674,-50,4,2,2,50,0,0 +74576,-55.5555555555556,4,2,1,50,0,0 +75421,-76.9230769230769,4,2,2,60,0,1 +100773,-100,4,2,2,50,0,0 +102463,-83.3333333333333,4,2,2,50,0,0 +115984,-100,4,2,2,50,0,0 +129505,-100,4,2,3,45,0,0 +143026,-83.3333333333333,4,2,2,50,0,0 +143660,-50,4,2,2,50,0,0 +143871,-83.3333333333333,4,2,2,50,0,0 +144505,-50,4,2,2,50,0,0 +144716,-83.3333333333333,4,2,2,50,0,0 +145350,-50,4,2,2,50,0,0 +145562,-83.3333333333333,4,2,2,50,0,0 +146195,-50,4,2,2,50,0,0 +146407,-83.3333333333333,4,2,2,50,0,0 +147040,-50,4,2,2,50,0,0 +148942,-55.5555555555556,4,2,1,50,0,0 +149787,-76.9230769230769,4,2,2,60,0,1 +175139,-100,4,2,2,50,0,0 +175562,-83.3333333333333,4,2,3,50,0,0 +185280,-76.9230769230769,4,2,3,50,0,0 +186970,-71.4285714285714,4,2,3,50,0,0 +190350,-83.3333333333333,4,2,2,50,0,0 +214012,-71.4285714285714,4,2,2,50,0,0 +219083,-71.4285714285714,4,2,2,60,0,1 +244435,-100,4,2,2,50,0,0 +246125,-71.4285714285714,4,2,2,60,0,1 +273167,-83.3333333333333,4,2,2,50,0,0 +286688,-200,4,2,2,50,0,0 +293238,-200,4,2,0,30,0,0 +293343,-200,4,2,0,5,0,0 + +[HitObjects] +92,96,633,5,0,0:0:0:0: +92,96,844,1,0,0:0:0:0: +92,96,1055,6,0,L|76:164,1,67.4999979400635,2|0,3:0|0:0,0:0:0:0: +200,100,1477,2,0,L|184:34,1,67.4999979400635,0|2,0:0|0:3,0:0:0:0: +164,228,1900,1,0,0:0:0:0: +256,240,2111,1,0,0:0:0:0: +340,192,2322,2,0,P|352:160|348:120,1,67.4999979400635,2|0,0:3|0:0,0:0:0:0: +440,200,2745,6,0,P|438:233|450:264,1,67.4999979400635,2|0,0:3|0:0,0:0:0:0: +332,316,3167,1,0,0:0:0:0: +332,316,3378,2,0,B|280:296|224:320|224:320|268:344,1,134.999995880127,2|0,0:3|0:0,0:0:0:0: +332,316,4012,1,2,0:3:0:0: +312,224,4224,1,0,0:0:0:0: +284,132,4435,6,0,P|248:124|216:132,1,67.4999979400635,2|0,0:3|0:0,0:0:0:0: +400,192,4857,2,0,P|436:200|468:192,1,67.4999979400635,2|0,0:3|0:3,0:3:0:0: +312,224,5280,2,0,P|304:260|312:292,1,67.4999979400635,2|0,0:3|0:0,0:0:0:0: +376,108,5702,1,2,0:3:0:0: +376,108,5914,2,0,B|336:132|336:132|232:108,1,134.999995880127,2|0,0:3|0:0,0:0:0:0: +154,122,6547,6,0,P|159:80|174:56,1,67.4999979400635,2|0,0:3|0:0,0:0:0:0: +107,195,6970,2,0,P|73:160|68:132,2,67.4999979400635,2|0|2,0:3|0:0|0:3,0:0:0:0: +216,232,7604,1,0,0:0:0:0: +116,280,7815,6,0,P|76:280|51:263,1,67.4999979400635,2|0,0:3|0:0,0:0:0:0: +176,160,8238,1,0,0:0:0:0: +248,291,8449,2,0,P|292:291|336:335,1,101.249996910095,2|0,0:3|0:0,0:0:0:0: +334,328,8871,2,0,L|318:189,1,134.999995880127,2|0,0:3|0:0,0:0:0:0: +428,184,9505,6,0,L|436:250,1,67.4999979400635,2|0,0:3|0:0,0:0:0:0: +328,128,9928,2,0,L|319:194,1,67.4999979400635,0|2,0:0|0:3,0:0:0:0: +320,108,10350,1,0,0:0:0:0: +308,88,10773,1,2,0:3:0:0: +296,68,11195,6,0,L|212:64,1,67.4999979400635,2|0,0:3|0:0,0:0:0:0: +318,194,11618,1,0,0:0:0:0: +288,52,11829,2,0,L|204:48,1,67.4999979400635,2|0,0:3|0:0,0:0:0:0: +236,248,12252,1,0,0:0:0:0: +299,170,12463,1,2,0:3:0:0: +300,300,12674,1,0,0:0:0:0: +168,204,12885,6,0,L|84:200,2,67.4999979400635,2|0|0,0:3|0:0|0:0,0:0:0:0: +227,332,13519,2,0,L|160:336,1,67.4999979400635,2|0,0:3|0:0,0:0:0:0: +303,366,13942,1,2,0:0:0:0: +302,365,14153,2,0,P|308:332|299:299,1,67.4999979400635,2|0,0:0|0:0,0:0:0:0: +469,258,14576,6,0,L|452:333,1,75,4|0,0:0|0:0,0:0:0:0: +376,256,14998,2,0,L|359:182,1,75,0|2,0:0|0:0,0:0:0:0: +384,80,15421,1,0,0:0:0:0: +282,102,15632,1,2,0:0:0:0: +436,148,15843,1,0,0:0:0:0: +274,186,16055,5,2,0:0:0:0: +274,186,16160,1,0,0:0:0:0: +274,186,16266,2,0,L|257:261,1,75,0|2,0:0|0:0,0:0:0:0: +160,202,16688,2,0,L|143:128,1,75,0|2,0:0|0:0,0:0:0:0: +79,35,17111,1,0,0:0:0:0: +23,123,17322,1,2,0:0:0:0: +161,42,17533,1,0,0:0:0:0: +76,188,17745,1,2,0:0:0:0: +79,35,17956,6,0,L|105:126,1,75,0|2,0:0|0:0,0:0:0:0: +211,104,18378,2,0,L|237:195,1,75,0|2,0:0|0:0,0:0:0:0: +344,170,18801,2,0,L|370:261,1,75,0|2,0:0|0:0,0:0:0:0: +433,132,19224,1,0,0:0:0:0: +372,249,19435,5,2,0:0:0:0: +372,249,19540,1,0,0:0:0:0: +372,249,19646,2,0,P|414:259|452:250,1,75,0|2,0:0|0:0,0:0:0:0: +468,104,20069,1,0,0:0:0:0: +413,180,20280,1,2,0:0:0:0: +324,58,20491,1,2,0:0:0:0: +414,31,20702,1,2,0:0:0:0: +324,151,20914,1,10,0:0:0:0: +244,40,21125,1,2,0:0:0:0: +301,186,21336,6,0,P|242:205|184:174,1,112.5,6|0,0:0|0:0,0:0:0:0: +197,187,21759,2,0,P|190:229|215:287,1,75,0|2,0:0|0:0,0:0:0:0: +287,362,22181,1,0,0:0:0:0: +330,234,22393,1,2,0:0:0:0: +197,260,22604,1,8,0:0:0:0: +360,319,22815,1,2,0:0:0:0: +360,319,23026,6,0,P|419:338|479:313,1,112.5 +465,323,23449,1,0,0:0:0:0: +402,180,23660,1,2,0:0:0:0: +402,180,23871,2,0,L|417:265,1,75,0|2,0:0|0:0,0:0:0:0: +314,145,24294,6,0,L|327:71,1,75,8|2,0:0|0:0,0:0:0:0: +472,72,24716,2,0,L|485:145,1,75,4|2,0:0|0:0,0:0:0:0: +320,222,25139,1,0,0:0:0:0: +235,116,25350,1,2,0:0:0:0: +276,295,25562,5,8,0:0:0:0: +304,305,25667,1,8,0:0:0:0: +333,306,25773,1,8,0:0:0:0: +362,299,25878,1,8,0:0:0:0: +392,280,25984,5,8,0:0:0:0: +425,239,26090,1,8,0:0:0:0: +447,193,26195,1,8,0:0:0:0: +454,143,26301,1,8,0:0:0:0: +452,88,26407,6,0,P|426:34|384:95,1,150,4|0,0:0|0:0,0:0:0:0: +368,160,27463,1,0,0:0:0:0: +487,58,27674,1,12,0:0:0:0: +300,200,28097,6,0,P|288:158|305:117,1,75,12|0,0:0|0:0,0:0:0:0: +377,238,28519,1,10,0:0:0:0: +222,217,28731,1,2,0:0:0:0: +369,92,28942,2,0,P|377:128|366:163,1,75,10|0,0:0|0:0,0:0:0:0: +223,136,29364,2,0,P|214:99|225:64,1,75,10|0,0:0|0:0,0:0:0:0: +251,276,29787,5,10,0:0:0:0: +135,240,29998,1,0,0:0:0:0: +244,356,30209,1,10,0:0:0:0: +137,161,30421,1,0,0:0:0:0: +166,327,30632,1,10,0:0:0:0: +219,187,30843,1,0,0:0:0:0: +68,322,31055,1,10,0:0:0:0: +311,192,31266,1,0,0:0:0:0: +140,89,31477,6,0,P|128:130|145:172,1,75,10|0,0:0|0:0,0:0:0:0: +217,51,31899,1,10,0:0:0:0: +62,72,32111,1,2,0:0:0:0: +209,197,32322,2,0,P|217:160|206:125,1,75,8|0,0:0|0:0,0:0:0:0: +64,168,32744,2,0,P|55:204|66:239,1,75,10|0,0:0|0:0,0:0:0:0: +209,197,33167,6,0,P|172:188|137:199,1,75,8|0,0:0|0:0,0:0:0:0: +136,340,33589,2,0,P|171:351|206:343,1,75,8|0,0:0|0:0,0:0:0:0: +285,167,34012,1,10,0:0:0:0: +308,326,34224,1,2,0:0:0:0: +176,276,34435,1,10,0:0:0:0: +362,263,34646,1,2,0:0:0:0: +184,201,34857,6,0,L|172:305,1,75,14|0,0:0|0:0,0:0:0:0: +118,138,35280,1,10,0:0:0:0: +272,162,35491,1,2,0:0:0:0: +120,57,35702,2,0,P|146:11|197:5,1,75,10|0,0:0|0:0,0:0:0:0: +294,133,36125,2,0,P|267:178|216:184,1,75,10|0,0:0|0:0,0:0:0:0: +243,11,36547,6,0,P|288:37|294:88,1,75,10|0,0:0|0:0,0:0:0:0: +171,183,36970,2,0,P|125:156|119:105,1,75,10|0,0:0|0:0,0:0:0:0: +368,94,37393,1,10,0:0:0:0: +228,243,37604,1,0,0:0:0:0: +222,94,37815,1,10,0:0:0:0: +374,238,38026,1,0,0:0:0:0: +368,94,38238,6,0,L|468:115,1,75,10|0,0:0|0:0,0:0:0:0: +240,170,38660,2,0,L|340:191,1,75,10|0,0:0|0:0,0:0:0:0: +110,240,39083,2,0,L|210:261,1,75,10|0,0:0|0:0,0:0:0:0: +106,321,39505,1,10,0:0:0:0: +148,159,39716,1,2,0:0:0:0: +35,279,39928,5,10,0:0:0:0: +213,325,40139,1,2,0:0:0:0: +61,312,40350,1,8,0:0:0:0: +237,299,40561,1,2,0:0:0:0: +120,92,40773,1,8,0:0:0:0: +124,129,40878,1,8,0:0:0:0: +128,166,40984,1,8,0:0:0:0: +132,203,41089,1,8,0:0:0:0: +136,241,41195,1,12,0:0:0:0: +281,114,41407,5,8,0:0:0:0: +281,114,41512,1,8,0:0:0:0: +281,114,41618,2,0,L|377:107,1,75,12|2,0:0|0:0,0:0:0:0: +292,34,42040,2,0,L|388:27,1,75,0|2,0:0|0:0,0:0:0:0: +400,177,42463,2,0,L|407:273,1,75,0|2,0:0|0:0,0:0:0:0: +480,188,42885,2,0,L|487:284,1,75,0|2,0:0|0:0,0:0:0:0: +330,317,43308,6,0,L|234:310,1,75,0|2,0:0|0:0,0:0:0:0: +319,237,43730,2,0,L|223:230,1,75,0|2,0:0|0:0,0:0:0:0: +129,357,44153,1,0,0:0:0:0: +43,239,44364,1,2,0:0:0:0: +181,284,44576,1,2,0:0:0:0: +43,329,44787,1,2,0:0:0:0: +129,211,44998,6,0,L|136:121,1,75,0|2,0:0|0:0,0:0:0:0: +224,157,45421,2,0,L|217:67,1,75,0|2,0:0|0:0,0:0:0:0: +312,60,45843,1,0,0:0:0:0: +414,106,46055,1,2,0:0:0:0: +401,1,46266,1,0,0:0:0:0: +310,142,46477,5,2,0:0:0:0: +310,142,46583,1,0,0:0:0:0: +310,142,46688,2,0,L|317:232,1,75,0|2,0:0|0:0,0:0:0:0: +405,196,47111,2,0,L|398:286,1,75,0|2,0:0|0:0,0:0:0:0: +280,288,47533,1,0,0:0:0:0: +388,352,47745,1,2,0:0:0:0: +492,176,47956,1,10,0:0:0:0: +465,312,48167,1,2,0:0:0:0: +315,216,48378,6,0,P|271:207|228:227,1,75,4|2,0:0|0:0,0:0:0:0: +280,288,48801,1,0,0:0:0:0: +392,188,49012,2,0,P|367:150|322:134,1,75,2|0,0:0|0:0,0:0:0:0: +472,212,49435,2,0,P|472:166|445:127,1,75,2|0,0:0|0:0,0:0:0:0: +399,270,49857,1,2,0:0:0:0: +341,136,50069,6,0,L|356:42,1,75,0|2,0:0|0:0,0:0:0:0: +430,31,50491,1,0,0:0:0:0: +274,83,50702,1,2,0:0:0:0: +423,111,50914,2,0,L|497:122,1,75,0|2,0:0|0:0,0:0:0:0: +338,215,51336,2,0,L|408:188,1,75,8|2,0:0|0:0,0:0:0:0: +282,268,51759,6,0,P|261:214|276:169,1,75,4|2,0:0|0:0,0:0:0:0: +358,289,52181,1,0,0:0:0:0: +184,202,52393,2,0,P|207:148|249:127,1,75,2|0,0:0|0:0,0:0:0:0: +190,281,52815,1,2,0:0:0:0: +119,158,53026,1,10,0:0:0:0: +262,200,53238,1,0,0:0:0:0: +99,230,53449,6,0,L|123:142,1,75,4|2,0:0|0:0,0:0:0:0: +31,295,53871,2,0,L|7:207,1,75,8|2,0:0|0:0,0:0:0:0: +131,316,54294,1,8,0:0:0:0: +222,242,54505,1,8,0:0:0:0: +118,157,54716,1,8,0:0:0:0: +118,157,54822,1,8,0:0:0:0: +118,157,54928,1,8,0:0:0:0: +226,332,55139,6,0,P|281:349|357:310,1,112.5,4|0,0:0|0:0,0:0:0:0: +332,333,55562,2,0,L|352:238,1,75,2|0,0:0|0:0,0:0:0:0: +289,191,55984,1,2,0:0:0:0: +338,116,56195,1,0,0:0:0:0: +427,103,56407,1,10,0:0:0:0: +502,151,56618,1,0,0:0:0:0: +371,38,56829,6,0,P|316:21|240:60,1,112.5,2|0,0:0|0:0,0:0:0:0: +265,37,57252,2,0,L|245:132,1,75,2|0,0:0|0:0,0:0:0:0: +132,25,57674,2,0,L|159:150,2,112.5,2|0|0,0:0|0:0|0:0,0:0:0:0: +79,150,58519,6,0,P|160:212|192:199,1,112.5,4|0,0:0|0:0,0:0:0:0: +158,212,58942,2,0,L|253:191,1,75,2|0,0:0|0:0,0:0:0:0: +249,110,59364,1,2,0:0:0:0: +324,159,59575,1,0,0:0:0:0: +337,248,59787,1,10,0:0:0:0: +289,323,59998,1,0,0:0:0:0: +406,192,60209,6,0,P|468:273|455:305,1,112.5,2|0,0:0|0:0,0:0:0:0: +469,272,60632,2,0,L|447:366,1,75,2|0,0:0|0:0,0:0:0:0: +337,248,61055,2,0,L|361:363,2,112.5,2|0|0,0:0|0:0|0:0,0:0:0:0: +232,195,61900,6,0,L|210:289,1,75,4|0,0:0|0:0,0:0:0:0: +129,122,62322,2,0,L|146:196,1,75,10|0,0:0|0:0,0:0:0:0: +177,358,62745,1,2,0:0:0:0: +108,282,62956,1,0,0:0:0:0: +286,341,63167,2,0,L|359:357,1,75,10|0,0:0|0:0,0:0:0:0: +410,231,63590,6,0,L|336:247,1,75,2|0,0:0|0:0,0:0:0:0: +465,158,64012,2,0,L|391:141,1,75,10|0,0:0|0:0,0:0:0:0: +226,111,64435,1,2,0:0:0:0: +320,175,64646,1,0,0:0:0:0: +222,34,64857,2,0,P|180:44|159:87,1,75,8|2,0:0|0:0,0:0:0:0: +218,189,65280,6,0,P|176:179|155:136,1,75,4|2,0:0|0:0,0:0:0:0: +296,70,65702,2,0,L|270:164,1,75,0|2,0:0|0:0,0:0:0:0: +236,337,66125,1,0,0:0:0:0: +325,219,66336,1,2,0:0:0:0: +152,247,66547,1,0,0:0:0:0: +316,312,66758,1,2,0:0:0:0: +88,184,66970,6,0,P|46:194|25:237,1,75,0|2,0:0|0:0,0:0:0:0: +172,320,67392,2,0,L|146:226,1,75,0|2,0:0|0:0,0:0:0:0: +194,118,67815,2,0,P|157:95|111:110,1,75,0|2,0:0|0:0,0:0:0:0: +297,315,68238,2,0,L|271:221,1,75,8|2,0:0|0:0,0:0:0:0: +300,75,68660,6,0,L|276:166,1,89.9999972534181,12|0,0:0|0:0,0:0:0:0: +337,56,68977,2,0,L|313:147,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +374,43,69294,2,0,L|354:115,1,75,8|0,0:0|0:0,0:0:0:0: +385,192,69505,6,0,B|417:183|417:183|470:203,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +360,235,69822,2,0,B|391:225|391:225|444:245,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +341,274,70139,2,0,B|372:264|372:264|412:278,1,75,8|0,0:0|0:0,0:0:0:0: +245,332,70350,6,0,P|226:291|239:249,1,89.9999972534181,12|0,0:0|0:0,0:0:0:0: +185,311,70667,2,0,P|200:269|239:248,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +169,248,70984,2,0,P|202:235|237:247,1,75,8|0,0:0|0:0,0:0:0:0: +78,207,71195,6,0,B|66:171|66:171|74:157|74:157|62:118,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +108,176,71512,2,0,B|96:140|96:140|104:126|104:126|92:87,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +143,143,71829,2,0,B|130:108|130:108|138:94|138:94|131:73,1,75,8|0,0:0|0:0,0:0:0:0: +307,58,72040,6,0,P|254:35|207:58,1,89.9999972534181,12|0,0:0|0:0,0:0:0:0: +388,72,72357,2,0,P|335:49|288:72,1,89.9999972534181,12|0,0:0|0:0,0:0:0:0: +454,91,72674,2,0,P|401:68|364:89,1,75,12|0,0:0|0:0,0:0:0:0: +338,180,72885,5,8,0:0:0:0: +269,308,73097,1,8,0:0:0:0: +304,334,73202,1,8,0:0:0:0: +348,344,73308,1,8,0:0:0:0: +391,335,73414,1,8,0:0:0:0: +428,309,73519,1,8,0:0:0:0: +450,271,73625,1,8,0:0:0:0: +453,227,73730,1,8,0:0:0:0: +453,227,74576,6,0,L|490:228,9,22.4999993133545 +506,152,74998,1,12,0:0:0:0: +222,89,75421,5,12,0:0:0:0: +194,259,75632,1,0,0:0:0:0: +320,218,75843,1,8,0:0:0:0: +150,190,76054,1,2,0:0:0:0: +339,335,76266,1,8,0:0:0:0: +372,130,76477,1,2,0:0:0:0: +221,180,76688,1,10,0:0:0:0: +425,212,76899,1,2,0:0:0:0: +285,121,77111,6,0,P|341:109|385:165,1,97.4999955368044,8|0,0:0|0:0,0:0:0:0: +194,259,77533,1,8,0:0:0:0: +323,182,77745,1,2,0:0:0:0: +244,316,77956,2,0,P|200:336|140:312,1,97.4999955368044,8|2,0:0|0:0,0:0:0:0: +245,179,78378,1,10,0:0:0:0: +350,277,78590,1,2,0:0:0:0: +160,228,78801,6,0,L|164:68,1,146.249993305207,8|0,0:0|0:0,0:0:0:0: +194,90,79224,2,0,P|254:105|296:75,1,97.4999955368044,8|2,0:0|0:0,0:0:0:0: +129,0,79646,1,10,0:0:0:0: +22,146,79857,1,2,0:0:0:0: +194,90,80069,1,10,0:0:0:0: +22,33,80280,1,2,0:0:0:0: +129,180,80491,6,0,P|174:195|218:179,1,97.4999955368044,8|2,0:0|0:0,0:0:0:0: +308,80,80913,1,10,0:0:0:0: +280,252,81125,1,0,0:0:0:0: +446,206,81336,1,8,0:0:0:0: +339,60,81547,1,2,0:0:0:0: +511,116,81759,1,10,0:0:0:0: +339,173,81970,1,2,0:0:0:0: +446,26,82181,5,12,0:0:0:0: +280,118,82393,1,0,0:0:0:0: +435,118,82604,1,8,0:0:0:0: +259,26,82816,1,2,0:0:0:0: +339,173,83026,1,8,0:0:0:0: +154,128,83238,1,2,0:0:0:0: +304,88,83449,1,10,0:0:0:0: +157,222,83661,1,2,0:0:0:0: +352,280,83871,5,8,0:0:0:0: +160,173,84083,1,0,0:0:0:0: +339,173,84294,1,8,0:0:0:0: +135,280,84506,1,2,0:0:0:0: +259,130,84716,5,8,0:0:0:0: +65,235,84928,1,2,0:0:0:0: +244,235,85139,1,10,0:0:0:0: +40,129,85351,1,2,0:0:0:0: +300,92,85562,6,0,L|274:200,1,97.4999955368044,8|0,0:0|0:0,0:0:0:0: +192,43,85984,1,8,0:0:0:0: +361,34,86195,1,2,0:0:0:0: +327,233,86407,2,0,L|219:207,1,97.4999955368044,8|2,0:0|0:0,0:0:0:0: +376,125,86829,1,10,0:0:0:0: +385,294,87040,1,2,0:0:0:0: +195,265,87252,6,0,L|221:157,1,97.4999955368044,8|0,0:0|0:0,0:0:0:0: +303,314,87674,1,8,0:0:0:0: +134,323,87885,1,2,0:0:0:0: +177,108,88097,1,8,0:0:0:0: +223,95,88202,1,8,0:0:0:0: +267,114,88308,1,8,0:0:0:0: +291,155,88413,1,8,0:0:0:0: +284,203,88519,1,12,0:0:0:0: +102,204,88731,1,8,0:0:0:0: +224,16,88942,5,12,0:0:0:0: +207,200,89153,1,0,0:0:0:0: +96,112,89364,1,8,0:0:0:0: +113,296,89575,1,2,0:0:0:0: +0,152,89787,1,8,0:0:0:0: +184,169,89998,1,2,0:0:0:0: +16,296,90209,1,10,0:0:0:0: +211,242,90420,1,2,0:0:0:0: +88,52,90632,6,0,L|76:172,1,97.4999955368044,8|0,0:0|0:0,0:0:0:0: +231,2,91055,2,0,L|160:99,1,97.4999955368044,8|2,0:0|0:0,0:0:0:0: +383,22,91477,2,0,L|273:71,1,97.4999955368044,8|2,0:0|0:0,0:0:0:0: +491,110,91900,2,2,L|356:101,1,97.4999955368044,10|2,0:0|0:0,0:0:0:0: +436,284,92322,6,0,L|444:144,1,97.4999955368044,8|0,0:0|0:0,0:0:0:0: +304,159,92745,1,8,0:0:0:0: +304,159,92956,1,2,0:0:0:0: +412,328,93167,6,0,L|420:188,1,97.4999955368044,8|2,0:0|0:0,0:0:0:0: +292,176,93590,1,8,0:0:0:0: +292,176,93801,1,2,0:0:0:0: +392,364,94012,6,2,L|400:224,1,97.4999955368044,10|2,0:0|0:0,0:0:0:0: +280,196,94435,1,8,0:0:0:0: +280,196,94646,1,2,0:0:0:0: +160,155,94857,2,0,P|148:207|192:259,1,97.4999955368044,8|2,0:0|0:0,0:0:0:0: +424,112,95280,2,2,P|436:60|392:8,1,97.4999955368044,10|2,0:0|0:0,0:0:0:0: +224,192,95702,5,12,0:0:0:0: +421,192,95913,1,2,0:0:0:0: +280,56,96125,1,8,0:0:0:0: +280,253,96336,1,2,0:0:0:0: +431,112,96547,1,8,0:0:0:0: +195,112,96758,1,2,0:0:0:0: +364,268,96970,1,10,0:0:0:0: +364,32,97181,1,2,0:0:0:0: +176,264,97393,5,14,0:0:0:0: +426,108,97604,1,2,0:0:0:0: +200,184,97815,1,10,0:0:0:0: +459,264,98026,1,2,0:0:0:0: +200,108,98238,1,8,0:0:0:0: +426,184,98449,1,2,0:0:0:0: +164,32,98660,1,10,0:0:0:0: +447,32,98871,1,2,0:0:0:0: +312,264,99083,6,0,L|304:148,1,97.4999955368044,12|2,0:0|0:0,0:0:0:0: +412,236,99505,1,8,0:0:0:0: +224,224,99716,1,2,0:0:0:0: +420,144,99928,1,8,0:0:0:0: +408,332,100139,1,2,0:0:0:0: +252,136,100350,1,10,0:0:0:0: +191,314,100561,1,2,0:0:0:0: +412,236,100773,6,0,L|504:236,1,75,4|0,0:0|0:0,0:0:0:0: +348,288,101195,2,0,L|256:288,1,75,2|0,0:0|0:0,0:0:0:0: +415,339,101618,2,8,B|435:283|435:283|399:211,1,112.5 +411,235,102040,1,8,0:0:0:0: +347,127,102252,5,8,0:0:0:0: +347,127,102357,1,8,0:0:0:0: +347,127,102463,2,0,P|399:143|455:119,1,89.9999972534181,14|0,0:0|0:0,0:0:0:0: +444,20,102885,1,10,0:0:0:0: +280,60,103097,1,2,0:0:0:0: +433,135,103308,2,0,L|421:243,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +232,120,103731,2,0,L|222:30,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +92,254,104153,5,2,0:0:0:0: +139,123,104364,1,0,0:0:0:0: +0,157,104575,1,10,0:0:0:0: +158,201,104787,1,0,0:0:0:0: +204,26,104998,1,10,0:0:0:0: +34,71,105209,1,0,0:0:0:0: +267,106,105421,1,8,0:0:0:0: +30,179,105632,1,2,0:0:0:0: +163,290,105843,6,0,L|155:166,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +273,144,106266,2,0,P|327:167|371:143,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +512,116,106688,2,0,P|468:108|430:130,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +384,4,107111,2,0,P|360:58|384:102,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +396,288,107533,6,0,P|419:233|395:189,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +408,368,107956,2,0,P|462:346|477:297,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +332,336,108378,1,10,0:0:0:0: +480,244,108590,1,2,0:0:0:0: +332,336,108801,1,10,0:0:0:0: +372,168,109013,1,2,0:0:0:0: +247,313,109224,6,0,P|272:252|252:204,1,89.9999972534181,14|0,0:0|0:0,0:0:0:0: +96,136,109646,1,10,0:0:0:0: +196,252,109858,1,2,0:0:0:0: +260,120,110069,2,0,L|152:132,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +28,236,110491,2,0,L|118:246,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +86,46,110914,6,0,L|95:135,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +186,341,111337,2,0,L|196:251,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +216,88,111759,1,10,0:0:0:0: +95,135,111970,1,0,0:0:0:0: +264,168,112181,1,10,0:0:0:0: +191,8,112393,1,0,0:0:0:0: +142,221,112604,6,0,L|130:329,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +264,168,113026,2,0,L|252:276,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +396,112,113449,2,0,L|384:220,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +312,104,113871,1,10,0:0:0:0: +456,240,114083,1,0,0:0:0:0: +442,48,114294,6,0,P|401:30|360:44,1,89.9999972534181,10|2,0:0|0:0,0:0:0:0: +303,196,114716,2,0,P|343:213|386:201,1,89.9999972534181,10|2,0:0|0:0,0:0:0:0: +208,80,115139,1,8,0:0:0:0: +213,124,115244,1,8,0:0:0:0: +218,169,115350,1,8,0:0:0:0: +224,214,115455,1,8,0:0:0:0: +229,258,115561,1,12,0:0:0:0: +136,192,115773,5,8,0:0:0:0: +136,192,115878,1,8,0:0:0:0: +136,192,115984,2,0,L|40:185,1,75,12|2,0:0|0:0,0:0:0:0: +60,104,116407,2,0,L|156:110,1,75,0|2,0:0|0:0,0:0:0:0: +202,5,116829,2,0,L|209:101,1,75,0|2,0:0|0:0,0:0:0:0: +288,104,117251,2,0,L|293:29,1,75,0|2,0:0|0:0,0:0:0:0: +336,184,117674,6,0,L|240:177,1,75,0|2,0:0|0:0,0:0:0:0: +340,264,118096,2,0,L|414:258,1,75,0|2,0:0|0:0,0:0:0:0: +414,112,118519,1,0,0:0:0:0: +500,230,118730,1,2,0:0:0:0: +362,185,118942,1,10,0:0:0:0: +500,140,119153,1,2,0:0:0:0: +414,258,119364,6,0,L|340:264,1,75,0|2,0:0|0:0,0:0:0:0: +186,173,119787,2,0,L|260:178,1,75,0|2,0:0|0:0,0:0:0:0: +260,292,120209,1,0,0:0:0:0: +169,344,120421,1,2,0:0:0:0: +182,239,120632,1,0,0:0:0:0: +244,372,120843,1,2,0:0:0:0: +104,296,121054,6,0,L|14:303,1,75,0|2,0:0|0:0,0:0:0:0: +186,173,121477,2,0,L|260:178,1,75,0|2,0:0|0:0,0:0:0:0: +104,208,121899,1,0,0:0:0:0: +78,106,122111,1,2,0:0:0:0: +104,248,122322,1,10,0:0:0:0: +177,144,122534,1,2,0:0:0:0: +288,256,122744,6,0,P|244:265|201:245,1,75,4|2,0:0|0:0,0:0:0:0: +216,144,123167,1,0,0:0:0:0: +367,280,123378,2,0,P|342:318|297:334,1,75,2|0,0:0|0:0,0:0:0:0: +450,260,123801,2,0,P|447:305|416:342,1,75,2|0,0:0|0:0,0:0:0:0: +277,260,124223,1,2,0:0:0:0: +332,128,124435,6,0,L|420:160,1,75,0|2,0:0|0:0,0:0:0:0: +367,280,124857,1,0,0:0:0:0: +272,180,125069,1,2,0:0:0:0: +470,129,125280,2,0,P|475:166|460:200,1,75,0|2,0:0|0:0,0:0:0:0: +356,52,125702,1,8,0:0:0:0: +402,153,125914,1,2,0:0:0:0: +232,72,126125,6,0,P|211:126|226:171,1,75,4|2,0:0|0:0,0:0:0:0: +288,124,126547,1,0,0:0:0:0: +134,138,126759,2,0,P|157:192|199:213,1,75,2|0,0:0|0:0,0:0:0:0: +335,212,127181,1,2,0:0:0:0: +212,141,127393,1,8,0:0:0:0: +254,284,127604,1,2,0:0:0:0: +286,130,127815,6,0,L|190:143,1,75,4|2,0:0|0:0,0:0:0:0: +384,51,128237,2,0,L|296:27,1,75,8|2,0:0|0:0,0:0:0:0: +480,108,128660,1,8,0:0:0:0: +396,232,128871,1,8,0:0:0:0: +241,225,129082,1,8,0:0:0:0: +241,225,129188,1,8,0:0:0:0: +241,225,129294,1,8,0:0:0:0: +295,288,129505,6,0,P|244:309|192:292,1,112.5,6|0,0:0|0:0,0:0:0:0: +192,292,129928,2,0,L|176:365,1,75,2|0,0:0|0:0,0:0:0:0: +148,220,130350,1,2,0:0:0:0: +68,187,130561,1,0,0:0:0:0: +36,267,130772,1,10,0:0:0:0: +115,300,130983,1,0,0:0:0:0: +16,127,131195,6,0,P|67:106|124:128,1,112.5,2|0,0:0|0:0,0:0:0:0: +119,124,131618,2,0,L|192:108,1,75,2|0,0:0|0:0,0:0:0:0: +280,44,132040,2,0,L|155:17,2,112.5,2|0|0,0:0|0:0|0:0,0:0:0:0: +96,56,132885,6,0,P|72:105|91:157,1,112.5,6|0,0:0|0:0,0:0:0:0: +91,157,133308,2,0,L|164:140,1,75 +44,216,133731,1,0,0:0:0:0: +123,249,133942,1,0,0:0:0:0: +91,329,134153,1,8,0:0:0:0: +11,296,134364,1,0,0:0:0:0: +200,268,134576,6,0,P|264:280|320:244,1,112.5 +304,260,134998,2,0,L|282:354,1,75 +436,348,135421,2,0,L|413:237,2,112.5,2|0|0,0:0|0:0|0:0,0:0:0:0: +448,168,136266,6,0,P|408:156|364:180,1,75,6|0,0:0|0:0,0:0:0:0: +232,260,136688,2,0,P|272:272|316:248,1,75,10|0,0:0|0:0,0:0:0:0: +340,100,137111,1,2,0:0:0:0: +268,196,137322,1,0,0:0:0:0: +240,48,137533,2,0,L|252:136,1,75,10|0,0:0|0:0,0:0:0:0: +92,44,137956,6,0,P|132:32|172:44,1,75,2|0,0:0|0:0,0:0:0:0: +168,180,138378,2,0,P|132:192|94:177,1,75,10|0,0:0|0:0,0:0:0:0: +12,56,138801,1,2,0:0:0:0: +132,112,139012,1,0,0:0:0:0: +44,236,139223,2,0,P|20:207|20:171,1,75,10|2,0:0|0:0,0:0:0:0: +244,172,139646,6,0,P|244:208|220:236,1,75,4|2,0:0|0:0,0:0:0:0: +216,104,140069,2,0,P|215:67|239:39,1,75,0|2,0:0|0:0,0:0:0:0: +436,68,140491,1,0,0:0:0:0: +289,88,140702,1,2,0:0:0:0: +459,156,140913,1,0,0:0:0:0: +317,50,141124,1,2,0:0:0:0: +336,232,141336,6,0,L|326:306,1,75,0|2,0:0|0:0,0:0:0:0: +468,230,141759,2,0,L|458:155,1,75,0|2,0:0|0:0,0:0:0:0: +436,324,142181,2,0,L|510:333,1,75,0|2,0:0|0:0,0:0:0:0: +336,124,142604,2,0,L|261:133,1,75,8|2,0:0|0:0,0:0:0:0: +210,89,143026,6,0,P|208:140|183:171,1,89.9999972534181,12|0,0:0|0:0,0:0:0:0: +261,132,143343,2,0,P|223:166|183:170,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +256,184,143660,2,0,P|204:181|181:167,1,75,8|0,0:0|0:0,0:0:0:0: +124,70,143871,6,0,L|108:173,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +96,247,144188,2,0,L|112:144,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +184,170,144505,2,0,L|79:153,1,75,8|0,0:0|0:0,0:0:0:0: +261,132,144716,6,8,L|368:150,1,89.9999972534181,12|0,0:0|0:0,0:0:0:0: +336,84,145033,2,8,L|398:172,1,89.9999972534181,12|0,0:0|0:0,0:0:0:0: +428,96,145350,2,8,L|412:189,1,75,12|0,0:0|0:0,0:0:0:0: +411,278,145562,6,8,P|456:273|497:240,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +324,276,145878,2,8,P|367:265|417:282,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +252,272,146195,2,8,P|295:282|340:265,1,75,8|0,0:0|0:0,0:0:0:0: +317,119,146407,6,8,L|287:227,1,89.9999972534181,12|0,0:0|0:0,0:0:0:0: +240,74,146724,2,8,L|268:182,1,89.9999972534181,12|0,0:0|0:0,0:0:0:0: +166,90,147040,2,8,L|237:160,1,75,12|0,0:0|0:0,0:0:0:0: +170,152,147252,5,8,0:0:0:0: +38,120,147464,1,8,0:0:0:0: +12,155,147569,1,8,0:0:0:0: +2,199,147675,1,8,0:0:0:0: +11,242,147781,1,8,0:0:0:0: +37,279,147886,1,8,0:0:0:0: +75,301,147992,1,8,0:0:0:0: +119,304,148097,1,8,0:0:0:0: +245,208,148942,6,0,L|268:196,9,22.4999993133545 +232,288,149364,1,12,0:0:0:0: +217,38,149787,5,12,0:0:0:0: +56,98,149998,1,0,0:0:0:0: +155,187,150209,1,8,0:0:0:0: +94,26,150420,1,2,0:0:0:0: +63,262,150632,5,8,0:0:0:0: +257,188,150843,1,2,0:0:0:0: +138,82,151054,1,10,0:0:0:0: +212,275,151265,1,2,0:0:0:0: +288,60,151477,6,0,L|260:184,1,97.4999955368044,8|0,0:0|0:0,0:0:0:0: +204,48,151899,1,8,0:0:0:0: +346,175,152111,1,2,0:0:0:0: +130,263,152322,6,0,L|158:138,1,97.4999955368044,8|2,0:0|0:0,0:0:0:0: +232,244,152744,1,10,0:0:0:0: +56,170,152956,1,2,0:0:0:0: +64,352,153167,6,0,P|136:316|220:364,1,146.249993305207,8|0,0:0|0:0,0:0:0:0: +224,348,153590,2,0,P|284:363|326:333,1,97.4999955368044,8|2,0:0|0:0,0:0:0:0: +376,140,154012,1,10,0:0:0:0: +269,286,154223,1,2,0:0:0:0: +441,230,154435,1,10,0:0:0:0: +269,173,154646,1,2,0:0:0:0: +376,320,154857,6,0,P|436:335|478:305,1,97.4999955368044,8|2,0:0|0:0,0:0:0:0: +496,136,155280,1,10,0:0:0:0: +420,256,155491,1,0,0:0:0:0: +330,80,155702,1,10,0:0:0:0: +223,226,155913,1,2,0:0:0:0: +395,170,156125,1,10,0:0:0:0: +223,113,156336,1,2,0:0:0:0: +330,260,156547,5,12,0:0:0:0: +408,92,156759,1,0,0:0:0:0: +168,168,156970,1,8,0:0:0:0: +408,244,157182,1,2,0:0:0:0: +256,44,157392,5,8,0:0:0:0: +264,296,157604,1,2,0:0:0:0: +436,168,157815,1,10,0:0:0:0: +188,92,158027,1,2,0:0:0:0: +212,336,158238,5,8,0:0:0:0: +290,168,158450,1,0,0:0:0:0: +50,244,158661,1,8,0:0:0:0: +290,320,158871,1,2,0:0:0:0: +138,120,159083,5,8,0:0:0:0: +146,372,159295,1,2,0:0:0:0: +318,244,159506,1,10,0:0:0:0: +70,168,159716,1,2,0:0:0:0: +324,164,159928,6,0,P|384:197|399:266,1,97.4999955368044,8|0,0:0|0:0,0:0:0:0: +291,354,160350,1,8,0:0:0:0: +209,190,160562,1,2,0:0:0:0: +377,321,160773,6,0,P|317:355|255:335,1,97.4999955368044,8|0,0:0|0:0,0:0:0:0: +209,190,161195,1,10,0:0:0:0: +396,220,161407,1,2,0:0:0:0: +200,283,161618,6,0,P|198:212|240:163,1,97.4999955368044,8|2,0:0|0:0,0:0:0:0: +396,221,162040,1,8,0:0:0:0: +290,353,162251,1,2,0:0:0:0: +264,56,162463,5,8,0:0:0:0: +277,102,162568,1,8,0:0:0:0: +290,149,162674,1,8,0:0:0:0: +304,196,162779,1,8,0:0:0:0: +317,243,162885,1,12,0:0:0:0: +172,164,163097,1,8,0:0:0:0: +416,108,163308,5,12,0:0:0:0: +232,91,163519,1,0,0:0:0:0: +400,12,163730,1,8,0:0:0:0: +383,196,163941,1,2,0:0:0:0: +217,0,164153,5,8,0:0:0:0: +200,184,164364,1,2,0:0:0:0: +313,16,164575,1,10,0:0:0:0: +112,32,164786,1,2,0:0:0:0: +200,184,164998,6,0,P|216:136|204:88,1,97.4999955368044,8|0,0:0|0:0,0:0:0:0: +112,256,165421,2,0,P|96:304|108:352,1,97.4999955368044,8|2,0:0|0:0,0:0:0:0: +116,176,165843,2,0,P|68:160|20:172,1,97.4999955368044,8|2,0:0|0:0,0:0:0:0: +196,264,166266,2,2,P|244:280|292:268,1,97.4999955368044,10|2,0:0|0:0,0:0:0:0: +248,60,166688,5,8,0:0:0:0: +248,201,166899,1,0,0:0:0:0: +333,55,167111,1,8,0:0:0:0: +248,201,167322,1,2,0:0:0:0: +424,101,167533,5,8,0:0:0:0: +248,201,167744,1,2,0:0:0:0: +468,224,167956,1,10,0:0:0:0: +292,124,168167,1,2,0:0:0:0: +364,328,168378,5,8,0:0:0:0: +364,158,168589,1,0,0:0:0:0: +244,304,168801,1,8,0:0:0:0: +464,327,169013,1,2,0:0:0:0: +192,248,169224,6,0,L|184:359,1,97.4999955368044,8|2,0:0|0:0,0:0:0:0: +508,272,169646,2,2,L|500:161,1,97.4999955368044,10|2,0:0|0:0,0:0:0:0: +268,60,170068,5,12,0:0:0:0: +268,257,170279,1,2,0:0:0:0: +404,116,170491,1,8,0:0:0:0: +207,116,170702,1,2,0:0:0:0: +348,267,170913,5,8,0:0:0:0: +348,31,171124,1,2,0:0:0:0: +192,200,171336,1,8,0:0:0:0: +428,200,171547,1,2,0:0:0:0: +268,60,171759,5,12,0:0:0:0: +386,236,171970,1,2,0:0:0:0: +386,11,172181,1,8,0:0:0:0: +268,187,172393,1,2,0:0:0:0: +149,55,172604,5,10,0:0:0:0: +30,231,172815,1,2,0:0:0:0: +30,7,173026,1,10,0:0:0:0: +149,183,173238,1,2,0:0:0:0: +30,7,173449,6,0,L|58:127,1,97.4999955368044,12|0,0:0|0:0,0:0:0:0: +240,64,173871,2,0,L|122:28,1,97.4999955368044,8|2,0:0|0:0,0:0:0:0: +80,216,174294,2,0,L|169:131,1,97.4999955368044,8|2,0:0|0:0,0:0:0:0: +124,280,174716,1,10,0:0:0:0: +56,128,174928,1,2,0:0:0:0: +216,212,175139,6,0,L|200:312,1,75,4|0,0:0|0:0,0:0:0:0: +296,216,175562,6,0,L|276:332,1,89.9999972534181,2|0,0:0|0:0,0:0:0:0: +376,208,175984,6,8,L|352:352,1,134.999995880127 +353,341,176406,1,8,0:0:0:0: +328,144,176618,5,8,0:0:0:0: +328,144,176723,1,8,0:0:0:0: +328,144,176829,2,0,P|376:128|432:160,1,89.9999972534181,12|0,0:0|0:0,0:0:0:0: +248,152,177252,2,0,P|200:168|144:136,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +344,120,177674,2,0,P|392:104|448:136,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +236,168,178097,2,0,P|188:184|132:152,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +192,272,178519,6,0,P|208:320|176:376,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +152,172,178942,2,0,P|136:124|168:68,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +228,284,179364,2,0,P|244:332|212:388,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +116,152,179787,2,0,P|100:104|132:48,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +100,256,180209,6,0,P|52:272|-4:240,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +240,184,180632,2,0,P|288:168|344:200,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +288,336,181055,2,0,L|284:232,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +432,84,181477,2,0,L|420:204,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +368,352,181900,6,0,L|364:248,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +512,100,182322,2,0,L|500:220,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +272,104,182745,2,0,L|392:116,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +356,132,183062,1,0,0:0:0:0: +352,156,183167,1,8,0:0:0:0: +276,20,183378,1,0,0:0:0:0: +304,240,183590,6,0,P|264:256|216:240,1,89.9999972534181,12|0,0:0|0:0,0:0:0:0: +392,272,184012,2,0,P|425:298|436:348,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +376,184,184435,2,0,P|382:141|419:107,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +320,336,184857,1,8,0:0:0:0: +260,180,185069,1,0,0:0:0:0: +176,304,185280,6,0,B|160:372|160:372|144:344,1,97.4999955368044,8|0,0:0|0:0,0:0:0:0: +207,176,185702,2,0,B|273:155|273:155|257:183,1,97.4999955368044,8|0,0:0|0:0,0:0:0:0: +84,224,186125,2,0,B|33:176|33:176|65:176,1,97.4999955368044,8|0,0:0|0:0,0:0:0:0: +244,260,186547,1,8,0:0:0:0: +88,300,186759,1,0,0:0:0:0: +128,44,186970,6,0,L|136:188,1,104.999996795654,8|0,0:0|0:0,0:0:0:0: +340,208,187393,2,0,L|348:64,1,104.999996795654,8|0,0:0|0:0,0:0:0:0: +244,260,187815,1,8,0:0:0:0: +424,240,188026,1,0,0:0:0:0: +211,244,188238,1,8,0:0:0:0: +377,317,188449,1,0,0:0:0:0: +196,336,188660,5,8,0:0:0:0: +224,154,188871,1,0,0:0:0:0: +367,270,189083,1,8,0:0:0:0: +132,216,189294,1,0,0:0:0:0: +338,135,189505,1,8,0:0:0:0: +330,186,189610,1,8,0:0:0:0: +322,238,189716,1,8,0:0:0:0: +314,290,189821,1,8,0:0:0:0: +306,342,189927,1,12,0:0:0:0: +228,252,190139,1,8,0:0:0:0: +420,216,190350,5,12,0:0:0:0: +247,160,190562,1,0,0:0:0:0: +406,252,190773,1,8,0:0:0:0: +368,74,190985,1,2,0:0:0:0: +373,269,191195,1,8,0:0:0:0: +507,146,191407,1,2,0:0:0:0: +335,271,191618,1,10,0:0:0:0: +508,325,191830,1,2,0:0:0:0: +219,271,192040,6,0,P|199:219|231:155,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +279,327,192463,2,0,P|217:353|163:323,1,89.9999972534181,8|2,0:0|0:0,0:0:0:0: +335,271,192885,2,0,P|361:332|331:387,1,89.9999972534181,8|2,0:0|0:0,0:0:0:0: +279,219,193308,2,2,P|340:193|395:223,1,89.9999972534181,10|2,0:0|0:0,0:0:0:0: +108,296,193731,6,0,L|112:124,1,134.999995880127,8|0,0:0|0:0,0:0:0:0: +72,100,194153,2,0,P|120:116|172:84,1,89.9999972534181,8|2,0:0|0:0,0:0:0:0: +24,24,194576,1,8,0:0:0:0: +36,168,194787,1,2,0:0:0:0: +116,40,194998,1,10,0:0:0:0: +184,184,195209,1,2,0:0:0:0: +256,56,195421,5,8,0:0:0:0: +112,155,195632,1,2,0:0:0:0: +276,224,195843,2,0,L|268:132,1,89.9999972534181 +160,72,196266,1,10,0:0:0:0: +16,171,196477,1,2,0:0:0:0: +180,240,196688,1,8,0:0:0:0: +72,108,196899,1,2,0:0:0:0: +76,328,197111,5,12,0:0:0:0: +249,274,197323,1,0,0:0:0:0: +83,171,197534,1,8,0:0:0:0: +217,295,197745,1,2,0:0:0:0: +218,119,197956,1,8,0:0:0:0: +179,297,198168,1,2,0:0:0:0: +317,223,198379,1,10,0:0:0:0: +144,279,198591,1,2,0:0:0:0: +295,284,198801,6,0,L|271:164,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +489,254,199224,2,0,L|465:374,1,89.9999972534181,8|2,0:0|0:0,0:0:0:0: +277,195,199646,2,0,L|253:75,1,89.9999972534181,8|2,0:0|0:0,0:0:0:0: +506,165,200069,2,2,L|482:285,1,89.9999972534181,10|2,0:0|0:0,0:0:0:0: +301,42,200491,6,0,P|361:10|425:38,1,134.999995880127,8|0,0:0|0:0,0:0:0:0: +432,52,200914,2,0,L|420:164,1,89.9999972534181,8|2,0:0|0:0,0:0:0:0: +262,226,201336,1,8,0:0:0:0: +352,103,201547,1,2,0:0:0:0: +352,256,201759,1,10,0:0:0:0: +262,132,201970,1,2,0:0:0:0: +407,179,202181,5,8,0:0:0:0: +240,253,202393,1,2,0:0:0:0: +418,291,202604,1,8,0:0:0:0: +296,155,202815,1,2,0:0:0:0: +315,338,203026,1,8,0:0:0:0: +281,308,203131,1,8,0:0:0:0: +239,292,203237,1,8,0:0:0:0: +195,291,203342,1,8,0:0:0:0: +152,306,203448,1,12,0:0:0:0: +328,380,203660,1,8,0:0:0:0: +312,204,203871,5,12,0:0:0:0: +120,266,204083,1,0,0:0:0:0: +284,136,204294,1,8,0:0:0:0: +241,334,204506,1,2,0:0:0:0: +210,130,204716,5,8,0:0:0:0: +359,267,204928,1,2,0:0:0:0: +152,180,205139,1,10,0:0:0:0: +345,120,205351,1,2,0:0:0:0: +84,136,205562,6,0,P|72:176|88:228,1,89.9999972534181,8|0,0:0|0:0,0:0:0:0: +284,136,205984,2,0,P|296:96|280:44,1,89.9999972534181,8|2,0:0|0:0,0:0:0:0: +184,248,206407,2,0,P|224:260|276:244,1,89.9999972534181,8|2,0:0|0:0,0:0:0:0: +180,28,206829,2,2,P|140:16|88:32,1,89.9999972534181,10|2,0:0|0:0,0:0:0:0: +153,305,207252,6,0,P|173:233|137:163,1,134.999995880127,12|0,0:0|0:0,0:0:0:0: +140,160,207674,2,0,P|100:148|48:164,1,89.9999972534181,8|2,0:0|0:0,0:0:0:0: +72,336,208097,5,8,0:0:0:0: +256,292,208308,1,2,0:0:0:0: +100,224,208519,1,10,0:0:0:0: +204,381,208730,1,2,0:0:0:0: +351,209,208942,5,8,0:0:0:0: +178,305,209153,1,2,0:0:0:0: +312,344,209364,1,8,0:0:0:0: +217,171,209576,1,2,0:0:0:0: +472,144,209787,5,8,0:0:0:0: +264,259,209998,1,2,0:0:0:0: +425,306,210209,1,10,0:0:0:0: +311,98,210421,1,2,0:0:0:0: +332,312,210632,5,12,0:0:0:0: +396,100,210843,1,2,0:0:0:0: +192,160,211055,1,8,0:0:0:0: +403,224,211266,1,2,0:0:0:0: +328,24,211477,5,8,0:0:0:0: +255,267,211688,1,2,0:0:0:0: +488,198,211900,1,10,0:0:0:0: +247,125,212111,1,2,0:0:0:0: +392,312,212322,5,12,0:0:0:0: +334,66,212533,1,2,0:0:0:0: +342,351,212745,1,8,0:0:0:0: +372,100,212956,1,2,0:0:0:0: +251,373,213167,5,8,0:0:0:0: +402,170,213378,1,2,0:0:0:0: +136,327,213590,1,10,0:0:0:0: +382,270,213801,1,2,0:0:0:0: +212,144,214012,6,0,P|200:204|224:244,1,104.999996795654,12|2,0:0|0:0,0:0:0:0: +152,88,214435,2,0,P|106:47|59:48,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +232,64,214857,2,0,P|289:44|312:3,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +80,120,215280,1,10,0:0:0:0: +272,188,215491,1,2,0:0:0:0: +192,8,215702,6,0,B|183:98|183:98|216:72,1,104.999996795654,12|2,0:0|0:0,0:0:0:0: +384,64,216125,2,0,B|314:122|314:122|355:126,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +432,244,216547,1,8,0:0:0:0: +260,264,216759,1,8,0:0:0:0: +328,123,216970,1,8,0:0:0:0: +333,175,217075,1,8,0:0:0:0: +338,227,217181,1,8,0:0:0:0: +344,279,217286,1,8,0:0:0:0: +349,331,217392,1,8,0:0:0:0: +349,331,218238,5,8,0:0:0:0: +310,323,218343,1,8,0:0:0:0: +273,317,218449,1,8,0:0:0:0: +236,312,218554,1,8,0:0:0:0: +198,306,218660,5,8,0:0:0:0: +253,296,218765,1,8,0:0:0:0: +309,287,218871,1,8,0:0:0:0: +365,278,218976,1,8,0:0:0:0: +421,268,219082,5,12,0:0:0:0: +348,92,219294,1,0,0:0:0:0: +205,236,219505,5,8,0:0:0:0: +381,163,219717,1,2,0:0:0:0: +237,24,219928,5,8,0:0:0:0: +310,200,220140,1,2,0:0:0:0: +449,52,220350,5,10,0:0:0:0: +273,125,220562,1,2,0:0:0:0: +392,272,220773,6,0,P|441:288|509:276,1,104.999996795654,8|0,0:0|0:0,0:0:0:0: +257,249,221195,2,0,P|206:264|159:314,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +380,189,221618,2,0,P|411:146|420:79,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +317,308,222040,2,2,P|347:350|409:380,1,104.999996795654,10|2,0:0|0:0,0:0:0:0: +297,175,222463,6,0,P|297:122|248:24,1,157.499995193482,8|0,0:0|0:0,0:0:0:0: +253,29,222885,2,0,P|308:68|384:64,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +168,34,223308,1,10,0:0:0:0: +63,216,223519,1,2,0:0:0:0: +220,125,223731,1,10,0:0:0:0: +10,125,223942,1,2,0:0:0:0: +168,216,224153,5,10,0:0:0:0: +63,34,224364,1,2,0:0:0:0: +0,264,224576,2,0,P|60:296|120:268,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +144,140,224998,6,0,L|153:48,1,52.4999983978272,8|8,0:0|0:0,0:0:0:0: +208,304,225209,2,0,L|202:356,1,52.4999983978272,8|8,0:0|0:0,0:0:0:0: +256,144,225421,2,0,L|265:52,1,52.4999983978272,8|8,0:0|0:0,0:0:0:0: +320,308,225632,2,0,L|314:360,1,52.4999983978272,8|8,0:0|0:0,0:0:0:0: +425,265,225843,5,12,0:0:0:0: +256,188,226055,1,0,0:0:0:0: +425,102,226266,5,8,0:0:0:0: +299,248,226477,1,2,0:0:0:0: +271,53,226688,5,8,0:0:0:0: +369,225,226900,1,2,0:0:0:0: +176,183,227111,5,10,0:0:0:0: +369,151,227322,1,2,0:0:0:0: +274,339,227533,5,8,0:0:0:0: +307,116,227745,1,0,0:0:0:0: +458,279,227956,5,8,0:0:0:0: +256,187,228168,1,2,0:0:0:0: +458,83,228379,5,10,0:0:0:0: +308,256,228590,1,2,0:0:0:0: +274,25,228801,5,10,0:0:0:0: +391,231,229013,1,2,0:0:0:0: +160,181,229224,6,0,P|159:106|212:65,1,104.999996795654,8|0,0:0|0:0,0:0:0:0: +257,263,229646,1,8,0:0:0:0: +288,39,229858,1,2,0:0:0:0: +348,227,230069,6,0,P|282:266|220:241,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +366,100,230491,1,10,0:0:0:0: +160,181,230703,1,2,0:0:0:0: +288,39,230914,6,0,P|353:76|372:145,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +175,84,231336,1,8,0:0:0:0: +348,227,231547,1,2,0:0:0:0: +184,336,231759,5,8,0:0:0:0: +181,283,231864,1,8,0:0:0:0: +179,231,231970,1,8,0:0:0:0: +176,178,232075,1,8,0:0:0:0: +174,126,232181,1,12,0:0:0:0: +366,100,232393,1,8,0:0:0:0: +268,228,232604,5,12,0:0:0:0: +412,280,232815,1,0,0:0:0:0: +268,188,233026,5,8,0:0:0:0: +451,187,233237,1,2,0:0:0:0: +256,152,233449,5,8,0:0:0:0: +473,113,233660,1,2,0:0:0:0: +328,248,233871,5,10,0:0:0:0: +289,31,234082,1,2,0:0:0:0: +192,204,234294,5,8,0:0:0:0: +410,241,234505,1,0,0:0:0:0: +112,188,234716,5,8,0:0:0:0: +305,297,234927,1,2,0:0:0:0: +36,176,235139,5,10,0:0:0:0: +181,344,235350,1,2,0:0:0:0: +252,136,235562,5,10,0:0:0:0: +84,281,235773,1,2,0:0:0:0: +316,188,235984,6,0,P|333:134|317:84,1,104.999996795654,8|0,0:0|0:0,0:0:0:0: +328,268,236407,2,0,P|378:242|401:195,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +276,333,236829,2,0,P|329:350|379:334,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +316,188,237252,1,10,0:0:0:0: +204,296,237463,1,2,0:0:0:0: +452,336,237674,6,0,L|470:232,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +209,104,238097,2,0,L|228:208,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +425,45,238519,6,0,L|517:54,1,52.4999983978272,8|8,0:0|0:0,0:0:0:0: +421,157,238731,2,0,L|513:166,1,52.4999983978272,8|8,0:0|0:0,0:0:0:0: +227,207,238942,2,0,L|174:201,1,52.4999983978272,8|8,0:0|0:0,0:0:0:0: +223,319,239153,2,0,L|170:313,1,52.4999983978272,8|8,0:0|0:0,0:0:0:0: +475,370,239364,5,12,0:0:0:0: +496,228,239576,1,2,0:0:0:0: +380,344,239787,5,8,0:0:0:0: +405,173,239999,1,2,0:0:0:0: +272,320,240209,5,8,0:0:0:0: +302,114,240421,1,2,0:0:0:0: +156,300,240632,5,8,0:0:0:0: +192,52,240844,1,2,0:0:0:0: +20,164,241055,5,12,0:0:0:0: +252,84,241267,1,0,0:0:0:0: +40,8,241477,5,8,0:0:0:0: +240,164,241689,1,2,0:0:0:0: +116,28,241900,5,8,0:0:0:0: +80,274,242111,1,2,0:0:0:0: +32,88,242322,5,8,0:0:0:0: +227,242,242534,1,2,0:0:0:0: +218,61,242745,6,0,L|241:172,1,104.999996795654,12|0,0:0|0:0,0:0:0:0: +131,120,243167,2,0,L|23:84,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +292,32,243590,2,0,L|315:143,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +132,204,244012,2,2,L|24:168,1,104.999996795654,10|2,0:0|0:0,0:0:0:0: +368,4,244435,6,0,L|396:164,1,150,12|0,0:0|0:0,0:0:0:0: +136,288,245280,2,8,L|20:252,1,112.5 +28,254,245702,1,8,0:0:0:0: +204,244,245914,5,8,0:0:0:0: +204,244,246020,1,8,0:0:0:0: +204,244,246125,2,0,P|220:296|188:348,1,104.999996795654,12|2,0:0|0:0,0:0:0:0: +100,188,246547,2,0,P|78:141|94:92,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +120,272,246970,2,0,P|68:288|16:256,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +176,160,247393,2,2,P|228:144|280:176,1,104.999996795654,10|2,0:0|0:0,0:0:0:0: +277,260,247815,6,0,P|255:213|271:164,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +357,288,248238,2,0,P|327:329|276:340,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +341,208,248660,2,2,P|392:212|426:251,1,104.999996795654,10|2,0:0|0:0,0:0:0:0: +276,340,249083,1,10,0:0:0:0: +341,208,249294,1,2,0:0:0:0: +200,120,249505,6,0,P|152:104|92:128,1,104.999996795654,12|2,0:0|0:0,0:0:0:0: +64,300,249928,1,8,0:0:0:0: +152,176,250139,1,2,0:0:0:0: +12,196,250350,1,8,0:0:0:0: +164,210,250561,1,2,0:0:0:0: +32,88,250773,1,10,0:0:0:0: +49,269,250984,1,2,0:0:0:0: +218,129,251195,5,8,0:0:0:0: +293,294,251406,1,2,0:0:0:0: +341,84,251618,1,8,0:0:0:0: +164,210,251829,1,2,0:0:0:0: +400,176,252040,1,10,0:0:0:0: +232,80,252251,1,2,0:0:0:0: +340,272,252463,1,10,0:0:0:0: +456,80,252674,1,2,0:0:0:0: +452,316,252885,5,12,0:0:0:0: +452,316,253307,2,0,L|480:188,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +284,220,253730,2,0,L|312:92,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +116,132,254153,2,2,L|144:4,1,104.999996795654,10|2,0:0|0:0,0:0:0:0: +36,236,254576,5,12,0:0:0:0: +36,236,254998,2,0,B|120:232|120:232|104:268,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +204,152,255421,2,2,B|288:148|288:148|272:184,1,104.999996795654,10|2,0:0|0:0,0:0:0:0: +356,56,255843,2,2,B|440:52|440:52|424:88,1,104.999996795654,10|2,0:0|0:0,0:0:0:0: +356,204,256266,5,12,0:0:0:0: +356,204,256688,2,0,P|376:248|344:312,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +252,184,257111,1,8,0:0:0:0: +296,340,257322,1,2,0:0:0:0: +192,272,257533,2,2,L|316:252,1,104.999996795654,10|2,0:0|0:0,0:0:0:0: +117,119,257956,5,12,0:0:0:0: +285,31,258167,1,2,0:0:0:0: +137,31,258378,1,8,0:0:0:0: +305,119,258589,1,2,0:0:0:0: +49,55,258801,1,8,0:0:0:0: +26,101,258906,1,8,0:0:0:0: +32,153,259012,1,8,0:0:0:0: +64,194,259117,1,8,0:0:0:0: +112,212,259223,1,12,0:0:0:0: +255,75,259435,1,8,0:0:0:0: +240,252,259646,5,12,0:0:0:0: +112,212,259857,1,0,0:0:0:0: +236,330,260068,1,8,0:0:0:0: +114,133,260280,1,2,0:0:0:0: +146,308,260491,1,8,0:0:0:0: +204,154,260702,1,2,0:0:0:0: +51,304,260914,1,10,0:0:0:0: +298,156,261125,1,2,0:0:0:0: +28,232,261336,6,0,P|44:180|16:124,1,104.999996795654,12|0,0:0|0:0,0:0:0:0: +320,228,261759,2,0,P|304:280|332:336,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +64,208,262181,2,2,P|76:149|40:90,1,104.999996795654,10|2,0:0|0:0,0:0:0:0: +364,248,262604,2,2,P|351:307|387:365,1,104.999996795654,10|2,0:0|0:0,0:0:0:0: +484,148,263026,6,4,B|448:184|448:184|320:136,1,157.499995193482,12|0,0:0|0:0,0:0:0:0: +315,131,263449,2,0,P|268:112|218:124,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +192,300,263871,1,8,0:0:0:0: +264,188,264083,1,2,0:0:0:0: +172,208,264294,1,10,0:0:0:0: +284,280,264506,1,2,0:0:0:0: +160,44,264716,6,0,B|124:80|124:80|172:208,1,157.499995193482,12|0,0:0|0:0,0:0:0:0: +172,208,265139,2,0,P|184:258|164:305,1,104.999996795654,8|2,0:0|0:0,0:0:0:0: +104,252,265562,1,10,0:0:0:0: +264,352,265773,1,2,0:0:0:0: +76,352,265984,1,10,0:0:0:0: +248,252,266195,1,2,0:0:0:0: +132,112,266407,5,12,0:0:0:0: +22,288,266618,1,2,0:0:0:0: +22,81,266829,1,8,0:0:0:0: +132,270,267040,1,2,0:0:0:0: +240,112,267252,1,8,0:0:0:0: +350,288,267463,1,2,0:0:0:0: +350,81,267674,1,8,0:0:0:0: +240,270,267885,1,2,0:0:0:0: +512,212,268097,5,12,0:0:0:0: +290,94,268308,1,2,0:0:0:0: +415,310,268519,1,8,0:0:0:0: +417,47,268730,1,2,0:0:0:0: +168,180,268942,1,8,0:0:0:0: +416,214,269153,1,2,0:0:0:0: +225,54,269364,1,10,0:0:0:0: +313,302,269576,1,2,0:0:0:0: +376,172,269787,5,12,0:0:0:0: +177,242,269998,1,2,0:0:0:0: +345,147,270209,1,8,0:0:0:0: +215,254,270420,1,2,0:0:0:0: +325,146,270632,1,8,0:0:0:0: +237,249,270843,1,2,0:0:0:0: +333,238,271055,1,8,0:0:0:0: +230,151,271266,1,2,0:0:0:0: +292,312,271477,1,12,0:0:0:0: +256,192,271583,12,0,272745,0:0:0:0: +163,256,273167,6,0,P|123:240|67:256,1,89.9999972534181,14|0,0:0|0:0,0:0:0:0: +68,364,273590,1,10,0:0:0:0: +236,324,273801,1,2,0:0:0:0: +79,249,274012,2,0,L|91:141,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +280,264,274435,2,0,L|290:354,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +420,130,274857,5,2,0:0:0:0: +373,261,275068,1,0,0:0:0:0: +512,227,275279,1,10,0:0:0:0: +354,183,275491,1,0,0:0:0:0: +308,358,275702,1,10,0:0:0:0: +478,313,275913,1,0,0:0:0:0: +245,278,276125,1,8,0:0:0:0: +482,205,276336,1,2,0:0:0:0: +349,94,276547,6,0,L|357:218,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +239,240,276970,2,0,P|185:217|141:241,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +0,268,277393,2,0,P|44:276|82:254,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +128,380,277815,2,0,P|152:326|128:282,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +116,96,278237,6,0,P|93:151|117:195,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +104,16,278660,2,0,P|50:38|35:87,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +180,48,279082,1,10,0:0:0:0: +32,140,279294,1,2,0:0:0:0: +180,48,279505,1,10,0:0:0:0: +140,216,279717,1,2,0:0:0:0: +265,71,279928,6,0,P|240:132|260:184,1,89.9999972534181,14|0,0:0|0:0,0:0:0:0: +416,248,280350,1,10,0:0:0:0: +316,132,280562,1,2,0:0:0:0: +252,264,280773,2,0,L|360:252,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +484,148,281196,2,0,L|394:138,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +426,338,281618,6,0,L|417:249,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +326,43,282041,2,0,L|316:133,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +296,296,282463,1,10,0:0:0:0: +417,249,282674,1,0,0:0:0:0: +248,216,282885,1,10,0:0:0:0: +321,376,283097,1,0,0:0:0:0: +370,163,283308,6,0,L|382:55,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +248,216,283730,2,0,L|260:108,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +122,266,284153,2,0,L|134:158,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +200,280,284575,1,10,0:0:0:0: +56,144,284787,1,0,0:0:0:0: +69,335,284998,6,0,P|110:353|152:340,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +213,180,285420,2,0,P|173:163|131:176,1,89.9999972534181,10|0,0:0|0:0,0:0:0:0: +304,272,285843,1,8,0:0:0:0: +299,228,285948,1,8,0:0:0:0: +294,183,286054,1,8,0:0:0:0: +288,138,286159,1,8,0:0:0:0: +283,94,286265,1,12,0:0:0:0: +164,52,286477,5,8,0:0:0:0: +164,52,286583,1,8,0:0:0:0: +164,52,286688,2,0,B|194:164|194:164|114:260|114:260|236:263|236:263|299:364|299:364|339:251|339:251|455:226|455:226|361:152|361:152|373:36|373:36|275:99|275:99|218:72,1,1124.99994039536,4|0,0:0|0:0,0:1:0:0: +228,76,293238,5,0,0:0:0:0: +256,192,293343,12,0,301900,0:0:0:0: diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic-expected-conversion.json b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/basic-expected-conversion.json similarity index 100% rename from osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic-expected-conversion.json rename to osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/basic-expected-conversion.json diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic.osu b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/basic.osu similarity index 96% rename from osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic.osu rename to osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/basic.osu index 40b4409760..abd2ff2ee6 100644 --- a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic.osu +++ b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/basic.osu @@ -1,27 +1,27 @@ -osu file format v14 - -[Difficulty] -HPDrainRate:6 -CircleSize:4 -OverallDifficulty:7 -ApproachRate:8.3 -SliderMultiplier:1.6 -SliderTickRate:1 - -[TimingPoints] -500,500,4,2,1,50,1,0 -13426,-100,4,3,1,45,0,0 -14884,-100,4,2,1,50,0,0 - -[HitObjects] -96,192,500,6,0,L|416:192,2,320 -256,192,3000,12,0,4000,0:0:0:0: -256,192,4500,12,0,5500,0:0:0:0: -256,192,6000,12,0,6500,0:0:0:0: -256,128,7000,6,0,L|352:128,4,80 -32,192,8500,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800 -256,192,11500,12,0,12000,0:0:0:0: -512,320,12500,6,0,B|0:256|0:256|512:96|512:96|256:32,1,1280 -256,256,17000,6,0,L|160:256,4,80 -256,192,18500,12,0,19450,0:0:0:0: -216,231,19875,6,0,B|216:135|280:135|344:135|344:199|344:263|248:327|248:327|120:327|120:327|56:39|408:39|408:39|472:150|408:342,1,1280 +osu file format v14 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:7 +ApproachRate:8.3 +SliderMultiplier:1.6 +SliderTickRate:1 + +[TimingPoints] +500,500,4,2,1,50,1,0 +13426,-100,4,3,1,45,0,0 +14884,-100,4,2,1,50,0,0 + +[HitObjects] +96,192,500,6,0,L|416:192,2,320 +256,192,3000,12,0,4000,0:0:0:0: +256,192,4500,12,0,5500,0:0:0:0: +256,192,6000,12,0,6500,0:0:0:0: +256,128,7000,6,0,L|352:128,4,80 +32,192,8500,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800 +256,192,11500,12,0,12000,0:0:0:0: +512,320,12500,6,0,B|0:256|0:256|512:96|512:96|256:32,1,1280 +256,256,17000,6,0,L|160:256,4,80 +256,192,18500,12,0,19450,0:0:0:0: +216,231,19875,6,0,B|216:135|280:135|344:135|344:199|344:263|248:327|248:327|120:327|120:327|56:39|408:39|408:39|472:150|408:342,1,1280 diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json similarity index 100% rename from osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json rename to osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/colinear-perfect-curve.osu similarity index 100% rename from osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu rename to osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/colinear-perfect-curve.osu diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/diffcalc-test.osu b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/diffcalc-test.osu similarity index 100% rename from osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/diffcalc-test.osu rename to osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/diffcalc-test.osu diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/multi-segment-slider-expected-conversion.json b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/multi-segment-slider-expected-conversion.json similarity index 100% rename from osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/multi-segment-slider-expected-conversion.json rename to osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/multi-segment-slider-expected-conversion.json diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/multi-segment-slider.osu b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/multi-segment-slider.osu similarity index 100% rename from osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/multi-segment-slider.osu rename to osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/multi-segment-slider.osu diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider-expected-conversion.json b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/nan-slider-expected-conversion.json similarity index 100% rename from osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider-expected-conversion.json rename to osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/nan-slider-expected-conversion.json diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider.osu b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/nan-slider.osu similarity index 100% rename from osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider.osu rename to osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/nan-slider.osu diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/old-stacking-expected-conversion.json b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/old-stacking-expected-conversion.json similarity index 100% rename from osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/old-stacking-expected-conversion.json rename to osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/old-stacking-expected-conversion.json diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/old-stacking.osu b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/old-stacking.osu similarity index 100% rename from osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/old-stacking.osu rename to osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/old-stacking.osu diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/repeat-slider-expected-conversion.json b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/repeat-slider-expected-conversion.json similarity index 100% rename from osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/repeat-slider-expected-conversion.json rename to osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/repeat-slider-expected-conversion.json diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/repeat-slider.osu b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/repeat-slider.osu similarity index 100% rename from osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/repeat-slider.osu rename to osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/repeat-slider.osu diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/slider-paths-edge-case-expected-conversion.json b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/slider-paths-edge-case-expected-conversion.json new file mode 100644 index 0000000000..5b04027cd6 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/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.Tests/Resources/Testing/Beatmaps/slider-paths-edge-case.osu b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/slider-paths-edge-case.osu new file mode 100644 index 0000000000..1b6cd0417b --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/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.Tests/Resources/Testing/Beatmaps/slider-ticks-edge-case-expected-conversion.json b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/slider-ticks-edge-case-expected-conversion.json new file mode 100644 index 0000000000..0bfe776dc7 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/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": 7843.0, + "EndTime": 7843.0, + "X": 33.7820168, + "Y": 208.9957, + "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 + } + } + ] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/slider-ticks-edge-case.osu b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/slider-ticks-edge-case.osu new file mode 100644 index 0000000000..daf35e1d2b --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/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/slider-ticks-expected-conversion.json b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/slider-ticks-expected-conversion.json similarity index 100% rename from osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks-expected-conversion.json rename to osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/slider-ticks-expected-conversion.json diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks.osu b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/slider-ticks.osu similarity index 100% rename from osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks.osu rename to osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/slider-ticks.osu diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/uneven-repeat-slider-expected-conversion.json b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/uneven-repeat-slider-expected-conversion.json new file mode 100644 index 0000000000..dda9078e57 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/uneven-repeat-slider-expected-conversion.json @@ -0,0 +1,579 @@ +{ + "Mappings": [ + { + "StartTime": 369.0, + "Objects": [ + { + "StartTime": 369.0, + "EndTime": 369.0, + "X": 127.0, + "Y": 194.0, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 450.0, + "EndTime": 450.0, + "X": 166.53389, + "Y": 193.8691, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 532.0, + "EndTime": 532.0, + "X": 206.555847, + "Y": 193.736572, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 614.0, + "EndTime": 614.0, + "X": 246.57782, + "Y": 193.60405, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 696.0, + "EndTime": 696.0, + "X": 286.5998, + "Y": 193.471527, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 778.0, + "EndTime": 778.0, + "X": 326.621765, + "Y": 193.339, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 860.0, + "EndTime": 860.0, + "X": 366.6437, + "Y": 193.206482, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 942.0, + "EndTime": 942.0, + "X": 406.66568, + "Y": 193.073959, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 970.0, + "EndTime": 970.0, + "X": 420.331726, + "Y": 193.0287, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 997.0, + "EndTime": 997.0, + "X": 407.153748, + "Y": 193.072342, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 1079.0, + "EndTime": 1079.0, + "X": 367.131775, + "Y": 193.204865, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 1161.0, + "EndTime": 1161.0, + "X": 327.1098, + "Y": 193.337387, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 1243.0, + "EndTime": 1243.0, + "X": 287.08783, + "Y": 193.46991, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 1325.0, + "EndTime": 1325.0, + "X": 247.0659, + "Y": 193.602432, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 1407.0, + "EndTime": 1407.0, + "X": 207.043915, + "Y": 193.734955, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 1489.0, + "EndTime": 1489.0, + "X": 167.021988, + "Y": 193.867477, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 1571.0, + "EndTime": 1571.0, + "X": 127.0, + "Y": 194.0, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 1653.0, + "EndTime": 1653.0, + "X": 167.021988, + "Y": 193.867477, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 1735.0, + "EndTime": 1735.0, + "X": 207.043976, + "Y": 193.734955, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 1817.0, + "EndTime": 1817.0, + "X": 247.065887, + "Y": 193.602432, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 1899.0, + "EndTime": 1899.0, + "X": 287.08783, + "Y": 193.46991, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 1981.0, + "EndTime": 1981.0, + "X": 327.1098, + "Y": 193.337387, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 2062.0, + "EndTime": 2062.0, + "X": 366.643738, + "Y": 193.206482, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 2144.0, + "EndTime": 2144.0, + "X": 406.665649, + "Y": 193.073959, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 2172.0, + "EndTime": 2172.0, + "X": 420.331726, + "Y": 193.0287, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 2199.0, + "EndTime": 2199.0, + "X": 407.153748, + "Y": 193.072342, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 2281.0, + "EndTime": 2281.0, + "X": 367.1318, + "Y": 193.204865, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 2363.0, + "EndTime": 2363.0, + "X": 327.1098, + "Y": 193.337387, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 2445.0, + "EndTime": 2445.0, + "X": 287.08783, + "Y": 193.46991, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 2527.0, + "EndTime": 2527.0, + "X": 247.065887, + "Y": 193.602432, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 2609.0, + "EndTime": 2609.0, + "X": 207.043976, + "Y": 193.734955, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 2691.0, + "EndTime": 2691.0, + "X": 167.021988, + "Y": 193.867477, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 2773.0, + "EndTime": 2773.0, + "X": 127.0, + "Y": 194.0, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 2855.0, + "EndTime": 2855.0, + "X": 167.021988, + "Y": 193.867477, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 2937.0, + "EndTime": 2937.0, + "X": 207.043976, + "Y": 193.734955, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 3019.0, + "EndTime": 3019.0, + "X": 247.065948, + "Y": 193.602432, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 3101.0, + "EndTime": 3101.0, + "X": 287.087952, + "Y": 193.46991, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 3183.0, + "EndTime": 3183.0, + "X": 327.109772, + "Y": 193.337387, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 3265.0, + "EndTime": 3265.0, + "X": 367.131775, + "Y": 193.204865, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 3347.0, + "EndTime": 3347.0, + "X": 407.153748, + "Y": 193.072342, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 3374.0, + "EndTime": 3374.0, + "X": 420.331726, + "Y": 193.0287, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 3401.0, + "EndTime": 3401.0, + "X": 407.153748, + "Y": 193.072342, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 3483.0, + "EndTime": 3483.0, + "X": 367.131775, + "Y": 193.204865, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 3565.0, + "EndTime": 3565.0, + "X": 327.109772, + "Y": 193.337387, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 3647.0, + "EndTime": 3647.0, + "X": 287.087952, + "Y": 193.46991, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 3729.0, + "EndTime": 3729.0, + "X": 247.065948, + "Y": 193.602432, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 3811.0, + "EndTime": 3811.0, + "X": 207.043976, + "Y": 193.734955, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 3893.0, + "EndTime": 3893.0, + "X": 167.021988, + "Y": 193.867477, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 3975.0, + "EndTime": 3975.0, + "X": 127.0, + "Y": 194.0, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 4057.0, + "EndTime": 4057.0, + "X": 167.021988, + "Y": 193.867477, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 4139.0, + "EndTime": 4139.0, + "X": 207.043976, + "Y": 193.734955, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 4221.0, + "EndTime": 4221.0, + "X": 247.065948, + "Y": 193.602432, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 4303.0, + "EndTime": 4303.0, + "X": 287.087952, + "Y": 193.46991, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 4385.0, + "EndTime": 4385.0, + "X": 327.109772, + "Y": 193.337387, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 4467.0, + "EndTime": 4467.0, + "X": 367.131775, + "Y": 193.204865, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 4549.0, + "EndTime": 4549.0, + "X": 407.153748, + "Y": 193.072342, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 4540.0, + "EndTime": 4540.0, + "X": 420.331726, + "Y": 193.0287, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + } + ] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/uneven-repeat-slider.osu b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/uneven-repeat-slider.osu similarity index 100% rename from osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/uneven-repeat-slider.osu rename to osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/uneven-repeat-slider.osu diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/very-fast-slider.osu b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/very-fast-slider.osu new file mode 100644 index 0000000000..58ef36e70c --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/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/Resources/Testing/Beatmaps/zero-length-sliders.osu b/osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/zero-length-sliders.osu similarity index 100% rename from osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/zero-length-sliders.osu rename to osu.Game.Rulesets.Osu.Tests/Resources/Testing/Beatmaps/zero-length-sliders.osu 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/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 5da54f8fcf..5f5596cbb3 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs @@ -25,16 +25,16 @@ namespace osu.Game.Rulesets.Osu.Tests [Resolved] private OsuConfigManager config { get; set; } = null!; - private readonly List> pools; + private readonly List> pools = new List>(); - public TestSceneDrawableJudgement() + [TestCaseSource(nameof(validResults))] + public void Test(HitResult result) { - pools = new List>(); - - foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType().Skip(1)) - showResult(result); + showResult(result); } + private static IEnumerable validResults => Enum.GetValues().Skip(1); + [Test] public void TestHitLightingDisabled() { @@ -72,32 +72,33 @@ namespace osu.Game.Rulesets.Osu.Tests pools.Add(pool = new DrawablePool(1)); else { - 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); + pool = pools[poolIndex]; + ((Container)pool.Parent!).Clear(false); } var container = new Container { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - pool, - pool.Get(j => j.Apply(new JudgementResult(new HitObject - { - StartTime = Time.Current - }, new Judgement()) - { - Type = result, - }, null)).With(j => - { - j.Anchor = Anchor.Centre; - j.Origin = Anchor.Centre; - }) - } + Child = pool, }; + // Must be scheduled so the pool is loaded before we try and retrieve from it. + Schedule(() => + { + container.Add(pool.Get(j => j.Apply(new JudgementResult(new HitObject + { + StartTime = Time.Current + }, new Judgement()) + { + Type = result, + }, null)).With(j => + { + j.Anchor = Anchor.Centre; + j.Origin = Anchor.Centre; + })); + }); + poolIndex++; return container; }); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index c84a6ab70f..e6696032ae 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -94,16 +94,16 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("load content", loadContent); - AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale); + AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.CursorScale.Value == OsuCursor.GetScaleForCircleSize(circleSize) * userScale); AddStep("set user scale to 1", () => config.SetValue(OsuSetting.GameplayCursorSize, 1f)); - AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize)); + AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.CursorScale.Value == OsuCursor.GetScaleForCircleSize(circleSize)); AddStep("turn off autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, false)); - AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == 1); + AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.CursorScale.Value == 1); AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale)); - AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == userScale); + AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.CursorScale.Value == userScale); } [Test] diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index 0314afc1ac..30b0451a3b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -34,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] @@ -66,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; } @@ -84,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; @@ -99,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 }); @@ -131,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/TestSceneHitCircleLateFade.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs index 3c32b4fa65..7824f26251 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() @@ -131,13 +167,15 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + 500, Position = new Vector2(250), - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(0, 100), }) }); + 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/TestSceneLegacyHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs new file mode 100644 index 0000000000..e460da9bd5 --- /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/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/TestSceneOsuLegacyHealthProcessor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuLegacyHealthProcessor.cs new file mode 100644 index 0000000000..a7ae06a9ce --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuLegacyHealthProcessor.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 NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Scoring; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public class TestSceneOsuLegacyHealthProcessor + { + [Test] + public void TestNoBreak() + { + OsuLegacyHealthProcessor hp = new OsuLegacyHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 2000 } + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(1.4E-5).Within(0.1E-5)); + } + + [Test] + public void TestSingleBreak() + { + OsuLegacyHealthProcessor hp = new OsuLegacyHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 2000 } + }, + Breaks = + { + new BreakPeriod(500, 1500) + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(4.3E-5).Within(0.1E-5)); + } + + [Test] + public void TestOverlappingBreak() + { + OsuLegacyHealthProcessor hp = new OsuLegacyHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 2000 } + }, + Breaks = + { + new BreakPeriod(500, 1400), + new BreakPeriod(750, 1500), + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(4.3E-5).Within(0.1E-5)); + } + + [Test] + public void TestSequentialBreak() + { + OsuLegacyHealthProcessor hp = new OsuLegacyHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 2000 } + }, + Breaks = + { + new BreakPeriod(500, 1000), + new BreakPeriod(1000, 1500), + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(4.3E-5).Within(0.1E-5)); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs index 2e62689e2c..25fe8170b1 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs @@ -133,8 +133,11 @@ namespace osu.Game.Rulesets.Osu.Tests } [Test] - public void TestSimpleInput() + public void TestSimpleInput([Values] bool disableMouseButtons) { + // OsuSetting.MouseDisableButtons should not affect touch taps + AddStep($"{(disableMouseButtons ? "disable" : "enable")} mouse buttons", () => config.SetValue(OsuSetting.MouseDisableButtons, disableMouseButtons)); + beginTouch(TouchSource.Touch1); assertKeyCounter(1, 0); @@ -468,7 +471,7 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestInputWhileMouseButtonsDisabled() { - AddStep("Disable mouse buttons", () => config.SetValue(OsuSetting.MouseDisableButtons, true)); + AddStep("Disable gameplay taps", () => config.SetValue(OsuSetting.TouchDisableGameplayTaps, true)); beginTouch(TouchSource.Touch1); @@ -620,6 +623,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Release all touches", () => { config.SetValue(OsuSetting.MouseDisableButtons, false); + config.SetValue(OsuSetting.TouchDisableGameplayTaps, false); foreach (TouchSource source in InputManager.CurrentState.Touch.ActiveSources) InputManager.EndTouch(new Touch(source, osuInputManager.ScreenSpaceDrawQuad.Centre)); }); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs index 0bb27cff0f..25d0b0a3d3 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs @@ -1,38 +1,69 @@ // 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.Cursor; +using osu.Framework.Testing; +using osu.Game.Configuration; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Screens.Play; +using osu.Game.Tests.Gameplay; using osu.Game.Tests.Visual; +using osuTK; namespace osu.Game.Rulesets.Osu.Tests { public partial class TestSceneResumeOverlay : OsuManualInputManagerTestScene { + private ManualOsuInputManager osuInputManager = null!; + private CursorContainer cursor = null!; + private ResumeOverlay resume = null!; + + private bool resumeFired; + + private OsuConfigManager localConfig = null!; + + [Cached] + private GameplayState gameplayState; + public TestSceneResumeOverlay() { - ManualOsuInputManager osuInputManager; - CursorContainer cursor; - ResumeOverlay resume; + gameplayState = TestGameplayState.Create(new OsuRuleset()); + } - bool resumeFired = false; + [BackgroundDependencyLoader] + private void load() + { + Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage)); + } - Child = osuInputManager = new ManualOsuInputManager(new OsuRuleset().RulesetInfo) + protected override void LoadComplete() + { + base.LoadComplete(); + AddSliderStep("cursor size", 0.1f, 2f, 1f, v => localConfig.SetValue(OsuSetting.GameplayCursorSize, v)); + AddSliderStep("circle size", 0f, 10f, 0f, val => { - Children = new Drawable[] - { - cursor = new CursorContainer(), - resume = new OsuResumeOverlay - { - GameplayCursor = cursor - }, - } - }; + gameplayState.Beatmap.Difficulty.CircleSize = val; + SetUp(); + }); - resume.ResumeAction = () => resumeFired = true; + AddToggleStep("auto size", v => localConfig.SetValue(OsuSetting.AutoCursorSize, v)); + } + + [SetUp] + public void SetUp() => Schedule(loadContent); + + [TestCase(1)] + [TestCase(0.5f)] + [TestCase(2)] + public void TestResume(float cursorSize) + { + AddStep($"set cursor size to {cursorSize}", () => localConfig.SetValue(OsuSetting.GameplayCursorSize, cursorSize)); AddStep("move mouse to center", () => InputManager.MoveMouseTo(ScreenSpaceDrawQuad.Centre)); AddStep("show", () => resume.Show()); @@ -41,11 +72,39 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("click", () => osuInputManager.GameClick()); AddAssert("not dismissed", () => !resumeFired && resume.State.Value == Visibility.Visible); - AddStep("move mouse back", () => InputManager.MoveMouseTo(ScreenSpaceDrawQuad.Centre)); + AddStep("move mouse just out of range", () => + { + var resumeOverlay = this.ChildrenOfType().Single(); + var resumeOverlayCursor = resumeOverlay.ChildrenOfType().Single(); + + Vector2 offset = resumeOverlay.ToScreenSpace(new Vector2(OsuCursor.SIZE / 2)) - resumeOverlay.ToScreenSpace(Vector2.Zero); + InputManager.MoveMouseTo(resumeOverlayCursor.ScreenSpaceDrawQuad.Centre - offset - new Vector2(1)); + }); + + AddStep("click", () => osuInputManager.GameClick()); + AddAssert("not dismissed", () => !resumeFired && resume.State.Value == Visibility.Visible); + + AddStep("move mouse just within range", () => + { + var resumeOverlay = this.ChildrenOfType().Single(); + var resumeOverlayCursor = resumeOverlay.ChildrenOfType().Single(); + + Vector2 offset = resumeOverlay.ToScreenSpace(new Vector2(OsuCursor.SIZE / 2)) - resumeOverlay.ToScreenSpace(Vector2.Zero); + InputManager.MoveMouseTo(resumeOverlayCursor.ScreenSpaceDrawQuad.Centre - offset + new Vector2(1)); + }); + AddStep("click", () => osuInputManager.GameClick()); AddAssert("dismissed", () => resumeFired && resume.State.Value == Visibility.Hidden); } + private void loadContent() + { + Child = osuInputManager = new ManualOsuInputManager(new OsuRuleset().RulesetInfo) { Children = new Drawable[] { cursor = new CursorContainer(), resume = new OsuResumeOverlay { GameplayCursor = cursor }, } }; + + resumeFired = false; + resume.ResumeAction = () => resumeFired = true; + } + private partial class ManualOsuInputManager : OsuInputManager { public ManualOsuInputManager(RulesetInfo ruleset) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs new file mode 100644 index 0000000000..627c8f416e --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs @@ -0,0 +1,208 @@ +// 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.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +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.Rulesets.Scoring.Legacy; +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(IReadOnlyList selectedMods) + => new ScoreV1(selectedMods) + { + ScoreMultiplier = { BindTarget = scoreMultiplier } + }; + + protected override IScoringAlgorithm CreateScoreV2(int maxCombo, IReadOnlyList selectedMods) + => new ScoreV2(maxCombo, selectedMods); + + protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode, IReadOnlyList mods) + => new OsuProcessorBasedScoringAlgorithm(beatmap, mode, mods); + + [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 readonly double modMultiplier; + public BindableDouble ScoreMultiplier { get; } = new BindableDouble(); + + private int currentCombo; + + public ScoreV1(IReadOnlyList selectedMods) + { + var ruleset = new OsuRuleset(); + modMultiplier = ruleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(selectedMods, new LegacyBeatmapConversionDifficultyInfo + { + SourceRuleset = ruleset.RulesetInfo + }); + } + + 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 * modMultiplier))); + + 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 modMultiplier; + + private readonly double comboPortionMax; + private readonly int maxCombo; + + public ScoreV2(int maxCombo, IReadOnlyList selectedMods) + { + this.maxCombo = maxCombo; + + var ruleset = new OsuRuleset(); + modMultiplier = ruleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier( + selectedMods.Append(new ModScoreV2()).ToList(), + new LegacyBeatmapConversionDifficultyInfo + { + SourceRuleset = ruleset.RulesetInfo + }); + + 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) + ) * modMultiplier); + } + } + } + + private class OsuProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm + { + public OsuProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode, IReadOnlyList selectedMods) + : base(beatmap, mode, selectedMods) + { + } + + 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/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index 3fac7c8c6d..b18c77e8ee 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() { @@ -197,7 +220,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + time_offset, Position = new Vector2(239, 176), - Path = new SliderPath(PathType.PerfectCurve, new[] + Path = new SliderPath(PathType.PERFECT_CURVE, new[] { Vector2.Zero, new Vector2(154, 28), @@ -207,7 +230,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); @@ -230,9 +253,10 @@ 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[] + Path = new SliderPath(PathType.PERFECT_CURVE, new[] { Vector2.Zero, new Vector2(0, distance), @@ -241,7 +265,7 @@ namespace osu.Game.Rulesets.Osu.Tests StackHeight = stackHeight }; - return createDrawable(slider, circleSize, speedMultiplier); + return createDrawable(slider, circleSize); } private Drawable testPerfect(int repeats = 0) @@ -250,7 +274,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + time_offset, Position = new Vector2(-max_length / 2, 0), - Path = new SliderPath(PathType.PerfectCurve, new[] + Path = new SliderPath(PathType.PERFECT_CURVE, new[] { Vector2.Zero, new Vector2(max_length / 2, max_length / 2), @@ -259,7 +283,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); @@ -270,7 +294,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + time_offset, Position = new Vector2(-max_length / 2, 0), - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(max_length * 0.375f, max_length * 0.18f), @@ -282,7 +306,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); @@ -293,7 +317,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + time_offset, Position = new Vector2(-max_length / 2, 0), - Path = new SliderPath(PathType.Bezier, new[] + Path = new SliderPath(PathType.BEZIER, new[] { Vector2.Zero, new Vector2(max_length * 0.375f, max_length * 0.18f), @@ -304,7 +328,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); @@ -315,7 +339,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + time_offset, Position = new Vector2(0, 0), - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(-max_length / 2, 0), @@ -327,7 +351,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); @@ -342,7 +366,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + time_offset, Position = new Vector2(-max_length / 4, 0), - Path = new SliderPath(PathType.Catmull, new[] + Path = new SliderPath(PathType.CATMULL, new[] { Vector2.Zero, new Vector2(max_length * 0.125f, max_length * 0.125f), @@ -353,15 +377,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/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs index 88b70a8836..380a2087ac 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs @@ -32,12 +32,12 @@ namespace osu.Game.Rulesets.Osu.Tests { DrawableSlider dho = null; - AddStep("create slider", () => Child = dho = new DrawableSlider(prepareObject(new Slider + AddStep("create slider", () => Child = dho = new DrawableSlider(applyDefaults(new Slider { Position = new Vector2(256, 192), IndexInCurrentCombo = 0, StartTime = Time.Current, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(150, 100), @@ -47,12 +47,12 @@ namespace osu.Game.Rulesets.Osu.Tests AddWaitStep("wait for progression", 1); - AddStep("apply new slider", () => dho.Apply(prepareObject(new Slider + AddStep("apply new slider", () => dho.Apply(applyDefaults(new Slider { Position = new Vector2(256, 192), ComboIndex = 1, StartTime = dho.HitObject.StartTime, - Path = new SliderPath(PathType.Bezier, new[] + Path = new SliderPath(PathType.BEZIER, new[] { Vector2.Zero, new Vector2(150, 100), @@ -75,12 +75,12 @@ namespace osu.Game.Rulesets.Osu.Tests Child = new SkinProvidingContainer(provider) { RelativeSizeAxes = Axes.Both, - Child = dho = new DrawableSlider(prepareObject(new Slider + Child = dho = new DrawableSlider(applyDefaults(new Slider { Position = new Vector2(256, 192), IndexInCurrentCombo = 0, StartTime = Time.Current, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(150, 100), @@ -97,7 +97,38 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("ball is red", () => dho.ChildrenOfType().Single().BallColour == Color4.Red); } - private Slider prepareObject(Slider slider) + [Test] + public void TestIncreaseRepeatCount() + { + DrawableSlider dho = null; + + AddStep("create slider", () => + { + Child = dho = new DrawableSlider(applyDefaults(new Slider + { + Position = new Vector2(256, 192), + IndexInCurrentCombo = 0, + StartTime = Time.Current, + Path = new SliderPath(PathType.LINEAR, new[] + { + Vector2.Zero, + new Vector2(150, 100), + new Vector2(300, 0), + }) + })); + }); + + AddStep("increase repeat count", () => + { + dho.HitObject.RepeatCount++; + applyDefaults(dho.HitObject); + }); + + AddAssert("repeat got custom anchor", () => + dho.ChildrenOfType().Single().RelativeAnchorPosition == Vector2.Divide(dho.SliderBody!.PathOffset, dho.DrawSize)); + } + + private Slider applyDefaults(Slider slider) { slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); return slider; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderEarlyHitJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderEarlyHitJudgement.cs new file mode 100644 index 0000000000..19883060a0 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderEarlyHitJudgement.cs @@ -0,0 +1,229 @@ +// 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.Game.Audio; +using osu.Game.Beatmaps; +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.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 TestSceneSliderEarlyHitJudgement : RateAdjustedBeatmapTestScene + { + private const double time_slider_start = 1000; + private const double time_slider_end = 3000; + + private static readonly Vector2 slider_start_position = new Vector2(256 - slider_path_length / 2, 192); + private static readonly Vector2 slider_end_position = new Vector2(256 + slider_path_length / 2, 192); + private static readonly Vector2 offset_inside_follow = new Vector2(35, 0); + private static readonly Vector2 offset_outside_follow = offset_inside_follow * 2; + + private ScoreAccessibleReplayPlayer currentPlayer = null!; + + private const float slider_path_length = 200; + + private readonly List judgementResults = new List(); + + [Test] + public void TestHitEarlyMoveIntoFollowRegion() + { + performTest(new List + { + new OsuReplayFrame(time_slider_start - 150, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_start - 100, slider_start_position + offset_inside_follow, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end - 100, slider_end_position + offset_inside_follow, OsuAction.LeftButton), + }); + + assertHeadJudgement(HitResult.Meh); + assertTickJudgement(HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); + assertSliderJudgement(HitResult.IgnoreHit); + } + + [Test] + public void TestHitEarlyAndReleaseInFollowRegion() + { + performTest(new List + { + new OsuReplayFrame(time_slider_start - 150, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_start - 100, slider_start_position + offset_inside_follow, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_start - 50, slider_start_position + offset_inside_follow), + new OsuReplayFrame(time_slider_end - 50, slider_end_position + offset_inside_follow, OsuAction.LeftButton), + }); + + assertHeadJudgement(HitResult.Meh); + assertTickJudgement(HitResult.LargeTickMiss); + assertTailJudgement(HitResult.IgnoreMiss); + assertSliderJudgement(HitResult.IgnoreHit); + } + + [Test] + public void TestHitEarlyAndRepressInFollowRegion() + { + performTest(new List + { + new OsuReplayFrame(time_slider_start - 150, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_start - 100, slider_start_position + offset_inside_follow, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_start - 75, slider_start_position + offset_inside_follow), + new OsuReplayFrame(time_slider_start - 50, slider_start_position + offset_inside_follow, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end - 50, slider_end_position + offset_inside_follow, OsuAction.LeftButton), + }); + + assertHeadJudgement(HitResult.Meh); + assertTickJudgement(HitResult.LargeTickMiss); + assertTailJudgement(HitResult.IgnoreMiss); + assertSliderJudgement(HitResult.IgnoreHit); + } + + [Test] + public void TestHitEarlyMoveOutsideFollowRegion() + { + performTest(new List + { + new OsuReplayFrame(time_slider_start - 150, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_start - 100, slider_start_position + offset_outside_follow, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end - 100, slider_end_position + offset_outside_follow, OsuAction.LeftButton), + }); + + assertHeadJudgement(HitResult.Meh); + assertTickJudgement(HitResult.LargeTickMiss); + assertTailJudgement(HitResult.IgnoreMiss); + assertSliderJudgement(HitResult.IgnoreHit); + } + + private void assertHeadJudgement(HitResult result) + { + AddAssert( + "check head result", + () => judgementResults.SingleOrDefault(r => r.HitObject is SliderHeadCircle)?.Type, + () => Is.EqualTo(result)); + } + + private void assertTickJudgement(HitResult result) + { + AddAssert( + "check tick result", + () => judgementResults.SingleOrDefault(r => r.HitObject is SliderTick)?.Type, + () => Is.EqualTo(result)); + } + + private void assertRepeatJudgement(HitResult result) + { + AddAssert( + "check tick result", + () => judgementResults.SingleOrDefault(r => r.HitObject is SliderRepeat)?.Type, + () => Is.EqualTo(result)); + } + + private void assertTailJudgement(HitResult result) + { + AddAssert( + "check tail result", + () => judgementResults.SingleOrDefault(r => r.HitObject is SliderTailCircle)?.Type, + () => Is.EqualTo(result)); + } + + private void assertSliderJudgement(HitResult result) + { + AddAssert( + "check slider result", + () => judgementResults.SingleOrDefault(r => r.HitObject is Slider)?.Type, + () => Is.EqualTo(result)); + } + + private Vector2 computePositionFromTime(double time) + { + Vector2 dist = slider_end_position - slider_start_position; + double t = (time - time_slider_start) / (time_slider_end - time_slider_start); + return slider_start_position + dist * (float)t; + } + + private void performTest(List frames, Action? adjustSliderFunc = null, bool classic = false) + { + Slider slider = new Slider + { + StartTime = time_slider_start, + Position = new Vector2(256 - slider_path_length / 2, 192), + TickDistanceMultiplier = 3, + ClassicSliderBehaviour = classic, + Samples = new[] + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL) + }, + Path = new SliderPath(PathType.LINEAR, new[] + { + Vector2.Zero, + new Vector2(slider_path_length, 0), + }, slider_path_length), + }; + + adjustSliderFunc?.Invoke(slider); + + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + HitObjects = { slider }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty + { + SliderMultiplier = 1, + SliderTickRate = 3, + OverallDifficulty = 0 + }, + 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()); + 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/TestSceneSliderFollowCircleInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs index fc2e6d1f72..fc9bb16cb7 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,8 +47,8 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_slider_start, Position = new Vector2(0, 0), - SliderVelocity = velocity, - Path = new SliderPath(PathType.Linear, new[] + SliderVelocityMultiplier = velocity, + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(followCircleRadius, 0), diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index d83926ab9b..12be74c4cc 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 + 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(); } /// @@ -65,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_1 }, }); - AddAssert("Tracking lost", assertMidSliderJudgementFail); + assertMidSliderJudgementFail(); } /// @@ -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(); } /// @@ -146,7 +278,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_before_slider }, }); - AddAssert("Tracking retained, sliderhead miss", assertHeadMissTailTracked); + assertHeadMissTailTracked(); } /// @@ -170,7 +302,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 }, }); - AddAssert("Tracking re-acquired", assertMidSliderJudgements); + assertMidSliderJudgements(); } /// @@ -196,7 +328,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 }, }); - AddAssert("Tracking lost", assertMidSliderJudgementFail); + assertMidSliderJudgementFail(); } /// @@ -218,7 +350,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 }, }); - AddAssert("Tracking acquired", assertMidSliderJudgements); + assertMidSliderJudgements(); } /// @@ -241,7 +373,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_2 }, }); - AddAssert("Tracking acquired", assertMidSliderJudgements); + assertMidSliderJudgements(); } [Test] @@ -255,7 +387,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_2 }, }); - AddAssert("Tracking acquired", assertMidSliderJudgements); + assertMidSliderJudgements(); } /// @@ -280,7 +412,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 }, }); - AddAssert("Tracking acquired", assertMidSliderJudgements); + assertMidSliderJudgements(); } /// @@ -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(); } /// @@ -322,46 +454,67 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(slider_path_length, OsuHitObject.OBJECT_RADIUS * 1.201f), Actions = { OsuAction.LeftButton }, Time = time_slider_end }, }); - AddAssert("Tracking dropped", assertMidSliderJudgementFail); + assertMidSliderJudgementFail(); } - private bool assertMaxJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == t.Judgement.MaxResult); - - private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.SmallTickHit && !judgementResults.First().IsHit; - - private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.SmallTickHit; - - 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 assertAllMaxJudgements() { + AddAssert("All judgements max", () => + { + return judgementResults.Select(j => (j.HitObject, j.Type)); + }, () => Is.EqualTo(judgementResults.Select(j => (j.HitObject, j.Judgement.MaxResult)))); + } + + private void assertHeadMissTailTracked() + { + AddAssert("Tracking retained", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.SliderTailHit)); + AddAssert("Slider head missed", () => judgementResults.First().IsHit, () => Is.False); + } + + private void assertMidSliderJudgements() + { + AddAssert("Tracking acquired", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.SliderTailHit)); + } + + private void assertMidSliderJudgementFail() + { + AddAssert("Tracking lost", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.IgnoreMiss)); + } + + 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.PERFECT_CURVE, 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, + SliderMultiplier = 1, + }, + Ruleset = new OsuRuleset().RulesetInfo, }, + ControlPointInfo = cpi, }); var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); @@ -375,7 +528,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/TestSceneSliderLateHitJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs new file mode 100644 index 0000000000..1ba4a60b75 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs @@ -0,0 +1,528 @@ +// 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.Graphics; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Sprites; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; +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 TestSceneSliderLateHitJudgement : RateAdjustedBeatmapTestScene + { + // Note: In the following tests, the terminology "in range of the follow circle" is used as meaning + // the equivalent of "in range of the follow circle as if it were in its expanded state". + + private const double time_slider_start = 1000; + private const double time_slider_end = 1500; + + private static readonly Vector2 slider_start_position = new Vector2(256 - slider_path_length / 2, 192); + private static readonly Vector2 slider_end_position = new Vector2(256 + slider_path_length / 2, 192); + + private ScoreAccessibleReplayPlayer currentPlayer = null!; + + private const float slider_path_length = 200; + + private readonly List judgementResults = new List(); + + /// + /// If the head circle is hit and the mouse is in range of the follow circle, + /// then tracking should be enabled. + /// + [Test] + public void TestHitLateInRangeTracks() + { + performTest(new List + { + new OsuReplayFrame(time_slider_start + 100, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 100, slider_end_position, OsuAction.LeftButton), + }); + + assertHeadJudgement(HitResult.Ok); + assertTailJudgement(HitResult.SliderTailHit); + assertSliderJudgement(HitResult.IgnoreHit); + } + + /// + /// If the head circle is hit and the mouse is NOT in range of the follow circle, + /// then tracking should NOT be enabled. + /// + [Test] + public void TestHitLateOutOfRangeDoesNotTrack() + { + performTest(new List + { + new OsuReplayFrame(time_slider_start + 100, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 100, slider_end_position, OsuAction.LeftButton), + }, s => + { + s.SliderVelocityMultiplier = 2; + }); + + assertHeadJudgement(HitResult.Ok); + assertTailJudgement(HitResult.IgnoreMiss); + assertSliderJudgement(HitResult.IgnoreHit); + } + + /// + /// If the head circle is hit late and the mouse is in range of the follow circle, + /// then all ticks that the follow circle has passed through should be hit. + /// + [Test] + public void TestHitLateInRangeHitsTicks() + { + performTest(new List + { + new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 150, slider_end_position, OsuAction.LeftButton), + }, s => + { + s.TickDistanceMultiplier = 0.2f; + }); + + assertHeadJudgement(HitResult.Meh); + assertTickJudgement(0, HitResult.LargeTickHit); + assertTickJudgement(1, HitResult.LargeTickHit); + assertTickJudgement(2, HitResult.LargeTickHit); + assertTickJudgement(3, HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); + assertSliderJudgement(HitResult.IgnoreHit); + } + + /// + /// If the head circle is hit late and the mouse is NOT in range of the follow circle, + /// then all ticks that the follow circle has passed through should NOT be hit. + /// + [Test] + public void TestHitLateOutOfRangeDoesNotHitTicks() + { + performTest(new List + { + new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 150, slider_end_position, OsuAction.LeftButton), + }, s => + { + s.SliderVelocityMultiplier = 2; + s.TickDistanceMultiplier = 0.2f; + }); + + assertHeadJudgement(HitResult.Meh); + assertTickJudgement(0, HitResult.LargeTickMiss); + assertTickJudgement(1, HitResult.LargeTickMiss); + assertTailJudgement(HitResult.IgnoreMiss); + assertSliderJudgement(HitResult.IgnoreHit); + } + + /// + /// If the head circle is pressed after it's missed and the mouse is in range of the follow circle, + /// then tracking should NOT be enabled. + /// + [Test] + public void TestMissHeadInRangeDoesNotTrack() + { + performTest(new List + { + new OsuReplayFrame(time_slider_start + 151, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 151, slider_end_position, OsuAction.LeftButton), + }, s => + { + s.TickDistanceMultiplier = 0.2f; + }); + + assertHeadJudgement(HitResult.Miss); + assertTickJudgement(0, HitResult.LargeTickMiss); + assertTickJudgement(1, HitResult.LargeTickMiss); + assertTickJudgement(2, HitResult.LargeTickMiss); + assertTickJudgement(3, HitResult.LargeTickMiss); + assertTailJudgement(HitResult.IgnoreMiss); + assertSliderJudgement(HitResult.IgnoreMiss); + } + + /// + /// If the head circle is hit late but after the completion of the slider and the mouse is in range of the follow circle, + /// then all nested objects (ticks/repeats/tail) should be hit. + /// + [Test] + public void TestHitLateShortSliderHitsAll() + { + performTest(new List + { + new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton), + }, s => + { + s.Path = new SliderPath(PathType.LINEAR, new[] + { + Vector2.Zero, + new Vector2(20, 0), + }, 20); + + s.TickDistanceMultiplier = 0.01f; + s.RepeatCount = 1; + }); + + assertHeadJudgement(HitResult.Meh); + assertAllTickJudgements(HitResult.LargeTickHit); + assertRepeatJudgement(HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); + assertSliderJudgement(HitResult.IgnoreHit); + } + + /// + /// If the head circle is hit late and the mouse is in range of the follow circle, + /// then all the repeats that the follow circle has passed through should be hit. + /// + [Test] + public void TestHitLateInRangeHitsRepeat() + { + performTest(new List + { + new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton), + }, s => + { + s.Path = new SliderPath(PathType.LINEAR, new[] + { + Vector2.Zero, + new Vector2(50, 0), + }, 50); + + s.RepeatCount = 1; + }); + + assertHeadJudgement(HitResult.Meh); + assertRepeatJudgement(HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); + assertSliderJudgement(HitResult.IgnoreHit); + } + + /// + /// If the head circle is hit and the mouse is in range of the follow circle, + /// then only the ticks that are in range of the cursor position should be hit. + /// If any hitobject does not meet this criteria, ALL hitobjects after that one should be missed. + /// + [Test] + public void TestHitLateDoesNotHitTicksIfAnyOutOfRange() + { + performTest(new List + { + new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton), + }, s => + { + s.Path = new SliderPath(PathType.PERFECT_CURVE, new[] + { + Vector2.Zero, + new Vector2(70, 70), + new Vector2(20, 0), + }); + + s.TickDistanceMultiplier = 0.03f; + s.SliderVelocityMultiplier = 6f; + }); + + assertHeadJudgement(HitResult.Meh); + + // At least one tick was out of range, so they all should be missed. + assertAllTickJudgements(HitResult.LargeTickMiss); + + // This particular test actually starts tracking the slider just before the end, so the tail should be hit because of its leniency. + assertTailJudgement(HitResult.SliderTailHit); + + assertSliderJudgement(HitResult.IgnoreHit); + } + + /// + /// If the head circle is hit and the mouse is in range of the follow circle, + /// then a tick not within the follow radius from the cursor position should not be hit. + /// + [Test] + public void TestHitLateInRangeDoesNotHitOutOfRangeTick() + { + performTest(new List + { + new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton), + }, s => + { + s.Path = new SliderPath(PathType.PERFECT_CURVE, new[] + { + Vector2.Zero, + new Vector2(50, 50), + new Vector2(20, 0), + }); + + s.TickDistanceMultiplier = 0.3f; + s.SliderVelocityMultiplier = 3; + }); + + assertHeadJudgement(HitResult.Meh); + assertTickJudgement(0, HitResult.LargeTickMiss); + assertTailJudgement(HitResult.SliderTailHit); + assertSliderJudgement(HitResult.IgnoreHit); + } + + /// + /// Same as except the tracking is limited to the ball + /// because the tick was missed. + /// + [Test] + public void TestHitLateInRangeDoesNotHitOutOfRangeTickAndTrackingLimitedToBall() + { + performTest(new List + { + new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton), + }, s => + { + s.Path = new SliderPath(PathType.PERFECT_CURVE, new[] + { + Vector2.Zero, + new Vector2(50, 50), + new Vector2(20, 0), + }); + + s.TickDistanceMultiplier = 0.25f; + s.SliderVelocityMultiplier = 3; + }); + + assertHeadJudgement(HitResult.Meh); + assertTickJudgement(0, HitResult.LargeTickMiss); + assertTickJudgement(1, HitResult.LargeTickMiss); + assertTailJudgement(HitResult.SliderTailHit); + assertSliderJudgement(HitResult.IgnoreHit); + } + + /// + /// If the head circle is hit and the mouse is in range of the follow circle, + /// then a tick not within the follow radius from the cursor position should not be hit. + /// + [Test] + public void TestHitLateWithEdgeHit() + { + performTest(new List + { + new OsuReplayFrame(time_slider_start + 150, slider_start_position - new Vector2(20), OsuAction.LeftButton), + new OsuReplayFrame(time_slider_end + 150, slider_start_position - new Vector2(20), OsuAction.LeftButton), + }, s => + { + s.Path = new SliderPath(PathType.PERFECT_CURVE, new[] + { + Vector2.Zero, + new Vector2(50, 50), + new Vector2(20, 0), + }); + + s.TickDistanceMultiplier = 0.35f; + s.SliderVelocityMultiplier = 4; + }); + + assertHeadJudgement(HitResult.Meh); + assertTickJudgement(0, HitResult.LargeTickMiss); + assertTailJudgement(HitResult.IgnoreMiss); + assertSliderJudgement(HitResult.IgnoreHit); + } + + /// + /// Late hit and release on each slider head of a slider stream. + /// + [Test] + public void TestLateHitSliderStream() + { + var beatmap = new Beatmap(); + + for (int i = 0; i < 20; i++) + { + beatmap.HitObjects.Add(new Slider + { + StartTime = time_slider_start + 75 * i, // 200BPM @ 1/4 + Position = new Vector2(256 - slider_path_length / 2, 192), + TickDistanceMultiplier = 3, + Path = new SliderPath(PathType.LINEAR, new[] + { + Vector2.Zero, + new Vector2(20, 0), + }), + }); + } + + var replay = new List(); + + for (int i = 0; i < 20; i++) + { + replay.Add(new OsuReplayFrame(time_slider_start + 75 * i + 75, slider_start_position, i % 2 == 0 ? OsuAction.LeftButton : OsuAction.RightButton)); + replay.Add(new OsuReplayFrame(time_slider_start + 75 * i + 140, slider_start_position)); + } + + performTest(replay, beatmap); + + AddAssert( + $"all heads = {HitResult.Ok}", + () => judgementResults.Where(r => r.HitObject is SliderHeadCircle).Select(r => r.Type), + () => Has.All.EqualTo(HitResult.Ok)); + } + + private void assertHeadJudgement(HitResult result) + { + AddAssert( + $"head = {result}", + () => judgementResults.SingleOrDefault(r => r.HitObject is SliderHeadCircle)?.Type, + () => Is.EqualTo(result)); + } + + private void assertTickJudgement(int index, HitResult result) + { + AddAssert( + $"tick({index}) = {result}", + () => judgementResults.Where(r => r.HitObject is SliderTick).ElementAtOrDefault(index)?.Type, + () => Is.EqualTo(result)); + } + + private void assertAllTickJudgements(HitResult result) + { + AddAssert( + $"all ticks = {result}", + () => judgementResults.Where(r => r.HitObject is SliderTick).Select(t => t.Type), + () => Has.All.EqualTo(result)); + } + + private void assertRepeatJudgement(HitResult result) + { + AddAssert( + $"repeat = {result}", + () => judgementResults.SingleOrDefault(r => r.HitObject is SliderRepeat)?.Type, + () => Is.EqualTo(result)); + } + + private void assertTailJudgement(HitResult result) + { + AddAssert( + $"tail = {result}", + () => judgementResults.SingleOrDefault(r => r.HitObject is SliderTailCircle)?.Type, + () => Is.EqualTo(result)); + } + + private void assertSliderJudgement(HitResult result) + { + AddAssert( + $"slider = {result}", + () => judgementResults.SingleOrDefault(r => r.HitObject is Slider)?.Type, + () => Is.EqualTo(result)); + } + + private void performTest(List frames, Action? adjustSliderFunc = null, bool classic = false) + { + Slider slider = new Slider + { + StartTime = time_slider_start, + Position = new Vector2(256 - slider_path_length / 2, 192), + TickDistanceMultiplier = 3, + ClassicSliderBehaviour = classic, + Path = new SliderPath(PathType.LINEAR, new[] + { + Vector2.Zero, + new Vector2(slider_path_length, 0), + }, slider_path_length), + }; + + adjustSliderFunc?.Invoke(slider); + + var beatmap = new Beatmap + { + HitObjects = { slider }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty + { + SliderMultiplier = 4, + SliderTickRate = 3 + }, + Ruleset = new OsuRuleset().RulesetInfo, + } + }; + + performTest(frames, beatmap); + } + + private void performTest(List frames, Beatmap beatmap) + { + beatmap.BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo; + beatmap.BeatmapInfo.StackLeniency = 0; + beatmap.BeatmapInfo.Difficulty = new BeatmapDifficulty + { + SliderMultiplier = 4, + SliderTickRate = 3, + }; + + 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); + + DrawableHitObject drawableObj = this.ChildrenOfType().Single(h => h.HitObject == result.HitObject); + + var text = new OsuSpriteText + { + Origin = Anchor.Centre, + Position = Content.ToLocalSpace(drawableObj.ToScreenSpace(drawableObj.OriginPosition)) - new Vector2(0, 20), + Text = result.IsHit ? "hit" : "miss" + }; + + Add(text); + + text.FadeOutFromOne(1000).Expire(); + }; + }; + + LoadScreen(currentPlayer = p); + judgementResults.Clear(); + }); + + 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.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index 630049f408..912b2b0626 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(); @@ -235,7 +217,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = 3000, Position = new Vector2(100, 100), - Path = new SliderPath(PathType.PerfectCurve, new[] + Path = new SliderPath(PathType.PERFECT_CURVE, new[] { Vector2.Zero, new Vector2(300, 200) @@ -245,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = 13000, Position = new Vector2(100, 100), - Path = new SliderPath(PathType.PerfectCurve, new[] + Path = new SliderPath(PathType.PERFECT_CURVE, new[] { Vector2.Zero, new Vector2(300, 200) @@ -256,7 +238,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = 23000, Position = new Vector2(100, 100), - Path = new SliderPath(PathType.PerfectCurve, new[] + Path = new SliderPath(PathType.PERFECT_CURVE, new[] { Vector2.Zero, new Vector2(300, 200) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index 8cfd674f88..77b16dd0c5 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -3,9 +3,11 @@ #nullable disable +using System; 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 +28,21 @@ 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); + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Reset rate", () => spinRate.Value = 1); + } + [TestCase(true)] [TestCase(false)] public void TestVariousSpinners(bool autoplay) @@ -36,6 +53,36 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep($"{term} Small", () => SetContents(_ => testSingle(7, autoplay))); } + [Test] + public void TestSpinnerNoBonus() + { + AddStep("Set high spin rate", () => spinRate.Value = 5); + + Spinner spinner; + + AddStep("add spinner", () => SetContents(_ => + { + spinner = new Spinner + { + StartTime = Time.Current, + EndTime = Time.Current + 750, + Samples = new List + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL) + } + }; + + spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { OverallDifficulty = 0 }); + + return drawableSpinner = new TestDrawableSpinner(spinner, true, spinRate) + { + Anchor = Anchor.Centre, + Depth = depthIndex++, + Scale = new Vector2(0.75f) + }; + })); + } + [Test] public void TestSpinningSamplePitchShift() { @@ -43,7 +90,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 +112,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 +161,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 +177,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)Math.Min(180, 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/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 index c969cb11b4..8d8c2e9639 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.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 NUnit.Framework; @@ -11,14 +10,12 @@ using osu.Game.Beatmaps; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Replays; 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; -using osuTK; namespace osu.Game.Rulesets.Osu.Tests { @@ -59,26 +56,9 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("all max judgements", () => judgementResults.All(result => result.Type == result.Judgement.MaxResult)); } - private static List generateReplay(int spins) - { - var replayFrames = new List(); - - const int frames_per_spin = 30; - - for (int i = 0; i < spins * frames_per_spin; ++i) - { - float totalProgress = i / (float)(spins * frames_per_spin); - float spinProgress = (i % frames_per_spin) / (float)frames_per_spin; - double time = time_spinner_start + (time_spinner_end - time_spinner_start) * totalProgress; - float posX = MathF.Cos(2 * MathF.PI * spinProgress); - float posY = MathF.Sin(2 * MathF.PI * spinProgress); - Vector2 finalPos = OsuPlayfield.BASE_SIZE / 2 + new Vector2(posX, posY) * 50; - - replayFrames.Add(new OsuReplayFrame(time, finalPos, OsuAction.LeftButton)); - } - - return replayFrames; - } + 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) { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 116c974f32..6706d20080 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -58,16 +58,13 @@ namespace osu.Game.Rulesets.Osu.Tests double trackerRotationTolerance = 0; addSeekStep(5000); - AddStep("calculate rotation tolerance", () => - { - trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f); - }); + AddStep("calculate rotation tolerance", () => { 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 +79,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 +89,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] @@ -133,9 +130,11 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("player score matching expected bonus score", () => { + var scoreProcessor = ((ScoreExposedPlayer)Player).ScoreProcessor; + // 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; + long totalScore = scoreProcessor.TotalScore.Value * 2; + return totalScore == (int)(drawableSpinner.Result.TotalRotation / 360) * scoreProcessor.GetBaseScoreForResult(new SpinnerTick().CreateJudgement().MaxResult); }); addSeekStep(0); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs index f4257a9ee7..895e9bbdee 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs @@ -196,7 +196,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_slider, Position = positionSlider, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(25, 0), @@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_slider, Position = positionSlider, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(25, 0), @@ -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); @@ -317,7 +318,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_slider, Position = positionSlider, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(25, 0), @@ -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..9c248abd66 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/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index 9e786caf0c..3c051a6bb1 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -44,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 ()) + { + if (obj is not Spinner && (lastObj == null || lastObj is Spinner)) + obj.NewCombo = true; + lastObj = obj; + } + + base.PreProcess(); + } + public override void PostProcess() { base.PostProcess(); @@ -95,15 +113,15 @@ namespace osu.Game.Rulesets.Osu.Beatmaps { int n = i; /* We should check every note which has not yet got a stack. - * Consider the case we have two interwound stacks and this will make sense. - * - * o <-1 o <-2 - * o <-3 o <-4 - * - * We first process starting from 4 and handle 2, - * then we come backwards on the i loop iteration until we reach 3 and handle 1. - * 2 and 1 will be ignored in the i loop because they already have a stack value. - */ + * Consider the case we have two interwound stacks and this will make sense. + * + * o <-1 o <-2 + * o <-3 o <-4 + * + * We first process starting from 4 and handle 2, + * then we come backwards on the i loop iteration until we reach 3 and handle 1. + * 2 and 1 will be ignored in the i loop because they already have a stack value. + */ OsuHitObject objectI = beatmap.HitObjects[i]; if (objectI.StackHeight != 0 || objectI is Spinner) continue; @@ -111,9 +129,9 @@ namespace osu.Game.Rulesets.Osu.Beatmaps double stackThreshold = objectI.TimePreempt * beatmap.BeatmapInfo.StackLeniency; /* If this object is a hitcircle, then we enter this "special" case. - * It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider. - * Any other case is handled by the "is Slider" code below this. - */ + * It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider. + * Any other case is handled by the "is Slider" code below this. + */ if (objectI is HitCircle) { while (--n >= 0) @@ -135,10 +153,10 @@ namespace osu.Game.Rulesets.Osu.Beatmaps } /* This is a special case where hticircles are moved DOWN and RIGHT (negative stacking) if they are under the *last* slider in a stacked pattern. - * o==o <- slider is at original location - * o <- hitCircle has stack of -1 - * o <- hitCircle has stack of -2 - */ + * o==o <- slider is at original location + * o <- hitCircle has stack of -1 + * o <- hitCircle has stack of -2 + */ if (objectN is Slider && Vector2Extensions.Distance(objectN.EndPosition, objectI.Position) < stack_distance) { int offset = objectI.StackHeight - objectN.StackHeight + 1; @@ -169,8 +187,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps else if (objectI is Slider) { /* We have hit the first slider in a possible stack. - * From this point on, we ALWAYS stack positive regardless. - */ + * From this point on, we ALWAYS stack positive regardless. + */ while (--n >= startIndex) { OsuHitObject objectN = beatmap.HitObjects[n]; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index cf35b08822..3d1939acac 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -24,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; @@ -37,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 @@ -48,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; @@ -122,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/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 24d5635104..83538a2f42 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate); yield return (ATTRIB_ID_DIFFICULTY, StarRating); - if (ShouldSerializeFlashlightRating()) + if (ShouldSerializeFlashlightDifficulty()) yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty); yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor); @@ -128,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // unless the fields are also renamed. [UsedImplicitly] - public bool ShouldSerializeFlashlightRating() => Mods.Any(m => m is ModFlashlight); + public bool ShouldSerializeFlashlightDifficulty() => Mods.Any(m => m is ModFlashlight); #endregion } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index b92092c674..3b580a5b59 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -26,12 +26,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty public override int Version => 20220902; - private readonly IWorkingBeatmap workingBeatmap; - public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { - workingBeatmap = beatmap; } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) @@ -109,15 +106,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty SpinnerCount = spinnerCount, }; - if (ComputeLegacyScoringValues) - { - OsuLegacyScoreSimulator sv1Simulator = new OsuLegacyScoreSimulator(); - sv1Simulator.Simulate(workingBeatmap, beatmap, mods); - attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore; - attributes.LegacyComboScore = sv1Simulator.ComboScore; - attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio; - } - return attributes; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs index 980d86e4ad..b808deab5c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs @@ -5,34 +5,30 @@ 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.Legacy; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; namespace osu.Game.Rulesets.Osu.Difficulty { internal class OsuLegacyScoreSimulator : ILegacyScoreSimulator { - public int AccuracyScore { get; private set; } - - public int ComboScore { get; private set; } - - public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore; + private readonly ScoreProcessor scoreProcessor = new OsuScoreProcessor(); private int legacyBonusScore; - private int modernBonusScore; + private int standardisedBonusScore; private int combo; private double scoreMultiplier; - private IBeatmap playableBeatmap = null!; - public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap) { - this.playableBeatmap = playableBeatmap; - IBeatmap baseBeatmap = workingBeatmap.Beatmap; int countNormal = 0; @@ -67,19 +63,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty 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 = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength); - scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + LegacyScoreAttributes attributes = new LegacyScoreAttributes(); foreach (var obj in playableBeatmap.HitObjects) - simulateHit(obj); + simulateHit(obj, ref attributes); + + attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore; + attributes.BonusScore = legacyBonusScore; + attributes.MaxCombo = combo; + + return attributes; } - private void simulateHit(HitObject hitObject) + private void simulateHit(HitObject hitObject, ref LegacyScoreAttributes attributes) { bool increaseCombo = true; bool addScoreComboMultiplier = false; @@ -122,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty case Slider: foreach (var nested in hitObject.NestedHitObjects) - simulateHit(nested); + simulateHit(nested, ref attributes); scoreIncrease = 300; increaseCombo = false; @@ -133,22 +131,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty // 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; - double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(playableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5); + + // 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 * minimumRotationsPerSecond); + 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()); + simulateHit(new SpinnerBonusTick(), ref attributes); else if (i > 1 && i % 2 == 0) - simulateHit(new SpinnerTick()); + simulateHit(new SpinnerTick(), ref attributes); } scoreIncrease = 300; @@ -159,19 +162,72 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (addScoreComboMultiplier) { // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) - ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier)); + attributes.ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier)); } if (isBonus) { legacyBonusScore += scoreIncrease; - modernBonusScore += Judgement.ToNumericResult(bonusResult); + standardisedBonusScore += scoreProcessor.GetBaseScoreForResult(bonusResult); } else - AccuracyScore += scoreIncrease; + 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/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index e627c9ad67..488d1e2e9f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -3,6 +3,7 @@ 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; @@ -214,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) @@ -223,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; @@ -236,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. @@ -263,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/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 670e98ca50..c585f09b00 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs @@ -16,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/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs index 67685d21a7..7e7d653dbd 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs @@ -51,10 +51,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components base.LoadComplete(); hitObjectPosition = hitObject.PositionBindable.GetBoundCopy(); - hitObjectPosition.BindValueChanged(_ => updateConnectingPath()); + hitObjectPosition.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath)); pathVersion = hitObject.Path.Version.GetBoundCopy(); - pathVersion.BindValueChanged(_ => updateConnectingPath()); + pathVersion.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath)); updateConnectingPath(); } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 12e5ca0236..e741d67e3b 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -4,20 +4,15 @@ #nullable disable using System; -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; 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.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Localisation; -using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -41,8 +36,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public Action DragInProgress; public Action DragEnded; - public List PointsInSegment; - public readonly BindableBool IsSelected = new BindableBool(); public readonly PathControlPoint ControlPoint; @@ -56,27 +49,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private IBindable hitObjectPosition; private IBindable hitObjectScale; - [UsedImplicitly] - private readonly IBindable hitObjectVersion; - public PathControlPointPiece(T hitObject, PathControlPoint controlPoint) { this.hitObject = hitObject; ControlPoint = controlPoint; - // we don't want to run the path type update on construction as it may inadvertently change the hit object. - cachePoints(hitObject); - - hitObjectVersion = hitObject.Path.Version.GetBoundCopy(); - - // schedule ensure that updates are only applied after all operations from a single frame are applied. - // this avoids inadvertently changing the hit object path type for batch operations. - hitObjectVersion.BindValueChanged(_ => Scheduler.AddOnce(() => - { - cachePoints(hitObject); - updatePathType(); - })); - controlPoint.Changed += updateMarkerDisplay; Origin = Anchor.Centre; @@ -214,28 +191,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components protected override void OnDragEnd(DragEndEvent e) => DragEnded?.Invoke(); - private void cachePoints(T hitObject) => PointsInSegment = hitObject.Path.PointsInSegment(ControlPoint); - - /// - /// Handles correction of invalid path types. - /// - private void updatePathType() - { - if (ControlPoint.Type != PathType.PerfectCurve) - return; - - if (PointsInSegment.Count > 3) - ControlPoint.Type = PathType.Bezier; - - if (PointsInSegment.Count != 3) - return; - - ReadOnlySpan points = PointsInSegment.Select(p => p.Position).ToArray(); - RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points); - if (boundingBox.Width >= 640 || boundingBox.Height >= 480) - ControlPoint.Type = PathType.Bezier; - } - /// /// Updates the state of the circular control point marker. /// @@ -256,18 +211,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private Color4 getColourFromNodeType() { - if (!(ControlPoint.Type is PathType pathType)) + if (ControlPoint.Type is not PathType pathType) return colours.Yellow; - switch (pathType) + switch (pathType.Type) { - case PathType.Catmull: + case SplineType.Catmull: return colours.SeaFoam; - case PathType.Bezier: - return colours.Pink; + case SplineType.BSpline: + if (!pathType.Degree.HasValue) + return colours.PinkLighter; - case PathType.PerfectCurve: + int idx = Math.Clamp(pathType.Degree.Value, 0, 3); + return new[] { colours.PinkDarker, colours.PinkDark, colours.Pink, colours.PinkLight }[idx]; + + case SplineType.PerfectCurve: return colours.PurpleDark; default: @@ -275,6 +234,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } } - public LocalisableString TooltipText => ControlPoint.Type.ToString() ?? string.Empty; + public LocalisableString TooltipText => ControlPoint.Type?.Description ?? string.Empty; } } 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..b2d1709531 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -14,10 +14,12 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Primitives; 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.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -47,7 +49,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) { @@ -73,6 +78,50 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components controlPoints.BindTo(hitObject.Path.ControlPoints); } + /// + /// Handles correction of invalid path types. + /// + public void EnsureValidPathTypes() + { + List pointsInCurrentSegment = new List(); + + foreach (var controlPoint in controlPoints) + { + if (controlPoint.Type != null) + { + pointsInCurrentSegment.Add(controlPoint); + ensureValidPathType(pointsInCurrentSegment); + pointsInCurrentSegment.Clear(); + } + + pointsInCurrentSegment.Add(controlPoint); + } + + ensureValidPathType(pointsInCurrentSegment); + } + + private void ensureValidPathType(IReadOnlyList segment) + { + if (segment.Count == 0) + return; + + var first = segment[0]; + + if (first.Type != PathType.PERFECT_CURVE) + return; + + if (segment.Count > 3) + first.Type = PathType.BEZIER; + + if (segment.Count != 3) + return; + + ReadOnlySpan points = segment.Select(p => p.Position).ToArray(); + RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points); + if (boundingBox.Width >= 640 || boundingBox.Height >= 480) + first.Type = PathType.BEZIER; + } + /// /// Selects the corresponding to the given , /// and deselects all other s. @@ -156,9 +205,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (allowSelection) d.RequestSelection = selectionRequested; - d.DragStarted = dragStarted; - d.DragInProgress = dragInProgress; - d.DragEnded = dragEnded; + d.DragStarted = DragStarted; + d.DragInProgress = DragInProgress; + d.DragEnded = DragEnded; })); Connections.Add(new PathControlPointConnectionPiece(hitObject, e.NewStartingIndex + i)); @@ -237,20 +286,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components /// The path type we want to assign to the given control point piece. private void updatePathType(PathControlPointPiece piece, PathType? type) { - int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint); + var pointsInSegment = hitObject.Path.PointsInSegment(piece.ControlPoint); + int indexInSegment = pointsInSegment.IndexOf(piece.ControlPoint); - switch (type) + if (type?.Type == SplineType.PerfectCurve) { - case PathType.PerfectCurve: - // Can't always create a circular arc out of 4 or more points, - // so we split the segment into one 3-point circular arc segment - // and one segment of the previous type. - int thirdPointIndex = indexInSegment + 2; + // Can't always create a circular arc out of 4 or more points, + // so we split the segment into one 3-point circular arc segment + // and one segment of the previous type. + int thirdPointIndex = indexInSegment + 2; - if (piece.PointsInSegment.Count > thirdPointIndex + 1) - piece.PointsInSegment[thirdPointIndex].Type = piece.PointsInSegment[0].Type; - - break; + if (pointsInSegment.Count > thirdPointIndex + 1) + pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type; } hitObject.Path.ExpectedDistance.Value = null; @@ -267,7 +314,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private int draggedControlPointIndex; private HashSet selectedControlPoints; - private void dragStarted(PathControlPoint controlPoint) + public void DragStarted(PathControlPoint controlPoint) { dragStartPositions = hitObject.Path.ControlPoints.Select(point => point.Position).ToArray(); dragPathTypes = hitObject.Path.ControlPoints.Select(point => point.Type).ToArray(); @@ -279,7 +326,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components changeHandler?.BeginChange(); } - private void dragInProgress(DragEvent e) + public void DragInProgress(DragEvent e) { Vector2[] oldControlPoints = hitObject.Path.ControlPoints.Select(cp => cp.Position).ToArray(); var oldPosition = hitObject.Position; @@ -288,10 +335,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 +356,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 +369,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,16 +379,18 @@ 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; } // Maintain the path types in case they got defaulted to bezier at some point during the drag. for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++) hitObject.Path.ControlPoints[i].Type = dragPathTypes[i]; + + EnsureValidPathTypes(); } - private void dragEnded() => changeHandler?.EndChange(); + public void DragEnded() => changeHandler?.EndChange(); #endregion @@ -364,13 +413,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components List curveTypeItems = new List(); if (!selectedPieces.Contains(Pieces[0])) + { curveTypeItems.Add(createMenuItemForPathType(null)); + curveTypeItems.Add(new OsuMenuItemSpacer()); + } // todo: hide/disable items which aren't valid for selected points - curveTypeItems.Add(createMenuItemForPathType(PathType.Linear)); - curveTypeItems.Add(createMenuItemForPathType(PathType.PerfectCurve)); - curveTypeItems.Add(createMenuItemForPathType(PathType.Bezier)); - curveTypeItems.Add(createMenuItemForPathType(PathType.Catmull)); + curveTypeItems.Add(createMenuItemForPathType(PathType.LINEAR)); + curveTypeItems.Add(createMenuItemForPathType(PathType.PERFECT_CURVE)); + curveTypeItems.Add(createMenuItemForPathType(PathType.BEZIER)); + curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(4))); + + if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull)) + curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL)); var menuItems = new List { @@ -402,10 +457,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components int totalCount = Pieces.Count(p => p.IsSelected.Value); int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == type); - var item = new TernaryStateRadioMenuItem(type == null ? "Inherit" : type.ToString().Humanize(), MenuItemType.Standard, _ => + var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ => { foreach (var p in Pieces.Where(p => p.IsSelected.Value)) updatePathType(p, type); + + EnsureValidPathTypes(); }); if (countOfState == totalCount) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 966092c6fe..0fa84c91fc 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -3,6 +3,8 @@ #nullable disable +using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using JetBrains.Annotations; @@ -10,6 +12,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -39,7 +42,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private int currentSegmentLength; [Resolved(CanBeNull = true)] - private IDistanceSnapProvider snapProvider { get; set; } + [CanBeNull] + private IPositionSnapProvider positionSnapProvider { get; set; } + + [Resolved(CanBeNull = true)] + [CanBeNull] + private IDistanceSnapProvider distanceSnapProvider { get; set; } + + [Resolved(CanBeNull = true)] + [CanBeNull] + private FreehandSliderToolboxGroup freehandToolboxGroup { get; set; } + + private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 }; protected override bool IsValidForPlacement => HitObject.Path.HasValidLength; @@ -48,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { RelativeSizeAxes = Axes.Both; - HitObject.Path.ControlPoints.Add(segmentStart = new PathControlPoint(Vector2.Zero, PathType.Linear)); + HitObject.Path.ControlPoints.Add(segmentStart = new PathControlPoint(Vector2.Zero, PathType.LINEAR)); currentSegmentLength = 1; } @@ -63,13 +77,33 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders controlPointVisualiser = new PathControlPointVisualiser(HitObject, false) }; - setState(SliderPlacementState.Initial); + state = SliderPlacementState.Initial; } protected override void LoadComplete() { base.LoadComplete(); inputManager = GetContainingInputManager(); + + if (freehandToolboxGroup != null) + { + freehandToolboxGroup.Tolerance.BindValueChanged(e => + { + bSplineBuilder.Tolerance = e.NewValue; + Scheduler.AddOnce(updateSliderPathFromBSplineBuilder); + }, true); + + freehandToolboxGroup.CornerThreshold.BindValueChanged(e => + { + bSplineBuilder.CornerThreshold = e.NewValue; + Scheduler.AddOnce(updateSliderPathFromBSplineBuilder); + }, true); + + freehandToolboxGroup.CircleThreshold.BindValueChanged(e => + { + Scheduler.AddOnce(updateSliderPathFromBSplineBuilder); + }, true); + } } [Resolved] @@ -84,10 +118,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders case SliderPlacementState.Initial: BeginPlacement(); - double? nearestSliderVelocity = (editorBeatmap.HitObjects - .LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocity; + double? nearestSliderVelocity = (editorBeatmap + .HitObjects + .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. @@ -95,7 +130,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders ApplyDefaultsToHitObject(); break; - case SliderPlacementState.Body: + case SliderPlacementState.ControlPoints: updateCursor(); break; } @@ -112,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders beginCurve(); break; - case SliderPlacementState.Body: + case SliderPlacementState.ControlPoints: if (canPlaceNewControlPoint(out var lastPoint)) { // Place a new point by detatching the current cursor. @@ -125,7 +160,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders Debug.Assert(lastPoint != null); segmentStart = lastPoint; - segmentStart.Type = PathType.Linear; + segmentStart.Type = PathType.LINEAR; currentSegmentLength = 1; } @@ -136,25 +171,58 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders return true; } + protected override bool OnDragStart(DragStartEvent e) + { + if (e.Button != MouseButton.Left) + return base.OnDragStart(e); + + if (state != SliderPlacementState.ControlPoints) + return base.OnDragStart(e); + + // Only enter drawing mode if no additional control points have been placed. + int controlPointCount = HitObject.Path.ControlPoints.Count; + if (controlPointCount > 2 || (controlPointCount == 2 && HitObject.Path.ControlPoints.Last() != cursor)) + return base.OnDragStart(e); + + bSplineBuilder.AddLinearPoint(Vector2.Zero); + bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position); + state = SliderPlacementState.Drawing; + return true; + } + + protected override void OnDrag(DragEvent e) + { + base.OnDrag(e); + + if (state == SliderPlacementState.Drawing) + { + bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMousePosition) - HitObject.Position); + Scheduler.AddOnce(updateSliderPathFromBSplineBuilder); + } + } + + protected override void OnDragEnd(DragEndEvent e) + { + base.OnDragEnd(e); + + if (state == SliderPlacementState.Drawing) + { + bSplineBuilder.Finish(); + updateSliderPathFromBSplineBuilder(); + + // Change the state so it will snap the expected distance in endCurve. + state = SliderPlacementState.Finishing; + endCurve(); + } + } + protected override void OnMouseUp(MouseUpEvent e) { - if (state == SliderPlacementState.Body && e.Button == MouseButton.Right) + if (state == SliderPlacementState.ControlPoints && e.Button == MouseButton.Right) endCurve(); base.OnMouseUp(e); } - private void beginCurve() - { - BeginPlacement(commitStart: true); - setState(SliderPlacementState.Body); - } - - private void endCurve() - { - updateSlider(); - EndPlacement(true); - } - protected override void Update() { base.Update(); @@ -164,23 +232,43 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders updatePathType(); } + private void beginCurve() + { + BeginPlacement(commitStart: true); + state = SliderPlacementState.ControlPoints; + } + + private void endCurve() + { + updateSlider(); + EndPlacement(true); + } + private void updatePathType() { + if (state == SliderPlacementState.Drawing) + { + segmentStart.Type = PathType.BSpline(4); + return; + } + switch (currentSegmentLength) { case 1: case 2: - segmentStart.Type = PathType.Linear; + segmentStart.Type = PathType.LINEAR; break; case 3: - segmentStart.Type = PathType.PerfectCurve; + segmentStart.Type = PathType.PERFECT_CURVE; break; default: - segmentStart.Type = PathType.Bezier; + segmentStart.Type = PathType.BEZIER; break; } + + controlPointVisualiser.EnsureValidPathTypes(); } private void updateCursor() @@ -192,13 +280,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = Vector2.Zero }); - // The path type should be adjusted in the progression of updatePathType() (Linear -> PC -> Bezier). + // The path type should be adjusted in the progression of updatePathType() (LINEAR -> PC -> BEZIER). currentSegmentLength++; updatePathType(); } // 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.ControlPoints ? SnapType.GlobalGrids : SnapType.All); cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position; } else if (cursor != null) @@ -207,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders HitObject.Path.ControlPoints.Remove(cursor); cursor = null; - // The path type should be adjusted in the reverse progression of updatePathType() (Bezier -> PC -> Linear). + // The path type should be adjusted in the reverse progression of updatePathType() (BEZIER -> PC -> LINEAR). currentSegmentLength--; updatePathType(); } @@ -230,22 +318,138 @@ 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; + if (state == SliderPlacementState.Drawing) + HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance; + else + HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; bodyPiece.UpdateFrom(HitObject); headCirclePiece.UpdateFrom(HitObject.HeadCircle); tailCirclePiece.UpdateFrom(HitObject.TailCircle); } - private void setState(SliderPlacementState newState) + private void updateSliderPathFromBSplineBuilder() { - state = newState; + IReadOnlyList> builderPoints = bSplineBuilder.ControlPoints; + + if (builderPoints.Count == 0 || builderPoints[0].Count == 0) + return; + + HitObject.Path.ControlPoints.Clear(); + + // Iterate through generated segments and adding non-inheriting path types where appropriate. + for (int i = 0; i < builderPoints.Count; i++) + { + bool isLastSegment = i == builderPoints.Count - 1; + var segment = builderPoints[i]; + + if (segment.Count == 0) + continue; + + // Replace this segment with a circular arc if it is a reasonable substitute. + var circleArcSegment = tryCircleArc(segment); + + if (circleArcSegment is not null) + { + HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[0], PathType.PERFECT_CURVE)); + HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[1])); + } + else + { + HitObject.Path.ControlPoints.Add(new PathControlPoint(segment[0], PathType.BSpline(4))); + for (int j = 1; j < segment.Count - 1; j++) + HitObject.Path.ControlPoints.Add(new PathControlPoint(segment[j])); + } + + if (isLastSegment) + HitObject.Path.ControlPoints.Add(new PathControlPoint(segment[^1])); + } + } + + private Vector2[] tryCircleArc(List segment) + { + if (segment.Count < 3 || freehandToolboxGroup?.CircleThreshold.Value == 0) return null; + + // Assume the segment creates a reasonable circular arc and then check if it reasonable + var points = PathApproximator.BSplineToPiecewiseLinear(segment.ToArray(), bSplineBuilder.Degree); + var circleArcControlPoints = new[] { points[0], points[points.Count / 2], points[^1] }; + var circleArc = new CircularArcProperties(circleArcControlPoints); + + if (!circleArc.IsValid) return null; + + double length = circleArc.ThetaRange * circleArc.Radius; + + if (length > 1000) return null; + + double loss = 0; + Vector2? lastPoint = null; + Vector2? lastVec = null; + Vector2? lastVec2 = null; + int? lastDir = null; + int? lastDir2 = null; + double totalWinding = 0; + + // Loop through the points and check if they are not too far away from the circular arc. + // Also make sure it curves monotonically in one direction and at most one loop is done. + foreach (var point in points) + { + var vec = point - circleArc.Centre; + loss += Math.Pow((vec.Length - circleArc.Radius) / length, 2); + + if (lastVec.HasValue) + { + double det = lastVec.Value.X * vec.Y - lastVec.Value.Y * vec.X; + int dir = Math.Sign(det); + + if (dir == 0) + continue; + + if (lastDir.HasValue && dir != lastDir) + return null; // Circle center is not inside the polygon + + lastDir = dir; + } + + lastVec = vec; + + if (lastPoint.HasValue) + { + var vec2 = point - lastPoint.Value; + + if (lastVec2.HasValue) + { + double dot = Vector2.Dot(vec2, lastVec2.Value); + double det = lastVec2.Value.X * vec2.Y - lastVec2.Value.Y * vec2.X; + double angle = Math.Atan2(det, dot); + int dir2 = Math.Sign(angle); + + if (dir2 == 0) + continue; + + if (lastDir2.HasValue && dir2 != lastDir2) + return null; // Curvature changed, like in an S-shape + + totalWinding += Math.Abs(angle); + lastDir2 = dir2; + } + + lastVec2 = vec2; + } + + lastPoint = point; + } + + loss /= points.Count; + + return loss > freehandToolboxGroup?.CircleThreshold.Value || totalWinding > MathHelper.TwoPi ? null : circleArcControlPoints; } private enum SliderPlacementState { Initial, - Body, + ControlPoints, + Drawing, + Finishing } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 6685507ee0..3575e15d1d 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders protected PathControlPointVisualiser ControlPointVisualiser { get; private set; } [Resolved(CanBeNull = true)] - private IDistanceSnapProvider snapProvider { get; set; } + private IDistanceSnapProvider distanceSnapProvider { get; set; } [Resolved(CanBeNull = true)] private IPlacementHandler placementHandler { get; set; } @@ -188,21 +188,30 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [CanBeNull] private PathControlPoint placementControlPoint; - protected override bool OnDragStart(DragStartEvent e) => placementControlPoint != null; + protected override bool OnDragStart(DragStartEvent e) + { + if (placementControlPoint == null) + return base.OnDragStart(e); + + ControlPointVisualiser?.DragStarted(placementControlPoint); + return true; + } protected override void OnDrag(DragEvent e) { + base.OnDrag(e); + if (placementControlPoint != null) - { - var result = snapProvider?.FindSnappedPositionAndTime(ToScreenSpace(e.MousePosition)); - placementControlPoint.Position = ToLocalSpace(result?.ScreenSpacePosition ?? ToScreenSpace(e.MousePosition)) - HitObject.Position; - } + ControlPointVisualiser?.DragInProgress(e); } protected override void OnMouseUp(MouseUpEvent e) { if (placementControlPoint != null) { + if (IsDragged) + ControlPointVisualiser?.DragEnded(); + placementControlPoint = null; changeHandler?.EndChange(); } @@ -245,7 +254,9 @@ 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); + ControlPointVisualiser?.EnsureValidPathTypes(); + + HitObject.SnapTo(distanceSnapProvider); return pathControlPoint; } @@ -266,8 +277,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders controlPoints.Remove(c); } + ControlPointVisualiser?.EnsureValidPathTypes(); + // 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 +328,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/FreehandSliderToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/FreehandSliderToolboxGroup.cs new file mode 100644 index 0000000000..f17118ba34 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/FreehandSliderToolboxGroup.cs @@ -0,0 +1,132 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Edit; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public partial class FreehandSliderToolboxGroup : EditorToolboxGroup + { + public FreehandSliderToolboxGroup() + : base("slider") + { + } + + public BindableFloat Tolerance { get; } = new BindableFloat(1.8f) + { + MinValue = 0.05f, + MaxValue = 2.0f, + Precision = 0.01f + }; + + public BindableFloat CornerThreshold { get; } = new BindableFloat(0.4f) + { + MinValue = 0.05f, + MaxValue = 1f, + Precision = 0.01f + }; + + public BindableFloat CircleThreshold { get; } = new BindableFloat(0.0015f) + { + MinValue = 0f, + MaxValue = 0.005f, + Precision = 0.0001f + }; + + // We map internal ranges to a more standard range of values for display to the user. + private readonly BindableInt displayTolerance = new BindableInt(90) + { + MinValue = 5, + MaxValue = 100 + }; + + private readonly BindableInt displayCornerThreshold = new BindableInt(40) + { + MinValue = 5, + MaxValue = 100 + }; + + private readonly BindableInt displayCircleThreshold = new BindableInt(30) + { + MinValue = 0, + MaxValue = 100 + }; + + private ExpandableSlider toleranceSlider = null!; + private ExpandableSlider cornerThresholdSlider = null!; + private ExpandableSlider circleThresholdSlider = null!; + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + toleranceSlider = new ExpandableSlider + { + Current = displayTolerance + }, + cornerThresholdSlider = new ExpandableSlider + { + Current = displayCornerThreshold + }, + circleThresholdSlider = new ExpandableSlider + { + Current = displayCircleThreshold + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + displayTolerance.BindValueChanged(tolerance => + { + toleranceSlider.ContractedLabelText = $"C. P. S.: {tolerance.NewValue:N0}"; + toleranceSlider.ExpandedLabelText = $"Control Point Spacing: {tolerance.NewValue:N0}"; + + Tolerance.Value = displayToInternalTolerance(tolerance.NewValue); + }, true); + + displayCornerThreshold.BindValueChanged(threshold => + { + cornerThresholdSlider.ContractedLabelText = $"C. T.: {threshold.NewValue:N0}"; + cornerThresholdSlider.ExpandedLabelText = $"Corner Threshold: {threshold.NewValue:N0}"; + + CornerThreshold.Value = displayToInternalCornerThreshold(threshold.NewValue); + }, true); + + displayCircleThreshold.BindValueChanged(threshold => + { + circleThresholdSlider.ContractedLabelText = $"P. C. T.: {threshold.NewValue:N0}"; + circleThresholdSlider.ExpandedLabelText = $"Perfect Curve Threshold: {threshold.NewValue:N0}"; + + CircleThreshold.Value = displayToInternalCircleThreshold(threshold.NewValue); + }, true); + + Tolerance.BindValueChanged(tolerance => + displayTolerance.Value = internalToDisplayTolerance(tolerance.NewValue) + ); + CornerThreshold.BindValueChanged(threshold => + displayCornerThreshold.Value = internalToDisplayCornerThreshold(threshold.NewValue) + ); + CircleThreshold.BindValueChanged(threshold => + displayCircleThreshold.Value = internalToDisplayCircleThreshold(threshold.NewValue) + ); + + float displayToInternalTolerance(float v) => v / 50f; + int internalToDisplayTolerance(float v) => (int)Math.Round(v * 50f); + + float displayToInternalCornerThreshold(float v) => v / 100f; + int internalToDisplayCornerThreshold(float v) => (int)Math.Round(v * 100f); + + float displayToInternalCircleThreshold(float v) => v / 20000f; + int internalToDisplayCircleThreshold(float v) => (int)Math.Round(v * 20000f); + } + } +} 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 0b80750a02..448cfaf84c 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; @@ -16,8 +17,8 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics; 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 +31,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,18 +50,30 @@ 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 = OsuIcon.EditorGridSnap }) + }); private BindableList selectedHitObjects; private Bindable placementObject; + [Cached(typeof(IDistanceSnapProvider))] + protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider(); + + [Cached] + protected readonly FreehandSliderToolboxGroup FreehandlSliderToolboxGroup = new FreehandSliderToolboxGroup(); + [BackgroundDependencyLoader] private void load() { + AddInternal(DistanceSnapProvider); + DistanceSnapProvider.AttachToToolbox(RightToolbox); + // Give a bit of breathing room around the playfield content. PlayfieldContentContainer.Padding = new MarginPadding(10); @@ -81,10 +94,17 @@ 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.AddRange(new EditorToolboxGroup[] + { + new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, }, + FreehandlSliderToolboxGroup + } + ); } protected override ComposeBlueprintContainer CreateBlueprintContainer() @@ -93,6 +113,34 @@ namespace osu.Game.Rulesets.Osu.Edit public override string ConvertSelectionToString() => string.Join(',', selectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString())); + // 1,2,3,4 ... + private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled); + + public override void SelectFromTimestamp(double timestamp, string objectDescription) + { + if (!selection_regex.IsMatch(objectDescription)) + return; + + List remainingHitObjects = EditorBeatmap.HitObjects.Cast().Where(h => h.StartTime >= timestamp).ToList(); + string[] splitDescription = objectDescription.Split(',').ToArray(); + + for (int i = 0; i < splitDescription.Length; i++) + { + if (!int.TryParse(splitDescription[i], out int combo) || combo < 1) + continue; + + OsuHitObject current = remainingHitObjects.FirstOrDefault(h => h.IndexInCurrentCombo + 1 == combo); + + if (current == null) + continue; + + EditorBeatmap.SelectedHitObjects.Add(current); + + if (i < splitDescription.Length - 1) + remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList(); + } + } + private DistanceSnapGrid distanceSnapGrid; private Container distanceSnapGridContainer; @@ -101,14 +149,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(); @@ -138,7 +178,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)) @@ -152,7 +192,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)); @@ -215,7 +255,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) @@ -257,14 +297,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/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 2a6d6ce4c3..cea2adc6e2 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,18 +38,17 @@ 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.CanScaleDiagonally = SelectionBox.CanScaleX && SelectionBox.CanScaleY; SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider); } protected override void OnOperationEnded() { base.OnOperationEnded(); - referenceOrigin = null; referencePathTypes = null; } @@ -109,13 +104,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 +164,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 +196,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 +212,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 +230,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 +260,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 +285,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. /// @@ -367,7 +321,7 @@ namespace osu.Game.Rulesets.Osu.Edit if (mergedHitObject.Path.ControlPoints.Count == 0) { - mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(Vector2.Zero, PathType.Linear)); + mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(Vector2.Zero, PathType.LINEAR)); } // Merge all the selected hit objects into one slider path. @@ -397,7 +351,7 @@ namespace osu.Game.Rulesets.Osu.Edit // Turn the last control point into a linear type if this is the first merging circle in a sequence, so the subsequent control points can be inherited path type. if (!lastCircle) { - mergedHitObject.Path.ControlPoints.Last().Type = PathType.Linear; + mergedHitObject.Path.ControlPoints.Last().Type = PathType.LINEAR; } mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(selectedMergeableObject.Position - mergedHitObject.Position)); 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/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/OsuSpinnerJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs index 941cb667cf..e8fc1d99bc 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs @@ -4,6 +4,7 @@ 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 { @@ -15,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/Mods/OsuModAccuracyChallenge.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAccuracyChallenge.cs index 5b79753632..e6daa3846f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAccuracyChallenge.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAccuracyChallenge.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. -using System; -using System.Linq; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Osu.Mods { public class OsuModAccuracyChallenge : ModAccuracyChallenge { - public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray(); } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 3841c9c716..efcc728d55 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -16,7 +16,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModAutopilot : Mod, IApplicableFailOverride, IUpdatableByPlayfield, IApplicableToDrawableRuleset + public class OsuModAutopilot : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset { public override string Name => "Autopilot"; public override string Acronym => "AP"; @@ -29,17 +29,12 @@ namespace osu.Game.Rulesets.Osu.Mods { typeof(OsuModSpunOut), typeof(ModRelax), - typeof(ModFailCondition), - typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), - typeof(OsuModRepel) + typeof(OsuModRepel), + typeof(ModTouchDevice) }; - public bool PerformFail() => false; - - public bool RestartOnFail => false; - private OsuInputManager inputManager = null!; private List replayFrames = null!; 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..b34cc29741 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs @@ -2,11 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -22,6 +20,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Mods { @@ -61,10 +60,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; @@ -88,21 +89,18 @@ namespace osu.Game.Rulesets.Osu.Mods break; default: - addBubble(); + BubbleDrawable bubble = bubblePool.Get(); + + bubble.WasHit = drawable.IsHit; + bubble.Position = getPosition(drawableOsuHitObject); + bubble.AccentColour = drawable.AccentColour.Value; + bubble.InitialSize = new Vector2(bubbleSize); + bubble.FadeTime = bubbleFade; + bubble.MaxSize = maxSize; + + bubbleContainer.Add(bubble); break; } - - void addBubble() - { - BubbleDrawable bubble = bubblePool.Get(); - - bubble.DrawableOsuHitObject = drawableOsuHitObject; - bubble.InitialSize = new Vector2(bubbleSize); - bubble.FadeTime = bubbleFade; - bubble.MaxSize = maxSize; - - bubbleContainer.Add(bubble); - } }; drawableObject.OnRevertResult += (drawable, _) => @@ -116,18 +114,38 @@ namespace osu.Game.Rulesets.Osu.Mods }; } + private Vector2 getPosition(DrawableOsuHitObject drawableObject) + { + switch (drawableObject) + { + // SliderHeads are derived from HitCircles, + // so we must handle them before to avoid them using the wrong positioning logic + case DrawableSliderHead: + return drawableObject.HitObject.Position; + + // Using hitobject position will cause issues with HitCircle placement due to stack leniency. + case DrawableHitCircle: + return drawableObject.Position; + + default: + return drawableObject.HitObject.Position; + } + } + #region Pooled Bubble drawable private partial class BubbleDrawable : PoolableDrawable { - public DrawableOsuHitObject? DrawableOsuHitObject { get; set; } - public Vector2 InitialSize { get; set; } public float MaxSize { get; set; } public double FadeTime { get; set; } + public bool WasHit { get; set; } + + public Color4 AccentColour { get; set; } + private readonly Box colourBox; private readonly CircularContainer content; @@ -155,15 +173,12 @@ namespace osu.Game.Rulesets.Osu.Mods protected override void PrepareForUse() { - Debug.Assert(DrawableOsuHitObject.IsNotNull()); - - Colour = DrawableOsuHitObject.IsHit ? Colour4.White : Colour4.Black; + Colour = WasHit ? Colour4.White : Colour4.Black; Scale = new Vector2(1); - Position = getPosition(DrawableOsuHitObject); Size = InitialSize; //We want to fade to a darker colour to avoid colours such as white hiding the "ripple" effect. - ColourInfo colourDarker = DrawableOsuHitObject.AccentColour.Value.Darken(0.1f); + ColourInfo colourDarker = AccentColour.Darken(0.1f); // The absolute length of the bubble's animation, can be used in fractions for animations of partial length double duration = 1700 + Math.Pow(FadeTime, 1.07f); @@ -176,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Mods .ScaleTo(MaxSize * 1.5f, duration * 0.2f, Easing.OutQuint) .FadeOut(duration * 0.2f, Easing.OutCirc).Expire(); - if (!DrawableOsuHitObject.IsHit) return; + if (!WasHit) return; content.BorderThickness = InitialSize.X / 3.5f; content.BorderColour = Colour4.White; @@ -190,24 +205,6 @@ namespace osu.Game.Rulesets.Osu.Mods // Avoids transparency overlap issues during the bubble "pop" .TransformTo(nameof(BorderThickness), 0f); } - - private Vector2 getPosition(DrawableOsuHitObject drawableObject) - { - switch (drawableObject) - { - // SliderHeads are derived from HitCircles, - // so we must handle them before to avoid them using the wrong positioning logic - case DrawableSliderHead: - return drawableObject.HitObject.Position; - - // Using hitobject position will cause issues with HitCircle placement due to stack leniency. - case DrawableHitCircle: - return drawableObject.Position; - - default: - return drawableObject.HitObject.Position; - } - } } #endregion diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 250d97c537..10d7af5e58 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -11,22 +11,20 @@ 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; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset + public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset, IApplicableHealthProcessor { public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModStrictTracking)).ToArray(); [SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")] public Bindable 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); @@ -36,6 +34,9 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("Fade out hit circles earlier", "Make hit circles fade out into a miss, rather than after it.")] public Bindable FadeHitCircleEarly { get; } = new Bindable(true); + [SettingSource("Classic health", "More closely resembles the original HP drain mechanics.")] + public Bindable ClassicHealth { get; } = new Bindable(true); + private bool usingHiddenFading; public void ApplyToHitObject(HitObject hitObject) @@ -43,11 +44,7 @@ namespace osu.Game.Rulesets.Osu.Mods switch (hitObject) { case Slider slider: - slider.OnlyJudgeNestedObjects = !NoSliderHeadAccuracy.Value; - - foreach (var head in slider.NestedHitObjects.OfType()) - head.JudgeAsNormalHitCircle = !NoSliderHeadAccuracy.Value; - + slider.ClassicSliderBehaviour = NoSliderHeadAccuracy.Value; break; } } @@ -57,7 +54,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 +67,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,21 +82,43 @@ 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); + } } }; } + + public HealthProcessor? CreateHealthProcessor(double drainStartTime) => ClassicHealth.Value ? new OsuLegacyHealthProcessor(drainStartTime) : null; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs new file mode 100644 index 0000000000..b70d607ca1 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs @@ -0,0 +1,162 @@ +// 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.Bindables; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.UI; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Mods +{ + public class OsuModDepth : ModWithVisibilityAdjustment, IUpdatableByPlayfield, IApplicableToDrawableRuleset + { + public override string Name => "Depth"; + public override string Acronym => "DP"; + public override IconUsage? Icon => FontAwesome.Solid.Cube; + public override ModType Type => ModType.Fun; + public override LocalisableString Description => "3D. Almost."; + public override double ScoreMultiplier => 1; + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame), typeof(ModWithVisibilityAdjustment) }).ToArray(); + + private static readonly Vector3 camera_position = new Vector3(OsuPlayfield.BASE_SIZE.X * 0.5f, OsuPlayfield.BASE_SIZE.Y * 0.5f, -200); + private readonly float sliderMinDepth = depthForScale(1.5f); // Depth at which slider's scale will be 1.5f + + [SettingSource("Maximum depth", "How far away objects appear.", 0)] + public BindableFloat MaxDepth { get; } = new BindableFloat(100) + { + Precision = 10, + MinValue = 50, + MaxValue = 200 + }; + + [SettingSource("Show Approach Circles", "Whether approach circles should be visible.", 1)] + public BindableBool ShowApproachCircles { get; } = new BindableBool(true); + + protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) => applyTransform(hitObject, state); + + protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) => applyTransform(hitObject, state); + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + // Hide judgment displays and follow points as they won't make any sense. + // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. + drawableRuleset.Playfield.DisplayJudgements.Value = false; + (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); + } + + private void applyTransform(DrawableHitObject drawable, ArmedState state) + { + switch (drawable) + { + case DrawableHitCircle circle: + if (!ShowApproachCircles.Value) + { + var hitObject = (OsuHitObject)drawable.HitObject; + double appearTime = hitObject.StartTime - hitObject.TimePreempt; + + using (circle.BeginAbsoluteSequence(appearTime)) + circle.ApproachCircle.Hide(); + } + + break; + } + } + + public void Update(Playfield playfield) + { + double time = playfield.Time.Current; + + foreach (var drawable in playfield.HitObjectContainer.AliveObjects) + { + switch (drawable) + { + case DrawableHitCircle circle: + processHitObject(time, circle); + break; + + case DrawableSlider slider: + processSlider(time, slider); + break; + } + } + } + + private void processHitObject(double time, DrawableOsuHitObject drawable) + { + var hitObject = drawable.HitObject; + + // Circles are always moving at the constant speed. They'll fade out before reaching the camera even at extreme conditions (AR 11, max depth). + double speed = MaxDepth.Value / hitObject.TimePreempt; + double appearTime = hitObject.StartTime - hitObject.TimePreempt; + float z = MaxDepth.Value - (float)((Math.Max(time, appearTime) - appearTime) * speed); + + float scale = scaleForDepth(z); + drawable.Position = toPlayfieldPosition(scale, hitObject.StackedPosition); + drawable.Scale = new Vector2(scale); + } + + private void processSlider(double time, DrawableSlider drawableSlider) + { + var hitObject = drawableSlider.HitObject; + + double baseSpeed = MaxDepth.Value / hitObject.TimePreempt; + double appearTime = hitObject.StartTime - hitObject.TimePreempt; + + // Allow slider to move at a constant speed if its scale at the end time will be lower than 1.5f + float zEnd = MaxDepth.Value - (float)((Math.Max(hitObject.StartTime + hitObject.Duration, appearTime) - appearTime) * baseSpeed); + + if (zEnd > sliderMinDepth) + { + processHitObject(time, drawableSlider); + return; + } + + double offsetAfterStartTime = hitObject.Duration + 500; + double slowSpeed = Math.Min(-sliderMinDepth / offsetAfterStartTime, baseSpeed); + + double decelerationTime = hitObject.TimePreempt * 0.2; + float decelerationDistance = (float)(decelerationTime * (baseSpeed + slowSpeed) * 0.5); + + float z; + + if (time < hitObject.StartTime - decelerationTime) + { + float fullDistance = decelerationDistance + (float)(baseSpeed * (hitObject.TimePreempt - decelerationTime)); + z = fullDistance - (float)((Math.Max(time, appearTime) - appearTime) * baseSpeed); + } + else if (time < hitObject.StartTime) + { + double timeOffset = time - (hitObject.StartTime - decelerationTime); + double deceleration = (slowSpeed - baseSpeed) / decelerationTime; + z = decelerationDistance - (float)(baseSpeed * timeOffset + deceleration * timeOffset * timeOffset * 0.5); + } + else + { + double endTime = hitObject.StartTime + offsetAfterStartTime; + z = -(float)((Math.Min(time, endTime) - hitObject.StartTime) * slowSpeed); + } + + float scale = scaleForDepth(z); + drawableSlider.Position = toPlayfieldPosition(scale, hitObject.StackedPosition); + drawableSlider.Scale = new Vector2(scale); + } + + private static float scaleForDepth(float depth) => -camera_position.Z / Math.Max(1f, depth - camera_position.Z); + + private static float depthForScale(float scale) => -camera_position.Z / scale + camera_position.Z; + + private static Vector2 toPlayfieldPosition(float scale, Vector2 positionAtZeroDepth) + { + return (positionAtZeroDepth - camera_position.Xy) * scale + camera_position.Xy; + } + } +} 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..5a6cc50082 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableHitObject(DrawableHitObject drawable) { if (drawable is DrawableSlider s) - s.Tracking.ValueChanged += flashlight.OnSliderTrackingChange; + s.OnUpdate += _ => flashlight.OnSliderTrackingChange(s); } private partial class OsuFlashlight : Flashlight, IRequireHighFrequencyMousePosition @@ -66,10 +66,10 @@ namespace osu.Game.Rulesets.Osu.Mods FlashlightSmoothness = 1.4f; } - public void OnSliderTrackingChange(ValueChangedEvent e) + public void OnSliderTrackingChange(DrawableSlider 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 = Time.Current >= e.HitObject.StartTime && e.Tracking.Value ? 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..06cb9c3419 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), typeof(OsuModDepth) }).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..6dc0d5d522 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override LocalisableString Description => @"Play with no approach circles and fading circles/sliders."; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1; - public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) }; + public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModDepth) }; public const double FADE_IN_DURATION_MULTIPLIER = 0.4; public const double FADE_OUT_DURATION_MULTIPLIER = 0.3; @@ -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/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index c8c4cd6a14..befee4af5a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override LocalisableString Description => "No need to chase the circles – your cursor is a magnet!"; public override double ScoreMultiplier => 0.5; - public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles), typeof(OsuModDepth) }; [SettingSource("Attraction strength", "How strong the pull is.", 0)] public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNoFail.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNoFail.cs index 9f707a5aa6..53c67cd1c3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModNoFail.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModNoFail.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. -using System; -using System.Linq; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Osu.Mods { public class OsuModNoFail : ModNoFail { - public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray(); } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs index 6f1206382a..1df344648a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods protected virtual float EndScale => 1; - public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) }; + public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween), typeof(OsuModDepth) }; protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModPerfect.cs b/osu.Game.Rulesets.Osu/Mods/OsuModPerfect.cs index 33581405a6..da462eb6e8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModPerfect.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModPerfect.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. -using System; -using System.Linq; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Osu.Mods { public class OsuModPerfect : ModPerfect { - public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray(); } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index aaa7c70a8d..40fadfb77e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (!slider.HeadCircle.IsHit) handleHitCircle(slider.HeadCircle); - requiresHold |= slider.Ball.IsHovered || h.IsHovered; + requiresHold |= slider.SliderInputManager.IsMouseInFollowArea(true); break; case DrawableSpinner spinner: diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs index 28d459cedb..91feb33931 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override LocalisableString Description => "Hit objects run away!"; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModBubbles) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModBubbles), typeof(OsuModDepth) }; [SettingSource("Repulsion strength", "How strong the repulsion is.", 0)] public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs index b0533d0cfa..59a1342480 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods // todo: this mod needs to be incompatible with "hidden" due to forcing the circle to remain opaque, // further implementation will be required for supporting that. - public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModObjectScaleTween), typeof(OsuModHidden) }; + public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModObjectScaleTween), typeof(OsuModHidden), typeof(OsuModDepth) }; private const int rotate_offset = 360; private const float rotate_starting_width = 2; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index 72031b4958..2c9292c58b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -36,6 +36,9 @@ namespace osu.Game.Rulesets.Osu.Mods { if (e.NewValue || slider.Judged) return; + if (slider.Time.Current < slider.HitObject.StartTime) + return; + var tail = slider.NestedHitObjects.OfType().First(); if (!tail.Judged) @@ -96,14 +99,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 +132,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/OsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs index b4edb1581e..e661610fe7 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs @@ -11,7 +11,6 @@ namespace osu.Game.Rulesets.Osu.Mods { public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { - typeof(OsuModAutopilot), typeof(OsuModTargetPractice), }).ToArray(); } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs index 9537f8b388..e1123807cd 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Mods d.HitObjectApplied += _ => { - // slider tails are a painful edge case, as their start time is offset 36ms back (see `LegacyLastTick`). + // slider tails are a painful edge case, as their start time is offset 36ms back (see `LastTick`). // to work around this, look up the slider tail's parenting slider's end time instead to ensure proper snap. double snapTime = d is DrawableSliderTail tail ? tail.Slider.GetEndTime() diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs index 77cf340b95..a5846efdfe 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs @@ -47,7 +47,8 @@ namespace osu.Game.Rulesets.Osu.Mods typeof(OsuModRandom), typeof(OsuModSpunOut), typeof(OsuModStrictTracking), - typeof(OsuModSuddenDeath) + typeof(OsuModSuddenDeath), + typeof(OsuModDepth) }).ToArray(); [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))] diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs index fd5c46a226..f1468d414e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs @@ -1,18 +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.Framework.Localisation; +using System; +using System.Linq; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModTouchDevice : Mod + public class OsuModTouchDevice : ModTouchDevice { - public override string Name => "Touch Device"; - public override string Acronym => "TD"; - public override LocalisableString Description => "Automatically applied to plays on devices with a touchscreen."; - public override double ScoreMultiplier => 1; - - public override ModType Type => ModType.System; + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray(); } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index 25d05a88a8..9671f53bea 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override LocalisableString Description => "Put your faith in the approach circles..."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) }; + public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) }; protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs index 2354cd50ae..b6907af119 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), typeof(OsuModDepth) }).ToArray(); private float theta; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs index a45338d91f..d14a821541 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override LocalisableString Description => "They just won't stay still..."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModDepth) }; private const int wiggle_duration = 100; // (ms) Higher = fewer wiggles diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 3458069dd1..0d665cad0c 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; @@ -26,13 +28,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public partial class DrawableHitCircle : DrawableOsuHitObject, IHasApproachCircle { - public OsuAction? HitAction => HitArea.HitAction; + public OsuAction? HitAction => HitArea?.HitAction; protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle; public SkinnableDrawable ApproachCircle { get; private set; } 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..5b379a0d90 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); } } @@ -93,12 +97,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// public virtual void Shake() { } + /// + /// Causes this to get hit, disregarding all conditions in implementations of . + /// + public void HitForcefully() => ApplyResult(r => r.Type = r.Judgement.MaxResult); + /// /// Causes this to get missed, disregarding all conditions in implementations of . /// 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 01174d4d61..baec200107 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -4,12 +4,14 @@ #nullable disable using System; +using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Layout; using osu.Game.Audio; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Objects; @@ -35,13 +37,21 @@ 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. /// public Container OverlayElementContainer { get; private set; } - public override bool DisplayResult => !HitObject.OnlyJudgeNestedObjects; + public override bool DisplayResult => HitObject.ClassicSliderBehaviour; [CanBeNull] public PlaySliderBody SliderBody => Body.Drawable as PlaySliderBody; @@ -49,12 +59,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public IBindable PathVersion => pathVersion; private readonly Bindable pathVersion = new Bindable(); + public readonly SliderInputManager SliderInputManager; + private Container headContainer; private Container tailContainer; private Container tickContainer; private Container repeatContainer; private PausableSkinnableSound slidingSample; + private readonly LayoutValue relativeAnchorPositionLayout; + public DrawableSlider() : this(null) { @@ -63,13 +77,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public DrawableSlider([CanBeNull] Slider s = null) : base(s) { + SliderInputManager = new SliderInputManager(this); + Ball = new DrawableSliderBall { - GetInitialHitAction = () => HeadCircle.HitAction, BypassAutoSizeAxes = Axes.Both, AlwaysPresent = true, Alpha = 0 }; + AddLayout(relativeAnchorPositionLayout = new LayoutValue(Invalidation.DrawSize | Invalidation.MiscGeometry)); } [BackgroundDependencyLoader] @@ -79,6 +95,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables AddRangeInternal(new Drawable[] { + SliderInputManager, shakeContainer = new ShakeContainer { ShakeDuration = 30, @@ -99,7 +116,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables 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); @@ -111,8 +132,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables foreach (var drawableHitObject in NestedHitObjects) drawableHitObject.AccentColour.Value = colour.NewValue; }, true); - - Tracking.BindValueChanged(updateSlidingSample); } protected override void OnApply() @@ -149,14 +168,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables slidingSample?.Stop(); } - private void updateSlidingSample(ValueChangedEvent tracking) - { - if (tracking.NewValue) - slidingSample?.Play(); - else - slidingSample?.Stop(); - } - protected override void AddNestedHitObject(DrawableHitObject hitObject) { base.AddNestedHitObject(hitObject); @@ -179,6 +190,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables repeatContainer.Add(repeat); break; } + + relativeAnchorPositionLayout.Invalidate(); } protected override void ClearNestedHitObjects() @@ -219,32 +232,49 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.Update(); - Tracking.Value = Ball.Tracking; + Tracking.Value = SliderInputManager.Tracking; - if (Tracking.Value && slidingSample != null) - // keep the sliding sample playing at the current tracking position - slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball)); + if (slidingSample != null) + { + if (Tracking.Value && Time.Current >= HitObject.StartTime) + { + // keep the sliding sample playing at the current tracking position + if (!slidingSample.IsPlaying) + slidingSample.Play(); + slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball)); + } + else if (slidingSample.IsPlaying) + slidingSample.Stop(); + } + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + // During slider path editing, the PlaySliderBody is scheduled to refresh once on Update. + // It is crucial to perform the code below in UpdateAfterChildren. This ensures that the SliderBody has the opportunity + // to update its Size and PathOffset beforehand, ensuring correct placement. 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) - { - if (hitObject is ITrackSnaking s) s.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0)); - if (hitObject is IRequireTracking t) t.Tracking = Ball.Tracking; - } + foreach (DrawableSliderRepeat repeat in repeatContainer) + repeat.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0)); Size = SliderBody?.Size ?? Vector2.Zero; OriginPosition = SliderBody?.PathOffset ?? Vector2.Zero; - if (DrawSize != Vector2.Zero) + if (!relativeAnchorPositionLayout.IsValid) { - var childAnchorPosition = Vector2.Divide(OriginPosition, DrawSize); + Vector2 pos = Vector2.Divide(OriginPosition, DrawSize); foreach (var obj in NestedHitObjects) - obj.RelativeAnchorPosition = childAnchorPosition; - Ball.RelativeAnchorPosition = childAnchorPosition; + obj.RelativeAnchorPosition = pos; + Ball.RelativeAnchorPosition = pos; + + relativeAnchorPositionLayout.Validate(); } } @@ -256,39 +286,40 @@ 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. - // But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc). - if (HitObject.OnlyJudgeNestedObjects) + if (HitObject.ClassicSliderBehaviour) { - ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult); - return; - } - - // Otherwise, if this slider also needs to be judged, apply judgement proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring. - ApplyResult(r => - { - int totalTicks = NestedHitObjects.Count; - int hitTicks = NestedHitObjects.Count(h => h.IsHit); - - if (hitTicks == totalTicks) - r.Type = HitResult.Great; - else if (hitTicks == 0) - r.Type = HitResult.Miss; - else + // Classic behaviour means a slider is judged proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring. + ApplyResult(r => { - double hitFraction = (double)hitTicks / totalTicks; - r.Type = hitFraction >= 0.5 ? HitResult.Ok : HitResult.Meh; - } - }); + int totalTicks = NestedHitObjects.Count; + int hitTicks = NestedHitObjects.Count(h => h.IsHit); + + if (hitTicks == totalTicks) + r.Type = HitResult.Great; + else if (hitTicks == 0) + r.Type = HitResult.Miss; + else + { + double hitFraction = (double)hitTicks / totalTicks; + r.Type = hitFraction >= 0.5 ? HitResult.Ok : HitResult.Meh; + } + }); + } + else + { + // If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes. + // But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc). + ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult); + } } 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(); } @@ -317,7 +348,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 d06fb5b4de..46f0231981 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs @@ -4,13 +4,9 @@ #nullable disable using System; -using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input; -using osu.Framework.Input.Events; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Skinning.Default; @@ -20,13 +16,10 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public partial class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition + public partial class DrawableSliderBall : CircularContainer, ISliderProgress { public const float FOLLOW_AREA = 2.4f; - public Func GetInitialHitAction; - - private Drawable followCircleReceptor; private DrawableSlider drawableSlider; private Drawable ball; @@ -37,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Size = OsuHitObject.OBJECT_DIMENSIONS; Children = new[] { @@ -47,13 +40,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Anchor = Anchor.Centre, RelativeSizeAxes = Axes.Both, }, - followCircleReceptor = new CircularContainer - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true - }, ball = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall()) { Anchor = Anchor.Centre, @@ -62,14 +48,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }; } - private Vector2? lastScreenSpaceMousePosition; - - protected override bool OnMouseMove(MouseMoveEvent e) - { - lastScreenSpaceMousePosition = e.ScreenSpaceMousePosition; - return base.OnMouseMove(e); - } - public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null) { // Consider the case of rewinding - children's transforms are handled internally, so propagating down @@ -85,99 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.ApplyTransformsAt(time, false); } - private bool tracking; - - public bool Tracking - { - get => tracking; - private set - { - if (value == tracking) - return; - - tracking = value; - - followCircleReceptor.Scale = new Vector2(tracking ? FOLLOW_AREA : 1f); - } - } - - /// - /// If the cursor moves out of the ball's radius we still need to be able to receive positional updates to stop tracking. - /// - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - - /// - /// The point in time after which we can accept any key for tracking. Before this time, we may need to restrict tracking to the key used to hit the head circle. - /// - /// This is a requirement to stop the case where a player holds down one key (from before the slider) and taps the second key while maintaining full scoring (tracking) of sliders. - /// Visually, this special case can be seen below (time increasing from left to right): - /// - /// Z Z+X Z - /// o========o - /// - /// Without this logic, tracking would continue through the entire slider even though no key hold action is directly attributing to it. - /// - /// In all other cases, no special handling is required (either key being pressed is allowable as valid tracking). - /// - /// The reason for storing this as a time value (rather than a bool) is to correctly handle rewind scenarios. - /// - private double? timeToAcceptAnyKeyAfter; - - /// - /// The actions that were pressed in the previous frame. - /// - private readonly List lastPressedActions = new List(); - - protected override void Update() - { - base.Update(); - - // from the point at which the head circle is hit, this will be non-null. - // it may be null if the head circle was missed. - var headCircleHitAction = GetInitialHitAction(); - - if (headCircleHitAction == null) - timeToAcceptAnyKeyAfter = null; - - var actions = drawableSlider.OsuActionInputManager?.PressedActions; - - // if the head circle was hit with a specific key, tracking should only occur while that key is pressed. - if (headCircleHitAction != null && timeToAcceptAnyKeyAfter == null) - { - var otherKey = headCircleHitAction == OsuAction.RightButton ? OsuAction.LeftButton : OsuAction.RightButton; - - // we can start accepting any key once all other keys have been released in the previous frame. - if (!lastPressedActions.Contains(otherKey)) - timeToAcceptAnyKeyAfter = Time.Current; - } - - Tracking = - // in valid time range - Time.Current >= drawableSlider.HitObject.StartTime && Time.Current < drawableSlider.HitObject.EndTime && - // in valid position range - lastScreenSpaceMousePosition.HasValue && followCircleReceptor.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) && - // valid action - (actions?.Any(isValidTrackingAction) ?? false); - - lastPressedActions.Clear(); - if (actions != null) - lastPressedActions.AddRange(actions); - } - - /// - /// Check whether a given user input is a valid tracking action. - /// - private bool isValidTrackingAction(OsuAction action) - { - bool headCircleHit = GetInitialHitAction().HasValue; - - // if the head circle was hit, we may not yet be allowed to accept any key, so we must use the initial hit action. - if (headCircleHit && (!timeToAcceptAnyKeyAfter.HasValue || Time.Current <= timeToAcceptAnyKeyAfter.Value)) - return action == GetInitialHitAction(); - - return action == OsuAction.LeftButton || action == OsuAction.RightButton; - } - private Vector2? lastPosition; public void UpdateProgress(double completionProgress) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index b8a1efabe0..76b9fdc3ce 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,18 +14,18 @@ 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; + public override bool DisplayResult + { + get + { + if (HitObject?.ClassicSliderBehaviour == true) + return false; - /// - /// 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; + return base.DisplayResult; + } + } private readonly IBindable pathVersion = new Bindable(); @@ -60,36 +58,29 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables pathVersion.BindTo(DrawableSlider.PathVersion); - CheckHittable = (d, t) => DrawableSlider.CheckHittable?.Invoke(d, t) ?? true; + CheckHittable = (d, t, r) => DrawableSlider.CheckHittable?.Invoke(d, t, r) ?? ClickAction.Hit; } - protected override void Update() + protected override void CheckForResult(bool userTriggered, double timeOffset) { - 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); - } + base.CheckForResult(userTriggered, timeOffset); + DrawableSlider.SliderInputManager.PostProcessHeadJudgement(this); } protected override HitResult ResultFor(double timeOffset) { Debug.Assert(HitObject != null); - if (HitObject.JudgeAsNormalHitCircle) - return base.ResultFor(timeOffset); + if (HitObject.ClassicSliderBehaviour) + { + // With classic slider behaviour, heads are considered fully hit if in the largest hit window. + // We can't award a full Great because the true Great judgement is awarded on the Slider itself, + // reduced based on number of ticks hit, + // so we use the most suitable LargeTick judgement here instead. + return base.ResultFor(timeOffset).IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss; + } - // If not judged as a normal hitcircle, judge as a slider tick instead. This is the classic osu!stable scoring. - var result = base.ResultFor(timeOffset); - return result.IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss; + return base.ResultFor(timeOffset); } public override void Shake() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index fc4863f164..3239565528 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 { public new SliderRepeat HitObject => (SliderRepeat)base.HitObject; @@ -30,12 +30,10 @@ 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 DrawableSliderRepeat() : base(null) { @@ -50,7 +48,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 +63,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, + }, } }); @@ -79,11 +81,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Position = HitObject.Position - DrawableSlider.Position; } - 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); - } + protected override void CheckForResult(bool userTriggered, double timeOffset) => DrawableSlider.SliderInputManager.TryJudgeNestedObject(this, timeOffset); protected override void UpdateInitialTransforms() { @@ -114,11 +112,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..c4731118a1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public partial class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking + public partial class DrawableSliderTail : DrawableOsuHitObject { public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject; @@ -24,19 +24,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; - /// - /// The judgement text is provided by the . - /// - public override bool DisplayResult => false; - /// /// Whether the hit samples only play on successful hits. /// If false, the hit samples will also play on misses. /// public bool SamplePlaysOnlyOnHit { get; set; } = true; - public bool Tracking { get; set; } - public SkinnableDrawable CirclePiece { get; private set; } private Container scaleContainer; @@ -55,7 +48,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[] { @@ -123,11 +116,7 @@ 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); - } + protected override void CheckForResult(bool userTriggered, double timeOffset) => DrawableSlider.SliderInputManager.TryJudgeNestedObject(this, timeOffset); 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..d64fb0bcc6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -14,16 +14,12 @@ using osu.Framework.Graphics.Containers; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public partial class DrawableSliderTick : DrawableOsuHitObject, IRequireTracking + public partial class DrawableSliderTick : DrawableOsuHitObject { public const double ANIM_DURATION = 150; private const float default_tick_size = 16; - public bool Tracking { get; set; } - - public override bool DisplayResult => false; - protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; private SkinnableDrawable scaleContainer; @@ -41,7 +37,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 @@ -73,11 +69,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Position = HitObject.Position - DrawableSlider.HitObject.Position; } - protected override void CheckForResult(bool userTriggered, double timeOffset) - { - if (timeOffset >= 0) - ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult); - } + protected override void CheckForResult(bool userTriggered, double timeOffset) => DrawableSlider.SliderInputManager.TryJudgeNestedObject(this, timeOffset); protected override void UpdateInitialTransforms() { @@ -97,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables case ArmedState.Miss: this.FadeOut(ANIM_DURATION); - this.FadeColour(Color4.Red, ANIM_DURATION / 2); + this.TransformBindableTo(AccentColour, Color4.Red, 0); break; case ArmedState.Hit: diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 0ceda1d4b0..bf4b07eaab 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -16,6 +17,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Scoring; @@ -45,12 +47,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private const float spinning_sample_initial_frequency = 1.0f; private const float spinning_sample_modulated_base_frequency = 0.5f; + private PausableSkinnableSound maxBonusSample; + /// /// 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,8 +110,13 @@ 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 } + }, + maxBonusSample = new PausableSkinnableSound + { + MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME, } }); @@ -120,6 +136,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.OnFree(); spinningSample.ClearSamples(); + maxBonusSample.ClearSamples(); } protected override void LoadSamples() @@ -128,6 +145,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables spinningSample.Samples = HitObject.CreateSpinningSamples().Cast().ToArray(); spinningSample.Frequency.Value = spinning_sample_initial_frequency; + + maxBonusSample.Samples = new ISampleInfo[] { new SpinnerBonusMaxSampleInfo(HitObject.CreateHitSampleInfo()) }; } private void updateSpinningSample(ValueChangedEvent tracking) @@ -149,6 +168,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.StopAllSamples(); spinningSample?.Stop(); + maxBonusSample?.Stop(); } protected override void AddNestedHitObject(DrawableHitObject hitObject) @@ -218,7 +238,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); } } @@ -267,6 +287,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (spinningSample != null && spinnerFrequencyModulate) spinningSample.Frequency.Value = spinning_sample_modulated_base_frequency + Progress; + + // Ticks can theoretically be judged at any point in the spinner's duration. + // A tick must be alive to correctly play back samples, + // but for performance reasons, we only want to keep the next tick alive. + var next = NestedHitObjects.FirstOrDefault(h => !h.Judged); + + // See default `LifetimeStart` as set in `DrawableSpinnerTick`. + if (next?.LifetimeStart == double.MaxValue) + next.LifetimeStart = HitObject.StartTime; } protected override void UpdateAfterChildren() @@ -279,43 +308,63 @@ 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 static readonly int score_per_tick = new OsuScoreProcessor().GetBaseScoreForResult(new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxResult); 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) + if (tick == null) { + // we still want to play a sound. this will probably be a new sound in the future, but for now let's continue playing the bonus sound. + // TODO: this doesn't concurrency. i can't figure out how to make it concurrency. samples are bad and need a refactor. + maxBonusSample.Play(); + } + else tick.TriggerResult(true); - if (tick is DrawableSpinnerBonusTick) - gainedBonus.Value = score_per_tick * (spins - HitObject.SpinsRequired); - } + completedFullSpins.Value++; + } + } - wholeSpins++; + public class SpinnerBonusMaxSampleInfo : HitSampleInfo + { + public override IEnumerable LookupNames + { + get + { + foreach (string name in base.LookupNames) + yield return name; + + foreach (string name in base.LookupNames) + yield return name.Replace("-max", string.Empty); + } + } + + public SpinnerBonusMaxSampleInfo(HitSampleInfo sampleInfo) + : base("spinnerbonus-max", sampleInfo.Bank, sampleInfo.Suffix, sampleInfo.Volume) + + { } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index 34253e3d4f..5b55533edd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -11,8 +11,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public override bool DisplayResult => false; - protected DrawableSpinner DrawableSpinner => (DrawableSpinner)ParentHitObject; - public DrawableSpinnerTick() : this(null) { @@ -25,6 +23,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre; } + protected override void OnApply() + { + base.OnApply(); + + // Lifetime will be managed by `DrawableSpinner`. + LifetimeStart = double.MaxValue; + } + /// /// Apply a judgement result. /// diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/IRequireTracking.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/IRequireTracking.cs deleted file mode 100644 index b1815b23c9..0000000000 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/IRequireTracking.cs +++ /dev/null @@ -1,13 +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.Osu.Objects.Drawables -{ - public interface IRequireTracking - { - /// - /// Whether the is currently being tracked by the user. - /// - bool Tracking { get; set; } - } -} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/ITrackSnaking.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/ITrackSnaking.cs deleted file mode 100644 index cae2a7c36d..0000000000 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/ITrackSnaking.cs +++ /dev/null @@ -1,15 +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 osuTK; - -namespace osu.Game.Rulesets.Osu.Objects.Drawables -{ - /// - /// A component which tracks the current end snaking position of a slider. - /// - public interface ITrackSnaking - { - void UpdateSnakingPosition(Vector2 start, Vector2 end); - } -} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs new file mode 100644 index 0000000000..95896c7c91 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs @@ -0,0 +1,260 @@ +// 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.Graphics; +using osu.Framework.Input; +using osu.Framework.Input.Events; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables +{ + public partial class SliderInputManager : Component, IRequireHighFrequencyMousePosition + { + /// + /// Whether the slider is currently being tracked. + /// + public bool Tracking { get; private set; } + + /// + /// The point in time after which we can accept any key for tracking. Before this time, we may need to restrict tracking to the key used to hit the head circle. + /// + /// This is a requirement to stop the case where a player holds down one key (from before the slider) and taps the second key while maintaining full scoring (tracking) of sliders. + /// Visually, this special case can be seen below (time increasing from left to right): + /// + /// Z Z+X Z + /// o========o + /// + /// Without this logic, tracking would continue through the entire slider even though no key hold action is directly attributing to it. + /// + /// In all other cases, no special handling is required (either key being pressed is allowable as valid tracking). + /// + /// The reason for storing this as a time value (rather than a bool) is to correctly handle rewind scenarios. + /// + private double? timeToAcceptAnyKeyAfter; + + /// + /// The actions that were pressed in the previous frame. + /// + private readonly List lastPressedActions = new List(); + + private Vector2? screenSpaceMousePosition; + private readonly DrawableSlider slider; + + public SliderInputManager(DrawableSlider slider) + { + this.slider = slider; + } + + /// + /// This component handles all input of the slider, so it should receive input no matter the position. + /// + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + + protected override bool OnMouseMove(MouseMoveEvent e) + { + screenSpaceMousePosition = e.ScreenSpaceMousePosition; + return base.OnMouseMove(e); + } + + protected override void Update() + { + base.Update(); + updateTracking(IsMouseInFollowArea(Tracking)); + } + + public void PostProcessHeadJudgement(DrawableSliderHead head) + { + if (!head.Judged || !head.Result.IsHit) + return; + + if (!IsMouseInFollowArea(true)) + return; + + Debug.Assert(screenSpaceMousePosition != null); + + Vector2 mousePositionInSlider = slider.ToLocalSpace(screenSpaceMousePosition.Value) - slider.OriginPosition; + + // When the head is hit late: + // - If the cursor has at all times been within range of the expanded follow area, hit all nested objects that have been passed through. + // - If the cursor has at some point left the expanded follow area, miss those nested objects instead. + + bool allTicksInRange = true; + + foreach (var nested in slider.NestedHitObjects.OfType()) + { + // Skip nested objects that are already judged. + if (nested.Judged) + continue; + + // Stop the process when a nested object is reached that can't be hit before the current time. + if (nested.HitObject.StartTime > Time.Current) + break; + + float radius = getFollowRadius(true); + double objectProgress = Math.Clamp((nested.HitObject.StartTime - slider.HitObject.StartTime) / slider.HitObject.Duration, 0, 1); + Vector2 objectPosition = slider.HitObject.CurvePositionAt(objectProgress); + + // When the first nested object that is further outside the follow area is reached, + // forcefully miss all other nested objects that would otherwise be valid to be hit. + // This covers a case of a slider overlapping itself that requires tracking to a tick on an outer edge. + if ((objectPosition - mousePositionInSlider).LengthSquared > radius * radius) + { + allTicksInRange = false; + break; + } + } + + foreach (var nested in slider.NestedHitObjects.OfType()) + { + // Skip nested objects that are already judged. + if (nested.Judged) + continue; + + // Stop the process when a nested object is reached that can't be hit before the current time. + if (nested.HitObject.StartTime > Time.Current) + break; + + if (allTicksInRange) + nested.HitForcefully(); + else + nested.MissForcefully(); + } + + // If all ticks were hit so far, enable tracking the full extent. + // If any ticks were missed, assume tracking would've broken at some point, and should only activate if the cursor is within the slider ball. + // For the second case, this may be the last chance we have to enable tracking before other objects get judged, otherwise the same would normally happen via Update(). + updateTracking(allTicksInRange || IsMouseInFollowArea(false)); + } + + public void TryJudgeNestedObject(DrawableOsuHitObject nestedObject, double timeOffset) + { + switch (nestedObject) + { + case DrawableSliderRepeat: + case DrawableSliderTick: + if (timeOffset < 0) + return; + + break; + + case DrawableSliderTail: + if (timeOffset < SliderEventGenerator.TAIL_LENIENCY) + 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 = slider.NestedHitObjects.LastOrDefault(o => o.HitObject is SliderTick || o.HitObject is SliderRepeat); + if (lastTick?.Judged == false) + return; + + break; + + default: + return; + } + + if (!slider.HeadCircle.Judged) + return; + + if (Tracking) + nestedObject.HitForcefully(); + else if (timeOffset >= 0) + nestedObject.MissForcefully(); + } + + /// + /// Whether the mouse is currently in the follow area. + /// + /// Whether to test against the maximum area of the follow circle. + public bool IsMouseInFollowArea(bool expanded) + { + if (screenSpaceMousePosition is not Vector2 pos) + return false; + + float radius = getFollowRadius(expanded); + + double followProgress = Math.Clamp((Time.Current - slider.HitObject.StartTime) / slider.HitObject.Duration, 0, 1); + Vector2 followCirclePosition = slider.HitObject.CurvePositionAt(followProgress); + Vector2 mousePositionInSlider = slider.ToLocalSpace(pos) - slider.OriginPosition; + + return (mousePositionInSlider - followCirclePosition).LengthSquared <= radius * radius; + } + + /// + /// Retrieves the radius of the follow area. + /// + /// Whether to return the maximum area of the follow circle. + private float getFollowRadius(bool expanded) + { + float radius = (float)slider.HitObject.Radius; + + if (expanded) + radius *= DrawableSliderBall.FOLLOW_AREA; + + return radius; + } + + /// + /// Updates the tracking state. + /// + /// Whether the current mouse position is valid to begin tracking. + private void updateTracking(bool isValidTrackingPosition) + { + // from the point at which the head circle is hit, this will be non-null. + // it may be null if the head circle was missed. + OsuAction? headCircleHitAction = getInitialHitAction(); + + if (headCircleHitAction == null) + timeToAcceptAnyKeyAfter = null; + + var actions = slider.OsuActionInputManager?.PressedActions; + + // if the head circle was hit with a specific key, tracking should only occur while that key is pressed. + if (headCircleHitAction != null && timeToAcceptAnyKeyAfter == null) + { + var otherKey = headCircleHitAction == OsuAction.RightButton ? OsuAction.LeftButton : OsuAction.RightButton; + + // we can start accepting any key once all other keys have been released in the previous frame. + if (!lastPressedActions.Contains(otherKey)) + timeToAcceptAnyKeyAfter = Time.Current; + } + + Tracking = + // 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. + (!slider.AllJudged || Time.Current <= slider.HitObject.GetEndTime()) + // in valid position range + && isValidTrackingPosition + // valid action + && (actions?.Any(isValidTrackingAction) ?? false); + + lastPressedActions.Clear(); + if (actions != null) + lastPressedActions.AddRange(actions); + } + + private OsuAction? getInitialHitAction() => slider.HeadCircle?.HitAction; + + /// + /// Check whether a given user input is a valid tracking action. + /// + private bool isValidTrackingAction(OsuAction action) + { + OsuAction? hitAction = getInitialHitAction(); + + // if the head circle was hit, we may not yet be allowed to accept any key, so we must use the initial hit action. + if (hitAction.HasValue && (!timeToAcceptAnyKeyAfter.HasValue || Time.Current <= timeToAcceptAnyKeyAfter.Value)) + return action == hitAction; + + return action == OsuAction.LeftButton || action == OsuAction.RightButton; + } + } +} 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/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index fd5741698a..74631400ca 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -7,6 +7,7 @@ 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; @@ -21,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). /// @@ -31,6 +37,16 @@ namespace osu.Game.Rulesets.Osu.Objects ///
public const double PREEMPT_MIN = 450; + /// + /// Median preempt time at AR=5. + /// + public const double PREEMPT_MID = 1200; + + /// + /// Maximum preempt time at AR=0. + /// + public const double PREEMPT_MAX = 1800; + public double TimePreempt = 600; public double TimeFadeIn = 400; @@ -142,7 +158,7 @@ namespace osu.Game.Rulesets.Osu.Objects { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN); + TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN); // Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR. // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above. @@ -150,7 +166,34 @@ 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); + } + + public void UpdateComboInformation(IHasComboInformation? lastObj) + { + // Note that this implementation is shared with the osu!catch ruleset's implementation. + // If a change is made here, CatchHitObject.cs should also be updated. + ComboIndex = lastObj?.ComboIndex ?? 0; + ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; + IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; + + if (this is Spinner) + { + // For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. + return; + } + + // At decode time, the first hitobject in the beatmap and the first hitobject after a spinner are both enforced to be a new combo, + // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. + if (NewCombo || lastObj == null || lastObj is Spinner) + { + IndexInCurrentCombo = 0; + ComboIndex++; + ComboIndexWithOffsets += ComboOffset + 1; + + if (lastObj != null) + lastObj.LastInCombo = 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 5ae76f1ac7..c4076f8866 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. @@ -103,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects set { repeatCount = value; - updateNestedPositions(); + endPositionCache.Invalidate(); } } @@ -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; } @@ -129,22 +124,34 @@ namespace osu.Game.Rulesets.Osu.Objects public double TickDistanceMultiplier = 1; /// - /// Whether this 's judgement is fully handled by its nested s. - /// If false, this will be judged proportionally to the number of nested s hit. + /// If , 's judgement is fully handled by its nested s. + /// If , this will be judged proportionally to the number of nested s hit. /// - public bool OnlyJudgeNestedObjects = true; - - public BindableNumber SliderVelocityBindable { get; } = new BindableDouble(1) + public bool ClassicSliderBehaviour + { + get => classicSliderBehaviour; + set + { + classicSliderBehaviour = value; + if (HeadCircle != null) + HeadCircle.ClassicSliderBehaviour = value; + if (TailCircle != null) + TailCircle.ClassicSliderBehaviour = value; + } + } + + private bool classicSliderBehaviour; + + 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; @@ -158,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Objects public Slider() { SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples(); - Path.Version.ValueChanged += _ => updateNestedPositions(); + Path.Version.ValueChanged += _ => endPositionCache.Invalidate(); } protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) @@ -167,9 +174,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 +186,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) { @@ -191,7 +200,6 @@ namespace osu.Game.Rulesets.Osu.Objects StartTime = e.Time, Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, - Scale = Scale, }); break; @@ -201,19 +209,18 @@ namespace osu.Game.Rulesets.Osu.Objects StartTime = e.Time, Position = Position, StackHeight = StackHeight, + ClassicSliderBehaviour = ClassicSliderBehaviour, }); 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, StartTime = e.Time, Position = EndPosition, - StackHeight = StackHeight + StackHeight = StackHeight, + ClassicSliderBehaviour = ClassicSliderBehaviour, }); break; @@ -224,7 +231,6 @@ namespace osu.Game.Rulesets.Osu.Objects StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration, Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, - Scale = Scale, }); break; } @@ -264,12 +270,18 @@ 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); } - public override Judgement CreateJudgement() => OnlyJudgeNestedObjects ? new OsuIgnoreJudgement() : new OsuJudgement(); + public override Judgement CreateJudgement() => ClassicSliderBehaviour + // Final combo is provided by the slider itself - see logic in `DrawableSlider.CheckForResult()` + ? new OsuJudgement() + // Final combo is provided by the tail circle - see `SliderTailCircle` + : new OsuIgnoreJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs index ddbbb300ca..2d5a5b7727 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs @@ -3,6 +3,8 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects @@ -12,16 +14,16 @@ namespace osu.Game.Rulesets.Osu.Objects /// public abstract class SliderEndCircle : HitCircle { - private readonly Slider slider; + protected readonly Slider Slider; protected SliderEndCircle(Slider slider) { - this.slider = slider; + Slider = slider; } public int RepeatIndex { get; set; } - public double SpanDuration => slider.SpanDuration; + public double SpanDuration => Slider.SpanDuration; protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { @@ -38,10 +40,17 @@ namespace osu.Game.Rulesets.Osu.Objects else { // The first end circle should fade in with the slider. - TimePreempt += StartTime - slider.StartTime; + TimePreempt += StartTime - Slider.StartTime; } } protected override HitWindows CreateHitWindows() => HitWindows.Empty; + + public override Judgement CreateJudgement() => new SliderEndJudgement(); + + public class SliderEndJudgement : OsuJudgement + { + public override HitResult MaxResult => HitResult.LargeTickHit; + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs index 73c222653e..8305481788 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs @@ -9,11 +9,11 @@ namespace osu.Game.Rulesets.Osu.Objects public class SliderHeadCircle : HitCircle { /// - /// Whether to treat this as a normal for judgement purposes. - /// If false, this will be judged as a instead. + /// If , treat this as a normal for judgement purposes. + /// If , this will be judged as a instead. /// - public bool JudgeAsNormalHitCircle = true; + public bool ClassicSliderBehaviour; - public override Judgement CreateJudgement() => JudgeAsNormalHitCircle ? base.CreateJudgement() : new SliderTickJudgement(); + public override Judgement CreateJudgement() => ClassicSliderBehaviour ? new SliderTickJudgement() : base.CreateJudgement(); } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs index cca86361c2..e95cfd369d 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.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. -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Osu.Judgements; -using osu.Game.Rulesets.Scoring; - namespace osu.Game.Rulesets.Osu.Objects { public class SliderRepeat : SliderEndCircle @@ -13,12 +9,5 @@ namespace osu.Game.Rulesets.Osu.Objects : base(slider) { } - - public override Judgement CreateJudgement() => new SliderRepeatJudgement(); - - public class SliderRepeatJudgement : OsuJudgement - { - public override HitResult MaxResult => HitResult.LargeTickHit; - } } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index b4574791d2..ceee513412 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -2,28 +2,35 @@ // See the LICENCE file in the repository root for full licence text. 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 { + /// + /// Whether to treat this as a normal for judgement purposes. + /// If false, this will be judged as a instead. + /// + public bool ClassicSliderBehaviour; + public SliderTailCircle(Slider slider) : base(slider) { } - public override Judgement CreateJudgement() => new SliderTailJudgement(); + public override Judgement CreateJudgement() => ClassicSliderBehaviour ? new LegacyTailJudgement() : new TailJudgement(); - public class SliderTailJudgement : OsuJudgement + public class LegacyTailJudgement : OsuJudgement { public override HitResult MaxResult => HitResult.SmallTickHit; } + + public class TailJudgement : SliderEndJudgement + { + public override HitResult MaxResult => HitResult.SliderTailHit; + public override HitResult MinResult => HitResult.IgnoreMiss; + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index b800b03c92..e3dfe8e69a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -18,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; @@ -31,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 . /// @@ -42,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++) { @@ -68,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/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 220dc53705..0496d1f680 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; @@ -47,6 +48,8 @@ namespace osu.Game.Rulesets.Osu public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); + public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new OsuHealthProcessor(drainStartTime); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap, this); public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new OsuBeatmapProcessor(beatmap); @@ -208,7 +211,8 @@ namespace osu.Game.Rulesets.Osu new ModAdaptiveSpeed(), new OsuModFreezeFrame(), new OsuModBubbles(), - new OsuModSynesthesia() + new OsuModSynesthesia(), + new OsuModDepth() }; case ModType.System: @@ -273,6 +277,7 @@ namespace osu.Game.Rulesets.Osu HitResult.LargeTickHit, HitResult.SmallTickHit, + HitResult.SliderTailHit, HitResult.SmallBonus, HitResult.LargeBonus, }; @@ -285,6 +290,7 @@ namespace osu.Game.Rulesets.Osu case HitResult.LargeTickHit: return "slider tick"; + case HitResult.SliderTailHit: case HitResult.SmallTickHit: return "slider end"; @@ -328,5 +334,23 @@ namespace osu.Game.Rulesets.Osu } public override RulesetSetupSection CreateEditorSetupSection() => new OsuSetupSection(); + + /// + /// + public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate) + { + BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty); + + double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN); + preempt /= rate; + adjustedDifficulty.ApproachRate = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN); + + var greatHitWindowRange = OsuHitWindows.OSU_RANGES.Single(range => range.Result == HitResult.Great); + double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max); + greatHitWindow /= rate; + adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max); + + return adjustedDifficulty; + } } } diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 5a3d882ef0..1cf6bc91f0 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -339,6 +339,11 @@ namespace osu.Game.Rulesets.Osu.Replays AddFrameToReplay(startFrame); + // 0.05 rad/ms, or ~477 RPM, as per stable. + // the redundant conversion from RPM to rad/ms is here for ease of testing custom SPM specs. + const float spin_rpm = 0.05f / (2 * MathF.PI) * 60000; + float radsPerMillisecond = MathUtils.DegreesToRadians(spin_rpm * 360) / 60000; + switch (h) { // We add intermediate frames for spinning / following a slider here. @@ -354,7 +359,7 @@ namespace osu.Game.Rulesets.Osu.Replays for (double nextFrame = h.StartTime + GetFrameDelay(h.StartTime); nextFrame < spinner.EndTime; nextFrame += GetFrameDelay(nextFrame)) { t = ApplyModsToTimeDelta(previousFrame, nextFrame) * spinnerDirection; - angle += (float)t / 20; + angle += (float)t * radsPerMillisecond; Vector2 pos = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS); AddFrameToReplay(new OsuReplayFrame((int)nextFrame, new Vector2(pos.X, pos.Y), action)); @@ -363,7 +368,7 @@ namespace osu.Game.Rulesets.Osu.Replays } t = ApplyModsToTimeDelta(previousFrame, spinner.EndTime) * spinnerDirection; - angle += (float)t / 20; + angle += (float)t * radsPerMillisecond; Vector2 endPosition = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS); diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/uneven-repeat-slider-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/uneven-repeat-slider-expected-conversion.json deleted file mode 100644 index 12d1645c04..0000000000 --- a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/uneven-repeat-slider-expected-conversion.json +++ /dev/null @@ -1,348 +0,0 @@ -{ - "Mappings": [{ - "StartTime": 369, - "Objects": [{ - "StartTime": 369, - "EndTime": 369, - "X": 127, - "Y": 194 - }, - { - "StartTime": 450, - "EndTime": 450, - "X": 166.53389, - "Y": 193.8691 - }, - { - "StartTime": 532, - "EndTime": 532, - "X": 206.555847, - "Y": 193.736572 - }, - { - "StartTime": 614, - "EndTime": 614, - "X": 246.57782, - "Y": 193.60405 - }, - { - "StartTime": 696, - "EndTime": 696, - "X": 286.5998, - "Y": 193.471527 - }, - { - "StartTime": 778, - "EndTime": 778, - "X": 326.621765, - "Y": 193.339 - }, - { - "StartTime": 860, - "EndTime": 860, - "X": 366.6437, - "Y": 193.206482 - }, - { - "StartTime": 942, - "EndTime": 942, - "X": 406.66568, - "Y": 193.073959 - }, - { - "StartTime": 970, - "EndTime": 970, - "X": 420.331726, - "Y": 193.0287 - }, - { - "StartTime": 997, - "EndTime": 997, - "X": 407.153748, - "Y": 193.072342 - }, - { - "StartTime": 1079, - "EndTime": 1079, - "X": 367.131775, - "Y": 193.204865 - }, - { - "StartTime": 1161, - "EndTime": 1161, - "X": 327.1098, - "Y": 193.337387 - }, - { - "StartTime": 1243, - "EndTime": 1243, - "X": 287.08783, - "Y": 193.46991 - }, - { - "StartTime": 1325, - "EndTime": 1325, - "X": 247.0659, - "Y": 193.602432 - }, - { - "StartTime": 1407, - "EndTime": 1407, - "X": 207.043915, - "Y": 193.734955 - }, - { - "StartTime": 1489, - "EndTime": 1489, - "X": 167.021988, - "Y": 193.867477 - }, - { - "StartTime": 1571, - "EndTime": 1571, - "X": 127, - "Y": 194 - }, - { - "StartTime": 1653, - "EndTime": 1653, - "X": 167.021988, - "Y": 193.867477 - }, - { - "StartTime": 1735, - "EndTime": 1735, - "X": 207.043976, - "Y": 193.734955 - }, - { - "StartTime": 1817, - "EndTime": 1817, - "X": 247.065887, - "Y": 193.602432 - }, - { - "StartTime": 1899, - "EndTime": 1899, - "X": 287.08783, - "Y": 193.46991 - }, - { - "StartTime": 1981, - "EndTime": 1981, - "X": 327.1098, - "Y": 193.337387 - }, - { - "StartTime": 2062, - "EndTime": 2062, - "X": 366.643738, - "Y": 193.206482 - }, - { - "StartTime": 2144, - "EndTime": 2144, - "X": 406.665649, - "Y": 193.073959 - }, - { - "StartTime": 2172, - "EndTime": 2172, - "X": 420.331726, - "Y": 193.0287 - }, - { - "StartTime": 2199, - "EndTime": 2199, - "X": 407.153748, - "Y": 193.072342 - }, - { - "StartTime": 2281, - "EndTime": 2281, - "X": 367.1318, - "Y": 193.204865 - }, - { - "StartTime": 2363, - "EndTime": 2363, - "X": 327.1098, - "Y": 193.337387 - }, - { - "StartTime": 2445, - "EndTime": 2445, - "X": 287.08783, - "Y": 193.46991 - }, - { - "StartTime": 2527, - "EndTime": 2527, - "X": 247.065887, - "Y": 193.602432 - }, - { - "StartTime": 2609, - "EndTime": 2609, - "X": 207.043976, - "Y": 193.734955 - }, - { - "StartTime": 2691, - "EndTime": 2691, - "X": 167.021988, - "Y": 193.867477 - }, - { - "StartTime": 2773, - "EndTime": 2773, - "X": 127, - "Y": 194 - }, - { - "StartTime": 2855, - "EndTime": 2855, - "X": 167.021988, - "Y": 193.867477 - }, - { - "StartTime": 2937, - "EndTime": 2937, - "X": 207.043976, - "Y": 193.734955 - }, - { - "StartTime": 3019, - "EndTime": 3019, - "X": 247.065948, - "Y": 193.602432 - }, - { - "StartTime": 3101, - "EndTime": 3101, - "X": 287.087952, - "Y": 193.46991 - }, - { - "StartTime": 3183, - "EndTime": 3183, - "X": 327.109772, - "Y": 193.337387 - }, - { - "StartTime": 3265, - "EndTime": 3265, - "X": 367.131775, - "Y": 193.204865 - }, - { - "StartTime": 3347, - "EndTime": 3347, - "X": 407.153748, - "Y": 193.072342 - }, - { - "StartTime": 3374, - "EndTime": 3374, - "X": 420.331726, - "Y": 193.0287 - }, - { - "StartTime": 3401, - "EndTime": 3401, - "X": 407.153748, - "Y": 193.072342 - }, - { - "StartTime": 3483, - "EndTime": 3483, - "X": 367.131775, - "Y": 193.204865 - }, - { - "StartTime": 3565, - "EndTime": 3565, - "X": 327.109772, - "Y": 193.337387 - }, - { - "StartTime": 3647, - "EndTime": 3647, - "X": 287.087952, - "Y": 193.46991 - }, - { - "StartTime": 3729, - "EndTime": 3729, - "X": 247.065948, - "Y": 193.602432 - }, - { - "StartTime": 3811, - "EndTime": 3811, - "X": 207.043976, - "Y": 193.734955 - }, - { - "StartTime": 3893, - "EndTime": 3893, - "X": 167.021988, - "Y": 193.867477 - }, - { - "StartTime": 3975, - "EndTime": 3975, - "X": 127, - "Y": 194 - }, - { - "StartTime": 4057, - "EndTime": 4057, - "X": 167.021988, - "Y": 193.867477 - }, - { - "StartTime": 4139, - "EndTime": 4139, - "X": 207.043976, - "Y": 193.734955 - }, - { - "StartTime": 4221, - "EndTime": 4221, - "X": 247.065948, - "Y": 193.602432 - }, - { - "StartTime": 4303, - "EndTime": 4303, - "X": 287.087952, - "Y": 193.46991 - }, - { - "StartTime": 4385, - "EndTime": 4385, - "X": 327.109772, - "Y": 193.337387 - }, - { - "StartTime": 4467, - "EndTime": 4467, - "X": 367.131775, - "Y": 193.204865 - }, - { - "StartTime": 4540, - "EndTime": 4540, - "X": 420.331726, - "Y": 193.0287 - }, - { - "StartTime": 4549, - "EndTime": 4549, - "X": 407.153748, - "Y": 193.072342 - } - ] - }] -} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs new file mode 100644 index 0000000000..fe6da9af35 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -0,0 +1,128 @@ +// 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.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Scoring +{ + public partial class OsuHealthProcessor : DrainingHealthProcessor + { + private ComboResult currentComboResult = ComboResult.Perfect; + + public OsuHealthProcessor(double drainStartTime, double drainLenience = 0) + : base(drainStartTime, drainLenience) + { + } + + protected override double GetHealthIncreaseFor(JudgementResult result) + { + if (IsSimulating) + return getHealthIncreaseFor(result); + + if (result.HitObject is not IHasComboInformation combo) + return getHealthIncreaseFor(result); + + if (combo.NewCombo) + currentComboResult = ComboResult.Perfect; + + switch (result.Type) + { + case HitResult.LargeTickMiss: + case HitResult.Ok: + setComboResult(ComboResult.Good); + break; + + case HitResult.Meh: + case HitResult.Miss: + setComboResult(ComboResult.None); + break; + } + + // The slider tail has a special judgement that can't accurately be described above. + if (result.HitObject is SliderTailCircle && !result.IsHit) + setComboResult(ComboResult.Good); + + if (combo.LastInCombo && result.Type.IsHit()) + { + switch (currentComboResult) + { + case ComboResult.Perfect: + return getHealthIncreaseFor(result) + 0.07; + + case ComboResult.Good: + return getHealthIncreaseFor(result) + 0.05; + + default: + return getHealthIncreaseFor(result) + 0.03; + } + } + + return getHealthIncreaseFor(result); + + void setComboResult(ComboResult comboResult) => currentComboResult = (ComboResult)Math.Min((int)currentComboResult, (int)comboResult); + } + + protected override void Reset(bool storeResults) + { + base.Reset(storeResults); + currentComboResult = ComboResult.Perfect; + } + + private double getHealthIncreaseFor(JudgementResult result) + { + switch (result.Type) + { + case HitResult.SmallTickMiss: + return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.02, -0.075, -0.14); + + case HitResult.LargeTickMiss: + return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.02, -0.075, -0.14); + + case HitResult.Miss: + return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.03, -0.125, -0.2); + + case HitResult.SmallTickHit: + // When classic slider mechanics are enabled, this result comes from the tail. + return 0.02; + + case HitResult.SliderTailHit: + case HitResult.LargeTickHit: + switch (result.HitObject) + { + case SliderTick: + return 0.015; + + case SliderHeadCircle: + case SliderTailCircle: + case SliderRepeat: + return 0.02; + } + + break; + + case HitResult.Meh: + return 0.002; + + case HitResult.Ok: + return 0.011; + + case HitResult.Great: + return 0.03; + + case HitResult.SmallBonus: + return 0.0085; + + case HitResult.LargeBonus: + return 0.01; + } + + return base.GetHealthIncreaseFor(result); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs index 6f55e1790f..fd86e0eeda 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Scoring ///
public const double MISS_WINDOW = 400; - private static readonly DifficultyRange[] osu_ranges = + internal static readonly DifficultyRange[] OSU_RANGES = { new DifficultyRange(HitResult.Great, 80, 50, 20), new DifficultyRange(HitResult.Ok, 140, 100, 60), @@ -34,6 +34,6 @@ namespace osu.Game.Rulesets.Osu.Scoring return false; } - protected override DifficultyRange[] GetRanges() => osu_ranges; + protected override DifficultyRange[] GetRanges() => OSU_RANGES; } } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuLegacyHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuLegacyHealthProcessor.cs new file mode 100644 index 0000000000..57d2f64e2c --- /dev/null +++ b/osu.Game.Rulesets.Osu/Scoring/OsuLegacyHealthProcessor.cs @@ -0,0 +1,90 @@ +// 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.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Scoring +{ + public partial class OsuLegacyHealthProcessor : LegacyDrainingHealthProcessor + { + public OsuLegacyHealthProcessor(double drainStartTime) + : base(drainStartTime) + { + } + + protected override IEnumerable EnumerateTopLevelHitObjects() => Beatmap.HitObjects; + + protected override IEnumerable EnumerateNestedHitObjects(HitObject hitObject) + { + switch (hitObject) + { + case Slider slider: + foreach (var nested in slider.NestedHitObjects) + yield return nested; + + break; + + case Spinner spinner: + foreach (var nested in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick)) + yield return nested; + + break; + } + } + + protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result) + { + double increase = 0; + + switch (result) + { + case HitResult.SmallTickMiss: + return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.02, -0.075, -0.14); + + case HitResult.LargeTickMiss: + return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.02, -0.075, -0.14); + + case HitResult.Miss: + return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.03, -0.125, -0.2); + + case HitResult.SmallTickHit: + // This result always comes from the slider tail, which is judged the same as a repeat. + increase = 0.02; + break; + + case HitResult.SliderTailHit: + case HitResult.LargeTickHit: + // This result comes from either a slider tick or repeat. + increase = hitObject is SliderTick ? 0.015 : 0.02; + break; + + case HitResult.Meh: + increase = 0.002; + break; + + case HitResult.Ok: + increase = 0.011; + break; + + case HitResult.Great: + increase = 0.03; + break; + + case HitResult.SmallBonus: + increase = 0.0085; + break; + + case HitResult.LargeBonus: + increase = 0.01; + break; + } + + return HpMultiplierNormal * increase; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index f97be0d7ff..4d8381cf42 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.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. -using System; +using System.Collections.Generic; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { @@ -15,14 +16,23 @@ namespace osu.Game.Rulesets.Osu.Scoring { } + public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary results) + { + ScoreRank rank = base.RankFromScore(accuracy, results); + + switch (rank) + { + case ScoreRank.S: + case ScoreRank.X: + if (results.GetValueOrDefault(HitResult.Miss) > 0) + rank = ScoreRank.A; + break; + } + + return rank; + } + protected override HitEvent CreateHitEvent(JudgementResult result) => base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit); - - protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) - { - return 700000 * comboProgress - + 300000 * Math.Pow(Accuracy.Value, 10) * accuracyProgress - + bonusPortion; - } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursor.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursor.cs index 4ca6abfdf7..15838f3e1b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursor.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursor.cs @@ -13,7 +13,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Argon { - public partial class ArgonCursor : OsuCursorSprite + public partial class ArgonCursor : SkinnableCursor { public ArgonCursor() { diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs index fca3e70236..ea21d71d5f 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs @@ -88,9 +88,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon protected override void OnSliderTick() { - this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint) - .Then() - .ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint); + if (Scale.X >= DrawableSliderBall.FOLLOW_AREA * 0.98f) + { + this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint) + .Then() + .ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint); + } } protected override void OnSliderBreak() diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs index 6f55d93eff..9a5abba4fb 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs @@ -62,28 +62,36 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon /// public virtual void PlayAnimation() { - switch (Result) + if (Result == HitResult.IgnoreMiss || Result == HitResult.LargeTickMiss) { - default: - JudgementText - .FadeInFromZero(300, Easing.OutQuint) - .ScaleTo(Vector2.One) - .ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint); - break; + this.RotateTo(-45); + this.ScaleTo(1.6f); + this.ScaleTo(1.2f, 100, Easing.In); - case HitResult.Miss: - this.ScaleTo(1.6f); - this.ScaleTo(1, 100, Easing.In); - - this.MoveTo(Vector2.Zero); - this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); - - this.RotateTo(0); - this.RotateTo(40, 800, Easing.InQuint); - break; + this.FadeOutFromOne(400); } + else if (Result.IsMiss()) + { + this.FadeOutFromOne(800); - this.FadeOutFromOne(800); + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); + + this.MoveTo(Vector2.Zero); + this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + this.RotateTo(0); + this.RotateTo(40, 800, Easing.InQuint); + } + else + { + this.FadeOutFromOne(800); + + JudgementText + .FadeInFromZero(300, Easing.OutQuint) + .ScaleTo(Vector2.One) + .ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint); + } ringExplosion?.PlayAnimation(); } diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs index 3427031dc8..7508a689d2 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon private Bindable configHitLighting = null!; - private static readonly Vector2 circle_size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + private static readonly Vector2 circle_size = OsuHitObject.OBJECT_DIMENSIONS; [Resolved] private DrawableHitObject drawableObject { get; set; } = null!; 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/Argon/ArgonSpinnerProgressArc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs index 31cdc0dc0f..76afeeb2c4 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs @@ -2,10 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Runtime.InteropServices; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Shaders.Types; +using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.UserInterface; using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; @@ -19,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon private const float arc_fill = 0.15f; private const float arc_radius = 0.12f; - private CircularProgress fill = null!; + private ProgressFill fill = null!; private DrawableSpinner spinner = null!; @@ -45,13 +52,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon InnerRadius = arc_radius, RoundedCaps = true, }, - fill = new CircularProgress + fill = new ProgressFill { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, InnerRadius = arc_radius, RoundedCaps = true, + GlowColour = new Color4(171, 255, 255, 180) } }; } @@ -67,5 +75,115 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon fill.Rotation = (float)(90 - fill.Current.Value * 180); } + + private partial class ProgressFill : CircularProgress + { + private Color4 glowColour = Color4.White; + + public Color4 GlowColour + { + get => glowColour; + set + { + glowColour = value; + Invalidate(Invalidation.DrawNode); + } + } + + private Texture glowTexture = null!; + private IShader glowShader = null!; + private float glowSize; + + [BackgroundDependencyLoader] + private void load(TextureStore textures, ShaderManager shaders) + { + glowTexture = textures.Get("Gameplay/osu/spinner-glow"); + glowShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "SpinnerGlow"); + glowSize = Blur.KernelSize(50); // Half of the maximum blur sigma in the design (which is 100) + } + + protected override DrawNode CreateDrawNode() => new ProgressFillDrawNode(this); + + private class ProgressFillDrawNode : CircularProgressDrawNode + { + protected new ProgressFill Source => (ProgressFill)base.Source; + + public ProgressFillDrawNode(CircularProgress source) + : base(source) + { + } + + private Texture glowTexture = null!; + private IShader glowShader = null!; + private Quad glowQuad; + private float relativeGlowSize; + private Color4 glowColour; + + public override void ApplyState() + { + base.ApplyState(); + + glowTexture = Source.glowTexture; + glowShader = Source.glowShader; + glowColour = Source.glowColour; + + // Inflated draw quad by the size of the blur. + glowQuad = Source.ToScreenSpace(DrawRectangle.Inflate(Source.glowSize)); + relativeGlowSize = Source.glowSize / Source.DrawSize.X; + } + + protected override void Draw(IRenderer renderer) + { + base.Draw(renderer); + drawGlow(renderer); + } + + private void drawGlow(IRenderer renderer) + { + renderer.SetBlend(BlendingParameters.Additive); + + glowShader.Bind(); + bindGlowUniformResources(glowShader, renderer); + + ColourInfo col = DrawColourInfo.Colour; + col.ApplyChild(glowColour); + + renderer.DrawQuad(glowTexture, glowQuad, col); + + glowShader.Unbind(); + } + + private IUniformBuffer? progressGlowParametersBuffer; + + private void bindGlowUniformResources(IShader shader, IRenderer renderer) + { + progressGlowParametersBuffer ??= renderer.CreateUniformBuffer(); + progressGlowParametersBuffer.Data = new ProgressGlowParameters + { + InnerRadius = InnerRadius, + Progress = Progress, + TexelSize = TexelSize, + GlowSize = relativeGlowSize + }; + + shader.BindUniformBlock("m_ProgressGlowParameters", progressGlowParametersBuffer); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + progressGlowParametersBuffer?.Dispose(); + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private record struct ProgressGlowParameters + { + public UniformFloat InnerRadius; + public UniformFloat Progress; + public UniformFloat TexelSize; + public UniformFloat GlowSize; + } + } + } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs index f98a47097d..0f9c97059c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon { case GameplaySkinComponentLookup resultComponent: // This should eventually be moved to a skin setting, when supported. - if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great) + if (Skin is ArgonProSkin && (resultComponent.Component == HitResult.Great || resultComponent.Component == HitResult.Perfect)) return Drawable.Empty(); return new ArgonJudgementPiece(resultComponent.Component); 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/DefaultApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultApproachCircle.cs index b65f46c414..272f4b5658 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultApproachCircle.cs @@ -3,47 +3,39 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; +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.Skinning; using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Default { - public partial class DefaultApproachCircle : SkinnableSprite + public partial class DefaultApproachCircle : Sprite { - private readonly IBindable accentColour = new Bindable(); - [Resolved] private DrawableHitObject drawableObject { get; set; } = null!; - public DefaultApproachCircle() - : base("Gameplay/osu/approachcircle") - { - } + private IBindable accentColour = null!; [BackgroundDependencyLoader] - private void load() + private void load(TextureStore textures) { - accentColour.BindTo(drawableObject.AccentColour); + Texture = textures.Get(@"Gameplay/osu/approachcircle").WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2); + + // account for the sprite being used for the default approach circle being taken from stable, + // when hitcircles have 5px padding on each size. this should be removed if we update the sprite. + Scale = new Vector2(128 / 118f); } protected override void LoadComplete() { base.LoadComplete(); - accentColour.BindValueChanged(colour => Colour = colour.NewValue, true); - } - protected override Drawable CreateDefault(ISkinComponentLookup lookup) - { - var drawable = base.CreateDefault(lookup); - - // Although this is a non-legacy component, osu-resources currently stores approach circle as a legacy-like texture. - // See LegacyApproachCircle for documentation as to why this is required. - drawable.Scale = new Vector2(128 / 118f); - - return drawable; + accentColour = drawableObject.AccentColour.GetBoundCopy(); + accentColour.BindValueChanged(colour => Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs index 3c41d473f4..4adbfc3928 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs @@ -59,9 +59,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default protected override void OnSliderTick() { - this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint) - .Then() - .ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint); + if (Scale.X >= DrawableSliderBall.FOLLOW_AREA * 0.98f) + { + this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint) + .Then() + .ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint); + } } protected override void OnSliderBreak() 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..fb31f88d3c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default ScaleBindable.BindValueChanged(scale => PathRadius = OsuHitObject.OBJECT_RADIUS * scale.NewValue, true); pathVersion = drawableSlider.PathVersion.GetBoundCopy(); - pathVersion.BindValueChanged(_ => Refresh()); + pathVersion.BindValueChanged(_ => Scheduler.AddOnce(Refresh)); AccentColourBindable = drawableObject.AccentColour.GetBoundCopy(); AccentColourBindable.BindValueChanged(accent => AccentColour = GetBodyAccentColour(skin, accent.NewValue), true); @@ -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 719cf57d98..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,24 +56,30 @@ 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); + + lastAngle = thisAngle; + } 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/Default/TrianglesPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs index f1143cf14d..512ac8ee3e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.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.Graphics; using osu.Game.Graphics.Backgrounds; namespace osu.Game.Rulesets.Osu.Skinning.Default @@ -15,6 +16,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { TriangleScale = 1.2f; HideAlphaDiscrepancies = false; + ClampAxes = Axes.None; } protected override void Update() diff --git a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs index 355d3f9a2f..4fadb09948 100644 --- a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.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.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -26,13 +27,17 @@ namespace osu.Game.Rulesets.Osu.Skinning ((DrawableSlider?)ParentObject)?.Tracking.BindValueChanged(tracking => { Debug.Assert(ParentObject != null); + if (ParentObject.Judged) return; - if (tracking.NewValue) - OnSliderPress(); - else - OnSliderRelease(); + using (BeginAbsoluteSequence(Math.Max(Time.Current, ParentObject.HitObject?.StartTime ?? 0))) + { + if (tracking.NewValue) + OnSliderPress(); + else + OnSliderRelease(); + } }, true); } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs index e9342bbdbb..0bdea0cab1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs @@ -1,49 +1,43 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; 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 { - public partial class LegacyApproachCircle : SkinnableSprite + public partial class LegacyApproachCircle : Sprite { - private readonly IBindable accentColour = new Bindable(); - [Resolved] private DrawableHitObject drawableObject { get; set; } = null!; - public LegacyApproachCircle() - : base("Gameplay/osu/approachcircle") - { - } + private IBindable accentColour = null!; [BackgroundDependencyLoader] - private void load() + private void load(ISkinSource skin) { - accentColour.BindTo(drawableObject.AccentColour); + var texture = skin.GetTexture(@"approachcircle"); + Debug.Assert(texture != null); + Texture = texture.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2); + + // account for the sprite being used for the default approach circle being taken from stable, + // when hitcircles have 5px padding on each size. this should be removed if we update the sprite. + Scale = new Vector2(128 / 118f); } protected override void LoadComplete() { base.LoadComplete(); + + accentColour = drawableObject.AccentColour.GetBoundCopy(); accentColour.BindValueChanged(colour => Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); } - - protected override Drawable CreateDefault(ISkinComponentLookup lookup) - { - var drawable = base.CreateDefault(lookup); - - // account for the sprite being used for the default approach circle being taken from stable, - // when hitcircles have 5px padding on each size. this should be removed if we update the sprite. - drawable.Scale = new Vector2(128 / 118f); - - return drawable; - } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs index b0c01d2925..375d81049d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs @@ -9,8 +9,11 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { - public partial class LegacyCursor : OsuCursorSprite + public partial class LegacyCursor : SkinnableCursor { + private const float pressed_scale = 1.3f; + private const float released_scale = 1f; + private readonly ISkin skin; private bool spin; @@ -51,5 +54,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy if (spin) ExpandTarget.Spin(10000, RotationDirection.Clockwise); } + + public override void Expand() + { + ExpandTarget?.ScaleTo(released_scale) + .ScaleTo(pressed_scale, 100, Easing.Out); + } + + public override void Contract() + { + ExpandTarget?.ScaleTo(released_scale, 100, Easing.Out); + } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs index f8dcb9e8a2..fa2bb9b2ad 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs @@ -44,8 +44,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected override void OnSliderTick() { - this.ScaleTo(2.2f) - .ScaleTo(2f, 200); + if (Scale.X >= 2f) + { + this.ScaleTo(2.2f) + .ScaleTo(2f, 200); + } } protected override void OnSliderBreak() diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index cadac4d319..ef616ae964 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, @@ -158,7 +160,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { decimal? legacyVersion = skin.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value; - if (legacyVersion >= 2.0m) + if (legacyVersion > 1.0m) // legacy skins of version 2.0 and newer only apply very short fade out to the number piece. hitCircleText.FadeOut(legacy_fade_duration / 4); else diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs index 67a6d5e41a..d4a0f243e4 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs @@ -135,7 +135,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected override void Update() { base.Update(); - spinningMiddle.Rotation = discTop.Rotation = DrawableSpinner.RotationTracker.Rotation; + + float turnRatio = spinningMiddle.Texture != null ? 0.5f : 1; + discTop.Rotation = DrawableSpinner.RotationTracker.Rotation * turnRatio; + spinningMiddle.Rotation = DrawableSpinner.RotationTracker.Rotation; + discBottom.Rotation = discTop.Rotation / 3; glow.Alpha = DrawableSpinner.Progress; diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs index e6166e9441..ad1fb98aef 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) { + const string lookup_name = @"reversearrow"; + + drawableRepeat = (DrawableSliderRepeat)drawableObject; + AutoSizeAxes = Axes.Both; - string lookupName = new OsuSkinComponentLookup(OsuSkinComponents.ReverseArrow).LookupName; + var skin = skinSource.FindProvider(s => s.GetTexture(lookup_name) != null); - var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null); + InternalChild = arrow = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = skin?.GetTexture(lookup_name)?.WithMaximumSize(maxSize: OsuHitObject.OBJECT_DIMENSIONS * 2), + }; - InternalChild = arrow = (skin?.GetAnimation(lookupName, true, true) ?? Empty()); 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..d2ebc68c52 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: @@ -155,7 +163,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return null; case OsuSkinComponents.ApproachCircle: - return new LegacyApproachCircle(); + if (GetTexture(@"approachcircle") != null) + return new LegacyApproachCircle(); + + return null; default: throw new UnsupportedSkinComponentException(lookup); diff --git a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs index 9d64c354e2..9838cb2c37 100644 --- a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs +++ b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs @@ -231,7 +231,7 @@ namespace osu.Game.Rulesets.Osu.Skinning points.AddRange(Source.SmokePoints.Skip(firstVisiblePointIndex).Take(futurePointIndex - firstVisiblePointIndex)); } - public sealed override void Draw(IRenderer renderer) + protected sealed override void Draw(IRenderer renderer) { base.Draw(renderer); @@ -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 6564a086fe..83bab7dc01 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -13,6 +13,7 @@ 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; @@ -208,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..4cb91aa103 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs @@ -33,6 +33,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private void load(OsuRulesetConfigManager? rulesetConfig) { rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorRipples, showRipples); + + AddInternal(ripplePool); } public bool OnPressed(KeyBindingPressEvent e) @@ -95,7 +97,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/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index a29faac5a0..95a052dadb 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -258,7 +258,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private IUniformBuffer cursorTrailParameters; - public override void Draw(IRenderer renderer) + protected override void Draw(IRenderer renderer) { base.Draw(renderer); diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs index 66c86ee09d..d8f50c1f5d 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs @@ -4,12 +4,16 @@ #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.Effects; using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Rulesets.Osu.Skinning; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -18,19 +22,78 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { public partial class OsuCursor : SkinReloadableDrawable { - private const float size = 28; + public const float SIZE = 28; private bool cursorExpand; private SkinnableDrawable cursorSprite; + private Container cursorScaleContainer = null!; - private Drawable expandTarget => (cursorSprite.Drawable as OsuCursorSprite)?.ExpandTarget ?? cursorSprite; + private SkinnableCursor skinnableCursor => (SkinnableCursor)cursorSprite.Drawable; + + public IBindable CursorScale => cursorScale; + + private readonly Bindable cursorScale = new BindableFloat(1); + + private Bindable userCursorScale = null!; + private Bindable autoCursorScale = null!; + + [Resolved(canBeNull: true)] + private GameplayState state { get; set; } + + [Resolved] + private OsuConfigManager config { get; set; } public OsuCursor() { Origin = Anchor.Centre; - Size = new Vector2(size); + Size = new Vector2(SIZE); + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = CreateCursorContent(); + + userCursorScale = config.GetBindable(OsuSetting.GameplayCursorSize); + userCursorScale.ValueChanged += _ => cursorScale.Value = CalculateCursorScale(); + + autoCursorScale = config.GetBindable(OsuSetting.AutoCursorSize); + autoCursorScale.ValueChanged += _ => cursorScale.Value = CalculateCursorScale(); + + cursorScale.BindValueChanged(e => cursorScaleContainer.Scale = new Vector2(e.NewValue), true); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + cursorScale.Value = CalculateCursorScale(); + } + + protected virtual Drawable CreateCursorContent() => cursorScaleContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Child = cursorSprite = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling) + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + }, + }; + + protected virtual float CalculateCursorScale() + { + float scale = userCursorScale.Value; + + if (autoCursorScale.Value && state != null) + { + // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier. + scale *= GetScaleForCircleSize(state.Beatmap.Difficulty.CircleSize); + } + + return scale; } protected override void SkinChanged(ISkinSource skin) @@ -38,35 +101,22 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor cursorExpand = skin.GetConfig(OsuSkinConfiguration.CursorExpand)?.Value ?? true; } - [BackgroundDependencyLoader] - private void load() - { - InternalChild = new Container - { - RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Child = cursorSprite = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling) - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - } - }; - } - - private const float pressed_scale = 1.2f; - private const float released_scale = 1f; - public void Expand() { if (!cursorExpand) return; - expandTarget.ScaleTo(released_scale).ScaleTo(pressed_scale, 400, Easing.OutElasticHalf); + skinnableCursor.Expand(); } - public void Contract() => expandTarget.ScaleTo(released_scale, 400, Easing.OutQuad); + public void Contract() => skinnableCursor.Contract(); - private partial class DefaultCursor : OsuCursorSprite + /// + /// Get the scale applicable to the ActiveCursor based on a beatmap's circle size. + /// + public static float GetScaleForCircleSize(float circleSize) => + 1f - 0.7f * (1f + circleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY; + + private partial class DefaultCursor : SkinnableCursor { public DefaultCursor() { @@ -83,7 +133,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Masking = true, - BorderThickness = size / 6, + BorderThickness = SIZE / 6, BorderColour = Color4.White, EdgeEffect = new EdgeEffectParameters { @@ -105,7 +155,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Anchor = Anchor.Centre, RelativeSizeAxes = Axes.Both, Masking = true, - BorderThickness = size / 3, + BorderThickness = SIZE / 3, BorderColour = Color4.White.Opacity(0.5f), Children = new Drawable[] { diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index bf1ff872dd..ba8a634ff7 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -11,11 +11,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; @@ -23,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { public partial class OsuCursorContainer : GameplayCursorContainer, IKeyBindingHandler { + public new OsuCursor ActiveCursor => (OsuCursor)base.ActiveCursor; + protected override Drawable CreateCursor() => new OsuCursor(); protected override Container Content => fadeContainer; @@ -33,13 +32,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private readonly Drawable cursorTrail; - public IBindable CursorScale => cursorScale; - - private readonly Bindable cursorScale = new BindableFloat(1); - - private Bindable userCursorScale; - private Bindable autoCursorScale; - private readonly CursorRippleVisualiser rippleVisualiser; public OsuCursorContainer() @@ -56,12 +48,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor }; } - [Resolved(canBeNull: true)] - private GameplayState state { get; set; } - - [Resolved] - private OsuConfigManager config { get; set; } - [BackgroundDependencyLoader(true)] private void load(OsuRulesetConfigManager rulesetConfig) { @@ -74,46 +60,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor showTrail.BindValueChanged(v => cursorTrail.FadeTo(v.NewValue ? 1 : 0, 200), true); - userCursorScale = config.GetBindable(OsuSetting.GameplayCursorSize); - userCursorScale.ValueChanged += _ => calculateScale(); - - autoCursorScale = config.GetBindable(OsuSetting.AutoCursorSize); - autoCursorScale.ValueChanged += _ => calculateScale(); - - CursorScale.BindValueChanged(e => + ActiveCursor.CursorScale.BindValueChanged(e => { var newScale = new Vector2(e.NewValue); - ActiveCursor.Scale = newScale; rippleVisualiser.CursorScale = newScale; cursorTrail.Scale = newScale; }, true); - - calculateScale(); - } - - /// - /// Get the scale applicable to the ActiveCursor based on a beatmap's circle size. - /// - public static float GetScaleForCircleSize(float circleSize) => - 1f - 0.7f * (1f + circleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY; - - private void calculateScale() - { - float scale = userCursorScale.Value; - - if (autoCursorScale.Value && state != null) - { - // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier. - scale *= GetScaleForCircleSize(state.Beatmap.Difficulty.CircleSize); - } - - cursorScale.Value = scale; - - var newScale = new Vector2(scale); - - ActiveCursor.ScaleTo(newScale, 400, Easing.OutQuint); - cursorTrail.Scale = newScale; } private int downCount; @@ -121,9 +74,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private void updateExpandedState() { if (downCount > 0) - (ActiveCursor as OsuCursor)?.Expand(); + ActiveCursor.Expand(); else - (ActiveCursor as OsuCursor)?.Contract(); + ActiveCursor.Contract(); } public bool OnPressed(KeyBindingPressEvent e) @@ -160,13 +113,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor protected override void PopIn() { fadeContainer.FadeTo(1, 300, Easing.OutQuint); - ActiveCursor.ScaleTo(CursorScale.Value, 400, Easing.OutQuint); + ActiveCursor.ScaleTo(1f, 400, Easing.OutQuint); } protected override void PopOut() { fadeContainer.FadeTo(0.05f, 450, Easing.OutQuint); - ActiveCursor.ScaleTo(CursorScale.Value * 0.8f, 450, Easing.OutQuint); + ActiveCursor.ScaleTo(0.8f, 450, Easing.OutQuint); } private partial class DefaultCursorTrail : CursorTrail diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorSprite.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorSprite.cs deleted file mode 100644 index aaf8949084..0000000000 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorSprite.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. - -#nullable disable - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; - -namespace osu.Game.Rulesets.Osu.UI.Cursor -{ - public abstract partial class OsuCursorSprite : CompositeDrawable - { - /// - /// The an optional piece of the cursor to expand when in a clicked state. - /// If null, the whole cursor will be affected by expansion. - /// - public Drawable ExpandTarget { get; protected set; } - } -} diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/SkinnableCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/SkinnableCursor.cs new file mode 100644 index 0000000000..09e6f989a4 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/Cursor/SkinnableCursor.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.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Rulesets.Osu.UI.Cursor +{ + public abstract partial class SkinnableCursor : CompositeDrawable + { + private const float pressed_scale = 1.2f; + private const float released_scale = 1f; + + public virtual void Expand() + { + ExpandTarget?.ScaleTo(released_scale) + .ScaleTo(pressed_scale, 400, Easing.OutElasticHalf); + } + + public virtual void Contract() + { + ExpandTarget?.ScaleTo(released_scale, 400, Easing.OutQuad); + } + + /// + /// The an optional piece of the cursor to expand when in a clicked state. + /// If null, the whole cursor will be affected by expansion. + /// + public Drawable? ExpandTarget { get; protected set; } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs index b509796742..44d3b37408 100644 --- a/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs @@ -3,6 +3,7 @@ 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 @@ -19,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..411a02c5af 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -4,13 +4,11 @@ #nullable disable using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Pooling; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -20,7 +18,6 @@ using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; -using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -36,6 +33,8 @@ namespace osu.Game.Rulesets.Osu.UI private readonly ProxyContainer spinnerProxies; private readonly JudgementContainer judgementLayer; + private readonly JudgementPooler judgementPooler; + public SmokeContainer Smoke { get; } public FollowPointRenderer FollowPoints { get; } @@ -43,8 +42,6 @@ namespace osu.Game.Rulesets.Osu.UI protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer(); - private readonly IDictionary> poolDictionary = new Dictionary>(); - private readonly Container judgementAboveHitObjectLayer; public OsuPlayfield() @@ -66,11 +63,15 @@ namespace osu.Game.Rulesets.Osu.UI HitPolicy = new StartTimeOrderedHitPolicy(); - var hitWindows = new OsuHitWindows(); - foreach (var result in Enum.GetValues().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r))) - poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgementLoaded)); - - AddRangeInternal(poolDictionary.Values); + AddInternal(judgementPooler = new JudgementPooler(new[] + { + HitResult.Great, + HitResult.Ok, + HitResult.Meh, + HitResult.Miss, + HitResult.LargeTickMiss, + HitResult.IgnoreMiss, + }, onJudgementLoaded)); NewResult += onNewResult; } @@ -89,7 +90,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; @@ -170,7 +171,10 @@ namespace osu.Game.Rulesets.Osu.UI if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; - DrawableOsuJudgement explosion = poolDictionary[result.Type].Get(doj => doj.Apply(result, judgedObject)); + var explosion = judgementPooler.Get(result.Type, doj => doj.Apply(result, judgedObject)); + + if (explosion == null) + return; judgementLayer.Add(explosion); @@ -186,31 +190,6 @@ namespace osu.Game.Rulesets.Osu.UI public void Add(Drawable proxy) => AddInternal(proxy); } - private partial class DrawableJudgementPool : DrawablePool - { - private readonly HitResult result; - private readonly Action onLoaded; - - public DrawableJudgementPool(HitResult result, Action onLoaded) - : base(20) - { - this.result = result; - this.onLoaded = onLoaded; - } - - protected override DrawableOsuJudgement CreateNewDrawable() - { - var judgement = base.CreateNewDrawable(); - - // just a placeholder to initialise the correct drawable hierarchy for this pool. - judgement.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null); - - onLoaded?.Invoke(judgement); - - return judgement; - } - } - private class OsuHitObjectLifetimeEntry : HitObjectLifetimeEntry { public OsuHitObjectLifetimeEntry(HitObject hitObject) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs index 2c3a77a6bc..b139296c61 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs @@ -60,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/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index 555610a3b6..8a84fe14e5 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -14,7 +13,6 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Screens.Play; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI @@ -25,7 +23,6 @@ namespace osu.Game.Rulesets.Osu.UI private OsuClickToResumeCursor clickToResumeCursor; private OsuCursorContainer localCursorContainer; - private IBindable localCursorScale; public override CursorContainer LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null; @@ -49,13 +46,7 @@ namespace osu.Game.Rulesets.Osu.UI clickToResumeCursor.Appear(); if (localCursorContainer == null) - { Add(localCursorContainer = new OsuCursorContainer()); - - localCursorScale = new BindableFloat(); - localCursorScale.BindTo(localCursorContainer.CursorScale); - localCursorScale.BindValueChanged(scale => cursorScaleContainer.Scale = new Vector2(scale.NewValue), true); - } } protected override void PopOut() @@ -74,12 +65,25 @@ namespace osu.Game.Rulesets.Osu.UI public override bool HandlePositionalInput => true; public Action ResumeRequested; + private Container scaleTransitionContainer; public OsuClickToResumeCursor() { RelativePositionAxes = Axes.Both; } + protected override Container CreateCursorContent() => scaleTransitionContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Child = base.CreateCursorContent(), + }; + + protected override float CalculateCursorScale() => + // Force minimum cursor size so it's easily clickable + Math.Max(1f, base.CalculateCursorScale()); + protected override bool OnHover(HoverEvent e) { updateColour(); @@ -98,9 +102,10 @@ namespace osu.Game.Rulesets.Osu.UI { case OsuAction.LeftButton: case OsuAction.RightButton: - if (!IsHovered) return false; + if (!IsHovered) + return false; - this.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint); + scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint); ResumeRequested?.Invoke(); return true; @@ -116,7 +121,10 @@ namespace osu.Game.Rulesets.Osu.UI public void Appear() => Schedule(() => { updateColour(); - this.ScaleTo(4).Then().ScaleTo(1, 1000, Easing.OutQuint); + + // importantly, we perform the scale transition on an underlying container rather than the whole cursor + // to prevent attempts of abuse by the scale change in the cursor's hitbox (see: https://github.com/ppy/osu/issues/26477). + scaleTransitionContainer.ScaleTo(4).Then().ScaleTo(1, 1000, Easing.OutQuint); }); private void updateColour() diff --git a/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs b/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs index 5277a1f7d6..e815d7873e 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.UI private readonly OsuInputManager osuInputManager; - private Bindable mouseDisabled = null!; + private Bindable tapsDisabled = null!; public OsuTouchInputMapper(OsuInputManager inputManager) { @@ -43,9 +43,7 @@ namespace osu.Game.Rulesets.Osu.UI [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - // The mouse button disable setting affects touch. It's a bit weird. - // This is mostly just doing the same as what is done in RulesetInputManager to match behaviour. - mouseDisabled = config.GetBindable(OsuSetting.MouseDisableButtons); + tapsDisabled = config.GetBindable(OsuSetting.TouchDisableGameplayTaps); } // Required to handle touches outside of the playfield when screen scaling is enabled. @@ -64,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.UI : OsuAction.LeftButton; // Ignore any taps which trigger an action which is already handled. But track them for potential positional input in the future. - bool shouldResultInAction = osuInputManager.AllowGameplayInputs && !mouseDisabled.Value && trackedTouches.All(t => t.Action != action); + bool shouldResultInAction = osuInputManager.AllowGameplayInputs && !tapsDisabled.Value && trackedTouches.All(t => t.Action != action); // If we can actually accept as an action, check whether this tap was on a circle's receptor. // This case gets special handling to allow for empty-space stream tapping. 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.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/Editor/TestSceneTaikoEditorSaving.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoEditorSaving.cs index af7db2251b..fb05502158 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoEditorSaving.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoEditorSaving.cs @@ -1,10 +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.Globalization; +using System.IO; +using System.Linq; using NUnit.Framework; +using osu.Framework.Extensions; using osu.Framework.Utils; -using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Beatmaps; using osu.Game.Tests.Visual; +using SharpCompress.Archives.Zip; namespace osu.Game.Rulesets.Taiko.Tests.Editor { @@ -12,6 +18,44 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor { protected override Ruleset CreateRuleset() => new TaikoRuleset(); + [TestCase(null)] + [TestCase(1f)] + [TestCase(2f)] + [TestCase(2.4f)] + public void TestTaikoSliderMultiplierInExport(float? multiplier) + { + if (multiplier.HasValue) + AddStep("Set slider multiplier", () => EditorBeatmap.Difficulty.SliderMultiplier = multiplier.Value); + + SaveEditor(); + AddStep("export beatmap", () => Game.BeatmapManager.Export(EditorBeatmap.BeatmapInfo.BeatmapSet!).WaitSafely()); + + AddAssert("check slider multiplier correct in file", () => + { + string export = LocalStorage.GetFiles("exports").First(); + + using (var stream = LocalStorage.GetStream(export)) + using (var zip = ZipArchive.Open(stream)) + { + using (var osuStream = zip.Entries.First().OpenEntryStream()) + using (var reader = new StreamReader(osuStream)) + { + string? line; + + while ((line = reader.ReadLine()) != null) + { + if (line.StartsWith("SliderMultiplier", StringComparison.Ordinal)) + { + return float.Parse(line.Split(':', StringSplitOptions.TrimEntries).Last(), provider: CultureInfo.InvariantCulture); + } + } + } + } + + return 0; + }, () => Is.EqualTo(multiplier ?? new BeatmapDifficulty().SliderMultiplier).Within(Precision.FLOAT_EPSILON)); + } + [Test] public void TestTaikoSliderMultiplier() { @@ -27,11 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor bool assertTaikoSliderMulitplier() { - // we can only assert value correctness on TaikoMultiplierAppliedDifficulty, because that is the final difficulty converted taiko beatmaps use. - // therefore, ensure that we have that difficulty type by calling .CopyFrom(), which is a no-op if the type is already correct. - var taikoDifficulty = new TaikoBeatmapConverter.TaikoMultiplierAppliedDifficulty(); - taikoDifficulty.CopyFrom(EditorBeatmap.Difficulty); - return Precision.AlmostEquals(taikoDifficulty.SliderMultiplier, 2); + return Precision.AlmostEquals(EditorBeatmap.Difficulty.SliderMultiplier, 2); } } } diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs index 4abad98eab..6e42ae7eb5 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs @@ -115,6 +115,48 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements AddAssert("all tick offsets are 0", () => JudgementResults.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0)); } + [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. /// diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs index aed08f33e0..8a1157a7f8 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs @@ -10,7 +10,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests.Mods { - public partial class TestSceneTaikoModPerfect : ModPerfectTestScene + public partial class TestSceneTaikoModPerfect : ModFailConditionTestScene { protected override Ruleset CreatePlayerRuleset() => new TestTaikoRuleset(); diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoHealthProcessorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoHealthProcessorTest.cs new file mode 100644 index 0000000000..f4a1e888c9 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoHealthProcessorTest.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 NUnit.Framework; +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; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [TestFixture] + public class TaikoHealthProcessorTest + { + [Test] + public void TestHitsOnlyGreat() + { + var beatmap = new TaikoBeatmap + { + HitObjects = + { + new Hit(), + new Hit { StartTime = 1000 }, + new Hit { StartTime = 2000 }, + new Hit { StartTime = 3000 }, + new Hit { StartTime = 4000 }, + } + }; + + var healthProcessor = new TaikoHealthProcessor(); + healthProcessor.ApplyBeatmap(beatmap); + + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Great }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], new TaikoJudgement()) { Type = HitResult.Great }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], new TaikoJudgement()) { Type = HitResult.Great }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], new TaikoJudgement()) { Type = HitResult.Great }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[4], new TaikoJudgement()) { Type = HitResult.Great }); + + Assert.Multiple(() => + { + Assert.That(healthProcessor.Health.Value, Is.EqualTo(1)); + Assert.That(healthProcessor.HasFailed, Is.False); + }); + } + + [Test] + public void TestHitsAboveThreshold() + { + var beatmap = new TaikoBeatmap + { + HitObjects = + { + new Hit(), + new Hit { StartTime = 1000 }, + new Hit { StartTime = 2000 }, + new Hit { StartTime = 3000 }, + new Hit { StartTime = 4000 }, + } + }; + + var healthProcessor = new TaikoHealthProcessor(); + healthProcessor.ApplyBeatmap(beatmap); + + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Great }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], new TaikoJudgement()) { Type = HitResult.Ok }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], new TaikoJudgement()) { Type = HitResult.Ok }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], new TaikoJudgement()) { Type = HitResult.Ok }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[4], new TaikoJudgement()) { Type = HitResult.Miss }); + + Assert.Multiple(() => + { + Assert.That(healthProcessor.Health.Value, Is.GreaterThan(0.5)); + Assert.That(healthProcessor.HasFailed, Is.False); + }); + } + + [Test] + public void TestHitsBelowThreshold() + { + var beatmap = new TaikoBeatmap + { + HitObjects = + { + new Hit(), + new Hit { StartTime = 1000 }, + new Hit { StartTime = 2000 }, + new Hit { StartTime = 3000 }, + new Hit { StartTime = 4000 }, + } + }; + + var healthProcessor = new TaikoHealthProcessor(); + healthProcessor.ApplyBeatmap(beatmap); + + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Miss }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], new TaikoJudgement()) { Type = HitResult.Ok }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], new TaikoJudgement()) { Type = HitResult.Ok }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], new TaikoJudgement()) { Type = HitResult.Ok }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[4], new TaikoJudgement()) { Type = HitResult.Miss }); + + Assert.Multiple(() => + { + Assert.That(healthProcessor.Health.Value, Is.LessThan(0.5)); + Assert.That(healthProcessor.HasFailed, Is.True); + }); + } + + [Test] + public void TestDrumRollOnly() + { + var beatmap = new TaikoBeatmap + { + HitObjects = + { + new DrumRoll { Duration = 2000 } + } + }; + + foreach (var ho in beatmap.HitObjects) + ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); + + var healthProcessor = new TaikoHealthProcessor(); + healthProcessor.ApplyBeatmap(beatmap); + + foreach (var nested in beatmap.HitObjects[0].NestedHitObjects) + { + var nestedJudgement = nested.CreateJudgement(); + healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult }); + } + + var judgement = beatmap.HitObjects[0].CreateJudgement(); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], judgement) { Type = judgement.MaxResult }); + + Assert.Multiple(() => + { + Assert.That(healthProcessor.Health.Value, Is.EqualTo(1)); + Assert.That(healthProcessor.HasFailed, Is.False); + }); + } + + [Test] + public void TestSwellOnly() + { + var beatmap = new TaikoBeatmap + { + HitObjects = + { + new DrumRoll { Duration = 2000 } + } + }; + + foreach (var ho in beatmap.HitObjects) + ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); + + var healthProcessor = new TaikoHealthProcessor(); + healthProcessor.ApplyBeatmap(beatmap); + + foreach (var nested in beatmap.HitObjects[0].NestedHitObjects) + { + var nestedJudgement = nested.CreateJudgement(); + healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult }); + } + + var judgement = beatmap.HitObjects[0].CreateJudgement(); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], judgement) { Type = judgement.MaxResult }); + + Assert.Multiple(() => + { + Assert.That(healthProcessor.Health.Value, Is.EqualTo(1)); + Assert.That(healthProcessor.HasFailed, Is.False); + }); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs index 5f7a78ddf1..c15dc17ae4 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs @@ -25,7 +25,6 @@ namespace osu.Game.Rulesets.Taiko.Tests new object[] { LegacyMods.HalfTime, new[] { typeof(TaikoModHalfTime) } }, 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.ScoreV2, new[] { typeof(ModScoreV2) } }, }; diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoRateAdjustedDisplayDifficultyTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoRateAdjustedDisplayDifficultyTest.cs new file mode 100644 index 0000000000..4ab3f502ad --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoRateAdjustedDisplayDifficultyTest.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 NUnit.Framework; +using osu.Game.Beatmaps; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [TestFixture] + public class TaikoRateAdjustedDisplayDifficultyTest + { + private static IEnumerable difficultyValuesToTest() + { + for (float i = 0; i <= 10; i += 0.5f) + yield return i; + } + + [TestCaseSource(nameof(difficultyValuesToTest))] + public void TestOverallDifficultyIsUnchangedWithRateEqualToOne(float originalOverallDifficulty) + { + var ruleset = new TaikoRuleset(); + var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty }; + + var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1); + + Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty)); + } + + [Test] + public void TestRateBelowOne() + { + var ruleset = new TaikoRuleset(); + var difficulty = new BeatmapDifficulty(); + + var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75); + + Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(1.11).Within(0.01)); + } + + [Test] + public void TestRateAboveOne() + { + var ruleset = new TaikoRuleset(); + var difficulty = new BeatmapDifficulty(); + + var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5); + + Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(8.89).Within(0.01)); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoScoreProcessorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoScoreProcessorTest.cs new file mode 100644 index 0000000000..d74fe99a9f --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoScoreProcessorTest.cs @@ -0,0 +1,41 @@ +// 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.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Judgements; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Scoring; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [TestFixture] + public class TaikoScoreProcessorTest + { + [Test] + public void TestInaccurateHitScore() + { + var beatmap = new Beatmap + { + HitObjects = + { + new Hit(), + new Hit { StartTime = 1000 } + } + }; + + var scoreProcessor = new TaikoScoreProcessor(); + scoreProcessor.ApplyBeatmap(beatmap); + + // Apply a miss judgement + scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Great }); + scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], new TaikoJudgement()) { Type = HitResult.Ok }); + + Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(453745)); + Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(0.75).Within(0.0001)); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs index ced2e4b98c..6c925f566b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Tests [SetUp] public void SetUp() => Schedule(() => { - gameplayClock = new GameplayClockContainer(manualClock) + gameplayClock = new GameplayClockContainer(manualClock, false, false) { RelativeSizeAxes = Axes.Both, Children = new Drawable[] diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneScoring.cs new file mode 100644 index 0000000000..cf8e3767da --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneScoring.cs @@ -0,0 +1,216 @@ +// 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.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Scoring.Legacy; +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(IReadOnlyList selectedMods) + => new ScoreV1(selectedMods) + { + ScoreMultiplier = { BindTarget = scoreMultiplier } + }; + + protected override IScoringAlgorithm CreateScoreV2(int maxCombo, IReadOnlyList selectedMods) + => new ScoreV2(maxCombo, selectedMods); + + protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode, IReadOnlyList selectedMods) + => new TaikoProcessorBasedScoringAlgorithm(beatmap, mode, selectedMods); + + [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 readonly double modMultiplier; + + private int currentCombo; + + public ScoreV1(IReadOnlyList selectedMods) + { + var ruleset = new TaikoRuleset(); + modMultiplier = ruleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(selectedMods, new LegacyBeatmapConversionDifficultyInfo + { + SourceRuleset = ruleset.RulesetInfo + }); + } + + 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) * modMultiplier) * (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 modMultiplier; + private readonly double comboPortionMax; + private readonly int maxCombo; + + private const double combo_base = 4; + + public ScoreV2(int maxCombo, IReadOnlyList selectedMods) + { + this.maxCombo = maxCombo; + + var ruleset = new TaikoRuleset(); + modMultiplier = ruleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier( + selectedMods.Append(new ModScoreV2()).ToArray(), + new LegacyBeatmapConversionDifficultyInfo + { + SourceRuleset = ruleset.RulesetInfo + }); + + 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) + ) * modMultiplier); + } + } + } + + private class TaikoProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm + { + public TaikoProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode, IReadOnlyList selectedMods) + : base(beatmap, mode, selectedMods) + { + } + + 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/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index 0c39ad988b..4eb6b0ab3d 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/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index dddd1e3c5a..010b1f0a7a 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -10,15 +10,25 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Utils; using System.Threading; -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 { internal class TaikoBeatmapConverter : BeatmapConverter { + /// + /// A speed multiplier applied globally to osu!taiko. + /// + /// + /// osu! is generally slower than taiko, so a factor was historically added to increase speed for converts. + /// This must be used everywhere slider length or beat length is used in taiko. + /// + /// Of note, this has never been exposed to the end user, and is considered a hidden internal multiplier. + /// + public const float VELOCITY_MULTIPLIER = 1.4f; + /// /// Because swells are easier in taiko than spinners are in osu!, /// legacy taiko multiplies a factor when converting the number of required hits. @@ -30,11 +40,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps /// private const float osu_base_scoring_distance = 100; - /// - /// Drum roll distance that results in a duration of 1 speed-adjusted beat length. - /// - private const float taiko_base_distance = 100; - private readonly bool isForCurrentRuleset; public TaikoBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) @@ -47,12 +52,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) { - if (!(original.Difficulty is TaikoMultiplierAppliedDifficulty)) - { - // Rewrite the beatmap info to add the slider velocity multiplier - original.Difficulty = new TaikoMultiplierAppliedDifficulty(original.Difficulty); - } - Beatmap converted = base.ConvertBeatmap(original, cancellationToken); if (original.BeatmapInfo.Ruleset.OnlineID == 0) @@ -64,7 +63,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)) @@ -108,9 +107,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps switch (obj) { - case IHasDistance distanceData: + case IHasPath pathData: { - if (shouldConvertSliderToHits(obj, beatmap, distanceData, out int taikoDuration, out double tickSpacing)) + if (shouldConvertSliderToHits(obj, beatmap, pathData, out int taikoDuration, out double tickSpacing)) { IList> allSamples = obj is IHasPathWithRepeats curveData ? curveData.NodeSamples : new List>(new[] { samples }); @@ -139,7 +138,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps StartTime = obj.StartTime, Samples = obj.Samples, Duration = taikoDuration, - SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1 }; } @@ -174,7 +172,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps } } - private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDistance distanceData, out int taikoDuration, out double tickSpacing) + private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasPath pathData, out int taikoDuration, out double tickSpacing) { // DO NOT CHANGE OR REFACTOR ANYTHING IN HERE WITHOUT TESTING AGAINST _ALL_ BEATMAPS. // Some of these calculations look redundant, but they are not - extremely small floating point errors are introduced to maintain 1:1 compatibility with stable. @@ -182,19 +180,22 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps // The true distance, accounting for any repeats. This ends up being the drum roll distance later int spans = (obj as IHasRepeats)?.SpanCount() ?? 1; - double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; + double distance = pathData.Path.ExpectedDistance.Value ?? 0; + + // Do not combine the following two lines! + distance *= VELOCITY_MULTIPLIER; + distance *= spans; 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; - double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate; + double sliderScoringPointDistance = osu_base_scoring_distance * (beatmap.Difficulty.SliderMultiplier * VELOCITY_MULTIPLIER) / beatmap.Difficulty.SliderTickRate; // The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll. double taikoVelocity = sliderScoringPointDistance * beatmap.Difficulty.SliderTickRate; @@ -220,41 +221,5 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps } protected override Beatmap CreateBeatmap() => new TaikoBeatmap(); - - // Important to note that this is subclassing a realm object. - // Realm doesn't allow this, but for now this can work since we aren't (in theory?) persisting this to the database. - // It is only used during beatmap conversion and processing. - internal class TaikoMultiplierAppliedDifficulty : BeatmapDifficulty - { - public TaikoMultiplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty) - { - CopyFrom(difficulty); - } - - [UsedImplicitly] - public TaikoMultiplierAppliedDifficulty() - { - } - - #region Overrides of BeatmapDifficulty - - public override BeatmapDifficulty Clone() => new TaikoMultiplierAppliedDifficulty(this); - - public override void CopyTo(BeatmapDifficulty other) - { - base.CopyTo(other); - if (!(other is TaikoMultiplierAppliedDifficulty)) - other.SliderMultiplier /= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; - } - - public override void CopyFrom(IBeatmapDifficultyInfo other) - { - base.CopyFrom(other); - if (!(other is TaikoMultiplierAppliedDifficulty)) - SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; - } - - #endregion - } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 25adba5ab6..ab193caaa3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -25,12 +25,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public override int Version => 20220902; - private readonly IWorkingBeatmap workingBeatmap; - public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { - workingBeatmap = beatmap; } protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) @@ -99,15 +96,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty MaxCombo = beatmap.HitObjects.Count(h => h is Hit), }; - if (ComputeLegacyScoringValues) - { - TaikoLegacyScoreSimulator sv1Simulator = new TaikoLegacyScoreSimulator(); - sv1Simulator.Simulate(workingBeatmap, beatmap, mods); - attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore; - attributes.LegacyComboScore = sv1Simulator.ComboScore; - attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio; - } - return attributes; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs index e77327d622..66ff0fc3d9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs @@ -5,36 +5,32 @@ 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.Legacy; 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; +using osu.Game.Rulesets.Taiko.Scoring; namespace osu.Game.Rulesets.Taiko.Difficulty { internal class TaikoLegacyScoreSimulator : ILegacyScoreSimulator { - public int AccuracyScore { get; private set; } - - public int ComboScore { get; private set; } - - public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore; + private readonly ScoreProcessor scoreProcessor = new TaikoScoreProcessor(); private int legacyBonusScore; - private int modernBonusScore; + private int standardisedBonusScore; private int combo; - private double modMultiplier; private int difficultyPeppyStars; private IBeatmap playableBeatmap = null!; - private IReadOnlyList mods = null!; - public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods) + public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap) { this.playableBeatmap = playableBeatmap; - this.mods = mods; IBeatmap baseBeatmap = workingBeatmap.Beatmap; @@ -70,19 +66,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty 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); + difficultyPeppyStars = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength); - modMultiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + LegacyScoreAttributes attributes = new LegacyScoreAttributes(); foreach (var obj in playableBeatmap.HitObjects) - simulateHit(obj); + simulateHit(obj, ref attributes); + + attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore; + attributes.BonusScore = legacyBonusScore; + attributes.MaxCombo = combo; + + return attributes; } - private void simulateHit(HitObject hitObject) + private void simulateHit(HitObject hitObject, ref LegacyScoreAttributes attributes) { bool increaseCombo = true; bool addScoreComboMultiplier = false; @@ -109,21 +107,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty 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... - double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(playableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5); - double secondsDuration = swell.Duration / 1000; + + // 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)(secondsDuration * minimumRotationsPerSecond); - + int halfSpinsRequiredForCompletion = (int)(swell.Duration / 1000 * minimum_rotations_per_second); halfSpinsRequiredForCompletion = (int)Math.Max(1, halfSpinsRequiredForCompletion * 1.65f); - if (mods.Any(m => m is ModDoubleTime)) - halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 0.75f)); - if (mods.Any(m => m is ModHalfTime)) - halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 1.5f)); + // + // 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()); + simulateHit(new SwellTick(), ref attributes); scoreIncrease = 300; addScoreComboMultiplier = true; @@ -139,7 +140,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty case DrumRoll: foreach (var nested in hitObject.NestedHitObjects) - simulateHit(nested); + simulateHit(nested, ref attributes); return; } @@ -159,8 +160,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { int oldScoreIncrease = scoreIncrease; - // ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...) - scoreIncrease += (int)(scoreIncrease / 35 * 2 * (difficultyPeppyStars + 1) * modMultiplier) * (Math.Min(100, combo) / 10); + scoreIncrease += scoreIncrease / 35 * 2 * (difficultyPeppyStars + 1) * (Math.Min(100, combo) / 10); if (hitObject is Swell) { @@ -185,18 +185,60 @@ namespace osu.Game.Rulesets.Taiko.Difficulty scoreIncrease -= comboScoreIncrease; if (addScoreComboMultiplier) - ComboScore += comboScoreIncrease; + attributes.ComboScore += comboScoreIncrease; if (isBonus) { legacyBonusScore += scoreIncrease; - modernBonusScore += Judgement.ToNumericResult(bonusResult); + standardisedBonusScore += scoreProcessor.GetBaseScoreForResult(bonusResult); } else - AccuracyScore += scoreIncrease; + 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/Edit/Blueprints/TaikoSelectionBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs index 93b7c7061b..2a824d1f68 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs @@ -30,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/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs index bc4129c982..b0919417a4 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs @@ -6,6 +6,7 @@ using System; using osu.Framework.Graphics; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -25,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints private readonly IHasDuration spanPlacementObject; - protected override bool IsValidForPlacement => spanPlacementObject.Duration > 0; + protected override bool IsValidForPlacement => Precision.DefinitelyBigger(spanPlacementObject.Duration, 0); public TaikoSpanPlacementBlueprint(HitObject hitObject) : base(hitObject) 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/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/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs index 3e63d624e7..6020f6e04c 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs @@ -2,14 +2,17 @@ // 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; @@ -25,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/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 3fa6f4b756..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; @@ -285,7 +292,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables 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/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 083b8cc547..f3143de345 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -1,20 +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.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; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Beatmaps; 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 * TaikoBeatmapConverter.VELOCITY_MULTIPLIER) * effectPoint.ScrollSpeed; Velocity = scoringDistance / timingPoint.BeatLength; TickRate = difficulty.SliderTickRate == 3 ? 3 : 4; @@ -129,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Objects double IHasDistance.Distance => Duration * Velocity; SliderPath IHasPath.Path - => new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER); + => new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / TaikoBeatmapConverter.VELOCITY_MULTIPLIER); #endregion } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index d75906f3aa..dfca4ac8c7 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -31,11 +31,39 @@ namespace osu.Game.Rulesets.Taiko.Scoring ///
private double hpMissMultiplier; + /// + /// Sum of all achievable health increases throughout the map. + /// Used to determine if there are any objects that give health. + /// If there are none, health will be forcibly pulled up to 1 to avoid cases of impassable maps. + /// + private double sumOfMaxHealthIncreases; + public TaikoHealthProcessor() : base(0.5) { } + protected override void ApplyResultInternal(JudgementResult result) + { + base.ApplyResultInternal(result); + sumOfMaxHealthIncreases += result.Judgement.MaxHealthIncrease; + } + + protected override void RevertResultInternal(JudgementResult result) + { + base.RevertResultInternal(result); + sumOfMaxHealthIncreases -= result.Judgement.MaxHealthIncrease; + } + + protected override void Reset(bool storeResults) + { + base.Reset(storeResults); + + if (storeResults && sumOfMaxHealthIncreases == 0) + Health.Value = 1; + sumOfMaxHealthIncreases = 0; + } + public override void ApplyBeatmap(IBeatmap beatmap) { base.ApplyBeatmap(beatmap); diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs index cf806c0c97..b44ef8ee93 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs @@ -7,7 +7,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring { public class TaikoHitWindows : HitWindows { - private static readonly DifficultyRange[] taiko_ranges = + internal static readonly DifficultyRange[] TAIKO_RANGES = { new DifficultyRange(HitResult.Great, 50, 35, 20), new DifficultyRange(HitResult.Ok, 120, 80, 50), @@ -27,6 +27,6 @@ namespace osu.Game.Rulesets.Taiko.Scoring return false; } - protected override DifficultyRange[] GetRanges() => taiko_ranges; + protected override DifficultyRange[] GetRanges() => TAIKO_RANGES; } } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index a77e6db6f3..7e40d575bc 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Taiko.Scoring { @@ -28,11 +30,38 @@ namespace osu.Game.Rulesets.Taiko.Scoring protected override double GetComboScoreChange(JudgementResult result) { - return Judgement.ToNumericResult(result.Type) + return GetBaseScoreForResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base)) * strongScaleValue(result); } + public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary results) + { + ScoreRank rank = base.RankFromScore(accuracy, results); + + switch (rank) + { + case ScoreRank.S: + case ScoreRank.X: + if (results.GetValueOrDefault(HitResult.Miss) > 0) + rank = ScoreRank.A; + break; + } + + return rank; + } + + public override int GetBaseScoreForResult(HitResult result) + { + switch (result) + { + case HitResult.Ok: + return 150; + } + + return base.GetBaseScoreForResult(result); + } + private double strongScaleValue(JudgementResult result) { if (result.HitObject is StrongNestedHitObject strong) 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/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/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 5516e025cd..dbc8718f02 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs @@ -22,6 +22,9 @@ 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; @@ -45,40 +48,45 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy [BackgroundDependencyLoader] 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"))); + AddInternal(backgroundLayer = new LegacyKiaiFlashingDrawable(() => getDrawableFor("circle", false)) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + + foregroundLayer = getDrawableFor("circleoverlay", true); - foregroundLayer = getDrawableFor("circleoverlay"); 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); - // 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) - { - (c as IFramedAnimation)?.Stop(); - - c.Anchor = Anchor.Centre; - c.Origin = Anchor.Centre; - } - if (gameplayState != null) currentCombo.BindTo(gameplayState.ScoreProcessor.Combo); } @@ -96,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; @@ -116,12 +124,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } else { - animatableForegroundLayer.GotoFrame(0); + animation.GotoFrame(0); return; } animationFrame = Math.Abs(Time.Current - timingPoint.Time) % ((timingPoint.BeatLength * 2) / multiplier) >= timingPoint.BeatLength / multiplier ? 0 : 1; - animatableForegroundLayer.GotoFrame(animationFrame); + animation.GotoFrame(animationFrame); } private Color4 accentColour; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs index 8ad419f8bd..28415bb72a 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.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.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -153,16 +152,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy if (target != null) { - const float alpha_amount = 1; - const float down_time = 80; const float up_time = 50; - target.Animate( - t => t.FadeTo(Math.Min(target.Alpha + alpha_amount, 1), down_time) - ).Then( - t => t.FadeOut(up_time) - ); + target + .FadeTo(1, down_time * (1 - target.Alpha), Easing.Out) + .Delay(100).FadeOut(up_time); } return false; diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 584a91bfdc..24b0ec5d57 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 @@ -68,9 +69,9 @@ namespace osu.Game.Rulesets.Taiko public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] { - new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre), new KeyBinding(InputKey.MouseRight, TaikoAction.LeftRim), new KeyBinding(InputKey.D, TaikoAction.LeftRim), + new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre), new KeyBinding(InputKey.F, TaikoAction.LeftCentre), new KeyBinding(InputKey.J, TaikoAction.RightCentre), new KeyBinding(InputKey.K, TaikoAction.RightRim), @@ -114,23 +115,10 @@ namespace osu.Game.Rulesets.Taiko if (mods.HasFlagFast(LegacyMods.Relax)) yield return new TaikoModRelax(); - if (mods.HasFlagFast(LegacyMods.Random)) - yield return new TaikoModRandom(); - if (mods.HasFlagFast(LegacyMods.ScoreV2)) yield return new ModScoreV2(); } - public override LegacyMods ConvertToLegacyMods(Mod[] mods) - { - var value = base.ConvertToLegacyMods(mods); - - if (mods.OfType().Any()) - value |= LegacyMods.Random; - - return value; - } - public override IEnumerable GetModsFor(ModType type) { switch (type) @@ -263,5 +251,18 @@ namespace osu.Game.Rulesets.Taiko }), true) }; } + + /// + public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate) + { + BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty); + + var greatHitWindowRange = TaikoHitWindows.TAIKO_RANGES.Single(range => range.Result == HitResult.Great); + double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max); + greatHitWindow /= rate; + adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max); + + return adjustedDifficulty; + } } } diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index 64d406a308..49b0ad811d 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -16,6 +16,7 @@ using osu.Game.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Replays; using osu.Game.Rulesets.Timing; @@ -35,8 +36,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 +44,7 @@ namespace osu.Game.Rulesets.Taiko.UI : base(ruleset, beatmap, mods) { Direction.Value = ScrollingDirection.Left; + VisualisationMethod = ScrollVisualisationMethod.Overlapping; } [BackgroundDependencyLoader] @@ -65,6 +65,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 +78,11 @@ 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; + // Stable internally increased the slider velocity of objects by a factor of `VELOCITY_MULTIPLIER`. + // To simulate this, we shrink the time range by that factor here. + // This, when combined with the rest of the scrolling ruleset machinery (see `MultiplierControlPoint` et al.), + // has the effect of increasing each multiplier control point's multiplier by `VELOCITY_MULTIPLIER`, ensuring parity with stable. + return (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate / TaikoBeatmapConverter.VELOCITY_MULTIPLIER; } protected override void UpdateAfterChildren() diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 23ffac1f63..7e3ed7a4d4 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -1,8 +1,6 @@ // Copyright (c) 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; @@ -10,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Primitives; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; @@ -40,29 +37,31 @@ namespace osu.Game.Rulesets.Taiko.UI ///
public Bindable ClassicHitTargetPosition = new BindableBool(); - private Container hitExplosionContainer; - private Container kiaiExplosionContainer; - private JudgementContainer judgementContainer; - private ScrollingHitObjectContainer drumRollHitContainer; - internal Drawable HitTarget; - private SkinnableDrawable mascot; + public Container UnderlayElements { get; private set; } = null!; - private readonly IDictionary> judgementPools = new Dictionary>(); + private Container hitExplosionContainer = null!; + private Container kiaiExplosionContainer = null!; + private JudgementContainer judgementContainer = null!; + private ScrollingHitObjectContainer drumRollHitContainer = null!; + internal Drawable HitTarget = null!; + private SkinnableDrawable mascot = null!; + + private JudgementPooler judgementPooler = null!; private readonly IDictionary explosionPools = new Dictionary(); - private ProxyContainer topLevelHitContainer; - private InputDrum inputDrum; - private Container rightArea; + private ProxyContainer topLevelHitContainer = null!; + private InputDrum inputDrum = null!; + private Container rightArea = null!; /// /// is purposefully not called on this to prevent i.e. being able to interact /// with bar lines in the editor. /// - private BarLinePlayfield barLinePlayfield; + private BarLinePlayfield barLinePlayfield = null!; - private Container barLineContent; - private Container hitObjectContent; - private Container overlayContent; + private Container barLineContent = null!; + private Container hitObjectContent = null!; + private Container overlayContent = null!; [BackgroundDependencyLoader] private void load(OsuColour colours) @@ -130,7 +129,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 { @@ -193,13 +199,12 @@ namespace osu.Game.Rulesets.Taiko.UI var hitWindows = new TaikoHitWindows(); - foreach (var result in Enum.GetValues().Where(r => hitWindows.IsHitResultAllowed(r))) - { - judgementPools.Add(result, new DrawablePool(15)); - explosionPools.Add(result, new HitExplosionPool(result)); - } + HitResult[] usableHitResults = Enum.GetValues().Where(r => hitWindows.IsHitResultAllowed(r)).ToArray(); - AddRangeInternal(judgementPools.Values); + AddInternal(judgementPooler = new JudgementPooler(usableHitResults)); + + foreach (var result in usableHitResults) + explosionPools.Add(result, new HitExplosionPool(result)); AddRangeInternal(explosionPools.Values); } @@ -330,7 +335,12 @@ namespace osu.Game.Rulesets.Taiko.UI if (!result.Type.IsScorable()) break; - judgementContainer.Add(judgementPools[result.Type].Get(j => j.Apply(result, judgedObject))); + var judgement = judgementPooler.Get(result.Type, j => j.Apply(result, judgedObject)); + + if (judgement == null) + return; + + judgementContainer.Add(judgement); var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre; addExplosion(judgedObject, result.Type, type); 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/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 970b6aaf60..02432a1935 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -12,14 +12,17 @@ using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Timing; using osu.Game.IO; +using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch.Beatmaps; +using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Taiko; using osu.Game.Skinning; using osu.Game.Tests.Resources; using osuTK; @@ -33,7 +36,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 +48,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() { @@ -414,12 +436,12 @@ namespace osu.Game.Tests.Beatmaps.Formats new OsuBeatmapProcessor(converted).PreProcess(); new OsuBeatmapProcessor(converted).PostProcess(); - Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndexWithOffsets); - Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndexWithOffsets); - Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndexWithOffsets); - Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndexWithOffsets); - Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndexWithOffsets); - Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndexWithOffsets); + Assert.AreEqual(1, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndexWithOffsets); + Assert.AreEqual(2, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndexWithOffsets); + Assert.AreEqual(3, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndexWithOffsets); + Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndexWithOffsets); + Assert.AreEqual(8, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndexWithOffsets); + Assert.AreEqual(9, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndexWithOffsets); } } @@ -437,12 +459,12 @@ namespace osu.Game.Tests.Beatmaps.Formats new CatchBeatmapProcessor(converted).PreProcess(); new CatchBeatmapProcessor(converted).PostProcess(); - Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndexWithOffsets); - Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndexWithOffsets); - Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndexWithOffsets); - Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndexWithOffsets); - Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndexWithOffsets); - Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndexWithOffsets); + Assert.AreEqual(1, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndexWithOffsets); + Assert.AreEqual(2, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndexWithOffsets); + Assert.AreEqual(3, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndexWithOffsets); + Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndexWithOffsets); + Assert.AreEqual(8, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndexWithOffsets); + Assert.AreEqual(9, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndexWithOffsets); } } @@ -621,6 +643,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); + } + + static 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() { @@ -757,14 +811,14 @@ namespace osu.Game.Tests.Beatmaps.Formats var first = ((IHasPath)decoded.HitObjects[0]).Path; Assert.That(first.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); - Assert.That(first.ControlPoints[0].Type, Is.EqualTo(PathType.PerfectCurve)); + Assert.That(first.ControlPoints[0].Type, Is.EqualTo(PathType.PERFECT_CURVE)); Assert.That(first.ControlPoints[1].Position, Is.EqualTo(new Vector2(161, -244))); Assert.That(first.ControlPoints[1].Type, Is.EqualTo(null)); // ReSharper disable once HeuristicUnreachableCode // weird one, see https://youtrack.jetbrains.com/issue/RIDER-70159. Assert.That(first.ControlPoints[2].Position, Is.EqualTo(new Vector2(376, -3))); - Assert.That(first.ControlPoints[2].Type, Is.EqualTo(PathType.Bezier)); + Assert.That(first.ControlPoints[2].Type, Is.EqualTo(PathType.BEZIER)); Assert.That(first.ControlPoints[3].Position, Is.EqualTo(new Vector2(68, 15))); Assert.That(first.ControlPoints[3].Type, Is.EqualTo(null)); Assert.That(first.ControlPoints[4].Position, Is.EqualTo(new Vector2(259, -132))); @@ -776,7 +830,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var second = ((IHasPath)decoded.HitObjects[1]).Path; Assert.That(second.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); - Assert.That(second.ControlPoints[0].Type, Is.EqualTo(PathType.PerfectCurve)); + Assert.That(second.ControlPoints[0].Type, Is.EqualTo(PathType.PERFECT_CURVE)); Assert.That(second.ControlPoints[1].Position, Is.EqualTo(new Vector2(161, -244))); Assert.That(second.ControlPoints[1].Type, Is.EqualTo(null)); Assert.That(second.ControlPoints[2].Position, Is.EqualTo(new Vector2(376, -3))); @@ -786,14 +840,14 @@ namespace osu.Game.Tests.Beatmaps.Formats var third = ((IHasPath)decoded.HitObjects[2]).Path; Assert.That(third.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); - Assert.That(third.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier)); + Assert.That(third.ControlPoints[0].Type, Is.EqualTo(PathType.BEZIER)); Assert.That(third.ControlPoints[1].Position, Is.EqualTo(new Vector2(0, 192))); Assert.That(third.ControlPoints[1].Type, Is.EqualTo(null)); Assert.That(third.ControlPoints[2].Position, Is.EqualTo(new Vector2(224, 192))); Assert.That(third.ControlPoints[2].Type, Is.EqualTo(null)); Assert.That(third.ControlPoints[3].Position, Is.EqualTo(new Vector2(224, 0))); - Assert.That(third.ControlPoints[3].Type, Is.EqualTo(PathType.Bezier)); + Assert.That(third.ControlPoints[3].Type, Is.EqualTo(PathType.BEZIER)); Assert.That(third.ControlPoints[4].Position, Is.EqualTo(new Vector2(224, -192))); Assert.That(third.ControlPoints[4].Type, Is.EqualTo(null)); Assert.That(third.ControlPoints[5].Position, Is.EqualTo(new Vector2(480, -192))); @@ -805,7 +859,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var fourth = ((IHasPath)decoded.HitObjects[3]).Path; Assert.That(fourth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); - Assert.That(fourth.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier)); + Assert.That(fourth.ControlPoints[0].Type, Is.EqualTo(PathType.BEZIER)); Assert.That(fourth.ControlPoints[1].Position, Is.EqualTo(new Vector2(1, 1))); Assert.That(fourth.ControlPoints[1].Type, Is.EqualTo(null)); Assert.That(fourth.ControlPoints[2].Position, Is.EqualTo(new Vector2(2, 2))); @@ -819,7 +873,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var fifth = ((IHasPath)decoded.HitObjects[4]).Path; Assert.That(fifth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); - Assert.That(fifth.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier)); + Assert.That(fifth.ControlPoints[0].Type, Is.EqualTo(PathType.BEZIER)); Assert.That(fifth.ControlPoints[1].Position, Is.EqualTo(new Vector2(1, 1))); Assert.That(fifth.ControlPoints[1].Type, Is.EqualTo(null)); Assert.That(fifth.ControlPoints[2].Position, Is.EqualTo(new Vector2(2, 2))); @@ -830,7 +884,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(fifth.ControlPoints[4].Type, Is.EqualTo(null)); Assert.That(fifth.ControlPoints[5].Position, Is.EqualTo(new Vector2(4, 4))); - Assert.That(fifth.ControlPoints[5].Type, Is.EqualTo(PathType.Bezier)); + Assert.That(fifth.ControlPoints[5].Type, Is.EqualTo(PathType.BEZIER)); Assert.That(fifth.ControlPoints[6].Position, Is.EqualTo(new Vector2(5, 5))); Assert.That(fifth.ControlPoints[6].Type, Is.EqualTo(null)); @@ -838,12 +892,12 @@ namespace osu.Game.Tests.Beatmaps.Formats var sixth = ((IHasPath)decoded.HitObjects[5]).Path; Assert.That(sixth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); - Assert.That(sixth.ControlPoints[0].Type == PathType.Bezier); + Assert.That(sixth.ControlPoints[0].Type == PathType.BEZIER); Assert.That(sixth.ControlPoints[1].Position, Is.EqualTo(new Vector2(75, 145))); Assert.That(sixth.ControlPoints[1].Type == null); Assert.That(sixth.ControlPoints[2].Position, Is.EqualTo(new Vector2(170, 75))); - Assert.That(sixth.ControlPoints[2].Type == PathType.Bezier); + Assert.That(sixth.ControlPoints[2].Type == PathType.BEZIER); Assert.That(sixth.ControlPoints[3].Position, Is.EqualTo(new Vector2(300, 145))); Assert.That(sixth.ControlPoints[3].Type == null); Assert.That(sixth.ControlPoints[4].Position, Is.EqualTo(new Vector2(410, 20))); @@ -853,12 +907,12 @@ namespace osu.Game.Tests.Beatmaps.Formats var seventh = ((IHasPath)decoded.HitObjects[6]).Path; Assert.That(seventh.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); - Assert.That(seventh.ControlPoints[0].Type == PathType.PerfectCurve); + Assert.That(seventh.ControlPoints[0].Type == PathType.PERFECT_CURVE); Assert.That(seventh.ControlPoints[1].Position, Is.EqualTo(new Vector2(75, 145))); Assert.That(seventh.ControlPoints[1].Type == null); Assert.That(seventh.ControlPoints[2].Position, Is.EqualTo(new Vector2(170, 75))); - Assert.That(seventh.ControlPoints[2].Type == PathType.PerfectCurve); + Assert.That(seventh.ControlPoints[2].Type == PathType.PERFECT_CURVE); Assert.That(seventh.ControlPoints[3].Position, Is.EqualTo(new Vector2(300, 145))); Assert.That(seventh.ControlPoints[3].Type == null); Assert.That(seventh.ControlPoints[4].Position, Is.EqualTo(new Vector2(410, 20))); @@ -883,10 +937,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)) @@ -964,7 +1019,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints; Assert.That(controlPoints.Count, Is.EqualTo(6)); - Assert.That(controlPoints.Single(c => c.Type != null).Type, Is.EqualTo(PathType.Catmull)); + Assert.That(controlPoints.Single(c => c.Type != null).Type, Is.EqualTo(PathType.CATMULL)); } } @@ -980,9 +1035,9 @@ namespace osu.Game.Tests.Beatmaps.Formats var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints; Assert.That(controlPoints.Count, Is.EqualTo(4)); - Assert.That(controlPoints[0].Type, Is.EqualTo(PathType.Catmull)); - Assert.That(controlPoints[1].Type, Is.EqualTo(PathType.Catmull)); - Assert.That(controlPoints[2].Type, Is.EqualTo(PathType.Catmull)); + Assert.That(controlPoints[0].Type, Is.EqualTo(PathType.CATMULL)); + Assert.That(controlPoints[1].Type, Is.EqualTo(PathType.CATMULL)); + Assert.That(controlPoints[2].Type, Is.EqualTo(PathType.CATMULL)); Assert.That(controlPoints[3].Type, Is.Null); } } @@ -999,7 +1054,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints; Assert.That(controlPoints.Count, Is.EqualTo(4)); - Assert.That(controlPoints[0].Type, Is.EqualTo(PathType.Catmull)); + Assert.That(controlPoints[0].Type, Is.EqualTo(PathType.CATMULL)); Assert.That(controlPoints[0].Position, Is.EqualTo(Vector2.Zero)); Assert.That(controlPoints[1].Type, Is.Null); Assert.That(controlPoints[1].Position, Is.Not.EqualTo(Vector2.Zero)); @@ -1024,10 +1079,113 @@ 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)); + } + } + + [Test] + public void TestNewComboAfterBreak() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("break-between-objects.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var beatmap = decoder.Decode(stream); + Assert.That(((IHasCombo)beatmap.HitObjects[0]).NewCombo, Is.True); + Assert.That(((IHasCombo)beatmap.HitObjects[1]).NewCombo, Is.True); + Assert.That(((IHasCombo)beatmap.HitObjects[2]).NewCombo, Is.False); + } + } + + /// + /// Test cases that involve a spinner between two hitobjects. + /// + [Test] + public void TestSpinnerNewComboBetweenObjects([Values("osu", "catch")] string rulesetName) + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("spinner-between-objects.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + Ruleset ruleset; + + switch (rulesetName) + { + case "osu": + ruleset = new OsuRuleset(); + break; + + case "catch": + ruleset = new CatchRuleset(); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(rulesetName), rulesetName, null); + } + + var working = new TestWorkingBeatmap(decoder.Decode(stream)); + var playable = working.GetPlayableBeatmap(ruleset.RulesetInfo, Array.Empty()); + + // There's no good way to figure out these values other than to compare (in code) with osu!stable... + + Assert.That(((IHasComboInformation)playable.HitObjects[0]).ComboIndexWithOffsets, Is.EqualTo(1)); + Assert.That(((IHasComboInformation)playable.HitObjects[2]).ComboIndexWithOffsets, Is.EqualTo(2)); + Assert.That(((IHasComboInformation)playable.HitObjects[3]).ComboIndexWithOffsets, Is.EqualTo(2)); + Assert.That(((IHasComboInformation)playable.HitObjects[5]).ComboIndexWithOffsets, Is.EqualTo(3)); + Assert.That(((IHasComboInformation)playable.HitObjects[6]).ComboIndexWithOffsets, Is.EqualTo(3)); + Assert.That(((IHasComboInformation)playable.HitObjects[8]).ComboIndexWithOffsets, Is.EqualTo(4)); + Assert.That(((IHasComboInformation)playable.HitObjects[9]).ComboIndexWithOffsets, Is.EqualTo(4)); + Assert.That(((IHasComboInformation)playable.HitObjects[11]).ComboIndexWithOffsets, Is.EqualTo(5)); + Assert.That(((IHasComboInformation)playable.HitObjects[12]).ComboIndexWithOffsets, Is.EqualTo(6)); + Assert.That(((IHasComboInformation)playable.HitObjects[14]).ComboIndexWithOffsets, Is.EqualTo(7)); + Assert.That(((IHasComboInformation)playable.HitObjects[15]).ComboIndexWithOffsets, Is.EqualTo(8)); + Assert.That(((IHasComboInformation)playable.HitObjects[17]).ComboIndexWithOffsets, Is.EqualTo(9)); + } + } + + [Test] + public void TestSliderConversionWithCustomDistance([Values("taiko", "mania")] string rulesetName) + { + using (var resStream = TestResources.OpenResource("custom-slider-length.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + Ruleset ruleset; + + switch (rulesetName) + { + case "taiko": + ruleset = new TaikoRuleset(); + break; + + case "mania": + ruleset = new ManiaRuleset(); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(rulesetName), rulesetName, null); + } + + var decoder = Decoder.GetDecoder(stream); + var working = new TestWorkingBeatmap(decoder.Decode(stream)); + IBeatmap beatmap = working.GetPlayableBeatmap(ruleset.RulesetInfo, Array.Empty()); + + Assert.That(beatmap.HitObjects[0].GetEndTime(), Is.EqualTo(3153)); } } } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 5d9049ead7..e847b61fbe 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Beatmaps.Formats compareBeatmaps(decoded, decodedAfterEncode); - ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo) + static ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo) { // emulate non-legacy control points by cloning the non-legacy portion. // the assertion is that the encoder can recreate this losslessly from hitobject data. @@ -113,6 +113,33 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsTrue(areComboColoursEqual(expected.skin.Configuration, actual.skin.Configuration)); } + [Test] + public void TestEncodeBSplineCurveType() + { + var beatmap = new Beatmap + { + HitObjects = + { + new Slider + { + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero, PathType.BSpline(3)), + new PathControlPoint(new Vector2(50)), + new PathControlPoint(new Vector2(100), PathType.BSpline(3)), + new PathControlPoint(new Vector2(150)) + }) + }, + } + }; + + var decodedAfterEncode = decodeFromLegacy(encodeToLegacy((beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty))), string.Empty); + var decodedSlider = (Slider)decodedAfterEncode.beatmap.HitObjects[0]; + Assert.That(decodedSlider.Path.ControlPoints.Count, Is.EqualTo(4)); + Assert.That(decodedSlider.Path.ControlPoints[0].Type, Is.EqualTo(PathType.BSpline(3))); + Assert.That(decodedSlider.Path.ControlPoints[2].Type, Is.EqualTo(PathType.BSpline(3))); + } + [Test] public void TestEncodeMultiSegmentSliderWithFloatingPointError() { @@ -125,10 +152,10 @@ namespace osu.Game.Tests.Beatmaps.Formats Position = new Vector2(0.6f), Path = new SliderPath(new[] { - new PathControlPoint(Vector2.Zero, PathType.Bezier), + new PathControlPoint(Vector2.Zero, PathType.BEZIER), new PathControlPoint(new Vector2(0.5f)), new PathControlPoint(new Vector2(0.51f)), // This is actually on the same position as the previous one in legacy beatmaps (truncated to int). - new PathControlPoint(new Vector2(1f), PathType.Bezier), + new PathControlPoint(new Vector2(1f), PathType.BEZIER), new PathControlPoint(new Vector2(2f)) }) }, @@ -174,8 +201,8 @@ namespace osu.Game.Tests.Beatmaps.Formats private class TestLegacySkin : LegacySkin { - public TestLegacySkin(IResourceStore storage, string fileName) - : base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, null, storage, fileName) + public TestLegacySkin(IResourceStore fallbackStore, string fileName) + : base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, null, fallbackStore, fileName) { } } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 93cda34ef7..7e3967dc95 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -3,14 +3,17 @@ #nullable disable +using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using NUnit.Framework; -using osu.Framework.Utils; +using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; +using osu.Game.Beatmaps.Legacy; +using osu.Game.IO.Legacy; using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; @@ -19,6 +22,7 @@ using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays; @@ -55,14 +59,14 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(2, score.ScoreInfo.Statistics[HitResult.Great]); Assert.AreEqual(1, score.ScoreInfo.Statistics[HitResult.Good]); - Assert.AreEqual(829_931, score.ScoreInfo.TotalScore); + Assert.AreEqual(829_931, score.ScoreInfo.LegacyTotalScore); Assert.AreEqual(3, score.ScoreInfo.MaxCombo); Assert.IsTrue(score.ScoreInfo.Mods.Any(m => m is ManiaModClassic)); Assert.IsTrue(score.ScoreInfo.APIMods.Any(m => m.Acronym == "CL")); Assert.IsTrue(score.ScoreInfo.ModsJson.Contains("CL")); - Assert.IsTrue(Precision.AlmostEquals(0.8889, score.ScoreInfo.Accuracy, 0.0001)); + Assert.That((2 * 300d + 1 * 200) / (3 * 305d), Is.EqualTo(score.ScoreInfo.Accuracy).Within(0.0001)); Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank); Assert.That(score.Replay.Frames, Is.Not.Empty); @@ -87,6 +91,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)] @@ -191,6 +223,8 @@ namespace osu.Game.Tests.Beatmaps.Formats { new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } }; + scoreInfo.OnlineID = 123123; + scoreInfo.ClientVersion = "2023.1221.0"; var beatmap = new TestBeatmap(ruleset); var score = new Score @@ -209,9 +243,172 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.Multiple(() => { + Assert.That(decodedAfterEncode.ScoreInfo.OnlineID, Is.EqualTo(123123)); Assert.That(decodedAfterEncode.ScoreInfo.Statistics, Is.EqualTo(scoreInfo.Statistics)); Assert.That(decodedAfterEncode.ScoreInfo.MaximumStatistics, Is.EqualTo(scoreInfo.MaximumStatistics)); Assert.That(decodedAfterEncode.ScoreInfo.Mods, Is.EqualTo(scoreInfo.Mods)); + Assert.That(decodedAfterEncode.ScoreInfo.ClientVersion, Is.EqualTo("2023.1221.0")); + }); + } + + [Test] + public void AccuracyOfStableScoreRecomputed() + { + var memoryStream = new MemoryStream(); + + // local partial implementation of legacy score encoder + // this is done half for readability, half because `LegacyScoreEncoder` forces `LATEST_VERSION` + // and we want to emulate a stable score here + using (var sw = new SerializationWriter(memoryStream, true)) + { + sw.Write((byte)3); // ruleset id (mania). + // mania is used intentionally as it is the only ruleset wherein default accuracy calculation is changed in lazer + sw.Write(20240116); // version (anything below `LegacyScoreEncoder.FIRST_LAZER_VERSION` is stable) + sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test + sw.Write("username"); // irrelevant to this test + sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test + sw.Write((ushort)1); // count300 + sw.Write((ushort)0); // count100 + sw.Write((ushort)0); // count50 + sw.Write((ushort)198); // countGeki (perfects / "rainbow 300s" in mania) + sw.Write((ushort)0); // countKatu + sw.Write((ushort)1); // countMiss + sw.Write(12345678); // total score, irrelevant to this test + sw.Write((ushort)1000); // max combo, irrelevant to this test + sw.Write(false); // full combo, irrelevant to this test + sw.Write((int)LegacyMods.Hidden); // mods + sw.Write(string.Empty); // hp graph, irrelevant + sw.Write(DateTime.Now); // date, irrelevant + sw.Write(Array.Empty()); // replay data, irrelevant + sw.Write((long)1234); // legacy online ID, irrelevant + } + + memoryStream.Seek(0, SeekOrigin.Begin); + var decoded = new TestLegacyScoreDecoder().Parse(memoryStream); + + Assert.Multiple(() => + { + Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 305 + 300) / (200 * 305))); + Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH)); + }); + } + + [Test] + public void RankOfStableScoreUsesLazerDefinitions() + { + var memoryStream = new MemoryStream(); + + // local partial implementation of legacy score encoder + // this is done half for readability, half because `LegacyScoreEncoder` forces `LATEST_VERSION` + // and we want to emulate a stable score here + using (var sw = new SerializationWriter(memoryStream, true)) + { + sw.Write((byte)0); // ruleset id (osu!) + sw.Write(20240116); // version (anything below `LegacyScoreEncoder.FIRST_LAZER_VERSION` is stable) + sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test + sw.Write("username"); // irrelevant to this test + sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test + sw.Write((ushort)195); // count300 + sw.Write((ushort)1); // count100 + sw.Write((ushort)4); // count50 + sw.Write((ushort)0); // countGeki + sw.Write((ushort)0); // countKatu + sw.Write((ushort)0); // countMiss + sw.Write(12345678); // total score, irrelevant to this test + sw.Write((ushort)1000); // max combo, irrelevant to this test + sw.Write(false); // full combo, irrelevant to this test + sw.Write((int)LegacyMods.Hidden); // mods + sw.Write(string.Empty); // hp graph, irrelevant + sw.Write(DateTime.Now); // date, irrelevant + sw.Write(Array.Empty()); // replay data, irrelevant + sw.Write((long)1234); // legacy online ID, irrelevant + } + + memoryStream.Seek(0, SeekOrigin.Begin); + var decoded = new TestLegacyScoreDecoder().Parse(memoryStream); + + Assert.Multiple(() => + { + // In stable this would be an A because there are over 1% 50s. But that's not a thing in lazer. + Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH)); + }); + } + + [Test] + public void AccuracyRankAndTotalScoreOfLazerScorePreserved() + { + var ruleset = new OsuRuleset().RulesetInfo; + + var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); + scoreInfo.Mods = new Mod[] { new OsuModFlashlight() }; + scoreInfo.Statistics = new Dictionary + { + [HitResult.Great] = 199, + [HitResult.Miss] = 1, + [HitResult.LargeTickHit] = 1, + }; + scoreInfo.MaximumStatistics = new Dictionary + { + [HitResult.Great] = 200, + [HitResult.LargeTickHit] = 1, + }; + + var beatmap = new TestBeatmap(ruleset); + var score = new Score + { + ScoreInfo = scoreInfo, + }; + + var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap); + + Assert.Multiple(() => + { + Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(284_537)); + Assert.That(decodedAfterEncode.ScoreInfo.LegacyTotalScore, Is.Null); + Assert.That(decodedAfterEncode.ScoreInfo.Accuracy, Is.EqualTo((double)(199 * 300 + 30) / (200 * 300 + 30))); + Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A)); + }); + } + + [Test] + public void AccuracyAndRankOfLazerScoreWithoutLegacyReplaySoloScoreInfoUsesBestEffortFallbackToLegacy() + { + var memoryStream = new MemoryStream(); + + // local partial implementation of legacy score encoder + // this is done half for readability, half because we want to emulate an old lazer score here + // that does not have everything that `LegacyScoreEncoder` now writes to the replay + using (var sw = new SerializationWriter(memoryStream, true)) + { + sw.Write((byte)0); // ruleset id (osu!) + sw.Write(LegacyScoreEncoder.FIRST_LAZER_VERSION); // version + sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test + sw.Write("username"); // irrelevant to this test + sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test + sw.Write((ushort)198); // count300 + sw.Write((ushort)0); // count100 + sw.Write((ushort)1); // count50 + sw.Write((ushort)0); // countGeki + sw.Write((ushort)0); // countKatu + sw.Write((ushort)1); // countMiss + sw.Write(12345678); // total score, irrelevant to this test + sw.Write((ushort)1000); // max combo, irrelevant to this test + sw.Write(false); // full combo, irrelevant to this test + sw.Write((int)LegacyMods.Hidden); // mods + sw.Write(string.Empty); // hp graph, irrelevant + sw.Write(DateTime.Now); // date, irrelevant + sw.Write(Array.Empty()); // replay data, irrelevant + sw.Write((long)1234); // legacy online ID, irrelevant + // importantly, no compressed `LegacyReplaySoloScoreInfo` here + } + + memoryStream.Seek(0, SeekOrigin.Begin); + var decoded = new TestLegacyScoreDecoder().Parse(memoryStream); + + Assert.Multiple(() => + { + Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 300 + 50) / (200 * 300))); + Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A)); }); } @@ -262,6 +459,12 @@ namespace osu.Game.Tests.Beatmaps.Formats Ruleset = new OsuRuleset().RulesetInfo, Difficulty = new BeatmapDifficulty(), BeatmapVersion = beatmapVersion, + }, + // needs to have at least one objects so that `StandardisedScoreMigrationTools` doesn't die + // when trying to recompute total score. + HitObjects = + { + new HitCircle() } }); } 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/SliderEventGenerationTest.cs b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs index a26c8121dd..c7cf3fe956 100644 --- a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs +++ b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs @@ -16,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)); @@ -31,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)); @@ -52,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)); @@ -83,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] @@ -97,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/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 47f4410b07..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] diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs index 3a4c55c65c..eae12edebd 100644 --- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs +++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs @@ -112,7 +112,7 @@ namespace osu.Game.Tests.Chat }); AddStep("post message", () => channelManager.PostMessage("Something interesting")); - AddUntilStep("message postesd", () => !channel.Messages.Any(m => m is LocalMessage)); + AddUntilStep("message posted", () => !channel.Messages.Any(m => m is LocalMessage)); AddStep("post /help command", () => channelManager.PostCommand("help", channel)); AddStep("post /me command with no action", () => channelManager.PostCommand("me", channel)); @@ -146,6 +146,23 @@ namespace osu.Game.Tests.Chat AddAssert("channel has no more messages", () => channel.Messages, () => Is.Empty); } + [Test] + public void TestCommandNameCaseInsensitivity() + { + Channel channel = null; + + AddStep("join channel and select it", () => + { + channelManager.JoinChannel(channel = createChannel(1, ChannelType.Public)); + channelManager.CurrentChannel.Value = channel; + }); + + AddStep("post /me command", () => channelManager.PostCommand("ME DANCES")); + AddUntilStep("/me command received", () => channel.Messages.Last().Content.Contains("DANCES")); + AddStep("post /help command", () => channelManager.PostCommand("HeLp")); + AddUntilStep("/help command received", () => channel.Messages.Last().Content.Contains("Supported commands")); + } + private void handlePostMessageRequest(PostMessageRequest request) { var message = new Message(++currentMessageId) diff --git a/osu.Game.Tests/Database/BackgroundBeatmapProcessorTests.cs b/osu.Game.Tests/Database/BackgroundBeatmapProcessorTests.cs deleted file mode 100644 index c876316be4..0000000000 --- a/osu.Game.Tests/Database/BackgroundBeatmapProcessorTests.cs +++ /dev/null @@ -1,132 +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.Bindables; -using osu.Framework.Extensions; -using osu.Framework.Testing; -using osu.Game.Beatmaps; -using osu.Game.Screens.Play; -using osu.Game.Tests.Beatmaps.IO; -using osu.Game.Tests.Visual; - -namespace osu.Game.Tests.Database -{ - [HeadlessTest] - public partial class BackgroundBeatmapProcessorTests : OsuTestScene, ILocalUserPlayInfo - { - public IBindable IsPlaying => isPlaying; - - private readonly Bindable isPlaying = new Bindable(); - - private BeatmapSetInfo importedSet = null!; - - [BackgroundDependencyLoader] - private void load(OsuGameBase osu) - { - importedSet = BeatmapImportHelper.LoadQuickOszIntoOsu(osu).GetResultSafely(); - } - - [SetUpSteps] - public void SetUpSteps() - { - AddStep("Set not playing", () => isPlaying.Value = false); - } - - [Test] - public void TestDifficultyProcessing() - { - AddAssert("Difficulty is initially set", () => - { - return Realm.Run(r => - { - var beatmapSetInfo = r.Find(importedSet.ID)!; - return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); - }); - }); - - AddStep("Reset difficulty", () => - { - Realm.Write(r => - { - var beatmapSetInfo = r.Find(importedSet.ID)!; - foreach (var b in beatmapSetInfo.Beatmaps) - b.StarRating = -1; - }); - }); - - AddStep("Run background processor", () => - { - Add(new TestBackgroundBeatmapProcessor()); - }); - - AddUntilStep("wait for difficulties repopulated", () => - { - return Realm.Run(r => - { - var beatmapSetInfo = r.Find(importedSet.ID)!; - return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); - }); - }); - } - - [Test] - public void TestDifficultyProcessingWhilePlaying() - { - AddAssert("Difficulty is initially set", () => - { - return Realm.Run(r => - { - var beatmapSetInfo = r.Find(importedSet.ID)!; - return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); - }); - }); - - AddStep("Set playing", () => isPlaying.Value = true); - - AddStep("Reset difficulty", () => - { - Realm.Write(r => - { - var beatmapSetInfo = r.Find(importedSet.ID)!; - foreach (var b in beatmapSetInfo.Beatmaps) - b.StarRating = -1; - }); - }); - - AddStep("Run background processor", () => - { - Add(new TestBackgroundBeatmapProcessor()); - }); - - AddWaitStep("wait some", 500); - - AddAssert("Difficulty still not populated", () => - { - return Realm.Run(r => - { - var beatmapSetInfo = r.Find(importedSet.ID)!; - return beatmapSetInfo.Beatmaps.All(b => b.StarRating == -1); - }); - }); - - AddStep("Set not playing", () => isPlaying.Value = false); - - AddUntilStep("wait for difficulties repopulated", () => - { - return Realm.Run(r => - { - var beatmapSetInfo = r.Find(importedSet.ID)!; - return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); - }); - }); - } - - public partial class TestBackgroundBeatmapProcessor : BackgroundBeatmapProcessor - { - protected override int TimeToSleepDuringGameplay => 10; - } - } -} diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs new file mode 100644 index 0000000000..e960995c45 --- /dev/null +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -0,0 +1,221 @@ +// 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.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +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; + +namespace osu.Game.Tests.Database +{ + [HeadlessTest] + public partial class BackgroundDataStoreProcessorTests : OsuTestScene, ILocalUserPlayInfo + { + public IBindable IsPlaying => isPlaying; + + private readonly Bindable isPlaying = new Bindable(); + + private BeatmapSetInfo importedSet = null!; + + [BackgroundDependencyLoader] + private void load(OsuGameBase osu) + { + importedSet = BeatmapImportHelper.LoadQuickOszIntoOsu(osu).GetResultSafely(); + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Set not playing", () => isPlaying.Value = false); + } + + [Test] + public void TestDifficultyProcessing() + { + AddAssert("Difficulty is initially set", () => + { + return Realm.Run(r => + { + var beatmapSetInfo = r.Find(importedSet.ID)!; + return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); + }); + }); + + AddStep("Reset difficulty", () => + { + Realm.Write(r => + { + var beatmapSetInfo = r.Find(importedSet.ID)!; + foreach (var b in beatmapSetInfo.Beatmaps) + b.StarRating = -1; + }); + }); + + AddStep("Run background processor", () => + { + Add(new TestBackgroundDataStoreProcessor()); + }); + + AddUntilStep("wait for difficulties repopulated", () => + { + return Realm.Run(r => + { + var beatmapSetInfo = r.Find(importedSet.ID)!; + return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); + }); + }); + } + + [Test] + public void TestDifficultyProcessingWhilePlaying() + { + AddAssert("Difficulty is initially set", () => + { + return Realm.Run(r => + { + var beatmapSetInfo = r.Find(importedSet.ID)!; + return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); + }); + }); + + AddStep("Set playing", () => isPlaying.Value = true); + + AddStep("Reset difficulty", () => + { + Realm.Write(r => + { + var beatmapSetInfo = r.Find(importedSet.ID)!; + foreach (var b in beatmapSetInfo.Beatmaps) + b.StarRating = -1; + }); + }); + + AddStep("Run background processor", () => + { + Add(new TestBackgroundDataStoreProcessor()); + }); + + AddWaitStep("wait some", 500); + + AddAssert("Difficulty still not populated", () => + { + return Realm.Run(r => + { + var beatmapSetInfo = r.Find(importedSet.ID)!; + return beatmapSetInfo.Beatmaps.All(b => b.StarRating == -1); + }); + }); + + AddStep("Set not playing", () => isPlaying.Value = false); + + AddUntilStep("wait for difficulties repopulated", () => + { + return Realm.Run(r => + { + var beatmapSetInfo = r.Find(importedSet.ID)!; + return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0); + }); + }); + } + + [TestCase(30000001)] + [TestCase(30000002)] + [TestCase(30000003)] + [TestCase(30000004)] + [TestCase(30000005)] + 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)); + } + + [Test] + public void TestCustomRulesetScoreNotSubjectToUpgrades([Values] bool available) + { + RulesetInfo rulesetInfo = null!; + ScoreInfo scoreInfo = null!; + TestBackgroundDataStoreProcessor processor = null!; + + AddStep("Add unavailable ruleset", () => Realm.Write(r => r.Add(rulesetInfo = new RulesetInfo + { + ShortName = Guid.NewGuid().ToString(), + Available = available + }))); + + AddStep("Add score for unavailable ruleset", () => Realm.Write(r => r.Add(scoreInfo = new ScoreInfo( + ruleset: rulesetInfo, + beatmap: r.All().First()) + { + TotalScoreVersion = 30000001 + }))); + + AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor())); + AddUntilStep("Wait for completion", () => processor.Completed); + + AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False); + AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000001)); + } + + public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor + { + protected override int TimeToSleepDuringGameplay => 10; + + public bool Completed => ProcessingTask.IsCompleted; + } + } +} diff --git a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs index b237556d11..5f722e381c 100644 --- a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs +++ b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs @@ -1,19 +1,23 @@ // 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.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(); @@ -40,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) @@ -60,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/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/Editing/Checks/CheckDelayedHitsoundsTest.cs b/osu.Game.Tests/Editing/Checks/CheckDelayedHitsoundsTest.cs new file mode 100644 index 0000000000..20b9643ab4 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckDelayedHitsoundsTest.cs @@ -0,0 +1,105 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using System.Linq; +using ManagedBass; +using Moq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Models; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; +using osuTK.Audio; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckDelayedHitsoundsTest + { + private CheckDelayedHitsounds check = null!; + private IBeatmap beatmap = null!; + + [SetUp] + public void SetUp() + { + check = new CheckDelayedHitsounds(); + beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + BeatmapSet = new BeatmapSetInfo + { + Files = + { + new RealmNamedFileUsage(new RealmFile { Hash = "abcdef" }, "normal-hitnormal.wav"), + } + } + } + }; + + if (!Bass.Init(0) && Bass.LastError != Errors.Already) + throw new AudioException("Could not initialize Bass."); + } + + [Test] + public void TestNoDelayedHitsounds() + { + using var resourceStream = TestResources.OpenResource("Samples/hitsound-no-delay.wav"); + Assert.IsEmpty(check.Run(getContext(resourceStream))); + } + + [Test] + public void TestMinorDelayedHitsounds() + { + // 1 ms of silence -> 1 ms of noise at 0.3 amplitude -> hitsound transient + // => The transient is delayed by 2 ms + // Waveform: https://github.com/ppy/osu/assets/39100084/d5b9edbe-0ba2-401d-94b0-6d57228bdbd3 + using (var resourceStream = TestResources.OpenResource("Samples/hitsound-minor-delay.wav")) + { + var issues = check.Run(getContext(resourceStream)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckDelayedHitsounds.IssueTemplateMinorDelay); + } + } + + [Test] + public void TestDelayedHitsounds() + { + // 3 ms of silence -> 3 ms of noise at 0.3 amplitude -> hitsound transient + // => The transient is delayed by 6 ms + // Waveform: https://github.com/ppy/osu/assets/39100084/2509ff35-d908-414b-b7b9-583681348772 + using var resourceStream = TestResources.OpenResource("Samples/hitsound-delay.wav"); + + var issues = check.Run(getContext(resourceStream)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckDelayedHitsounds.IssueTemplateDelay); + } + + [Test] + public void TestConsequentlyDelayedHitsounds() + { + // The hitsound is delayed by 10 ms + // Waveform: https://github.com/ppy/osu/assets/39100084/3a7ede0d-8523-4b99-a222-3624cd208267 + using var resourceStream = TestResources.OpenResource("Samples/hitsound-consequent-delay.wav"); + + var issues = check.Run(getContext(resourceStream)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckDelayedHitsounds.IssueTemplateConsequentDelay); + } + + private BeatmapVerifierContext getContext(Stream? resourceStream) + { + var mockWorkingBeatmap = new Mock(beatmap, null, null); + mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream); + + return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object); + } + } +} diff --git a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs index 5af0366e6e..21d8a165ff 100644 --- a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs +++ b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs @@ -162,7 +162,7 @@ namespace osu.Game.Tests.Editing { new PathControlPoint(Vector2.Zero), new PathControlPoint(Vector2.One), - new PathControlPoint(new Vector2(2), PathType.Bezier), + new PathControlPoint(new Vector2(2), PathType.BEZIER), new PathControlPoint(new Vector2(3)), }, 50) }, @@ -179,7 +179,7 @@ namespace osu.Game.Tests.Editing StartTime = 2000, Path = new SliderPath(new[] { - new PathControlPoint(Vector2.Zero, PathType.Bezier), + new PathControlPoint(Vector2.Zero, PathType.BEZIER), new PathControlPoint(new Vector2(4)), new PathControlPoint(new Vector2(5)), }, 100) 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/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index fd0bff101f..584a9e09c0 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -192,7 +192,8 @@ namespace osu.Game.Tests.Gameplay AddStep("apply perfect hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect })); AddAssert("not failed", () => !processor.HasFailed); - AddStep($"apply {resultApplied.ToString().ToLowerInvariant()} hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied })); + AddStep($"apply {resultApplied.ToString().ToLowerInvariant()} hit result", + () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied })); AddAssert("failed", () => processor.HasFailed); } @@ -232,6 +233,84 @@ namespace osu.Game.Tests.Gameplay assertHealthEqualTo(1); } + [Test] + public void TestNoBreakDrainRate() + { + DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new JudgeableHitObject { StartTime = 0 }, + new JudgeableHitObject { StartTime = 2000 } + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(4.5E-5).Within(0.1E-5)); + } + + [Test] + public void TestSingleBreakDrainRate() + { + DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new JudgeableHitObject { StartTime = 0 }, + new JudgeableHitObject { StartTime = 2000 } + }, + Breaks = + { + new BreakPeriod(500, 1500) + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(9.1E-5).Within(0.1E-5)); + } + + [Test] + public void TestOverlappingBreakDrainRate() + { + DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new JudgeableHitObject { StartTime = 0 }, + new JudgeableHitObject { StartTime = 2000 } + }, + Breaks = + { + new BreakPeriod(500, 1400), + new BreakPeriod(750, 1500), + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(9.1E-5).Within(0.1E-5)); + } + + [Test] + public void TestSequentialBreakDrainRate() + { + DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new JudgeableHitObject { StartTime = 0 }, + new JudgeableHitObject { StartTime = 2000 } + }, + Breaks = + { + new BreakPeriod(500, 1000), + new BreakPeriod(1000, 1500), + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(9.1E-5).Within(0.1E-5)); + } + private Beatmap createBeatmap(double startTime, double endTime, params BreakPeriod[] breaks) { var beatmap = new Beatmap diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs index f38c2c9416..acb14f86fc 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs @@ -94,9 +94,6 @@ namespace osu.Game.Tests.Gameplay private class TestHitObjectWithCombo : ConvertHitObject, IHasComboInformation { - public bool NewCombo { get; set; } - public int ComboOffset => 0; - public Bindable IndexInCurrentComboBindable { get; } = new Bindable(); public int IndexInCurrentCombo 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 1cf72cf937..1a644ad600 100644 --- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Gameplay // Apply a judgement scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new TestJudgement(HitResult.LargeBonus)) { Type = HitResult.LargeBonus }); - Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(Judgement.LARGE_BONUS_SCORE)); + Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(scoreProcessor.GetBaseScoreForResult(HitResult.LargeBonus))); } [Test] 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 3f0f8a4f14..27b8d3f21e 100644 --- a/osu.Game.Tests/ImportTest.cs +++ b/osu.Game.Tests/ImportTest.cs @@ -9,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 @@ -46,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/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/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index aa41fd830b..decb0a31ac 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -7,7 +7,9 @@ using Moq; using NUnit.Framework; using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Utils; namespace osu.Game.Tests.Mods @@ -147,11 +149,11 @@ namespace osu.Game.Tests.Mods new Mod[] { new OsuModDeflate(), new OsuModApproachDifferent() }, new[] { typeof(OsuModDeflate), typeof(OsuModApproachDifferent) } }, - // system mod. + // system mod not applicable in lazer. new object[] { - new Mod[] { new OsuModHidden(), new OsuModTouchDevice() }, - new[] { typeof(OsuModTouchDevice) } + new Mod[] { new OsuModHidden(), new ModScoreV2() }, + new[] { typeof(ModScoreV2) } }, // multi mod. new object[] @@ -310,6 +312,36 @@ namespace osu.Game.Tests.Mods Assert.That(invalid?.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid)); } + [Test] + public void TestModBelongsToRuleset() + { + Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), Array.Empty())); + Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime() })); + Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime(), new OsuModAccuracyChallenge() })); + Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime(), new ModAccuracyChallenge() }), Is.False); + Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime(), new TaikoModFlashlight() }), Is.False); + } + + [Test] + public void TestFormatScoreMultiplier() + { + Assert.AreEqual(ModUtils.FormatScoreMultiplier(0.9999).ToString(), "0.99x"); + Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.0).ToString(), "1.00x"); + Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.0001).ToString(), "1.01x"); + + Assert.AreEqual(ModUtils.FormatScoreMultiplier(0.899999999999999).ToString(), "0.90x"); + Assert.AreEqual(ModUtils.FormatScoreMultiplier(0.9).ToString(), "0.90x"); + Assert.AreEqual(ModUtils.FormatScoreMultiplier(0.900000000000001).ToString(), "0.90x"); + + Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.099999999999999).ToString(), "1.10x"); + Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.1).ToString(), "1.10x"); + Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.100000000000001).ToString(), "1.10x"); + + Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.045).ToString(), "1.05x"); + Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.05).ToString(), "1.05x"); + Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.055).ToString(), "1.06x"); + } + public abstract class CustomMod1 : Mod, IModCompatibilitySpecification { } @@ -339,6 +371,16 @@ namespace osu.Game.Tests.Mods public override bool ValidForMultiplayerAsFreeMod => false; } + public class EditableMod : Mod + { + public override string Name => string.Empty; + public override LocalisableString Description => string.Empty; + public override string Acronym => string.Empty; + public override double ScoreMultiplier => Multiplier; + + public double Multiplier = 1; + } + public interface IModCompatibilitySpecification { } 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/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index ce95e921b9..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 @@ -382,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/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/Ranking/UnstableRateTest.cs b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs index 27c8270f0f..5a416d05d7 100644 --- a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs +++ b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.NonVisual.Ranking public void TestDistributedHits() { var events = Enumerable.Range(-5, 11) - .Select(t => new HitEvent(t - 5, HitResult.Great, new HitObject(), null, null)); + .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)); var unstableRate = new UnstableRate(events); @@ -33,14 +33,46 @@ namespace osu.Game.Tests.NonVisual.Ranking { var events = new[] { - new HitEvent(-100, HitResult.Miss, new HitObject(), null, null), - new HitEvent(0, HitResult.Great, new HitObject(), null, null), - new HitEvent(200, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null), + new HitEvent(-100, 1.0, HitResult.Miss, new HitObject(), null, null), + new HitEvent(0, 1.0, HitResult.Great, new HitObject(), null, null), + new HitEvent(200, 1.0, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null), }; var unstableRate = new UnstableRate(events); Assert.AreEqual(0, unstableRate.Value); } + + [Test] + public void TestStaticRateChange() + { + var events = new[] + { + new HitEvent(-150, 1.5, HitResult.Great, new HitObject(), null, null), + new HitEvent(-150, 1.5, HitResult.Great, new HitObject(), null, null), + new HitEvent(150, 1.5, HitResult.Great, new HitObject(), null, null), + new HitEvent(150, 1.5, HitResult.Great, new HitObject(), null, null), + }; + + var unstableRate = new UnstableRate(events); + + Assert.AreEqual(10 * 100, unstableRate.Value); + } + + [Test] + public void TestDynamicRateChange() + { + var events = new[] + { + new HitEvent(-50, 0.5, HitResult.Great, new HitObject(), null, null), + new HitEvent(75, 0.75, HitResult.Great, new HitObject(), null, null), + new HitEvent(-100, 1.0, HitResult.Great, new HitObject(), null, null), + new HitEvent(125, 1.25, HitResult.Great, new HitObject(), null, null), + }; + + var unstableRate = new UnstableRate(events); + + Assert.AreEqual(10 * 100, unstableRate.Value); + } } } diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs index ca0d4d3cf3..fbe5a0e4d7 100644 --- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs +++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs @@ -56,24 +56,6 @@ namespace osu.Game.Tests.NonVisual.Skinning "Gameplay/osu/followpoint", 1 }, new object[] - { - new[] { "followpoint@2x", "followpoint" }, - "Gameplay/osu/followpoint", - "followpoint@2x", 2 - }, - new object[] - { - new[] { "followpoint@2x" }, - "Gameplay/osu/followpoint", - "followpoint@2x", 2 - }, - new object[] - { - new[] { "followpoint" }, - "Gameplay/osu/followpoint", - "followpoint", 1 - }, - new object[] { // Looking up a filename with extension specified should work. new[] { "followpoint.png" }, @@ -127,8 +109,50 @@ namespace osu.Game.Tests.NonVisual.Skinning Assert.IsNull(texture); } + [Test] + public void TestDisallowHighResolutionSprites() + { + var textureStore = new TestTextureStore("hitcircle", "hitcircle@2x"); + var legacySkin = new TestLegacySkin(textureStore) { HighResolutionSprites = false }; + + var texture = legacySkin.GetTexture("hitcircle"); + + Assert.IsNotNull(texture); + Assert.That(texture.ScaleAdjust, Is.EqualTo(1)); + + var twoTimesTexture = legacySkin.GetTexture("hitcircle@2x"); + + Assert.IsNotNull(twoTimesTexture); + Assert.That(twoTimesTexture.ScaleAdjust, Is.EqualTo(1)); + + Assert.AreNotEqual(texture, twoTimesTexture); + } + + [Test] + public void TestAllowHighResolutionSprites() + { + var textureStore = new TestTextureStore("hitcircle", "hitcircle@2x"); + var legacySkin = new TestLegacySkin(textureStore) { HighResolutionSprites = true }; + + var texture = legacySkin.GetTexture("hitcircle"); + + Assert.IsNotNull(texture); + Assert.That(texture.ScaleAdjust, Is.EqualTo(2)); + + var twoTimesTexture = legacySkin.GetTexture("hitcircle@2x"); + + Assert.IsNotNull(twoTimesTexture); + Assert.That(twoTimesTexture.ScaleAdjust, Is.EqualTo(2)); + + Assert.AreEqual(texture, twoTimesTexture); + } + private class TestLegacySkin : LegacySkin { + public bool HighResolutionSprites { get; set; } = true; + + protected override bool AllowHighResolutionSprites => HighResolutionSprites; + public TestLegacySkin(IResourceStore textureStore) : base(new SkinInfo(), new TestResourceProvider(textureStore), null, string.Empty) { diff --git a/osu.Game.Tests/Online/TestSoloScoreInfoJsonSerialization.cs b/osu.Game.Tests/Online/TestSoloScoreInfoJsonSerialization.cs index 19bc96c677..509768530f 100644 --- a/osu.Game.Tests/Online/TestSoloScoreInfoJsonSerialization.cs +++ b/osu.Game.Tests/Online/TestSoloScoreInfoJsonSerialization.cs @@ -5,6 +5,7 @@ using Newtonsoft.Json; using NUnit.Framework; using osu.Game.IO.Serialization; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Scoring; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Online @@ -36,5 +37,31 @@ namespace osu.Game.Tests.Online Assert.That(serialised, Contains.Substring("large_tick_hit")); Assert.That(serialised, Contains.Substring("\"rank\":\"S\"")); } + + /// + /// Ensures that the proxy implementations of by + /// do not get serialised to JSON. + /// + [Test] + public void TestScoreSerialisationSkipsInterfaceMembers() + { + var score = SoloScoreInfo.ForSubmission(TestResources.CreateTestScoreInfo()); + + string[] variants = + { + JsonConvert.SerializeObject(score), + score.Serialize() + }; + + foreach (string serialised in variants) + { + Assert.That(serialised, Does.Not.Contain("\"online_id\":")); + Assert.That(serialised, Does.Not.Contain("\"user\":")); + Assert.That(serialised, Does.Not.Contain("\"date\":")); + Assert.That(serialised, Does.Not.Contain("\"legacy_online_id\":")); + Assert.That(serialised, Does.Not.Contain("\"beatmap\":")); + Assert.That(serialised, Does.Not.Contain("\"ruleset\":")); + } + } } } 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-20231106.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20231106.osk new file mode 100644 index 0000000000..70c4ff64d7 Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/modified-argon-20231106.osk differ diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20231108.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20231108.osk new file mode 100644 index 0000000000..d56c4d4dcd Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/modified-argon-20231108.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/Samples/hitsound-consequent-delay.wav b/osu.Game.Tests/Resources/Samples/hitsound-consequent-delay.wav new file mode 100644 index 0000000000..049e54c62f Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/hitsound-consequent-delay.wav differ diff --git a/osu.Game.Tests/Resources/Samples/hitsound-delay.wav b/osu.Game.Tests/Resources/Samples/hitsound-delay.wav new file mode 100644 index 0000000000..4a3be92f9c Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/hitsound-delay.wav differ diff --git a/osu.Game.Tests/Resources/Samples/hitsound-minor-delay.wav b/osu.Game.Tests/Resources/Samples/hitsound-minor-delay.wav new file mode 100644 index 0000000000..76dbce77f4 Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/hitsound-minor-delay.wav differ diff --git a/osu.Game.Tests/Resources/Samples/hitsound-no-delay.wav b/osu.Game.Tests/Resources/Samples/hitsound-no-delay.wav new file mode 100644 index 0000000000..cdda5709b8 Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/hitsound-no-delay.wav 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/break-between-objects.osu b/osu.Game.Tests/Resources/break-between-objects.osu new file mode 100644 index 0000000000..91821e2c58 --- /dev/null +++ b/osu.Game.Tests/Resources/break-between-objects.osu @@ -0,0 +1,15 @@ +osu file format v14 + +[General] +Mode: 0 + +[Events] +2,200,1200 + +[TimingPoints] +0,307.692307692308,4,2,1,60,1,0 + +[HitObjects] +142,99,0,1,0,0:0:0:0: +323,88,3000,1,0,0:0:0:0: +323,88,4000,1,0,0:0:0:0: diff --git a/osu.Game.Tests/Resources/custom-slider-length.osu b/osu.Game.Tests/Resources/custom-slider-length.osu new file mode 100644 index 0000000000..f7529918a9 --- /dev/null +++ b/osu.Game.Tests/Resources/custom-slider-length.osu @@ -0,0 +1,19 @@ +osu file format v14 + +[General] +Mode: 0 + +[Difficulty] +HPDrainRate:6 +CircleSize:7 +OverallDifficulty:7 +ApproachRate:10 +SliderMultiplier:1.7 +SliderTickRate:1 + +[TimingPoints] +29,333.333333333333,4,1,0,100,1,0 +29,-10000,4,1,0,100,0,0 + +[HitObjects] +256,192,29,6,0,P|384:192|384:192,1,159.375 \ No newline at end of file diff --git a/osu.Game.Tests/Resources/hitobject-combo-offset.osu b/osu.Game.Tests/Resources/hitobject-combo-offset.osu index d39a3e8548..9f39229d87 100644 --- a/osu.Game.Tests/Resources/hitobject-combo-offset.osu +++ b/osu.Game.Tests/Resources/hitobject-combo-offset.osu @@ -3,30 +3,30 @@ osu file format v14 [HitObjects] // Circle with combo offset (3) 255,193,1000,49,0,0:0:0:0: -// Combo index = 4 +// Combo index = 1 // Spinner with new combo followed by circle with no new combo 256,192,2000,12,0,2000,0:0:0:0: 255,193,3000,1,0,0:0:0:0: -// Combo index = 5 +// Combo index = 2 // Spinner without new combo followed by circle with no new combo 256,192,4000,8,0,5000,0:0:0:0: 255,193,6000,1,0,0:0:0:0: -// Combo index = 5 +// Combo index = 3 // Spinner without new combo followed by circle with new combo 256,192,7000,8,0,8000,0:0:0:0: 255,193,9000,5,0,0:0:0:0: -// Combo index = 6 +// Combo index = 4 // Spinner with new combo and offset (1) followed by circle with new combo and offset (3) 256,192,10000,28,0,11000,0:0:0:0: 255,193,12000,53,0,0:0:0:0: -// Combo index = 11 +// Combo index = 8 // Spinner with new combo and offset (2) followed by slider with no new combo followed by circle with no new combo 256,192,13000,44,0,14000,0:0:0:0: 256,192,15000,8,0,16000,0:0:0:0: 255,193,17000,1,0,0:0:0:0: -// Combo index = 14 \ No newline at end of file +// Combo index = 9 \ No newline at end of file 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/mania-skin-broken-array.ini b/osu.Game.Tests/Resources/mania-skin-broken-array.ini new file mode 100644 index 0000000000..5a6d37eef6 --- /dev/null +++ b/osu.Game.Tests/Resources/mania-skin-broken-array.ini @@ -0,0 +1,3 @@ +[Mania] +Keys: 4 +ColumnLineWidth: 3,,3,3,3 \ No newline at end of file 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/spinner-between-objects.osu b/osu.Game.Tests/Resources/spinner-between-objects.osu new file mode 100644 index 0000000000..03e61d965c --- /dev/null +++ b/osu.Game.Tests/Resources/spinner-between-objects.osu @@ -0,0 +1,38 @@ +osu file format v14 + +[General] +Mode: 0 + +[TimingPoints] +0,571.428571428571,4,2,1,5,1,0 + +[HitObjects] +// +C -> +C -> +C +104,95,0,5,0,0:0:0:0: +256,192,1000,12,0,2000,0:0:0:0: +178,171,3000,5,0,0:0:0:0: + +// -C -> +C -> +C +178,171,4000,1,0,0:0:0:0: +256,192,5000,12,0,6000,0:0:0:0: +178,171,7000,5,0,0:0:0:0: + +// -C -> -C -> +C +178,171,8000,1,0,0:0:0:0: +256,192,9000,8,0,10000,0:0:0:0: +178,171,11000,5,0,0:0:0:0: + +// -C -> -C -> -C +178,171,12000,1,0,0:0:0:0: +256,192,13000,8,0,14000,0:0:0:0: +178,171,15000,1,0,0:0:0:0: + +// +C -> -C -> -C +178,171,16000,5,0,0:0:0:0: +256,192,17000,8,0,18000,0:0:0:0: +178,171,19000,1,0,0:0:0:0: + +// +C -> +C -> -C +178,171,20000,5,0,0:0:0:0: +256,192,21000,12,0,22000,0:0:0:0: +178,171,23000,1,0,0:0:0:0: \ No newline at end of file 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..e003c9c534 --- /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, HitResult.IgnoreMiss })] + [TestCase(new[] { HitResult.LargeTickHit }, new[] { HitResult.LargeTickMiss, HitResult.IgnoreMiss })] + [TestCase(new[] { HitResult.SmallTickHit }, new[] { HitResult.SmallTickMiss, HitResult.IgnoreMiss })] + [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..73465fae08 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -10,15 +10,20 @@ using NUnit.Framework; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.UI; +using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using osu.Game.Tests.Beatmaps; @@ -42,12 +47,12 @@ namespace osu.Game.Tests.Rulesets.Scoring }; } - [TestCase(ScoringMode.Standardised, HitResult.Meh, 116_667)] - [TestCase(ScoringMode.Standardised, HitResult.Ok, 233_338)] + [TestCase(ScoringMode.Standardised, HitResult.Meh, 83_398)] + [TestCase(ScoringMode.Standardised, HitResult.Ok, 168_724)] [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, 8_343)] + [TestCase(ScoringMode.Classic, HitResult.Ok, 16_878)] + [TestCase(ScoringMode.Classic, HitResult.Great, 100_033)] public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore) { scoreProcessor.ApplyBeatmap(beatmap); @@ -72,29 +77,31 @@ namespace osu.Game.Tests.Rulesets.Scoring /// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and 50% max combo. /// [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.Great, HitResult.Great, 492_894)] - [TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 492_894)] + [TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 34_734)] + [TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 69_925)] + [TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 154_499)] + [TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 326_963)] + [TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 326_963)] [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] - [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 541_894)] + [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 493_652)] [TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] - [TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 492_894)] + [TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 326_963)] + [TestCase(ScoringMode.Standardised, HitResult.SliderTailHit, HitResult.SliderTailHit, 326_963)] [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, 3_492)] + [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 7_029)] + [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 15_530)] + [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 32_867)] + [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 32_867)] [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] - [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 11)] + [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 49_365)] [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, 32_696)] + [TestCase(ScoringMode.Classic, HitResult.SliderTailHit, HitResult.SliderTailHit, 32_696)] + [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 +114,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 }; @@ -117,6 +124,35 @@ namespace osu.Game.Tests.Rulesets.Scoring Assert.That(scoreProcessor.GetDisplayScore(scoringMode), Is.EqualTo(expectedScore).Within(0.5d)); } + [TestCase(typeof(OsuRuleset))] + [TestCase(typeof(TaikoRuleset))] + [TestCase(typeof(CatchRuleset))] + [TestCase(typeof(ManiaRuleset))] + public void TestBeatmapWithALotOfObjectsDoesNotOverflowClassicScore(Type rulesetType) + { + const int object_count = 999999; + + var ruleset = (Ruleset)Activator.CreateInstance(rulesetType)!; + scoreProcessor = new ScoreProcessor(ruleset); + + var largeBeatmap = new TestBeatmap(ruleset.RulesetInfo) + { + HitObjects = new List(Enumerable.Repeat(new TestHitObject(HitResult.Great), object_count)) + }; + scoreProcessor.ApplyBeatmap(largeBeatmap); + + for (int i = 0; i < object_count; ++i) + { + var judgementResult = new JudgementResult(largeBeatmap.HitObjects[i], largeBeatmap.HitObjects[i].CreateJudgement()) + { + Type = HitResult.Great + }; + scoreProcessor.ApplyResult(judgementResult); + } + + Assert.That(scoreProcessor.GetDisplayScore(ScoringMode.Classic), Is.GreaterThan(0)); + } + [Test] public void TestEmptyBeatmap( [Values(ScoringMode.Standardised, ScoringMode.Classic)] @@ -135,6 +171,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.Perfect, HitResult.Miss)] [TestCase(HitResult.SmallTickHit, HitResult.SmallTickMiss)] [TestCase(HitResult.LargeTickHit, HitResult.LargeTickMiss)] + [TestCase(HitResult.SliderTailHit, HitResult.LargeTickMiss)] [TestCase(HitResult.SmallBonus, HitResult.IgnoreMiss)] [TestCase(HitResult.LargeBonus, HitResult.IgnoreMiss)] public void TestMinResults(HitResult hitResult, HitResult expectedMinResult) @@ -155,6 +192,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.SmallTickHit, false)] [TestCase(HitResult.LargeTickMiss, true)] [TestCase(HitResult.LargeTickHit, true)] + [TestCase(HitResult.SliderTailHit, true)] [TestCase(HitResult.SmallBonus, false)] [TestCase(HitResult.LargeBonus, false)] public void TestAffectsCombo(HitResult hitResult, bool expectedReturnValue) @@ -175,6 +213,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.SmallTickHit, true)] [TestCase(HitResult.LargeTickMiss, true)] [TestCase(HitResult.LargeTickHit, true)] + [TestCase(HitResult.SliderTailHit, true)] [TestCase(HitResult.SmallBonus, false)] [TestCase(HitResult.LargeBonus, false)] public void TestAffectsAccuracy(HitResult hitResult, bool expectedReturnValue) @@ -195,6 +234,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.SmallTickHit, false)] [TestCase(HitResult.LargeTickMiss, false)] [TestCase(HitResult.LargeTickHit, false)] + [TestCase(HitResult.SliderTailHit, false)] [TestCase(HitResult.SmallBonus, true)] [TestCase(HitResult.LargeBonus, true)] public void TestIsBonus(HitResult hitResult, bool expectedReturnValue) @@ -215,6 +255,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.SmallTickHit, true)] [TestCase(HitResult.LargeTickMiss, false)] [TestCase(HitResult.LargeTickHit, true)] + [TestCase(HitResult.SliderTailHit, true)] [TestCase(HitResult.SmallBonus, true)] [TestCase(HitResult.LargeBonus, true)] public void TestIsHit(HitResult hitResult, bool expectedReturnValue) @@ -235,6 +276,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.SmallTickHit, true)] [TestCase(HitResult.LargeTickMiss, true)] [TestCase(HitResult.LargeTickHit, true)] + [TestCase(HitResult.SliderTailHit, true)] [TestCase(HitResult.SmallBonus, true)] [TestCase(HitResult.LargeBonus, true)] public void TestIsScorable(HitResult hitResult, bool expectedReturnValue) @@ -259,6 +301,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 +352,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 }); @@ -289,28 +366,92 @@ namespace osu.Game.Tests.Rulesets.Scoring Assert.That(actual, Is.EqualTo(expected).Within(Precision.FLOAT_EPSILON)); } + [TestCase(HitResult.Great)] + [TestCase(HitResult.LargeTickHit)] + public void TestAccuracyUpdateFromIgnoreMiss(HitResult maxResult) + { + scoreProcessor.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new TestHitObject(maxResult, HitResult.IgnoreMiss) + } + }); + + var judgementResult = new JudgementResult(beatmap.HitObjects.Single(), new TestJudgement(maxResult, HitResult.IgnoreMiss)) + { + Type = HitResult.IgnoreMiss + }; + scoreProcessor.ApplyResult(judgementResult); + + Assert.That(scoreProcessor.Accuracy.Value, Is.Not.EqualTo(1)); + } + + [Test] + public void TestNormalGrades() + { + scoreProcessor.ApplyBeatmap(new Beatmap()); + + Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.X)); + + scoreProcessor.Accuracy.Value = 0.99f; + Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.S)); + } + + [Test] + public void TestSilverGrades() + { + scoreProcessor.ApplyBeatmap(new Beatmap()); + Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.X)); + + scoreProcessor.Mods.Value = new[] { new OsuModHidden() }; + Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.XH)); + + scoreProcessor.Accuracy.Value = 0.99f; + Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.SH)); + } + + [Test] + public void TestSilverGradesModsAppliedFirst() + { + scoreProcessor.Mods.Value = new[] { new OsuModHidden() }; + scoreProcessor.ApplyBeatmap(new Beatmap()); + + Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.XH)); + + scoreProcessor.Accuracy.Value = 0.99f; + Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.SH)); + } + private class TestJudgement : Judgement { 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..ebbc329b9d 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() { @@ -83,6 +196,7 @@ namespace osu.Game.Tests.Scores.IO User = new APIUser { Username = "Test user" }, BeatmapInfo = beatmap.Beatmaps.First(), Ruleset = new OsuRuleset().RulesetInfo, + ClientVersion = "12345", Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, }; @@ -90,6 +204,7 @@ namespace osu.Game.Tests.Scores.IO Assert.IsTrue(imported.Mods.Any(m => m is OsuModHardRock)); Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime)); + Assert.That(imported.ClientVersion, Is.EqualTo(toImport.ClientVersion)); } finally { diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index 1c1ebe271e..606a5afac2 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -13,6 +13,7 @@ using osu.Game.Database; using osu.Game.Extensions; using osu.Game.IO; using osu.Game.Skinning; +using osu.Game.Tests.Resources; using SharpCompress.Archives.Zip; namespace osu.Game.Tests.Skins.IO @@ -21,13 +22,32 @@ namespace osu.Game.Tests.Skins.IO { #region Testing filename metadata inclusion + [TestCase("Archives/modified-classic-20220723.osk")] + [TestCase("Archives/modified-default-20230117.osk")] + [TestCase("Archives/modified-argon-20231106.osk")] + public Task TestImportModifiedSkinHasResources(string archive) => runSkinTest(async osu => + { + using (var stream = TestResources.OpenResource(archive)) + { + var imported = await loadSkinIntoOsu(osu, new ImportTask(stream, "skin.osk")); + + // When the import filename doesn't match, it should be appended (and update the skin.ini). + + var skinManager = osu.Dependencies.Get(); + + skinManager.CurrentSkinInfo.Value = imported; + + Assert.That(skinManager.CurrentSkin.Value.LayoutInfos.Count, Is.EqualTo(2)); + } + }); + [Test] public Task TestSingleImportDifferentFilename() => runSkinTest(async osu => { 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] @@ -36,7 +56,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] @@ -45,7 +65,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] @@ -54,7 +74,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] @@ -63,7 +83,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] @@ -72,7 +92,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 @@ -102,7 +122,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")); @@ -115,14 +135,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); }); @@ -133,14 +153,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] @@ -150,7 +170,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 @@ -183,8 +203,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] @@ -194,8 +214,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] @@ -264,7 +284,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 => { @@ -276,6 +296,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 b96bf09255..d577e0fedf 100644 --- a/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs +++ b/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs @@ -114,5 +114,25 @@ namespace osu.Game.Tests.Skins Assert.That(configs[0].MinimumColumnWidth, Is.EqualTo(16)); } } + + [Test] + public void TestParseArrayWithSomeEmptyElements() + { + var decoder = new LegacyManiaSkinDecoder(); + + using (var resStream = TestResources.OpenResource("mania-skin-broken-array.ini")) + using (var stream = new LineBufferedReader(resStream)) + { + var configs = decoder.Decode(stream); + + Assert.That(configs.Count, Is.EqualTo(1)); + Assert.That(configs[0].ColumnLineWidth.Length, Is.EqualTo(5)); + Assert.That(configs[0].ColumnLineWidth[0], Is.EqualTo(3)); + Assert.That(configs[0].ColumnLineWidth[1], Is.EqualTo(0)); // malformed entry, should be parsed as zero + Assert.That(configs[0].ColumnLineWidth[2], Is.EqualTo(3)); + Assert.That(configs[0].ColumnLineWidth[3], Is.EqualTo(3)); + Assert.That(configs[0].ColumnLineWidth[4], Is.EqualTo(3)); + } + } } } diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 82d204f134..6423e061c5 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -15,6 +15,7 @@ using osu.Game.IO.Archives; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Skinning; +using osu.Game.Skinning.Components; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Skins @@ -52,7 +53,13 @@ namespace osu.Game.Tests.Skins // Covers player avatar and flag. "Archives/modified-argon-20230305.osk", // Covers key counters - "Archives/modified-argon-pro-20230618.osk" + "Archives/modified-argon-pro-20230618.osk", + // Covers "Argon" health display + "Archives/modified-argon-pro-20231001.osk", + // Covers player name text component. + "Archives/modified-argon-20231106.osk", + // Covers "Argon" accuracy/score/combo counters, and wedges + "Archives/modified-argon-20231108.osk", }; /// @@ -96,6 +103,20 @@ namespace osu.Game.Tests.Skins } } + [Test] + public void TestDeserialiseModifiedArgon() + { + using (var stream = TestResources.OpenResource("Archives/modified-argon-20231106.osk")) + using (var storage = new ZipArchiveReader(stream)) + { + var skin = new TestSkin(new SkinInfo(), null, storage); + + Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2)); + Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(10)); + Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(PlayerName))); + } + } + [Test] public void TestDeserialiseModifiedClassic() { @@ -128,8 +149,8 @@ namespace osu.Game.Tests.Skins private class TestSkin : Skin { - public TestSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? storage = null, string configurationFilename = "skin.ini") - : base(skin, resources, storage, configurationFilename) + public TestSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? fallbackStore = null, string configurationFilename = "skin.ini") + : base(skin, resources, fallbackStore, configurationFilename) { } diff --git a/osu.Game.Tests/Skins/TestSceneSkinResources.cs b/osu.Game.Tests/Skins/TestSceneSkinResources.cs index aaec319b57..e77affd817 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinResources.cs @@ -95,8 +95,8 @@ namespace osu.Game.Tests.Skins { public const string SAMPLE_NAME = "test-sample"; - public TestSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? storage = null, string configurationFilename = "skin.ini") - : base(skin, resources, storage, configurationFilename) + public TestSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? fallbackStore = null, string configurationFilename = "skin.ini") + : base(skin, resources, fallbackStore, configurationFilename) { } diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs index 1523ae7027..37f2ee0b3f 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs @@ -181,6 +181,54 @@ namespace osu.Game.Tests.Visual.Background AddStep("restore default beatmap", () => Beatmap.SetDefault()); } + [Test] + public void TestBeatmapBackgroundWithStoryboardUnloadedOnSuspension() + { + BackgroundScreenBeatmap nestedScreen = null; + + setSupporter(true); + setSourceMode(BackgroundSource.BeatmapWithStoryboard); + + AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithStoryboard()); + AddAssert("background changed", () => screen.CheckLastLoadChange() == true); + AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackgroundWithStoryboard)); + + AddUntilStep("storyboard present", () => screen.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); + + AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value))); + AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen()); + + AddUntilStep("storyboard unloaded", () => !screen.ChildrenOfType().Any()); + + AddStep("go back", () => screen.MakeCurrent()); + + AddUntilStep("storyboard reloaded", () => screen.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); + } + + [Test] + public void TestBeatmapBackgroundWithStoryboardButBeatmapHasNone() + { + BackgroundScreenBeatmap nestedScreen = null; + + setSupporter(true); + setSourceMode(BackgroundSource.BeatmapWithStoryboard); + + AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground()); + AddAssert("background changed", () => screen.CheckLastLoadChange() == true); + AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackgroundWithStoryboard)); + + AddUntilStep("no storyboard loaded", () => !screen.ChildrenOfType().Any()); + + AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value))); + AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen()); + + AddUntilStep("still no storyboard", () => !screen.ChildrenOfType().Any()); + + AddStep("go back", () => screen.MakeCurrent()); + + AddUntilStep("still no storyboard", () => !screen.ChildrenOfType().Any()); + } + [Test] public void TestBackgroundTypeSwitch() { diff --git a/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs b/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs index 378dd99664..dd4c372193 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs @@ -29,7 +29,8 @@ namespace osu.Game.Tests.Visual.Background ColourDark = Color4.Gray, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(0.9f) + Size = new Vector2(0.9f), + ClampAxes = Axes.None } }; } @@ -40,7 +41,10 @@ namespace osu.Game.Tests.Visual.Background AddSliderStep("Triangle scale", 0f, 10f, 1f, s => triangles.TriangleScale = s); AddSliderStep("Seed", 0, 1000, 0, s => triangles.Reset(s)); - AddToggleStep("Masking", m => triangles.Masking = m); + AddStep("ClampAxes X", () => triangles.ClampAxes = Axes.X); + AddStep("ClampAxes Y", () => triangles.ClampAxes = Axes.Y); + AddStep("ClampAxes Both", () => triangles.ClampAxes = Axes.Both); + AddStep("ClampAxes None", () => triangles.ClampAxes = Axes.None); } } } diff --git a/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs b/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs index 01a2464b8e..4713852c0b 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs @@ -86,7 +86,8 @@ namespace osu.Game.Tests.Visual.Background { Anchor = Anchor.Centre, Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + ClampAxes = Axes.None } } }, @@ -128,7 +129,10 @@ namespace osu.Game.Tests.Visual.Background AddStep("White colour", () => box.Colour = triangles.Colour = maskedTriangles.Colour = Color4.White); AddStep("Vertical gradient", () => box.Colour = triangles.Colour = maskedTriangles.Colour = ColourInfo.GradientVertical(Color4.White, Color4.Red)); AddStep("Horizontal gradient", () => box.Colour = triangles.Colour = maskedTriangles.Colour = ColourInfo.GradientHorizontal(Color4.White, Color4.Red)); - AddToggleStep("Masking", m => maskedTriangles.Masking = m); + AddStep("ClampAxes X", () => maskedTriangles.ClampAxes = Axes.X); + AddStep("ClampAxes Y", () => maskedTriangles.ClampAxes = Axes.Y); + AddStep("ClampAxes Both", () => maskedTriangles.ClampAxes = Axes.Both); + AddStep("ClampAxes None", () => maskedTriangles.ClampAxes = Axes.None); } } } 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/TestSceneBeatmapSetOnlineStatusPill.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs index 88e47ea560..dcc4654437 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs @@ -47,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/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index cfa45ec6ef..747cf73baf 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -166,6 +166,29 @@ namespace osu.Game.Tests.Visual.Collections }))); } + [Test] + public void TestCollectionNameCollisionsWithBuiltInItems() + { + AddStep("add dropdown", () => + { + Add(new CollectionDropdown + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.X, + Width = 0.4f, + }); + }); + AddStep("add two collections which collide with default items", () => Realm.Write(r => r.Add(new[] + { + new BeatmapCollection(name: "All beatmaps"), + new BeatmapCollection(name: "Manage collections...") + { + BeatmapMD5Hashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash } + }, + }))); + } + [Test] public void TestRemoveCollectionViaButton() { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 7a0b3d0c1a..f6637d0e80 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,12 @@ namespace osu.Game.Tests.Visual.Editing { RelativeSizeAxes = Axes.Both, - CanRotate = true, CanScaleX = true, CanScaleY = true, + CanScaleDiagonally = true, CanFlipX = true, CanFlipY = true, - OnRotation = handleRotation, OnScale = handleScale } } @@ -71,11 +81,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/TestSceneDifficultySwitching.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs index 69070b0b64..76ed5063b0 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.Editing if (sameRuleset) { AddUntilStep("prompt for save dialog shown", () => DialogOverlay.CurrentDialog is PromptForSaveDialog); - AddStep("discard changes", () => ((PromptForSaveDialog)DialogOverlay.CurrentDialog).PerformOkAction()); + AddStep("discard changes", () => ((PromptForSaveDialog)DialogOverlay.CurrentDialog)?.PerformOkAction()); } // ensure editor loader didn't resume. 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..db87987815 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -18,12 +18,14 @@ 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; using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Edit.Setup; using osu.Game.Storyboards; using osu.Game.Tests.Resources; @@ -91,29 +93,13 @@ 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); + AddStep("enter compose mode", () => InputManager.Key(Key.F1)); + AddUntilStep("wait for timeline load", () => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual); AddAssert("switch track to real track", () => { var setup = Editor.ChildrenOfType().First(); @@ -143,7 +129,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 +186,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 +273,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 +346,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 +384,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 +419,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 +439,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/TestSceneEditorClipboard.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs index c4c05278b5..a766b253aa 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Editing ControlPoints = { new PathControlPoint(), - new PathControlPoint(new Vector2(100, 0), PathType.Bezier) + new PathControlPoint(new Vector2(100, 0), PathType.BEZIER) } } }; 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/TestSceneEditorMenuBar.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs index dbcf66f005..fe47f5885d 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs @@ -34,51 +34,51 @@ namespace osu.Game.Tests.Visual.Editing { new MenuItem("File") { - Items = new[] + Items = new OsuMenuItem[] { new EditorMenuItem("Clear All Notes"), new EditorMenuItem("Open Difficulty..."), new EditorMenuItem("Save"), new EditorMenuItem("Create a new Difficulty..."), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem("Revert to Saved"), new EditorMenuItem("Revert to Saved (Full)"), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem("Test Beatmap"), new EditorMenuItem("Open AiMod"), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem("Upload Beatmap..."), new EditorMenuItem("Export Package"), new EditorMenuItem("Export Map Package"), new EditorMenuItem("Import from..."), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem("Open Song Folder"), new EditorMenuItem("Open .osu in Notepad"), new EditorMenuItem("Open .osb in Notepad"), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem("Exit"), } }, new MenuItem("Timing") { - Items = new[] + Items = new OsuMenuItem[] { new EditorMenuItem("Time Signature"), new EditorMenuItem("Metronome Clicks"), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem("Add Timing Section"), new EditorMenuItem("Add Inheriting Section"), new EditorMenuItem("Reset Current Section"), new EditorMenuItem("Delete Timing Section"), new EditorMenuItem("Resnap Current Section"), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem("Timing Setup"), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem("Resnap All Notes", MenuItemType.Destructive), new EditorMenuItem("Move all notes in time...", MenuItemType.Destructive), new EditorMenuItem("Recalculate Slider Lengths", MenuItemType.Destructive), new EditorMenuItem("Delete All Timing Sections", MenuItemType.Destructive), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem("Set Current Position as Preview Point"), } }, 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 a38c481003..f392841ac7 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; @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Editing new Slider { Position = new Vector2(128, 256), - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(216, 0), @@ -178,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 c78f50c821..530b3db208 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectDifficultyPointAdjustments.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Editing new PathControlPoint(new Vector2(100, 0)) } }, - SliderVelocity = 2 + SliderVelocityMultiplier = 2 }); }); } @@ -110,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)); @@ -194,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/TestSceneOpenEditorTimestamp.cs b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs new file mode 100644 index 0000000000..1f46a08831 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs @@ -0,0 +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 System.Linq; +using NUnit.Framework; +using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Localisation; +using osu.Game.Online.Chat; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Menu; +using osu.Game.Screens.Select; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.Editing +{ + public partial class TestSceneOpenEditorTimestamp : OsuGameTestScene + { + private Editor? editor => Game.ScreenStack.CurrentScreen as Editor; + private EditorBeatmap editorBeatmap => editor.ChildrenOfType().Single(); + private EditorClock editorClock => editor.ChildrenOfType().Single(); + + [Test] + public void TestErrorNotifications() + { + RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo; + + addStepClickLink("00:00:000", waitForSeek: false); + AddUntilStep("received 'must be in edit'", + () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEditorToHandleLinks), + () => Is.EqualTo(1)); + + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); + AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); + + addStepClickLink("00:00:000 (1)", waitForSeek: false); + AddUntilStep("received 'must be in edit'", + () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEditorToHandleLinks), + () => Is.EqualTo(2)); + + setUpEditor(rulesetInfo); + AddAssert("ruleset is osu!", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); + + addStepClickLink("00:000", "invalid link", waitForSeek: false); + AddUntilStep("received 'failed to process'", + () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToParseEditorLink), + () => Is.EqualTo(1)); + + addStepClickLink("50000:00:000", "too long link", waitForSeek: false); + AddUntilStep("received 'failed to process'", + () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToParseEditorLink), + () => Is.EqualTo(2)); + } + + [Test] + public void TestHandleCurrentScreenChanges() + { + RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo; + + setUpEditor(rulesetInfo); + AddAssert("is osu! ruleset", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); + + addStepClickLink("100:00:000", "long link"); + AddUntilStep("moved to end of track", () => editorClock.CurrentTime, () => Is.EqualTo(editorClock.TrackLength)); + + addStepScreenModeTo(EditorScreenMode.SongSetup); + addStepClickLink("00:00:000"); + assertOnScreenAt(EditorScreenMode.SongSetup, 0); + + addStepClickLink("00:05:000 (0|0)"); + assertMovedScreenTo(EditorScreenMode.Compose); + + addStepScreenModeTo(EditorScreenMode.Design); + addStepClickLink("00:10:000"); + assertOnScreenAt(EditorScreenMode.Design, 10_000); + + addStepClickLink("00:15:000 (1)"); + assertMovedScreenTo(EditorScreenMode.Compose); + + addStepScreenModeTo(EditorScreenMode.Timing); + addStepClickLink("00:20:000"); + assertOnScreenAt(EditorScreenMode.Timing, 20_000); + + addStepClickLink("00:25:000 (0,1)"); + assertMovedScreenTo(EditorScreenMode.Compose); + + addStepScreenModeTo(EditorScreenMode.Verify); + addStepClickLink("00:30:000"); + assertOnScreenAt(EditorScreenMode.Verify, 30_000); + + addStepClickLink("00:35:000 (0,1)"); + assertMovedScreenTo(EditorScreenMode.Compose); + + addStepClickLink("00:00:000"); + assertOnScreenAt(EditorScreenMode.Compose, 0); + } + + private void addStepClickLink(string timestamp, string step = "", bool waitForSeek = true) + { + AddStep($"{step} {timestamp}", () => + Game.HandleLink(new LinkDetails(LinkAction.OpenEditorTimestamp, timestamp)) + ); + + if (waitForSeek) + AddUntilStep("wait for seek", () => editorClock.SeekingOrStopped.Value); + } + + private void addStepScreenModeTo(EditorScreenMode screenMode) => + AddStep("change screen to " + screenMode, () => editor!.Mode.Value = screenMode); + + private void assertOnScreenAt(EditorScreenMode screen, double time) + { + AddAssert($"stayed on {screen} at {time}", () => + editor!.Mode.Value == screen + && editorClock.CurrentTime == time + ); + } + + private void assertMovedScreenTo(EditorScreenMode screen, string text = "moved to") => + AddAssert($"{text} {screen}", () => editor!.Mode.Value == screen); + + private void setUpEditor(RulesetInfo ruleset) + { + 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 + ); + AddStep("Switch ruleset", () => Game.Ruleset.Value = ruleset); + AddStep("Open editor for ruleset", () => + ((PlaySongSelect)Game.ScreenStack.CurrentScreen) + .Edit(beatmapSet.Beatmaps.Last(beatmap => beatmap.Ruleset.Name == ruleset.Name)) + ); + AddUntilStep("Wait for editor open", () => editor?.ReadyForUse == true); + } + } +} 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/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs index 216c35de65..6181024230 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs @@ -4,11 +4,13 @@ #nullable disable using System; +using System.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; @@ -17,6 +19,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Timing.RowAttributes; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Editing @@ -69,6 +72,48 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("Wait for rows to load", () => Child.ChildrenOfType().Any()); } + [Test] + public void TestSelectedRetainedOverUndo() + { + AddStep("Select first timing point", () => + { + InputManager.MoveMouseTo(Child.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 2170); + AddUntilStep("Ensure seeked to correct time", () => EditorClock.CurrentTimeAccurate == 2170); + + AddStep("Adjust offset", () => + { + InputManager.MoveMouseTo(timingScreen.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre + new Vector2(20, 0)); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("wait for offset changed", () => + { + return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170; + }); + + AddStep("simulate undo", () => + { + var clone = editorBeatmap.ControlPointInfo.DeepClone(); + + editorBeatmap.ControlPointInfo.Clear(); + + foreach (var group in clone.Groups) + { + foreach (var cp in group.ControlPoints) + editorBeatmap.ControlPointInfo.Add(group.Time, cp); + } + }); + + AddUntilStep("selection retained", () => + { + return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170; + }); + } + [Test] public void TestTrackingCurrentTimeWhileRunning() { @@ -134,6 +179,43 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("Scrolled to end", () => timingScreen.ChildrenOfType().First().IsScrolledToEnd()); } + [Test] + public void TestEditThenClickAwayAppliesChanges() + { + AddStep("Add two control points", () => + { + editorBeatmap.ControlPointInfo.Clear(); + editorBeatmap.ControlPointInfo.Add(1000, new TimingControlPoint()); + editorBeatmap.ControlPointInfo.Add(2000, new TimingControlPoint()); + }); + + AddStep("Select second timing point", () => + { + InputManager.MoveMouseTo(Child.ChildrenOfType().Last()); + InputManager.Click(MouseButton.Left); + }); + + AddStep("Scroll to end", () => timingScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToEnd(false)); + AddStep("Modify time signature", () => + { + var timeSignatureTextBox = Child.ChildrenOfType().Single().ChildrenOfType().Single(); + InputManager.MoveMouseTo(timeSignatureTextBox); + InputManager.Click(MouseButton.Left); + + Debug.Assert(!timeSignatureTextBox.Current.Value.Equals("1", StringComparison.Ordinal)); + timeSignatureTextBox.Current.Value = "1"; + }); + + AddStep("Select first timing point", () => + { + InputManager.MoveMouseTo(Child.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("Second timing point changed time signature", () => editorBeatmap.ControlPointInfo.TimingPoints.Last().TimeSignature.Numerator == 1); + AddAssert("First timing point preserved time signature", () => editorBeatmap.ControlPointInfo.TimingPoints.First().TimeSignature.Numerator == 4); + } + protected override void Dispose(bool isDisposing) { Beatmap.Disabled = false; 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/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..5d2921107e --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs @@ -0,0 +1,174 @@ +// 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.Framework.Threading; +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!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddSliderStep("Height", 0, 64, 0, val => + { + if (healthDisplay.IsNotNull()) + healthDisplay.BarHeight.Value = val; + }); + + AddSliderStep("Width", 0, 1f, 0.98f, val => + { + if (healthDisplay.IsNotNull()) + healthDisplay.Width = val; + }); + } + + [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, + }, + }; + }); + } + + [Test] + public void TestHealthDisplayIncrementing() + { + AddRepeatStep("apply miss judgement", applyMiss, 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; + applyPerfectHit(); + }, 3); + } + + [Test] + public void TestLateMissAfterConsequentMisses() + { + AddUntilStep("wait for health", () => healthDisplay.Current.Value == 1); + AddStep("apply sequence", () => + { + for (int i = 0; i < 10; i++) + applyMiss(); + + Scheduler.AddDelayed(applyMiss, 500 + 30); + }); + } + + [Test] + public void TestMissAlmostExactlyAfterLastMissAnimation() + { + AddUntilStep("wait for health", () => healthDisplay.Current.Value == 1); + AddStep("apply sequence", () => + { + const double interval = 500 + 15; + + for (int i = 0; i < 5; i++) + { + if (i % 2 == 0) + Scheduler.AddDelayed(applyMiss, i * interval); + else + { + Scheduler.AddDelayed(applyMiss, i * interval); + Scheduler.AddDelayed(applyMiss, i * interval); + } + } + }); + } + + [Test] + public void TestMissThenHitAtSameUpdateFrame() + { + AddUntilStep("wait for health", () => healthDisplay.Current.Value == 1); + AddStep("set half health", () => healthProcessor.Health.Value = 0.5f); + + AddStep("apply miss and hit", () => + { + applyMiss(); + applyMiss(); + applyPerfectHit(); + applyPerfectHit(); + }); + + AddWaitStep("wait", 3); + + AddStep("apply miss and cancel with hit", () => + { + applyMiss(); + applyPerfectHit(); + applyPerfectHit(); + applyPerfectHit(); + applyPerfectHit(); + }); + } + + private void applyMiss() + { + healthProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }); + } + + private void applyPerfectHit() + { + healthProcessor.ApplyResult(new JudgementResult(new HitCircle(), new OsuJudgement()) + { + Type = HitResult.Perfect + }); + } + + [Test] + public void TestSimulateDrain() + { + ScheduledDelegate del = null!; + + AddStep("simulate drain", () => del = Scheduler.AddDelayed(() => healthProcessor.Health.Value -= 0.00025f * Time.Elapsed, 0, true)); + AddUntilStep("wait until zero", () => healthProcessor.Health.Value == 0); + AddStep("cancel drain", () => del.Cancel()); + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index f3701b664c..83fc5c2013 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; using osu.Game.Screens.Play.PlayerSettings; +using osu.Game.Tests.Resources; using osu.Game.Tests.Visual.Ranking; namespace osu.Game.Tests.Visual.Gameplay @@ -44,7 +45,23 @@ namespace osu.Game.Tests.Visual.Gameplay { offsetControl.ReferenceScore.Value = new ScoreInfo { - HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(0, 2) + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(0, 2), + BeatmapInfo = Beatmap.Value.BeatmapInfo, + }; + }); + + AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); + } + + [Test] + public void TestScoreFromDifferentBeatmap() + { + AddStep("Set short reference score", () => + { + offsetControl.ReferenceScore.Value = new ScoreInfo + { + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(10), + BeatmapInfo = TestResources.CreateTestBeatmapSetInfo().Beatmaps.First(), }; }); @@ -59,7 +76,8 @@ namespace osu.Game.Tests.Visual.Gameplay offsetControl.ReferenceScore.Value = new ScoreInfo { HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(10), - Mods = new Mod[] { new OsuModRelax() } + Mods = new Mod[] { new OsuModRelax() }, + BeatmapInfo = Beatmap.Value.BeatmapInfo, }; }); @@ -77,7 +95,8 @@ namespace osu.Game.Tests.Visual.Gameplay { offsetControl.ReferenceScore.Value = new ScoreInfo { - HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error) + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error), + BeatmapInfo = Beatmap.Value.BeatmapInfo, }; }); @@ -105,7 +124,8 @@ namespace osu.Game.Tests.Visual.Gameplay { offsetControl.ReferenceScore.Value = new ScoreInfo { - HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error) + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error), + BeatmapInfo = Beatmap.Value.BeatmapInfo, }; }); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index 18a180589b..a2ce62105e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -69,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. diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs index a40eab5948..27497f77be 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs @@ -114,23 +114,25 @@ namespace osu.Game.Tests.Visual.Gameplay { } - [TestCase(PathType.Linear)] - [TestCase(PathType.Bezier)] - [TestCase(PathType.Catmull)] - [TestCase(PathType.PerfectCurve)] - public void TestSingleSegment(PathType type) - => AddStep("create path", () => path.ControlPoints.AddRange(createSegment(type, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + [TestCase(SplineType.Linear, null)] + [TestCase(SplineType.BSpline, null)] + [TestCase(SplineType.BSpline, 3)] + [TestCase(SplineType.Catmull, null)] + [TestCase(SplineType.PerfectCurve, null)] + public void TestSingleSegment(SplineType splineType, int? degree) + => AddStep("create path", () => path.ControlPoints.AddRange(createSegment(new PathType { Type = splineType, Degree = degree }, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); - [TestCase(PathType.Linear)] - [TestCase(PathType.Bezier)] - [TestCase(PathType.Catmull)] - [TestCase(PathType.PerfectCurve)] - public void TestMultipleSegment(PathType type) + [TestCase(SplineType.Linear, null)] + [TestCase(SplineType.BSpline, null)] + [TestCase(SplineType.BSpline, 3)] + [TestCase(SplineType.Catmull, null)] + [TestCase(SplineType.PerfectCurve, null)] + public void TestMultipleSegment(SplineType splineType, int? degree) { AddStep("create path", () => { - path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero)); - path.ControlPoints.AddRange(createSegment(type, new Vector2(0, 100), new Vector2(100), Vector2.Zero)); + path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero)); + path.ControlPoints.AddRange(createSegment(new PathType { Type = splineType, Degree = degree }, new Vector2(0, 100), new Vector2(100), Vector2.Zero)); }); } @@ -139,9 +141,9 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("create path", () => { - path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(100, 0))); - path.ControlPoints.AddRange(createSegment(PathType.Bezier, new Vector2(100, 0), new Vector2(150, 30), new Vector2(100, 100))); - path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, new Vector2(100, 100), new Vector2(25, 50), Vector2.Zero)); + path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(100, 0))); + path.ControlPoints.AddRange(createSegment(PathType.BEZIER, new Vector2(100, 0), new Vector2(150, 30), new Vector2(100, 100))); + path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, new Vector2(100, 100), new Vector2(25, 50), Vector2.Zero)); }); } @@ -157,7 +159,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("create path", () => { - path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(width / 2, height), new Vector2(width, 0))); + path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(width / 2, height), new Vector2(width, 0))); }); } @@ -170,11 +172,11 @@ namespace osu.Game.Tests.Visual.Gameplay switch (points) { case 2: - path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100))); + path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(0, 100))); break; case 4: - path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); + path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); break; } }); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index 287b7d43b4..e4d39bb6de 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -198,7 +198,7 @@ namespace osu.Game.Tests.Visual.Gameplay { var beatmap = createBeatmap(); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range }); - beatmap.Difficulty.SliderMultiplier = 2; + beatmap.BeatmapInfo.Difficulty.SliderMultiplier = 2; createTest(beatmap); AddStep("adjust time range", () => drawableRuleset.TimeRange.Value = 2000); @@ -237,7 +237,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); private void assertPosition(int index, float relativeY) => AddAssert($"hitobject {index} at {relativeY}", - () => Precision.AlmostEquals(getDrawableHitObject(index)?.DrawPosition.Y ?? -1, yScale * relativeY)); + () => getDrawableHitObject(index)?.DrawPosition.Y / yScale ?? -1, () => Is.EqualTo(relativeY).Within(Precision.FLOAT_EPSILON)); private void setTime(double time) { @@ -251,7 +251,17 @@ namespace osu.Game.Tests.Visual.Gameplay /// The . private IBeatmap createBeatmap(Func createAction = null) { - var beatmap = new Beatmap { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } }; + var beatmap = new Beatmap + { + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty + { + SliderMultiplier = 1 + }, + Ruleset = new OsuRuleset().RulesetInfo + } + }; for (int i = 0; i < 10; i++) { @@ -311,14 +321,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/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs index 235ada2d63..684d263a58 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs @@ -22,6 +22,8 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly Bindable showHealth = new Bindable(); + private HealthProcessor healthProcessor; + [Resolved] private OsuConfigManager config { get; set; } @@ -29,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("create layer", () => { - Child = new HealthProcessorContainer(healthProcessor) + Child = new HealthProcessorContainer(this.healthProcessor = healthProcessor) { RelativeSizeAxes = Axes.Both, Child = layer = new FailingLayer() @@ -50,12 +52,12 @@ namespace osu.Game.Tests.Visual.Gameplay AddSliderStep("current health", 0.0, 1.0, 1.0, val => { if (layer != null) - layer.Current.Value = val; + healthProcessor.Health.Value = val; }); - AddStep("set health to 0.10", () => layer.Current.Value = 0.1); + AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1); AddUntilStep("layer fade is visible", () => layer.ChildrenOfType().First().Alpha > 0.1f); - AddStep("set health to 1", () => layer.Current.Value = 1f); + AddStep("set health to 1", () => healthProcessor.Health.Value = 1f); AddUntilStep("layer fade is invisible", () => !layer.ChildrenOfType().First().IsPresent); } @@ -65,7 +67,7 @@ namespace osu.Game.Tests.Visual.Gameplay create(new DrainingHealthProcessor(0)); AddUntilStep("layer is visible", () => layer.IsPresent); AddStep("disable layer", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false)); - AddStep("set health to 0.10", () => layer.Current.Value = 0.1); + AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1); AddUntilStep("layer is not visible", () => !layer.IsPresent); } @@ -74,7 +76,7 @@ namespace osu.Game.Tests.Visual.Gameplay { create(new AccumulatingHealthProcessor(1)); AddUntilStep("layer is not visible", () => !layer.IsPresent); - AddStep("set health to 0.10", () => layer.Current.Value = 0.1); + AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1); AddUntilStep("layer is not visible", () => !layer.IsPresent); } @@ -82,7 +84,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestLayerVisibilityWithDrainingProcessor() { create(new DrainingHealthProcessor(0)); - AddStep("set health to 0.10", () => layer.Current.Value = 0.1); + AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1); AddWaitStep("wait for potential fade", 10); AddAssert("layer is still visible", () => layer.IsPresent); } @@ -92,7 +94,7 @@ namespace osu.Game.Tests.Visual.Gameplay { create(new DrainingHealthProcessor(0)); - AddStep("set health to 0.10", () => layer.Current.Value = 0.1); + AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1); AddStep("don't show health", () => showHealth.Value = false); AddStep("disable FadePlayfieldWhenHealthLow", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false)); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs index 534348bed3..98a97e1d23 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs @@ -129,10 +129,8 @@ namespace osu.Game.Tests.Visual.Gameplay checkRate(1); } - private const int max_frames_catchup = 50; - private void createStabilityContainer(double gameplayStartTime = double.MinValue) => AddStep("create container", () => - mainContainer.Child = new FrameStabilityContainer(gameplayStartTime) { MaxCatchUpFrames = max_frames_catchup } + mainContainer.Child = new FrameStabilityContainer(gameplayStartTime) .WithChild(consumer = new ClockConsumingChild())); private void seekManualTo(double time) => AddStep($"seek manual clock to {time}", () => manualClock.CurrentTime = time); 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/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index 0f16d3f394..3cbd5eefac 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -88,7 +88,7 @@ namespace osu.Game.Tests.Visual.Gameplay { HitWindows = new HitWindows(), StartTime = t += spacing, - Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }), + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, Vector2.UnitY * 200 }), Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT) }, }, }); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index aa89342926..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; @@ -41,7 +41,7 @@ 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.ChildrenOfType().First(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs index f117569657..894f08e5b2 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; @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Gameplay }; }); - protected override Ruleset CreateRuleset() => new ManiaRuleset(); + protected override Ruleset CreateRuleset() => new OsuRuleset(); private void applyOneJudgement(HitResult result) { @@ -147,6 +147,16 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("Assert max judgement hidden", () => counterDisplay.CounterFlow.ChildrenOfType().First().Alpha == 0); } + [Test] + public void TestNoDuplicates() + { + AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay()); + AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All); + AddAssert("Check no duplicates", + () => counterDisplay.CounterFlow.ChildrenOfType().Count(), + () => Is.EqualTo(counterDisplay.CounterFlow.ChildrenOfType().Select(c => c.ResultName.Text).Distinct().Count())); + } + [Test] public void TestCycleDisplayModes() { @@ -163,7 +173,7 @@ namespace osu.Game.Tests.Visual.Gameplay private int hiddenCount() { - var num = counterDisplay.CounterFlow.Children.First(child => child.Result.Type == HitResult.LargeTickHit); + var num = counterDisplay.CounterFlow.Children.First(child => child.Result.Types.Contains(HitResult.LargeTickHit)); return num.Result.ResultCount.Value; } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 5a66a5c7a6..2d2b6c3bed 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -31,19 +31,66 @@ namespace osu.Game.Tests.Visual.Gameplay Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Spacing = new Vector2(72.7f), - Children = new KeyCounterDisplay[] + Spacing = new Vector2(20), + Children = new Drawable[] { new DefaultKeyCounterDisplay { Origin = Anchor.Centre, Anchor = Anchor.Centre, }, + new DefaultKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Scale = new Vector2(1, -1) + }, + new ArgonKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + }, new ArgonKeyCounterDisplay { Origin = Anchor.Centre, Anchor = Anchor.Centre, - } + Scale = new Vector2(1, -1) + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Spacing = new Vector2(20), + Children = new Drawable[] + { + new DefaultKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Rotation = -90, + }, + new DefaultKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Rotation = 90, + }, + new ArgonKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Rotation = -90, + }, + new ArgonKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Rotation = 90, + }, + } + }, } } }; @@ -77,8 +124,15 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Disable counting", () => controller.IsCounting.Value = false); addPressKeyStep(); AddAssert($"Check {testKey} count has not changed", () => testTrigger.ActivationCount.Value == 2); + AddStep("Enable counting", () => controller.IsCounting.Value = true); + addPressKeyStep(100); + addPressKeyStep(1000); - void addPressKeyStep() => AddStep($"Press {testKey} key", () => InputManager.Key(testKey)); + void addPressKeyStep(int repeat = 1) => AddStep($"Press {testKey} key {repeat} times", () => + { + for (int i = 0; i < repeat; i++) + InputManager.Key(testKey); + }); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index b072ce191e..73aa3be73d 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,21 @@ 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 >= lastStopTime; alwaysGoingForward &= goingForward; if (!goingForward) - Logger.Log($"Backwards time occurred ({currentTime:N1} -> {lastTime:N1})"); - - lastTime = currentTime; + Logger.Log($"Went too far backwards (last stop: {lastStopTime:N1} current: {currentTime:N1})"); }; }); @@ -121,7 +122,7 @@ namespace osu.Game.Tests.Visual.Gameplay resumeAndConfirm(); - AddAssert("Resumed without seeking forward", () => Player.LastResumeTime, () => Is.LessThanOrEqualTo(Player.LastPauseTime)); + AddAssert("continued playing forward", () => Player.LastResumeTime, () => Is.GreaterThanOrEqualTo(Player.LastPauseTime)); AddUntilStep("player playing", () => Player.LocalUserPlaying.Value); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index dbd1ce1f6e..f97372e9b6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -13,7 +13,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Localisation; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Utils; @@ -487,13 +486,8 @@ namespace osu.Game.Tests.Visual.Gameplay } } - private class TestMod : Mod, IApplicableToScoreProcessor + private class TestMod : OsuModDoubleTime, IApplicableToScoreProcessor { - public override string Name => string.Empty; - public override string Acronym => string.Empty; - public override double ScoreMultiplier => 1; - public override LocalisableString Description => string.Empty; - public bool Applied { get; private set; } public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs index feda251744..fafd1330cc 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; @@ -25,6 +29,7 @@ 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 @@ -36,6 +41,9 @@ namespace osu.Game.Tests.Visual.Gameplay private BeatmapSetInfo? importedSet; + [Resolved] + private OsuGameBase osu { get; set; } = null!; + [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { @@ -97,6 +105,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(); @@ -147,6 +156,41 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen); AddUntilStep("score in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) != null)); + AddUntilStep("score has correct version", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID)!.ClientVersion), () => Is.EqualTo(osu.Version)); + } + + [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] @@ -173,11 +217,19 @@ namespace osu.Game.Tests.Visual.Gameplay string? filePath = null; // Files starting with _ are temporary, created by CreateFileSafely call. - AddUntilStep("wait for export file", () => filePath = LocalStorage.GetFiles("exports").SingleOrDefault(f => !f.StartsWith("_", StringComparison.Ordinal)), () => Is.Not.Null); - AddAssert("filesize is non-zero", () => + 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", () => { - using (var stream = LocalStorage.GetStream(filePath)) - return stream.Length; + try + { + using (var stream = LocalStorage.GetStream(filePath)) + return stream.Length; + } + catch (IOException) + { + // file move may still be in progress. + return 0; + } }, () => Is.Not.Zero); } 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/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index 1a7ea20cc0..5e22e47572 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -15,12 +15,15 @@ using osu.Game.Online.Rooms; using osu.Game.Online.Solo; using osu.Game.Rulesets; using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko; +using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Scoring; +using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Tests.Beatmaps; @@ -34,12 +37,19 @@ namespace osu.Game.Tests.Visual.Gameplay private Func createCustomBeatmap; private Func createCustomRuleset; + private Func createCustomMods; private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; protected override bool HasCustomSteps => true; - protected override TestPlayer CreatePlayer(Ruleset ruleset) => new FakeImportingPlayer(false); + protected override TestPlayer CreatePlayer(Ruleset ruleset) + { + if (createCustomMods != null) + SelectedMods.Value = SelectedMods.Value.Concat(createCustomMods()).ToList(); + + return new FakeImportingPlayer(false); + } protected new FakeImportingPlayer Player => (FakeImportingPlayer)base.Player; @@ -179,7 +189,6 @@ namespace osu.Game.Tests.Visual.Gameplay addFakeHit(); AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); - AddStep("exit", () => Player.Exit()); AddUntilStep("wait for submission", () => Player.SubmittedScore != null); AddAssert("ensure failing submission", () => Player.SubmittedScore.ScoreInfo.Passed == false); @@ -278,13 +287,28 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("ensure no submission", () => Player.SubmittedScore == null); } - private void createPlayerTest(bool allowFail = false, Func createBeatmap = null, Func createRuleset = null) + [Test] + public void TestNoSubmissionWithModsOfDifferentRuleset() + { + prepareTestAPI(true); + + createPlayerTest(createRuleset: () => new OsuRuleset(), createMods: () => new Mod[] { new TaikoModHidden() }); + + AddUntilStep("wait for token request", () => Player.TokenCreationRequested); + AddAssert("gameplay not loaded", () => Player.DrawableRuleset == null); + + AddStep("exit", () => Player.Exit()); + AddAssert("ensure no submission", () => Player.SubmittedScore == null); + } + + private void createPlayerTest(bool allowFail = false, Func createBeatmap = null, Func createRuleset = null, Func createMods = null) { CreateTest(() => AddStep("set up requirements", () => { this.allowFail = allowFail; createCustomBeatmap = createBeatmap; createCustomRuleset = createRuleset; + createCustomMods = createMods; })); } @@ -360,6 +384,11 @@ namespace osu.Game.Tests.Visual.Gameplay AllowImportCompletion = new SemaphoreSlim(1); } + protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart) + { + ShouldValidatePlaybackRate = false, + }; + protected override async Task ImportScore(Score score) { ScoreImportStarted = true; 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 c722d67ac9..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.Scoring; -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 OsuScoreProcessor(), ScoringMode.Standardised); - runForProcessor("lazer-classic", Color4.MediumPurple, new OsuScoreProcessor(), 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 4e5db5d46e..3c97700fb0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; @@ -96,7 +97,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Begin drag top left", () => { - InputManager.MoveMouseTo(box1.ScreenSpaceDrawQuad.TopLeft - new Vector2(box1.ScreenSpaceDrawQuad.Width / 4)); + InputManager.MoveMouseTo(box1.ScreenSpaceDrawQuad.TopLeft - new Vector2(box1.ScreenSpaceDrawQuad.Width / 4, box1.ScreenSpaceDrawQuad.Height / 8)); InputManager.PressButton(MouseButton.Left); }); @@ -146,8 +147,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("Add big black box", () => { - InputManager.MoveMouseTo(skinEditor.ChildrenOfType().First()); - InputManager.Click(MouseButton.Left); + skinEditor.ChildrenOfType().First(b => b.ChildrenOfType().FirstOrDefault() != null).TriggerClick(); }); AddStep("store box", () => @@ -243,7 +243,9 @@ namespace osu.Game.Tests.Visual.Gameplay void revertAndCheckUnchanged() { AddStep("Revert changes", () => changeHandler.RestoreState(int.MinValue)); - AddAssert("Current state is same as default", () => defaultState.SequenceEqual(changeHandler.GetCurrentState())); + AddAssert("Current state is same as default", + () => Encoding.UTF8.GetString(defaultState), + () => Is.EqualTo(Encoding.UTF8.GetString(changeHandler.GetCurrentState()))); } } @@ -333,6 +335,40 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default); } + [Test] + public void TestCopyPaste() + { + AddStep("paste", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.V); + InputManager.ReleaseKey(Key.LControl); + }); + // no assertions. just make sure nothing crashes. + + AddStep("select bar hit error blueprint", () => + { + var blueprint = skinEditor.ChildrenOfType().First(b => b.Item is BarHitErrorMeter); + skinEditor.SelectedComponents.Clear(); + skinEditor.SelectedComponents.Add(blueprint.Item); + }); + AddStep("copy", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.C); + InputManager.ReleaseKey(Key.LControl); + }); + AddStep("paste", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.V); + InputManager.ReleaseKey(Key.LControl); + }); + AddAssert("three hit error meters present", + () => skinEditor.ChildrenOfType().Count(b => b.Item is BarHitErrorMeter), + () => Is.EqualTo(3)); + } + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); private partial class TestSkinEditorChangeHandler : SkinEditorChangeHandler diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs index 515bc09bbb..b7b2a6c175 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs @@ -8,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 { @@ -19,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 81ce088b9d..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; @@ -32,7 +32,7 @@ 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(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs index 59a1f938e6..e088d2ca87 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs @@ -17,6 +17,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); + protected override Drawable CreateArgonImplementation() => new ArgonAccuracyCounter(); protected override Drawable CreateDefaultImplementation() => new DefaultAccuracyCounter(); protected override Drawable CreateLegacyImplementation() => new LegacyAccuracyCounter(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs index 0c351a93bb..72f40d9c6f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs @@ -17,6 +17,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); + protected override Drawable CreateArgonImplementation() => new ArgonComboCounter(); protected override Drawable CreateDefaultImplementation() => new DefaultComboCounter(); protected override Drawable CreateLegacyImplementation() => new LegacyComboCounter(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index a8580ebf77..4cb0d5c0ff 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -8,11 +8,11 @@ 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; @@ -39,7 +39,7 @@ 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(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs index b4ebb7c410..1849e8abd0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs @@ -6,11 +6,13 @@ 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 { @@ -19,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), Width = 600, UseRelativeSize = { Value = false } }; + 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() @@ -28,15 +31,28 @@ 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. }); } + protected override void Update() + { + base.Update(); + + healthProcessor.Health.Value -= 0.0001f * Time.Elapsed; + } + [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 7079b93d3e..186680a79c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs @@ -17,6 +17,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(ScoreProcessor))] private ScoreProcessor scoreProcessor = TestGameplayState.Create(new OsuRuleset()).ScoreProcessor; + protected override Drawable CreateArgonImplementation() => new ArgonScoreCounter(); protected override Drawable CreateDefaultImplementation() => new DefaultScoreCounter(); protected override Drawable CreateLegacyImplementation() => new LegacyScoreCounter(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs index dfa9fdf03b..44a2e5fb9b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs @@ -52,59 +52,68 @@ namespace osu.Game.Tests.Visual.Gameplay { } - [TestCase(PathType.Linear)] - [TestCase(PathType.Bezier)] - [TestCase(PathType.Catmull)] - [TestCase(PathType.PerfectCurve)] - public void TestSingleSegment(PathType type) - => AddStep("create path", () => path.ControlPoints.AddRange(createSegment(type, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + [TestCase(SplineType.Linear, null)] + [TestCase(SplineType.BSpline, null)] + [TestCase(SplineType.BSpline, 3)] + [TestCase(SplineType.Catmull, null)] + [TestCase(SplineType.PerfectCurve, null)] + public void TestSingleSegment(SplineType splineType, int? degree) + => AddStep("create path", () => path.ControlPoints.AddRange(createSegment( + new PathType { Type = splineType, Degree = degree }, + Vector2.Zero, + new Vector2(0, 100), + new Vector2(100), + new Vector2(0, 200), + new Vector2(200) + ))); - [TestCase(PathType.Linear)] - [TestCase(PathType.Bezier)] - [TestCase(PathType.Catmull)] - [TestCase(PathType.PerfectCurve)] - public void TestMultipleSegment(PathType type) + [TestCase(SplineType.Linear, null)] + [TestCase(SplineType.BSpline, null)] + [TestCase(SplineType.BSpline, 3)] + [TestCase(SplineType.Catmull, null)] + [TestCase(SplineType.PerfectCurve, null)] + public void TestMultipleSegment(SplineType splineType, int? degree) { AddStep("create path", () => { - path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero)); - path.ControlPoints.AddRange(createSegment(type, new Vector2(0, 100), new Vector2(100), Vector2.Zero)); + path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero)); + path.ControlPoints.AddRange(createSegment(new PathType { Type = splineType, Degree = degree }, new Vector2(0, 100), new Vector2(100), Vector2.Zero)); }); } [Test] public void TestAddControlPoint() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100)))); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100)))); AddStep("add point", () => path.ControlPoints.Add(new PathControlPoint { Position = new Vector2(100) })); } [Test] public void TestInsertControlPoint() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(100)))); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(100)))); AddStep("insert point", () => path.ControlPoints.Insert(1, new PathControlPoint { Position = new Vector2(0, 100) })); } [Test] public void TestRemoveControlPoint() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); AddStep("remove second point", () => path.ControlPoints.RemoveAt(1)); } [Test] public void TestChangePathType() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); - AddStep("change type to bezier", () => path.ControlPoints[0].Type = PathType.Bezier); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + AddStep("change type to bezier", () => path.ControlPoints[0].Type = PathType.BEZIER); } [Test] public void TestAddSegmentByChangingType() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)))); - AddStep("change second point type to bezier", () => path.ControlPoints[1].Type = PathType.Bezier); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)))); + AddStep("change second point type to bezier", () => path.ControlPoints[1].Type = PathType.BEZIER); } [Test] @@ -112,8 +121,8 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("create path", () => { - path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); - path.ControlPoints[1].Type = PathType.Bezier; + path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); + path.ControlPoints[1].Type = PathType.BEZIER; }); AddStep("change second point type to null", () => path.ControlPoints[1].Type = null); @@ -124,8 +133,8 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("create path", () => { - path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); - path.ControlPoints[1].Type = PathType.Bezier; + path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); + path.ControlPoints[1].Type = PathType.BEZIER; }); AddStep("remove second point", () => path.ControlPoints.RemoveAt(1)); @@ -140,11 +149,11 @@ namespace osu.Game.Tests.Visual.Gameplay switch (points) { case 2: - path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100))); + path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(0, 100))); break; case 4: - path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); + path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); break; } }); @@ -153,38 +162,69 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestLengthenLastSegment() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); AddStep("lengthen last segment", () => path.ExpectedDistance.Value = 300); } [Test] public void TestShortenLastSegment() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); AddStep("shorten last segment", () => path.ExpectedDistance.Value = 150); } [Test] public void TestShortenFirstSegment() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); AddStep("shorten first segment", () => path.ExpectedDistance.Value = 50); } [Test] public void TestShortenToZeroLength() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); AddStep("shorten to 0 length", () => path.ExpectedDistance.Value = 0); } [Test] public void TestShortenToNegativeLength() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); 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 e975a85401..99f0ffb9d0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Threading; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; @@ -47,7 +48,7 @@ namespace osu.Game.Tests.Visual.Gameplay { Child = frameStabilityContainer = new FrameStabilityContainer { - MaxCatchUpFrames = 1 + Child = new FakeLoad() } } }); @@ -56,6 +57,15 @@ namespace osu.Game.Tests.Visual.Gameplay Dependencies.CacheAs(frameStabilityContainer); } + private partial class FakeLoad : Drawable + { + protected override void Update() + { + base.Update(); + Thread.Sleep(1); + } + } + [SetUpSteps] public void SetupSteps() { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index ffd034e4d2..1c7ede2b19 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay private TestSpectatorClient spectatorClient => dependenciesScreen.SpectatorClient; private DependenciesScreen dependenciesScreen; - private SoloSpectator spectatorScreen; + private SoloSpectatorScreen spectatorScreen; private BeatmapSetInfo importedBeatmap; private int importedBeatmapId; @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); - waitForPlayer(); + waitForPlayerCurrent(); sendFrames(startTime: gameplay_start); @@ -100,23 +100,22 @@ namespace osu.Game.Tests.Visual.Gameplay start(); - AddUntilStep("wait for player loader", () => (Stack.CurrentScreen as PlayerLoader)?.IsLoaded == true); + AddUntilStep("wait for player loader", () => this.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); AddUntilStep("queue send frames on player load", () => { - var loadingPlayer = (Stack.CurrentScreen as PlayerLoader)?.CurrentPlayer; + var loadingPlayer = this.ChildrenOfType().SingleOrDefault()?.CurrentPlayer; if (loadingPlayer == null) return false; loadingPlayer.OnLoadComplete += _ => - { spectatorClient.SendFramesFromUser(streamingUser.Id, 10, gameplay_start); - }; + return true; }); - waitForPlayer(); + waitForPlayerCurrent(); AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); AddAssert("time is greater than seek target", () => currentFrameStableTime, () => Is.GreaterThan(gameplay_start)); @@ -127,10 +126,10 @@ namespace osu.Game.Tests.Visual.Gameplay { loadSpectatingScreen(); - AddAssert("screen hasn't changed", () => Stack.CurrentScreen is SoloSpectator); + AddAssert("screen hasn't changed", () => Stack.CurrentScreen is SoloSpectatorScreen); start(); - waitForPlayer(); + waitForPlayerCurrent(); sendFrames(); AddAssert("ensure frames arrived", () => replayHandler.HasFrames); @@ -156,7 +155,7 @@ namespace osu.Game.Tests.Visual.Gameplay loadSpectatingScreen(); start(); - waitForPlayer(); + waitForPlayerCurrent(); checkPaused(true); // send enough frames to ensure play won't be paused @@ -172,7 +171,7 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(300); loadSpectatingScreen(); - waitForPlayer(); + waitForPlayerCurrent(); sendFrames(300); @@ -187,7 +186,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); Player lastPlayer = null; AddStep("store first player", () => lastPlayer = player); @@ -195,7 +194,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddAssert("player is different", () => lastPlayer != player); } @@ -206,7 +205,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); - waitForPlayer(); + waitForPlayerCurrent(); checkPaused(true); sendFrames(); @@ -224,7 +223,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddStep("stop spectating", () => (Stack.CurrentScreen as Player)?.Exit()); AddUntilStep("spectating stopped", () => spectatorScreen.GetChildScreen() == null); @@ -237,14 +236,14 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddStep("stop spectating", () => (Stack.CurrentScreen as Player)?.Exit()); AddUntilStep("spectating stopped", () => spectatorScreen.GetChildScreen() == null); // host starts playing a new session start(); - waitForPlayer(); + waitForPlayerCurrent(); } [Test] @@ -255,7 +254,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(-1234); sendFrames(); - AddAssert("screen didn't change", () => Stack.CurrentScreen is SoloSpectator); + AddAssert("screen didn't change", () => Stack.CurrentScreen is SoloSpectatorScreen); } [Test] @@ -299,7 +298,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } @@ -310,14 +309,14 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddStep("send passed", () => spectatorClient.SendEndPlay(streamingUser.Id, SpectatedUserState.Passed)); AddUntilStep("state is passed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Passed); start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } @@ -328,44 +327,72 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddStep("send quit", () => spectatorClient.SendEndPlay(streamingUser.Id)); AddUntilStep("state is quit", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Quit); + AddAssert("wait for player exit", () => Stack.CurrentScreen is SoloSpectatorScreen); + start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } [Test] - public void TestFailedState() + public void TestFailedStateDuringPlay() { loadSpectatingScreen(); start(); sendFrames(); - waitForPlayer(); + + waitForPlayerCurrent(); AddStep("send failed", () => spectatorClient.SendEndPlay(streamingUser.Id, SpectatedUserState.Failed)); AddUntilStep("state is failed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Failed); + AddUntilStep("wait for player to fail", () => player.GameplayState.HasFailed); + start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); + AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); + } + + [Test] + public void TestFailedStateDuringLoading() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + + waitForPlayerLoader(); + + AddStep("send failed", () => spectatorClient.SendEndPlay(streamingUser.Id, SpectatedUserState.Failed)); + AddUntilStep("state is failed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Failed); + + AddAssert("wait for player exit", () => Stack.CurrentScreen is SoloSpectatorScreen); + + start(); + sendFrames(); + waitForPlayerCurrent(); AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } private OsuFramedReplayInputHandler replayHandler => (OsuFramedReplayInputHandler)Stack.ChildrenOfType().First().ReplayInputHandler; - private Player player => Stack.CurrentScreen as Player; + private Player player => this.ChildrenOfType().Single(); private double currentFrameStableTime => player.ChildrenOfType().First().CurrentTime; - private void waitForPlayer() => AddUntilStep("wait for player", () => (Stack.CurrentScreen as Player)?.IsLoaded == true); + private void waitForPlayerLoader() => AddUntilStep("wait for loading", () => this.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); + + private void waitForPlayerCurrent() => AddUntilStep("wait for player current", () => this.ChildrenOfType().SingleOrDefault()?.IsCurrentScreen() == true); private void start(int? beatmapId = null) => AddStep("start play", () => spectatorClient.SendStartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); @@ -381,7 +408,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void loadSpectatingScreen() { - AddStep("load spectator", () => LoadScreen(spectatorScreen = new SoloSpectator(streamingUser))); + AddStep("load spectator", () => LoadScreen(spectatorScreen = new SoloSpectatorScreen(streamingUser))); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded); } 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/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/TestSceneUnstableRateCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneUnstableRateCounter.cs index d0e516ed39..73ec6ea335 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneUnstableRateCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneUnstableRateCounter.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -56,6 +57,7 @@ namespace osu.Game.Tests.Visual.Gameplay scoreProcessor.RevertResult( new JudgementResult(new HitCircle { HitWindows = hitWindows }, new Judgement()) { + GameplayRate = 1.0, TimeOffset = 25, Type = HitResult.Perfect, }); @@ -80,6 +82,27 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("UR = 250", () => counter.Current.Value == 250.0); } + [Test] + public void TestStaticRateChange() + { + AddStep("Create Display", recreateDisplay); + + AddRepeatStep("Set UR to 250 at 1.5x", () => applyJudgement(25, true, 1.5), 4); + + AddUntilStep("UR = 250/1.5", () => counter.Current.Value == Math.Round(250.0 / 1.5)); + } + + [Test] + public void TestDynamicRateChange() + { + AddStep("Create Display", recreateDisplay); + + AddRepeatStep("Set UR to 100 at 1.0x", () => applyJudgement(10, true, 1.0), 4); + AddRepeatStep("Bring UR to 100 at 1.5x", () => applyJudgement(15, true, 1.5), 4); + + AddUntilStep("UR = 100", () => counter.Current.Value == 100.0); + } + private void recreateDisplay() { Clear(); @@ -92,7 +115,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - private void applyJudgement(double offsetMs, bool alt) + private void applyJudgement(double offsetMs, bool alt, double gameplayRate = 1.0) { double placement = offsetMs; @@ -105,6 +128,7 @@ namespace osu.Game.Tests.Visual.Gameplay scoreProcessor.ApplyResult(new JudgementResult(new HitCircle { HitWindows = hitWindows }, new Judgement()) { TimeOffset = placement, + GameplayRate = gameplayRate, Type = HitResult.Perfect, }); } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroMusicActionHandling.cs new file mode 100644 index 0000000000..01aeaff1db --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroMusicActionHandling.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.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Input.Bindings; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual.Menus +{ + public partial class TestSceneIntroMusicActionHandling : OsuGameTestScene + { + private GlobalActionContainer globalActionContainer => Game.ChildrenOfType().First(); + + public override void SetUpSteps() + { + CreateNewGame(); + // we do not want to progress to main menu immediately, hence the override and lack of `ConfirmAtMainMenu()` call here. + } + + [Test] + public void TestPauseDuringIntro() + { + AddUntilStep("Wait for music", () => Game?.MusicController.IsPlaying == true); + + // Check that pause doesn't work during intro sequence. + AddStep("Toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay)); + AddAssert("Still playing before menu", () => Game?.MusicController.IsPlaying == true); + AddUntilStep("Wait for main menu", () => Game?.ScreenStack.CurrentScreen is MainMenu menu && menu.IsLoaded); + + // Check that toggling after intro still works. + AddStep("Toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay)); + AddUntilStep("Music paused", () => Game?.MusicController.IsPlaying == false && Game?.MusicController.UserPauseRequested == true); + AddStep("Toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay)); + AddUntilStep("Music resumed", () => Game?.MusicController.IsPlaying == true && Game?.MusicController.UserPauseRequested == false); + } + } +} diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs new file mode 100644 index 0000000000..7053a9d544 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.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 System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.Menu; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Menus +{ + public partial class TestSceneMainMenu : OsuGameTestScene + { + private SystemTitle systemTitle => Game.ChildrenOfType().Single(); + + [Test] + public void TestSystemTitle() + { + AddStep("set system title", () => systemTitle.Current.Value = new APISystemTitle + { + Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", + Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023", + }); + AddAssert("system title not visible", () => systemTitle.State.Value, () => Is.EqualTo(Visibility.Hidden)); + AddStep("enter menu", () => InputManager.Key(Key.Enter)); + AddUntilStep("system title visible", () => systemTitle.State.Value, () => Is.EqualTo(Visibility.Visible)); + AddStep("set another title", () => systemTitle.Current.Value = new APISystemTitle + { + Image = @"https://assets.ppy.sh/main-menu/wf2023-vote@2x.png", + Url = @"https://osu.ppy.sh/community/contests/189", + }); + AddStep("set title with nonexistent image", () => systemTitle.Current.Value = new APISystemTitle + { + Image = @"https://test.invalid/@2x", // .invalid TLD reserved by https://datatracker.ietf.org/doc/html/rfc2606#section-2 + Url = @"https://osu.ppy.sh/community/contests/189", + }); + AddStep("unset system title", () => systemTitle.Current.Value = null); + } + } +} diff --git a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs index b12f3e7946..bb327e5962 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Menus foreach (var fountain in Children.OfType()) { if (RNG.NextSingle() > 0.8f) - fountain.Shoot(); + fountain.Shoot(RNG.Next(-1, 2)); } }, 150); } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs b/osu.Game.Tests/Visual/Menus/TestSceneSupporterDisplay.cs similarity index 55% rename from osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs rename to osu.Game.Tests/Visual/Menus/TestSceneSupporterDisplay.cs index fb82b0df80..8b18adbe0d 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneSupporterDisplay.cs @@ -1,19 +1,27 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the 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.Allocation; +using NUnit.Framework; +using osu.Framework.Graphics; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Screens.Menu; namespace osu.Game.Tests.Visual.Menus { - public partial class TestSceneDisclaimer : ScreenTestScene + public partial class TestSceneSupporterDisplay : OsuTestScene { - [BackgroundDependencyLoader] - private void load() + [Test] + public void TestBasic() { - AddStep("load disclaimer", () => LoadScreen(new Disclaimer())); + AddStep("create display", () => + { + Child = new SupporterDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + }); AddStep("toggle support", () => { diff --git a/osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs b/osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs index 6bdb9132e1..c5e56c6453 100644 --- a/osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs +++ b/osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs @@ -8,6 +8,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osuTK; @@ -29,7 +30,7 @@ namespace osu.Game.Tests.Visual.Mods public void TestMaximumAchievableAccuracy() => CreateModTest(new ModTestData { - Mod = new ModAccuracyChallenge + Mod = new OsuModAccuracyChallenge { MinimumAccuracy = { Value = 0.6 } }, @@ -49,7 +50,7 @@ namespace osu.Game.Tests.Visual.Mods public void TestStandardAccuracy() => CreateModTest(new ModTestData { - Mod = new ModAccuracyChallenge + Mod = new OsuModAccuracyChallenge { MinimumAccuracy = { Value = 0.6 }, AccuracyJudgeMode = { Value = ModAccuracyChallenge.AccuracyMode.Standard } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 312135402f..6446ebd35f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -19,8 +19,10 @@ using osu.Game.Beatmaps.Drawables; using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterface; using osu.Game.Models; using osu.Game.Online.API; +using osu.Game.Online.Chat; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; @@ -302,6 +304,37 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } + [Test] + public void TestSelectableMouseHandling() + { + bool resultsRequested = false; + + AddStep("reset flag", () => resultsRequested = false); + createPlaylist(p => + { + p.AllowSelection = true; + p.AllowShowingResults = true; + p.RequestResults = _ => resultsRequested = true; + }); + + AddStep("move mouse to first item title", () => + { + var drawQuad = playlist.ChildrenOfType().First().ScreenSpaceDrawQuad; + var location = (drawQuad.TopLeft + drawQuad.BottomLeft) / 2 + new Vector2(drawQuad.Width * 0.2f, 0); + InputManager.MoveMouseTo(location); + }); + AddUntilStep("wait for text load", () => playlist.ChildrenOfType().Any()); + AddAssert("first item title not hovered", () => playlist.ChildrenOfType().First().IsHovered, () => Is.False); + AddStep("click left mouse", () => InputManager.Click(MouseButton.Left)); + AddUntilStep("first item selected", () => playlist.ChildrenOfType().First().IsSelectedItem, () => Is.True); + // implies being clickable. + AddUntilStep("first item title hovered", () => playlist.ChildrenOfType().First().IsHovered, () => Is.True); + + AddStep("move mouse to second item results button", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(5))); + AddStep("click left mouse", () => InputManager.Click(MouseButton.Left)); + AddUntilStep("results requested", () => resultsRequested); + } + private void moveToItem(int index, Vector2? offset = null) => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(index), offset)); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs index 66ba908879..a4feffddfb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs @@ -10,12 +10,14 @@ 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; using osu.Framework.Testing; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay; +using osu.Game.Utils; using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer @@ -23,6 +25,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public partial class TestSceneFreeModSelectOverlay : MultiplayerTestScene { private FreeModSelectOverlay freeModSelectOverlay; + private FooterButtonFreeMods footerButtonFreeMods; private readonly Bindable>> availableMods = new Bindable>>(); [BackgroundDependencyLoader] @@ -119,11 +122,46 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("select all button enabled", () => this.ChildrenOfType().Single().Enabled.Value); } + [Test] + public void TestSelectAllViaFooterButtonThenDeselectFromOverlay() + { + createFreeModSelect(); + + AddAssert("overlay select all button enabled", () => freeModSelectOverlay.ChildrenOfType().Single().Enabled.Value); + AddAssert("footer button displays off", () => footerButtonFreeMods.ChildrenOfType().Any(t => t.Text == "off")); + + AddStep("click footer select all button", () => + { + InputManager.MoveMouseTo(footerButtonFreeMods); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("all mods selected", assertAllAvailableModsSelected); + AddAssert("footer button displays all", () => footerButtonFreeMods.ChildrenOfType().Any(t => t.Text == "all")); + + AddStep("click deselect all button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("all mods deselected", () => !freeModSelectOverlay.SelectedMods.Value.Any()); + AddAssert("footer button displays off", () => footerButtonFreeMods.ChildrenOfType().Any(t => t.Text == "off")); + } + private void createFreeModSelect() { - AddStep("create free mod select screen", () => Child = freeModSelectOverlay = new FreeModSelectOverlay + AddStep("create free mod select screen", () => Children = new Drawable[] { - State = { Value = Visibility.Visible } + freeModSelectOverlay = new FreeModSelectOverlay + { + State = { Value = Visibility.Visible } + }, + footerButtonFreeMods = new FooterButtonFreeMods(freeModSelectOverlay) + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Current = { BindTarget = freeModSelectOverlay.SelectedMods }, + }, }); AddUntilStep("all column content loaded", () => freeModSelectOverlay.ChildrenOfType().Any() @@ -133,10 +171,14 @@ namespace osu.Game.Tests.Visual.Multiplayer private bool assertAllAvailableModsSelected() { var allAvailableMods = availableMods.Value - .SelectMany(pair => pair.Value) + .Where(pair => pair.Key != ModType.System) + .SelectMany(pair => ModUtils.FlattenMods(pair.Value)) .Where(mod => mod.UserPlayable && mod.HasImplementation) .ToList(); + if (freeModSelectOverlay.SelectedMods.Value.Count != allAvailableMods.Count) + return false; + foreach (var availableMod in allAvailableMods) { if (freeModSelectOverlay.SelectedMods.Value.All(selectedMod => selectedMod.GetType() != availableMod.GetType())) 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/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index 6d309078e6..2d61c26a6b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -378,6 +378,41 @@ namespace osu.Game.Tests.Visual.Multiplayer }, users); } + [Test] + public void TestAbortMatch() + { + AddStep("setup client", () => + { + multiplayerClient.Setup(m => m.StartMatch()) + .Callback(() => + { + multiplayerClient.Raise(m => m.LoadRequested -= null); + multiplayerClient.Object.Room!.State = MultiplayerRoomState.WaitingForLoad; + + // The local user state doesn't really matter, so let's do the same as the base implementation for these tests. + changeUserState(localUser.UserID, MultiplayerUserState.Idle); + }); + + multiplayerClient.Setup(m => m.AbortMatch()) + .Callback(() => + { + multiplayerClient.Object.Room!.State = MultiplayerRoomState.Open; + raiseRoomUpdated(); + }); + }); + + // Ready + ClickButtonWhenEnabled(); + + // Start match + ClickButtonWhenEnabled(); + AddUntilStep("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + + // Abort + ClickButtonWhenEnabled(); + AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once)); + } + private void verifyGameplayStartFlow() { checkLocalUserState(MultiplayerUserState.Ready); 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 e81dc87d4f..cebc75f90c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -67,6 +67,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [TestCase(1)] [TestCase(4)] + [TestCase(9)] public void TestGeneral(int count) { int[] userIds = getPlayerIds(count); @@ -78,6 +79,33 @@ namespace osu.Game.Tests.Visual.Multiplayer 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() { @@ -435,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..747805edc8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -29,6 +29,9 @@ using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Taiko; +using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; @@ -690,10 +693,19 @@ namespace osu.Game.Tests.Visual.Multiplayer } AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen); + + AddAssert("check is fail", () => + { + var scoreInfo = ((ResultsScreen)multiplayerComponents.CurrentScreen).Score; + + return !scoreInfo.Passed && scoreInfo.Rank == ScoreRank.F; + }); } [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; @@ -999,6 +1011,43 @@ namespace osu.Game.Tests.Visual.Multiplayer } } + [Test] + public void TestGameplayStartsWhileInSongSelectWithDifferentRuleset() + { + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + QueueMode = { Value = QueueMode.AllPlayers }, + Playlist = + { + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + AllowedMods = new[] { new APIMod { Acronym = "HD" } }, + }, + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 1)).BeatmapInfo) + { + RulesetID = new TaikoRuleset().RulesetInfo.OnlineID, + AllowedMods = new[] { new APIMod { Acronym = "HD" } }, + }, + } + }); + + AddStep("select hidden", () => multiplayerClient.ChangeUserMods(new[] { new APIMod { Acronym = "HD" } })); + AddStep("make user ready", () => multiplayerClient.ChangeState(MultiplayerUserState.Ready)); + AddStep("press edit on second item", () => this.ChildrenOfType().Single(i => i.Item.RulesetID == 1) + .ChildrenOfType().Single().TriggerClick()); + + AddUntilStep("wait for song select", () => InputManager.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true); + AddAssert("ruleset is taiko", () => Ruleset.Value.OnlineID == 1); + + AddStep("start match", () => multiplayerClient.StartMatch().WaitSafely()); + + AddUntilStep("wait for loading", () => multiplayerClient.ClientRoom?.State == MultiplayerRoomState.WaitingForLoad); + AddUntilStep("wait for gameplay to start", () => multiplayerClient.ClientRoom?.State == MultiplayerRoomState.Playing); + AddAssert("hidden is selected", () => SelectedMods.Value, () => Has.One.TypeOf(typeof(OsuModHidden))); + } + private void enterGameplay() { pressReadyButton(); 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/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index 54ee1659e1..9930349b1b 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -6,16 +6,20 @@ using NUnit.Framework; using osu.Framework.Extensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Overlays; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; 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; @@ -203,6 +207,62 @@ namespace osu.Game.Tests.Visual.Navigation 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)); + } + + [Test] + public void TestCreateNewDifficultyOnNonExistentBeatmap() + { + AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); + + AddStep("open editor", () => Game.ChildrenOfType().Single().OnEditBeatmap?.Invoke()); + AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.IsLoaded); + AddStep("click on file", () => + { + var item = getEditor().ChildrenOfType().Single(i => i.Item.Text.Value.ToString() == "File"); + item.TriggerClick(); + }); + AddStep("click on create new difficulty", () => + { + var item = getEditor().ChildrenOfType().Single(i => i.Item.Text.Value.ToString() == "Create new difficulty"); + item.TriggerClick(); + }); + AddStep("click on catch", () => + { + var item = getEditor().ChildrenOfType().Single(i => i.Item.Text.Value.ToString() == "osu!catch"); + item.TriggerClick(); + }); + AddAssert("save dialog displayed", () => Game.ChildrenOfType().Single().CurrentDialog is SaveRequiredPopupDialog); + + AddStep("press save", () => Game.ChildrenOfType().Single().CurrentDialog!.PerformOkAction()); + AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.IsLoaded); + AddAssert("editor beatmap uses catch ruleset", () => getEditorBeatmap().BeatmapInfo.Ruleset.ShortName == "fruits"); + } + private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType().Single(); private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs index 5467a64b85..3a3af43cb1 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs @@ -78,7 +78,7 @@ 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()? diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneInterProcessCommunication.cs b/osu.Game.Tests/Visual/Navigation/TestSceneInterProcessCommunication.cs index 1ecd38e1d3..83430b5665 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneInterProcessCommunication.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneInterProcessCommunication.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Navigation }); AddStep("create IPC sender channels", () => { - ipcSenderHost = new HeadlessGameHost(gameHost.Name, new HostOptions { BindIPC = true }); + ipcSenderHost = new HeadlessGameHost(gameHost.Name, new HostOptions { IPCPort = OsuGame.IPC_PORT }); osuSchemeLinkIPCSender = new OsuSchemeLinkIPCChannel(ipcSenderHost); archiveImportIPCSender = new ArchiveImportIPCChannel(ipcSenderHost); }); diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index 4bcd6b100a..459a8b0df5 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestFromSongSelectWithFilter([Values] ScorePresentType type) { - AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); AddStep("filter to nothing", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).FilterControl.CurrentTextSearch.Value = "fdsajkl;fgewq"); @@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestFromSongSelectWithConvertRulesetChange([Values] ScorePresentType type) { - AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); AddStep("set convert to false", () => Game.LocalConfig.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestFromSongSelect([Values] ScorePresentType type) { - AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); var firstImport = importScore(1); @@ -135,7 +135,7 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestFromSongSelectDifferentRuleset([Values] ScorePresentType type) { - AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); var firstImport = importScore(1); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 3acc2f0384..f59fbc75ac 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -7,15 +7,18 @@ 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; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Screens; 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; @@ -32,8 +35,10 @@ using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; 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; @@ -139,13 +144,13 @@ namespace osu.Game.Tests.Visual.Navigation PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddStep("set filter", () => songSelect.ChildrenOfType().Single().Current.Value = "test"); + AddStep("set filter", () => filterControlTextBox().Current.Value = "test"); AddStep("press back", () => InputManager.Click(MouseButton.Button1)); AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen == songSelect); - AddAssert("filter cleared", () => string.IsNullOrEmpty(songSelect.ChildrenOfType().Single().Current.Value)); + AddAssert("filter cleared", () => string.IsNullOrEmpty(filterControlTextBox().Current.Value)); - AddStep("set filter again", () => songSelect.ChildrenOfType().Single().Current.Value = "test"); + AddStep("set filter again", () => filterControlTextBox().Current.Value = "test"); AddStep("open collections dropdown", () => { InputManager.MoveMouseTo(songSelect.ChildrenOfType().Single()); @@ -159,10 +164,47 @@ namespace osu.Game.Tests.Visual.Navigation .ChildrenOfType.DropdownMenu>().Single().State == MenuState.Closed); AddStep("press back a second time", () => InputManager.Click(MouseButton.Button1)); - AddAssert("filter cleared", () => string.IsNullOrEmpty(songSelect.ChildrenOfType().Single().Current.Value)); + AddAssert("filter cleared", () => string.IsNullOrEmpty(filterControlTextBox().Current.Value)); AddStep("press back a third time", () => InputManager.Click(MouseButton.Button1)); ConfirmAtMainMenu(); + + TextBox filterControlTextBox() => songSelect.ChildrenOfType().Single(); + } + + [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; } /// @@ -209,12 +251,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() { @@ -738,11 +800,7 @@ namespace osu.Game.Tests.Visual.Navigation }); }); - AddStep("attempt exit", () => - { - for (int i = 0; i < 2; ++i) - Game.ScreenStack.CurrentScreen.Exit(); - }); + AddRepeatStep("attempt force exit", () => Game.ScreenStack.CurrentScreen.Exit(), 2); AddUntilStep("stopped at exit confirm", () => Game.ChildrenOfType().Single().CurrentDialog is ConfirmExitDialog); } @@ -777,7 +835,165 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("exit dialog is shown", () => Game.Dependencies.Get().CurrentDialog is ConfirmExitDialog); } + [Test] + public void TestQuickSkinEditorDoesntNukeSkin() + { + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + + AddStep("open", () => InputManager.Key(Key.Space)); + AddStep("skin", () => InputManager.Key(Key.E)); + AddStep("editor", () => InputManager.Key(Key.S)); + AddStep("and close immediately", () => InputManager.Key(Key.Escape)); + + AddStep("open again", () => InputManager.Key(Key.S)); + + Player player = null; + + AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); + AddUntilStep("wait for gameplay still has health bar", () => player.ChildrenOfType().Any()); + } + + [Test] + public void TestTouchScreenDetectionAtSongSelect() + { + AddStep("touch logo", () => + { + var button = Game.ChildrenOfType().Single(); + var touch = new Touch(TouchSource.Touch1, button.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + InputManager.EndTouch(touch); + }); + AddAssert("touch screen detected active", () => Game.Dependencies.Get().Get(Static.TouchInputActive), () => Is.True); + + AddStep("click settings button", () => + { + var button = Game.ChildrenOfType().Last(); + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + AddAssert("touch screen detected inactive", () => Game.Dependencies.Get().Get(Static.TouchInputActive), () => Is.False); + + AddStep("close settings sidebar", () => InputManager.Key(Key.Escape)); + + Screens.Select.SongSelect songSelect = null; + AddRepeatStep("go to solo", () => InputManager.Key(Key.P), 3); + AddUntilStep("wait for song select", () => (songSelect = Game.ScreenStack.CurrentScreen as Screens.Select.SongSelect) != null); + AddUntilStep("wait for beatmap sets loaded", () => songSelect.BeatmapSetsLoaded); + + AddStep("switch to osu! ruleset", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.Number1); + InputManager.ReleaseKey(Key.LControl); + }); + AddStep("touch beatmap wedge", () => + { + var wedge = Game.ChildrenOfType().Single(); + var touch = new Touch(TouchSource.Touch2, wedge.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + InputManager.EndTouch(touch); + }); + AddUntilStep("touch device mod activated", () => Game.SelectedMods.Value, () => Has.One.InstanceOf()); + + AddStep("switch to mania ruleset", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.Number4); + InputManager.ReleaseKey(Key.LControl); + }); + AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf()); + AddStep("touch beatmap wedge", () => + { + var wedge = Game.ChildrenOfType().Single(); + var touch = new Touch(TouchSource.Touch2, wedge.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + InputManager.EndTouch(touch); + }); + AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf()); + + AddStep("switch to osu! ruleset", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.Number1); + InputManager.ReleaseKey(Key.LControl); + }); + AddUntilStep("touch device mod activated", () => Game.SelectedMods.Value, () => Has.One.InstanceOf()); + + AddStep("click beatmap wedge", () => + { + InputManager.MoveMouseTo(Game.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf()); + } + + [Test] + public void TestTouchScreenDetectionInGame() + { + PushAndConfirm(() => new TestPlaySongSelect()); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + AddStep("select", () => InputManager.Key(Key.Enter)); + + Player player = null; + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + return (player = Game.ScreenStack.CurrentScreen as Player) != null; + }); + + AddUntilStep("wait for track playing", () => Game.Beatmap.Value.Track.IsRunning); + + AddStep("touch", () => + { + var touch = new Touch(TouchSource.Touch2, Game.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + InputManager.EndTouch(touch); + }); + AddUntilStep("touch device mod added to score", () => player.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); + + AddStep("exit player", () => player.Exit()); + AddUntilStep("touch device mod still active", () => Game.SelectedMods.Value, () => Has.One.InstanceOf()); + } + + [Test] + public void TestExitSongSelectAndImmediatelyClickLogo() + { + Screens.Select.SongSelect songSelect = null; + 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("press escape and then click logo immediately", () => + { + InputManager.Key(Key.Escape); + clickLogoWhenNotCurrent(); + }); + + void clickLogoWhenNotCurrent() + { + if (songSelect.IsCurrentScreen()) + Scheduler.AddOnce(clickLogoWhenNotCurrent); + else + { + InputManager.MoveMouseTo(Game.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + } + } + } + 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; @@ -803,7 +1019,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 88904bf85b..57f1b2fbe9 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -5,19 +5,24 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Threading; +using osu.Game.Online.API; +using osu.Game.Beatmaps; using osu.Game.Overlays.Settings; using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; 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,11 +36,14 @@ namespace osu.Game.Tests.Visual.Navigation private SkinEditor skinEditor => Game.ChildrenOfType().FirstOrDefault(); [Test] - public void TestEditComponentDuringGameplay() + public void TestEditComponentFromGameplayScene() { advanceToSongSelect(); openSkinEditor(); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + switchToGameplayScene(); BarHitErrorMeter hitErrorMeter = null; @@ -69,11 +77,37 @@ 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() { advanceToSongSelect(); openSkinEditor(); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + switchToGameplayScene(); AddUntilStep("wait for components", () => skinEditor.ChildrenOfType().Any()); @@ -138,6 +172,9 @@ namespace osu.Game.Tests.Visual.Navigation openSkinEditor(); AddStep("select DT", () => Game.SelectedMods.Value = new Mod[] { new OsuModDoubleTime() }); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + switchToGameplayScene(); AddAssert("DT still selected", () => ((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Single() is OsuModDoubleTime); @@ -150,6 +187,9 @@ namespace osu.Game.Tests.Visual.Navigation openSkinEditor(); AddStep("select relax and spun out", () => Game.SelectedMods.Value = new Mod[] { new OsuModRelax(), new OsuModSpunOut() }); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + switchToGameplayScene(); AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any()); @@ -162,6 +202,9 @@ namespace osu.Game.Tests.Visual.Navigation openSkinEditor(); AddStep("select autoplay", () => Game.SelectedMods.Value = new Mod[] { new OsuModAutoplay() }); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + switchToGameplayScene(); AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any()); @@ -174,6 +217,9 @@ namespace osu.Game.Tests.Visual.Navigation openSkinEditor(); AddStep("select cinema", () => Game.SelectedMods.Value = new Mod[] { new OsuModCinema() }); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + switchToGameplayScene(); AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any()); @@ -216,6 +262,45 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("editor sidebars not empty", () => skinEditor.ChildrenOfType().SelectMany(sidebar => sidebar.Children).Count(), () => Is.GreaterThan(0)); } + [Test] + public void TestOpenSkinEditorGameplaySceneOnBeatmapWithNoObjects() + { + AddStep("set dummy beatmap", () => Game.Beatmap.SetDefault()); + advanceToSongSelect(); + + AddStep("create empty beatmap", () => Game.BeatmapManager.CreateNew(new OsuRuleset().RulesetInfo, new GuestUser())); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + openSkinEditor(); + switchToGameplayScene(); + } + + [Test] + public void TestOpenSkinEditorGameplaySceneWhenDummyBeatmapActive() + { + AddStep("set dummy beatmap", () => Game.Beatmap.SetDefault()); + + openSkinEditor(); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + public void TestOpenSkinEditorGameplaySceneWhenDifferentRulesetActive(int rulesetId) + { + BeatmapSetInfo beatmapSet = null!; + + AddStep("import beatmap", () => beatmapSet = BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely()); + AddStep($"select difficulty for ruleset w/ ID {rulesetId}", () => + { + var beatmap = beatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == rulesetId); + Game.Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(beatmap); + }); + + openSkinEditor(); + switchToGameplayScene(); + } + private void advanceToSongSelect() { PushAndConfirm(() => songSelect = new TestPlaySongSelect()); @@ -242,9 +327,6 @@ namespace osu.Game.Tests.Visual.Navigation private void switchToGameplayScene() { - AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); - AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); - AddStep("Click gameplay scene button", () => { InputManager.MoveMouseTo(skinEditor.ChildrenOfType().First(b => b.Text.ToString() == "Gameplay")); diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 5e49cb633e..8691f46605 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; }); } @@ -368,7 +368,7 @@ namespace osu.Game.Tests.Visual.Online { var cardContainer = this.ChildrenOfType>().Single().Parent; var expandedContent = this.ChildrenOfType().Single(); - return expandedContent.ScreenSpaceDrawQuad.GetVertices().ToArray().All(v => cardContainer.ScreenSpaceDrawQuad.Contains(v)); + return expandedContent.ScreenSpaceDrawQuad.GetVertices().ToArray().All(v => cardContainer!.ScreenSpaceDrawQuad.Contains(v)); }); } diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index 60fb6b8c86..325cb9e0cb 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -220,7 +220,7 @@ namespace osu.Game.Tests.Visual.Online public void TestSelectedModsDontAffectStatistics() { AddStep("show map", () => overlay.ShowBeatmapSet(getBeatmapSet())); - AddAssert("AR displayed as 0", () => overlay.ChildrenOfType().Single(s => s.Title == BeatmapsetsStrings.ShowStatsAr).Value == (0, null)); + AddAssert("AR displayed as 0", () => overlay.ChildrenOfType().Single(s => s.Title == BeatmapsetsStrings.ShowStatsAr).Value, () => Is.EqualTo((0, 0))); AddStep("set AR10 diff adjust", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust @@ -228,7 +228,7 @@ namespace osu.Game.Tests.Visual.Online ApproachRate = { Value = 10 } } }); - AddAssert("AR still displayed as 0", () => overlay.ChildrenOfType().Single(s => s.Title == BeatmapsetsStrings.ShowStatsAr).Value == (0, null)); + AddAssert("AR still displayed as 0", () => overlay.ChildrenOfType().Single(s => s.Title == BeatmapsetsStrings.ShowStatsAr).Value, () => Is.EqualTo((0, 0))); } [Test] 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/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index ecf9b68297..c793535255 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -1,8 +1,6 @@ // Copyright (c) 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; @@ -56,87 +54,76 @@ namespace osu.Game.Tests.Visual.Online textContainer.Clear(); }); - [Test] - public void TestLinksGeneral() + [TestCase("test!")] + [TestCase("dev.ppy.sh!")] + [TestCase("https://dev.ppy.sh!", LinkAction.External)] + [TestCase("http://dev.ppy.sh!", LinkAction.External)] + [TestCase("forgothttps://dev.ppy.sh!", LinkAction.External)] + [TestCase("forgothttp://dev.ppy.sh!", LinkAction.External)] + [TestCase("00:12:345 - Test?", LinkAction.OpenEditorTimestamp)] + [TestCase("00:12:345 (1,2) - Test?", LinkAction.OpenEditorTimestamp)] + [TestCase($"{OsuGameBase.OSU_PROTOCOL}edit/00:12:345 - Test?", LinkAction.OpenEditorTimestamp)] + [TestCase($"{OsuGameBase.OSU_PROTOCOL}edit/00:12:345 (1,2) - Test?", LinkAction.OpenEditorTimestamp)] + [TestCase($"{OsuGameBase.OSU_PROTOCOL}00:12:345 - not an editor timestamp", LinkAction.External)] + [TestCase("Wiki link for tasty [[Performance Points]]", LinkAction.OpenWiki)] + [TestCase("(osu forums)[https://dev.ppy.sh/forum] (old link format)", LinkAction.External)] + [TestCase("[https://dev.ppy.sh/home New site] (new link format)", LinkAction.External)] + [TestCase("[osu forums](https://dev.ppy.sh/forum) (new link format 2)", LinkAction.External)] + [TestCase("[https://dev.ppy.sh/home This is only a link to the new osu webpage but this is supposed to test word wrap.]", LinkAction.External)] + [TestCase("Let's (try)[https://dev.ppy.sh/home] [https://dev.ppy.sh/b/252238 multiple links] https://dev.ppy.sh/home", LinkAction.External, LinkAction.OpenBeatmap, LinkAction.External)] + [TestCase("[https://dev.ppy.sh/home New link format with escaped [and \\[ paired] braces]", LinkAction.External)] + [TestCase("[Markdown link format with escaped [and \\[ paired] braces](https://dev.ppy.sh/home)", LinkAction.External)] + [TestCase("(Old link format with escaped (and \\( paired) parentheses)[https://dev.ppy.sh/home] and [[also a rogue wiki link]]", LinkAction.External, LinkAction.OpenWiki)] + [TestCase("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present).")] // note that there's 0 links here (they get removed if a channel is not found) + [TestCase("Join my multiplayer game osump://12346.", LinkAction.JoinMultiplayerMatch)] + [TestCase("Join my multiplayer gameosump://12346.", LinkAction.JoinMultiplayerMatch)] + [TestCase("Join my [multiplayer game](osump://12346).", LinkAction.JoinMultiplayerMatch)] + [TestCase($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", LinkAction.OpenChannel)] + [TestCase($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", LinkAction.OpenChannel)] + [TestCase($"Join my{OsuGameBase.OSU_PROTOCOL}chan/#english.", LinkAction.OpenChannel)] + [TestCase("Join my #english or #japanese channels.", LinkAction.OpenChannel, LinkAction.OpenChannel)] + [TestCase("Join my #english or #nonexistent #hashtag channels.", LinkAction.OpenChannel)] + [TestCase("Hello world\uD83D\uDE12(<--This is an emoji). There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20")] + public void TestLinksGeneral(string text, params LinkAction[] actions) { - int messageIndex = 0; + addMessageWithChecks(text, expectedActions: actions); + } - addMessageWithChecks("test!"); - addMessageWithChecks("dev.ppy.sh!"); - addMessageWithChecks("https://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); - addMessageWithChecks("[https://dev.ppy.sh/home New site] (new link format)", 1, expectedActions: LinkAction.External); - addMessageWithChecks("[osu forums](https://dev.ppy.sh/forum) (new link format 2)", 1, expectedActions: LinkAction.External); - addMessageWithChecks("[https://dev.ppy.sh/home This is only a link to the new osu webpage but this is supposed to test word wrap.]", 1, expectedActions: LinkAction.External); - addMessageWithChecks("is now listening to [https://dev.ppy.sh/s/93523 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmapSet); - addMessageWithChecks("is now playing [https://dev.ppy.sh/b/252238 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmap); - addMessageWithChecks("Let's (try)[https://dev.ppy.sh/home] [https://dev.ppy.sh/b/252238 multiple links] https://dev.ppy.sh/home", 3, - expectedActions: new[] { LinkAction.External, LinkAction.OpenBeatmap, LinkAction.External }); - addMessageWithChecks("[https://dev.ppy.sh/home New link format with escaped [and \\[ paired] braces]", 1, expectedActions: LinkAction.External); - addMessageWithChecks("[Markdown link format with escaped [and \\[ paired] braces](https://dev.ppy.sh/home)", 1, expectedActions: LinkAction.External); - addMessageWithChecks("(Old link format with escaped (and \\( paired) parentheses)[https://dev.ppy.sh/home] and [[also a rogue wiki link]]", 2, - expectedActions: new[] { LinkAction.External, LinkAction.OpenWiki }); - // note that there's 0 links here (they get removed if a channel is not found) - addMessageWithChecks("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present)."); - addMessageWithChecks("I am important!", 0, false, true); - 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 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 #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"); + [TestCase("is now listening to [https://dev.ppy.sh/s/93523 IMAGE -MATERIAL- ]", true, false, LinkAction.OpenBeatmapSet)] + [TestCase("is now playing [https://dev.ppy.sh/b/252238 IMAGE -MATERIAL- ]", true, false, LinkAction.OpenBeatmap)] + [TestCase("I am important!", false, true)] + [TestCase("feels important", true, true)] + [TestCase("likes to post this [https://dev.ppy.sh/home link].", true, true, LinkAction.External)] + public void TestActionAndImportantLinks(string text, bool isAction, bool isImportant, params LinkAction[] expectedActions) + { + addMessageWithChecks(text, isAction, isImportant, expectedActions); + } - void addMessageWithChecks(string text, int linkAmount = 0, bool isAction = false, bool isImportant = false, params LinkAction[] expectedActions) + private void addMessageWithChecks(string text, bool isAction = false, bool isImportant = false, params LinkAction[] expectedActions) + { + ChatLine newLine = null!; + + AddStep("add message", () => { - ChatLine newLine = null; - int index = messageIndex++; + newLine = new ChatLine(new DummyMessage(text, isAction, isImportant)); + textContainer.Add(newLine); + }); - AddStep("add message", () => - { - newLine = new ChatLine(new DummyMessage(text, isAction, isImportant, index)); - textContainer.Add(newLine); - }); + AddAssert("msg has the right action", () => newLine.Message.Links.Select(l => l.Action), () => Is.EqualTo(expectedActions)); + AddAssert($"msg shows {expectedActions.Length} link(s)", isShowingLinks); - AddAssert($"msg #{index} has {linkAmount} link(s)", () => newLine.Message.Links.Count == linkAmount); - AddAssert($"msg #{index} has the right action", hasExpectedActions); - //AddAssert($"msg #{index} is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic()); - AddAssert($"msg #{index} shows {linkAmount} link(s)", isShowingLinks); + bool isShowingLinks() + { + bool hasBackground = !string.IsNullOrEmpty(newLine.Message.Sender.Colour); - bool hasExpectedActions() - { - var expectedActionsList = expectedActions.ToList(); + Color4 textColour = isAction && hasBackground ? Color4Extensions.FromHex(newLine.Message.Sender.Colour) : Color4.White; - if (expectedActionsList.Count != newLine.Message.Links.Count) - return false; + var linkCompilers = newLine.DrawableContentFlow.Where(d => d is DrawableLinkCompiler).ToList(); + var linkSprites = linkCompilers.SelectMany(comp => ((DrawableLinkCompiler)comp).Parts); - for (int i = 0; i < newLine.Message.Links.Count; i++) - { - var action = newLine.Message.Links[i].Action; - if (action != expectedActions[i]) return false; - } - - return true; - } - - //bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast().All(sprite => sprite.Font.Italics); - - bool isShowingLinks() - { - bool hasBackground = !string.IsNullOrEmpty(newLine.Message.Sender.Colour); - - Color4 textColour = isAction && hasBackground ? Color4Extensions.FromHex(newLine.Message.Sender.Colour) : Color4.White; - - var linkCompilers = newLine.DrawableContentFlow.Where(d => d is DrawableLinkCompiler).ToList(); - var linkSprites = linkCompilers.SelectMany(comp => ((DrawableLinkCompiler)comp).Parts); - - return linkSprites.All(d => d.Colour == linkColour) - && newLine.DrawableContentFlow.Except(linkSprites.Concat(linkCompilers)).All(d => d.Colour == textColour); - } + return linkSprites.All(d => d.Colour == linkColour) + && newLine.DrawableContentFlow.Except(linkSprites.Concat(linkCompilers)).All(d => d.Colour == textColour) + && linkCompilers.Count == expectedActions.Length; } } @@ -150,7 +137,7 @@ namespace osu.Game.Tests.Visual.Online addEchoWithWait("[https://dev.ppy.sh/forum let's try multiple words too!]"); addEchoWithWait("(long loading times! clickable while loading?)[https://dev.ppy.sh/home]", null, 5000); - void addEchoWithWait(string text, string completeText = null, double delay = 250) + void addEchoWithWait(string text, string? completeText = null, double delay = 250) { int index = messageIndex++; @@ -179,21 +166,12 @@ namespace osu.Game.Tests.Visual.Online { private static long messageCounter; - internal static readonly APIUser TEST_SENDER_BACKGROUND = new APIUser - { - Username = @"i-am-important", - Id = 42, - Colour = "#250cc9", - }; - internal static readonly APIUser TEST_SENDER = new APIUser { Username = @"Somebody", Id = 1, }; - public new DateTimeOffset Timestamp = DateTimeOffset.Now; - public DummyMessage(string text, bool isAction = false, bool isImportant = false, int number = 0) : base(messageCounter++) { diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 55e6b54af7..58feab4ebb 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -180,11 +180,8 @@ namespace osu.Game.Tests.Visual.Online }); AddStep("Show overlay", () => chatOverlay.Show()); AddAssert("Overlay uses config height", () => chatOverlay.Height == configChatHeight.Default); - AddStep("Click top bar", () => - { - InputManager.MoveMouseTo(chatOverlayTopBar); - InputManager.PressButton(MouseButton.Left); - }); + AddStep("Move mouse to drag bar", () => InputManager.MoveMouseTo(chatOverlayTopBar.DragBar)); + AddStep("Click drag bar", () => InputManager.PressButton(MouseButton.Left)); AddStep("Drag overlay to new height", () => InputManager.MoveMouseTo(chatOverlayTopBar, new Vector2(0, -300))); AddStep("Stop dragging", () => InputManager.ReleaseButton(MouseButton.Left)); AddStep("Store new height", () => newHeight = chatOverlay.Height); @@ -634,7 +631,7 @@ namespace osu.Game.Tests.Visual.Online AddAssert("Nothing happened", () => this.ChildrenOfType().Any()); AddStep("Set report data", () => { - var field = this.ChildrenOfType().Single().ChildrenOfType().Single(); + var field = this.ChildrenOfType().Single().ChildrenOfType().First(); field.Current.Value = "test other"; }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs index dbf3b52572..10fdffb8e1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs @@ -262,7 +262,7 @@ namespace osu.Game.Tests.Visual.Online AddAssert("Nothing happened", () => this.ChildrenOfType().Any()); AddStep("Set report data", () => { - var field = this.ChildrenOfType().Single().ChildrenOfType().Single(); + var field = this.ChildrenOfType().Single().ChildrenOfType().First(); field.Current.Value = report_text; var reason = this.ChildrenOfType>().Single(); reason.Current.Value = CommentReportReason.Other; diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs new file mode 100644 index 0000000000..b696c5d8ca --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.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 System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Database; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; +using osu.Game.Online.Spectator; +using osu.Game.Overlays; +using osu.Game.Overlays.Dashboard; +using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Tests.Visual.Metadata; +using osu.Game.Tests.Visual.Spectator; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Online +{ + public partial class TestSceneCurrentlyOnlineDisplay : OsuTestScene + { + private readonly APIUser streamingUser = new APIUser { Id = 2, Username = "Test user" }; + + private TestSpectatorClient spectatorClient = null!; + private TestMetadataClient metadataClient = null!; + private CurrentlyOnlineDisplay currentlyOnline = null!; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("set up components", () => + { + spectatorClient = new TestSpectatorClient(); + metadataClient = new TestMetadataClient(); + var lookupCache = new TestUserLookupCache(); + + Children = new Drawable[] + { + lookupCache, + spectatorClient, + metadataClient, + new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] + { + (typeof(SpectatorClient), spectatorClient), + (typeof(MetadataClient), metadataClient), + (typeof(UserLookupCache), lookupCache), + (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Purple)), + }, + Child = currentlyOnline = new CurrentlyOnlineDisplay + { + RelativeSizeAxes = Axes.Both, + } + }, + }; + }); + } + + [Test] + public void TestBasicDisplay() + { + AddStep("Begin watching user presence", () => metadataClient.BeginWatchingUserPresence()); + AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() })); + AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType().FirstOrDefault()?.User.Id == 2); + AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.False); + + AddStep("User began playing", () => spectatorClient.SendStartPlay(streamingUser.Id, 0)); + AddAssert("Spectate button enabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.True); + + AddStep("User finished playing", () => spectatorClient.SendEndPlay(streamingUser.Id)); + AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.False); + + AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null)); + AddUntilStep("Panel no longer present", () => !currentlyOnline.ChildrenOfType().Any()); + AddStep("End watching user presence", () => metadataClient.EndWatchingUserPresence()); + } + + [Test] + public void TestUserWasPlayingBeforeWatchingUserPresence() + { + AddStep("User began playing", () => spectatorClient.SendStartPlay(streamingUser.Id, 0)); + AddStep("Begin watching user presence", () => metadataClient.BeginWatchingUserPresence()); + AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() })); + AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType().FirstOrDefault()?.User.Id == 2); + AddAssert("Spectate button enabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.True); + + AddStep("User finished playing", () => spectatorClient.SendEndPlay(streamingUser.Id)); + AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.False); + AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null)); + AddStep("End watching user presence", () => metadataClient.EndWatchingUserPresence()); + } + + internal partial class TestUserLookupCache : UserLookupCache + { + private static readonly string[] usernames = + { + "fieryrage", + "Kerensa", + "MillhioreF", + "Player01", + "smoogipoo", + "Ephemeral", + "BTMC", + "Cilvery", + "m980", + "HappyStick", + "LittleEndu", + "frenzibyte", + "Zallius", + "BanchoBot", + "rocketminer210", + "pishifat" + }; + + protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) + { + // tests against failed lookups + if (lookup == 13) + return Task.FromResult(null); + + return Task.FromResult(new APIUser + { + Id = lookup, + Username = usernames[lookup % usernames.Length], + }); + } + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs deleted file mode 100644 index 5237238f63..0000000000 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs +++ /dev/null @@ -1,103 +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.Linq; -using System.Threading; -using System.Threading.Tasks; -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Testing; -using osu.Game.Database; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Spectator; -using osu.Game.Overlays; -using osu.Game.Overlays.Dashboard; -using osu.Game.Tests.Visual.Spectator; -using osu.Game.Users; - -namespace osu.Game.Tests.Visual.Online -{ - public partial class TestSceneCurrentlyPlayingDisplay : OsuTestScene - { - private readonly APIUser streamingUser = new APIUser { Id = 2, Username = "Test user" }; - - private TestSpectatorClient spectatorClient = null!; - private CurrentlyPlayingDisplay currentlyPlaying = null!; - - [SetUpSteps] - public void SetUpSteps() - { - AddStep("add streaming client", () => - { - spectatorClient = new TestSpectatorClient(); - var lookupCache = new TestUserLookupCache(); - - Children = new Drawable[] - { - lookupCache, - spectatorClient, - new DependencyProvidingContainer - { - RelativeSizeAxes = Axes.Both, - CachedDependencies = new (Type, object)[] - { - (typeof(SpectatorClient), spectatorClient), - (typeof(UserLookupCache), lookupCache), - (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Purple)), - }, - Child = currentlyPlaying = new CurrentlyPlayingDisplay - { - RelativeSizeAxes = Axes.Both, - } - }, - }; - }); - } - - [Test] - public void TestBasicDisplay() - { - AddStep("Add playing user", () => spectatorClient.SendStartPlay(streamingUser.Id, 0)); - 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()); - } - - internal partial class TestUserLookupCache : UserLookupCache - { - private static readonly string[] usernames = - { - "fieryrage", - "Kerensa", - "MillhioreF", - "Player01", - "smoogipoo", - "Ephemeral", - "BTMC", - "Cilvery", - "m980", - "HappyStick", - "LittleEndu", - "frenzibyte", - "Zallius", - "BanchoBot", - "rocketminer210", - "pishifat" - }; - - protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) - { - // tests against failed lookups - if (lookup == 13) - return Task.FromResult(null); - - return Task.FromResult(new APIUser - { - Id = lookup, - Username = usernames[lookup % usernames.Length], - }); - } - } - } -} diff --git a/osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs b/osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs index ac80463d3a..5e83dd4fb3 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 \"Big Image\")" }, + new[] { "Small Image", "![](Cursor/cursortrail)" }, new[] { "Heading", @"# Heading 1 diff --git a/osu.Game.Tests/Visual/Online/TestSceneGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneGraph.cs index 4f19003638..f4bde159e5 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneGraph.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneGraph.cs @@ -17,18 +17,16 @@ namespace osu.Game.Tests.Visual.Online { BarGraph graph; - Children = new[] + Child = graph = new BarGraph { - graph = new BarGraph - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(0.5f), - }, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.5f), }; AddStep("values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Select(i => (float)i)); + AddStep("small values", () => graph.Values = Enumerable.Range(1, 10).Select(i => i * 0.01f).Concat(new[] { 100f })); AddStep("values from 1-100", () => graph.Values = Enumerable.Range(1, 100).Select(i => (float)i)); AddStep("reversed values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Reverse().Select(i => (float)i)); AddStep("empty values", () => graph.Values = Array.Empty()); @@ -36,6 +34,14 @@ namespace osu.Game.Tests.Visual.Online AddStep("Top to bottom", () => graph.Direction = BarDirection.TopToBottom); AddStep("Left to right", () => graph.Direction = BarDirection.LeftToRight); AddStep("Right to left", () => graph.Direction = BarDirection.RightToLeft); + + AddToggleStep("Toggle movement", enabled => + { + if (enabled) + graph.MoveToY(-10, 1000).Then().MoveToY(10, 1000).Loop(); + else + graph.ClearTransforms(); + }); } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs index fb36580a42..1e9b0317fb 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs @@ -11,8 +11,8 @@ 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.Rulesets.Osu; using osu.Game.Users; namespace osu.Game.Tests.Visual.Online @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestPlayActivity() { - AddStep("Set activity", () => api.Activity.Value = new UserActivity.InSoloGame(new BeatmapInfo(), new RulesetInfo())); + AddStep("Set activity", () => api.Activity.Value = new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo)); AddStep("Run command", () => Add(new NowPlayingCommand(new Channel()))); @@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestModPresence() { - AddStep("Set activity", () => api.Activity.Value = new UserActivity.InSoloGame(new BeatmapInfo(), new RulesetInfo())); + AddStep("Set activity", () => api.Activity.Value = new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo)); AddStep("Add Hidden mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod() }); 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/TestSceneSoloStatisticsWatcher.cs b/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs index e62e53bd02..be819afa3e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs @@ -268,6 +268,26 @@ namespace osu.Game.Tests.Visual.Online AddAssert("update not received", () => update == null); } + [Test] + public void TestGlobalStatisticsUpdatedAfterRegistrationAddedAndScoreProcessed() + { + int userId = getUserId(); + long scoreId = getScoreId(); + setUpUser(userId); + + var ruleset = new OsuRuleset().RulesetInfo; + + SoloStatisticsUpdate? update = null; + registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); + + feignScoreProcessing(userId, ruleset, 5_000_000); + + AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId)); + AddUntilStep("update received", () => update != null); + AddAssert("local user values are correct", () => dummyAPI.LocalUser.Value.Statistics.TotalScore, () => Is.EqualTo(5_000_000)); + AddAssert("statistics values are correct", () => dummyAPI.Statistics.Value!.TotalScore, () => Is.EqualTo(5_000_000)); + } + private int nextUserId = 2000; private long nextScoreId = 50000; diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs new file mode 100644 index 0000000000..4539eae25f --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs @@ -0,0 +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.Linq; +using NUnit.Framework; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Testing; +using osu.Game.Graphics.Cursor; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users; +using osu.Game.Users.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Online +{ + public partial class TestSceneUserClickableAvatar : OsuManualInputManagerTestScene + { + [SetUp] + public void SetUp() => Schedule(() => + { + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(10f), + Children = new[] + { + generateUser(@"peppy", 2, CountryCode.AU, @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", false, "99EB47"), + generateUser(@"flyte", 3103765, CountryCode.JP, @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", true), + generateUser(@"joshika39", 17032217, CountryCode.RS, @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", false), + new UpdateableAvatar(), + new UpdateableAvatar() + }, + }; + }); + + [Test] + public void TestClickableAvatarHover() + { + AddStep("hover avatar with user panel", () => InputManager.MoveMouseTo(this.ChildrenOfType().ElementAt(1))); + AddUntilStep("wait for tooltip to show", () => this.ChildrenOfType().FirstOrDefault()?.State.Value == Visibility.Visible); + AddStep("hover out", () => InputManager.MoveMouseTo(new Vector2(0))); + AddUntilStep("wait for tooltip to hide", () => this.ChildrenOfType().FirstOrDefault()?.State.Value == Visibility.Hidden); + + AddStep("hover avatar without user panel", () => InputManager.MoveMouseTo(this.ChildrenOfType().ElementAt(0))); + AddUntilStep("wait for tooltip to show", () => this.ChildrenOfType().FirstOrDefault()?.State.Value == Visibility.Visible); + AddStep("hover out", () => InputManager.MoveMouseTo(new Vector2(0))); + AddUntilStep("wait for tooltip to hide", () => this.ChildrenOfType().FirstOrDefault()?.State.Value == Visibility.Hidden); + } + + private Drawable generateUser(string username, int id, CountryCode countryCode, string cover, bool showPanel, string? color = null) + { + var user = new APIUser + { + Username = username, + Id = id, + CountryCode = countryCode, + CoverUrl = cover, + Colour = color ?? "000000", + Status = + { + Value = UserStatus.Online + }, + }; + + return new ClickableAvatar(user, showPanel) + { + Width = 50, + Height = 50, + CornerRadius = 10, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = 1, + Colour = Color4.Black.Opacity(0.2f), + }, + }; + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index c61b572d8c..4df34e6244 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.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; @@ -24,11 +26,14 @@ namespace osu.Game.Tests.Visual.Online public partial class TestSceneUserPanel : OsuTestScene { private readonly Bindable activity = new Bindable(); - private readonly Bindable status = new Bindable(); + private readonly Bindable status = new Bindable(); private UserGridPanel boundPanel1; private TestUserListPanel boundPanel2; + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + [Resolved] private IRulesetStore rulesetStore { get; set; } @@ -66,7 +71,7 @@ namespace osu.Game.Tests.Visual.Online Id = 3103765, CountryCode = CountryCode.JP, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", - Status = { Value = new UserStatusOnline() } + Status = { Value = UserStatus.Online } }) { Width = 300 }, boundPanel1 = new UserGridPanel(new APIUser { @@ -85,8 +90,25 @@ namespace osu.Game.Tests.Visual.Online CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", IsOnline = false, LastVisit = DateTimeOffset.Now - }) - }, + }), + new UserRankPanel(new APIUser + { + Username = @"flyte", + Id = 3103765, + CountryCode = CountryCode.JP, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", + Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 } + }) { Width = 300 }, + new UserRankPanel(new APIUser + { + Username = @"peppy", + Id = 2, + Colour = "99EB47", + CountryCode = CountryCode.AU, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } + }) { Width = 300 } + } }; boundPanel1.Status.BindTo(status); @@ -99,16 +121,16 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestUserStatus() { - AddStep("online", () => status.Value = new UserStatusOnline()); - AddStep("do not disturb", () => status.Value = new UserStatusDoNotDisturb()); - AddStep("offline", () => status.Value = new UserStatusOffline()); + AddStep("online", () => status.Value = UserStatus.Online); + AddStep("do not disturb", () => status.Value = UserStatus.DoNotDisturb); + AddStep("offline", () => status.Value = UserStatus.Offline); AddStep("null status", () => status.Value = null); } [Test] public void TestUserActivity() { - AddStep("set online status", () => status.Value = new UserStatusOnline()); + AddStep("set online status", () => status.Value = UserStatus.Online); AddStep("idle", () => activity.Value = null); AddStep("watching replay", () => activity.Value = new UserActivity.WatchingReplay(createScore(@"nats"))); @@ -127,15 +149,32 @@ namespace osu.Game.Tests.Visual.Online public void TestUserActivityChange() { AddAssert("visit message is visible", () => boundPanel2.LastVisitMessage.IsPresent); - AddStep("set online status", () => status.Value = new UserStatusOnline()); + AddStep("set online status", () => status.Value = UserStatus.Online); AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent); AddStep("set choosing activity", () => activity.Value = new UserActivity.ChoosingBeatmap()); - AddStep("set offline status", () => status.Value = new UserStatusOffline()); + AddStep("set offline status", () => status.Value = UserStatus.Offline); AddAssert("visit message is visible", () => boundPanel2.LastVisitMessage.IsPresent); - AddStep("set online status", () => status.Value = new UserStatusOnline()); + AddStep("set online status", () => status.Value = UserStatus.Online); AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent); } + [Test] + public void TestUserStatisticsChange() + { + AddStep("update statistics", () => + { + API.UpdateStatistics(new UserStatistics + { + GlobalRank = RNG.Next(100000), + CountryRank = RNG.Next(100000) + }); + }); + AddStep("set statistics to empty", () => + { + API.UpdateStatistics(new UserStatistics()); + }); + } + 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..1375689075 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -78,6 +78,31 @@ namespace osu.Game.Tests.Visual.Online AddStep("complete request", () => pendingRequest.TriggerSuccess(TEST_USER)); } + [Test] + public void TestLogin() + { + GetUserRequest pendingRequest = null!; + + AddStep("set up request handling", () => + { + dummyAPI.HandleRequest = req => + { + if (dummyAPI.State.Value == APIState.Online && req is GetUserRequest getUserRequest) + { + pendingRequest = getUserRequest; + return true; + } + + return false; + }; + }); + AddStep("logout", () => dummyAPI.Logout()); + AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 })); + AddStep("login", () => dummyAPI.Login("username", "password")); + AddWaitStep("wait some", 3); + AddStep("complete request", () => pendingRequest.TriggerSuccess(TEST_USER)); + } + public static readonly APIUser TEST_USER = new APIUser { Username = @"Somebody", @@ -121,12 +146,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 +176,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/TestSceneWikiHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiHeader.cs index 4e71c5977e..d259322d4a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiHeader.cs @@ -24,8 +24,8 @@ namespace osu.Game.Tests.Visual.Online [Cached] private readonly Bindable wikiPageData = new Bindable(new APIWikiPage { - Title = "Main Page", - Path = "Main_Page", + Title = "Main page", + Path = WikiOverlay.INDEX_PATH, }); private TestHeader header; diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMainPage.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMainPage.cs index 9967be73e8..7b4eadd46d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiMainPage.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMainPage.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Online }; } - // From https://osu.ppy.sh/api/v2/wiki/en/Main_Page + // From https://osu.ppy.sh/api/v2/wiki/en/Main_page private const string main_page_markdown = "---\nlayout: main_page\n---\n\n\n\n
\nWelcome to the osu! wiki, a project containing a wide range of osu! related information.\n
\n\n
\n
\n\n# Getting started\n\n[Welcome](/wiki/Welcome) • [Installation](/wiki/Installation) • [Registration](/wiki/Registration) • [Help Centre](/wiki/Help_Centre) • [FAQ](/wiki/FAQ)\n\n
\n
\n\n# Game client\n\n[Interface](/wiki/Interface) • [Options](/wiki/Options) • [Visual settings](/wiki/Visual_Settings) • [Shortcut key reference](/wiki/Shortcut_key_reference) • [Configuration file](/wiki/osu!_Program_Files/User_Configuration_File) • [Program files](/wiki/osu!_Program_Files)\n\n[File formats](/wiki/osu!_File_Formats): [.osz](/wiki/osu!_File_Formats/Osz_(file_format)) • [.osk](/wiki/osu!_File_Formats/Osk_(file_format)) • [.osr](/wiki/osu!_File_Formats/Osr_(file_format)) • [.osu](/wiki/osu!_File_Formats/Osu_(file_format)) • [.osb](/wiki/osu!_File_Formats/Osb_(file_format)) • [.db](/wiki/osu!_File_Formats/Db_(file_format))\n\n
\n
\n\n# Gameplay\n\n[Game modes](/wiki/Game_mode): [osu!](/wiki/Game_mode/osu!) • [osu!taiko](/wiki/Game_mode/osu!taiko) • [osu!catch](/wiki/Game_mode/osu!catch) • [osu!mania](/wiki/Game_mode/osu!mania)\n\n[Beatmap](/wiki/Beatmap) • [Hit object](/wiki/Hit_object) • [Mods](/wiki/Game_modifier) • [Score](/wiki/Score) • [Replay](/wiki/Replay) • [Multi](/wiki/Multi)\n\n
\n
\n\n# [Beatmap editor](/wiki/Beatmap_Editor)\n\nSections: [Compose](/wiki/Beatmap_Editor/Compose) • [Design](/wiki/Beatmap_Editor/Design) • [Timing](/wiki/Beatmap_Editor/Timing) • [Song setup](/wiki/Beatmap_Editor/Song_Setup)\n\nComponents: [AiMod](/wiki/Beatmap_Editor/AiMod) • [Beat snap divisor](/wiki/Beatmap_Editor/Beat_Snap_Divisor) • [Distance snap](/wiki/Beatmap_Editor/Distance_Snap) • [Menu](/wiki/Beatmap_Editor/Menu) • [SB load](/wiki/Beatmap_Editor/SB_Load) • [Timelines](/wiki/Beatmap_Editor/Timelines)\n\n[Beatmapping](/wiki/Beatmapping) • [Difficulty](/wiki/Beatmap/Difficulty) • [Mapping techniques](/wiki/Mapping_Techniques) • [Storyboarding](/wiki/Storyboarding)\n\n
\n
\n\n# Beatmap submission and ranking\n\n[Submission](/wiki/Submission) • [Modding](/wiki/Modding) • [Ranking procedure](/wiki/Beatmap_ranking_procedure) • [Mappers' Guild](/wiki/Mappers_Guild) • [Project Loved](/wiki/Project_Loved)\n\n[Ranking criteria](/wiki/Ranking_Criteria): [osu!](/wiki/Ranking_Criteria/osu!) • [osu!taiko](/wiki/Ranking_Criteria/osu!taiko) • [osu!catch](/wiki/Ranking_Criteria/osu!catch) • [osu!mania](/wiki/Ranking_Criteria/osu!mania)\n\n
\n
\n\n# Community\n\n[Tournaments](/wiki/Tournaments) • [Skinning](/wiki/Skinning) • [Projects](/wiki/Projects) • [Guides](/wiki/Guides) • [osu!dev Discord server](/wiki/osu!dev_Discord_server) • [How you can help](/wiki/How_You_Can_Help!) • [Glossary](/wiki/Glossary)\n\n
\n
\n\n# People\n\n[The Team](/wiki/People/The_Team): [Developers](/wiki/People/The_Team/Developers) • [Global Moderation Team](/wiki/People/The_Team/Global_Moderation_Team) • [Support Team](/wiki/People/The_Team/Support_Team) • [Nomination Assessment Team](/wiki/People/The_Team/Nomination_Assessment_Team) • [Beatmap Nominators](/wiki/People/The_Team/Beatmap_Nominators) • [osu! Alumni](/wiki/People/The_Team/osu!_Alumni) • [Project Loved Team](/wiki/People/The_Team/Project_Loved_Team)\n\nOrganisations: [osu! UCI](/wiki/Organisations/osu!_UCI)\n\n[Community Contributors](/wiki/People/Community_Contributors) • [Users with unique titles](/wiki/People/Users_with_unique_titles)\n\n
\n
\n\n# For developers\n\n[API](/wiki/osu!api) • [Bot account](/wiki/Bot_account) • [Brand identity guidelines](/wiki/Brand_identity_guidelines)\n\n
\n
\n\n# About the wiki\n\n[Sitemap](/wiki/Sitemap) • [Contribution guide](/wiki/osu!_wiki_Contribution_Guide) • [Article styling criteria](/wiki/Article_Styling_Criteria) • [News styling criteria](/wiki/News_Styling_Criteria)\n\n
\n
\n"; } diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs index 0aa0295f7d..8909305602 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs @@ -10,7 +10,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Containers.Markdown; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; @@ -70,8 +69,8 @@ namespace osu.Game.Tests.Visual.Online { AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/"); - AddStep("set '/wiki/Main_Page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_Page)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Main_Page"); + AddStep("set '/wiki/Main_page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_page)"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Main_page"); AddStep("set '../FAQ''", () => markdownContainer.Text = "[FAQ](../FAQ)"); AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/FAQ"); @@ -276,7 +275,7 @@ Phasellus eu nunc nec ligula semper fringilla. Aliquam magna neque, placerat sed AddStep("set content", () => { markdownContainer.Text = @" -This is a paragraph containing `inline code` synatax. +This is a paragraph containing `inline code` syntax. Oh wow I do love the `WikiMarkdownContainer`, it is very cool! This is a line before the fenced code block: @@ -298,7 +297,7 @@ This is a line after the fenced code block! { public LinkInline Link; - public override MarkdownTextFlowContainer CreateTextFlow() => new TestMarkdownTextFlowContainer + public override OsuMarkdownTextFlowContainer CreateTextFlow() => new TestMarkdownTextFlowContainer { UrlAdded = link => Link = link, }; diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs index 79c7e3a22e..e70d35f74a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs @@ -107,12 +107,12 @@ namespace osu.Game.Tests.Visual.Online }; }); - // From https://osu.ppy.sh/api/v2/wiki/en/Main_Page + // From https://osu.ppy.sh/api/v2/wiki/en/Main_page private APIWikiPage responseMainPage => new APIWikiPage { - Title = "Main Page", - Layout = "main_page", - Path = "Main_Page", + Title = "Main page", + Layout = WikiOverlay.INDEX_PATH.ToLowerInvariant(), // custom classes are always lower snake. + Path = WikiOverlay.INDEX_PATH, Locale = "en", Subtitle = null, Markdown = diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index cb422d8c06..e805b03542 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -17,6 +17,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Placeholders; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; @@ -49,8 +50,8 @@ namespace osu.Game.Tests.Visual.Playlists // Previous test instances of the results screen may still exist at this point so wait for // those screens to be cleaned up by the base SetUpSteps before re-initialising test state. - // The the screen also holds a leased Beatmap bindable so reassigning it must happen after - // the screen as been exited. + // The screen also holds a leased Beatmap bindable so reassigning it must happen after + // the screen has been exited. AddStep("initialise user scores and beatmap", () => { lowestScoreId = 1; @@ -63,8 +64,6 @@ namespace osu.Game.Tests.Visual.Playlists userScore.Statistics = new Dictionary(); userScore.MaximumStatistics = new Dictionary(); - bindHandler(); - // Beatmap is required to be an actual beatmap so the scores can get their scores correctly // calculated for standardised scoring, else the tests that rely on ordering will fall over. Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); @@ -77,6 +76,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); createResults(() => userScore); + waitForDisplay(); AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded); AddAssert($"score panel position is {real_user_position}", @@ -86,7 +86,10 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestShowNullUserScore() { + AddStep("bind user score info handler", () => bindHandler()); + createResults(); + waitForDisplay(); AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); } @@ -97,6 +100,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind user score info handler", () => bindHandler(true, userScore)); createResults(() => userScore); + waitForDisplay(); AddAssert("more than 1 panel displayed", () => this.ChildrenOfType().Count() > 1); AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded); @@ -108,6 +112,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind delayed handler", () => bindHandler(true)); createResults(); + waitForDisplay(); AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); } @@ -115,10 +120,11 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestFetchWhenScrolledToTheRight() { - createResults(); - AddStep("bind delayed handler", () => bindHandler(true)); + createResults(); + waitForDisplay(); + for (int i = 0; i < 2; i++) { int beforePanelCount = 0; @@ -134,12 +140,44 @@ namespace osu.Game.Tests.Visual.Playlists } } + [Test] + public void TestNoMoreScoresToTheRight() + { + AddStep("bind delayed handler with scores", () => bindHandler(delayed: true)); + + createResults(); + waitForDisplay(); + + int beforePanelCount = 0; + + AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); + AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToEnd(false)); + + AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); + waitForDisplay(); + + AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() >= beforePanelCount + scores_per_result); + AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); + + AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); + AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true)); + AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToEnd(false)); + + AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); + waitForDisplay(); + + AddAssert("count not increased", () => this.ChildrenOfType().Count() == beforePanelCount); + AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); + AddAssert("no placeholders shown", () => this.ChildrenOfType().Count(), () => Is.Zero); + } + [Test] public void TestFetchWhenScrolledToTheLeft() { AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); createResults(() => userScore); + waitForDisplay(); AddStep("bind delayed handler", () => bindHandler(true)); @@ -158,6 +196,15 @@ namespace osu.Game.Tests.Visual.Playlists } } + [Test] + public void TestShowWithNoScores() + { + AddStep("bind user score info handler", () => bindHandler(noScores: true)); + createResults(); + AddAssert("no scores visible", () => !resultsScreen.ScorePanelList.GetScorePanels().Any()); + AddAssert("placeholder shown", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); + } + private void createResults(Func getScore = null) { AddStep("load results", () => @@ -169,7 +216,6 @@ namespace osu.Game.Tests.Visual.Playlists }); AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded); - waitForDisplay(); } private void waitForDisplay() @@ -183,7 +229,7 @@ namespace osu.Game.Tests.Visual.Playlists AddWaitStep("wait for display", 5); } - private void bindHandler(bool delayed = false, ScoreInfo userScore = null, bool failRequests = false) => ((DummyAPIAccess)API).HandleRequest = request => + private void bindHandler(bool delayed = false, ScoreInfo userScore = null, bool failRequests = false, bool noScores = false) => ((DummyAPIAccess)API).HandleRequest = request => { // pre-check for requests we should be handling (as they are scheduled below). switch (request) @@ -219,7 +265,7 @@ namespace osu.Game.Tests.Visual.Playlists break; case IndexPlaylistScoresRequest i: - triggerSuccess(i, createIndexResponse(i)); + triggerSuccess(i, createIndexResponse(i, noScores)); break; } }, delay); @@ -301,10 +347,12 @@ namespace osu.Game.Tests.Visual.Playlists return multiplayerUserScore; } - private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req) + private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req, bool noScores = false) { var result = new IndexedMultiplayerScores(); + if (noScores) return result; + string sort = req.IndexParams?.Properties["sort"].ToObject() ?? "score_desc"; for (int i = 1; i <= scores_per_result; i++) 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/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index 03b168c72c..7aa36429a7 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -9,6 +10,8 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -22,31 +25,48 @@ namespace osu.Game.Tests.Visual.Ranking { public partial class TestSceneAccuracyCircle : OsuTestScene { - [TestCase(0)] - [TestCase(0.2)] - [TestCase(0.5)] - [TestCase(0.6999)] - [TestCase(0.7)] - [TestCase(0.75)] - [TestCase(0.7999)] - [TestCase(0.8)] - [TestCase(0.85)] - [TestCase(0.8999)] - [TestCase(0.9)] - [TestCase(0.925)] - [TestCase(0.9499)] - [TestCase(0.95)] - [TestCase(0.975)] - [TestCase(0.9999)] - [TestCase(1)] - public void TestRank(double accuracy) + [Test] + public void TestOsuRank() { - var score = createScore(accuracy, ScoreProcessor.RankFromAccuracy(accuracy)); - - addCircleStep(score); + addCircleStep(createScore(0, new OsuRuleset())); + addCircleStep(createScore(0.5, new OsuRuleset())); + addCircleStep(createScore(0.699, new OsuRuleset())); + addCircleStep(createScore(0.7, new OsuRuleset())); + addCircleStep(createScore(0.75, new OsuRuleset())); + addCircleStep(createScore(0.799, new OsuRuleset())); + addCircleStep(createScore(0.8, new OsuRuleset())); + addCircleStep(createScore(0.85, new OsuRuleset())); + addCircleStep(createScore(0.899, new OsuRuleset())); + addCircleStep(createScore(0.9, new OsuRuleset())); + addCircleStep(createScore(0.925, new OsuRuleset())); + addCircleStep(createScore(0.9499, new OsuRuleset())); + addCircleStep(createScore(0.95, new OsuRuleset())); + addCircleStep(createScore(0.975, new OsuRuleset())); + addCircleStep(createScore(0.99, new OsuRuleset())); + addCircleStep(createScore(1, new OsuRuleset())); } - private void addCircleStep(ScoreInfo score) => AddStep("add panel", () => + [Test] + public void TestCatchRank() + { + addCircleStep(createScore(0, new CatchRuleset())); + addCircleStep(createScore(0.5, new CatchRuleset())); + addCircleStep(createScore(0.8499, new CatchRuleset())); + addCircleStep(createScore(0.85, new CatchRuleset())); + addCircleStep(createScore(0.875, new CatchRuleset())); + addCircleStep(createScore(0.899, new CatchRuleset())); + addCircleStep(createScore(0.9, new CatchRuleset())); + addCircleStep(createScore(0.925, new CatchRuleset())); + addCircleStep(createScore(0.9399, new CatchRuleset())); + addCircleStep(createScore(0.94, new CatchRuleset())); + addCircleStep(createScore(0.9675, new CatchRuleset())); + addCircleStep(createScore(0.9799, new CatchRuleset())); + addCircleStep(createScore(0.98, new CatchRuleset())); + addCircleStep(createScore(0.99, new CatchRuleset())); + addCircleStep(createScore(1, new CatchRuleset())); + } + + private void addCircleStep(ScoreInfo score) => AddStep($"add panel ({score.DisplayAccuracy})", () => { Children = new Drawable[] { @@ -73,28 +93,35 @@ namespace osu.Game.Tests.Visual.Ranking }; }); - private ScoreInfo createScore(double accuracy, ScoreRank rank) => new ScoreInfo + private ScoreInfo createScore(double accuracy, Ruleset ruleset) { - User = new APIUser - { - Id = 2, - Username = "peppy", - }, - BeatmapInfo = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, - Ruleset = new OsuRuleset().RulesetInfo, - Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, - TotalScore = 2845370, - Accuracy = accuracy, - MaxCombo = 999, - Rank = rank, - Date = DateTimeOffset.Now, - Statistics = + var scoreProcessor = ruleset.CreateScoreProcessor(); + + var statistics = new Dictionary { { HitResult.Miss, 1 }, { HitResult.Meh, 50 }, { HitResult.Good, 100 }, { HitResult.Great, 300 }, - } - }; + }; + + return new ScoreInfo + { + User = new APIUser + { + Id = 2, + Username = "peppy", + }, + BeatmapInfo = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, + Ruleset = ruleset.RulesetInfo, + Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, + TotalScore = 2845370, + Accuracy = accuracy, + MaxCombo = 999, + Rank = scoreProcessor.RankFromScore(accuracy, statistics), + Date = DateTimeOffset.Now, + Statistics = statistics, + }; + } } } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs index a40cb41e2c..325a535731 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestAroundCentre() { - createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList()); + createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList()); } [Test] @@ -57,12 +57,12 @@ namespace osu.Game.Tests.Visual.Ranking { createTest(new List { - new HitEvent(-7, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(-6, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(-5, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(5, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(6, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(7, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(-7, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(-6, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(-5, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(5, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(6, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(7, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), }); } @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Ranking : offset > 16 ? HitResult.Good : offset > 8 ? HitResult.Great : HitResult.Perfect; - return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null); + return new HitEvent(h.TimeOffset, 1.0, result, placeholder_object, placeholder_object, null); }).ToList()); } @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Ranking : offset > 8 ? HitResult.Great : HitResult.Perfect; - return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null); + return new HitEvent(h.TimeOffset, 1.0, result, placeholder_object, placeholder_object, null); }); var narrow = CreateDistributedHitEvents(0, 50).Select(h => { @@ -106,7 +106,7 @@ namespace osu.Game.Tests.Visual.Ranking : offset > 10 ? HitResult.Good : offset > 5 ? HitResult.Great : HitResult.Perfect; - return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null); + return new HitEvent(h.TimeOffset, 1.0, result, placeholder_object, placeholder_object, null); }); createTest(wide.Concat(narrow).ToList()); } @@ -114,7 +114,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestZeroTimeOffset() { - createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList()); + createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList()); } [Test] @@ -129,9 +129,9 @@ namespace osu.Game.Tests.Visual.Ranking createTest(Enumerable.Range(0, 100).Select(i => { if (i % 2 == 0) - return new HitEvent(0, HitResult.Perfect, placeholder_object, placeholder_object, null); + return new HitEvent(0, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null); - return new HitEvent(30, HitResult.Miss, placeholder_object, placeholder_object, null); + return new HitEvent(30, 1.0, HitResult.Miss, placeholder_object, placeholder_object, null); }).ToList()); } @@ -162,7 +162,7 @@ namespace osu.Game.Tests.Visual.Ranking int count = (int)(Math.Pow(range - Math.Abs(i - range), 2)) / 10; for (int j = 0; j < count; j++) - hitEvents.Add(new HitEvent(centre + i - range, HitResult.Perfect, placeholder_object, placeholder_object, null)); + hitEvents.Add(new HitEvent(centre + i - range, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null)); } return hitEvents; diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 146482e6fb..866e20d063 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -21,6 +21,7 @@ using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; @@ -71,15 +72,16 @@ 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) + [TestCase(1, ScoreRank.X, 0)] + [TestCase(0.9999, ScoreRank.S, 0)] + [TestCase(0.975, ScoreRank.S, 0)] + [TestCase(0.975, ScoreRank.A, 1)] + [TestCase(0.925, ScoreRank.A, 5)] + [TestCase(0.85, ScoreRank.B, 9)] + [TestCase(0.75, ScoreRank.C, 11)] + [TestCase(0.5, ScoreRank.D, 21)] + [TestCase(0.2, ScoreRank.D, 51)] + public void TestResultsWithPlayer(double accuracy, ScoreRank rank, int missCount) { TestResultsScreen screen = null; @@ -91,6 +93,7 @@ namespace osu.Game.Tests.Visual.Ranking score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); score.Accuracy = accuracy; score.Rank = rank; + score.Statistics[HitResult.Miss] = missCount; return screen = createResultsScreen(score); }); @@ -362,7 +365,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); } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 93005271a9..d0a45856b2 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -203,6 +203,7 @@ namespace osu.Game.Tests.Visual.Ranking public IBeatmap Beatmap { get; } + // ReSharper disable once NotNullOrRequiredMemberIsNotInitialized public TestBeatmapConverter(IBeatmap beatmap) { Beatmap = beatmap; diff --git a/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs b/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs new file mode 100644 index 0000000000..85cde966b1 --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Configuration; +using osu.Game.Overlays.Settings.Sections.Audio; +using osu.Game.Scoring; +using osu.Game.Tests.Visual.Ranking; + +namespace osu.Game.Tests.Visual.Settings +{ + public partial class TestSceneAudioOffsetAdjustControl : OsuTestScene + { + [Resolved] + private SessionStatics statics { get; set; } = null!; + + [Cached] + private SessionAverageHitErrorTracker tracker = new SessionAverageHitErrorTracker(); + + private Container content = null!; + protected override Container Content => content; + + private OsuConfigManager localConfig = null!; + private AudioOffsetAdjustControl adjustControl = null!; + + [BackgroundDependencyLoader] + private void load() + { + localConfig = new OsuConfigManager(LocalStorage); + Dependencies.CacheAs(localConfig); + + base.Content.AddRange(new Drawable[] + { + tracker, + content = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 400, + AutoSizeAxes = Axes.Y + } + }); + } + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = adjustControl = new AudioOffsetAdjustControl + { + Current = localConfig.GetBindable(OsuSetting.AudioOffset), + }; + + localConfig.SetValue(OsuSetting.AudioOffset, 0.0); + tracker.ClearHistory(); + }); + + [Test] + public void TestDisplay() + { + AddStep("set new score", () => statics.SetValue(Static.LastLocalUserScore, new ScoreInfo + { + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(RNG.NextDouble(-100, 100)), + BeatmapInfo = Beatmap.Value.BeatmapInfo, + })); + AddStep("clear history", () => tracker.ClearHistory()); + } + + [Test] + public void TestBehaviour() + { + AddStep("set score with -20ms", () => setScore(-20)); + AddAssert("suggested global offset is 20ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(20)); + AddStep("clear history", () => tracker.ClearHistory()); + + AddStep("set score with 40ms", () => setScore(40)); + AddAssert("suggested global offset is -40ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(-40)); + AddStep("clear history", () => tracker.ClearHistory()); + } + + [Test] + public void TestNonZeroGlobalOffset() + { + AddStep("set global offset to -20ms", () => localConfig.SetValue(OsuSetting.AudioOffset, -20.0)); + AddStep("set score with -20ms", () => setScore(-20)); + AddAssert("suggested global offset is 0ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(0)); + AddStep("clear history", () => tracker.ClearHistory()); + + AddStep("set global offset to 20ms", () => localConfig.SetValue(OsuSetting.AudioOffset, 20.0)); + AddStep("set score with 40ms", () => setScore(40)); + AddAssert("suggested global offset is -20ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(-20)); + AddStep("clear history", () => tracker.ClearHistory()); + } + + [Test] + public void TestMultiplePlays() + { + AddStep("set score with -20ms", () => setScore(-20)); + AddStep("set score with -10ms", () => setScore(-10)); + AddAssert("suggested global offset is 15ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(15)); + AddStep("clear history", () => tracker.ClearHistory()); + + AddStep("set score with -20ms", () => setScore(-20)); + AddStep("set global offset to 30ms", () => localConfig.SetValue(OsuSetting.AudioOffset, 30.0)); + AddStep("set score with 10ms", () => setScore(10)); + AddAssert("suggested global offset is 20ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(20)); + AddStep("clear history", () => tracker.ClearHistory()); + } + + private void setScore(double averageHitError) + { + statics.SetValue(Static.LastLocalUserScore, new ScoreInfo + { + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(averageHitError), + BeatmapInfo = Beatmap.Value.BeatmapInfo, + }); + } + + protected override void Dispose(bool isDisposing) + { + if (localConfig.IsNotNull()) + localConfig.Dispose(); + + base.Dispose(isDisposing); + } + } +} 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 449ca0f258..57c9770c9a 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -10,8 +10,11 @@ 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.Sections.Input; +using osu.Game.Rulesets.Taiko; using osuTK.Input; namespace osu.Game.Tests.Visual.Settings @@ -148,13 +151,15 @@ namespace osu.Game.Tests.Visual.Settings AddStep("click first row with two bindings", () => { multiBindingRow = panel.ChildrenOfType().First(row => row.Defaults.Count() > 1); - InputManager.MoveMouseTo(multiBindingRow); + InputManager.MoveMouseTo(multiBindingRow.ChildrenOfType().First()); InputManager.Click(MouseButton.Left); }); 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 +171,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() { @@ -207,7 +214,7 @@ namespace osu.Game.Tests.Visual.Settings 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] @@ -237,7 +244,7 @@ namespace osu.Game.Tests.Visual.Settings 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] @@ -248,7 +255,7 @@ namespace osu.Game.Tests.Visual.Settings AddStep("click first row with two bindings", () => { multiBindingRow = panel.ChildrenOfType().First(row => row.Defaults.Count() > 1); - InputManager.MoveMouseTo(multiBindingRow); + InputManager.MoveMouseTo(multiBindingRow.ChildrenOfType().First()); InputManager.Click(MouseButton.Left); }); @@ -288,6 +295,102 @@ 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)); + 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)); + 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)); + 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)); + 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/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs index ec0ad685c5..ee0c64aa3f 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs @@ -40,8 +40,15 @@ namespace osu.Game.Tests.Visual.Settings AddStep("change value from default", () => textBox.Current.Value = "non-default"); AddUntilStep("restore button shown", () => revertToDefaultButton.Alpha > 0); + AddStep("disable setting", () => textBox.Current.Disabled = true); + AddUntilStep("restore button still shown", () => revertToDefaultButton.Alpha > 0); + + AddStep("enable setting", () => textBox.Current.Disabled = false); AddStep("restore default", () => textBox.Current.SetDefault()); AddUntilStep("restore button hidden", () => revertToDefaultButton.Alpha == 0); + + AddStep("disable setting", () => textBox.Current.Disabled = true); + AddUntilStep("restore button still hidden", () => revertToDefaultButton.Alpha == 0); } [Test] @@ -66,20 +73,20 @@ namespace osu.Game.Tests.Visual.Settings }); 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)); + 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(revertToDefaultButton.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(revertToDefaultButton.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(revertToDefaultButton.Parent.DrawHeight, control.DrawHeight, 1)); + AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(revertToDefaultButton.Parent!.DrawHeight, control.DrawHeight, 1)); } /// diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs index 24c2eee783..df0fc8de57 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs @@ -41,6 +41,7 @@ namespace osu.Game.Tests.Visual.Settings public void TestBasic() { AddStep("do nothing", () => { }); + AddToggleStep("toggle visibility", visible => settings.State.Value = visible ? Visibility.Visible : Visibility.Hidden); } [Test] @@ -49,12 +50,12 @@ namespace osu.Game.Tests.Visual.Settings AddStep("reset mouse", () => InputManager.MoveMouseTo(settings)); if (beforeLoad) - AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType().First().Current.Value = "scaling"); + AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType().First().Current.Value = "scaling"); AddUntilStep("wait for items to load", () => settings.SectionsContainer.ChildrenOfType().Any()); if (!beforeLoad) - AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType().First().Current.Value = "scaling"); + AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType().First().Current.Value = "scaling"); AddAssert("ensure all items match filter", () => settings.SectionsContainer .ChildrenOfType().Where(f => f.IsPresent) @@ -76,7 +77,7 @@ namespace osu.Game.Tests.Visual.Settings AddUntilStep("wait for items to load", () => settings.SectionsContainer.ChildrenOfType().Any()); - AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType().First().Current.Value = "scaling"); + AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType().First().Current.Value = "scaling"); } [Test] @@ -94,7 +95,7 @@ namespace osu.Game.Tests.Visual.Settings AddStep("reset mouse", () => InputManager.MoveMouseTo(settings)); AddUntilStep("sections loaded", () => settings.SectionsContainer.Children.Count > 0); - AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType().FirstOrDefault()?.HasFocus == true); + AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType().FirstOrDefault()?.HasFocus == true); AddStep("open key binding subpanel", () => { @@ -106,13 +107,13 @@ namespace osu.Game.Tests.Visual.Settings AddUntilStep("binding panel textbox focused", () => settings .ChildrenOfType().FirstOrDefault()? - .ChildrenOfType().FirstOrDefault()?.HasFocus == true); + .ChildrenOfType().FirstOrDefault()?.HasFocus == true); AddStep("Press back", () => settings .ChildrenOfType().FirstOrDefault()? - .ChildrenOfType().FirstOrDefault()?.TriggerClick()); + .ChildrenOfType().FirstOrDefault()?.TriggerClick()); - AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType().FirstOrDefault()?.HasFocus == true); + AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType().FirstOrDefault()?.HasFocus == true); } [Test] @@ -121,7 +122,7 @@ namespace osu.Game.Tests.Visual.Settings AddStep("reset mouse", () => InputManager.MoveMouseTo(settings)); AddUntilStep("sections loaded", () => settings.SectionsContainer.Children.Count > 0); - AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType().FirstOrDefault()?.HasFocus == true); + AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType().FirstOrDefault()?.HasFocus == true); AddStep("open key binding subpanel", () => { @@ -133,11 +134,22 @@ namespace osu.Game.Tests.Visual.Settings AddUntilStep("binding panel textbox focused", () => settings .ChildrenOfType().FirstOrDefault()? - .ChildrenOfType().FirstOrDefault()?.HasFocus == true); + .ChildrenOfType().FirstOrDefault()?.HasFocus == true); AddStep("Escape", () => InputManager.Key(Key.Escape)); - AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType().FirstOrDefault()?.HasFocus == true); + AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType().FirstOrDefault()?.HasFocus == true); + } + + [Test] + public void TestSearchTextBoxSelectedOnShow() + { + SettingsSearchTextBox 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] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs index 8650119dd4..4bb2b557ff 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs @@ -14,7 +14,9 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Select.Details; using osuTK.Graphics; @@ -38,6 +40,12 @@ namespace osu.Game.Tests.Visual.SongSelect Width = 500 }); + [SetUpSteps] + public void SetUpSteps() + { + AddStep("reset game ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo); + } + private BeatmapInfo exampleBeatmapInfo => new BeatmapInfo { Ruleset = rulesets.AvailableRulesets.First(), @@ -66,8 +74,10 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - public void TestManiaFirstBarText() + public void TestManiaFirstBarTextManiaBeatmap() { + AddStep("set game ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo); + AddStep("set beatmap", () => advancedStats.BeatmapInfo = new BeatmapInfo { Ruleset = rulesets.GetRuleset(3) ?? throw new InvalidOperationException("osu!mania ruleset not found"), @@ -84,6 +94,27 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("first bar text is correct", () => advancedStats.ChildrenOfType().First().Text == BeatmapsetsStrings.ShowStatsCsMania); } + [Test] + public void TestManiaFirstBarTextConvert() + { + AddStep("set game ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo); + + AddStep("set beatmap", () => advancedStats.BeatmapInfo = new BeatmapInfo + { + Ruleset = new OsuRuleset().RulesetInfo, + Difficulty = new BeatmapDifficulty + { + CircleSize = 5, + DrainRate = 4.3f, + OverallDifficulty = 4.5f, + ApproachRate = 3.1f + }, + StarRating = 8 + }); + + AddAssert("first bar text is correct", () => advancedStats.ChildrenOfType().First().Text == BeatmapsetsStrings.ShowStatsCsMania); + } + [Test] public void TestEasyMod() { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 61f95dc628..aa4c879468 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,23 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false)); } + [Test] + public void TestRewind() + { + const int local_set_count = 3; + const int random_select_count = local_set_count * 3; + loadBeatmaps(setCount: local_set_count); + + for (int i = 0; i < random_select_count; i++) + nextRandom(); + + for (int i = 0; i < random_select_count; i++) + { + prevRandom(); + AddAssert("correct random last selected", () => selectedSets.Peek(), () => Is.EqualTo(carousel.SelectedBeatmapSet)); + } + } + [Test] public void TestRewindToDeletedBeatmap() { @@ -501,6 +519,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() { @@ -662,7 +708,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(); @@ -709,7 +755,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(); @@ -758,32 +804,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] @@ -838,7 +906,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(); @@ -885,7 +953,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++) { @@ -907,7 +975,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); @@ -930,7 +998,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); @@ -955,6 +1023,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() { @@ -1081,20 +1186,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..fd102da026 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; @@ -14,7 +15,9 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Extensions; 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,11 +191,41 @@ 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; }); } + [TestCase] + public void TestLengthUpdates() + { + IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); + double drain = beatmap.CalculateDrainLength(); + beatmap.BeatmapInfo.Length = drain; + + OsuModDoubleTime doubleTime = null; + + selectBeatmap(beatmap); + checkDisplayedLength(drain); + + AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() }); + checkDisplayedLength(Math.Round(drain / 1.5f)); + + AddStep("change DT rate", () => doubleTime.SpeedChange.Value = 2); + checkDisplayedLength(Math.Round(drain / 2)); + } + + private void checkDisplayedLength(double drain) + { + var displayedLength = drain.ToFormattedDuration(); + + AddUntilStep($"check map drain ({displayedLength})", () => + { + var label = infoWedge.DisplayedContent.ChildrenOfType().Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsTotalLength(displayedLength)); + return label.Statistic.Content == displayedLength.ToString(); + }); + } + private void setRuleset(RulesetInfo rulesetInfo) { Container containerBefore = null; 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/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 00a0d4a849..a639d50eee 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -76,6 +76,20 @@ namespace osu.Game.Tests.Visual.SongSelect assertCollectionDropdownContains("2"); } + [Test] + public void TestCollectionsCleared() + { + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3")))); + + AddAssert("check count 5", () => control.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); + + AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll())); + + AddAssert("check count 2", () => control.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); + } + [Test] public void TestCollectionRemovedFromDropdown() { @@ -192,7 +206,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select collection", () => { - InputManager.MoveMouseTo(getCollectionDropdownItems().ElementAt(1)); + InputManager.MoveMouseTo(getCollectionDropdownItemAt(1)); InputManager.Click(MouseButton.Left); }); @@ -206,7 +220,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("click manage collections filter", () => { - InputManager.MoveMouseTo(getCollectionDropdownItems().Last()); + int lastItemIndex = control.ChildrenOfType().Single().Items.Count() - 1; + InputManager.MoveMouseTo(getCollectionDropdownItemAt(lastItemIndex)); InputManager.Click(MouseButton.Left); }); @@ -232,10 +247,10 @@ namespace osu.Game.Tests.Visual.SongSelect private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) => AddUntilStep($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'", // A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872 - () => shouldContain == (getCollectionDropdownItems().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName))); + () => shouldContain == control.ChildrenOfType().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName)); private IconButton getAddOrRemoveButton(int index) - => getCollectionDropdownItems().ElementAt(index).ChildrenOfType().Single(); + => getCollectionDropdownItemAt(index).ChildrenOfType().Single(); private void addExpandHeaderStep() => AddStep("expand header", () => { @@ -249,7 +264,11 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Click(MouseButton.Left); }); - private IEnumerable.DropdownMenu.DrawableDropdownMenuItem> getCollectionDropdownItems() - => control.ChildrenOfType().Single().ChildrenOfType.DropdownMenu.DrawableDropdownMenuItem>(); + private Menu.DrawableMenuItem getCollectionDropdownItemAt(int index) + { + // todo: we should be able to use Items, but apparently that's not guaranteed to be ordered... see: https://github.com/ppy/osu-framework/pull/6079 + CollectionFilterMenuItem item = control.ChildrenOfType().Single().ItemSource.ElementAt(index); + return control.ChildrenOfType().Single(i => i.Item.Text.Value == item.CollectionName); + } } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 0bff40f258..ce241f3676 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; @@ -21,10 +22,10 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Extensions; -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; @@ -311,7 +312,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 +323,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); @@ -415,6 +420,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] + [Ignore("temporary while peppy investigates. probably realm batching related.")] public void TestSelectionRetainedOnBeatmapUpdate() { createSongSelect(); @@ -459,7 +465,7 @@ namespace osu.Game.Tests.Visual.SongSelect manager.Import(testBeatmapSetInfo); }, 10); - AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID); + AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID, () => Is.EqualTo(originalOnlineSetID)); Task?> updateTask = null!; @@ -471,7 +477,7 @@ namespace osu.Game.Tests.Visual.SongSelect }); AddUntilStep("wait for update completion", () => updateTask.IsCompleted); - AddUntilStep("retained selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID); + AddUntilStep("retained selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID, () => Is.EqualTo(originalOnlineSetID)); } [Test] @@ -574,6 +580,24 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("start not requested", () => !startRequested); } + [Test] + public void TestSearchTextWithRulesetCriteria() + { + createSongSelect(); + + addRulesetImportStep(0); + + AddStep("disallow convert display", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); + + AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo != null); + + AddStep("set filter to match all", () => songSelect!.FilterControl.CurrentTextSearch.Value = "Some"); + + changeRuleset(1); + + AddUntilStep("has no selection", () => songSelect!.Carousel.SelectedBeatmapInfo == null); + } + [TestCase(false)] [TestCase(true)] public void TestExternalBeatmapChangeWhileFiltered(bool differentRuleset) @@ -590,7 +614,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo != null); - AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType().First().Text = "nonono"); + AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType().First().Text = "nonono"); AddUntilStep("dummy selected", () => Beatmap.Value is DummyWorkingBeatmap); @@ -624,7 +648,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("carousel has correct", () => songSelect!.Carousel.SelectedBeatmapInfo?.MatchesOnlineID(target) == true); AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(target)); - AddStep("reset filter text", () => songSelect!.FilterControl.ChildrenOfType().First().Text = string.Empty); + AddStep("reset filter text", () => songSelect!.FilterControl.ChildrenOfType().First().Text = string.Empty); AddAssert("game still correct", () => Beatmap.Value?.BeatmapInfo.MatchesOnlineID(target) == true); AddAssert("carousel still correct", () => songSelect!.Carousel.SelectedBeatmapInfo.MatchesOnlineID(target)); @@ -642,7 +666,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo != null); - AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType().First().Text = "nonono"); + AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType().First().Text = "nonono"); AddUntilStep("dummy selected", () => Beatmap.Value is DummyWorkingBeatmap); @@ -665,7 +689,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("carousel has correct", () => songSelect!.Carousel.SelectedBeatmapInfo?.MatchesOnlineID(target) == true); AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(target)); - AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType().First().Text = "nononoo"); + AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType().First().Text = "nononoo"); AddUntilStep("game lost selection", () => Beatmap.Value is DummyWorkingBeatmap); AddAssert("carousel lost selection", () => songSelect!.Carousel.SelectedBeatmapInfo == null); @@ -1087,6 +1111,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); 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/UserInterface/TestSceneButtonSystem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs index ac811aeb65..8f72be37df 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs @@ -67,14 +67,15 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("Enter mode", performEnterMode); } - [TestCase(Key.P, true)] - [TestCase(Key.M, true)] - [TestCase(Key.L, true)] - [TestCase(Key.E, false)] - [TestCase(Key.D, false)] - [TestCase(Key.Q, false)] - [TestCase(Key.O, false)] - public void TestShortcutKeys(Key key, bool entersPlay) + [TestCase(Key.P, Key.P)] + [TestCase(Key.M, Key.P)] + [TestCase(Key.L, Key.P)] + [TestCase(Key.B, Key.E)] + [TestCase(Key.S, Key.E)] + [TestCase(Key.D, null)] + [TestCase(Key.Q, null)] + [TestCase(Key.O, null)] + public void TestShortcutKeys(Key key, Key? subMenuEnterKey) { int activationCount = -1; AddStep("set up action", () => @@ -96,8 +97,12 @@ namespace osu.Game.Tests.Visual.UserInterface buttons.OnPlaylists = action; break; - case Key.E: - buttons.OnEdit = action; + case Key.B: + buttons.OnEditBeatmap = action; + break; + + case Key.S: + buttons.OnEditSkin = action; break; case Key.D: @@ -117,10 +122,10 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep($"press {key}", () => InputManager.Key(key)); AddAssert("state is top level", () => buttons.State == ButtonSystemState.TopLevel); - if (entersPlay) + if (subMenuEnterKey != null) { - AddStep("press P", () => InputManager.Key(Key.P)); - AddAssert("state is play", () => buttons.State == ButtonSystemState.Play); + AddStep($"press {subMenuEnterKey}", () => InputManager.Key(subMenuEnterKey.Value)); + AddAssert("state is not top menu", () => buttons.State != ButtonSystemState.TopLevel); } AddStep($"press {key}", () => InputManager.Key(key)); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs index 81b692004b..f2313022ec 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs @@ -9,6 +9,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; @@ -19,11 +20,15 @@ namespace osu.Game.Tests.Visual.UserInterface { private DialogOverlay overlay; + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay()); + } + [Test] public void TestBasic() { - AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay()); - TestPopupDialog firstDialog = null; TestPopupDialog secondDialog = null; @@ -84,7 +89,31 @@ namespace osu.Game.Tests.Visual.UserInterface })); AddAssert("second dialog displayed", () => overlay.CurrentDialog == secondDialog); - AddAssert("first dialog is not part of hierarchy", () => firstDialog.Parent == null); + AddUntilStep("first dialog is not part of hierarchy", () => firstDialog.Parent == null); + } + + [Test] + public void TestTooMuchText() + { + AddStep("dialog #1", () => overlay.Push(new TestPopupDialog + { + Icon = FontAwesome.Regular.TrashAlt, + HeaderText = @"Confirm deletion ofConfirm deletion ofConfirm deletion ofConfirm deletion ofConfirm deletion ofConfirm deletion of", + BodyText = @"Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver. ", + Buttons = new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = @"I never want to see this again.", + Action = () => Console.WriteLine(@"OK"), + }, + new PopupDialogCancelButton + { + Text = @"Firetruck, I still want quick ranks!", + Action = () => Console.WriteLine(@"Cancel"), + }, + }, + })); } [Test] @@ -92,7 +121,7 @@ namespace osu.Game.Tests.Visual.UserInterface { PopupDialog dialog = null; - AddStep("create dialog overlay", () => overlay = new SlowLoadingDialogOverlay()); + AddStep("create slow loading dialog overlay", () => overlay = new SlowLoadingDialogOverlay()); AddStep("start loading overlay", () => LoadComponentAsync(overlay, Add)); @@ -128,8 +157,6 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestDismissBeforePush() { - AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay()); - TestPopupDialog testDialog = null; AddStep("dismissed dialog push", () => { @@ -146,8 +173,6 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestDismissBeforePushViaButtonPress() { - AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay()); - TestPopupDialog testDialog = null; AddStep("dismissed dialog push", () => { @@ -163,7 +188,7 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddAssert("no dialog pushed", () => overlay.CurrentDialog == null); - AddAssert("dialog is not part of hierarchy", () => testDialog.Parent == null); + AddUntilStep("dialog is not part of hierarchy", () => testDialog.Parent == null); } private partial class TestPopupDialog : PopupDialog diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs index 4e1bf1390a..a95bb2c9e3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs @@ -9,6 +9,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Select; +using osu.Game.Utils; namespace osu.Game.Tests.Visual.UserInterface { @@ -74,7 +75,7 @@ namespace osu.Game.Tests.Visual.UserInterface private bool assertModsMultiplier(IEnumerable mods) { double multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); - string expectedValue = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; + string expectedValue = multiplier == 1 ? string.Empty : ModUtils.FormatScoreMultiplier(multiplier).ToString(); return expectedValue == footerButtonMods.MultiplierText.Current.Value; } 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/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 1779b240cc..b7c1428397 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.UserInterface } [Test] - public void TestAddingFlow() + public void TestAddingFlow([Values] bool withSystemModActive) { ModPresetColumn modPresetColumn = null!; @@ -181,7 +181,13 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded); AddAssert("add preset button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); - AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModDaycore(), new OsuModClassic() }); + AddStep("set mods", () => + { + var newMods = new Mod[] { new OsuModDaycore(), new OsuModClassic() }; + if (withSystemModActive) + newMods = newMods.Append(new OsuModTouchDevice()).ToArray(); + SelectedMods.Value = newMods; + }); AddAssert("add preset button enabled", () => this.ChildrenOfType().Single().Enabled.Value); AddStep("click add preset button", () => @@ -209,6 +215,9 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddUntilStep("popover closed", () => !this.ChildrenOfType().Any()); AddUntilStep("preset creation occurred", () => this.ChildrenOfType().Count() == 4); + AddAssert("preset has correct mods", + () => this.ChildrenOfType().Single(panel => panel.Preset.Value.Name == "new preset").Preset.Value.Mods, + () => Has.Count.EqualTo(2)); AddStep("click add preset button", () => { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs index 35e352534b..c79cbd3691 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs @@ -86,6 +86,10 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("set mods to HD+HR+DT", () => SelectedMods.Value = new Mod[] { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() }); AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value); + + // system mods are not included in presets. + AddStep("set mods to HR+DT+TD", () => SelectedMods.Value = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime(), new OsuModTouchDevice() }); + AddAssert("panel is active", () => panel.AsNonNull().Active.Value); } [Test] @@ -113,6 +117,10 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("set customised mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } }); AddStep("activate panel", () => panel.AsNonNull().TriggerClick()); assertSelectedModsEquivalentTo(new Mod[] { new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } }); + + AddStep("set system mod", () => SelectedMods.Value = new[] { new OsuModTouchDevice() }); + AddStep("activate panel", () => panel.AsNonNull().TriggerClick()); + assertSelectedModsEquivalentTo(new Mod[] { new OsuModTouchDevice(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } }); } private void assertSelectedModsEquivalentTo(IEnumerable mods) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index ad79865ad9..046954db47 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -38,6 +39,9 @@ namespace osu.Game.Tests.Visual.UserInterface private TestModSelectOverlay modSelectOverlay = null!; + [Resolved] + private OsuConfigManager configManager { get; set; } = null!; + [BackgroundDependencyLoader] private void load() { @@ -51,6 +55,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 => @@ -92,6 +97,7 @@ namespace osu.Game.Tests.Visual.UserInterface { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, + Beatmap = Beatmap.Value, SelectedMods = { BindTarget = SelectedMods } }); waitForColumnLoad(); @@ -113,7 +119,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 +134,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()); @@ -536,10 +542,23 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("press enter", () => InputManager.Key(Key.Enter)); AddAssert("hidden selected", () => getPanelForMod(typeof(OsuModHidden)).Active.Value); + AddAssert("all text selected in textbox", () => + { + var textBox = modSelectOverlay.ChildrenOfType().Single(); + return textBox.SelectedText == textBox.Text; + }); AddStep("press enter again", () => InputManager.Key(Key.Enter)); AddAssert("hidden deselected", () => !getPanelForMod(typeof(OsuModHidden)).Active.Value); + AddStep("apply search matching nothing", () => modSelectOverlay.SearchTerm = "ZZZ"); + AddStep("press enter", () => InputManager.Key(Key.Enter)); + AddAssert("all text not selected in textbox", () => + { + var textBox = modSelectOverlay.ChildrenOfType().Single(); + return textBox.SelectedText != textBox.Text; + }); + AddStep("clear search", () => modSelectOverlay.SearchTerm = string.Empty); AddStep("press enter", () => InputManager.Key(Key.Enter)); AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden); @@ -564,17 +583,58 @@ namespace osu.Game.Tests.Visual.UserInterface } [Test] - public void TestSearchFocusChangeViaKey() + public void TestTextSearchActiveByDefault() { + AddStep("text search starts active", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true)); createScreen(); - const Key focus_switch_key = Key.Tab; + AddUntilStep("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus); - AddStep("press tab", () => InputManager.Key(focus_switch_key)); - AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus); + AddStep("press tab", () => InputManager.Key(Key.Tab)); + AddAssert("search text box unfocused", () => !modSelectOverlay.SearchTextBox.HasFocus); - AddStep("press tab", () => InputManager.Key(focus_switch_key)); - AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus); + AddStep("press tab", () => InputManager.Key(Key.Tab)); + AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus); + } + + [Test] + public void TestTextSearchNotActiveByDefault() + { + AddStep("text search does not start active", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, false)); + createScreen(); + + AddUntilStep("search text box not focused", () => !modSelectOverlay.SearchTextBox.HasFocus); + + AddStep("press tab", () => InputManager.Key(Key.Tab)); + AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus); + + AddStep("press tab", () => InputManager.Key(Key.Tab)); + AddAssert("search text box unfocused", () => !modSelectOverlay.SearchTextBox.HasFocus); + } + + [Test] + public void TestTextSearchDoesNotBlockCustomisationPanelKeyboardInteractions() + { + AddStep("text search starts active", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true)); + createScreen(); + + AddUntilStep("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus); + + AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() }); + AddAssert("DT selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value), () => Is.EqualTo(1)); + + AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick()); + assertCustomisationToggleState(false, true); + AddStep("hover over mod settings slider", () => + { + var slider = modSelectOverlay.ChildrenOfType().Single().ChildrenOfType>().First(); + InputManager.MoveMouseTo(slider); + }); + AddStep("press right arrow", () => InputManager.PressKey(Key.Right)); + AddAssert("DT speed changed", () => !SelectedMods.Value.OfType().Single().SpeedChange.IsDefault); + + AddStep("close customisation area", () => InputManager.PressKey(Key.Escape)); + AddUntilStep("search text box reacquired focus", () => modSelectOverlay.SearchTextBox.HasFocus); } [Test] @@ -785,7 +845,8 @@ 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.1).Within(Precision.DOUBLE_EPSILON)); // 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. @@ -794,11 +855,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)); + AddUntilStep("difficulty multiplier display shows correct value", + () => modSelectOverlay.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON)); } - 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) { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs index 2d953fe9ad..b7cf3fef9b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs @@ -2,16 +2,19 @@ // 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.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; @@ -34,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)); @@ -43,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) @@ -62,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)) }) }; @@ -81,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 4d3ae079e3..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(() => { @@ -60,6 +64,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep(@"simple #2", sendAmazingNotification); AddStep(@"progress #1", sendUploadProgress); AddStep(@"progress #2", sendDownloadProgress); + AddStep(@"User notification", sendUserNotification); checkProgressingCount(2); @@ -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/TestSceneOsuDropdown.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs index b0548d7e9f..1678890b73 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs @@ -1,9 +1,17 @@ // 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.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Framework.Input.States; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; namespace osu.Game.Tests.Visual.UserInterface { @@ -13,8 +21,29 @@ namespace osu.Game.Tests.Visual.UserInterface new OsuEnumDropdown { Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Origin = Anchor.TopCentre, Width = 150 }; + + [Test] + // todo: this can be written much better if ThemeComparisonTestScene has a manual input manager + public void TestBackAction() + { + AddStep("open", () => dropdown().ChildrenOfType().Single().Open()); + AddStep("press back", () => dropdown().OnPressed(new KeyBindingPressEvent(new InputState(), GlobalAction.Back))); + AddAssert("closed", () => dropdown().ChildrenOfType().Single().State == MenuState.Closed); + + AddStep("open", () => dropdown().ChildrenOfType().Single().Open()); + AddStep("type something", () => dropdown().ChildrenOfType().Single().SearchTerm.Value = "something"); + AddAssert("search bar visible", () => dropdown().ChildrenOfType().Single().State.Value == Visibility.Visible); + AddStep("press back", () => dropdown().OnPressed(new KeyBindingPressEvent(new InputState(), GlobalAction.Back))); + AddAssert("text clear", () => dropdown().ChildrenOfType().Single().SearchTerm.Value == string.Empty); + AddAssert("search bar hidden", () => dropdown().ChildrenOfType().Single().State.Value == Visibility.Hidden); + AddAssert("still open", () => dropdown().ChildrenOfType().Single().State == MenuState.Open); + AddStep("press back", () => dropdown().OnPressed(new KeyBindingPressEvent(new InputState(), GlobalAction.Back))); + AddAssert("closed", () => dropdown().ChildrenOfType().Single().State == MenuState.Closed); + + OsuEnumDropdown dropdown() => this.ChildrenOfType>().First(); + } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs index a927b0931b..5db7223bdf 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using osu.Framework.Allocation; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; using osuTK.Graphics; namespace osu.Game.Tests.Visual.UserInterface @@ -153,7 +154,7 @@ namespace osu.Game.Tests.Visual.UserInterface public TestTitle() { Title = "title"; - IconTexture = "Icons/changelog"; + Icon = OsuIcon.ChangelogB; } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs index 9537ab63be..96d19911bd 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.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 NUnit.Framework; -using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; using osu.Game.Overlays.Dialog; @@ -15,24 +12,25 @@ namespace osu.Game.Tests.Visual.UserInterface { public partial class TestScenePopupDialog : OsuManualInputManagerTestScene { - private TestPopupDialog dialog; + private TestPopupDialog dialog = null!; [SetUpSteps] public void SetUpSteps() { AddStep("new popup", () => { - Add(dialog = new TestPopupDialog + Child = dialog = new TestPopupDialog { - RelativeSizeAxes = Axes.Both, State = { Value = Framework.Graphics.Containers.Visibility.Visible }, - }); + }; }); } [Test] public void TestDangerousButton([Values(false, true)] bool atEdge) { + AddStep("finish transforms", () => dialog.FinishTransforms(true)); + if (atEdge) { AddStep("move mouse to button edge", () => 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/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/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/TestSceneVolumePieces.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneVolumePieces.cs index 311bae0d50..b85e4c19d1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneVolumePieces.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneVolumePieces.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual.UserInterface { VolumeMeter meter; MuteButton mute; - Add(meter = new VolumeMeter("MASTER", 125, Color4.Blue) { Position = new Vector2(10) }); + Add(meter = new VolumeMeter("MASTER", 125, Color4.Green) { Position = new Vector2(10) }); AddSliderStep("master volume", 0, 10, 0, i => meter.Bindable.Value = i * 0.1); Add(new VolumeMeter("BIG", 250, Color4.Red) @@ -22,6 +22,15 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.Centre, Origin = Anchor.Centre, Position = new Vector2(10), + Margin = new MarginPadding { Left = 250 }, + }); + + Add(new VolumeMeter("SML", 125, Color4.Blue) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(10), + Margin = new MarginPadding { Right = 500 }, }); Add(mute = new MuteButton diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 59a786a11d..7b08524240 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/TestSceneDrawableTournamentTeam.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs index a809d0747a..43adcc61bf 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.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.Bindables; using osu.Framework.Graphics; using osu.Game.Tests.Visual; @@ -14,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" }, @@ -30,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/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/TestSceneTournamentMatchChatDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs index 044e5ebbbf..de91a66e56 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs @@ -4,7 +4,6 @@ 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; @@ -80,11 +79,11 @@ namespace osu.Game.Tournament.Tests.Components { Team1 = { - Value = new TournamentTeam { Players = new BindableList { redUser } } + Value = new TournamentTeam { Players = { redUser } } }, Team2 = { - Value = new TournamentTeam { Players = new BindableList { blueUser, blueUserWithCustomColour } } + Value = new TournamentTeam { Players = { blueUser, blueUserWithCustomColour } } } }); 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/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 962a6fbd6c..acb4a87ffc 100644 --- a/osu.Game.Tournament.Tests/NonVisual/LadderInfoSerialisationTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/LadderInfoSerialisationTest.cs @@ -35,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/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs index abf0905647..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; diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneLadderEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneLadderEditorScreen.cs index 29ecdaf873..b9b150d264 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneLadderEditorScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneLadderEditorScreen.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tournament.Tests.Screens 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); + 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/TestSceneMapPoolScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs index 2c17cd0d52..7e008a6897 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; @@ -11,28 +9,47 @@ using osu.Framework.Testing; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.MapPool; +using osuTK; +using osuTK.Input; namespace osu.Game.Tournament.Tests.Screens { public partial class TestSceneMapPoolScreen : TournamentScreenTestScene { - private MapPoolScreen screen; + private MapPoolScreen screen = null!; [BackgroundDependencyLoader] private void load() { - Add(screen = new MapPoolScreen { Width = 0.7f }); + Add(screen = new TestMapPoolScreen { Width = 0.7f }); + } + + [SetUpSteps] + public override void SetUpSteps() + { + AddStep("reset state", resetState); + } + + private void resetState() + { + Ladder.SplitMapPoolByMods.Value = true; + + Ladder.CurrentMatch.Value = new TournamentMatch(); + Ladder.CurrentMatch.Value = Ladder.Matches.First(); + Ladder.CurrentMatch.Value.PicksBans.Clear(); } [SetUp] - public void SetUp() => Schedule(() => Ladder.SplitMapPoolByMods.Value = true); + public void SetUp() => Schedule(() => + { + }); [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(); @@ -41,7 +58,6 @@ namespace osu.Game.Tournament.Tests.Screens AddStep("reset match", () => { Ladder.CurrentMatch.Value = new TournamentMatch(); - Ladder.CurrentMatch.Value = Ladder.Matches.First(); }); assertTwoWide(); @@ -52,17 +68,13 @@ 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(); }); - AddStep("reset match", () => - { - Ladder.CurrentMatch.Value = new TournamentMatch(); - Ladder.CurrentMatch.Value = Ladder.Matches.First(); - }); + AddStep("reset state", resetState); assertTwoWide(); } @@ -72,17 +84,13 @@ 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(); }); - AddStep("reset match", () => - { - Ladder.CurrentMatch.Value = new TournamentMatch(); - Ladder.CurrentMatch.Value = Ladder.Matches.First(); - }); + AddStep("reset state", resetState); assertThreeWide(); } @@ -92,17 +100,13 @@ 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 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM"); }); - AddStep("reset match", () => - { - Ladder.CurrentMatch.Value = new TournamentMatch(); - Ladder.CurrentMatch.Value = Ladder.Matches.First(); - }); + AddStep("reset state", resetState); assertTwoWide(); } @@ -118,17 +122,13 @@ 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 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM"); }); - AddStep("reset match", () => - { - Ladder.CurrentMatch.Value = new TournamentMatch(); - Ladder.CurrentMatch.Value = Ladder.Matches.First(); - }); + AddStep("reset state", resetState); assertThreeWide(); } @@ -138,7 +138,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 < 12; i++) addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM"); @@ -146,20 +146,214 @@ namespace osu.Game.Tournament.Tests.Screens AddStep("disable splitting map pool by mods", () => Ladder.SplitMapPoolByMods.Value = false); + AddStep("reset state", resetState); + } + + [Test] + public void TestBanOrderMultipleBans() + { + AddStep("set ban count", () => Ladder.CurrentMatch.Value!.Round.Value!.BanCount.Value = 2); + + AddStep("load some maps", () => + { + Ladder.CurrentMatch.Value!.Round.Value!.Beatmaps.Clear(); + + for (int i = 0; i < 5; i++) + addBeatmap(); + }); + + AddStep("update displayed maps", () => Ladder.SplitMapPoolByMods.Value = false); + + AddStep("start bans from blue team", () => screen.ChildrenOfType().First(btn => btn.Text == "Blue Ban").TriggerClick()); + + AddStep("ban map", () => clickBeatmapPanel(0)); + checkTotalPickBans(1); + checkLastPick(ChoiceType.Ban, TeamColour.Blue); + + AddStep("ban map", () => clickBeatmapPanel(1)); + checkTotalPickBans(2); + checkLastPick(ChoiceType.Ban, TeamColour.Red); + + AddStep("ban map", () => clickBeatmapPanel(2)); + checkTotalPickBans(3); + checkLastPick(ChoiceType.Ban, TeamColour.Red); + + AddStep("pick map", () => clickBeatmapPanel(3)); + checkTotalPickBans(4); + checkLastPick(ChoiceType.Ban, TeamColour.Blue); + + AddStep("pick map", () => clickBeatmapPanel(4)); + checkTotalPickBans(5); + checkLastPick(ChoiceType.Pick, TeamColour.Blue); + } + + [Test] + public void TestPickBanOrder() + { + AddStep("set ban count", () => Ladder.CurrentMatch.Value!.Round.Value!.BanCount.Value = 1); + + AddStep("load some maps", () => + { + Ladder.CurrentMatch.Value!.Round.Value!.Beatmaps.Clear(); + + for (int i = 0; i < 5; i++) + addBeatmap(); + }); + + AddStep("update displayed maps", () => Ladder.SplitMapPoolByMods.Value = false); + + AddStep("start bans from blue team", () => screen.ChildrenOfType().First(btn => btn.Text == "Blue Ban").TriggerClick()); + + AddStep("ban map", () => clickBeatmapPanel(0)); + checkTotalPickBans(1); + checkLastPick(ChoiceType.Ban, TeamColour.Blue); + + AddStep("ban map", () => clickBeatmapPanel(1)); + checkTotalPickBans(2); + checkLastPick(ChoiceType.Ban, TeamColour.Red); + + AddStep("pick map", () => clickBeatmapPanel(2)); + checkTotalPickBans(3); + checkLastPick(ChoiceType.Pick, TeamColour.Red); + + AddStep("pick map", () => clickBeatmapPanel(3)); + checkTotalPickBans(4); + checkLastPick(ChoiceType.Pick, TeamColour.Blue); + + AddStep("pick map", () => clickBeatmapPanel(4)); + checkTotalPickBans(5); + checkLastPick(ChoiceType.Pick, TeamColour.Red); + AddStep("reset match", () => { Ladder.CurrentMatch.Value = new TournamentMatch(); Ladder.CurrentMatch.Value = Ladder.Matches.First(); + Ladder.CurrentMatch.Value.PicksBans.Clear(); }); } + [Test] + public void TestMultipleTeamBans() + { + AddStep("set ban count", () => Ladder.CurrentMatch.Value!.Round.Value!.BanCount.Value = 3); + + AddStep("load some maps", () => + { + Ladder.CurrentMatch.Value!.Round.Value!.Beatmaps.Clear(); + + for (int i = 0; i < 12; i++) + addBeatmap(); + }); + + AddStep("update displayed maps", () => Ladder.SplitMapPoolByMods.Value = false); + + AddStep("start bans with red team", () => screen.ChildrenOfType().First(btn => btn.Text == "Red Ban").TriggerClick()); + + AddStep("first ban", () => clickBeatmapPanel(0)); + AddAssert("red ban registered", + () => Ladder.CurrentMatch.Value!.PicksBans.Count(pb => pb.Type == ChoiceType.Ban && pb.Team == TeamColour.Red), + () => Is.EqualTo(1)); + + AddStep("ban two more maps", () => + { + clickBeatmapPanel(1); + clickBeatmapPanel(2); + }); + + AddAssert("three bans registered", + () => Ladder.CurrentMatch.Value!.PicksBans.Count(pb => pb.Type == ChoiceType.Ban), + () => Is.EqualTo(3)); + AddAssert("both new bans for blue team", + () => Ladder.CurrentMatch.Value!.PicksBans.Count(pb => pb.Type == ChoiceType.Ban && pb.Team == TeamColour.Blue), + () => Is.EqualTo(2)); + + AddStep("ban two more maps", () => + { + clickBeatmapPanel(3); + clickBeatmapPanel(4); + }); + + AddAssert("five bans registered", + () => Ladder.CurrentMatch.Value!.PicksBans.Count(pb => pb.Type == ChoiceType.Ban), + () => Is.EqualTo(5)); + AddAssert("both new bans for red team", + () => Ladder.CurrentMatch.Value!.PicksBans.Count(pb => pb.Type == ChoiceType.Ban && pb.Team == TeamColour.Red), + () => Is.EqualTo(3)); + + AddStep("ban last map", () => clickBeatmapPanel(5)); + AddAssert("six bans registered", + () => Ladder.CurrentMatch.Value!.PicksBans.Count(pb => pb.Type == ChoiceType.Ban), + () => Is.EqualTo(6)); + AddAssert("red banned three", + () => Ladder.CurrentMatch.Value!.PicksBans.Count(pb => pb.Type == ChoiceType.Ban && pb.Team == TeamColour.Red), + () => Is.EqualTo(3)); + AddAssert("blue banned three", + () => Ladder.CurrentMatch.Value!.PicksBans.Count(pb => pb.Type == ChoiceType.Ban && pb.Team == TeamColour.Blue), + () => Is.EqualTo(3)); + + AddStep("pick map", () => clickBeatmapPanel(6)); + AddAssert("one pick registered", + () => Ladder.CurrentMatch.Value!.PicksBans.Count(pb => pb.Type == ChoiceType.Pick), + () => Is.EqualTo(1)); + AddAssert("pick was blue's", + () => Ladder.CurrentMatch.Value!.PicksBans.Last().Team, + () => Is.EqualTo(TeamColour.Blue)); + + AddStep("pick map", () => clickBeatmapPanel(7)); + AddAssert("two picks registered", + () => Ladder.CurrentMatch.Value!.PicksBans.Count(pb => pb.Type == ChoiceType.Pick), + () => Is.EqualTo(2)); + AddAssert("pick was red's", + () => Ladder.CurrentMatch.Value!.PicksBans.Last().Team, + () => Is.EqualTo(TeamColour.Red)); + + AddStep("pick map", () => clickBeatmapPanel(8)); + AddAssert("three picks registered", + () => Ladder.CurrentMatch.Value!.PicksBans.Count(pb => pb.Type == ChoiceType.Pick), + () => Is.EqualTo(3)); + AddAssert("pick was blue's", + () => Ladder.CurrentMatch.Value!.PicksBans.Last().Team, + () => Is.EqualTo(TeamColour.Blue)); + + AddStep("reset match", () => + { + Ladder.CurrentMatch.Value = new TournamentMatch(); + Ladder.CurrentMatch.Value = Ladder.Matches.First(); + Ladder.CurrentMatch.Value.PicksBans.Clear(); + }); + } + + private void checkTotalPickBans(int expected) => AddAssert($"total pickbans is {expected}", () => Ladder.CurrentMatch.Value!.PicksBans, () => Has.Count.EqualTo(expected)); + + private void checkLastPick(ChoiceType expectedChoice, TeamColour expectedColour) => + AddAssert($"last choice was {expectedChoice} by {expectedColour}", + () => Ladder.CurrentMatch.Value!.PicksBans.Select(pb => (pb.Type, pb.Team)).Last(), + () => Is.EqualTo((expectedChoice, expectedColour))); + private void addBeatmap(string mods = "NM") { - Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Add(new RoundBeatmap + Ladder.CurrentMatch.Value!.Round.Value!.Beatmaps.Add(new RoundBeatmap { Beatmap = CreateSampleBeatmap(), Mods = mods }); } + + private void clickBeatmapPanel(int index) + { + InputManager.MoveMouseTo(screen.ChildrenOfType().ElementAt(index)); + InputManager.Click(MouseButton.Left); + } + + private partial class TestMapPoolScreen : MapPoolScreen + { + // this is a bit of a test-specific workaround. + // the way pick/ban is implemented is a bit funky; the screen itself is what handles the mouse there, + // rather than the beatmap panels themselves. + // in some extreme situations headless it may turn out that the panels overflow the screen, + // and as such picking stops working anymore outside of the bounds of the screen drawable. + // this override makes it so the screen sees all of the input at all times, making that impossible to happen. + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + } } } diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs index 5071fbbfe3..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; @@ -14,6 +12,13 @@ namespace osu.Game.Tournament.Tests.Screens { 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 15d4fb1f3f..a7ef4d7a29 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.Editors; @@ -17,7 +18,7 @@ namespace osu.Game.Tournament.Tests.Screens { 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/TestSceneTeamWinScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs index 05c21fa0b1..1e3ff72d43 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs @@ -17,7 +17,7 @@ namespace osu.Game.Tournament.Tests.Screens { 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/TournamentScreenTestScene.cs b/osu.Game.Tournament.Tests/TournamentScreenTestScene.cs index 8adffe1468..e8cca00c92 100644 --- a/osu.Game.Tournament.Tests/TournamentScreenTestScene.cs +++ b/osu.Game.Tournament.Tests/TournamentScreenTestScene.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tournament.Tests { public TournamentScalingContainer() { - TargetDrawSize = new Vector2(1920, 1080); + TargetDrawSize = new Vector2(1024, 768); RelativeSizeAxes = Axes.Both; } diff --git a/osu.Game.Tournament.Tests/TournamentTestRunner.cs b/osu.Game.Tournament.Tests/TournamentTestRunner.cs index 5f642b14f5..e09d1be22c 100644 --- a/osu.Game.Tournament.Tests/TournamentTestRunner.cs +++ b/osu.Game.Tournament.Tests/TournamentTestRunner.cs @@ -12,7 +12,7 @@ namespace osu.Game.Tournament.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu-development", new HostOptions { BindIPC = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu-development")) { host.Run(new TournamentTestBrowser()); return 0; diff --git a/osu.Game.Tournament.Tests/TournamentTestScene.cs b/osu.Game.Tournament.Tests/TournamentTestScene.cs index 84e5da62f4..4106556ee1 100644 --- a/osu.Game.Tournament.Tests/TournamentTestScene.cs +++ b/osu.Game.Tournament.Tests/TournamentTestScene.cs @@ -40,10 +40,10 @@ 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)); @@ -67,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 @@ -140,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" }, @@ -152,7 +153,7 @@ namespace osu.Game.Tournament.Tests }, Round = { - Value = new TournamentRound { Name = { Value = "Quarterfinals" } } + Value = new TournamentRound { Name = { Value = "Quarterfinals" } }, } }; diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 5847079161..3b00f103c4 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/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs index 0993a4c05e..dd70d5856d 100644 --- a/osu.Game.Tournament/Components/DateTextBox.cs +++ b/osu.Game.Tournament/Components/DateTextBox.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Globalization; using osu.Framework.Bindables; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; @@ -10,12 +11,12 @@ namespace osu.Game.Tournament.Components { public partial class DateTextBox : SettingsTextBox { - private readonly BindableWithCurrent current = new BindableWithCurrent(); + private readonly BindableWithCurrent current = new BindableWithCurrent(DateTimeOffset.Now); public new Bindable? Current { get => current; - set => current.Current = value; + set => current.Current = value!; } public DateTextBox() @@ -23,13 +24,13 @@ namespace osu.Game.Tournament.Components base.Current = new Bindable(string.Empty); current.BindValueChanged(dto => - base.Current.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"), true); + 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/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 89f45fc1d3..7d8fc847d4 100644 --- a/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs +++ b/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs @@ -18,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 4f0c7d6b72..fd7a51140b 100644 --- a/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs +++ b/osu.Game.Tournament/Components/DrawableTeamWithPlayers.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.Linq; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; @@ -17,6 +19,11 @@ namespace osu.Game.Tournament.Components { AutoSizeAxes = Axes.Both; + var players = team?.Players ?? new BindableList(); + + // split the players into two even columns, favouring the first column if odd. + int split = (int)Math.Ceiling(players.Count / 2f); + InternalChildren = new Drawable[] { new FillFlowContainer @@ -39,13 +46,13 @@ namespace osu.Game.Tournament.Components { Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Both, - ChildrenEnumerable = team?.Players.Select(createPlayerText).Take(5) ?? Enumerable.Empty() + ChildrenEnumerable = players.Take(split).Select(createPlayerText), }, new FillFlowContainer { Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Both, - ChildrenEnumerable = team?.Players.Select(createPlayerText).Skip(5) ?? Enumerable.Empty() + ChildrenEnumerable = players.Skip(split).Select(createPlayerText), }, } }, 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/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index 4f4a02ccf1..ae59e92e33 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; @@ -14,9 +12,9 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Legacy; using osu.Game.Extensions; using osu.Game.Graphics; +using osu.Game.Models; using osu.Game.Rulesets; using osu.Game.Screens.Menu; -using osu.Game.Tournament.Models; using osuTK; using osuTK.Graphics; @@ -24,14 +22,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 +37,7 @@ namespace osu.Game.Tournament.Components return; beatmap = value; - update(); + refreshContent(); } } @@ -51,11 +49,11 @@ namespace osu.Game.Tournament.Components set { mods = value; - update(); + refreshContent(); } } - private FillFlowContainer flow; + private FillFlowContainer flow = null!; private bool expanded; @@ -73,19 +71,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,13 +100,27 @@ namespace osu.Game.Tournament.Components Expanded = true; } - private void update() + private void refreshContent() { - if (beatmap == null) + beatmap ??= new BeatmapInfo { - flow.Clear(); - return; - } + Metadata = new BeatmapMetadata + { + Artist = "unknown", + Title = "no beatmap selected", + Author = new RealmUser { Username = "unknown" }, + }, + DifficultyName = "unknown", + BeatmapSet = new BeatmapSetInfo(), + StarRating = 0, + Difficulty = new BeatmapDifficulty + { + CircleSize = 0, + DrainRate = 0, + OverallDifficulty = 0, + ApproachRate = 0, + }, + }; double bpm = beatmap.BPM; double length = beatmap.Length; diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 49478a2174..514ba482c4 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tournament.Components { public partial class TournamentBeatmapPanel : CompositeDrawable { - public readonly TournamentBeatmap? Beatmap; + public readonly IBeatmapInfo? Beatmap; private readonly string mod; @@ -30,7 +30,7 @@ namespace osu.Game.Tournament.Components private Box flash = null!; - public TournamentBeatmapPanel(TournamentBeatmap? beatmap, string mod = "") + public TournamentBeatmapPanel(IBeatmapInfo? beatmap, string mod = "") { Beatmap = beatmap; this.mod = mod; @@ -58,7 +58,7 @@ namespace osu.Game.Tournament.Components { RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(0.5f), - OnlineInfo = Beatmap, + OnlineInfo = (Beatmap as IBeatmapSetOnlineInfo), }, new FillFlowContainer { @@ -146,7 +146,12 @@ namespace osu.Game.Tournament.Components private void updateState() { - var newChoice = currentMatch.Value?.PicksBans.FirstOrDefault(p => p.BeatmapID == Beatmap?.OnlineID); + if (currentMatch.Value == null) + { + return; + } + + var newChoice = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == Beatmap?.OnlineID); bool shouldFlash = newChoice != choice; @@ -189,7 +194,7 @@ namespace osu.Game.Tournament.Components // Use DelayedLoadWrapper to avoid content unloading when switching away to another screen. protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func createContentFunc, double timeBeforeLoad) - => new DelayedLoadWrapper(createContentFunc, timeBeforeLoad); + => new DelayedLoadWrapper(createContentFunc(), timeBeforeLoad); } } } diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index e943cb8b8c..0998e606e9 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -92,9 +92,9 @@ namespace osu.Game.Tournament.Components { if (info.CurrentMatch.Value is TournamentMatch match) { - if (match.Team1.Value.Players.Any(u => u.OnlineID == Message.Sender.OnlineID)) + 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)) + 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/TournamentSpriteTextWithBackground.cs b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs index 97cb610021..21439482e3 100644 --- a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs +++ b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs @@ -30,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/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/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 ff557717d5..b4575144e7 100644 --- a/osu.Game.Tournament/IPC/MatchIPCInfo.cs +++ b/osu.Game.Tournament/IPC/MatchIPCInfo.cs @@ -10,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/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs index 229837c94e..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. // 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) { @@ -44,5 +42,7 @@ 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 f2ec261246..b03b28b3b8 100644 --- a/osu.Game.Tournament/Models/RoundBeatmap.cs +++ b/osu.Game.Tournament/Models/RoundBeatmap.cs @@ -8,7 +8,6 @@ namespace osu.Game.Tournament.Models public class RoundBeatmap { public int ID; - public string Mods = string.Empty; [JsonProperty("BeatmapInfo")] 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/TournamentBeatmap.cs b/osu.Game.Tournament/Models/TournamentBeatmap.cs index 7f57b6a151..a7ba5b7db1 100644 --- a/osu.Game.Tournament/Models/TournamentBeatmap.cs +++ b/osu.Game.Tournament/Models/TournamentBeatmap.cs @@ -21,6 +21,10 @@ namespace osu.Game.Tournament.Models public double StarRating { get; set; } + public int EndTimeObjectCount { get; set; } + + public int TotalObjectCount { get; set; } + public IBeatmapMetadataInfo Metadata { get; set; } = new BeatmapMetadata(); public IBeatmapDifficultyInfo Difficulty { get; set; } = new BeatmapDifficulty(); @@ -41,6 +45,8 @@ namespace osu.Game.Tournament.Models Metadata = beatmap.Metadata; Difficulty = beatmap.Difficulty; Covers = beatmap.BeatmapSet?.Covers ?? new BeatmapSetOnlineCovers(); + EndTimeObjectCount = beatmap.EndTimeObjectCount; + TotalObjectCount = beatmap.TotalObjectCount; } public bool Equals(IBeatmapInfo? other) => other is TournamentBeatmap b && this.MatchesOnlineID(b); 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/TournamentRound.cs b/osu.Game.Tournament/Models/TournamentRound.cs index a92bab690e..7aa8bbb44f 100644 --- a/osu.Game.Tournament/Models/TournamentRound.cs +++ b/osu.Game.Tournament/Models/TournamentRound.cs @@ -18,6 +18,7 @@ namespace osu.Game.Tournament.Models public readonly Bindable Description = new Bindable(string.Empty); public readonly BindableInt BestOf = new BindableInt(9) { Default = 9, MinValue = 3, MaxValue = 23 }; + public readonly BindableInt BanCount = new BindableInt(1) { Default = 1, MinValue = 0, MaxValue = 5 }; [JsonProperty] public readonly BindableList Beatmaps = new BindableList(); 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/SaveChangesOverlay.cs b/osu.Game.Tournament/SaveChangesOverlay.cs index 7838d4ba48..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!; @@ -78,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 e977012803..32a50d2222 100644 --- a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs +++ b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs @@ -37,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/Group.cs b/osu.Game.Tournament/Screens/Drawings/Components/Group.cs index a79e2253a4..9d4474a58c 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/Group.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/Group.cs @@ -84,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) @@ -112,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/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/LadderEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs index 9411892dc5..4074e681f9 100644 --- a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.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.Drawing; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -35,13 +32,12 @@ namespace osu.Game.Tournament.Screens.Editors [Cached] private LadderEditorInfo editorInfo = new LadderEditorInfo(); - private WarningBox rightClickMessage; + private WarningBox rightClickMessage = null!; - private RectangularPositionSnapGrid grid; + private RectangularPositionSnapGrid grid = null!; - [Resolved(canBeNull: true)] - [CanBeNull] - private IDialogOverlay dialogOverlay { get; set; } + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } protected override bool DrawLoserPaths => true; @@ -94,36 +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, () => + 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(() => { - 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(); - })); - }) - }; - } - } + foreach (var p in MatchesContainer) + p.Match.Reset(); + })); + }) + }; public void Remove(TournamentMatch match) { @@ -135,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; @@ -153,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 f887c41749..253cca8c98 100644 --- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs @@ -82,6 +82,12 @@ namespace osu.Game.Tournament.Screens.Editors Current = Model.StartDate }, new SettingsSlider + { + LabelText = "# of Bans", + Width = 0.33f, + Current = Model.BanCount + }, + new SettingsSlider { LabelText = "Best of", Width = 0.33f, diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index 241692d515..250d5acaae 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -10,7 +10,9 @@ 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.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Settings; using osu.Game.Tournament.Models; @@ -128,7 +130,7 @@ namespace osu.Game.Tournament.Screens.Editors Width = 0.2f, Current = Model.Seed }, - new SettingsSlider + new SettingsSlider { LabelText = "Last Year Placement", Width = 0.33f, @@ -175,6 +177,11 @@ namespace osu.Game.Tournament.Screens.Editors }; } + private partial class LastYearPlacementSlider : RoundedSliderBar + { + public override LocalisableString TooltipText => Current.Value == 0 ? "N/A" : base.TooltipText; + } + public partial class PlayerEditor : CompositeDrawable { private readonly TournamentTeam team; diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs index 9dd028fa79..aa3718150c 100644 --- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs @@ -82,7 +82,7 @@ namespace osu.Game.Tournament.Screens.Editors new TourneyButton { RelativeSizeAxes = Axes.X, - BackgroundColour = colours.Pink3, + BackgroundColour = colours.DangerousButtonColour, Text = "Clear all", Action = () => { 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/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index c23327a43f..3eec67c639 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -14,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("???"); @@ -35,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; @@ -95,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, @@ -119,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/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 838e5fa071..f8de34a511 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs @@ -1,183 +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 MatchScoreDiffCounter scoreDiffText; - - 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 - }, - scoreDiffText = new MatchScoreDiffCounter - { - Anchor = Anchor.TopCentre, - Margin = new MarginPadding - { - Top = bar_height / 4, - Horizontal = 8 - }, - Alpha = 0 - } - }; - } - [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); - - scoreDiffText.Alpha = diff != 0 ? 1 : 0; - scoreDiffText.Current.Value = -diff; - scoreDiffText.Origin = score1.Value > score2.Value ? Anchor.TopLeft : Anchor.TopRight; - } - - 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); - } - - 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); - }); + 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 4258522cff..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 => { @@ -142,7 +140,7 @@ namespace osu.Game.Tournament.Screens.Gameplay State.BindValueChanged(_ => updateState(), true); } - protected override void CurrentMatchChanged(ValueChangedEvent match) + protected override void CurrentMatchChanged(ValueChangedEvent match) { base.CurrentMatchChanged(match); @@ -153,29 +151,35 @@ namespace osu.Game.Tournament.Screens.Gameplay scheduledScreenChange?.Cancel(); } - private ScheduledDelegate scheduledScreenChange; - private ScheduledDelegate scheduledContract; + private ScheduledDelegate? scheduledScreenChange; + private ScheduledDelegate? scheduledContract; - private TournamentMatchScoreDisplay scoreDisplay; + private TournamentMatchScoreDisplay scoreDisplay = null!; private TourneyState lastState; - private MatchHeader header; + private MatchHeader header = null!; private void contract() { + if (!IsLoaded) + return; + scheduledContract?.Cancel(); SongBar.Expanded = false; scoreDisplay.FadeOut(100); - using (chat?.BeginDelayedSequence(500)) - chat?.Expand(); + using (chat.BeginDelayedSequence(500)) + chat.Expand(); } private void expand() { + if (!IsLoaded) + return; + scheduledContract?.Cancel(); - chat?.Contract(); + chat.Contract(); using (BeginDelayedSequence(300)) { @@ -252,7 +256,7 @@ namespace osu.Game.Tournament.Screens.Gameplay 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 c394877ae9..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; @@ -27,13 +26,13 @@ namespace osu.Game.Tournament.Screens.Ladder.Components protected readonly FillFlowContainer Flow; private readonly Drawable selectionBox; private readonly Drawable currentMatchSelectionBox; - private Bindable globalSelection; + 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) { @@ -129,7 +128,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components /// /// Fired when something changed that requires a ladder redraw. /// - public Action Changed; + public Action? Changed; private readonly List refBindables = new List(); @@ -201,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) @@ -268,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; @@ -344,6 +345,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components 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 5c9c14cc30..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; @@ -23,17 +21,17 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { 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; } + private LadderInfo ladderInfo { get; set; } = null!; [BackgroundDependencyLoader] private void load() @@ -77,7 +75,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components }; } - private void roundDropdownChanged(ValueChangedEvent round) + private void roundDropdownChanged(ValueChangedEvent round) { if (editorInfo.Selected.Value?.Date.Value < round.NewValue?.StartDate.Value) { @@ -101,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 eae11dd8f1..90efac30fd 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/ProgressionPath.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/ProgressionPath.cs @@ -61,8 +61,8 @@ namespace osu.Game.Tournament.Screens.Ladder.Components var topLeft = new Vector2(minX, minY); OriginPosition = new Vector2(PathRadius); - Position = Parent.ToLocalSpace(topLeft); - Vertices = points.Select(p => Parent.ToLocalSpace(p) - Parent.ToLocalSpace(topLeft)).ToList(); + 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 00e5353edd..7e35190e2e 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs @@ -12,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/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs index 2d5281b893..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() diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index f45da861cb..665d3c131a 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,20 @@ namespace osu.Game.Tournament.Screens.MapPool { public partial class MapPoolScreen : TournamentMatchScreen { - private 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 OsuButton buttonRedBan; - private OsuButton buttonBlueBan; - private OsuButton buttonRedPick; - private OsuButton buttonBluePick; + private OsuButton buttonRedBan = null!; + private OsuButton buttonBlueBan = null!; + private OsuButton buttonRedPick = null!; + private OsuButton buttonBluePick = null!; - private ScheduledDelegate scheduledScreenChange; + private ScheduledDelegate? scheduledScreenChange; [BackgroundDependencyLoader] private void load(MatchIPCInfo ipc) @@ -113,7 +111,7 @@ namespace osu.Game.Tournament.Screens.MapPool ipc.Beatmap.BindValueChanged(beatmapChanged); } - private Bindable splitMapPoolByMods; + private Bindable? splitMapPoolByMods; protected override void LoadComplete() { @@ -123,13 +121,13 @@ namespace osu.Game.Tournament.Screens.MapPool splitMapPoolByMods.BindValueChanged(_ => updateDisplay()); } - private void beatmapChanged(ValueChangedEvent beatmap) + 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); } @@ -138,24 +136,45 @@ namespace osu.Game.Tournament.Screens.MapPool pickColour = colour; pickType = choiceType; - static Color4 setColour(bool active) => active ? Color4.White : Color4.Gray; - buttonRedBan.Colour = setColour(pickColour == TeamColour.Red && pickType == ChoiceType.Ban); buttonBlueBan.Colour = setColour(pickColour == TeamColour.Blue && pickType == ChoiceType.Ban); buttonRedPick.Colour = setColour(pickColour == TeamColour.Red && pickType == ChoiceType.Pick); buttonBluePick.Colour = setColour(pickColour == TeamColour.Blue && pickType == ChoiceType.Pick); + + static Color4 setColour(bool active) => active ? Color4.White : Color4.Gray; } private void setNextMode() { - const TeamColour roll_winner = TeamColour.Red; //todo: draw from match + if (CurrentMatch.Value?.Round.Value == null) + return; - var nextColour = (CurrentMatch.Value.PicksBans.LastOrDefault()?.Team ?? roll_winner) == TeamColour.Red ? TeamColour.Blue : TeamColour.Red; + int totalBansRequired = CurrentMatch.Value.Round.Value.BanCount.Value * 2; - if (pickType == ChoiceType.Ban && CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) >= 2) - setMode(pickColour, ChoiceType.Pick); + TeamColour lastPickColour = CurrentMatch.Value.PicksBans.LastOrDefault()?.Team ?? TeamColour.Red; + + TeamColour nextColour; + + bool hasAllBans = CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) >= totalBansRequired; + + if (!hasAllBans) + { + // Ban phase: switch teams every second ban. + nextColour = CurrentMatch.Value.PicksBans.Count % 2 == 1 + ? getOppositeTeamColour(lastPickColour) + : lastPickColour; + } else - setMode(nextColour, CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) >= 2 ? ChoiceType.Pick : ChoiceType.Ban); + { + // Pick phase : switch teams every pick, except for the first pick which generally goes to the team that placed the last ban. + nextColour = pickType == ChoiceType.Pick + ? getOppositeTeamColour(lastPickColour) + : lastPickColour; + } + + setMode(nextColour, hasAllBans ? ChoiceType.Pick : ChoiceType.Ban); + + TeamColour getOppositeTeamColour(TeamColour colour) => colour == TeamColour.Red ? TeamColour.Blue : TeamColour.Red; } protected override bool OnMouseDown(MouseDownEvent e) @@ -169,11 +188,11 @@ namespace osu.Game.Tournament.Screens.MapPool 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(); } } @@ -186,13 +205,13 @@ namespace osu.Game.Tournament.Screens.MapPool private void reset() { - CurrentMatch.Value.PicksBans.Clear(); + CurrentMatch.Value?.PicksBans.Clear(); setNextMode(); } 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)) @@ -228,7 +247,7 @@ namespace osu.Game.Tournament.Screens.MapPool base.Hide(); } - protected override void CurrentMatchChanged(ValueChangedEvent match) + protected override void CurrentMatchChanged(ValueChangedEvent match) { base.CurrentMatchChanged(match); updateDisplay(); @@ -245,8 +264,8 @@ namespace osu.Game.Tournament.Screens.MapPool if (CurrentMatch.Value.Round.Value != null) { - FillFlowContainer currentFlow = null; - string currentMods = null; + FillFlowContainer? currentFlow = null; + string? currentMods = null; int flowCount = 0; foreach (var b in CurrentMatch.Value.Round.Value.Beatmaps) diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 9232b4c689..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 ?? string.Empty) + 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) } @@ -284,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/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 5c7bbed69c..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; @@ -24,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) @@ -115,7 +113,7 @@ namespace osu.Game.Tournament.Screens.Setup 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.", @@ -142,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 ae49ccb63b..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,12 +11,12 @@ namespace osu.Game.Tournament.Screens.Setup { internal partial class TournamentSwitcher : ActionableInfo { - private OsuDropdown dropdown; - private OsuButton folderButton; - private OsuButton reloadTournamentsButton; + 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) diff --git a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs index db4d6198e6..ae2ec0c291 100644 --- a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs +++ b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs @@ -42,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/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/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index c79cc9cd57..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; @@ -35,12 +32,12 @@ 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 Drawable heightWarning = null!; - private Bindable windowMode; + private Bindable windowMode = null!; private readonly BindableSize windowSize = new BindableSize(); - private LoadingSpinner loadingSpinner; + private LoadingSpinner loadingSpinner = null!; [Cached(typeof(IDialogOverlay))] private readonly DialogOverlay dialogOverlay = new DialogOverlay(); @@ -50,8 +47,6 @@ namespace osu.Game.Tournament { frameworkConfig.BindWith(FrameworkSetting.WindowedSize, windowSize); - windowSize.MinValue = new Size(TournamentSceneManager.REQUIRED_WIDTH, TournamentSceneManager.STREAM_AREA_HEIGHT); - windowMode = frameworkConfig.GetBindable(FrameworkSetting.WindowMode); Add(loadingSpinner = new LoadingSpinner(true, true) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 509c8bb940..eecd097a97 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.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.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; @@ -31,11 +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 BeatmapLookupCache beatmapCache; + 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; @@ -54,7 +53,7 @@ namespace osu.Game.Tournament return new ProductionEndpointConfiguration(); } - private TournamentSpriteText initialisationText; + private TournamentSpriteText initialisationText = null!; [BackgroundDependencyLoader] private void load(Storage baseStorage) @@ -100,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(await sr.ReadToEndAsync().ConfigureAwait(false), 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(); @@ -283,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); @@ -348,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 9ac5a9de12..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,8 +33,8 @@ 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 int CONTROL_AREA_WIDTH = 200; @@ -50,8 +48,8 @@ namespace osu.Game.Tournament [Cached] private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay(); - private Container chatContainer; - private FillFlowContainer buttons; + private Container chatContainer = null!; + private FillFlowContainer buttons = null!; public TournamentSceneManager() { @@ -166,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) { @@ -284,7 +282,7 @@ namespace osu.Game.Tournament Y = -2, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = shortcutKey.ToString(), + Text = shortcutKey.Value.ToString(), } } }); @@ -304,7 +302,7 @@ namespace osu.Game.Tournament private bool isSelected; - public Action RequestSelection; + public Action? RequestSelection; public bool IsSelected { diff --git a/osu.Game/Audio/PreviewTrack.cs b/osu.Game/Audio/PreviewTrack.cs index d625566ee7..961990a1bd 100644 --- a/osu.Game/Audio/PreviewTrack.cs +++ b/osu.Game/Audio/PreviewTrack.cs @@ -96,10 +96,14 @@ namespace osu.Game.Audio hasStarted = false; - Track.Stop(); + // This pre-check is important, fixes a BASS deadlock in some scenarios. + if (!Track.HasCompleted) + { + Track.Stop(); - // Ensure the track is reset immediately on stopping, so the next time it is started it has a correct time value. - Track.Seek(0); + // Ensure the track is reset immediately on stopping, so the next time it is started it has a correct time value. + Track.Seek(0); + } Stopped?.Invoke(); } diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 4f81b26c3e..6db9febf36 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -1,8 +1,6 @@ // Copyright (c) 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.Timing; using osu.Game.Rulesets.Objects; @@ -26,8 +24,7 @@ namespace osu.Game.Beatmaps { difficulty = value; - if (beatmapInfo != null) - beatmapInfo.Difficulty = difficulty.Clone(); + beatmapInfo.Difficulty = difficulty.Clone(); } } @@ -40,8 +37,7 @@ namespace osu.Game.Beatmaps { beatmapInfo = value; - if (beatmapInfo?.Difficulty != null) - Difficulty = beatmapInfo.Difficulty.Clone(); + Difficulty = beatmapInfo.Difficulty.Clone(); } } @@ -119,12 +115,11 @@ namespace osu.Game.Beatmaps IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() => (Beatmap)MemberwiseClone(); + + public override string ToString() => BeatmapInfo.ToString(); } public class Beatmap : Beatmap { - public new Beatmap Clone() => (Beatmap)base.Clone(); - - public override string ToString() => BeatmapInfo?.ToString() ?? base.ToString(); } } diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 217f3b89a4..ac2267380d 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -18,7 +18,7 @@ namespace osu.Game.Beatmaps public float OverallDifficulty { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; public float ApproachRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; - public double SliderMultiplier { get; set; } = 1; + public double SliderMultiplier { get; set; } = 1.4; public double SliderTickRate { get; set; } = 1; public BeatmapDifficulty() diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index c840b4fa94..7bb52eef52 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -20,6 +20,7 @@ using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects.Types; using Realms; namespace osu.Game.Beatmaps @@ -152,6 +153,8 @@ namespace osu.Game.Beatmaps if (archive != null) beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet, realm)); + beatmapSet.DateAdded = getDateAdded(archive); + foreach (BeatmapInfo b in beatmapSet.Beatmaps) { b.BeatmapSet = beatmapSet; @@ -278,7 +281,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)); @@ -305,11 +308,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. /// @@ -360,7 +388,7 @@ namespace osu.Game.Beatmaps OverallDifficulty = decodedDifficulty.OverallDifficulty, ApproachRate = decodedDifficulty.ApproachRate, SliderMultiplier = decodedDifficulty.SliderMultiplier, - SliderTickRate = decodedDifficulty.SliderTickRate, + SliderTickRate = decodedDifficulty.SliderTickRate }; var metadata = new BeatmapMetadata @@ -398,6 +426,8 @@ namespace osu.Game.Beatmaps GridSize = decodedInfo.GridSize, TimelineZoom = decodedInfo.TimelineZoom, MD5Hash = memoryStream.ComputeMD5Hash(), + EndTimeObjectCount = decoded.HitObjects.Count(h => h is IHasDuration), + TotalObjectCount = decoded.HitObjects.Count }; beatmaps.Add(beatmap); diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index c1aeec1f71..425fd98d27 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -120,6 +120,10 @@ namespace osu.Game.Beatmaps [JsonIgnore] public bool Hidden { get; set; } + public int EndTimeObjectCount { get; set; } = -1; + + public int TotalObjectCount { get; set; } = -1; + /// /// Reset any fetched online linking information (and history). /// diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index 3aab9a24e1..b00d0ba316 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -1,8 +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.Collections.Generic; using osu.Framework.Localisation; +using osu.Game.Screens.Select; namespace osu.Game.Beatmaps { @@ -29,20 +29,22 @@ namespace osu.Game.Beatmaps return new RomanisableString($"{metadata.GetPreferred(true)}".Trim(), $"{metadata.GetPreferred(false)}".Trim()); } - public static List GetSearchableTerms(this IBeatmapInfo beatmapInfo) + public static bool Match(this IBeatmapInfo beatmapInfo, params FilterCriteria.OptionalTextFilter[] filters) { - var termsList = new List(BeatmapMetadataInfoExtensions.MAX_SEARCHABLE_TERM_COUNT + 1); - - addIfNotNull(beatmapInfo.DifficultyName); - - BeatmapMetadataInfoExtensions.CollectSearchableTerms(beatmapInfo.Metadata, termsList); - return termsList; - - void addIfNotNull(string? s) + foreach (var filter in filters) { - if (!string.IsNullOrEmpty(s)) - termsList.Add(s); + if (filter.Matches(beatmapInfo.DifficultyName)) + continue; + + if (BeatmapMetadataInfoExtensions.Match(beatmapInfo.Metadata, filter)) + continue; + + // failed to match a single filter at all - fail the whole match. + return false; } + + // got through all filters without failing any - pass the whole match. + return true; } private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]"; diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index d71d7b7f67..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 { @@ -284,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. diff --git a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs index be96a66614..198469dba6 100644 --- a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs @@ -3,11 +3,14 @@ using System.Collections.Generic; using osu.Framework.Localisation; +using osu.Game.Screens.Select; namespace osu.Game.Beatmaps { public static class BeatmapMetadataInfoExtensions { + internal const int MAX_SEARCHABLE_TERM_COUNT = 7; + /// /// An array of all searchable terms provided in contained metadata. /// @@ -18,7 +21,18 @@ namespace osu.Game.Beatmaps return termsList.ToArray(); } - internal const int MAX_SEARCHABLE_TERM_COUNT = 7; + public static bool Match(IBeatmapMetadataInfo metadataInfo, FilterCriteria.OptionalTextFilter filter) + { + if (filter.Matches(metadataInfo.Author.Username)) return true; + if (filter.Matches(metadataInfo.Artist)) return true; + if (filter.Matches(metadataInfo.ArtistUnicode)) return true; + if (filter.Matches(metadataInfo.Title)) return true; + if (filter.Matches(metadataInfo.TitleUnicode)) return true; + if (filter.Matches(metadataInfo.Source)) return true; + if (filter.Matches(metadataInfo.Tags)) return true; + + return false; + } internal static void CollectSearchableTerms(IBeatmapMetadataInfo metadataInfo, IList termsList) { diff --git a/osu.Game/Beatmaps/BeatmapProcessor.cs b/osu.Game/Beatmaps/BeatmapProcessor.cs index fb5313469f..89d6e9d3f8 100644 --- a/osu.Game/Beatmaps/BeatmapProcessor.cs +++ b/osu.Game/Beatmaps/BeatmapProcessor.cs @@ -24,12 +24,6 @@ namespace osu.Game.Beatmaps foreach (var obj in Beatmap.HitObjects.OfType()) { - if (lastObj == null) - { - // first hitobject should always be marked as a new combo for sanity. - obj.NewCombo = true; - } - obj.UpdateComboInformation(lastObj); lastObj = obj; } diff --git a/osu.Game/Beatmaps/BeatmapUpdater.cs b/osu.Game/Beatmaps/BeatmapUpdater.cs index 56bfdc5001..e897d28916 100644 --- a/osu.Game/Beatmaps/BeatmapUpdater.cs +++ b/osu.Game/Beatmaps/BeatmapUpdater.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Logging; @@ -10,6 +11,7 @@ using osu.Framework.Platform; using osu.Framework.Threading; using osu.Game.Database; using osu.Game.Online.API; +using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Beatmaps { @@ -44,7 +46,8 @@ namespace osu.Game.Beatmaps public void Queue(Live beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) { Logger.Log($"Queueing change for local beatmap {beatmapSet}"); - Task.Factory.StartNew(() => beatmapSet.PerformRead(b => Process(b, lookupScope)), default, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + Task.Factory.StartNew(() => beatmapSet.PerformRead(b => Process(b, lookupScope)), default, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, + updateScheduler); } /// @@ -74,12 +77,29 @@ namespace osu.Game.Beatmaps beatmap.StarRating = calculator.Calculate().StarRating; beatmap.Length = working.Beatmap.CalculatePlayableLength(); beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength(); + beatmap.EndTimeObjectCount = working.Beatmap.HitObjects.Count(h => h is IHasDuration); + beatmap.TotalObjectCount = working.Beatmap.HitObjects.Count; } // And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required. workingBeatmapCache.Invalidate(beatmapSet); }); + public void ProcessObjectCounts(BeatmapInfo beatmapInfo, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapInfo.Realm!.Write(_ => + { + // Before we use below, we want to invalidate. + workingBeatmapCache.Invalidate(beatmapInfo); + + var working = workingBeatmapCache.GetWorkingBeatmap(beatmapInfo); + var beatmap = working.Beatmap; + + beatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration); + beatmapInfo.TotalObjectCount = beatmap.HitObjects.Count; + + // And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required. + workingBeatmapCache.Invalidate(beatmapInfo); + }); + #region Implementation of IDisposable public void Dispose() 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/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/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/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 ad91615031..5a26a988fb 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -3,15 +3,15 @@ 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 { @@ -25,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; @@ -33,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, @@ -68,7 +75,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load() { progress.Colour = colourProvider.Highlight1; } @@ -89,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/Statistics/BeatmapCardStatistic.cs b/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardStatistic.cs index 10de2b9128..6fd7142c05 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardStatistic.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardStatistic.cs @@ -74,7 +74,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Statistics #region Tooltip implementation - public virtual ITooltip GetCustomTooltip() => null; + public virtual ITooltip GetCustomTooltip() => null!; public virtual object TooltipContent => null; #endregion diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 1665ec52fa..eecf79aa34 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -92,7 +92,6 @@ namespace osu.Game.Beatmaps.Drawables EdgeEffect = new EdgeEffectParameters { Colour = Color4.Black.Opacity(0.06f), - Type = EdgeEffectType.Shadow, Radius = 3, }, diff --git a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs index df8953d57c..55ef6f705e 100644 --- a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.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.Extensions.Color4Extensions; @@ -38,6 +39,8 @@ namespace osu.Game.Beatmaps.Drawables private readonly Bindable displayedStars = new BindableDouble(); + private readonly Container textContainer; + /// /// The currently displayed stars of this display wrapped in a bindable. /// This bindable gets transformed on change rather than instantaneous, if animation is enabled. @@ -116,15 +119,19 @@ namespace osu.Game.Beatmaps.Drawables Size = new Vector2(8f), }, Empty(), - starsText = new OsuSpriteText + textContainer = new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Margin = new MarginPadding { Bottom = 1.5f }, - // todo: this should be size: 12f, but to match up with the design, it needs to be 14.4f - // see https://github.com/ppy/osu-framework/issues/3271. - Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), - Shadow = false, + AutoSizeAxes = Axes.Y, + Child = starsText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Margin = new MarginPadding { Bottom = 1.5f }, + // todo: this should be size: 12f, but to match up with the design, it needs to be 14.4f + // see https://github.com/ppy/osu-framework/issues/3271. + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + Shadow = false, + }, }, } } @@ -155,6 +162,11 @@ namespace osu.Game.Beatmaps.Drawables starIcon.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4Extensions.FromHex("303d47"); starsText.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4.Black.Opacity(0.75f); + + // In order to avoid autosize throwing the width of these displays all over the place, + // let's lock in some sane defaults for the text width based on how many digits we're + // displaying. + textContainer.Width = 24 + Math.Max(starsText.Text.ToString().Length - 4, 0) * 6; }, true); } } 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 76% rename from osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs rename to osu.Game/Beatmaps/FlatWorkingBeatmap.cs index d20baf1edb..c2505ec109 100644 --- a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/FlatWorkingBeatmap.cs @@ -12,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/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 65a01befb4..1ee4670ae2 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; @@ -94,6 +93,8 @@ namespace osu.Game.Beatmaps.Formats // The parsing order of hitobjects matters in mania difficulty calculation this.beatmap.HitObjects = this.beatmap.HitObjects.OrderBy(h => h.StartTime).ToList(); + postProcessBreaks(this.beatmap); + foreach (var hitObject in this.beatmap.HitObjects) { applyDefaults(hitObject); @@ -101,19 +102,36 @@ namespace osu.Game.Beatmaps.Formats } } + /// + /// Processes the beatmap such that a new combo is started the first hitobject following each break. + /// + private void postProcessBreaks(Beatmap beatmap) + { + int currentBreak = 0; + bool forceNewCombo = false; + + foreach (var h in beatmap.HitObjects.OfType()) + { + while (currentBreak < beatmap.Breaks.Count && beatmap.Breaks[currentBreak].EndTime < h.StartTime) + { + forceNewCombo = true; + currentBreak++; + } + + h.NewCombo |= forceNewCombo; + forceNewCombo = false; + } + } + private void applyDefaults(HitObject hitObject) { 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 +222,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 +241,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 +518,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 041b00c7e1..290d29090a 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; @@ -26,16 +23,9 @@ namespace osu.Game.Beatmaps.Formats { public const int FIRST_LAZER_VERSION = 128; - /// - /// osu! is generally slower than taiko, so a factor is added to increase - /// speed. This must be used everywhere slider length or beat length is used. - /// - public const float LEGACY_TAIKO_VELOCITY_MULTIPLIER = 1.4f; - private readonly IBeatmap beatmap; - [CanBeNull] - private readonly ISkin skin; + private readonly ISkin? skin; private readonly int onlineRulesetID; @@ -44,7 +34,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; @@ -153,11 +143,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(FormattableString.Invariant($"OverallDifficulty: {beatmap.Difficulty.OverallDifficulty}")); writer.WriteLine(FormattableString.Invariant($"ApproachRate: {beatmap.Difficulty.ApproachRate}")); - // Taiko adjusts the slider multiplier (see: LEGACY_TAIKO_VELOCITY_MULTIPLIER) - writer.WriteLine(onlineRulesetID == 1 - ? FormattableString.Invariant($"SliderMultiplier: {beatmap.Difficulty.SliderMultiplier / LEGACY_TAIKO_VELOCITY_MULTIPLIER}") - : FormattableString.Invariant($"SliderMultiplier: {beatmap.Difficulty.SliderMultiplier}")); - + writer.WriteLine(FormattableString.Invariant($"SliderMultiplier: {beatmap.Difficulty.SliderMultiplier}")); writer.WriteLine(FormattableString.Invariant($"SliderTickRate: {beatmap.Difficulty.SliderTickRate}")); } @@ -180,8 +166,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 +259,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 }; } } @@ -441,7 +427,7 @@ namespace osu.Game.Beatmaps.Formats // Explicit segments have a new format in which the type is injected into the middle of the control point string. // To preserve compatibility with osu-stable as much as possible, explicit segments with the same type are converted to use implicit segments by duplicating the control point. // One exception are consecutive perfect curves, which aren't supported in osu!stable and can lead to decoding issues if encoded as implicit segments - bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PerfectCurve; + bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECT_CURVE; // Another exception to this is when the last two control points of the last segment were duplicated. This is not a scenario supported by osu!stable. // Lazer does not add implicit segments for the last two control points of _any_ explicit segment, so an explicit segment is forced in order to maintain consistency with the decoder. @@ -457,21 +443,21 @@ namespace osu.Game.Beatmaps.Formats if (needsExplicitSegment) { - switch (point.Type) + switch (point.Type?.Type) { - case PathType.Bezier: - writer.Write("B|"); + case SplineType.BSpline: + writer.Write(point.Type.Value.Degree > 0 ? $"B{point.Type.Value.Degree}|" : "B|"); break; - case PathType.Catmull: + case SplineType.Catmull: writer.Write("C|"); break; - case PathType.PerfectCurve: + case SplineType.PerfectCurve: writer.Write("P|"); break; - case PathType.Linear: + case SplineType.Linear: writer.Write("L|"); break; } @@ -585,7 +571,7 @@ namespace osu.Game.Beatmaps.Formats return type; } - private LegacySampleBank toLegacySampleBank(string sampleBank) + private LegacySampleBank toLegacySampleBank(string? sampleBank) { switch (sampleBank?.ToLowerInvariant()) { @@ -603,7 +589,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/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/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 399e2fde7f..d0ffbdd459 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,21 +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. - /// - public readonly BindableDouble ExternalPauseFrequencyAdjust = new BindableDouble(1); - private readonly OffsetCorrectionClock? userGlobalOffsetClock; private readonly OffsetCorrectionClock? platformOffsetClock; private readonly OffsetCorrectionClock? userBeatmapOffsetClock; @@ -53,7 +37,7 @@ namespace osu.Game.Beatmaps private IDisposable? beatmapOffsetSubscription; - private readonly DecoupleableInterpolatingFramedClock decoupledClock; + private readonly DecouplingFramedClock decoupledTrack; [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -66,35 +50,31 @@ namespace osu.Game.Beatmaps public bool IsRewinding { get; private set; } - public bool IsCoupled - { - get => decoupledClock.IsCoupled; - set => decoupledClock.IsCoupled = value; - } - - 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) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; // User global offset (set in settings) should also be applied. - userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock, ExternalPauseFrequencyAdjust); + userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock); // User per-beatmap offset will be applied to this final clock. - finalClockSource = userBeatmapOffsetClock = new OffsetCorrectionClock(userGlobalOffsetClock, ExternalPauseFrequencyAdjust); + finalClockSource = userBeatmapOffsetClock = new OffsetCorrectionClock(userGlobalOffsetClock); } else { - finalClockSource = decoupledClock; + finalClockSource = interpolatedTrack; } } @@ -110,6 +90,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, @@ -124,17 +105,7 @@ 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; @@ -157,46 +128,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 @@ -216,8 +183,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/IBeatmapDifficultyInfo.cs b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs index 78234a9dd9..48f6564084 100644 --- a/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.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; + namespace osu.Game.Beatmaps { /// @@ -55,13 +57,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. /// @@ -85,5 +94,21 @@ namespace osu.Game.Beatmaps /// Value to which the difficulty value maps in the specified range. static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range) => DifficultyRange(difficulty, range.od0, range.od5, range.od10); + + /// + /// Inverse function to . + /// Maps a value returned by the function above back to the difficulty that produced it. + /// + /// The difficulty-dependent value to be unmapped. + /// Minimum of the resulting range which will be achieved by a difficulty value of 0. + /// Midpoint of the resulting range which will be achieved by a difficulty value of 5. + /// Maximum of the resulting range which will be achieved by a difficulty value of 10. + /// Value to which the difficulty value maps in the specified range. + static double InverseDifficultyRange(double difficultyValue, double diff0, double diff5, double diff10) + { + return Math.Sign(difficultyValue - diff5) == Math.Sign(diff10 - diff5) + ? (difficultyValue - diff5) / (diff10 - diff5) * 5 + 5 + : (difficultyValue - diff5) / (diff5 - diff0) * 5 + 5; + } } } diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs index b8c69cc525..04c2017ded 100644 --- a/osu.Game/Beatmaps/IBeatmapInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapInfo.cs @@ -59,7 +59,23 @@ namespace osu.Game.Beatmaps /// /// The basic star rating for this beatmap (with no mods applied). + /// Defaults to -1 (meaning not-yet-calculated). /// double StarRating { get; } + + /// + /// The number of hitobjects in the beatmap with a distinct end time. + /// Defaults to -1 (meaning not-yet-calculated). + /// + /// + /// Canonically, these are hitobjects are either sliders or spinners. + /// + int EndTimeObjectCount { get; } + + /// + /// The total number of hitobjects in the beatmap. + /// Defaults to -1 (meaning not-yet-calculated). + /// + int TotalObjectCount { get; } } } diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 78eed626f2..2c500146c5 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -86,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; diff --git a/osu.Game/Collections/CollectionDropdown.cs b/osu.Game/Collections/CollectionDropdown.cs index e435992381..249a0c35e7 100644 --- a/osu.Game/Collections/CollectionDropdown.cs +++ b/osu.Game/Collections/CollectionDropdown.cs @@ -43,11 +43,14 @@ namespace osu.Game.Collections private IDisposable? realmSubscription; + private readonly CollectionFilterMenuItem allBeatmapsItem = new AllBeatmapsCollectionFilterMenuItem(); + public CollectionDropdown() { ItemSource = filters; - Current.Value = new AllBeatmapsCollectionFilterMenuItem(); + Current.Value = allBeatmapsItem; + AlwaysShowSearchBar = true; } protected override void LoadComplete() @@ -61,37 +64,52 @@ namespace osu.Game.Collections private void collectionsChanged(IRealmCollection collections, ChangeSet? changes) { - var selectedItem = SelectedItem?.Value?.Collection; - - var allBeatmaps = new AllBeatmapsCollectionFilterMenuItem(); - - filters.Clear(); - filters.Add(allBeatmaps); - filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c.ToLive(realm)))); - - if (ShowManageCollectionsItem) - filters.Add(new ManageCollectionsFilterMenuItem()); - - // This current update and schedule is required to work around dropdown headers not updating text even when the selected item - // changes. It's not great but honestly the whole dropdown menu structure isn't great. This needs to be fixed, but I'll issue - // a warning that it's going to be a frustrating journey. - Current.Value = allBeatmaps; - Schedule(() => + if (changes == null) { - // current may have changed before the scheduled call is run. - if (Current.Value != allBeatmaps) - return; - - Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection.ID == selectedItem?.ID) ?? filters[0]; - }); - - // Trigger a re-filter if the current item was in the change set. - if (selectedItem != null && changes != null) + filters.Clear(); + filters.Add(allBeatmapsItem); + filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c.ToLive(realm)))); + if (ShowManageCollectionsItem) + filters.Add(new ManageCollectionsFilterMenuItem()); + } + else { - foreach (int index in changes.ModifiedIndices) + foreach (int i in changes.DeletedIndices.OrderByDescending(i => i)) + filters.RemoveAt(i + 1); + + foreach (int i in changes.InsertedIndices) + filters.Insert(i + 1, new CollectionFilterMenuItem(collections[i].ToLive(realm))); + + var selectedItem = SelectedItem?.Value; + + foreach (int i in changes.NewModifiedIndices) { - if (collections[index].ID == selectedItem.ID) + var updatedItem = collections[i]; + + // This is responsible for updating the state of the +/- button and the collection's name. + // TODO: we can probably make the menu items update with changes to avoid this. + filters.RemoveAt(i + 1); + filters.Insert(i + 1, new CollectionFilterMenuItem(updatedItem.ToLive(realm))); + + if (updatedItem.ID == selectedItem?.Collection?.ID) + { + // This current update and schedule is required to work around dropdown headers not updating text even when the selected item + // changes. It's not great but honestly the whole dropdown menu structure isn't great. This needs to be fixed, but I'll issue + // a warning that it's going to be a frustrating journey. + Current.Value = allBeatmapsItem; + Schedule(() => + { + // current may have changed before the scheduled call is run. + if (Current.Value != allBeatmapsItem) + return; + + Current.Value = filters.SingleOrDefault(f => f.Collection?.ID == selectedItem.Collection?.ID) ?? filters[0]; + }); + + // Trigger an external re-filter if the current item was in the change set. RequestFilter?.Invoke(); + break; + } } } } diff --git a/osu.Game/Collections/CollectionFilterMenuItem.cs b/osu.Game/Collections/CollectionFilterMenuItem.cs index 2ac5784f09..49262ed917 100644 --- a/osu.Game/Collections/CollectionFilterMenuItem.cs +++ b/osu.Game/Collections/CollectionFilterMenuItem.cs @@ -37,22 +37,17 @@ namespace osu.Game.Collections CollectionName = name; } - public bool Equals(CollectionFilterMenuItem? other) + public virtual bool Equals(CollectionFilterMenuItem? other) { - if (other == null) - return false; + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; - // collections may have the same name, so compare first on reference equality. - // this relies on the assumption that only one instance of the BeatmapCollection exists game-wide, managed by CollectionManager. - if (Collection != null) - return Collection.ID == other.Collection?.ID; + if (Collection == null) return false; - // fallback to name-based comparison. - // this is required for special dropdown items which don't have a collection (all beatmaps / manage collections items below). - return CollectionName == other.CollectionName; + return Collection.ID == other.Collection?.ID; } - public override int GetHashCode() => CollectionName.GetHashCode(); + public override int GetHashCode() => Collection?.ID.GetHashCode() ?? 0; } public class AllBeatmapsCollectionFilterMenuItem : CollectionFilterMenuItem @@ -61,6 +56,10 @@ namespace osu.Game.Collections : base("All beatmaps") { } + + public override bool Equals(CollectionFilterMenuItem? other) => other is AllBeatmapsCollectionFilterMenuItem; + + public override int GetHashCode() => 1; } public class ManageCollectionsFilterMenuItem : CollectionFilterMenuItem @@ -69,5 +68,9 @@ namespace osu.Game.Collections : base("Manage collections...") { } + + public override bool Equals(CollectionFilterMenuItem? other) => other is ManageCollectionsFilterMenuItem; + + public override int GetHashCode() => 2; } } diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 4131148f3f..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); diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index 31016b807b..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; diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index edcbb94368..6b2cb4ee74 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -17,9 +17,11 @@ using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.Mods.Input; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Skinning; +using osu.Game.Users; namespace osu.Game.Configuration { @@ -37,7 +39,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.Ruleset, string.Empty); SetDefault(OsuSetting.Skin, SkinInfo.ARGON_SKIN.ToString()); - SetDefault(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details); + SetDefault(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Local); SetDefault(OsuSetting.BeatmapDetailModsFilter, false); SetDefault(OsuSetting.ShowConvertedBeatmaps, true); @@ -49,6 +51,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.RandomSelectAlgorithm, RandomSelectAlgorithm.RandomPermutation); SetDefault(OsuSetting.ModSelectHotkeyStyle, ModSelectHotkeyStyle.Sequential); + SetDefault(OsuSetting.ModSelectTextSearchStartsActive, true); SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f); @@ -64,7 +67,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 => { @@ -89,6 +98,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.MenuVoice, true); SetDefault(OsuSetting.MenuMusic, true); + SetDefault(OsuSetting.MenuTips, true); SetDefault(OsuSetting.AudioOffset, 0, -500.0, 500.0, 1); @@ -102,6 +112,8 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.MouseDisableWheel, false); SetDefault(OsuSetting.ConfineMouseMode, OsuConfineMouseMode.DuringGameplay); + SetDefault(OsuSetting.TouchDisableGameplayTaps, false); + // Graphics SetDefault(OsuSetting.ShowFpsDisplay, false); @@ -130,6 +142,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true); SetDefault(OsuSetting.KeyOverlay, false); SetDefault(OsuSetting.ReplaySettingsOverlay, true); + SetDefault(OsuSetting.ReplayPlaybackControlsExpanded, true); SetDefault(OsuSetting.GameplayLeaderboard, true); SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true); @@ -178,10 +191,14 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.EditorShowHitMarkers, true); SetDefault(OsuSetting.EditorAutoSeekOnPlacement, true); SetDefault(OsuSetting.EditorLimitedDistanceSnap, false); + SetDefault(OsuSetting.EditorShowSpeedChanges, false); + + SetDefault(OsuSetting.MultiplayerRoomFilter, RoomPermissionsFilter.All); SetDefault(OsuSetting.LastProcessedMetadataId, -1); SetDefault(OsuSetting.ComboColourNormalisationAmount, 0.2f, 0f, 1f, 0.01f); + SetDefault(OsuSetting.UserOnlineStatus, null); } protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup) @@ -215,6 +232,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 +260,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, @@ -311,6 +340,10 @@ namespace osu.Game.Configuration ShowHealthDisplayWhenCantFail, FadePlayfieldWhenHealthLow, + + /// + /// Disables mouse buttons clicks during gameplay. + /// MouseDisableButtons, MouseDisableWheel, ConfineMouseMode, @@ -324,6 +357,7 @@ namespace osu.Game.Configuration VolumeInactive, MenuMusic, MenuVoice, + MenuTips, CursorRotation, MenuParallax, Prefer24HourTime, @@ -377,13 +411,23 @@ namespace osu.Game.Configuration EditorShowHitMarkers, EditorAutoSeekOnPlacement, DiscordRichPresence, + + [Obsolete($"Use {nameof(AutomaticallyDownloadMissingBeatmaps)} instead.")] // can be removed 20240318 AutomaticallyDownloadWhenSpectating, + ShowOnlineExplicitContent, LastProcessedMetadataId, SafeAreaConsiderations, ComboColourNormalisationAmount, ProfileCoverExpanded, EditorLimitedDistanceSnap, - ReplaySettingsOverlay + ReplaySettingsOverlay, + ReplayPlaybackControlsExpanded, + AutomaticallyDownloadMissingBeatmaps, + EditorShowSpeedChanges, + TouchDisableGameplayTaps, + ModSelectTextSearchStartsActive, + UserOnlineStatus, + MultiplayerRoomFilter } } diff --git a/osu.Game/Configuration/SessionAverageHitErrorTracker.cs b/osu.Game/Configuration/SessionAverageHitErrorTracker.cs new file mode 100644 index 0000000000..cd21eb6fa8 --- /dev/null +++ b/osu.Game/Configuration/SessionAverageHitErrorTracker.cs @@ -0,0 +1,73 @@ +// 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.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; + +namespace osu.Game.Configuration +{ + /// + /// Tracks the local user's average hit error during the ongoing play session. + /// + [Cached] + public partial class SessionAverageHitErrorTracker : Component + { + public IBindableList AverageHitErrorHistory => averageHitErrorHistory; + private readonly BindableList averageHitErrorHistory = new BindableList(); + + private readonly Bindable latestScore = new Bindable(); + + [Resolved] + private OsuConfigManager configManager { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(SessionStatics statics) + { + statics.BindWith(Static.LastLocalUserScore, latestScore); + latestScore.BindValueChanged(score => calculateAverageHitError(score.NewValue), true); + } + + private void calculateAverageHitError(ScoreInfo? newScore) + { + if (newScore == null) + return; + + if (newScore.Mods.Any(m => !m.UserPlayable || m is IHasNoTimedInputs)) + return; + + if (newScore.HitEvents.Count < 10) + return; + + if (newScore.HitEvents.CalculateAverageHitError() is not double averageError) + return; + + // keep a sane maximum number of entries. + if (averageHitErrorHistory.Count >= 50) + averageHitErrorHistory.RemoveAt(0); + + double globalOffset = configManager.Get(OsuSetting.AudioOffset); + averageHitErrorHistory.Add(new DataPoint(averageError, globalOffset)); + } + + public void ClearHistory() => averageHitErrorHistory.Clear(); + + public readonly struct DataPoint + { + public double AverageHitError { get; } + public double GlobalAudioOffset { get; } + + public double SuggestedGlobalAudioOffset => GlobalAudioOffset - AverageHitError; + + public DataPoint(double averageHitError, double globalOffset) + { + AverageHitError = averageHitError; + GlobalAudioOffset = globalOffset; + } + } + } +} diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 5e2f0c2128..1548b781a7 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -3,10 +3,13 @@ #nullable disable +using osu.Framework; using osu.Game.Graphics.UserInterface; +using osu.Game.Input; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Mods; +using osu.Game.Scoring; namespace osu.Game.Configuration { @@ -24,6 +27,8 @@ namespace osu.Game.Configuration SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null); SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null); SetDefault(Static.SeasonalBackgrounds, null); + SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile); + SetDefault(Static.LastLocalUserScore, null); } /// @@ -63,6 +68,17 @@ namespace osu.Game.Configuration /// 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 + LastModSelectPanelSamplePlaybackTime, + + /// + /// Whether the last positional input received was a touch input. + /// Used in touchscreen detection scenarios (). + /// + TouchInputActive, + + /// + /// Stores the local user's last score (can be completed or aborted). + /// + LastLocalUserScore, } } diff --git a/osu.Game/BackgroundBeatmapProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs similarity index 50% rename from osu.Game/BackgroundBeatmapProcessor.cs rename to osu.Game/Database/BackgroundDataStoreProcessor.cs index b553fee503..be0c83bdb3 100644 --- a/osu.Game/BackgroundBeatmapProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -13,7 +12,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; -using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; @@ -22,10 +21,15 @@ using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using osu.Game.Screens.Play; -namespace osu.Game +namespace osu.Game.Database { - public partial class BackgroundBeatmapProcessor : Component + /// + /// Performs background updating of data stores at startup. + /// + public partial class BackgroundDataStoreProcessor : Component { + protected Task ProcessingTask { get; private set; } = null!; + [Resolved] private RulesetStore rulesetStore { get; set; } = null!; @@ -59,13 +63,17 @@ namespace osu.Game { base.LoadComplete(); - Task.Factory.StartNew(() => + ProcessingTask = Task.Factory.StartNew(() => { - Logger.Log("Beginning background beatmap processing.."); + Logger.Log("Beginning background data store processing.."); + checkForOutdatedStarRatings(); processBeatmapSetsWithMissingMetrics(); + // Note that the previous method will also update these on a fresh run. + processBeatmapsWithMissingObjectCounts(); processScoresWithMissingStatistics(); convertLegacyTotalScoreToStandardised(); + upgradeScoreRanks(); }, TaskCreationOptions.LongRunning).ContinueWith(t => { if (t.Exception?.InnerException is ObjectDisposedException) @@ -74,7 +82,7 @@ namespace osu.Game return; } - Logger.Log("Finished background beatmap processing!"); + Logger.Log("Finished background data store processing!"); }); } @@ -130,28 +138,34 @@ namespace osu.Game // 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); - } + foreach (var b in r.All().Where(b => (b.StarRating < 0 || (b.OnlineID > 0 && b.LastOnlineUpdate == null)) && 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); - } + foreach (var b in r.All().Where(b => b.StarRating < 0 && b.BeatmapSet != null)) + beatmapSetIds.Add(b.BeatmapSet!.ID); } }); + if (beatmapSetIds.Count == 0) + return; + Logger.Log($"Found {beatmapSetIds.Count} beatmap sets which require reprocessing."); - int i = 0; + // Technically this is doing more than just star ratings, but easier for the end user to understand. + var notification = showProgressNotification(beatmapSetIds.Count, "Reprocessing star rating for beatmaps", "beatmaps' star ratings have been updated"); + + int processedCount = 0; + int failedCount = 0; foreach (var id in beatmapSetIds) { + if (notification?.State == ProgressNotificationState.Cancelled) + break; + + updateNotificationProgress(notification, processedCount, beatmapSetIds.Count); + sleepIfRequired(); realmAccess.Run(r => @@ -162,16 +176,73 @@ namespace osu.Game { try { - Logger.Log($"Background processing {set} ({++i} / {beatmapSetIds.Count})"); beatmapUpdater.Process(set); + ++processedCount; } catch (Exception e) { Logger.Log($"Background processing failed on {set}: {e}"); + ++failedCount; } } }); } + + completeNotification(notification, processedCount, beatmapSetIds.Count, failedCount); + } + + private void processBeatmapsWithMissingObjectCounts() + { + Logger.Log("Querying for beatmaps with missing hitobject counts to reprocess..."); + + HashSet beatmapIds = new HashSet(); + + realmAccess.Run(r => + { + foreach (var b in r.All().Where(b => b.TotalObjectCount < 0 || b.EndTimeObjectCount < 0)) + beatmapIds.Add(b.ID); + }); + + if (beatmapIds.Count == 0) + return; + + Logger.Log($"Found {beatmapIds.Count} beatmaps which require statistics population."); + + var notification = showProgressNotification(beatmapIds.Count, "Populating missing statistics for beatmaps", "beatmaps have been populated with missing statistics"); + + int processedCount = 0; + int failedCount = 0; + + foreach (var id in beatmapIds) + { + if (notification?.State == ProgressNotificationState.Cancelled) + break; + + updateNotificationProgress(notification, processedCount, beatmapIds.Count); + + sleepIfRequired(); + + realmAccess.Run(r => + { + var beatmap = r.Find(id); + + if (beatmap != null) + { + try + { + beatmapUpdater.ProcessObjectCounts(beatmap); + ++processedCount; + } + catch (Exception e) + { + Logger.Log($"Background processing failed on {beatmap}: {e}"); + ++failedCount; + } + } + }); + } + + completeNotification(notification, processedCount, beatmapIds.Count, failedCount); } private void processScoresWithMissingStatistics() @@ -182,7 +253,7 @@ namespace osu.Game realmAccess.Run(r => { - foreach (var score in r.All()) + foreach (var score in r.All().Where(s => !s.BackgroundReprocessingFailed)) { if (score.BeatmapInfo != null && score.Statistics.Sum(kvp => kvp.Value) > 0 @@ -193,10 +264,23 @@ namespace osu.Game } }); - Logger.Log($"Found {scoreIds.Count} scores which require reprocessing."); + if (scoreIds.Count == 0) + return; + + Logger.Log($"Found {scoreIds.Count} scores which require statistics population."); + + var notification = showProgressNotification(scoreIds.Count, "Populating missing statistics for scores", "scores have been populated with missing statistics"); + + int processedCount = 0; + int failedCount = 0; foreach (var id in scoreIds) { + if (notification?.State == ProgressNotificationState.Cancelled) + break; + + updateNotificationProgress(notification, processedCount, scoreIds.Count); + sleepIfRequired(); try @@ -212,7 +296,7 @@ namespace osu.Game r.Find(id)!.MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics); }); - Logger.Log($"Populated maximum statistics for score {id}"); + ++processedCount; } catch (ObjectDisposedException) { @@ -221,55 +305,60 @@ namespace osu.Game catch (Exception e) { Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}"); + realmAccess.Write(r => r.Find(id)!.BackgroundReprocessingFailed = true); + ++failedCount; } } + + completeNotification(notification, processedCount, scoreIds.Count, failedCount); } 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.BeatmapInfo != null && s.TotalScoreVersion == 30000002) - .AsEnumerable().Select(s => s.ID))); + HashSet scoreIds = realmAccess.Run(r => new HashSet( + r.All() + .Where(s => !s.BackgroundReprocessingFailed + && s.BeatmapInfo != null + && s.IsLegacyScore + && s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION) + .AsEnumerable() + // must be done after materialisation, as realm doesn't want to support + // nested property predicates + .Where(s => s.Ruleset.IsLegacyRuleset()) + .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); + var notification = showProgressNotification(scoreIds.Count, "Upgrading scores to new scoring algorithm", "scores have been upgraded to the new scoring algorithm"); int processedCount = 0; int failedCount = 0; foreach (var id in scoreIds) { - if (notification.State == ProgressNotificationState.Cancelled) + if (notification?.State == ProgressNotificationState.Cancelled) break; - notification.Text = $"Upgrading scores to new scoring algorithm ({processedCount} of {scoreIds.Count})"; - notification.Progress = (float)processedCount / scoreIds.Count; + updateNotificationProgress(notification, 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; + StandardisedScoreMigrationTools.UpdateFromLegacy(s, beatmapManager.GetWorkingBeatmap(s.BeatmapInfo)); s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION; }); - Logger.Log($"Converted total score for score {id}"); ++processedCount; } catch (ObjectDisposedException) @@ -279,28 +368,129 @@ namespace osu.Game 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) + completeNotification(notification, processedCount, scoreIds.Count, failedCount); + } + + private void upgradeScoreRanks() + { + Logger.Log("Querying for scores that need rank upgrades..."); + + HashSet scoreIds = realmAccess.Run(r => new HashSet( + r.All() + .Where(s => s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION) + .AsEnumerable() + // must be done after materialisation, as realm doesn't support + // filtering on nested property predicates or projection via `.Select()` + .Where(s => s.Ruleset.IsLegacyRuleset()) + .Select(s => s.ID))); + + Logger.Log($"Found {scoreIds.Count} scores which require rank upgrades."); + + if (scoreIds.Count == 0) + return; + + var notification = showProgressNotification(scoreIds.Count, "Adjusting ranks of scores", "scores now have more correct ranks"); + + int processedCount = 0; + int failedCount = 0; + + foreach (var id in scoreIds) { - notification.CompletionText = $"{processedCount} score(s) have been upgraded to the new scoring algorithm"; + if (notification?.State == ProgressNotificationState.Cancelled) + break; + + updateNotificationProgress(notification, processedCount, scoreIds.Count); + + sleepIfRequired(); + + try + { + // 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.Rank = StandardisedScoreMigrationTools.ComputeRank(s); + s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION; + }); + + ++processedCount; + } + catch (ObjectDisposedException) + { + throw; + } + catch (Exception e) + { + Logger.Log($"Failed to update rank score {id}: {e}"); + realmAccess.Write(r => r.Find(id)!.BackgroundReprocessingFailed = true); + ++failedCount; + } + } + + completeNotification(notification, processedCount, scoreIds.Count, failedCount); + } + + private void updateNotificationProgress(ProgressNotification? notification, int processedCount, int totalCount) + { + if (notification == null) + return; + + notification.Text = notification.Text.ToString().Split('(').First().TrimEnd() + $" ({processedCount} of {totalCount})"; + notification.Progress = (float)processedCount / totalCount; + + if (processedCount % 100 == 0) + Logger.Log(notification.Text.ToString()); + } + + private void completeNotification(ProgressNotification? notification, int processedCount, int totalCount, int? failedCount = null) + { + if (notification == null) + return; + + if (processedCount == totalCount) + { + notification.CompletionText = $"{processedCount} {notification.CompletionText}"; notification.Progress = 1; notification.State = ProgressNotificationState.Completed; } else { - notification.Text = $"{processedCount} of {scoreIds.Count} score(s) have been upgraded to the new scoring algorithm."; + notification.Text = $"{processedCount} of {totalCount} {notification.CompletionText}"; // 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.Text += $" Check logs for issues with {failedCount} failed items."; notification.State = ProgressNotificationState.Cancelled; } } + private ProgressNotification? showProgressNotification(int totalCount, string running, string completed) + { + if (notificationOverlay == null) + return null; + + if (totalCount < 10) + return null; + + ProgressNotification notification = new ProgressNotification + { + Text = running, + CompletionText = completed, + State = ProgressNotificationState.Active + }; + + notificationOverlay?.Post(notification); + + return notification; + } + private void sleepIfRequired() { while (localUserPlayInfo?.IsPlaying.Value == true) 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/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/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index e054652efa..69120ea885 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -29,9 +29,9 @@ namespace osu.Game.Database protected override Stream? GetFileContents(BeatmapSetInfo model, INamedFileUsage file) { - bool isBeatmap = model.Beatmaps.Any(o => o.Hash == file.File.Hash); + var beatmapInfo = model.Beatmaps.SingleOrDefault(o => o.Hash == file.File.Hash); - if (!isBeatmap) + if (beatmapInfo == null) return base.GetFileContents(model, file); // Read the beatmap contents and skin @@ -43,6 +43,9 @@ namespace osu.Game.Database 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) @@ -56,10 +59,10 @@ namespace osu.Game.Database // 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 beatmapContent.ControlPointInfo.AllControlPoints) + foreach (var controlPoint in playableBeatmap.ControlPointInfo.AllControlPoints) controlPoint.Time = Math.Floor(controlPoint.Time); - foreach (var hitObject in beatmapContent.HitObjects) + 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) @@ -67,7 +70,23 @@ namespace osu.Game.Database hitObject.StartTime = Math.Floor(hitObject.StartTime); - if (hitObject is not IHasPath hasPath || BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue; + 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 + && hasPath.Path.ControlPoints[0].Type!.Value.Degree == null) continue; var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); @@ -86,7 +105,7 @@ namespace osu.Game.Database // Encode to legacy format var stream = new MemoryStream(); using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); + new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin).Encode(sw); stream.Seek(0, SeekOrigin.Begin); 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/LegacyImportManager.cs b/osu.Game/Database/LegacyImportManager.cs index 20738f859e..7e1641d16f 100644 --- a/osu.Game/Database/LegacyImportManager.cs +++ b/osu.Game/Database/LegacyImportManager.cs @@ -3,6 +3,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework; @@ -54,6 +57,49 @@ namespace osu.Game.Database public void UpdateStorage(string stablePath) => cachedStorage = new StableStorage(stablePath, gameHost as DesktopGameHost); + /// + /// Checks whether a valid location to run a stable import from can be determined starting from the supplied . + /// + /// The directory to check for stable import eligibility. + /// + /// If the return value is , + /// this parameter will contain the to use as the root directory for importing. + /// + public bool IsUsableForStableImport(DirectoryInfo? directory, [NotNullWhen(true)] out DirectoryInfo? stableRoot) + { + if (directory == null) + { + stableRoot = null; + return false; + } + + // A full stable installation will have a configuration file present. + // This is the best case scenario, as it may contain a custom beatmap directory we need to traverse to. + if (directory.GetFiles(@"osu!.*.cfg").Any()) + { + stableRoot = directory; + return true; + } + + // The user may only have their songs or skins folders left. + // We still want to allow them to import based on this. + if (directory.GetDirectories(@"Songs").Any() || directory.GetDirectories(@"Skins").Any()) + { + stableRoot = directory; + return true; + } + + // The user may have traversed *inside* their songs or skins folders. + if (directory.Parent != null && (directory.Name == @"Songs" || directory.Name == @"Skins")) + { + stableRoot = directory.Parent; + return true; + } + + stableRoot = null; + return false; + } + public bool CheckSongsFolderHardLinkAvailability() { var stableStorage = GetCurrentStableStorage(); 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 47feb8a8f9..39dae61d36 100644 --- a/osu.Game/Database/ModelManager.cs +++ b/osu.Game/Database/ModelManager.cs @@ -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); diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index f32b161bb6..4bd7f36cdd 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -25,6 +25,7 @@ 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; @@ -34,6 +35,7 @@ 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; @@ -82,8 +84,15 @@ namespace osu.Game.Database /// 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. + /// 38 2023-12-10 Add EndTimeObjectCount and TotalObjectCount to BeatmapInfo. + /// 39 2023-12-19 Migrate any EndTimeObjectCount and TotalObjectCount values of 0 to -1 to better identify non-calculated values. + /// 40 2023-12-21 Add ScoreInfo.Version to keep track of which build scores were set on. /// - private const int schema_version = 32; + private const int schema_version = 40; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -771,6 +780,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(); @@ -784,6 +794,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. @@ -838,6 +849,7 @@ namespace osu.Game.Database break; case 11: + { string keyBindingClassName = getMappedOrOriginalName(typeof(RealmKeyBinding)); if (!migration.OldRealm.Schema.TryFindObjectSchema(keyBindingClassName, out _)) @@ -864,6 +876,7 @@ namespace osu.Game.Database } break; + } case 14: foreach (var beatmap in migration.NewRealm.All()) @@ -1012,6 +1025,92 @@ namespace osu.Game.Database 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; + } + + case 39: + foreach (var b in migration.NewRealm.All()) + { + // Either actually no objects, or processing ran and failed. + // Reset to -1 so the next time they become zero we know that processing was attempted. + if (b.TotalObjectCount == 0 && b.EndTimeObjectCount == 0) + { + b.TotalObjectCount = -1; + b.EndTimeObjectCount = -1; + } + } + + break; } Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); 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/RealmFileStore.cs b/osu.Game/Database/RealmFileStore.cs index 1da64d5be8..9683baec69 100644 --- a/osu.Game/Database/RealmFileStore.cs +++ b/osu.Game/Database/RealmFileStore.cs @@ -2,8 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.IO; -using System.Linq; using osu.Framework.Extensions; using osu.Framework.IO.Stores; using osu.Framework.Logging; @@ -98,15 +98,11 @@ namespace osu.Game.Database // can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal. realm.Write(r => { - // TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707) - var files = r.All().ToList(); - - foreach (var file in files) + foreach (var file in r.All().Filter(@$"{nameof(RealmFile.Usages)}.@count = 0")) { totalFiles++; - if (file.BacklinksCount > 0) - continue; + Debug.Assert(file.BacklinksCount == 0); try { diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 509fabec59..9e99cba45c 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -30,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) @@ -62,7 +62,7 @@ namespace osu.Game.Database return; } - perform(retrieveFromID(r)); + perform(r.FindWithRefresh(ID)!); RealmLiveStatistics.USAGE_ASYNC.Value++; }); } @@ -84,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) @@ -141,25 +141,10 @@ namespace osu.Game.Database } 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/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 11a5e252a3..403e73ab77 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -10,11 +10,11 @@ using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.IO.Legacy; using osu.Game.Rulesets; -using osu.Game.Rulesets.Difficulty; 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 @@ -57,14 +57,14 @@ namespace osu.Game.Database // 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)) + .OrderByDescending(kvp => processor.GetBaseScoreForResult(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)) + .OrderByDescending(kvp => processor.GetBaseScoreForResult(kvp.Key)) .SelectMany(kvp => Enumerable.Repeat(new FakeJudgement(kvp.Key), kvp.Value)) .ToList(); @@ -169,10 +169,10 @@ namespace osu.Game.Database 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)score.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => numericScoreFor(kvp.Key) * kvp.Value) + / score.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => numericScoreFor(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 bonusScore = score.Statistics.Where(kvp => kvp.Key.IsBonus()).Sum(kvp => numericScoreFor(kvp.Key) * kvp.Value); double accuracyPortion = 0.3; @@ -193,22 +193,99 @@ namespace osu.Game.Database modMultiplier *= mod.ScoreMultiplier; return (long)Math.Round((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier); + + static int numericScoreFor(HitResult result) + { + switch (result) + { + default: + return 0; + + case HitResult.SmallTickHit: + return 10; + + case HitResult.LargeTickHit: + return 30; + + case HitResult.Meh: + return 50; + + case HitResult.Ok: + return 100; + + case HitResult.Good: + return 200; + + case HitResult.Great: + return 300; + + case HitResult.Perfect: + return 315; + + case HitResult.SmallBonus: + return 10; + + case HitResult.LargeBonus: + return 50; + } + } + } + + /// + /// Updates a to standardised scoring. + /// This will recompite the score's (always), (always), + /// and (if the score comes from stable). + /// The total score from stable - if any applicable - will be stored to . + /// + /// The score to update. + /// The applicable for this score. + public static void UpdateFromLegacy(ScoreInfo score, WorkingBeatmap beatmap) + { + var ruleset = score.Ruleset.CreateInstance(); + var scoreProcessor = ruleset.CreateScoreProcessor(); + + // warning: ordering is important here - both total score and ranks are dependent on accuracy! + score.Accuracy = computeAccuracy(score, scoreProcessor); + score.Rank = computeRank(score, scoreProcessor); + score.TotalScore = convertFromLegacyTotalScore(score, ruleset, beatmap); + } + + /// + /// Updates a to standardised scoring. + /// This will recompute the score's (always), (always), + /// and (if the score comes from stable). + /// The total score from stable - if any applicable - will be stored to . + /// + /// + /// This overload is intended for server-side flows. + /// See: https://github.com/ppy/osu-queue-score-statistics/blob/3681e92ac91c6c61922094bdbc7e92e6217dd0fc/osu.Server.Queues.ScoreStatisticsProcessor/Commands/Queue/BatchInserter.cs + /// + /// The score to update. + /// The in which the score was set. + /// The beatmap difficulty. + /// The legacy scoring attributes for the beatmap which the score was set on. + public static void UpdateFromLegacy(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) + { + var scoreProcessor = ruleset.CreateScoreProcessor(); + + // warning: ordering is important here - both total score and ranks are dependent on accuracy! + score.Accuracy = computeAccuracy(score, scoreProcessor); + score.Rank = computeRank(score, scoreProcessor); + score.TotalScore = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); } /// /// Converts from to the new standardised scoring of . /// /// The score to convert the total score of. - /// A used for lookups. + /// The in which the score was set. + /// The applicable for this score. /// The standardised total score. - public static long ConvertFromLegacyTotalScore(ScoreInfo score, BeatmapManager beatmaps) + private static long convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, WorkingBeatmap beatmap) { 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; @@ -222,75 +299,324 @@ namespace osu.Game.Database throw new InvalidOperationException("Beatmap contains no hit objects!"); ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator(); + LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap); - sv1Simulator.Simulate(beatmap, playableBeatmap, mods); - - return ConvertFromLegacyTotalScore(score, new DifficultyAttributes - { - LegacyAccuracyScore = sv1Simulator.AccuracyScore, - LegacyComboScore = sv1Simulator.ComboScore, - LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio - }); + return convertFromLegacyTotalScore(score, ruleset, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes); } /// /// Converts from to the new standardised scoring of . /// /// The score to convert the total score of. - /// Difficulty attributes providing the legacy scoring values - /// (, , and ) - /// for the beatmap which the score was set on. + /// The in which the score was set. + /// 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, DifficultyAttributes attributes) + private static long convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) { if (!score.IsLegacyScore) return score.TotalScore; Debug.Assert(score.LegacyTotalScore != null); - int maximumLegacyAccuracyScore = attributes.LegacyAccuracyScore; - int maximumLegacyComboScore = attributes.LegacyComboScore; - double maximumLegacyBonusRatio = attributes.LegacyBonusScoreRatio; + 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; + long maximumLegacyBonusScore = attributes.BonusScore; + + double legacyAccScore = maximumLegacyAccuracyScore * score.Accuracy; + + double comboProportion; + + if (maximumLegacyComboScore + maximumLegacyBonusScore > 0) + { + // We can not separate the ComboScore from the BonusScore, so we keep the bonus in the ratio. + comboProportion = Math.Max((double)score.LegacyTotalScore - legacyAccScore, 0) / (maximumLegacyComboScore + maximumLegacyBonusScore); + } + else + { + // Two possible causes: + // the beatmap has no bonus objects *AND* + // either the active mods have a zero mod multiplier, in which case assume 0, + // or the *beatmap* has a zero `difficultyPeppyStars` (or just no combo-giving objects), in which case assume 1. + comboProportion = legacyModMultiplier == 0 ? 0 : 1; + } + + // We assume the bonus proportion only makes up the rest of the score that exceeds maximumLegacyBaseScore. + long maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore; + 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); - // The part of total score that doesn't include bonus. - int 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); + long convertedTotalScore; switch (score.Ruleset.OnlineID) { case 0: - return (long)Math.Round(( - 700000 * comboProportion - + 300000 * Math.Pow(score.Accuracy, 10) + if (score.MaxCombo == 0 || score.Accuracy == 0) + { + return (long)Math.Round(( + 0 + + 500000 * Math.Pow(score.Accuracy, 5) + + bonusProportion) * modMultiplier); + } + + // Assumptions: + // - sliders and slider ticks are uniformly distributed in the beatmap, and thus can be ignored without losing much precision. + // We thus consider a map of hit-circles only, which gives objectCount == maximumCombo. + // - the Ok/Meh hit results are uniformly spread in the score, and thus can be ignored without losing much precision. + // We simplify and consider each hit result to have the same hit value of `300 * score.Accuracy` + // (which represents the average hit value over the entire play), + // which allows us to isolate the accuracy multiplier. + + // This is a very ballpark estimate of the maximum magnitude of the combo portion in score V1. + // It is derived by assuming a full combo play and summing up the contribution to combo portion from each individual object. + // Because each object's combo contribution is proportional to the current combo at the time of judgement, + // this can be roughly represented by summing / integrating f(combo) = combo. + // All mod- and beatmap-dependent multipliers and constants are not included here, + // as we will only be using the magnitude of this to compute ratios. + int maximumLegacyCombo = attributes.MaxCombo; + double maximumAchievableComboPortionInScoreV1 = Math.Pow(maximumLegacyCombo, 2); + // Similarly, estimate the maximum magnitude of the combo portion in standardised score. + // Roughly corresponds to integrating f(combo) = combo ^ COMBO_EXPONENT (omitting constants) + double maximumAchievableComboPortionInStandardisedScore = Math.Pow(maximumLegacyCombo, 1 + ScoreProcessor.COMBO_EXPONENT); + + // This is - roughly - how much score, in the combo portion, the longest combo on this particular play would gain in score V1. + double comboPortionFromLongestComboInScoreV1 = Math.Pow(score.MaxCombo, 2); + // Same for standardised score. + double comboPortionFromLongestComboInStandardisedScore = Math.Pow(score.MaxCombo, 1 + ScoreProcessor.COMBO_EXPONENT); + + // We estimate the combo portion of the score in score V1 terms. + // The division by accuracy is supposed to lessen the impact of accuracy on the combo portion, + // but in some edge cases it cannot sanely undo it. + // Therefore the resultant value is clamped from both sides for sanity. + // The clamp from below to `comboPortionFromLongestComboInScoreV1` targets near-FC scores wherein + // the player had bad accuracy at the end of their longest combo, which causes the division by accuracy + // to underestimate the combo portion. + // Ideally, this would be clamped from above to `maximumAchievableComboPortionInScoreV1` too, + // but in practice this appears to fail for some scores (https://github.com/ppy/osu/pull/25876#issuecomment-1862248413). + // TODO: investigate the above more closely + double comboPortionInScoreV1 = Math.Max(maximumAchievableComboPortionInScoreV1 * comboProportion / score.Accuracy, comboPortionFromLongestComboInScoreV1); + + // Calculate how many times the longest combo the user has achieved in the play can repeat + // without exceeding the combo portion in score V1 as achieved by the player. + // This is a pessimistic estimate; it intentionally does not operate on object count and uses only score instead. + double maximumOccurrencesOfLongestCombo = Math.Floor(comboPortionInScoreV1 / comboPortionFromLongestComboInScoreV1); + double comboPortionFromRepeatedLongestCombosInScoreV1 = maximumOccurrencesOfLongestCombo * comboPortionFromLongestComboInScoreV1; + + double remainingComboPortionInScoreV1 = comboPortionInScoreV1 - comboPortionFromRepeatedLongestCombosInScoreV1; + // `remainingComboPortionInScoreV1` is in the "score ballpark" realm, which means it's proportional to combo squared. + // To convert that back to a raw combo length, we need to take the square root... + double remainingCombo = Math.Sqrt(remainingComboPortionInScoreV1); + // ...and then based on that raw combo length, we calculate how much this last combo is worth in standardised score. + double remainingComboPortionInStandardisedScore = Math.Pow(remainingCombo, 1 + ScoreProcessor.COMBO_EXPONENT); + + double lowerEstimateOfComboPortionInStandardisedScore + = maximumOccurrencesOfLongestCombo * comboPortionFromLongestComboInStandardisedScore + + remainingComboPortionInStandardisedScore; + + // Compute approximate upper estimate new score for that play. + // This time, divide the remaining combo among remaining objects equally to achieve longest possible combo lengths. + // There is no rigorous proof that doing this will yield a correct upper bound, but it seems to work out in practice. + remainingComboPortionInScoreV1 = comboPortionInScoreV1 - comboPortionFromLongestComboInScoreV1; + double remainingCountOfObjectsGivingCombo = maximumLegacyCombo - score.MaxCombo - score.Statistics.GetValueOrDefault(HitResult.Miss); + // Because we assumed all combos were equal, `remainingComboPortionInScoreV1` + // can be approximated by n * x^2, wherein n is the assumed number of equal combos, + // and x is the assumed length of every one of those combos. + // The remaining count of objects giving combo is, using those terms, equal to n * x. + // Therefore, dividing the two will result in x, i.e. the assumed length of the remaining combos. + double lengthOfRemainingCombos = remainingCountOfObjectsGivingCombo > 0 + ? remainingComboPortionInScoreV1 / remainingCountOfObjectsGivingCombo + : 0; + // In standardised scoring, each combo yields a score proportional to combo length to the power 1 + COMBO_EXPONENT. + // Using the symbols introduced above, that would be x ^ 1.5 per combo, n times (because there are n assumed equal-length combos). + // However, because `remainingCountOfObjectsGivingCombo` - using the symbols introduced above - is assumed to be equal to n * x, + // we can skip adding the 1 and just multiply by x ^ 0.5. + remainingComboPortionInStandardisedScore = remainingCountOfObjectsGivingCombo * Math.Pow(lengthOfRemainingCombos, ScoreProcessor.COMBO_EXPONENT); + + double upperEstimateOfComboPortionInStandardisedScore = comboPortionFromLongestComboInStandardisedScore + remainingComboPortionInStandardisedScore; + + // Approximate by combining lower and upper estimates. + // As the lower-estimate is very pessimistic, we use a 30/70 ratio + // and cap it with 1.2 times the middle-point to avoid overestimates. + double estimatedComboPortionInStandardisedScore = Math.Min( + 0.3 * lowerEstimateOfComboPortionInStandardisedScore + 0.7 * upperEstimateOfComboPortionInStandardisedScore, + 1.2 * (lowerEstimateOfComboPortionInStandardisedScore + upperEstimateOfComboPortionInStandardisedScore) / 2 + ); + + double newComboScoreProportion = estimatedComboPortionInStandardisedScore / maximumAchievableComboPortionInStandardisedScore; + + convertedTotalScore = (long)Math.Round(( + 500000 * newComboScoreProportion * score.Accuracy + + 500000 * Math.Pow(score.Accuracy, 5) + bonusProportion) * modMultiplier); + break; case 1: - return (long)Math.Round(( + convertedTotalScore = (long)Math.Round(( 250000 * comboProportion + 750000 * Math.Pow(score.Accuracy, 3.6) + bonusProportion) * modMultiplier); + break; case 2: - return (long)Math.Round(( - 600000 * comboProportion - + 400000 * score.Accuracy + // compare logic in `CatchScoreProcessor`. + + // this could technically be slightly incorrect in the case of stable scores. + // because large droplet misses are counted as full misses in stable scores, + // `score.MaximumStatistics.GetValueOrDefault(Great)` will be equal to the count of fruits *and* large droplets + // rather than just fruits (which was the intent). + // this is not fixable without introducing an extra legacy score attribute dedicated for catch, + // and this is a ballpark conversion process anyway, so attempt to trudge on. + int fruitTinyScaleDivisor = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) + score.MaximumStatistics.GetValueOrDefault(HitResult.Great); + double fruitTinyScale = fruitTinyScaleDivisor == 0 + ? 0 + : (double)score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) / fruitTinyScaleDivisor; + + const int max_tiny_droplets_portion = 400000; + + double comboPortion = 1000000 - max_tiny_droplets_portion + max_tiny_droplets_portion * (1 - fruitTinyScale); + double dropletsPortion = max_tiny_droplets_portion * fruitTinyScale; + double dropletsHit = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) == 0 + ? 0 + : (double)score.Statistics.GetValueOrDefault(HitResult.SmallTickHit) / score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit); + + convertedTotalScore = (long)Math.Round(( + comboPortion * estimateComboProportionForCatch(attributes.MaxCombo, score.MaxCombo, score.Statistics.GetValueOrDefault(HitResult.Miss)) + + dropletsPortion * dropletsHit + bonusProportion) * modMultiplier); + break; case 3: - return (long)Math.Round(( - 990000 * comboProportion - + 10000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy) + convertedTotalScore = (long)Math.Round(( + 850000 * comboProportion + + 150000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy) + bonusProportion) * modMultiplier); + break; default: - return score.TotalScore; + convertedTotalScore = score.TotalScore; + break; } + + if (convertedTotalScore < 0) + throw new InvalidOperationException($"Total score conversion operation returned invalid total of {convertedTotalScore}"); + + return convertedTotalScore; + } + + /// + /// + /// For catch, the general method of calculating the combo proportion used for other rulesets is generally useless. + /// This is because in stable score V1, catch has quadratic score progression, + /// while in stable score V2, score progression is logarithmic up to 200 combo and then linear. + /// + /// + /// This means that applying the naive rescale method to scores with lots of short combos (think 10x 100-long combos on a 1000-object map) + /// by linearly rescaling the combo portion as given by score V1 leads to horribly underestimating it. + /// Therefore this method attempts to counteract this by calculating the best case estimate for the combo proportion that takes all of the above into account. + /// + /// + /// The general idea is that aside from the which the player is known to have hit, + /// the remaining misses are evenly distributed across the rest of the objects that give combo. + /// This is therefore a worst-case estimate. + /// + /// + private static double estimateComboProportionForCatch(int beatmapMaxCombo, int scoreMaxCombo, int scoreMissCount) + { + if (beatmapMaxCombo == 0) + return 1; + + if (scoreMaxCombo == 0) + return 0; + + if (beatmapMaxCombo == scoreMaxCombo) + return 1; + + double estimatedBestCaseTotal = estimateBestCaseComboTotal(beatmapMaxCombo); + + int remainingCombo = beatmapMaxCombo - (scoreMaxCombo + scoreMissCount); + double totalDroppedScore = 0; + + int assumedLengthOfRemainingCombos = (int)Math.Floor((double)remainingCombo / scoreMissCount); + + if (assumedLengthOfRemainingCombos > 0) + { + int assumedCombosCount = (int)Math.Floor((double)remainingCombo / assumedLengthOfRemainingCombos); + totalDroppedScore += assumedCombosCount * estimateDroppedComboScoreAfterMiss(assumedLengthOfRemainingCombos); + + remainingCombo -= assumedCombosCount * assumedLengthOfRemainingCombos; + + if (remainingCombo > 0) + totalDroppedScore += estimateDroppedComboScoreAfterMiss(remainingCombo); + } + else + { + // there are so many misses that attempting to evenly divide remaining combo results in 0 length per combo, + // i.e. all remaining judgements are combo breaks. + // in that case, presume every single remaining object is a miss and did not give any combo score. + totalDroppedScore = estimatedBestCaseTotal - estimateBestCaseComboTotal(scoreMaxCombo); + } + + return estimatedBestCaseTotal == 0 + ? 1 + : 1 - Math.Clamp(totalDroppedScore / estimatedBestCaseTotal, 0, 1); + + double estimateBestCaseComboTotal(int maxCombo) + { + if (maxCombo == 0) + return 1; + + double estimatedTotal = 0.5 * Math.Min(maxCombo, 2); + + if (maxCombo <= 2) + return estimatedTotal; + + // int_2^x log_4(t) dt + estimatedTotal += (Math.Min(maxCombo, 200) * (Math.Log(Math.Min(maxCombo, 200)) - 1) + 2 - Math.Log(4)) / Math.Log(4); + + if (maxCombo <= 200) + return estimatedTotal; + + estimatedTotal += (maxCombo - 200) * Math.Log(200) / Math.Log(4); + return estimatedTotal; + } + + double estimateDroppedComboScoreAfterMiss(int lengthOfComboAfterMiss) + { + if (lengthOfComboAfterMiss >= 200) + lengthOfComboAfterMiss = 200; + + // int_0^x (log_4(200) - log_4(t)) dt + // note that this is an pessimistic estimate, i.e. it may subtract too much if the miss happened before reaching 200 combo + return lengthOfComboAfterMiss * (1 + Math.Log(200) - Math.Log(lengthOfComboAfterMiss)) / Math.Log(4); + } + } + + private static double computeAccuracy(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor) + { + int baseScore = scoreInfo.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()) + .Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key)); + int maxBaseScore = scoreInfo.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()) + .Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key)); + + return maxBaseScore == 0 ? 1 : baseScore / (double)maxBaseScore; + } + + public static ScoreRank ComputeRank(ScoreInfo scoreInfo) => computeRank(scoreInfo, scoreInfo.Ruleset.CreateInstance().CreateScoreProcessor()); + + private static ScoreRank computeRank(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor) + { + var rank = scoreProcessor.RankFromScore(scoreInfo.Accuracy, scoreInfo.Statistics); + + foreach (var mod in scoreInfo.Mods.OfType()) + rank = mod.AdjustRank(rank, scoreInfo.Accuracy); + + return rank; } /// 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/Extensions/TimeDisplayExtensions.cs b/osu.Game/Extensions/TimeDisplayExtensions.cs index 98633958ee..1b224cfeb7 100644 --- a/osu.Game/Extensions/TimeDisplayExtensions.cs +++ b/osu.Game/Extensions/TimeDisplayExtensions.cs @@ -59,7 +59,8 @@ namespace osu.Game.Extensions /// A short relative string representing the input time. public static string ToShortRelativeTime(this DateTimeOffset time, TimeSpan lowerCutoff) { - if (time == default) + // covers all `DateTimeOffset` instances with the date portion of 0001-01-01. + if (time.Date == default) return "-"; var now = DateTime.Now; diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs index 9c0d109ce4..784c8e4b44 100644 --- a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -10,6 +12,7 @@ using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Rulesets.Mods; +using osu.Game.Screens; using osu.Game.Storyboards.Drawables; namespace osu.Game.Graphics.Backgrounds @@ -18,6 +21,10 @@ namespace osu.Game.Graphics.Backgrounds { private readonly InterpolatingFramedClock storyboardClock; + private AudioContainer storyboardContainer = null!; + private DrawableStoryboard? drawableStoryboard; + private CancellationTokenSource? loadCancellationSource = new CancellationTokenSource(); + [Resolved(CanBeNull = true)] private MusicController? musicController { get; set; } @@ -33,18 +40,59 @@ namespace osu.Game.Graphics.Backgrounds [BackgroundDependencyLoader] private void load() { - if (!Beatmap.Storyboard.HasDrawable) - return; - - if (Beatmap.Storyboard.ReplacesBackground) - Sprite.Alpha = 0; - - LoadComponentAsync(new AudioContainer + AddInternal(storyboardContainer = new AudioContainer { RelativeSizeAxes = Axes.Both, Volume = { Value = 0 }, - Child = new DrawableStoryboard(Beatmap.Storyboard, mods.Value) { Clock = storyboardClock } - }, AddInternal); + }); + + LoadStoryboard(false); + } + + public void LoadStoryboard(bool async = true) + { + Debug.Assert(drawableStoryboard == null); + + if (!Beatmap.Storyboard.HasDrawable) + return; + + drawableStoryboard = new DrawableStoryboard(Beatmap.Storyboard, mods.Value) + { + Clock = storyboardClock + }; + + if (async) + LoadComponentAsync(drawableStoryboard, finishLoad, (loadCancellationSource = new CancellationTokenSource()).Token); + else + { + LoadComponent(drawableStoryboard); + finishLoad(drawableStoryboard); + } + + void finishLoad(DrawableStoryboard s) + { + if (Beatmap.Storyboard.ReplacesBackground) + Sprite.FadeOut(BackgroundScreen.TRANSITION_LENGTH, Easing.InQuint); + + storyboardContainer.FadeInFromZero(BackgroundScreen.TRANSITION_LENGTH, Easing.OutQuint); + storyboardContainer.Add(s); + } + } + + public void UnloadStoryboard() + { + if (drawableStoryboard == null) + return; + + loadCancellationSource?.Cancel(); + loadCancellationSource = null; + + // clear is intentionally used here for the storyboard to be disposed asynchronously. + storyboardContainer.Clear(); + + drawableStoryboard = null; + + Sprite.Alpha = 1f; } protected override void LoadComplete() diff --git a/osu.Game/Graphics/Backgrounds/SkinBackground.cs b/osu.Game/Graphics/Backgrounds/SkinBackground.cs index 17af5e6991..23806ae1d4 100644 --- a/osu.Game/Graphics/Backgrounds/SkinBackground.cs +++ b/osu.Game/Graphics/Backgrounds/SkinBackground.cs @@ -22,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/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 0ee42c69d5..e877915fac 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Allocation; using System.Collections.Generic; using osu.Framework.Graphics.Rendering; -using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Lists; using osu.Framework.Bindables; @@ -78,10 +77,10 @@ namespace osu.Game.Graphics.Backgrounds } /// - /// If enabled, only the portion of triangles that falls within this 's - /// shape is drawn to the screen. + /// Controls on which the portion of triangles that falls within this 's + /// shape is drawn to the screen. Default is Axes.Both. /// - public bool Masking { get; set; } + public Axes ClampAxes { get; set; } = Axes.Both; /// /// Whether we should drop-off alpha values of triangles more quickly to improve @@ -258,13 +257,12 @@ namespace osu.Game.Graphics.Backgrounds private IShader shader; private Texture texture; - private bool masking; + private Axes clampAxes; private readonly List parts = new List(); private readonly Vector2 triangleSize = new Vector2(1f, equilateral_triangle_ratio) * triangle_size; private Vector2 size; - private IVertexBatch vertexBatch; public TrianglesDrawNode(Triangles source) : base(source) @@ -278,7 +276,7 @@ namespace osu.Game.Graphics.Backgrounds shader = Source.shader; texture = Source.texture; size = Source.DrawSize; - masking = Source.Masking; + clampAxes = Source.ClampAxes; parts.Clear(); parts.AddRange(Source.parts); @@ -286,16 +284,10 @@ namespace osu.Game.Graphics.Backgrounds private IUniformBuffer borderDataBuffer; - public override void Draw(IRenderer renderer) + protected override void Draw(IRenderer renderer) { base.Draw(renderer); - if (Source.AimCount > 0 && (vertexBatch == null || vertexBatch.Size != Source.AimCount)) - { - vertexBatch?.Dispose(); - vertexBatch = renderer.CreateQuadBatch(Source.AimCount, 1); - } - borderDataBuffer ??= renderer.CreateUniformBuffer(); borderDataBuffer.Data = borderDataBuffer.Data with { @@ -314,7 +306,7 @@ namespace osu.Game.Graphics.Backgrounds Vector2 topLeft = particle.Position - new Vector2(relativeSize.X * 0.5f, 0f); - Quad triangleQuad = masking ? clampToDrawable(topLeft, relativeSize) : new Quad(topLeft.X, topLeft.Y, relativeSize.X, relativeSize.Y); + Quad triangleQuad = getClampedQuad(clampAxes, topLeft, relativeSize); var drawQuad = new Quad( Vector2Extensions.Transform(triangleQuad.TopLeft * size, DrawInfo.Matrix), @@ -333,30 +325,35 @@ namespace osu.Game.Graphics.Backgrounds triangleQuad.Height ) / relativeSize; - renderer.DrawQuad(texture, drawQuad, colourInfo, new RectangleF(0, 0, 1, 1), vertexBatch.AddAction, textureCoords: textureCoords); + renderer.DrawQuad(texture, drawQuad, colourInfo, new RectangleF(0, 0, 1, 1), textureCoords: textureCoords); } shader.Unbind(); } - private static Quad clampToDrawable(Vector2 topLeft, Vector2 size) + private static Quad getClampedQuad(Axes clampAxes, Vector2 topLeft, Vector2 size) { - float leftClamped = Math.Clamp(topLeft.X, 0f, 1f); - float topClamped = Math.Clamp(topLeft.Y, 0f, 1f); + Vector2 clampedTopLeft = topLeft; - return new Quad( - leftClamped, - topClamped, - Math.Clamp(topLeft.X + size.X, 0f, 1f) - leftClamped, - Math.Clamp(topLeft.Y + size.Y, 0f, 1f) - topClamped - ); + if (clampAxes == Axes.X || clampAxes == Axes.Both) + { + clampedTopLeft.X = Math.Clamp(topLeft.X, 0f, 1f); + size.X = Math.Clamp(topLeft.X + size.X, 0f, 1f) - clampedTopLeft.X; + } + + if (clampAxes == Axes.Y || clampAxes == Axes.Both) + { + clampedTopLeft.Y = Math.Clamp(topLeft.Y, 0f, 1f); + size.Y = Math.Clamp(topLeft.Y + size.Y, 0f, 1f) - clampedTopLeft.Y; + } + + return new Quad(clampedTopLeft.X, clampedTopLeft.Y, size.X, size.Y); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - vertexBatch?.Dispose(); borderDataBuffer?.Dispose(); } } diff --git a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs index 750e96440d..706b05f5ad 100644 --- a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs +++ b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Allocation; using System.Collections.Generic; using osu.Framework.Graphics.Rendering; -using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -34,10 +33,10 @@ namespace osu.Game.Graphics.Backgrounds protected virtual bool CreateNewTriangles => true; /// - /// If enabled, only the portion of triangles that falls within this 's - /// shape is drawn to the screen. + /// Controls on which the portion of triangles that falls within this 's + /// shape is drawn to the screen. Default is Axes.Both. /// - public bool Masking { get; set; } + public Axes ClampAxes { get; set; } = Axes.Both; private readonly BindableFloat spawnRatio = new BindableFloat(1f); @@ -194,9 +193,7 @@ namespace osu.Game.Graphics.Backgrounds private Vector2 size; private float thickness; private float texelSize; - private bool masking; - - private IVertexBatch? vertexBatch; + private Axes clampAxes; public TrianglesDrawNode(TrianglesV2 source) : base(source) @@ -211,7 +208,7 @@ namespace osu.Game.Graphics.Backgrounds texture = Source.texture; size = Source.DrawSize; thickness = Source.Thickness; - masking = Source.Masking; + clampAxes = Source.ClampAxes; Quad triangleQuad = new Quad( Vector2Extensions.Transform(Vector2.Zero, DrawInfo.Matrix), @@ -228,19 +225,13 @@ namespace osu.Game.Graphics.Backgrounds private IUniformBuffer? borderDataBuffer; - public override void Draw(IRenderer renderer) + protected override void Draw(IRenderer renderer) { base.Draw(renderer); if (Source.AimCount == 0 || thickness == 0) return; - if (vertexBatch == null || vertexBatch.Size != Source.AimCount) - { - vertexBatch?.Dispose(); - vertexBatch = renderer.CreateQuadBatch(Source.AimCount, 1); - } - borderDataBuffer ??= renderer.CreateUniformBuffer(); borderDataBuffer.Data = borderDataBuffer.Data with { @@ -257,7 +248,7 @@ namespace osu.Game.Graphics.Backgrounds { Vector2 topLeft = particle.Position - new Vector2(relativeSize.X * 0.5f, 0f); - Quad triangleQuad = masking ? clampToDrawable(topLeft, relativeSize) : new Quad(topLeft.X, topLeft.Y, relativeSize.X, relativeSize.Y); + Quad triangleQuad = getClampedQuad(clampAxes, topLeft, relativeSize); var drawQuad = new Quad( Vector2Extensions.Transform(triangleQuad.TopLeft * size, DrawInfo.Matrix), @@ -273,30 +264,35 @@ namespace osu.Game.Graphics.Backgrounds triangleQuad.Height ) / relativeSize; - renderer.DrawQuad(texture, drawQuad, DrawColourInfo.Colour.Interpolate(triangleQuad), new RectangleF(0, 0, 1, 1), vertexBatch.AddAction, textureCoords: textureCoords); + renderer.DrawQuad(texture, drawQuad, DrawColourInfo.Colour.Interpolate(triangleQuad), new RectangleF(0, 0, 1, 1), textureCoords: textureCoords); } shader.Unbind(); } - private static Quad clampToDrawable(Vector2 topLeft, Vector2 size) + private static Quad getClampedQuad(Axes clampAxes, Vector2 topLeft, Vector2 size) { - float leftClamped = Math.Clamp(topLeft.X, 0f, 1f); - float topClamped = Math.Clamp(topLeft.Y, 0f, 1f); + Vector2 clampedTopLeft = topLeft; - return new Quad( - leftClamped, - topClamped, - Math.Clamp(topLeft.X + size.X, 0f, 1f) - leftClamped, - Math.Clamp(topLeft.Y + size.Y, 0f, 1f) - topClamped - ); + if (clampAxes == Axes.X || clampAxes == Axes.Both) + { + clampedTopLeft.X = Math.Clamp(topLeft.X, 0f, 1f); + size.X = Math.Clamp(topLeft.X + size.X, 0f, 1f) - clampedTopLeft.X; + } + + if (clampAxes == Axes.Y || clampAxes == Axes.Both) + { + clampedTopLeft.Y = Math.Clamp(topLeft.Y, 0f, 1f); + size.Y = Math.Clamp(topLeft.Y + size.Y, 0f, 1f) - clampedTopLeft.Y; + } + + return new Quad(clampedTopLeft.X, clampedTopLeft.Y, size.X, size.Y); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - vertexBatch?.Dispose(); borderDataBuffer?.Dispose(); } } diff --git a/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs b/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs index 7722374c69..63ac84fcf7 100644 --- a/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs +++ b/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs @@ -4,7 +4,6 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osuTK; namespace osu.Game.Graphics.Containers @@ -17,21 +16,9 @@ namespace osu.Game.Graphics.Containers public Drawable Icon { get => InternalChild; - set => InternalChild = value; } - /// - /// Determines an edge effect of this . - /// Edge effects are e.g. glow or a shadow. - /// Only has an effect when is true. - /// - public new EdgeEffectParameters EdgeEffect - { - get => base.EdgeEffect; - set => base.EdgeEffect = value; - } - protected override void Update() { base.Update(); @@ -49,10 +36,5 @@ namespace osu.Game.Graphics.Containers InternalChild.Origin = Anchor.Centre; } } - - public ConstrainedIconContainer() - { - Masking = true; - } } } diff --git a/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs b/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs deleted file mode 100644 index 5abb4096ac..0000000000 --- a/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs +++ /dev/null @@ -1,21 +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.Graphics.Containers -{ - /// - /// An with a long hover expansion delay. - /// - /// - /// Mostly used for buttons with explanatory labels, in which the label would display after a "long hover". - /// - public partial class ExpandingButtonContainer : ExpandingContainer - { - protected ExpandingButtonContainer(float contractedWidth, float expandedWidth) - : base(contractedWidth, expandedWidth) - { - } - - protected override double HoverExpansionDelay => 400; - } -} diff --git a/osu.Game/Graphics/Containers/ExpandingContainer.cs b/osu.Game/Graphics/Containers/ExpandingContainer.cs index 60b9e6a167..2abdb508ae 100644 --- a/osu.Game/Graphics/Containers/ExpandingContainer.cs +++ b/osu.Game/Graphics/Containers/ExpandingContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy 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; @@ -26,6 +24,8 @@ namespace osu.Game.Graphics.Containers /// protected virtual double HoverExpansionDelay => 0; + protected virtual bool ExpandOnHover => true; + protected override Container Content => FillFlow; protected FillFlowContainer FillFlow { get; } @@ -53,7 +53,7 @@ namespace osu.Game.Graphics.Containers }; } - private ScheduledDelegate hoverExpandEvent; + private ScheduledDelegate? hoverExpandEvent; protected override void LoadComplete() { @@ -93,6 +93,9 @@ namespace osu.Game.Graphics.Containers private void updateHoverExpansion() { + if (!ExpandOnHover) + return; + hoverExpandEvent?.Cancel(); if (IsHovered && !Expanded.Value) diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index 2d27ce906b..aa72996fff 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -12,9 +12,11 @@ using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Online; using osu.Game.Users; +using osu.Game.Localisation; namespace osu.Game.Graphics.Containers { @@ -46,9 +48,16 @@ namespace osu.Game.Graphics.Containers foreach (var link in links) { + string displayText = text.Substring(link.Index, link.Length); + + if (previousLinkEnd > link.Index) + { + Logger.Log($@"Link ""{link.Url}"" with text ""{displayText}"" overlaps previous link, ignoring."); + continue; + } + AddText(text[previousLinkEnd..link.Index]); - string displayText = text.Substring(link.Index, link.Length); object linkArgument = link.Argument; string tooltip = displayText == link.Url ? null : link.Url; @@ -74,7 +83,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/Footnotes/OsuMarkdownFootnoteTooltip.cs b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteTooltip.cs index af64913212..b9725de5f4 100644 --- a/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteTooltip.cs +++ b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteTooltip.cs @@ -5,7 +5,6 @@ using Markdig.Extensions.Footnotes; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Containers.Markdown; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Game.Overlays; @@ -62,7 +61,7 @@ namespace osu.Game.Graphics.Containers.Markdown.Footnotes lastFootnote = Text = footnote; } - public override MarkdownTextFlowContainer CreateTextFlow() => new FootnoteMarkdownTextFlowContainer(); + public override OsuMarkdownTextFlowContainer CreateTextFlow() => new FootnoteMarkdownTextFlowContainer(); } private partial class FootnoteMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs index 5da785603a..b4031752db 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs @@ -63,7 +63,7 @@ namespace osu.Game.Graphics.Containers.Markdown Font = OsuFont.GetFont(Typeface.Inter, size: 14, weight: FontWeight.Regular), }; - public override MarkdownTextFlowContainer CreateTextFlow() => new OsuMarkdownTextFlowContainer(); + public override OsuMarkdownTextFlowContainer CreateTextFlow() => new OsuMarkdownTextFlowContainer(); protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new OsuMarkdownHeading(headingBlock); diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index f92cfc2306..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; diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index da6996c170..124becc35a 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -44,9 +44,6 @@ namespace osu.Game.Graphics.Containers private bool shouldPerformRightMouseScroll(MouseButtonEvent e) => RightMouseScrollbar && e.Button == MouseButton.Right; - private void scrollFromMouseEvent(MouseEvent e) => - ScrollTo(Clamp(ToLocalSpace(e.ScreenSpaceMousePosition)[ScrollDim] / DrawSize[ScrollDim]) * Content.DrawSize[ScrollDim], true, DistanceDecayOnRightMouseScrollbar); - private bool rightMouseDragging; protected override bool IsDragging => base.IsDragging || rightMouseDragging; @@ -80,7 +77,7 @@ namespace osu.Game.Graphics.Containers { if (shouldPerformRightMouseScroll(e)) { - scrollFromMouseEvent(e); + ScrollFromMouseEvent(e); return true; } @@ -91,7 +88,7 @@ namespace osu.Game.Graphics.Containers { if (rightMouseDragging) { - scrollFromMouseEvent(e); + ScrollFromMouseEvent(e); return; } @@ -129,6 +126,9 @@ namespace osu.Game.Graphics.Containers return base.OnScroll(e); } + protected virtual void ScrollFromMouseEvent(MouseEvent e) => + ScrollTo(Clamp(ToLocalSpace(e.ScreenSpaceMousePosition)[ScrollDim] / DrawSize[ScrollDim]) * Content.DrawSize[ScrollDim], true, DistanceDecayOnRightMouseScrollbar); + protected override ScrollbarContainer CreateScrollbar(Direction direction) => new OsuScrollbar(direction); protected partial class OsuScrollbar : ScrollbarContainer 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/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 6934c95385..354a57b7d2 100644 --- a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs +++ b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; +using osu.Framework.Input.Events; namespace osu.Game.Graphics.Containers { @@ -46,6 +47,12 @@ namespace osu.Game.Graphics.Containers base.ScrollIntoView(target, animated); } + protected override void ScrollFromMouseEvent(MouseEvent e) + { + UserScrolling = true; + base.ScrollFromMouseEvent(e); + } + public new void ScrollTo(float value, bool animated = true, double? distanceDecay = null) { UserScrolling = false; diff --git a/osu.Game/Graphics/Containers/WaveContainer.cs b/osu.Game/Graphics/Containers/WaveContainer.cs index 9fd3d103c9..2ae4dc5a76 100644 --- a/osu.Game/Graphics/Containers/WaveContainer.cs +++ b/osu.Game/Graphics/Containers/WaveContainer.cs @@ -2,6 +2,9 @@ // See the LICENCE file in the repository root for full licence text. 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; @@ -32,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; @@ -56,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; @@ -106,18 +122,23 @@ namespace osu.Game.Graphics.Containers protected override void PopIn() { - foreach (var w in wavesContainer.Children) + foreach (var w in wavesContainer) w.Show(); contentContainer.MoveToY(0, APPEAR_DURATION, Easing.OutQuint); + samplePopIn?.Play(); + wasShown = true; } protected override void PopOut() { - foreach (var w in wavesContainer.Children) + foreach (var w in wavesContainer) w.Hide(); contentContainer.MoveToY(2, DISAPPEAR_DURATION, Easing.In); + + if (wasShown) + samplePopOut?.Play(); } protected override void UpdateAfterChildren() @@ -157,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)); @@ -167,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/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 1b21f79c0a..1b5877b966 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -75,9 +75,13 @@ namespace osu.Game.Graphics { switch (result) { + case HitResult.IgnoreMiss: case HitResult.SmallTickMiss: - case HitResult.LargeTickMiss: + return Orange1; + case HitResult.Miss: + case HitResult.LargeTickMiss: + case HitResult.ComboBreak: return Red; case HitResult.Meh: @@ -91,6 +95,7 @@ namespace osu.Game.Graphics case HitResult.SmallTickHit: case HitResult.LargeTickHit: + case HitResult.SliderTailHit: case HitResult.Great: return Blue; @@ -161,7 +166,7 @@ namespace osu.Game.Graphics return Pink1; case ModType.System: - return Gray7; + return Yellow; default: throw new ArgumentOutOfRangeException(nameof(modType), modType, "Unknown mod type"); @@ -396,5 +401,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 1f4d6a62da..3cd10b1315 100644 --- a/osu.Game/Graphics/OsuIcon.cs +++ b/osu.Game/Graphics/OsuIcon.cs @@ -1,94 +1,444 @@ // 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.ComponentModel; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Extensions; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Text; namespace osu.Game.Graphics { public static class OsuIcon { - public static IconUsage Get(int icon) => new IconUsage((char)icon, "osuFont"); + #region Legacy spritesheet-based icons + + private static IconUsage get(int icon) => new IconUsage((char)icon, @"osuFont"); // ruleset icons in circles - public static IconUsage RulesetOsu => Get(0xe000); - public static IconUsage RulesetMania => Get(0xe001); - public static IconUsage RulesetCatch => Get(0xe002); - public static IconUsage RulesetTaiko => Get(0xe003); + public static IconUsage RulesetOsu => get(0xe000); + public static IconUsage RulesetMania => get(0xe001); + public static IconUsage RulesetCatch => get(0xe002); + public static IconUsage RulesetTaiko => get(0xe003); // ruleset icons without circles - public static IconUsage FilledCircle => Get(0xe004); - public static IconUsage CrossCircle => Get(0xe005); - public static IconUsage Logo => Get(0xe006); - public static IconUsage ChevronDownCircle => Get(0xe007); - public static IconUsage EditCircle => Get(0xe033); - public static IconUsage LeftCircle => Get(0xe034); - public static IconUsage RightCircle => Get(0xe035); - public static IconUsage Charts => Get(0xe036); - public static IconUsage Solo => Get(0xe037); - public static IconUsage Multi => Get(0xe038); - public static IconUsage Gear => Get(0xe039); + public static IconUsage FilledCircle => get(0xe004); + public static IconUsage Logo => get(0xe006); + public static IconUsage ChevronDownCircle => get(0xe007); + public static IconUsage EditCircle => get(0xe033); + public static IconUsage LeftCircle => get(0xe034); + public static IconUsage RightCircle => get(0xe035); + public static IconUsage Charts => get(0xe036); + public static IconUsage Solo => get(0xe037); + public static IconUsage Multi => get(0xe038); + public static IconUsage Gear => get(0xe039); // misc icons - public static IconUsage Bat => Get(0xe008); - public static IconUsage Bubble => Get(0xe009); - public static IconUsage BubblePop => Get(0xe02e); - public static IconUsage Dice => Get(0xe011); - public static IconUsage Heart => Get(0xe02f); - public static IconUsage HeartBreak => Get(0xe030); - public static IconUsage Hot => Get(0xe031); - public static IconUsage ListSearch => Get(0xe032); + public static IconUsage Bat => get(0xe008); + public static IconUsage Bubble => get(0xe009); + public static IconUsage BubblePop => get(0xe02e); + public static IconUsage Dice => get(0xe011); + public static IconUsage HeartBreak => get(0xe030); + public static IconUsage Hot => get(0xe031); + public static IconUsage ListSearch => get(0xe032); //osu! playstyles - public static IconUsage PlayStyleTablet => Get(0xe02a); - public static IconUsage PlayStyleMouse => Get(0xe029); - public static IconUsage PlayStyleKeyboard => Get(0xe02b); - public static IconUsage PlayStyleTouch => Get(0xe02c); + public static IconUsage PlayStyleTablet => get(0xe02a); + public static IconUsage PlayStyleMouse => get(0xe029); + public static IconUsage PlayStyleKeyboard => get(0xe02b); + public static IconUsage PlayStyleTouch => get(0xe02c); // osu! difficulties - public static IconUsage EasyOsu => Get(0xe015); - public static IconUsage NormalOsu => Get(0xe016); - public static IconUsage HardOsu => Get(0xe017); - public static IconUsage InsaneOsu => Get(0xe018); - public static IconUsage ExpertOsu => Get(0xe019); + public static IconUsage EasyOsu => get(0xe015); + public static IconUsage NormalOsu => get(0xe016); + public static IconUsage HardOsu => get(0xe017); + public static IconUsage InsaneOsu => get(0xe018); + public static IconUsage ExpertOsu => get(0xe019); // taiko difficulties - public static IconUsage EasyTaiko => Get(0xe01a); - public static IconUsage NormalTaiko => Get(0xe01b); - public static IconUsage HardTaiko => Get(0xe01c); - public static IconUsage InsaneTaiko => Get(0xe01d); - public static IconUsage ExpertTaiko => Get(0xe01e); + public static IconUsage EasyTaiko => get(0xe01a); + public static IconUsage NormalTaiko => get(0xe01b); + public static IconUsage HardTaiko => get(0xe01c); + public static IconUsage InsaneTaiko => get(0xe01d); + public static IconUsage ExpertTaiko => get(0xe01e); // fruits difficulties - public static IconUsage EasyFruits => Get(0xe01f); - public static IconUsage NormalFruits => Get(0xe020); - public static IconUsage HardFruits => Get(0xe021); - public static IconUsage InsaneFruits => Get(0xe022); - public static IconUsage ExpertFruits => Get(0xe023); + public static IconUsage EasyFruits => get(0xe01f); + public static IconUsage NormalFruits => get(0xe020); + public static IconUsage HardFruits => get(0xe021); + public static IconUsage InsaneFruits => get(0xe022); + public static IconUsage ExpertFruits => get(0xe023); // mania difficulties - public static IconUsage EasyMania => Get(0xe024); - public static IconUsage NormalMania => Get(0xe025); - public static IconUsage HardMania => Get(0xe026); - public static IconUsage InsaneMania => Get(0xe027); - public static IconUsage ExpertMania => Get(0xe028); + public static IconUsage EasyMania => get(0xe024); + public static IconUsage NormalMania => get(0xe025); + public static IconUsage HardMania => get(0xe026); + public static IconUsage InsaneMania => get(0xe027); + public static IconUsage ExpertMania => get(0xe028); // mod icons - public static IconUsage ModPerfect => Get(0xe049); - public static IconUsage ModAutopilot => Get(0xe03a); - public static IconUsage ModAuto => Get(0xe03b); - public static IconUsage ModCinema => Get(0xe03c); - public static IconUsage ModDoubleTime => Get(0xe03d); - public static IconUsage ModEasy => Get(0xe03e); - public static IconUsage ModFlashlight => Get(0xe03f); - public static IconUsage ModHalftime => Get(0xe040); - public static IconUsage ModHardRock => Get(0xe041); - public static IconUsage ModHidden => Get(0xe042); - public static IconUsage ModNightcore => Get(0xe043); - public static IconUsage ModNoFail => Get(0xe044); - public static IconUsage ModRelax => Get(0xe045); - public static IconUsage ModSpunOut => Get(0xe046); - public static IconUsage ModSuddenDeath => Get(0xe047); - public static IconUsage ModTarget => Get(0xe048); - public static IconUsage ModBg => Get(0xe04a); + public static IconUsage ModPerfect => get(0xe049); + public static IconUsage ModAutopilot => get(0xe03a); + public static IconUsage ModAuto => get(0xe03b); + public static IconUsage ModCinema => get(0xe03c); + public static IconUsage ModDoubleTime => get(0xe03d); + public static IconUsage ModEasy => get(0xe03e); + public static IconUsage ModFlashlight => get(0xe03f); + public static IconUsage ModHalftime => get(0xe040); + public static IconUsage ModHardRock => get(0xe041); + public static IconUsage ModHidden => get(0xe042); + public static IconUsage ModNightcore => get(0xe043); + public static IconUsage ModNoFail => get(0xe044); + public static IconUsage ModRelax => get(0xe045); + public static IconUsage ModSpunOut => get(0xe046); + public static IconUsage ModSuddenDeath => get(0xe047); + public static IconUsage ModTarget => get(0xe048); + + // Use "Icons/BeatmapDetails/mod-icon" instead + // public static IconUsage ModBg => Get(0xe04a); + + #endregion + + #region New single-file-based icons + + public const string FONT_NAME = @"Icons"; + + public static IconUsage Audio => get(OsuIconMapping.Audio); + public static IconUsage Beatmap => get(OsuIconMapping.Beatmap); + public static IconUsage Calendar => get(OsuIconMapping.Calendar); + public static IconUsage ChangelogA => get(OsuIconMapping.ChangelogA); + public static IconUsage ChangelogB => get(OsuIconMapping.ChangelogB); + public static IconUsage Chat => get(OsuIconMapping.Chat); + public static IconUsage CheckCircle => get(OsuIconMapping.CheckCircle); + public static IconUsage CollapseA => get(OsuIconMapping.CollapseA); + public static IconUsage Collections => get(OsuIconMapping.Collections); + public static IconUsage Cross => get(OsuIconMapping.Cross); + public static IconUsage CrossCircle => get(OsuIconMapping.CrossCircle); + public static IconUsage Crown => get(OsuIconMapping.Crown); + public static IconUsage Debug => get(OsuIconMapping.Debug); + public static IconUsage Delete => get(OsuIconMapping.Delete); + public static IconUsage Details => get(OsuIconMapping.Details); + public static IconUsage Discord => get(OsuIconMapping.Discord); + public static IconUsage EllipsisHorizontal => get(OsuIconMapping.EllipsisHorizontal); + public static IconUsage EllipsisVertical => get(OsuIconMapping.EllipsisVertical); + public static IconUsage ExpandA => get(OsuIconMapping.ExpandA); + public static IconUsage ExpandB => get(OsuIconMapping.ExpandB); + public static IconUsage FeaturedArtist => get(OsuIconMapping.FeaturedArtist); + public static IconUsage FeaturedArtistCircle => get(OsuIconMapping.FeaturedArtistCircle); + public static IconUsage GameplayA => get(OsuIconMapping.GameplayA); + public static IconUsage GameplayB => get(OsuIconMapping.GameplayB); + public static IconUsage GameplayC => get(OsuIconMapping.GameplayC); + public static IconUsage Global => get(OsuIconMapping.Global); + public static IconUsage Graphics => get(OsuIconMapping.Graphics); + public static IconUsage Heart => get(OsuIconMapping.Heart); + public static IconUsage Home => get(OsuIconMapping.Home); + public static IconUsage Input => get(OsuIconMapping.Input); + public static IconUsage Maintenance => get(OsuIconMapping.Maintenance); + public static IconUsage Megaphone => get(OsuIconMapping.Megaphone); + public static IconUsage Music => get(OsuIconMapping.Music); + public static IconUsage News => get(OsuIconMapping.News); + public static IconUsage Next => get(OsuIconMapping.Next); + public static IconUsage NextCircle => get(OsuIconMapping.NextCircle); + public static IconUsage Notification => get(OsuIconMapping.Notification); + public static IconUsage Online => get(OsuIconMapping.Online); + public static IconUsage Play => get(OsuIconMapping.Play); + public static IconUsage Player => get(OsuIconMapping.Player); + public static IconUsage PlayerFollow => get(OsuIconMapping.PlayerFollow); + public static IconUsage Prev => get(OsuIconMapping.Prev); + public static IconUsage PrevCircle => get(OsuIconMapping.PrevCircle); + public static IconUsage Ranking => get(OsuIconMapping.Ranking); + public static IconUsage Rulesets => get(OsuIconMapping.Rulesets); + public static IconUsage Search => get(OsuIconMapping.Search); + public static IconUsage Settings => get(OsuIconMapping.Settings); + public static IconUsage SkinA => get(OsuIconMapping.SkinA); + public static IconUsage SkinB => get(OsuIconMapping.SkinB); + public static IconUsage Star => get(OsuIconMapping.Star); + public static IconUsage Storyboard => get(OsuIconMapping.Storyboard); + public static IconUsage Team => get(OsuIconMapping.Team); + public static IconUsage ThumbsUp => get(OsuIconMapping.ThumbsUp); + public static IconUsage Tournament => get(OsuIconMapping.Tournament); + public static IconUsage Twitter => get(OsuIconMapping.Twitter); + public static IconUsage UserInterface => get(OsuIconMapping.UserInterface); + public static IconUsage Wiki => get(OsuIconMapping.Wiki); + public static IconUsage EditorAddControlPoint => get(OsuIconMapping.EditorAddControlPoint); + public static IconUsage EditorConvertToStream => get(OsuIconMapping.EditorConvertToStream); + public static IconUsage EditorDistanceSnap => get(OsuIconMapping.EditorDistanceSnap); + public static IconUsage EditorFinish => get(OsuIconMapping.EditorFinish); + public static IconUsage EditorGridSnap => get(OsuIconMapping.EditorGridSnap); + public static IconUsage EditorNewComboA => get(OsuIconMapping.EditorNewComboA); + public static IconUsage EditorNewComboB => get(OsuIconMapping.EditorNewComboB); + public static IconUsage EditorSelect => get(OsuIconMapping.EditorSelect); + public static IconUsage EditorSound => get(OsuIconMapping.EditorSound); + public static IconUsage EditorWhistle => get(OsuIconMapping.EditorWhistle); + + private static IconUsage get(OsuIconMapping glyph) => new IconUsage((char)glyph, FONT_NAME); + + private enum OsuIconMapping + { + [Description(@"audio")] + Audio, + + [Description(@"beatmap")] + Beatmap, + + [Description(@"calendar")] + Calendar, + + [Description(@"changelog-a")] + ChangelogA, + + [Description(@"changelog-b")] + ChangelogB, + + [Description(@"chat")] + Chat, + + [Description(@"check-circle")] + CheckCircle, + + [Description(@"collapse-a")] + CollapseA, + + [Description(@"collections")] + Collections, + + [Description(@"cross")] + Cross, + + [Description(@"cross-circle")] + CrossCircle, + + [Description(@"crown")] + Crown, + + [Description(@"debug")] + Debug, + + [Description(@"delete")] + Delete, + + [Description(@"details")] + Details, + + [Description(@"discord")] + Discord, + + [Description(@"ellipsis-horizontal")] + EllipsisHorizontal, + + [Description(@"ellipsis-vertical")] + EllipsisVertical, + + [Description(@"expand-a")] + ExpandA, + + [Description(@"expand-b")] + ExpandB, + + [Description(@"featured-artist")] + FeaturedArtist, + + [Description(@"featured-artist-circle")] + FeaturedArtistCircle, + + [Description(@"gameplay-a")] + GameplayA, + + [Description(@"gameplay-b")] + GameplayB, + + [Description(@"gameplay-c")] + GameplayC, + + [Description(@"global")] + Global, + + [Description(@"graphics")] + Graphics, + + [Description(@"heart")] + Heart, + + [Description(@"home")] + Home, + + [Description(@"input")] + Input, + + [Description(@"maintenance")] + Maintenance, + + [Description(@"megaphone")] + Megaphone, + + [Description(@"music")] + Music, + + [Description(@"news")] + News, + + [Description(@"next")] + Next, + + [Description(@"next-circle")] + NextCircle, + + [Description(@"notification")] + Notification, + + [Description(@"online")] + Online, + + [Description(@"play")] + Play, + + [Description(@"player")] + Player, + + [Description(@"player-follow")] + PlayerFollow, + + [Description(@"prev")] + Prev, + + [Description(@"prev-circle")] + PrevCircle, + + [Description(@"ranking")] + Ranking, + + [Description(@"rulesets")] + Rulesets, + + [Description(@"search")] + Search, + + [Description(@"settings")] + Settings, + + [Description(@"skin-a")] + SkinA, + + [Description(@"skin-b")] + SkinB, + + [Description(@"star")] + Star, + + [Description(@"storyboard")] + Storyboard, + + [Description(@"team")] + Team, + + [Description(@"thumbs-up")] + ThumbsUp, + + [Description(@"tournament")] + Tournament, + + [Description(@"twitter")] + Twitter, + + [Description(@"user-interface")] + UserInterface, + + [Description(@"wiki")] + Wiki, + + [Description(@"Editor/add-control-point")] + EditorAddControlPoint = 1000, + + [Description(@"Editor/convert-to-stream")] + EditorConvertToStream, + + [Description(@"Editor/distance-snap")] + EditorDistanceSnap, + + [Description(@"Editor/finish")] + EditorFinish, + + [Description(@"Editor/grid-snap")] + EditorGridSnap, + + [Description(@"Editor/new-combo-a")] + EditorNewComboA, + + [Description(@"Editor/new-combo-b")] + EditorNewComboB, + + [Description(@"Editor/select")] + EditorSelect, + + [Description(@"Editor/sound")] + EditorSound, + + [Description(@"Editor/whistle")] + EditorWhistle, + } + + public class OsuIconStore : ITextureStore, ITexturedGlyphLookupStore + { + private readonly TextureStore textures; + + public OsuIconStore(TextureStore textures) + { + this.textures = textures; + } + + public ITexturedCharacterGlyph? Get(string? fontName, char character) + { + if (fontName == FONT_NAME) + return new Glyph(textures.Get($@"{fontName}/{((OsuIconMapping)character).GetDescription()}")); + + return null; + } + + public Task GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character)); + + public Texture? Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => null; + + public Texture Get(string name) => throw new NotImplementedException(); + + public Task GetAsync(string name, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + public Stream GetStream(string name) => throw new NotImplementedException(); + + public IEnumerable GetAvailableResources() => throw new NotImplementedException(); + + public Task GetAsync(string name, WrapMode wrapModeS, WrapMode wrapModeT, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + public class Glyph : ITexturedCharacterGlyph + { + public float XOffset => default; + public float YOffset => default; + public float XAdvance => default; + public float Baseline => default; + public char Character => default; + + public float GetKerning(T lastGlyph) where T : ICharacterGlyph => throw new NotImplementedException(); + + public Texture Texture { get; } + public float Width => Texture.Width; + public float Height => Texture.Height; + + public Glyph(Texture texture) + { + Texture = texture; + } + } + + public void Dispose() + { + textures.Dispose(); + } + } + + #endregion } } diff --git a/osu.Game/Graphics/ParticleSpewer.cs b/osu.Game/Graphics/ParticleSpewer.cs index 37a4fe77bd..64c70095bf 100644 --- a/osu.Game/Graphics/ParticleSpewer.cs +++ b/osu.Game/Graphics/ParticleSpewer.cs @@ -49,6 +49,18 @@ namespace osu.Game.Graphics 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(); @@ -56,12 +68,8 @@ namespace osu.Game.Graphics Invalidate(Invalidation.DrawNode); if (!Active.Value || !CanSpawnParticles) - { - lastParticleAdded = null; return; - } - // Always want to spawn the first particle in an activation immediately. if (lastParticleAdded == null) { lastParticleAdded = Time.Current; diff --git a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs index 1355bfc272..669c5da01e 100644 --- a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs +++ b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs @@ -5,95 +5,90 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Framework.Utils; using osuTK; namespace osu.Game.Graphics.Sprites { - public partial class GlowingSpriteText : Container, IHasText + public partial class GlowingSpriteText : BufferedContainer, IHasText { - private readonly OsuSpriteText spriteText, blurredText; + private const float blur_sigma = 3f; + + // Inflate draw quad to prevent glow from trimming at the edges. + // Padding won't suffice since it will affect text position in cases when it's not centered. + protected override Quad ComputeScreenSpaceDrawQuad() => base.ComputeScreenSpaceDrawQuad().AABBFloat.Inflate(Blur.KernelSize(blur_sigma)); + + private readonly OsuSpriteText text; public LocalisableString Text { - get => spriteText.Text; - set => blurredText.Text = spriteText.Text = value; + get => text.Text; + set => text.Text = value; } public FontUsage Font { - get => spriteText.Font; - set => blurredText.Font = spriteText.Font = value.With(fixedWidth: true); + get => text.Font; + set => text.Font = value.With(fixedWidth: true); } public Vector2 TextSize { - get => spriteText.Size; - set => blurredText.Size = spriteText.Size = value; + get => text.Size; + set => text.Size = value; } public ColourInfo TextColour { - get => spriteText.Colour; - set => spriteText.Colour = value; + get => text.Colour; + set => text.Colour = value; } public ColourInfo GlowColour { - get => blurredText.Colour; - set => blurredText.Colour = value; + get => EffectColour; + set + { + EffectColour = value; + BackgroundColour = value.MultiplyAlpha(0f); + } } public Vector2 Spacing { - get => spriteText.Spacing; - set => spriteText.Spacing = blurredText.Spacing = value; + get => text.Spacing; + set => text.Spacing = value; } public bool UseFullGlyphHeight { - get => spriteText.UseFullGlyphHeight; - set => spriteText.UseFullGlyphHeight = blurredText.UseFullGlyphHeight = value; + get => text.UseFullGlyphHeight; + set => text.UseFullGlyphHeight = value; } public Bindable Current { - get => spriteText.Current; - set => spriteText.Current = value; + get => text.Current; + set => text.Current = value; } public GlowingSpriteText() + : base(cachedFrameBuffer: true) { AutoSizeAxes = Axes.Both; - - Children = new Drawable[] + BlurSigma = new Vector2(blur_sigma); + RedrawOnScale = false; + DrawOriginal = true; + EffectBlending = BlendingParameters.Additive; + EffectPlacement = EffectPlacement.InFront; + Child = text = new OsuSpriteText { - new BufferedContainer(cachedFrameBuffer: true) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - BlurSigma = new Vector2(4), - RedrawOnScale = false, - RelativeSizeAxes = Axes.Both, - Blending = BlendingParameters.Additive, - Size = new Vector2(3f), - Children = new[] - { - blurredText = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Shadow = false, - }, - }, - }, - spriteText = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Shadow = false, - }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shadow = false, }; } } diff --git a/osu.Game/Graphics/UserInterface/BarGraph.cs b/osu.Game/Graphics/UserInterface/BarGraph.cs index 27a41eb7e3..e7592128b0 100644 --- a/osu.Game/Graphics/UserInterface/BarGraph.cs +++ b/osu.Game/Graphics/UserInterface/BarGraph.cs @@ -134,7 +134,7 @@ namespace osu.Game.Graphics.UserInterface lengths.AddRange(Source.bars.InstantaneousLengths); } - public override void Draw(IRenderer renderer) + protected override void Draw(IRenderer renderer) { base.Draw(renderer); @@ -145,6 +145,13 @@ namespace osu.Game.Graphics.UserInterface float barHeight = drawSize.Y * ((direction == BarDirection.TopToBottom || direction == BarDirection.BottomToTop) ? lengths[i] : barBreadth); float barWidth = drawSize.X * ((direction == BarDirection.LeftToRight || direction == BarDirection.RightToLeft) ? lengths[i] : barBreadth); + if (barHeight == 0 || barWidth == 0) + continue; + + // Apply minimum sizing to hide the fact that we don't have fractional anti-aliasing. + barHeight = Math.Max(barHeight, 1.5f); + barWidth = Math.Max(barWidth, 1.5f); + Vector2 topLeft; switch (direction) diff --git a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs index fc0770d896..af4b3849af 100644 --- a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs +++ b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs @@ -33,7 +33,7 @@ namespace osu.Game.Graphics.UserInterface Current.ValueChanged += index => { - foreach (var t in TabContainer.Children.OfType()) + foreach (var t in TabContainer.OfType()) { int tIndex = TabContainer.IndexOf(t); int tabIndex = TabContainer.IndexOf(TabMap[index.NewValue]); diff --git a/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs b/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs index b237dd9b71..39ef7924b9 100644 --- a/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs +++ b/osu.Game/Graphics/UserInterface/DangerousRoundedButton.cs @@ -11,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 db81bc991d..c39f41bf72 100644 --- a/osu.Game/Graphics/UserInterface/DialogButton.cs +++ b/osu.Game/Graphics/UserInterface/DialogButton.cs @@ -25,7 +25,7 @@ namespace osu.Game.Graphics.UserInterface private const float idle_width = 0.8f; private const float hover_width = 0.9f; - private const float hover_duration = 500; + private const float hover_duration = 300; private const float click_duration = 200; public event Action? StateChanged; @@ -54,7 +54,7 @@ namespace osu.Game.Graphics.UserInterface private readonly Box rightGlow; private readonly Box background; private readonly SpriteText spriteText; - private Vector2 hoverSpacing => new Vector2(3f, 0f); + private Vector2 hoverSpacing => new Vector2(1.4f, 0f); public DialogButton(HoverSampleSet sampleSet = HoverSampleSet.Button) : base(sampleSet) @@ -150,6 +150,7 @@ namespace osu.Game.Graphics.UserInterface TriangleScale = 4, ColourDark = OsuColour.Gray(0.88f), Shear = new Vector2(-0.2f, 0), + ClampAxes = Axes.Y }, }, }, @@ -279,15 +280,15 @@ namespace osu.Game.Graphics.UserInterface if (newState == SelectionState.Selected) { - spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutElastic); - ColourContainer.ResizeWidthTo(hover_width, hover_duration, Easing.OutElastic); + spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutQuint); + ColourContainer.ResizeWidthTo(hover_width, hover_duration, Easing.OutQuint); glowContainer.FadeIn(hover_duration, Easing.OutQuint); } else { - ColourContainer.ResizeWidthTo(idle_width, hover_duration, Easing.OutElastic); - spriteText.TransformSpacingTo(Vector2.Zero, hover_duration, Easing.OutElastic); - glowContainer.FadeOut(hover_duration, Easing.OutQuint); + ColourContainer.ResizeWidthTo(idle_width, hover_duration / 2, Easing.OutQuint); + spriteText.TransformSpacingTo(Vector2.Zero, hover_duration / 2, Easing.OutQuint); + glowContainer.FadeOut(hover_duration / 2, Easing.OutQuint); } } 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/FPSCounter.cs b/osu.Game/Graphics/UserInterface/FPSCounter.cs index c1ef573848..000b85b900 100644 --- a/osu.Game/Graphics/UserInterface/FPSCounter.cs +++ b/osu.Game/Graphics/UserInterface/FPSCounter.cs @@ -213,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/OsuAnimatedButton.cs b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs index de4df96942..0eec04541c 100644 --- a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs @@ -46,8 +46,8 @@ namespace osu.Game.Graphics.UserInterface 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 { diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index b530172f3e..2dc701dc9d 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -22,7 +22,7 @@ using osuTK.Graphics; namespace osu.Game.Graphics.UserInterface { - public partial class OsuDropdown : Dropdown + public partial class OsuDropdown : Dropdown, IKeyBindingHandler { private const float corner_radius = 5; @@ -30,9 +30,23 @@ namespace osu.Game.Graphics.UserInterface protected override DropdownMenu CreateMenu() => new OsuDropdownMenu(); + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Repeat) return false; + + if (e.Action == GlobalAction.Back) + return Back(); + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + #region OsuDropdownMenu - protected partial class OsuDropdownMenu : DropdownMenu, IKeyBindingHandler + protected partial class OsuDropdownMenu : DropdownMenu { public override bool HandleNonPositionalInput => State == MenuState.Open; @@ -276,23 +290,6 @@ namespace osu.Game.Graphics.UserInterface } #endregion - - public bool OnPressed(KeyBindingPressEvent e) - { - if (e.Repeat) return false; - - if (e.Action == GlobalAction.Back) - { - State = MenuState.Closed; - return true; - } - - return false; - } - - public void OnReleased(KeyBindingReleaseEvent e) - { - } } #endregion @@ -355,11 +352,81 @@ namespace osu.Game.Graphics.UserInterface AddInternal(new HoverClickSounds()); } - [BackgroundDependencyLoader(true)] - private void load(OverlayColourProvider? colourProvider, OsuColour colours) + [Resolved] + private OverlayColourProvider? colourProvider { get; set; } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + protected override void LoadComplete() { - BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f); - BackgroundColourHover = colourProvider?.Light4 ?? colours.PinkDarker; + base.LoadComplete(); + + SearchBar.State.ValueChanged += _ => updateColour(); + Enabled.BindValueChanged(_ => updateColour()); + updateColour(); + } + + protected override bool OnHover(HoverEvent e) + { + updateColour(); + return false; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateColour(); + } + + private void updateColour() + { + bool hovered = Enabled.Value && IsHovered; + var hoveredColour = colourProvider?.Light4 ?? colours.PinkDarker; + var unhoveredColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f); + + Colour = Color4.White; + Alpha = Enabled.Value ? 1 : 0.3f; + + if (SearchBar.State.Value == Visibility.Visible) + { + Icon.Colour = hovered ? hoveredColour.Lighten(0.5f) : Colour4.White; + Background.Colour = unhoveredColour; + } + else + { + Icon.Colour = Color4.White; + Background.Colour = hovered ? hoveredColour : unhoveredColour; + } + } + + protected override DropdownSearchBar CreateSearchBar() => new OsuDropdownSearchBar + { + Padding = new MarginPadding { Right = 26 }, + }; + + private partial class OsuDropdownSearchBar : DropdownSearchBar + { + protected override void PopIn() => this.FadeIn(); + + protected override void PopOut() => this.FadeOut(); + + protected override TextBox CreateTextBox() => new DropdownSearchTextBox + { + FontSize = OsuFont.Default.Size, + }; + + private partial class DropdownSearchTextBox : SearchTextBox + { + public override bool OnPressed(KeyBindingPressEvent e) + { + if (e.Action == GlobalAction.Back) + // this method is blocking Dropdown from receiving the back action, despite this text box residing in a separate input manager. + // to fix this properly, a local global action container needs to be added as well, but for simplicity, just don't handle the back action here. + return false; + + return base.OnPressed(e); + } + } } } } diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index 73d57af793..e2aac297e3 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -6,13 +6,15 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; -using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osuTK; +using osuTK.Graphics; namespace osu.Game.Graphics.UserInterface { @@ -78,6 +80,9 @@ namespace osu.Game.Graphics.UserInterface { case StatefulMenuItem stateful: return new DrawableStatefulMenuItem(stateful); + + case OsuMenuItemSpacer spacer: + return new DrawableSpacer(spacer); } return new DrawableOsuMenuItem(item); @@ -89,5 +94,28 @@ namespace osu.Game.Graphics.UserInterface { Anchor = Direction == Direction.Horizontal ? Anchor.BottomLeft : Anchor.TopRight }; + + protected partial class DrawableSpacer : DrawableOsuMenuItem + { + public DrawableSpacer(MenuItem item) + : base(item) + { + 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; + + protected override bool OnClick(ClickEvent e) => true; + } } } diff --git a/osu.Game/Graphics/UserInterface/OsuMenuItemSpacer.cs b/osu.Game/Graphics/UserInterface/OsuMenuItemSpacer.cs new file mode 100644 index 0000000000..8a3a928c60 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/OsuMenuItemSpacer.cs @@ -0,0 +1,13 @@ +// 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.Graphics.UserInterface +{ + public class OsuMenuItemSpacer : OsuMenuItem + { + public OsuMenuItemSpacer() + : base(" ") + { + } + } +} diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs index 123854a2dd..0be7b4dc48 100644 --- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs @@ -23,7 +23,7 @@ namespace osu.Game.Graphics.UserInterface protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer { AutoSizeAxes = Axes.Both, - Child = new PasswordMaskChar(CalculatedTextSize), + Child = new PasswordMaskChar(FontSize), }; protected override bool AllowUniqueCharacterSamples => false; 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/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index 05309760e7..c260c92b43 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -37,7 +37,7 @@ namespace osu.Game.Graphics.UserInterface if (Dropdown is IHasAccentColour dropdown) dropdown.AccentColour = value; - foreach (var i in TabContainer.Children.OfType()) + foreach (var i in TabContainer.OfType()) i.AccentColour = value; } } @@ -48,7 +48,7 @@ namespace osu.Game.Graphics.UserInterface protected override TabItem CreateTabItem(T value) => new OsuTabItem(value); - protected virtual float StripWidth => TabContainer.Children.Sum(c => c.IsPresent ? c.DrawWidth + TabContainer.Spacing.X : 0) - TabContainer.Spacing.X; + protected virtual float StripWidth => TabContainer.Sum(c => c.IsPresent ? c.DrawWidth + TabContainer.Spacing.X : 0) - TabContainer.Spacing.X; /// /// Whether entries should be automatically populated if is an type. diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 99803e2956..08d38837f6 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}"); @@ -267,7 +268,7 @@ namespace osu.Game.Graphics.UserInterface protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer { AutoSizeAxes = Axes.Both, - Child = new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) }, + Child = new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: FontSize) }, }; protected override Caret CreateCaret() => caret = new OsuCaret @@ -313,18 +314,16 @@ namespace osu.Game.Graphics.UserInterface public OsuCaret() { - RelativeSizeAxes = Axes.Y; - Size = new Vector2(1, 0.9f); - Colour = Color4.Transparent; - Anchor = Anchor.CentreLeft; - Origin = Anchor.CentreLeft; - Masking = true; - CornerRadius = 1; InternalChild = beatSync = new CaretBeatSyncedContainer { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Masking = true, + CornerRadius = 1f, RelativeSizeAxes = Axes.Both, + Height = 0.9f, }; } diff --git a/osu.Game/Graphics/UserInterface/PercentageCounter.cs b/osu.Game/Graphics/UserInterface/PercentageCounter.cs index ed06211e8f..1f9103b3bd 100644 --- a/osu.Game/Graphics/UserInterface/PercentageCounter.cs +++ b/osu.Game/Graphics/UserInterface/PercentageCounter.cs @@ -14,7 +14,7 @@ namespace osu.Game.Graphics.UserInterface /// public partial class PercentageCounter : RollingCounter { - protected override double RollingDuration => 750; + protected override double RollingDuration => 375; private float epsilon => 1e-10f; diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs index b80c0e3b58..e69727e047 100644 --- a/osu.Game/Graphics/UserInterface/RollingCounter.cs +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -45,7 +45,7 @@ namespace osu.Game.Graphics.UserInterface /// /// Easing for the counter rollover animation. /// - protected virtual Easing RollingEasing => Easing.OutQuint; + protected virtual Easing RollingEasing => Easing.OutQuad; private T displayedCount; diff --git a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs index 56af23ff10..0981881ead 100644 --- a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs @@ -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/SegmentedGraph.cs b/osu.Game/Graphics/UserInterface/SegmentedGraph.cs index 91971e5af9..9f467687a4 100644 --- a/osu.Game/Graphics/UserInterface/SegmentedGraph.cs +++ b/osu.Game/Graphics/UserInterface/SegmentedGraph.cs @@ -221,7 +221,7 @@ namespace osu.Game.Graphics.UserInterface tierColours.AddRange(Source.tierColours); } - public override void Draw(IRenderer renderer) + protected override void Draw(IRenderer renderer) { base.Draw(renderer); 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 3940bf8bca..c3a9f8a586 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs @@ -48,6 +48,8 @@ namespace osu.Game.Graphics.UserInterface public void KillFocus() => textBox.KillFocus(); + public bool SelectAll() => textBox.SelectAll(); + public ShearedSearchTextBox() { Height = 42; @@ -110,7 +112,7 @@ namespace osu.Game.Graphics.UserInterface BackgroundFocused = colourProvider.Background4; BackgroundUnfocused = colourProvider.Background4; - Placeholder.Font = OsuFont.GetFont(size: CalculatedTextSize, weight: FontWeight.SemiBold); + Placeholder.Font = OsuFont.GetFont(size: FontSize, weight: FontWeight.SemiBold); PlaceholderText = CommonStrings.InputSearch; CornerRadius = corner_radius; 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/StarCounter.cs b/osu.Game/Graphics/UserInterface/StarCounter.cs index 82f6f8e0a4..720f479216 100644 --- a/osu.Game/Graphics/UserInterface/StarCounter.cs +++ b/osu.Game/Graphics/UserInterface/StarCounter.cs @@ -30,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; /// @@ -64,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()) } @@ -97,7 +101,7 @@ namespace osu.Game.Graphics.UserInterface public void StopAnimation() { animate(current); - foreach (var star in stars.Children) + foreach (var star in stars) star.FinishTransforms(true); } 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/OsuPopover.cs b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs index 381193d539..9b4689958c 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. 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; @@ -21,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(); @@ -38,9 +48,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 } [BackgroundDependencyLoader(true)] - private void load(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(); @@ -49,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) diff --git a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs new file mode 100644 index 0000000000..e5ba7f61bf --- /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, CultureInfo.CurrentCulture); + 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 84% rename from osu.Game/IO/Archives/LegacyByteArrayReader.cs rename to osu.Game/IO/Archives/ByteArrayArchiveReader.cs index 06db02b335..0e2dee3456 100644 --- a/osu.Game/IO/Archives/LegacyByteArrayReader.cs +++ b/osu.Game/IO/Archives/ByteArrayArchiveReader.cs @@ -9,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 75% rename from osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs rename to osu.Game/IO/Archives/DirectoryArchiveReader.cs index dfae58aed7..f2012b7b49 100644 --- a/osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs +++ b/osu.Game/IO/Archives/DirectoryArchiveReader.cs @@ -8,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 83% rename from osu.Game/IO/Archives/LegacyFileArchiveReader.cs rename to osu.Game/IO/Archives/SingleFileArchiveReader.cs index c317cae5fc..79d9c5de71 100644 --- a/osu.Game/IO/Archives/LegacyFileArchiveReader.cs +++ b/osu.Game/IO/Archives/SingleFileArchiveReader.cs @@ -7,14 +7,14 @@ 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/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/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 01c454e3f9..436334cfe1 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,34 +141,41 @@ 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), + new KeyBinding(InputKey.Plus, GlobalAction.IncreaseOffset), + new KeyBinding(InputKey.Minus, GlobalAction.DecreaseOffset), }; - 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(InputKey.Comma, GlobalAction.StepReplayBackward), + new KeyBinding(InputKey.Period, GlobalAction.StepReplayForward), 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), @@ -141,7 +184,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), @@ -158,21 +201,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 @@ -204,7 +232,6 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleMute))] ToggleMute, - // In-Game Keybindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.SkipCutscene))] SkipCutscene, @@ -232,7 +259,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, @@ -260,7 +286,6 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.PauseGameplay))] PauseGameplay, - // Editor [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSetupMode))] EditorSetupMode, @@ -285,7 +310,6 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleInGameInterface))] ToggleInGameInterface, - // Song select keybindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleModSelection))] ToggleModSelection, @@ -378,5 +402,34 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleReplaySettings))] ToggleReplaySettings, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleInGameLeaderboard))] + ToggleInGameLeaderboard, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))] + EditorToggleRotateControl, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseOffset))] + IncreaseOffset, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseOffset))] + DecreaseOffset, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.StepReplayForward))] + StepReplayForward, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.StepReplayBackward))] + StepReplayBackward, + } + + 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/ConfineMouseTracker.cs b/osu.Game/Input/ConfineMouseTracker.cs index de8660dbce..926f68df45 100644 --- a/osu.Game/Input/ConfineMouseTracker.cs +++ b/osu.Game/Input/ConfineMouseTracker.cs @@ -22,6 +22,7 @@ namespace osu.Game.Input { private Bindable frameworkConfineMode; private Bindable frameworkWindowMode; + private Bindable frameworkMinimiseOnFocusLossInFullscreen; private Bindable osuConfineMode; private IBindable localUserPlaying; @@ -31,7 +32,9 @@ namespace osu.Game.Input { frameworkConfineMode = frameworkConfigManager.GetBindable(FrameworkSetting.ConfineMouseMode); frameworkWindowMode = frameworkConfigManager.GetBindable(FrameworkSetting.WindowMode); + frameworkMinimiseOnFocusLossInFullscreen = frameworkConfigManager.GetBindable(FrameworkSetting.MinimiseOnFocusLossInFullscreen); frameworkWindowMode.BindValueChanged(_ => updateConfineMode()); + frameworkMinimiseOnFocusLossInFullscreen.BindValueChanged(_ => updateConfineMode()); osuConfineMode = osuConfigManager.GetBindable(OsuSetting.ConfineMouseMode); localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy(); @@ -46,7 +49,8 @@ namespace osu.Game.Input if (frameworkConfineMode.Disabled) return; - if (frameworkWindowMode.Value == WindowMode.Fullscreen) + // override confine mode only when clicking outside the window minimises it. + if (frameworkWindowMode.Value == WindowMode.Fullscreen && frameworkMinimiseOnFocusLossInFullscreen.Value) { frameworkConfineMode.Value = ConfineMouseMode.Fullscreen; return; 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/Input/TouchInputInterceptor.cs b/osu.Game/Input/TouchInputInterceptor.cs new file mode 100644 index 0000000000..368d8469ae --- /dev/null +++ b/osu.Game/Input/TouchInputInterceptor.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.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.TypeExtensions; +using osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osu.Framework.Input.StateChanges; +using osu.Framework.Logging; +using osu.Game.Configuration; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Input +{ + /// + /// Intercepts all positional input events and sets the appropriate value + /// for consumption by particular game screens. + /// + public partial class TouchInputInterceptor : Component + { + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + + private readonly BindableBool touchInputActive = new BindableBool(); + + [BackgroundDependencyLoader] + private void load(SessionStatics statics) + { + statics.BindWith(Static.TouchInputActive, touchInputActive); + } + + protected override bool Handle(UIEvent e) + { + bool touchInputWasActive = touchInputActive.Value; + + switch (e) + { + case MouseEvent: + if (e.CurrentState.Mouse.LastSource is not ISourcedFromTouch) + { + if (touchInputWasActive) + Logger.Log($@"Touch input deactivated due to received {e.GetType().ReadableName()}", LoggingTarget.Input); + touchInputActive.Value = false; + } + + break; + + case TouchEvent: + if (!touchInputWasActive) + Logger.Log($@"Touch input activated due to received {e.GetType().ReadableName()}", LoggingTarget.Input); + touchInputActive.Value = true; + break; + + case KeyDownEvent keyDown: + if (keyDown.Key == Key.T && keyDown.ControlPressed && keyDown.ShiftPressed) + debugToggleTouchInputActive(); + break; + } + + return false; + } + + [Conditional("TOUCH_INPUT_DEBUG")] + private void debugToggleTouchInputActive() + { + Logger.Log($@"Debug-toggling touch input to {(touchInputActive.Value ? @"inactive" : @"active")}", LoggingTarget.Information, LogLevel.Important); + touchInputActive.Toggle(); + } + } +} diff --git a/osu.Game/Localisation/BeatmapOffsetControlStrings.cs b/osu.Game/Localisation/BeatmapOffsetControlStrings.cs index 632a1ad0ea..b905b7ae1c 100644 --- a/osu.Game/Localisation/BeatmapOffsetControlStrings.cs +++ b/osu.Game/Localisation/BeatmapOffsetControlStrings.cs @@ -10,9 +10,9 @@ namespace osu.Game.Localisation private const string prefix = @"osu.Game.Resources.Localisation.BeatmapOffsetControl"; /// - /// "Beatmap offset" + /// "Audio offset (this beatmap)" /// - public static LocalisableString BeatmapOffset => new TranslatableString(getKey(@"beatmap_offset"), @"Beatmap offset"); + public static LocalisableString AudioOffsetThisBeatmap => new TranslatableString(getKey(@"beatmap_offset"), @"Audio offset (this beatmap)"); /// /// "Previous play:" diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 480d68619d..2c377a81d9 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -169,6 +169,11 @@ namespace osu.Game.Localisation /// 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..cb18a2159c 100644 --- a/osu.Game/Localisation/ContextMenuStrings.cs +++ b/osu.Game/Localisation/ContextMenuStrings.cs @@ -19,6 +19,16 @@ namespace osu.Game.Localisation /// public static LocalisableString ViewBeatmap => new TranslatableString(getKey(@"view_beatmap"), @"View beatmap"); + /// + /// "Invite to room" + /// + public static LocalisableString InvitePlayer => new TranslatableString(getKey(@"invite_player"), @"Invite to room"); + + /// + /// "Spectate" + /// + public static LocalisableString SpectatePlayer => new TranslatableString(getKey(@"spectate_player"), @"Spectate"); + 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/EditorSetupStrings.cs b/osu.Game/Localisation/EditorSetupStrings.cs index 401411365b..eff6f9e6b8 100644 --- a/osu.Game/Localisation/EditorSetupStrings.cs +++ b/osu.Game/Localisation/EditorSetupStrings.cs @@ -179,21 +179,11 @@ namespace osu.Game.Localisation /// public static LocalisableString ClickToSelectTrack => new TranslatableString(getKey(@"click_to_select_track"), @"Click to select a track"); - /// - /// "Click to replace the track" - /// - public static LocalisableString ClickToReplaceTrack => new TranslatableString(getKey(@"click_to_replace_track"), @"Click to replace the track"); - /// /// "Click to select a background image" /// public static LocalisableString ClickToSelectBackground => new TranslatableString(getKey(@"click_to_select_background"), @"Click to select a background image"); - /// - /// "Click to replace the background image" - /// - public static LocalisableString ClickToReplaceBackground => new TranslatableString(getKey(@"click_to_replace_background"), @"Click to replace the background image"); - /// /// "Ruleset ({0})" /// diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 93e52746c5..6ad12f54df 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -9,6 +9,11 @@ namespace osu.Game.Localisation { private const string prefix = @"osu.Game.Resources.Localisation.Editor"; + /// + /// "Beatmap editor" + /// + public static LocalisableString BeatmapEditor => new TranslatableString(getKey(@"beatmap_editor"), @"Beatmap editor"); + /// /// "Waveform opacity" /// @@ -119,6 +124,16 @@ namespace osu.Game.Localisation /// public static LocalisableString LimitedDistanceSnap => new TranslatableString(getKey(@"limited_distance_snap_grid"), @"Limit distance snap placement to current time"); + /// + /// "Must be in edit mode to handle editor links" + /// + public static LocalisableString MustBeInEditorToHandleLinks => new TranslatableString(getKey(@"must_be_in_editor_to_handle_links"), @"Must be in edit mode to handle editor links"); + + /// + /// "Failed to parse editor link" + /// + public static LocalisableString FailedToParseEditorLink => new TranslatableString(getKey(@"failed_to_parse_edtior_link"), @"Failed to parse editor link"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs index f52f6abb89..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" /// diff --git a/osu.Game/Localisation/GeneralSettingsStrings.cs b/osu.Game/Localisation/GeneralSettingsStrings.cs index a525af508b..42623f4632 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" /// @@ -54,6 +49,11 @@ namespace osu.Game.Localisation ///
public static LocalisableString OpenOsuFolder => new TranslatableString(getKey(@"open_osu_folder"), @"Open osu! folder"); + /// + /// "Export logs" + /// + public static LocalisableString ExportLogs => new TranslatableString(getKey(@"export_logs"), @"Export logs"); + /// /// "Change folder location..." /// diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index f93d86225c..703e0ff1ca 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" /// @@ -319,6 +324,16 @@ namespace osu.Game.Localisation ///
public static LocalisableString SeekReplayBackward => new TranslatableString(getKey(@"seek_replay_backward"), @"Seek replay backward"); + /// + /// "Seek replay forward one frame" + /// + public static LocalisableString StepReplayForward => new TranslatableString(getKey(@"step_replay_forward"), @"Seek replay forward one frame"); + + /// + /// "Step replay backward one frame" + /// + public static LocalisableString StepReplayBackward => new TranslatableString(getKey(@"step_replay_backward"), @"Step replay backward one frame"); + /// /// "Toggle chat focus" /// @@ -339,6 +354,21 @@ namespace osu.Game.Localisation ///
public static LocalisableString ExportReplay => new TranslatableString(getKey(@"export_replay"), @"Export replay"); + /// + /// "Increase offset" + /// + public static LocalisableString IncreaseOffset => new TranslatableString(getKey(@"increase_offset"), @"Increase offset"); + + /// + /// "Decrease offset" + /// + public static LocalisableString DecreaseOffset => new TranslatableString(getKey(@"decrease_offset"), @"Decrease offset"); + + /// + /// "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/GraphicsSettingsStrings.cs b/osu.Game/Localisation/GraphicsSettingsStrings.cs index 422704514f..1d14b0a596 100644 --- a/osu.Game/Localisation/GraphicsSettingsStrings.cs +++ b/osu.Game/Localisation/GraphicsSettingsStrings.cs @@ -152,9 +152,13 @@ namespace osu.Game.Localisation /// /// "In order to change the renderer, the game will close. Please open it again." /// - public static LocalisableString ChangeRendererConfirmation => - new TranslatableString(getKey(@"change_renderer_configuration"), @"In order to change the renderer, the game will close. Please open it again."); + public static LocalisableString ChangeRendererConfirmation => new TranslatableString(getKey(@"change_renderer_configuration"), @"In order to change the renderer, the game will close. Please open it again."); - private static string getKey(string key) => $"{prefix}:{key}"; + /// + /// "Minimise osu! when switching to another app" + /// + public static LocalisableString MinimiseOnFocusLoss => new TranslatableString(getKey(@"minimise_on_focus_loss"), @"Minimise osu! when switching to another app"); + + 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/ModSelectOverlayStrings.cs b/osu.Game/Localisation/ModSelectOverlayStrings.cs index 05dcf138d7..86ebebd293 100644 --- a/osu.Game/Localisation/ModSelectOverlayStrings.cs +++ b/osu.Game/Localisation/ModSelectOverlayStrings.cs @@ -44,6 +44,11 @@ namespace osu.Game.Localisation ///
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}"; } -} \ No newline at end of file +} diff --git a/osu.Game/Localisation/MouseSettingsStrings.cs b/osu.Game/Localisation/MouseSettingsStrings.cs index 1772f03b29..e61af07364 100644 --- a/osu.Game/Localisation/MouseSettingsStrings.cs +++ b/osu.Game/Localisation/MouseSettingsStrings.cs @@ -40,14 +40,14 @@ namespace osu.Game.Localisation public static LocalisableString DisableMouseWheelVolumeAdjust => new TranslatableString(getKey(@"disable_mouse_wheel_volume_adjust"), @"Disable mouse wheel adjusting volume during gameplay"); /// - /// "Volume can still be adjusted using the mouse wheel by holding "Alt"" + /// "Volume can still be adjusted using the mouse wheel by holding "Alt"" /// public static LocalisableString DisableMouseWheelVolumeAdjustTooltip => new TranslatableString(getKey(@"disable_mouse_wheel_volume_adjust_tooltip"), @"Volume can still be adjusted using the mouse wheel by holding ""Alt"""); /// - /// "Disable mouse buttons during gameplay" + /// "Disable clicks during gameplay" /// - public static LocalisableString DisableMouseButtons => new TranslatableString(getKey(@"disable_mouse_buttons"), @"Disable mouse buttons during gameplay"); + public static LocalisableString DisableClicksDuringGameplay => new TranslatableString(getKey(@"disable_clicks"), @"Disable clicks during gameplay"); /// /// "Enable high precision mouse to adjust sensitivity" diff --git a/osu.Game/Localisation/NamedOverlayComponentStrings.cs b/osu.Game/Localisation/NamedOverlayComponentStrings.cs index 475bea2a4a..72e63d699a 100644 --- a/osu.Game/Localisation/NamedOverlayComponentStrings.cs +++ b/osu.Game/Localisation/NamedOverlayComponentStrings.cs @@ -20,12 +20,12 @@ namespace osu.Game.Localisation public static LocalisableString ChangelogDescription => new TranslatableString(getKey(@"changelog_description"), @"track recent dev updates in the osu! ecosystem"); /// - /// "view your friends and other information" + /// "view your friends and spectate other players" /// - public static LocalisableString DashboardDescription => new TranslatableString(getKey(@"dashboard_description"), @"view your friends and other information"); + public static LocalisableString DashboardDescription => new TranslatableString(getKey(@"dashboard_description"), @"view your friends and spectate other players"); /// - /// "find out who's the best right now" + /// "find out who's the best right now" /// public static LocalisableString RankingsDescription => new TranslatableString(getKey(@"rankings_description"), @"find out who's the best right now"); @@ -39,6 +39,6 @@ namespace osu.Game.Localisation /// public static LocalisableString WikiDescription => new TranslatableString(getKey(@"wiki_description"), @"knowledge base"); - private static string getKey(string key) => $"{prefix}:{key}"; + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index 53687f2b28..fb3dab032d 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -93,6 +93,26 @@ Please try changing your audio device to a working setting."); ///
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/PlayerSettingsOverlayStrings.cs b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs new file mode 100644 index 0000000000..60874da561 --- /dev/null +++ b/osu.Game/Localisation/PlayerSettingsOverlayStrings.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 osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class PlayerSettingsOverlayStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.PlaybackSettings"; + + /// + /// "Step backward one frame" + /// + public static LocalisableString StepBackward => new TranslatableString(getKey(@"step_backward_frame"), @"Step backward one frame"); + + /// + /// "Step forward one frame" + /// + public static LocalisableString StepForward => new TranslatableString(getKey(@"step_forward_frame"), @"Step forward one frame"); + + /// + /// "Seek backward {0} seconds" + /// + public static LocalisableString SeekBackwardSeconds(double arg0) => new TranslatableString(getKey(@"seek_backward_seconds"), @"Seek backward {0} seconds", arg0); + + /// + /// "Seek forward {0} seconds" + /// + public static LocalisableString SeekForwardSeconds(double arg0) => new TranslatableString(getKey(@"seek_forward_seconds"), @"Seek forward {0} seconds", arg0); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs index 7c11ea6ac6..d5c8d5ccec 100644 --- a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs +++ b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs @@ -12,43 +12,53 @@ namespace osu.Game.Localisation.SkinComponents /// /// "Sprite name" /// - public static LocalisableString SpriteName => new TranslatableString(getKey(@"sprite_name"), "Sprite name"); + public static LocalisableString SpriteName => new TranslatableString(getKey(@"sprite_name"), @"Sprite name"); /// /// "The filename of the sprite" /// - public static LocalisableString SpriteNameDescription => new TranslatableString(getKey(@"sprite_name_description"), "The filename of the sprite"); + public static LocalisableString SpriteNameDescription => new TranslatableString(getKey(@"sprite_name_description"), @"The filename of the sprite"); /// /// "Font" /// - public static LocalisableString Font => new TranslatableString(getKey(@"font"), "Font"); + public static LocalisableString Font => new TranslatableString(getKey(@"font"), @"Font"); /// /// "The font to use." /// - public static LocalisableString FontDescription => new TranslatableString(getKey(@"font_description"), "The font to use."); + public static LocalisableString FontDescription => new TranslatableString(getKey(@"font_description"), @"The font to use."); /// /// "Text" /// - public static LocalisableString TextElementText => new TranslatableString(getKey(@"text_element_text"), "Text"); + public static LocalisableString TextElementText => new TranslatableString(getKey(@"text_element_text"), @"Text"); /// /// "The text to be displayed." /// - public static LocalisableString TextElementTextDescription => new TranslatableString(getKey(@"text_element_text_description"), "The text to be displayed."); + public static LocalisableString TextElementTextDescription => new TranslatableString(getKey(@"text_element_text_description"), @"The text to be displayed."); /// /// "Corner radius" /// - public static LocalisableString CornerRadius => new TranslatableString(getKey(@"corner_radius"), "Corner radius"); + public static LocalisableString CornerRadius => new TranslatableString(getKey(@"corner_radius"), @"Corner radius"); /// /// "How rounded the corners should be." /// - public static LocalisableString CornerRadiusDescription => new TranslatableString(getKey(@"corner_radius_description"), "How rounded the corners should be."); + public static LocalisableString CornerRadiusDescription => new TranslatableString(getKey(@"corner_radius_description"), @"How rounded the corners should be."); - private static string getKey(string key) => $"{prefix}:{key}"; + /// + /// "Show label" + /// + public static LocalisableString ShowLabel => new TranslatableString(getKey(@"show_label"), @"Show label"); + + /// + /// "Whether the component's label should be shown." + /// + public static LocalisableString ShowLabelDescription => new TranslatableString(getKey(@"show_label_description"), @"Whether the component's label should be shown."); + + private static string getKey(string key) => $@"{prefix}:{key}"; } } 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/TouchSettingsStrings.cs b/osu.Game/Localisation/TouchSettingsStrings.cs new file mode 100644 index 0000000000..785b333100 --- /dev/null +++ b/osu.Game/Localisation/TouchSettingsStrings.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; + +namespace osu.Game.Localisation +{ + public static class TouchSettingsStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.TouchSettings"; + + /// + /// "Touch" + /// + public static LocalisableString Touch => new TranslatableString(getKey(@"touch"), @"Touch"); + + /// + /// "Disable taps during gameplay" + /// + public static LocalisableString DisableTapsDuringGameplay => new TranslatableString(getKey(@"disable_taps_during_gameplay"), @"Disable taps during gameplay"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} \ No newline at end of file diff --git a/osu.Game/Localisation/UserInterfaceStrings.cs b/osu.Game/Localisation/UserInterfaceStrings.cs index ea664d7b50..dceedca05c 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" /// @@ -29,6 +24,11 @@ namespace osu.Game.Localisation ///
public static LocalisableString MenuCursorSize => new TranslatableString(getKey(@"menu_cursor_size"), @"Menu cursor size"); + /// + /// "Menu tips" + /// + public static LocalisableString ShowMenuTips => new TranslatableString(getKey(@"show_menu_tips"), @"Menu tips"); + /// /// "Parallax" /// @@ -109,6 +109,11 @@ namespace osu.Game.Localisation ///
public static LocalisableString ModSelectHotkeyStyle => new TranslatableString(getKey(@"mod_select_hotkey_style"), @"Mod select hotkey style"); + /// + /// "Automatically focus search text box in mod select" + /// + public static LocalisableString ModSelectTextSearchStartsActive => new TranslatableString(getKey(@"mod_select_text_search_starts_active"), @"Automatically focus search text box in mod select"); + /// /// "no limit" /// @@ -154,6 +159,6 @@ namespace osu.Game.Localisation ///
public static LocalisableString TrueRandom => new TranslatableString(getKey(@"true_random"), @"True Random"); - private static string getKey(string key) => $"{prefix}:{key}"; + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Models/RealmFile.cs b/osu.Game/Models/RealmFile.cs index 2faa3f0ca6..4d1642fb5f 100644 --- a/osu.Game/Models/RealmFile.cs +++ b/osu.Game/Models/RealmFile.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.Linq; using osu.Game.IO; using Realms; @@ -11,5 +12,8 @@ namespace osu.Game.Models { [PrimaryKey] public string Hash { get; set; } = string.Empty; + + [Backlink(nameof(RealmNamedFileUsage.File))] + public IQueryable Usages { get; } = null!; } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 4f586c8fff..17bf8bcc37 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -53,6 +53,7 @@ namespace osu.Game.Online.API public IBindable LocalUser => localUser; public IBindableList Friends => friends; public IBindable Activity => activity; + public IBindable Statistics => statistics; public Language Language => game.CurrentLanguage.Value; @@ -62,6 +63,11 @@ namespace osu.Game.Online.API private Bindable activity { get; } = new Bindable(); + private Bindable configStatus { get; } = new Bindable(); + private Bindable localUserStatus { get; } = new Bindable(); + + private Bindable statistics { get; } = new Bindable(); + protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); @@ -85,12 +91,20 @@ namespace osu.Game.Online.API authentication.TokenString = config.Get(OsuSetting.Token); authentication.Token.ValueChanged += onTokenChanged; + config.BindWith(OsuSetting.UserOnlineStatus, configStatus); + localUser.BindValueChanged(u => { u.OldValue?.Activity.UnbindFrom(activity); u.NewValue.Activity.BindTo(activity); + + if (u.OldValue != null) + localUserStatus.UnbindFrom(u.OldValue.Status); + localUserStatus.BindTo(u.NewValue.Status); }, true); + localUserStatus.BindValueChanged(val => configStatus.Value = val.NewValue); + var thread = new Thread(run) { Name = "APIAccess", @@ -200,6 +214,7 @@ namespace osu.Game.Online.API setLocalUser(new APIUser { Username = ProvidedUsername, + Status = { Value = configStatus.Value ?? UserStatus.Online } }); } @@ -246,8 +261,7 @@ namespace osu.Game.Online.API }; userReq.Success += user => { - // todo: save/pull from settings - user.Status.Value = new UserStatusOnline(); + user.Status.Value = configStatus.Value ?? UserStatus.Online; setLocalUser(user); @@ -506,9 +520,21 @@ namespace osu.Game.Online.API flushQueue(); } + public void UpdateStatistics(UserStatistics newStatistics) + { + statistics.Value = newStatistics; + + if (IsLoggedIn) + localUser.Value.Statistics = newStatistics; + } + private static APIUser createGuestUser() => new GuestUser(); - private void setLocalUser(APIUser user) => Scheduler.Add(() => localUser.Value = user, false); + private void setLocalUser(APIUser user) => Scheduler.Add(() => + { + localUser.Value = user; + statistics.Value = user.Statistics; + }, false); protected override void Dispose(bool isDisposing) { 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 01169828b0..4b4f8061e0 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -20,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, }); @@ -28,6 +28,8 @@ namespace osu.Game.Online.API public Bindable Activity { get; } = new Bindable(); + public Bindable Statistics { get; } = new Bindable(); + public Language Language => Language.en; public string AccessToken => "token"; @@ -112,7 +114,13 @@ namespace osu.Game.Online.API LocalUser.Value = new APIUser { Username = username, - Id = 1001, + Id = DUMMY_USER_ID, + }; + + Statistics.Value = new UserStatistics + { + GlobalRank = 1, + CountryRank = 1 }; state.Value = APIState.Online; @@ -126,6 +134,14 @@ namespace osu.Game.Online.API LocalUser.Value = new GuestUser(); } + public void UpdateStatistics(UserStatistics newStatistics) + { + Statistics.Value = newStatistics; + + if (IsLoggedIn) + LocalUser.Value.Statistics = newStatistics; + } + public IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null; public NotificationsClientConnector GetNotificationsConnector() => new PollingNotificationsClientConnector(this); @@ -141,6 +157,7 @@ namespace osu.Game.Online.API IBindable IAPIProvider.LocalUser => LocalUser; IBindableList IAPIProvider.Friends => Friends; IBindable IAPIProvider.Activity => Activity; + IBindable IAPIProvider.Statistics => Statistics; /// /// During the next simulated login, the process will fail immediately. diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index a1d7006c8c..b58d4a363a 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -28,6 +28,11 @@ namespace osu.Game.Online.API /// IBindable Activity { get; } + /// + /// The current user's online statistics. + /// + IBindable Statistics { get; } + /// /// The language supplied by this provider to API requests. /// @@ -111,6 +116,11 @@ namespace osu.Game.Online.API ///
void Logout(); + /// + /// Sets Statistics bindable. + /// + void UpdateStatistics(UserStatistics newStatistics); + /// /// Constructs a new . May be null if not supported. /// diff --git a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs index 8df2d3fc2d..3fad032531 100644 --- a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs +++ b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs @@ -35,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/Requests/DownloadReplayRequest.cs b/osu.Game/Online/API/Requests/DownloadReplayRequest.cs index 77174f0bb5..3ea57cf637 100644 --- a/osu.Game/Online/API/Requests/DownloadReplayRequest.cs +++ b/osu.Game/Online/API/Requests/DownloadReplayRequest.cs @@ -14,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/GetKudosuRankingsRequest.cs b/osu.Game/Online/API/Requests/GetKudosuRankingsRequest.cs new file mode 100644 index 0000000000..cd361bf7b8 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetKudosuRankingsRequest.cs @@ -0,0 +1,28 @@ +// 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.IO.Network; + +namespace osu.Game.Online.API.Requests +{ + public class GetKudosuRankingsRequest : APIRequest + { + private readonly int page; + + public GetKudosuRankingsRequest(int page = 1) + { + this.page = page; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + + req.AddParameter(@"page", page.ToString()); + + return req; + } + + protected override string Target => @"rankings/kudosu"; + } +} diff --git a/osu.Game/Online/API/Requests/GetKudosuRankingsResponse.cs b/osu.Game/Online/API/Requests/GetKudosuRankingsResponse.cs new file mode 100644 index 0000000000..4e3ade3795 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetKudosuRankingsResponse.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 System.Collections.Generic; +using Newtonsoft.Json; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class GetKudosuRankingsResponse + { + [JsonProperty("ranking")] + public List Users = null!; + } +} diff --git a/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs b/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs new file mode 100644 index 0000000000..659e46bb11 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetSystemTitleRequest.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 System; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class GetSystemTitleRequest : OsuJsonWebRequest + { + public GetSystemTitleRequest() + : base($@"https://assets.ppy.sh/lazer-status.json?{DateTimeOffset.UtcNow.ToUnixTimeSeconds() / 1800}") + { + } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index 902b651be9..e5ecfe2c99 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -41,6 +41,10 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"difficulty_rating")] public double StarRating { get; set; } + public int EndTimeObjectCount => SliderCount + SpinnerCount; + + public int TotalObjectCount => CircleCount + SliderCount + SpinnerCount; + [JsonProperty(@"drain")] public float DrainRate { get; set; } @@ -108,7 +112,7 @@ namespace osu.Game.Online.API.Requests.Responses DrainRate = DrainRate, CircleSize = CircleSize, ApproachRate = ApproachRate, - OverallDifficulty = OverallDifficulty, + OverallDifficulty = OverallDifficulty }; IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet; diff --git a/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs b/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs new file mode 100644 index 0000000000..bfa5c1043b --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APISystemTitle.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; +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APISystemTitle : IEquatable + { + [JsonProperty(@"image")] + public string Image { get; set; } = string.Empty; + + [JsonProperty(@"url")] + public string Url { get; set; } = string.Empty; + + public bool Equals(APISystemTitle? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return Image == other.Image && Url == other.Url; + } + + public override bool Equals(object? obj) => obj is APISystemTitle other && Equals(other); + + // ReSharper disable NonReadonlyMemberInGetHashCode + public override int GetHashCode() => HashCode.Combine(Image, Url); + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs b/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs index 76d1941d9d..dac72f2488 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs @@ -28,6 +28,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("latest_build")] public APIChangelogBuild LatestBuild { get; set; } + [JsonProperty("user_count")] + public int UserCount { get; set; } + public bool Equals(APIUpdateStream other) => Id == other?.Id; internal static readonly Dictionary KNOWN_STREAMS = new Dictionary diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index e63395fe26..56eec19fa1 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -29,26 +29,21 @@ 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; - private CountryCode? countryCode; + [JsonProperty(@"country_code")] + private string countryCodeString; public CountryCode CountryCode { - get => countryCode ??= (Enum.TryParse(country?.Code, out CountryCode result) ? result : default); - set => countryCode = value; + get => Enum.TryParse(countryCodeString, out CountryCode result) ? result : CountryCode.Unknown; + set => countryCodeString = value.ToString(); } -#pragma warning disable 649 - [CanBeNull] - [JsonProperty(@"country")] - private Country country; -#pragma warning restore 649 - - public readonly Bindable Status = new Bindable(); + public readonly Bindable Status = new Bindable(); public readonly Bindable Activity = new Bindable(); @@ -234,9 +229,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/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs index 15f4bace96..732da3d5da 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,24 @@ 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 + + /// + /// Whether this represents a legacy (osu!stable) score. + /// + [JsonIgnore] + public bool IsLegacyScore => LegacyScoreId != null; + public override string ToString() => $"score_id: {ID} user_id: {UserID}"; /// @@ -178,6 +196,8 @@ namespace osu.Game.Online.API.Requests.Responses var score = new ScoreInfo { OnlineID = OnlineID, + LegacyOnlineID = (long?)LegacyScoreId ?? -1, + IsLegacyScore = IsLegacyScore, User = User ?? new APIUser { Id = UserID }, BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID }, Ruleset = new RulesetInfo { OnlineID = RulesetID }, @@ -189,7 +209,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 +243,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/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index e95bc128c8..23989caae2 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -247,7 +247,7 @@ namespace osu.Game.Online.Chat string command = parameters[0]; string content = parameters.Length == 2 ? parameters[1] : string.Empty; - switch (command) + switch (command.ToLowerInvariant()) { case "np": AddInternal(new NowPlayingCommand(target)); diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 10a005d71a..f055633d64 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets.Edit; namespace osu.Game.Online.Chat { @@ -28,7 +29,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) @@ -41,10 +42,6 @@ namespace osu.Game.Online.Chat @"(?:#(?:[a-z0-9$_\+!\*\',;:\(\)@&=\/~-]|%[0-9a-f]{2})*)?)?)", RegexOptions.IgnoreCase); - // 00:00:000 (1,2,3) - test - // regex from https://github.com/ppy/osu-web/blob/651a9bac2b60d031edd7e33b8073a469bf11edaa/resources/assets/coffee/_classes/beatmap-discussion-helper.coffee#L10 - private static readonly Regex time_regex = new Regex(@"\b(((\d{2,}):([0-5]\d)[:.](\d{3}))(\s\((?:\d+[,|])*\d+\))?)"); - // #osu private static readonly Regex channel_regex = new Regex(@"(#[a-zA-Z]+[a-zA-Z0-9]+)"); @@ -88,8 +85,8 @@ namespace osu.Game.Online.Chat if (escapeChars != null) displayText = escapeChars.Aggregate(displayText, (current, c) => current.Replace($"\\{c}", c.ToString())); - // Check for encapsulated links - if (result.Links.Find(l => (l.Index <= index && l.Index + l.Length >= index + m.Length) || (index <= l.Index && index + m.Length >= l.Index + l.Length)) == null) + // Check for overlapping links + if (!result.Links.Exists(l => l.Overlaps(index, m.Length))) { result.Text = result.Text.Remove(index, m.Length).Insert(index, displayText); @@ -274,7 +271,7 @@ namespace osu.Game.Online.Chat handleAdvanced(advanced_link_regex, result, startIndex); // handle editor times - handleMatches(time_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp); + handleMatches(EditorTimestampParser.TIME_REGEX, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp); // handle channels handleMatches(channel_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}chan/{{0}}", result, startIndex, LinkAction.OpenChannel); @@ -367,7 +364,9 @@ namespace osu.Game.Online.Chat Argument = argument; } - public bool Overlaps(Link otherLink) => Index < otherLink.Index + otherLink.Length && otherLink.Index < Index + Length; + public bool Overlaps(Link otherLink) => Overlaps(otherLink.Index, otherLink.Length); + + public bool Overlaps(int otherIndex, int otherLength) => Index < otherIndex + otherLength && otherIndex < Index + Length; public int CompareTo(Link? otherLink) => Index > otherLink?.Index ? 1 : -1; } diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 65aac723da..56f490cb21 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -182,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/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs index e7018d6993..0e6f6f0bf6 100644 --- a/osu.Game/Online/Chat/NowPlayingCommand.cs +++ b/osu.Game/Online/Chat/NowPlayingCommand.cs @@ -7,7 +7,6 @@ using System.Text; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Rulesets; @@ -33,9 +32,6 @@ namespace osu.Game.Online.Chat [Resolved] private IBindable currentRuleset { get; set; } = null!; - [Resolved] - private LocalisationManager localisation { get; set; } = null!; - private readonly Channel? target; /// @@ -52,23 +48,28 @@ namespace osu.Game.Online.Chat base.LoadComplete(); string verb; - IBeatmapInfo beatmapInfo; + + int beatmapOnlineID; + string beatmapDisplayTitle; switch (api.Activity.Value) { case UserActivity.InGame game: verb = "playing"; - beatmapInfo = game.BeatmapInfo; + beatmapOnlineID = game.BeatmapID; + beatmapDisplayTitle = game.BeatmapDisplayTitle; break; case UserActivity.EditingBeatmap edit: verb = "editing"; - beatmapInfo = edit.BeatmapInfo; + beatmapOnlineID = edit.BeatmapID; + beatmapDisplayTitle = edit.BeatmapDisplayTitle; break; default: verb = "listening to"; - beatmapInfo = currentBeatmap.Value.BeatmapInfo; + beatmapOnlineID = currentBeatmap.Value.BeatmapInfo.OnlineID; + beatmapDisplayTitle = currentBeatmap.Value.BeatmapInfo.GetDisplayTitle(); break; } @@ -86,9 +87,7 @@ namespace osu.Game.Online.Chat string getBeatmapPart() { - string beatmapInfoString = localisation.GetLocalisedBindableString(beatmapInfo.GetDisplayTitleRomanisable()).Value; - - return beatmapInfo.OnlineID > 0 ? $"[{api.WebsiteRootUrl}/b/{beatmapInfo.OnlineID} {beatmapInfoString}]" : beatmapInfoString; + return beatmapOnlineID > 0 ? $"[{api.WebsiteRootUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle; } string getRulesetPart() diff --git a/osu.Game/Online/DevelopmentEndpointConfiguration.cs b/osu.Game/Online/DevelopmentEndpointConfiguration.cs index 69276f452a..5f3c353f4d 100644 --- a/osu.Game/Online/DevelopmentEndpointConfiguration.cs +++ b/osu.Game/Online/DevelopmentEndpointConfiguration.cs @@ -10,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/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index 8fd79bd703..9d414deade 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -27,7 +27,6 @@ namespace osu.Game.Online private readonly string endpoint; private readonly string versionHash; private readonly bool preferMessagePack; - private readonly IAPIProvider api; /// /// The current connection opened by this connector. @@ -47,7 +46,6 @@ namespace osu.Game.Online { ClientName = clientName; this.endpoint = endpoint; - this.api = api; this.versionHash = versionHash; this.preferMessagePack = preferMessagePack; @@ -70,7 +68,7 @@ namespace osu.Game.Online options.Proxy.Credentials = CredentialCache.DefaultCredentials; } - options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); + options.Headers.Add("Authorization", $"Bearer {API.AccessToken}"); options.Headers.Add("OsuVersionHash", versionHash); }); @@ -102,6 +100,12 @@ namespace osu.Game.Online return Task.FromResult((PersistentEndpointClient)new HubClient(newConnection)); } + async Task IHubClientConnector.Disconnect() + { + await Disconnect().ConfigureAwait(false); + API.Logout(); + } + protected override string ClientName { get; } } } diff --git a/osu.Game/Online/IHubClientConnector.cs b/osu.Game/Online/IHubClientConnector.cs index 53c4897e73..052972e6b4 100644 --- a/osu.Game/Online/IHubClientConnector.cs +++ b/osu.Game/Online/IHubClientConnector.cs @@ -30,6 +30,11 @@ namespace osu.Game.Online /// public Action? ConfigureConnection { get; set; } + /// + /// Forcefully disconnects the client from the server. + /// + Task Disconnect(); + /// /// Reconnect if already connected. /// diff --git a/osu.Game/Online/IStatefulUserHubClient.cs b/osu.Game/Online/IStatefulUserHubClient.cs new file mode 100644 index 0000000000..86105dd629 --- /dev/null +++ b/osu.Game/Online/IStatefulUserHubClient.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 System.Threading.Tasks; + +namespace osu.Game.Online +{ + /// + /// Common interface for clients of "stateful user hubs", i.e. server-side hubs + /// that preserve user state. + /// In the case of such hubs, concurrency constraints are enforced (only one client + /// can be connected at a time). + /// + public interface IStatefulUserHubClient + { + Task DisconnectRequested(); + } +} diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 93aa0b95a7..0fd9597ac0 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -152,6 +152,15 @@ namespace osu.Game.Online.Leaderboards /// public void RefetchScores() => Scheduler.AddOnce(refetchScores); + /// + /// Clear all scores from the display. + /// + public void ClearScores() + { + cancelPendingWork(); + SetScores(null); + } + /// /// Call when a retrieval or display failure happened to show a relevant message to the user. /// @@ -220,9 +229,7 @@ namespace osu.Game.Online.Leaderboards { Debug.Assert(ThreadSafety.IsUpdateThread); - cancelPendingWork(); - - SetScores(null); + ClearScores(); setState(LeaderboardState.Retrieving); currentFetchCancellationSource = new CancellationTokenSource(); @@ -280,7 +287,7 @@ namespace osu.Game.Online.Leaderboards double delay = 0; - foreach (var s in scoreFlowContainer.Children) + foreach (var s in scoreFlowContainer) { using (s.BeginDelayedSequence(delay)) s.Show(); @@ -377,7 +384,7 @@ namespace osu.Game.Online.Leaderboards if (scoreFlowContainer == null) return; - foreach (var c in scoreFlowContainer.Children) + foreach (var c in scoreFlowContainer) { float topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, scoreFlowContainer).Y; float bottomY = topY + LeaderboardScore.HEIGHT; diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 229cfd2aa6..964f065813 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 @@ -163,7 +164,8 @@ namespace osu.Game.Online.Leaderboards { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - AutoSizeAxes = Axes.Both, + AutoSizeAxes = Axes.X, + Height = 28, Direction = FillDirection.Horizontal, Spacing = new Vector2(10f, 0f), Children = new Drawable[] @@ -242,7 +244,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) }) }, }, }, @@ -356,14 +358,12 @@ namespace osu.Game.Online.Leaderboards }, }, }, - new GlowingSpriteText + new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - TextColour = Color4.White, - GlowColour = Color4Extensions.FromHex(@"83ccfa"), Text = statistic.Value, - Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold), + Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold, fixedWidth: true) }, }, }; @@ -420,7 +420,7 @@ namespace osu.Game.Online.Leaderboards { List items = new List(); - if (Score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered) && songSelect != null) + if (Score.Mods.Length > 0 && songSelect != null) items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods)); if (Score.Files.Count > 0) 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/Metadata/IMetadataClient.cs b/osu.Game/Online/Metadata/IMetadataClient.cs index ad1e7ebbaf..7102554ae9 100644 --- a/osu.Game/Online/Metadata/IMetadataClient.cs +++ b/osu.Game/Online/Metadata/IMetadataClient.cs @@ -2,11 +2,23 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading.Tasks; +using osu.Game.Users; namespace osu.Game.Online.Metadata { - public interface IMetadataClient + /// + /// Interface for metadata-related remote procedure calls to be executed on the client side. + /// + public interface IMetadataClient : IStatefulUserHubClient { + /// + /// Delivers the set of requested to the client. + /// Task BeatmapSetsUpdated(BeatmapUpdates updates); + + /// + /// Delivers an update of the of the user with the supplied . + /// + Task UserPresenceUpdated(int userId, UserPresence? status); } } diff --git a/osu.Game/Online/Metadata/IMetadataServer.cs b/osu.Game/Online/Metadata/IMetadataServer.cs index 994f60f877..9780045333 100644 --- a/osu.Game/Online/Metadata/IMetadataServer.cs +++ b/osu.Game/Online/Metadata/IMetadataServer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading.Tasks; +using osu.Game.Users; namespace osu.Game.Online.Metadata { @@ -17,5 +18,25 @@ namespace osu.Game.Online.Metadata /// The last processed queue ID. /// Task GetChangesSince(int queueId); + + /// + /// Signals to the server that the current user's has changed. + /// + Task UpdateActivity(UserActivity? activity); + + /// + /// Signals to the server that the current user's has changed. + /// + Task UpdateStatus(UserStatus? status); + + /// + /// Signals to the server that the current user would like to begin receiving updates on other users' online presence. + /// + Task BeginWatchingUserPresence(); + + /// + /// Signals to the server that the current user would like to stop receiving updates on other users' online presence. + /// + Task EndWatchingUserPresence(); } } diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index d4e7540fe7..8e99a9b2cb 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -4,22 +4,71 @@ using System; using System.Linq; using System.Threading.Tasks; +using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Users; namespace osu.Game.Online.Metadata { public abstract partial class MetadataClient : Component, IMetadataClient, IMetadataServer { - public abstract Task BeatmapSetsUpdated(BeatmapUpdates updates); + public abstract IBindable IsConnected { get; } + + #region Beatmap metadata updates public abstract Task GetChangesSince(int queueId); - public Action? ChangedBeatmapSetsArrived; + public abstract Task BeatmapSetsUpdated(BeatmapUpdates updates); + + public event Action? ChangedBeatmapSetsArrived; protected Task ProcessChanges(int[] beatmapSetIDs) { ChangedBeatmapSetsArrived?.Invoke(beatmapSetIDs.Distinct().ToArray()); return Task.CompletedTask; } + + #endregion + + #region User presence updates + + /// + /// Whether the client is currently receiving user presence updates from the server. + /// + public abstract IBindable IsWatchingUserPresence { get; } + + /// + /// Dictionary keyed by user ID containing all of the information about currently online users received from the server. + /// + public abstract IBindableDictionary UserStates { get; } + + /// + public abstract Task UpdateActivity(UserActivity? activity); + + /// + public abstract Task UpdateStatus(UserStatus? status); + + /// + public abstract Task BeginWatchingUserPresence(); + + /// + public abstract Task EndWatchingUserPresence(); + + /// + public abstract Task UserPresenceUpdated(int userId, UserPresence? presence); + + #endregion + + #region Disconnection handling + + public event Action? Disconnecting; + + public virtual Task DisconnectRequested() + { + Schedule(() => Disconnecting?.Invoke()); + return Task.CompletedTask; + } + + #endregion } } diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index 57311419f7..c42c3378b7 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; using osu.Framework.Allocation; @@ -10,17 +11,31 @@ using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Game.Configuration; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users; namespace osu.Game.Online.Metadata { public partial class OnlineMetadataClient : MetadataClient { + public override IBindable IsConnected { get; } = new Bindable(); + + public override IBindable IsWatchingUserPresence => isWatchingUserPresence; + private readonly BindableBool isWatchingUserPresence = new BindableBool(); + + public override IBindableDictionary UserStates => userStates; + private readonly BindableDictionary userStates = new BindableDictionary(); + private readonly string endpoint; private IHubClientConnector? connector; private Bindable lastQueueId = null!; + private IBindable localUser = null!; + private IBindable userActivity = null!; + private IBindable? userStatus; + private HubConnection? connection => connector?.CurrentConnection; public OnlineMetadataClient(EndpointConfiguration endpoints) @@ -33,7 +48,7 @@ namespace osu.Game.Online.Metadata { // Importantly, we are intentionally not using MessagePack here to correctly support derived class serialization. // More information on the limitations / reasoning can be found in osu-server-spectator's initialisation code. - connector = api.GetHubConnector(nameof(OnlineMetadataClient), endpoint); + connector = api.GetHubConnector(nameof(OnlineMetadataClient), endpoint, false); if (connector != null) { @@ -42,12 +57,37 @@ namespace osu.Game.Online.Metadata // this is kind of SILLY // https://github.com/dotnet/aspnetcore/issues/15198 connection.On(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated); + connection.On(nameof(IMetadataClient.UserPresenceUpdated), ((IMetadataClient)this).UserPresenceUpdated); }; - connector.IsConnected.BindValueChanged(isConnectedChanged, true); + IsConnected.BindTo(connector.IsConnected); + IsConnected.BindValueChanged(isConnectedChanged, true); } lastQueueId = config.GetBindable(OsuSetting.LastProcessedMetadataId); + + localUser = api.LocalUser.GetBoundCopy(); + userActivity = api.Activity.GetBoundCopy()!; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + localUser.BindValueChanged(_ => + { + if (localUser.Value is not GuestUser) + { + userStatus = localUser.Value.Status.GetBoundCopy(); + userStatus.BindValueChanged(status => UpdateStatus(status.NewValue), true); + } + else + userStatus = null; + }, true); + userActivity.BindValueChanged(activity => + { + if (localUser.Value is not GuestUser) + UpdateActivity(activity.NewValue); + }, true); } private bool catchingUp; @@ -55,7 +95,20 @@ namespace osu.Game.Online.Metadata private void isConnectedChanged(ValueChangedEvent connected) { if (!connected.NewValue) + { + Schedule(() => + { + isWatchingUserPresence.Value = false; + userStates.Clear(); + }); return; + } + + if (localUser.Value is not GuestUser) + { + UpdateActivity(userActivity.Value); + UpdateStatus(userStatus?.Value); + } if (lastQueueId.Value >= 0) { @@ -116,6 +169,71 @@ namespace osu.Game.Online.Metadata return connection.InvokeAsync(nameof(IMetadataServer.GetChangesSince), queueId); } + public override Task UpdateActivity(UserActivity? activity) + { + if (connector?.IsConnected.Value != true) + return Task.FromCanceled(new CancellationToken(true)); + + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMetadataServer.UpdateActivity), activity); + } + + public override Task UpdateStatus(UserStatus? status) + { + if (connector?.IsConnected.Value != true) + return Task.FromCanceled(new CancellationToken(true)); + + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMetadataServer.UpdateStatus), status); + } + + public override Task UserPresenceUpdated(int userId, UserPresence? presence) + { + Schedule(() => + { + if (presence?.Status != null) + userStates[userId] = presence.Value; + else + userStates.Remove(userId); + }); + + return Task.CompletedTask; + } + + public override async Task BeginWatchingUserPresence() + { + if (connector?.IsConnected.Value != true) + throw new OperationCanceledException(); + + Debug.Assert(connection != null); + await connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence)).ConfigureAwait(false); + Schedule(() => isWatchingUserPresence.Value = true); + } + + public override async Task EndWatchingUserPresence() + { + try + { + if (connector?.IsConnected.Value != true) + throw new OperationCanceledException(); + + // must be scheduled before any remote calls to avoid mis-ordering. + Schedule(() => userStates.Clear()); + Debug.Assert(connection != null); + await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false); + } + finally + { + Schedule(() => isWatchingUserPresence.Value = false); + } + } + + public override async Task DisconnectRequested() + { + await base.DisconnectRequested().ConfigureAwait(false); + await EndWatchingUserPresence().ConfigureAwait(false); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Online/Multiplayer/GameplayAbortReason.cs b/osu.Game/Online/Multiplayer/GameplayAbortReason.cs new file mode 100644 index 0000000000..15151ea68b --- /dev/null +++ b/osu.Game/Online/Multiplayer/GameplayAbortReason.cs @@ -0,0 +1,11 @@ +// 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.Online.Multiplayer +{ + public enum GameplayAbortReason + { + LoadTookTooLong, + HostAbortedTheMatch + } +} diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index 995bac1af5..0452d8b79c 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -13,7 +13,7 @@ namespace osu.Game.Online.Multiplayer /// /// An interface defining a multiplayer client instance. /// - public interface IMultiplayerClient + public interface IMultiplayerClient : IStatefulUserHubClient { /// /// Signals that the room has changed state. @@ -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. /// @@ -99,17 +107,18 @@ namespace osu.Game.Online.Multiplayer /// Task LoadRequested(); - /// - /// Signals that loading of gameplay is to be aborted. - /// - Task LoadAborted(); - /// /// Signals that gameplay has started. /// All users in the or states should begin gameplay as soon as possible. /// Task GameplayStarted(); + /// + /// Signals that gameplay has been aborted. + /// + /// The reason why gameplay was aborted. + Task GameplayAborted(GameplayAbortReason reason); + /// /// Signals that the match has ended, all players have finished and results are ready to be displayed. /// diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index b7a5faf7c9..55f00b447f 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -77,6 +77,11 @@ namespace osu.Game.Online.Multiplayer /// If an attempt to start the game occurs when the game's (or users') state disallows it. Task StartMatch(); + /// + /// As the host of a room, aborts an on-going match. + /// + Task AbortMatch(); + /// /// Aborts an ongoing gameplay load. /// @@ -99,5 +104,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/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 5716b7ad3b..bbf0e3697a 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -12,7 +12,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Development; using osu.Framework.Graphics; -using osu.Framework.Logging; using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -23,6 +22,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 +30,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. /// @@ -71,9 +73,9 @@ namespace osu.Game.Online.Multiplayer public virtual event Action? LoadRequested; /// - /// Invoked when the multiplayer server requests loading of play to be aborted. + /// Invoked when the multiplayer server requests gameplay to be aborted. /// - public event Action? LoadAborted; + public event Action? GameplayAborted; /// /// Invoked when the multiplayer server requests gameplay to be started. @@ -85,6 +87,11 @@ namespace osu.Game.Online.Multiplayer /// public event Action? ResultsReady; + /// + /// Invoked just prior to disconnection requested by the server via . + /// + public event Action? Disconnecting; + /// /// Whether the is currently connected. /// This is NOT thread safe and usage should be scheduled. @@ -152,10 +159,7 @@ namespace osu.Game.Online.Multiplayer { // clean up local room state on server disconnect. if (!connected.NewValue && Room != null) - { - Logger.Log("Clearing room due to multiplayer server connection loss.", LoggingTarget.Runtime, LogLevel.Important); LeaveRoom(); - } })); } @@ -260,6 +264,8 @@ namespace osu.Game.Online.Multiplayer protected abstract Task LeaveRoomInternal(); + public abstract Task InvitePlayer(int userId); + /// /// Change the current settings. /// @@ -352,6 +358,8 @@ namespace osu.Game.Online.Multiplayer public abstract Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability); + public abstract Task DisconnectInternal(); + /// /// Change the local user's mods in the currently joined room. /// @@ -366,6 +374,8 @@ namespace osu.Game.Online.Multiplayer public abstract Task AbortGameplay(); + public abstract Task AbortMatch(); + public abstract Task AddPlaylistItem(MultiplayerPlaylistItem item); public abstract Task EditPlaylistItem(MultiplayerPlaylistItem item); @@ -440,6 +450,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); @@ -642,14 +684,14 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } - Task IMultiplayerClient.LoadAborted() + Task IMultiplayerClient.GameplayAborted(GameplayAbortReason reason) { Scheduler.Add(() => { if (Room == null) return; - LoadAborted?.Invoke(); + GameplayAborted?.Invoke(reason); }, false); return Task.CompletedTask; @@ -839,5 +881,15 @@ namespace osu.Game.Online.Multiplayer return tcs.Task; } + + Task IStatefulUserHubClient.DisconnectRequested() + { + Schedule(() => + { + Disconnecting?.Invoke(); + DisconnectInternal(); + }); + return Task.CompletedTask; + } } } diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 8ff0ce4065..40436d730e 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,12 +52,13 @@ 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); connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested); connection.On(nameof(IMultiplayerClient.GameplayStarted), ((IMultiplayerClient)this).GameplayStarted); - connection.On(nameof(IMultiplayerClient.LoadAborted), ((IMultiplayerClient)this).LoadAborted); + connection.On(nameof(IMultiplayerClient.GameplayAborted), ((IMultiplayerClient)this).GameplayAborted); connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); connection.On>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged); connection.On(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged); @@ -65,6 +68,7 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.PlaylistItemAdded), ((IMultiplayerClient)this).PlaylistItemAdded); connection.On(nameof(IMultiplayerClient.PlaylistItemRemoved), ((IMultiplayerClient)this).PlaylistItemRemoved); connection.On(nameof(IMultiplayerClient.PlaylistItemChanged), ((IMultiplayerClient)this).PlaylistItemChanged); + connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMultiplayerClient)this).DisconnectRequested); }; IsConnected.BindTo(connector.IsConnected); @@ -106,6 +110,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) @@ -196,6 +226,16 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.AbortGameplay)); } + public override Task AbortMatch() + { + if (!IsConnected.Value) + return Task.CompletedTask; + + Debug.Assert(connection != null); + + return connection.InvokeAsync(nameof(IMultiplayerServer.AbortMatch)); + } + public override Task AddPlaylistItem(MultiplayerPlaylistItem item) { if (!IsConnected.Value) @@ -226,6 +266,14 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId); } + public override Task DisconnectInternal() + { + if (connector == null) + return Task.CompletedTask; + + return connector.Disconnect(); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); 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/OnlineStatusNotifier.cs b/osu.Game/Online/OnlineStatusNotifier.cs new file mode 100644 index 0000000000..c36e4ab894 --- /dev/null +++ b/osu.Game/Online/OnlineStatusNotifier.cs @@ -0,0 +1,144 @@ +// 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.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Screens; +using osu.Game.Online.API; +using osu.Game.Online.Metadata; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Spectator; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Screens.OnlinePlay; + +namespace osu.Game.Online +{ + /// + /// Handles various scenarios where connection is lost and we need to let the user know what and why. + /// + public partial class OnlineStatusNotifier : Component + { + private readonly Func getCurrentScreen; + + [Resolved] + private MultiplayerClient multiplayerClient { get; set; } = null!; + + [Resolved] + private SpectatorClient spectatorClient { get; set; } = null!; + + [Resolved] + private MetadataClient metadataClient { get; set; } = null!; + + [Resolved] + private INotificationOverlay? notificationOverlay { get; set; } + + private IBindable apiState = null!; + private IBindable multiplayerState = null!; + private IBindable spectatorState = null!; + + /// + /// This flag will be set to true when the user has been notified so we don't show more than one notification. + /// + private bool userNotified; + + public OnlineStatusNotifier(Func getCurrentScreen) + { + this.getCurrentScreen = getCurrentScreen; + } + + [BackgroundDependencyLoader] + private void load(IAPIProvider api) + { + apiState = api.State.GetBoundCopy(); + multiplayerState = multiplayerClient.IsConnected.GetBoundCopy(); + spectatorState = spectatorClient.IsConnected.GetBoundCopy(); + + multiplayerClient.Disconnecting += notifyAboutForcedDisconnection; + spectatorClient.Disconnecting += notifyAboutForcedDisconnection; + metadataClient.Disconnecting += notifyAboutForcedDisconnection; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + apiState.BindValueChanged(state => + { + if (state.NewValue == APIState.Online) + { + userNotified = false; + return; + } + + if (userNotified) return; + + if (state.NewValue == APIState.Offline && getCurrentScreen() is OnlinePlayScreen) + { + userNotified = true; + notificationOverlay?.Post(new SimpleErrorNotification + { + Icon = FontAwesome.Solid.ExclamationCircle, + Text = "Connection to API was lost. Can't continue with online play." + }); + } + }); + + multiplayerState.BindValueChanged(connected => Schedule(() => + { + if (connected.NewValue) + { + userNotified = false; + return; + } + + if (userNotified) return; + + if (multiplayerClient.Room != null) + { + userNotified = true; + notificationOverlay?.Post(new SimpleErrorNotification + { + Icon = FontAwesome.Solid.ExclamationCircle, + Text = "Connection to the multiplayer server was lost. Exiting multiplayer." + }); + } + })); + + spectatorState.BindValueChanged(_ => + { + // TODO: handle spectator server failure somehow? + }); + } + + private void notifyAboutForcedDisconnection() + { + if (userNotified) return; + + userNotified = true; + notificationOverlay?.Post(new SimpleErrorNotification + { + Icon = FontAwesome.Solid.ExclamationCircle, + Text = "You have been logged out on this device due to a login to your account on another device." + }); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (spectatorClient.IsNotNull()) + spectatorClient.Disconnecting -= notifyAboutForcedDisconnection; + + if (multiplayerClient.IsNotNull()) + multiplayerClient.Disconnecting -= notifyAboutForcedDisconnection; + + if (metadataClient.IsNotNull()) + metadataClient.Disconnecting -= notifyAboutForcedDisconnection; + } + } +} diff --git a/osu.Game/Online/PersistentEndpointClientConnector.cs b/osu.Game/Online/PersistentEndpointClientConnector.cs index e33924047d..8c1b58a750 100644 --- a/osu.Game/Online/PersistentEndpointClientConnector.cs +++ b/osu.Game/Online/PersistentEndpointClientConnector.cs @@ -159,6 +159,8 @@ namespace osu.Game.Online await Task.Run(connect, default).ConfigureAwait(false); } + protected Task Disconnect() => disconnect(true); + private async Task disconnect(bool takeLock) { cancelExistingConnect(); diff --git a/osu.Game/Online/Placeholders/MessagePlaceholder.cs b/osu.Game/Online/Placeholders/MessagePlaceholder.cs index bef889a576..b4a9c2086b 100644 --- a/osu.Game/Online/Placeholders/MessagePlaceholder.cs +++ b/osu.Game/Online/Placeholders/MessagePlaceholder.cs @@ -22,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/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/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index de42292372..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,6 +48,7 @@ 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, _) => { diff --git a/osu.Game/Online/SignalRWorkaroundTypes.cs b/osu.Game/Online/SignalRWorkaroundTypes.cs index 0e3eb0aab0..59a12b3bf1 100644 --- a/osu.Game/Online/SignalRWorkaroundTypes.cs +++ b/osu.Game/Online/SignalRWorkaroundTypes.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; +using osu.Game.Users; namespace osu.Game.Online { @@ -18,6 +19,7 @@ namespace osu.Game.Online { internal static readonly IReadOnlyList<(Type derivedType, Type baseType)> BASE_TYPE_MAPPING = new[] { + // multiplayer (typeof(ChangeTeamRequest), typeof(MatchUserRequest)), (typeof(StartMatchCountdownRequest), typeof(MatchUserRequest)), (typeof(StopCountdownRequest), typeof(MatchUserRequest)), @@ -28,6 +30,20 @@ namespace osu.Game.Online (typeof(MatchStartCountdown), typeof(MultiplayerCountdown)), (typeof(ForceGameplayStartCountdown), typeof(MultiplayerCountdown)), (typeof(ServerShuttingDownCountdown), typeof(MultiplayerCountdown)), + + // metadata + (typeof(UserActivity.ChoosingBeatmap), typeof(UserActivity)), + (typeof(UserActivity.InSoloGame), typeof(UserActivity)), + (typeof(UserActivity.WatchingReplay), typeof(UserActivity)), + (typeof(UserActivity.SpectatingUser), typeof(UserActivity)), + (typeof(UserActivity.SearchingForLobby), typeof(UserActivity)), + (typeof(UserActivity.InLobby), typeof(UserActivity)), + (typeof(UserActivity.InMultiplayerGame), typeof(UserActivity)), + (typeof(UserActivity.SpectatingMultiplayerGame), typeof(UserActivity)), + (typeof(UserActivity.InPlaylistGame), typeof(UserActivity)), + (typeof(UserActivity.EditingBeatmap), typeof(UserActivity)), + (typeof(UserActivity.ModdingBeatmap), typeof(UserActivity)), + (typeof(UserActivity.TestingBeatmap), typeof(UserActivity)), }; } } diff --git a/osu.Game/Online/Solo/SoloStatisticsWatcher.cs b/osu.Game/Online/Solo/SoloStatisticsWatcher.cs index 46449fea73..55b27fb364 100644 --- a/osu.Game/Online/Solo/SoloStatisticsWatcher.cs +++ b/osu.Game/Online/Solo/SoloStatisticsWatcher.cs @@ -127,6 +127,8 @@ namespace osu.Game.Online.Solo { string rulesetName = callback.Score.Ruleset.ShortName; + api.UpdateStatistics(updatedStatistics); + if (latestStatistics == null) return; diff --git a/osu.Game/Online/Spectator/ISpectatorClient.cs b/osu.Game/Online/Spectator/ISpectatorClient.cs index 9605604966..2dc2283c23 100644 --- a/osu.Game/Online/Spectator/ISpectatorClient.cs +++ b/osu.Game/Online/Spectator/ISpectatorClient.cs @@ -8,7 +8,7 @@ namespace osu.Game.Online.Spectator /// /// An interface defining a spectator client instance. /// - public interface ISpectatorClient + public interface ISpectatorClient : IStatefulUserHubClient { /// /// Signals that a user has begun a new play session. diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs index 3118e05053..036cfa1d76 100644 --- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs +++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs @@ -42,6 +42,7 @@ namespace osu.Game.Online.Spectator connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); connection.On(nameof(ISpectatorClient.UserScoreProcessed), ((ISpectatorClient)this).UserScoreProcessed); + connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested); }; IsConnected.BindTo(connector.IsConnected); @@ -113,5 +114,15 @@ namespace osu.Game.Online.Spectator return connection.InvokeAsync(nameof(ISpectatorServer.EndWatchingUser), userId); } + + protected override async Task DisconnectInternal() + { + await base.DisconnectInternal().ConfigureAwait(false); + + if (connector == null) + return; + + await connector.Disconnect().ConfigureAwait(false); + } } } diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 14e137caf1..7911701853 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -48,7 +48,7 @@ namespace osu.Game.Online.Spectator /// /// Whether the local user is playing. /// - protected internal bool IsPlaying { get; private set; } + private bool isPlaying { get; set; } /// /// Called whenever new frames arrive from the server. @@ -58,17 +58,22 @@ namespace osu.Game.Online.Spectator /// /// Called whenever a user starts a play session, or immediately if the user is being watched and currently in a play session. /// - public virtual event Action? OnUserBeganPlaying; + public event Action? OnUserBeganPlaying; /// /// Called whenever a user finishes a play session. /// - public virtual event Action? OnUserFinishedPlaying; + public event Action? OnUserFinishedPlaying; /// /// Called whenever a user-submitted score has been fully processed. /// - public virtual event Action? OnUserScoreProcessed; + public event Action? OnUserScoreProcessed; + + /// + /// Invoked just prior to disconnection requested by the server via . + /// + public event Action? Disconnecting; /// /// A dictionary containing all users currently being watched, with the number of watching components for each user. @@ -114,7 +119,7 @@ namespace osu.Game.Online.Spectator } // re-send state in case it wasn't received - if (IsPlaying) + if (isPlaying) // TODO: this is likely sent out of order after a reconnect scenario. needs further consideration. BeginPlayingInternal(currentScoreToken, currentState); } @@ -174,15 +179,21 @@ namespace osu.Game.Online.Spectator return Task.CompletedTask; } + Task IStatefulUserHubClient.DisconnectRequested() + { + Schedule(() => DisconnectInternal()); + return Task.CompletedTask; + } + public void BeginPlaying(long? scoreToken, GameplayState state, Score score) { // This schedule is only here to match the one below in `EndPlaying`. Schedule(() => { - if (IsPlaying) + if (isPlaying) throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing"); - IsPlaying = true; + isPlaying = true; // transfer state at point of beginning play currentState.BeatmapID = score.ScoreInfo.BeatmapInfo!.OnlineID; @@ -202,7 +213,7 @@ namespace osu.Game.Online.Spectator public void HandleFrame(ReplayFrame frame) => Schedule(() => { - if (!IsPlaying) + if (!isPlaying) { Logger.Log($"Frames arrived at {nameof(SpectatorClient)} outside of gameplay scope and will be ignored."); return; @@ -224,7 +235,7 @@ namespace osu.Game.Online.Spectator // We probably need to find a better way to handle this... Schedule(() => { - if (!IsPlaying) + if (!isPlaying) return; // Disposal can take some time, leading to EndPlaying potentially being called after a future play session. @@ -235,7 +246,7 @@ namespace osu.Game.Online.Spectator if (pendingFrames.Count > 0) purgePendingFrames(); - IsPlaying = false; + isPlaying = false; currentBeatmap = null; if (state.HasPassed) @@ -291,6 +302,12 @@ namespace osu.Game.Online.Spectator protected abstract Task StopWatchingUserInternal(int userId); + protected virtual Task DisconnectInternal() + { + Disconnecting?.Invoke(); + return Task.CompletedTask; + } + protected override void Update() { base.Update(); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 1a40bb8e3d..c244708385 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -46,6 +46,7 @@ using osu.Game.IO; using osu.Game.Localisation; using osu.Game.Online; 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,7 +58,9 @@ using osu.Game.Performance; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens; +using osu.Game.Screens.Edit; 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; @@ -77,6 +80,13 @@ namespace osu.Game [Cached(typeof(OsuGame))] public partial class OsuGame : OsuGameBase, IKeyBindingHandler, ILocalUserPlayInfo, IPerformFromScreenRunner, IOverlayManager, ILinkHandler { +#if DEBUG + // Different port allows runnning release and debug builds alongside each other. + public const int IPC_PORT = 44824; +#else + public const int IPC_PORT = 44823; +#endif + /// /// The amount of global offset to apply when a left/right anchored overlay is displayed (ie. settings or notifications). /// @@ -431,6 +441,9 @@ namespace osu.Game break; case LinkAction.OpenEditorTimestamp: + HandleTimestamp(argString); + break; + case LinkAction.JoinMultiplayerMatch: case LinkAction.Spectate: waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification @@ -548,6 +561,25 @@ namespace osu.Game /// The build version of the update stream public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version)); + /// + /// Seeks to the provided if the editor is currently open. + /// Can also select objects as indicated by the (depends on ruleset implementation). + /// + public void HandleTimestamp(string timestamp) + { + if (ScreenStack.CurrentScreen is not Editor editor) + { + Schedule(() => Notifications.Post(new SimpleErrorNotification + { + Icon = FontAwesome.Solid.ExclamationTriangle, + Text = EditorStrings.MustBeInEditorToHandleLinks + })); + return; + } + + editor.HandleTimestamp(timestamp); + } + /// /// Present a skin select immediately. /// @@ -643,6 +675,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. @@ -658,6 +708,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); @@ -853,6 +906,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. @@ -947,8 +1001,11 @@ namespace osu.Game Margin = new MarginPadding(5), }, topMostOverlayContent.Add); - if (!args?.Any(a => a == @"--no-version-overlay") ?? true) - loadComponentSingleFile(versionManager = new VersionManager { Depth = int.MinValue }, ScreenContainer.Add); + if (!IsDeployedBuild) + { + dependencies.Cache(versionManager = new VersionManager { Depth = int.MinValue }); + loadComponentSingleFile(versionManager, ScreenContainer.Add); + } loadComponentSingleFile(osuLogo, _ => { @@ -1025,11 +1082,12 @@ namespace osu.Game loadComponentSingleFile(CreateHighPerformanceSession(), Add); - loadComponentSingleFile(new BackgroundBeatmapProcessor(), Add); + loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add); Add(difficultyRecommender); Add(externalLinkOpener = new ExternalLinkOpener()); Add(new MusicKeyBindingHandler()); + Add(new OnlineStatusNotifier(() => ScreenStack.CurrentScreen)); // side overlays which cancel each other. var singleDisplaySideOverlays = new OverlayContainer[] { Settings, Notifications, FirstRunOverlay }; @@ -1142,7 +1200,7 @@ namespace osu.Game } else if (recentLogCount == short_term_display_limit) { - string logFile = $@"{entry.Target.Value.ToString().ToLowerInvariant()}.log"; + string logFile = Logger.GetLogger(entry.Target.Value).Filename; Schedule(() => Notifications.Post(new SimpleNotification { @@ -1150,7 +1208,7 @@ namespace osu.Game Text = NotificationsStrings.SubsequentMessagesLogged, Activated = () => { - Storage.GetStorageForDirectory(@"logs").PresentFileExternally(logFile); + Logger.Storage.PresentFileExternally(logFile); return true; } })); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 6737caa5f9..4e465f59df 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -200,6 +200,8 @@ namespace osu.Game private RulesetConfigCache rulesetConfigCache; + private SessionAverageHitErrorTracker hitErrorTracker; + protected SpectatorClient SpectatorClient { get; private set; } protected MultiplayerClient MultiplayerClient { get; private set; } @@ -215,7 +217,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; @@ -349,6 +351,7 @@ namespace osu.Game dependencies.CacheAs(powerStatus); dependencies.Cache(SessionStatics = new SessionStatics()); + dependencies.Cache(hitErrorTracker = new SessionAverageHitErrorTracker()); dependencies.Cache(Colours = new OsuColour()); RegisterImportHandler(BeatmapManager); @@ -392,20 +395,24 @@ 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 + }), + } }) }); + base.Content.Add(new TouchInputInterceptor()); + base.Content.Add(hitErrorTracker); + KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider); KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets); @@ -440,16 +447,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() { @@ -483,6 +481,8 @@ namespace osu.Game AddFont(Resources, @"Fonts/Venera/Venera-Light"); AddFont(Resources, @"Fonts/Venera/Venera-Bold"); AddFont(Resources, @"Fonts/Venera/Venera-Black"); + + Fonts.AddStore(new OsuIcon.OsuIconStore(Textures)); } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => @@ -531,14 +531,21 @@ namespace osu.Game { ManualResetEventSlim readyToRun = new ManualResetEventSlim(); + bool success = false; + Scheduler.Add(() => { - realmBlocker = realm.BlockAllOperations("migration"); + try + { + realmBlocker = realm.BlockAllOperations("migration"); + success = true; + } + catch { } readyToRun.Set(); }, false); - if (!readyToRun.Wait(30000)) + if (!readyToRun.Wait(30000) || !success) throw new TimeoutException("Attempting to block for migration took too long."); bool? cleanupSucceded = (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); @@ -583,14 +590,14 @@ namespace osu.Game case JoystickHandler jh: return new JoystickSettings(jh); - - case TouchHandler th: - return new TouchSettings(th); } } switch (handler) { + case TouchHandler th: + return new TouchSettings(th); + case MidiHandler: return new InputSection.HandlerSection(handler); 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/BeatmapListingHeader.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs index 3336c383ff..075dfd02b0 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs @@ -4,6 +4,7 @@ #nullable disable using osu.Framework.Graphics; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Resources.Localisation.Web; @@ -23,7 +24,7 @@ namespace osu.Game.Overlays.BeatmapListing { Title = PageTitleStrings.MainBeatmapsetsControllerIndex; Description = NamedOverlayComponentStrings.BeatmapListingDescription; - IconTexture = "Icons/Hexacons/beatmap"; + Icon = OsuIcon.Beatmap; } } } diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index f8784504b8..a645683c5f 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -183,9 +183,7 @@ namespace osu.Game.Overlays // new results may contain beatmaps from a previous page, // this is dodgy but matches web behaviour for now. // see: https://github.com/ppy/osu-web/issues/9270 - // todo: replace custom equality compraer with ExceptBy in net6.0 - // newCards = newCards.ExceptBy(foundContent.Select(c => c.BeatmapSet.OnlineID), c => c.BeatmapSet.OnlineID); - newCards = newCards.Except(foundContent, BeatmapCardEqualityComparer.Default); + newCards = newCards.ExceptBy(foundContent.Select(c => c.BeatmapSet.OnlineID), c => c.BeatmapSet.OnlineID); panelLoadTask = LoadComponentsAsync(newCards, loaded => { @@ -378,21 +376,5 @@ namespace osu.Game.Overlays if (shouldShowMore) filterControl.FetchNextPage(); } - - private class BeatmapCardEqualityComparer : IEqualityComparer - { - public static BeatmapCardEqualityComparer Default { get; } = new BeatmapCardEqualityComparer(); - - public bool Equals(BeatmapCard x, BeatmapCard y) - { - if (ReferenceEquals(x, y)) return true; - if (ReferenceEquals(x, null)) return false; - if (ReferenceEquals(y, null)) return false; - - return x.BeatmapSet.Equals(y.BeatmapSet); - } - - public int GetHashCode(BeatmapCard obj) => obj.BeatmapSet.GetHashCode(); - } } } diff --git a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs index 1d01495188..99ad5a5c7d 100644 --- a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs +++ b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs @@ -55,7 +55,7 @@ namespace osu.Game.Overlays.BeatmapSet AutoSizeAxes = Axes.Both, CornerRadius = 4, Masking = true, - Child = avatar = new UpdateableAvatar(showGuestOnNull: false) + Child = avatar = new UpdateableAvatar(showUserPanelOnHover: true, showGuestOnNull: false) { Size = new Vector2(height), }, diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs index 858742648c..1df246ae77 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Effects; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; @@ -59,7 +60,7 @@ namespace osu.Game.Overlays.BeatmapSet public BeatmapHeaderTitle() { Title = PageTitleStrings.MainBeatmapsetsControllerShow; - IconTexture = "Icons/Hexacons/beatmap"; + Icon = OsuIcon.Beatmap; } } } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 615a3e39af..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 @@ -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/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index c92b79cb4d..72e590b009 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -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/Changelog/ChangelogEntry.cs b/osu.Game/Overlays/Changelog/ChangelogEntry.cs index 06ec2043f9..9c40440778 100644 --- a/osu.Game/Overlays/Changelog/ChangelogEntry.cs +++ b/osu.Game/Overlays/Changelog/ChangelogEntry.cs @@ -12,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; @@ -29,6 +30,9 @@ namespace osu.Game.Overlays.Changelog [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + [Resolved] + private ILinkHandler? linkHandler { get; set; } + private FontUsage fontLarge; private FontUsage fontMedium; @@ -86,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/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index e9be67e977..f738d70370 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -12,6 +12,7 @@ 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.Localisation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; @@ -123,7 +124,7 @@ namespace osu.Game.Overlays.Changelog { Title = PageTitleStrings.MainChangelogControllerDefault; Description = NamedOverlayComponentStrings.ChangelogDescription; - IconTexture = "Icons/Hexacons/devtools"; + Icon = OsuIcon.ChangelogB; } } } diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs index 08ea373fb1..30273d2405 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -24,7 +24,7 @@ namespace osu.Game.Overlays.Changelog protected override LocalisableString AdditionalText => Value.LatestBuild.DisplayVersion; - protected override LocalisableString InfoText => Value.LatestBuild.Users > 0 ? $"{"user".ToQuantity(Value.LatestBuild.Users, "N0")} online" : null; + protected override LocalisableString InfoText => Value.UserCount > 0 ? $"{"user".ToQuantity(Value.UserCount, "N0")} online" : null; protected override Color4 GetBarColour(OsuColour colours) => Value.Colour; } diff --git a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs index 0410174dc1..3ecdb09976 100644 --- a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs +++ b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs @@ -1,8 +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.Framework; using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -13,25 +13,22 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Resources.Localisation.Web; using osuTK; -using osuTK.Graphics; namespace osu.Game.Overlays.Chat { public partial class ChatOverlayTopBar : Container { - private Box background = null!; - - private Color4 backgroundColour; + public Drawable DragBar { get; private set; } = null!; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, TextureStore textures) { - Children = new Drawable[] + Children = new[] { - background = new Box + new Box { RelativeSizeAxes = Axes.Both, - Colour = backgroundColour = colourProvider.Background3, + Colour = colourProvider.Background3, }, new GridContainer { @@ -45,12 +42,12 @@ namespace osu.Game.Overlays.Chat { new Drawable[] { - new Sprite + new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = textures.Get("Icons/Hexacons/messaging"), - Size = new Vector2(18), + Icon = OsuIcon.Chat, + Size = new Vector2(24), }, // Placeholder text new OsuSpriteText @@ -64,19 +61,92 @@ namespace osu.Game.Overlays.Chat }, }, }, + DragBar = new DragArea + { + Alpha = RuntimeInfo.IsMobile ? 1 : 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colourProvider.Background4, + } }; } protected override bool OnHover(HoverEvent e) { - background.FadeColour(backgroundColour.Lighten(0.1f), 300, Easing.OutQuint); + if (!RuntimeInfo.IsMobile) + DragBar.FadeIn(100); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - background.FadeColour(backgroundColour, 300, Easing.OutQuint); + if (!RuntimeInfo.IsMobile) + DragBar.FadeOut(100); base.OnHoverLost(e); } + + private partial class DragArea : CompositeDrawable + { + private readonly Circle circle; + + public DragArea() + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + circle = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(150, 7), + Margin = new MarginPadding(12), + } + }; + } + + protected override bool OnHover(HoverEvent e) + { + updateScale(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateScale(); + base.OnHoverLost(e); + } + + private bool dragging; + + protected override bool OnMouseDown(MouseDownEvent e) + { + dragging = true; + updateScale(); + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + dragging = false; + updateScale(); + base.OnMouseUp(e); + } + + private void updateScale() + { + if (dragging || IsHovered) + circle.FadeIn(100); + else + circle.FadeTo(0.6f, 100); + + if (dragging) + circle.ScaleTo(1f, 400, Easing.OutQuint); + else if (IsHovered) + circle.ScaleTo(1.05f, 400, Easing.OutElasticHalf); + else + circle.ScaleTo(1f, 500, Easing.OutQuint); + } + } } } diff --git a/osu.Game/Overlays/Chat/Listing/ChannelListing.cs b/osu.Game/Overlays/Chat/Listing/ChannelListing.cs index 809ea2f11d..1699dcceb0 100644 --- a/osu.Game/Overlays/Chat/Listing/ChannelListing.cs +++ b/osu.Game/Overlays/Chat/Listing/ChannelListing.cs @@ -63,7 +63,7 @@ namespace osu.Game.Overlays.Chat.Listing flow.ChildrenEnumerable = newChannels.Where(c => c.Type == ChannelType.Public) .Select(c => new ChannelListingItem(c)); - foreach (var item in flow.Children) + foreach (var item in flow) { item.OnRequestJoin += channel => OnRequestJoin?.Invoke(channel); item.OnRequestLeave += channel => OnRequestLeave?.Invoke(channel); diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 87df08ceec..8f3b7031c2 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -11,11 +11,13 @@ 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; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; @@ -29,7 +31,7 @@ namespace osu.Game.Overlays { public partial class ChatOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent, IKeyBindingHandler { - public string IconTexture => "Icons/Hexacons/messaging"; + public IconUsage Icon => OsuIcon.Chat; public LocalisableString Title => ChatStrings.HeaderTitle; public LocalisableString Description => ChatStrings.HeaderDescription; @@ -55,6 +57,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!; @@ -248,10 +253,14 @@ namespace osu.Game.Overlays { } + protected override bool OnMouseDown(MouseDownEvent e) + { + isDraggingTopBar = topBar.DragBar.IsHovered; + return base.OnMouseDown(e); + } + protected override bool OnDragStart(DragStartEvent e) { - isDraggingTopBar = topBar.IsHovered; - if (!isDraggingTopBar) return base.OnDragStart(e); @@ -264,7 +273,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; } diff --git a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs index 278cef9112..51e1b863c7 100644 --- a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs +++ b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs @@ -2,8 +2,14 @@ // See the LICENCE file in the repository root for full licence text. 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 { @@ -16,6 +22,8 @@ namespace osu.Game.Overlays.Comments protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new CommentMarkdownHeading(headingBlock); + public override OsuMarkdownTextFlowContainer CreateTextFlow() => new CommentMarkdownTextFlowContainer(); + private partial class CommentMarkdownHeading : OsuMarkdownHeading { public CommentMarkdownHeading(HeadingBlock headingBlock) @@ -40,5 +48,64 @@ namespace osu.Game.Overlays.Comments } } } + + private partial class CommentMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer + { + protected override void AddImage(LinkInline linkInline) => AddDrawable(new CommentMarkdownImage(linkInline)); + + private partial class CommentMarkdownImage : OsuMarkdownImage + { + public CommentMarkdownImage(LinkInline linkInline) + : base(linkInline) + { + } + + 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 af5f4dd280..b4e9a80ff1 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -102,7 +102,7 @@ namespace osu.Game.Overlays.Comments Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 20 }, Children = new Drawable[] { - avatar = new UpdateableAvatar(api.LocalUser.Value) + avatar = new UpdateableAvatar(api.LocalUser.Value, isInteractive: false) { Size = new Vector2(50), CornerExponent = 2, diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index ba1c7ca8b2..ceae17aa5d 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -144,7 +144,7 @@ namespace osu.Game.Overlays.Comments Size = new Vector2(avatar_size), Children = new Drawable[] { - new UpdateableAvatar(Comment.User) + new UpdateableAvatar(Comment.User, showUserPanelOnHover: true) { Size = new Vector2(avatar_size), Masking = true, diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs similarity index 62% rename from osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs rename to osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index 5047992c8b..ee277ff538 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -20,6 +20,7 @@ using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; using osu.Game.Online.Spectator; using osu.Game.Resources.Localisation.Web; using osu.Game.Screens; @@ -30,19 +31,27 @@ using osuTK; namespace osu.Game.Overlays.Dashboard { - internal partial class CurrentlyPlayingDisplay : CompositeDrawable + internal partial class CurrentlyOnlineDisplay : CompositeDrawable { private const float search_textbox_height = 40; private const float padding = 10; private readonly IBindableList playingUsers = new BindableList(); + private readonly IBindableDictionary onlineUsers = new BindableDictionary(); + private readonly Dictionary userPanels = new Dictionary(); - private SearchContainer userFlow; + private SearchContainer userFlow; private BasicSearchTextBox searchTextBox; + [Resolved] + private IAPIProvider api { get; set; } + [Resolved] private SpectatorClient spectatorClient { get; set; } + [Resolved] + private MetadataClient metadataClient { get; set; } + [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { @@ -72,7 +81,7 @@ namespace osu.Game.Overlays.Dashboard PlaceholderText = HomeStrings.SearchPlaceholder, }, }, - userFlow = new SearchContainer + userFlow = new SearchContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -97,6 +106,9 @@ namespace osu.Game.Overlays.Dashboard { base.LoadComplete(); + onlineUsers.BindTo(metadataClient.UserStates); + onlineUsers.BindCollectionChanged(onUserUpdated, true); + playingUsers.BindTo(spectatorClient.PlayingUsers); playingUsers.BindCollectionChanged(onPlayingUsersChanged, true); } @@ -108,7 +120,70 @@ namespace osu.Game.Overlays.Dashboard searchTextBox.TakeFocus(); } - private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() => + private void onUserUpdated(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => + { + switch (e.Action) + { + case NotifyDictionaryChangedAction.Add: + Debug.Assert(e.NewItems != null); + + foreach (var kvp in e.NewItems) + { + int userId = kvp.Key; + + users.GetUserAsync(userId).ContinueWith(task => + { + APIUser user = task.GetResultSafely(); + + if (user == null) + return; + + Schedule(() => + { + // explicitly refetch the user's status. + // things may have changed in between the time of scheduling and the time of actual execution. + if (onlineUsers.TryGetValue(userId, out var updatedStatus)) + { + user.Activity.Value = updatedStatus.Activity; + user.Status.Value = updatedStatus.Status; + } + + userFlow.Add(userPanels[userId] = createUserPanel(user)); + }); + }); + } + + break; + + case NotifyDictionaryChangedAction.Replace: + Debug.Assert(e.NewItems != null); + + foreach (var kvp in e.NewItems) + { + if (userPanels.TryGetValue(kvp.Key, out var panel)) + { + panel.User.Activity.Value = kvp.Value.Activity; + panel.User.Status.Value = kvp.Value.Status; + } + } + + break; + + case NotifyDictionaryChangedAction.Remove: + Debug.Assert(e.OldItems != null); + + foreach (var kvp in e.OldItems) + { + int userId = kvp.Key; + if (userPanels.Remove(userId, out var userPanel)) + userPanel.Expire(); + } + + break; + } + }); + + private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { @@ -117,22 +192,8 @@ namespace osu.Game.Overlays.Dashboard foreach (int userId in e.NewItems) { - users.GetUserAsync(userId).ContinueWith(task => - { - var user = task.GetResultSafely(); - - if (user == null) - return; - - Schedule(() => - { - // user may no longer be playing. - if (!playingUsers.Contains(user.Id)) - return; - - userFlow.Add(createUserPanel(user)); - }); - }); + if (userPanels.TryGetValue(userId, out var panel)) + panel.CanSpectate.Value = userId != api.LocalUser.Value.Id; } break; @@ -141,22 +202,29 @@ namespace osu.Game.Overlays.Dashboard Debug.Assert(e.OldItems != null); foreach (int userId in e.OldItems) - userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire(); + { + if (userPanels.TryGetValue(userId, out var panel)) + panel.CanSpectate.Value = false; + } + break; } - }); + } - private PlayingUserPanel createUserPanel(APIUser user) => - new PlayingUserPanel(user).With(panel => + private OnlineUserPanel createUserPanel(APIUser user) => + new OnlineUserPanel(user).With(panel => { panel.Anchor = Anchor.TopCentre; panel.Origin = Anchor.TopCentre; + panel.CanSpectate.Value = playingUsers.Contains(user.Id); }); - public partial class PlayingUserPanel : CompositeDrawable, IFilterable + public partial class OnlineUserPanel : CompositeDrawable, IFilterable { public readonly APIUser User; + public BindableBool CanSpectate { get; } = new BindableBool(); + public IEnumerable FilterTerms { get; } [Resolved(canBeNull: true)] @@ -175,7 +243,7 @@ namespace osu.Game.Overlays.Dashboard } } - public PlayingUserPanel(APIUser user) + public OnlineUserPanel(APIUser user) { User = user; @@ -185,7 +253,7 @@ namespace osu.Game.Overlays.Dashboard } [BackgroundDependencyLoader] - private void load(IAPIProvider api) + private void load() { InternalChildren = new Drawable[] { @@ -202,6 +270,9 @@ namespace osu.Game.Overlays.Dashboard RelativeSizeAxes = Axes.X, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, + // this is SHOCKING + Activity = { BindTarget = User.Activity }, + Status = { BindTarget = User.Status }, }, new PurpleRoundedButton { @@ -209,8 +280,8 @@ namespace osu.Game.Overlays.Dashboard Text = "Spectate", Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Action = () => performer?.PerformFromScreen(s => s.Push(new SoloSpectator(User))), - Enabled = { Value = User.Id != api.LocalUser.Value.Id } + Action = () => performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(User))), + Enabled = { BindTarget = CanSpectate } } } }, diff --git a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs index 0f4697e33c..8fd8f6b332 100644 --- a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs +++ b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Resources.Localisation.Web; @@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Dashboard { Title = PageTitleStrings.MainHomeControllerIndex; Description = NamedOverlayComponentStrings.DashboardDescription; - IconTexture = "Icons/Hexacons/social"; + Icon = OsuIcon.Global; } } } @@ -28,7 +29,7 @@ namespace osu.Game.Overlays.Dashboard [LocalisableDescription(typeof(FriendsStrings), nameof(FriendsStrings.TitleCompact))] Friends, - [Description("Currently Playing")] + [Description("Currently online")] CurrentlyPlaying } } diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 2f96421531..1861f892bd 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -2,6 +2,11 @@ // 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.Containers; +using osu.Game.Online.Metadata; +using osu.Game.Online.Multiplayer; using osu.Game.Overlays.Dashboard; using osu.Game.Overlays.Dashboard.Friends; @@ -9,6 +14,11 @@ namespace osu.Game.Overlays { public partial class DashboardOverlay : TabbableOnlineOverlay { + [Resolved] + private MetadataClient metadataClient { get; set; } = null!; + + private IBindable metadataConnected = null!; + public DashboardOverlay() : base(OverlayColourScheme.Purple) { @@ -27,12 +37,33 @@ namespace osu.Game.Overlays break; case DashboardOverlayTabs.CurrentlyPlaying: - LoadDisplay(new CurrentlyPlayingDisplay()); + LoadDisplay(new CurrentlyOnlineDisplay()); break; default: throw new NotImplementedException($"Display for {tab} tab is not implemented"); } } + + protected override void LoadComplete() + { + base.LoadComplete(); + + metadataConnected = metadataClient.IsConnected.GetBoundCopy(); + metadataConnected.BindValueChanged(_ => updateUserPresenceState()); + State.BindValueChanged(_ => updateUserPresenceState()); + updateUserPresenceState(); + } + + private void updateUserPresenceState() + { + if (!metadataConnected.Value) + return; + + if (State.Value == Visibility.Visible) + metadataClient.BeginWatchingUserPresence().FireAndForget(); + else + metadataClient.EndWatchingUserPresence().FireAndForget(); + } } } diff --git a/osu.Game/Overlays/Dialog/DangerousActionDialog.cs b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs index c86570386f..42a3ff827c 100644 --- a/osu.Game/Overlays/Dialog/DangerousActionDialog.cs +++ b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs @@ -23,6 +23,11 @@ namespace osu.Game.Overlays.Dialog /// protected Action? DangerousAction { get; set; } + /// + /// The action to perform if cancelled. + /// + protected Action? CancelAction { get; set; } + protected DangerousActionDialog() { HeaderText = DeleteConfirmationDialogStrings.HeaderText; @@ -38,7 +43,8 @@ namespace osu.Game.Overlays.Dialog }, new PopupDialogCancelButton { - Text = DeleteConfirmationDialogStrings.Cancel + Text = DeleteConfirmationDialogStrings.Cancel, + Action = () => CancelAction?.Invoke() } }; } diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index 2e9087fdbd..4ac37a63e2 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -3,6 +3,9 @@ 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; @@ -11,6 +14,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; using osuTK; @@ -22,11 +26,13 @@ namespace osu.Game.Overlays.Dialog public abstract partial class PopupDialog : VisibilityContainer { public const float ENTER_DURATION = 500; - public const float EXIT_DURATION = 200; + public const float EXIT_DURATION = 500; private readonly Vector2 ringSize = new Vector2(100f); private readonly Vector2 ringMinifiedSize = new Vector2(20f); - private readonly Vector2 buttonsEnterSpacing = new Vector2(0f, 50f); + + private readonly Box flashLayer; + private Sample? flashSample; private readonly Container content; private readonly Container ring; @@ -102,13 +108,20 @@ namespace osu.Game.Overlays.Dialog protected PopupDialog() { - RelativeSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; Children = new Drawable[] { content = new Container { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Alpha = 0f, Children = new Drawable[] { @@ -116,11 +129,13 @@ namespace osu.Game.Overlays.Dialog { RelativeSizeAxes = Axes.Both, Masking = true, + CornerRadius = 20, + CornerExponent = 2.5f, EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(0.5f), - Radius = 8, + Colour = Color4.Black.Opacity(0.2f), + Radius = 14, }, Children = new Drawable[] { @@ -136,23 +151,29 @@ namespace osu.Game.Overlays.Dialog ColourDark = Color4Extensions.FromHex(@"1e171e"), TriangleScale = 4, }, + flashLayer = new Box + { + Alpha = 0, + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Colour = Color4Extensions.FromHex(@"221a21"), + }, }, }, new FillFlowContainer { - Anchor = Anchor.Centre, - Origin = Anchor.BottomCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(0f, 10f), - Padding = new MarginPadding { Bottom = 10 }, + Padding = new MarginPadding { Vertical = 60 }, Children = new Drawable[] { new Container { Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, + Padding = new MarginPadding { Bottom = 30 }, Size = ringSize, Children = new Drawable[] { @@ -175,6 +196,7 @@ namespace osu.Game.Overlays.Dialog Origin = Anchor.Centre, Anchor = Anchor.Centre, Icon = FontAwesome.Solid.TimesCircle, + Y = -2, Size = new Vector2(50), }, }, @@ -188,6 +210,7 @@ namespace osu.Game.Overlays.Dialog RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, TextAnchor = Anchor.TopCentre, + Padding = new MarginPadding { Horizontal = 5 }, }, body = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 18)) { @@ -196,18 +219,19 @@ namespace osu.Game.Overlays.Dialog TextAnchor = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(5), + Padding = new MarginPadding { Horizontal = 5 }, + }, + buttonsContainer = new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Top = 30 }, }, }, }, - buttonsContainer = new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - }, }, }, }; @@ -217,6 +241,12 @@ namespace osu.Game.Overlays.Dialog Show(); } + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuColour colours) + { + flashSample = audio.Samples.Get(@"UI/default-select-disabled"); + } + /// /// Programmatically clicks the first . /// @@ -232,6 +262,14 @@ namespace osu.Game.Overlays.Dialog 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) { if (e.Repeat) return false; @@ -261,15 +299,15 @@ namespace osu.Game.Overlays.Dialog // Reset various animations but only if the dialog animation fully completed if (content.Alpha == 0) { - buttonsContainer.TransformSpacingTo(buttonsEnterSpacing); - buttonsContainer.MoveToY(buttonsEnterSpacing.Y); + content.ScaleTo(0.7f); ring.ResizeTo(ringMinifiedSize); } - content.FadeIn(ENTER_DURATION, Easing.OutQuint); - ring.ResizeTo(ringSize, ENTER_DURATION, Easing.OutQuint); - buttonsContainer.TransformSpacingTo(Vector2.Zero, ENTER_DURATION, Easing.OutQuint); - buttonsContainer.MoveToY(0, ENTER_DURATION, Easing.OutQuint); + content + .ScaleTo(1, 750, Easing.OutElasticHalf) + .FadeIn(ENTER_DURATION, Easing.OutQuint); + + ring.ResizeTo(ringSize, ENTER_DURATION * 1.5f, Easing.OutQuint); } protected override void PopOut() @@ -279,7 +317,9 @@ namespace osu.Game.Overlays.Dialog // This is presumed to always be a sane default "cancel" action. buttonsContainer.Last().TriggerClick(); - content.FadeOut(EXIT_DURATION, Easing.InSine); + content + .ScaleTo(0.7f, EXIT_DURATION, Easing.Out) + .FadeOut(EXIT_DURATION, Easing.OutQuint); } private void pressButtonAtIndex(int index) diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index 005162bbcc..a85f1ecbcd 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -29,16 +29,18 @@ namespace osu.Game.Overlays public DialogOverlay() { - RelativeSizeAxes = Axes.Both; + AutoSizeAxes = Axes.Y; Child = dialogContainer = new Container { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, }; - Width = 0.4f; - Anchor = Anchor.BottomCentre; - Origin = Anchor.BottomCentre; + Width = 500; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; } [BackgroundDependencyLoader] 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/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index 23f3b3e1af..24ac5e72e8 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -244,6 +244,8 @@ namespace osu.Game.Overlays.FirstRunSetup [Resolved(canBeNull: true)] // Can't really be null but required to handle potential of disposal before DI completes. private OsuGameBase? game { get; set; } + private bool changingDirectory; + protected override void LoadComplete() { base.LoadComplete(); @@ -259,24 +261,37 @@ namespace osu.Game.Overlays.FirstRunSetup private void onDirectorySelected(ValueChangedEvent directory) { - if (directory.NewValue == null) - { - Current.Value = string.Empty; + if (changingDirectory) return; + + try + { + changingDirectory = true; + + if (directory.NewValue == null) + { + Current.Value = string.Empty; + return; + } + + // DirectorySelectors can trigger a noop value changed, but `DirectoryInfo` equality doesn't catch this. + if (directory.OldValue?.FullName == directory.NewValue.FullName) + return; + + if (legacyImportManager.IsUsableForStableImport(directory.NewValue, out var stableRoot)) + { + this.HidePopover(); + + string path = stableRoot.FullName; + + legacyImportManager.UpdateStorage(path); + Current.Value = path; + currentDirectory.Value = stableRoot; + } } - - // DirectorySelectors can trigger a noop value changed, but `DirectoryInfo` equality doesn't catch this. - if (directory.OldValue?.FullName == directory.NewValue.FullName) - return; - - if (directory.NewValue?.GetFiles(@"osu!.*.cfg").Any() ?? false) + finally { - this.HidePopover(); - - string path = directory.NewValue.FullName; - - legacyImportManager.UpdateStorage(path); - Current.Value = path; + changingDirectory = false; } } diff --git a/osu.Game/Overlays/FullscreenOverlay.cs b/osu.Game/Overlays/FullscreenOverlay.cs index 7ae5167081..6ddf1eecf0 100644 --- a/osu.Game/Overlays/FullscreenOverlay.cs +++ b/osu.Game/Overlays/FullscreenOverlay.cs @@ -7,6 +7,7 @@ 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.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Online.API; @@ -17,7 +18,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 IconUsage Icon => Header.Title.Icon; public virtual LocalisableString Title => Header.Title.Title; public virtual LocalisableString Description => Header.Title.Description; diff --git a/osu.Game/Overlays/INamedOverlayComponent.cs b/osu.Game/Overlays/INamedOverlayComponent.cs index 65664b12e7..ef3c029aac 100644 --- a/osu.Game/Overlays/INamedOverlayComponent.cs +++ b/osu.Game/Overlays/INamedOverlayComponent.cs @@ -1,13 +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.Framework.Graphics.Sprites; using osu.Framework.Localisation; namespace osu.Game.Overlays { public interface INamedOverlayComponent { - string IconTexture { get; } + IconUsage Icon { get; } LocalisableString Title { get; } diff --git a/osu.Game/Overlays/KudosuTable.cs b/osu.Game/Overlays/KudosuTable.cs new file mode 100644 index 0000000000..93884435a4 --- /dev/null +++ b/osu.Game/Overlays/KudosuTable.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.Collections.Generic; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays.Rankings.Tables; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Users; + +namespace osu.Game.Overlays +{ + public partial class KudosuTable : RankingsTable + { + public KudosuTable(int page, List users) + : base(page, users) + { + } + + protected override Drawable CreateRowBackground(APIUser item) + { + var background = base.CreateRowBackground(item); + + // see: https://github.com/ppy/osu-web/blob/9de00a0b874c56893d98261d558d78d76259d81b/resources/views/multiplayer/rooms/_rankings_table.blade.php#L23 + if (!item.Active) + background.Alpha = 0.5f; + + return background; + } + + protected override Drawable[] CreateRowContent(int index, APIUser item) + { + var content = base.CreateRowContent(index, item); + + // see: https://github.com/ppy/osu-web/blob/9de00a0b874c56893d98261d558d78d76259d81b/resources/views/multiplayer/rooms/_rankings_table.blade.php#L23 + if (!item.Active) + { + foreach (var d in content) + d.Alpha = 0.5f; + } + + return content; + } + + protected override RankingsTableColumn[] CreateAdditionalHeaders() + { + const int min_width = 120; + return new[] + { + new RankingsTableColumn(RankingsStrings.KudosuTotal, Anchor.Centre, new Dimension(GridSizeMode.AutoSize, minSize: min_width), true), + new RankingsTableColumn(RankingsStrings.KudosuAvailable, Anchor.Centre, new Dimension(GridSizeMode.AutoSize, minSize: min_width)), + new RankingsTableColumn(RankingsStrings.KudosuUsed, Anchor.Centre, new Dimension(GridSizeMode.AutoSize, minSize: min_width)), + }; + } + + protected override Drawable[] CreateAdditionalContent(APIUser item) + { + int kudosuTotal = item.Kudosu.Total; + int kudosuAvailable = item.Kudosu.Available; + return new Drawable[] + { + new RowText + { + Text = kudosuTotal.ToLocalisableString(@"N0") + }, + new ColouredRowText + { + Text = kudosuAvailable.ToLocalisableString(@"N0") + }, + new ColouredRowText + { + Text = (kudosuTotal - kudosuAvailable).ToLocalisableString(@"N0") + }, + }; + } + + protected override CountryCode GetCountryCode(APIUser item) => item.CountryCode; + + protected override Drawable CreateFlagContent(APIUser item) + { + var username = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: TEXT_SIZE, italics: true)) + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + TextAnchor = Anchor.CentreLeft + }; + username.AddUserLink(item); + return username; + } + } +} diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 71ecf2e75a..ea65656eb1 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -30,7 +30,6 @@ namespace osu.Game.Overlays.Login [Resolved] private OsuColour colours { get; set; } = null!; - private UserGridPanel panel = null!; private UserDropdown dropdown = null!; /// @@ -39,6 +38,7 @@ namespace osu.Game.Overlays.Login public Action? RequestHide; private readonly IBindable apiState = new Bindable(); + private readonly Bindable userStatus = new Bindable(); [Resolved] private IAPIProvider api { get; set; } = null!; @@ -131,7 +131,7 @@ namespace osu.Game.Overlays.Login Text = LoginPanelStrings.SignedIn, Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold), }, - panel = new UserGridPanel(api.LocalUser.Value) + new UserRankPanel(api.LocalUser.Value) { RelativeSizeAxes = Axes.X, Action = RequestHide @@ -140,25 +140,25 @@ namespace osu.Game.Overlays.Login }, }; - panel.Status.BindTo(api.LocalUser.Value.Status); - panel.Activity.BindTo(api.LocalUser.Value.Activity); + userStatus.BindTo(api.LocalUser.Value.Status); + userStatus.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true); dropdown.Current.BindValueChanged(action => { switch (action.NewValue) { case UserAction.Online: - api.LocalUser.Value.Status.Value = new UserStatusOnline(); + api.LocalUser.Value.Status.Value = UserStatus.Online; dropdown.StatusColour = colours.Green; break; case UserAction.DoNotDisturb: - api.LocalUser.Value.Status.Value = new UserStatusDoNotDisturb(); + api.LocalUser.Value.Status.Value = UserStatus.DoNotDisturb; dropdown.StatusColour = colours.Red; break; case UserAction.AppearOffline: - api.LocalUser.Value.Status.Value = new UserStatusOffline(); + api.LocalUser.Value.Status.Value = UserStatus.Offline; dropdown.StatusColour = colours.Gray7; break; @@ -174,6 +174,24 @@ namespace osu.Game.Overlays.Login ScheduleAfterChildren(() => GetContainingInputManager()?.ChangeFocus(form)); }); + private void updateDropdownCurrent(UserStatus? status) + { + switch (status) + { + case UserStatus.Online: + dropdown.Current.Value = UserAction.Online; + break; + + case UserStatus.DoNotDisturb: + dropdown.Current.Value = UserAction.DoNotDisturb; + break; + + case UserStatus.Offline: + dropdown.Current.Value = UserAction.AppearOffline; + break; + } + } + public override bool AcceptsFocus => true; protected override bool OnClick(ClickEvent e) => true; diff --git a/osu.Game/Overlays/LoginOverlay.cs b/osu.Game/Overlays/LoginOverlay.cs index a575253e71..c0aff6aae9 100644 --- a/osu.Game/Overlays/LoginOverlay.cs +++ b/osu.Game/Overlays/LoginOverlay.cs @@ -20,6 +20,8 @@ namespace osu.Game.Overlays private const float transition_time = 400; + protected override double PopInOutSampleBalance => OsuGameBase.SFX_STEREO_STRENGTH * 0.75f; + [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); 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 638592a9b5..b782b5d6ba 100644 --- a/osu.Game/Overlays/Mods/AddPresetPopover.cs +++ b/osu.Game/Overlays/Mods/AddPresetPopover.cs @@ -115,7 +115,7 @@ namespace osu.Game.Overlays.Mods { Name = nameTextBox.Current.Value, Description = descriptionTextBox.Current.Value, - Mods = selectedMods.Value.ToArray(), + Mods = selectedMods.Value.Where(mod => mod.Type != ModType.System).ToArray(), Ruleset = r.Find(ruleset.Value.ShortName)! })); diff --git a/osu.Game/Overlays/Mods/AdjustedAttributesTooltip.cs b/osu.Game/Overlays/Mods/AdjustedAttributesTooltip.cs new file mode 100644 index 0000000000..957ee23e3b --- /dev/null +++ b/osu.Game/Overlays/Mods/AdjustedAttributesTooltip.cs @@ -0,0 +1,142 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Overlays.Mods +{ + public partial class AdjustedAttributesTooltip : VisibilityContainer, ITooltip + { + private FillFlowContainer attributesFillFlow = null!; + + private Container content = null!; + + private Data? data; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + + Masking = true; + CornerRadius = 5; + + InternalChildren = new Drawable[] + { + content = new Container + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray3, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding { Vertical = 10, Horizontal = 15 }, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = "One or more values are being adjusted by mods that change speed.", + }, + attributesFillFlow = new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both + } + } + } + } + }, + }; + + updateDisplay(); + } + + private void updateDisplay() + { + attributesFillFlow.Clear(); + + if (data != null) + { + attemptAdd("CS", bd => bd.CircleSize); + attemptAdd("HP", bd => bd.DrainRate); + attemptAdd("OD", bd => bd.OverallDifficulty); + attemptAdd("AR", bd => bd.ApproachRate); + } + + if (attributesFillFlow.Any()) + content.Show(); + else + content.Hide(); + + void attemptAdd(string name, Func lookup) + { + double originalValue = lookup(data.OriginalDifficulty); + double adjustedValue = lookup(data.AdjustedDifficulty); + + if (!Precision.AlmostEquals(originalValue, adjustedValue)) + attributesFillFlow.Add(new AttributeDisplay(name, originalValue, adjustedValue)); + } + } + + public void SetContent(Data? data) + { + if (this.data == data) + return; + + this.data = data; + updateDisplay(); + } + + protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); + protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); + + public void Move(Vector2 pos) => Position = pos; + + public class Data + { + public BeatmapDifficulty OriginalDifficulty { get; } + public BeatmapDifficulty AdjustedDifficulty { get; } + + public Data(BeatmapDifficulty originalDifficulty, BeatmapDifficulty adjustedDifficulty) + { + OriginalDifficulty = originalDifficulty; + AdjustedDifficulty = adjustedDifficulty; + } + } + + private partial class AttributeDisplay : CompositeDrawable + { + public AttributeDisplay(string name, double original, double adjusted) + { + AutoSizeAxes = Axes.Both; + + InternalChild = new OsuSpriteText + { + Font = OsuFont.Default.With(weight: FontWeight.Bold), + Text = $"{name}: {original:0.0#} → {adjusted:0.0#}" + }; + } + } + } +} diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs new file mode 100644 index 0000000000..b9e4896b21 --- /dev/null +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -0,0 +1,212 @@ +// 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 System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osuTK; + +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, IHasCustomTooltip + { + 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!; + + [Resolved] + private OsuGameBase game { get; set; } = null!; + + private IBindable gameRuleset = null!; + + private CancellationTokenSource? cancellationSource; + private IBindable starDifficulty = null!; + + public ITooltip GetCustomTooltip() => new AdjustedAttributesTooltip(); + + public AdjustedAttributesTooltip.Data? TooltipContent { get; private set; } + + 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), }, + overallDifficultyDisplay = new VerticalAttributeDisplay("OD") { Shear = new Vector2(-shear, 0), }, + approachRateDisplay = new VerticalAttributeDisplay("AR") { 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(); + }); + + gameRuleset = game.Ruleset.GetBoundCopy(); + gameRuleset.BindValueChanged(_ => updateValues()); + + BeatmapInfo.BindValueChanged(_ => updateValues(), true); + + 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() + { + LeftContent.AutoSizeEasing = Content.AutoSizeEasing = Easing.OutQuint; + LeftContent.AutoSizeDuration = Content.AutoSizeDuration = transition_duration; + } + + 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 originalDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty); + + foreach (var mod in mods.Value.OfType()) + mod.ApplyToDifficulty(originalDifficulty); + + Ruleset ruleset = gameRuleset.Value.CreateInstance(); + BeatmapDifficulty adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(originalDifficulty, rate); + + TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, adjustedDifficulty); + + approachRateDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.ApproachRate, adjustedDifficulty.ApproachRate); + overallDifficultyDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.OverallDifficulty, adjustedDifficulty.OverallDifficulty); + + circleSizeDisplay.Current.Value = adjustedDifficulty.CircleSize; + drainRateDisplay.Current.Value = adjustedDifficulty.DrainRate; + approachRateDisplay.Current.Value = adjustedDifficulty.ApproachRate; + overallDifficultyDisplay.Current.Value = adjustedDifficulty.OverallDifficulty; + }); + + private void updateCollapsedState() + { + RightContent.FadeTo(Collapsed.Value && !IsHovered ? 0 : 1, transition_duration, Easing.OutQuint); + } + + private partial class BPMDisplay : RollingCounter + { + protected override double RollingDuration => 250; + + 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/DifficultyMultiplierDisplay.cs b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs deleted file mode 100644 index 9e16aa926f..0000000000 --- a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs +++ /dev/null @@ -1,41 +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.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 d755825a95..8bce57c96a 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -153,13 +153,13 @@ namespace osu.Game.Overlays.Mods private void useCurrentMods() { - saveableMods = selectedMods.Value.ToHashSet(); + saveableMods = selectedMods.Value.Where(mod => mod.Type != ModType.System).ToHashSet(); updateState(); } private void updateState() { - scrollContent.ChildrenEnumerable = saveableMods.Select(mod => new ModPresetRow(mod)); + scrollContent.ChildrenEnumerable = saveableMods.AsOrdered().Select(mod => new ModPresetRow(mod)); useCurrentModsButton.Enabled.Value = checkSelectedModsDiffersFromSaved(); } @@ -168,7 +168,7 @@ namespace osu.Game.Overlays.Mods if (!selectedMods.Value.Any()) return false; - return !saveableMods.SetEquals(selectedMods.Value); + return !saveableMods.SetEquals(selectedMods.Value.Where(mod => mod.Type != ModType.System)); } private void save() diff --git a/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs index 59a631a7b5..bf58efc339 100644 --- a/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs +++ b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Mods.Input { [Key.Q] = new[] { typeof(ModEasy) }, [Key.W] = new[] { typeof(ModNoFail) }, - [Key.E] = new[] { typeof(ModHalfTime) }, + [Key.E] = new[] { typeof(ModHalfTime), typeof(ModDaycore) }, [Key.A] = new[] { typeof(ModHardRock) }, [Key.S] = new[] { typeof(ModSuddenDeath), typeof(ModPerfect) }, [Key.D] = new[] { typeof(ModDoubleTime), typeof(ModNightcore) }, 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/ModPresetColumn.cs b/osu.Game/Overlays/Mods/ModPresetColumn.cs index 3b12eec195..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; @@ -65,7 +69,11 @@ namespace osu.Game.Overlays.Mods { 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 00f6e36972..3982abeba7 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.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 osu.Framework.Allocation; @@ -56,17 +55,14 @@ namespace osu.Game.Overlays.Mods protected override void Select() { - // if the preset is not active at the point of the user click, then set the mods using the preset directly, discarding any previous selections, - // which will also have the side effect of activating the preset (see `updateActiveState()`). - selectedMods.Value = Preset.Value.Mods.ToArray(); + var selectedSystemMods = selectedMods.Value.Where(mod => mod.Type == ModType.System); + // will also have the side effect of activating the preset (see `updateActiveState()`). + selectedMods.Value = Preset.Value.Mods.Concat(selectedSystemMods).ToArray(); } protected override void Deselect() { - // if the preset is active when the user has clicked it, then it means that the set of active mods is exactly equal to the set of mods in the preset - // (there are no other active mods than what the preset specifies, and the mod settings match exactly). - // therefore it's safe to just clear selected mods, since it will have the effect of toggling the preset off. - selectedMods.Value = Array.Empty(); + selectedMods.Value = selectedMods.Value.Except(Preset.Value.Mods).ToArray(); } private void selectedModsChanged() @@ -79,7 +75,7 @@ namespace osu.Game.Overlays.Mods private void updateActiveState() { - Active.Value = new HashSet(Preset.Value.Mods).SetEquals(selectedMods.Value); + Active.Value = new HashSet(Preset.Value.Mods).SetEquals(selectedMods.Value.Where(mod => mod.Type != ModType.System)); } #region Filtering support 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/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index 338ebdaef4..b2c5a054e1 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Mods var hsv = new Colour4(value.R, value.G, value.B, 1f).ToHSV(); var trianglesColour = Colour4.FromHSV(hsv.X, hsv.Y + 0.2f, hsv.Z - 0.1f); - triangles.Colour = ColourInfo.GradientVertical(trianglesColour, trianglesColour.MultiplyAlpha(0f)); + triangles.Colour = ColourInfo.GradientVertical(trianglesColour, value); } } @@ -61,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); @@ -93,6 +95,7 @@ namespace osu.Game.Overlays.Mods Height = header_height, Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Velocity = 0.7f, + ClampAxes = Axes.Y }, headerText = new OsuTextFlowContainer(t => { diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 9e92e9d959..7271c53e7a 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -17,6 +17,7 @@ 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; @@ -77,9 +78,9 @@ namespace osu.Game.Overlays.Mods 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. @@ -114,29 +115,49 @@ namespace osu.Game.Overlays.Mods public IEnumerable AllAvailableMods => AvailableMods.Value.SelectMany(pair => pair.Value); private readonly BindableBool customisationVisible = new BindableBool(); + private Bindable textSearchStartsActive = null!; private ModSettingsArea modSettingsArea = null!; private ColumnScrollContainer columnScroll = null!; private ColumnFlowContainer columnFlow = null!; private FillFlowContainer footerButtonFlow = null!; + private FillFlowContainer footerContentFlow = null!; private DeselectAllModsButton deselectAllModsButton = null!; private Container aboveColumnsContent = null!; - private DifficultyMultiplierDisplay? multiplierDisplay; + 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 bool textBoxShouldFocus; + 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) { } [BackgroundDependencyLoader] - private void load(OsuGameBase game, OsuColour colours, AudioManager audio) + private void load(OsuGameBase game, OsuColour colours, AudioManager audio, OsuConfigManager configManager) { Header.Title = ModSelectOverlayStrings.ModSelectTitle; Header.Description = ModSelectOverlayStrings.ModSelectDescription; @@ -164,7 +185,7 @@ namespace osu.Game.Overlays.Mods aboveColumnsContent = new Container { RelativeSizeAxes = Axes.X, - Height = ModsEffectDisplay.HEIGHT, + Height = ScoreMultiplierDisplay.HEIGHT, Padding = new MarginPadding { Horizontal = 100 }, Child = SearchTextBox = new ShearedSearchTextBox { @@ -179,7 +200,7 @@ namespace osu.Game.Overlays.Mods { Padding = new MarginPadding { - Top = ModsEffectDisplay.HEIGHT + PADDING, + Top = ScoreMultiplierDisplay.HEIGHT + PADDING, Bottom = PADDING }, RelativeSizeAxes = Axes.Both, @@ -210,16 +231,7 @@ namespace osu.Game.Overlays.Mods } }); - if (ShowTotalMultiplier) - { - aboveColumnsContent.Add(multiplierDisplay = new DifficultyMultiplierDisplay - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight - }); - } - - FooterContent.Child = footerButtonFlow = new FillFlowContainer + FooterContent.Add(footerButtonFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -239,9 +251,42 @@ 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); + + textSearchStartsActive = configManager.GetBindable(OsuSetting.ModSelectTextSearchStartsActive); } public override void Hide() @@ -309,6 +354,25 @@ namespace osu.Game.Overlays.Mods 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; + } } /// @@ -383,7 +447,7 @@ namespace osu.Game.Overlays.Mods private void filterMods() { foreach (var modState in AllAvailableMods) - modState.ValidForSelection.Value = modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod); + modState.ValidForSelection.Value = modState.Mod.Type != ModType.System && modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod); } private void updateMultiplier() @@ -446,6 +510,11 @@ namespace osu.Game.Overlays.Mods modSettingsArea.ResizeHeightTo(modAreaHeight, transition_duration, Easing.InOutCubic); TopLevelContent.MoveToY(-modAreaHeight, transition_duration, Easing.InOutCubic); + + if (customisationVisible.Value) + SearchTextBox.KillFocus(); + else + setTextBoxFocus(textBoxShouldFocus); } /// @@ -545,7 +614,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); @@ -558,6 +627,8 @@ namespace osu.Game.Overlays.Mods nonFilteredColumnCount += 1; } + + setTextBoxFocus(textSearchStartsActive.Value); } protected override void PopOut() @@ -648,7 +719,10 @@ namespace osu.Game.Overlays.Mods 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; + SearchTextBox.SelectAll(); + } return true; } @@ -696,14 +770,20 @@ namespace osu.Game.Overlays.Mods return false; // TODO: should probably eventually support typical platform search shortcuts (`Ctrl-F`, `/`) - if (SearchTextBox.HasFocus) - SearchTextBox.KillFocus(); - else - SearchTextBox.TakeFocus(); - + setTextBoxFocus(!textBoxShouldFocus); return true; } + private void setTextBoxFocus(bool keepFocus) + { + textBoxShouldFocus = keepFocus; + + if (textBoxShouldFocus) + SearchTextBox.TakeFocus(); + else + SearchTextBox.KillFocus(); + } + #endregion #region Sample playback control @@ -886,6 +966,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/ModSettingsArea.cs b/osu.Game/Overlays/Mods/ModSettingsArea.cs index fe1d683d59..54bfcc7199 100644 --- a/osu.Game/Overlays/Mods/ModSettingsArea.cs +++ b/osu.Game/Overlays/Mods/ModSettingsArea.cs @@ -32,6 +32,8 @@ namespace osu.Game.Overlays.Mods [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + public override bool AcceptsFocus => true; + public ModSettingsArea() { RelativeSizeAxes = Axes.X; @@ -84,7 +86,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/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..a86eba81e4 --- /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.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 osu.Game.Utils; +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 => 250; + + protected override LocalisableString FormatCount(double count) => ModUtils.FormatScoreMultiplier(count); + + 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 bb61cdc35d..1da762d164 100644 --- a/osu.Game/Overlays/Mods/SelectAllModsButton.cs +++ b/osu.Game/Overlays/Mods/SelectAllModsButton.cs @@ -42,6 +42,7 @@ namespace osu.Game.Overlays.Mods { Enabled.Value = availableMods.Value .SelectMany(pair => pair.Value) + .Where(modState => modState.ValidForSelection.Value) .Any(modState => !modState.Active.Value && modState.Visible); } } diff --git a/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs b/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs new file mode 100644 index 0000000000..a3e24b486f --- /dev/null +++ b/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs @@ -0,0 +1,137 @@ +// 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.UserInterface; +using osu.Framework.Localisation; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Mods; +using osuTK.Graphics; + +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(); + + public Bindable AdjustType = new Bindable(); + + /// + /// Text to display in the top area of the display. + /// + public LocalisableString Label { get; protected set; } + + private readonly EffectCounter counter; + private readonly OsuSpriteText text; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + private void updateTextColor() + { + Color4 newColor; + + switch (AdjustType.Value) + { + case ModEffect.NotChanged: + newColor = Color4.White; + break; + + case ModEffect.DifficultyReduction: + newColor = colours.ForModType(ModType.DifficultyReduction); + break; + + case ModEffect.DifficultyIncrease: + newColor = colours.ForModType(ModType.DifficultyIncrease); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(AdjustType.Value)); + } + + text.Colour = newColor; + counter.Colour = newColor; + } + + public VerticalAttributeDisplay(LocalisableString label) + { + Label = label; + + AutoSizeAxes = Axes.X; + + Origin = Anchor.CentreLeft; + Anchor = Anchor.CentreLeft; + + AdjustType.BindValueChanged(_ => updateTextColor()); + + InternalChild = new FillFlowContainer + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + AutoSizeAxes = Axes.Y, + Width = 50, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + text = 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) + }, + counter = new EffectCounter + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Current = { BindTarget = Current }, + } + } + }; + } + + public static ModEffect CalculateEffect(double oldValue, double newValue) + { + if (Precision.AlmostEquals(newValue, oldValue, 0.01)) + return ModEffect.NotChanged; + if (newValue < oldValue) + return ModEffect.DifficultyReduction; + + return ModEffect.DifficultyIncrease; + } + + public enum ModEffect + { + NotChanged, + DifficultyReduction, + DifficultyIncrease, + } + + private partial class EffectCounter : RollingCounter + { + protected override double RollingDuration => 250; + + 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/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/NewsHeader.cs b/osu.Game/Overlays/News/NewsHeader.cs index 44e2f6a8cb..92d71a21ef 100644 --- a/osu.Game/Overlays/News/NewsHeader.cs +++ b/osu.Game/Overlays/News/NewsHeader.cs @@ -7,6 +7,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Resources.Localisation.Web; @@ -68,7 +69,7 @@ namespace osu.Game.Overlays.News { Title = PageTitleStrings.MainNewsControllerDefault; Description = NamedOverlayComponentStrings.NewsDescription; - IconTexture = "Icons/Hexacons/news"; + Icon = OsuIcon.News; } } } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index c9d09848f8..18a487a312 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -13,9 +13,11 @@ 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.Localisation; using osu.Framework.Logging; using osu.Framework.Threading; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Overlays.Notifications; using osu.Game.Resources.Localisation.Web; @@ -27,10 +29,12 @@ namespace osu.Game.Overlays { public partial class NotificationOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent, INotificationOverlay { - public string IconTexture => "Icons/Hexacons/notification"; + public IconUsage Icon => OsuIcon.Notification; 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; @@ -43,6 +47,9 @@ namespace osu.Game.Overlays [Resolved] private AudioManager audio { get; set; } = null!; + [Resolved] + private OsuGame? game { get; set; } + [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); @@ -108,7 +115,8 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.X, Children = new[] { - new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }), + // 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) }), } } @@ -176,6 +184,12 @@ namespace osu.Game.Overlays playDebouncedSample(notification.PopInSampleName); + if (notification.IsImportant) + { + game?.Window?.Flash(); + notification.Closed += () => game?.Window?.CancelFlash(); + } + if (State.Value == Visibility.Hidden) { notification.IsInToastTray = true; @@ -194,7 +208,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); @@ -212,7 +227,7 @@ namespace osu.Game.Overlays protected override void PopIn() { this.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint); - mainContent.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint); + mainContent.FadeTo(1, TRANSITION_LENGTH / 2, Easing.OutQuint); mainContent.FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out); toastTray.FlushAllToasts(); @@ -225,7 +240,7 @@ namespace osu.Game.Overlays markAllRead(); this.MoveToX(WIDTH, TRANSITION_LENGTH, Easing.OutQuint); - mainContent.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint); + mainContent.FadeTo(0, TRANSITION_LENGTH / 2, Easing.OutQuint); mainContent.FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.In); } diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 8cdc373417..d48524d8b0 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -53,13 +53,15 @@ namespace osu.Game.Overlays.Notifications public virtual string PopInSampleName => "UI/notification-default"; public virtual string PopOutSampleName => "UI/overlay-pop-out"; + protected const float CORNER_RADIUS = 6; + protected NotificationLight Light; protected Container IconContent; public bool WasClosed { get; private set; } - private readonly Container content; + private readonly FillFlowContainer content; protected override Container Content => content; @@ -128,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, @@ -166,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) }, } }, @@ -471,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 10c2900d63..895f13ee9c 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -37,13 +37,17 @@ namespace osu.Game.Overlays.Notifications notifications.Insert((int)position, notification); } - public IEnumerable AcceptedNotificationTypes { get; } + /// + /// 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) + public NotificationSection(LocalisableString title, IEnumerable? acceptedNotificationTypes = null) { - AcceptedNotificationTypes = acceptedNotificationTypes.ToArray(); + AcceptedNotificationTypes = acceptedNotificationTypes?.ToArray(); titleText = title; } diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 6ea032213e..2362cb11f6 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Notifications set { text = value; - Schedule(() => textDrawable.Text = text); + Scheduler.AddOnce(t => textDrawable.Text = t, text); } } 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 6abde713b6..ab99370603 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays { public partial class NowPlayingOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent { - public string IconTexture => "Icons/Hexacons/music"; + public IconUsage Icon => OsuIcon.Music; public LocalisableString Title => NowPlayingStrings.HeaderTitle; public LocalisableString Description => NowPlayingStrings.HeaderDescription; @@ -55,8 +55,7 @@ namespace osu.Game.Overlays 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; } = null!; @@ -248,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; @@ -406,6 +405,8 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(150), FillMode = FillMode.Fill, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, }, new Box { 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/OverlayStreamControl.cs b/osu.Game/Overlays/OverlayStreamControl.cs index 84de384fb5..bc37a57cab 100644 --- a/osu.Game/Overlays/OverlayStreamControl.cs +++ b/osu.Game/Overlays/OverlayStreamControl.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays protected override bool OnHover(HoverEvent e) { - foreach (var streamBadge in TabContainer.Children.OfType>()) + foreach (var streamBadge in TabContainer.OfType>()) streamBadge.UserHoveringArea = true; return base.OnHover(e); @@ -49,7 +49,7 @@ namespace osu.Game.Overlays protected override void OnHoverLost(HoverLostEvent e) { - foreach (var streamBadge in TabContainer.Children.OfType>()) + foreach (var streamBadge in TabContainer.OfType>()) streamBadge.UserHoveringArea = false; base.OnHoverLost(e); diff --git a/osu.Game/Overlays/OverlayTitle.cs b/osu.Game/Overlays/OverlayTitle.cs index 1d207e5f7d..a2ff7032b5 100644 --- a/osu.Game/Overlays/OverlayTitle.cs +++ b/osu.Game/Overlays/OverlayTitle.cs @@ -1,13 +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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -20,7 +16,7 @@ namespace osu.Game.Overlays public const float ICON_SIZE = 30; private readonly OsuSpriteText titleText; - private readonly Container icon; + private readonly Container iconContainer; private LocalisableString title; @@ -32,12 +28,20 @@ namespace osu.Game.Overlays public LocalisableString Description { get; protected set; } - private string iconTexture; + private IconUsage icon; - public string IconTexture + public IconUsage Icon { - get => iconTexture; - protected set => icon.Child = new OverlayTitleIcon(iconTexture = value); + get => icon; + protected set => iconContainer.Child = new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fit, + + Icon = icon = value, + }; } protected OverlayTitle() @@ -51,7 +55,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Horizontal, Children = new Drawable[] { - icon = new Container + iconContainer = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -68,26 +72,5 @@ namespace osu.Game.Overlays } }; } - - private partial class OverlayTitleIcon : Sprite - { - private readonly string textureName; - - public OverlayTitleIcon(string textureName) - { - this.textureName = textureName; - - RelativeSizeAxes = Axes.Both; - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - FillMode = FillMode.Fit; - } - - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - Texture = textures.Get(textureName); - } - } } } 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/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs index 08a816930e..85751e7457 100644 --- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs @@ -145,7 +145,7 @@ namespace osu.Game.Overlays.Profile.Header bool anyInfoAdded = false; anyInfoAdded |= tryAddInfo(FontAwesome.Solid.MapMarker, user.Location); - anyInfoAdded |= tryAddInfo(OsuIcon.Heart, user.Interests); + anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Heart, user.Interests); anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Suitcase, user.Occupation); if (anyInfoAdded) 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..c9e5068b2a 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() @@ -225,6 +232,14 @@ namespace osu.Game.Overlays.Profile.Header bool expanded = coverToggle.CoverExpanded.Value; cover.ResizeHeightTo(expanded ? 250 : 0, transition_duration, Easing.OutQuint); + + // Without this a very tiny slither of the cover will be visible even with a size of zero. + // Integer masking woes, no doubt. + if (expanded) + cover.FadeIn(transition_duration, Easing.OutQuint); + else + cover.FadeOut(transition_duration, Easing.InQuint); + avatar.ResizeTo(new Vector2(expanded ? 120 : content_height), transition_duration, Easing.OutQuint); avatar.TransformTo(nameof(avatar.CornerRadius), expanded ? 40f : 20f, transition_duration, Easing.OutQuint); flow.TransformTo(nameof(flow.Spacing), new Vector2(expanded ? 20f : 10f), transition_duration, Easing.OutQuint); diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 80d48ae09e..42bec50022 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Overlays.Profile.Header; using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Resources.Localisation.Web; @@ -86,7 +87,7 @@ namespace osu.Game.Overlays.Profile public ProfileHeaderTitle() { Title = PageTitleStrings.MainUsersControllerDefault; - IconTexture = "Icons/Hexacons/profile"; + Icon = OsuIcon.Player; } } } 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/Rankings/RankingsOverlayHeader.cs b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs index 44f278a237..a23ec18afe 100644 --- a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs +++ b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Game.Localisation; using osu.Game.Resources.Localisation.Web; using osu.Framework.Graphics; +using osu.Game.Graphics; using osu.Game.Rulesets; using osu.Game.Users; @@ -35,7 +36,7 @@ namespace osu.Game.Overlays.Rankings { Title = PageTitleStrings.MainRankingControllerDefault; Description = NamedOverlayComponentStrings.RankingsDescription; - IconTexture = "Icons/Hexacons/rankings"; + Icon = OsuIcon.Ranking; } } } diff --git a/osu.Game/Overlays/Rankings/RankingsScope.cs b/osu.Game/Overlays/Rankings/RankingsScope.cs index 3392db9360..356a861764 100644 --- a/osu.Game/Overlays/Rankings/RankingsScope.cs +++ b/osu.Game/Overlays/Rankings/RankingsScope.cs @@ -18,6 +18,9 @@ namespace osu.Game.Overlays.Rankings Score, [LocalisableDescription(typeof(RankingsStrings), nameof(RankingsStrings.TypeCountry))] - Country + Country, + + [LocalisableDescription(typeof(RankingsStrings), nameof(RankingsStrings.TypeKudosu))] + Kudosu, } } diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs index f25bf80b6a..6a32515cbc 100644 --- a/osu.Game/Overlays/RankingsOverlay.cs +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -135,6 +135,9 @@ namespace osu.Game.Overlays case RankingsScope.Score: return new GetUserRankingsRequest(ruleset.Value, UserRankingsType.Score); + + case RankingsScope.Kudosu: + return new GetKudosuRankingsRequest(); } return null; @@ -166,6 +169,12 @@ namespace osu.Game.Overlays return new CountriesTable(1, countryRequest.Response.Countries); } + + case GetKudosuRankingsRequest kudosuRequest: + if (kudosuRequest.Response == null) + return null; + + return new KudosuTable(1, kudosuRequest.Response.Users); } return null; diff --git a/osu.Game/Overlays/RevertToDefaultButton.cs b/osu.Game/Overlays/RevertToDefaultButton.cs index c43d233fc1..6fa5209f64 100644 --- a/osu.Game/Overlays/RevertToDefaultButton.cs +++ b/osu.Game/Overlays/RevertToDefaultButton.cs @@ -13,7 +13,6 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osuTK; @@ -56,11 +55,6 @@ namespace osu.Game.Overlays } } - public RevertToDefaultButton() - : base(HoverSampleSet.Button) - { - } - [BackgroundDependencyLoader] private void load() { @@ -121,7 +115,12 @@ namespace osu.Game.Overlays Enabled.Value = !current.Disabled; - this.FadeTo(current.Disabled ? 0.2f : (current.IsDefault ? 0 : 1), fade_duration, Easing.OutQuint); + if (current.IsDefault) + this.FadeTo(0, fade_duration, Easing.OutQuint); + else if (current.Disabled) + this.FadeTo(0.2f, fade_duration, Easing.OutQuint); + else + this.FadeTo(1, fade_duration, Easing.OutQuint); if (IsHovered && Enabled.Value) { diff --git a/osu.Game/Overlays/Settings/DangerousSettingsButton.cs b/osu.Game/Overlays/Settings/DangerousSettingsButton.cs index 42b042ae75..9a9d40cada 100644 --- a/osu.Game/Overlays/Settings/DangerousSettingsButton.cs +++ b/osu.Game/Overlays/Settings/DangerousSettingsButton.cs @@ -14,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/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/Sections/Audio/AudioOffsetAdjustControl.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs new file mode 100644 index 0000000000..ef1691534f --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.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 System; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Linq; +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.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Screens.Play.PlayerSettings; +using osuTK; + +namespace osu.Game.Overlays.Settings.Sections.Audio +{ + public partial class AudioOffsetAdjustControl : SettingsItem + { + public IBindable SuggestedOffset => ((AudioOffsetPreview)Control).SuggestedOffset; + + [BackgroundDependencyLoader] + private void load() + { + LabelText = AudioSettingsStrings.AudioOffset; + } + + protected override Drawable CreateControl() => new AudioOffsetPreview(); + + private partial class AudioOffsetPreview : CompositeDrawable, IHasCurrentValue + { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(); + + private readonly IBindableList averageHitErrorHistory = new BindableList(); + + public readonly Bindable SuggestedOffset = new Bindable(); + + private Container notchContainer = null!; + private TextFlowContainer hintText = null!; + private RoundedButton applySuggestion = null!; + + [BackgroundDependencyLoader] + private void load(SessionAverageHitErrorTracker hitErrorTracker) + { + averageHitErrorHistory.BindTo(hitErrorTracker.AverageHitErrorHistory); + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(10), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OffsetSliderBar + { + RelativeSizeAxes = Axes.X, + Current = { BindTarget = Current }, + KeyboardStep = 1, + }, + notchContainer = new Container + { + RelativeSizeAxes = Axes.X, + Height = 10, + Padding = new MarginPadding { Horizontal = Nub.DEFAULT_EXPANDED_SIZE / 2 }, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + hintText = new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 16)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + applySuggestion = new RoundedButton + { + RelativeSizeAxes = Axes.X, + Text = "Apply suggested offset", + Action = () => + { + if (SuggestedOffset.Value.HasValue) + current.Value = SuggestedOffset.Value.Value; + hitErrorTracker.ClearHistory(); + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + averageHitErrorHistory.BindCollectionChanged(updateDisplay, true); + SuggestedOffset.BindValueChanged(_ => updateHintText(), true); + } + + private void updateDisplay(object? _, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (SessionAverageHitErrorTracker.DataPoint dataPoint in e.NewItems!) + { + notchContainer.ForEach(n => n.Alpha *= 0.95f); + notchContainer.Add(new Box + { + RelativeSizeAxes = Axes.Y, + Width = 2, + RelativePositionAxes = Axes.X, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + X = getXPositionForOffset(dataPoint.SuggestedGlobalAudioOffset) + }); + } + + break; + + case NotifyCollectionChangedAction.Remove: + foreach (SessionAverageHitErrorTracker.DataPoint dataPoint in e.OldItems!) + { + var notch = notchContainer.FirstOrDefault(n => n.X == getXPositionForOffset(dataPoint.SuggestedGlobalAudioOffset)); + Debug.Assert(notch != null); + notchContainer.Remove(notch, true); + } + + break; + + case NotifyCollectionChangedAction.Reset: + notchContainer.Clear(); + break; + } + + SuggestedOffset.Value = averageHitErrorHistory.Any() ? averageHitErrorHistory.Average(dataPoint => dataPoint.SuggestedGlobalAudioOffset) : null; + } + + private float getXPositionForOffset(double offset) => (float)(Math.Clamp(offset, current.MinValue, current.MaxValue) / (2 * current.MaxValue)); + + private void updateHintText() + { + hintText.Text = SuggestedOffset.Value == null + ? @"Play a few beatmaps to receive a suggested offset!" + : $@"Based on the last {averageHitErrorHistory.Count} play(s), the suggested offset is {SuggestedOffset.Value:N0} ms."; + applySuggestion.Enabled.Value = SuggestedOffset.Value != null; + } + + private partial class OffsetSliderBar : RoundedSliderBar + { + public override LocalisableString TooltipText => BeatmapOffsetControl.GetOffsetExplanatoryText(Current.Value); + } + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs index 6b5c769853..e05d20a5db 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs @@ -7,7 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Configuration; -using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.Audio @@ -16,23 +15,17 @@ 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", "delay", "latency" }); + public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "universal", "uo", "timing", "delay", "latency", "wizard" }); [BackgroundDependencyLoader] private void load(OsuConfigManager config) { Children = new Drawable[] { - new SettingsSlider + new AudioOffsetAdjustControl { - LabelText = AudioSettingsStrings.AudioOffset, Current = config.GetBindable(OsuSetting.AudioOffset), - KeyboardStep = 1f }, - new SettingsButton - { - Text = AudioSettingsStrings.OffsetWizard - } }; } } diff --git a/osu.Game/Overlays/Settings/Sections/AudioSection.cs b/osu.Game/Overlays/Settings/Sections/AudioSection.cs index fb3d486776..1ab0d6c886 100644 --- a/osu.Game/Overlays/Settings/Sections/AudioSection.cs +++ b/osu.Game/Overlays/Settings/Sections/AudioSection.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Localisation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Audio; @@ -17,7 +18,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Solid.VolumeUp + Icon = OsuIcon.Audio }; public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "sound" }); diff --git a/osu.Game/Overlays/Settings/Sections/DebugSection.cs b/osu.Game/Overlays/Settings/Sections/DebugSection.cs index 33a6f4c673..b84c441057 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSection.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSection.cs @@ -5,6 +5,7 @@ using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.DebugSettings; @@ -16,7 +17,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Solid.Bug + Icon = OsuIcon.Debug }; public DebugSection() diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs index cf97743fde..049ccedf37 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs @@ -15,7 +15,7 @@ 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] private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig, IPerformFromScreenRunner? performer) diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs index 9efdfa9955..69566d85f4 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs @@ -30,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) }, @@ -47,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 c7f5aa5665..83e9140b33 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -12,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/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index b60689b611..463b3d1d09 100644 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Gameplay; @@ -15,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Regular.DotCircle + Icon = OsuIcon.GameplayC }; public GameplaySection() diff --git a/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs index cf7f63211e..2af6e36b7f 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs @@ -23,6 +23,7 @@ namespace osu.Game.Overlays.Settings.Sections.General { LabelText = GeneralSettingsStrings.LanguageDropdown, Current = game.CurrentLanguage, + AlwaysShowSearchBar = true, }, new SettingsCheckbox { diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 2f68b3a82f..3ff5556f4d 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.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.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Configuration; @@ -16,23 +15,27 @@ using osu.Game.Localisation; using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Settings.Sections.Maintenance; using osu.Game.Updater; +using SharpCompress.Archives.Zip; namespace osu.Game.Overlays.Settings.Sections.General { public partial class UpdateSettings : SettingsSubsection { - [Resolved(CanBeNull = true)] - private UpdateManager updateManager { get; set; } - protected override LocalisableString Header => GeneralSettingsStrings.UpdateHeader; - private SettingsButton checkForUpdatesButton; + private SettingsButton checkForUpdatesButton = null!; - [Resolved(CanBeNull = true)] - private INotificationOverlay notifications { get; set; } + [Resolved] + private UpdateManager? updateManager { get; set; } - [BackgroundDependencyLoader(true)] - private void load(Storage storage, OsuConfigManager config, OsuGame game) + [Resolved] + private INotificationOverlay? notifications { get; set; } + + [Resolved] + private Storage storage { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config, OsuGame? game) { Add(new SettingsEnumDropdown { @@ -54,7 +57,7 @@ namespace osu.Game.Overlays.Settings.Sections.General { notifications?.Post(new SimpleNotification { - Text = GeneralSettingsStrings.RunningLatestRelease(game.Version), + Text = GeneralSettingsStrings.RunningLatestRelease(game!.Version), Icon = FontAwesome.Solid.CheckCircle, }); } @@ -74,6 +77,13 @@ namespace osu.Game.Overlays.Settings.Sections.General Action = () => storage.PresentExternally(), }); + Add(new SettingsButton + { + Text = GeneralSettingsStrings.ExportLogs, + Keywords = new[] { @"bug", "report", "logs", "files" }, + Action = () => Task.Run(exportLogs), + }); + Add(new SettingsButton { Text = GeneralSettingsStrings.ChangeFolderLocation, @@ -81,5 +91,44 @@ namespace osu.Game.Overlays.Settings.Sections.General }); } } + + private void exportLogs() + { + ProgressNotification notification = new ProgressNotification + { + State = ProgressNotificationState.Active, + Text = "Exporting logs...", + }; + + notifications?.Post(notification); + + const string archive_filename = "exports/compressed-logs.zip"; + + try + { + var logStorage = Logger.Storage; + + using (var outStream = storage.CreateFileSafely(archive_filename)) + using (var zip = ZipArchive.Create()) + { + foreach (string? f in logStorage.GetFiles(string.Empty, "*.log")) zip.AddEntry(f, logStorage.GetStream(f), true); + + zip.SaveTo(outStream); + } + } + catch + { + notification.State = ProgressNotificationState.Cancelled; + + // cleanup if export is failed or canceled. + storage.Delete(archive_filename); + throw; + } + + notification.CompletionText = "Exported logs! Click to view."; + notification.CompletionClickAction = () => storage.PresentFileExternally(archive_filename); + + notification.State = ProgressNotificationState.Completed; + } } } diff --git a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs index f4a79d65e6..2aa1008b1d 100644 --- a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs @@ -19,11 +19,11 @@ 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 { - Icon = FontAwesome.Solid.Cog + Icon = OsuIcon.Settings }; [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index a3290bc81c..71afec88d4 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -51,6 +51,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private SettingsDropdown resolutionDropdown = null!; private SettingsDropdown displayDropdown = null!; private SettingsDropdown windowModeDropdown = null!; + private SettingsCheckbox minimiseOnFocusLossCheckbox = null!; private SettingsCheckbox safeAreaConsiderationsCheckbox = null!; private Bindable scalingPositionX = null!; @@ -106,6 +107,12 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics ItemSource = resolutions, Current = sizeFullscreen }, + minimiseOnFocusLossCheckbox = new SettingsCheckbox + { + LabelText = GraphicsSettingsStrings.MinimiseOnFocusLoss, + Current = config.GetBindable(FrameworkSetting.MinimiseOnFocusLossInFullscreen), + Keywords = new[] { "alt-tab", "minimize", "focus", "hide" }, + }, safeAreaConsiderationsCheckbox = new SettingsCheckbox { LabelText = "Shrink game to avoid cameras and notches", @@ -255,6 +262,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics { resolutionDropdown.CanBeShown.Value = resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen; displayDropdown.CanBeShown.Value = displayDropdown.Items.Count() > 1; + minimiseOnFocusLossCheckbox.CanBeShown.Value = RuntimeInfo.IsDesktop && windowModeDropdown.Current.Value == WindowMode.Fullscreen; safeAreaConsiderationsCheckbox.CanBeShown.Value = host.Window?.SafeAreaPadding.Value.Total != Vector2.Zero; } diff --git a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs index 98f6908512..e1fa1eef9c 100644 --- a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Graphics; @@ -15,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Solid.Laptop + Icon = OsuIcon.Graphics }; public GraphicsSection() 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 7296003c7f..4c5610a15e 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingPanel.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingPanel.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Rulesets; @@ -14,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 1e2283b58b..e82cebe9f4 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; @@ -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.Write(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..adf05a71b9 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_KeyButton.cs @@ -0,0 +1,184 @@ +// 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) => + // TODO: Distinct() can be removed after https://github.com/ppy/osu-framework/pull/6130 is merged. + UpdateKeyCombination(new KeyCombination(fullState.Keys.Where(KeyCombination.IsModifierKey).Append(triggerKey).Distinct().ToArray())); + + 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/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index dfaeafbf5d..7805ed5834 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -28,6 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private Bindable localSensitivity; private Bindable windowMode; + private Bindable minimiseOnFocusLoss; private SettingsEnumDropdown confineMouseModeSetting; private Bindable relativeMode; @@ -47,6 +48,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input relativeMode = mouseHandler.UseRelativeMode.GetBoundCopy(); windowMode = config.GetBindable(FrameworkSetting.WindowMode); + minimiseOnFocusLoss = config.GetBindable(FrameworkSetting.MinimiseOnFocusLossInFullscreen); Children = new Drawable[] { @@ -75,7 +77,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input }, new SettingsCheckbox { - LabelText = MouseSettingsStrings.DisableMouseButtons, + LabelText = MouseSettingsStrings.DisableClicksDuringGameplay, Current = osuConfig.GetBindable(OsuSetting.MouseDisableButtons) }, }; @@ -98,21 +100,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input localSensitivity.BindValueChanged(val => handlerSensitivity.Value = val.NewValue); - windowMode.BindValueChanged(mode => - { - bool isFullscreen = mode.NewValue == WindowMode.Fullscreen; - - if (isFullscreen) - { - confineMouseModeSetting.Current.Disabled = true; - confineMouseModeSetting.TooltipText = MouseSettingsStrings.NotApplicableFullscreen; - } - else - { - confineMouseModeSetting.Current.Disabled = false; - confineMouseModeSetting.TooltipText = string.Empty; - } - }, true); + windowMode.BindValueChanged(_ => updateConfineMouseModeSettingVisibility()); + minimiseOnFocusLoss.BindValueChanged(_ => updateConfineMouseModeSettingVisibility(), true); highPrecisionMouse.Current.BindValueChanged(highPrecision => { @@ -126,6 +115,25 @@ namespace osu.Game.Overlays.Settings.Sections.Input }, true); } + /// + /// Updates disabled state and tooltip of to match when is overriding the confine mode. + /// + private void updateConfineMouseModeSettingVisibility() + { + bool confineModeOverriden = windowMode.Value == WindowMode.Fullscreen && minimiseOnFocusLoss.Value; + + if (confineModeOverriden) + { + confineMouseModeSetting.Current.Disabled = true; + confineMouseModeSetting.TooltipText = MouseSettingsStrings.NotApplicableFullscreen; + } + else + { + confineMouseModeSetting.Current.Disabled = false; + confineMouseModeSetting.TooltipText = string.Empty; + } + } + public partial class SensitivitySetting : SettingsSlider { public SensitivitySetting() diff --git a/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs b/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs index 3b5002b423..f326592016 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.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.Localisation; using osu.Game.Rulesets; @@ -18,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/TouchSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs index 8d1b12d5b2..0056de6674 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs @@ -3,38 +3,48 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework; using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Input.Handlers.Touch; +using osu.Framework.Input.Handlers; using osu.Framework.Localisation; +using osu.Game.Configuration; using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.Input { + /// + /// Touch input settings subsection common to all touch handlers (even on different platforms). + /// public partial class TouchSettings : SettingsSubsection { - private readonly TouchHandler handler; + private readonly InputHandler handler; - public TouchSettings(TouchHandler handler) + protected override LocalisableString Header => TouchSettingsStrings.Touch; + + public TouchSettings(InputHandler handler) { this.handler = handler; } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager osuConfig) { - Children = new Drawable[] + if (!RuntimeInfo.IsMobile) // don't allow disabling the only input method (touch) on mobile. { - new SettingsCheckbox + Add(new SettingsCheckbox { LabelText = CommonStrings.Enabled, Current = handler.Enabled - }, - }; + }); + } + + Add(new SettingsCheckbox + { + LabelText = TouchSettingsStrings.DisableTapsDuringGameplay, + Current = osuConfig.GetBindable(OsuSetting.TouchDisableGameplayTaps) + }); } public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { @"touchscreen" }); - - protected override LocalisableString Header => handler.Description; } } 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 a8f19cc91d..0204aa5e64 100644 --- a/osu.Game/Overlays/Settings/Sections/InputSection.cs +++ b/osu.Game/Overlays/Settings/Sections/InputSection.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Handlers; using osu.Framework.Localisation; using osu.Framework.Platform; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Input; @@ -20,7 +21,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Solid.Keyboard + Icon = OsuIcon.Input }; public InputSection(KeyBindingPanel keyConfig) diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectorySelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectorySelectScreen.cs index 1b935b0cec..3f12b9c0df 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectorySelectScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/StableDirectorySelectScreen.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 System; using System.IO; -using System.Linq; using System.Threading.Tasks; +using osu.Framework.Allocation; using osu.Framework.Localisation; using osu.Framework.Screens; +using osu.Game.Database; namespace osu.Game.Overlays.Settings.Sections.Maintenance { @@ -13,9 +15,12 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { private readonly TaskCompletionSource taskCompletionSource; + [Resolved] + private LegacyImportManager legacyImportManager { get; set; } = null!; + protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.Disabled; - protected override bool IsValidDirectory(DirectoryInfo? info) => info?.GetFiles("osu!.*.cfg").Any() ?? false; + protected override bool IsValidDirectory(DirectoryInfo? info) => legacyImportManager.IsUsableForStableImport(info, out _); public override LocalisableString HeaderText => "Please select your osu!stable install location"; @@ -26,7 +31,10 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance protected override void OnSelection(DirectoryInfo directory) { - taskCompletionSource.TrySetResult(directory.FullName); + if (!legacyImportManager.IsUsableForStableImport(directory, out var stableRoot)) + throw new InvalidOperationException($@"{nameof(OnSelection)} was called on an invalid directory. This should never happen."); + + taskCompletionSource.TrySetResult(stableRoot.FullName); this.Exit(); } diff --git a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs index bb0a952164..bd90e4c35d 100644 --- a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs +++ b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Maintenance; @@ -15,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Solid.Wrench + Icon = OsuIcon.Maintenance }; public MaintenanceSection() diff --git a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs index d34b01ebf3..ce5c85bed0 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs @@ -31,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 c8faa3b697..1484f2c756 100644 --- a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs +++ b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Online; @@ -15,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Solid.GlobeAsia + Icon = OsuIcon.Online }; public OnlineSection() diff --git a/osu.Game/Overlays/Settings/Sections/RulesetSection.cs b/osu.Game/Overlays/Settings/Sections/RulesetSection.cs index aaad1ec4e2..626264151f 100644 --- a/osu.Game/Overlays/Settings/Sections/RulesetSection.cs +++ b/osu.Game/Overlays/Settings/Sections/RulesetSection.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Logging; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Rulesets; @@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Solid.Chess + Icon = OsuIcon.Rulesets }; [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index e997e70157..9b04f208a7 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Game.Database; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Overlays.SkinEditor; @@ -31,7 +32,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Solid.PaintBrush + Icon = OsuIcon.SkinB }; private static readonly Live random_skin_info = new SkinInfo @@ -57,9 +58,11 @@ namespace osu.Game.Overlays.Settings.Sections { skinDropdown = new SkinSettingsDropdown { + AlwaysShowSearchBar = true, + AllowNonContiguousMatching = true, LabelText = SkinSettingsStrings.CurrentSkin, Current = skins.CurrentSkinInfo, - Keywords = new[] { @"skins" } + Keywords = new[] { @"skins" }, }, new SettingsButton { diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs index 844b8aeac6..3f39980b43 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs @@ -12,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/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs index 4577fadb01..5e42c3035c 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs @@ -29,6 +29,11 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface Children = new Drawable[] { + new SettingsCheckbox + { + LabelText = UserInterfaceStrings.ShowMenuTips, + Current = config.GetBindable(OsuSetting.MenuTips) + }, new SettingsCheckbox { LabelText = UserInterfaceStrings.InterfaceVoices, diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs index addf5ce163..49bd17dfde 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs @@ -42,6 +42,12 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface ClassicDefault = ModSelectHotkeyStyle.Classic }, new SettingsCheckbox + { + LabelText = UserInterfaceStrings.ModSelectTextSearchStartsActive, + Current = config.GetBindable(OsuSetting.ModSelectTextSearchStartsActive), + ClassicDefault = false + }, + new SettingsCheckbox { LabelText = GameplaySettingsStrings.BackgroundBlur, Current = config.GetBindable(OsuSetting.SongSelectBackgroundBlur), diff --git a/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs b/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs index 2ec9e32ea9..953ede25e1 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.UserInterface; @@ -15,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Solid.LayerGroup + Icon = OsuIcon.UserInterface }; public UserInterfaceSection() diff --git a/osu.Game/Overlays/Settings/SettingsDropdown.cs b/osu.Game/Overlays/Settings/SettingsDropdown.cs index 5798d02e03..ec69224bb6 100644 --- a/osu.Game/Overlays/Settings/SettingsDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsDropdown.cs @@ -16,6 +16,18 @@ namespace osu.Game.Overlays.Settings { protected new OsuDropdown Control => (OsuDropdown)base.Control; + public bool AlwaysShowSearchBar + { + get => Control.AlwaysShowSearchBar; + set => Control.AlwaysShowSearchBar = value; + } + + public bool AllowNonContiguousMatching + { + get => Control.AllowNonContiguousMatching; + set => Control.AllowNonContiguousMatching = value; + } + public IEnumerable Items { get => Control.Items; diff --git a/osu.Game/Overlays/Settings/SettingsSidebar.cs b/osu.Game/Overlays/Settings/SettingsSidebar.cs index 06bc2fd788..ddbcd60ef6 100644 --- a/osu.Game/Overlays/Settings/SettingsSidebar.cs +++ b/osu.Game/Overlays/Settings/SettingsSidebar.cs @@ -1,21 +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 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.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; namespace osu.Game.Overlays.Settings { - public partial class SettingsSidebar : ExpandingButtonContainer + public partial class SettingsSidebar : ExpandingContainer { - public const float DEFAULT_WIDTH = 70; - public const int EXPANDED_WIDTH = 200; + public const float CONTRACTED_WIDTH = 70; + public const int EXPANDED_WIDTH = 170; - public SettingsSidebar() - : base(DEFAULT_WIDTH, EXPANDED_WIDTH) + public Action? BackButtonAction; + + protected override bool ExpandOnHover => false; + + private readonly bool showBackButton; + + public SettingsSidebar(bool showBackButton) + : base(CONTRACTED_WIDTH, EXPANDED_WIDTH) { + this.showBackButton = showBackButton; + Expanded.Value = true; } [BackgroundDependencyLoader] @@ -27,6 +42,71 @@ namespace osu.Game.Overlays.Settings RelativeSizeAxes = Axes.Both, Depth = float.MaxValue }); + + if (showBackButton) + { + AddInternal(new BackButton + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Action = () => BackButtonAction?.Invoke(), + }); + } + } + + public partial class BackButton : SidebarButton + { + private Drawable content = null!; + + public BackButton() + : base(HoverSampleSet.Default) + { + } + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(SettingsSidebar.EXPANDED_WIDTH); + + Padding = new MarginPadding(40); + + AddRange(new[] + { + content = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(30), + Shadow = true, + Icon = FontAwesome.Solid.ChevronLeft + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), + Text = @"back", + }, + } + } + }); + } + + protected override void UpdateState() + { + base.UpdateState(); + + content.FadeColour(IsHovered ? ColourProvider.Light1 : ColourProvider.Light3, FADE_DURATION, Easing.OutQuint); + } } } } diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs index 5fe0e1afc0..f58c2f41ef 100644 --- a/osu.Game/Overlays/Settings/SidebarButton.cs +++ b/osu.Game/Overlays/Settings/SidebarButton.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; @@ -14,8 +15,8 @@ namespace osu.Game.Overlays.Settings [Resolved] protected OverlayColourProvider ColourProvider { get; private set; } = null!; - protected SidebarButton() - : base(HoverSampleSet.ButtonSidebar) + protected SidebarButton(HoverSampleSet? hoverSounds = HoverSampleSet.ButtonSidebar) + : base(hoverSounds) { } @@ -23,6 +24,7 @@ namespace osu.Game.Overlays.Settings private void load() { BackgroundColour = ColourProvider.Background5; + Hover.Colour = ColourProvider.Light4; } protected override void LoadComplete() @@ -40,6 +42,9 @@ namespace osu.Game.Overlays.Settings protected override void OnHoverLost(HoverLostEvent e) => UpdateState(); - protected abstract void UpdateState(); + protected virtual void UpdateState() + { + Hover.FadeTo(IsHovered ? 0.1f : 0, FADE_DURATION, Easing.OutQuint); + } } } diff --git a/osu.Game/Overlays/Settings/SidebarIconButton.cs b/osu.Game/Overlays/Settings/SidebarIconButton.cs index 4e5b361460..f4b71207e3 100644 --- a/osu.Game/Overlays/Settings/SidebarIconButton.cs +++ b/osu.Game/Overlays/Settings/SidebarIconButton.cs @@ -60,26 +60,28 @@ namespace osu.Game.Overlays.Settings RelativeSizeAxes = Axes.X; Height = 46; + Padding = new MarginPadding(5); + AddRange(new Drawable[] { textIconContent = new Container { - Width = SettingsSidebar.DEFAULT_WIDTH, - RelativeSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(0.6f), Children = new Drawable[] { - headerText = new OsuSpriteText - { - Position = new Vector2(SettingsSidebar.DEFAULT_WIDTH + 10, 0), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, iconContainer = new ConstrainedIconContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Size = new Vector2(20), + Margin = new MarginPadding { Left = 25 } + }, + headerText = new OsuSpriteText + { + Position = new Vector2(60, 0), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, }, } }, @@ -113,6 +115,8 @@ namespace osu.Game.Overlays.Settings protected override void UpdateState() { + base.UpdateState(); + if (Selected) { textIconContent.FadeColour(ColourProvider.Content1, FADE_DURATION, Easing.OutQuint); diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 291281124c..9076dadf93 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -3,23 +3,27 @@ #nullable disable +using System.Collections.Generic; +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.Framework.Localisation; +using osu.Framework.Testing; +using osu.Game.Graphics; +using osu.Game.Localisation; using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings.Sections; using osu.Game.Overlays.Settings.Sections.Input; using osuTK.Graphics; -using System.Collections.Generic; -using osu.Framework.Bindables; -using osu.Framework.Localisation; -using osu.Game.Localisation; namespace osu.Game.Overlays { public partial class SettingsOverlay : SettingsPanel, INamedOverlayComponent { - public string IconTexture => "Icons/Hexacons/settings"; + public IconUsage Icon => OsuIcon.Settings; public LocalisableString Title => SettingsStrings.HeaderTitle; public LocalisableString Description => SettingsStrings.HeaderDescription; @@ -47,12 +51,27 @@ namespace osu.Game.Overlays protected override Drawable CreateFooter() => new SettingsFooter(); public SettingsOverlay() - : base(true) + : base(false) { } public override bool AcceptsFocus => lastOpenedSubPanel == null || lastOpenedSubPanel.State.Value == Visibility.Hidden; + public void ShowAtControl() + where T : Drawable + { + Show(); + + // wait for load of sections + if (!SectionsContainer.Any()) + { + Scheduler.Add(ShowAtControl); + return; + } + + SectionsContainer.ScrollTo(SectionsContainer.ChildrenOfType().Single()); + } + private T createSubPanel(T subPanel) where T : SettingsSubPanel { @@ -70,16 +89,19 @@ namespace osu.Game.Overlays switch (state.NewValue) { case Visibility.Visible: - Sidebar?.FadeColour(Color4.DarkGray, 300, Easing.OutQuint); + Sidebar.Expanded.Value = false; + Sidebar.FadeColour(Color4.DarkGray, 300, Easing.OutQuint); SectionsContainer.FadeOut(300, Easing.OutQuint); ContentContainer.MoveToX(-PANEL_WIDTH, 500, Easing.OutQuint); lastOpenedSubPanel = panel; + break; case Visibility.Hidden: - Sidebar?.FadeColour(Color4.White, 300, Easing.OutQuint); + Sidebar.Expanded.Value = true; + Sidebar.FadeColour(Color4.White, 300, Easing.OutQuint); SectionsContainer.FadeIn(500, Easing.OutQuint); ContentContainer.MoveToX(0, 500, Easing.OutQuint); diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index d7f39a9d8f..748673035b 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; @@ -32,7 +33,7 @@ namespace osu.Game.Overlays public const float TRANSITION_LENGTH = 600; - private const float sidebar_width = SettingsSidebar.DEFAULT_WIDTH; + private const float sidebar_width = SettingsSidebar.EXPANDED_WIDTH; /// /// The width of the settings panel content, excluding the sidebar. @@ -56,8 +57,9 @@ 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; + private readonly bool showBackButton; private LoadingLayer loading; @@ -70,9 +72,9 @@ namespace osu.Game.Overlays [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - protected SettingsPanel(bool showSidebar) + protected SettingsPanel(bool showBackButton) { - this.showSidebar = showSidebar; + this.showBackButton = showBackButton; RelativeSizeAxes = Axes.Y; AutoSizeAxes = Axes.X; } @@ -105,45 +107,50 @@ 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) + AddInternal(Sidebar = new SettingsSidebar(showBackButton) { - AddInternal(Sidebar = new SettingsSidebar { Width = sidebar_width }); - } + BackButtonAction = Hide, + Width = sidebar_width + }); CreateSections()?.ForEach(AddSection); } @@ -174,7 +181,7 @@ namespace osu.Game.Overlays Scheduler.AddDelayed(loadSections, TRANSITION_LENGTH / 3); Sidebar?.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint); - this.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint); + this.FadeTo(1, TRANSITION_LENGTH / 2, Easing.OutQuint); searchTextBox.TakeFocus(); searchTextBox.HoldFocus = true; @@ -190,7 +197,7 @@ namespace osu.Game.Overlays ContentContainer.MoveToX(-WIDTH + ExpandedPosition, TRANSITION_LENGTH, Easing.OutQuint); Sidebar?.MoveToX(-sidebar_width, TRANSITION_LENGTH, Easing.OutQuint); - this.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint); + this.FadeTo(0, TRANSITION_LENGTH / 2, Easing.OutQuint); searchTextBox.HoldFocus = false; if (searchTextBox.HasFocus) @@ -279,7 +286,6 @@ namespace osu.Game.Overlays return; SectionsContainer.ScrollTo(section); - Sidebar.Expanded.Value = false; }, }; } 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..440639f06b 100644 --- a/osu.Game/Overlays/SettingsSubPanel.cs +++ b/osu.Game/Overlays/SettingsSubPanel.cs @@ -1,16 +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 osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Overlays.Settings; -using osuTK; namespace osu.Game.Overlays { @@ -24,58 +15,8 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load() { - AddInternal(new BackButton - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Action = Hide - }); } protected override bool DimMainContent => false; // dimming is handled by main overlay - - public partial class BackButton : SidebarButton - { - private Container content; - - [BackgroundDependencyLoader] - private void load() - { - Size = new Vector2(SettingsSidebar.DEFAULT_WIDTH); - - AddRange(new Drawable[] - { - content = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(15), - Shadow = true, - Icon = FontAwesome.Solid.ChevronLeft - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Y = 15, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), - Text = @"back", - }, - } - } - }); - } - - protected override void UpdateState() - { - content.FadeColour(IsHovered ? ColourProvider.Light1 : ColourProvider.Light3, FADE_DURATION, Easing.OutQuint); - } - } } } diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index c0948c1eab..de13bd96d4 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -151,9 +151,12 @@ namespace osu.Game.Overlays base.Update(); if (!headerTextVisibilityCache.IsValid) + { // These toolbox grouped may be contracted to only show icons. // For now, let's hide the header to avoid text truncation weirdness in such cases. headerText.FadeTo(headerText.DrawWidth < DrawWidth ? 1 : 0, 150, Easing.OutQuint); + headerTextVisibilityCache.Validate(); + } } protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs index c090878899..8f8d899fad 100644 --- a/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs +++ b/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs @@ -136,9 +136,10 @@ namespace osu.Game.Overlays.SkinEditor { base.Update(); + Vector2 scale = drawable.DrawInfo.MatrixInverse.ExtractScale().Xy; drawableQuad = drawable.ToScreenSpace( drawable.DrawRectangle - .Inflate(SkinSelectionHandler.INFLATE_SIZE)); + .Inflate(SkinSelectionHandler.INFLATE_SIZE * scale)); var localSpaceQuad = ToLocalSpace(drawableQuad); @@ -202,7 +203,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/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..d3af928907 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -46,7 +47,7 @@ namespace osu.Game.Overlays.SkinEditor protected override bool StartHidden => true; - private Drawable targetScreen = null!; + private Drawable? targetScreen; private OsuTextFlowContainer headerText = null!; @@ -150,21 +151,23 @@ namespace osu.Game.Overlays.SkinEditor { new MenuItem(CommonStrings.MenuBarFile) { - Items = new[] + Items = new OsuMenuItem[] { new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), + new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + new OsuMenuItemSpacer(), new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()), }, }, new MenuItem(CommonStrings.MenuBarEdit) { - Items = new[] + Items = new OsuMenuItem[] { undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut), copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy), pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste), @@ -356,7 +359,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 +369,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 }); @@ -406,7 +409,14 @@ namespace osu.Game.Overlays.SkinEditor cp.Colour = colours.Yellow; }); + changeHandler?.Dispose(); + skins.EnsureMutableSkin(); + + var targetContainer = getTarget(selectedTarget.Value); + + if (targetContainer != null) + changeHandler = new SkinEditorChangeHandler(targetContainer); hasBegunMutating = true; } @@ -500,6 +510,9 @@ namespace osu.Game.Overlays.SkinEditor protected void Paste() { + if (!canPaste.Value) + return; + changeHandler?.BeginChange(); var drawableInfo = JsonConvert.DeserializeObject(clipboard.Content.Value); @@ -528,8 +541,14 @@ namespace osu.Game.Overlays.SkinEditor if (!hasBegunMutating) return; + if (targetScreen?.IsLoaded != true) + return; + SkinComponentsContainer[] targetContainers = availableTargets.ToArray(); + if (!targetContainers.All(c => c.ComponentsLoaded)) + return; + foreach (var t in targetContainers) currentSkin.Value.UpdateDrawableTarget(t); diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 68d6b7ced5..40cd31934f 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -1,7 +1,10 @@ // 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; @@ -9,12 +12,22 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; +using osu.Game.Screens.Menu; +using osu.Game.Screens.Play; +using osu.Game.Screens.Select; +using osu.Game.Users; +using osu.Game.Utils; using osuTK; namespace osu.Game.Overlays.SkinEditor @@ -31,12 +44,27 @@ namespace osu.Game.Overlays.SkinEditor private SkinEditor? skinEditor; + [Resolved] + private IPerformFromScreenRunner? performer { get; set; } + [Cached] public readonly EditorClipboard Clipboard = new EditorClipboard(); [Resolved] private OsuGame game { get; set; } = null!; + [Resolved] + private MusicController music { get; set; } = null!; + + [Resolved] + private Bindable> mods { get; set; } = null!; + + [Resolved] + private Bindable ruleset { get; set; } = null!; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + private OsuScreen? lastTargetScreen; private Vector2 lastDrawSize; @@ -72,6 +100,9 @@ namespace osu.Game.Overlays.SkinEditor { globallyDisableBeatmapSkinSetting(); + if (lastTargetScreen is MainMenu) + PresentGameplay(); + if (skinEditor != null) { skinEditor.Show(); @@ -105,6 +136,55 @@ namespace osu.Game.Overlays.SkinEditor globallyReenableBeatmapSkinSetting(); } + public void PresentGameplay() => presentGameplay(false); + + private void presentGameplay(bool attemptedBeatmapSwitch) + { + performer?.PerformFromScreen(screen => + { + if (State.Value != Visibility.Visible) + return; + + if (beatmap.Value is DummyWorkingBeatmap) + { + // presume we don't have anything good to play and just bail. + return; + } + + // If we're playing the intro, switch away to another beatmap. + if (beatmap.Value.BeatmapSetInfo.Protected) + { + if (!attemptedBeatmapSwitch) + { + music.NextTrack(); + Schedule(() => presentGameplay(true)); + } + + return; + } + + if (screen is Player) + return; + + // the validity of the current game-wide beatmap + ruleset combination is enforced by song select. + // if we're anywhere else, the state is unknown and may not make sense, so forcibly set something that does. + if (screen is not PlaySongSelect) + ruleset.Value = beatmap.Value.BeatmapInfo.Ruleset; + var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod(); + + IReadOnlyList usableMods = mods.Value; + + if (replayGeneratingMod != null) + usableMods = usableMods.Append(replayGeneratingMod).ToArray(); + + if (!ModUtils.CheckCompatibleSet(usableMods, out var invalid)) + mods.Value = mods.Value.Except(invalid).ToArray(); + + if (replayGeneratingMod != null) + screen.Push(new EndlessPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods))); + }, new[] { typeof(Player), typeof(PlaySongSelect) }); + } + protected override void Update() { base.Update(); @@ -188,7 +268,10 @@ namespace osu.Game.Overlays.SkinEditor } if (skinEditor.State.Value == Visibility.Visible) + { + skinEditor.Save(false); skinEditor.UpdateTargetScreen(target); + } else { skinEditor.Hide(); @@ -207,6 +290,9 @@ namespace osu.Game.Overlays.SkinEditor // 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. + // + // This causes a full reload of the skin, which is pretty ugly. + // TODO: Investigate if we can avoid this when a beatmap skin is not being applied by the current beatmap. leasedBeatmapSkins = beatmapSkins.BeginLease(true); leasedBeatmapSkins.Value = false; } @@ -216,5 +302,42 @@ namespace osu.Game.Overlays.SkinEditor leasedBeatmapSkins?.Return(); leasedBeatmapSkins = null; } + + private partial class EndlessPlayer : ReplayPlayer + { + protected override UserActivity? InitialActivity => null; + + public override bool DisallowExternalBeatmapRulesetChanges => true; + + public override bool? AllowGlobalTrackControl => false; + + public EndlessPlayer(Func, Score> createScore) + : base(createScore, new PlayerConfiguration + { + ShowResults = false, + AutomaticallySkipIntro = true, + }) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (!LoadedBeatmapSuccessfully) + Scheduler.AddDelayed(this.Exit, 3000); + } + + protected override void Update() + { + base.Update(); + + if (!LoadedBeatmapSuccessfully) + return; + + if (GameplayState.HasPassed) + GameplayClockContainer.Seek(0); + } + } } } diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs b/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs index 9b021632cf..5a283c0e8d 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.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. -using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -14,12 +11,8 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osu.Game.Screens; -using osu.Game.Screens.Play; using osu.Game.Screens.Select; -using osu.Game.Utils; using osuTK; namespace osu.Game.Overlays.SkinEditor @@ -36,10 +29,7 @@ namespace osu.Game.Overlays.SkinEditor private IPerformFromScreenRunner? performer { get; set; } [Resolved] - private IBindable ruleset { get; set; } = null!; - - [Resolved] - private Bindable> mods { get; set; } = null!; + private SkinEditorOverlay? skinEditorOverlay { get; set; } public SkinEditorSceneLibrary() { @@ -96,24 +86,7 @@ namespace osu.Game.Overlays.SkinEditor Text = SkinEditorStrings.Gameplay, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Action = () => performer?.PerformFromScreen(screen => - { - if (screen is Player) - return; - - var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod(); - - IReadOnlyList usableMods = mods.Value; - - if (replayGeneratingMod != null) - usableMods = usableMods.Append(replayGeneratingMod).ToArray(); - - if (!ModUtils.CheckCompatibleSet(usableMods, out var invalid)) - mods.Value = mods.Value.Except(invalid).ToArray(); - - if (replayGeneratingMod != null) - screen.Push(new PlayerLoader(() => new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods)))); - }, new[] { typeof(Player), typeof(PlaySongSelect) }) + Action = () => skinEditorOverlay?.PresentGameplay(), }, } }, diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index b43f4eeb00..cf6fb60636 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -7,15 +7,16 @@ 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.Primitives; using osu.Framework.Graphics.UserInterface; using osu.Framework.Utils; using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; 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,34 +26,49 @@ 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(); + UpdatePosition = updateDrawablePosition + }; - 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; - } + private bool allSelectedSupportManualSizing(Axes axis) => SelectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(axis) == false); public override bool HandleScale(Vector2 scale, Anchor anchor) { + Axes adjustAxis; + + switch (anchor) + { + // for corners, adjust scale. + case Anchor.TopLeft: + case Anchor.TopRight: + case Anchor.BottomLeft: + case Anchor.BottomRight: + adjustAxis = Axes.Both; + break; + + // for edges, adjust size. + // autosize elements can't be easily handled so just disable sizing for now. + case Anchor.TopCentre: + case Anchor.BottomCentre: + if (!allSelectedSupportManualSizing(Axes.Y)) + return false; + + adjustAxis = Axes.Y; + break; + + case Anchor.CentreLeft: + case Anchor.CentreRight: + if (!allSelectedSupportManualSizing(Axes.X)) + return false; + + adjustAxis = Axes.X; + break; + + default: + throw new ArgumentOutOfRangeException(nameof(anchor), anchor, null); + } + // convert scale to screen space scale = ToScreenSpace(scale) - ToScreenSpace(Vector2.Zero); @@ -67,10 +83,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 +94,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 +108,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 +151,25 @@ 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); + + switch (adjustAxis) + { + case Axes.X: + drawableItem.Width *= currentScaledDelta.X; + break; + + case Axes.Y: + drawableItem.Height *= currentScaledDelta.Y; + break; + + case Axes.Both: + drawableItem.Scale *= currentScaledDelta; + break; + } } return true; @@ -137,7 +184,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,9 +218,9 @@ namespace osu.Game.Overlays.SkinEditor { base.OnSelectionChanged(); - SelectionBox.CanRotate = true; - SelectionBox.CanScaleX = true; - SelectionBox.CanScaleY = true; + SelectionBox.CanScaleX = allSelectedSupportManualSizing(Axes.X); + SelectionBox.CanScaleY = allSelectedSupportManualSizing(Axes.Y); + SelectionBox.CanScaleDiagonally = true; SelectionBox.CanFlipX = true; SelectionBox.CanFlipY = true; SelectionBox.CanReverse = false; @@ -201,19 +248,41 @@ namespace osu.Game.Overlays.SkinEditor Items = createAnchorItems((d, o) => ((Drawable)d).Origin == o, applyOrigins).ToArray() }; + yield return new OsuMenuItemSpacer(); + yield return new OsuMenuItem("Reset position", MenuItemType.Standard, () => { foreach (var blueprint in SelectedBlueprints) ((Drawable)blueprint.Item).Position = Vector2.Zero; }); - yield return new EditorMenuItemSpacer(); + yield return new OsuMenuItem("Reset rotation", MenuItemType.Standard, () => + { + foreach (var blueprint in SelectedBlueprints) + ((Drawable)blueprint.Item).Rotation = 0; + }); + + yield return new OsuMenuItem("Reset scale", MenuItemType.Standard, () => + { + foreach (var blueprint in SelectedBlueprints) + { + var blueprintItem = ((Drawable)blueprint.Item); + blueprintItem.Scale = Vector2.One; + + if (blueprintItem.RelativeSizeAxes.HasFlagFast(Axes.X)) + blueprintItem.Width = 1; + if (blueprintItem.RelativeSizeAxes.HasFlagFast(Axes.Y)) + blueprintItem.Height = 1; + } + }); + + yield return new OsuMenuItemSpacer(); yield return new OsuMenuItem("Bring to front", MenuItemType.Standard, () => skinEditor.BringSelectionToFront()); yield return new OsuMenuItem("Send to back", MenuItemType.Standard, () => skinEditor.SendSelectionToBack()); - yield return new EditorMenuItemSpacer(); + yield return new OsuMenuItemSpacer(); foreach (var item in base.GetContextMenuItemsForSelection(selection)) yield return item; @@ -245,7 +314,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 +344,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 +381,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 +403,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/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 93294a9d30..52fad2ba3b 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -164,11 +164,11 @@ namespace osu.Game.Overlays.Toolbar { new ToolbarNewsButton(), new ToolbarChangelogButton(), + new ToolbarWikiButton(), new ToolbarRankingsButton(), new ToolbarBeatmapListingButton(), new ToolbarChatButton(), new ToolbarSocialButton(), - new ToolbarWikiButton(), new ToolbarMusicButton(), //new ToolbarButton //{ @@ -224,9 +224,9 @@ namespace osu.Game.Overlays.Toolbar RelativeSizeAxes = Axes.X, Anchor = Anchor.BottomLeft, Alpha = 0, - Height = 100, + Height = 80, Colour = ColourInfo.GradientVertical( - OsuColour.Gray(0).Opacity(0.9f), OsuColour.Gray(0).Opacity(0)), + OsuColour.Gray(0f).Opacity(0.7f), OsuColour.Gray(0).Opacity(0)), }, }; } @@ -241,9 +241,9 @@ namespace osu.Game.Overlays.Toolbar private void updateState() { if (ShowGradient.Value) - gradientBackground.FadeIn(transition_time, Easing.OutQuint); + gradientBackground.FadeIn(2500, Easing.OutQuint); else - gradientBackground.FadeOut(transition_time, Easing.OutQuint); + gradientBackground.FadeOut(200, Easing.OutQuint); } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index e181322dda..1da2e1b744 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -7,15 +7,13 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.EnumExtensions; 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.Textures; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Database; using osu.Framework.Localisation; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; @@ -28,6 +26,8 @@ namespace osu.Game.Overlays.Toolbar { public abstract partial class ToolbarButton : OsuClickableContainer, IKeyBindingHandler { + public const float PADDING = 3; + protected GlobalAction? Hotkey { get; set; } public void SetIcon(Drawable icon) @@ -36,16 +36,13 @@ namespace osu.Game.Overlays.Toolbar IconContainer.Show(); } - [Resolved] - private TextureStore textures { get; set; } = null!; - [Resolved] private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!; - public void SetIcon(string texture) => - SetIcon(new Sprite + public void SetIcon(IconUsage icon) => + SetIcon(new SpriteIcon { - Texture = textures.Get(texture), + Icon = icon, }); public LocalisableString Text @@ -68,6 +65,7 @@ namespace osu.Game.Overlays.Toolbar protected virtual Anchor TooltipAnchor => Anchor.TopLeft; + protected readonly Container ButtonContent; protected ConstrainedIconContainer IconContainer; protected SpriteText DrawableText; protected Box HoverBackground; @@ -78,52 +76,73 @@ namespace osu.Game.Overlays.Toolbar private readonly SpriteText keyBindingTooltip; protected FillFlowContainer Flow; + protected readonly Container BackgroundContent; + [Resolved] private RealmAccess realm { get; set; } = null!; protected ToolbarButton() { - Width = Toolbar.HEIGHT; + AutoSizeAxes = Axes.X; RelativeSizeAxes = Axes.Y; Children = new Drawable[] { - HoverBackground = new Box + ButtonContent = new Container { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(80).Opacity(180), - Blending = BlendingParameters.Additive, - Alpha = 0, - }, - flashBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - Colour = Color4.White.Opacity(100), - Blending = BlendingParameters.Additive, - }, - Flow = new FillFlowContainer - { - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Padding = new MarginPadding { Left = Toolbar.HEIGHT / 2, Right = Toolbar.HEIGHT / 2 }, + Width = Toolbar.HEIGHT, RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, + Padding = new MarginPadding(PADDING), Children = new Drawable[] { - IconContainer = new ConstrainedIconContainer + BackgroundContent = new Container { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(26), - Alpha = 0, + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 6, + CornerExponent = 3f, + Children = new Drawable[] + { + HoverBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(80).Opacity(180), + Blending = BlendingParameters.Additive, + Alpha = 0, + }, + flashBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Colour = Color4.White.Opacity(100), + Blending = BlendingParameters.Additive, + }, + } }, - DrawableText = new OsuSpriteText + Flow = new FillFlowContainer { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Padding = new MarginPadding { Left = Toolbar.HEIGHT / 2, Right = Toolbar.HEIGHT / 2 }, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Children = new Drawable[] + { + IconContainer = new ConstrainedIconContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(20), + Alpha = 0, + }, + DrawableText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + }, }, }, }, @@ -161,19 +180,26 @@ namespace osu.Game.Overlays.Toolbar }; } + [BackgroundDependencyLoader] + private void load() + { + if (Hotkey != null) + { + realm.SubscribeToPropertyChanged(r => r.All().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value), kb => kb.KeyCombinationString, updateKeyBindingTooltip); + } + } + protected override bool OnMouseDown(MouseDownEvent e) => false; protected override bool OnClick(ClickEvent e) { - flashBackground.FadeOutFromOne(800, Easing.OutQuint); + flashBackground.FadeIn(50).Then().FadeOutFromOne(800, Easing.OutQuint); tooltipContainer.FadeOut(100); return base.OnClick(e); } protected override bool OnHover(HoverEvent e) { - updateKeyBindingTooltip(); - HoverBackground.FadeIn(200); tooltipContainer.FadeIn(100); @@ -201,19 +227,13 @@ namespace osu.Game.Overlays.Toolbar { } - private void updateKeyBindingTooltip() + private void updateKeyBindingTooltip(string keyCombination) { - if (Hotkey == null) return; + string keyBindingString = keyCombinationProvider.GetReadableString(keyCombination); - var realmKeyBinding = realm.Realm.All().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value); - - if (realmKeyBinding != null) - { - string keyBindingString = keyCombinationProvider.GetReadableString(realmKeyBinding.KeyCombination); - - if (!string.IsNullOrEmpty(keyBindingString)) - keyBindingTooltip.Text = $" ({keyBindingString})"; - } + keyBindingTooltip.Text = !string.IsNullOrEmpty(keyBindingString) + ? $" ({keyBindingString})" + : string.Empty; } } @@ -222,14 +242,6 @@ namespace osu.Game.Overlays.Toolbar public OpaqueBackground() { RelativeSizeAxes = Axes.Both; - Masking = true; - MaskingSmoothness = 0; - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(40), - Radius = 5, - }; Children = new Drawable[] { diff --git a/osu.Game/Overlays/Toolbar/ToolbarClock.cs b/osu.Game/Overlays/Toolbar/ToolbarClock.cs index f1310d8535..e1d658b811 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarClock.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarClock.cs @@ -44,38 +44,57 @@ namespace osu.Game.Overlays.Toolbar Children = new Drawable[] { - hoverBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(80).Opacity(180), - Blending = BlendingParameters.Additive, - Alpha = 0, - }, - flashBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - Colour = Color4.White.Opacity(100), - Blending = BlendingParameters.Additive, - }, - new FillFlowContainer + new Container { RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - Padding = new MarginPadding(10), + Padding = new MarginPadding(ToolbarButton.PADDING), Children = new Drawable[] { - analog = new AnalogClockDisplay + new Container { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 6, + CornerExponent = 3f, + Children = new Drawable[] + { + hoverBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(80).Opacity(180), + Blending = BlendingParameters.Additive, + Alpha = 0, + }, + flashBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Colour = Color4.White.Opacity(100), + Blending = BlendingParameters.Additive, + }, + } }, - digital = new DigitalClockDisplay + new FillFlowContainer { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Padding = new MarginPadding(10), + Children = new Drawable[] + { + analog = new AnalogClockDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + digital = new DigitalClockDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs index ba2c8282c5..499ca804c9 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Game.Graphics; using osu.Game.Input.Bindings; using osu.Game.Localisation; @@ -11,7 +12,7 @@ namespace osu.Game.Overlays.Toolbar { public ToolbarHomeButton() { - Width *= 1.4f; + ButtonContent.Width *= 1.4f; Hotkey = GlobalAction.Home; } @@ -20,7 +21,7 @@ namespace osu.Game.Overlays.Toolbar { TooltipMain = ToolbarStrings.HomeHeaderTitle; TooltipSub = ToolbarStrings.HomeHeaderDescription; - SetIcon("Icons/Hexacons/home"); + SetIcon(OsuIcon.Home); } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs index 69597c6b46..5da0056787 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs @@ -28,7 +28,7 @@ namespace osu.Game.Overlays.Toolbar public ToolbarMusicButton() { Hotkey = GlobalAction.ToggleNowPlaying; - AutoSizeAxes = Axes.X; + ButtonContent.AutoSizeAxes = Axes.X; } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs index 7bd48174db..06755a9da9 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs @@ -3,6 +3,7 @@ #nullable disable +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -14,7 +15,7 @@ namespace osu.Game.Overlays.Toolbar { public partial class ToolbarOverlayToggleButton : ToolbarButton { - private readonly Box stateBackground; + private Box stateBackground; private OverlayContainer stateContainer; @@ -39,19 +40,20 @@ namespace osu.Game.Overlays.Toolbar { TooltipMain = named.Title; TooltipSub = named.Description; - SetIcon(named.IconTexture); + SetIcon(named.Icon); } } } - public ToolbarOverlayToggleButton() + [BackgroundDependencyLoader] + private void load(OsuColour colours) { - Add(stateBackground = new Box + BackgroundContent.Add(stateBackground = new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(150).Opacity(180), + Colour = colours.Carmine.Opacity(180), Blending = BlendingParameters.Additive, - Depth = 2, + Depth = float.MaxValue, Alpha = 0, }); @@ -63,11 +65,11 @@ namespace osu.Game.Overlays.Toolbar switch (state.NewValue) { case Visibility.Hidden: - stateBackground.FadeOut(200); + stateBackground.FadeOut(200, Easing.OutQuint); break; case Visibility.Visible: - stateBackground.FadeIn(200); + stateBackground.FadeIn(200, Easing.OutQuint); break; } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs index 715076b368..723c24597a 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs @@ -4,20 +4,19 @@ #nullable disable using System.Collections.Generic; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osuTK; -using osuTK.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; -using osuTK.Input; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Game.Rulesets; +using osuTK; +using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Overlays.Toolbar { @@ -41,25 +40,24 @@ namespace osu.Game.Overlays.Toolbar new OpaqueBackground { Depth = 1, + Masking = true, }, ModeButtonLine = new Container { Size = new Vector2(Toolbar.HEIGHT, 3), Anchor = Anchor.BottomLeft, - Origin = Anchor.TopLeft, - Masking = true, - EdgeEffect = new EdgeEffectParameters + Origin = Anchor.BottomLeft, + Y = -1, + Children = new Drawable[] { - Type = EdgeEffectType.Glow, - Colour = new Color4(255, 194, 224, 100), - Radius = 15, - Roundness = 15, - }, - Child = new Box - { - RelativeSizeAxes = Axes.Both, + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(18, 3), + } } - } + }, }); foreach (var ruleset in Rulesets.AvailableRulesets) @@ -89,7 +87,7 @@ namespace osu.Game.Overlays.Toolbar { if (SelectedTab != null) { - ModeButtonLine.MoveToX(SelectedTab.DrawPosition.X, !hasInitialPosition ? 0 : 200, Easing.OutQuint); + ModeButtonLine.MoveToX(SelectedTab.DrawPosition.X, !hasInitialPosition ? 0 : 500, Easing.OutElasticQuarter); if (hasInitialPosition) selectionSamples[SelectedTab.Value.ShortName]?.Play(); diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs index 7d75acb9d1..3287ac6eaa 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.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. +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.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Rulesets; -using osuTK.Graphics; namespace osu.Game.Overlays.Toolbar { @@ -38,32 +40,30 @@ namespace osu.Game.Overlays.Toolbar private partial class RulesetButton : ToolbarButton { + protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(); + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public RulesetButton() + { + ButtonContent.Padding = new MarginPadding(PADDING) + { + Bottom = 5 + }; + } + public bool Active { - set + set => Scheduler.AddOnce(() => { - if (value) - { - IconContainer.Colour = Color4.White; - IconContainer.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = new Color4(255, 194, 224, 100), - Radius = 15, - Roundness = 15, - }; - } - else - { - IconContainer.Colour = new Color4(255, 194, 224, 255); - IconContainer.EdgeEffect = new EdgeEffectParameters(); - } - } + IconContainer.Colour = value ? Color4Extensions.FromHex("#00FFAA") : colours.GrayF; + }); } 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 78df060252..899f58c9c0 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs @@ -10,7 +10,7 @@ namespace osu.Game.Overlays.Toolbar { public ToolbarSettingsButton() { - Width *= 1.4f; + ButtonContent.Width *= 1.4f; Hotkey = GlobalAction.ToggleSettings; } diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index 028decea1e..230974cd59 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -34,14 +34,12 @@ namespace osu.Game.Overlays.Toolbar public ToolbarUserButton() { - AutoSizeAxes = Axes.X; + ButtonContent.AutoSizeAxes = Axes.X; } [BackgroundDependencyLoader] private void load(OsuColour colours, IAPIProvider api, LoginOverlay? login) { - Add(new OpaqueBackground { Depth = 1 }); - Flow.Add(new Container { Masking = true, diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 0ab842c907..9840551d9f 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; 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; @@ -18,6 +19,7 @@ using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Profile; @@ -42,6 +44,11 @@ namespace osu.Game.Overlays private ProfileSectionsContainer? sectionsContainer; private ProfileSectionTabControl? tabs; + private IUser? user; + private IRulesetInfo? ruleset; + + private readonly IBindable apiState = new Bindable(); + [Resolved] private RulesetStore rulesets { get; set; } = null!; @@ -58,16 +65,36 @@ namespace osu.Game.Overlays }); } + [BackgroundDependencyLoader] + private void load() + { + apiState.BindTo(API.State); + apiState.BindValueChanged(state => Schedule(() => + { + if (state.NewValue == APIState.Online && user != null) + Scheduler.AddOnce(fetchAndSetContent); + })); + } + protected override ProfileHeader CreateHeader() => new ProfileHeader(); protected override Color4 BackgroundColour => ColourProvider.Background5; - public void ShowUser(IUser user, IRulesetInfo? ruleset = null) + public void ShowUser(IUser userToShow, IRulesetInfo? userRuleset = null) { - if (user.OnlineID == APIUser.SYSTEM_USER_ID) + if (userToShow.OnlineID == APIUser.SYSTEM_USER_ID) return; + user = userToShow; + ruleset = userRuleset; + Show(); + Scheduler.AddOnce(fetchAndSetContent); + } + + private void fetchAndSetContent() + { + Debug.Assert(user != null); if (user.OnlineID == Header.User.Value?.User.Id && ruleset?.MatchesOnlineID(Header.User.Value?.Ruleset) == true) return; @@ -143,24 +170,28 @@ namespace osu.Game.Overlays sectionsContainer.ScrollToTop(); - userReq = user.OnlineID > 1 ? new GetUserRequest(user.OnlineID, ruleset) : new GetUserRequest(user.Username, ruleset); - userReq.Success += u => userLoadComplete(u, ruleset); - API.Queue(userReq); - loadingLayer.Show(); + if (API.State.Value != APIState.Offline) + { + userReq = user.OnlineID > 1 ? new GetUserRequest(user.OnlineID, ruleset) : new GetUserRequest(user.Username, ruleset); + userReq.Success += u => userLoadComplete(u, ruleset); + + API.Queue(userReq); + loadingLayer.Show(); + } } - private void userLoadComplete(APIUser user, IRulesetInfo? ruleset) + private void userLoadComplete(APIUser loadedUser, IRulesetInfo? userRuleset) { Debug.Assert(sections != null && sectionsContainer != null && tabs != null); - var actualRuleset = rulesets.GetRuleset(ruleset?.ShortName ?? user.PlayMode).AsNonNull(); + var actualRuleset = rulesets.GetRuleset(userRuleset?.ShortName ?? loadedUser.PlayMode).AsNonNull(); - var userProfile = new UserProfileData(user, actualRuleset); + var userProfile = new UserProfileData(loadedUser, actualRuleset); Header.User.Value = userProfile; - if (user.ProfileOrder != null) + if (loadedUser.ProfileOrder != null) { - foreach (string id in user.ProfileOrder) + foreach (string id in loadedUser.ProfileOrder) { var sec = sections.FirstOrDefault(s => s.Identifier == id); diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs index d366f0bddb..9ca4c25ab9 100644 --- a/osu.Game/Overlays/Volume/VolumeMeter.cs +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -313,6 +313,33 @@ namespace osu.Game.Overlays.Volume private void resetAcceleration() => accelerationModifier = 1; + private float dragDelta; + + protected override bool OnDragStart(DragStartEvent e) + { + dragDelta = 0; + adjustFromDrag(e.Delta); + return true; + } + + protected override void OnDrag(DragEvent e) + { + adjustFromDrag(e.Delta); + base.OnDrag(e); + } + + private void adjustFromDrag(Vector2 delta) + { + const float mouse_drag_divisor = 200; + + dragDelta += delta.Y / mouse_drag_divisor; + + if (Math.Abs(dragDelta) < 0.01) return; + + Volume -= dragDelta; + dragDelta = 0; + } + private void adjust(double delta, bool isPrecise) { if (delta == 0) diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs index 153f7f5412..0295ff467a 100644 --- a/osu.Game/Overlays/WaveOverlayContainer.cs +++ b/osu.Game/Overlays/WaveOverlayContainer.cs @@ -18,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; diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs index 9107ad342b..e6bfb75026 100644 --- a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs +++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs @@ -7,7 +7,6 @@ using Markdig.Extensions.Yaml; using Markdig.Syntax; using Markdig.Syntax.Inlines; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Containers.Markdown; using osu.Game.Graphics.Containers.Markdown; namespace osu.Game.Overlays.Wiki.Markdown @@ -53,7 +52,7 @@ namespace osu.Game.Overlays.Wiki.Markdown base.AddMarkdownComponent(markdownObject, container, level); } - public override MarkdownTextFlowContainer CreateTextFlow() => new WikiMarkdownTextFlowContainer(); + public override OsuMarkdownTextFlowContainer CreateTextFlow() => new WikiMarkdownTextFlowContainer(); private partial class WikiMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer { diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs index bb7c232a13..cae2d0167a 100644 --- a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs +++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs @@ -81,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/WikiHeader.cs b/osu.Game/Overlays/Wiki/WikiHeader.cs index 9317813fc4..d64d6b934a 100644 --- a/osu.Game/Overlays/Wiki/WikiHeader.cs +++ b/osu.Game/Overlays/Wiki/WikiHeader.cs @@ -8,6 +8,7 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; @@ -16,8 +17,6 @@ namespace osu.Game.Overlays.Wiki { public partial class WikiHeader : BreadcrumbControlOverlayHeader { - private const string index_path = "Main_Page"; - public static LocalisableString IndexPageString => LayoutStrings.HeaderHelpIndex; public readonly Bindable WikiPageData = new Bindable(); @@ -44,7 +43,7 @@ namespace osu.Game.Overlays.Wiki TabControl.AddItem(IndexPageString); - if (e.NewValue.Path == index_path) + if (e.NewValue.Path == WikiOverlay.INDEX_PATH) { Current.Value = IndexPageString; return; @@ -81,7 +80,7 @@ namespace osu.Game.Overlays.Wiki { Title = PageTitleStrings.MainWikiControllerDefault; Description = NamedOverlayComponentStrings.WikiDescription; - IconTexture = "Icons/Hexacons/wiki"; + Icon = OsuIcon.Wiki; } } } diff --git a/osu.Game/Overlays/Wiki/WikiPanelContainer.cs b/osu.Game/Overlays/Wiki/WikiPanelContainer.cs index ef31e9cfdd..cbffe5732e 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 @@ -93,7 +93,7 @@ namespace osu.Game.Overlays.Wiki public override SpriteText CreateSpriteText() => base.CreateSpriteText().With(t => t.Font = t.Font.With(Typeface.Torus, weight: FontWeight.Bold)); - public override MarkdownTextFlowContainer CreateTextFlow() => base.CreateTextFlow().With(f => f.TextAnchor = Anchor.TopCentre); + public override OsuMarkdownTextFlowContainer CreateTextFlow() => base.CreateTextFlow().With(f => f.TextAnchor = Anchor.TopCentre); protected override MarkdownParagraph CreateParagraph(ParagraphBlock paragraphBlock, int level) => base.CreateParagraph(paragraphBlock, level).With(p => p.Margin = new MarginPadding { Bottom = 10 }); diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index c816eca776..ffbc168fb7 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -19,11 +19,11 @@ namespace osu.Game.Overlays { public partial class WikiOverlay : OnlineOverlay { - private const string index_path = @"main_page"; + public const string INDEX_PATH = @"Main_page"; public string CurrentPath => path.Value; - private readonly Bindable path = new Bindable(index_path); + private readonly Bindable path = new Bindable(INDEX_PATH); private readonly Bindable wikiData = new Bindable(); @@ -43,7 +43,7 @@ namespace osu.Game.Overlays { } - public void ShowPage(string pagePath = index_path) + public void ShowPage(string pagePath = INDEX_PATH) { path.Value = pagePath.Trim('/'); Show(); @@ -137,7 +137,7 @@ namespace osu.Game.Overlays wikiData.Value = response; path.Value = response.Path; - if (response.Layout == index_path) + if (response.Layout.Equals(INDEX_PATH, StringComparison.OrdinalIgnoreCase)) { LoadDisplay(new WikiMainPage { @@ -161,7 +161,7 @@ namespace osu.Game.Overlays path.Value = "error"; LoadDisplay(articlePage = new WikiArticlePage($@"{api.WebsiteRootUrl}/wiki/", - $"Something went wrong when trying to fetch page \"{originalPath}\".\n\n[Return to the main page](Main_Page).")); + $"Something went wrong when trying to fetch page \"{originalPath}\".\n\n[Return to the main page]({INDEX_PATH}).")); } private void showParentPage() diff --git a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs index 0eea1ff215..418dc3576f 100644 --- a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs +++ b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Configuration if (setting != null) { - bindable.Parse(setting.Value); + bindable.Parse(setting.Value, CultureInfo.InvariantCulture); } else { diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 5a01faa417..9690924b1c 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Difficulty { @@ -27,9 +26,6 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_FLASHLIGHT = 17; protected const int ATTRIB_ID_SLIDER_FACTOR = 19; protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21; - protected const int ATTRIB_ID_LEGACY_ACCURACY_SCORE = 23; - protected const int ATTRIB_ID_LEGACY_COMBO_SCORE = 25; - protected const int ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO = 27; /// /// The mods which were applied to the beatmap. @@ -48,22 +44,6 @@ namespace osu.Game.Rulesets.Difficulty [JsonProperty("max_combo", Order = -2)] public int MaxCombo { get; set; } - /// - /// The accuracy portion of the legacy (ScoreV1) total score. - /// - public int LegacyAccuracyScore { get; set; } - - /// - /// The combo-multiplied portion of the legacy (ScoreV1) total score. - /// - public int LegacyComboScore { get; set; } - - /// - /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. - /// This is made up of all judgements that would be or . - /// - public double LegacyBonusScoreRatio { get; set; } - /// /// Creates new . /// @@ -91,9 +71,6 @@ namespace osu.Game.Rulesets.Difficulty public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); - yield return (ATTRIB_ID_LEGACY_ACCURACY_SCORE, LegacyAccuracyScore); - yield return (ATTRIB_ID_LEGACY_COMBO_SCORE, LegacyComboScore); - yield return (ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO, LegacyBonusScoreRatio); } /// @@ -104,9 +81,6 @@ namespace osu.Game.Rulesets.Difficulty public virtual void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; - LegacyAccuracyScore = (int)values[ATTRIB_ID_LEGACY_ACCURACY_SCORE]; - LegacyComboScore = (int)values[ATTRIB_ID_LEGACY_COMBO_SCORE]; - LegacyBonusScoreRatio = (int)values[ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO]; } } } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index d005bbfc7a..00c90bd317 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -23,13 +23,6 @@ namespace osu.Game.Rulesets.Difficulty { public abstract class DifficultyCalculator { - /// - /// Whether legacy scoring values (ScoreV1) should be computed to populate the difficulty attributes - /// , , - /// and . - /// - public bool ComputeLegacyScoringValues; - /// /// The beatmap for which difficulty will be calculated. /// diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index 6782c4324a..dcf5eb4da9 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Edit new CheckFewHitsounds(), new CheckTooShortAudioFiles(), new CheckAudioInVideo(), + new CheckDelayedHitsounds(), // Files new CheckZeroByteFiles(), diff --git a/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs b/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs new file mode 100644 index 0000000000..d6cd4f4caa --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs @@ -0,0 +1,181 @@ +// 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; +using System.Linq; +using osu.Framework.Audio.Track; +using osu.Game.Audio; +using osu.Game.Extensions; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckDelayedHitsounds : ICheck + { + /// + /// Threshold at which point the sample is considered silent. + /// + private const float silence_threshold = 0.001f; + + private const float falloff_factor = 0.95f; + private const int delay_threshold = 5; + private const int delay_threshold_negligible = 1; + + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Delayed hit sounds."); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateConsequentDelay(this), + new IssueTemplateDelay(this), + new IssueTemplateDelayNoSilence(this), + new IssueTemplateMinorDelay(this), + new IssueTemplateMinorDelayNoSilence(this), + }; + + private float getAverageAmplitude(Waveform.Point point) => (point.AmplitudeLeft + point.AmplitudeRight) / 2; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet; + + if (beatmapSet == null) + yield break; + + foreach (var file in beatmapSet.Files) + { + using (Stream? stream = context.WorkingBeatmap.GetStream(file.File.GetStoragePath())) + { + if (stream == null) + continue; + + if (!isHitSound(file.Filename)) + continue; + + using Waveform waveform = new Waveform(stream); + + var points = waveform.GetPoints(); + + // Skip muted samples + if (points.Length == 0 || points.Sum(getAverageAmplitude) <= silence_threshold) + continue; + + float maxAmplitude = points.Select(getAverageAmplitude).Max(); + + int consequentDelay = 0; + int delay = 0; + float amplitude = 0; + + while (delay + consequentDelay < points.Length) + { + amplitude += getAverageAmplitude(points[delay]); + + // Reached peak amplitude/transient + if (amplitude >= maxAmplitude) + break; + + amplitude *= falloff_factor; + + if (amplitude < silence_threshold) + { + amplitude = 0; + consequentDelay++; + } + + delay++; + } + + if (consequentDelay >= delay_threshold) + yield return new IssueTemplateConsequentDelay(this).Create(file.Filename, consequentDelay); + else if (consequentDelay + delay >= delay_threshold) + { + if (consequentDelay > 0) + yield return new IssueTemplateDelay(this).Create(file.Filename, consequentDelay, delay); + else + yield return new IssueTemplateDelayNoSilence(this).Create(file.Filename, delay); + } + else if (consequentDelay + delay >= delay_threshold_negligible) + { + if (consequentDelay > 0) + yield return new IssueTemplateMinorDelay(this).Create(file.Filename, consequentDelay, delay); + else + yield return new IssueTemplateMinorDelayNoSilence(this).Create(file.Filename, delay); + } + } + } + } + + private bool isHitSound(string filename) + { + if (!AudioCheckUtils.HasAudioExtension(filename)) + return false; + + // - + string[] parts = filename.ToLowerInvariant().Split('-'); + + if (parts.Length != 2) + return false; + + string bank = parts[0]; + string sampleSet = parts[1]; + + return HitSampleInfo.AllBanks.Contains(bank) + && HitSampleInfo.AllAdditions.Append(HitSampleInfo.HIT_NORMAL).Any(sampleSet.StartsWith); + } + + public class IssueTemplateConsequentDelay : IssueTemplate + { + public IssueTemplateConsequentDelay(ICheck check) + : base(check, IssueType.Problem, + "\"{0}\" has a {1:0.##} ms period of complete silence at the start.") + { + } + + public Issue Create(string filename, int pureDelay) => new Issue(this, filename, pureDelay); + } + + public class IssueTemplateDelay : IssueTemplate + { + public IssueTemplateDelay(ICheck check) + : base(check, IssueType.Warning, + "\"{0}\" has a transient delay of ~{1:0.##} ms, of which {2:0.##} ms is complete silence.") + { + } + + public Issue Create(string filename, int consequentDelay, int delay) => new Issue(this, filename, delay, consequentDelay); + } + + public class IssueTemplateDelayNoSilence : IssueTemplate + { + public IssueTemplateDelayNoSilence(ICheck check) + : base(check, IssueType.Warning, + "\"{0}\" has a transient delay of ~{1:0.##} ms.") + { + } + + public Issue Create(string filename, int delay) => new Issue(this, filename, delay); + } + + public class IssueTemplateMinorDelay : IssueTemplate + { + public IssueTemplateMinorDelay(ICheck check) + : base(check, IssueType.Negligible, + "\"{0}\" has a transient delay of ~{1:0.##} ms, of which {2:0.##} ms is complete silence.") + { + } + + public Issue Create(string filename, int consequentDelay, int delay) => new Issue(this, filename, delay, consequentDelay); + } + + public class IssueTemplateMinorDelayNoSilence : IssueTemplate + { + public IssueTemplateMinorDelayNoSilence(ICheck check) + : base(check, IssueType.Negligible, + "\"{0}\" has a transient delay of ~{1:0.##} ms.") + { + } + + public Issue Create(string filename, int delay) => new Issue(this, filename, delay); + } + } +} diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs index 1c2ea36948..32a3aa5ad9 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; -using System.Linq; using ManagedBass; using osu.Framework.Audio.Callbacks; using osu.Game.Extensions; @@ -16,8 +15,6 @@ namespace osu.Game.Rulesets.Edit.Checks private const int ms_threshold = 25; private const int min_bytes_threshold = 100; - private readonly string[] audioExtensions = { "mp3", "ogg", "wav" }; - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Too short audio files"); public IEnumerable PossibleTemplates => new IssueTemplate[] @@ -46,7 +43,7 @@ namespace osu.Game.Rulesets.Edit.Checks { // If the file is not likely to be properly parsed by Bass, we don't produce Error issues about it. // Image files and audio files devoid of audio data both fail, for example, but neither would be issues in this check. - if (hasAudioExtension(file.Filename) && probablyHasAudioData(data)) + if (AudioCheckUtils.HasAudioExtension(file.Filename) && probablyHasAudioData(data)) yield return new IssueTemplateBadFormat(this).Create(file.Filename); continue; @@ -63,7 +60,6 @@ namespace osu.Game.Rulesets.Edit.Checks } } - private bool hasAudioExtension(string filename) => audioExtensions.Any(filename.ToLowerInvariant().EndsWith); private bool probablyHasAudioData(Stream data) => data.Length > min_bytes_threshold; public class IssueTemplateTooShort : IssueTemplate diff --git a/osu.Game/Rulesets/Edit/Checks/Components/AudioCheckUtils.cs b/osu.Game/Rulesets/Edit/Checks/Components/AudioCheckUtils.cs new file mode 100644 index 0000000000..b8cbe63c1e --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/Components/AudioCheckUtils.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 System.IO; +using System.Linq; + +namespace osu.Game.Rulesets.Edit.Checks.Components +{ + public static class AudioCheckUtils + { + public static readonly string[] AUDIO_EXTENSIONS = { "mp3", "ogg", "wav" }; + + public static bool HasAudioExtension(string filename) => AUDIO_EXTENSIONS.Any(Path.GetExtension(filename).ToLowerInvariant().EndsWith); + } +} diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs similarity index 79% rename from osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs rename to osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index 817e8bd5fe..b3ca59a5b0 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; @@ -17,6 +16,7 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Overlays; @@ -24,16 +24,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 +41,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 +98,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; @@ -138,41 +168,10 @@ namespace osu.Game.Rulesets.Edit } } - protected override void LoadComplete() + public IEnumerable CreateTernaryButtons() => new[] { - base.LoadComplete(); - - if (DistanceSpacingMultiplier.Disabled) - { - distanceSpacingSlider.Hide(); - return; - } - - 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[] - { - new TernaryButton(DistanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }) - }); + new TernaryButton(DistanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = OsuIcon.EditorDistanceSnap }) + }; protected override bool OnKeyDown(KeyDownEvent e) { @@ -242,26 +241,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) { @@ -269,9 +270,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. @@ -281,6 +282,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/EditorTimestampParser.cs b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs new file mode 100644 index 0000000000..bdfdce432e --- /dev/null +++ b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs @@ -0,0 +1,50 @@ +// 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.CodeAnalysis; +using System.Text.RegularExpressions; + +namespace osu.Game.Rulesets.Edit +{ + public static class EditorTimestampParser + { + // 00:00:000 (...) - test + // original osu-web regex: https://github.com/ppy/osu-web/blob/3b1698639244cfdaf0b41c68bfd651ea729ec2e3/resources/js/utils/beatmapset-discussion-helper.ts#L78 + public static readonly Regex TIME_REGEX = new Regex(@"\b(((?\d{2,}):(?[0-5]\d)[:.](?\d{3}))(?\s\([^)]+\))?)", RegexOptions.Compiled); + + public static bool TryParse(string timestamp, [NotNullWhen(true)] out TimeSpan? parsedTime, out string? parsedSelection) + { + Match match = TIME_REGEX.Match(timestamp); + + if (!match.Success) + { + parsedTime = null; + parsedSelection = null; + return false; + } + + bool result = true; + + result &= int.TryParse(match.Groups[@"minutes"].Value, out int timeMin); + result &= int.TryParse(match.Groups[@"seconds"].Value, out int timeSec); + result &= int.TryParse(match.Groups[@"milliseconds"].Value, out int timeMsec); + + // somewhat sane limit for timestamp duration (10 hours). + result &= timeMin < 600; + + if (!result) + { + parsedTime = null; + parsedSelection = null; + return false; + } + + parsedTime = TimeSpan.FromMinutes(timeMin) + TimeSpan.FromSeconds(timeSec) + TimeSpan.FromMilliseconds(timeMsec); + parsedSelection = match.Groups[@"selection"].Value.Trim(); + if (!string.IsNullOrEmpty(parsedSelection)) + parsedSelection = parsedSelection[1..^1]; + return true; + } + } +} diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index c967187b5c..50e6393895 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -76,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; @@ -87,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) { @@ -104,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 @@ -116,9 +119,12 @@ 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 { @@ -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 => @@ -267,7 +273,7 @@ namespace osu.Game.Rulesets.Edit 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. @@ -304,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 @@ -520,8 +526,20 @@ namespace osu.Game.Rulesets.Edit /// public abstract bool CursorInPlacementArea { get; } + /// + /// Returns a string representing the current selection. + /// The inverse method to . + /// public virtual string ConvertSelectionToString() => string.Empty; + /// + /// Selects objects based on the supplied and . + /// The inverse method to . + /// + /// The time instant to seek to, in milliseconds. + /// The ruleset-specific description of objects to select at the given timestamp. + public virtual void SelectFromTimestamp(double timestamp, string objectDescription) { } + #region IPositionSnapProvider public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All); diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index da44b42831..380038eadf 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -12,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/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 158c3c102c..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; @@ -51,6 +52,7 @@ namespace osu.Game.Rulesets.Edit private SelectionState state; + [CanBeNull] public event Action StateChanged; public SelectionState State diff --git a/osu.Game/Rulesets/Edit/Tools/SelectTool.cs b/osu.Game/Rulesets/Edit/Tools/SelectTool.cs index 9640830a09..a272e9f480 100644 --- a/osu.Game/Rulesets/Edit/Tools/SelectTool.cs +++ b/osu.Game/Rulesets/Edit/Tools/SelectTool.cs @@ -5,6 +5,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Edit.Tools { @@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Edit.Tools { } - public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.Solid.MousePointer }; + public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorSelect }; public override PlacementBlueprint CreatePlacementBlueprint() => null; } diff --git a/osu.Game/Rulesets/ILegacyRuleset.cs b/osu.Game/Rulesets/ILegacyRuleset.cs index 24aa672219..18d86f477a 100644 --- a/osu.Game/Rulesets/ILegacyRuleset.cs +++ b/osu.Game/Rulesets/ILegacyRuleset.cs @@ -1,7 +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; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Scoring.Legacy; namespace osu.Game.Rulesets { @@ -14,6 +15,12 @@ namespace osu.Game.Rulesets /// int LegacyID { get; } + /// + /// Retrieves the number of mania keys required to play the beatmap. + /// + /// + int GetKeyCount(IBeatmapInfo beatmapInfo) => 0; + ILegacyScoreSimulator CreateLegacyScoreSimulator(); } } diff --git a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs index d5f586dc35..7330f138ce 100644 --- a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs +++ b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs @@ -38,18 +38,30 @@ namespace osu.Game.Rulesets.Judgements /// public virtual void PlayAnimation() { - switch (Result) + // TODO: make these better. currently they are using a text `-` and it's not centered properly. + // Should be an explicit drawable. + // + // When this is done, remove the [Description] attributes from HitResults which were added for this purpose. + if (Result == HitResult.IgnoreMiss || Result == HitResult.LargeTickMiss) { - case HitResult.Miss: - this.ScaleTo(1.6f); - this.ScaleTo(1, 100, Easing.In); + this.RotateTo(-45); + this.ScaleTo(1.6f); + this.ScaleTo(1.2f, 100, Easing.In); - this.MoveTo(Vector2.Zero); - this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + this.FadeOutFromOne(400); + return; + } - this.RotateTo(0); - this.RotateTo(40, 800, Easing.InQuint); - break; + if (Result.IsMiss()) + { + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); + + this.MoveTo(Vector2.Zero); + this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + this.RotateTo(0); + this.RotateTo(40, 800, Easing.InQuint); } this.FadeOutFromOne(800); diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 15434fcc04..b4686c52f3 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -133,12 +133,11 @@ namespace osu.Game.Rulesets.Judgements case HitResult.None: break; - case HitResult.Miss: - ApplyMissAnimations(); - break; - default: - ApplyHitAnimations(); + if (Result.Type.IsHit()) + ApplyHitAnimations(); + else + ApplyMissAnimations(); break; } diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index 99dce82ec2..93386de483 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -11,16 +11,6 @@ namespace osu.Game.Rulesets.Judgements /// public class Judgement { - /// - /// The score awarded for a small bonus. - /// - public const int SMALL_BONUS_SCORE = 10; - - /// - /// The score awarded for a large bonus. - /// - public const int LARGE_BONUS_SCORE = 50; - /// /// The default health increase for a maximum judgement, as a proportion of total health. /// By default, each maximum judgement restores 5% of total health. @@ -35,7 +25,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 { @@ -50,6 +73,7 @@ namespace osu.Game.Rulesets.Judgements return HitResult.SmallTickMiss; case HitResult.LargeTickHit: + case HitResult.SliderTailHit: return HitResult.LargeTickMiss; default: @@ -58,23 +82,11 @@ namespace osu.Game.Rulesets.Judgements } } - /// - /// The numeric score representation for the maximum achievable result. - /// - public int MaxNumericResult => ToNumericResult(MaxResult); - /// /// The health increase for the maximum achievable result. /// public double MaxHealthIncrease => HealthIncreaseFor(MaxResult); - /// - /// Retrieves the numeric score representation of a . - /// - /// The to find the numeric score representation for. - /// The numeric score representation of . - public int NumericResultFor(JudgementResult result) => ToNumericResult(result.Type); - /// /// Retrieves the numeric health increase of a . /// @@ -93,6 +105,7 @@ namespace osu.Game.Rulesets.Judgements case HitResult.SmallTickMiss: return -DEFAULT_MAX_HEALTH_INCREASE * 0.5; + case HitResult.SliderTailHit: case HitResult.LargeTickHit: return DEFAULT_MAX_HEALTH_INCREASE; @@ -132,42 +145,6 @@ namespace osu.Game.Rulesets.Judgements /// The numeric health increase of . public double HealthIncreaseFor(JudgementResult result) => HealthIncreaseFor(result.Type); - public override string ToString() => $"MaxResult:{MaxResult} MaxScore:{MaxNumericResult}"; - - public static int ToNumericResult(HitResult result) - { - switch (result) - { - default: - return 0; - - case HitResult.SmallTickHit: - return 10; - - case HitResult.LargeTickHit: - return 30; - - case HitResult.Meh: - return 50; - - case HitResult.Ok: - return 100; - - case HitResult.Good: - return 200; - - case HitResult.Great: - return 300; - - case HitResult.Perfect: - return 315; - - case HitResult.SmallBonus: - return SMALL_BONUS_SCORE; - - case HitResult.LargeBonus: - return LARGE_BONUS_SCORE; - } - } + public override string ToString() => $"MaxResult:{MaxResult}"; } } diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs index c67f8b9fd5..b781a13929 100644 --- a/osu.Game/Rulesets/Judgements/JudgementResult.cs +++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs @@ -54,6 +54,11 @@ namespace osu.Game.Rulesets.Judgements /// public double TimeAbsolute => RawTime != null ? Math.Min(RawTime.Value, HitObject.GetEndTime() + HitObject.MaximumJudgementOffset) : HitObject.GetEndTime(); + /// + /// The gameplay rate at the time this occurred. + /// + public double? GameplayRate { get; internal set; } + /// /// The combo prior to this occurring. /// @@ -89,6 +94,11 @@ namespace osu.Game.Rulesets.Judgements /// public bool IsHit => Type.IsHit(); + /// + /// The increase in health resulting from this judgement result. + /// + public double HealthIncrease => Judgement.HealthIncreaseFor(this); + /// /// Creates a new . /// @@ -107,6 +117,6 @@ namespace osu.Game.Rulesets.Judgements RawTime = null; } - public override string ToString() => $"{Type} (Score:{Judgement.NumericResultFor(this)} HP:{Judgement.HealthIncreaseFor(this)} {Judgement})"; + public override string ToString() => $"{Type} ({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/IApplicableHealthProcessor.cs b/osu.Game/Rulesets/Mods/IApplicableHealthProcessor.cs new file mode 100644 index 0000000000..be46828069 --- /dev/null +++ b/osu.Game/Rulesets/Mods/IApplicableHealthProcessor.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.Scoring; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// Interface for a that provides its own health processor. + /// + public interface IApplicableHealthProcessor + { + /// + /// Creates the . May be null to use the ruleset default. + /// + HealthProcessor? CreateHealthProcessor(double drainStartTime); + } +} diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index 05b2510e53..744d02a4fa 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. /// @@ -52,6 +59,13 @@ namespace osu.Game.Rulesets.Mods /// bool ValidForMultiplayerAsFreeMod { get; } + /// + /// Indicates that this mod is always permitted in scenarios wherein a user is submitting a score regardless of other circumstances. + /// Intended for mods that are informational in nature and do not really affect gameplay by themselves, + /// but are more of a gauge of increased/decreased difficulty due to the user's configuration (e.g. ). + /// + bool AlwaysValidForSubmission { get; } + /// /// Create a fresh instance based on this mod. /// diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index f9812d6c00..0500b49513 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Reflection; using Newtonsoft.Json; @@ -27,6 +28,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 +75,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))); @@ -91,15 +108,59 @@ namespace osu.Game.Rulesets.Mods [JsonIgnore] public virtual bool HasImplementation => this is IApplicableMod; + /// + /// Whether this mod can be played by a real human user. + /// Non-user-playable mods are not viable for single-player score submission. + /// + /// + /// + /// is user-playable. + /// is not user-playable. + /// + /// [JsonIgnore] public virtual bool UserPlayable => true; + /// + /// Whether this mod can be specified as a "required" mod in a multiplayer context. + /// + /// + /// + /// is valid for multiplayer. + /// + /// is valid for multiplayer as long as it is a required mod, + /// as that ensures the same duration of gameplay for all users in the room. + /// + /// + /// is not valid for multiplayer, as it leads to varying + /// gameplay duration depending on how the users in the room play. + /// + /// is not valid for multiplayer. + /// + /// [JsonIgnore] public virtual bool ValidForMultiplayer => true; + /// + /// Whether this mod can be specified as a "free" or "allowed" mod in a multiplayer context. + /// + /// + /// + /// is valid for multiplayer as a free mod. + /// + /// is not valid for multiplayer as a free mod, + /// as it could to varying gameplay duration between users in the room depending on whether they picked it. + /// + /// is not valid for multiplayer as a free mod. + /// + /// [JsonIgnore] public virtual bool ValidForMultiplayerAsFreeMod => true; + /// + [JsonIgnore] + public virtual bool AlwaysValidForSubmission => false; + /// /// Whether this mod requires configuration to apply changes to the game. /// @@ -134,7 +195,7 @@ namespace osu.Game.Rulesets.Mods /// /// Whether all settings in this mod are set to their default state. /// - protected virtual bool UsesDefaultConfiguration => SettingsBindables.All(s => s.IsDefault); + public virtual bool UsesDefaultConfiguration => SettingsBindables.All(s => s.IsDefault); /// /// Creates a copy of this initialised to a default state. @@ -224,7 +285,7 @@ namespace osu.Game.Rulesets.Mods if (!(target is IParseable parseable)) throw new InvalidOperationException($"Bindable type {target.GetType().ReadableName()} is not {nameof(IParseable)}."); - parseable.Parse(source); + parseable.Parse(source, CultureInfo.InvariantCulture); } } diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index e7127abcf0..77aa5cdc15 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; @@ -30,12 +31,12 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 0.5; - public override bool ValidForMultiplayer => false; - public override bool ValidForMultiplayerAsFreeMod => false; + public sealed override bool ValidForMultiplayer => false; + public sealed override bool ValidForMultiplayerAsFreeMod => false; 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 a3a4adc53d..302cdf69c0 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -20,11 +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 override bool UserPlayable => false; - public override bool ValidForMultiplayer => false; - public override bool ValidForMultiplayerAsFreeMod => false; + public sealed override bool UserPlayable => false; + public sealed override bool ValidForMultiplayer => false; + public sealed override bool ValidForMultiplayerAsFreeMod => false; - public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModAdaptiveSpeed) }; + public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModAdaptiveSpeed), typeof(ModTouchDevice) }; public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; diff --git a/osu.Game/Rulesets/Mods/ModBlockFail.cs b/osu.Game/Rulesets/Mods/ModBlockFail.cs deleted file mode 100644 index cdfb36ebbc..0000000000 --- a/osu.Game/Rulesets/Mods/ModBlockFail.cs +++ /dev/null @@ -1,31 +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.Bindables; -using osu.Game.Configuration; -using osu.Game.Screens.Play; - -namespace osu.Game.Rulesets.Mods -{ - public abstract class ModBlockFail : Mod, IApplicableFailOverride, IApplicableToHUD, IReadFromConfig - { - private readonly Bindable showHealthBar = new Bindable(); - - /// - /// We never fail, 'yo. - /// - public bool PerformFail() => false; - - public bool RestartOnFail => false; - - public void ReadFromConfig(OsuConfigManager config) - { - config.BindWith(OsuSetting.ShowHealthDisplayWhenCantFail, showHealthBar); - } - - public void ApplyToHUD(HUDOverlay overlay) - { - overlay.ShowHealthBar.BindTo(showHealthBar); - } - } -} diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index ae661c5f25..7c88a8a588 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -23,14 +23,14 @@ namespace osu.Game.Rulesets.Mods } } - public class ModCinema : ModAutoplay, IApplicableToHUD, IApplicableToPlayer + public class ModCinema : ModAutoplay, IApplicableToHUD, IApplicableToPlayer, IApplicableFailOverride { public override string Name => "Cinema"; public override string Acronym => "CN"; public override IconUsage? Icon => OsuIcon.ModCinema; public override LocalisableString Description => "Watch the video without visual distractions."; - public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAutoplay)).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModAutoplay), typeof(ModNoFail), typeof(ModFailCondition) }).ToArray(); public void ApplyToHUD(HUDOverlay overlay) { @@ -45,5 +45,9 @@ namespace osu.Game.Rulesets.Mods player.BreakOverlay.Hide(); } + + public bool PerformFail() => false; + + public bool RestartOnFail => false; } } diff --git a/osu.Game/Rulesets/Mods/ModClassic.cs b/osu.Game/Rulesets/Mods/ModClassic.cs index 55b16297e2..16cb928bd4 100644 --- a/osu.Game/Rulesets/Mods/ModClassic.cs +++ b/osu.Game/Rulesets/Mods/ModClassic.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mods public override string Acronym => "CL"; - public override double ScoreMultiplier => 1; + public override double ScoreMultiplier => 0.96; public override IconUsage? Icon => FontAwesome.Solid.History; diff --git a/osu.Game/Rulesets/Mods/ModDaycore.cs b/osu.Game/Rulesets/Mods/ModDaycore.cs index de1a5ab56c..09b35c249e 100644 --- a/osu.Game/Rulesets/Mods/ModDaycore.cs +++ b/osu.Game/Rulesets/Mods/ModDaycore.cs @@ -5,21 +5,37 @@ using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Overlays.Settings; 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", SettingControlType = typeof(MultiplierSettingsSlider))] + 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 +45,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 e671c065cf..0b229766c1 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) }; + public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModCinema) }; [SettingSource("Restart on fail", "Automatically restarts when failed.")] public BindableBool Restart { get; } = new BindableBool(); diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index 215fc877dc..dc2ad6f47e 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -56,9 +56,6 @@ namespace osu.Game.Rulesets.Mods public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) { Combo.BindTo(scoreProcessor.Combo); - - // Default value of ScoreProcessor's Rank in Flashlight Mod should be SS+ - scoreProcessor.Rank.Value = ScoreRank.XH; } public ScoreRank AdjustRank(ScoreRank rank, double accuracy) @@ -252,7 +249,7 @@ namespace osu.Game.Rulesets.Mods private IUniformBuffer? flashlightParametersBuffer; - public override void Draw(IRenderer renderer) + protected override void Draw(IRenderer renderer) { base.Draw(renderer); 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/ModHidden.cs b/osu.Game/Rulesets/Mods/ModHidden.cs index 5a8226115f..8b25768575 100644 --- a/osu.Game/Rulesets/Mods/ModHidden.cs +++ b/osu.Game/Rulesets/Mods/ModHidden.cs @@ -17,8 +17,6 @@ namespace osu.Game.Rulesets.Mods public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) { - // Default value of ScoreProcessor's Rank in Hidden Mod should be SS+ - scoreProcessor.Rank.Value = ScoreRank.XH; } public ScoreRank AdjustRank(ScoreRank rank, double accuracy) diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 9b1f7d5cf7..b42927256c 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -11,30 +11,43 @@ 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.Overlays.Settings; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; 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", SettingControlType = typeof(MultiplierSettingsSlider))] + 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 +57,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 8c61d948a4..cc451772b2 100644 --- a/osu.Game/Rulesets/Mods/ModNoFail.cs +++ b/osu.Game/Rulesets/Mods/ModNoFail.cs @@ -2,13 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Screens.Play; namespace osu.Game.Rulesets.Mods { - public abstract class ModNoFail : ModBlockFail + public abstract class ModNoFail : Mod, IApplicableFailOverride, IApplicableToHUD, IReadFromConfig { public override string Name => "No Fail"; public override string Acronym => "NF"; @@ -16,6 +19,25 @@ 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) }; + public override Type[] IncompatibleMods => new[] { typeof(ModFailCondition), typeof(ModCinema) }; + + private readonly Bindable showHealthBar = new Bindable(); + + /// + /// We never fail, 'yo. + /// + public bool PerformFail() => false; + + public bool RestartOnFail => false; + + public void ReadFromConfig(OsuConfigManager config) + { + config.BindWith(OsuSetting.ShowHealthDisplayWhenCantFail, showHealthBar); + } + + public void ApplyToHUD(HUDOverlay overlay) + { + overlay.ShowHealthBar.BindTo(showHealthBar); + } } } diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index 6f0bb7ad3b..0ba40ba070 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -28,7 +28,9 @@ namespace osu.Game.Rulesets.Mods } protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) - => result.Type.AffectsAccuracy() + => (isRelevantResult(result.Judgement.MinResult) || isRelevantResult(result.Judgement.MaxResult) || isRelevantResult(result.Type)) && result.Type != result.Judgement.MaxResult; + + private bool isRelevantResult(HitResult result) => result.AffectsAccuracy() || result.AffectsCombo(); } } diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 7b55ba4ad0..e5af758b4f 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -9,14 +9,11 @@ namespace osu.Game.Rulesets.Mods { public abstract class ModRateAdjust : Mod, IApplicableToRate { - public override bool ValidForMultiplayerAsFreeMod => false; + public sealed override bool ValidForMultiplayerAsFreeMod => false; 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/ModRelax.cs b/osu.Game/Rulesets/Mods/ModRelax.cs index 49c10339ee..3d672b5ef8 100644 --- a/osu.Game/Rulesets/Mods/ModRelax.cs +++ b/osu.Game/Rulesets/Mods/ModRelax.cs @@ -7,13 +7,13 @@ using osu.Game.Graphics; namespace osu.Game.Rulesets.Mods { - public abstract class ModRelax : ModBlockFail + public abstract class ModRelax : Mod { public override string Name => "Relax"; public override string Acronym => "RX"; public override IconUsage? Icon => OsuIcon.ModRelax; public override ModType Type => ModType.Automation; public override double ScoreMultiplier => 0.1; - public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModNoFail), typeof(ModFailCondition) }; + public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay) }; } } diff --git a/osu.Game/Rulesets/Mods/ModScoreV2.cs b/osu.Game/Rulesets/Mods/ModScoreV2.cs index 6d56b2d86f..6a77cafa30 100644 --- a/osu.Game/Rulesets/Mods/ModScoreV2.cs +++ b/osu.Game/Rulesets/Mods/ModScoreV2.cs @@ -9,12 +9,15 @@ 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 sealed 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; + public override bool ValidForMultiplayer => false; + public override bool ValidForMultiplayerAsFreeMod => false; } } diff --git a/osu.Game/Rulesets/Mods/ModSynesthesia.cs b/osu.Game/Rulesets/Mods/ModSynesthesia.cs index 23cb135c50..9084127f33 100644 --- a/osu.Game/Rulesets/Mods/ModSynesthesia.cs +++ b/osu.Game/Rulesets/Mods/ModSynesthesia.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mods 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 double ScoreMultiplier => 0.8; public override ModType Type => ModType.Fun; } } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 7285315c3b..36e4522771 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,16 +21,16 @@ 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")] public abstract BindableBool AdjustPitch { get; } - public override bool ValidForMultiplayerAsFreeMod => false; + public sealed override bool ValidForMultiplayerAsFreeMod => false; public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) }; @@ -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/ModTouchDevice.cs b/osu.Game/Rulesets/Mods/ModTouchDevice.cs new file mode 100644 index 0000000000..e91a398700 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModTouchDevice.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 System; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics; + +namespace osu.Game.Rulesets.Mods +{ + public class ModTouchDevice : Mod, IApplicableMod + { + public sealed override string Name => "Touch Device"; + public sealed override string Acronym => "TD"; + public sealed override IconUsage? Icon => OsuIcon.PlayStyleTouch; + public sealed override LocalisableString Description => "Automatically applied to plays on devices with a touchscreen."; + public sealed override double ScoreMultiplier => 1; + public sealed override ModType Type => ModType.System; + public sealed override bool ValidForMultiplayer => false; + public sealed override bool ValidForMultiplayerAsFreeMod => false; + public sealed override bool AlwaysValidForSubmission => true; + public override Type[] IncompatibleMods => new[] { typeof(ICreateReplayData) }; + } +} diff --git a/osu.Game/Rulesets/Mods/RateAdjustModHelper.cs b/osu.Game/Rulesets/Mods/RateAdjustModHelper.cs new file mode 100644 index 0000000000..8bc481921f --- /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) + return 1 + value / 5; + else + return 0.6 + 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/Mods/UnknownMod.cs b/osu.Game/Rulesets/Mods/UnknownMod.cs index abe05996ff..31fc09b0a6 100644 --- a/osu.Game/Rulesets/Mods/UnknownMod.cs +++ b/osu.Game/Rulesets/Mods/UnknownMod.cs @@ -5,7 +5,7 @@ using osu.Framework.Localisation; namespace osu.Game.Rulesets.Mods { - public class UnknownMod : Mod + public sealed class UnknownMod : Mod { /// /// The acronym of the mod which could not be resolved. diff --git a/osu.Game/Rulesets/Objects/BezierConverter.cs b/osu.Game/Rulesets/Objects/BezierConverter.cs index 0c878fa1fd..638975630e 100644 --- a/osu.Game/Rulesets/Objects/BezierConverter.cs +++ b/osu.Game/Rulesets/Objects/BezierConverter.cs @@ -68,32 +68,35 @@ namespace osu.Game.Rulesets.Objects // The current vertex ends the segment var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); - var segmentType = controlPoints[start].Type ?? PathType.Linear; + var segmentType = controlPoints[start].Type ?? PathType.LINEAR; - switch (segmentType) + switch (segmentType.Type) { - case PathType.Catmull: + case SplineType.Catmull: result.AddRange(from segment in ConvertCatmullToBezierAnchors(segmentVertices) from v in segment select v + position); - break; - case PathType.Linear: + case SplineType.Linear: result.AddRange(from segment in ConvertLinearToBezierAnchors(segmentVertices) from v in segment select v + position); - break; - case PathType.PerfectCurve: + case SplineType.PerfectCurve: result.AddRange(ConvertCircleToBezierAnchors(segmentVertices).Select(v => v + position)); - break; - default: + case SplineType.BSpline: + if (segmentType.Degree != null) + throw new NotImplementedException("BSpline conversion of arbitrary degree is not implemented."); + foreach (Vector2 v in segmentVertices) { result.Add(v + position); } break; + + default: + throw new ArgumentOutOfRangeException(nameof(segmentType.Type), segmentType.Type, "Unsupported segment type found when converting to legacy Bezier"); } // Start the new segment at the current vertex @@ -104,7 +107,7 @@ namespace osu.Game.Rulesets.Objects } /// - /// Converts a path of control points to an identical path using only Bezier type control points. + /// Converts a path of control points to an identical path using only BEZIER type control points. /// /// The control points of the path. /// The list of bezier control points. @@ -124,49 +127,56 @@ namespace osu.Game.Rulesets.Objects // The current vertex ends the segment var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); - var segmentType = controlPoints[start].Type ?? PathType.Linear; + var segmentType = controlPoints[start].Type ?? PathType.LINEAR; - switch (segmentType) + switch (segmentType.Type) { - case PathType.Catmull: + case SplineType.Catmull: foreach (var segment in ConvertCatmullToBezierAnchors(segmentVertices)) { for (int j = 0; j < segment.Length - 1; j++) { - result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.Bezier : null)); + result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null)); } } break; - case PathType.Linear: + case SplineType.Linear: foreach (var segment in ConvertLinearToBezierAnchors(segmentVertices)) { for (int j = 0; j < segment.Length - 1; j++) { - result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.Bezier : null)); + result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null)); } } break; - case PathType.PerfectCurve: + case SplineType.PerfectCurve: var circleResult = ConvertCircleToBezierAnchors(segmentVertices); for (int j = 0; j < circleResult.Length - 1; j++) { - result.Add(new PathControlPoint(circleResult[j], j == 0 ? PathType.Bezier : null)); + result.Add(new PathControlPoint(circleResult[j], j == 0 ? PathType.BEZIER : null)); + } + + break; + + case SplineType.BSpline: + var bSplineResult = segmentType.Degree == null + ? segmentVertices + : PathApproximator.BSplineToBezier(segmentVertices, segmentType.Degree.Value); + + for (int j = 0; j < bSplineResult.Length - 1; j++) + { + result.Add(new PathControlPoint(bSplineResult[j], j == 0 ? PathType.BEZIER : null)); } break; default: - for (int j = 0; j < segmentVertices.Length - 1; j++) - { - result.Add(new PathControlPoint(segmentVertices[j], j == 0 ? PathType.Bezier : null)); - } - - break; + throw new ArgumentOutOfRangeException(nameof(segmentType.Type), segmentType.Type, "Unsupported segment type found when converting to legacy Bezier"); } // Start the new segment at the current vertex diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index bf649a0a15..bce28361cb 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -159,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 . /// @@ -181,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; @@ -260,7 +283,6 @@ namespace osu.Game.Rulesets.Objects.Drawables } StartTimeBindable.BindTo(HitObject.StartTimeBindable); - StartTimeBindable.BindValueChanged(onStartTimeChanged); if (HitObject is IHasComboInformation combo) { @@ -311,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; @@ -333,6 +352,8 @@ namespace osu.Game.Rulesets.Objects.Drawables 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; @@ -375,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() @@ -394,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); } @@ -571,7 +599,9 @@ namespace osu.Game.Rulesets.Objects.Drawables float balanceAdjustAmount = positionalHitsoundsLevel.Value * 2; double returnedValue = balanceAdjustAmount * (position - 0.5f); - return returnedValue; + // Rounded to reduce the overhead of audio adjustments (which are currently bindable heavy). + // Balance is very hard to perceive in small increments anyways. + return Math.Round(returnedValue, 2); } /// @@ -667,6 +697,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( @@ -674,6 +706,7 @@ namespace osu.Game.Rulesets.Objects.Drawables } Result.RawTime = Time.Current; + Result.GameplayRate = (Clock as IGameplayClock)?.GetTrueGameplayRate() ?? Clock.Rate; if (Result.HasResult) updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index e87fb56b73..9781a86c3d 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/Legacy/Catch/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs index 12b4812824..96c779e79b 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs @@ -9,16 +9,12 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch /// /// Legacy osu!catch Hit-type, used for parsing Beatmaps. /// - internal sealed class ConvertHit : ConvertHitObject, IHasPosition, IHasCombo + internal sealed class ConvertHit : ConvertHitObject, IHasPosition { public float X => Position.X; public float Y => Position.Y; public Vector2 Position { get; set; } - - public bool NewCombo { get; set; } - - public int ComboOffset { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs index 4861e8b3f7..a5c1a73fa7 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs @@ -14,44 +14,31 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch /// public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser { + private ConvertHitObject lastObject; + public ConvertHitObjectParser(double offset, int formatVersion) : base(offset, formatVersion) { } - private bool forceNewCombo; - private int extraComboOffset; - protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) { - newCombo |= forceNewCombo; - comboOffset += extraComboOffset; - - forceNewCombo = false; - extraComboOffset = 0; - - return new ConvertHit + return lastObject = new ConvertHit { Position = position, - NewCombo = newCombo, - ComboOffset = comboOffset + NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, + ComboOffset = newCombo ? comboOffset : 0 }; } protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, IList> nodeSamples) { - newCombo |= forceNewCombo; - comboOffset += extraComboOffset; - - forceNewCombo = false; - extraComboOffset = 0; - - return new ConvertSlider + return lastObject = new ConvertSlider { Position = position, - NewCombo = FirstObject || newCombo, - ComboOffset = comboOffset, + NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, + ComboOffset = newCombo ? comboOffset : 0, Path = new SliderPath(controlPoints, length), NodeSamples = nodeSamples, RepeatCount = repeatCount @@ -60,20 +47,17 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) { - // Convert spinners don't create the new combo themselves, but force the next non-spinner hitobject to create a new combo - // Their combo offset is still added to that next hitobject's combo index - forceNewCombo |= FormatVersion <= 8 || newCombo; - extraComboOffset += comboOffset; - - return new ConvertSpinner + return lastObject = new ConvertSpinner { - Duration = duration + Duration = duration, + NewCombo = newCombo + // Spinners cannot have combo offset. }; } protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) { - return null; + return lastObject = null; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs index fb1afed3b4..bcf1c7fae2 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs @@ -9,16 +9,12 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch /// /// Legacy osu!catch Slider-type, used for parsing Beatmaps. /// - internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition, IHasCombo + internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition { public float X => Position.X; public float Y => Position.Y; public Vector2 Position { get; set; } - - public bool NewCombo { get; set; } - - public int ComboOffset { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs index 014494ec54..5ef3d51cb3 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs @@ -8,16 +8,12 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch /// /// Legacy osu!catch Spinner-type, used for parsing Beatmaps. /// - internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition, IHasCombo + internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition { public double EndTime => StartTime + Duration; public double Duration { get; set; } public float X => 256; // Required for CatchBeatmapConverter - - public bool NewCombo { get; set; } - - public int ComboOffset { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs index 54dbd28c76..bb36aab0b3 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Objects.Legacy @@ -9,8 +10,12 @@ namespace osu.Game.Rulesets.Objects.Legacy /// /// A hit object only used for conversion, not actual gameplay. /// - internal abstract class ConvertHitObject : HitObject + internal abstract class ConvertHitObject : HitObject, IHasCombo { + public bool NewCombo { get; set; } + + public int ComboOffset { get; set; } + public override Judgement CreateJudgement() => new IgnoreJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 3dbe7b6519..f9e32fe26f 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -190,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") @@ -219,16 +224,19 @@ namespace osu.Game.Rulesets.Objects.Legacy { default: case 'C': - return PathType.Catmull; + return PathType.CATMULL; case 'B': - return PathType.Bezier; + if (input.Length > 1 && int.TryParse(input.Substring(1), out int degree) && degree > 0) + return PathType.BSpline(degree); + + return PathType.BEZIER; case 'L': - return PathType.Linear; + return PathType.LINEAR; case 'P': - return PathType.PerfectCurve; + return PathType.PERFECT_CURVE; } } @@ -265,8 +273,8 @@ namespace osu.Game.Rulesets.Objects.Legacy while (++endIndex < pointSplit.Length) { - // Keep incrementing endIndex while it's not the start of a new segment (indicated by having a type descriptor of length 1). - if (pointSplit[endIndex].Length > 1) + // Keep incrementing endIndex while it's not the start of a new segment (indicated by having an alpha character at position 0). + if (!char.IsLetter(pointSplit[endIndex][0])) continue; // Multi-segmented sliders DON'T contain the end point as part of the current segment as it's assumed to be the start of the next segment. @@ -315,14 +323,14 @@ namespace osu.Game.Rulesets.Objects.Legacy readPoint(endPoint, offset, out vertices[^1]); // Edge-case rules (to match stable). - if (type == PathType.PerfectCurve) + if (type == PathType.PERFECT_CURVE) { if (vertices.Length != 3) - type = PathType.Bezier; + type = PathType.BEZIER; else if (isLinear(vertices)) { // osu-stable special-cased colinear perfect curves to a linear path - type = PathType.Linear; + type = PathType.LINEAR; } } @@ -344,10 +352,10 @@ namespace osu.Game.Rulesets.Objects.Legacy if (vertices[endIndex].Position != vertices[endIndex - 1].Position) continue; - // Legacy Catmull sliders don't support multiple segments, so adjacent Catmull segments should be treated as a single one. + // Legacy CATMULL sliders don't support multiple segments, so adjacent CATMULL segments should be treated as a single one. // Importantly, this is not applied to the first control point, which may duplicate the slider path's position // resulting in a duplicate (0,0) control point in the resultant list. - if (type == PathType.Catmull && endIndex > 1 && FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION) + if (type == PathType.CATMULL && endIndex > 1 && FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION) continue; // The last control point of each segment is not allowed to start a new implicit segment. 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..2a5a11161b --- /dev/null +++ b/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs @@ -0,0 +1,96 @@ +// 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); + } + + public static int CalculateDifficultyPeppyStars(BeatmapDifficulty difficulty, int objectCount, int drainLength) + { + /* + * WARNING: DO NOT TOUCH IF YOU DO NOT KNOW WHAT YOU ARE DOING + * + * It so happens that in stable, due to .NET Framework internals, float math would be performed + * using x87 registers and opcodes. + * .NET (Core) however uses SSE instructions on 32- and 64-bit words. + * x87 registers are _80 bits_ wide. Which is notably wider than _both_ float and double. + * Therefore, on a significant number of beatmaps, the rounding would not produce correct values. + * + * Thus, to crudely - but, seemingly *mostly* accurately, after checking across all ranked maps - emulate this, + * use `decimal`, which is slow, but has bigger precision than `double`. + * At the time of writing, there is _one_ ranked exception to this - namely https://osu.ppy.sh/beatmapsets/1156087#osu/2625853 - + * but it is considered an "acceptable casualty", since in that case scores aren't inflated by _that_ much compared to others. + */ + + decimal objectToDrainRatio = drainLength != 0 + ? Math.Clamp((decimal)objectCount / drainLength * 8, 0, 16) + : 16; + + /* + * Notably, THE `double` CASTS BELOW ARE IMPORTANT AND MUST REMAIN. + * Their goal is to trick the compiler / runtime into NOT promoting from single-precision float, as doing so would prompt it + * to attempt to "silently" fix the single-precision values when converting to decimal, + * which is NOT what the x87 FPU does. + */ + + decimal drainRate = (decimal)(double)difficulty.DrainRate; + decimal overallDifficulty = (decimal)(double)difficulty.OverallDifficulty; + decimal circleSize = (decimal)(double)difficulty.CircleSize; + + return (int)Math.Round((drainRate + overallDifficulty + circleSize + objectToDrainRatio) / 38 * 5); + } + } +} diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs index 069366bad3..b7cd4b0dcc 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs @@ -9,16 +9,12 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu /// /// Legacy osu! Hit-type, used for parsing Beatmaps. /// - internal sealed class ConvertHit : ConvertHitObject, IHasPosition, IHasCombo + internal sealed class ConvertHit : ConvertHitObject, IHasPosition { public Vector2 Position { get; set; } public float X => Position.X; public float Y => Position.Y; - - public bool NewCombo { get; set; } - - public int ComboOffset { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs index 7a88a31bd5..43c346b621 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs @@ -14,44 +14,31 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu /// public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser { + private ConvertHitObject lastObject; + public ConvertHitObjectParser(double offset, int formatVersion) : base(offset, formatVersion) { } - private bool forceNewCombo; - private int extraComboOffset; - protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) { - newCombo |= forceNewCombo; - comboOffset += extraComboOffset; - - forceNewCombo = false; - extraComboOffset = 0; - - return new ConvertHit + return lastObject = new ConvertHit { Position = position, - NewCombo = FirstObject || newCombo, - ComboOffset = comboOffset + NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, + ComboOffset = newCombo ? comboOffset : 0 }; } protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, IList> nodeSamples) { - newCombo |= forceNewCombo; - comboOffset += extraComboOffset; - - forceNewCombo = false; - extraComboOffset = 0; - - return new ConvertSlider + return lastObject = new ConvertSlider { Position = position, - NewCombo = FirstObject || newCombo, - ComboOffset = comboOffset, + NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, + ComboOffset = newCombo ? comboOffset : 0, Path = new SliderPath(controlPoints, length), NodeSamples = nodeSamples, RepeatCount = repeatCount @@ -60,21 +47,18 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) { - // Convert spinners don't create the new combo themselves, but force the next non-spinner hitobject to create a new combo - // Their combo offset is still added to that next hitobject's combo index - forceNewCombo |= FormatVersion <= 8 || newCombo; - extraComboOffset += comboOffset; - - return new ConvertSpinner + return lastObject = new ConvertSpinner { Position = position, - Duration = duration + Duration = duration, + NewCombo = newCombo + // Spinners cannot have combo offset. }; } protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) { - return null; + return lastObject = null; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs index 790af6cfc1..8c37154f95 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs @@ -9,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, IHasGenerateTicks + internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition, IHasGenerateTicks { public Vector2 Position { get; set; } @@ -17,10 +17,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public float Y => Position.Y; - 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 e9e5ca8c94..d6e24b6bbf 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu /// /// Legacy osu! Spinner-type, used for parsing Beatmaps. /// - internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasPosition, IHasCombo + internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasPosition { public double Duration { get; set; } @@ -20,9 +20,5 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public float X => Position.X; public float Y => Position.Y; - - public bool NewCombo { get; set; } - - public int ComboOffset { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs index 3b45acc7bb..1b0176cae5 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs @@ -153,6 +153,9 @@ namespace osu.Game.Rulesets.Objects.Pooling protected override bool CheckChildrenLife() { + if (!IsPresent) + return false; + bool aliveChanged = base.CheckChildrenLife(); aliveChanged |= lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension); return aliveChanged; diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index 7013d32cbc..9b8375f208 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -10,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. @@ -76,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 @@ -173,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 028f8b6839..dc71608132 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Objects /// public readonly Bindable ExpectedDistance = new Bindable(); - public bool HasValidLength => Distance > 0; + public bool HasValidLength => Precision.DefinitelyBigger(Distance, 0); /// /// The control points of the path. @@ -40,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 . /// @@ -196,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() @@ -243,16 +260,26 @@ namespace osu.Game.Rulesets.Objects // The current vertex ends the segment var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); - var segmentType = ControlPoints[start].Type ?? PathType.Linear; + 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; @@ -261,16 +288,16 @@ namespace osu.Game.Rulesets.Objects private List calculateSubPath(ReadOnlySpan subControlPoints, PathType type) { - switch (type) + switch (type.Type) { - case PathType.Linear: - return PathApproximator.ApproximateLinear(subControlPoints); + case SplineType.Linear: + return PathApproximator.LinearToPiecewiseLinear(subControlPoints); - case PathType.PerfectCurve: + case SplineType.PerfectCurve: if (subControlPoints.Length != 3) break; - List subPath = PathApproximator.ApproximateCircularArc(subControlPoints); + List subPath = PathApproximator.CircularArcToPiecewiseLinear(subControlPoints); // If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable bezier approximation. if (subPath.Count == 0) @@ -278,11 +305,11 @@ namespace osu.Game.Rulesets.Objects return subPath; - case PathType.Catmull: - return PathApproximator.ApproximateCatmull(subControlPoints); + case SplineType.Catmull: + return PathApproximator.CatmullToPiecewiseLinear(subControlPoints); } - return PathApproximator.ApproximateBezier(subControlPoints); + return PathApproximator.BSplineToPiecewiseLinear(subControlPoints, type.Degree ?? subControlPoints.Length); } private void calculateLength() @@ -298,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; @@ -319,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..29b34ae4f0 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.PERFECT_CURVE && 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/Types/IHasCombo.cs b/osu.Game/Rulesets/Objects/Types/IHasCombo.cs index d1a4683a1d..5de5424bdc 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasCombo.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasCombo.cs @@ -16,6 +16,12 @@ namespace osu.Game.Rulesets.Objects.Types /// /// When starting a new combo, the offset of the new combo relative to the current one. /// + /// + /// This is generally a setting provided by a beatmap creator to choreograph interesting colour patterns + /// which can only be achieved by skipping combo colours with per-hitobject level. + /// + /// It is exposed via . + /// int ComboOffset { get; } } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs index d34e71021f..3aa68197ec 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs @@ -12,6 +12,9 @@ namespace osu.Game.Rulesets.Objects.Types /// public interface IHasComboInformation : IHasCombo { + /// + /// Bindable exposure of . + /// Bindable IndexInCurrentComboBindable { get; } /// @@ -19,13 +22,21 @@ namespace osu.Game.Rulesets.Objects.Types /// int IndexInCurrentCombo { get; set; } + /// + /// Bindable exposure of . + /// Bindable ComboIndexBindable { get; } /// /// The index of this combo in relation to the beatmap. + /// + /// In other words, this is incremented by 1 each time a is reached. /// int ComboIndex { get; set; } + /// + /// Bindable exposure of . + /// Bindable ComboIndexWithOffsetsBindable { get; } /// @@ -39,6 +50,9 @@ namespace osu.Game.Rulesets.Objects.Types /// new bool NewCombo { get; set; } + /// + /// Bindable exposure of . + /// Bindable LastInComboBindable { get; } /// 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/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/Objects/Types/PathType.cs b/osu.Game/Rulesets/Objects/Types/PathType.cs index 923ce9eba4..23f1ccf0bc 100644 --- a/osu.Game/Rulesets/Objects/Types/PathType.cs +++ b/osu.Game/Rulesets/Objects/Types/PathType.cs @@ -1,13 +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.Bindables; + namespace osu.Game.Rulesets.Objects.Types { - public enum PathType + public enum SplineType { Catmull, - Bezier, + BSpline, Linear, PerfectCurve } + + public readonly struct PathType : IEquatable, IHasDescription + { + /// + /// The type of the spline that should be used to interpret the control points of the path. + /// + public SplineType Type { get; init; } + + /// + /// The degree of a BSpline. Unused if is not . + /// Null means the degree is equal to the number of control points, 1 means linear, 2 means quadratic, etc. + /// + public int? Degree { get; init; } + + public PathType(SplineType splineType) + { + Type = splineType; + Degree = null; + } + + public static readonly PathType CATMULL = new PathType(SplineType.Catmull); + public static readonly PathType BEZIER = new PathType(SplineType.BSpline); + public static readonly PathType LINEAR = new PathType(SplineType.Linear); + public static readonly PathType PERFECT_CURVE = new PathType(SplineType.PerfectCurve); + + public static PathType BSpline(int degree) + { + if (degree <= 0) + throw new ArgumentOutOfRangeException(nameof(degree), "The degree of a B-Spline path must be greater than zero."); + + return new PathType { Type = SplineType.BSpline, Degree = degree }; + } + + public string Description + { + get + { + switch (Type) + { + case SplineType.Catmull: + return "Catmull"; + + case SplineType.BSpline: + return Degree == null ? "Bezier" : "B-spline"; + + case SplineType.Linear: + return "Linear"; + + case SplineType.PerfectCurve: + return "Perfect curve"; + + default: + return Type.ToString(); + } + } + } + + public override int GetHashCode() + => HashCode.Combine(Type, Degree); + + public override bool Equals(object? obj) + => obj is PathType pathType && Equals(pathType); + + public bool Equals(PathType other) + => Type == other.Type && Degree == other.Degree; + + public static bool operator ==(PathType a, PathType b) => a.Equals(b); + public static bool operator !=(PathType a, PathType b) => !a.Equals(b); + + public override string ToString() => Description; + } } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index be0d757e06..37a35fd3ae 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -204,6 +204,8 @@ namespace osu.Game.Rulesets public ModAutoplay? GetAutoplayMod() => CreateMod(); + public ModTouchDevice? GetTouchDeviceMod() => CreateMod(); + /// /// Create a transformer which adds lookups specific to a ruleset to skin sources. /// @@ -375,6 +377,17 @@ namespace osu.Game.Rulesets /// The display name. public virtual LocalisableString GetDisplayNameForHitResult(HitResult result) => result.GetLocalisableDescription(); + /// + /// Applies changes to difficulty attributes for presenting to a user a rough estimate of how rate adjust mods affect difficulty. + /// Importantly, this should NOT BE USED FOR ANY CALCULATIONS. + /// + /// It is also not always correct, and arguably is never correct depending on your frame of mind. + /// + /// >The that will be adjusted. + /// The rate adjustment multiplier from mods. For example 1.5 for DT. + /// The adjusted difficulty attributes. + public virtual BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate) => new BeatmapDifficulty(difficulty); + /// /// Creates ruleset-specific beatmap filter criteria to be used on the song select screen. /// diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 592dcbfeb8..629a84ea62 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -41,16 +41,30 @@ namespace osu.Game.Rulesets.Scoring /// private const double max_health_target = 0.4; - private IBeatmap beatmap; + /// + /// The drain rate as a proportion of the total health drained per millisecond. + /// + public double DrainRate { get; private set; } + + /// + /// The beatmap. + /// + protected IBeatmap Beatmap { get; private set; } + + /// + /// The time at which health starts draining. + /// + protected readonly double DrainStartTime; + + /// + /// An amount of lenience to apply to the drain rate. + /// + protected readonly double DrainLenience; + + private readonly List healthIncreases = new List(); private double gameplayEndTime; - - private readonly double drainStartTime; - private readonly double drainLenience; - - private readonly List<(double time, double health)> healthIncreases = new List<(double, double)>(); private double targetMinimumHealth; - private double drainRate = 1; private PeriodTracker noDrainPeriodTracker; @@ -64,8 +78,8 @@ namespace osu.Game.Rulesets.Scoring /// A value of 1 completely removes drain. public DrainingHealthProcessor(double drainStartTime, double drainLenience = 0) { - this.drainStartTime = drainStartTime; - this.drainLenience = Math.Clamp(drainLenience, 0, 1); + DrainStartTime = drainStartTime; + DrainLenience = Math.Clamp(drainLenience, 0, 1); } protected override void Update() @@ -76,37 +90,39 @@ namespace osu.Game.Rulesets.Scoring return; // When jumping in and out of gameplay time within a single frame, health should only be drained for the period within the gameplay time - double lastGameplayTime = Math.Clamp(Time.Current - Time.Elapsed, drainStartTime, gameplayEndTime); - double currentGameplayTime = Math.Clamp(Time.Current, drainStartTime, gameplayEndTime); + double lastGameplayTime = Math.Clamp(Time.Current - Time.Elapsed, DrainStartTime, gameplayEndTime); + double currentGameplayTime = Math.Clamp(Time.Current, DrainStartTime, gameplayEndTime); - if (drainLenience < 1) - Health.Value -= drainRate * (currentGameplayTime - lastGameplayTime); + if (DrainLenience < 1) + Health.Value -= DrainRate * (currentGameplayTime - lastGameplayTime); } public override void ApplyBeatmap(IBeatmap beatmap) { - this.beatmap = beatmap; + Beatmap = beatmap; if (beatmap.HitObjects.Count > 0) gameplayEndTime = beatmap.HitObjects[^1].GetEndTime(); - noDrainPeriodTracker = new PeriodTracker(beatmap.Breaks.Select(breakPeriod => new Period( - beatmap.HitObjects - .Select(hitObject => hitObject.GetEndTime()) - .Where(endTime => endTime <= breakPeriod.StartTime) - .DefaultIfEmpty(double.MinValue) - .Last(), - beatmap.HitObjects - .Select(hitObject => hitObject.StartTime) - .Where(startTime => startTime >= breakPeriod.EndTime) - .DefaultIfEmpty(double.MaxValue) - .First() - ))); + noDrainPeriodTracker = new PeriodTracker( + beatmap.Breaks.Select(breakPeriod => + new Period( + beatmap.HitObjects + .Select(hitObject => hitObject.GetEndTime()) + .Where(endTime => endTime <= breakPeriod.StartTime) + .DefaultIfEmpty(double.MinValue) + .Last(), + beatmap.HitObjects + .Select(hitObject => hitObject.StartTime) + .Where(startTime => startTime >= breakPeriod.EndTime) + .DefaultIfEmpty(double.MaxValue) + .First() + ))); targetMinimumHealth = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, min_health_target, mid_health_target, max_health_target); // Add back a portion of the amount of HP to be drained, depending on the lenience requested. - targetMinimumHealth += drainLenience * (1 - targetMinimumHealth); + targetMinimumHealth += DrainLenience * (1 - targetMinimumHealth); // Ensure the target HP is within an acceptable range. targetMinimumHealth = Math.Clamp(targetMinimumHealth, 0, 1); @@ -118,23 +134,25 @@ namespace osu.Game.Rulesets.Scoring { base.ApplyResultInternal(result); - if (!result.Type.IsBonus()) - healthIncreases.Add((result.HitObject.GetEndTime() + result.TimeOffset, GetHealthIncreaseFor(result))); + if (IsSimulating && !result.Type.IsBonus()) + { + healthIncreases.Add(new HealthIncrease( + result.HitObject.GetEndTime() + result.TimeOffset, + GetHealthIncreaseFor(result))); + } } protected override void Reset(bool storeResults) { base.Reset(storeResults); - drainRate = 1; - if (storeResults) - drainRate = computeDrainRate(); + DrainRate = ComputeDrainRate(); healthIncreases.Clear(); } - private double computeDrainRate() + protected virtual double ComputeDrainRate() { if (healthIncreases.Count <= 1) return 0; @@ -148,28 +166,26 @@ namespace osu.Game.Rulesets.Scoring { double currentHealth = 1; double lowestHealth = 1; - int currentBreak = -1; + int currentBreak = 0; for (int i = 0; i < healthIncreases.Count; i++) { - double currentTime = healthIncreases[i].time; - double lastTime = i > 0 ? healthIncreases[i - 1].time : drainStartTime; + double currentTime = healthIncreases[i].Time; + double lastTime = i > 0 ? healthIncreases[i - 1].Time : DrainStartTime; - // Subtract any break time from the duration since the last object - if (beatmap.Breaks.Count > 0) + while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= currentTime) { - // Advance the last break occuring before the current time - while (currentBreak + 1 < beatmap.Breaks.Count && beatmap.Breaks[currentBreak + 1].EndTime < currentTime) - currentBreak++; - - if (currentBreak >= 0) - lastTime = Math.Max(lastTime, beatmap.Breaks[currentBreak].EndTime); + // If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects. + // This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered, + // but this shouldn't have a noticeable impact in practice. + lastTime = currentTime; + currentBreak++; } // Apply health adjustments - currentHealth -= (healthIncreases[i].time - lastTime) * result; + currentHealth -= (currentTime - lastTime) * result; lowestHealth = Math.Min(lowestHealth, currentHealth); - currentHealth = Math.Min(1, currentHealth + healthIncreases[i].health); + currentHealth = Math.Min(1, currentHealth + healthIncreases[i].Amount); // Common scenario for when the drain rate is definitely too harsh if (lowestHealth < 0) @@ -187,5 +203,7 @@ namespace osu.Game.Rulesets.Scoring return result; } + + private record struct HealthIncrease(double Time, double Amount); } } diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index 3e0b6433c2..b5eb755650 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Scoring /// /// The . /// The health increase. - protected virtual double GetHealthIncreaseFor(JudgementResult result) => result.Judgement.HealthIncreaseFor(result); + protected virtual double GetHealthIncreaseFor(JudgementResult result) => result.HealthIncrease; /// /// The default conditions for failing. diff --git a/osu.Game/Rulesets/Scoring/HitEvent.cs b/osu.Game/Rulesets/Scoring/HitEvent.cs index cabbf40a7d..1763190899 100644 --- a/osu.Game/Rulesets/Scoring/HitEvent.cs +++ b/osu.Game/Rulesets/Scoring/HitEvent.cs @@ -19,6 +19,11 @@ namespace osu.Game.Rulesets.Scoring /// public readonly double TimeOffset; + /// + /// The true gameplay rate at the time of the event. + /// + public readonly double? GameplayRate; + /// /// The hit result. /// @@ -46,12 +51,14 @@ namespace osu.Game.Rulesets.Scoring /// /// The time offset from the end of at which the event occurs. /// The . + /// The true gameplay rate at the time of the event. /// The that triggered the event. /// The previous . /// A position corresponding to the event. - public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position) + public HitEvent(double timeOffset, double? gameplayRate, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position) { TimeOffset = timeOffset; + GameplayRate = gameplayRate; Result = result; HitObject = hitObject; LastHitObject = lastHitObject; @@ -63,6 +70,6 @@ namespace osu.Game.Rulesets.Scoring /// /// The positional offset. /// The new . - public HitEvent With(Vector2? positionOffset) => new HitEvent(TimeOffset, Result, HitObject, LastHitObject, positionOffset); + public HitEvent With(Vector2? positionOffset) => new HitEvent(TimeOffset, GameplayRate, Result, HitObject, LastHitObject, positionOffset); } } diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index b4bdd8a1ea..9fb61c6cd9 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace osu.Game.Rulesets.Scoring @@ -18,7 +19,10 @@ namespace osu.Game.Rulesets.Scoring /// public static double? CalculateUnstableRate(this IEnumerable hitEvents) { - double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset).ToArray(); + Debug.Assert(hitEvents.All(ev => ev.GameplayRate != null)); + + // Division by gameplay rate is to account for TimeOffset scaling with gameplay rate. + double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset / ev.GameplayRate!.Value).ToArray(); return 10 * standardDeviation(timeOffsets); } diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 0013a9f20d..7e58df3cfa 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Scoring /// [Description(@"")] [EnumMember(Value = "none")] - [Order(14)] + [Order(15)] None, /// @@ -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)] @@ -64,7 +71,7 @@ namespace osu.Game.Rulesets.Scoring /// Indicates small tick miss. /// [EnumMember(Value = "small_tick_miss")] - [Order(11)] + [Order(12)] SmallTickMiss, /// @@ -79,7 +86,8 @@ namespace osu.Game.Rulesets.Scoring /// Indicates a large tick miss. /// [EnumMember(Value = "large_tick_miss")] - [Order(10)] + [Description("-")] + [Order(11)] LargeTickMiss, /// @@ -95,7 +103,7 @@ namespace osu.Game.Rulesets.Scoring /// [Description("S Bonus")] [EnumMember(Value = "small_bonus")] - [Order(9)] + [Order(10)] SmallBonus, /// @@ -103,28 +111,49 @@ namespace osu.Game.Rulesets.Scoring /// [Description("L Bonus")] [EnumMember(Value = "large_bonus")] - [Order(8)] + [Order(9)] LargeBonus, /// /// Indicates a miss that should be ignored for scoring purposes. /// [EnumMember(Value = "ignore_miss")] - [Order(13)] + [Description("-")] + [Order(14)] IgnoreMiss, /// /// Indicates a hit that should be ignored for scoring purposes. /// [EnumMember(Value = "ignore_hit")] - [Order(12)] + [Order(13)] IgnoreHit, /// - /// 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). + /// Indicates that a combo break should occur, but does not otherwise affect score. /// /// - /// DO NOT USE. + /// May be paired with . + /// + [EnumMember(Value = "combo_break")] + [Order(16)] + ComboBreak, + + /// + /// A special judgement similar to that's used to increase the valuation of the final tick of a slider. + /// + [EnumMember(Value = "slider_tail_hit")] + [Order(8)] + SliderTailHit, + + /// + /// 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). + /// + /// DO NOT USE FOR ANYTHING EVER. + /// + /// + /// This is used when dealing with legacy scores, which historically only have counts stored for 300/100/50/miss. + /// For these scores, we pad the hit statistics with `LegacyComboIncrease` to meet the correct max combo for the score. /// [EnumMember(Value = "legacy_combo_increase")] [Order(99)] @@ -165,6 +194,8 @@ namespace osu.Game.Rulesets.Scoring case HitResult.LargeTickHit: case HitResult.LargeTickMiss: case HitResult.LegacyComboIncrease: + case HitResult.ComboBreak: + case HitResult.SliderTailHit: return true; default: @@ -177,11 +208,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 +228,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); + } } /// @@ -207,6 +254,7 @@ namespace osu.Game.Rulesets.Scoring case HitResult.LargeTickMiss: case HitResult.SmallTickHit: case HitResult.SmallTickMiss: + case HitResult.SliderTailHit: return true; default: @@ -230,9 +278,34 @@ namespace osu.Game.Rulesets.Scoring } } + /// + /// Whether a represents a miss of any type. + /// + /// + /// Of note, both and return for . + /// + public static bool IsMiss(this HitResult result) + { + switch (result) + { + case HitResult.IgnoreMiss: + case HitResult.Miss: + case HitResult.SmallTickMiss: + case HitResult.LargeTickMiss: + case HitResult.ComboBreak: + return true; + + default: + return false; + } + } + /// /// Whether a represents a successful hit. /// + /// + /// Of note, both and return for . + /// public static bool IsHit(this HitResult result) { switch (result) @@ -242,6 +315,7 @@ namespace osu.Game.Rulesets.Scoring case HitResult.Miss: case HitResult.SmallTickMiss: case HitResult.LargeTickMiss: + case HitResult.ComboBreak: return false; default: @@ -254,11 +328,23 @@ 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; + + case HitResult.SliderTailHit: + 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 +377,36 @@ 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 (minResult == HitResult.IgnoreMiss) + return; + + if (maxResult == HitResult.SliderTailHit && minResult != HitResult.LargeTickMiss) + throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.LargeTickMiss} 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/ILegacyScoreSimulator.cs b/osu.Game/Rulesets/Scoring/ILegacyScoreSimulator.cs deleted file mode 100644 index 7240f0d73e..0000000000 --- a/osu.Game/Rulesets/Scoring/ILegacyScoreSimulator.cs +++ /dev/null @@ -1,40 +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.Collections.Generic; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Mods; - -namespace osu.Game.Rulesets.Scoring -{ - /// - /// Generates attributes which are required to calculate old-style Score V1 scores. - /// - public interface ILegacyScoreSimulator - { - /// - /// The accuracy portion of the legacy (ScoreV1) total score. - /// - int AccuracyScore { get; } - - /// - /// The combo-multiplied portion of the legacy (ScoreV1) total score. - /// - int ComboScore { get; } - - /// - /// A ratio of new_bonus_score / old_bonus_score for converting the bonus score of legacy scores to the new scoring. - /// This is made up of all judgements that would be or . - /// - double BonusScoreRatio { get; } - - /// - /// Performs the simulation, computing the maximum , , - /// and achievable for the given beatmap. - /// - /// The working beatmap. - /// A playable version of the beatmap for the ruleset. - /// The applied mods. - void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList mods); - } -} 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..7d69069455 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/Legacy/LegacyBeatmapConversionDifficultyInfo.cs @@ -0,0 +1,69 @@ +// 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 number of hitobjects in the beatmap with a distinct end time. + /// + /// + /// Canonically, these are hitobjects are either sliders or spinners. + /// + public int EndTimeObjectCount { 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) => FromBeatmapInfo(apiBeatmap); + + public static LegacyBeatmapConversionDifficultyInfo FromBeatmap(IBeatmap beatmap) => new LegacyBeatmapConversionDifficultyInfo + { + SourceRuleset = beatmap.BeatmapInfo.Ruleset, + CircleSize = beatmap.Difficulty.CircleSize, + OverallDifficulty = beatmap.Difficulty.OverallDifficulty, + EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration), + TotalObjectCount = beatmap.HitObjects.Count + }; + + public static LegacyBeatmapConversionDifficultyInfo FromBeatmapInfo(IBeatmapInfo beatmapInfo) => new LegacyBeatmapConversionDifficultyInfo + { + SourceRuleset = beatmapInfo.Ruleset, + CircleSize = beatmapInfo.Difficulty.CircleSize, + OverallDifficulty = beatmapInfo.Difficulty.OverallDifficulty, + EndTimeObjectCount = beatmapInfo.EndTimeObjectCount, + TotalObjectCount = beatmapInfo.TotalObjectCount + }; + } +} diff --git a/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreAttributes.cs b/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreAttributes.cs new file mode 100644 index 0000000000..6f6740c641 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreAttributes.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. + +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; + + /// + /// The bonus portion of the legacy (ScoreV1) total score. + /// + public int BonusScore; + + /// + /// The max combo of the legacy (ScoreV1) total score. + /// + public int MaxCombo; + } +} diff --git a/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs new file mode 100644 index 0000000000..ce2f7d5624 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs @@ -0,0 +1,158 @@ +// 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.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Scoring +{ + /// + /// A that matches legacy drain rate calculations as best as possible. + /// + public abstract partial class LegacyDrainingHealthProcessor : DrainingHealthProcessor + { + public Action? OnIterationFail; + public Action? OnIterationSuccess; + + protected double HpMultiplierNormal { get; private set; } + + private double lowestHpEver; + private double lowestHpEnd; + private double hpRecoveryAvailable; + + protected LegacyDrainingHealthProcessor(double drainStartTime) + : base(drainStartTime) + { + } + + public override void ApplyBeatmap(IBeatmap beatmap) + { + lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.975, 0.8, 0.3); + lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.99, 0.9, 0.4); + hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.04, 0.02, 0); + + base.ApplyBeatmap(beatmap); + } + + protected override void Reset(bool storeResults) + { + HpMultiplierNormal = 1; + base.Reset(storeResults); + } + + protected override double ComputeDrainRate() + { + double testDrop = 0.00025; + double currentHp; + double currentHpUncapped; + + while (true) + { + currentHp = 1; + currentHpUncapped = 1; + + double lowestHp = currentHp; + double lastTime = DrainStartTime; + int currentBreak = 0; + bool fail = false; + int topLevelObjectCount = 0; + + foreach (var h in EnumerateTopLevelHitObjects()) + { + topLevelObjectCount++; + + while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= h.StartTime) + { + // If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects. + // This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered, + // but this shouldn't have a noticeable impact in practice. + lastTime = h.StartTime; + currentBreak++; + } + + reduceHp(testDrop * (h.StartTime - lastTime)); + + lastTime = h.GetEndTime(); + + if (currentHp < lowestHp) + lowestHp = currentHp; + + if (currentHp <= lowestHpEver) + { + fail = true; + testDrop *= 0.96; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: hp too low ({currentHp} < {lowestHpEver})"); + break; + } + + double hpReduction = testDrop * (h.GetEndTime() - h.StartTime); + double hpOverkill = Math.Max(0, hpReduction - currentHp); + reduceHp(hpReduction); + + foreach (var nested in EnumerateNestedHitObjects(h)) + increaseHp(nested); + + // Note: Because HP is capped during the above increases, long sliders (with many ticks) or spinners + // will appear to overkill at lower drain levels than they should. However, it is also not correct to simply use the uncapped version. + if (hpOverkill > 0 && currentHp - hpOverkill <= lowestHpEver) + { + fail = true; + testDrop *= 0.96; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: overkill ({currentHp} - {hpOverkill} <= {lowestHpEver})"); + break; + } + + increaseHp(h); + } + + if (!fail && currentHp < lowestHpEnd) + { + fail = true; + testDrop *= 0.94; + HpMultiplierNormal *= 1.01; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: end hp too low ({currentHp} < {lowestHpEnd})"); + } + + double recovery = (currentHpUncapped - 1) / Math.Max(1, topLevelObjectCount); + + if (!fail && recovery < hpRecoveryAvailable) + { + fail = true; + testDrop *= 0.96; + HpMultiplierNormal *= 1.01; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: recovery too low ({recovery} < {hpRecoveryAvailable})"); + } + + if (!fail) + { + OnIterationSuccess?.Invoke($"PASSED drop {testDrop}"); + return testDrop; + } + } + + void reduceHp(double amount) + { + currentHpUncapped = Math.Max(0, currentHpUncapped - amount); + currentHp = Math.Max(0, currentHp - amount); + } + + void increaseHp(HitObject hitObject) + { + double amount = GetHealthIncreaseFor(hitObject, hitObject.CreateJudgement().MaxResult); + currentHpUncapped += amount; + currentHp = Math.Max(0, Math.Min(1, currentHp + amount)); + } + } + + protected sealed override double GetHealthIncreaseFor(JudgementResult result) => GetHealthIncreaseFor(result.HitObject, result.Type); + + protected abstract IEnumerable EnumerateTopLevelHitObjects(); + + protected abstract IEnumerable EnumerateNestedHitObjects(HitObject hitObject); + + protected abstract double GetHealthIncreaseFor(HitObject hitObject, HitResult result); + } +} diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 35a7dfe369..a092829317 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -21,6 +21,14 @@ namespace osu.Game.Rulesets.Scoring { public partial class ScoreProcessor : JudgementProcessor { + /// + /// The exponent applied to combo in the default implementation of . + /// + /// + /// If a custom implementation overrides this may not be relevant. + /// + public const double COMBO_EXPONENT = 0.5; + public const double MAX_SCORE = 1000000; private const double accuracy_cutoff_x = 1; @@ -78,7 +86,9 @@ namespace osu.Game.Rulesets.Scoring /// /// The current rank. /// - public readonly Bindable Rank = new Bindable(ScoreRank.X); + public IBindable Rank => rank; + + private readonly Bindable rank = new Bindable(ScoreRank.X); /// /// The highest combo achieved by this score. @@ -159,14 +169,14 @@ namespace osu.Game.Rulesets.Scoring if (!beatmapApplied) throw new InvalidOperationException($"Cannot access maximum statistics before calling {nameof(ApplyBeatmap)}."); - return new Dictionary(maximumResultCounts); + return new Dictionary(MaximumResultCounts); } } private bool beatmapApplied; - private readonly Dictionary scoreResultCounts = new Dictionary(); - private readonly Dictionary maximumResultCounts = new Dictionary(); + protected readonly Dictionary ScoreResultCounts = new Dictionary(); + protected readonly Dictionary MaximumResultCounts = new Dictionary(); private readonly List hitEvents = new List(); private HitObject? lastHitObject; @@ -176,12 +186,7 @@ namespace osu.Game.Rulesets.Scoring Ruleset = ruleset; Combo.ValueChanged += combo => HighestCombo.Value = Math.Max(HighestCombo.Value, combo.NewValue); - Accuracy.ValueChanged += accuracy => - { - Rank.Value = RankFromAccuracy(accuracy.NewValue); - foreach (var mod in Mods.Value.OfType()) - Rank.Value = mod.AdjustRank(Rank.Value, accuracy.NewValue); - }; + Accuracy.ValueChanged += _ => updateRank(); Mods.ValueChanged += mods => { @@ -191,6 +196,7 @@ namespace osu.Game.Rulesets.Scoring scoreMultiplier *= m.ScoreMultiplier; updateScore(); + updateRank(); }; } @@ -208,10 +214,7 @@ namespace osu.Game.Rulesets.Scoring if (result.FailedAtJudgement) return; - scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1; - - if (!result.Type.IsScorable()) - return; + ScoreResultCounts[result.Type] = ScoreResultCounts.GetValueOrDefault(result.Type) + 1; if (result.Type.IncreasesCombo()) Combo.Value++; @@ -220,16 +223,18 @@ namespace osu.Game.Rulesets.Scoring result.ComboAfterJudgement = Combo.Value; - if (result.Type.AffectsAccuracy()) + if (result.Judgement.MaxResult.AffectsAccuracy()) { - currentMaximumBaseScore += Judgement.ToNumericResult(result.Judgement.MaxResult); - currentBaseScore += Judgement.ToNumericResult(result.Type); + currentMaximumBaseScore += GetBaseScoreForResult(result.Judgement.MaxResult); currentAccuracyJudgementCount++; } + if (result.Type.AffectsAccuracy()) + currentBaseScore += GetBaseScoreForResult(result.Type); + if (result.Type.IsBonus()) currentBonusPortion += GetBonusScoreChange(result); - else + else if (result.Type.IsScorable()) currentComboPortion += GetComboScoreChange(result); ApplyScoreChange(result); @@ -252,7 +257,7 @@ namespace osu.Game.Rulesets.Scoring /// The to describe. /// The . protected virtual HitEvent CreateHitEvent(JudgementResult result) - => new HitEvent(result.TimeOffset, result.Type, result.HitObject, lastHitObject, null); + => new HitEvent(result.TimeOffset, result.GameplayRate, result.Type, result.HitObject, lastHitObject, null); protected sealed override void RevertResultInternal(JudgementResult result) { @@ -265,21 +270,20 @@ namespace osu.Game.Rulesets.Scoring if (result.FailedAtJudgement) return; - scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1; + ScoreResultCounts[result.Type] = ScoreResultCounts.GetValueOrDefault(result.Type) - 1; - if (!result.Type.IsScorable()) - return; - - if (result.Type.AffectsAccuracy()) + if (result.Judgement.MaxResult.AffectsAccuracy()) { - currentMaximumBaseScore -= Judgement.ToNumericResult(result.Judgement.MaxResult); - currentBaseScore -= Judgement.ToNumericResult(result.Type); + currentMaximumBaseScore -= GetBaseScoreForResult(result.Judgement.MaxResult); currentAccuracyJudgementCount--; } + if (result.Type.AffectsAccuracy()) + currentBaseScore -= GetBaseScoreForResult(result.Type); + if (result.Type.IsBonus()) currentBonusPortion -= GetBonusScoreChange(result); - else + else if (result.Type.IsScorable()) currentComboPortion -= GetComboScoreChange(result); RemoveScoreChange(result); @@ -291,9 +295,54 @@ namespace osu.Game.Rulesets.Scoring updateScore(); } - protected virtual double GetBonusScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Type); + /// + /// Gets the final score change to be applied to the bonus portion of the score. + /// + /// The judgement result. + protected virtual double GetBonusScoreChange(JudgementResult result) => GetBaseScoreForResult(result.Type); - protected virtual double GetComboScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Type) * (1 + result.ComboAfterJudgement / 10d); + /// + /// Gets the final score change to be applied to the combo portion of the score. + /// + /// The judgement result. + protected virtual double GetComboScoreChange(JudgementResult result) => GetBaseScoreForResult(result.Judgement.MaxResult) * Math.Pow(result.ComboAfterJudgement, COMBO_EXPONENT); + + public virtual int GetBaseScoreForResult(HitResult result) + { + switch (result) + { + default: + return 0; + + case HitResult.SmallTickHit: + return 10; + + case HitResult.LargeTickHit: + return 30; + + case HitResult.SliderTailHit: + return 150; + + case HitResult.Meh: + return 50; + + case HitResult.Ok: + return 100; + + case HitResult.Good: + return 200; + + case HitResult.Great: + case HitResult.Perfect: // Perfect doesn't actually give more score / accuracy directly. + return 300; + + case HitResult.SmallBonus: + return 10; + + case HitResult.LargeBonus: + return 50; + } + } protected virtual void ApplyScoreChange(JudgementResult result) { @@ -315,10 +364,21 @@ namespace osu.Game.Rulesets.Scoring TotalScore.Value = (long)Math.Round(ComputeTotalScore(comboProgress, accuracyProcess, currentBonusPortion) * scoreMultiplier); } + private void updateRank() + { + // Once failed, we shouldn't update the rank anymore. + if (rank.Value == ScoreRank.F) + return; + + rank.Value = RankFromScore(Accuracy.Value, ScoreResultCounts); + foreach (var mod in Mods.Value.OfType()) + rank.Value = mod.AdjustRank(Rank.Value, Accuracy.Value); + } + protected virtual double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) { - return 700000 * comboProgress + - 300000 * Math.Pow(Accuracy.Value, 10) * accuracyProgress + + return 500000 * Accuracy.Value * comboProgress + + 500000 * Math.Pow(Accuracy.Value, 5) * accuracyProgress + bonusPortion; } @@ -343,13 +403,13 @@ namespace osu.Game.Rulesets.Scoring maximumComboPortion = currentComboPortion; maximumAccuracyJudgementCount = currentAccuracyJudgementCount; - maximumResultCounts.Clear(); - maximumResultCounts.AddRange(scoreResultCounts); + MaximumResultCounts.Clear(); + MaximumResultCounts.AddRange(ScoreResultCounts); MaximumTotalScore = TotalScore.Value; } - scoreResultCounts.Clear(); + ScoreResultCounts.Clear(); currentBaseScore = 0; currentMaximumBaseScore = 0; @@ -360,9 +420,8 @@ namespace osu.Game.Rulesets.Scoring TotalScore.Value = 0; Accuracy.Value = 1; Combo.Value = 0; - Rank.Disabled = false; - Rank.Value = ScoreRank.X; HighestCombo.Value = 0; + updateRank(); } /// @@ -379,10 +438,10 @@ namespace osu.Game.Rulesets.Scoring score.MaximumStatistics.Clear(); foreach (var result in HitResultExtensions.ALL_TYPES) - score.Statistics[result] = scoreResultCounts.GetValueOrDefault(result); + score.Statistics[result] = ScoreResultCounts.GetValueOrDefault(result); foreach (var result in HitResultExtensions.ALL_TYPES) - score.MaximumStatistics[result] = maximumResultCounts.GetValueOrDefault(result); + score.MaximumStatistics[result] = MaximumResultCounts.GetValueOrDefault(result); // Populate total score after everything else. score.TotalScore = TotalScore.Value; @@ -397,7 +456,7 @@ namespace osu.Game.Rulesets.Scoring return; score.Passed = false; - Rank.Value = ScoreRank.F; + rank.Value = ScoreRank.F; PopulateScore(score); } @@ -413,8 +472,8 @@ namespace osu.Game.Rulesets.Scoring HighestCombo.Value = frame.Header.MaxCombo; TotalScore.Value = frame.Header.TotalScore; - scoreResultCounts.Clear(); - scoreResultCounts.AddRange(frame.Header.Statistics); + ScoreResultCounts.Clear(); + ScoreResultCounts.AddRange(frame.Header.Statistics); SetScoreProcessorStatistics(frame.Header.ScoreProcessorStatistics); @@ -446,7 +505,7 @@ namespace osu.Game.Rulesets.Scoring /// /// Given an accuracy (0..1), return the correct . /// - public static ScoreRank RankFromAccuracy(double accuracy) + public virtual ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary results) { if (accuracy == accuracy_cutoff_x) return ScoreRank.X; @@ -466,7 +525,7 @@ namespace osu.Game.Rulesets.Scoring /// Given a , return the cutoff accuracy (0..1). /// Accuracy must be greater than or equal to the cutoff to qualify for the provided rank. /// - public static double AccuracyCutoffFromRank(ScoreRank rank) + public virtual double AccuracyCutoffFromRank(ScoreRank rank) { switch (rank) { @@ -522,7 +581,7 @@ namespace osu.Game.Rulesets.Scoring /// /// /// Used to compute accuracy. - /// See: and . + /// See: and . /// [Key(0)] public double BaseScore { get; set; } 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 90cffab714..8c9cb262af 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -25,17 +25,15 @@ namespace osu.Game.Rulesets.UI public ReplayInputHandler? ReplayInputHandler { get; set; } /// - /// The number of frames (per parent frame) which can be run in an attempt to catch-up to real-time. + /// The number of CPU milliseconds to spend at most during seek catch-up. /// - public int MaxCatchUpFrames { get; set; } = 5; + private const double max_catchup_milliseconds = 10; /// /// Whether to enable frame-stable playback. /// internal bool FrameStablePlayback { get; set; } = true; - protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && state != PlaybackState.NotValid; - private readonly Bindable isCatchingUp = new Bindable(); private readonly Bindable waitingOnFrames = new Bindable(); @@ -61,6 +59,8 @@ namespace osu.Game.Rulesets.UI /// private readonly FramedClock framedClock; + private readonly Stopwatch stopwatch = new Stopwatch(); + /// /// The current direction of playback to be exposed to frame stable children. /// @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.UI public override bool UpdateSubTree() { - int loops = MaxCatchUpFrames; + stopwatch.Restart(); do { @@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.UI base.UpdateSubTree(); UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat); - } while (state == PlaybackState.RequiresCatchUp && loops-- > 0); + } while (state == PlaybackState.RequiresCatchUp && stopwatch.ElapsedMilliseconds < max_catchup_milliseconds); return true; } @@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.UI // if waiting on frames, run one update loop to determine if frames have arrived. state = PlaybackState.Valid; } - else if (IsPaused.Value) + else if (IsPaused.Value && !hasReplayAttached) { // time should not advance while paused, nor should anything run. state = PlaybackState.NotValid; @@ -264,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/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index fed262868d..b61e8d9674 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -7,6 +7,7 @@ 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; @@ -45,7 +46,10 @@ namespace osu.Game.Rulesets.UI Child = hitSounds = new Container { Name = "concurrent sample pool", - ChildrenEnumerable = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new PausableSkinnableSound()) + ChildrenEnumerable = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new PausableSkinnableSound + { + MinimumSampleVolume = DrawableHitObject.MINIMUM_SAMPLE_VOLUME + }) } }; } diff --git a/osu.Game/Rulesets/UI/JudgementPooler.cs b/osu.Game/Rulesets/UI/JudgementPooler.cs new file mode 100644 index 0000000000..efec760f15 --- /dev/null +++ b/osu.Game/Rulesets/UI/JudgementPooler.cs @@ -0,0 +1,77 @@ +// 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.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.UI +{ + /// + /// Handles the task of preparing poolable drawable judgements for gameplay usage. + /// + /// The drawable judgement type. + public partial class JudgementPooler : CompositeComponent + where T : DrawableJudgement, new() + { + private readonly IDictionary> poolDictionary = new Dictionary>(); + + private readonly IEnumerable usableHitResults; + private readonly Action? onJudgementInitialLoad; + + public JudgementPooler(IEnumerable usableHitResults, Action? onJudgementInitialLoad = null) + { + this.usableHitResults = usableHitResults; + this.onJudgementInitialLoad = onJudgementInitialLoad; + } + + public T? Get(HitResult result, Action? setupAction) + { + if (!poolDictionary.TryGetValue(result, out var pool)) + return null; + + return pool.Get(setupAction); + } + + [BackgroundDependencyLoader] + private void load() + { + foreach (HitResult result in usableHitResults) + { + var pool = new DrawableJudgementPool(result, onJudgementInitialLoad); + poolDictionary.Add(result, pool); + AddInternal(pool); + } + } + + private partial class DrawableJudgementPool : DrawablePool + { + private readonly HitResult result; + private readonly Action? onLoaded; + + public DrawableJudgementPool(HitResult result, Action? onLoaded) + : base(20) + { + this.result = result; + this.onLoaded = onLoaded; + } + + protected override T CreateNewDrawable() + { + var judgement = base.CreateNewDrawable(); + + // just a placeholder to initialise the correct drawable hierarchy for this pool. + judgement.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null); + + onLoaded?.Invoke(judgement); + + return judgement; + } + } + } +} diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index bf212ad72f..d1776c5c0b 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,101 @@ 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, + Alpha = 0, + Font = OsuFont.Numeric.With(null, 22f), + UseFullGlyphHeight = false, + Text = mod.Acronym + }, + modIcon = new SpriteIcon + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Size = new Vector2(45), + Icon = FontAwesome.Solid.Question + }, + } }, }; } @@ -109,6 +166,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 +190,30 @@ 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; + modAcronym.Colour = modIcon.Colour = OsuColour.Gray(84); + + 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..6e96cc8e6f 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,16 +84,14 @@ namespace osu.Game.Rulesets.UI }); tinySwitch.Scale = new Vector2(0.3f); } - } - [BackgroundDependencyLoader(true)] - private void load(OsuColour colours, OverlayColourProvider? colourProvider) - { + var modTypeColour = colours.ForModType(mod.Type); + inactiveForegroundColour = colourProvider?.Background5 ?? colours.Gray3; - activeForegroundColour = colours.ForModType(mod.Type); + activeForegroundColour = modTypeColour; inactiveBackgroundColour = colourProvider?.Background2 ?? colours.Gray5; - activeBackgroundColour = Interpolation.ValueAt(0.1f, Colour4.Black, activeForegroundColour, 0, 1); + activeBackgroundColour = Interpolation.ValueAt(0.1f, Colour4.Black, modTypeColour, 0, 1); } protected override void LoadComplete() diff --git a/osu.Game/Rulesets/UI/ModSwitchTiny.cs b/osu.Game/Rulesets/UI/ModSwitchTiny.cs index a5cf75bd07..4d50e702af 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 + } + }, + }, } }; } @@ -67,11 +106,13 @@ namespace osu.Game.Rulesets.UI [BackgroundDependencyLoader(true)] private void load(OsuColour colours, OverlayColourProvider? colourProvider) { + var modTypeColour = colours.ForModType(Mod.Type); + inactiveBackgroundColour = colourProvider?.Background5 ?? colours.Gray3; - activeBackgroundColour = colours.ForModType(mod.Type); + activeBackgroundColour = modTypeColour; inactiveForegroundColour = colourProvider?.Background2 ?? colours.Gray5; - activeForegroundColour = Interpolation.ValueAt(0.1f, Colour4.Black, activeForegroundColour, 0, 1); + activeForegroundColour = Interpolation.ValueAt(0.1f, Colour4.Black, modTypeColour, 0, 1); } protected override void LoadComplete() @@ -80,12 +121,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 3f263aba63..e9c35555c8 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -23,6 +23,7 @@ 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 { @@ -94,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/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 26b9d06f73..041c7a13ae 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -72,6 +72,7 @@ namespace osu.Game.Rulesets.UI private void load(OsuConfigManager config) { mouseDisabled = config.GetBindable(OsuSetting.MouseDisableButtons); + tapsDisabled = config.GetBindable(OsuSetting.TouchDisableGameplayTaps); } #region Action mapping (for replays) @@ -124,6 +125,7 @@ namespace osu.Game.Rulesets.UI #region Setting application (disables etc.) private Bindable mouseDisabled; + private Bindable tapsDisabled; protected override bool Handle(UIEvent e) { @@ -147,9 +149,9 @@ namespace osu.Game.Rulesets.UI protected override bool HandleMouseTouchStateChange(TouchStateChangeEvent e) { - if (mouseDisabled.Value) + if (tapsDisabled.Value) { - // Only propagate positional data when mouse buttons are disabled. + // Only propagate positional data when taps are disabled. e = new TouchStateChangeEvent(e.State, e.Input, e.Touch, false, e.LastPosition); } @@ -165,7 +167,6 @@ namespace osu.Game.Rulesets.UI var triggers = KeyBindingContainer.DefaultKeyBindings .Select(b => b.GetAction()) .Distinct() - .OrderBy(action => action) .Select(action => new KeyCounterActionTrigger(action)) .ToArray(); @@ -218,6 +219,7 @@ namespace osu.Game.Rulesets.UI base.ReloadMappings(realmKeyBindings); KeyBindings = KeyBindings.Where(b => RealmKeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList(); + RealmKeyBindingStore.ClearDuplicateBindings(KeyBindings); } } } 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 cd85932599..4a79c1a447 100644 --- a/osu.Game/Rulesets/UI/Scrolling/IScrollingInfo.cs +++ b/osu.Game/Rulesets/UI/Scrolling/IScrollingInfo.cs @@ -15,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/Scoring/IScoreInfo.cs b/osu.Game/Scoring/IScoreInfo.cs index d17558f800..a1d076b8c2 100644 --- a/osu.Game/Scoring/IScoreInfo.cs +++ b/osu.Game/Scoring/IScoreInfo.cs @@ -9,7 +9,7 @@ using osu.Game.Users; namespace osu.Game.Scoring { - public interface IScoreInfo : IHasOnlineID, IHasNamedFiles + public interface IScoreInfo : IHasOnlineID { IUser User { get; } @@ -22,7 +22,7 @@ namespace osu.Game.Scoring double Accuracy { get; } - bool HasReplay { get; } + long LegacyOnlineID { get; } DateTimeOffset Date { get; } diff --git a/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs b/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs index f2e8cf141b..2c5b91f10f 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(); @@ -28,11 +35,16 @@ namespace osu.Game.Scoring.Legacy [JsonProperty("maximum_statistics")] public Dictionary MaximumStatistics { get; set; } = new Dictionary(); + [JsonProperty("client_version")] + public string ClientVersion = string.Empty; + 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), + ClientVersion = score.ClientVersion, }; } } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 8c00110909..e51a95798b 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -11,6 +12,7 @@ using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Legacy; +using osu.Game.Database; using osu.Game.IO.Legacy; using osu.Game.Online.API.Requests.Responses; using osu.Game.Replays; @@ -18,6 +20,7 @@ using osu.Game.Replays.Legacy; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; using SharpCompress.Compressors.LZMA; namespace osu.Game.Scoring.Legacy @@ -101,9 +104,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; @@ -121,14 +124,21 @@ 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(); + score.ScoreInfo.ClientVersion = readScore.ClientVersion; }); } } - PopulateAccuracy(score.ScoreInfo); + PopulateMaximumStatistics(score.ScoreInfo, workingBeatmap); + + if (score.ScoreInfo.IsLegacyScore) + score.ScoreInfo.LegacyTotalScore = score.ScoreInfo.TotalScore; + + StandardisedScoreMigrationTools.UpdateFromLegacy(score.ScoreInfo, workingBeatmap); // before returning for database import, we must restore the database-sourced BeatmapInfo. // if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception. @@ -166,109 +176,65 @@ namespace osu.Game.Scoring.Legacy } /// - /// Populates the accuracy of a given from its contained statistics. + /// Populates the for a given . /// - /// - /// Legacy use only. - /// - /// The to populate. - public static void PopulateAccuracy(ScoreInfo score) + /// The score to populate the statistics of. + /// The corresponding . + internal static void PopulateMaximumStatistics(ScoreInfo score, WorkingBeatmap workingBeatmap) { - int countMiss = score.GetCountMiss() ?? 0; - int count50 = score.GetCount50() ?? 0; - int count100 = score.GetCount100() ?? 0; - int count300 = score.GetCount300() ?? 0; - int countGeki = score.GetCountGeki() ?? 0; - int countKatu = score.GetCountKatu() ?? 0; + Debug.Assert(score.BeatmapInfo != null); - switch (score.Ruleset.OnlineID) + if (score.MaximumStatistics.Select(kvp => kvp.Value).Sum() > 0) + return; + + var ruleset = score.Ruleset.Detach(); + var rulesetInstance = ruleset.CreateInstance(); + var scoreProcessor = rulesetInstance.CreateScoreProcessor(); + + // Populate the maximum statistics. + HitResult maxBasicResult = rulesetInstance.GetHitResults() + .Select(h => h.result) + .Where(h => h.IsBasic()).MaxBy(scoreProcessor.GetBaseScoreForResult); + + foreach ((HitResult result, int count) in score.Statistics) { - case 0: + switch (result) { - int totalHits = count50 + count100 + count300 + countMiss; - score.Accuracy = totalHits > 0 ? (double)(count50 * 50 + count100 * 100 + count300 * 300) / (totalHits * 300) : 1; + case HitResult.LargeTickHit: + case HitResult.LargeTickMiss: + score.MaximumStatistics[HitResult.LargeTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.LargeTickHit) + count; + break; - float ratio300 = (float)count300 / totalHits; - float ratio50 = (float)count50 / totalHits; + case HitResult.SmallTickHit: + case HitResult.SmallTickMiss: + score.MaximumStatistics[HitResult.SmallTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) + count; + break; - if (ratio300 == 1) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X; - else if (ratio300 > 0.9 && ratio50 <= 0.01 && countMiss == 0) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S; - else if ((ratio300 > 0.8 && countMiss == 0) || ratio300 > 0.9) - score.Rank = ScoreRank.A; - else if ((ratio300 > 0.7 && countMiss == 0) || ratio300 > 0.8) - score.Rank = ScoreRank.B; - else if (ratio300 > 0.6) - score.Rank = ScoreRank.C; - else - score.Rank = ScoreRank.D; - break; - } + case HitResult.IgnoreHit: + case HitResult.IgnoreMiss: + case HitResult.SmallBonus: + case HitResult.LargeBonus: + break; - case 1: - { - int totalHits = count50 + count100 + count300 + countMiss; - score.Accuracy = totalHits > 0 ? (double)(count100 * 150 + count300 * 300) / (totalHits * 300) : 1; - - float ratio300 = (float)count300 / totalHits; - float ratio50 = (float)count50 / totalHits; - - if (ratio300 == 1) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X; - else if (ratio300 > 0.9 && ratio50 <= 0.01 && countMiss == 0) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S; - else if ((ratio300 > 0.8 && countMiss == 0) || ratio300 > 0.9) - score.Rank = ScoreRank.A; - else if ((ratio300 > 0.7 && countMiss == 0) || ratio300 > 0.8) - score.Rank = ScoreRank.B; - else if (ratio300 > 0.6) - score.Rank = ScoreRank.C; - else - score.Rank = ScoreRank.D; - break; - } - - case 2: - { - int totalHits = count50 + count100 + count300 + countMiss + countKatu; - score.Accuracy = totalHits > 0 ? (double)(count50 + count100 + count300) / totalHits : 1; - - if (score.Accuracy == 1) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X; - else if (score.Accuracy > 0.98) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S; - else if (score.Accuracy > 0.94) - score.Rank = ScoreRank.A; - else if (score.Accuracy > 0.9) - score.Rank = ScoreRank.B; - else if (score.Accuracy > 0.85) - score.Rank = ScoreRank.C; - else - score.Rank = ScoreRank.D; - break; - } - - case 3: - { - int totalHits = count50 + count100 + count300 + countMiss + countGeki + countKatu; - score.Accuracy = totalHits > 0 ? (double)(count50 * 50 + count100 * 100 + countKatu * 200 + (count300 + countGeki) * 300) / (totalHits * 300) : 1; - - if (score.Accuracy == 1) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X; - else if (score.Accuracy > 0.95) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S; - else if (score.Accuracy > 0.9) - score.Rank = ScoreRank.A; - else if (score.Accuracy > 0.8) - score.Rank = ScoreRank.B; - else if (score.Accuracy > 0.7) - score.Rank = ScoreRank.C; - else - score.Rank = ScoreRank.D; - break; + default: + score.MaximumStatistics[maxBasicResult] = score.MaximumStatistics.GetValueOrDefault(maxBasicResult) + count; + break; } } + + if (!score.IsLegacyScore) + return; + +#pragma warning disable CS0618 + // In osu! and osu!mania, some judgements affect combo but aren't stored to scores. + // A special hit result is used to pad out the combo value to match, based on the max combo from the difficulty attributes. + var calculator = rulesetInstance.CreateDifficultyCalculator(workingBeatmap); + var attributes = calculator.Calculate(score.Mods); + + int maxComboFromStatistics = score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Select(kvp => kvp.Value).DefaultIfEmpty(0).Sum(); + if (attributes.MaxCombo > maxComboFromStatistics) + score.MaximumStatistics[HitResult.LegacyComboIncrease] = attributes.MaxCombo - maxComboFromStatistics; +#pragma warning restore CS0618 } private void readLegacyReplay(Replay replay, StreamReader reader) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 6868c89d26..c74980abb6 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -13,6 +13,7 @@ using osu.Game.Extensions; using osu.Game.IO.Legacy; using osu.Game.IO.Serialization; using osu.Game.Replays.Legacy; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using SharpCompress.Compressors.LZMA; @@ -30,9 +31,22 @@ 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. + /// 30000005: Introduce combo exponent in the osu! gamemode. Reconvert all scores. + /// 30000006: Fix edge cases in conversion after combo exponent introduction that lead to NaNs. Reconvert all scores. + /// 30000007: Adjust osu!mania combo and accuracy portions and judgement scoring values. Reconvert all scores. + /// 30000008: Add accuracy conversion. Reconvert all scores. + /// 30000009: Fix edge cases in conversion for scores which have 0.0x mod multiplier on stable. Reconvert all scores. + /// 30000010: Fix mania score V1 conversion using score V1 accuracy rather than V2 accuracy. Reconvert all scores. + /// 30000011: Re-do catch scoring to mirror stable Score V2 as closely as feasible. Reconvert all scores. + /// + /// 30000012: Fix incorrect total score conversion on selected beatmaps after implementing the more correct + /// method. Reconvert all scores. + /// + /// 30000013: All local scores will use lazer definitions of ranks for consistency. Recalculates the rank of all scores. /// /// - public const int LATEST_VERSION = 30000003; + public const int LATEST_VERSION = 30000013; /// /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays. @@ -83,7 +97,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()); } diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs index 980b742585..07c35a334f 100644 --- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs @@ -4,6 +4,7 @@ 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 @@ -16,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) @@ -27,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((Math.Pow(objectCount, 2) * 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/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 81b9f57bbc..768c28cc38 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -17,8 +17,6 @@ using osu.Game.Scoring.Legacy; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Scoring; using Realms; namespace osu.Game.Scoring @@ -42,7 +40,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 +50,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; } } @@ -78,8 +90,6 @@ namespace osu.Game.Scoring ArgumentNullException.ThrowIfNull(model.BeatmapInfo); ArgumentNullException.ThrowIfNull(model.Ruleset); - PopulateMaximumStatistics(model); - if (string.IsNullOrEmpty(model.StatisticsJson)) model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics); @@ -90,74 +100,6 @@ namespace osu.Game.Scoring // 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()); - } - } - - /// - /// Populates the for a given . - /// - /// 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 ruleset = score.Ruleset.Detach(); - var rulesetInstance = ruleset.CreateInstance(); - - Debug.Assert(rulesetInstance != null); - - // Populate the maximum statistics. - HitResult maxBasicResult = rulesetInstance.GetHitResults() - .Select(h => h.result) - .Where(h => h.IsBasic()).MaxBy(Judgement.ToNumericResult); - - foreach ((HitResult result, int count) in score.Statistics) - { - switch (result) - { - case HitResult.LargeTickHit: - case HitResult.LargeTickMiss: - score.MaximumStatistics[HitResult.LargeTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.LargeTickHit) + count; - break; - - case HitResult.SmallTickHit: - case HitResult.SmallTickMiss: - score.MaximumStatistics[HitResult.SmallTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) + count; - break; - - case HitResult.IgnoreHit: - case HitResult.IgnoreMiss: - case HitResult.SmallBonus: - case HitResult.LargeBonus: - break; - - default: - score.MaximumStatistics[maxBasicResult] = score.MaximumStatistics.GetValueOrDefault(maxBasicResult) + count; - break; - } - } - - if (!score.IsLegacyScore) - return; - -#pragma warning disable CS0618 - // In osu! and osu!mania, some judgements affect combo but aren't stored to scores. - // A special hit result is used to pad out the combo value to match, based on the max combo from the difficulty attributes. - var calculator = rulesetInstance.CreateDifficultyCalculator(beatmaps().GetWorkingBeatmap(beatmap)); - var attributes = calculator.Calculate(score.Mods); - - int maxComboFromStatistics = score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Select(kvp => kvp.Value).DefaultIfEmpty(0).Sum(); - if (attributes.MaxCombo > maxComboFromStatistics) - score.MaximumStatistics[HitResult.LegacyComboIncrease] = attributes.MaxCombo - maxComboFromStatistics; -#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). @@ -168,6 +110,12 @@ namespace osu.Game.Scoring base.PostImport(model, realm, parameters); 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; } /// @@ -176,6 +124,9 @@ namespace osu.Game.Scoring /// 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)) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index c6f4433824..32e4bbbf29 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -46,6 +46,12 @@ namespace osu.Game.Scoring /// public BeatmapInfo? BeatmapInfo { get; set; } + /// + /// The version of the client this score was set using. + /// Sourced from at the point of score submission. + /// + public string ClientVersion { get; set; } = string.Empty; + /// /// The at the point in time when the score was set. /// @@ -66,7 +72,7 @@ namespace osu.Game.Scoring /// If this does not match , /// the total score has not yet been updated to reflect the current scoring values. /// - /// See 's conversion logic. + /// See 's conversion logic. /// /// /// This may not match the version stored in the replay files. @@ -81,19 +87,45 @@ namespace osu.Game.Scoring /// 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!; @@ -159,7 +191,6 @@ namespace osu.Game.Scoring IRulesetInfo IScoreInfo.Ruleset => Ruleset; IBeatmapInfo? IScoreInfo.Beatmap => BeatmapInfo; IUser IScoreInfo.User => User; - IEnumerable IHasNamedFiles.Files => Files; #region Properties required to make things work with existing usages @@ -176,6 +207,7 @@ namespace osu.Game.Scoring clone.Statistics = new Dictionary(clone.Statistics); clone.MaximumStatistics = new Dictionary(clone.MaximumStatistics); + clone.HitEvents = new List(clone.HitEvents); // Ensure we have fresh mods to avoid any references (ie. after gameplay). clone.clearAllMods(); @@ -317,27 +349,12 @@ namespace osu.Game.Scoring switch (r.result) { case HitResult.SmallTickHit: - { - int total = value + Statistics.GetValueOrDefault(HitResult.SmallTickMiss); - if (total > 0) - yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName); - - break; - } - case HitResult.LargeTickHit: - { - int total = value + Statistics.GetValueOrDefault(HitResult.LargeTickMiss); - if (total > 0) - yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName); - - break; - } - + case HitResult.SliderTailHit: 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/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 31b5bd8365..1ee99e9e93 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Threading; @@ -26,6 +27,7 @@ namespace osu.Game.Scoring { public class ScoreManager : ModelManager, IModelImporter { + private readonly Func beatmaps; private readonly OsuConfigManager configManager; private readonly ScoreImporter scoreImporter; private readonly LegacyScoreExporter scoreExporter; @@ -44,6 +46,7 @@ namespace osu.Game.Scoring OsuConfigManager configManager = null) : base(storage, realm) { + this.beatmaps = beatmaps; this.configManager = configManager; scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm, api) @@ -150,7 +153,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; @@ -167,7 +174,11 @@ namespace osu.Game.Scoring /// Populates the for a given . /// /// The score to populate the statistics of. - public void PopulateMaximumStatistics(ScoreInfo score) => scoreImporter.PopulateMaximumStatistics(score); + public void PopulateMaximumStatistics(ScoreInfo score) + { + Debug.Assert(score.BeatmapInfo != null); + LegacyScoreDecoder.PopulateMaximumStatistics(score, beatmaps().GetWorkingBeatmap(score.BeatmapInfo.Detach())); + } #region Implementation of IPresentImports diff --git a/osu.Game/Screens/BackgroundScreen.cs b/osu.Game/Screens/BackgroundScreen.cs index a7502f22d5..73af9b1bf2 100644 --- a/osu.Game/Screens/BackgroundScreen.cs +++ b/osu.Game/Screens/BackgroundScreen.cs @@ -13,7 +13,8 @@ namespace osu.Game.Screens { public abstract partial class BackgroundScreen : Screen, IEquatable { - protected const float TRANSITION_LENGTH = 500; + public const float TRANSITION_LENGTH = 500; + private const float x_movement_amount = 50; private readonly bool animateOnEnter; diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs index 312fd496a1..185e2cab99 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(); /// @@ -48,7 +51,7 @@ namespace osu.Game.Screens.Backgrounds /// public readonly Bindable DimWhenUserSettingsIgnored = new Bindable(); - internal readonly IBindable IsBreakTime = new Bindable(); + internal readonly Bindable IsBreakTime = new Bindable(); private readonly DimmableBackground dimmable; @@ -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/BackgroundScreenCustom.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenCustom.cs index 14331c1978..3862e2a7e0 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenCustom.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenCustom.cs @@ -15,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 d9554c10e2..e46b92795a 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -3,11 +3,14 @@ #nullable disable +using System.Diagnostics; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Screens; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -34,6 +37,9 @@ namespace osu.Game.Screens.Backgrounds [Resolved] private IBindable beatmap { get; set; } + [Resolved] + private GameHost gameHost { get; set; } + protected virtual bool AllowStoryboardBackground => true; public BackgroundScreenDefault(bool animateOnEnter = true) @@ -71,6 +77,34 @@ namespace osu.Game.Screens.Backgrounds void next() => Next(); } + private ScheduledDelegate storyboardUnloadDelegate; + + public override void OnSuspending(ScreenTransitionEvent e) + { + var backgroundScreenStack = Parent as BackgroundScreenStack; + Debug.Assert(backgroundScreenStack != null); + + if (background is BeatmapBackgroundWithStoryboard storyboardBackground) + storyboardUnloadDelegate = gameHost.UpdateThread.Scheduler.AddDelayed(storyboardBackground.UnloadStoryboard, TRANSITION_LENGTH); + + base.OnSuspending(e); + } + + public override void OnResuming(ScreenTransitionEvent e) + { + if (background is BeatmapBackgroundWithStoryboard storyboardBackground) + { + if (storyboardUnloadDelegate?.Completed == false) + storyboardUnloadDelegate.Cancel(); + else + storyboardBackground.LoadStoryboard(); + + storyboardUnloadDelegate = null; + } + + base.OnResuming(e); + } + private ScheduledDelegate nextTask; private CancellationTokenSource cancellationTokenSource; 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 fb0ae2df73..0e125d0ec0 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -4,11 +4,9 @@ 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; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; @@ -48,12 +46,12 @@ namespace osu.Game.Screens.Edit.Components.Menus Padding = new MarginPadding(8), Children = new Drawable[] { - new Sprite + new SpriteIcon { Size = new Vector2(26), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Texture = textures.Get("Icons/Hexacons/editor"), + Icon = OsuIcon.EditCircle, }, text = new TextFlowContainer { @@ -151,7 +149,7 @@ namespace osu.Game.Screens.Edit.Components.Menus { switch (item) { - case EditorMenuItemSpacer spacer: + case OsuMenuItemSpacer spacer: return new DrawableSpacer(spacer); case StatefulMenuItem stateful: @@ -195,29 +193,6 @@ namespace osu.Game.Screens.Edit.Components.Menus Foreground.Padding = new MarginPadding { Vertical = 2 }; } } - - private partial class DrawableSpacer : DrawableOsuMenuItem - { - public DrawableSpacer(MenuItem item) - : base(item) - { - 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; - - protected override bool OnClick(ClickEvent e) => true; - } } } } diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuItemSpacer.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuItemSpacer.cs deleted file mode 100644 index 4e75a92e19..0000000000 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuItemSpacer.cs +++ /dev/null @@ -1,13 +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.Screens.Edit.Components.Menus -{ - public class EditorMenuItemSpacer : EditorMenuItem - { - public EditorMenuItemSpacer() - : base(" ") - { - } - } -} 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/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 59b0bd1785..b33edb9edb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -262,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; @@ -320,7 +321,7 @@ namespace osu.Game.Screens.Edit.Compose.Components Spacing = new Vector2(10), Children = new Drawable[] { - divisorTextBox = new OsuNumberBox + divisorTextBox = new AutoSelectTextBox { RelativeSizeAxes = Axes.X, PlaceholderText = "Beat divisor" @@ -340,8 +341,6 @@ namespace osu.Game.Screens.Edit.Compose.Components base.LoadComplete(); BeatDivisor.BindValueChanged(_ => updateState(), true); divisorTextBox.OnCommit += (_, _) => setPresetsFromTextBoxEntry(); - - Schedule(() => GetContainingInputManager().ChangeFocus(divisorTextBox)); } private void setPresetsFromTextBoxEntry() @@ -589,5 +588,16 @@ namespace osu.Game.Screens.Edit.Compose.Components } } } + + private partial class AutoSelectTextBox : OsuNumberBox + { + protected override void LoadComplete() + { + base.LoadComplete(); + + GetContainingInputManager().ChangeFocus(this); + SelectAll(); + } + } } } 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 1de6c8364c..2d6e234e57 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>(); @@ -114,7 +114,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnMouseDown(MouseDownEvent e) { bool selectionPerformed = performMouseDownActions(e); - bool movementPossible = prepareSelectionMovement(); + bool movementPossible = prepareSelectionMovement(e); // check if selection has occurred if (selectionPerformed) @@ -536,9 +536,13 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Attempts to begin the movement of any selected blueprints. /// + /// The defining the beginning of a movement. /// Whether a movement is possible. - private bool prepareSelectionMovement() + private bool prepareSelectionMovement(MouseDownEvent e) { + if (e.Button == MouseButton.Right) + return false; + if (!SelectionHandler.SelectedBlueprints.Any()) return false; diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 2f51643ee3..f65ca43dad 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) @@ -222,7 +225,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected virtual IEnumerable CreateTernaryButtons() { //TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects. - yield return new TernaryButton(NewCombo, "New combo", () => new SpriteIcon { Icon = FontAwesome.Regular.DotCircle }); + yield return new TernaryButton(NewCombo, "New combo", () => new SpriteIcon { Icon = OsuIcon.EditorNewComboA }); foreach (var kvp in SelectionHandler.SelectionSampleStates) yield return new TernaryButton(kvp.Value, kvp.Key.Replace("hit", string.Empty).Titleize(), () => GetIconForSample(kvp.Key)); @@ -269,10 +272,10 @@ namespace osu.Game.Screens.Edit.Compose.Components return new SpriteIcon { Icon = FontAwesome.Solid.Hands }; case HitSampleInfo.HIT_WHISTLE: - return new SpriteIcon { Icon = FontAwesome.Solid.Bullhorn }; + return new SpriteIcon { Icon = OsuIcon.EditorWhistle }; case HitSampleInfo.HIT_FINISH: - return new SpriteIcon { Icon = FontAwesome.Solid.DrumSteelpan }; + return new SpriteIcon { Icon = OsuIcon.EditorFinish }; } return null; diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index ad0e8b124b..378d378be3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -50,6 +51,7 @@ namespace osu.Game.Screens.Edit.Compose.Components Beatmap.HitObjectAdded += AddBlueprintFor; Beatmap.HitObjectRemoved += RemoveBlueprintFor; + Beatmap.SelectedHitObjects.CollectionChanged += updateSelectionLifetime; if (Composer != null) { @@ -144,6 +146,25 @@ namespace osu.Game.Screens.Edit.Compose.Components SelectedItems.AddRange(Beatmap.HitObjects.Except(SelectedItems).ToArray()); } + /// + /// Ensures that newly-selected hitobjects are kept alive + /// and drops that keep-alive from newly-deselected objects. + /// + private void updateSelectionLifetime(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.NewItems != null) + { + foreach (HitObject newSelection in e.NewItems) + Composer.Playfield.SetKeepAlive(newSelection, true); + } + + if (e.OldItems != null) + { + foreach (HitObject oldSelection in e.OldItems) + Composer.Playfield.SetKeepAlive(oldSelection, false); + } + } + protected override void OnBlueprintSelected(SelectionBlueprint blueprint) { base.OnBlueprintSelected(blueprint); @@ -166,6 +187,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { Beatmap.HitObjectAdded -= AddBlueprintFor; Beatmap.HitObjectRemoved -= RemoveBlueprintFor; + Beatmap.SelectedHitObjects.CollectionChanged -= updateSelectionLifetime; } usageEventBuffer?.Dispose(); 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/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 5d9fac739c..0b16941bc4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -3,6 +3,8 @@ 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; @@ -22,7 +24,9 @@ namespace osu.Game.Screens.Edit.Compose.Components private const float button_padding = 5; - public Func? OnRotation; + [Resolved] + private SelectionRotationHandler? rotationHandler { get; set; } + public Func? OnScale; public Func? OnFlip; public Func? OnReverse; @@ -51,27 +55,12 @@ 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; /// - /// Whether horizontal scaling support should be enabled. + /// Whether horizontal scaling (from the left or right edge) support should be enabled. /// public bool CanScaleX { @@ -88,7 +77,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private bool canScaleY; /// - /// Whether vertical scaling support should be enabled. + /// Whether vertical scaling (from the top or bottom edge) support should be enabled. /// public bool CanScaleY { @@ -102,6 +91,27 @@ namespace osu.Game.Screens.Edit.Compose.Components } } + private bool canScaleDiagonally; + + /// + /// Whether diagonal scaling (from a corner) support should be enabled. + /// + /// + /// There are some cases where we only want to allow proportional resizing, and not allow + /// one or both explicit directions of scale. + /// + public bool CanScaleDiagonally + { + get => canScaleDiagonally; + set + { + if (canScaleDiagonally == value) return; + + canScaleDiagonally = value; + recreate(); + } + } + private bool canFlipX; /// @@ -161,7 +171,13 @@ namespace osu.Game.Screens.Edit.Compose.Components 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) { @@ -174,10 +190,10 @@ namespace osu.Game.Screens.Edit.Compose.Components return CanReverse && reverseButton?.TriggerClick() == true; case Key.Comma: - return CanRotate && rotateCounterClockwiseButton?.TriggerClick() == true; + return canRotate.Value && rotateCounterClockwiseButton?.TriggerClick() == true; case Key.Period: - return CanRotate && rotateClockwiseButton?.TriggerClick() == true; + return canRotate.Value && rotateClockwiseButton?.TriggerClick() == true; } return base.OnKeyDown(e); @@ -250,18 +266,18 @@ namespace osu.Game.Screens.Edit.Compose.Components }; if (CanScaleX) addXScaleComponents(); - if (CanScaleX && CanScaleY) addFullScaleComponents(); + if (CanScaleDiagonally) addFullScaleComponents(); if (CanScaleY) addYScaleComponents(); if (CanFlipX) addXFlipComponents(); if (CanFlipY) addYFlipComponents(); - if (CanRotate) addRotationComponents(); + if (canRotate.Value) addRotationComponents(); if (CanReverse) reverseButton = addButton(FontAwesome.Solid.Backward, "Reverse pattern (Ctrl-G)", () => OnReverse?.Invoke()); } private void addRotationComponents() { - rotateCounterClockwiseButton = addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise (Ctrl-<)", () => OnRotation?.Invoke(-90)); - rotateClockwiseButton = addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise (Ctrl->)", () => 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); @@ -313,6 +329,25 @@ namespace osu.Game.Screens.Edit.Compose.Components 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) { var handle = new SelectionBoxScaleHandle @@ -331,7 +366,6 @@ namespace osu.Game.Screens.Edit.Compose.Components var handle = new SelectionBoxRotationHandle { Anchor = anchor, - HandleRotate = angle => OnRotation?.Invoke(angle) }; handle.OperationStarted += operationStarted; @@ -370,7 +404,7 @@ 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; @@ -383,7 +417,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { buttons.Anchor = Anchor.BottomCentre; buttons.Origin = Anchor.BottomCentre; - buttons.Y = Math.Min(0, ToLocalSpace(Parent.ScreenSpaceDrawQuad.BottomLeft).Y - DrawHeight); + buttons.Y = Math.Min(0, ToLocalSpace(Parent!.ScreenSpaceDrawQuad.BottomLeft).Y - DrawHeight); } else if (topExcess > bottomExcess) { 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..5270162189 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,13 @@ 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 (e.Button != MouseButton.Left) + return false; + + if (rotationHandler == null) return false; + + rotationHandler.Begin(); + return true; } protected override void OnDrag(DragEvent e) @@ -99,7 +101,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 +120,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 27664afdb0..7dadf58a4f 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. /// @@ -401,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/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 268fb073d7..5e3d6c5239 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -20,6 +20,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Components.TernaryButtons; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Screens.Edit.Timing; using osuTK; using osuTK.Graphics; @@ -149,7 +150,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/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 75de15fe56..a2704e550c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -141,11 +141,29 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline waveformOpacity = config.GetBindable(OsuSetting.EditorWaveformOpacity); track.BindTo(editorClock.Track); - track.BindValueChanged(_ => waveform.Waveform = beatmap.Value.Waveform, true); + track.BindValueChanged(_ => + { + waveform.Waveform = beatmap.Value.Waveform; + Scheduler.AddOnce(applyVisualOffset, beatmap); + }, true); Zoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom); } + private void applyVisualOffset(IBindable beatmap) + { + waveform.RelativePositionAxes = Axes.X; + + if (beatmap.Value.Track.Length > 0) + waveform.X = -(float)(Editor.WAVEFORM_VISUAL_OFFSET / beatmap.Value.Track.Length); + else + { + // sometimes this can be the case immediately after a track switch. + // reschedule with the hope that the track length eventually populates. + Scheduler.AddOnce(applyVisualOffset, beatmap); + } + } + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index d3a045db07..cfdd5c0e67 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -424,12 +424,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 @@ -438,7 +438,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline double lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1); int proposedCount = Math.Max(0, (int)Math.Round(proposedDuration / lengthOfOneRepeat) - 1); - if (proposedCount == repeatHitObject.RepeatCount || lengthOfOneRepeat == 0) + if (proposedCount == repeatHitObject.RepeatCount || Precision.AlmostEquals(lengthOfOneRepeat, 0)) return; repeatHitObject.RepeatCount = proposedCount; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b885eee46f..c1f6c02301 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -14,6 +14,7 @@ using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; @@ -39,6 +40,7 @@ using osu.Game.Overlays.Notifications; using osu.Game.Overlays.OSD; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose.Components.Timeline; @@ -58,6 +60,19 @@ namespace osu.Game.Screens.Edit [Cached] public partial class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider, ISamplePlaybackDisabler, IBeatSyncProvider { + /// + /// An offset applied to waveform visuals to align them with expectations. + /// + /// + /// Historically, osu! beatmaps have an assumption of full system latency baked in. + /// This comes from a culmination of stable's platform offset, average hardware playback + /// latency, and users having their universal offsets tweaked to previous beatmaps. + /// + /// Coming to this value involved running various tests with existing users / beatmaps. + /// This included both visual and audible comparisons. Ballpark confidence is ≈2 ms. + /// + public const float WAVEFORM_VISUAL_OFFSET = 20; + public override float BackgroundParallaxAmount => 0.1f; public override bool AllowBackButton => false; @@ -199,6 +214,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); @@ -319,7 +336,7 @@ namespace osu.Game.Screens.Edit { undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut), copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy), pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste), @@ -423,9 +440,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(); })); } @@ -711,8 +729,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) { @@ -764,7 +785,7 @@ namespace osu.Game.Screens.Edit private void confirmExitWithSave() { - Save(); + if (!Save()) return; ExitConfirmed = true; this.Exit(); @@ -856,7 +877,7 @@ namespace osu.Game.Screens.Edit private void resetTrack(bool seekToStart = false) { - Beatmap.Value.Track.Stop(); + clock.Stop(); if (seekToStart) { @@ -999,12 +1020,12 @@ namespace osu.Game.Screens.Edit { createDifficultyCreationMenu(), createDifficultySwitchMenu(), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } }, - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), createExportMenu(), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit) }; @@ -1012,23 +1033,36 @@ namespace osu.Game.Screens.Edit { var exportItems = new List { - new EditorMenuItem(EditorStrings.ExportForEditing, MenuItemType.Standard, exportBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, - new EditorMenuItem(EditorStrings.ExportForCompatibility, MenuItemType.Standard, exportLegacyBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + 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() + private void exportBeatmap(bool legacy) { - Save(); - beatmapManager.Export(Beatmap.Value.BeatmapSetInfo); - } + if (HasUnsavedChanges) + { + dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to export it.", () => + { + if (!Save()) return; - private void exportLegacyBeatmap() - { - Save(); - beatmapManager.ExportLegacy(Beatmap.Value.BeatmapSetInfo); + runExport(); + })); + } + else + { + runExport(); + } + + void runExport() + { + if (legacy) + beatmapManager.ExportLegacy(Beatmap.Value.BeatmapSetInfo); + else + beatmapManager.Export(Beatmap.Value.BeatmapSetInfo); + } } /// @@ -1076,6 +1110,19 @@ namespace osu.Game.Screens.Edit protected void CreateNewDifficulty(RulesetInfo rulesetInfo) { + if (isNewBeatmap) + { + dialogOverlay.Push(new SaveRequiredPopupDialog("This beatmap will be saved in order to create another difficulty.", () => + { + if (!Save()) + return; + + CreateNewDifficulty(rulesetInfo); + })); + + return; + } + if (!rulesetInfo.Equals(editorBeatmap.BeatmapInfo.Ruleset)) { switchToNewDifficulty(rulesetInfo, false); @@ -1098,7 +1145,7 @@ namespace osu.Game.Screens.Edit foreach (var rulesetBeatmaps in groupedOrderedBeatmaps) { if (difficultyItems.Count > 0) - difficultyItems.Add(new EditorMenuItemSpacer()); + difficultyItems.Add(new OsuMenuItemSpacer()); foreach (var beatmap in rulesetBeatmaps) { @@ -1118,6 +1165,45 @@ namespace osu.Game.Screens.Edit loader?.CancelPendingDifficultySwitch(); } + public void HandleTimestamp(string timestamp) + { + if (!EditorTimestampParser.TryParse(timestamp, out var timeSpan, out string selection)) + { + Schedule(() => notifications?.Post(new SimpleErrorNotification + { + Icon = FontAwesome.Solid.ExclamationTriangle, + Text = EditorStrings.FailedToParseEditorLink + })); + return; + } + + editorBeatmap.SelectedHitObjects.Clear(); + + if (clock.IsRunning) + clock.Stop(); + + double position = timeSpan.Value.TotalMilliseconds; + + if (string.IsNullOrEmpty(selection)) + { + clock.SeekSmoothlyTo(position); + return; + } + + // Seek to the next closest HitObject instead + HitObject nextObject = editorBeatmap.HitObjects.FirstOrDefault(x => x.StartTime >= position); + + if (nextObject != null) + position = nextObject.StartTime; + + clock.SeekSmoothlyTo(position); + + Mode.Value = EditorScreenMode.Compose; + + // Delegate handling the selection to the ruleset. + currentScreen.Dependencies.Get().SelectFromTimestamp(position, selection); + } + public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime); 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/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index ea2790b50a..575a66d421 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -11,13 +11,14 @@ using osu.Game.Screens.Edit.Compose.Components.Timeline; namespace osu.Game.Screens.Edit { + [Cached] public abstract partial class EditorScreenWithTimeline : EditorScreen { public const float PADDING = 10; - private Container timelineContainer = null!; + public Container TimelineContent { get; private set; } = null!; - private Container mainContent = null!; + public Container MainContent { get; private set; } = null!; private LoadingSpinner spinner = null!; @@ -70,7 +71,7 @@ namespace osu.Game.Screens.Edit { new Drawable[] { - timelineContainer = new Container + TimelineContent = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -93,7 +94,7 @@ namespace osu.Game.Screens.Edit }, new Drawable[] { - mainContent = new Container + MainContent = new Container { Name = "Main content", RelativeSizeAxes = Axes.Both, @@ -116,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/EditorTable.cs b/osu.Game/Screens/Edit/EditorTable.cs index b79d71b42b..e5dc540b06 100644 --- a/osu.Game/Screens/Edit/EditorTable.cs +++ b/osu.Game/Screens/Edit/EditorTable.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.Extensions.LocalisationExtensions; using osu.Framework.Graphics; @@ -46,15 +47,42 @@ namespace osu.Game.Screens.Edit }); } - protected void SetSelectedRow(object? item) + protected int GetIndexForObject(object? item) { + for (int i = 0; i < BackgroundFlow.Count; i++) + { + if (BackgroundFlow[i].Item == item) + return i; + } + + return -1; + } + + protected virtual bool SetSelectedRow(object? item) + { + bool foundSelection = false; + foreach (var b in BackgroundFlow) { b.Selected = ReferenceEquals(b.Item, item); if (b.Selected) + { + Debug.Assert(!foundSelection); OnRowSelected?.Invoke(b); + foundSelection = true; + } } + + return foundSelection; + } + + protected object? GetObjectAtIndex(int index) + { + if (index < 0 || index > BackgroundFlow.Count - 1) + return null; + + return BackgroundFlow[index].Item; } protected override Drawable CreateHeader(int index, TableColumn? column) => new HeaderText(column?.Header ?? default); 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 60% rename from osu.Game/Screens/Edit/GameplayTest/SaveBeforeGameplayTestDialog.cs rename to osu.Game/Screens/Edit/SaveRequiredPopupDialog.cs index eb1df43ddd..3ca92876f1 100644 --- a/osu.Game/Screens/Edit/GameplayTest/SaveBeforeGameplayTestDialog.cs +++ b/osu.Game/Screens/Edit/SaveRequiredPopupDialog.cs @@ -1,17 +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. 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; @@ -20,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 1915b0cfd1..8028df6c0f 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -88,7 +88,7 @@ namespace osu.Game.Screens.Edit.Setup Description = EditorSetupStrings.BaseVelocityDescription, Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier) { - Default = 1, + Default = 1.4, MinValue = 0.4, MaxValue = 3.6, Precision = 0.01f, 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/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 8c84ad90ba..f6d20319cb 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -146,13 +146,8 @@ namespace osu.Game.Screens.Edit.Setup private void updatePlaceholderText() { - audioTrackChooser.Text = audioTrackChooser.Current.Value == null - ? EditorSetupStrings.ClickToSelectTrack - : EditorSetupStrings.ClickToReplaceTrack; - - backgroundChooser.Text = backgroundChooser.Current.Value == null - ? EditorSetupStrings.ClickToSelectBackground - : EditorSetupStrings.ClickToReplaceBackground; + audioTrackChooser.Text = audioTrackChooser.Current.Value?.Name ?? EditorSetupStrings.ClickToSelectTrack; + backgroundChooser.Text = backgroundChooser.Current.Value?.Name ?? EditorSetupStrings.ClickToSelectBackground; } } } diff --git a/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs index 788beba9d9..022da36abc 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Overlays; using osuTK.Graphics; @@ -79,7 +80,7 @@ namespace osu.Game.Screens.Edit.Setup { Title = EditorSetupStrings.BeatmapSetup.ToLower(); Description = EditorSetupStrings.BeatmapSetupDescription; - IconTexture = "Icons/Hexacons/social"; + Icon = OsuIcon.Beatmap; } } diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index 555c36aac0..7cd1dbc630 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -109,8 +109,13 @@ namespace osu.Game.Screens.Edit.Timing controlPointGroups.BindTo(Beatmap.ControlPointInfo.Groups); controlPointGroups.BindCollectionChanged((_, _) => { - table.ControlGroups = controlPointGroups; - changeHandler?.SaveState(); + // This callback can happen many times in a change operation. It gets expensive. + // We really should be handling the `CollectionChanged` event properly. + Scheduler.AddOnce(() => + { + table.ControlGroups = controlPointGroups; + changeHandler?.SaveState(); + }); }, true); table.OnRowSelected += drawable => scroll.ScrollIntoView(drawable); @@ -147,13 +152,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/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index b078e3fa44..219575a380 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -21,7 +21,7 @@ namespace osu.Game.Screens.Edit.Timing public partial class ControlPointTable : EditorTable { [Resolved] - private Bindable selectedGroup { get; set; } = null!; + private Bindable selectedGroup { get; set; } = null!; [Resolved] private EditorClock clock { get; set; } = null!; @@ -32,6 +32,8 @@ namespace osu.Game.Screens.Edit.Timing { set { + int selectedIndex = GetIndexForObject(selectedGroup.Value); + Content = null; BackgroundFlow.Clear(); @@ -42,18 +44,28 @@ namespace osu.Game.Screens.Edit.Timing { BackgroundFlow.Add(new RowBackground(group) { - Action = () => + // schedule to give time for any modified focused text box to lose focus and commit changes (e.g. BPM / time signature textboxes) before switching to new point. + Action = () => Schedule(() => { - selectedGroup.Value = group; + SetSelectedRow(group); clock.SeekSmoothlyTo(group.Time); - } + }) }); } Columns = createHeaders(); Content = value.Select(createContent).ToArray().ToRectangular(); - updateSelectedGroup(); + // Attempt to retain selection. + if (SetSelectedRow(selectedGroup.Value)) + return; + + // Some operations completely obliterate references, so best-effort reselect based on index. + if (SetSelectedRow(GetObjectAtIndex(selectedIndex))) + return; + + // Selection could not be retained. + selectedGroup.Value = null; } } @@ -61,10 +73,18 @@ namespace osu.Game.Screens.Edit.Timing { base.LoadComplete(); - selectedGroup.BindValueChanged(_ => updateSelectedGroup(), true); + // Handle external selections. + selectedGroup.BindValueChanged(g => SetSelectedRow(g.NewValue), true); } - private void updateSelectedGroup() => SetSelectedRow(selectedGroup.Value); + protected override bool SetSelectedRow(object? item) + { + if (!base.SetSelectedRow(item)) + return false; + + selectedGroup.Value = item as ControlPointGroup; + return true; + } private TableColumn[] createHeaders() { diff --git a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs index eabe9b9f64..151d469415 100644 --- a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs +++ b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs @@ -103,7 +103,7 @@ namespace osu.Game.Screens.Edit.Timing break; default: - slider.Current.Parse(t.Text); + slider.Current.Parse(t.Text, CultureInfo.CurrentCulture); break; } } diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index 9f03281d0c..29e730c865 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -34,16 +34,18 @@ namespace osu.Game.Screens.Edit.Timing private IAdjustableClock metronomeClock = null!; - private Sample? sampleTick; - private Sample? sampleTickDownbeat; private Sample? sampleLatch; - private ScheduledDelegate? tickPlaybackDelegate; + private readonly MetronomeTick metronomeTick = new MetronomeTick(); [Resolved] private OverlayColourProvider overlayColourProvider { get; set; } = null!; - public bool EnableClicking { get; set; } = true; + public bool EnableClicking + { + get => metronomeTick.EnableClicking; + set => metronomeTick.EnableClicking = value; + } public MetronomeDisplay() { @@ -53,8 +55,6 @@ namespace osu.Game.Screens.Edit.Timing [BackgroundDependencyLoader] private void load(AudioManager audio) { - sampleTick = audio.Samples.Get(@"UI/metronome-tick"); - sampleTickDownbeat = audio.Samples.Get(@"UI/metronome-tick-downbeat"); sampleLatch = audio.Samples.Get(@"UI/metronome-latch"); const float taper = 25; @@ -67,8 +67,11 @@ namespace osu.Game.Screens.Edit.Timing AutoSizeAxes = Axes.Both; + metronomeTick.Ticked = onTickPlayed; + InternalChildren = new Drawable[] { + metronomeTick, new Container { Name = @"Taper adjust", @@ -265,9 +268,6 @@ namespace osu.Game.Screens.Edit.Timing isSwinging = false; - tickPlaybackDelegate?.Cancel(); - tickPlaybackDelegate = null; - // instantly latch if pendulum arm is close enough to center (to prevent awkward delayed playback of latch sound) if (Precision.AlmostEquals(swing.Rotation, 0, 1)) { @@ -306,27 +306,53 @@ namespace osu.Game.Screens.Edit.Timing float targetAngle = currentAngle > 0 ? -angle : angle; swing.RotateTo(targetAngle, beatLength, Easing.InOutQuad); + } - if (currentAngle != 0 && Math.Abs(currentAngle - targetAngle) > angle * 1.8f && isSwinging) + private void onTickPlayed() + { + // Originally, this flash only occurred when the pendulum correctly passess the centre. + // Mappers weren't happy with the metronome tick not playing immediately after starting playback + // so now this matches the actual tick sample. + stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint); + } + + private partial class MetronomeTick : BeatSyncedContainer + { + public bool EnableClicking; + + private Sample? sampleTick; + private Sample? sampleTickDownbeat; + + public Action? Ticked; + + public MetronomeTick() { - using (BeginDelayedSequence(beatLength / 2)) - { - stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint); + AllowMistimedEventFiring = false; + } - tickPlaybackDelegate = Schedule(() => - { - if (!EnableClicking) - return; + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleTick = audio.Samples.Get(@"UI/metronome-tick"); + sampleTickDownbeat = audio.Samples.Get(@"UI/metronome-tick-downbeat"); + } - var channel = beatIndex % timingPoint.TimeSignature.Numerator == 0 ? sampleTickDownbeat?.GetChannel() : sampleTick?.GetChannel(); + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); - if (channel == null) - return; + if (!IsBeatSyncedWithTrack || !EnableClicking) + return; - channel.Frequency.Value = RNG.NextDouble(0.98f, 1.02f); - channel.Play(); - }); - } + var channel = beatIndex % timingPoint.TimeSignature.Numerator == 0 ? sampleTickDownbeat?.GetChannel() : sampleTick?.GetChannel(); + + if (channel == null) + return; + + channel.Frequency.Value = RNG.NextDouble(0.98f, 1.02f); + channel.Play(); + + Ticked?.Invoke(); } } } 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/WaveformComparisonDisplay.cs b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs index 3b3acea935..45213b7bdb 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. @@ -212,12 +219,12 @@ namespace osu.Game.Screens.Edit.Timing // offset to the required beat index. double time = selectedGroupStartTime + index * timingPoint.BeatLength; - float offset = (float)(time - visible_width / 2) / trackLength * scale; + float offset = (float)(time - visible_width / 2 + Editor.WAVEFORM_VISUAL_OFFSET) / trackLength * scale; row.Alpha = time < selectedGroupStartTime || time > selectedGroupEndTime ? 0.2f : 1; row.WaveformOffsetTo(-offset, animated); row.WaveformScale = new Vector2(scale, 1); - row.BeatIndex = (int)Math.Floor(index); + row.BeatIndex = (int)Math.Round(index); index++; } diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index 907949aee8..d07190fca0 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Edit.Verify new RoundedButton { Text = "Refresh", - Action = refresh, + Action = Refresh, Size = new Vector2(120, 40), Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, @@ -86,13 +86,13 @@ namespace osu.Game.Screens.Edit.Verify { base.LoadComplete(); - verify.InterpretedDifficulty.BindValueChanged(_ => refresh()); - verify.HiddenIssueTypes.BindCollectionChanged((_, _) => refresh()); + verify.InterpretedDifficulty.BindValueChanged(_ => Refresh()); + verify.HiddenIssueTypes.BindCollectionChanged((_, _) => Refresh()); - refresh(); + Refresh(); } - private void refresh() + public void Refresh() { var issues = generalVerifier.Run(context); diff --git a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs index b17cf3379e..b6e0450e23 100644 --- a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs +++ b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs @@ -56,5 +56,11 @@ namespace osu.Game.Screens.Edit.Verify } }; } + + protected override void PopIn() + { + base.PopIn(); + IssueList.Refresh(); + } } } diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 372cfe748e..4dba512cbd 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -21,8 +21,6 @@ namespace osu.Game.Screens { public partial class Loader : StartupScreen { - private bool showDisclaimer; - public Loader() { ValidForResume = false; @@ -35,13 +33,7 @@ namespace osu.Game.Screens private LoadingSpinner spinner; private ScheduledDelegate spinnerShow; - protected virtual OsuScreen CreateLoadableScreen() - { - if (showDisclaimer) - return new Disclaimer(getIntroSequence()); - - return getIntroSequence(); - } + protected virtual OsuScreen CreateLoadableScreen() => getIntroSequence(); private IntroScreen getIntroSequence() { @@ -107,9 +99,8 @@ namespace osu.Game.Screens } [BackgroundDependencyLoader] - private void load(OsuGameBase game, OsuConfigManager config) + private void load(OsuConfigManager config) { - showDisclaimer = game.IsDeployedBuild; introSequence = config.Get(OsuSetting.IntroSequence); } diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 2ead18c3d6..d742d2377f 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -1,21 +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 JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Extensions.LocalisationExtensions; 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.Framework.Logging; @@ -36,29 +33,29 @@ namespace osu.Game.Screens.Menu { public partial class ButtonSystem : Container, IStateful, IKeyBindingHandler { - public event Action StateChanged; - - private readonly IBindable isIdle = new BindableBool(); - - public Action OnEdit; - public Action OnExit; - public Action OnBeatmapListing; - public Action OnSolo; - public Action OnSettings; - public Action OnMultiplayer; - public Action OnPlaylists; - public const float BUTTON_WIDTH = 140f; public const float WEDGE_WIDTH = 20; - [CanBeNull] - private OsuLogo logo; + public event Action? StateChanged; + + public Action? OnEditBeatmap; + public Action? OnEditSkin; + public Action? OnExit; + public Action? OnBeatmapListing; + public Action? OnSolo; + public Action? OnSettings; + public Action? OnMultiplayer; + public Action? OnPlaylists; + + private readonly IBindable isIdle = new BindableBool(); + + private OsuLogo? logo; /// /// Assign the that this ButtonSystem should manage the position of. /// /// The instance of the logo to be assigned. If null, we are suspending from the screen that uses this ButtonSystem. - public void SetOsuLogo(OsuLogo logo) + public void SetOsuLogo(OsuLogo? logo) { this.logo = logo; @@ -84,8 +81,10 @@ namespace osu.Game.Screens.Menu private readonly List buttonsTopLevel = new List(); private readonly List buttonsPlay = new List(); + private readonly List buttonsEdit = new List(); - private Sample sampleBack; + private Sample? sampleBackToLogo; + private Sample? sampleLogoSwoosh; private readonly LogoTrackingContainer logoTrackingContainer; @@ -103,11 +102,12 @@ namespace osu.Game.Screens.Menu buttonArea.AddRange(new Drawable[] { - new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O), - backButton = new MainMenuButton(ButtonSystemStrings.Back, @"button-back-select", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, + new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O), + backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH) { - VisibleState = ButtonSystemState.Play, + VisibleStateMin = ButtonSystemState.Play, + VisibleStateMax = ButtonSystemState.Edit, }, logoTrackingContainer.LogoFacade.With(d => d.Scale = new Vector2(0.74f)) }); @@ -115,33 +115,36 @@ namespace osu.Game.Screens.Menu buttonArea.Flow.CentreTarget = logoTrackingContainer.LogoFacade; } - [Resolved(CanBeNull = true)] - private OsuGame game { get; set; } + [Resolved] + private IAPIProvider api { get; set; } = null!; [Resolved] - private IAPIProvider api { get; set; } + private OsuGame? game { get; set; } - [Resolved(CanBeNull = true)] - private LoginOverlay loginOverlay { get; set; } + [Resolved] + private LoginOverlay? loginOverlay { get; set; } - [BackgroundDependencyLoader(true)] - private void load(AudioManager audio, IdleTracker idleTracker, GameHost host) + [BackgroundDependencyLoader] + private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host) { - buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); - buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M)); - buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L)); + buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); + buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", OsuIcon.Online, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M)); + buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Tournament, 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.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)); + buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), () => OnEditBeatmap?.Invoke(), WEDGE_WIDTH, Key.B)); + buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), () => OnEditSkin?.Invoke(), 0, Key.S)); + buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit); + + 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-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => State = ButtonSystemState.Edit, 0, Key.E)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, 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)); buttonArea.AddRange(buttonsPlay); + buttonArea.AddRange(buttonsEdit); buttonArea.AddRange(buttonsTopLevel); buttonArea.ForEach(b => @@ -157,7 +160,8 @@ namespace osu.Game.Screens.Menu if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle); - sampleBack = audio.Samples.Get(@"Menu/button-back-select"); + sampleBackToLogo = audio.Samples.Get(@"Menu/back-to-logo"); + sampleLogoSwoosh = audio.Samples.Get(@"Menu/osu-logo-swoosh"); } private void onMultiplayer() @@ -199,6 +203,7 @@ namespace osu.Game.Screens.Menu { if (State == ButtonSystemState.Initial) { + StopSamplePlayback(); logo?.TriggerClick(); return true; } @@ -262,10 +267,16 @@ namespace osu.Game.Screens.Menu { case ButtonSystemState.TopLevel: State = ButtonSystemState.Initial; - sampleBack?.Play(); + + // Samples are explicitly played here in response to user interaction and not when transitioning due to idle. + StopSamplePlayback(); + sampleBackToLogo?.Play(); + return true; + case ButtonSystemState.Edit: case ButtonSystemState.Play: + StopSamplePlayback(); backButton.TriggerClick(); return true; @@ -274,6 +285,13 @@ namespace osu.Game.Screens.Menu } } + public void StopSamplePlayback() + { + buttonsPlay.ForEach(button => button.StopSamplePlayback()); + buttonsTopLevel.ForEach(button => button.StopSamplePlayback()); + logo?.StopSamplePlayback(); + } + private bool onOsuLogo() { switch (state) @@ -292,6 +310,10 @@ namespace osu.Game.Screens.Menu case ButtonSystemState.Play: buttonsPlay.First().TriggerClick(); return false; + + case ButtonSystemState.Edit: + buttonsEdit.First().TriggerClick(); + return false; } } @@ -315,11 +337,13 @@ namespace osu.Game.Screens.Menu Logger.Log($"{nameof(ButtonSystem)}'s state changed from {lastState} to {state}"); + buttonArea.FinishTransforms(true); + using (buttonArea.BeginDelayedSequence(lastState == ButtonSystemState.Initial ? 150 : 0)) { buttonArea.ButtonSystemState = state; - foreach (var b in buttonArea.Children.OfType()) + foreach (var b in buttonArea.OfType()) b.ButtonSystemState = state; } @@ -327,7 +351,7 @@ namespace osu.Game.Screens.Menu } } - private ScheduledDelegate logoDelayedAction; + private ScheduledDelegate? logoDelayedAction; private void updateLogoState(ButtonSystemState lastState = ButtonSystemState.Initial) { @@ -348,6 +372,9 @@ namespace osu.Game.Screens.Menu logo?.MoveTo(new Vector2(0.5f), 800, Easing.OutExpo); logo?.ScaleTo(1, 800, Easing.OutExpo); }, buttonArea.Alpha * 150); + + if (lastState == ButtonSystemState.TopLevel) + sampleLogoSwoosh?.Play(); break; case ButtonSystemState.TopLevel: @@ -398,6 +425,7 @@ namespace osu.Game.Screens.Menu Initial, TopLevel, Play, + Edit, EnteringMode, } } diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs deleted file mode 100644 index 539d58d2d7..0000000000 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ /dev/null @@ -1,257 +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.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Screens; -using osu.Framework.Utils; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.Menu -{ - public partial class Disclaimer : StartupScreen - { - private SpriteIcon icon; - private Color4 iconColour; - private LinkFlowContainer textFlow; - private LinkFlowContainer supportFlow; - - private Drawable heart; - - private const float icon_y = -85; - private const float icon_size = 30; - - private readonly OsuScreen nextScreen; - - private readonly Bindable currentUser = new Bindable(); - private FillFlowContainer fill; - - private readonly List expendableText = new List(); - - public Disclaimer(OsuScreen nextScreen = null) - { - this.nextScreen = nextScreen; - ValidForResume = false; - } - - [Resolved] - private IAPIProvider api { get; set; } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - InternalChildren = new Drawable[] - { - icon = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Icon = OsuIcon.Logo, - Size = new Vector2(icon_size), - Y = icon_y, - }, - fill = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Y = icon_y, - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - Children = new Drawable[] - { - textFlow = new LinkFlowContainer - { - Width = 680, - AutoSizeAxes = Axes.Y, - TextAnchor = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Spacing = new Vector2(0, 2), - }, - } - }, - supportFlow = new LinkFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - TextAnchor = Anchor.BottomCentre, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Padding = new MarginPadding(20), - Alpha = 0, - Spacing = new Vector2(0, 2), - }, - }; - - textFlow.AddText("this is osu!", t => t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.Regular)); - - expendableText.Add(textFlow.AddText("lazer", t => - { - t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.Regular); - t.Colour = colours.PinkLight; - })); - - static void formatRegular(SpriteText t) => t.Font = OsuFont.GetFont(size: 20, weight: FontWeight.Regular); - static void formatSemiBold(SpriteText t) => t.Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold); - - textFlow.NewParagraph(); - - textFlow.AddText("the next ", formatRegular); - textFlow.AddText("major update", t => - { - t.Font = t.Font.With(Typeface.Torus, 20, FontWeight.SemiBold); - t.Colour = colours.Pink; - }); - expendableText.Add(textFlow.AddText(" coming to osu!", formatRegular)); - textFlow.AddText(".", formatRegular); - - textFlow.NewParagraph(); - textFlow.NewParagraph(); - - textFlow.AddParagraph("today's tip:", formatSemiBold); - textFlow.AddParagraph(getRandomTip(), formatRegular); - textFlow.NewParagraph(); - - textFlow.NewParagraph(); - - iconColour = colours.Yellow; - - // manually transfer the user once, but only do the final bind in LoadComplete to avoid thread woes (API scheduler could run while this screen is still loading). - // the manual transfer is here to ensure all text content is loaded ahead of time as this is very early in the game load process and we want to avoid stutters. - currentUser.Value = api.LocalUser.Value; - currentUser.BindValueChanged(e => - { - supportFlow.Children.ForEach(d => d.FadeOut().Expire()); - - if (e.NewValue.IsSupporter) - { - supportFlow.AddText("Eternal thanks to you for supporting osu!", formatSemiBold); - } - else - { - supportFlow.AddText("Consider becoming an ", formatSemiBold); - supportFlow.AddLink("osu!supporter", "https://osu.ppy.sh/home/support", formatSemiBold); - supportFlow.AddText(" to help support osu!'s development", formatSemiBold); - } - - supportFlow.AddIcon(FontAwesome.Solid.Heart, t => - { - heart = t; - - t.Padding = new MarginPadding { Left = 5, Top = 3 }; - t.Font = t.Font.With(size: 20); - t.Origin = Anchor.Centre; - t.Colour = colours.Pink; - - Schedule(() => heart?.FlashColour(Color4.White, 750, Easing.OutQuint).Loop()); - }); - - if (supportFlow.IsPresent) - supportFlow.FadeInFromZero(500); - }, true); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - if (nextScreen != null) - LoadComponentAsync(nextScreen); - - ((IBindable)currentUser).BindTo(api.LocalUser); - } - - public override void OnSuspending(ScreenTransitionEvent e) - { - base.OnSuspending(e); - - // Once this screen has finished being displayed, we don't want to unnecessarily handle user change events. - currentUser.UnbindAll(); - } - - public override void OnEntering(ScreenTransitionEvent e) - { - base.OnEntering(e); - - icon.RotateTo(10); - icon.FadeOut(); - icon.ScaleTo(0.5f); - - icon.Delay(500).FadeIn(500).ScaleTo(1, 500, Easing.OutQuint); - - using (BeginDelayedSequence(3000)) - { - icon.FadeColour(iconColour, 200, Easing.OutQuint); - icon.MoveToY(icon_y * 1.3f, 500, Easing.OutCirc) - .RotateTo(-360, 520, Easing.OutQuint) - .Then() - .MoveToY(icon_y, 160, Easing.InQuart) - .FadeColour(Color4.White, 160); - - using (BeginDelayedSequence(520 + 160)) - { - fill.MoveToOffset(new Vector2(0, 15), 160, Easing.OutQuart); - Schedule(() => expendableText.SelectMany(t => t.Drawables).ForEach(t => - { - t.FadeOut(100); - t.ScaleTo(new Vector2(0, 1), 100, Easing.OutQuart); - })); - } - } - - supportFlow.FadeOut().Delay(2000).FadeIn(500); - double delay = 500; - foreach (var c in textFlow.Children) - c.FadeTo(0.001f).Delay(delay += 20).FadeIn(500); - - this - .FadeInFromZero(500) - .Then(5500) - .FadeOut(250) - .ScaleTo(0.9f, 250, Easing.InQuint) - .Finally(_ => - { - if (nextScreen != null) - this.Push(nextScreen); - }); - } - - private string getRandomTip() - { - string[] tips = - { - "You can press Ctrl-T anywhere in the game to toggle the toolbar!", - "You can press Ctrl-O anywhere in the game to access options!", - "All settings are dynamic and take effect in real-time. Try pausing and changing the skin while playing!", - "New features are coming online every update. Make sure to stay up-to-date!", - "If you find the UI too large or small, try adjusting UI scale in settings!", - "Try adjusting the \"Screen Scaling\" mode to change your gameplay or UI area, even in fullscreen!", - "What used to be \"osu!direct\" is available to all users just like on the website. You can access it anywhere using Ctrl-B!", - "Seeking in replays is available by dragging on the difficulty bar at the bottom of the screen!", - "Multithreading support means that even with low \"FPS\" your input and judgements will be accurate!", - "Try scrolling down in the mod select panel to find a bunch of new fun mods!", - "Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!", - "Get more details, hide or delete a beatmap by right-clicking on its panel at song select!", - "All delete operations are temporary until exiting. Restore accidentally deleted content from the maintenance settings!", - "Check out the \"playlists\" system, which lets users create their own custom and permanent leaderboards!", - "Toggle advanced frame / thread statistics with Ctrl-F11!", - "Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!", - }; - - return tips[RNG.Next(0, tips.Length)]; - } - } -} diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index de7732dd5e..ac7dffc241 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -95,6 +95,8 @@ namespace osu.Game.Screens.Menu Colour = Color4.Black }; + public override bool? AllowGlobalTrackControl => false; + protected IntroScreen([CanBeNull] Func createNextScreen = null) { this.createNextScreen = createNextScreen; 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 index a4d58e398a..07c06dcdb9 100644 --- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -2,10 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; 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; @@ -13,6 +13,9 @@ namespace osu.Game.Screens.Menu { public partial class KiaiMenuFountains : BeatSyncedContainer { + private StarFountain leftFountain = null!; + private StarFountain rightFountain = null!; + [BackgroundDependencyLoader] private void load() { @@ -20,13 +23,13 @@ namespace osu.Game.Screens.Menu Children = new[] { - new StarFountain + leftFountain = new StarFountain { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, X = 250, }, - new StarFountain + rightFountain = new StarFountain { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, @@ -58,8 +61,25 @@ namespace osu.Game.Screens.Menu if (lastTrigger != null && Clock.CurrentTime - lastTrigger < 500) return; - foreach (var fountain in Children.OfType()) - fountain.Shoot(); + 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 fa26cfab46..b722b83280 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -189,7 +189,7 @@ namespace osu.Game.Screens.Menu Source.frequencyAmplitudes.AsSpan().CopyTo(audioData); } - public override void Draw(IRenderer renderer) + protected override void Draw(IRenderer renderer) { base.Draw(renderer); diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 510c9a5373..a75edd1cff 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -5,7 +5,11 @@ using System; using System.Diagnostics; +using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -22,6 +26,8 @@ using osu.Game.Input.Bindings; using osu.Game.IO; using osu.Game.Online.API; using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; +using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; @@ -45,6 +51,8 @@ namespace osu.Game.Screens.Menu public override bool AllowExternalScreenChange => true; + public override bool? AllowGlobalTrackControl => true; + private Screen songSelect; private MenuSideFlashes sideFlashes; @@ -72,6 +80,9 @@ namespace osu.Game.Screens.Menu [Resolved(canBeNull: true)] private IDialogOverlay dialogOverlay { get; set; } + [Resolved(canBeNull: true)] + private VersionManager versionManager { get; set; } + protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault(); protected override bool PlayExitSound => false; @@ -87,9 +98,18 @@ namespace osu.Game.Screens.Menu private ParallaxContainer buttonsContainer; private SongTicker songTicker; private Container logoTarget; + private SystemTitle systemTitle; + private MenuTip menuTip; + private FillFlowContainer bottomElementsFlow; + private SupporterDisplay supporterDisplay; + + private Sample reappearSampleSwoosh; + + [Resolved(canBeNull: true)] + private SkinEditorOverlay skinEditor { get; set; } [BackgroundDependencyLoader(true)] - private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics) + private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics, AudioManager audio) { holdDelay = config.GetBindable(OsuSetting.UIHoldActivationDelay); loginDisplayed = statics.GetBindable(Static.LoginOverlayDisplayed); @@ -115,11 +135,15 @@ namespace osu.Game.Screens.Menu { Buttons = new ButtonSystem { - OnEdit = delegate + OnEditBeatmap = () => { Beatmap.SetDefault(); this.Push(new EditorLoader()); }, + OnEditSkin = () => + { + skinEditor?.Show(); + }, OnSolo = loadSoloSongSelect, OnMultiplayer = () => this.Push(new Multiplayer()), OnPlaylists = () => this.Push(new Playlists()), @@ -140,6 +164,33 @@ namespace osu.Game.Screens.Menu Margin = new MarginPadding { Right = 15, Top = 5 } }, new KiaiMenuFountains(), + bottomElementsFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Spacing = new Vector2(5), + Children = new Drawable[] + { + menuTip = new MenuTip + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + systemTitle = new SystemTitle + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + } + } + }, + supporterDisplay = new SupporterDisplay + { + Margin = new MarginPadding(5), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }, holdToExitGameOverlay?.CreateProxy() ?? Empty() }); @@ -150,10 +201,12 @@ namespace osu.Game.Screens.Menu case ButtonSystemState.Initial: case ButtonSystemState.Exit: ApplyToBackground(b => b.FadeColour(Color4.White, 500, Easing.OutSine)); + systemTitle.State.Value = Visibility.Hidden; break; default: ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.8f), 500, Easing.OutSine)); + systemTitle.State.Value = Visibility.Visible; break; } }; @@ -161,6 +214,8 @@ namespace osu.Game.Screens.Menu Buttons.OnSettings = () => settings?.ToggleVisibility(); Buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility(); + reappearSampleSwoosh = audio.Samples.Get(@"Menu/reappear-swoosh"); + preloadSongSelect(); } @@ -202,6 +257,9 @@ namespace osu.Game.Screens.Menu dialogOverlay?.Push(new StorageErrorDialog(osuStorage, osuStorage.Error)); } + [CanBeNull] + private Drawable proxiedLogo; + protected override void LogoArriving(OsuLogo logo, bool resuming) { base.LogoArriving(logo, resuming); @@ -211,7 +269,7 @@ namespace osu.Game.Screens.Menu logo.FadeColour(Color4.White, 100, Easing.OutQuint); logo.FadeIn(100, Easing.OutQuint); - logo.ProxyToContainer(logoTarget); + proxiedLogo = logo.ProxyToContainer(logoTarget); if (resuming) { @@ -245,17 +303,42 @@ namespace osu.Game.Screens.Menu } } + protected override void Update() + { + base.Update(); + + bottomElementsFlow.Margin = new MarginPadding + { + Bottom = (versionManager?.DrawHeight + 5) ?? 0 + }; + } + protected override void LogoSuspending(OsuLogo logo) { var seq = logo.FadeOut(300, Easing.InSine) .ScaleTo(0.2f, 300, Easing.InSine); - logo.ReturnProxy(); + 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); @@ -266,18 +349,36 @@ namespace osu.Game.Screens.Menu buttonsContainer.MoveTo(new Vector2(-800, 0), FADE_OUT_DURATION, Easing.InSine); sideFlashes.FadeOut(64, Easing.OutQuint); + + bottomElementsFlow + .ScaleTo(0.9f, 1000, Easing.OutQuint) + .FadeOut(500, Easing.OutQuint); + + supporterDisplay + .FadeOut(500, Easing.OutQuint); } public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); + // Ensures any playing `ButtonSystem` samples are stopped when returning to MainMenu (as to not overlap with the 'back' sample) + Buttons.StopSamplePlayback(); + reappearSampleSwoosh?.Play(); + ApplyToBackground(b => (b as BackgroundScreenDefault)?.Next()); // we may have consumed our preloaded instance, so let's make another. preloadSongSelect(); musicController.EnsurePlayingSomething(); + + // Cycle tip on resuming + menuTip.ShowNextTip(); + + bottomElementsFlow + .ScaleTo(1, 1000, Easing.OutQuint) + .FadeIn(1000, Easing.OutQuint); } public override bool OnExiting(ScreenExitEvent e) @@ -293,7 +394,12 @@ namespace osu.Game.Screens.Menu if (requiresConfirmation) { if (dialogOverlay.CurrentDialog is ConfirmExitDialog exitDialog) - exitDialog.PerformOkAction(); + { + if (exitDialog.Buttons.OfType().FirstOrDefault() != null) + exitDialog.PerformOkAction(); + else + exitDialog.Flash(); + } else { dialogOverlay.Push(new ConfirmExitDialog(() => @@ -315,6 +421,13 @@ namespace osu.Game.Screens.Menu songTicker.Hide(); this.FadeOut(3000); + + bottomElementsFlow + .FadeOut(500, Easing.OutQuint); + + supporterDisplay + .FadeOut(500, Easing.OutQuint); + return base.OnExiting(e); } diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index cd3795711e..422599a4a8 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.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.Linq; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -32,9 +31,12 @@ namespace osu.Game.Screens.Menu /// public partial class MainMenuButton : BeatSyncedContainer, IStateful { - public event Action StateChanged; + public const float BOUNCE_COMPRESSION = 0.9f; + public const float HOVER_SCALE = 1.2f; + public const float BOUNCE_ROTATION = 8; + public event Action? StateChanged; - public readonly Key TriggerKey; + public readonly Key[] TriggerKeys; private readonly Container iconText; private readonly Container box; @@ -43,21 +45,32 @@ namespace osu.Game.Screens.Menu private readonly string sampleName; /// - /// The menu state for which we are visible for. + /// The menu state for which we are visible for (assuming only one). /// - public ButtonSystemState VisibleState = ButtonSystemState.TopLevel; + public ButtonSystemState VisibleState + { + set + { + VisibleStateMin = value; + VisibleStateMax = value; + } + } - private readonly Action clickAction; - private Sample sampleClick; - private Sample sampleHover; + public ButtonSystemState VisibleStateMin = ButtonSystemState.TopLevel; + public ButtonSystemState VisibleStateMax = ButtonSystemState.TopLevel; + + private readonly Action? clickAction; + private Sample? sampleClick; + private Sample? sampleHover; + private SampleChannel? sampleChannel; 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; @@ -115,8 +128,9 @@ namespace osu.Game.Screens.Menu Shadow = true, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(30), + Size = new Vector2(32), Position = new Vector2(0, 0), + Margin = new MarginPadding { Top = -4 }, Icon = symbol }, new OsuSpriteText @@ -126,6 +140,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.Centre, Origin = Anchor.Centre, Position = new Vector2(0, 35), + Margin = new MarginPadding { Left = -3 }, Text = text } } @@ -143,14 +158,14 @@ namespace osu.Game.Screens.Menu double duration = timingPoint.BeatLength / 2; - icon.RotateTo(rightward ? 10 : -10, duration * 2, Easing.InOutSine); + icon.RotateTo(rightward ? BOUNCE_ROTATION : -BOUNCE_ROTATION, duration * 2, Easing.InOutSine); icon.Animate( i => i.MoveToY(-10, duration, Easing.Out), - i => i.ScaleTo(1, duration, Easing.Out) + i => i.ScaleTo(HOVER_SCALE, duration, Easing.Out) ).Then( i => i.MoveToY(0, duration, Easing.In), - i => i.ScaleTo(new Vector2(1, 0.9f), duration, Easing.In) + i => i.ScaleTo(new Vector2(HOVER_SCALE, HOVER_SCALE * BOUNCE_COMPRESSION), duration, Easing.In) ); rightward = !rightward; @@ -167,8 +182,8 @@ namespace osu.Game.Screens.Menu double duration = TimeUntilNextBeat; icon.ClearTransforms(); - icon.RotateTo(rightward ? -10 : 10, duration, Easing.InOutSine); - icon.ScaleTo(new Vector2(1, 0.9f), duration, Easing.Out); + icon.RotateTo(rightward ? -BOUNCE_ROTATION : BOUNCE_ROTATION, duration, Easing.InOutSine); + icon.ScaleTo(new Vector2(HOVER_SCALE, HOVER_SCALE * BOUNCE_COMPRESSION), duration, Easing.Out); return true; } @@ -213,7 +228,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; @@ -224,7 +239,8 @@ namespace osu.Game.Screens.Menu private void trigger() { - sampleClick?.Play(); + sampleChannel = sampleClick?.GetChannel(); + sampleChannel?.Play(); clickAction?.Invoke(); @@ -236,6 +252,8 @@ namespace osu.Game.Screens.Menu public override bool HandleNonPositionalInput => state == ButtonState.Expanded; public override bool HandlePositionalInput => state != ButtonState.Exploded && box.Scale.X >= 0.8f; + public void StopSamplePlayback() => sampleChannel?.Stop(); + protected override void Update() { iconText.Alpha = Math.Clamp((box.Scale.X - 0.5f) / 0.3f, 0, 1); @@ -310,9 +328,9 @@ namespace osu.Game.Screens.Menu break; default: - if (value == VisibleState) + if (value <= VisibleStateMax && value >= VisibleStateMin) State = ButtonState.Expanded; - else if (value < VisibleState) + else if (value < VisibleStateMin) State = ButtonState.Contracted; else State = ButtonState.Exploded; diff --git a/osu.Game/Screens/Menu/MenuTip.cs b/osu.Game/Screens/Menu/MenuTip.cs new file mode 100644 index 0000000000..da349373c3 --- /dev/null +++ b/osu.Game/Screens/Menu/MenuTip.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 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.Utils; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Menu +{ + public partial class MenuTip : CompositeDrawable + { + [Resolved] + private OsuConfigManager config { get; set; } = null!; + + private LinkFlowContainer textFlow = null!; + + private Bindable showMenuTips = null!; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerExponent = 2.5f, + CornerRadius = 15, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + Alpha = 0.4f, + }, + } + }, + textFlow = new LinkFlowContainer + { + Width = 600, + AutoSizeAxes = Axes.Y, + TextAnchor = Anchor.TopCentre, + Spacing = new Vector2(0, 2), + Margin = new MarginPadding(10) + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + showMenuTips = config.GetBindable(OsuSetting.MenuTips); + showMenuTips.BindValueChanged(_ => ShowNextTip(), true); + } + + public void ShowNextTip() + { + if (!showMenuTips.Value) + { + this.FadeOut(100, Easing.OutQuint); + return; + } + + static void formatRegular(SpriteText t) => t.Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular); + static void formatSemiBold(SpriteText t) => t.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold); + + string tip = getRandomTip(); + + textFlow.Clear(); + textFlow.AddParagraph("a tip for you:", formatSemiBold); + textFlow.AddParagraph(tip, formatRegular); + + this.FadeInFromZero(200, Easing.OutQuint) + .Delay(1000 + 80 * tip.Length) + .Then() + .FadeOutFromOne(2000, Easing.OutQuint); + } + + private string getRandomTip() + { + string[] tips = + { + "Press Ctrl-T anywhere in the game to toggle the toolbar!", + "Press Ctrl-O anywhere in the game to access options!", + "All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!", + "New features are coming online every update. Make sure to stay up-to-date!", + "If you find the UI too large or small, try adjusting UI scale in settings!", + "Try adjusting the \"Screen Scaling\" mode to change your gameplay or UI area, even in fullscreen!", + "What used to be \"osu!direct\" is available to all users just like on the website. You can access it anywhere using Ctrl-B!", + "Seeking in replays is available by dragging on the progress bar at the bottom of the screen or by using the left and right arrow keys!", + "Multithreading support means that even with low \"FPS\" your input and judgements will be accurate!", + "Try scrolling right in mod select to find a bunch of new fun mods!", + "Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!", + "Get more details, hide or delete a beatmap by right-clicking on its panel at song select!", + "All delete operations are temporary until exiting. Restore accidentally deleted content from the maintenance settings!", + "Check out the \"playlists\" system, which lets users create their own custom and permanent leaderboards!", + "Toggle advanced frame / thread statistics with Ctrl-F11!", + "Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!", + "You can pause during a replay by pressing Space!", + "Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!", + "When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!", + "Your gameplay HUD can be customized by using the skin layout editor. Open it at any time via Ctrl-Shift-S!", + "Drag and drop any image into the skin editor to load it in quickly!", + "You can create mod presets to make toggling your favorite mod combinations easier!", + "Many mods have customisation settings that drastically change how they function. Click the Mod Customisation button in mod select to view settings!", + "Press Ctrl-Shift-R to switch to a random skin!", + "Press Ctrl-Shift-F to toggle the FPS Counter. But make sure not to pay too much attention to it!", + "While watching a replay, press Ctrl-H to toggle replay settings!", + "You can easily copy the mods from scores on a leaderboard by right-clicking on them!", + "Ctrl-Enter at song select will start a beatmap in autoplay mode!" + }; + + return tips[RNG.Next(0, tips.Length)]; + } + } +} diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 8867ecfb2a..75ef8be02e 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -52,6 +52,8 @@ namespace osu.Game.Screens.Menu private readonly IntroSequence intro; private Sample sampleClick; + private SampleChannel sampleClickChannel; + private Sample sampleBeat; private Sample sampleDownbeat; @@ -391,7 +393,11 @@ namespace osu.Game.Screens.Menu flashLayer.FadeOut(1500, Easing.OutExpo); if (Action?.Invoke() == true) - sampleClick.Play(); + { + StopSamplePlayback(); + sampleClickChannel = sampleClick.GetChannel(); + sampleClickChannel.Play(); + } return true; } @@ -440,6 +446,8 @@ namespace osu.Game.Screens.Menu private Container currentProxyTarget; private Drawable proxy; + public void StopSamplePlayback() => sampleClickChannel?.Stop(); + public Drawable ProxyToContainer(Container c) { if (currentProxyTarget != null) diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs index 0d35f6e0e0..dd5171c6be 100644 --- a/osu.Game/Screens/Menu/StarFountain.cs +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Textures; +using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Skinning; @@ -23,7 +24,7 @@ namespace osu.Game.Screens.Menu InternalChild = spewer = new StarFountainSpewer(); } - public void Shoot() => spewer.Shoot(); + public void Shoot(int direction) => spewer.Shoot(direction); protected override void SkinChanged(ISkinSource skin) { @@ -43,8 +44,6 @@ namespace osu.Game.Screens.Menu private const double shoot_duration = 800; - protected override bool CanSpawnParticles => lastShootTime != null && Time.Current - lastShootTime < shoot_duration; - [Resolved] private ISkinSource skin { get; set; } = null!; @@ -57,7 +56,6 @@ namespace osu.Game.Screens.Menu private void load(TextureStore textures) { Texture = skin.GetTexture("Menu/fountain-star") ?? textures.Get("Menu/fountain-star"); - Active.Value = true; } protected override FallingParticle CreateParticle() @@ -81,10 +79,17 @@ namespace osu.Game.Screens.Menu return lastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / shoot_duration) + getRandomVariance(x_velocity_random_variance); } - public void Shoot() + private ScheduledDelegate? deactivateDelegate; + + public void Shoot(int direction) { + Active.Value = true; + + deactivateDelegate?.Cancel(); + deactivateDelegate = Scheduler.AddDelayed(() => Active.Value = false, shoot_duration); + lastShootTime = Clock.CurrentTime; - lastShootDirection = RNG.Next(-1, 2); + lastShootDirection = direction; } private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance); diff --git a/osu.Game/Screens/Menu/SupporterDisplay.cs b/osu.Game/Screens/Menu/SupporterDisplay.cs new file mode 100644 index 0000000000..6639300f4a --- /dev/null +++ b/osu.Game/Screens/Menu/SupporterDisplay.cs @@ -0,0 +1,167 @@ +// 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.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Framework.Threading; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Menu +{ + public partial class SupporterDisplay : CompositeDrawable + { + private LinkFlowContainer supportFlow = null!; + + private Drawable heart = null!; + + private readonly IBindable currentUser = new Bindable(); + + private Box backgroundBox = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + Height = 40; + + AutoSizeAxes = Axes.X; + AutoSizeDuration = 1000; + AutoSizeEasing = Easing.OutQuint; + + Masking = true; + CornerExponent = 2.5f; + CornerRadius = 15; + + InternalChildren = new Drawable[] + { + backgroundBox = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.4f, + }, + supportFlow = new LinkFlowContainer + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + Spacing = new Vector2(0, 2), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + const float font_size = 14; + + static void formatSemiBold(SpriteText t) => t.Font = OsuFont.GetFont(size: font_size, weight: FontWeight.SemiBold); + + currentUser.BindTo(api.LocalUser); + currentUser.BindValueChanged(e => + { + supportFlow.Children.ForEach(d => d.FadeOut().Expire()); + + if (e.NewValue.IsSupporter) + { + supportFlow.AddText("Eternal thanks to you for supporting osu!", formatSemiBold); + + backgroundBox.FadeColour(colours.Pink, 250); + } + else + { + supportFlow.AddText("Consider becoming an ", formatSemiBold); + supportFlow.AddLink("osu!supporter", "https://osu.ppy.sh/home/support", formatSemiBold); + supportFlow.AddText(" to help support osu!'s development", formatSemiBold); + + backgroundBox.FadeColour(colours.Pink4, 250); + } + + supportFlow.AddIcon(FontAwesome.Solid.Heart, t => + { + heart = t; + + t.Padding = new MarginPadding { Left = 5, Top = 1 }; + t.Font = t.Font.With(size: font_size); + t.Origin = Anchor.Centre; + t.Colour = colours.Pink; + + Schedule(() => + { + heart?.FlashColour(Color4.White, 750, Easing.OutQuint).Loop(); + }); + }); + }, true); + + this + .FadeOut() + .Delay(1000) + .FadeInFromZero(800, Easing.OutQuint); + + scheduleDismissal(); + } + + protected override bool OnClick(ClickEvent e) + { + dismissalDelegate?.Cancel(); + + supportFlow.BypassAutoSizeAxes = Axes.X; + this.FadeOut(500, Easing.OutQuint); + return base.OnClick(e); + } + + protected override bool OnHover(HoverEvent e) + { + backgroundBox.FadeTo(0.6f, 500, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + backgroundBox.FadeTo(0.4f, 500, Easing.OutQuint); + base.OnHoverLost(e); + } + + private ScheduledDelegate? dismissalDelegate; + + private void scheduleDismissal() + { + dismissalDelegate?.Cancel(); + dismissalDelegate = Scheduler.AddDelayed(() => + { + // If the user is hovering they may want to interact with the link. + // Give them more time. + if (IsHovered) + { + scheduleDismissal(); + return; + } + + dismissalDelegate?.Cancel(); + + AutoSizeEasing = Easing.In; + supportFlow.BypassAutoSizeAxes = Axes.X; + this + .Delay(200) + .FadeOut(750, Easing.Out); + }, 6000); + } + } +} diff --git a/osu.Game/Screens/Menu/SystemTitle.cs b/osu.Game/Screens/Menu/SystemTitle.cs new file mode 100644 index 0000000000..813a470ed6 --- /dev/null +++ b/osu.Game/Screens/Menu/SystemTitle.cs @@ -0,0 +1,186 @@ +// 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 System.Threading.Tasks; +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.Graphics.Textures; +using osu.Framework.Input.Events; +using osu.Framework.Threading; +using osu.Game.Graphics.Containers; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Screens.Menu +{ + public partial class SystemTitle : VisibilityContainer + { + internal Bindable Current { get; } = new Bindable(); + + private const float transition_duration = 500; + + private Container content = null!; + private CancellationTokenSource? cancellationTokenSource; + private SystemTitleImage? currentImage; + + private ScheduledDelegate? openUrlAction; + + [BackgroundDependencyLoader] + private void load(OsuGame? game) + { + AutoSizeAxes = Axes.Both; + AutoSizeDuration = transition_duration; + AutoSizeEasing = Easing.OutQuint; + + InternalChild = content = new OsuClickableContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Action = () => + { + currentImage?.Flash(); + + // Delay slightly to allow animation to play out. + openUrlAction?.Cancel(); + openUrlAction = Scheduler.AddDelayed(() => + { + if (!string.IsNullOrEmpty(Current.Value?.Url)) + game?.HandleLink(Current.Value.Url); + }, 250); + } + }; + } + + protected override void PopIn() => content.FadeInFromZero(transition_duration, Easing.OutQuint); + + protected override void PopOut() => content.FadeOut(transition_duration, Easing.OutQuint); + + protected override bool OnHover(HoverEvent e) + { + content.ScaleTo(1.05f, 2000, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + content.ScaleTo(1f, 500, Easing.OutQuint); + base.OnHoverLost(e); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + content.ScaleTo(0.95f, 500, Easing.OutQuint); + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + content + .ScaleTo(0.95f) + .ScaleTo(1, 500, Easing.OutElastic); + base.OnMouseUp(e); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(_ => loadNewImage(), true); + + checkForUpdates(); + } + + private void checkForUpdates() + { + var request = new GetSystemTitleRequest(); + Task.Run(() => request.Perform()) + .ContinueWith(r => + { + if (r.IsCompletedSuccessfully) + Schedule(() => Current.Value = request.ResponseObject); + + // if the request failed, "observe" the exception. + // it isn't very important why this failed, as it's only for display. + // the inner error will be logged by framework mechanisms anyway. + if (r.IsFaulted) + _ = r.Exception; + + Scheduler.AddDelayed(checkForUpdates, TimeSpan.FromMinutes(5).TotalMilliseconds); + }); + } + + private void loadNewImage() + { + cancellationTokenSource?.Cancel(); + cancellationTokenSource = null; + currentImage?.FadeOut(500, Easing.OutQuint).Expire(); + + if (string.IsNullOrEmpty(Current.Value?.Image)) + return; + + LoadComponentAsync(new SystemTitleImage(Current.Value), loaded => + { + if (!loaded.SystemTitle.Equals(Current.Value)) + loaded.Dispose(); + + content.Add(currentImage = loaded); + }, (cancellationTokenSource ??= new CancellationTokenSource()).Token); + } + + [LongRunningLoad] + private partial class SystemTitleImage : CompositeDrawable + { + public readonly APISystemTitle SystemTitle; + + private Sprite flash = null!; + + public SystemTitleImage(APISystemTitle systemTitle) + { + SystemTitle = systemTitle; + } + + [BackgroundDependencyLoader] + private void load(LargeTextureStore textureStore) + { + Texture? texture = textureStore.Get(SystemTitle.Image); + if (texture != null && SystemTitle.Image.Contains(@"@2x")) + texture.ScaleAdjust *= 2; + + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Sprite { Texture = texture }, + flash = new Sprite + { + Texture = texture, + Blending = BlendingParameters.Additive, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + this.FadeInFromZero(500, Easing.OutQuint); + flash.FadeOutFromOne(4000, Easing.OutQuint); + } + + public Drawable Flash() + { + flash.FadeInFromZero(50) + .Then() + .FadeOut(500, Easing.OutQuint); + + return this; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs index 00f0889cc8..c4aefe4f99 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs @@ -115,7 +115,7 @@ namespace osu.Game.Screens.OnlinePlay.Components RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex(@"27252d"), }, - avatar = new UpdateableAvatar(showUsernameTooltip: true) { RelativeSizeAxes = Axes.Both }, + avatar = new UpdateableAvatar(showUserPanelOnHover: true) { RelativeSizeAxes = Axes.Both }, }; } } diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 539d5b74b3..e892f9280f 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -98,7 +98,9 @@ namespace osu.Game.Screens.OnlinePlay.Components if (JoinedRoom.Value == null) return; - api.Queue(new PartRoomRequest(joinedRoom.Value)); + if (api.State.Value == APIState.Online) + api.Queue(new PartRoomRequest(joinedRoom.Value)); + joinedRoom.Value = null; } diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 8abdec9ade..5a1648c91f 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -165,7 +165,11 @@ namespace osu.Game.Screens.OnlinePlay { d.SelectedItem.BindTarget = SelectedItem; d.RequestDeletion = i => RequestDeletion?.Invoke(i); - d.RequestResults = i => RequestResults?.Invoke(i); + d.RequestResults = i => + { + SelectedItem.Value = i; + RequestResults?.Invoke(i); + }; d.RequestEdit = i => RequestEdit?.Invoke(i); d.AllowReordering = AllowReordering; d.AllowDeletion = AllowDeletion; diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 3fab0fc180..800c73cceb 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -118,8 +118,6 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(CanBeNull = true)] private ManageCollectionsDialog manageCollectionsDialog { get; set; } - protected override bool ShouldBeConsideredForInput(Drawable child) => AllowReordering || AllowDeletion || !AllowSelection || SelectedItem.Value == Model; - public DrawableRoomPlaylistItem(PlaylistItem item) : base(item) { @@ -367,7 +365,7 @@ namespace osu.Game.Screens.OnlinePlay AutoSizeAxes = Axes.Both, Margin = new MarginPadding { Left = 8, Right = 8 }, }, - mainFillFlow = new FillFlowContainer + mainFillFlow = new MainFlow(() => SelectedItem.Value == Model || !AllowSelection) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -456,6 +454,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 +462,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 +498,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)); @@ -666,5 +668,17 @@ namespace osu.Game.Screens.OnlinePlay public LocalisableString TooltipText => avatar.TooltipText; } } + + public partial class MainFlow : FillFlowContainer + { + private readonly Func allowInteraction; + + public override bool PropagatePositionalInputSubTree => allowInteraction(); + + public MainFlow(Func allowInteraction) + { + this.allowInteraction = allowInteraction; + } + } } } diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 3825cf18b9..dd6536cf26 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -23,7 +23,18 @@ namespace osu.Game.Screens.OnlinePlay { public partial class FooterButtonFreeMods : FooterButton, IHasCurrentValue> { - public Bindable> Current { get; set; } = new BindableWithCurrent>(); + private readonly BindableWithCurrent> current = new BindableWithCurrent>(Array.Empty()); + + public Bindable> Current + { + get => current.Current; + set + { + ArgumentNullException.ThrowIfNull(value); + + current.Current = value; + } + } private OsuSpriteText count = null!; @@ -106,17 +117,17 @@ namespace osu.Game.Screens.OnlinePlay private void updateModDisplay() { - int current = Current.Value.Count; + int currentCount = Current.Value.Count; - if (current == allAvailableAndValidMods.Count()) + if (currentCount == allAvailableAndValidMods.Count()) { count.Text = "all"; count.FadeColour(colours.Gray2, 200, Easing.OutQuint); circle.FadeColour(colours.Yellow, 200, Easing.OutQuint); } - else if (current > 0) + else if (currentCount > 0) { - count.Text = $"{current} mods"; + count.Text = $"{currentCount} mods"; count.FadeColour(colours.Gray2, 200, Easing.OutQuint); circle.FadeColour(colours.YellowDark, 200, Easing.OutQuint); } diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 4d5d724089..7f090aca57 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -14,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; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index 06f9f35479..60e05285d9 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -289,7 +289,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components set => avatar.User = value; } - private readonly UpdateableAvatar avatar = new UpdateableAvatar(showUsernameTooltip: true) { RelativeSizeAxes = Axes.Both }; + private readonly UpdateableAvatar avatar = new UpdateableAvatar(showUserPanelOnHover: true) { RelativeSizeAxes = Axes.Both }; [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) 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/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index fc4a5357c6..3792a67896 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -4,8 +4,8 @@ #nullable disable using System; -using System.Diagnostics; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -19,6 +19,7 @@ using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; +using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input; @@ -39,8 +40,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { public override string Title => "Lounge"; - protected override bool PlayExitSound => false; - protected override BackgroundScreen CreateBackground() => new LoungeBackgroundScreen { SelectedRoom = { BindTarget = SelectedRoom } @@ -77,6 +76,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [CanBeNull] private LeasedBindable selectionLease; + [Resolved] + protected OsuConfigManager Config { get; private set; } + private readonly Bindable filter = new Bindable(new FilterCriteria()); private readonly IBindable operationInProgress = new Bindable(); private readonly IBindable isIdle = new BindableBool(); diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs index 05232fe0e2..916b799d50 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs @@ -113,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; @@ -135,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/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 8d08de4168..f35b205bc4 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.OnlinePlay.Match private BeatmapManager beatmapManager { get; set; } [Resolved] - private RulesetStore rulesets { get; set; } + protected RulesetStore Rulesets { get; private set; } [Resolved] private IAPIProvider api { get; set; } = null!; @@ -371,8 +371,11 @@ namespace osu.Game.Screens.OnlinePlay.Match 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(() => { @@ -419,7 +422,7 @@ namespace osu.Game.Screens.OnlinePlay.Match if (selected == null) return; - var rulesetInstance = rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + var rulesetInstance = Rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); Debug.Assert(rulesetInstance != null); var allowedMods = SelectedItem.Value.AllowedMods.Select(m => m.ToMod(rulesetInstance)); @@ -452,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() @@ -460,7 +463,7 @@ namespace osu.Game.Screens.OnlinePlay.Match if (SelectedItem.Value == null || !this.IsCurrentScreen()) return; - var rulesetInstance = rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + var rulesetInstance = Rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); Debug.Assert(rulesetInstance != null); Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); } @@ -470,7 +473,7 @@ namespace osu.Game.Screens.OnlinePlay.Match if (SelectedItem.Value == null || !this.IsCurrentScreen()) return; - Ruleset.Value = rulesets.GetRuleset(SelectedItem.Value.RulesetID); + Ruleset.Value = Rulesets.GetRuleset(SelectedItem.Value.RulesetID); } private void beginHandlingTrack() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index 44e18dd2bb..ba3508b24f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -16,6 +16,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Threading; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.Countdown; +using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match @@ -28,6 +30,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [CanBeNull] private IDisposable clickOperation; + [Resolved(canBeNull: true)] + private IDialogOverlay dialogOverlay { get; set; } + private Sample sampleReady; private Sample sampleReadyAll; private Sample sampleUnready; @@ -56,7 +61,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { RelativeSizeAxes = Axes.Both, Size = Vector2.One, - Action = onReadyClick, + Action = onReadyButtonClick, }, countdownButton = new MultiplayerCountdownButton { @@ -101,7 +106,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match endOperation(); } - private void onReadyClick() + private void onReadyButtonClick() { if (Room == null) return; @@ -109,9 +114,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Debug.Assert(clickOperation == null); clickOperation = ongoingOperationTracker.BeginOperation(); - if (isReady() && Client.IsHost && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown)) - startMatch(); - else + if (Client.IsHost) + { + if (Room.State == MultiplayerRoomState.Open) + { + if (isReady() && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown)) + startMatch(); + else + toggleReady(); + } + else + { + if (dialogOverlay == null) + abortMatch(); + else + dialogOverlay.Push(new ConfirmAbortDialog(abortMatch, endOperation)); + } + } + else if (Room.State != MultiplayerRoomState.Closed) toggleReady(); bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating; @@ -128,6 +148,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match // gameplay was not started due to an exception; unblock button. endOperation(); }); + + void abortMatch() => Client.AbortMatch().FireAndForget(endOperation, _ => endOperation()); } private void startCountdown(TimeSpan duration) @@ -189,7 +211,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } readyButton.Enabled.Value = countdownButton.Enabled.Value = - Room.State == MultiplayerRoomState.Open + Room.State != MultiplayerRoomState.Closed && CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId && !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired && !operationInProgress.Value; @@ -198,6 +220,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (localUser?.State == MultiplayerUserState.Spectating) readyButton.Enabled.Value &= Client.IsHost && newCountReady > 0 && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown); + // When the local user is not the host, the button should only be enabled when no match is in progress. + if (!Client.IsHost) + readyButton.Enabled.Value &= Room.State == MultiplayerRoomState.Open; + + // At all times, the countdown button should only be enabled when no match is in progress. + countdownButton.Enabled.Value &= Room.State == MultiplayerRoomState.Open; + if (newCountReady == countReady) return; @@ -219,5 +248,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match countReady = newCountReady; }); } + + public partial class ConfirmAbortDialog : DangerousActionDialog + { + public ConfirmAbortDialog(Action abortMatch, Action cancel) + { + HeaderText = "Are you sure you want to abort the match?"; + + DangerousAction = abortMatch; + CancelAction = cancel; + } + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 1be573bdb8..7ce3dde7c2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -149,16 +149,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { switch (localUser?.State) { - default: - Text = "Ready"; - break; - case MultiplayerUserState.Spectating: case MultiplayerUserState.Ready: - Text = room.Host?.Equals(localUser) == true + Text = multiplayerClient.IsHost ? $"Start match {countText}" : $"Waiting for host... {countText}"; + break; + default: + // Show the abort button for the host as long as gameplay is in progress. + if (multiplayerClient.IsHost && room.State != MultiplayerRoomState.Open) + Text = "Abort the match"; + else + Text = "Ready"; break; } } @@ -193,12 +196,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match switch (localUser?.State) { default: - setGreen(); + // Show the abort button for the host as long as gameplay is in progress. + if (multiplayerClient.IsHost && room.State != MultiplayerRoomState.Open) + setRed(); + else + setGreen(); break; case MultiplayerUserState.Spectating: case MultiplayerUserState.Ready: - if (room?.Host?.Equals(localUser) == true && !room.ActiveCountdowns.Any(c => c is MatchStartCountdown)) + if (multiplayerClient.IsHost && !room.ActiveCountdowns.Any(c => c is MatchStartCountdown)) setGreen(); else setYellow(); @@ -206,15 +213,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match break; } - void setYellow() - { - BackgroundColour = colours.YellowDark; - } + void setYellow() => BackgroundColour = colours.YellowDark; - void setGreen() - { - BackgroundColour = colours.Green; - } + void setGreen() => BackgroundColour = colours.Green; + + void setRed() => BackgroundColour = colours.Red; } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 514b80b999..7d27725775 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -7,6 +7,7 @@ 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; @@ -22,7 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.LoadComplete(); client.RoomUpdated += onRoomUpdated; - client.LoadAborted += onLoadAborted; + client.GameplayAborted += onGameplayAborted; onRoomUpdated(); } @@ -38,12 +39,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer transitionFromResults(); } - private void onLoadAborted() + private void onGameplayAborted(GameplayAbortReason reason) { // If the server aborts gameplay for this user (due to loading too slow), exit gameplay screens. if (!this.IsCurrentScreen()) { - Logger.Log("Gameplay aborted because loading the beatmap took too long.", LoggingTarget.Runtime, LogLevel.Important); + switch (reason) + { + case GameplayAbortReason.LoadTookTooLong: + Logger.Log("Gameplay aborted because loading the beatmap took too long.", LoggingTarget.Runtime, LogLevel.Important); + break; + + case GameplayAbortReason.HostAbortedTheMatch: + Logger.Log("The host aborted the match.", LoggingTarget.Runtime, LogLevel.Important); + break; + } + this.MakeCurrent(); } } @@ -90,6 +101,8 @@ 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); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 4478179726..a3a6fd2d8e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; @@ -51,6 +52,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer roomAccessTypeDropdown = new SlimEnumDropdown { RelativeSizeAxes = Axes.None, + Current = Config.GetBindable(OsuSetting.MultiplayerRoomFilter), Width = 160, }; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index f13b47c0f5..a37314de0e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -44,8 +44,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer public override string ShortTitle => "room"; - protected override bool PlayExitSound => !exitConfirmed; - [Resolved] private MultiplayerClient client { get; set; } @@ -241,8 +239,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer // update local mods based on room's reported status for the local user (omitting the base call implementation). // this makes the server authoritative, and avoids the local user potentially setting mods that the server is not aware of (ie. if the match was started during the selection being changed). - var ruleset = Ruleset.Value.CreateInstance(); - Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(ruleset))).ToList(); + var rulesetInstance = Rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + Debug.Assert(rulesetInstance != null); + Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(rulesetInstance)).Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); } [Resolved(canBeNull: true)] @@ -349,6 +348,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer addItemButton.Alpha = localUserCanAddItem ? 1 : 0; Scheduler.AddOnce(UpdateMods); + + Activity.Value = new UserActivity.InLobby(Room); } private bool localUserCanAddItem => client.IsHost || Room.QueueMode.Value != QueueMode.HostOnly; @@ -371,9 +372,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()) @@ -391,6 +389,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(); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 7b448e4b5c..d9043df1d5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -26,9 +26,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { protected override bool PauseOnFocusLost => false; - // Disallow fails in multiplayer for now. - protected override bool CheckModsAllowFailure() => false; - protected override UserActivity InitialActivity => new UserActivity.InMultiplayerGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); [Resolved] @@ -55,6 +52,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { AllowPause = false, AllowRestart = false, + AllowFailAnimation = false, AllowSkipping = room.AutoSkip.Value, AutomaticallySkipIntro = room.AutoSkip.Value, AlwaysShowLeaderboard = true, @@ -148,6 +146,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 9708a94cd7..f665ed2d41 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs @@ -10,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/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/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 7528ff4064..e2159f0e3b 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; @@ -198,25 +199,57 @@ 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) { } - protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) - => instances.Single(i => i.UserId == userId).LoadScore(spectatorGameplayState.Score); + protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) => Schedule(() => + { + var playerArea = instances.Single(i => i.UserId == userId); - protected override void QuitGameplay(int 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 FailGameplay(int userId) + { + // We probably want to visualise this in the future. + } + + protected override void QuitGameplay(int userId) => Schedule(() => { RemoveUser(userId); @@ -224,7 +257,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate instance.FadeColour(colours.Gray4, 400, Easing.OutQuint); syncManager.RemoveManagedClock(instance.SpectatorPlayerClock); - } + }); public override bool OnBackButton() { @@ -232,6 +265,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/PlayerGrid_Cell.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs index 4e624da17f..bc31299615 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs @@ -74,7 +74,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate ); // If we don't track the animating state, the animation will also occur when resizing the window. - isAnimating &= !Precision.AlmostEquals(Position, targetPos, 0.01f); + isAnimating &= !Precision.AlmostEquals(Size, targetSize, 0.5f); } /// @@ -95,11 +95,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } private Vector2 getFinalPosition() => - Parent.ToLocalSpace(facade.ScreenSpaceDrawQuad.Centre); + Parent!.ToLocalSpace(facade.ScreenSpaceDrawQuad.Centre); private Vector2 getFinalSize() => - Parent.ToLocalSpace(facade.ScreenSpaceDrawQuad.BottomRight) - - Parent.ToLocalSpace(facade.ScreenSpaceDrawQuad.TopLeft); + Parent!.ToLocalSpace(facade.ScreenSpaceDrawQuad.BottomRight) + - Parent!.ToLocalSpace(facade.ScreenSpaceDrawQuad.TopLeft); protected override bool OnClick(ClickEvent e) { 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..9de458b5c6 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))] @@ -68,7 +71,7 @@ namespace osu.Game.Screens.OnlinePlay screenStack = new OnlinePlaySubScreenStack { RelativeSizeAxes = Axes.Both }, new Header(ScreenTitle, screenStack), RoomManager, - ongoingOperationTracker + ongoingOperationTracker, } }; } @@ -76,10 +79,7 @@ namespace osu.Game.Screens.OnlinePlay private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => { if (state.NewValue != APIState.Online) - { - Logger.Log("API connection was lost, can't continue with online play", LoggingTarget.Network, LogLevel.Important); Schedule(forcefullyExit); - } }); protected override void LoadComplete() @@ -89,7 +89,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 +120,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 +224,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/OnlinePlaySubScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs index b527bf98a2..fa1ee004c9 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs @@ -13,6 +13,8 @@ namespace osu.Game.Screens.OnlinePlay public virtual string ShortTitle => Title; + protected sealed override bool PlayExitSound => false; + [Resolved] protected IRoomManager? RoomManager { get; private set; } 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/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 2dc9d5d49d..f719ef67c9 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -89,9 +89,9 @@ namespace osu.Game.Screens public virtual bool? AllowGlobalTrackControl => null; - public Bindable Beatmap { get; private set; } + public Bindable Beatmap { get; private set; } = null!; - public Bindable Ruleset { get; private set; } + public Bindable Ruleset { get; private set; } = null!; public Bindable> Mods { get; private set; } @@ -223,7 +223,12 @@ namespace osu.Game.Screens public override bool OnExiting(ScreenExitEvent e) { - if (ValidForResume && PlayExitSound) + // Only play the exit sound if we are the last screen in the exit sequence. + // This stops many sample playbacks from stacking when a huge screen purge happens (ie. returning to menu via the home button + // from a deeply nested screen). + bool arrivingAtFinalDestination = e.Next == e.Destination; + + if (ValidForResume && PlayExitSound && arrivingAtFinalDestination) sampleExit?.Play(); if (ValidForResume && logo != null) diff --git a/osu.Game/Screens/Play/ArgonKeyCounter.cs b/osu.Game/Screens/Play/ArgonKeyCounter.cs index 2d725898d8..874fcde329 100644 --- a/osu.Game/Screens/Play/ArgonKeyCounter.cs +++ b/osu.Game/Screens/Play/ArgonKeyCounter.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. +using System; using osu.Framework.Allocation; 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.Sprites; using osu.Game.Screens.Play.HUD; -using osuTK; namespace osu.Game.Screens.Play { @@ -17,6 +19,8 @@ namespace osu.Game.Screens.Play private OsuSpriteText keyNameText = null!; private OsuSpriteText countText = null!; + private UprightAspectMaintainingContainer uprightContainer = null!; + // These values were taken from Figma private const float line_height = 3; private const float name_font_size = 10; @@ -25,6 +29,8 @@ namespace osu.Game.Screens.Play // Make things look bigger without using Scale private const float scale_factor = 1.5f; + private const float indicator_press_offset = 4; + [Resolved] private OsuColour colours { get; set; } = null!; @@ -40,26 +46,40 @@ namespace osu.Game.Screens.Play { inputIndicator = new Circle { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, Height = line_height * scale_factor, Alpha = 0.5f }, - keyNameText = new OsuSpriteText + new Container { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Position = new Vector2(0, -13) * scale_factor, - Font = OsuFont.Torus.With(size: name_font_size * scale_factor, weight: FontWeight.Bold), - Colour = colours.Blue0, - Text = Trigger.Name - }, - countText = new OsuSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Font = OsuFont.Torus.With(size: count_font_size * scale_factor, weight: FontWeight.Bold), + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = line_height * scale_factor + indicator_press_offset }, + Children = new Drawable[] + { + uprightContainer = new UprightAspectMaintainingContainer + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + keyNameText = new OsuSpriteText + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Font = OsuFont.Torus.With(size: name_font_size * scale_factor, weight: FontWeight.Bold), + Colour = colours.Blue0, + Text = Trigger.Name + }, + countText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Font = OsuFont.Torus.With(size: count_font_size * scale_factor, weight: FontWeight.Bold), + }, + } + } + } }, }; @@ -76,6 +96,21 @@ namespace osu.Game.Screens.Play CountPresses.BindValueChanged(e => countText.Text = e.NewValue.ToString(@"#,0"), true); } + protected override void Update() + { + base.Update(); + + const float allowance = 6; + float absRotation = Math.Abs(uprightContainer.Rotation) % 180; + bool isRotated = absRotation > allowance && absRotation < (180 - allowance); + + keyNameText.Anchor = + keyNameText.Origin = isRotated ? Anchor.TopCentre : Anchor.TopLeft; + + countText.Anchor = + countText.Origin = isRotated ? Anchor.BottomCentre : Anchor.BottomLeft; + } + protected override void Activate(bool forwardPlayback = true) { base.Activate(forwardPlayback); @@ -87,7 +122,7 @@ namespace osu.Game.Screens.Play .FadeIn(10, Easing.OutQuint) .MoveToY(0) .Then() - .MoveToY(4, 60, Easing.OutQuint); + .MoveToY(indicator_press_offset, 60, Easing.OutQuint); } protected override void Deactivate(bool forwardPlayback = true) diff --git a/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs b/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs index 984c2a7287..44b90fcad0 100644 --- a/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs @@ -10,8 +10,6 @@ namespace osu.Game.Screens.Play { public partial class ArgonKeyCounterDisplay : KeyCounterDisplay { - private const int duration = 100; - protected override FillFlowContainer KeyFlow { get; } public ArgonKeyCounterDisplay() @@ -25,16 +23,6 @@ namespace osu.Game.Screens.Play }; } - protected override void Update() - { - base.Update(); - - Size = KeyFlow.Size; - } - protected override KeyCounter CreateCounter(InputTrigger trigger) => new ArgonKeyCounter(trigger); - - protected override void UpdateVisibility() - => KeyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration); } } 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/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 4927800059..e18612c955 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -4,12 +4,14 @@ #nullable disable using System.Collections.Generic; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; using osu.Game.Screens.Play.Break; namespace osu.Game.Screens.Play @@ -46,13 +48,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 +105,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); + ((IBindable)info.GradeDisplay.Current).BindTo(scoreProcessor.Rank); + } } private void initializeBreaks() 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/FailOverlay.cs b/osu.Game/Screens/Play/FailOverlay.cs index abfc401998..210ae5ceb6 100644 --- a/osu.Game/Screens/Play/FailOverlay.cs +++ b/osu.Game/Screens/Play/FailOverlay.cs @@ -26,11 +26,22 @@ namespace osu.Game.Screens.Play public override LocalisableString Header => GameplayMenuOverlayStrings.FailedHeader; + private readonly bool showButtons; + + public FailOverlay(bool showButtons = true) + { + this.showButtons = showButtons; + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { - AddButton(GameplayMenuOverlayStrings.Retry, colours.YellowDark, () => OnRetry?.Invoke()); - AddButton(GameplayMenuOverlayStrings.Quit, new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); + if (showButtons) + { + AddButton(GameplayMenuOverlayStrings.Retry, colours.YellowDark, () => OnRetry?.Invoke()); + AddButton(GameplayMenuOverlayStrings.Quit, new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); + } + // from #10339 maybe this is a better visual effect Add(new Container { diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 22e6884526..c039d1e535 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -17,17 +17,13 @@ namespace osu.Game.Screens.Play /// Encapsulates gameplay timing logic and provides a via DI for gameplay components to use. /// [Cached(typeof(IGameplayClock))] + [Cached(typeof(GameplayClockContainer))] public partial class GameplayClockContainer : Container, IAdjustableClock, IGameplayClock { public IBindable IsPaused => isPaused; public bool IsRewinding => GameplayClock.IsRewinding; - /// - /// The source clock. Should generally not be used for any timekeeping purposes. - /// - public IClock SourceClock { get; private set; } - /// /// Invoked when a seek has been performed via /// @@ -60,15 +56,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 }; } @@ -83,10 +78,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. // Because we generally update our own current time quicker than children can query it (via Start/Seek/Update), // this means that the first frame ever exposed to children may have a non-zero current time. @@ -106,14 +97,6 @@ namespace osu.Game.Screens.Play }); } - /// - /// When is called, this will be run to give an opportunity to prepare the clock at the correct - /// start location. - /// - protected virtual void PrepareStart() - { - } - /// /// Seek to a specific time in gameplay. /// @@ -146,14 +129,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; @@ -168,20 +153,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 @@ -219,7 +191,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/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index 0680842891..440b8d37b9 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -44,7 +44,15 @@ namespace osu.Game.Screens.Play /// /// Action that is invoked when is triggered. /// - protected virtual Action BackAction => () => InternalButtons.LastOrDefault()?.TriggerClick(); + protected virtual Action BackAction => () => + { + // We prefer triggering the button click as it will animate... + // but sometimes buttons aren't present (see FailOverlay's constructor as an example). + if (Buttons.Any()) + Buttons.Last().TriggerClick(); + else + OnQuit?.Invoke(); + }; /// /// Action that is invoked when is triggered. diff --git a/osu.Game/Screens/Play/GameplayOffsetControl.cs b/osu.Game/Screens/Play/GameplayOffsetControl.cs new file mode 100644 index 0000000000..2f0cb821ec --- /dev/null +++ b/osu.Game/Screens/Play/GameplayOffsetControl.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Threading; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Overlays; +using osu.Game.Screens.Play.PlayerSettings; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Play +{ + /// + /// This provides the ability to change the offset while in gameplay. + /// Eventually this should be replaced with all settings from PlayerLoader being accessible from the game. + /// + internal partial class GameplayOffsetControl : VisibilityContainer + { + protected override bool StartHidden => true; + + public override bool PropagateNonPositionalInputSubTree => true; + + // Disable interaction for now to avoid any funny business with slider bar dragging. + public override bool PropagatePositionalInputSubTree => false; + + private BeatmapOffsetControl offsetControl = null!; + + private OsuTextFlowContainer text = null!; + + private ScheduledDelegate? hideOp; + + public GameplayOffsetControl() + { + AutoSizeAxes = Axes.Y; + Width = SettingsToolboxGroup.CONTAINER_WIDTH; + + Masking = true; + CornerRadius = 5; + + // Allow BeatmapOffsetControl to handle keyboard input. + AlwaysPresent = true; + + Anchor = Anchor.CentreRight; + Origin = Anchor.CentreRight; + + X = 100; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider? colourProvider) + { + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.8f, + Colour = colourProvider?.Background4 ?? Color4.Black, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(10), + Spacing = new Vector2(5), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + offsetControl = new BeatmapOffsetControl(), + text = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(weight: FontWeight.SemiBold)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + TextAnchor = Anchor.TopCentre, + } + } + }, + }; + + offsetControl.Current.BindValueChanged(val => + { + text.Text = BeatmapOffsetControl.GetOffsetExplanatoryText(val.NewValue); + Show(); + + hideOp?.Cancel(); + hideOp = Scheduler.AddDelayed(Hide, 500); + }); + } + + protected override void PopIn() + { + this.FadeIn(500, Easing.OutQuint) + .MoveToX(0, 500, Easing.OutQuint); + } + + protected override void PopOut() + { + this.FadeOut(500, Easing.InQuint) + .MoveToX(100, 500, Easing.InQuint); + } + } +} diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index c2162d4df2..cc399a0fbe 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Play public bool HasPassed { get; set; } /// - /// Whether the user failed during gameplay. + /// Whether the user failed during gameplay. This is only set when the gameplay session has completed due to the fail. /// public bool HasFailed { get; set; } diff --git a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs new file mode 100644 index 0000000000..ca00ab12c7 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.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 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.Localisation; +using osu.Game.Configuration; +using osu.Game.Localisation.SkinComponents; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class ArgonAccuracyCounter : GameplayAccuracyCounter, ISerialisableDrawable + { + protected override double RollingDuration => 250; + + [SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")] + public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f) + { + Precision = 0.01f, + MinValue = 0, + MaxValue = 1, + }; + + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))] + public Bindable ShowLabel { get; } = new BindableBool(true); + + public bool UsesFixedAnchor { get; set; } + + protected override IHasText CreateText() => new ArgonAccuracyTextComponent + { + WireframeOpacity = { BindTarget = WireframeOpacity }, + ShowLabel = { BindTarget = ShowLabel }, + }; + + private partial class ArgonAccuracyTextComponent : CompositeDrawable, IHasText + { + private readonly ArgonCounterTextComponent wholePart; + private readonly ArgonCounterTextComponent fractionPart; + private readonly ArgonCounterTextComponent percentText; + + public IBindable WireframeOpacity { get; } = new BindableFloat(); + + public Bindable ShowLabel { get; } = new BindableBool(); + + public LocalisableString Text + { + get => wholePart.Text; + set + { + string[] split = value.ToString().Replace("%", string.Empty).Split("."); + + wholePart.Text = split[0]; + fractionPart.Text = "." + split[1]; + } + } + + public ArgonAccuracyTextComponent() + { + AutoSizeAxes = Axes.Both; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Child = wholePart = new ArgonCounterTextComponent(Anchor.TopRight, BeatmapsetsStrings.ShowScoreboardHeadersAccuracy.ToUpper()) + { + RequiredDisplayDigits = { Value = 3 }, + WireframeOpacity = { BindTarget = WireframeOpacity }, + ShowLabel = { BindTarget = ShowLabel }, + } + }, + fractionPart = new ArgonCounterTextComponent(Anchor.TopLeft) + { + RequiredDisplayDigits = { Value = 2 }, + WireframeOpacity = { BindTarget = WireframeOpacity }, + Scale = new Vector2(0.5f), + }, + percentText = new ArgonCounterTextComponent(Anchor.TopLeft) + { + Text = @"%", + RequiredDisplayDigits = { Value = 1 }, + WireframeOpacity = { BindTarget = WireframeOpacity } + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ShowLabel.BindValueChanged(s => + { + fractionPart.Margin = new MarginPadding { Top = s.NewValue ? 12f * 2f + 4f : 4f }; // +4 to account for the extra spaces above the digits. + percentText.Margin = new MarginPadding { Top = s.NewValue ? 12f : 0 }; + }, true); + } + } + } +} diff --git a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs new file mode 100644 index 0000000000..369c753cb0 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs @@ -0,0 +1,92 @@ +// 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.Sprites; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Localisation.SkinComponents; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets.Scoring; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class ArgonComboCounter : ComboCounter + { + private ArgonCounterTextComponent text = null!; + + protected override double RollingDuration => 250; + + [SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")] + public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f) + { + Precision = 0.01f, + MinValue = 0, + MaxValue = 1, + }; + + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))] + public Bindable ShowLabel { get; } = new BindableBool(true); + + [BackgroundDependencyLoader] + private void load(ScoreProcessor scoreProcessor) + { + Current.BindTo(scoreProcessor.Combo); + Current.BindValueChanged(combo => + { + bool wasIncrease = combo.NewValue > combo.OldValue; + bool wasMiss = combo.OldValue > 1 && combo.NewValue == 0; + + float newScale = Math.Clamp(text.NumberContainer.Scale.X * (wasIncrease ? 1.1f : 0.8f), 0.6f, 1.4f); + + float duration = wasMiss ? 2000 : 500; + + text.NumberContainer + .ScaleTo(new Vector2(newScale)) + .ScaleTo(Vector2.One, duration, Easing.OutQuint); + + if (wasMiss) + text.FlashColour(Color4.Red, duration, Easing.OutQuint); + }); + } + + public override int DisplayedCount + { + get => base.DisplayedCount; + set + { + base.DisplayedCount = value; + updateWireframe(); + } + } + + private void updateWireframe() + { + text.RequiredDisplayDigits.Value = getDigitsRequiredForDisplayCount(); + } + + private int getDigitsRequiredForDisplayCount() + { + // one for the single presumed starting digit, one for the "x" at the end. + int digitsRequired = 2; + long c = DisplayedCount; + while ((c /= 10) > 0) + digitsRequired++; + return digitsRequired; + } + + protected override LocalisableString FormatCount(int count) => $@"{count}x"; + + protected override IHasText CreateText() => text = new ArgonCounterTextComponent(Anchor.TopLeft, MatchesStrings.MatchScoreStatsCombo.ToUpper()) + { + WireframeOpacity = { BindTarget = WireframeOpacity }, + ShowLabel = { BindTarget = ShowLabel }, + }; + } +} diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs new file mode 100644 index 0000000000..f8c82feddd --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -0,0 +1,189 @@ +// 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.Threading.Tasks; +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.Graphics.Textures; +using osu.Framework.Localisation; +using osu.Framework.Text; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class ArgonCounterTextComponent : CompositeDrawable, IHasText + { + private readonly ArgonCounterSpriteText wireframesPart; + private readonly ArgonCounterSpriteText textPart; + private readonly OsuSpriteText labelText; + + public IBindable WireframeOpacity { get; } = new BindableFloat(); + public Bindable RequiredDisplayDigits { get; } = new BindableInt(); + public Bindable ShowLabel { get; } = new BindableBool(); + + public Container NumberContainer { get; private set; } + + public LocalisableString Text + { + get => textPart.Text; + set => textPart.Text = value; + } + + public ArgonCounterTextComponent(Anchor anchor, LocalisableString? label = null) + { + Anchor = anchor; + Origin = anchor; + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + labelText = new OsuSpriteText + { + Alpha = 0, + Text = label.GetValueOrDefault(), + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.Bold), + Margin = new MarginPadding { Left = 2.5f }, + }, + NumberContainer = new Container + { + AutoSizeAxes = Axes.Both, + Children = new[] + { + wireframesPart = new ArgonCounterSpriteText(wireframesLookup) + { + Anchor = anchor, + Origin = anchor, + }, + textPart = new ArgonCounterSpriteText(textLookup) + { + Anchor = anchor, + Origin = anchor, + }, + } + } + }; + + RequiredDisplayDigits.BindValueChanged(digits => wireframesPart.Text = new string('#', digits.NewValue)); + } + + private string textLookup(char c) + { + switch (c) + { + case '.': + return @"dot"; + + case '%': + return @"percentage"; + + default: + return c.ToString(); + } + } + + private string wireframesLookup(char c) + { + if (c == '.') return @"dot"; + + return @"wireframes"; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + labelText.Colour = colours.Blue0; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + WireframeOpacity.BindValueChanged(v => wireframesPart.Alpha = v.NewValue, true); + ShowLabel.BindValueChanged(s => + { + labelText.Alpha = s.NewValue ? 1 : 0; + NumberContainer.Y = s.NewValue ? 12 : 0; + }, true); + } + + private partial class ArgonCounterSpriteText : OsuSpriteText + { + private readonly Func getLookup; + + private GlyphStore glyphStore = null!; + + protected override char FixedWidthReferenceCharacter => '5'; + + public ArgonCounterSpriteText(Func getLookup) + { + this.getLookup = getLookup; + + Shadow = false; + UseFullGlyphHeight = false; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + const string font_name = @"argon-counter"; + + Spacing = new Vector2(-2f, 0f); + Font = new FontUsage(font_name, 1); + glyphStore = new GlyphStore(font_name, textures, getLookup); + + // cache common lookups ahead of time. + foreach (char c in new[] { '.', '%', 'x' }) + glyphStore.Get(font_name, c); + for (int i = 0; i < 10; i++) + glyphStore.Get(font_name, (char)('0' + i)); + } + + protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore); + + private class GlyphStore : ITexturedGlyphLookupStore + { + private readonly string fontName; + private readonly TextureStore textures; + private readonly Func getLookup; + + private readonly Dictionary cache = new Dictionary(); + + public GlyphStore(string fontName, TextureStore textures, Func getLookup) + { + this.fontName = fontName; + this.textures = textures; + this.getLookup = getLookup; + } + + public ITexturedCharacterGlyph? Get(string? fontName, char character) + { + // We only service one font. + if (fontName != this.fontName) + return null; + + if (cache.TryGetValue(character, out var cached)) + return cached; + + string lookup = getLookup(character); + var texture = textures.Get($"Gameplay/Fonts/{fontName}-{lookup}"); + + TexturedCharacterGlyph? glyph = null; + + if (texture != null) + glyph = new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null), texture, 0.125f); + + cache[character] = glyph; + return glyph; + } + + public Task GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character)); + } + } + } +} diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs new file mode 100644 index 0000000000..71996718d9 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -0,0 +1,268 @@ +// 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.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Layout; +using osu.Framework.Threading; +using osu.Framework.Utils; +using osu.Game.Configuration; +using osu.Game.Rulesets.Judgements; +using osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts; +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("Use relative size")] + public BindableBool UseRelativeSize { get; } = new BindableBool(true); + + private ArgonHealthDisplayBar mainBar = null!; + + /// + /// Used to show a glow at the end of the main bar, or red "damage" area when missing. + /// + private ArgonHealthDisplayBar glowBar = null!; + + private Container content = 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 bool displayingMiss => resetMissBarDelegate != null; + + private double glowBarValue; + + private double healthBarValue; + + public const float MAIN_PATH_RADIUS = 10f; + private const float padding = MAIN_PATH_RADIUS * 2; + private const float glow_path_radius = 40f; + private const float main_path_glow_portion = 0.6f; + + private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize); + + public ArgonHealthDisplay() + { + AddLayout(drawSizeLayout); + + // sane default width specification. + // this only matters if the health display isn't part of the default skin + // (in which case width will be set to 300 via `ArgonSkin.GetDrawableComponent()`), + // and if the user hasn't applied their own modifications + // (which are applied via `SerialisedDrawableInfo.ApplySerialisedInfo()`). + Width = 0.98f; + } + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Y; + + InternalChild = content = new Container + { + Children = new Drawable[] + { + new ArgonHealthDisplayBackground + { + RelativeSizeAxes = Axes.Both + }, + new Container + { + RelativeSizeAxes = Axes.Both, + // since we are using bigger path radius we need to expand the draw area outwards to preserve the curve placement + Padding = new MarginPadding(MAIN_PATH_RADIUS - glow_path_radius), + Child = glowBar = new ArgonHealthDisplayBar + { + RelativeSizeAxes = Axes.Both, + BarColour = Color4.White, + GlowColour = main_bar_glow_colour, + Blending = BlendingParameters.Additive, + Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.8f), Color4.White), + PathRadius = glow_path_radius, + GlowPortion = (glow_path_radius - MAIN_PATH_RADIUS * (1f - main_path_glow_portion)) / glow_path_radius, + } + }, + mainBar = new ArgonHealthDisplayBar + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + BarColour = main_bar_colour, + GlowColour = main_bar_glow_colour, + PathRadius = MAIN_PATH_RADIUS, + GlowPortion = main_path_glow_portion + } + } + }; + } + + private bool pendingMissAnimation; + + protected override void LoadComplete() + { + base.LoadComplete(); + + HealthProcessor.NewJudgement += onNewJudgement; + + // we're about to set `RelativeSizeAxes` depending on the value of `UseRelativeSize`. + // setting `RelativeSizeAxes` internally transforms absolute sizing to relative and back to keep the size the same, + // but that is not what we want in this case, since the width at this point is valid in the *target* sizing mode. + // to counteract this, store the numerical value here, and restore it after setting the correct initial relative sizing axes. + float previousWidth = Width; + UseRelativeSize.BindValueChanged(v => RelativeSizeAxes = v.NewValue ? Axes.X : Axes.None, true); + Width = previousWidth; + + BarHeight.BindValueChanged(_ => updateContentSize(), true); + } + + private void onNewJudgement(JudgementResult result) + { + // Check the health increase because cases like osu!catch bananas fire `IgnoreMiss`, + // which counts as a miss but doesn't actually subtract any health. + pendingMissAnimation |= !result.IsHit && result.HealthIncrease < 0; + } + + protected override void Update() + { + base.Update(); + + if (!drawSizeLayout.IsValid) + { + updateContentSize(); + drawSizeLayout.Validate(); + } + + healthBarValue = Interpolation.DampContinuously(healthBarValue, Current.Value, 50, Time.Elapsed); + if (!displayingMiss) + glowBarValue = Interpolation.DampContinuously(glowBarValue, Current.Value, 50, Time.Elapsed); + + 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); + + updatePathProgress(); + } + + protected override void HealthChanged(bool increase) + { + if (Current.Value >= glowBarValue) + finishMissDisplay(); + + if (pendingMissAnimation) + { + triggerMissDisplay(); + pendingMissAnimation = false; + } + + base.HealthChanged(increase); + } + + protected override void FinishInitialAnimation(double value) + { + base.FinishInitialAnimation(value); + this.TransformTo(nameof(healthBarValue), value, 500, Easing.OutQuint); + this.TransformTo(nameof(glowBarValue), value, 250, Easing.OutQuint); + } + + protected override void Flash() + { + base.Flash(); + + if (!displayingMiss) + { + glowBar.TransformTo(nameof(ArgonHealthDisplayBar.GlowColour), Colour4.White, 30, Easing.OutQuint) + .Then() + .TransformTo(nameof(ArgonHealthDisplayBar.GlowColour), main_bar_glow_colour, 300, Easing.OutQuint); + } + } + + private void triggerMissDisplay() + { + resetMissBarDelegate?.Cancel(); + resetMissBarDelegate = null; + + this.Delay(500).Schedule(() => + { + this.TransformTo(nameof(glowBarValue), Current.Value, 300, Easing.OutQuint); + finishMissDisplay(); + }, out resetMissBarDelegate); + + glowBar.TransformTo(nameof(ArgonHealthDisplayBar.BarColour), new Colour4(255, 147, 147, 255), 100, Easing.OutQuint).Then() + .TransformTo(nameof(ArgonHealthDisplayBar.BarColour), new Colour4(255, 93, 93, 255), 800, Easing.OutQuint); + + glowBar.TransformTo(nameof(ArgonHealthDisplayBar.GlowColour), new Colour4(253, 0, 0, 255).Lighten(0.2f)) + .TransformTo(nameof(ArgonHealthDisplayBar.GlowColour), new Colour4(253, 0, 0, 255), 800, Easing.OutQuint); + } + + private void finishMissDisplay() + { + if (!displayingMiss) + return; + + if (Current.Value > 0) + { + glowBar.TransformTo(nameof(ArgonHealthDisplayBar.BarColour), main_bar_colour, 300, Easing.In); + glowBar.TransformTo(nameof(ArgonHealthDisplayBar.GlowColour), main_bar_glow_colour, 300, Easing.In); + } + + resetMissBarDelegate?.Cancel(); + resetMissBarDelegate = null; + } + + private void updateContentSize() + { + float usableWidth = DrawWidth - padding; + + if (usableWidth < 0) enforceMinimumWidth(); + + content.Size = new Vector2(DrawWidth, BarHeight.Value + padding); + updatePathProgress(); + + void enforceMinimumWidth() + { + // Switch to absolute in order to be able to define a minimum width. + // Then switch back is required. Framework will handle the conversion for us. + Axes relativeAxes = RelativeSizeAxes; + RelativeSizeAxes = Axes.None; + + Width = padding; + + RelativeSizeAxes = relativeAxes; + } + } + + private void updatePathProgress() + { + mainBar.ProgressRange = new Vector2(0f, (float)healthBarValue); + glowBar.ProgressRange = new Vector2((float)healthBarValue, (float)Math.Max(glowBarValue, healthBarValue)); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (HealthProcessor.IsNotNull()) + HealthProcessor.NewJudgement -= onNewJudgement; + } + } +} diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs new file mode 100644 index 0000000000..b486465cb0 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs @@ -0,0 +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.Runtime.InteropServices; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Shaders.Types; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts +{ + public partial class ArgonHealthDisplayBackground : Box + { + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "ArgonBarPathBackground"); + } + + protected override DrawNode CreateDrawNode() => new ArgonBarPathDrawNode(this); + + private class ArgonBarPathDrawNode : SpriteDrawNode + { + protected new ArgonHealthDisplayBackground Source => (ArgonHealthDisplayBackground)base.Source; + + private IUniformBuffer? parametersBuffer; + + public ArgonBarPathDrawNode(ArgonHealthDisplayBackground source) + : base(source) + { + } + + private Vector2 size; + + public override void ApplyState() + { + base.ApplyState(); + size = Source.DrawSize; + } + + protected override void BindUniformResources(IShader shader, IRenderer renderer) + { + base.BindUniformResources(shader, renderer); + + parametersBuffer ??= renderer.CreateUniformBuffer(); + parametersBuffer.Data = new ArgonBarPathBackgroundParameters { Size = size }; + + shader.BindUniformBlock("m_ArgonBarPathBackgroundParameters", parametersBuffer); + } + + protected override bool CanDrawOpaqueInterior => false; + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + parametersBuffer?.Dispose(); + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private record struct ArgonBarPathBackgroundParameters + { + public UniformVector2 Size; + private readonly UniformPadding8 pad; + } + } + } +} diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs new file mode 100644 index 0000000000..28e56183bf --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs @@ -0,0 +1,181 @@ +// 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.InteropServices; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Shaders.Types; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts +{ + public partial class ArgonHealthDisplayBar : Box + { + private Vector2 progressRange = new Vector2(0f, 1f); + + public Vector2 ProgressRange + { + get => progressRange; + set + { + if (progressRange == value) + return; + + progressRange = value; + Invalidate(Invalidation.DrawNode); + } + } + + private float radius = 10f; + + public float PathRadius + { + get => radius; + set + { + if (radius == value) + return; + + radius = value; + Invalidate(Invalidation.DrawNode); + } + } + + private float glowPortion; + + public float GlowPortion + { + get => glowPortion; + set + { + if (glowPortion == value) + return; + + glowPortion = value; + Invalidate(Invalidation.DrawNode); + } + } + + private Colour4 barColour = Color4.White; + + public Colour4 BarColour + { + get => barColour; + set + { + if (barColour == value) + return; + + barColour = value; + Invalidate(Invalidation.DrawNode); + } + } + + private Colour4 glowColour = Color4.White.Opacity(0); + + public Colour4 GlowColour + { + get => glowColour; + set + { + if (glowColour == value) + return; + + glowColour = value; + Invalidate(Invalidation.DrawNode); + } + } + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "ArgonBarPath"); + } + + protected override DrawNode CreateDrawNode() => new ArgonBarPathDrawNode(this); + + private class ArgonBarPathDrawNode : SpriteDrawNode + { + protected new ArgonHealthDisplayBar Source => (ArgonHealthDisplayBar)base.Source; + + private IUniformBuffer? parametersBuffer; + + public ArgonBarPathDrawNode(ArgonHealthDisplayBar source) + : base(source) + { + } + + private Vector2 size; + private Vector2 progressRange; + private float pathRadius; + private float glowPortion; + private Color4 barColour; + private Color4 glowColour; + + public override void ApplyState() + { + base.ApplyState(); + + size = Source.DrawSize; + progressRange = new Vector2(Math.Min(Source.progressRange.X, Source.progressRange.Y), Source.progressRange.Y); + pathRadius = Source.PathRadius; + glowPortion = Source.GlowPortion; + barColour = Source.barColour; + glowColour = Source.glowColour; + } + + protected override void Draw(IRenderer renderer) + { + if (pathRadius == 0) + return; + + base.Draw(renderer); + } + + protected override void BindUniformResources(IShader shader, IRenderer renderer) + { + base.BindUniformResources(shader, renderer); + + parametersBuffer ??= renderer.CreateUniformBuffer(); + parametersBuffer.Data = new ArgonBarPathParameters + { + BarColour = new Vector4(barColour.R, barColour.G, barColour.B, barColour.A), + GlowColour = new Vector4(glowColour.R, glowColour.G, glowColour.B, glowColour.A), + GlowPortion = glowPortion, + Size = size, + ProgressRange = progressRange, + PathRadius = pathRadius + }; + + shader.BindUniformBlock("m_ArgonBarPathParameters", parametersBuffer); + } + + protected override bool CanDrawOpaqueInterior => false; + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + parametersBuffer?.Dispose(); + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private record struct ArgonBarPathParameters + { + public UniformVector4 BarColour; + public UniformVector4 GlowColour; + public UniformVector2 Size; + public UniformVector2 ProgressRange; + public UniformFloat PathRadius; + public UniformFloat GlowPortion; + private readonly UniformPadding8 pad; + } + } + } +} diff --git a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs new file mode 100644 index 0000000000..348327d710 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.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 System; +using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Localisation.SkinComponents; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Skinning; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class ArgonScoreCounter : GameplayScoreCounter, ISerialisableDrawable + { + private ArgonScoreTextComponent scoreText = null!; + + protected override double RollingDuration => 250; + + [SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")] + public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f) + { + Precision = 0.01f, + MinValue = 0, + MaxValue = 1, + }; + + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))] + public Bindable ShowLabel { get; } = new BindableBool(true); + + public bool UsesFixedAnchor { get; set; } + + protected override LocalisableString FormatCount(long count) => count.ToLocalisableString(); + + protected override IHasText CreateText() => scoreText = new ArgonScoreTextComponent(Anchor.TopRight, BeatmapsetsStrings.ShowScoreboardHeadersScore.ToUpper()) + { + WireframeOpacity = { BindTarget = WireframeOpacity }, + ShowLabel = { BindTarget = ShowLabel }, + }; + + public ArgonScoreCounter() + { + RequiredDisplayDigits.BindValueChanged(_ => updateWireframe()); + } + + public override long DisplayedCount + { + get => base.DisplayedCount; + set + { + base.DisplayedCount = value; + updateWireframe(); + } + } + + private void updateWireframe() + { + scoreText.RequiredDisplayDigits.Value = + Math.Max(RequiredDisplayDigits.Value, getDigitsRequiredForDisplayCount()); + } + + private int getDigitsRequiredForDisplayCount() + { + int digitsRequired = 1; + long c = DisplayedCount; + while ((c /= 10) > 0) + digitsRequired++; + return digitsRequired; + } + + private partial class ArgonScoreTextComponent : ArgonCounterTextComponent + { + public ArgonScoreTextComponent(Anchor anchor, LocalisableString? label = null) + : base(anchor, label) + { + } + } + } +} diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs index be2ce3b272..cb38854bca 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs @@ -19,6 +19,7 @@ namespace osu.Game.Screens.Play.HUD private readonly ArgonSongProgressGraph graph; private readonly ArgonSongProgressBar bar; private readonly Container graphContainer; + private readonly Container content; private const float bar_height = 10; @@ -30,43 +31,50 @@ namespace osu.Game.Screens.Play.HUD public ArgonSongProgress() { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Anchor = Anchor.BottomCentre; Origin = Anchor.BottomCentre; Masking = true; CornerRadius = 5; - Children = new Drawable[] + + Child = content = new Container { - info = new SongProgressInfo + RelativeSizeAxes = Axes.X, + Children = new Drawable[] { - Origin = Anchor.TopLeft, - Name = "Info", - Anchor = Anchor.TopLeft, - RelativeSizeAxes = Axes.X, - ShowProgress = false - }, - bar = new ArgonSongProgressBar(bar_height) - { - Name = "Seek bar", - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - OnSeek = time => player?.Seek(time), - }, - graphContainer = new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Masking = true, - CornerRadius = 5, - Child = graph = new ArgonSongProgressGraph + info = new SongProgressInfo { - Name = "Difficulty graph", - RelativeSizeAxes = Axes.Both, - Blending = BlendingParameters.Additive + Origin = Anchor.TopLeft, + Name = "Info", + Anchor = Anchor.TopLeft, + RelativeSizeAxes = Axes.X, + ShowProgress = false }, - RelativeSizeAxes = Axes.X, - }, + bar = new ArgonSongProgressBar(bar_height) + { + Name = "Seek bar", + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + OnSeek = time => player?.Seek(time), + }, + graphContainer = new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Masking = true, + CornerRadius = 5, + Child = graph = new ArgonSongProgressGraph + { + Name = "Difficulty graph", + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive + }, + RelativeSizeAxes = Axes.X, + }, + } }; - RelativeSizeAxes = Axes.X; } [BackgroundDependencyLoader] @@ -100,7 +108,7 @@ namespace osu.Game.Screens.Play.HUD protected override void Update() { base.Update(); - Height = bar.Height + bar_height + info.Height; + content.Height = bar.Height + bar_height + info.Height; graphContainer.Height = bar.Height; } diff --git a/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs b/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs new file mode 100644 index 0000000000..3c2e3e05ea --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs @@ -0,0 +1,51 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Configuration; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class ArgonWedgePiece : CompositeDrawable, ISerialisableDrawable + { + public bool UsesFixedAnchor { get; set; } + + [SettingSource("Inverted shear")] + public BindableBool InvertShear { get; } = new BindableBool(); + + public ArgonWedgePiece() + { + CornerRadius = 10f; + + Size = new Vector2(400, 100); + } + + [BackgroundDependencyLoader] + private void load() + { + Masking = true; + Shear = new Vector2(0.8f, 0f); + + InternalChild = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#66CCFF").Opacity(0.0f), Color4Extensions.FromHex("#66CCFF").Opacity(0.25f)), + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + InvertShear.BindValueChanged(v => Shear = new Vector2(0.8f, 0f) * (v.NewValue ? -1 : 1), true); + } + } +} diff --git a/osu.Game/Screens/Play/HUD/BPMCounter.cs b/osu.Game/Screens/Play/HUD/BPMCounter.cs index cd24237493..9cd285db4c 100644 --- a/osu.Game/Screens/Play/HUD/BPMCounter.cs +++ b/osu.Game/Screens/Play/HUD/BPMCounter.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Play.HUD { public partial class BPMCounter : RollingCounter, ISerialisableDrawable { - protected override double RollingDuration => 750; + protected override double RollingDuration => 375; [Resolved] private IBindable beatmap { get; set; } = null!; diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs index 9b5ea309b0..a1cccdef0a 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Play.HUD.ClicksPerSecond [Resolved] private ClicksPerSecondController controller { get; set; } = null!; - protected override double RollingDuration => 350; + protected override double RollingDuration => 175; public bool UsesFixedAnchor { get; set; } diff --git a/osu.Game/Screens/Play/HUD/ComboCounter.cs b/osu.Game/Screens/Play/HUD/ComboCounter.cs index 17531281aa..93802e11c2 100644 --- a/osu.Game/Screens/Play/HUD/ComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/ComboCounter.cs @@ -11,11 +11,6 @@ namespace osu.Game.Screens.Play.HUD { public bool UsesFixedAnchor { get; set; } - protected ComboCounter() - { - Current.Value = DisplayedCount = 0; - } - protected override double GetProportionalDuration(int currentValue, int newValue) { return Math.Abs(currentValue - newValue) * RollingDuration * 100.0f; diff --git a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs index c4d04c5580..f531b1c609 100644 --- a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs @@ -8,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; @@ -112,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) { @@ -119,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/DefaultKeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs index e459574243..e0f96d32bc 100644 --- a/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs @@ -10,7 +10,6 @@ namespace osu.Game.Screens.Play.HUD { public partial class DefaultKeyCounterDisplay : KeyCounterDisplay { - private const int duration = 100; private const double key_fade_time = 80; protected override FillFlowContainer KeyFlow { get; } @@ -25,15 +24,6 @@ namespace osu.Game.Screens.Play.HUD }; } - protected override void Update() - { - base.Update(); - - // Don't use autosize as it will shrink to zero when KeyFlow is hidden. - // In turn this can cause the display to be masked off screen and never become visible again. - Size = KeyFlow.Size; - } - protected override KeyCounter CreateCounter(InputTrigger trigger) => new DefaultKeyCounter(trigger) { FadeTime = key_fade_time, @@ -41,10 +31,6 @@ namespace osu.Game.Screens.Play.HUD KeyUpTextColor = KeyUpTextColor, }; - protected override void UpdateVisibility() => - // Isolate changing visibility of the key counters from fading this component. - KeyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration); - private Color4 keyDownTextColor = Color4.DarkGray; public Color4 KeyDownTextColor diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs index 202ead2d66..48809796f3 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics; @@ -27,6 +28,7 @@ namespace osu.Game.Screens.Play.HUD private readonly DefaultSongProgressBar bar; private readonly DefaultSongProgressGraph graph; private readonly SongProgressInfo info; + private readonly Container content; [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowGraph), nameof(SongProgressStrings.ShowGraphDescription))] public Bindable ShowGraph { get; } = new BindableBool(true); @@ -37,31 +39,36 @@ namespace osu.Game.Screens.Play.HUD public DefaultSongProgress() { RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; Anchor = Anchor.BottomRight; Origin = Anchor.BottomRight; - Children = new Drawable[] + Child = content = new Container { - info = new SongProgressInfo + RelativeSizeAxes = Axes.X, + Children = new Drawable[] { - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - }, - graph = new DefaultSongProgressGraph - { - RelativeSizeAxes = Axes.X, - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - Height = graph_height, - Margin = new MarginPadding { Bottom = bottom_bar_height }, - }, - bar = new DefaultSongProgressBar(bottom_bar_height, graph_height, handle_size) - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - OnSeek = time => player?.Seek(time), - }, + info = new SongProgressInfo + { + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + }, + graph = new DefaultSongProgressGraph + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Height = graph_height, + Margin = new MarginPadding { Bottom = bottom_bar_height }, + }, + bar = new DefaultSongProgressBar(bottom_bar_height, graph_height, handle_size) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + OnSeek = time => player?.Seek(time), + }, + } }; } @@ -107,7 +114,7 @@ namespace osu.Game.Screens.Play.HUD float newHeight = bottom_bar_height + graph_height + handle_size.Y + info.Height - graph.Y; if (!Precision.AlmostEquals(Height, newHeight, 5f)) - Height = newHeight; + content.Height = newHeight; } private void updateBarVisibility() diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index 67e7ae8f3f..2bac7660b3 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; @@ -98,11 +100,11 @@ namespace osu.Game.Screens.Play.HUD protected override void Update() { + base.Update(); + double target = Math.Clamp(max_alpha * (1 - Current.Value / low_health_threshold), 0, max_alpha); boxes.Alpha = (float)Interpolation.Lerp(boxes.Alpha, target, Clock.ElapsedFrameTime * 0.01f); - - base.Update(); } } } diff --git a/osu.Game/Screens/Play/HUD/GameplayAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/GameplayAccuracyCounter.cs index 9da032e489..28d664a48b 100644 --- a/osu.Game/Screens/Play/HUD/GameplayAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/GameplayAccuracyCounter.cs @@ -23,11 +23,11 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - AccuracyDisplay.BindValueChanged(mod => + AccuracyDisplay.BindValueChanged(mode => { Current.UnbindBindings(); - switch (mod.NewValue) + switch (mode.NewValue) { case AccuracyDisplayMode.Standard: Current.BindTo(scoreProcessor.Accuracy); diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index d990af32e7..d2b6b834f8 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -128,7 +128,7 @@ namespace osu.Game.Screens.Play.HUD if (!scroll.IsScrolledToEnd()) fadeBottom -= panel_height; // logic is mostly shared with Leaderboard, copied here for simplicity. - foreach (var c in Flow.Children) + foreach (var c in Flow) { float topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, Flow).Y; float bottomY = topY + panel_height; diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index dcb2c1071e..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; @@ -311,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() @@ -389,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 9fdd735804..3ef3dcb417 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -1,11 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Threading; +using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -23,13 +26,25 @@ namespace osu.Game.Screens.Play.HUD [Resolved] 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 + MaxValue = 1, }; - protected virtual void Flash(JudgementResult result) + private BindableNumber health = null!; + + protected bool InitialAnimationPlaying => initialIncrease != 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() { } @@ -40,20 +55,93 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - Current.BindTo(HealthProcessor.Health); HealthProcessor.NewJudgement += onNewJudgement; + // Don't bind directly so we can animate the startup procedure. + health = HealthProcessor.Health.GetBoundCopy(); + 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); + + initialHealthValue = health.Value; + + if (PlayInitialIncreaseAnimation) + startInitialAnimation(); + else + Current.Value = health.Value; + } + + private double lastValue; + private double initialHealthValue; + + protected override void Update() + { + base.Update(); + + if (!InitialAnimationPlaying || health.Value != initialHealthValue) + { + Current.Value = health.Value; + + if (initialIncrease != null) + FinishInitialAnimation(Current.Value); + } + + // Health changes every frame in draining situations. + // Manually handle value changes to avoid bindable event flow overhead. + if (!Precision.AlmostEquals(lastValue, Current.Value, 0.001f)) + { + HealthChanged(Current.Value > lastValue); + lastValue = Current.Value; + } + } + + protected virtual void HealthChanged(bool increase) + { + } + + 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(health.Value); + }, increase_delay, true); + } + + protected virtual void FinishInitialAnimation(double value) + { + 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); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs index eb5221aa45..443863fb2f 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs @@ -485,7 +485,14 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters } } - public override void Clear() => judgementsContainer.Clear(); + public override void Clear() + { + foreach (var j in judgementsContainer) + { + j.ClearTransforms(); + j.Expire(); + } + } public enum CentreMarkerStyles { diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs index 5793713fca..0f2f9dc323 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs @@ -15,7 +15,6 @@ using osu.Game.Localisation.HUD; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD.HitErrorMeters { @@ -42,16 +41,21 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters [SettingSource(typeof(ColourHitErrorMeterStrings), nameof(ColourHitErrorMeterStrings.JudgementShape), nameof(ColourHitErrorMeterStrings.JudgementShapeDescription))] public Bindable JudgementShape { get; } = new Bindable(); + private readonly DrawablePool judgementShapePool; private readonly JudgementFlow judgementsFlow; public ColourHitErrorMeter() { AutoSizeAxes = Axes.Both; - InternalChild = judgementsFlow = new JudgementFlow + InternalChildren = new Drawable[] { - JudgementShape = { BindTarget = JudgementShape }, - JudgementSpacing = { BindTarget = JudgementSpacing }, - JudgementCount = { BindTarget = JudgementCount } + judgementShapePool = new DrawablePool(50), + judgementsFlow = new JudgementFlow + { + JudgementShape = { BindTarget = JudgementShape }, + JudgementSpacing = { BindTarget = JudgementSpacing }, + JudgementCount = { BindTarget = JudgementCount } + } }; } @@ -60,10 +64,17 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters if (!judgement.Type.IsScorable() || judgement.Type.IsBonus()) return; - judgementsFlow.Push(GetColourForHitResult(judgement.Type)); + judgementsFlow.Push(judgementShapePool.Get(shape => shape.Colour = GetColourForHitResult(judgement.Type))); } - public override void Clear() => judgementsFlow.Clear(); + public override void Clear() + { + foreach (var j in judgementsFlow) + { + j.ClearTransforms(); + j.Expire(); + } + } private partial class JudgementFlow : FillFlowContainer { @@ -98,15 +109,10 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters private readonly DrawablePool judgementLinePool = new DrawablePool(50); - public void Push(Color4 colour) + public void Push(HitErrorShape shape) { - judgementLinePool.Get(shape => - { - shape.Colour = colour; - Add(shape); - - removeExtraJudgements(); - }); + Add(shape); + removeExtraJudgements(); } private void removeExtraJudgements() diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 0921a9f18a..1cf3d25dad 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -16,6 +16,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -44,6 +45,8 @@ namespace osu.Game.Screens.Play.HUD Direction = FillDirection.Horizontal; Spacing = new Vector2(20, 0); Margin = new MarginPadding(10); + + AlwaysPresent = true; } [BackgroundDependencyLoader(true)] @@ -66,9 +69,15 @@ namespace osu.Game.Screens.Play.HUD Action = () => Action(), } }; + AutoSizeAxes = Axes.Both; } + [Resolved] + private SessionStatics sessionStatics { get; set; } + + private Bindable touchActive; + protected override void LoadComplete() { button.HoldActivationDelay.BindValueChanged(v => @@ -78,7 +87,20 @@ namespace osu.Game.Screens.Play.HUD : "press for menu"; }, true); - text.FadeInFromZero(500, Easing.OutQuint).Delay(1500).FadeOut(500, Easing.OutQuint); + touchActive = sessionStatics.GetBindable(Static.TouchInputActive); + + if (touchActive.Value) + { + Alpha = 1f; + text.FadeInFromZero(500, Easing.OutQuint) + .Delay(1500) + .FadeOut(500, Easing.OutQuint); + } + else + { + Alpha = 0; + text.Alpha = 0f; + } base.LoadComplete(); } @@ -99,9 +121,11 @@ namespace osu.Game.Screens.Play.HUD Alpha = 1; else { + float minAlpha = touchActive.Value ? .08f : 0; + Alpha = Interpolation.ValueAt( Math.Clamp(Clock.ElapsedFrameTime, 0, 200), - Alpha, Math.Clamp(1 - positionalAdjust, 0.04f, 1), 0, 200, Easing.OutQuint); + Alpha, Math.Clamp(1 - positionalAdjust, minAlpha, 1), 0, 200, Easing.OutQuint); } } diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs index 43c2ae442a..8134c97bac 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -21,18 +22,30 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter [Resolved] private ScoreProcessor scoreProcessor { get; set; } = null!; - public List Results = new List(); + private readonly Dictionary results = new Dictionary(); + + public IEnumerable Counters => counters; + + private readonly List counters = new List(); [BackgroundDependencyLoader] private void load(IBindable ruleset) { - foreach (var result in ruleset.Value.CreateInstance().GetHitResults()) + // Due to weirdness in judgements, some results have the same name and should be aggregated for display purposes. + // There's only one case of this right now ("slider end"). + foreach (var group in ruleset.Value.CreateInstance().GetHitResults().GroupBy(r => r.displayName)) { - Results.Add(new JudgementCount + var judgementCount = new JudgementCount { - Type = result.result, + DisplayName = group.Key, + Types = group.Select(r => r.result).ToArray(), ResultCount = new BindableInt() - }); + }; + + counters.Add(judgementCount); + + foreach (var r in group) + results[r.result] = judgementCount; } } @@ -46,13 +59,20 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter private void updateCount(JudgementResult judgement, bool revert) { - foreach (JudgementCount result in Results.Where(result => result.Type == judgement.Type)) - result.ResultCount.Value = revert ? result.ResultCount.Value - 1 : result.ResultCount.Value + 1; + if (!results.TryGetValue(judgement.Type, out var count)) + return; + + if (revert) + count.ResultCount.Value--; + else + count.ResultCount.Value++; } public struct JudgementCount { - public HitResult Type { get; set; } + public LocalisableString DisplayName { get; set; } + + public HitResult[] Types { get; set; } public BindableInt ResultCount { get; set; } } diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs index 6c417faac2..45ed8d749b 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.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.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -44,14 +45,14 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter { Alpha = 0, Font = OsuFont.Numeric.With(size: 8), - Text = ruleset.Value.CreateInstance().GetDisplayNameForHitResult(Result.Type) + Text = Result.DisplayName, } } }; - var result = Result.Type; + var result = Result.Types.First(); - Colour = result.IsBasic() ? colours.ForHitResult(Result.Type) : !result.IsBonus() ? colours.PurpleLight : colours.PurpleLighter; + Colour = result.IsBasic() ? colours.ForHitResult(result) : !result.IsBonus() ? colours.PurpleLight : colours.PurpleLighter; } protected override void LoadComplete() diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs index 1dbee19ee3..25e5464205 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.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.Graphics; @@ -22,16 +23,16 @@ 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 JudgementCountController judgementCountController { get; set; } = null!; @@ -49,7 +50,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter AutoSizeAxes = Axes.Both }; - foreach (var result in judgementCountController.Results) + foreach (var result in judgementCountController.Counters) CounterFlow.Add(createCounter(result)); } @@ -63,7 +64,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter CounterFlow.Direction = convertedDirection; - foreach (var counter in CounterFlow.Children) + foreach (var counter in CounterFlow) counter.Direction.Value = convertedDirection; }, true); @@ -88,7 +89,9 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter if (index == 0 && !ShowMaxJudgement.Value) return false; - if (counter.Result.Type.IsBasic()) + var hitResult = counter.Result.Types.First(); + + if (hitResult.IsBasic()) return true; switch (Mode.Value) @@ -97,7 +100,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter return false; case DisplayMode.Normal: - return !counter.Result.Type.IsBonus(); + return !hitResult.IsBonus(); case DisplayMode.All: return true; diff --git a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs index e7e866932e..0a5d6b763e 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs @@ -4,6 +4,7 @@ using System.Collections.Specialized; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Rulesets.UI; @@ -31,13 +32,27 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private InputCountController controller { get; set; } = null!; - protected abstract void UpdateVisibility(); + private const int duration = 100; + + protected void UpdateVisibility() + { + bool visible = AlwaysVisible.Value || ConfigVisibility.Value; + + // Isolate changing visibility of the key counters from fading this component. + KeyFlow.FadeTo(visible ? 1 : 0, duration); + + // Ensure a valid size is immediately obtained even if partially off-screen + // See https://github.com/ppy/osu/issues/14793. + KeyFlow.AlwaysPresent = visible; + } protected abstract KeyCounter CreateCounter(InputTrigger trigger); [BackgroundDependencyLoader] private void load(OsuConfigManager config, DrawableRuleset? drawableRuleset) { + AutoSizeAxes = Axes.Both; + config.BindWith(OsuSetting.KeyOverlay, ConfigVisibility); if (drawableRuleset != null) 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 c064cdb040..ba948b516e 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -63,7 +63,7 @@ namespace osu.Game.Screens.Play.HUD { iconsContainer.Clear(); - 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 0b2ce10ac3..f9cf025b47 100644 --- a/osu.Game/Screens/Play/HUD/ModFlowDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModFlowDisplay.cs @@ -68,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/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 82f116b4ae..f041e120f6 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play.HUD protected override bool IsRollingProportional => true; - protected override double RollingDuration => 1000; + protected override double RollingDuration => 500; private const float alpha_when_invalid = 0.3f; diff --git a/osu.Game/Screens/Play/HUD/PlayerAvatar.cs b/osu.Game/Screens/Play/HUD/PlayerAvatar.cs index 1d0331593a..06d0f7bc9a 100644 --- a/osu.Game/Screens/Play/HUD/PlayerAvatar.cs +++ b/osu.Game/Screens/Play/HUD/PlayerAvatar.cs @@ -7,6 +7,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Localisation.SkinComponents; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Settings; using osu.Game.Skinning; using osu.Game.Users.Drawables; @@ -18,7 +20,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, @@ -29,6 +31,14 @@ namespace osu.Game.Screens.Play.HUD private const float default_size = 80f; + [Resolved] + private GameplayState? gameplayState { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private IBindable? apiUser; + public PlayerAvatar() { Size = new Vector2(default_size); @@ -41,9 +51,15 @@ namespace osu.Game.Screens.Play.HUD } [BackgroundDependencyLoader] - private void load(GameplayState gameplayState) + private void load() { - avatar.User = gameplayState.Score.ScoreInfo.User; + if (gameplayState != null) + avatar.User = gameplayState.Score.ScoreInfo.User; + else + { + apiUser = api.LocalUser.GetBoundCopy(); + apiUser.BindValueChanged(u => avatar.User = u.NewValue, true); + } } protected override void LoadComplete() diff --git a/osu.Game/Screens/Play/HUD/PlayerFlag.cs b/osu.Game/Screens/Play/HUD/PlayerFlag.cs index 85799c03d3..c7e247d26a 100644 --- a/osu.Game/Screens/Play/HUD/PlayerFlag.cs +++ b/osu.Game/Screens/Play/HUD/PlayerFlag.cs @@ -2,8 +2,11 @@ // 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.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Skinning; using osu.Game.Users.Drawables; using osuTK; @@ -12,13 +15,24 @@ namespace osu.Game.Screens.Play.HUD { public partial class PlayerFlag : CompositeDrawable, ISerialisableDrawable { + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => false; + private readonly UpdateableFlag flag; private const float default_size = 40f; + [Resolved] + private GameplayState? gameplayState { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private IBindable? apiUser; + public PlayerFlag() { Size = new Vector2(default_size, default_size / 1.4f); + InternalChild = flag = new UpdateableFlag { RelativeSizeAxes = Axes.Both, @@ -26,9 +40,15 @@ namespace osu.Game.Screens.Play.HUD } [BackgroundDependencyLoader] - private void load(GameplayState gameplayState) + private void load() { - flag.CountryCode = gameplayState.Score.ScoreInfo.User.CountryCode; + if (gameplayState != null) + flag.CountryCode = gameplayState.Score.ScoreInfo.User.CountryCode; + else + { + apiUser = api.LocalUser.GetBoundCopy(); + apiUser.BindValueChanged(u => flag.CountryCode = u.NewValue.CountryCode, true); + } } public bool UsesFixedAnchor { get; set; } diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index dbb0456cd0..a2b49f6302 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -12,17 +12,19 @@ namespace osu.Game.Screens.Play.HUD { private const int fade_duration = 200; - public readonly PlaybackSettings PlaybackSettings; - public readonly VisualSettings VisualSettings; + protected override Container Content => content; + + private readonly FillFlowContainer content; + public PlayerSettingsOverlay() { Anchor = Anchor.TopRight; Origin = Anchor.TopRight; AutoSizeAxes = Axes.Both; - Child = new FillFlowContainer + InternalChild = content = new FillFlowContainer { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -31,7 +33,6 @@ namespace osu.Game.Screens.Play.HUD Spacing = new Vector2(0, 20), Children = new PlayerSettingsGroup[] { - PlaybackSettings = new PlaybackSettings { Expanded = { Value = false } }, VisualSettings = new VisualSettings { Expanded = { Value = false } }, new AudioSettings { Expanded = { Value = false } } } @@ -40,5 +41,7 @@ namespace osu.Game.Screens.Play.HUD protected override void PopIn() => this.FadeIn(fade_duration); protected override void PopOut() => this.FadeOut(fade_duration); + + public void AddAtStart(PlayerSettingsGroup drawable) => content.Insert(-1, drawable); } } diff --git a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs index 701b8a8732..ab7ab6b3a0 100644 --- a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs +++ b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Play.HUD { public bool UsesFixedAnchor { get; set; } - protected override double RollingDuration => 750; + protected override double RollingDuration => 375; private const float alpha_when_invalid = 0.3f; private readonly Bindable valid = new Bindable(); diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index d11171e3fe..b5482f2a5b 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -12,6 +12,7 @@ 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; @@ -69,7 +70,9 @@ namespace osu.Game.Screens.Play public Bindable ShowHealthBar = new Bindable(true); + [CanBeNull] private readonly DrawableRuleset drawableRuleset; + private readonly IReadOnlyList mods; /// @@ -78,6 +81,7 @@ 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(); @@ -95,6 +99,9 @@ namespace osu.Game.Screens.Play private readonly SkinComponentsContainer mainComponents; + [CanBeNull] + private readonly SkinComponentsContainer rulesetComponents; + /// /// A flow which sits at the left side of the screen to house leaderboard (and related) components. /// Will automatically be positioned to avoid colliding with top scoring elements. @@ -103,10 +110,10 @@ namespace osu.Game.Screens.Play private readonly List hideTargets; - public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList mods, bool alwaysShowLeaderboard = true) - { - Drawable rulesetComponents; + private readonly Drawable playfieldComponents; + public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList mods, bool alwaysShowLeaderboard = true) + { this.drawableRuleset = drawableRuleset; this.mods = mods; @@ -120,8 +127,11 @@ namespace osu.Game.Screens.Play clicksPerSecondController = new ClicksPerSecondController(), InputCountController = new InputCountController(), mainComponents = new HUDComponentsContainer { AlwaysPresent = true, }, - rulesetComponents = drawableRuleset != null - ? new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, } + drawableRuleset != null + ? (rulesetComponents = 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 { @@ -162,7 +172,10 @@ namespace osu.Game.Screens.Play }, }; - hideTargets = new List { mainComponents, rulesetComponents, topRightElements }; + hideTargets = new List { mainComponents, playfieldComponents, topRightElements }; + + if (rulesetComponents != null) + hideTargets.Add(rulesetComponents); if (!alwaysShowLeaderboard) hideTargets.Add(LeaderboardFlow); @@ -179,6 +192,7 @@ 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) @@ -230,6 +244,16 @@ namespace osu.Game.Screens.Play { 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; @@ -237,13 +261,37 @@ namespace osu.Game.Screens.Play // LINQ cast can be removed when IDrawable interface includes Anchor / RelativeSizeAxes. foreach (var element in mainComponents.Components.Cast()) + processDrawable(element); + + if (rulesetComponents != null) + { + foreach (var element in rulesetComponents.Components.Cast()) + processDrawable(element); + } + + if (lowestTopScreenSpaceRight.HasValue) + topRightElements.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - topRightElements.DrawHeight); + else + topRightElements.Y = 0; + + if (lowestTopScreenSpaceLeft.HasValue) + LeaderboardFlow.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight); + else + LeaderboardFlow.Y = 0; + + if (highestBottomScreenSpace.HasValue) + bottomRightElements.Y = BottomScoringElementsHeight = -MathHelper.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight); + else + bottomRightElements.Y = 0; + + void processDrawable(Drawable element) { // for now align some top components with the bottom-edge of the lowest top-anchored hud element. if (element.Anchor.HasFlagFast(Anchor.y0)) { // health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area. if (element is LegacyHealthDisplay) - continue; + return; float bottom = element.ScreenSpaceDrawQuad.BottomRight.Y; @@ -269,21 +317,6 @@ namespace osu.Game.Screens.Play highestBottomScreenSpace = topLeft; } } - - if (lowestTopScreenSpaceRight.HasValue) - topRightElements.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - topRightElements.DrawHeight); - else - topRightElements.Y = 0; - - if (lowestTopScreenSpaceLeft.HasValue) - LeaderboardFlow.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight); - else - LeaderboardFlow.Y = 0; - - if (highestBottomScreenSpace.HasValue) - bottomRightElements.Y = BottomScoringElementsHeight = -MathHelper.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight); - else - bottomRightElements.Y = 0; } private void updateVisibility() @@ -381,6 +414,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/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 489a4ef8b3..93bdcb1cab 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -7,7 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; -using osu.Framework.Graphics; +using osu.Framework.Logging; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -34,28 +34,31 @@ namespace osu.Game.Screens.Play public readonly BindableNumber UserPlaybackRate = new BindableDouble(1) { - MinValue = 0.5, + MinValue = 0.05, MaxValue = 2, - Precision = 0.1, + Precision = 0.01, }; + /// + /// Whether the audio playback rate should be validated. + /// Mostly disabled for tests. + /// + internal bool ShouldValidatePlaybackRate { get; init; } + + /// + /// Whether the audio playback is within acceptable ranges. + /// Will become false if audio playback is not going as expected. + /// + public IBindable PlaybackRateValid => playbackRateValid; + + private readonly Bindable playbackRateValid = new Bindable(true); + private readonly WorkingBeatmap beatmap; - private readonly Track track; + private Track track; private readonly double skipTargetTime; - /// - /// Stores the time at which the last call was triggered. - /// This is used to ensure we resume from that precise point in time, ignoring the proceeding frequency ramp. - /// - /// Optimally, we'd have gameplay ramp down with the frequency, but I believe this was intentionally disabled - /// to avoid fails occurring after the pause screen has been shown. - /// - /// In the future I want to change this. - /// - private double? actualStopTime; - [Resolved] private MusicController musicController { get; set; } = null!; @@ -65,7 +68,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; @@ -98,70 +101,17 @@ namespace osu.Game.Screens.Play return time; } - protected override void StopGameplayClock() - { - actualStopTime = GameplayClock.CurrentTime; - - if (IsLoaded) - { - // During normal operation, the source is stopped after performing a frequency ramp. - this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 0, 200, Easing.Out).OnComplete(_ => - { - if (IsPaused.Value) - base.StopGameplayClock(); - }); - } - else - { - base.StopGameplayClock(); - - // If not yet loaded, we still want to ensure relevant state is correct, as it is used for offset calculations. - GameplayClock.ExternalPauseFrequencyAdjust.Value = 0; - - // We must also process underlying gameplay clocks to update rate-adjusted offsets with the new frequency adjustment. - // Without doing this, an initial seek may be performed with the wrong offset. - GameplayClock.ProcessFrame(); - } - } - public override void Seek(double time) { - // Safety in case the clock is seeked while stopped. - actualStopTime = null; + elapsedValidationTime = null; base.Seek(time); } - protected override void PrepareStart() - { - if (actualStopTime != null) - { - Seek(actualStopTime.Value); - actualStopTime = null; - } - else - base.PrepareStart(); - } - protected override void StartGameplayClock() { - addSourceClockAdjustments(); - + addAdjustmentsToTrack(); base.StartGameplayClock(); - - if (IsLoaded) - { - this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 1, 200, Easing.In); - } - else - { - // If not yet loaded, we still want to ensure relevant state is correct, as it is used for offset calculations. - GameplayClock.ExternalPauseFrequencyAdjust.Value = 1; - - // We must also process underlying gameplay clocks to update rate-adjusted offsets with the new frequency adjustment. - // Without doing this, an initial seek may be performed with the wrong offset. - GameplayClock.ProcessFrame(); - } } /// @@ -186,14 +136,70 @@ 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(); } + protected override void Update() + { + base.Update(); + checkPlaybackValidity(); + } + + #region Clock validation (ensure things are running correctly for local gameplay) + + private double elapsedGameplayClockTime; + private double? elapsedValidationTime; + private int playbackDiscrepancyCount; + + private const int allowed_playback_discrepancies = 5; + + private void checkPlaybackValidity() + { + if (!ShouldValidatePlaybackRate) + return; + + if (GameplayClock.IsRunning) + { + elapsedGameplayClockTime += GameplayClock.ElapsedFrameTime; + + if (elapsedValidationTime == null) + elapsedValidationTime = elapsedGameplayClockTime; + else + elapsedValidationTime += GameplayClock.Rate * Time.Elapsed; + + if (Math.Abs(elapsedGameplayClockTime - elapsedValidationTime!.Value) > 300) + { + if (playbackDiscrepancyCount++ > allowed_playback_discrepancies) + { + if (playbackRateValid.Value) + { + playbackRateValid.Value = false; + Logger.Log("System audio playback is not working as expected. Some online functionality will not work.\n\nPlease check your audio drivers.", level: LogLevel.Important); + } + } + else + { + Logger.Log($"Playback discrepancy detected ({playbackDiscrepancyCount} of allowed {allowed_playback_discrepancies}): {elapsedGameplayClockTime:N1} vs {elapsedValidationTime:N1}"); + } + + elapsedValidationTime = null; + } + } + } + + #endregion + private bool speedAdjustmentsApplied; - private void addSourceClockAdjustments() + private void addAdjustmentsToTrack() { if (speedAdjustmentsApplied) return; @@ -201,20 +207,18 @@ namespace osu.Game.Screens.Play musicController.ResetTrackAdjustments(); track.BindAdjustments(AdjustmentsFromMods); - track.AddAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust); - track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + track.AddAdjustment(AdjustableProperty.Frequency, UserPlaybackRate); speedAdjustmentsApplied = true; } - private void removeSourceClockAdjustments() + private void removeAdjustmentsFromTrack() { if (!speedAdjustmentsApplied) return; track.UnbindAdjustments(AdjustmentsFromMods); - track.RemoveAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust); - track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + track.RemoveAdjustment(AdjustableProperty.Frequency, UserPlaybackRate); speedAdjustmentsApplied = false; } @@ -222,7 +226,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/OffsetCorrectionClock.cs b/osu.Game/Screens/Play/OffsetCorrectionClock.cs index 207980f45c..e83ed7e464 100644 --- a/osu.Game/Screens/Play/OffsetCorrectionClock.cs +++ b/osu.Game/Screens/Play/OffsetCorrectionClock.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. -using osu.Framework.Bindables; using osu.Framework.Timing; namespace osu.Game.Screens.Play { public class OffsetCorrectionClock : FramedOffsetClock { - private readonly BindableDouble pauseRateAdjust; - private double offset; public new double Offset @@ -28,10 +25,9 @@ namespace osu.Game.Screens.Play public double RateAdjustedOffset => base.Offset; - public OffsetCorrectionClock(IClock source, BindableDouble pauseRateAdjust) + public OffsetCorrectionClock(IClock source) : base(source) { - this.pauseRateAdjust = pauseRateAdjust; } public override void ProcessFrame() @@ -42,12 +38,8 @@ namespace osu.Game.Screens.Play private void updateOffset() { - // changing this during the pause transform effect will cause a potentially large offset to be suddenly applied as we approach zero rate. - if (pauseRateAdjust.Value == 1) - { - // we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this. - base.Offset = Offset * Rate; - } + // we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this. + base.Offset = Offset * Rate; } } } diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 88561ada71..2aa2793fd4 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -29,7 +29,13 @@ namespace osu.Game.Screens.Play private SkinnableSound pauseLoop; - protected override Action BackAction => () => InternalButtons.First().TriggerClick(); + protected override Action BackAction => () => + { + if (Buttons.Any()) + Buttons.First().TriggerClick(); + else + OnResume?.Invoke(); + }; [BackgroundDependencyLoader] private void load(OsuColour colours) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 7d67e2657d..ad1f9ec897 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -38,6 +38,7 @@ using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; using osu.Game.Skinning; using osu.Game.Users; +using osu.Game.Utils; using osuTK.Graphics; namespace osu.Game.Screens.Play @@ -109,6 +110,9 @@ namespace osu.Game.Screens.Play [Resolved] private MusicController musicController { get; set; } + [Resolved] + private OsuGameBase game { get; set; } + public GameplayState GameplayState { get; private set; } private Ruleset ruleset; @@ -210,6 +214,12 @@ namespace osu.Game.Screens.Play if (playableBeatmap == null) return; + if (!ModUtils.CheckModsBelongToRuleset(ruleset, gameplayMods)) + { + Logger.Log($@"Gameplay was started with a mod belonging to a ruleset different than '{ruleset.Description}'.", level: LogLevel.Important); + return; + } + mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); if (game != null) @@ -227,7 +237,8 @@ namespace osu.Game.Screens.Play dependencies.CacheAs(ScoreProcessor); - HealthProcessor = ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); + HealthProcessor = gameplayMods.OfType().FirstOrDefault()?.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); + HealthProcessor ??= ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); HealthProcessor.ApplyBeatmap(playableBeatmap); dependencies.CacheAs(HealthProcessor); @@ -257,7 +268,7 @@ namespace osu.Game.Screens.Play rulesetSkinProvider.AddRange(new Drawable[] { - failAnimationLayer = new FailAnimation(DrawableRuleset) + failAnimationContainer = new FailAnimationContainer(DrawableRuleset) { OnComplete = onFailComplete, Children = new[] @@ -267,7 +278,7 @@ namespace osu.Game.Screens.Play createGameplayComponents(Beatmap.Value) } }, - FailOverlay = new FailOverlay + FailOverlay = new FailOverlay(Configuration.AllowUserInteraction) { SaveReplay = async () => await prepareAndImportScoreAsync(true).ConfigureAwait(false), OnRetry = () => Restart(), @@ -279,8 +290,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); }, }, }); @@ -298,8 +311,10 @@ namespace osu.Game.Screens.Play { 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); }, }, }); @@ -310,7 +325,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) { @@ -462,9 +477,6 @@ namespace osu.Game.Screens.Play skipOutroOverlay.Expire(); } - if (GameplayClockContainer is MasterGameplayClockContainer master) - HUDOverlay.PlayerSettingsOverlay.PlaybackSettings.UserPlaybackRate.BindTarget = master.UserPlaybackRate; - return container; } @@ -565,20 +577,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; @@ -587,8 +588,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. @@ -597,21 +598,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() @@ -660,10 +672,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; @@ -673,7 +686,7 @@ namespace osu.Game.Screens.Play RestartRequested?.Invoke(quickRestart); - PerformExit(false); + return PerformExit(false); } /// @@ -720,7 +733,7 @@ namespace osu.Game.Screens.Play } // Only show the completion screen if the player hasn't failed - if (HealthProcessor.HasFailed) + if (GameplayState.HasFailed) return; GameplayState.HasPassed = true; @@ -729,9 +742,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. @@ -754,10 +764,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) @@ -783,8 +799,6 @@ namespace osu.Game.Screens.Play // This player instance may already be in the process of exiting. return; - Debug.Assert(ScoreProcessor.Rank.Value != ScoreRank.F); - this.Push(CreateResults(prepareScoreForDisplayTask.GetResultSafely())); }, Time.Current + delay, 50); @@ -810,10 +824,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); @@ -883,9 +900,16 @@ namespace osu.Game.Screens.Play #region Fail Logic + /// + /// Invoked when gameplay has permanently failed. + /// + protected virtual void OnFail() + { + } + protected FailOverlay FailOverlay { get; private set; } - private FailAnimation failAnimationLayer; + private FailAnimationContainer failAnimationContainer; private bool onFail() { @@ -896,24 +920,44 @@ namespace osu.Game.Screens.Play if (!CheckModsAllowFailure()) return false; - Debug.Assert(!GameplayState.HasFailed); - Debug.Assert(!GameplayState.HasPassed); - Debug.Assert(!GameplayState.HasQuit); + if (Configuration.AllowFailAnimation) + { + Debug.Assert(!GameplayState.HasFailed); + Debug.Assert(!GameplayState.HasPassed); + Debug.Assert(!GameplayState.HasQuit); - GameplayState.HasFailed = true; + GameplayState.HasFailed = true; - updateGameplayState(); + updateGameplayState(); - // There is a chance that we could be in a paused state as the ruleset's internal clock (see FrameStabilityContainer) - // could process an extra frame after the GameplayClock is stopped. - // In such cases we want the fail state to precede a user triggered pause. - if (PauseOverlay.State.Value == Visibility.Visible) - PauseOverlay.Hide(); + // There is a chance that we could be in a paused state as the ruleset's internal clock (see FrameStabilityContainer) + // could process an extra frame after the GameplayClock is stopped. + // In such cases we want the fail state to precede a user triggered pause. + if (PauseOverlay.State.Value == Visibility.Visible) + PauseOverlay.Hide(); - failAnimationLayer.Start(); + failAnimationContainer.Start(); - if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) - Restart(true); + // Failures can be triggered either by a judgement, or by a mod. + // + // For the case of a judgement, due to ordering considerations, ScoreProcessor will not have received + // the final judgement which triggered the failure yet (see DrawableRuleset.NewResult handling above). + // + // A schedule here ensures that any lingering judgements from the current frame are applied before we + // finalise the score as "failed". + Schedule(() => + { + ScoreProcessor.FailScore(Score.ScoreInfo); + OnFail(); + + if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) + Restart(true); + }); + } + else + { + ScoreProcessor.FailScore(Score.ScoreInfo); + } return true; } @@ -923,11 +967,6 @@ namespace osu.Game.Screens.Play /// private void onFailComplete() { - // fail completion is a good point to mark a score as failed, - // since the last judgement that caused the fail only applies to score processor after onFail. - // todo: this should probably be handled better. - ScoreProcessor.FailScore(Score.ScoreInfo); - GameplayClockContainer.Stop(); FailOverlay.Retries = RestartCount; @@ -1037,11 +1076,11 @@ namespace osu.Game.Screens.Play b.FadeColour(Color4.White, 250); // bind component bindables. - b.IsBreakTime.BindTo(breakTracker.IsBreakTime); + ((IBindable)b.IsBreakTime).BindTo(breakTracker.IsBreakTime); b.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); - failAnimationLayer.Background = b; + failAnimationContainer.Background = b; }); HUDOverlay.IsPlaying.BindTo(localUserPlaying); @@ -1049,8 +1088,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()) @@ -1077,7 +1114,7 @@ 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); @@ -1098,16 +1135,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); } @@ -1129,7 +1167,11 @@ namespace osu.Game.Screens.Play /// The . protected virtual Score CreateScore(IBeatmap beatmap) => new Score { - ScoreInfo = new ScoreInfo { User = api.LocalUser.Value }, + ScoreInfo = new ScoreInfo + { + User = api.LocalUser.Value, + ClientVersion = game.Version, + }, }; /// @@ -1143,27 +1185,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 => @@ -1199,8 +1234,22 @@ 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; + + // May be null if the load never completed. + if (breakTracker != null) + { + b.IsBreakTime.UnbindFrom(breakTracker.IsBreakTime); + b.IsBreakTime.Value = false; + } + }); + + storyboardReplacesBackground.Value = false; + } } #endregion diff --git a/osu.Game/Screens/Play/PlayerConfiguration.cs b/osu.Game/Screens/Play/PlayerConfiguration.cs index 122e25f406..466a691118 100644 --- a/osu.Game/Screens/Play/PlayerConfiguration.cs +++ b/osu.Game/Screens/Play/PlayerConfiguration.cs @@ -15,6 +15,12 @@ namespace osu.Game.Screens.Play /// public bool ShowResults { get; set; } = true; + /// + /// Whether the fail animation / screen should be triggered on failing. + /// If false, the score will still be marked as failed but gameplay will continue. + /// + public bool AllowFailAnimation { get; set; } = true; + /// /// Whether the player should be allowed to trigger a restart. /// diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 872425e3fd..232de53ac3 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -247,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. @@ -263,10 +263,6 @@ namespace osu.Game.Screens.Play Debug.Assert(CurrentPlayer != null); - var lastScore = CurrentPlayer.Score; - - AudioSettings.ReferenceScore.Value = lastScore?.ScoreInfo; - // prepare for a retry. CurrentPlayer = null; playerConsumed = false; @@ -414,13 +410,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/AudioSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs index 010d8115fa..3c79721590 100644 --- a/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { public partial class AudioSettings : PlayerSettingsGroup { - public Bindable ReferenceScore { get; } = new Bindable(); + private Bindable referenceScore { get; } = new Bindable(); private readonly PlayerCheckbox beatmapHitsoundsToggle; @@ -26,15 +26,16 @@ namespace osu.Game.Screens.Play.PlayerSettings beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = SkinSettingsStrings.BeatmapHitsounds }, new BeatmapOffsetControl { - ReferenceScore = { BindTarget = ReferenceScore }, + ReferenceScore = { BindTarget = referenceScore }, }, }; } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, SessionStatics statics) { beatmapHitsoundsToggle.Current = config.GetBindable(OsuSetting.BeatmapHitsounds); + statics.BindWith(Static.LastLocalUserScore, referenceScore); } } } diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 840077eb7f..9039604471 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -7,8 +7,11 @@ using System.Linq; using System.Threading.Tasks; 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.Input.Bindings; +using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -16,8 +19,11 @@ using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Input.Bindings; using osu.Game.Localisation; +using osu.Game.Overlays; using osu.Game.Overlays.Settings; +using osu.Game.Overlays.Settings.Sections.Audio; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -26,7 +32,7 @@ using osuTK; namespace osu.Game.Screens.Play.PlayerSettings { - public partial class BeatmapOffsetControl : CompositeDrawable + public partial class BeatmapOffsetControl : CompositeDrawable, IKeyBindingHandler { public Bindable ReferenceScore { get; } = new Bindable(); @@ -48,6 +54,12 @@ namespace osu.Game.Screens.Play.PlayerSettings [Resolved] private OsuColour colours { get; set; } = null!; + [Resolved] + private Player? player { get; set; } + + [Resolved] + private IGameplayClock? gameplayClock { get; set; } + private double lastPlayAverage; private double lastPlayBeatmapOffset; private HitEventTimingDistributionGraph? lastPlayGraph; @@ -74,7 +86,7 @@ namespace osu.Game.Screens.Play.PlayerSettings new OffsetSliderBar { KeyboardStep = 5, - LabelText = BeatmapOffsetControlStrings.BeatmapOffset, + LabelText = BeatmapOffsetControlStrings.AudioOffsetThisBeatmap, Current = Current, }, referenceScoreContainer = new FillFlowContainer @@ -88,28 +100,6 @@ namespace osu.Game.Screens.Play.PlayerSettings }; } - public partial class OffsetSliderBar : PlayerSliderBar - { - protected override Drawable CreateControl() => new CustomSliderBar(); - - protected partial class CustomSliderBar : SliderBar - { - public override LocalisableString TooltipText => - Current.Value == 0 - ? LocalisableString.Interpolate($@"{base.TooltipText} ms") - : LocalisableString.Interpolate($@"{base.TooltipText} ms {getEarlyLateText(Current.Value)}"); - - private LocalisableString getEarlyLateText(double value) - { - Debug.Assert(value != 0); - - return value > 0 - ? BeatmapOffsetControlStrings.HitObjectsAppearEarlier - : BeatmapOffsetControlStrings.HitObjectsAppearLater; - } - } - } - protected override void LoadComplete() { base.LoadComplete(); @@ -170,11 +160,11 @@ namespace osu.Game.Screens.Play.PlayerSettings // 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; + BeatmapUserSettings userSettings = b.UserSettings; double val = Current.Value; - if (settings.Offset != val) - settings.Offset = val; + if (userSettings.Offset != val) + userSettings.Offset = val; } }); } @@ -187,6 +177,9 @@ namespace osu.Game.Screens.Play.PlayerSettings if (score.NewValue == null) return; + if (!score.NewValue.BeatmapInfo.AsNonNull().Equals(beatmap.Value.BeatmapInfo)) + return; + if (score.NewValue.Mods.Any(m => !m.UserPlayable || m is IHasNoTimedInputs)) return; @@ -222,6 +215,8 @@ namespace osu.Game.Screens.Play.PlayerSettings lastPlayAverage = average; lastPlayBeatmapOffset = Current.Value; + LinkFlowContainer globalOffsetText; + referenceScoreContainer.AddRange(new Drawable[] { lastPlayGraph = new HitEventTimingDistributionGraph(hitEvents) @@ -235,13 +230,91 @@ namespace osu.Game.Screens.Play.PlayerSettings Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay, Action = () => Current.Value = lastPlayBeatmapOffset - lastPlayAverage }, + globalOffsetText = new LinkFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + } }); + + if (settings != null) + { + globalOffsetText.AddText("You can also "); + globalOffsetText.AddLink("adjust the global offset", () => settings.ShowAtControl()); + globalOffsetText.AddText(" based off this play."); + } } + [Resolved] + private SettingsOverlay? settings { get; set; } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); beatmapOffsetSubscription?.Dispose(); } + + public bool OnPressed(KeyBindingPressEvent e) + { + // General limitations to ensure players don't do anything too weird. + // These match stable for now. + if (player is SubmittingPlayer) + { + // TODO: the blocking conditions should probably display a message. + if (player?.IsBreakTime.Value == false && gameplayClock?.CurrentTime - gameplayClock?.StartTime > 10000) + return false; + + if (gameplayClock?.IsPaused.Value == true) + return false; + } + + // To match stable, this should adjust by 5 ms, or 1 ms when holding alt. + // But that is hard to make work with global actions due to the operating mode. + // Let's use the more precise as a default for now. + const double amount = 1; + + switch (e.Action) + { + case GlobalAction.IncreaseOffset: + Current.Value += amount; + return true; + + case GlobalAction.DecreaseOffset: + Current.Value -= amount; + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + + public static LocalisableString GetOffsetExplanatoryText(double offset) + { + return offset == 0 + ? LocalisableString.Interpolate($@"{offset:0.0} ms") + : LocalisableString.Interpolate($@"{offset:0.0} ms {getEarlyLateText(offset)}"); + + LocalisableString getEarlyLateText(double value) + { + Debug.Assert(value != 0); + + return value > 0 + ? BeatmapOffsetControlStrings.HitObjectsAppearEarlier + : BeatmapOffsetControlStrings.HitObjectsAppearLater; + } + } + + private partial class OffsetSliderBar : PlayerSliderBar + { + protected override Drawable CreateControl() => new CustomSliderBar(); + + protected partial class CustomSliderBar : SliderBar + { + public override LocalisableString TooltipText => GetOffsetExplanatoryText(Current.Value); + } + } } } diff --git a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs index cf261ba49b..1387e01305 100644 --- a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/InputSettings.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; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; @@ -10,21 +11,23 @@ namespace osu.Game.Screens.Play.PlayerSettings { public partial class InputSettings : PlayerSettingsGroup { - private readonly PlayerCheckbox mouseButtonsCheckbox; - public InputSettings() : base("Input Settings") { - Children = new Drawable[] - { - mouseButtonsCheckbox = new PlayerCheckbox - { - LabelText = MouseSettingsStrings.DisableMouseButtons - } - }; } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) => mouseButtonsCheckbox.Current = config.GetBindable(OsuSetting.MouseDisableButtons); + private void load(OsuConfigManager config) + { + Children = new Drawable[] + { + new PlayerCheckbox + { + // TODO: change to touchscreen detection once https://github.com/ppy/osu/pull/25348 makes it in + LabelText = RuntimeInfo.IsDesktop ? MouseSettingsStrings.DisableClicksDuringGameplay : TouchSettingsStrings.DisableTapsDuringGameplay, + Current = config.GetBindable(RuntimeInfo.IsDesktop ? OsuSetting.MouseDisableButtons : OsuSetting.TouchDisableGameplayTaps) + } + }; + } } } diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 4753effdb0..b3d07421ed 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -1,11 +1,17 @@ // 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.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; +using osu.Game.Screens.Edit.Timing; +using osuTK; namespace osu.Game.Screens.Play.PlayerSettings { @@ -15,49 +21,167 @@ namespace osu.Game.Screens.Play.PlayerSettings public readonly Bindable UserPlaybackRate = new BindableDouble(1) { - MinValue = 0.5, + MinValue = 0.05, MaxValue = 2, - Precision = 0.1, + Precision = 0.01, }; - private readonly PlayerSliderBar rateSlider; + private PlayerSliderBar rateSlider = null!; - private readonly OsuSpriteText multiplierText; + private OsuSpriteText multiplierText = null!; + + private readonly IBindable isPaused = new BindableBool(); + + [Resolved] + private ReplayPlayer replayPlayer { get; set; } = null!; + + [Resolved] + private GameplayClockContainer gameplayClock { get; set; } = null!; + + private IconButton pausePlay = null!; public PlaybackSettings() : base("playback") + { + } + + [BackgroundDependencyLoader] + private void load() { Children = new Drawable[] { - new Container + new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = padding }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, padding), Children = new Drawable[] { - new OsuSpriteText + new FillFlowContainer { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Text = "Playback speed", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), + Children = new Drawable[] + { + new SeekButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.FastBackward, + Action = () => replayPlayer.SeekInDirection(-10), + TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(10 * ReplayPlayer.BASE_SEEK_AMOUNT / 1000), + }, + new SeekButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.Backward, + Action = () => replayPlayer.SeekInDirection(-1), + TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(ReplayPlayer.BASE_SEEK_AMOUNT / 1000), + }, + new SeekButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.StepBackward, + Action = () => replayPlayer.StepFrame(-1), + TooltipText = PlayerSettingsOverlayStrings.StepBackward, + }, + pausePlay = new IconButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.4f), + IconScale = new Vector2(1.4f), + Action = () => + { + if (gameplayClock.IsRunning) + gameplayClock.Stop(); + else + gameplayClock.Start(); + }, + }, + new SeekButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.StepForward, + Action = () => replayPlayer.StepFrame(1), + TooltipText = PlayerSettingsOverlayStrings.StepForward, + }, + new SeekButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.Forward, + Action = () => replayPlayer.SeekInDirection(1), + TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(ReplayPlayer.BASE_SEEK_AMOUNT / 1000), + }, + new SeekButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.FastForward, + Action = () => replayPlayer.SeekInDirection(10), + TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(10 * ReplayPlayer.BASE_SEEK_AMOUNT / 1000), + }, + }, }, - multiplierText = new OsuSpriteText + new Container { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Font = OsuFont.GetFont(weight: FontWeight.Bold), - } + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + rateSlider = new PlayerSliderBar + { + LabelText = "Playback speed", + Current = UserPlaybackRate, + }, + multiplierText = new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Font = OsuFont.GetFont(weight: FontWeight.Bold), + Margin = new MarginPadding { Right = 20 }, + } + }, + }, }, }, - rateSlider = new PlayerSliderBar { Current = UserPlaybackRate } }; } protected override void LoadComplete() { base.LoadComplete(); - rateSlider.Current.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true); + rateSlider.Current.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.00}x", true); + + isPaused.BindTo(gameplayClock.IsPaused); + isPaused.BindValueChanged(paused => + { + if (!paused.NewValue) + { + pausePlay.TooltipText = ToastStrings.PauseTrack; + pausePlay.Icon = FontAwesome.Regular.PauseCircle; + } + else + { + pausePlay.TooltipText = ToastStrings.PlayTrack; + pausePlay.Icon = FontAwesome.Regular.PlayCircle; + } + }, true); + } + + private partial class SeekButton : IconButton + { + public SeekButton() + { + AddInternal(new RepeatingButtonBehaviour(this)); + } } } } diff --git a/osu.Game/Screens/Play/PlayerTouchInputDetector.cs b/osu.Game/Screens/Play/PlayerTouchInputDetector.cs new file mode 100644 index 0000000000..12fb748e7d --- /dev/null +++ b/osu.Game/Screens/Play/PlayerTouchInputDetector.cs @@ -0,0 +1,64 @@ +// 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.Game.Configuration; +using osu.Game.Rulesets.Mods; +using osu.Game.Utils; + +namespace osu.Game.Screens.Play +{ + public partial class PlayerTouchInputDetector : Component + { + [Resolved] + private Player player { get; set; } = null!; + + [Resolved] + private GameplayState gameplayState { get; set; } = null!; + + private IBindable touchActive = new BindableBool(); + private IBindable isBreakTime = null!; + + [BackgroundDependencyLoader] + private void load(SessionStatics statics) + { + touchActive = statics.GetBindable(Static.TouchInputActive); + touchActive.BindValueChanged(_ => updateState()); + + isBreakTime = player.IsBreakTime.GetBoundCopy(); + isBreakTime.BindValueChanged(_ => updateState(), true); + } + + private void updateState() + { + if (!touchActive.Value) + return; + + if (gameplayState.HasPassed || gameplayState.HasFailed || gameplayState.HasQuit) + return; + + if (gameplayState.Score.ScoreInfo.Mods.OfType().Any()) + return; + + if (isBreakTime.Value) + return; + + var touchDeviceMod = gameplayState.Ruleset.GetTouchDeviceMod(); + if (touchDeviceMod == null) + return; + + var candidateMods = player.Score.ScoreInfo.Mods.Append(touchDeviceMod).ToArray(); + + if (!ModUtils.CheckCompatibleSet(candidateMods, out _)) + return; + + // `Player` (probably rightly so) assumes immutability of mods, + // so this will not be shown immediately on the mod display in the top right. + // if this is to change, the mod immutability should be revisited. + player.Score.ScoreInfo.Mods = candidateMods; + } + } +} diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index ca71a89b48..3c5b85662a 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -7,21 +7,27 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking; using osu.Game.Users; namespace osu.Game.Screens.Play { + [Cached] public partial class ReplayPlayer : Player, IKeyBindingHandler { + public const double BASE_SEEK_AMOUNT = 1000; + private readonly Func, Score> createScore; private readonly bool replayIsFailedScore; @@ -49,6 +55,24 @@ namespace osu.Game.Screens.Play this.createScore = createScore; } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + if (!LoadedBeatmapSuccessfully) + return; + + var playbackSettings = new PlaybackSettings + { + Depth = float.MaxValue, + Expanded = { BindTarget = config.GetBindable(OsuSetting.ReplayPlaybackControlsExpanded) } + }; + + if (GameplayClockContainer is MasterGameplayClockContainer master) + playbackSettings.UserPlaybackRate.BindTo(master.UserPlaybackRate); + + HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); + } + protected override void PrepareReplay() { DrawableRuleset?.SetReplayScore(Score); @@ -72,16 +96,22 @@ namespace osu.Game.Screens.Play public bool OnPressed(KeyBindingPressEvent e) { - const double keyboard_seek_amount = 5000; - switch (e.Action) { + case GlobalAction.StepReplayBackward: + StepFrame(-1); + return true; + + case GlobalAction.StepReplayForward: + StepFrame(1); + return true; + case GlobalAction.SeekReplayBackward: - keyboardSeek(-1); + SeekInDirection(-5); return true; case GlobalAction.SeekReplayForward: - keyboardSeek(1); + SeekInDirection(5); return true; case GlobalAction.TogglePauseReplay: @@ -93,13 +123,28 @@ namespace osu.Game.Screens.Play } return false; + } - void keyboardSeek(int direction) - { - double target = Math.Clamp(GameplayClockContainer.CurrentTime + direction * keyboard_seek_amount, 0, GameplayState.Beatmap.GetLastObjectTime()); + public void StepFrame(int direction) + { + GameplayClockContainer.Stop(); - Seek(target); - } + var frames = GameplayState.Score.Replay.Frames; + + if (frames.Count == 0) + return; + + GameplayClockContainer.Seek(direction < 0 + ? (frames.LastOrDefault(f => f.Time < GameplayClockContainer.CurrentTime) ?? frames.First()).Time + : (frames.FirstOrDefault(f => f.Time > GameplayClockContainer.CurrentTime) ?? frames.Last()).Time + ); + } + + public void SeekInDirection(float amount) + { + double target = Math.Clamp(GameplayClockContainer.CurrentTime + amount * BASE_SEEK_AMOUNT, 0, GameplayState.Beatmap.GetLastObjectTime()); + + Seek(target); } public void OnReleased(KeyBindingReleaseEvent e) diff --git a/osu.Game/Screens/Play/SaveFailedScoreButton.cs b/osu.Game/Screens/Play/SaveFailedScoreButton.cs index 0a2696339c..b97c140250 100644 --- a/osu.Game/Screens/Play/SaveFailedScoreButton.cs +++ b/osu.Game/Screens/Play/SaveFailedScoreButton.cs @@ -102,6 +102,9 @@ namespace osu.Game.Screens.Play public bool OnPressed(KeyBindingPressEvent e) { + if (e.Repeat) + return false; + switch (e.Action) { case GlobalAction.SaveReplay: 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/SoloSpectatorPlayer.cs b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs index c9d1f4acaa..8d25a0148d 100644 --- a/osu.Game/Screens/Play/SoloSpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs @@ -17,8 +17,8 @@ namespace osu.Game.Screens.Play protected override UserActivity InitialActivity => new UserActivity.SpectatingUser(Score.ScoreInfo); - public SoloSpectatorPlayer(Score score, PlayerConfiguration configuration = null) - : base(score, configuration) + public SoloSpectatorPlayer(Score score) + : base(score, new PlayerConfiguration { AllowUserInteraction = false }) { this.score = score; } diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectatorScreen.cs similarity index 83% rename from osu.Game/Screens/Play/SoloSpectator.cs rename to osu.Game/Screens/Play/SoloSpectatorScreen.cs index a5c84e97ab..2db751402c 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectatorScreen.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.Diagnostics; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -33,41 +30,40 @@ using osuTK; namespace osu.Game.Screens.Play { [Cached(typeof(IPreviewTrackOwner))] - public partial class SoloSpectator : SpectatorScreen, IPreviewTrackOwner + public partial class SoloSpectatorScreen : SpectatorScreen, IPreviewTrackOwner { - [NotNull] - private readonly APIUser targetUser; + [Resolved] + private IAPIProvider api { get; set; } = null!; [Resolved] - private IAPIProvider api { get; set; } + private PreviewTrackManager previewTrackManager { get; set; } = null!; [Resolved] - private PreviewTrackManager previewTrackManager { get; set; } + private BeatmapManager beatmaps { get; set; } = null!; [Resolved] - private BeatmapManager beatmaps { get; set; } - - [Resolved] - private BeatmapModelDownloader beatmapDownloader { get; set; } + private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - private Container beatmapPanelContainer; - private RoundedButton watchButton; - private SettingsCheckbox automaticDownload; + private Container beatmapPanelContainer = null!; + private RoundedButton watchButton = null!; + private SettingsCheckbox automaticDownload = null!; + + private readonly APIUser targetUser; /// /// The player's immediate online gameplay state. /// This doesn't always reflect the gameplay state being watched. /// - private SpectatorGameplayState immediateSpectatorGameplayState; + private SpectatorGameplayState? immediateSpectatorGameplayState; - private GetBeatmapSetRequest onlineBeatmapRequest; + private GetBeatmapSetRequest? onlineBeatmapRequest; - private APIBeatmapSet beatmapSet; + private APIBeatmapSet? beatmapSet; - public SoloSpectator([NotNull] APIUser targetUser) + public SoloSpectatorScreen(APIUser targetUser) : base(targetUser.Id) { this.targetUser = targetUser; @@ -143,7 +139,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, }, @@ -168,28 +164,47 @@ namespace osu.Game.Screens.Play automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload()); } - protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState) + protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState) => Schedule(() => { clearDisplay(); showBeatmapPanel(spectatorState); - } + }); - protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) + protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) => Schedule(() => { immediateSpectatorGameplayState = spectatorGameplayState; watchButton.Enabled.Value = true; scheduleStart(spectatorGameplayState); + }); + + protected override void FailGameplay(int userId) + { + if (this.GetChildScreen() is SpectatorPlayerLoader loader) + { + if (loader.GetChildScreen() is SpectatorPlayer player) + { + player.AllowFail(); + resetStartState(); + } + else + QuitGameplay(userId); + } } protected override void QuitGameplay(int userId) + { + // Importantly, don't schedule this call, as a child screen may be present (and will cause the schedule to not be run as expected). + this.MakeCurrent(); + resetStartState(); + } + + private void resetStartState() => Schedule(() => { scheduledStart?.Cancel(); immediateSpectatorGameplayState = null; - watchButton.Enabled.Value = false; - clearDisplay(); - } + }); private void clearDisplay() { @@ -199,10 +214,12 @@ namespace osu.Game.Screens.Play previewTrackManager.StopAnyPlaying(this); } - private ScheduledDelegate scheduledStart; + private ScheduledDelegate? scheduledStart; - private void scheduleStart(SpectatorGameplayState spectatorGameplayState) + private void scheduleStart(SpectatorGameplayState? spectatorGameplayState) { + Debug.Assert(spectatorGameplayState != null); + // This function may be called multiple times in quick succession once the screen becomes current again. scheduledStart?.Cancel(); scheduledStart = Schedule(() => diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index 30a5ac3741..2faead0ee1 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -25,7 +25,17 @@ namespace osu.Game.Screens.Play private readonly Score score; - protected override bool CheckModsAllowFailure() => false; // todo: better support starting mid-way through beatmap + public override bool AllowBackButton => true; + + protected override bool CheckModsAllowFailure() + { + if (!allowFail) + return false; + + return base.CheckModsAllowFailure(); + } + + private bool allowFail; protected SpectatorPlayer(Score score, PlayerConfiguration configuration = null) : base(configuration) @@ -60,6 +70,12 @@ namespace osu.Game.Screens.Play }, true); } + /// + /// Should be called when it is apparent that the player being spectated has failed. + /// This will subsequently stop blocking the fail screen from displaying (usually done out of safety). + /// + public void AllowFail() => allowFail = true; + protected override void StartGameplay() { base.StartGameplay(); 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..171ceea84f 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -8,9 +8,11 @@ using System.Linq; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; @@ -37,6 +39,10 @@ namespace osu.Game.Screens.Play [Resolved] private SpectatorClient spectatorClient { get; set; } + [Resolved] + private SessionStatics statics { get; set; } + + private readonly object scoreSubmissionLock = new object(); private TaskCompletionSource scoreSubmissionSource; protected SubmittingPlayer(PlayerConfiguration configuration = null) @@ -44,6 +50,32 @@ namespace osu.Game.Screens.Play { } + [BackgroundDependencyLoader] + private void load() + { + if (DrawableRuleset == null) + { + // base load must have failed (e.g. due to an unknown mod); bail. + return; + } + + AddInternal(new PlayerTouchInputDetector()); + + // We probably want to move this display to something more global. + // Probably using the OSD somehow. + AddInternal(new GameplayOffsetControl + { + Margin = new MarginPadding(20), + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }); + } + + protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart) + { + ShouldValidatePlaybackRate = true, + }; + protected override void LoadAsyncComplete() { base.LoadAsyncComplete(); @@ -153,10 +185,23 @@ namespace osu.Game.Screens.Play spectatorClient.BeginPlaying(token, GameplayState, Score); } + protected override void OnFail() + { + base.OnFail(); + + submitFromFailOrQuit(); + } + public override bool OnExiting(ScreenExitEvent e) { bool exiting = base.OnExiting(e); + submitFromFailOrQuit(); + statics.SetValue(Static.LastLocalUserScore, Score?.ScoreInfo.DeepClone()); + return exiting; + } + private void submitFromFailOrQuit() + { if (LoadedBeatmapSuccessfully) { Task.Run(async () => @@ -165,8 +210,6 @@ namespace osu.Game.Screens.Play spectatorClient.EndPlaying(GameplayState); }).FireAndForget(); } - - return exiting; } /// @@ -186,18 +229,34 @@ namespace osu.Game.Screens.Play private Task submitScore(Score score) { + var masterClock = GameplayClockContainer as MasterGameplayClockContainer; + + if (masterClock?.PlaybackRateValid.Value != true) + { + Logger.Log("Score submission cancelled due to audio playback rate discrepancy."); + return Task.CompletedTask; + } + // 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; + lock (scoreSubmissionLock) + { + if (scoreSubmissionSource != null) + return scoreSubmissionSource.Task; + + scoreSubmissionSource = new TaskCompletionSource(); + } // if the user never hit anything, this score should not be counted in any way. if (!score.ScoreInfo.Statistics.Any(s => s.Key.IsHit() && s.Value > 0)) return Task.CompletedTask; - scoreSubmissionSource = new TaskCompletionSource(); + Logger.Log($"Beginning score submission (token:{token.Value})..."); var request = CreateSubmissionRequest(score, token.Value); request.Success += s => @@ -206,11 +265,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/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 2ec4270c3c..e7e54d0fae 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Ranking.Expanded.Accuracy { @@ -29,13 +30,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy /// public partial class AccuracyCircle : CompositeDrawable { - private static readonly double accuracy_x = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.X); - private static readonly double accuracy_s = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.S); - private static readonly double accuracy_a = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.A); - private static readonly double accuracy_b = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.B); - private static readonly double accuracy_c = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.C); - private static readonly double accuracy_d = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.D); - /// /// Duration for the transforms causing this component to appear. /// @@ -110,12 +104,32 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy private double lastTickPlaybackTime; private bool isTicking; + private readonly double accuracyX; + private readonly double accuracyS; + private readonly double accuracyA; + private readonly double accuracyB; + private readonly double accuracyC; + private readonly double accuracyD; private readonly bool withFlair; + private readonly bool isFailedSDueToMisses; + private RankText failedSRankText; + public AccuracyCircle(ScoreInfo score, bool withFlair = false) { this.score = score; this.withFlair = withFlair; + + ScoreProcessor scoreProcessor = score.Ruleset.CreateInstance().CreateScoreProcessor(); + accuracyX = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.X); + accuracyS = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.S); + + accuracyA = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.A); + accuracyB = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.B); + accuracyC = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.C); + accuracyD = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.D); + + isFailedSDueToMisses = score.Accuracy >= accuracyS && score.Rank == ScoreRank.A; } [BackgroundDependencyLoader] @@ -158,49 +172,49 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy RelativeSizeAxes = Axes.Both, Colour = OsuColour.ForRank(ScoreRank.X), InnerRadius = RANK_CIRCLE_RADIUS, - Current = { Value = accuracy_x } + Current = { Value = accuracyX } }, new CircularProgress { RelativeSizeAxes = Axes.Both, Colour = OsuColour.ForRank(ScoreRank.S), InnerRadius = RANK_CIRCLE_RADIUS, - Current = { Value = accuracy_x - virtual_ss_percentage } + Current = { Value = accuracyX - virtual_ss_percentage } }, new CircularProgress { RelativeSizeAxes = Axes.Both, Colour = OsuColour.ForRank(ScoreRank.A), InnerRadius = RANK_CIRCLE_RADIUS, - Current = { Value = accuracy_s } + Current = { Value = accuracyS } }, new CircularProgress { RelativeSizeAxes = Axes.Both, Colour = OsuColour.ForRank(ScoreRank.B), InnerRadius = RANK_CIRCLE_RADIUS, - Current = { Value = accuracy_a } + Current = { Value = accuracyA } }, new CircularProgress { RelativeSizeAxes = Axes.Both, Colour = OsuColour.ForRank(ScoreRank.C), InnerRadius = RANK_CIRCLE_RADIUS, - Current = { Value = accuracy_b } + Current = { Value = accuracyB } }, new CircularProgress { RelativeSizeAxes = Axes.Both, Colour = OsuColour.ForRank(ScoreRank.D), InnerRadius = RANK_CIRCLE_RADIUS, - Current = { Value = accuracy_c } + Current = { Value = accuracyC } }, - new RankNotch((float)accuracy_x), - new RankNotch((float)(accuracy_x - virtual_ss_percentage)), - new RankNotch((float)accuracy_s), - new RankNotch((float)accuracy_a), - new RankNotch((float)accuracy_b), - new RankNotch((float)accuracy_c), + new RankNotch((float)accuracyX), + new RankNotch((float)(accuracyX - virtual_ss_percentage)), + new RankNotch((float)accuracyS), + new RankNotch((float)accuracyA), + new RankNotch((float)accuracyB), + new RankNotch((float)accuracyC), new BufferedContainer { Name = "Graded circle mask", @@ -228,13 +242,13 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy Padding = new MarginPadding { Vertical = -15, Horizontal = -20 }, Children = new[] { + new RankBadge(accuracyD, Interpolation.Lerp(accuracyD, accuracyC, 0.5), getRank(ScoreRank.D)), + new RankBadge(accuracyC, Interpolation.Lerp(accuracyC, accuracyB, 0.5), getRank(ScoreRank.C)), + new RankBadge(accuracyB, Interpolation.Lerp(accuracyB, accuracyA, 0.5), getRank(ScoreRank.B)), // The S and A badges are moved down slightly to prevent collision with the SS badge. - new RankBadge(accuracy_x, accuracy_x, getRank(ScoreRank.X)), - new RankBadge(accuracy_s, Interpolation.Lerp(accuracy_s, (accuracy_x - virtual_ss_percentage), 0.25), getRank(ScoreRank.S)), - new RankBadge(accuracy_a, Interpolation.Lerp(accuracy_a, accuracy_s, 0.25), getRank(ScoreRank.A)), - new RankBadge(accuracy_b, Interpolation.Lerp(accuracy_b, accuracy_a, 0.5), getRank(ScoreRank.B)), - new RankBadge(accuracy_c, Interpolation.Lerp(accuracy_c, accuracy_b, 0.5), getRank(ScoreRank.C)), - new RankBadge(accuracy_d, Interpolation.Lerp(accuracy_d, accuracy_c, 0.5), getRank(ScoreRank.D)), + new RankBadge(accuracyA, Interpolation.Lerp(accuracyA, accuracyS, 0.25), getRank(ScoreRank.A)), + new RankBadge(accuracyS, Interpolation.Lerp(accuracyS, (accuracyX - virtual_ss_percentage), 0.25), getRank(ScoreRank.S)), + new RankBadge(accuracyX, accuracyX, getRank(ScoreRank.X)), } }, rankText = new RankText(score.Rank) @@ -242,6 +256,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy if (withFlair) { + if (isFailedSDueToMisses) + AddInternal(failedSRankText = new RankText(ScoreRank.S)); + AddRangeInternal(new Drawable[] { rankImpactSound = new PoolableSkinnableSample(new SampleInfo(impactSampleName)), @@ -280,10 +297,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy double targetAccuracy = score.Accuracy; double[] notchPercentages = { - accuracy_s, - accuracy_a, - accuracy_b, - accuracy_c, + accuracyS, + accuracyA, + accuracyB, + accuracyC, }; // Ensure the gauge overshoots or undershoots a bit so it doesn't land in the gaps of the inner graded circle (caused by `RankNotch`es), @@ -302,7 +319,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy if (score.Rank == ScoreRank.X || score.Rank == ScoreRank.XH) targetAccuracy = 1; else - targetAccuracy = Math.Min(accuracy_x - virtual_ss_percentage - NOTCH_WIDTH_PERCENTAGE / 2, targetAccuracy); + targetAccuracy = Math.Min(accuracyX - virtual_ss_percentage - NOTCH_WIDTH_PERCENTAGE / 2, targetAccuracy); // The accuracy circle gauge visually fills up a bit too much. // This wouldn't normally matter but we want it to align properly with the inner graded circle in the above cases. @@ -339,7 +356,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy if (badge.Accuracy > score.Accuracy) continue; - using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, Math.Min(accuracy_x - virtual_ss_percentage, badge.Accuracy) / targetAccuracy) * ACCURACY_TRANSFORM_DURATION)) + using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, Math.Min(accuracyX - virtual_ss_percentage, badge.Accuracy) / targetAccuracy) * ACCURACY_TRANSFORM_DURATION)) { badge.Appear(); @@ -380,6 +397,31 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy }); } } + + if (isFailedSDueToMisses) + { + const double adjust_duration = 200; + + using (BeginDelayedSequence(TEXT_APPEAR_DELAY - adjust_duration)) + { + failedSRankText.FadeIn(adjust_duration); + + using (BeginDelayedSequence(adjust_duration)) + { + failedSRankText + .FadeColour(Color4.Red, 800, Easing.Out) + .RotateTo(10, 1000, Easing.Out) + .MoveToY(100, 1000, Easing.In) + .FadeOut(800, Easing.Out); + + accuracyCircle + .FillTo(accuracyS - NOTCH_WIDTH_PERCENTAGE / 2 - visual_alignment_offset, 70, Easing.OutQuint); + + badges.Single(b => b.Rank == ScoreRank.S) + .FadeOut(70, Easing.OutQuint); + } + } + } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs index 7af327828e..8aea6045eb 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy /// private readonly double displayPosition; - private readonly ScoreRank rank; + public readonly ScoreRank Rank; private Drawable rankContainer; private Drawable overlay; @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy { Accuracy = accuracy; displayPosition = position; - this.rank = rank; + Rank = rank; RelativeSizeAxes = Axes.Both; Alpha = 0; @@ -62,7 +62,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy Size = new Vector2(28, 14), Children = new[] { - new DrawableRank(rank), + new DrawableRank(Rank), overlay = new CircularContainer { RelativeSizeAxes = Axes.Both, @@ -71,7 +71,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, - Colour = OsuColour.ForRank(rank).Opacity(0.2f), + Colour = OsuColour.ForRank(Rank).Opacity(0.2f), Radius = 10, }, Child = new Box diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs index 4b8c057235..f1f2c47e20 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs @@ -44,9 +44,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private partial class Counter : RollingCounter { - protected override double RollingDuration => AccuracyCircle.ACCURACY_TRANSFORM_DURATION; - - protected override Easing RollingEasing => AccuracyCircle.ACCURACY_TRANSFORM_EASING; + // FormatAccuracy doesn't round, which means if we use the OutPow10 easing the number will stick 0.01% short for some time. + // To avoid that let's use a shorter easing which looks roughly the same. + protected override double RollingDuration => AccuracyCircle.ACCURACY_TRANSFORM_DURATION / 2; + protected override Easing RollingEasing => Easing.OutQuad; protected override LocalisableString FormatCount(double count) => count.FormatAccuracy(); diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index b6166e97f6..df5f9c7a8a 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -37,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; diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 43ddcffb62..697d62ad6e 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -21,7 +21,9 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; +using osu.Game.Localisation; using osu.Game.Online.API; +using osu.Game.Online.Placeholders; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking.Statistics; @@ -55,7 +57,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; @@ -98,7 +101,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; @@ -107,7 +110,7 @@ namespace osu.Game.Screens.Ranking { RelativeSizeAxes = Axes.Both, SelectedScore = { BindTarget = SelectedScore }, - PostExpandAction = () => statisticsPanel.ToggleVisibility() + PostExpandAction = () => StatisticsPanel.ToggleVisibility() }, detachedPanelContainer = new Container { @@ -155,7 +158,7 @@ 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); } @@ -194,7 +197,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() @@ -234,7 +237,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(); @@ -244,6 +247,12 @@ namespace osu.Game.Screens.Ranking addScore(s); lastFetchCompleted = true; + + if (ScorePanelList.IsEmpty) + { + // This can happen if for example a beatmap that is part of a playlist hasn't been played yet. + VerticalScrollContent.Add(new MessagePlaceholder(LeaderboardStrings.NoRecordsYet)); + } }); public override void OnEntering(ScreenTransitionEvent e) @@ -272,9 +281,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; } @@ -328,7 +337,7 @@ 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, 250, Easing.OutElasticQuarter); @@ -353,7 +362,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/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 1d332d6b27..1f7ba3692a 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -82,6 +83,7 @@ namespace osu.Game.Screens.Ranking private static readonly Color4 contracted_top_layer_colour = Color4Extensions.FromHex("#353535"); private static readonly Color4 contracted_middle_layer_colour = Color4Extensions.FromHex("#353535"); + [CanBeNull] public event Action StateChanged; /// diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index b75f3d86ff..95c90e35a0 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -49,6 +49,8 @@ namespace osu.Game.Screens.Ranking public bool AllPanelsVisible => flow.All(p => p.IsPresent); + public bool IsEmpty => flow.Count == 0; + /// /// The current scroll position. /// diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index f187b8a302..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; @@ -67,7 +68,7 @@ namespace osu.Game.Screens.Ranking 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/SoloStatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/SoloStatisticsPanel.cs index 73b9897096..762be61853 100644 --- a/osu.Game/Screens/Ranking/Statistics/SoloStatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/SoloStatisticsPanel.cs @@ -37,7 +37,6 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.X, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Width = 0.5f, StatisticsUpdate = { BindTarget = StatisticsUpdate } })).ToArray(); } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 9af9a0ce72..70ecde3858 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.Select /// /// The total count of non-filtered beatmaps displayed. /// - public int CountDisplayed => beatmapSets.Where(s => !s.Filtered.Value).Sum(s => s.Beatmaps.Count(b => !b.Filtered.Value)); + public int CountDisplayed => beatmapSets.Where(s => !s.Filtered.Value).Sum(s => s.TotalItemsNotFiltered); /// /// The currently selected beatmap set. @@ -78,6 +78,8 @@ namespace osu.Game.Screens.Select private CarouselBeatmapSet? selectedBeatmapSet; + private List originalBeatmapSetsDetached = new List(); + /// /// Raised when the is changed. /// @@ -94,18 +96,19 @@ namespace osu.Game.Screens.Select /// /// Extend the range to retain already loaded pooled drawables. /// - private const float distance_offscreen_before_unload = 1024; + private const float distance_offscreen_before_unload = 2048; /// /// Extend the range to update positions / retrieve pooled drawables outside of visible range. /// - private const float distance_offscreen_to_preload = 512; // todo: adjust this appropriately once we can make set panel contents load while off-screen. + private const float distance_offscreen_to_preload = 768; /// /// Whether carousel items have completed asynchronously loaded. /// public bool BeatmapSetsLoaded { get; private set; } + [Cached] protected readonly CarouselScrollContainer Scroll; private readonly NoResultsPlaceholder noResultsPlaceholder; @@ -127,15 +130,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(); @@ -143,7 +169,19 @@ namespace osu.Game.Screens.Select applyActiveCriteria(false); if (loadedTestBeatmaps) - signalBeatmapsLoaded(); + { + invalidateAfterChange(); + BeatmapSetsLoaded = true; + } + + // 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(); @@ -232,8 +270,30 @@ namespace osu.Game.Screens.Select if (changes == null) return; - foreach (int i in changes.InsertedIndices) - removeBeatmapSet(sender[i].ID); + var removeableSets = changes.InsertedIndices.Select(i => sender[i].ID).ToHashSet(); + + // This schedule is required to retain selection of beatmaps over an ImportAsUpdate operation. + // This is covered by TestPlaySongSelect.TestSelectionRetainedOnBeatmapUpdate. + // + // In short, we have specialised logic in `beatmapSetsChanged` (directly below) to infer that an + // update operation has occurred. For this to work, we need to confirm the `DeletePending` flag + // of the current selection. + // + // If we don't schedule the following code, it is possible for the `deleteBeatmapSetsChanged` handler + // to be invoked before the `beatmapSetsChanged` handler (realm call order seems non-deterministic) + // which will lead to the currently selected beatmap changing via `CarouselGroupEagerSelect`. + // + // We need a better path forward here. A few ideas: + // - Avoid the necessity of having realm subscriptions on deleted/hidden items, maybe by storing all guids in realm + // to a local list so we can better look them up on receiving `DeletedIndices`. + // - Add a new property on `BeatmapSetInfo` to link to the pre-update set, and use that to handle the update case. + Schedule(() => + { + foreach (var set in removeableSets) + removeBeatmapSet(set); + + invalidateAfterChange(); + }); } private void beatmapSetsChanged(IRealmCollection sender, ChangeSet? changes) @@ -242,6 +302,9 @@ namespace osu.Game.Screens.Select if (loadedTestBeatmaps) return; + var setsRequiringUpdate = new HashSet(); + var setsRequiringRemoval = new HashSet(); + if (changes == null) { // During initial population, we must manually account for the fact that our original query was done on an async thread. @@ -255,64 +318,80 @@ namespace osu.Game.Screens.Select foreach (var id in realmSets) { if (!root.BeatmapSetsByID.ContainsKey(id)) - UpdateBeatmapSet(realm.Realm.Find(id)!.Detach()); + setsRequiringUpdate.Add(realm.Realm.Find(id)!.Detach()); } foreach (var id in root.BeatmapSetsByID.Keys) { if (!realmSets.Contains(id)) - removeBeatmapSet(id); + setsRequiringRemoval.Add(id); } + } + else + { + foreach (int i in changes.NewModifiedIndices) + setsRequiringUpdate.Add(sender[i].Detach()); - signalBeatmapsLoaded(); - return; + foreach (int i in changes.InsertedIndices) + setsRequiringUpdate.Add(sender[i].Detach()); } - foreach (int i in changes.NewModifiedIndices) - UpdateBeatmapSet(sender[i].Detach()); - - foreach (int i in changes.InsertedIndices) - UpdateBeatmapSet(sender[i].Detach()); - - if (changes.DeletedIndices.Length > 0 && SelectedBeatmapInfo != null) + // All local operations must be scheduled. + // + // If we don't schedule, beatmaps getting changed while song select is suspended (ie. last played being updated) + // will cause unexpected sounds and operations to occur in the background. + Schedule(() => { - // If SelectedBeatmapInfo is non-null, the set should also be non-null. - Debug.Assert(SelectedBeatmapSet != null); - - // To handle the beatmap update flow, attempt to track selection changes across delete-insert transactions. - // When an update occurs, the previous beatmap set is either soft or hard deleted. - // Check if the current selection was potentially deleted by re-querying its validity. - bool selectedSetMarkedDeleted = realm.Run(r => r.Find(SelectedBeatmapSet.ID))?.DeletePending != false; - - int[] modifiedAndInserted = changes.NewModifiedIndices.Concat(changes.InsertedIndices).ToArray(); - - if (selectedSetMarkedDeleted && modifiedAndInserted.Any()) + try { - // If it is no longer valid, make the bold assumption that an updated version will be available in the modified/inserted indices. - // This relies on the full update operation being in a single transaction, so please don't change that. - foreach (int i in modifiedAndInserted) + foreach (var set in setsRequiringRemoval) + removeBeatmapSet(set); + + foreach (var set in setsRequiringUpdate) + updateBeatmapSet(set); + + if (changes?.DeletedIndices.Length > 0 && SelectedBeatmapInfo != null) { - var beatmapSetInfo = sender[i]; + // If SelectedBeatmapInfo is non-null, the set should also be non-null. + Debug.Assert(SelectedBeatmapSet != null); - foreach (var beatmapInfo in beatmapSetInfo.Beatmaps) + // To handle the beatmap update flow, attempt to track selection changes across delete-insert transactions. + // When an update occurs, the previous beatmap set is either soft or hard deleted. + // Check if the current selection was potentially deleted by re-querying its validity. + bool selectedSetMarkedDeleted = realm.Run(r => r.Find(SelectedBeatmapSet.ID)?.DeletePending != false); + + if (selectedSetMarkedDeleted && setsRequiringUpdate.Any()) { - if (!((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata)) - continue; - - // Best effort matching. We can't use ID because in the update flow a new version will get its own GUID. - if (beatmapInfo.DifficultyName == SelectedBeatmapInfo.DifficultyName) + // If it is no longer valid, make the bold assumption that an updated version will be available in the modified/inserted indices. + // This relies on the full update operation being in a single transaction, so please don't change that. + foreach (var set in setsRequiringUpdate) { - SelectBeatmap(beatmapInfo); - return; + foreach (var beatmapInfo in set.Beatmaps) + { + if (!((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata)) + continue; + + // Best effort matching. We can't use ID because in the update flow a new version will get its own GUID. + if (beatmapInfo.DifficultyName == SelectedBeatmapInfo.DifficultyName) + { + SelectBeatmap(beatmapInfo); + return; + } + } } + + // If a direct selection couldn't be made, it's feasible that the difficulty name (or beatmap metadata) changed. + // Let's attempt to follow set-level selection anyway. + SelectBeatmap(setsRequiringUpdate.First().Beatmaps.First()); } } - - // If a direct selection couldn't be made, it's feasible that the difficulty name (or beatmap metadata) changed. - // Let's attempt to follow set-level selection anyway. - SelectBeatmap(sender[modifiedAndInserted.First()].Beatmaps.First()); } - } + finally + { + BeatmapSetsLoaded = true; + invalidateAfterChange(); + } + }); } private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) @@ -321,6 +400,8 @@ namespace osu.Game.Screens.Select if (changes == null) return; + bool changed = false; + foreach (int i in changes.InsertedIndices) { var beatmapInfo = sender[i]; @@ -330,71 +411,90 @@ 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()); + updateBeatmapSet(beatmapSet.Detach()); + changed = true; } } + + if (changed) + invalidateAfterChange(); } private IQueryable getBeatmapSets(Realm realm) => realm.All().Where(s => !s.DeletePending && !s.Protected); - public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => - removeBeatmapSet(beatmapSet.ID); - - private void removeBeatmapSet(Guid beatmapSetID) => Schedule(() => + public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => { - if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSet)) + removeBeatmapSet(beatmapSet.ID); + invalidateAfterChange(); + }); + + private void removeBeatmapSet(Guid beatmapSetID) + { + if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSets)) return; - foreach (var beatmap in existingSet.Beatmaps) - randomSelectedBeatmaps.Remove(beatmap); + originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSetID); - previouslyVisitedRandomSets.Remove(existingSet); + foreach (var set in existingSets) + { + foreach (var beatmap in set.Beatmaps) + randomSelectedBeatmaps.Remove(beatmap); + previouslyVisitedRandomSets.Remove(set); - root.RemoveItem(existingSet); - itemsCache.Invalidate(); - - if (!Scroll.UserScrolling) - ScrollToSelected(true); - - BeatmapSetsChanged?.Invoke(); - }); + root.RemoveItem(set); + } + } public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => { - Guid? previouslySelectedID = null; + updateBeatmapSet(beatmapSet); + invalidateAfterChange(); + }); - // 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; + private void updateBeatmapSet(BeatmapSetInfo beatmapSet) + { + originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSet.ID); + originalBeatmapSetsDetached.Add(beatmapSet.Detach()); - var newSet = createCarouselSet(beatmapSet); - var removedSet = root.RemoveChild(beatmapSet.ID); + var newSets = new List(); - // 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) + if (beatmapsSplitOut) { - root.AddItem(newSet); + foreach (var beatmap in beatmapSet.Beatmaps) + { + var newSet = createCarouselSet(new BeatmapSetInfo(new[] { beatmap }) + { + ID = beatmapSet.ID, + OnlineID = beatmapSet.OnlineID, + Status = beatmapSet.Status, + }); - // check if we can/need to maintain our current selection. - if (previouslySelectedID != null) - select((CarouselItem?)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet); + if (newSet != null) + newSets.Add(newSet); + } + } + else + { + var newSet = createCarouselSet(beatmapSet); + + if (newSet != null) + newSets.Add(newSet); } - itemsCache.Invalidate(); + var removedSets = root.ReplaceItem(beatmapSet, newSets); - if (!Scroll.UserScrolling) - ScrollToSelected(true); - - BeatmapSetsChanged?.Invoke(); - }); + // If we don't remove these 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. + foreach (var removedSet in removedSets) + { + var removedDrawable = Scroll.FirstOrDefault(c => c.Item == removedSet); + if (removedDrawable != null) + expirePanelImmediately(removedDrawable); + } + } /// /// Selects a given beatmap on the carousel. @@ -544,7 +644,7 @@ namespace osu.Game.Screens.Select while (randomSelectedBeatmaps.Any()) { var beatmap = randomSelectedBeatmaps[^1]; - randomSelectedBeatmaps.Remove(beatmap); + randomSelectedBeatmaps.RemoveAt(randomSelectedBeatmaps.Count - 1); if (!beatmap.Filtered.Value && beatmap.BeatmapInfo.BeatmapSet?.DeletePending != true) { @@ -632,6 +732,8 @@ namespace osu.Game.Screens.Select applyActiveCriteria(debounce); } + private bool beatmapsSplitOut; + private void applyActiveCriteria(bool debounce, bool alwaysResetScrollPosition = true) { PendingFilter?.Cancel(); @@ -652,6 +754,13 @@ namespace osu.Game.Screens.Select { PendingFilter = null; + if (activeCriteria.SplitOutDifficulties != beatmapsSplitOut) + { + beatmapsSplitOut = activeCriteria.SplitOutDifficulties; + loadBeatmapSets(originalBeatmapSetsDetached); + return; + } + root.Filter(activeCriteria); itemsCache.Invalidate(); @@ -662,15 +771,14 @@ namespace osu.Game.Screens.Select } } - private void signalBeatmapsLoaded() + private void invalidateAfterChange() { - if (!BeatmapSetsLoaded) - { - BeatmapSetsChanged?.Invoke(); - BeatmapSetsLoaded = true; - } - itemsCache.Invalidate(); + + if (!Scroll.UserScrolling) + ScrollToSelected(true); + + BeatmapSetsChanged?.Invoke(); } private float? scrollTarget; @@ -760,7 +868,7 @@ namespace osu.Game.Screens.Select { var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first + 1); - foreach (var panel in Scroll.Children) + foreach (var panel in Scroll) { Debug.Assert(panel.Item != null); @@ -791,7 +899,7 @@ namespace osu.Game.Screens.Select // Update externally controlled state of currently visible items (e.g. x-offset and opacity). // This is a per-frame update on all drawable panels. - foreach (DrawableCarouselItem item in Scroll.Children) + foreach (DrawableCarouselItem item in Scroll) { updateItem(item); @@ -986,7 +1094,7 @@ namespace osu.Game.Screens.Select // to enter clamp-special-case mode where it animates completely differently to normal. float scrollChange = scrollTarget.Value - Scroll.Current; Scroll.ScrollTo(scrollTarget.Value, false); - foreach (var i in Scroll.Children) + foreach (var i in Scroll) i.Y += scrollChange; break; } @@ -1055,7 +1163,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) { @@ -1069,20 +1177,62 @@ 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) + /// + /// A special method to handle replace operations (general for updating a beatmap). + /// Avoids event-driven selection flip-flopping during the remove/add process. + /// + /// The beatmap set to be replaced. + /// All new items to replace the removed beatmap set. + /// All removed items, for any further processing. + public IEnumerable ReplaceItem(BeatmapSetInfo oldItem, List newItems) { - if (BeatmapSetsByID.TryGetValue(beatmapSetID, out var carouselBeatmapSet)) + var previousSelection = (LastSelected as CarouselBeatmapSet)?.Beatmaps + .FirstOrDefault(s => s.State.Value == CarouselItemState.Selected) + ?.BeatmapInfo; + + bool wasSelected = previousSelection?.BeatmapSet?.ID == oldItem.ID; + + // Without doing this, the removal of the old beatmap will cause carousel's eager selection + // logic to invoke, causing one unnecessary selection. + DisableSelection = true; + var removedSets = RemoveItemsByID(oldItem.ID); + DisableSelection = false; + + foreach (var set in newItems) + AddItem(set); + + // Check if we can/need to maintain our current selection. + if (wasSelected) { - RemoveItem(carouselBeatmapSet); - return carouselBeatmapSet; + CarouselBeatmap? matchingBeatmap = newItems.SelectMany(s => s.Beatmaps) + .FirstOrDefault(b => b.BeatmapInfo.ID == previousSelection?.ID); + + if (matchingBeatmap != null) + matchingBeatmap.State.Value = CarouselItemState.Selected; } - return null; + return removedSets; + } + + public IEnumerable RemoveItemsByID(Guid beatmapSetID) + { + if (BeatmapSetsByID.TryGetValue(beatmapSetID, out var carouselBeatmapSets)) + { + foreach (var set in carouselBeatmapSets) + RemoveItem(set); + + return carouselBeatmapSets; + } + + return Enumerable.Empty(); } public override void RemoveItem(CarouselItem i) @@ -1102,10 +1252,12 @@ namespace osu.Game.Screens.Select } } - protected partial class CarouselScrollContainer : UserTrackingScrollContainer + public partial class CarouselScrollContainer : UserTrackingScrollContainer { 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/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index 712b610515..dec2c1c1de 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -3,7 +3,6 @@ using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -19,7 +18,6 @@ using osu.Game.Overlays.BeatmapSet; using osu.Game.Resources.Localisation.Web; using osu.Game.Screens.Select.Details; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.Select { @@ -28,7 +26,6 @@ namespace osu.Game.Screens.Select private const float spacing = 10; private const float transition_duration = 250; - private readonly AdvancedStats advanced; private readonly UserRatings ratingsDisplay; private readonly MetadataSection description, source, tags; private readonly Container failRetryContainer; @@ -68,12 +65,15 @@ namespace osu.Game.Screens.Select public BeatmapDetails() { + CornerRadius = 10; + Masking = true; + Children = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.5f), + Colour = Colour4.Black.Opacity(0.3f), }, new Container { @@ -109,12 +109,6 @@ namespace osu.Game.Screens.Select Padding = new MarginPadding { Right = spacing / 2 }, Children = new[] { - new DetailBox().WithChild(advanced = new AdvancedStats - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = spacing, Top = spacing * 2, Bottom = spacing }, - }), new DetailBox().WithChild(new OnlineViewContainer(string.Empty) { RelativeSizeAxes = Axes.X, @@ -129,7 +123,8 @@ namespace osu.Game.Screens.Select }, new OsuScrollContainer { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + Height = 250, Width = 0.5f, ScrollbarVisible = false, Padding = new MarginPadding { Left = spacing / 2 }, @@ -141,9 +136,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,17 +171,10 @@ namespace osu.Game.Screens.Select }, loading = new LoadingLayer(true) }; - - void searchOnSongSelect(string text) - { - if (songSelect != null) - songSelect.FilterControl.CurrentTextSearch.Value = text; - } } private void updateStatistics() { - advanced.BeatmapInfo = BeatmapInfo; description.Metadata = BeatmapInfo?.DifficultyName ?? string.Empty; source.Metadata = BeatmapInfo?.Metadata.Source ?? string.Empty; tags.Metadata = BeatmapInfo?.Metadata.Tags ?? string.Empty; @@ -285,11 +273,6 @@ namespace osu.Game.Screens.Select InternalChildren = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.5f), - }, content = new Container { RelativeSizeAxes = Axes.X, diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 81759f6787..c69cd6ead6 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 { @@ -60,7 +61,7 @@ namespace osu.Game.Screens.Select { Type = EdgeEffectType.Glow, Colour = new Color4(130, 204, 255, 150), - Radius = 20, + Radius = 15, Roundness = 15, }; } @@ -160,6 +161,7 @@ namespace osu.Game.Screens.Select private ILocalisedBindableString artistBinding; private FillFlowContainer infoLabelContainer; private Container bpmLabelContainer; + private Container lengthLabelContainer; private readonly WorkingBeatmap working; private readonly RulesetInfo ruleset; @@ -304,7 +306,7 @@ namespace osu.Game.Screens.Select }, infoLabelContainer = new FillFlowContainer { - Margin = new MarginPadding { Top = 20 }, + Margin = new MarginPadding { Top = 8 }, Spacing = new Vector2(20, 0), AutoSizeAxes = Axes.Both, } @@ -340,10 +342,10 @@ namespace osu.Game.Screens.Select { settingChangeTracker?.Dispose(); - refreshBPMLabel(); + refreshBPMAndLengthLabel(); settingChangeTracker = new ModSettingChangeTracker(m.NewValue); - settingChangeTracker.SettingChanged += _ => refreshBPMLabel(); + settingChangeTracker.SettingChanged += _ => refreshBPMAndLengthLabel(); }, true); } @@ -369,12 +371,10 @@ namespace osu.Game.Screens.Select infoLabelContainer.Children = new Drawable[] { - new InfoLabel(new BeatmapStatistic + lengthLabelContainer = new Container { - Name = $"Length (Drain: {playableBeatmap.CalculateDrainLength().ToFormattedDuration().ToString()})", - CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length), - Content = working.BeatmapInfo.Length.ToFormattedDuration().ToString(), - }), + AutoSizeAxes = Axes.Both, + }, bpmLabelContainer = new Container { AutoSizeAxes = Axes.Both, @@ -393,7 +393,7 @@ namespace osu.Game.Screens.Select } } - private void refreshBPMLabel() + private void refreshBPMAndLengthLabel() { var beatmap = working.Beatmap; @@ -415,10 +415,20 @@ namespace osu.Game.Screens.Select bpmLabelContainer.Child = new InfoLabel(new BeatmapStatistic { - Name = "BPM", + Name = BeatmapsetsStrings.ShowStatsBpm, CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm), Content = labelText }); + + double drainLength = Math.Round(beatmap.CalculateDrainLength() / rate); + double hitLength = Math.Round(beatmap.BeatmapInfo.Length / rate); + + lengthLabelContainer.Child = new InfoLabel(new BeatmapStatistic + { + Name = BeatmapsetsStrings.ShowStatsTotalLength(drainLength.ToFormattedDuration()), + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length), + Content = hitLength.ToFormattedDuration().ToString(), + }); } private Drawable getMapper(BeatmapMetadata metadata) 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 6917bd1da2..1ca4b371c3 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -41,6 +41,23 @@ namespace osu.Game.Screens.Select.Carousel return match; } + if (!match) return false; + + if (criteria.SearchTerms.Length > 0) + { + match = BeatmapInfo.Match(criteria.SearchTerms); + + // if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs. + // this should be done after text matching so we can prioritise matching numbers in metadata. + if (!match && criteria.SearchNumber.HasValue) + { + match = (BeatmapInfo.OnlineID == criteria.SearchNumber.Value) || + (BeatmapInfo.BeatmapSet?.OnlineID == criteria.SearchNumber.Value); + } + } + + if (!match) return false; + match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(BeatmapInfo.StarRating); match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(BeatmapInfo.Difficulty.ApproachRate); match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(BeatmapInfo.Difficulty.DrainRate); @@ -59,45 +76,11 @@ namespace osu.Game.Screens.Select.Carousel 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 searchableTerms = BeatmapInfo.GetSearchableTerms(); - - foreach (FilterCriteria.OptionalTextFilter criteriaTerm in criteria.SearchTerms) - { - bool any = false; - - // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator - foreach (string searchTerm in searchableTerms) - { - if (!criteriaTerm.Matches(searchTerm)) continue; - - any = true; - break; - } - - if (any) continue; - - match = false; - break; - } - - // if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs. - // this should be done after text matching so we can prioritise matching numbers in metadata. - if (!match && criteria.SearchNumber.HasValue) - { - match = (BeatmapInfo.OnlineID == criteria.SearchNumber.Value) || - (BeatmapInfo.BeatmapSet?.OnlineID == criteria.SearchNumber.Value); - } - } - - 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/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 67822a27ee..6d2e938fb7 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Select.Carousel .ForEach(AddItem); } - protected override CarouselItem? GetNextToSelect() + public override CarouselItem? GetNextToSelect() { if (LastSelected == null || LastSelected.Filtered.Value) { diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs index 9302578038..b2ca117cec 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs @@ -14,6 +14,8 @@ namespace osu.Game.Screens.Select.Carousel public IReadOnlyList Items => items; + public int TotalItemsNotFiltered { get; private set; } + private readonly List items = new List(); /// @@ -31,6 +33,9 @@ namespace osu.Game.Screens.Select.Carousel { items.Remove(i); + if (!i.Filtered.Value) + TotalItemsNotFiltered--; + // it's important we do the deselection after removing, so any further actions based on // State.ValueChanged make decisions post-removal. i.State.Value = CarouselItemState.Collapsed; @@ -55,6 +60,9 @@ namespace osu.Game.Screens.Select.Carousel // criteria may be null for initial population. the filtering will be applied post-add. items.Add(i); } + + if (!i.Filtered.Value) + TotalItemsNotFiltered++; } public CarouselGroup(List? items = null) @@ -84,18 +92,29 @@ namespace osu.Game.Screens.Select.Carousel { base.Filter(criteria); - items.ForEach(c => c.Filter(criteria)); + TotalItemsNotFiltered = 0; - criteriaComparer = Comparer.Create((x, y) => + foreach (var c in items) { - int comparison = x.CompareTo(criteria, y); - if (comparison != 0) - return comparison; + c.Filter(criteria); + if (!c.Filtered.Value) + TotalItemsNotFiltered++; + } - return x.ItemID.CompareTo(y.ItemID); - }); + // Sorting is expensive, so only perform if it's actually changed. + if (lastCriteria?.RequiresSorting(criteria) != false) + { + criteriaComparer = Comparer.Create((x, y) => + { + int comparison = x.CompareTo(criteria, y); + if (comparison != 0) + return comparison; - items.Sort(criteriaComparer); + return x.ItemID.CompareTo(y.ItemID); + }); + + items.Sort(criteriaComparer); + } lastCriteria = criteria; } diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs index 7f90e05744..cf4ba5924f 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs @@ -36,13 +36,13 @@ namespace osu.Game.Screens.Select.Carousel /// items have been filtered. This bool will be true during the base /// operation. /// - private bool filteringItems; + protected bool DisableSelection; public override void Filter(FilterCriteria criteria) { - filteringItems = true; + DisableSelection = true; base.Filter(criteria); - filteringItems = false; + DisableSelection = false; attemptSelection(); } @@ -95,7 +95,7 @@ namespace osu.Game.Screens.Select.Carousel private void attemptSelection() { - if (filteringItems) return; + if (DisableSelection) return; // we only perform eager selection if we are a currently selected group. if (State.Value != CarouselItemState.Selected) return; @@ -110,7 +110,7 @@ namespace osu.Game.Screens.Select.Carousel /// Finds the item this group would select next if it attempted selection /// /// An unfiltered item nearest to the last selected one or null if all items are filtered - protected virtual CarouselItem? GetNextToSelect() + public virtual CarouselItem? GetNextToSelect() { if (Items.Count == 0) return null; diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index f08d14720b..baf0a14062 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -27,6 +27,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; @@ -57,6 +58,8 @@ namespace osu.Game.Screens.Select.Carousel private StarCounter starCounter = null!; private DifficultyIcon difficultyIcon = null!; + private OsuSpriteText keyCountText = null!; + [Resolved] private BeatmapSetOverlay? beatmapOverlay { get; set; } @@ -69,6 +72,9 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private RealmAccess realm { get; set; } = null!; + [Resolved] + private IBindable ruleset { get; set; } = null!; + private IBindable starDifficultyBindable = null!; private CancellationTokenSource? starDifficultyCancellationSource; @@ -85,7 +91,7 @@ namespace osu.Game.Screens.Select.Carousel if (songSelect != null) { - mainMenuItems = songSelect.CreateForwardNavigationMenuItemsForBeatmap(beatmapInfo); + mainMenuItems = songSelect.CreateForwardNavigationMenuItemsForBeatmap(() => beatmapInfo); selectRequested = b => songSelect.FinaliseSelection(b); } @@ -133,6 +139,13 @@ namespace osu.Game.Screens.Select.Carousel AutoSizeAxes = Axes.Both, Children = new[] { + keyCountText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 20), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Alpha = 0, + }, new OsuSpriteText { Text = beatmapInfo.DifficultyName, @@ -167,6 +180,13 @@ namespace osu.Game.Screens.Select.Carousel }; } + protected override void LoadComplete() + { + base.LoadComplete(); + + ruleset.BindValueChanged(_ => updateKeyCount()); + } + protected override void Selected() { base.Selected(); @@ -216,11 +236,31 @@ namespace osu.Game.Screens.Select.Carousel if (d.NewValue != null) difficultyIcon.Current.Value = d.NewValue.Value; }, true); + + updateKeyCount(); } base.ApplyState(); } + private void updateKeyCount() + { + if (Item?.State.Value == CarouselItemState.Collapsed) + return; + + if (ruleset.Value.OnlineID == 3) + { + // Account for mania differences locally for now. + // Eventually this should be handled in a more modular way, allowing rulesets to add more information to the panel. + ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance(); + + keyCountText.Alpha = 1; + keyCountText.Text = $"[{legacyRuleset.GetKeyCount(beatmapInfo)}K]"; + } + else + keyCountText.Alpha = 0; + } + public MenuItem[] ContextMenuItems { get @@ -233,7 +273,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 4234184ad1..bd659d7423 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -5,11 +5,14 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -44,6 +47,10 @@ namespace osu.Game.Screens.Select.Carousel private Task? beatmapsLoadTask; + private MenuItem[]? mainMenuItems; + + private double timeSinceUnpool; + [Resolved] private BeatmapManager manager { get; set; } = null!; @@ -52,13 +59,17 @@ namespace osu.Game.Screens.Select.Carousel base.FreeAfterUse(); Item = null; + timeSinceUnpool = 0; ClearTransforms(); } [BackgroundDependencyLoader] - private void load(BeatmapSetOverlay? beatmapOverlay) + private void load(BeatmapSetOverlay? beatmapOverlay, SongSelect? songSelect) { + if (songSelect != null) + mainMenuItems = songSelect.CreateForwardNavigationMenuItemsForBeatmap(() => (((CarouselBeatmapSet)Item!).GetNextToSelect() as CarouselBeatmap)!.BeatmapInfo); + restoreHiddenRequested = s => { foreach (var b in s.Beatmaps) @@ -87,13 +98,21 @@ namespace osu.Game.Screens.Select.Carousel // algorithm for this is taken from ScrollContainer. // while it doesn't necessarily need to match 1:1, as we are emulating scroll in some cases this feels most correct. Y = (float)Interpolation.Lerp(targetY, Y, Math.Exp(-0.01 * Time.Elapsed)); + + loadContentIfRequired(); } + private CancellationTokenSource? loadCancellation; + protected override void UpdateItem() { + loadCancellation?.Cancel(); + loadCancellation = null; + base.UpdateItem(); Content.Clear(); + Header.Clear(); beatmapContainer = null; beatmapsLoadTask = null; @@ -102,32 +121,8 @@ namespace osu.Game.Screens.Select.Carousel return; beatmapSet = ((CarouselBeatmapSet)Item).BeatmapSet; - - DelayedLoadWrapper background; - DelayedLoadWrapper mainFlow; - - Header.Children = new Drawable[] - { - // Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set). - background = new DelayedLoadWrapper(() => new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID))) - { - RelativeSizeAxes = Axes.Both, - }, 200) - { - RelativeSizeAxes = Axes.Both - }, - mainFlow = new DelayedLoadWrapper(() => new SetPanelContent((CarouselBeatmapSet)Item), 50) - { - RelativeSizeAxes = Axes.Both - }, - }; - - background.DelayedLoadComplete += fadeContentIn; - mainFlow.DelayedLoadComplete += fadeContentIn; } - private void fadeContentIn(Drawable d) => d.FadeInFromZero(150); - protected override void Deselected() { base.Deselected(); @@ -185,6 +180,56 @@ namespace osu.Game.Screens.Select.Carousel } } + [Resolved] + private BeatmapCarousel.CarouselScrollContainer scrollContainer { get; set; } = null!; + + private void loadContentIfRequired() + { + Quad containingSsdq = scrollContainer.ScreenSpaceDrawQuad; + + // Using DelayedLoadWrappers would only allow us to load content when on screen, but we want to preload while off-screen + // to provide a better user experience. + + // This is tracking time that this drawable is updating since the last pool. + // This is intended to provide a debounce so very fast scrolls (from one end to the other of the carousel) + // don't cause huge overheads. + // + // We increase the delay based on distance from centre, so the beatmaps the user is currently looking at load first. + float timeUpdatingBeforeLoad = 50 + Math.Abs(containingSsdq.Centre.Y - ScreenSpaceDrawQuad.Centre.Y) / containingSsdq.Height * 100; + + Debug.Assert(Item != null); + + // A load is already in progress if the cancellation token is non-null. + if (loadCancellation != null) + return; + + timeSinceUnpool += Time.Elapsed; + + // We only trigger a load after this set has been in an updating state for a set amount of time. + if (timeSinceUnpool <= timeUpdatingBeforeLoad) + return; + + loadCancellation = new CancellationTokenSource(); + + LoadComponentsAsync(new CompositeDrawable[] + { + // Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set). + new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID))) + { + RelativeSizeAxes = Axes.Both, + }, + new SetPanelContent((CarouselBeatmapSet)Item) + { + Depth = float.MinValue, + RelativeSizeAxes = Axes.Both, + } + }, drawables => + { + Header.AddRange(drawables); + drawables.ForEach(d => d.FadeInFromZero(150)); + }, loadCancellation.Token); + } + private void updateBeatmapYPositions() { if (beatmapContainer == null) @@ -197,7 +242,7 @@ namespace osu.Game.Screens.Select.Carousel bool isSelected = Item?.State.Value == CarouselItemState.Selected; - foreach (var panel in beatmapContainer.Children) + foreach (var panel in beatmapContainer) { Debug.Assert(panel.Item != null); @@ -222,10 +267,18 @@ namespace osu.Game.Screens.Select.Carousel if (Item?.State.Value == CarouselItemState.NotSelected) items.Add(new OsuMenuItem("Expand", MenuItemType.Highlighted, () => Item.State.Value = CarouselItemState.Selected)); + if (mainMenuItems != null) + items.AddRange(mainMenuItems); + 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/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index a383298faa..0d68a0ec3c 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -25,10 +26,11 @@ using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; +using osu.Game.Overlays.Mods; namespace osu.Game.Screens.Select.Details { - public partial class AdvancedStats : Container + public partial class AdvancedStats : Container, IHasCustomTooltip { [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } @@ -44,6 +46,9 @@ namespace osu.Game.Screens.Select.Details protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate; private readonly StatisticRow starDifficulty; + public ITooltip GetCustomTooltip() => new AdjustedAttributesTooltip(); + public AdjustedAttributesTooltip.Data TooltipContent { get; private set; } + private IBeatmapInfo beatmapInfo; public IBeatmapInfo BeatmapInfo @@ -59,21 +64,67 @@ namespace osu.Game.Screens.Select.Details } } - public AdvancedStats() + public AdvancedStats(int columns = 1) { - Child = new FillFlowContainer + switch (columns) { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new[] - { - FirstValue = new StatisticRow(), // circle size/key amount - HpDrain = new StatisticRow { Title = BeatmapsetsStrings.ShowStatsDrain }, - Accuracy = new StatisticRow { Title = BeatmapsetsStrings.ShowStatsAccuracy }, - ApproachRate = new StatisticRow { Title = BeatmapsetsStrings.ShowStatsAr }, - starDifficulty = new StatisticRow(10, true) { Title = BeatmapsetsStrings.ShowStatsStars }, - }, - }; + case 1: + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new[] + { + FirstValue = new StatisticRow(), // circle size/key amount + HpDrain = new StatisticRow { Title = BeatmapsetsStrings.ShowStatsDrain }, + Accuracy = new StatisticRow { Title = BeatmapsetsStrings.ShowStatsAccuracy }, + ApproachRate = new StatisticRow { Title = BeatmapsetsStrings.ShowStatsAr }, + starDifficulty = new StatisticRow(10, true) { Title = BeatmapsetsStrings.ShowStatsStars }, + }, + }; + break; + + case 2: + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + Children = new[] + { + FirstValue = new StatisticRow + { + Width = 0.5f, + Padding = new MarginPadding { Right = 5, Vertical = 2.5f }, + }, // circle size/key amount + HpDrain = new StatisticRow + { + Title = BeatmapsetsStrings.ShowStatsDrain, + Width = 0.5f, + Padding = new MarginPadding { Left = 5, Vertical = 2.5f }, + }, + Accuracy = new StatisticRow + { + Title = BeatmapsetsStrings.ShowStatsAccuracy, + Width = 0.5f, + Padding = new MarginPadding { Right = 5, Vertical = 2.5f }, + }, + ApproachRate = new StatisticRow + { + Title = BeatmapsetsStrings.ShowStatsAr, + Width = 0.5f, + Padding = new MarginPadding { Left = 5, Vertical = 2.5f }, + }, + starDifficulty = new StatisticRow(10, true) + { + Title = BeatmapsetsStrings.ShowStatsStars, + Width = 0.5f, + Padding = new MarginPadding { Right = 5, Vertical = 2.5f }, + }, + }, + }; + break; + } } [BackgroundDependencyLoader] @@ -118,21 +169,44 @@ namespace osu.Game.Screens.Select.Details IBeatmapDifficultyInfo baseDifficulty = BeatmapInfo?.Difficulty; BeatmapDifficulty adjustedDifficulty = null; - if (baseDifficulty != null && mods.Value.Any(m => m is IApplicableToDifficulty)) + IRulesetInfo ruleset = gameRuleset?.Value ?? beatmapInfo.Ruleset; + + if (baseDifficulty != null) { - adjustedDifficulty = new BeatmapDifficulty(baseDifficulty); + BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(baseDifficulty); foreach (var mod in mods.Value.OfType()) - mod.ApplyToDifficulty(adjustedDifficulty); + mod.ApplyToDifficulty(originalDifficulty); + + adjustedDifficulty = originalDifficulty; + + if (gameRuleset != null) + { + double rate = 1; + foreach (var mod in mods.Value.OfType()) + rate = mod.ApplyToRate(0, rate); + + adjustedDifficulty = ruleset.CreateInstance().GetRateAdjustedDisplayDifficulty(originalDifficulty, rate); + + TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, adjustedDifficulty); + } } - switch (BeatmapInfo?.Ruleset.OnlineID) + switch (ruleset.OnlineID) { case 3: - // Account for mania differences locally for now - // Eventually this should be handled in a more modular way, allowing rulesets to return arbitrary difficulty attributes + // Account for mania differences locally for now. + // Eventually this should be handled in a more modular way, allowing rulesets to return arbitrary difficulty attributes. + ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.CreateInstance(); + + // For the time being, the key count is static no matter what, because: + // a) The method doesn't have knowledge of the active keymods. Doing so may require considerations for filtering. + // b) Using the difficulty adjustment mod to adjust OD doesn't have an effect on conversion. + int keyCount = baseDifficulty == null ? 0 : legacyRuleset.GetKeyCount(BeatmapInfo); + FirstValue.Title = BeatmapsetsStrings.ShowStatsCsMania; - FirstValue.Value = (baseDifficulty?.CircleSize ?? 0, null); + FirstValue.Value = (keyCount, keyCount); + break; default: @@ -261,23 +335,36 @@ namespace osu.Game.Screens.Select.Details Font = OsuFont.GetFont(size: 12) }, }, - bar = new Bar + new Container { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - Height = 5, - BackgroundColour = Color4.White.Opacity(0.5f), - Padding = new MarginPadding { Left = name_width + 10, Right = value_width + 10 }, - }, - ModBar = new Bar - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - Alpha = 0.5f, - Height = 5, + RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Left = name_width + 10, Right = value_width + 10 }, + Children = new Drawable[] + { + new Container + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + Height = 5, + + CornerRadius = 2, + Masking = true, + Children = new Drawable[] + { + bar = new Bar + { + RelativeSizeAxes = Axes.Both, + BackgroundColour = Color4.White.Opacity(0.5f), + }, + ModBar = new Bar + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.5f, + }, + } + }, + } }, new Container { diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 38520a85b7..1827eb58ca 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 { @@ -248,7 +250,7 @@ namespace osu.Game.Screens.Select protected override bool OnHover(HoverEvent e) => true; - private partial class FilterControlTextBox : SeekLimitedSearchTextBox + internal partial class FilterControlTextBox : SeekLimitedSearchTextBox { private const float filter_text_size = 12; @@ -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 ab4f85fc92..a7c8e7d093 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -19,6 +19,11 @@ namespace osu.Game.Screens.Select public GroupMode Group; public SortMode Sort; + /// + /// 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; @@ -33,6 +38,7 @@ namespace osu.Game.Screens.Select public OptionalTextFilter Creator; public OptionalTextFilter Artist; public OptionalTextFilter Title; + public OptionalTextFilter DifficultyName; public OptionalRange UserStarDifficulty = new OptionalRange { @@ -63,8 +69,23 @@ namespace osu.Game.Screens.Select 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(searchText, "(\"[^\"]+\"[!]?)")) + foreach (Match quotedSegment in Regex.Matches(value, "(\"[^\"]+\"[!]?)")) { terms.Add(new OptionalTextFilter { SearchTerm = quotedSegment.Value }); remainingText = remainingText.Replace(quotedSegment.Value, string.Empty); @@ -155,13 +176,15 @@ namespace osu.Game.Screens.Select { default: case MatchMode.Substring: - return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase); + // Note that we are using ordinal here to avoid performance issues caused by globalisation concerns. + // See https://github.com/ppy/osu/issues/11571 / https://github.com/dotnet/docs/issues/18423. + return value.Contains(SearchTerm, StringComparison.OrdinalIgnoreCase); 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; + return CultureInfo.InvariantCulture.CompareInfo.Compare(value, searchTerm, CompareOptions.OrdinalIgnoreCase) == 0; } } @@ -196,6 +219,44 @@ namespace osu.Game.Screens.Select public bool Equals(OptionalTextFilter other) => SearchTerm == other.SearchTerm; } + /// + /// Given a new filter criteria, decide whether a full sort needs to be performed. + /// + /// + /// + public bool RequiresSorting(FilterCriteria newCriteria) + { + if (Sort != newCriteria.Sort) + return true; + + switch (Sort) + { + // Some sorts are stable across all other changes. + // Running these sorts will sort all items, including currently hidden items. + case SortMode.Artist: + case SortMode.Author: + case SortMode.DateSubmitted: + case SortMode.DateAdded: + case SortMode.DateRanked: + case SortMode.Source: + case SortMode.Title: + return false; + + // Some sorts use aggregate max comparisons, which will change based on filtered items. + // These sorts generally ignore items hidden by filtered state, so we must force a sort under all circumstances here. + // + // This makes things very slow when typing a text search, and we probably want to consider a way to optimise things going forward. + case SortMode.LastPlayed: + case SortMode.BPM: + case SortMode.Length: + case SortMode.Difficulty: + return true; + + default: + throw new ArgumentOutOfRangeException(nameof(Sort), Sort, "Unknown sort mode"); + } + } + public enum MatchMode { /// diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 1238173b41..0d8905347b 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -76,6 +76,9 @@ namespace osu.Game.Screens.Select 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; } diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index 9a84f9a0aa..69782c25bb 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -19,6 +19,7 @@ using osu.Game.Graphics.Sprites; using osuTK; using osuTK.Graphics; using osu.Game.Input.Bindings; +using osu.Game.Utils; namespace osu.Game.Screens.Select { @@ -87,12 +88,11 @@ namespace osu.Game.Screens.Select private void updateMultiplierText() => Schedule(() => { double multiplier = Current.Value?.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier) ?? 1; + MultiplierText.Text = multiplier == 1 ? string.Empty : ModUtils.FormatScoreMultiplier(multiplier); - MultiplierText.Text = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; - - if (multiplier > 1.0) + if (multiplier > 1) MultiplierText.FadeColour(highMultiplierColour, 200); - else if (multiplier < 1.0) + else if (multiplier < 1) MultiplierText.FadeColour(lowMultiplierColour, 200); else MultiplierText.FadeColour(Color4.White, 200); 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/BeatmapLeaderboardScope.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs index 5bcb4c27a7..e2e3404877 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs @@ -12,12 +12,12 @@ namespace osu.Game.Screens.Select.Leaderboards [Description("Local Ranking")] Local, - [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowScoreboardCountry))] - Country, - [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowScoreboardGlobal))] Global, + [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowScoreboardCountry))] + Country, + [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowScoreboardFriend))] Friend, } diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs index 5753c268d9..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; diff --git a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs index 8a1b9ef3e1..deb1100dfc 100644 --- a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs @@ -80,8 +80,8 @@ namespace osu.Game.Screens.Select protected override BeatmapDetailAreaTabItem[] CreateTabItems() => base.CreateTabItems().Concat(new BeatmapDetailAreaTabItem[] { new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Local), - new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Country), new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Global), + new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Country), new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Friend), }).ToArray(); @@ -95,12 +95,12 @@ namespace osu.Game.Screens.Select case TabType.Local: return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Local); - case TabType.Country: - return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Country); - case TabType.Global: return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Global); + case TabType.Country: + return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Country); + case TabType.Friends: return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Friend); diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index fe13d6d5a8..7b7b8857f3 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.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 osu.Framework.Allocation; @@ -34,10 +35,10 @@ namespace osu.Game.Screens.Select public override bool AllowExternalScreenChange => true; - public override MenuItem[] CreateForwardNavigationMenuItemsForBeatmap(BeatmapInfo beatmap) => new MenuItem[] + public override MenuItem[] CreateForwardNavigationMenuItemsForBeatmap(Func getBeatmap) => new MenuItem[] { - new OsuMenuItem(ButtonSystemStrings.Play.ToSentence(), MenuItemType.Highlighted, () => FinaliseSelection(beatmap)), - new OsuMenuItem(ButtonSystemStrings.Edit.ToSentence(), MenuItemType.Standard, () => Edit(beatmap)) + new OsuMenuItem(ButtonSystemStrings.Play.ToSentence(), MenuItemType.Highlighted, () => FinaliseSelection(getBeatmap())), + new OsuMenuItem(ButtonSystemStrings.Edit.ToSentence(), MenuItemType.Standard, () => Edit(getBeatmap())) }; protected override UserActivity InitialActivity => new UserActivity.ChoosingBeatmap(); @@ -48,6 +49,8 @@ namespace osu.Game.Screens.Select private void load(OsuColour colours) { BeatmapOptions.AddButton(ButtonSystemStrings.Edit.ToSentence(), @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => Edit()); + + AddInternal(new SongSelectTouchInputDetector()); } protected void PresentScore(ScoreInfo score) => diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 58755878d0..bf1724995a 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -13,6 +13,7 @@ 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.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Bindings; @@ -35,6 +36,7 @@ using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; +using osu.Game.Screens.Select.Details; using osu.Game.Screens.Select.Options; using osu.Game.Skinning; using osuTK; @@ -45,7 +47,7 @@ namespace osu.Game.Screens.Select { public abstract partial class SongSelect : ScreenWithBeatmapBackground, IKeyBindingHandler { - public static readonly float WEDGE_HEIGHT = 245; + public static readonly float WEDGE_HEIGHT = 200; protected const float BACKGROUND_BLUR = 20; private const float left_area_padding = 20; @@ -89,11 +91,11 @@ namespace osu.Game.Screens.Select /// Creates any "action" menu items for the provided beatmap (ie. "Select", "Play", "Edit"). /// These will always be placed at the top of the context menu, with common items added below them. /// - /// The beatmap to create items for. + /// The beatmap to create items for. /// The menu items. - public virtual MenuItem[] CreateForwardNavigationMenuItemsForBeatmap(BeatmapInfo beatmap) => new MenuItem[] + public virtual MenuItem[] CreateForwardNavigationMenuItemsForBeatmap(Func getBeatmap) => new MenuItem[] { - new OsuMenuItem(@"Select", MenuItemType.Highlighted, () => FinaliseSelection(beatmap)) + new OsuMenuItem(@"Select", MenuItemType.Highlighted, () => FinaliseSelection(getBeatmap())) }; [Resolved] @@ -132,13 +134,15 @@ namespace osu.Game.Screens.Select private IDisposable? modSelectOverlayRegistration; + private AdvancedStats advancedStats = null!; + [Resolved] private MusicController music { get; set; } = null!; [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) @@ -162,7 +166,7 @@ namespace osu.Game.Screens.Select BleedBottom = Footer.HEIGHT, SelectionChanged = updateSelectedBeatmap, BeatmapSetsChanged = carouselBeatmapsLoaded, - FilterApplied = updateVisibleBeatmapCount, + FilterApplied = () => Scheduler.AddOnce(updateVisibleBeatmapCount), GetRecommendedBeatmap = s => recommender?.GetRecommendedBeatmap(s), }, c => carouselContainer.Child = c); @@ -171,11 +175,6 @@ namespace osu.Game.Screens.Select AddRangeInternal(new Drawable[] { - new ResetScrollContainer(() => Carousel.ScrollToSelected()) - { - RelativeSizeAxes = Axes.Y, - Width = 250, - }, new VerticalMaskingContainer { Children = new Drawable[] @@ -240,9 +239,13 @@ namespace osu.Game.Screens.Select Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = left_area_padding }, + Padding = new MarginPadding { Top = 5 }, Children = new Drawable[] { + new LeftSideInteractionContainer(() => Carousel.ScrollToSelected()) + { + RelativeSizeAxes = Axes.Both, + }, beatmapInfoWedge = new BeatmapInfoWedge { Height = WEDGE_HEIGHT, @@ -254,12 +257,48 @@ namespace osu.Game.Screens.Select }, }, new Container + { + RelativeSizeAxes = Axes.X, + Height = 90, + Padding = new MarginPadding(10) + { + Left = left_area_padding, + Right = left_area_padding * 2 + 5, + }, + Y = WEDGE_HEIGHT, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.Black.Opacity(0.3f), + }, + advancedStats = new AdvancedStats(2) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding(10) + }, + } + }, + } + }, + new Container { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Bottom = Footer.HEIGHT, - Top = WEDGE_HEIGHT, + Top = WEDGE_HEIGHT + 70, Left = left_area_padding, Right = left_area_padding * 2, }, @@ -311,9 +350,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"); @@ -390,6 +429,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. /// @@ -516,7 +564,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) { @@ -608,7 +660,8 @@ namespace osu.Game.Screens.Select logo.Action = () => { - FinaliseSelection(); + if (this.IsCurrentScreen()) + FinaliseSelection(); return false; }; } @@ -634,7 +687,7 @@ namespace osu.Game.Screens.Select beginLooping(); - if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending) + if (!Beatmap.Value.BeatmapSetInfo.DeletePending) { updateCarouselSelection(); @@ -783,6 +836,10 @@ namespace osu.Game.Screens.Select BeatmapDetails.Beatmap = beatmap; + ModSelect.Beatmap = beatmap; + + advancedStats.BeatmapInfo = beatmap.BeatmapInfo; + bool beatmapSelected = beatmap is not DummyWorkingBeatmap; if (beatmapSelected) @@ -829,7 +886,7 @@ namespace osu.Game.Screens.Select private void carouselBeatmapsLoaded() { bindBindables(); - updateVisibleBeatmapCount(); + Scheduler.AddOnce(updateVisibleBeatmapCount); Carousel.AllowSelection = true; @@ -863,7 +920,8 @@ 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 = Carousel.CountDisplayed != 1 ? $"{Carousel.CountDisplayed:#,0} matches" : $"{Carousel.CountDisplayed:#,0} match"; + int carouselCountDisplayed = Carousel.CountDisplayed; + FilterControl.InformationalText = carouselCountDisplayed != 1 ? $"{carouselCountDisplayed:#,0} matches" : $"{carouselCountDisplayed:#,0} match"; } private bool boundLocalBindables; @@ -916,14 +974,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; @@ -963,7 +1027,7 @@ namespace osu.Game.Screens.Select if (e.ShiftPressed) { if (!Beatmap.IsDefault) - delete(Beatmap.Value.BeatmapSetInfo); + DeleteBeatmap(Beatmap.Value.BeatmapSetInfo); return true; } @@ -996,18 +1060,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/Select/SongSelectTouchInputDetector.cs b/osu.Game/Screens/Select/SongSelectTouchInputDetector.cs new file mode 100644 index 0000000000..b726acb45f --- /dev/null +++ b/osu.Game/Screens/Select/SongSelectTouchInputDetector.cs @@ -0,0 +1,69 @@ +// 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.Graphics; +using osu.Game.Configuration; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Utils; + +namespace osu.Game.Screens.Select +{ + public partial class SongSelectTouchInputDetector : Component + { + [Resolved] + private Bindable ruleset { get; set; } = null!; + + [Resolved] + private Bindable> mods { get; set; } = null!; + + private IBindable touchActive = null!; + + [BackgroundDependencyLoader] + private void load(SessionStatics statics) + { + touchActive = statics.GetBindable(Static.TouchInputActive); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ruleset.BindValueChanged(_ => Scheduler.AddOnce(updateState)); + mods.BindValueChanged(_ => Scheduler.AddOnce(updateState)); + mods.BindDisabledChanged(_ => Scheduler.AddOnce(updateState)); + touchActive.BindValueChanged(_ => Scheduler.AddOnce(updateState)); + updateState(); + } + + private void updateState() + { + if (mods.Disabled) + return; + + var touchDeviceMod = ruleset.Value.CreateInstance().GetTouchDeviceMod(); + + if (touchDeviceMod == null) + return; + + bool touchDeviceModEnabled = mods.Value.Any(mod => mod is ModTouchDevice); + + if (touchActive.Value && !touchDeviceModEnabled) + { + var candidateMods = mods.Value.Append(touchDeviceMod).ToArray(); + + if (!ModUtils.CheckCompatibleSet(candidateMods, out _)) + return; + + mods.Value = candidateMods; + } + + if (!touchActive.Value && touchDeviceModEnabled) + mods.Value = mods.Value.Where(mod => mod is not ModTouchDevice).ToArray(); + } + } +} diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 48b5c210b8..c4aef3c878 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.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.Diagnostics; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -33,22 +30,27 @@ namespace osu.Game.Screens.Spectate private readonly List users = new List(); [Resolved] - private BeatmapManager beatmaps { get; set; } + private BeatmapManager beatmaps { get; set; } = null!; [Resolved] - private RulesetStore rulesets { get; set; } + private RulesetStore rulesets { get; set; } = null!; [Resolved] - private SpectatorClient spectatorClient { get; set; } + private SpectatorClient spectatorClient { get; set; } = null!; [Resolved] - private UserLookupCache userLookupCache { get; set; } + private UserLookupCache userLookupCache { get; set; } = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; private readonly IBindableDictionary userStates = new BindableDictionary(); private readonly Dictionary userMap = new Dictionary(); private readonly Dictionary gameplayStates = new Dictionary(); + private IDisposable? realmSubscription; + /// /// Creates a new . /// @@ -58,11 +60,6 @@ namespace osu.Game.Screens.Spectate this.users.AddRange(users); } - [Resolved] - private RealmAccess realm { get; set; } - - private IDisposable realmSubscription; - protected override void LoadComplete() { base.LoadComplete(); @@ -90,7 +87,7 @@ namespace osu.Game.Screens.Spectate })); } - private void beatmapsChanged(IRealmCollection items, ChangeSet changes) + private void beatmapsChanged(IRealmCollection items, ChangeSet? changes) { if (changes?.InsertedIndices == null) return; @@ -109,7 +106,7 @@ namespace osu.Game.Screens.Spectate } } - private void onUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e) + private void onUserStatesChanged(object? sender, NotifyDictionaryChangedEventArgs e) { switch (e.Action) { @@ -132,7 +129,7 @@ namespace osu.Game.Screens.Spectate switch (newState.State) { case SpectatedUserState.Playing: - Schedule(() => OnNewPlayingUserState(userId, newState)); + OnNewPlayingUserState(userId, newState); startGameplay(userId); break; @@ -140,6 +137,10 @@ namespace osu.Game.Screens.Spectate markReceivedAllFrames(userId); break; + case SpectatedUserState.Failed: + failGameplay(userId); + break; + case SpectatedUserState.Quit: quitGameplay(userId); break; @@ -176,7 +177,7 @@ namespace osu.Game.Screens.Spectate var gameplayState = new SpectatorGameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap)); gameplayStates[userId] = gameplayState; - Schedule(() => StartGameplay(userId, gameplayState)); + StartGameplay(userId, gameplayState); } /// @@ -188,6 +189,20 @@ namespace osu.Game.Screens.Spectate gameplayState.Score.Replay.HasReceivedAllFrames = true; } + private void failGameplay(int userId) + { + if (!userMap.ContainsKey(userId)) + return; + + if (!gameplayStates.ContainsKey(userId)) + return; + + markReceivedAllFrames(userId); + + gameplayStates.Remove(userId); + FailGameplay(userId); + } + private void quitGameplay(int userId) { if (!userMap.ContainsKey(userId)) @@ -199,29 +214,39 @@ namespace osu.Game.Screens.Spectate markReceivedAllFrames(userId); gameplayStates.Remove(userId); - Schedule(() => QuitGameplay(userId)); + QuitGameplay(userId); } /// /// Invoked when a spectated user's state has changed to a new state indicating the player is currently playing. + /// Thread safety is not guaranteed – should be scheduled as required. /// /// The user whose state has changed. /// The new state. - protected abstract void OnNewPlayingUserState(int userId, [NotNull] SpectatorState spectatorState); + protected abstract void OnNewPlayingUserState(int userId, SpectatorState spectatorState); /// /// Starts gameplay for a user. + /// Thread safety is not guaranteed – should be scheduled as required. /// /// The user to start gameplay for. /// The gameplay state. - protected abstract void StartGameplay(int userId, [NotNull] SpectatorGameplayState spectatorGameplayState); + protected abstract void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState); /// /// Quits gameplay for a user. + /// Thread safety is not guaranteed – should be scheduled as required. /// /// The user to quit gameplay for. protected abstract void QuitGameplay(int userId); + /// + /// Fails gameplay for a user. + /// Thread safety is not guaranteed – should be scheduled as required. + /// + /// The user to fail gameplay for. + protected abstract void FailGameplay(int userId); + /// /// Stops spectating a user. /// @@ -243,7 +268,7 @@ namespace osu.Game.Screens.Spectate { base.Dispose(isDisposing); - if (spectatorClient != null) + if (spectatorClient.IsNotNull()) { foreach ((int userId, var _) in userMap) spectatorClient.StopWatchingUser(userId); diff --git a/osu.Game/Screens/Utility/CircleGameplay.cs b/osu.Game/Screens/Utility/CircleGameplay.cs index d97812acb4..1f970c5121 100644 --- a/osu.Game/Screens/Utility/CircleGameplay.cs +++ b/osu.Game/Screens/Utility/CircleGameplay.cs @@ -224,7 +224,7 @@ namespace osu.Game.Screens.Utility .FadeOut(duration) .ScaleTo(1.5f, duration); - HitEvent = new HitEvent(Clock.CurrentTime - HitTime, HitResult.Good, new HitObject + HitEvent = new HitEvent(Clock.CurrentTime - HitTime, 1.0, HitResult.Good, new HitObject { HitWindows = new HitWindows(), }, null, null); diff --git a/osu.Game/Screens/Utility/ScrollingGameplay.cs b/osu.Game/Screens/Utility/ScrollingGameplay.cs index f1331d8fb2..5038c53b4a 100644 --- a/osu.Game/Screens/Utility/ScrollingGameplay.cs +++ b/osu.Game/Screens/Utility/ScrollingGameplay.cs @@ -186,7 +186,7 @@ namespace osu.Game.Screens.Utility .FadeOut(duration / 2) .ScaleTo(1.5f, duration / 2); - HitEvent = new HitEvent(Clock.CurrentTime - HitTime, HitResult.Good, new HitObject + HitEvent = new HitEvent(Clock.CurrentTime - HitTime, 1.0, HitResult.Good, new HitObject { HitWindows = new HitWindows(), }, null, null); diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index ba392386de..6fcab6a977 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -15,6 +15,7 @@ using osu.Game.IO; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; +using osu.Game.Skinning.Components; using osuTK; using osuTK.Graphics; @@ -40,7 +41,10 @@ namespace osu.Game.Skinning [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] public ArgonSkin(SkinInfo skin, IStorageResourceProvider resources) - : base(skin, resources) + : base( + skin, + resources + ) { Resources = resources; @@ -109,43 +113,50 @@ namespace osu.Game.Skinning case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container => { - var score = container.OfType().FirstOrDefault(); - var accuracy = container.OfType().FirstOrDefault(); - var combo = container.OfType().FirstOrDefault(); - var ppCounter = container.OfType().FirstOrDefault(); + var health = container.OfType().FirstOrDefault(); + var healthLine = container.OfType().FirstOrDefault(); + var wedgePieces = container.OfType().ToArray(); + var score = container.OfType().FirstOrDefault(); + var accuracy = container.OfType().FirstOrDefault(); + var combo = container.OfType().FirstOrDefault(); var songProgress = container.OfType().FirstOrDefault(); var keyCounter = container.OfType().FirstOrDefault(); - if (score != null) + if (health != null) { - score.Anchor = Anchor.TopCentre; - score.Origin = Anchor.TopCentre; - // elements default to beneath the health bar - const float vertical_offset = 30; + const float components_x_offset = 50; - const float horizontal_padding = 20; + health.Anchor = Anchor.TopLeft; + health.Origin = Anchor.TopLeft; + health.UseRelativeSize.Value = false; + health.Width = 300; + health.BarHeight.Value = 30f; + health.Position = new Vector2(components_x_offset, 20f); - score.Position = new Vector2(0, vertical_offset); - - if (ppCounter != null) + if (healthLine != null) { - ppCounter.Y = score.Position.Y + ppCounter.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).Y - 4; - ppCounter.Origin = Anchor.TopCentre; - ppCounter.Anchor = Anchor.TopCentre; + healthLine.Anchor = Anchor.TopLeft; + healthLine.Origin = Anchor.CentreLeft; + healthLine.Y = health.Y + ArgonHealthDisplay.MAIN_PATH_RADIUS; + healthLine.Size = new Vector2(45, 3); + } + + foreach (var wedgePiece in wedgePieces) + wedgePiece.Position += new Vector2(-50, 15); + + if (score != null) + { + score.Origin = Anchor.TopRight; + score.Position = new Vector2(components_x_offset + 200, wedgePieces.Last().Y + 30); } if (accuracy != null) { - accuracy.Position = new Vector2(-accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 - horizontal_padding, vertical_offset + 5); + // +4 to vertically align the accuracy counter with the score counter. + accuracy.Position = new Vector2(-20, 20); + accuracy.Anchor = Anchor.TopRight; accuracy.Origin = Anchor.TopRight; - accuracy.Anchor = Anchor.TopCentre; - - if (combo != null) - { - combo.Position = new Vector2(accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 + horizontal_padding, vertical_offset + 5); - combo.Anchor = Anchor.TopCentre; - } } var hitError = container.OfType().FirstOrDefault(); @@ -169,34 +180,58 @@ namespace osu.Game.Skinning if (songProgress != 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 = 36 + padding; 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)); } + + if (combo != null && hitError != null) + { + combo.Anchor = Anchor.BottomLeft; + combo.Origin = Anchor.BottomLeft; + combo.Position = new Vector2((hitError.Width + padding), -(padding * 2 + song_progress_offset_height)); + } } } }) { Children = new Drawable[] { - new DefaultComboCounter(), - new DefaultScoreCounter(), - new DefaultAccuracyCounter(), - new DefaultHealthDisplay(), + new ArgonWedgePiece + { + Size = new Vector2(380, 72), + }, + new ArgonWedgePiece + { + Size = new Vector2(380, 72), + Position = new Vector2(4, 5) + }, + new ArgonScoreCounter + { + ShowLabel = { Value = false }, + }, + new ArgonHealthDisplay(), + new BoxElement + { + CornerRadius = { Value = 0.5f } + }, + new ArgonAccuracyCounter(), + new ArgonComboCounter + { + Scale = new Vector2(1.3f) + }, + new BarHitErrorMeter(), + new BarHitErrorMeter(), new ArgonSongProgress(), new ArgonKeyCounterDisplay(), - new BarHitErrorMeter(), - new BarHitErrorMeter(), - new PerformancePointsCounter() } }; @@ -219,19 +254,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/Components/BoxElement.cs b/osu.Game/Skinning/Components/BoxElement.cs new file mode 100644 index 0000000000..34d389728c --- /dev/null +++ b/osu.Game/Skinning/Components/BoxElement.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 System; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Configuration; +using osu.Game.Localisation.SkinComponents; +using osu.Game.Overlays.Settings; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Skinning.Components +{ + public partial class BoxElement : CompositeDrawable, ISerialisableDrawable + { + public bool UsesFixedAnchor { get; set; } + + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.CornerRadius), nameof(SkinnableComponentStrings.CornerRadiusDescription), + SettingControlType = typeof(SettingsPercentageSlider))] + public new BindableFloat CornerRadius { get; } = new BindableFloat(0.25f) + { + MinValue = 0, + MaxValue = 0.5f, + Precision = 0.01f + }; + + public BoxElement() + { + Size = new Vector2(400, 80); + + InternalChildren = new Drawable[] + { + new Box + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + }, + }; + + Masking = true; + } + + protected override void Update() + { + base.Update(); + + base.CornerRadius = CornerRadius.Value * Math.Min(DrawWidth, DrawHeight); + } + } +} diff --git a/osu.Game/Skinning/Components/PlayerName.cs b/osu.Game/Skinning/Components/PlayerName.cs new file mode 100644 index 0000000000..21bf615bc6 --- /dev/null +++ b/osu.Game/Skinning/Components/PlayerName.cs @@ -0,0 +1,57 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.Play; + +namespace osu.Game.Skinning.Components +{ + [UsedImplicitly] + public partial class PlayerName : FontAdjustableSkinComponent + { + private readonly OsuSpriteText text; + + [Resolved] + private GameplayState? gameplayState { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private IBindable? apiUser; + + public PlayerName() + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + text = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + }; + } + + [BackgroundDependencyLoader] + private void load() + { + if (gameplayState != null) + text.Text = gameplayState.Score.ScoreInfo.User.Username; + else + { + apiUser = api.LocalUser.GetBoundCopy(); + apiUser.BindValueChanged(u => text.Text = u.NewValue.Username, true); + } + } + + protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); + } +} diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index fd9653e3e5..34ea0af122 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -31,8 +31,7 @@ namespace osu.Game.Skinning : base( skin, resources, - // In the case of the actual default legacy skin (ie. the fallback one, which a user hasn't applied any modifications to) we want to use the game provided resources. - skin.Protected ? new NamespacedResourceStore(resources.Resources, "Skins/Legacy") : null + new NamespacedResourceStore(resources.Resources, "Skins/Legacy") ) { Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255); diff --git a/osu.Game/Skinning/GameplaySkinComponentLookup.cs b/osu.Game/Skinning/GameplaySkinComponentLookup.cs index a44bf3a43d..ec159873f8 100644 --- a/osu.Game/Skinning/GameplaySkinComponentLookup.cs +++ b/osu.Game/Skinning/GameplaySkinComponentLookup.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -28,8 +27,5 @@ namespace osu.Game.Skinning protected virtual string RulesetPrefix => string.Empty; protected virtual string ComponentName => Component.ToString(); - - public string LookupName => - string.Join('/', new[] { "Gameplay", RulesetPrefix, ComponentName }.Where(s => !string.IsNullOrEmpty(s))); } } 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/ISerialisableDrawable.cs b/osu.Game/Skinning/ISerialisableDrawable.cs index 503b44c2dd..c9dcaca6d1 100644 --- a/osu.Game/Skinning/ISerialisableDrawable.cs +++ b/osu.Game/Skinning/ISerialisableDrawable.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Globalization; using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; @@ -46,7 +47,7 @@ namespace osu.Game.Skinning if (!(target is IParseable parseable)) throw new InvalidOperationException($"Bindable type {target.GetType().ReadableName()} is not {nameof(IParseable)}."); - parseable.Parse(source); + parseable.Parse(source, CultureInfo.InvariantCulture); } } } diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index 326257c25f..ed12292eb3 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -25,6 +25,7 @@ namespace osu.Game.Skinning { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, + FixedWidth = true, }; } } diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 1cbfda16cf..9cd072b607 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -19,9 +19,14 @@ namespace osu.Game.Skinning { public class LegacyBeatmapSkin : LegacySkin { - protected override bool AllowManiaSkin => false; + protected override bool AllowManiaConfigLookups => false; protected override bool UseCustomSampleBanks => true; + // matches stable. references: + // 1. https://github.com/peppy/osu-stable-reference/blob/dc0994645801010d4b628fff5ff79cd3c286ca83/osu!/Graphics/Textures/TextureManager.cs#L115-L137 (beatmap skin textures lookup) + // 2. https://github.com/peppy/osu-stable-reference/blob/dc0994645801010d4b628fff5ff79cd3c286ca83/osu!/Graphics/Textures/TextureManager.cs#L158-L196 (user skin textures lookup) + protected override bool AllowHighResolutionSprites => false; + /// /// Construct a new legacy beatmap skin instance. /// @@ -72,6 +77,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. + + LogLookupDebug(this, lookup, LookupDebugType.Miss); return null; } diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index f785022f84..9c06cbbfb5 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,14 @@ 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 HealthChanged(bool increase) + { + if (increase) + marker.Bulge(); + base.HealthChanged(increase); + } + + protected override void Flash() => marker.Flash(Current.Value >= epic_cutoff); private static Texture getTexture(ISkin skin, string name) => skin?.GetTexture($"scorebar-{name}"); @@ -113,19 +120,16 @@ namespace osu.Game.Skinning Origin = Anchor.Centre, }; - protected override void LoadComplete() + protected override void Update() { - base.LoadComplete(); + base.Update(); - Current.BindValueChanged(hp => - { - if (hp.NewValue < 0.2f) - Main.Texture = superDangerTexture; - else if (hp.NewValue < epic_cutoff) - Main.Texture = dangerTexture; - else - Main.Texture = normalTexture; - }); + if (Current.Value < 0.2f) + Main.Texture = superDangerTexture; + else if (Current.Value < epic_cutoff) + Main.Texture = dangerTexture; + else + Main.Texture = normalTexture; } } @@ -226,37 +230,30 @@ namespace osu.Game.Skinning public abstract Sprite CreateSprite(); - protected override void LoadComplete() + public override void Flash(bool isEpic) { - base.LoadComplete(); - - Current.BindValueChanged(val => - { - if (val.NewValue > val.OldValue) - bulgeMain(); - }); - } - - public override void Flash(JudgementResult result) - { - bulgeMain(); - - bool isEpic = Current.Value >= epic_cutoff; - + Bulge(); explode.Blending = isEpic ? BlendingParameters.Additive : BlendingParameters.Inherit; explode.ScaleTo(1).Then().ScaleTo(isEpic ? 2 : 1.6f, 120); explode.FadeOutFromOne(120); } - private void bulgeMain() => + public override void Bulge() + { + base.Bulge(); Main.ScaleTo(1.4f).Then().ScaleTo(1, 200, Easing.Out); + } } public partial class LegacyHealthPiece : CompositeDrawable { public Bindable Current { get; } = new Bindable(); - public virtual void Flash(JudgementResult result) + public virtual void Bulge() + { + } + + public virtual void Flash(bool isEpic) { } } diff --git a/osu.Game/Skinning/LegacyJudgementPieceNew.cs b/osu.Game/Skinning/LegacyJudgementPieceNew.cs index 9b1ff9b22f..5ff28726c0 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceNew.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceNew.cs @@ -50,7 +50,7 @@ namespace osu.Game.Skinning }); } - if (result != HitResult.Miss) + if (!result.IsMiss()) { //new judgement shows old as a temporary effect AddInternal(temporaryOldStyle = new LegacyJudgementPieceOld(result, createMainDrawable, 1.05f, true) diff --git a/osu.Game/Skinning/LegacyJudgementPieceOld.cs b/osu.Game/Skinning/LegacyJudgementPieceOld.cs index 082d0e4a67..a9f68bd378 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceOld.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceOld.cs @@ -50,17 +50,30 @@ namespace osu.Game.Skinning // legacy judgements don't play any transforms if they are an animation.... UNLESS they are the temporary displayed judgement from new piece. if (animation?.FrameCount > 1 && !forceTransforms) - return; - - switch (result) { - case HitResult.Miss: + if (isMissedTick()) + applyMissedTickScaling(); + return; + } + + if (result.IsMiss()) + { + decimal? legacyVersion = skin.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value; + + // missed ticks / slider end don't get the normal animation. + if (isMissedTick()) + { + this.ScaleTo(1.2f); + this.ScaleTo(1f, 100, Easing.In); + + this.FadeOutFromOne(400); + } + else + { this.ScaleTo(1.6f); this.ScaleTo(1, 100, Easing.In); - decimal? legacyVersion = skin.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value; - - if (legacyVersion >= 2.0m) + if (legacyVersion > 1.0m) { this.MoveTo(new Vector2(0, -5)); this.MoveToOffset(new Vector2(0, 80), fade_out_delay + fade_out_length, Easing.In); @@ -71,21 +84,28 @@ namespace osu.Game.Skinning this.RotateTo(0); this.RotateTo(rotation, fade_in_length) .Then().RotateTo(rotation * 2, fade_out_delay + fade_out_length - fade_in_length, Easing.In); - break; - - default: - - this.ScaleTo(0.6f).Then() - .ScaleTo(1.1f, fade_in_length * 0.8f).Then() // t = 0.8 - .Delay(fade_in_length * 0.2f) // t = 1.0 - .ScaleTo(0.9f, fade_in_length * 0.2f).Then() // t = 1.2 - - // stable dictates scale of 0.9->1 over time 1.0 to 1.4, but we are already at 1.2. - // so we need to force the current value to be correct at 1.2 (0.95) then complete the - // second half of the transform. - .ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f); // t = 1.4 - break; + } } + else + { + this.ScaleTo(0.6f).Then() + .ScaleTo(1.1f, fade_in_length * 0.8f).Then() // t = 0.8 + .Delay(fade_in_length * 0.2f) // t = 1.0 + .ScaleTo(0.9f, fade_in_length * 0.2f).Then() // t = 1.2 + + // stable dictates scale of 0.9->1 over time 1.0 to 1.4, but we are already at 1.2. + // so we need to force the current value to be correct at 1.2 (0.95) then complete the + // second half of the transform. + .ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f); // t = 1.4 + } + } + + private bool isMissedTick() => result.IsMiss() && result != HitResult.Miss; + + private void applyMissedTickScaling() + { + this.ScaleTo(0.6f); + this.ScaleTo(0.3f, 100, Easing.In); } public Drawable GetAboveHitObjectsProxiedContent() => CreateProxy(); diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index f1c99a315d..042836984a 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -21,6 +21,8 @@ namespace osu.Game.Skinning /// public const float DEFAULT_COLUMN_SIZE = 30 * POSITION_SCALE_FACTOR; + public const float DEFAULT_HIT_POSITION = (480 - 402) * POSITION_SCALE_FACTOR; + public readonly int Keys; public Dictionary CustomColours { get; } = new Dictionary(); @@ -35,11 +37,12 @@ namespace osu.Game.Skinning public readonly float[] ExplosionWidth; public readonly float[] HoldNoteLightWidth; - public float HitPosition = (480 - 402) * POSITION_SCALE_FACTOR; + public float HitPosition = DEFAULT_HIT_POSITION; public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR; 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..ff6e7fc38e 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; @@ -150,7 +155,15 @@ namespace osu.Game.Skinning if (i >= output.Length) break; - output[i] = float.Parse(values[i], CultureInfo.InvariantCulture) * (applyScaleFactor ? LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR : 1); + if (!float.TryParse(values[i], NumberStyles.Float, CultureInfo.InvariantCulture, out float parsedValue)) + // some skins may provide incorrect entries in array values. to match stable behaviour, read such entries as zero. + // see: https://github.com/ppy/osu/issues/26464, stable code: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Graphics/Skinning/Components/Section.cs#L134-L137 + parsedValue = 0; + + if (applyScaleFactor) + parsedValue *= LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; + + output[i] = parsedValue; } } } diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index d238369be1..a86f122836 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -28,6 +28,7 @@ namespace osu.Game.Skinning { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, + FixedWidth = true, }; } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index cf51fa8f74..cfa5f972d2 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.IO; using System.Linq; using JetBrains.Annotations; @@ -15,7 +17,6 @@ using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Game.Audio; using osu.Game.Beatmaps.Formats; -using osu.Game.Database; using osu.Game.Extensions; using osu.Game.IO; using osu.Game.Rulesets.Objects.Types; @@ -29,13 +30,7 @@ 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). @@ -56,15 +51,11 @@ namespace osu.Game.Skinning /// /// The model for this skin. /// Access to raw game resources. - /// An optional store which will be used for looking up skin resources. If null, one will be created from realm pattern. + /// An optional fallback store which will be used for file lookups that are not serviced by realm user storage. /// The user-facing filename of the configuration file to be parsed. Can accept an .osu or skin.ini file. - protected LegacySkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? storage, string configurationFilename = @"skin.ini") - : base(skin, resources, storage, configurationFilename) + protected LegacySkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? fallbackStore, string configurationFilename = @"skin.ini") + : base(skin, resources, fallbackStore, 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) @@ -82,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) @@ -271,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; @@ -326,7 +331,7 @@ namespace osu.Game.Skinning var bindable = new Bindable(); if (val != null) - bindable.Parse(val); + bindable.Parse(val, CultureInfo.InvariantCulture); return bindable; } } @@ -453,6 +458,12 @@ namespace osu.Game.Skinning case HitResult.Miss: return this.GetAnimation("hit0", true, false); + case HitResult.LargeTickMiss: + return this.GetAnimation("slidertickmiss", true, false); + + case HitResult.IgnoreMiss: + return this.GetAnimation("sliderendmiss", true, false); + case HitResult.Meh: return this.GetAnimation("hit50", true, false); @@ -466,6 +477,11 @@ namespace osu.Game.Skinning return null; } + /// + /// Whether high-resolution textures ("@2x"-suffixed) are allowed to be used by when available. + /// + protected virtual bool AllowHighResolutionSprites => true; + public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) { switch (componentName) @@ -475,32 +491,30 @@ namespace osu.Game.Skinning break; } - foreach (string name in getFallbackNames(componentName)) + Texture? texture = null; + float ratio = 1; + + if (AllowHighResolutionSprites) { // some component names (especially user-controlled ones, like `HitX` in mania) // may contain `@2x` scale specifications. // stable happens to check for that and strip them, so do the same to match stable behaviour. - string lookupName = name.Replace(@"@2x", string.Empty); + componentName = componentName.Replace(@"@2x", string.Empty); - float ratio = 2; - string twoTimesFilename = $"{Path.ChangeExtension(lookupName, null)}@2x{Path.GetExtension(lookupName)}"; + string twoTimesFilename = $"{Path.ChangeExtension(componentName, null)}@2x{Path.GetExtension(componentName)}"; - var texture = Textures?.Get(twoTimesFilename, wrapModeS, wrapModeT); + texture = Textures?.Get(twoTimesFilename, wrapModeS, wrapModeT); - if (texture == null) - { - ratio = 1; - texture = Textures?.Get(lookupName, wrapModeS, wrapModeT); - } - - if (texture == null) - continue; - - texture.ScaleAdjust = ratio; - return texture; + if (texture != null) + ratio = 2; } - return null; + texture ??= Textures?.Get(componentName, wrapModeS, wrapModeT); + + if (texture != null) + texture.ScaleAdjust = ratio; + + return texture; } public override ISample? GetSample(ISampleInfo sampleInfo) @@ -511,7 +525,7 @@ namespace osu.Game.Skinning lookupNames = getLegacyLookupNames(hitSample); else { - lookupNames = sampleInfo.LookupNames.SelectMany(getFallbackNames); + lookupNames = sampleInfo.LookupNames.SelectMany(getFallbackSampleNames); } foreach (string lookup in lookupNames) @@ -529,7 +543,7 @@ namespace osu.Game.Skinning private IEnumerable getLegacyLookupNames(HitSampleInfo hitSample) { - var lookupNames = hitSample.LookupNames.SelectMany(getFallbackNames); + var lookupNames = hitSample.LookupNames.SelectMany(getFallbackSampleNames); if (!UseCustomSampleBanks && !string.IsNullOrEmpty(hitSample.Suffix)) { @@ -548,13 +562,13 @@ namespace osu.Game.Skinning yield return hitSample.Name; } - private IEnumerable getFallbackNames(string componentName) + private IEnumerable getFallbackSampleNames(string name) { - // May be something like "Gameplay/osu/approachcircle" from lazer, or "Arrows/note1" from a user skin. - yield return componentName; + // May be something like "Gameplay/normal-hitnormal" from lazer. + yield return name; - // Fall back to using the last piece for components coming from lazer (e.g. "Gameplay/osu/approachcircle" -> "approachcircle"). - yield return componentName.Split('/').Last(); + // Fall back to using the last piece for components coming from lazer (e.g. "Gameplay/normal-hitnormal" -> "normal-hitnormal"). + yield return name.Split('/').Last(); } } } diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 0d2461567f..a8ec67d98b 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,29 @@ 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; + + // Importantly, check per-axis for the minimum dimension to avoid accidentally inflating + // textures with weird aspect ratios. + float newWidth = Math.Min(texture.Width, maxSize.X); + float newHeight = Math.Min(texture.Height, maxSize.Y); + + var croppedTexture = texture.Crop(new RectangleF( + texture.Width / 2f - newWidth / 2f, + texture.Height / 2f - newHeight / 2f, + newWidth, + newHeight + )); + + 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 +211,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 +229,7 @@ namespace osu.Game.Skinning return 1000f / textures.Length; } - return default_frame_time; + return SIXTY_FRAME_TIME; } } } diff --git a/osu.Game/Skinning/LegacySongProgress.cs b/osu.Game/Skinning/LegacySongProgress.cs index 22aea99291..4295060a3a 100644 --- a/osu.Game/Skinning/LegacySongProgress.cs +++ b/osu.Game/Skinning/LegacySongProgress.cs @@ -19,11 +19,16 @@ namespace osu.Game.Skinning public override bool HandleNonPositionalInput => false; public override bool HandlePositionalInput => false; + public LegacySongProgress() + { + // User shouldn't be able to adjust width/height of this as `CircularProgress` doesn't + // handle stretched cases well. + AutoSizeAxes = Axes.Both; + } + [BackgroundDependencyLoader] private void load() { - Size = new Vector2(33); - InternalChildren = new Drawable[] { new Container @@ -39,7 +44,7 @@ namespace osu.Game.Skinning }, new CircularContainer { - RelativeSizeAxes = Axes.Both, + Size = new Vector2(33), Masking = true, BorderColour = Colour4.White, BorderThickness = 2, diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index d6af52855b..fdd8716d5a 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.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.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; @@ -12,6 +14,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 +25,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 +45,17 @@ namespace osu.Game.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin) { - Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: true); + string fontPrefix = skin.GetFontPrefix(font); + base.Font = new FontUsage(fontPrefix, 1, fixedWidth: FixedWidth); Spacing = new Vector2(-skin.GetFontOverlap(font), 0); - glyphStore = new LegacyGlyphStore(skin); + glyphStore = new LegacyGlyphStore(fontPrefix, skin, MaxSizePerGlyph); + + // cache common lookups ahead of time. + foreach (char c in FixedWidthExcludeCharacters) + glyphStore.Get(fontPrefix, c); + for (int i = 0; i < 10; i++) + glyphStore.Get(fontPrefix, (char)('0' + i)); } protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore); @@ -41,22 +63,44 @@ namespace osu.Game.Skinning private class LegacyGlyphStore : ITexturedGlyphLookupStore { private readonly ISkin skin; + private readonly Vector2? maxSize; - public LegacyGlyphStore(ISkin skin) + private readonly string fontName; + + private readonly Dictionary cache = new Dictionary(); + + public LegacyGlyphStore(string fontName, ISkin skin, Vector2? maxSize) { + this.fontName = fontName; this.skin = skin; + this.maxSize = maxSize; } - public ITexturedCharacterGlyph? Get(string fontName, char character) + public ITexturedCharacterGlyph? Get(string? fontName, char character) { + // We only service one font. + if (fontName != this.fontName) + return null; + + if (cache.TryGetValue(character, out var cached)) + return cached; + string lookup = getLookupName(character); var texture = skin.GetTexture($"{fontName}-{lookup}"); - if (texture == null) - return null; + TexturedCharacterGlyph? glyph = null; - return new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null), texture, 1f / texture.ScaleAdjust); + if (texture != null) + { + if (maxSize != null) + texture = texture.WithMaximumSize(maxSize.Value); + + glyph = new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null), texture, 1f / texture.ScaleAdjust); + } + + cache[character] = glyph; + return glyph; } private static string getLookupName(char character) 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/SerialisableDrawableExtensions.cs b/osu.Game/Skinning/SerialisableDrawableExtensions.cs index 51b57a000d..97c4cc8f73 100644 --- a/osu.Game/Skinning/SerialisableDrawableExtensions.cs +++ b/osu.Game/Skinning/SerialisableDrawableExtensions.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; @@ -18,6 +19,10 @@ namespace osu.Game.Skinning // todo: can probably make this better via deserialisation directly using a common interface. component.Position = drawableInfo.Position; component.Rotation = drawableInfo.Rotation; + if (drawableInfo.Width is float width && width != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.X) != true) + component.Width = width; + if (drawableInfo.Height is float height && height != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.Y) != true) + component.Height = height; component.Scale = drawableInfo.Scale; component.Anchor = drawableInfo.Anchor; component.Origin = drawableInfo.Origin; diff --git a/osu.Game/Skinning/SerialisedDrawableInfo.cs b/osu.Game/Skinning/SerialisedDrawableInfo.cs index c515f228f7..2d6113ff70 100644 --- a/osu.Game/Skinning/SerialisedDrawableInfo.cs +++ b/osu.Game/Skinning/SerialisedDrawableInfo.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using osu.Framework.Bindables; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; @@ -35,6 +36,10 @@ namespace osu.Game.Skinning public Vector2 Scale { get; set; } + public float? Width { get; set; } + + public float? Height { get; set; } + public Anchor Anchor { get; set; } public Anchor Origin { get; set; } @@ -62,6 +67,13 @@ namespace osu.Game.Skinning Position = component.Position; Rotation = component.Rotation; Scale = component.Scale; + + if ((component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.X) != true) + Width = component.Width; + + if ((component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.Y) != true) + Height = component.Height; + Anchor = component.Anchor; Origin = component.Origin; diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index a6250d7488..9ee69d033d 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; @@ -52,24 +55,28 @@ namespace osu.Game.Skinning where TLookup : notnull where TValue : notnull; - private readonly RealmBackedResourceStore? realmBackedStorage; + private readonly ResourceStore store = new ResourceStore(); + + public string Name { get; } /// /// Construct a new skin. /// /// The skin's metadata. Usually a live realm object. /// Access to game-wide resources. - /// An optional store which will *replace* all file lookups that are usually sourced from . + /// An optional fallback store which will be used for file lookups that are not serviced by realm user storage. /// 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") + protected Skin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? fallbackStore = null, string configurationFilename = @"skin.ini") { + Name = skin.Name; + if (resources != null) { SkinInfo = skin.ToLive(resources.RealmAccess); - storage ??= realmBackedStorage = new RealmBackedResourceStore(SkinInfo, resources.Files, resources.RealmAccess); + store.AddStore(new RealmBackedResourceStore(SkinInfo, resources.Files, resources.RealmAccess)); - var samples = resources.AudioManager?.GetSampleStore(storage); + var samples = resources.AudioManager?.GetSampleStore(store); if (samples != null) { @@ -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, store)); } else { @@ -89,7 +96,10 @@ namespace osu.Game.Skinning SkinInfo = skin.ToLiveUnmanaged(); } - var configurationStream = storage?.GetStream(configurationFilename); + if (fallbackStore != null) + store.AddStore(fallbackStore); + + var configurationStream = store.GetStream(configurationFilename); if (configurationStream != null) { @@ -98,14 +108,21 @@ 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()) { string filename = $"{skinnableTarget}.json"; - byte[]? bytes = storage?.Get(filename); + byte[]? bytes = store?.Get(filename); if (bytes == null) continue; @@ -157,6 +174,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 +210,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: @@ -235,9 +255,54 @@ namespace osu.Game.Skinning Textures?.Dispose(); Samples?.Dispose(); - realmBackedStorage?.Dispose(); + store.Dispose(); } #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 f2103a45c4..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(); } 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..f153f4f8d3 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); } @@ -188,9 +194,33 @@ namespace osu.Game.Skinning /// /// Whether any samples are currently playing. /// - public bool IsPlaying => samplesContainer.Any(s => s.Playing); + public bool IsPlaying + { + get + { + foreach (PoolableSkinnableSample s in samplesContainer) + { + if (s.Playing) + return true; + } - public bool IsPlayed => samplesContainer.Any(s => s.Played); + return false; + } + } + + public bool IsPlayed + { + get + { + foreach (PoolableSkinnableSample s in samplesContainer) + { + if (s.Played) + return true; + } + + return false; + } + } public IBindable AggregateVolume => samplesContainer.AggregateVolume; 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 a68a7fd5b9..a2dca5d333 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -190,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/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 82c01ea6a1..fae9ec7f2e 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); } @@ -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.ChangeExtension(Animation.Path, null), 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 38e7ff1c70..40842fe7ed 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.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.Threading; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -30,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/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index ec0cb7ca19..ec875219b6 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -74,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; @@ -84,24 +90,28 @@ namespace osu.Game.Storyboards.Drawables LifetimeEnd = sprite.EndTimeForDisplay; } - [Resolved] - private ISkinSource skin { get; set; } = null!; - [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) { 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/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/StoryboardVideo.cs b/osu.Game/Storyboards/StoryboardVideo.cs index 4652e45852..8c11e19a06 100644 --- a/osu.Game/Storyboards/StoryboardVideo.cs +++ b/osu.Game/Storyboards/StoryboardVideo.cs @@ -14,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/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index bb4e06654a..1f491be7e3 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -132,8 +132,8 @@ namespace osu.Game.Tests.Beatmaps public AudioManager AudioManager => Audio; public IResourceStore Files => userSkinResourceStore; public new IResourceStore Resources => base.Resources; - public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null; - RealmAccess IStorageResourceProvider.RealmAccess => null; + public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null!; + RealmAccess IStorageResourceProvider.RealmAccess => null!; #endregion diff --git a/osu.Game/Tests/CleanRunHeadlessGameHost.cs b/osu.Game/Tests/CleanRunHeadlessGameHost.cs index f3c69201e2..00e5b38b1a 100644 --- a/osu.Game/Tests/CleanRunHeadlessGameHost.cs +++ b/osu.Game/Tests/CleanRunHeadlessGameHost.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests [CallerMemberName] string callingMethodName = @"") : base($"{callingMethodName}-{Guid.NewGuid()}", new HostOptions { - BindIPC = bindIPC, + IPCPort = bindIPC ? OsuGame.IPC_PORT : null, }, bypassCleanup: bypassCleanupOnDispose, realtime: realtime) { this.bypassCleanupOnSetup = bypassCleanupOnSetup; diff --git a/osu.Game/Tests/OsuTestBrowser.cs b/osu.Game/Tests/OsuTestBrowser.cs index 0bc51a0c1e..477ffb2908 100644 --- a/osu.Game/Tests/OsuTestBrowser.cs +++ b/osu.Game/Tests/OsuTestBrowser.cs @@ -1,12 +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.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 { @@ -16,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/Gameplay/ScoringTestScene.cs b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs new file mode 100644 index 0000000000..e7053e4202 --- /dev/null +++ b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs @@ -0,0 +1,670 @@ +// 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.Graphics.UserInterfaceV2; +using osu.Game.Overlays; +using osu.Game.Overlays.Mods; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +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(IReadOnlyList selectedMods); + protected abstract IScoringAlgorithm CreateScoreV2(int maxCombo, IReadOnlyList selectedMods); + protected abstract ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode, IReadOnlyList mods); + + 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); + + private RoundedButton changeModsButton = null!; + private OsuSpriteText modsText = null!; + private TestModSelectOverlay modSelect = null!; + + [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), + 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 Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 20 }, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Selected mods", + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10), + Children = new Drawable[] + { + changeModsButton = new RoundedButton + { + Text = "Change", + Width = 100, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }, + modsText = new OsuSpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }, + } + } + } + } + }, + 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 } + } + } + }, + }, + } + }, + modSelect = new TestModSelectOverlay + { + RelativeSizeAxes = Axes.Both, + SelectedMods = { BindTarget = SelectedMods } + } + }; + + 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); + + changeModsButton.Action = () => modSelect.Show(); + SelectedMods.BindValueChanged(mods => Rerun()); + + Rerun(); + }); + } + + protected void Rerun() + { + graphs.Clear(); + legend.Clear(); + + modsText.Text = SelectedMods.Value.Any() + ? string.Join(", ", SelectedMods.Value.Select(mod => mod.Acronym)) + : "(none)"; + + 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(SelectedMods.Value), + Visible = scoreV1Visible + }); + runForAlgorithm(new ScoringAlgorithmInfo + { + Name = "ScoreV2", + Colour = colours.Red1, + Algorithm = CreateScoreV2(sliderMaxCombo.Current.Value, SelectedMods.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, SelectedMods.Value); + + 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, IReadOnlyList selectedMods) + { + this.mode = mode; + scoreProcessor = CreateScoreProcessor(); + scoreProcessor.ApplyBeatmap(beatmap); + scoreProcessor.Mods.Value = selectedMods; + } + + 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; + } + } + + private partial class TestModSelectOverlay : UserModSelectOverlay + { + protected override bool ShowModEffects => true; + protected override bool ShowPresets => false; + + public TestModSelectOverlay() + : base(OverlayColourScheme.Aquamarine) + { + } + } + } +} diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs new file mode 100644 index 0000000000..16cbf879df --- /dev/null +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -0,0 +1,81 @@ +// 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.Bindables; +using osu.Game.Online.API; +using osu.Game.Online.Metadata; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Metadata +{ + public partial class TestMetadataClient : MetadataClient + { + public override IBindable IsConnected => new BindableBool(true); + + public override IBindable IsWatchingUserPresence => isWatchingUserPresence; + private readonly BindableBool isWatchingUserPresence = new BindableBool(); + + public override IBindableDictionary UserStates => userStates; + private readonly BindableDictionary userStates = new BindableDictionary(); + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + public override Task BeginWatchingUserPresence() + { + isWatchingUserPresence.Value = true; + return Task.CompletedTask; + } + + public override Task EndWatchingUserPresence() + { + isWatchingUserPresence.Value = false; + return Task.CompletedTask; + } + + public override Task UpdateActivity(UserActivity? activity) + { + if (isWatchingUserPresence.Value) + { + userStates.TryGetValue(api.LocalUser.Value.Id, out var localUserPresence); + localUserPresence = localUserPresence with { Activity = activity }; + userStates[api.LocalUser.Value.Id] = localUserPresence; + } + + return Task.CompletedTask; + } + + public override Task UpdateStatus(UserStatus? status) + { + if (isWatchingUserPresence.Value) + { + userStates.TryGetValue(api.LocalUser.Value.Id, out var localUserPresence); + localUserPresence = localUserPresence with { Status = status }; + userStates[api.LocalUser.Value.Id] = localUserPresence; + } + + return Task.CompletedTask; + } + + public override Task UserPresenceUpdated(int userId, UserPresence? presence) + { + if (isWatchingUserPresence.Value) + { + if (presence.HasValue) + userStates[userId] = presence.Value; + else + userStates.Remove(userId); + } + + return Task.CompletedTask; + } + + public override Task GetChangesSince(int queueId) + => Task.FromResult(new BeatmapUpdates(Array.Empty(), queueId)); + + public override Task BeatmapSetsUpdated(BeatmapUpdates updates) => Task.CompletedTask; + } +} diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModFailConditionTestScene.cs similarity index 72% rename from osu.Game/Tests/Visual/ModPerfectTestScene.cs rename to osu.Game/Tests/Visual/ModFailConditionTestScene.cs index 164faa16aa..8f0dff055d 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModFailConditionTestScene.cs @@ -8,11 +8,11 @@ using osu.Game.Rulesets.Objects; namespace osu.Game.Tests.Visual { - public abstract partial class ModPerfectTestScene : ModTestScene + public abstract partial class ModFailConditionTestScene : ModTestScene { - private readonly ModPerfect mod; + private readonly ModFailCondition mod; - protected ModPerfectTestScene(ModPerfect mod) + protected ModFailConditionTestScene(ModFailCondition mod) { this.mod = mod; } @@ -26,15 +26,15 @@ namespace osu.Game.Tests.Visual HitObjects = { testData.HitObject } }, Autoplay = !shouldMiss, - PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(shouldMiss && testData.FailOnMiss) + PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(shouldMiss && testData.FailOnMiss) }); - protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new PerfectModTestPlayer(); + protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new ModFailConditionTestPlayer(CurrentTestData, AllowFail); - private partial class PerfectModTestPlayer : TestPlayer + protected partial class ModFailConditionTestPlayer : ModTestPlayer { - public PerfectModTestPlayer() - : base(showResults: false) + public ModFailConditionTestPlayer(ModTestData data, bool allowFail) + : base(data, allowFail) { } diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index aa5b506343..c2ebcdefac 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -20,35 +20,35 @@ namespace osu.Game.Tests.Visual { protected sealed override bool HasCustomSteps => true; - private ModTestData currentTestData; + protected ModTestData CurrentTestData { get; private set; } protected void CreateModTest(ModTestData testData) => CreateTest(() => { - AddStep("set test data", () => currentTestData = testData); + AddStep("set test data", () => CurrentTestData = testData); }); public override void TearDownSteps() { AddUntilStep("test passed", () => { - if (currentTestData == null) + if (CurrentTestData == null) return true; - return currentTestData.PassCondition?.Invoke() ?? false; + return CurrentTestData.PassCondition?.Invoke() ?? false; }); base.TearDownSteps(); } - protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestData?.Beatmap ?? base.CreateBeatmap(ruleset); + protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => CurrentTestData?.Beatmap ?? base.CreateBeatmap(ruleset); protected sealed override TestPlayer CreatePlayer(Ruleset ruleset) { var mods = new List(SelectedMods.Value); - if (currentTestData.Mods != null) - mods.AddRange(currentTestData.Mods); - if (currentTestData.Autoplay) + if (CurrentTestData.Mods != null) + mods.AddRange(CurrentTestData.Mods); + if (CurrentTestData.Autoplay) mods.Add(ruleset.GetAutoplayMod()); SelectedMods.Value = mods; @@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual return CreateModPlayer(ruleset); } - protected virtual TestPlayer CreateModPlayer(Ruleset ruleset) => new ModTestPlayer(currentTestData, AllowFail); + protected virtual TestPlayer CreateModPlayer(Ruleset ruleset) => new ModTestPlayer(CurrentTestData, AllowFail); protected partial class ModTestPlayer : TestPlayer { diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index c27e30d5bb..4c3deac1d7 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); @@ -391,6 +396,12 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } + public override async Task AbortMatch() + { + ChangeUserState(api.LocalUser.Value.Id, MultiplayerUserState.Idle); + await ((IMultiplayerClient)this).GameplayAborted(GameplayAbortReason.HostAbortedTheMatch).ConfigureAwait(false); + } + public async Task AddUserPlaylistItem(int userId, MultiplayerPlaylistItem item) { Debug.Assert(ServerRoom != null); @@ -635,7 +646,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); } @@ -653,5 +664,11 @@ namespace osu.Game.Tests.Visual.Multiplayer PlayedAt = item.PlayedAt, StarRating = item.Beatmap.StarRating, }; + + public override Task DisconnectInternal() + { + isConnected.Value = false; + return Task.CompletedTask; + } } } diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index 94be4a375d..947305439e 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -58,6 +58,12 @@ namespace osu.Game.Tests.Visual [SetUpSteps] public virtual void SetUpSteps() + { + CreateNewGame(); + ConfirmAtMainMenu(); + } + + protected void CreateNewGame() { AddStep("Create new game instance", () => { @@ -71,8 +77,6 @@ namespace osu.Game.Tests.Visual AddUntilStep("Wait for load", () => Game.IsLoaded); AddUntilStep("Wait for intro", () => Game.ScreenStack.CurrentScreen is IntroScreen); - - ConfirmAtMainMenu(); } [TearDownSteps] diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs index 37260b3b13..ffe40243ab 100644 --- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs @@ -57,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[] { 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/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/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index aab1b72990..f371cf721f 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -201,8 +201,8 @@ namespace osu.Game.Tests.Visual { private readonly bool extrapolateAnimations; - public TestLegacySkin(SkinInfo skin, IResourceStore storage, IStorageResourceProvider resources, bool extrapolateAnimations) - : base(skin, resources, storage) + public TestLegacySkin(SkinInfo skin, IResourceStore fallbackStore, IStorageResourceProvider resources, bool extrapolateAnimations) + : base(skin, resources, fallbackStore) { this.extrapolateAnimations = extrapolateAnimations; } diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 305a615102..5aef85fa13 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -33,7 +33,8 @@ namespace osu.Game.Tests.Visual.Spectator public int FrameSendAttempts { get; private set; } - public override IBindable IsConnected { get; } = new Bindable(true); + public override IBindable IsConnected => isConnected; + private readonly BindableBool isConnected = new BindableBool(true); public IReadOnlyDictionary LastReceivedUserFrames => lastReceivedUserFrames; @@ -124,7 +125,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(); @@ -174,5 +180,11 @@ namespace osu.Game.Tests.Visual.Spectator State = SpectatedUserState.Playing }); } + + protected override async Task DisconnectInternal() + { + await base.DisconnectInternal().ConfigureAwait(false); + isConnected.Value = false; + } } } diff --git a/osu.Game/Tests/VisualTestRunner.cs b/osu.Game/Tests/VisualTestRunner.cs index e04c71d193..1a9e03b2a4 100644 --- a/osu.Game/Tests/VisualTestRunner.cs +++ b/osu.Game/Tests/VisualTestRunner.cs @@ -12,7 +12,7 @@ namespace osu.Game.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu-development", new HostOptions { BindIPC = true, })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu-development")) { host.Run(new OsuTestBrowser()); return 0; 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/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index e74ffc9d54..26622a1f30 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -1,39 +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 System; +using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Input.Events; -using osu.Framework.Localisation; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; +using osu.Game.Localisation; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osuTK; namespace osu.Game.Users.Drawables { - public partial class ClickableAvatar : OsuClickableContainer + public partial class ClickableAvatar : OsuClickableContainer, IHasCustomTooltip { - private const string default_tooltip_text = "view profile"; + public ITooltip GetCustomTooltip() => showCardOnHover ? new UserCardTooltip() : new NoCardTooltip(); - public override LocalisableString TooltipText - { - get - { - if (!Enabled.Value) - return string.Empty; - - return ShowUsernameTooltip ? (user?.Username ?? string.Empty) : default_tooltip_text; - } - set => throw new NotSupportedException(); - } - - /// - /// By default, the tooltip will show "view profile" as avatars are usually displayed next to a username. - /// Setting this to true exposes the username via tooltip for special cases where this is not true. - /// - public bool ShowUsernameTooltip { get; set; } + public APIUser? TooltipContent { get; } private readonly APIUser? user; + private readonly bool showCardOnHover; + [Resolved] private OsuGame? game { get; set; } @@ -41,12 +33,15 @@ namespace osu.Game.Users.Drawables /// A clickable avatar for the specified user, with UI sounds included. /// /// The user. A null value will get a placeholder avatar. - public ClickableAvatar(APIUser? user = null) + /// If set to true, the will be shown for the tooltip + public ClickableAvatar(APIUser? user = null, bool showCardOnHover = false) { - this.user = user; - if (user?.Id != APIUser.SYSTEM_USER_ID) Action = openProfile; + + this.showCardOnHover = showCardOnHover; + + TooltipContent = this.user = user ?? new GuestUser(); } [BackgroundDependencyLoader] @@ -68,5 +63,65 @@ namespace osu.Game.Users.Drawables return base.OnClick(e); } + + public partial class UserCardTooltip : VisibilityContainer, ITooltip + { + public UserCardTooltip() + { + AutoSizeAxes = Axes.Both; + } + + protected override void PopIn() => this.FadeIn(150, Easing.OutQuint); + protected override void PopOut() => this.Delay(150).FadeOut(500, Easing.OutQuint); + + public void Move(Vector2 pos) => Position = pos; + + private APIUser? user; + + public void SetContent(APIUser? content) + { + if (content == user && Children.Any()) + return; + + user = content; + + if (user != null) + { + LoadComponentAsync(new UserGridPanel(user) + { + Width = 300, + }, panel => Child = panel); + } + else + { + var tooltip = new OsuTooltipContainer.OsuTooltip(); + tooltip.SetContent(ContextMenuStrings.ViewProfile); + tooltip.Show(); + + Child = tooltip; + } + } + } + + public partial class NoCardTooltip : VisibilityContainer, ITooltip + { + private readonly OsuTooltipContainer.OsuTooltip tooltip; + + public NoCardTooltip() + { + tooltip = new OsuTooltipContainer.OsuTooltip(); + tooltip.SetContent(ContextMenuStrings.ViewProfile); + Child = tooltip; + } + + protected override void PopIn() => tooltip.Show(); + protected override void PopOut() => tooltip.Hide(); + + public void Move(Vector2 pos) => Position = pos; + + public void SetContent(APIUser? content) + { + } + } } } diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index c659685807..21153ecfc3 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs @@ -46,21 +46,24 @@ namespace osu.Game.Users.Drawables protected override double LoadDelay => 200; private readonly bool isInteractive; - private readonly bool showUsernameTooltip; private readonly bool showGuestOnNull; + private readonly bool showUserPanelOnHover; /// /// Construct a new UpdateableAvatar. /// /// The initial user to display. /// If set to true, hover/click sounds will play and clicking the avatar will open the user's profile. - /// Whether to show the username rather than "view profile" on the tooltip. (note: this only applies if is also true) + /// + /// If set to true, the user status panel will be displayed in the tooltip. + /// Only has an effect if is true. + /// /// Whether to show a default guest representation on null user (as opposed to nothing). - public UpdateableAvatar(APIUser? user = null, bool isInteractive = true, bool showUsernameTooltip = false, bool showGuestOnNull = true) + public UpdateableAvatar(APIUser? user = null, bool isInteractive = true, bool showUserPanelOnHover = false, bool showGuestOnNull = true) { this.isInteractive = isInteractive; - this.showUsernameTooltip = showUsernameTooltip; this.showGuestOnNull = showGuestOnNull; + this.showUserPanelOnHover = showUserPanelOnHover; User = user; } @@ -72,19 +75,16 @@ namespace osu.Game.Users.Drawables if (isInteractive) { - return new ClickableAvatar(user) + return new ClickableAvatar(user, showUserPanelOnHover) { - ShowUsernameTooltip = showUsernameTooltip, RelativeSizeAxes = Axes.Both, }; } - else + + return new DrawableAvatar(user) { - return new DrawableAvatar(user) - { - RelativeSizeAxes = Axes.Both, - }; - } + RelativeSizeAxes = Axes.Both, + }; } } } diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs index 3c1b68f9ef..e33fb7a44e 100644 --- a/osu.Game/Users/ExtendedUserPanel.cs +++ b/osu.Game/Users/ExtendedUserPanel.cs @@ -3,29 +3,31 @@ #nullable disable -using osuTK; 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.Cursor; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Users.Drawables; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Users { public abstract partial class ExtendedUserPanel : UserPanel { - public readonly Bindable Status = new Bindable(); + public readonly Bindable Status = new Bindable(); public readonly IBindable Activity = new Bindable(); protected TextFlowContainer LastVisitMessage { get; private set; } private StatusIcon statusIcon; - private OsuSpriteText statusMessage; + private StatusText statusMessage; protected ExtendedUserPanel(APIUser user) : base(user) @@ -50,14 +52,6 @@ namespace osu.Game.Users statusIcon.FinishTransforms(); } - protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar(User, false); - - protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.CountryCode) - { - Size = new Vector2(36, 26), - Action = Action, - }; - protected Container CreateStatusIcon() => statusIcon = new StatusIcon(); protected FillFlowContainer CreateStatusMessage(bool rightAlignedChildren) @@ -87,7 +81,7 @@ namespace osu.Game.Users } })); - statusContainer.Add(statusMessage = new OsuSpriteText + statusContainer.Add(statusMessage = new StatusText { Anchor = alignment, Origin = alignment, @@ -97,23 +91,25 @@ namespace osu.Game.Users return statusContainer; } - private void displayStatus(UserStatus status, UserActivity activity = null) + private void displayStatus(UserStatus? status, UserActivity activity = null) { if (status != null) { - LastVisitMessage.FadeTo(status is UserStatusOffline && User.LastVisit.HasValue ? 1 : 0); + LastVisitMessage.FadeTo(status == UserStatus.Offline && User.LastVisit.HasValue ? 1 : 0); // Set status message based on activity (if we have one) and status is not offline - if (activity != null && !(status is UserStatusOffline)) + if (activity != null && status != UserStatus.Offline) { statusMessage.Text = activity.GetStatus(); + statusMessage.TooltipText = activity.GetDetails(); statusIcon.FadeColour(activity.GetAppropriateColour(Colours), 500, Easing.OutQuint); return; } // Otherwise use only status - statusMessage.Text = status.Message; - statusIcon.FadeColour(status.GetAppropriateColour(Colours), 500, Easing.OutQuint); + statusMessage.Text = status.GetLocalisableDescription(); + statusMessage.TooltipText = string.Empty; + statusIcon.FadeColour(status.Value.GetAppropriateColour(Colours), 500, Easing.OutQuint); return; } @@ -121,11 +117,11 @@ namespace osu.Game.Users // Fallback to web status if local one is null if (User.IsOnline) { - Status.Value = new UserStatusOnline(); + Status.Value = UserStatus.Online; return; } - Status.Value = new UserStatusOffline(); + Status.Value = UserStatus.Offline; } protected override bool OnHover(HoverEvent e) @@ -139,5 +135,10 @@ namespace osu.Game.Users BorderThickness = 0; base.OnHoverLost(e); } + + private partial class StatusText : OsuSpriteText, IHasTooltip + { + public LocalisableString TooltipText { get; set; } + } } } diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index c82f642fdc..1b09666df6 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -1,8 +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 System; +using MessagePack; using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Online; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; @@ -10,43 +13,84 @@ using osuTK.Graphics; namespace osu.Game.Users { + /// + /// Base class for all structures describing the user's current activity. + /// + /// + /// Warning: keep specs consistent with + /// . + /// + [Serializable] + [MessagePackObject] + [Union(11, typeof(ChoosingBeatmap))] + [Union(12, typeof(InSoloGame))] + [Union(13, typeof(WatchingReplay))] + [Union(14, typeof(SpectatingUser))] + [Union(21, typeof(SearchingForLobby))] + [Union(22, typeof(InLobby))] + [Union(23, typeof(InMultiplayerGame))] + [Union(24, typeof(SpectatingMultiplayerGame))] + [Union(31, typeof(InPlaylistGame))] + [Union(41, typeof(EditingBeatmap))] + [Union(42, typeof(ModdingBeatmap))] + [Union(43, typeof(TestingBeatmap))] public abstract class UserActivity { public abstract string GetStatus(bool hideIdentifiableInformation = false); + public virtual string? GetDetails(bool hideIdentifiableInformation = false) => null; public virtual Color4 GetAppropriateColour(OsuColour colours) => colours.GreenDarker; - public class ModdingBeatmap : EditingBeatmap - { - public override string GetStatus(bool hideIdentifiableInformation = false) => "Modding a beatmap"; - public override Color4 GetAppropriateColour(OsuColour colours) => colours.PurpleDark; - - public ModdingBeatmap(IBeatmapInfo info) - : base(info) - { - } - } - + [MessagePackObject] public class ChoosingBeatmap : UserActivity { public override string GetStatus(bool hideIdentifiableInformation = false) => "Choosing a beatmap"; } + [MessagePackObject] public abstract class InGame : UserActivity { - public IBeatmapInfo BeatmapInfo { get; } + [Key(0)] + public int BeatmapID { get; set; } - public IRulesetInfo Ruleset { get; } + [Key(1)] + public string BeatmapDisplayTitle { get; set; } = string.Empty; + + [Key(2)] + public int RulesetID { get; set; } + + [Key(3)] + public string RulesetPlayingVerb { get; set; } = string.Empty; // TODO: i'm going with this for now, but this is wasteful protected InGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) { - BeatmapInfo = beatmapInfo; - Ruleset = ruleset; + BeatmapID = beatmapInfo.OnlineID; + BeatmapDisplayTitle = beatmapInfo.GetDisplayTitle(); + + RulesetID = ruleset.OnlineID; + RulesetPlayingVerb = ruleset.CreateInstance().PlayingVerb; } - public override string GetStatus(bool hideIdentifiableInformation = false) => Ruleset.CreateInstance().PlayingVerb; + [SerializationConstructor] + protected InGame() { } + + public override string GetStatus(bool hideIdentifiableInformation = false) => RulesetPlayingVerb; + public override string GetDetails(bool hideIdentifiableInformation = false) => BeatmapDisplayTitle; } + [MessagePackObject] + public class InSoloGame : InGame + { + public InSoloGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) + : base(beatmapInfo, ruleset) + { + } + + [SerializationConstructor] + public InSoloGame() { } + } + + [MessagePackObject] public class InMultiplayerGame : InGame { public InMultiplayerGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) @@ -54,9 +98,122 @@ namespace osu.Game.Users { } + [SerializationConstructor] + public InMultiplayerGame() + { + } + public override string GetStatus(bool hideIdentifiableInformation = false) => $@"{base.GetStatus(hideIdentifiableInformation)} with others"; } + [MessagePackObject] + public class InPlaylistGame : InGame + { + public InPlaylistGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) + : base(beatmapInfo, ruleset) + { + } + + [SerializationConstructor] + public InPlaylistGame() { } + } + + [MessagePackObject] + public class TestingBeatmap : InGame + { + public TestingBeatmap(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) + : base(beatmapInfo, ruleset) + { + } + + [SerializationConstructor] + public TestingBeatmap() { } + + public override string GetStatus(bool hideIdentifiableInformation = false) => "Testing a beatmap"; + } + + [MessagePackObject] + public class EditingBeatmap : UserActivity + { + [Key(0)] + public int BeatmapID { get; set; } + + [Key(1)] + public string BeatmapDisplayTitle { get; set; } = string.Empty; + + public EditingBeatmap(IBeatmapInfo info) + { + BeatmapID = info.OnlineID; + BeatmapDisplayTitle = info.GetDisplayTitle(); + } + + [SerializationConstructor] + public EditingBeatmap() { } + + public override string GetStatus(bool hideIdentifiableInformation = false) => @"Editing a beatmap"; + public override string GetDetails(bool hideIdentifiableInformation = false) => BeatmapDisplayTitle; + } + + [MessagePackObject] + public class ModdingBeatmap : EditingBeatmap + { + public ModdingBeatmap(IBeatmapInfo info) + : base(info) + { + } + + [SerializationConstructor] + public ModdingBeatmap() { } + + public override string GetStatus(bool hideIdentifiableInformation = false) => "Modding a beatmap"; + public override Color4 GetAppropriateColour(OsuColour colours) => colours.PurpleDark; + } + + [MessagePackObject] + public class WatchingReplay : UserActivity + { + [Key(0)] + public long ScoreID { get; set; } + + [Key(1)] + public string PlayerName { get; set; } = string.Empty; + + [Key(2)] + public int BeatmapID { get; set; } + + [Key(3)] + public string? BeatmapDisplayTitle { get; set; } + + public WatchingReplay(ScoreInfo score) + { + ScoreID = score.OnlineID; + PlayerName = score.User.Username; + BeatmapID = score.BeatmapInfo?.OnlineID ?? -1; + BeatmapDisplayTitle = score.BeatmapInfo?.GetDisplayTitle(); + } + + [SerializationConstructor] + public WatchingReplay() { } + + public override string GetStatus(bool hideIdentifiableInformation = false) => hideIdentifiableInformation ? @"Watching a replay" : $@"Watching {PlayerName}'s replay"; + public override string? GetDetails(bool hideIdentifiableInformation = false) => BeatmapDisplayTitle; + } + + [MessagePackObject] + public class SpectatingUser : WatchingReplay + { + public SpectatingUser(ScoreInfo score) + : base(score) + { + } + + [SerializationConstructor] + public SpectatingUser() { } + + public override string GetStatus(bool hideIdentifiableInformation = false) => hideIdentifiableInformation ? @"Spectating a user" : $@"Spectating {PlayerName}"; + } + + [MessagePackObject] public class SpectatingMultiplayerGame : InGame { public SpectatingMultiplayerGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) @@ -64,88 +221,41 @@ namespace osu.Game.Users { } + [SerializationConstructor] + public SpectatingMultiplayerGame() { } + public override string GetStatus(bool hideIdentifiableInformation = false) => $"Watching others {base.GetStatus(hideIdentifiableInformation).ToLowerInvariant()}"; } - public class InPlaylistGame : InGame - { - public InPlaylistGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) - : base(beatmapInfo, ruleset) - { - } - } - - public class InSoloGame : InGame - { - public InSoloGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) - : base(beatmapInfo, ruleset) - { - } - } - - public class TestingBeatmap : InGame - { - public override string GetStatus(bool hideIdentifiableInformation = false) => "Testing a beatmap"; - - public TestingBeatmap(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) - : base(beatmapInfo, ruleset) - { - } - } - - public class EditingBeatmap : UserActivity - { - public IBeatmapInfo BeatmapInfo { get; } - - public EditingBeatmap(IBeatmapInfo info) - { - BeatmapInfo = info; - } - - public override string GetStatus(bool hideIdentifiableInformation = false) => @"Editing a beatmap"; - } - - public class WatchingReplay : UserActivity - { - private readonly ScoreInfo score; - - protected string Username => score.User.Username; - - public BeatmapInfo? BeatmapInfo => score.BeatmapInfo; - - public WatchingReplay(ScoreInfo score) - { - this.score = score; - } - - public override string GetStatus(bool hideIdentifiableInformation = false) => hideIdentifiableInformation ? @"Watching a replay" : $@"Watching {Username}'s replay"; - } - - public class SpectatingUser : WatchingReplay - { - public override string GetStatus(bool hideIdentifiableInformation = false) => hideIdentifiableInformation ? @"Spectating a user" : $@"Spectating {Username}"; - - public SpectatingUser(ScoreInfo score) - : base(score) - { - } - } - + [MessagePackObject] public class SearchingForLobby : UserActivity { public override string GetStatus(bool hideIdentifiableInformation = false) => @"Looking for a lobby"; } + [MessagePackObject] public class InLobby : UserActivity { - public override string GetStatus(bool hideIdentifiableInformation = false) => @"In a lobby"; + [Key(0)] + public long RoomID { get; set; } - public readonly Room Room; + [Key(1)] + public string RoomName { get; set; } = string.Empty; public InLobby(Room room) { - Room = room; + RoomID = room.RoomID.Value ?? -1; + RoomName = room.Name.Value; } + + [SerializationConstructor] + public InLobby() { } + + public override string GetStatus(bool hideIdentifiableInformation = false) => @"In a lobby"; + + public override string? GetDetails(bool hideIdentifiableInformation = false) => hideIdentifiableInformation + ? null + : RoomName; } } } diff --git a/osu.Game/Users/UserGridPanel.cs b/osu.Game/Users/UserGridPanel.cs index f4ec1475b1..fe3435c248 100644 --- a/osu.Game/Users/UserGridPanel.cs +++ b/osu.Game/Users/UserGridPanel.cs @@ -10,6 +10,10 @@ using osuTK; namespace osu.Game.Users { + /// + /// A user "card", commonly used in a grid layout or in popovers. + /// Comes with a preset height, but width must be specified. + /// public partial class UserGridPanel : ExtendedUserPanel { private const int margin = 10; @@ -87,6 +91,7 @@ namespace osu.Game.Users Children = new Drawable[] { CreateFlag(), + // supporter icon is being added later } } }, @@ -104,6 +109,7 @@ namespace osu.Game.Users }, new[] { + // padding Empty(), Empty() }, diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index e2dc511391..b88619c8b7 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; @@ -12,12 +13,18 @@ using osu.Game.Overlays; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Framework.Graphics.Cursor; +using osu.Framework.Screens; using osu.Game.Graphics.Containers; using osu.Game.Online.API; 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; +using osu.Game.Screens; +using osu.Game.Screens.Play; +using osu.Game.Users.Drawables; +using osuTK; namespace osu.Game.Users { @@ -58,31 +65,32 @@ namespace osu.Game.Users [Resolved] protected OverlayColourProvider? ColourProvider { get; private set; } + [Resolved] + private IPerformFromScreenRunner? performer { get; set; } + [Resolved] protected OsuColour Colours { get; private set; } = null!; + [Resolved] + private MultiplayerClient? multiplayerClient { get; set; } + [BackgroundDependencyLoader] private void load() { Masking = true; - AddRange(new[] + Add(new Box { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourProvider?.Background5 ?? Colours.Gray1 - }, - Background = new UserCoverBackground - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - User = User, - }, - CreateLayout() + RelativeSizeAxes = Axes.Both, + Colour = ColourProvider?.Background5 ?? Colours.Gray1 }); + var background = CreateBackground(); + if (background != null) + Add(background); + + Add(CreateLayout()); + base.Action = ViewProfile = () => { Action?.Invoke(); @@ -90,8 +98,21 @@ namespace osu.Game.Users }; } + // TODO: this whole api is messy. half these Create methods are expected to by the implementation and half are implictly called. + protected abstract Drawable CreateLayout(); + /// + /// Panel background container. Can be null if a panel doesn't want a background under it's layout + /// + protected virtual Drawable? CreateBackground() => Background = new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + User = User + }; + protected OsuSpriteText CreateUsername() => new OsuSpriteText { Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold), @@ -99,6 +120,14 @@ namespace osu.Game.Users Text = User.Username, }; + protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar(User, false); + + protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.CountryCode) + { + Size = new Vector2(36, 26), + Action = Action, + }; + public MenuItem[] ContextMenuItems { get @@ -108,13 +137,26 @@ namespace osu.Game.Users new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, ViewProfile) }; - if (!User.Equals(api.LocalUser.Value)) + if (User.Equals(api.LocalUser.Value)) + return items.ToArray(); + + items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, () => { - items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, () => + channelManager?.OpenPrivateChannel(User); + chatOverlay?.Show(); + })); + + if (User.IsOnline) + { + items.Add(new OsuMenuItem(ContextMenuStrings.SpectatePlayer, MenuItemType.Standard, () => { - channelManager?.OpenPrivateChannel(User); - chatOverlay?.Show(); + performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(User))); })); + + if (multiplayerClient?.Room?.Users.All(u => u.UserID != User.Id) == true) + { + items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () => multiplayerClient.InvitePlayer(User.Id))); + } } return items.ToArray(); diff --git a/osu.Game/Users/UserPresence.cs b/osu.Game/Users/UserPresence.cs new file mode 100644 index 0000000000..dff40a9889 --- /dev/null +++ b/osu.Game/Users/UserPresence.cs @@ -0,0 +1,28 @@ +// 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 MessagePack; + +namespace osu.Game.Users +{ + /// + /// Structure containing all relevant information about a user's online presence. + /// + [Serializable] + [MessagePackObject] + public struct UserPresence + { + /// + /// The user's current activity. + /// + [Key(0)] + public UserActivity? Activity { get; set; } + + /// + /// The user's current status. + /// + [Key(1)] + public UserStatus? Status { get; set; } + } +} diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs new file mode 100644 index 0000000000..a38962dfc7 --- /dev/null +++ b/osu.Game/Users/UserRankPanel.cs @@ -0,0 +1,228 @@ +// 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.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays.Profile.Header.Components; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Users +{ + /// + /// User card that shows user's global and country ranks in the bottom. + /// Meant to be used in the toolbar login overlay. + /// + public partial class UserRankPanel : UserPanel + { + private const int padding = 10; + private const int main_content_height = 80; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private ProfileValueDisplay globalRankDisplay = null!; + private ProfileValueDisplay countryRankDisplay = null!; + + private readonly IBindable statistics = new Bindable(); + + public UserRankPanel(APIUser user) + : base(user) + { + AutoSizeAxes = Axes.Y; + CornerRadius = 10; + } + + [BackgroundDependencyLoader] + private void load() + { + BorderColour = ColourProvider?.Light1 ?? Colours.GreyVioletLighter; + + statistics.BindTo(api.Statistics); + statistics.BindValueChanged(stats => + { + globalRankDisplay.Content = stats.NewValue?.GlobalRank?.ToLocalisableString("\\##,##0") ?? "-"; + countryRankDisplay.Content = stats.NewValue?.CountryRank?.ToLocalisableString("\\##,##0") ?? "-"; + }, true); + } + + protected override Drawable CreateLayout() + { + FillFlowContainer details; + + var layout = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Container + { + Name = "Main content", + RelativeSizeAxes = Axes.X, + Height = main_content_height, + CornerRadius = 10, + Masking = true, + Children = new Drawable[] + { + new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + User = User, + Alpha = 0.3f + }, + new GridContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, padding), + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.Absolute, padding), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, padding), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new[] + { + // padding + Empty(), + Empty(), + Empty(), + Empty() + }, + new[] + { + Empty(), // padding + CreateAvatar().With(avatar => + { + avatar.Size = new Vector2(60); + avatar.Masking = true; + avatar.CornerRadius = 6; + }), + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = padding }, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension() + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension() + }, + Content = new[] + { + new Drawable[] + { + details = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(6), + Children = new Drawable[] + { + CreateFlag(), + // supporter icon is being added later + } + } + }, + new Drawable[] + { + CreateUsername().With(username => + { + username.Anchor = Anchor.CentreLeft; + username.Origin = Anchor.CentreLeft; + }) + } + } + } + }, + Empty() // padding + } + } + } + } + }, + new Container + { + Name = "Bottom content", + Margin = new MarginPadding { Top = main_content_height }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Left = 80, Vertical = padding }, + Child = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension() + }, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + Content = new[] + { + new Drawable[] + { + globalRankDisplay = new ProfileValueDisplay(true) + { + Title = UsersStrings.ShowRankGlobalSimple, + }, + countryRankDisplay = new ProfileValueDisplay(true) + { + Title = UsersStrings.ShowRankCountrySimple, + } + } + } + } + } + } + }; + + if (User.IsSupporter) + { + details.Add(new SupporterIcon + { + Height = 26, + SupportLevel = User.SupportLevel + }); + } + + return layout; + } + + protected override bool OnHover(HoverEvent e) + { + BorderThickness = 2; + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + BorderThickness = 0; + base.OnHoverLost(e); + } + + protected override Drawable? CreateBackground() => null; + } +} diff --git a/osu.Game/Users/UserStatus.cs b/osu.Game/Users/UserStatus.cs index ffd86b78c7..cd25add4d1 100644 --- a/osu.Game/Users/UserStatus.cs +++ b/osu.Game/Users/UserStatus.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.ComponentModel; using osu.Framework.Localisation; using osuTK.Graphics; using osu.Game.Graphics; @@ -8,32 +10,36 @@ using osu.Game.Resources.Localisation.Web; namespace osu.Game.Users { - public abstract class UserStatus + public enum UserStatus { - public abstract LocalisableString Message { get; } - public abstract Color4 GetAppropriateColour(OsuColour colours); + [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.StatusOffline))] + Offline, + + [Description("Do not disturb")] + DoNotDisturb, + + [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.StatusOnline))] + Online, } - public class UserStatusOnline : UserStatus + public static class UserStatusExtensions { - public override LocalisableString Message => UsersStrings.StatusOnline; - public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenLight; - } + public static Color4 GetAppropriateColour(this UserStatus userStatus, OsuColour colours) + { + switch (userStatus) + { + case UserStatus.Offline: + return Color4.Black; - public abstract class UserStatusBusy : UserStatusOnline - { - public override Color4 GetAppropriateColour(OsuColour colours) => colours.YellowDark; - } + case UserStatus.DoNotDisturb: + return colours.RedDark; - public class UserStatusOffline : UserStatus - { - public override LocalisableString Message => UsersStrings.StatusOffline; - public override Color4 GetAppropriateColour(OsuColour colours) => Color4.Black; - } + case UserStatus.Online: + return colours.GreenDark; - public class UserStatusDoNotDisturb : UserStatus - { - public override LocalisableString Message => "Do not disturb"; - public override Color4 GetAppropriateColour(OsuColour colours) => colours.RedDark; + default: + throw new ArgumentOutOfRangeException(nameof(userStatus), userStatus, "Unsupported user status"); + } + } } } 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/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index edf9cc80da..2c9eef41e3 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Localisation; using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -121,7 +123,7 @@ namespace osu.Game.Utils if (!CheckCompatibleSet(mods, out invalidMods)) return false; - return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation, out invalidMods); + return checkValid(mods, m => m.HasImplementation, out invalidMods); } /// @@ -226,5 +228,53 @@ namespace osu.Game.Utils return proposedWereValid; } + + /// + /// Verifies all mods provided belong to the given ruleset. + /// + /// The ruleset to check the proposed mods against. + /// The mods proposed for checking. + /// Whether all belong to the given . + public static bool CheckModsBelongToRuleset(Ruleset ruleset, IEnumerable proposedMods) + { + var rulesetModsTypes = ruleset.AllMods.Select(m => m.GetType()).ToList(); + + foreach (var proposedMod in proposedMods) + { + bool found = false; + + var proposedModType = proposedMod.GetType(); + + foreach (var rulesetModType in rulesetModsTypes) + { + if (rulesetModType.IsAssignableFrom(proposedModType)) + { + found = true; + break; + } + } + + if (!found) + return false; + } + + return true; + } + + /// + /// Given a value of a score multiplier, returns a string version with special handling for a value near 1.00x. + /// + /// The value of the score multiplier. + /// A formatted score multiplier with a trailing "x" symbol + public static LocalisableString FormatScoreMultiplier(double scoreMultiplier) + { + // Round multiplier values away from 1.00x to two significant digits. + if (scoreMultiplier > 1) + scoreMultiplier = Math.Ceiling(Math.Round(scoreMultiplier * 100, 12)) / 100; + else + scoreMultiplier = Math.Floor(Math.Round(scoreMultiplier * 100, 12)) / 100; + + return scoreMultiplier.ToLocalisableString("0.00x"); + } } } 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 909cfa57fb..1b1abe3971 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -21,27 +21,28 @@ - + - - - - - + + + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - + + + + + + + + diff --git a/osu.iOS.props b/osu.iOS.props index 8b8680d02c..98e8b136e5 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,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.sln.DotSettings b/osu.sln.DotSettings index ef39c12768..c8c5d6745c 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -15,6 +15,7 @@ HINT HINT WARNING + WARNING WARNING WARNING WARNING @@ -170,7 +171,7 @@ ERROR WARNING WARNING - HINT + DO_NOT_SHOW WARNING WARNING WARNING @@ -823,6 +824,7 @@ See the LICENCE file in the repository root for full licence text. True True True + True True True True