mirror of
https://github.com/ppy/osu.git
synced 2025-01-31 05:13:22 +08:00
Merge branch 'master' into slider_path_refactor
This commit is contained in:
commit
2ede8dd6c8
228
.github/workflows/_diffcalc_processor.yml
vendored
Normal file
228
.github/workflows/_diffcalc_processor.yml
vendored
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
name: "🔒diffcalc (do not use)"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
head-sha:
|
||||||
|
type: string
|
||||||
|
pr-url:
|
||||||
|
type: string
|
||||||
|
pr-text:
|
||||||
|
type: string
|
||||||
|
dispatch-inputs:
|
||||||
|
type: string
|
||||||
|
outputs:
|
||||||
|
target:
|
||||||
|
description: The comparison target.
|
||||||
|
value: ${{ jobs.generator.outputs.target }}
|
||||||
|
sheet:
|
||||||
|
description: The comparison spreadsheet.
|
||||||
|
value: ${{ jobs.generator.outputs.sheet }}
|
||||||
|
secrets:
|
||||||
|
DIFFCALC_GOOGLE_CREDENTIALS:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
GENERATOR_DIR: ${{ github.workspace }}/${{ inputs.id }}
|
||||||
|
GENERATOR_ENV: ${{ github.workspace }}/${{ inputs.id }}/.env
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash -euo pipefail {0}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
generator:
|
||||||
|
name: Run
|
||||||
|
runs-on: self-hosted
|
||||||
|
timeout-minutes: 720
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
target: ${{ steps.run.outputs.target }}
|
||||||
|
sheet: ${{ steps.run.outputs.sheet }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout diffcalc-sheet-generator
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
path: ${{ inputs.id }}
|
||||||
|
repository: 'smoogipoo/diffcalc-sheet-generator'
|
||||||
|
|
||||||
|
- name: Add base environment
|
||||||
|
env:
|
||||||
|
GOOGLE_CREDS_FILE: ${{ github.workspace }}/${{ inputs.id }}/google-credentials.json
|
||||||
|
VARS_JSON: ${{ (vars != null && toJSON(vars)) || '' }}
|
||||||
|
run: |
|
||||||
|
# Required by diffcalc-sheet-generator
|
||||||
|
cp '${{ env.GENERATOR_DIR }}/.env.sample' "${{ env.GENERATOR_ENV }}"
|
||||||
|
|
||||||
|
# Add Google credentials
|
||||||
|
echo '${{ secrets.DIFFCALC_GOOGLE_CREDENTIALS }}' | base64 -d > "${{ env.GOOGLE_CREDS_FILE }}"
|
||||||
|
|
||||||
|
# Add repository variables
|
||||||
|
echo "${VARS_JSON}" | jq -c '. | to_entries | .[]' | while read -r line; do
|
||||||
|
opt=$(jq -r '.key' <<< ${line})
|
||||||
|
val=$(jq -r '.value' <<< ${line})
|
||||||
|
|
||||||
|
if [[ "${opt}" =~ ^DIFFCALC_ ]]; then
|
||||||
|
optNoPrefix=$(echo "${opt}" | cut -d '_' -f2-)
|
||||||
|
sed -i "s;^${optNoPrefix}=.*$;${optNoPrefix}=${val};" "${{ env.GENERATOR_ENV }}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Add HEAD environment
|
||||||
|
run: |
|
||||||
|
sed -i "s;^OSU_A=.*$;OSU_A=${{ inputs.head-sha }};" "${{ env.GENERATOR_ENV }}"
|
||||||
|
|
||||||
|
- name: Add pull-request environment
|
||||||
|
if: ${{ inputs.pr-url != '' }}
|
||||||
|
run: |
|
||||||
|
sed -i "s;^OSU_B=.*$;OSU_B=${{ inputs.pr-url }};" "${{ env.GENERATOR_ENV }}"
|
||||||
|
|
||||||
|
- name: Add comment environment
|
||||||
|
if: ${{ inputs.pr-text != '' }}
|
||||||
|
env:
|
||||||
|
PR_TEXT: ${{ inputs.pr-text }}
|
||||||
|
run: |
|
||||||
|
# Add comment environment
|
||||||
|
echo "${PR_TEXT}" | sed -r 's/\r$//' | { grep -E '^\w+=' || true; } | while read -r line; do
|
||||||
|
opt=$(echo "${line}" | cut -d '=' -f1)
|
||||||
|
sed -i "s;^${opt}=.*$;${line};" "${{ env.GENERATOR_ENV }}"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Add dispatch environment
|
||||||
|
if: ${{ inputs.dispatch-inputs != '' }}
|
||||||
|
env:
|
||||||
|
DISPATCH_INPUTS_JSON: ${{ inputs.dispatch-inputs }}
|
||||||
|
run: |
|
||||||
|
function get_input() {
|
||||||
|
echo "${DISPATCH_INPUTS_JSON}" | jq -r ".\"$1\""
|
||||||
|
}
|
||||||
|
|
||||||
|
osu_a=$(get_input 'osu-a')
|
||||||
|
osu_b=$(get_input 'osu-b')
|
||||||
|
ruleset=$(get_input 'ruleset')
|
||||||
|
generators=$(get_input 'generators')
|
||||||
|
difficulty_calculator_a=$(get_input 'difficulty-calculator-a')
|
||||||
|
difficulty_calculator_b=$(get_input 'difficulty-calculator-b')
|
||||||
|
score_processor_a=$(get_input 'score-processor-a')
|
||||||
|
score_processor_b=$(get_input 'score-processor-b')
|
||||||
|
converts=$(get_input 'converts')
|
||||||
|
ranked_only=$(get_input 'ranked-only')
|
||||||
|
|
||||||
|
sed -i "s;^OSU_B=.*$;OSU_B=${osu_b};" "${{ env.GENERATOR_ENV }}"
|
||||||
|
sed -i "s/^RULESET=.*$/RULESET=${ruleset}/" "${{ env.GENERATOR_ENV }}"
|
||||||
|
sed -i "s/^GENERATORS=.*$/GENERATORS=${generators}/" "${{ env.GENERATOR_ENV }}"
|
||||||
|
|
||||||
|
if [[ "${osu_a}" != 'latest' ]]; then
|
||||||
|
sed -i "s;^OSU_A=.*$;OSU_A=${osu_a};" "${{ env.GENERATOR_ENV }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${difficulty_calculator_a}" != 'latest' ]]; then
|
||||||
|
sed -i "s;^DIFFICULTY_CALCULATOR_A=.*$;DIFFICULTY_CALCULATOR_A=${difficulty_calculator_a};" "${{ env.GENERATOR_ENV }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${difficulty_calculator_b}" != 'latest' ]]; then
|
||||||
|
sed -i "s;^DIFFICULTY_CALCULATOR_B=.*$;DIFFICULTY_CALCULATOR_B=${difficulty_calculator_b};" "${{ env.GENERATOR_ENV }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${score_processor_a}" != 'latest' ]]; then
|
||||||
|
sed -i "s;^SCORE_PROCESSOR_A=.*$;SCORE_PROCESSOR_A=${score_processor_a};" "${{ env.GENERATOR_ENV }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${score_processor_b}" != 'latest' ]]; then
|
||||||
|
sed -i "s;^SCORE_PROCESSOR_B=.*$;SCORE_PROCESSOR_B=${score_processor_b};" "${{ env.GENERATOR_ENV }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${converts}" == 'true' ]]; then
|
||||||
|
sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=0/' "${{ env.GENERATOR_ENV }}"
|
||||||
|
else
|
||||||
|
sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=1/' "${{ env.GENERATOR_ENV }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${ranked_only}" == 'true' ]]; then
|
||||||
|
sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=1/' "${{ env.GENERATOR_ENV }}"
|
||||||
|
else
|
||||||
|
sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=0/' "${{ env.GENERATOR_ENV }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Query latest scores
|
||||||
|
id: query-scores
|
||||||
|
run: |
|
||||||
|
ruleset=$(cat ${{ env.GENERATOR_ENV }} | grep -E '^RULESET=' | cut -d '=' -f2-)
|
||||||
|
performance_data_name=$(curl -s "https://data.ppy.sh/" | grep "performance_${ruleset}_top_1000\b" | tail -1 | awk -F "'" '{print $2}' | sed 's/\.tar\.bz2//g')
|
||||||
|
|
||||||
|
echo "TARGET_DIR=${{ env.GENERATOR_DIR }}/sql/${ruleset}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "DATA_NAME=${performance_data_name}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "DATA_PKG=${performance_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Restore score cache
|
||||||
|
id: restore-score-cache
|
||||||
|
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
||||||
|
with:
|
||||||
|
path: ${{ steps.query-scores.outputs.DATA_PKG }}
|
||||||
|
key: ${{ steps.query-scores.outputs.DATA_NAME }}
|
||||||
|
|
||||||
|
- name: Download scores
|
||||||
|
if: steps.restore-score-cache.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
wget -q -O "${{ steps.query-scores.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query-scores.outputs.DATA_PKG }}"
|
||||||
|
|
||||||
|
- name: Extract scores
|
||||||
|
run: |
|
||||||
|
tar -I lbzip2 -xf "${{ steps.query-scores.outputs.DATA_PKG }}"
|
||||||
|
rm -r "${{ steps.query-scores.outputs.TARGET_DIR }}"
|
||||||
|
mv "${{ steps.query-scores.outputs.DATA_NAME }}" "${{ steps.query-scores.outputs.TARGET_DIR }}"
|
||||||
|
|
||||||
|
- name: Query latest beatmaps
|
||||||
|
id: query-beatmaps
|
||||||
|
run: |
|
||||||
|
beatmaps_data_name=$(curl -s "https://data.ppy.sh/" | grep "osu_files" | tail -1 | awk -F "'" '{print $2}' | sed 's/\.tar\.bz2//g')
|
||||||
|
|
||||||
|
echo "TARGET_DIR=${{ env.GENERATOR_DIR }}/beatmaps" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "DATA_NAME=${beatmaps_data_name}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "DATA_PKG=${beatmaps_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Restore beatmap cache
|
||||||
|
id: restore-beatmap-cache
|
||||||
|
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
||||||
|
with:
|
||||||
|
path: ${{ steps.query-beatmaps.outputs.DATA_PKG }}
|
||||||
|
key: ${{ steps.query-beatmaps.outputs.DATA_NAME }}
|
||||||
|
|
||||||
|
- name: Download beatmap
|
||||||
|
if: steps.restore-beatmap-cache.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
wget -q -O "${{ steps.query-beatmaps.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query-beatmaps.outputs.DATA_PKG }}"
|
||||||
|
|
||||||
|
- name: Extract beatmap
|
||||||
|
run: |
|
||||||
|
tar -I lbzip2 -xf "${{ steps.query-beatmaps.outputs.DATA_PKG }}"
|
||||||
|
rm -r "${{ steps.query-beatmaps.outputs.TARGET_DIR }}"
|
||||||
|
mv "${{ steps.query-beatmaps.outputs.DATA_NAME }}" "${{ steps.query-beatmaps.outputs.TARGET_DIR }}"
|
||||||
|
|
||||||
|
- name: Run
|
||||||
|
id: run
|
||||||
|
run: |
|
||||||
|
# Add the GitHub token. This needs to be done here because it's unique per-job.
|
||||||
|
sed -i 's/^GH_TOKEN=.*$/GH_TOKEN=${{ github.token }}/' "${{ env.GENERATOR_ENV }}"
|
||||||
|
|
||||||
|
cd "${{ env.GENERATOR_DIR }}"
|
||||||
|
|
||||||
|
docker compose up --build --detach
|
||||||
|
docker compose logs --follow &
|
||||||
|
docker compose wait generator
|
||||||
|
|
||||||
|
link=$(docker compose logs --tail 10 generator | grep 'http' | sed -E 's/^.*(http.*)$/\1/')
|
||||||
|
target=$(cat "${{ env.GENERATOR_ENV }}" | grep -E '^OSU_B=' | cut -d '=' -f2-)
|
||||||
|
|
||||||
|
echo "target=${target}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "sheet=${link}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Shutdown
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: |
|
||||||
|
cd "${{ env.GENERATOR_DIR }}"
|
||||||
|
docker compose down --volumes
|
||||||
|
rm -rf "${{ env.GENERATOR_DIR }}"
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -88,7 +88,7 @@ jobs:
|
|||||||
# Attempt to upload results even if test fails.
|
# Attempt to upload results even if test fails.
|
||||||
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
|
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
with:
|
with:
|
||||||
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
|
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
|
||||||
|
269
.github/workflows/diffcalc.yml
vendored
269
.github/workflows/diffcalc.yml
vendored
@ -103,26 +103,11 @@ permissions:
|
|||||||
env:
|
env:
|
||||||
EXECUTION_ID: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
|
EXECUTION_ID: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash -euo pipefail {0}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
master-environment:
|
|
||||||
name: Save master environment
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
HEAD: ${{ steps.get-head.outputs.HEAD }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout osu
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: master
|
|
||||||
sparse-checkout: |
|
|
||||||
README.md
|
|
||||||
|
|
||||||
- name: Get HEAD ref
|
|
||||||
id: get-head
|
|
||||||
run: |
|
|
||||||
ref=$(git log -1 --format='%H')
|
|
||||||
echo "HEAD=https://github.com/${{ github.repository }}/commit/${ref}" >> "${GITHUB_OUTPUT}"
|
|
||||||
|
|
||||||
check-permissions:
|
check-permissions:
|
||||||
name: Check permissions
|
name: Check permissions
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -138,9 +123,23 @@ jobs:
|
|||||||
done
|
done
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
|
run-diffcalc:
|
||||||
|
name: Run spreadsheet generator
|
||||||
|
needs: check-permissions
|
||||||
|
uses: ./.github/workflows/_diffcalc_processor.yml
|
||||||
|
with:
|
||||||
|
# Can't reference env... Why GitHub, WHY?
|
||||||
|
id: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||||
|
head-sha: https://github.com/${{ github.repository }}/commit/${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
pr-url: ${{ github.event.issue.pull_request.html_url || '' }}
|
||||||
|
pr-text: ${{ github.event.comment.body || '' }}
|
||||||
|
dispatch-inputs: ${{ (github.event.type == 'workflow_dispatch' && toJSON(inputs)) || '' }}
|
||||||
|
secrets:
|
||||||
|
DIFFCALC_GOOGLE_CREDENTIALS: ${{ secrets.DIFFCALC_GOOGLE_CREDENTIALS }}
|
||||||
|
|
||||||
create-comment:
|
create-comment:
|
||||||
name: Create PR comment
|
name: Create PR comment
|
||||||
needs: [ master-environment, check-permissions ]
|
needs: check-permissions
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
||||||
steps:
|
steps:
|
||||||
@ -153,244 +152,34 @@ jobs:
|
|||||||
|
|
||||||
*This comment will update on completion*
|
*This comment will update on completion*
|
||||||
|
|
||||||
directory:
|
|
||||||
name: Prepare directory
|
|
||||||
needs: check-permissions
|
|
||||||
runs-on: self-hosted
|
|
||||||
outputs:
|
|
||||||
GENERATOR_DIR: ${{ steps.set-outputs.outputs.GENERATOR_DIR }}
|
|
||||||
GENERATOR_ENV: ${{ steps.set-outputs.outputs.GENERATOR_ENV }}
|
|
||||||
GOOGLE_CREDS_FILE: ${{ steps.set-outputs.outputs.GOOGLE_CREDS_FILE }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout diffcalc-sheet-generator
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
path: ${{ env.EXECUTION_ID }}
|
|
||||||
repository: 'smoogipoo/diffcalc-sheet-generator'
|
|
||||||
|
|
||||||
- name: Set outputs
|
|
||||||
id: set-outputs
|
|
||||||
run: |
|
|
||||||
echo "GENERATOR_DIR=${{ github.workspace }}/${{ env.EXECUTION_ID }}" >> "${GITHUB_OUTPUT}"
|
|
||||||
echo "GENERATOR_ENV=${{ github.workspace }}/${{ env.EXECUTION_ID }}/.env" >> "${GITHUB_OUTPUT}"
|
|
||||||
echo "GOOGLE_CREDS_FILE=${{ github.workspace }}/${{ env.EXECUTION_ID }}/google-credentials.json" >> "${GITHUB_OUTPUT}"
|
|
||||||
|
|
||||||
environment:
|
|
||||||
name: Setup environment
|
|
||||||
needs: [ master-environment, directory ]
|
|
||||||
runs-on: self-hosted
|
|
||||||
env:
|
|
||||||
VARS_JSON: ${{ toJSON(vars) }}
|
|
||||||
steps:
|
|
||||||
- name: Add base environment
|
|
||||||
run: |
|
|
||||||
# Required by diffcalc-sheet-generator
|
|
||||||
cp '${{ needs.directory.outputs.GENERATOR_DIR }}/.env.sample' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
|
||||||
|
|
||||||
# Add Google credentials
|
|
||||||
echo '${{ secrets.DIFFCALC_GOOGLE_CREDENTIALS }}' | base64 -d > "${{ needs.directory.outputs.GOOGLE_CREDS_FILE }}"
|
|
||||||
|
|
||||||
# Add repository variables
|
|
||||||
echo "${VARS_JSON}" | jq -c '. | to_entries | .[]' | while read -r line; do
|
|
||||||
opt=$(jq -r '.key' <<< ${line})
|
|
||||||
val=$(jq -r '.value' <<< ${line})
|
|
||||||
|
|
||||||
if [[ "${opt}" =~ ^DIFFCALC_ ]]; then
|
|
||||||
optNoPrefix=$(echo "${opt}" | cut -d '_' -f2-)
|
|
||||||
sed -i "s;^${optNoPrefix}=.*$;${optNoPrefix}=${val};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Add master environment
|
|
||||||
run: |
|
|
||||||
sed -i "s;^OSU_A=.*$;OSU_A=${{ needs.master-environment.outputs.HEAD }};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
|
||||||
|
|
||||||
- name: Add pull-request environment
|
|
||||||
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
|
||||||
run: |
|
|
||||||
sed -i "s;^OSU_B=.*$;OSU_B=${{ github.event.issue.pull_request.html_url }};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
|
||||||
|
|
||||||
- name: Add comment environment
|
|
||||||
if: ${{ github.event_name == 'issue_comment' }}
|
|
||||||
env:
|
|
||||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
|
||||||
run: |
|
|
||||||
# Add comment environment
|
|
||||||
echo "$COMMENT_BODY" | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do
|
|
||||||
opt=$(echo "${line}" | cut -d '=' -f1)
|
|
||||||
sed -i "s;^${opt}=.*$;${line};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Add dispatch environment
|
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
|
||||||
run: |
|
|
||||||
sed -i 's;^OSU_B=.*$;OSU_B=${{ inputs.osu-b }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
|
||||||
sed -i 's/^RULESET=.*$/RULESET=${{ inputs.ruleset }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
|
||||||
sed -i 's/^GENERATORS=.*$/GENERATORS=${{ inputs.generators }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
|
||||||
|
|
||||||
if [[ '${{ inputs.osu-a }}' != 'latest' ]]; then
|
|
||||||
sed -i 's;^OSU_A=.*$;OSU_A=${{ inputs.osu-a }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ '${{ inputs.difficulty-calculator-a }}' != 'latest' ]]; then
|
|
||||||
sed -i 's;^DIFFICULTY_CALCULATOR_A=.*$;DIFFICULTY_CALCULATOR_A=${{ inputs.difficulty-calculator-a }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ '${{ inputs.difficulty-calculator-b }}' != 'latest' ]]; then
|
|
||||||
sed -i 's;^DIFFICULTY_CALCULATOR_B=.*$;DIFFICULTY_CALCULATOR_B=${{ inputs.difficulty-calculator-b }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ '${{ inputs.score-processor-a }}' != 'latest' ]]; then
|
|
||||||
sed -i 's;^SCORE_PROCESSOR_A=.*$;SCORE_PROCESSOR_A=${{ inputs.score-processor-a }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ '${{ inputs.score-processor-b }}' != 'latest' ]]; then
|
|
||||||
sed -i 's;^SCORE_PROCESSOR_B=.*$;SCORE_PROCESSOR_B=${{ inputs.score-processor-b }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ '${{ inputs.converts }}' == 'true' ]]; then
|
|
||||||
sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=0/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
|
||||||
else
|
|
||||||
sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=1/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ '${{ inputs.ranked-only }}' == 'true' ]]; then
|
|
||||||
sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=1/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
|
||||||
else
|
|
||||||
sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=0/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
scores:
|
|
||||||
name: Setup scores
|
|
||||||
needs: [ directory, environment ]
|
|
||||||
runs-on: self-hosted
|
|
||||||
steps:
|
|
||||||
- name: Query latest data
|
|
||||||
id: query
|
|
||||||
run: |
|
|
||||||
ruleset=$(cat ${{ needs.directory.outputs.GENERATOR_ENV }} | grep -E '^RULESET=' | cut -d '=' -f2-)
|
|
||||||
performance_data_name=$(curl -s "https://data.ppy.sh/" | grep "performance_${ruleset}_top_1000\b" | tail -1 | awk -F "'" '{print $2}' | sed 's/\.tar\.bz2//g')
|
|
||||||
|
|
||||||
echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/sql/${ruleset}" >> "${GITHUB_OUTPUT}"
|
|
||||||
echo "DATA_NAME=${performance_data_name}" >> "${GITHUB_OUTPUT}"
|
|
||||||
|
|
||||||
- name: Restore cache
|
|
||||||
id: restore-cache
|
|
||||||
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
|
||||||
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@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
|
||||||
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:
|
output-cli:
|
||||||
name: Output info
|
name: Info
|
||||||
needs: generator
|
needs: run-diffcalc
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Output info
|
- name: Output info
|
||||||
run: |
|
run: |
|
||||||
echo "Target: ${{ needs.generator.outputs.TARGET }}"
|
echo "Target: ${{ needs.run-diffcalc.outputs.target }}"
|
||||||
echo "Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }}"
|
echo "Spreadsheet: ${{ needs.run-diffcalc.outputs.sheet }}"
|
||||||
|
|
||||||
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:
|
update-comment:
|
||||||
name: Update PR comment
|
name: Update PR comment
|
||||||
needs: [ create-comment, generator ]
|
needs: [ create-comment, run-diffcalc ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ always() && needs.create-comment.result == 'success' }}
|
if: ${{ always() && needs.create-comment.result == 'success' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Update comment on success
|
- name: Update comment on success
|
||||||
if: ${{ needs.generator.result == 'success' }}
|
if: ${{ needs.run-diffcalc.result == 'success' }}
|
||||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||||
with:
|
with:
|
||||||
comment_tag: ${{ env.EXECUTION_ID }}
|
comment_tag: ${{ env.EXECUTION_ID }}
|
||||||
mode: recreate
|
mode: recreate
|
||||||
message: |
|
message: |
|
||||||
Target: ${{ needs.generator.outputs.TARGET }}
|
Target: ${{ needs.run-diffcalc.outputs.target }}
|
||||||
Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }}
|
Spreadsheet: ${{ needs.run-diffcalc.outputs.sheet }}
|
||||||
|
|
||||||
- name: Update comment on failure
|
- name: Update comment on failure
|
||||||
if: ${{ needs.generator.result == 'failure' }}
|
if: ${{ needs.run-diffcalc.result == 'failure' }}
|
||||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||||
with:
|
with:
|
||||||
comment_tag: ${{ env.EXECUTION_ID }}
|
comment_tag: ${{ env.EXECUTION_ID }}
|
||||||
@ -399,7 +188,7 @@ jobs:
|
|||||||
Difficulty calculation failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
Difficulty calculation failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
|
||||||
- name: Update comment on cancellation
|
- name: Update comment on cancellation
|
||||||
if: ${{ needs.generator.result == 'cancelled' }}
|
if: ${{ needs.run-diffcalc.result == 'cancelled' }}
|
||||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||||
with:
|
with:
|
||||||
comment_tag: ${{ env.EXECUTION_ID }}
|
comment_tag: ${{ env.EXECUTION_ID }}
|
||||||
|
37
.github/workflows/report-nunit.yml
vendored
37
.github/workflows/report-nunit.yml
vendored
@ -5,33 +5,40 @@
|
|||||||
name: Annotate CI run with test results
|
name: Annotate CI run with test results
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
workflow_run:
|
||||||
workflows: ["Continuous Integration"]
|
workflows: [ "Continuous Integration" ]
|
||||||
types:
|
types:
|
||||||
- completed
|
- completed
|
||||||
permissions: {}
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
checks: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
annotate:
|
annotate:
|
||||||
permissions:
|
|
||||||
checks: write # to create checks (dorny/test-reporter)
|
|
||||||
|
|
||||||
name: Annotate CI run with test results
|
name: Annotate CI run with test results
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
|
if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os:
|
|
||||||
- { prettyname: Windows }
|
|
||||||
- { prettyname: macOS }
|
|
||||||
- { prettyname: Linux }
|
|
||||||
threadingMode: ['SingleThread', 'MultiThreaded']
|
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: ${{ github.event.workflow_run.repository.full_name }}
|
||||||
|
ref: ${{ github.event.workflow_run.head_sha }}
|
||||||
|
|
||||||
|
- name: Download results
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: osu-test-results-*
|
||||||
|
merge-multiple: true
|
||||||
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
|
||||||
- name: Annotate CI run with test results
|
- name: Annotate CI run with test results
|
||||||
uses: dorny/test-reporter@v1.8.0
|
uses: dorny/test-reporter@v1.8.0
|
||||||
with:
|
with:
|
||||||
artifact: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
|
name: Results
|
||||||
name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
|
|
||||||
path: "*.trx"
|
path: "*.trx"
|
||||||
reporter: dotnet-trx
|
reporter: dotnet-trx
|
||||||
list-suites: 'failed'
|
list-suites: 'failed'
|
||||||
|
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"ms-dotnettools.csharp"
|
"editorconfig.editorconfig",
|
||||||
|
"ms-dotnettools.csdevkit"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ Please make sure you have the following prerequisites:
|
|||||||
|
|
||||||
- A desktop platform with the [.NET 8.0 SDK](https://dotnet.microsoft.com/download) installed.
|
- A desktop platform with the [.NET 8.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/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) plugin 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/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) plugin installed.
|
||||||
|
|
||||||
### Downloading the source code
|
### Downloading the source code
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1009.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1025.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
@ -5,7 +5,6 @@ using Android.Content.PM;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game;
|
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
|
|
||||||
namespace osu.Android
|
namespace osu.Android
|
||||||
@ -28,7 +27,7 @@ namespace osu.Android
|
|||||||
{
|
{
|
||||||
gameActivity.RunOnUiThread(() =>
|
gameActivity.RunOnUiThread(() =>
|
||||||
{
|
{
|
||||||
gameActivity.RequestedOrientation = userPlaying.NewValue != LocalUserPlayingState.NotPlaying ? ScreenOrientation.Locked : gameActivity.DefaultOrientation;
|
gameActivity.RequestedOrientation = userPlaying.NewValue == LocalUserPlayingState.Playing ? ScreenOrientation.Locked : gameActivity.DefaultOrientation;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,12 +141,12 @@ namespace osu.Desktop
|
|||||||
|
|
||||||
// Make sure that this is a laptop.
|
// Make sure that this is a laptop.
|
||||||
IntPtr[] gpus = new IntPtr[64];
|
IntPtr[] gpus = new IntPtr[64];
|
||||||
if (checkError(EnumPhysicalGPUs(gpus, out int gpuCount)))
|
if (checkError(EnumPhysicalGPUs(gpus, out int gpuCount), nameof(EnumPhysicalGPUs)))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
for (int i = 0; i < gpuCount; i++)
|
for (int i = 0; i < gpuCount; i++)
|
||||||
{
|
{
|
||||||
if (checkError(GetSystemType(gpus[i], out var type)))
|
if (checkError(GetSystemType(gpus[i], out var type), nameof(GetSystemType)))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (type == NvSystemType.LAPTOP)
|
if (type == NvSystemType.LAPTOP)
|
||||||
@ -182,7 +182,7 @@ namespace osu.Desktop
|
|||||||
|
|
||||||
bool success = setSetting(NvSettingID.OGL_THREAD_CONTROL_ID, (uint)value);
|
bool success = setSetting(NvSettingID.OGL_THREAD_CONTROL_ID, (uint)value);
|
||||||
|
|
||||||
Logger.Log(success ? $"Threaded optimizations set to \"{value}\"!" : "Threaded optimizations set failed!");
|
Logger.Log(success ? $"[NVAPI] Threaded optimizations set to \"{value}\"!" : "[NVAPI] Threaded optimizations set failed!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,7 +205,7 @@ namespace osu.Desktop
|
|||||||
|
|
||||||
uint numApps = profile.NumOfApps;
|
uint numApps = profile.NumOfApps;
|
||||||
|
|
||||||
if (checkError(EnumApplications(sessionHandle, profileHandle, 0, ref numApps, applications)))
|
if (checkError(EnumApplications(sessionHandle, profileHandle, 0, ref numApps, applications), nameof(EnumApplications)))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
for (uint i = 0; i < numApps; i++)
|
for (uint i = 0; i < numApps; i++)
|
||||||
@ -236,10 +236,10 @@ namespace osu.Desktop
|
|||||||
|
|
||||||
isApplicationSpecific = true;
|
isApplicationSpecific = true;
|
||||||
|
|
||||||
if (checkError(FindApplicationByName(sessionHandle, osu_filename, out profileHandle, ref application)))
|
if (checkError(FindApplicationByName(sessionHandle, osu_filename, out profileHandle, ref application), nameof(FindApplicationByName)))
|
||||||
{
|
{
|
||||||
isApplicationSpecific = false;
|
isApplicationSpecific = false;
|
||||||
if (checkError(GetCurrentGlobalProfile(sessionHandle, out profileHandle)))
|
if (checkError(GetCurrentGlobalProfile(sessionHandle, out profileHandle), nameof(GetCurrentGlobalProfile)))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,12 +258,10 @@ namespace osu.Desktop
|
|||||||
Version = NvProfile.Stride,
|
Version = NvProfile.Stride,
|
||||||
IsPredefined = 0,
|
IsPredefined = 0,
|
||||||
ProfileName = PROFILE_NAME,
|
ProfileName = PROFILE_NAME,
|
||||||
GPUSupport = new uint[32]
|
GpuSupport = NvDrsGpuSupport.Geforce
|
||||||
};
|
};
|
||||||
|
|
||||||
newProfile.GPUSupport[0] = 1;
|
if (checkError(CreateProfile(sessionHandle, ref newProfile, out profileHandle), nameof(CreateProfile)))
|
||||||
|
|
||||||
if (checkError(CreateProfile(sessionHandle, ref newProfile, out profileHandle)))
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -284,7 +282,7 @@ namespace osu.Desktop
|
|||||||
SettingID = settingId
|
SettingID = settingId
|
||||||
};
|
};
|
||||||
|
|
||||||
if (checkError(GetSetting(sessionHandle, profileHandle, settingId, ref setting)))
|
if (checkError(GetSetting(sessionHandle, profileHandle, settingId, ref setting), nameof(GetSetting)))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -313,7 +311,7 @@ namespace osu.Desktop
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Set the thread state
|
// Set the thread state
|
||||||
if (checkError(SetSetting(sessionHandle, profileHandle, ref newSetting)))
|
if (checkError(SetSetting(sessionHandle, profileHandle, ref newSetting), nameof(SetSetting)))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Get the profile (needed to check app count)
|
// Get the profile (needed to check app count)
|
||||||
@ -321,7 +319,7 @@ namespace osu.Desktop
|
|||||||
{
|
{
|
||||||
Version = NvProfile.Stride
|
Version = NvProfile.Stride
|
||||||
};
|
};
|
||||||
if (checkError(GetProfileInfo(sessionHandle, profileHandle, ref profile)))
|
if (checkError(GetProfileInfo(sessionHandle, profileHandle, ref profile), nameof(GetProfileInfo)))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!containsApplication(profileHandle, profile, out application))
|
if (!containsApplication(profileHandle, profile, out application))
|
||||||
@ -332,12 +330,12 @@ namespace osu.Desktop
|
|||||||
application.AppName = osu_filename;
|
application.AppName = osu_filename;
|
||||||
application.UserFriendlyName = APPLICATION_NAME;
|
application.UserFriendlyName = APPLICATION_NAME;
|
||||||
|
|
||||||
if (checkError(CreateApplication(sessionHandle, profileHandle, ref application)))
|
if (checkError(CreateApplication(sessionHandle, profileHandle, ref application), nameof(CreateApplication)))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save!
|
// Save!
|
||||||
return !checkError(SaveSettings(sessionHandle));
|
return !checkError(SaveSettings(sessionHandle), nameof(SaveSettings));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -346,20 +344,25 @@ namespace osu.Desktop
|
|||||||
/// <returns>If the operation succeeded.</returns>
|
/// <returns>If the operation succeeded.</returns>
|
||||||
private static bool createSession()
|
private static bool createSession()
|
||||||
{
|
{
|
||||||
if (checkError(CreateSession(out sessionHandle)))
|
if (checkError(CreateSession(out sessionHandle), nameof(CreateSession)))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Load settings into session
|
// Load settings into session
|
||||||
if (checkError(LoadSettings(sessionHandle)))
|
if (checkError(LoadSettings(sessionHandle), nameof(LoadSettings)))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool checkError(NvStatus status)
|
private static bool checkError(NvStatus status, string caller)
|
||||||
{
|
{
|
||||||
Status = status;
|
Status = status;
|
||||||
return status != NvStatus.OK;
|
|
||||||
|
bool hasError = status != NvStatus.OK;
|
||||||
|
if (hasError)
|
||||||
|
Logger.Log($"[NVAPI] {caller} call failed with status code {status}");
|
||||||
|
|
||||||
|
return hasError;
|
||||||
}
|
}
|
||||||
|
|
||||||
static NVAPI()
|
static NVAPI()
|
||||||
@ -458,9 +461,7 @@ namespace osu.Desktop
|
|||||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
|
||||||
public string ProfileName;
|
public string ProfileName;
|
||||||
|
|
||||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
|
public NvDrsGpuSupport GpuSupport;
|
||||||
public uint[] GPUSupport;
|
|
||||||
|
|
||||||
public uint IsPredefined;
|
public uint IsPredefined;
|
||||||
public uint NumOfApps;
|
public uint NumOfApps;
|
||||||
public uint NumOfSettings;
|
public uint NumOfSettings;
|
||||||
@ -606,6 +607,7 @@ namespace osu.Desktop
|
|||||||
SYNC_NOT_ACTIVE = -194, // The requested action cannot be performed without Sync being enabled.
|
SYNC_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.
|
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.
|
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_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.
|
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_OUT_OF_DATE = -199, // The device's firmware is out of date.
|
||||||
@ -744,4 +746,12 @@ namespace osu.Desktop
|
|||||||
OGL_THREAD_CONTROL_NUM_VALUES = 2,
|
OGL_THREAD_CONTROL_NUM_VALUES = 2,
|
||||||
OGL_THREAD_CONTROL_DEFAULT = 0
|
OGL_THREAD_CONTROL_DEFAULT = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
internal enum NvDrsGpuSupport : uint
|
||||||
|
{
|
||||||
|
Geforce = 1 << 0,
|
||||||
|
Quadro = 1 << 1,
|
||||||
|
Nvs = 1 << 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
||||||
<PackageReference Include="nunit" Version="3.14.0" />
|
<PackageReference Include="nunit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -8,7 +8,6 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
{
|
{
|
||||||
public partial class CatchEditorPlayfield : CatchPlayfield
|
public partial class CatchEditorPlayfield : CatchPlayfield
|
||||||
{
|
{
|
||||||
// TODO fixme: the size of the catcher is not changed when circle size is changed in setup screen.
|
|
||||||
public CatchEditorPlayfield(IBeatmapDifficultyInfo difficulty)
|
public CatchEditorPlayfield(IBeatmapDifficultyInfo difficulty)
|
||||||
: base(difficulty)
|
: base(difficulty)
|
||||||
{
|
{
|
||||||
|
@ -2,16 +2,22 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Catch.UI;
|
using osu.Game.Rulesets.Catch.UI;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Edit
|
namespace osu.Game.Rulesets.Catch.Edit
|
||||||
{
|
{
|
||||||
public partial class DrawableCatchEditorRuleset : DrawableCatchRuleset
|
public partial class DrawableCatchEditorRuleset : DrawableCatchRuleset
|
||||||
{
|
{
|
||||||
|
[Resolved]
|
||||||
|
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||||
|
|
||||||
public readonly BindableDouble TimeRangeMultiplier = new BindableDouble(1);
|
public readonly BindableDouble TimeRangeMultiplier = new BindableDouble(1);
|
||||||
|
|
||||||
public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)
|
public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)
|
||||||
@ -28,6 +34,30 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
TimeRange.Value = gamePlayTimeRange * TimeRangeMultiplier.Value * playfieldStretch;
|
TimeRange.Value = gamePlayTimeRange * TimeRangeMultiplier.Value * playfieldStretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
editorBeatmap.BeatmapReprocessed += onBeatmapReprocessed;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
if (editorBeatmap.IsNotNull())
|
||||||
|
editorBeatmap.BeatmapReprocessed -= onBeatmapReprocessed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onBeatmapReprocessed()
|
||||||
|
{
|
||||||
|
if (Playfield is CatchEditorPlayfield catchPlayfield)
|
||||||
|
{
|
||||||
|
catchPlayfield.Catcher.ApplyDifficulty(editorBeatmap.Difficulty);
|
||||||
|
catchPlayfield.CatcherArea.CatcherTrails.UpdateCatcherTrailsScale(catchPlayfield.Catcher.BodyScale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.Difficulty);
|
protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.Difficulty);
|
||||||
|
|
||||||
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchEditorPlayfieldAdjustmentContainer();
|
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchEditorPlayfieldAdjustmentContainer();
|
||||||
|
@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default)
|
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default)
|
||||||
=> new BananaHitSampleInfo(newVolume.GetOr(Volume));
|
=> new BananaHitSampleInfo(newVolume.GetOr(Volume));
|
||||||
|
|
||||||
public bool Equals(BananaHitSampleInfo? other)
|
public bool Equals(BananaHitSampleInfo? other)
|
||||||
|
@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Width of the area that can be used to attempt catches during gameplay.
|
/// Width of the area that can be used to attempt catches during gameplay.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly float CatchWidth;
|
public float CatchWidth { get; private set; }
|
||||||
|
|
||||||
private readonly SkinnableCatcher body;
|
private readonly SkinnableCatcher body;
|
||||||
|
|
||||||
@ -142,10 +142,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
|
|
||||||
Size = new Vector2(BASE_SIZE);
|
Size = new Vector2(BASE_SIZE);
|
||||||
|
|
||||||
if (difficulty != null)
|
ApplyDifficulty(difficulty);
|
||||||
Scale = calculateScale(difficulty);
|
|
||||||
|
|
||||||
CatchWidth = CalculateCatchWidth(Scale);
|
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
@ -312,6 +309,17 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the scale and catch width.
|
||||||
|
/// </summary>
|
||||||
|
public void ApplyDifficulty(IBeatmapDifficultyInfo? difficulty)
|
||||||
|
{
|
||||||
|
if (difficulty != null)
|
||||||
|
Scale = calculateScale(difficulty);
|
||||||
|
|
||||||
|
CatchWidth = CalculateCatchWidth(Scale);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Drop any fruit off the plate.
|
/// Drop any fruit off the plate.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
|
|
||||||
private readonly CatchComboDisplay comboDisplay;
|
private readonly CatchComboDisplay comboDisplay;
|
||||||
|
|
||||||
private readonly CatcherTrailDisplay catcherTrails;
|
public readonly CatcherTrailDisplay CatcherTrails;
|
||||||
|
|
||||||
private Catcher catcher = null!;
|
private Catcher catcher = null!;
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
catcherContainer = new Container<Catcher> { RelativeSizeAxes = Axes.Both },
|
catcherContainer = new Container<Catcher> { RelativeSizeAxes = Axes.Both },
|
||||||
catcherTrails = new CatcherTrailDisplay(),
|
CatcherTrails = new CatcherTrailDisplay(),
|
||||||
comboDisplay = new CatchComboDisplay
|
comboDisplay = new CatchComboDisplay
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.None,
|
RelativeSizeAxes = Axes.None,
|
||||||
@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
{
|
{
|
||||||
const double trail_generation_interval = 16;
|
const double trail_generation_interval = 16;
|
||||||
|
|
||||||
if (Time.Current - catcherTrails.LastDashTrailTime >= trail_generation_interval)
|
if (Time.Current - CatcherTrails.LastDashTrailTime >= trail_generation_interval)
|
||||||
displayCatcherTrail(Catcher.HyperDashing ? CatcherTrailAnimation.HyperDashing : CatcherTrailAnimation.Dashing);
|
displayCatcherTrail(Catcher.HyperDashing ? CatcherTrailAnimation.HyperDashing : CatcherTrailAnimation.Dashing);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,6 +170,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void displayCatcherTrail(CatcherTrailAnimation animation) => catcherTrails.Add(new CatcherTrailEntry(Time.Current, Catcher.CurrentState, Catcher.X, Catcher.BodyScale, animation));
|
private void displayCatcherTrail(CatcherTrailAnimation animation) => CatcherTrails.Add(new CatcherTrailEntry(Time.Current, Catcher.CurrentState, Catcher.X, Catcher.BodyScale, animation));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -10,6 +11,7 @@ using osu.Framework.Graphics.Pooling;
|
|||||||
using osu.Game.Rulesets.Catch.Skinning;
|
using osu.Game.Rulesets.Catch.Skinning;
|
||||||
using osu.Game.Rulesets.Objects.Pooling;
|
using osu.Game.Rulesets.Objects.Pooling;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.UI
|
namespace osu.Game.Rulesets.Catch.UI
|
||||||
@ -55,6 +57,25 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the scale of all trails.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="scale">The new body scale of the Catcher</param>
|
||||||
|
public void UpdateCatcherTrailsScale(Vector2 scale)
|
||||||
|
{
|
||||||
|
var oldEntries = Entries.ToList();
|
||||||
|
|
||||||
|
Clear();
|
||||||
|
|
||||||
|
foreach (var oldEntry in oldEntries)
|
||||||
|
{
|
||||||
|
// use magnitude of the new scale while preserving the sign of the old one in the X direction.
|
||||||
|
// the end effect is preserving the direction in which the trail sprites face, which is important.
|
||||||
|
var targetScale = new Vector2(Math.Abs(scale.X) * Math.Sign(oldEntry.Scale.X), Math.Abs(scale.Y));
|
||||||
|
Add(new CatcherTrailEntry(oldEntry.LifetimeStart, oldEntry.CatcherState, oldEntry.Position, targetScale, oldEntry.Animation));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
@ -118,5 +118,45 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
AddAssert("all objects in last column", () => EditorBeatmap.HitObjects.All(ho => ((ManiaHitObject)ho).Column == 3));
|
AddAssert("all objects in last column", () => EditorBeatmap.HitObjects.All(ho => ((ManiaHitObject)ho).Column == 3));
|
||||||
AddAssert("all objects remain selected", () => EditorBeatmap.SelectedHitObjects.SequenceEqual(EditorBeatmap.HitObjects));
|
AddAssert("all objects remain selected", () => EditorBeatmap.SelectedHitObjects.SequenceEqual(EditorBeatmap.HitObjects));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOffScreenObjectsRemainSelectedOnHorizontalFlip()
|
||||||
|
{
|
||||||
|
AddStep("create objects", () =>
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
EditorBeatmap.Add(new Note { StartTime = 1000 * i, Column = i % 4 });
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||||
|
AddStep("flip", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.H);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("all objects remain selected", () => EditorBeatmap.SelectedHitObjects.SequenceEqual(EditorBeatmap.HitObjects));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOffScreenObjectsRemainSelectedOnVerticalFlip()
|
||||||
|
{
|
||||||
|
AddStep("create objects", () =>
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
EditorBeatmap.Add(new Note { StartTime = 1000 * i, Column = i % 4 });
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||||
|
AddStep("flip", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.J);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("all objects remain selected", () => EditorBeatmap.SelectedHitObjects.SequenceEqual(EditorBeatmap.HitObjects.Reverse()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -54,9 +54,8 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
int firstColumn = flipOverOrigin ? 0 : selectedObjects.Min(ho => ho.Column);
|
int firstColumn = flipOverOrigin ? 0 : selectedObjects.Min(ho => ho.Column);
|
||||||
int lastColumn = flipOverOrigin ? (int)EditorBeatmap.BeatmapInfo.Difficulty.CircleSize - 1 : selectedObjects.Max(ho => ho.Column);
|
int lastColumn = flipOverOrigin ? (int)EditorBeatmap.BeatmapInfo.Difficulty.CircleSize - 1 : selectedObjects.Max(ho => ho.Column);
|
||||||
|
|
||||||
EditorBeatmap.PerformOnSelection(hitObject =>
|
performOnSelection(maniaObject =>
|
||||||
{
|
{
|
||||||
var maniaObject = (ManiaHitObject)hitObject;
|
|
||||||
maniaPlayfield.Remove(maniaObject);
|
maniaPlayfield.Remove(maniaObject);
|
||||||
maniaObject.Column = firstColumn + (lastColumn - maniaObject.Column);
|
maniaObject.Column = firstColumn + (lastColumn - maniaObject.Column);
|
||||||
maniaPlayfield.Add(maniaObject);
|
maniaPlayfield.Add(maniaObject);
|
||||||
@ -71,7 +70,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
double selectionStartTime = selectedObjects.Min(ho => ho.StartTime);
|
double selectionStartTime = selectedObjects.Min(ho => ho.StartTime);
|
||||||
double selectionEndTime = selectedObjects.Max(ho => ho.GetEndTime());
|
double selectionEndTime = selectedObjects.Max(ho => ho.GetEndTime());
|
||||||
|
|
||||||
EditorBeatmap.PerformOnSelection(hitObject =>
|
performOnSelection(hitObject =>
|
||||||
{
|
{
|
||||||
hitObject.StartTime = selectionStartTime + (selectionEndTime - hitObject.GetEndTime());
|
hitObject.StartTime = selectionStartTime + (selectionEndTime - hitObject.GetEndTime());
|
||||||
});
|
});
|
||||||
@ -117,14 +116,21 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
|
|
||||||
columnDelta = Math.Clamp(columnDelta, -minColumn, maniaPlayfield.TotalColumns - 1 - maxColumn);
|
columnDelta = Math.Clamp(columnDelta, -minColumn, maniaPlayfield.TotalColumns - 1 - maxColumn);
|
||||||
|
|
||||||
EditorBeatmap.PerformOnSelection(h =>
|
performOnSelection(h =>
|
||||||
{
|
{
|
||||||
maniaPlayfield.Remove(h);
|
maniaPlayfield.Remove(h);
|
||||||
((ManiaHitObject)h).Column += columnDelta;
|
h.Column += columnDelta;
|
||||||
maniaPlayfield.Add(h);
|
maniaPlayfield.Add(h);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// `HitObjectUsageEventBuffer`'s usage transferal flows and the playfield's `SetKeepAlive()` functionality do not combine well with this operation's usage pattern,
|
private void performOnSelection(Action<ManiaHitObject> action)
|
||||||
|
{
|
||||||
|
var selectedObjects = EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>().ToArray();
|
||||||
|
|
||||||
|
EditorBeatmap.PerformOnSelection(h => action.Invoke((ManiaHitObject)h));
|
||||||
|
|
||||||
|
// `HitObjectUsageEventBuffer`'s usage transferal flows and the playfield's `SetKeepAlive()` functionality do not combine well with mania's usage patterns,
|
||||||
// leading to selections being sometimes partially dropped if some of the objects being moved are off screen
|
// leading to selections being sometimes partially dropped if some of the objects being moved are off screen
|
||||||
// (check blame for detailed explanation).
|
// (check blame for detailed explanation).
|
||||||
// thus, ensure that selection is preserved manually.
|
// thus, ensure that selection is preserved manually.
|
||||||
|
@ -9,6 +9,7 @@ using osu.Framework.Utils;
|
|||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Osu.Edit;
|
using osu.Game.Rulesets.Osu.Edit;
|
||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
@ -129,6 +130,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
|
|
||||||
private void gridActive<T>(bool active) where T : PositionSnapGrid
|
private void gridActive<T>(bool active) where T : PositionSnapGrid
|
||||||
{
|
{
|
||||||
|
AddAssert($"grid type is {typeof(T).Name}", () => this.ChildrenOfType<T>().Any());
|
||||||
AddStep("choose placement tool", () => InputManager.Key(Key.Number2));
|
AddStep("choose placement tool", () => InputManager.Key(Key.Number2));
|
||||||
AddStep("move cursor to spacing + (1, 1)", () =>
|
AddStep("move cursor to spacing + (1, 1)", () =>
|
||||||
{
|
{
|
||||||
@ -161,7 +163,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
return grid switch
|
return grid switch
|
||||||
{
|
{
|
||||||
RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value),
|
RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value),
|
||||||
TriangularPositionSnapGrid triangular => triangular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(triangular.Spacing.Value / 2, triangular.Spacing.Value / 2 * MathF.Sqrt(3)), -triangular.GridLineRotation.Value),
|
TriangularPositionSnapGrid triangular => triangular.StartPosition.Value + GeometryUtils.RotateVector(
|
||||||
|
new Vector2(triangular.Spacing.Value / 2, triangular.Spacing.Value / 2 * MathF.Sqrt(3)), -triangular.GridLineRotation.Value),
|
||||||
CircularPositionSnapGrid circular => circular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(circular.Spacing.Value, 0), -45),
|
CircularPositionSnapGrid circular => circular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(circular.Spacing.Value, 0), -45),
|
||||||
_ => Vector2.Zero
|
_ => Vector2.Zero
|
||||||
};
|
};
|
||||||
@ -170,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestGridSizeToggling()
|
public void TestGridSizeToggling()
|
||||||
{
|
{
|
||||||
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
|
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
|
||||||
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
|
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
|
||||||
gridSizeIs(4);
|
gridSizeIs(4);
|
||||||
|
|
||||||
@ -189,5 +192,97 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
private void gridSizeIs(int size)
|
private void gridSizeIs(int size)
|
||||||
=> AddAssert($"grid size is {size}", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Single().Spacing.Value == new Vector2(size)
|
=> AddAssert($"grid size is {size}", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Single().Spacing.Value == new Vector2(size)
|
||||||
&& EditorBeatmap.BeatmapInfo.GridSize == size);
|
&& EditorBeatmap.BeatmapInfo.GridSize == size);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestGridTypeToggling()
|
||||||
|
{
|
||||||
|
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
|
||||||
|
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
|
||||||
|
gridActive<RectangularPositionSnapGrid>(true);
|
||||||
|
|
||||||
|
nextGridTypeIs<TriangularPositionSnapGrid>();
|
||||||
|
nextGridTypeIs<CircularPositionSnapGrid>();
|
||||||
|
nextGridTypeIs<RectangularPositionSnapGrid>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void nextGridTypeIs<T>() where T : PositionSnapGrid
|
||||||
|
{
|
||||||
|
AddStep("toggle to next grid type", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ShiftLeft);
|
||||||
|
InputManager.Key(Key.G);
|
||||||
|
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||||
|
});
|
||||||
|
gridActive<T>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestGridPlacementTool()
|
||||||
|
{
|
||||||
|
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
|
||||||
|
|
||||||
|
AddStep("start grid placement", () => InputManager.Key(Key.Number5));
|
||||||
|
AddStep("move cursor to slider head + (1, 1)", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
InputManager.MoveMouseTo(composer.ToScreenSpace(((Slider)EditorBeatmap.HitObjects.First()).Position + new Vector2(1, 1)));
|
||||||
|
});
|
||||||
|
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
AddStep("move cursor to slider tail + (1, 1)", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
InputManager.MoveMouseTo(composer.ToScreenSpace(((Slider)EditorBeatmap.HitObjects.First()).EndPosition + new Vector2(1, 1)));
|
||||||
|
});
|
||||||
|
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
|
||||||
|
gridActive<RectangularPositionSnapGrid>(true);
|
||||||
|
AddAssert("grid position at slider head", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
return Precision.AlmostEquals(((Slider)EditorBeatmap.HitObjects.First()).Position, composer.StartPosition.Value);
|
||||||
|
});
|
||||||
|
AddAssert("grid spacing is distance to slider tail", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01)
|
||||||
|
&& Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y);
|
||||||
|
});
|
||||||
|
AddAssert("grid rotation points to slider tail", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
return Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("start grid placement", () => InputManager.Key(Key.Number5));
|
||||||
|
AddStep("move cursor to slider tail + (1, 1)", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
InputManager.MoveMouseTo(composer.ToScreenSpace(((Slider)EditorBeatmap.HitObjects.First()).EndPosition + new Vector2(1, 1)));
|
||||||
|
});
|
||||||
|
AddStep("double click", () =>
|
||||||
|
{
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
AddStep("move cursor to (0, 0)", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
InputManager.MoveMouseTo(composer.ToScreenSpace(Vector2.Zero));
|
||||||
|
});
|
||||||
|
|
||||||
|
gridActive<RectangularPositionSnapGrid>(true);
|
||||||
|
AddAssert("grid position at slider tail", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
return Precision.AlmostEquals(((Slider)EditorBeatmap.HitObjects.First()).EndPosition, composer.StartPosition.Value);
|
||||||
|
});
|
||||||
|
AddAssert("grid spacing and rotation unchanged", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||||
|
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01)
|
||||||
|
&& Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y)
|
||||||
|
&& Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
87
osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderDrawing.cs
Normal file
87
osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderDrawing.cs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||||
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public partial class TestSceneSliderDrawing : TestSceneOsuEditor
|
||||||
|
{
|
||||||
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTouchInputAfterTouchingComposeArea()
|
||||||
|
{
|
||||||
|
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "HitCircle")));
|
||||||
|
|
||||||
|
// this input is just for interacting with compose area
|
||||||
|
AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single()));
|
||||||
|
|
||||||
|
AddStep("move current time", () => InputManager.Key(Key.Right));
|
||||||
|
|
||||||
|
AddStep("tap to place circle", () => tap(this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(10, 10))));
|
||||||
|
AddAssert("circle placed correctly", () =>
|
||||||
|
{
|
||||||
|
var circle = (HitCircle)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate);
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(circle.Position.X, Is.EqualTo(10f).Within(0.01f));
|
||||||
|
Assert.That(circle.Position.Y, Is.EqualTo(10f).Within(0.01f));
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("tap slider", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "Slider")));
|
||||||
|
|
||||||
|
// this input is just for interacting with compose area
|
||||||
|
AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single()));
|
||||||
|
|
||||||
|
AddStep("move current time", () => InputManager.Key(Key.Right));
|
||||||
|
|
||||||
|
AddStep("hold to draw slider", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(50, 20)))));
|
||||||
|
AddStep("drag to draw", () => InputManager.MoveTouchTo(new Touch(TouchSource.Touch1, this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(200, 50)))));
|
||||||
|
AddAssert("selection not initiated", () => this.ChildrenOfType<DragBox>().All(d => d.State == Visibility.Hidden));
|
||||||
|
AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value)));
|
||||||
|
AddAssert("slider placed correctly", () =>
|
||||||
|
{
|
||||||
|
var slider = (Slider)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate);
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(slider.Position.X, Is.EqualTo(50f).Within(0.01f));
|
||||||
|
Assert.That(slider.Position.Y, Is.EqualTo(20f).Within(0.01f));
|
||||||
|
Assert.That(slider.Path.ControlPoints.Count, Is.EqualTo(2));
|
||||||
|
Assert.That(slider.Path.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
|
||||||
|
|
||||||
|
// the final position may be slightly off from the mouse position when drawing, account for that.
|
||||||
|
Assert.That(slider.Path.ControlPoints[1].Position.X, Is.EqualTo(150).Within(5));
|
||||||
|
Assert.That(slider.Path.ControlPoints[1].Position.Y, Is.EqualTo(30).Within(5));
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tap(Drawable drawable) => tap(drawable.ScreenSpaceDrawQuad.Centre);
|
||||||
|
|
||||||
|
private void tap(Vector2 position)
|
||||||
|
{
|
||||||
|
InputManager.BeginTouch(new Touch(TouchSource.Touch1, position));
|
||||||
|
InputManager.EndTouch(new Touch(TouchSource.Touch1, position));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
@ -392,6 +393,29 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
assertFinalControlPointType(3, null);
|
assertFinalControlPointType(3, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSliderDrawingViaTouch()
|
||||||
|
{
|
||||||
|
Vector2 startPoint = new Vector2(200);
|
||||||
|
|
||||||
|
AddStep("move mouse to a random point", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(Vector2.Zero)));
|
||||||
|
AddStep("begin touch at start point", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, InputManager.ToScreenSpace(startPoint))));
|
||||||
|
|
||||||
|
for (int i = 1; i < 20; i++)
|
||||||
|
addTouchMovementStep(startPoint + new Vector2(i * 40, MathF.Sin(i * MathF.PI / 5) * 50));
|
||||||
|
|
||||||
|
AddStep("release touch at end point", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value)));
|
||||||
|
|
||||||
|
assertPlaced(true);
|
||||||
|
assertLength(808, tolerance: 10);
|
||||||
|
assertControlPointCount(5);
|
||||||
|
assertFinalControlPointType(0, PathType.BSpline(4));
|
||||||
|
assertFinalControlPointType(1, null);
|
||||||
|
assertFinalControlPointType(2, null);
|
||||||
|
assertFinalControlPointType(3, null);
|
||||||
|
assertFinalControlPointType(4, null);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestPlacePerfectCurveSegmentAlmostLinearlyExterior()
|
public void TestPlacePerfectCurveSegmentAlmostLinearlyExterior()
|
||||||
{
|
{
|
||||||
@ -492,6 +516,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
|
|
||||||
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
|
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
|
||||||
|
|
||||||
|
private void addTouchMovementStep(Vector2 position) => AddStep($"move touch1 to {position}", () => InputManager.MoveTouchTo(new Touch(TouchSource.Touch1, InputManager.ToScreenSpace(position))));
|
||||||
|
|
||||||
private void addClickStep(MouseButton button)
|
private void addClickStep(MouseButton button)
|
||||||
{
|
{
|
||||||
AddStep($"click {button}", () => InputManager.Click(button));
|
AddStep($"click {button}", () => InputManager.Click(button));
|
||||||
|
@ -180,7 +180,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
{
|
{
|
||||||
if (slider == null) return;
|
if (slider == null) return;
|
||||||
|
|
||||||
sample = new HitSampleInfo("hitwhistle", HitSampleInfo.BANK_SOFT, volume: 70);
|
sample = new HitSampleInfo("hitwhistle", HitSampleInfo.BANK_SOFT, volume: 70, editorAutoBank: false);
|
||||||
slider.Samples.Add(sample.With());
|
slider.Samples.Add(sample.With());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
55
osu.Game.Rulesets.Osu.Tests/Editor/TestSceneToolSwitching.cs
Normal file
55
osu.Game.Rulesets.Osu.Tests/Editor/TestSceneToolSwitching.cs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||||
|
{
|
||||||
|
public partial class TestSceneToolSwitching : EditorTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSliderAnchorMoveOperationEndsOnSwitchingTool()
|
||||||
|
{
|
||||||
|
var initialPosition = Vector2.Zero;
|
||||||
|
|
||||||
|
AddStep("store original anchor position", () => initialPosition = EditorBeatmap.HitObjects.OfType<Slider>().First().Path.ControlPoints.ElementAt(1).Position);
|
||||||
|
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType<Slider>().First()));
|
||||||
|
AddStep("move to second anchor", () => InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1)));
|
||||||
|
AddStep("start dragging", () => InputManager.PressButton(MouseButton.Left));
|
||||||
|
AddStep("drag away", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(0, -200)));
|
||||||
|
AddStep("switch tool", () => InputManager.PressButton(MouseButton.Button1));
|
||||||
|
AddStep("undo", () => Editor.Undo());
|
||||||
|
AddAssert("anchor back at original position",
|
||||||
|
() => EditorBeatmap.HitObjects.OfType<Slider>().First().Path.ControlPoints.ElementAt(1).Position,
|
||||||
|
() => Is.EqualTo(initialPosition));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSliderAnchorCreationOperationEndsOnSwitchingTool()
|
||||||
|
{
|
||||||
|
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType<Slider>().First()));
|
||||||
|
AddStep("move to second anchor", () => InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1), new Vector2(-50, 0)));
|
||||||
|
AddStep("quick-create anchor", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.PressButton(MouseButton.Left);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
AddStep("drag away", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(0, -200)));
|
||||||
|
AddStep("switch tool", () => InputManager.PressKey(Key.Number3));
|
||||||
|
AddStep("drag away further", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(0, -200)));
|
||||||
|
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType<Slider>().First()));
|
||||||
|
AddStep("undo", () => Editor.Undo());
|
||||||
|
AddAssert("slider has three anchors again", () => EditorBeatmap.HitObjects.OfType<Slider>().First().Path.ControlPoints, () => Has.Count.EqualTo(3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMirror.cs
Normal file
64
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMirror.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
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.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||||
|
{
|
||||||
|
public partial class TestSceneOsuModMirror : OsuModTestScene
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestCorrectReflections([Values] OsuModMirror.MirrorType type, [Values] bool withStrictTracking) => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Autoplay = true,
|
||||||
|
Beatmap = new OsuBeatmap
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new Slider
|
||||||
|
{
|
||||||
|
Position = new Vector2(0),
|
||||||
|
Path = new SliderPath
|
||||||
|
{
|
||||||
|
ControlPoints =
|
||||||
|
{
|
||||||
|
new PathControlPoint(),
|
||||||
|
new PathControlPoint(new Vector2(100, 0))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TickDistanceMultiplier = 0.5,
|
||||||
|
RepeatCount = 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Mods = withStrictTracking
|
||||||
|
? [new OsuModMirror { Reflection = { Value = type } }, new OsuModStrictTracking()]
|
||||||
|
: [new OsuModMirror { Reflection = { Value = type } }],
|
||||||
|
PassCondition = () =>
|
||||||
|
{
|
||||||
|
var slider = this.ChildrenOfType<DrawableSlider>().SingleOrDefault();
|
||||||
|
var playfield = this.ChildrenOfType<OsuPlayfield>().Single();
|
||||||
|
|
||||||
|
if (slider == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Precision.AlmostEquals(playfield.ToLocalSpace(slider.HeadCircle.ScreenSpaceDrawQuad.Centre), slider.HitObject.Position)
|
||||||
|
&& Precision.AlmostEquals(playfield.ToLocalSpace(slider.TailCircle.ScreenSpaceDrawQuad.Centre), slider.HitObject.Position)
|
||||||
|
&& Precision.AlmostEquals(playfield.ToLocalSpace(slider.NestedHitObjects.OfType<DrawableSliderRepeat>().Single().ScreenSpaceDrawQuad.Centre),
|
||||||
|
slider.HitObject.Position + slider.HitObject.Path.PositionAt(1))
|
||||||
|
&& Precision.AlmostEquals(playfield.ToLocalSpace(slider.NestedHitObjects.OfType<DrawableSliderTick>().First().ScreenSpaceDrawQuad.Centre),
|
||||||
|
slider.HitObject.Position + slider.HitObject.Path.PositionAt(0.7f));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="Moq" Version="4.18.4" />
|
<PackageReference Include="Moq" Version="4.18.4" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
|
@ -25,6 +25,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
private int countMeh;
|
private int countMeh;
|
||||||
private int countMiss;
|
private int countMiss;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Missed slider ticks that includes missed reverse arrows. Will only be correct on non-classic scores
|
||||||
|
/// </summary>
|
||||||
|
private int countSliderTickMiss;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Amount of missed slider tails that don't break combo. Will only be correct on non-classic scores
|
||||||
|
/// </summary>
|
||||||
|
private int countSliderEndsDropped;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Estimated total amount of combo breaks
|
||||||
|
/// </summary>
|
||||||
private double effectiveMissCount;
|
private double effectiveMissCount;
|
||||||
|
|
||||||
public OsuPerformanceCalculator()
|
public OsuPerformanceCalculator()
|
||||||
@ -44,7 +57,38 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||||
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||||
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||||
effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
|
countSliderEndsDropped = osuAttributes.SliderCount - score.Statistics.GetValueOrDefault(HitResult.SliderTailHit);
|
||||||
|
countSliderTickMiss = score.Statistics.GetValueOrDefault(HitResult.LargeTickMiss);
|
||||||
|
effectiveMissCount = countMiss;
|
||||||
|
|
||||||
|
if (osuAttributes.SliderCount > 0)
|
||||||
|
{
|
||||||
|
if (usingClassicSliderAccuracy)
|
||||||
|
{
|
||||||
|
// Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
|
||||||
|
// In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map
|
||||||
|
double fullComboThreshold = attributes.MaxCombo - 0.1 * osuAttributes.SliderCount;
|
||||||
|
|
||||||
|
if (scoreMaxCombo < fullComboThreshold)
|
||||||
|
effectiveMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
||||||
|
|
||||||
|
// In classic scores there can't be more misses than a sum of all non-perfect judgements
|
||||||
|
effectiveMissCount = Math.Min(effectiveMissCount, totalImperfectHits);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
double fullComboThreshold = attributes.MaxCombo - countSliderEndsDropped;
|
||||||
|
|
||||||
|
if (scoreMaxCombo < fullComboThreshold)
|
||||||
|
effectiveMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
||||||
|
|
||||||
|
// Combine regular misses with tick misses since tick misses break combo as well
|
||||||
|
effectiveMissCount = Math.Min(effectiveMissCount, countSliderTickMiss + countMiss);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
effectiveMissCount = Math.Max(countMiss, effectiveMissCount);
|
||||||
|
effectiveMissCount = Math.Min(totalHits, effectiveMissCount);
|
||||||
|
|
||||||
double multiplier = PERFORMANCE_BASE_MULTIPLIER;
|
double multiplier = PERFORMANCE_BASE_MULTIPLIER;
|
||||||
|
|
||||||
@ -124,8 +168,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
if (attributes.SliderCount > 0)
|
if (attributes.SliderCount > 0)
|
||||||
{
|
{
|
||||||
double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
|
double estimateImproperlyFollowedDifficultSliders;
|
||||||
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + attributes.SliderFactor;
|
|
||||||
|
if (usingClassicSliderAccuracy)
|
||||||
|
{
|
||||||
|
// When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders
|
||||||
|
int maximumPossibleDroppedSliders = totalImperfectHits;
|
||||||
|
estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We add tick misses here since they too mean that the player didn't follow the slider properly
|
||||||
|
// We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
|
||||||
|
estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, estimateDifficultSliders);
|
||||||
|
}
|
||||||
|
|
||||||
|
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / estimateDifficultSliders, 3) + attributes.SliderFactor;
|
||||||
aimValue *= sliderNerfFactor;
|
aimValue *= sliderNerfFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,29 +305,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
return flashlightValue;
|
return flashlightValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double calculateEffectiveMissCount(OsuDifficultyAttributes attributes)
|
|
||||||
{
|
|
||||||
// Guess the number of misses + slider breaks from combo
|
|
||||||
double comboBasedMissCount = 0.0;
|
|
||||||
|
|
||||||
if (attributes.SliderCount > 0)
|
|
||||||
{
|
|
||||||
double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount;
|
|
||||||
if (scoreMaxCombo < fullComboThreshold)
|
|
||||||
comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clamp miss count to maximum amount of possible breaks
|
|
||||||
comboBasedMissCount = Math.Min(comboBasedMissCount, countOk + countMeh + countMiss);
|
|
||||||
|
|
||||||
return Math.Max(countMiss, comboBasedMissCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Miss penalty assumes that a player will miss on the hardest parts of a map,
|
// Miss penalty assumes that a player will miss on the hardest parts of a map,
|
||||||
// so we use the amount of relatively difficult sections to adjust miss penalty
|
// so we use the amount of relatively difficult sections to adjust miss penalty
|
||||||
// to make it more punishing on maps with lower amount of hard sections.
|
// to make it more punishing on maps with lower amount of hard sections.
|
||||||
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1);
|
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1);
|
||||||
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
|
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
|
||||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||||
|
private int totalImperfectHits => countOk + countMeh + countMiss;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -333,6 +333,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
foreach (var p in Pieces)
|
foreach (var p in Pieces)
|
||||||
p.ControlPoint.Changed -= controlPointChanged;
|
p.ControlPoint.Changed -= controlPointChanged;
|
||||||
|
|
||||||
|
if (draggedControlPointIndex >= 0)
|
||||||
|
DragEnded();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void selectionRequested(PathControlPointPiece<T> piece, MouseButtonEvent e)
|
private void selectionRequested(PathControlPointPiece<T> piece, MouseButtonEvent e)
|
||||||
@ -392,7 +395,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
private Vector2[] dragStartPositions;
|
private Vector2[] dragStartPositions;
|
||||||
private PathType?[] dragPathTypes;
|
private PathType?[] dragPathTypes;
|
||||||
private int draggedControlPointIndex;
|
private int draggedControlPointIndex = -1;
|
||||||
private HashSet<PathControlPoint> selectedControlPoints;
|
private HashSet<PathControlPoint> selectedControlPoints;
|
||||||
|
|
||||||
private List<MenuItem> curveTypeItems;
|
private List<MenuItem> curveTypeItems;
|
||||||
@ -473,7 +476,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
EnsureValidPathTypes();
|
EnsureValidPathTypes();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DragEnded() => changeHandler?.EndChange();
|
public void DragEnded()
|
||||||
|
{
|
||||||
|
changeHandler?.EndChange();
|
||||||
|
draggedControlPointIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
@ -178,6 +178,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
{
|
{
|
||||||
base.OnDeselected();
|
base.OnDeselected();
|
||||||
|
|
||||||
|
if (placementControlPoint != null)
|
||||||
|
endControlPointPlacement();
|
||||||
|
|
||||||
updateVisualDefinition();
|
updateVisualDefinition();
|
||||||
BodyPiece.RecyclePath();
|
BodyPiece.RecyclePath();
|
||||||
}
|
}
|
||||||
@ -377,13 +380,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
protected override void OnMouseUp(MouseUpEvent e)
|
protected override void OnMouseUp(MouseUpEvent e)
|
||||||
{
|
{
|
||||||
if (placementControlPoint != null)
|
if (placementControlPoint != null)
|
||||||
{
|
endControlPointPlacement();
|
||||||
if (IsDragged)
|
}
|
||||||
ControlPointVisualiser?.DragEnded();
|
|
||||||
|
|
||||||
placementControlPoint = null;
|
private void endControlPointPlacement()
|
||||||
changeHandler?.EndChange();
|
{
|
||||||
}
|
if (IsDragged)
|
||||||
|
ControlPointVisualiser?.DragEnded();
|
||||||
|
|
||||||
|
placementControlPoint = null;
|
||||||
|
changeHandler?.EndChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnKeyDown(KeyDownEvent e)
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
|
@ -213,6 +213,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
GridLinesRotation.Disabled = v.NewValue == PositionSnapGridType.Circle;
|
GridLinesRotation.Disabled = v.NewValue == PositionSnapGridType.Circle;
|
||||||
|
|
||||||
|
gridTypeButtons.Items[(int)v.NewValue].Select();
|
||||||
|
|
||||||
switch (v.NewValue)
|
switch (v.NewValue)
|
||||||
{
|
{
|
||||||
case PositionSnapGridType.Square:
|
case PositionSnapGridType.Square:
|
||||||
@ -241,17 +243,16 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
return ((rotation + 360 + period * 0.5f) % period) - period * 0.5f;
|
return ((rotation + 360 + period * 0.5f) % period) - period * 0.5f;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void nextGridSize()
|
|
||||||
{
|
|
||||||
Spacing.Value = Spacing.Value * 2 >= max_automatic_spacing ? Spacing.Value / 8 : Spacing.Value * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
{
|
{
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
case GlobalAction.EditorCycleGridDisplayMode:
|
case GlobalAction.EditorCycleGridSpacing:
|
||||||
nextGridSize();
|
Spacing.Value = Spacing.Value * 2 >= max_automatic_spacing ? Spacing.Value / 8 : Spacing.Value * 2;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case GlobalAction.EditorCycleGridType:
|
||||||
|
GridType.Value = (PositionSnapGridType)(((int)GridType.Value + 1) % Enum.GetValues<PositionSnapGridType>().Length);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,39 +240,74 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
points = originalConvexHull!;
|
points = originalConvexHull!;
|
||||||
|
|
||||||
foreach (var point in points)
|
foreach (var point in points)
|
||||||
{
|
scale = clampToBounds(scale, point, Vector2.Zero, OsuPlayfield.BASE_SIZE);
|
||||||
scale = clampToBound(scale, point, Vector2.Zero);
|
|
||||||
scale = clampToBound(scale, point, OsuPlayfield.BASE_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
return scale;
|
||||||
|
|
||||||
float minPositiveComponent(Vector2 v) => MathF.Min(v.X < 0 ? float.PositiveInfinity : v.X, v.Y < 0 ? float.PositiveInfinity : v.Y);
|
// Clamps the scale vector s such that the point p scaled by s is within the rectangle defined by lowerBounds and upperBounds
|
||||||
|
Vector2 clampToBounds(Vector2 s, Vector2 p, Vector2 lowerBounds, Vector2 upperBounds)
|
||||||
Vector2 clampToBound(Vector2 s, Vector2 p, Vector2 bound)
|
|
||||||
{
|
{
|
||||||
p -= actualOrigin;
|
p -= actualOrigin;
|
||||||
bound -= actualOrigin;
|
lowerBounds -= actualOrigin;
|
||||||
|
upperBounds -= actualOrigin;
|
||||||
|
// a.X is the rotated X component of p with respect to the X bounds
|
||||||
|
// a.Y is the rotated X component of p with respect to the Y bounds
|
||||||
|
// b.X is the rotated Y component of p with respect to the X bounds
|
||||||
|
// b.Y is the rotated Y component of p with respect to the Y bounds
|
||||||
var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y);
|
var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y);
|
||||||
var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y);
|
var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y);
|
||||||
|
|
||||||
|
float sLowerBound, sUpperBound;
|
||||||
|
|
||||||
switch (adjustAxis)
|
switch (adjustAxis)
|
||||||
{
|
{
|
||||||
case Axes.X:
|
case Axes.X:
|
||||||
s.X = MathF.Min(scale.X, minPositiveComponent(Vector2.Divide(bound - b, a)));
|
(sLowerBound, sUpperBound) = computeBounds(lowerBounds - b, upperBounds - b, a);
|
||||||
|
s.X = MathHelper.Clamp(s.X, sLowerBound, sUpperBound);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Axes.Y:
|
case Axes.Y:
|
||||||
s.Y = MathF.Min(scale.Y, minPositiveComponent(Vector2.Divide(bound - a, b)));
|
(sLowerBound, sUpperBound) = computeBounds(lowerBounds - a, upperBounds - a, b);
|
||||||
|
s.Y = MathHelper.Clamp(s.Y, sLowerBound, sUpperBound);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Axes.Both:
|
case Axes.Both:
|
||||||
s = Vector2.ComponentMin(s, s * minPositiveComponent(Vector2.Divide(bound, a * s.X + b * s.Y)));
|
// Here we compute the bounds for the magnitude multiplier of the scale vector
|
||||||
|
// Therefore the ratio s.X / s.Y will be maintained
|
||||||
|
(sLowerBound, sUpperBound) = computeBounds(lowerBounds, upperBounds, a * s.X + b * s.Y);
|
||||||
|
s.X = s.X < 0
|
||||||
|
? MathHelper.Clamp(s.X, s.X * sUpperBound, s.X * sLowerBound)
|
||||||
|
: MathHelper.Clamp(s.X, s.X * sLowerBound, s.X * sUpperBound);
|
||||||
|
s.Y = s.Y < 0
|
||||||
|
? MathHelper.Clamp(s.Y, s.Y * sUpperBound, s.Y * sLowerBound)
|
||||||
|
: MathHelper.Clamp(s.Y, s.Y * sLowerBound, s.Y * sUpperBound);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Computes the bounds for the magnitude of the scaled point p with respect to the bounds lowerBounds and upperBounds
|
||||||
|
(float, float) computeBounds(Vector2 lowerBounds, Vector2 upperBounds, Vector2 p)
|
||||||
|
{
|
||||||
|
var sLowerBounds = Vector2.Divide(lowerBounds, p);
|
||||||
|
var sUpperBounds = Vector2.Divide(upperBounds, p);
|
||||||
|
|
||||||
|
// If the point is negative, then the bounds are flipped
|
||||||
|
if (p.X < 0)
|
||||||
|
(sLowerBounds.X, sUpperBounds.X) = (sUpperBounds.X, sLowerBounds.X);
|
||||||
|
if (p.Y < 0)
|
||||||
|
(sLowerBounds.Y, sUpperBounds.Y) = (sUpperBounds.Y, sLowerBounds.Y);
|
||||||
|
|
||||||
|
// If the point is at zero, then any scale will have no effect on the point so the bounds are infinite
|
||||||
|
// The float division would already give us infinity for the bounds, but the sign is not consistent so we have to manually set it
|
||||||
|
if (Precision.AlmostEquals(p.X, 0))
|
||||||
|
(sLowerBounds.X, sUpperBounds.X) = (float.NegativeInfinity, float.PositiveInfinity);
|
||||||
|
if (Precision.AlmostEquals(p.Y, 0))
|
||||||
|
(sLowerBounds.Y, sUpperBounds.Y) = (float.NegativeInfinity, float.PositiveInfinity);
|
||||||
|
|
||||||
|
return (MathF.Max(sLowerBounds.X, sLowerBounds.Y), MathF.Min(sUpperBounds.X, sUpperBounds.Y));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void moveSelectionInBounds()
|
private void moveSelectionInBounds()
|
||||||
|
@ -53,6 +53,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
|
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
|
||||||
|
|
||||||
Child = new FillFlowContainer
|
Child = new FillFlowContainer
|
||||||
{
|
{
|
||||||
Width = 220,
|
Width = 220,
|
||||||
|
@ -5,10 +5,14 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
@ -22,13 +26,17 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
private readonly OsuGridToolboxGroup gridToolbox;
|
private readonly OsuGridToolboxGroup gridToolbox;
|
||||||
|
|
||||||
private readonly Bindable<PreciseRotationInfo> rotationInfo = new Bindable<PreciseRotationInfo>(new PreciseRotationInfo(0, RotationOrigin.GridCentre));
|
private readonly Bindable<PreciseRotationInfo> rotationInfo = new Bindable<PreciseRotationInfo>(new PreciseRotationInfo(0, EditorOrigin.GridCentre));
|
||||||
|
|
||||||
private SliderWithTextBoxInput<float> angleInput = null!;
|
private SliderWithTextBoxInput<float> angleInput = null!;
|
||||||
private EditorRadioButtonCollection rotationOrigin = null!;
|
private EditorRadioButtonCollection rotationOrigin = null!;
|
||||||
|
|
||||||
|
private RadioButton gridCentreButton = null!;
|
||||||
|
private RadioButton playfieldCentreButton = null!;
|
||||||
private RadioButton selectionCentreButton = null!;
|
private RadioButton selectionCentreButton = null!;
|
||||||
|
|
||||||
|
private Bindable<EditorOrigin> configRotationOrigin = null!;
|
||||||
|
|
||||||
public PreciseRotationPopover(SelectionRotationHandler rotationHandler, OsuGridToolboxGroup gridToolbox)
|
public PreciseRotationPopover(SelectionRotationHandler rotationHandler, OsuGridToolboxGroup gridToolbox)
|
||||||
{
|
{
|
||||||
this.rotationHandler = rotationHandler;
|
this.rotationHandler = rotationHandler;
|
||||||
@ -38,8 +46,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load(OsuConfigManager config)
|
||||||
{
|
{
|
||||||
|
configRotationOrigin = config.GetBindable<EditorOrigin>(OsuSetting.EditorRotationOrigin);
|
||||||
|
|
||||||
Child = new FillFlowContainer
|
Child = new FillFlowContainer
|
||||||
{
|
{
|
||||||
Width = 220,
|
Width = 220,
|
||||||
@ -55,6 +65,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
MaxValue = 360,
|
MaxValue = 360,
|
||||||
Precision = 1
|
Precision = 1
|
||||||
},
|
},
|
||||||
|
KeyboardStep = 1f,
|
||||||
Instantaneous = true
|
Instantaneous = true
|
||||||
},
|
},
|
||||||
rotationOrigin = new EditorRadioButtonCollection
|
rotationOrigin = new EditorRadioButtonCollection
|
||||||
@ -62,14 +73,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Items = new[]
|
Items = new[]
|
||||||
{
|
{
|
||||||
new RadioButton("Grid centre",
|
gridCentreButton = new RadioButton("Grid centre",
|
||||||
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.GridCentre },
|
() => rotationInfo.Value = rotationInfo.Value with { Origin = EditorOrigin.GridCentre },
|
||||||
() => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }),
|
() => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }),
|
||||||
new RadioButton("Playfield centre",
|
playfieldCentreButton = new RadioButton("Playfield centre",
|
||||||
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre },
|
() => rotationInfo.Value = rotationInfo.Value with { Origin = EditorOrigin.PlayfieldCentre },
|
||||||
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
||||||
selectionCentreButton = new RadioButton("Selection centre",
|
selectionCentreButton = new RadioButton("Selection centre",
|
||||||
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.SelectionCentre },
|
() => rotationInfo.Value = rotationInfo.Value with { Origin = EditorOrigin.SelectionCentre },
|
||||||
() => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare })
|
() => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -91,13 +102,63 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
angleInput.SelectAll();
|
angleInput.SelectAll();
|
||||||
});
|
});
|
||||||
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
|
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
|
||||||
rotationOrigin.Items.First().Select();
|
|
||||||
|
|
||||||
rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e =>
|
rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e =>
|
||||||
{
|
{
|
||||||
selectionCentreButton.Selected.Disabled = !e.NewValue;
|
selectionCentreButton.Selected.Disabled = !e.NewValue;
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
bool didSelect = false;
|
||||||
|
|
||||||
|
configRotationOrigin.BindValueChanged(val =>
|
||||||
|
{
|
||||||
|
switch (configRotationOrigin.Value)
|
||||||
|
{
|
||||||
|
case EditorOrigin.GridCentre:
|
||||||
|
if (!gridCentreButton.Selected.Disabled)
|
||||||
|
{
|
||||||
|
gridCentreButton.Select();
|
||||||
|
didSelect = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EditorOrigin.PlayfieldCentre:
|
||||||
|
if (!playfieldCentreButton.Selected.Disabled)
|
||||||
|
{
|
||||||
|
playfieldCentreButton.Select();
|
||||||
|
didSelect = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EditorOrigin.SelectionCentre:
|
||||||
|
if (!selectionCentreButton.Selected.Disabled)
|
||||||
|
{
|
||||||
|
selectionCentreButton.Select();
|
||||||
|
didSelect = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
if (!didSelect)
|
||||||
|
rotationOrigin.Items.First(b => !b.Selected.Disabled).Select();
|
||||||
|
|
||||||
|
gridCentreButton.Selected.BindValueChanged(b =>
|
||||||
|
{
|
||||||
|
if (b.NewValue) configRotationOrigin.Value = EditorOrigin.GridCentre;
|
||||||
|
});
|
||||||
|
playfieldCentreButton.Selected.BindValueChanged(b =>
|
||||||
|
{
|
||||||
|
if (b.NewValue) configRotationOrigin.Value = EditorOrigin.PlayfieldCentre;
|
||||||
|
});
|
||||||
|
selectionCentreButton.Selected.BindValueChanged(b =>
|
||||||
|
{
|
||||||
|
if (b.NewValue) configRotationOrigin.Value = EditorOrigin.SelectionCentre;
|
||||||
|
});
|
||||||
|
|
||||||
rotationInfo.BindValueChanged(rotation =>
|
rotationInfo.BindValueChanged(rotation =>
|
||||||
{
|
{
|
||||||
rotationHandler.Update(rotation.NewValue.Degrees, getOriginPosition(rotation.NewValue));
|
rotationHandler.Update(rotation.NewValue.Degrees, getOriginPosition(rotation.NewValue));
|
||||||
@ -107,9 +168,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
private Vector2? getOriginPosition(PreciseRotationInfo rotation) =>
|
private Vector2? getOriginPosition(PreciseRotationInfo rotation) =>
|
||||||
rotation.Origin switch
|
rotation.Origin switch
|
||||||
{
|
{
|
||||||
RotationOrigin.GridCentre => gridToolbox.StartPosition.Value,
|
EditorOrigin.GridCentre => gridToolbox.StartPosition.Value,
|
||||||
RotationOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2,
|
EditorOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2,
|
||||||
RotationOrigin.SelectionCentre => null,
|
EditorOrigin.SelectionCentre => null,
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(rotation))
|
_ => throw new ArgumentOutOfRangeException(nameof(rotation))
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -126,14 +187,18 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
if (IsLoaded)
|
if (IsLoaded)
|
||||||
rotationHandler.Commit();
|
rotationHandler.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
|
{
|
||||||
|
if (e.Action == GlobalAction.Select && !e.Repeat)
|
||||||
|
{
|
||||||
|
this.HidePopover();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnPressed(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum RotationOrigin
|
public record PreciseRotationInfo(float Degrees, EditorOrigin Origin);
|
||||||
{
|
|
||||||
GridCentre,
|
|
||||||
PlayfieldCentre,
|
|
||||||
SelectionCentre
|
|
||||||
}
|
|
||||||
|
|
||||||
public record PreciseRotationInfo(float Degrees, RotationOrigin Origin);
|
|
||||||
}
|
}
|
||||||
|
@ -5,16 +5,21 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||||
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
@ -25,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
private readonly OsuGridToolboxGroup gridToolbox;
|
private readonly OsuGridToolboxGroup gridToolbox;
|
||||||
|
|
||||||
private readonly Bindable<PreciseScaleInfo> scaleInfo = new Bindable<PreciseScaleInfo>(new PreciseScaleInfo(1, ScaleOrigin.GridCentre, true, true));
|
private readonly Bindable<PreciseScaleInfo> scaleInfo = new Bindable<PreciseScaleInfo>(new PreciseScaleInfo(1, EditorOrigin.GridCentre, true, true));
|
||||||
|
|
||||||
private SliderWithTextBoxInput<float> scaleInput = null!;
|
private SliderWithTextBoxInput<float> scaleInput = null!;
|
||||||
private BindableNumber<float> scaleInputBindable = null!;
|
private BindableNumber<float> scaleInputBindable = null!;
|
||||||
@ -38,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
private OsuCheckbox xCheckBox = null!;
|
private OsuCheckbox xCheckBox = null!;
|
||||||
private OsuCheckbox yCheckBox = null!;
|
private OsuCheckbox yCheckBox = null!;
|
||||||
|
|
||||||
|
private Bindable<EditorOrigin> configScaleOrigin = null!;
|
||||||
|
|
||||||
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
|
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
|
||||||
|
|
||||||
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler, OsuGridToolboxGroup gridToolbox)
|
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler, OsuGridToolboxGroup gridToolbox)
|
||||||
@ -49,10 +56,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(EditorBeatmap editorBeatmap)
|
private void load(EditorBeatmap editorBeatmap, OsuConfigManager config)
|
||||||
{
|
{
|
||||||
selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
|
selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
|
||||||
|
|
||||||
|
configScaleOrigin = config.GetBindable<EditorOrigin>(OsuSetting.EditorScaleOrigin);
|
||||||
|
|
||||||
Child = new FillFlowContainer
|
Child = new FillFlowContainer
|
||||||
{
|
{
|
||||||
Width = 220,
|
Width = 220,
|
||||||
@ -64,12 +73,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
Current = scaleInputBindable = new BindableNumber<float>
|
Current = scaleInputBindable = new BindableNumber<float>
|
||||||
{
|
{
|
||||||
MinValue = 0.5f,
|
MinValue = 0.05f,
|
||||||
MaxValue = 2,
|
MaxValue = 2,
|
||||||
Precision = 0.001f,
|
Precision = 0.001f,
|
||||||
Value = 1,
|
Value = 1,
|
||||||
Default = 1,
|
Default = 1,
|
||||||
},
|
},
|
||||||
|
KeyboardStep = 0.01f,
|
||||||
Instantaneous = true
|
Instantaneous = true
|
||||||
},
|
},
|
||||||
scaleOrigin = new EditorRadioButtonCollection
|
scaleOrigin = new EditorRadioButtonCollection
|
||||||
@ -78,13 +88,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
Items = new[]
|
Items = new[]
|
||||||
{
|
{
|
||||||
gridCentreButton = new RadioButton("Grid centre",
|
gridCentreButton = new RadioButton("Grid centre",
|
||||||
() => setOrigin(ScaleOrigin.GridCentre),
|
() => setOrigin(EditorOrigin.GridCentre),
|
||||||
() => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }),
|
() => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }),
|
||||||
playfieldCentreButton = new RadioButton("Playfield centre",
|
playfieldCentreButton = new RadioButton("Playfield centre",
|
||||||
() => setOrigin(ScaleOrigin.PlayfieldCentre),
|
() => setOrigin(EditorOrigin.PlayfieldCentre),
|
||||||
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
||||||
selectionCentreButton = new RadioButton("Selection centre",
|
selectionCentreButton = new RadioButton("Selection centre",
|
||||||
() => setOrigin(ScaleOrigin.SelectionCentre),
|
() => setOrigin(EditorOrigin.SelectionCentre),
|
||||||
() => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare })
|
() => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -136,14 +146,81 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
});
|
});
|
||||||
scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue });
|
scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue });
|
||||||
|
|
||||||
xCheckBox.Current.BindValueChanged(x => setAxis(x.NewValue, yCheckBox.Current.Value));
|
xCheckBox.Current.BindValueChanged(_ =>
|
||||||
yCheckBox.Current.BindValueChanged(y => setAxis(xCheckBox.Current.Value, y.NewValue));
|
{
|
||||||
|
if (!xCheckBox.Current.Value && !yCheckBox.Current.Value)
|
||||||
|
{
|
||||||
|
yCheckBox.Current.Value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAxes();
|
||||||
|
});
|
||||||
|
yCheckBox.Current.BindValueChanged(_ =>
|
||||||
|
{
|
||||||
|
if (!xCheckBox.Current.Value && !yCheckBox.Current.Value)
|
||||||
|
{
|
||||||
|
xCheckBox.Current.Value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAxes();
|
||||||
|
});
|
||||||
|
|
||||||
selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value);
|
selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value);
|
||||||
playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled;
|
playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled;
|
||||||
gridCentreButton.Selected.Disabled = playfieldCentreButton.Selected.Disabled;
|
gridCentreButton.Selected.Disabled = playfieldCentreButton.Selected.Disabled;
|
||||||
|
|
||||||
scaleOrigin.Items.First(b => !b.Selected.Disabled).Select();
|
bool didSelect = false;
|
||||||
|
|
||||||
|
configScaleOrigin.BindValueChanged(val =>
|
||||||
|
{
|
||||||
|
switch (configScaleOrigin.Value)
|
||||||
|
{
|
||||||
|
case EditorOrigin.GridCentre:
|
||||||
|
if (!gridCentreButton.Selected.Disabled)
|
||||||
|
{
|
||||||
|
gridCentreButton.Select();
|
||||||
|
didSelect = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EditorOrigin.PlayfieldCentre:
|
||||||
|
if (!playfieldCentreButton.Selected.Disabled)
|
||||||
|
{
|
||||||
|
playfieldCentreButton.Select();
|
||||||
|
didSelect = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EditorOrigin.SelectionCentre:
|
||||||
|
if (!selectionCentreButton.Selected.Disabled)
|
||||||
|
{
|
||||||
|
selectionCentreButton.Select();
|
||||||
|
didSelect = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
if (!didSelect)
|
||||||
|
scaleOrigin.Items.First(b => !b.Selected.Disabled).Select();
|
||||||
|
|
||||||
|
gridCentreButton.Selected.BindValueChanged(b =>
|
||||||
|
{
|
||||||
|
if (b.NewValue) configScaleOrigin.Value = EditorOrigin.GridCentre;
|
||||||
|
});
|
||||||
|
playfieldCentreButton.Selected.BindValueChanged(b =>
|
||||||
|
{
|
||||||
|
if (b.NewValue) configScaleOrigin.Value = EditorOrigin.PlayfieldCentre;
|
||||||
|
});
|
||||||
|
selectionCentreButton.Selected.BindValueChanged(b =>
|
||||||
|
{
|
||||||
|
if (b.NewValue) configScaleOrigin.Value = EditorOrigin.SelectionCentre;
|
||||||
|
});
|
||||||
|
|
||||||
scaleInfo.BindValueChanged(scale =>
|
scaleInfo.BindValueChanged(scale =>
|
||||||
{
|
{
|
||||||
@ -152,9 +229,15 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateAxes()
|
||||||
|
{
|
||||||
|
scaleInfo.Value = scaleInfo.Value with { XAxis = xCheckBox.Current.Value, YAxis = yCheckBox.Current.Value };
|
||||||
|
updateMinMaxScale();
|
||||||
|
}
|
||||||
|
|
||||||
private void updateAxisCheckBoxesEnabled()
|
private void updateAxisCheckBoxesEnabled()
|
||||||
{
|
{
|
||||||
if (scaleInfo.Value.Origin != ScaleOrigin.SelectionCentre)
|
if (scaleInfo.Value.Origin != EditorOrigin.SelectionCentre)
|
||||||
{
|
{
|
||||||
toggleAxisAvailable(xCheckBox.Current, true);
|
toggleAxisAvailable(xCheckBox.Current, true);
|
||||||
toggleAxisAvailable(yCheckBox.Current, true);
|
toggleAxisAvailable(yCheckBox.Current, true);
|
||||||
@ -175,12 +258,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
axisBindable.Disabled = !available;
|
axisBindable.Disabled = !available;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMaxScale()
|
private void updateMinMaxScale()
|
||||||
{
|
{
|
||||||
if (!scaleHandler.OriginalSurroundingQuad.HasValue)
|
if (!scaleHandler.OriginalSurroundingQuad.HasValue)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const float min_scale = 0.05f;
|
||||||
const float max_scale = 10;
|
const float max_scale = 10;
|
||||||
|
|
||||||
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
|
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
|
||||||
|
|
||||||
if (!scaleInfo.Value.XAxis)
|
if (!scaleInfo.Value.XAxis)
|
||||||
@ -189,12 +274,21 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
scale.Y = max_scale;
|
scale.Y = max_scale;
|
||||||
|
|
||||||
scaleInputBindable.MaxValue = MathF.Max(1, MathF.Min(scale.X, scale.Y));
|
scaleInputBindable.MaxValue = MathF.Max(1, MathF.Min(scale.X, scale.Y));
|
||||||
|
|
||||||
|
scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(min_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
|
||||||
|
|
||||||
|
if (!scaleInfo.Value.XAxis)
|
||||||
|
scale.X = min_scale;
|
||||||
|
if (!scaleInfo.Value.YAxis)
|
||||||
|
scale.Y = min_scale;
|
||||||
|
|
||||||
|
scaleInputBindable.MinValue = MathF.Min(1, MathF.Max(scale.X, scale.Y));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setOrigin(ScaleOrigin origin)
|
private void setOrigin(EditorOrigin origin)
|
||||||
{
|
{
|
||||||
scaleInfo.Value = scaleInfo.Value with { Origin = origin };
|
scaleInfo.Value = scaleInfo.Value with { Origin = origin };
|
||||||
updateMaxScale();
|
updateMinMaxScale();
|
||||||
updateAxisCheckBoxesEnabled();
|
updateAxisCheckBoxesEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,13 +296,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
switch (scale.Origin)
|
switch (scale.Origin)
|
||||||
{
|
{
|
||||||
case ScaleOrigin.GridCentre:
|
case EditorOrigin.GridCentre:
|
||||||
return gridToolbox.StartPosition.Value;
|
return gridToolbox.StartPosition.Value;
|
||||||
|
|
||||||
case ScaleOrigin.PlayfieldCentre:
|
case EditorOrigin.PlayfieldCentre:
|
||||||
return OsuPlayfield.BASE_SIZE / 2;
|
return OsuPlayfield.BASE_SIZE / 2;
|
||||||
|
|
||||||
case ScaleOrigin.SelectionCentre:
|
case EditorOrigin.SelectionCentre:
|
||||||
if (selectedItems.Count == 1 && selectedItems.First() is Slider slider)
|
if (selectedItems.Count == 1 && selectedItems.First() is Slider slider)
|
||||||
return slider.Position;
|
return slider.Position;
|
||||||
|
|
||||||
@ -219,21 +313,26 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y;
|
private Axes getAdjustAxis(PreciseScaleInfo scale)
|
||||||
|
|
||||||
private float getRotation(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.GridCentre ? gridToolbox.GridLinesRotation.Value : 0;
|
|
||||||
|
|
||||||
private void setAxis(bool x, bool y)
|
|
||||||
{
|
{
|
||||||
scaleInfo.Value = scaleInfo.Value with { XAxis = x, YAxis = y };
|
var result = Axes.None;
|
||||||
updateMaxScale();
|
|
||||||
|
if (scale.XAxis)
|
||||||
|
result |= Axes.X;
|
||||||
|
|
||||||
|
if (scale.YAxis)
|
||||||
|
result |= Axes.Y;
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private float getRotation(PreciseScaleInfo scale) => scale.Origin == EditorOrigin.GridCentre ? gridToolbox.GridLinesRotation.Value : 0;
|
||||||
|
|
||||||
protected override void PopIn()
|
protected override void PopIn()
|
||||||
{
|
{
|
||||||
base.PopIn();
|
base.PopIn();
|
||||||
scaleHandler.Begin();
|
scaleHandler.Begin();
|
||||||
updateMaxScale();
|
updateMinMaxScale();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void PopOut()
|
protected override void PopOut()
|
||||||
@ -242,14 +341,18 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
if (IsLoaded) scaleHandler.Commit();
|
if (IsLoaded) scaleHandler.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
|
{
|
||||||
|
if (e.Action == GlobalAction.Select && !e.Repeat)
|
||||||
|
{
|
||||||
|
this.HidePopover();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnPressed(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ScaleOrigin
|
public record PreciseScaleInfo(float Scale, EditorOrigin Origin, bool XAxis, bool YAxis);
|
||||||
{
|
|
||||||
GridCentre,
|
|
||||||
PlayfieldCentre,
|
|
||||||
SelectionCentre
|
|
||||||
}
|
|
||||||
|
|
||||||
public record PreciseScaleInfo(float Scale, ScaleOrigin Origin, bool XAxis, bool YAxis);
|
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,13 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Mods
|
namespace osu.Game.Rulesets.Osu.Mods
|
||||||
{
|
{
|
||||||
@ -25,5 +28,14 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void Update(Playfield playfield)
|
||||||
|
{
|
||||||
|
base.Update(playfield);
|
||||||
|
OsuPlayfield osuPlayfield = (OsuPlayfield)playfield;
|
||||||
|
Debug.Assert(osuPlayfield.Cursor != null);
|
||||||
|
|
||||||
|
osuPlayfield.Cursor.ActiveCursor.Rotation = -CurrentRotation;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -120,6 +120,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
Position = Position + Path.PositionAt(e.PathProgress),
|
Position = Position + Path.PositionAt(e.PathProgress),
|
||||||
StackHeight = StackHeight,
|
StackHeight = StackHeight,
|
||||||
Scale = Scale,
|
Scale = Scale,
|
||||||
|
PathProgress = e.PathProgress,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -150,6 +151,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
Position = Position + Path.PositionAt(e.PathProgress),
|
Position = Position + Path.PositionAt(e.PathProgress),
|
||||||
StackHeight = StackHeight,
|
StackHeight = StackHeight,
|
||||||
Scale = Scale,
|
Scale = Scale,
|
||||||
|
PathProgress = e.PathProgress,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||||
@ -117,10 +116,9 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
if (osuObject is not Slider slider)
|
if (osuObject is not Slider slider)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
void reflectNestedObject(OsuHitObject nested) => nested.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - nested.Position.X, nested.Position.Y);
|
|
||||||
static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
|
static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
|
||||||
|
|
||||||
modifySlider(slider, reflectNestedObject, reflectControlPoint);
|
modifySlider(slider, reflectControlPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -134,10 +132,9 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
if (osuObject is not Slider slider)
|
if (osuObject is not Slider slider)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
void reflectNestedObject(OsuHitObject nested) => nested.Position = new Vector2(nested.Position.X, OsuPlayfield.BASE_SIZE.Y - nested.Position.Y);
|
|
||||||
static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(point.Position.X, -point.Position.Y);
|
static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(point.Position.X, -point.Position.Y);
|
||||||
|
|
||||||
modifySlider(slider, reflectNestedObject, reflectControlPoint);
|
modifySlider(slider, reflectControlPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -146,10 +143,9 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
/// <param name="slider">The slider to be flipped.</param>
|
/// <param name="slider">The slider to be flipped.</param>
|
||||||
public static void FlipSliderInPlaceHorizontally(Slider slider)
|
public static void FlipSliderInPlaceHorizontally(Slider slider)
|
||||||
{
|
{
|
||||||
void flipNestedObject(OsuHitObject nested) => nested.Position = new Vector2(slider.X - (nested.X - slider.X), nested.Y);
|
|
||||||
static void flipControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
|
static void flipControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
|
||||||
|
|
||||||
modifySlider(slider, flipNestedObject, flipControlPoint);
|
modifySlider(slider, flipControlPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -159,18 +155,13 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
/// <param name="rotation">The angle, measured in radians, to rotate the slider by.</param>
|
/// <param name="rotation">The angle, measured in radians, to rotate the slider by.</param>
|
||||||
public static void RotateSlider(Slider slider, float rotation)
|
public static void RotateSlider(Slider slider, float rotation)
|
||||||
{
|
{
|
||||||
void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position;
|
|
||||||
void rotateControlPoint(PathControlPoint point) => point.Position = rotateVector(point.Position, rotation);
|
void rotateControlPoint(PathControlPoint point) => point.Position = rotateVector(point.Position, rotation);
|
||||||
|
|
||||||
modifySlider(slider, rotateNestedObject, rotateControlPoint);
|
modifySlider(slider, rotateControlPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void modifySlider(Slider slider, Action<OsuHitObject> modifyNestedObject, Action<PathControlPoint> modifyControlPoint)
|
private static void modifySlider(Slider slider, Action<PathControlPoint> modifyControlPoint)
|
||||||
{
|
{
|
||||||
// No need to update the head and tail circles, since slider handles that when the new slider path is set
|
|
||||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(modifyNestedObject);
|
|
||||||
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(modifyNestedObject);
|
|
||||||
|
|
||||||
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
|
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
|
||||||
foreach (var point in controlPoints)
|
foreach (var point in controlPoints)
|
||||||
modifyControlPoint(point);
|
modifyControlPoint(point);
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Tests.Editor
|
||||||
|
{
|
||||||
|
public partial class TestSceneEditorPlacement : EditorTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreateEditorRuleset() => new TaikoRuleset();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlacementBlueprintDoesNotCauseCrashes()
|
||||||
|
{
|
||||||
|
AddStep("clear objects", () => EditorBeatmap.Clear());
|
||||||
|
AddStep("add two objects", () =>
|
||||||
|
{
|
||||||
|
EditorBeatmap.Add(new Hit { StartTime = 1818 });
|
||||||
|
EditorBeatmap.Add(new Hit { StartTime = 1584 });
|
||||||
|
});
|
||||||
|
AddStep("seek back", () => EditorClock.Seek(1584));
|
||||||
|
AddStep("choose hit placement tool", () => InputManager.Key(Key.Number2));
|
||||||
|
AddStep("hover over first hit", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<DrawableHit>().ElementAt(1)));
|
||||||
|
AddStep("hover over second hit", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<DrawableHit>().ElementAt(0)));
|
||||||
|
AddStep("right click", () => InputManager.Click(MouseButton.Right));
|
||||||
|
AddUntilStep("context menu open", () => Editor.ChildrenOfType<OsuContextMenu>().Any(menu => menu.State == MenuState.Open));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
|||||||
AddStep("load player", () =>
|
AddStep("load player", () =>
|
||||||
{
|
{
|
||||||
Beatmap.Value = CreateWorkingBeatmap(beatmap);
|
Beatmap.Value = CreateWorkingBeatmap(beatmap);
|
||||||
|
Ruleset.Value = new TaikoRuleset().RulesetInfo;
|
||||||
SelectedMods.Value = mods ?? Array.Empty<Mod>();
|
SelectedMods.Value = mods ?? Array.Empty<Mod>();
|
||||||
|
|
||||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -1,33 +1,55 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
|
using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
|
||||||
|
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates the stamina coefficient of taiko difficulty.
|
/// Calculates the stamina coefficient of taiko difficulty.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Stamina : StrainDecaySkill
|
public class Stamina : StrainSkill
|
||||||
{
|
{
|
||||||
protected override double SkillMultiplier => 1.1;
|
private double skillMultiplier => 1.1;
|
||||||
protected override double StrainDecayBase => 0.4;
|
private double strainDecayBase => 0.4;
|
||||||
|
|
||||||
|
private readonly bool singleColourStamina;
|
||||||
|
|
||||||
|
private double currentStrain;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a <see cref="Stamina"/> skill.
|
/// Creates a <see cref="Stamina"/> skill.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="mods">Mods for use in skill calculations.</param>
|
/// <param name="mods">Mods for use in skill calculations.</param>
|
||||||
public Stamina(Mod[] mods)
|
/// <param name="singleColourStamina">Reads when Stamina is from a single coloured pattern.</param>
|
||||||
|
public Stamina(Mod[] mods, bool singleColourStamina)
|
||||||
: base(mods)
|
: base(mods)
|
||||||
{
|
{
|
||||||
|
this.singleColourStamina = singleColourStamina;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override double StrainValueOf(DifficultyHitObject current)
|
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||||
|
|
||||||
|
protected override double StrainValueAt(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
return StaminaEvaluator.EvaluateDifficultyOf(current);
|
currentStrain *= strainDecay(current.DeltaTime);
|
||||||
|
currentStrain += StaminaEvaluator.EvaluateDifficultyOf(current) * skillMultiplier;
|
||||||
|
|
||||||
|
// Safely prevents previous strains from shifting as new notes are added.
|
||||||
|
var currentObject = current as TaikoDifficultyHitObject;
|
||||||
|
int index = currentObject?.Colour.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0;
|
||||||
|
|
||||||
|
if (singleColourStamina)
|
||||||
|
return currentStrain / (1 + Math.Exp(-(index - 10) / 2.0));
|
||||||
|
|
||||||
|
return currentStrain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => singleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
[JsonProperty("stamina_difficulty")]
|
[JsonProperty("stamina_difficulty")]
|
||||||
public double StaminaDifficulty { get; set; }
|
public double StaminaDifficulty { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ratio of stamina difficulty from mono-color (single colour) streams to total stamina difficulty.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("mono_stamina_factor")]
|
||||||
|
public double MonoStaminaFactor { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The difficulty corresponding to the rhythm skill.
|
/// The difficulty corresponding to the rhythm skill.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -60,6 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||||
yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow);
|
yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow);
|
||||||
|
yield return (ATTRIB_ID_MONO_STAMINA_FACTOR, MonoStaminaFactor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||||
@ -69,6 +76,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||||
OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW];
|
OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW];
|
||||||
|
MonoStaminaFactor = values[ATTRIB_ID_MONO_STAMINA_FACTOR];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
{
|
{
|
||||||
new Rhythm(mods),
|
new Rhythm(mods),
|
||||||
new Colour(mods),
|
new Colour(mods),
|
||||||
new Stamina(mods)
|
new Stamina(mods, false),
|
||||||
|
new Stamina(mods, true)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,14 +80,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
Colour colour = (Colour)skills.First(x => x is Colour);
|
Colour colour = (Colour)skills.First(x => x is Colour);
|
||||||
Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm);
|
Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm);
|
||||||
Stamina stamina = (Stamina)skills.First(x => x is Stamina);
|
Stamina stamina = (Stamina)skills.First(x => x is Stamina);
|
||||||
|
Stamina singleColourStamina = (Stamina)skills.Last(x => x is Stamina);
|
||||||
|
|
||||||
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
|
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
|
||||||
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
|
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
|
||||||
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier;
|
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier;
|
||||||
|
double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier;
|
||||||
|
double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5);
|
||||||
|
|
||||||
double combinedRating = combinedDifficultyValue(rhythm, colour, stamina);
|
double combinedRating = combinedDifficultyValue(rhythm, colour, stamina);
|
||||||
double starRating = rescale(combinedRating * 1.4);
|
double starRating = rescale(combinedRating * 1.4);
|
||||||
|
|
||||||
|
// TODO: This is temporary measure as we don't detect abuse of multiple-input playstyles of converts within the current system.
|
||||||
|
if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0)
|
||||||
|
{
|
||||||
|
starRating *= 0.925;
|
||||||
|
// For maps with low colour variance and high stamina requirement, multiple inputs are more likely to be abused.
|
||||||
|
if (colourRating < 2 && staminaRating > 8)
|
||||||
|
starRating *= 0.80;
|
||||||
|
}
|
||||||
|
|
||||||
HitWindows hitWindows = new TaikoHitWindows();
|
HitWindows hitWindows = new TaikoHitWindows();
|
||||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||||
|
|
||||||
@ -95,6 +108,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
StarRating = starRating,
|
StarRating = starRating,
|
||||||
Mods = mods,
|
Mods = mods,
|
||||||
StaminaDifficulty = staminaRating,
|
StaminaDifficulty = staminaRating,
|
||||||
|
MonoStaminaFactor = monoStaminaFactor,
|
||||||
RhythmDifficulty = rhythmRating,
|
RhythmDifficulty = rhythmRating,
|
||||||
ColourDifficulty = colourRating,
|
ColourDifficulty = colourRating,
|
||||||
PeakDifficulty = combinedRating,
|
PeakDifficulty = combinedRating,
|
||||||
|
@ -42,18 +42,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
if (totalSuccessfulHits > 0)
|
if (totalSuccessfulHits > 0)
|
||||||
effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss;
|
effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss;
|
||||||
|
|
||||||
// TODO: The detection of rulesets is temporary until the leftover old skills have been reworked.
|
// Converts are detected and omitted from mod-specific bonuses due to the scope of current difficulty calculation.
|
||||||
bool isConvert = score.BeatmapInfo!.Ruleset.OnlineID != 1;
|
bool isConvert = score.BeatmapInfo!.Ruleset.OnlineID != 1;
|
||||||
|
|
||||||
double multiplier = 1.13;
|
double multiplier = 1.13;
|
||||||
|
|
||||||
if (score.Mods.Any(m => m is ModHidden))
|
if (score.Mods.Any(m => m is ModHidden) && !isConvert)
|
||||||
multiplier *= 1.075;
|
multiplier *= 1.075;
|
||||||
|
|
||||||
if (score.Mods.Any(m => m is ModEasy))
|
if (score.Mods.Any(m => m is ModEasy))
|
||||||
multiplier *= 0.975;
|
multiplier *= 0.950;
|
||||||
|
|
||||||
double difficultyValue = computeDifficultyValue(score, taikoAttributes, isConvert);
|
double difficultyValue = computeDifficultyValue(score, taikoAttributes);
|
||||||
double accuracyValue = computeAccuracyValue(score, taikoAttributes, isConvert);
|
double accuracyValue = computeAccuracyValue(score, taikoAttributes, isConvert);
|
||||||
double totalValue =
|
double totalValue =
|
||||||
Math.Pow(
|
Math.Pow(
|
||||||
@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert)
|
private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
|
||||||
{
|
{
|
||||||
double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0, 2.25) / 1150.0;
|
double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0, 2.25) / 1150.0;
|
||||||
|
|
||||||
@ -81,21 +81,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
difficultyValue *= Math.Pow(0.986, effectiveMissCount);
|
difficultyValue *= Math.Pow(0.986, effectiveMissCount);
|
||||||
|
|
||||||
if (score.Mods.Any(m => m is ModEasy))
|
if (score.Mods.Any(m => m is ModEasy))
|
||||||
difficultyValue *= 0.985;
|
difficultyValue *= 0.90;
|
||||||
|
|
||||||
if (score.Mods.Any(m => m is ModHidden) && !isConvert)
|
if (score.Mods.Any(m => m is ModHidden))
|
||||||
difficultyValue *= 1.025;
|
difficultyValue *= 1.025;
|
||||||
|
|
||||||
if (score.Mods.Any(m => m is ModHardRock))
|
if (score.Mods.Any(m => m is ModHardRock))
|
||||||
difficultyValue *= 1.10;
|
difficultyValue *= 1.10;
|
||||||
|
|
||||||
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
|
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
|
||||||
difficultyValue *= 1.050 * lengthBonus;
|
difficultyValue *= Math.Max(1, 1.050 - Math.Min(attributes.MonoStaminaFactor / 50, 1) * lengthBonus);
|
||||||
|
|
||||||
if (estimatedUnstableRate == null)
|
if (estimatedUnstableRate == null)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return difficultyValue * Math.Pow(SpecialFunctions.Erf(400 / (Math.Sqrt(2) * estimatedUnstableRate.Value)), 2.0);
|
// Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps.
|
||||||
|
double accScalingExponent = 2 + attributes.MonoStaminaFactor;
|
||||||
|
double accScalingShift = 300 - 100 * attributes.MonoStaminaFactor;
|
||||||
|
|
||||||
|
return difficultyValue * Math.Pow(SpecialFunctions.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert)
|
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert)
|
||||||
|
@ -7,6 +7,7 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Taiko.UI;
|
using osu.Game.Rulesets.Taiko.UI;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Edit
|
namespace osu.Game.Rulesets.Taiko.Edit
|
||||||
@ -20,6 +21,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override Playfield CreatePlayfield() => new TaikoEditorPlayfield();
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
25
osu.Game.Rulesets.Taiko/Edit/TaikoEditorPlayfield.cs
Normal file
25
osu.Game.Rulesets.Taiko/Edit/TaikoEditorPlayfield.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Rulesets.Taiko.UI;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Edit
|
||||||
|
{
|
||||||
|
public partial class TaikoEditorPlayfield : TaikoPlayfield
|
||||||
|
{
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
// This is the simplest way to extend the taiko playfield beyond the left of the drum area.
|
||||||
|
// Required in the editor to not look weird underneath left toolbox area.
|
||||||
|
AddInternal(new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.PlayfieldBackgroundRight), _ => new PlayfieldBackgroundRight())
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopLeft,
|
||||||
|
Origin = Anchor.TopRight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -54,17 +54,17 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
|||||||
|
|
||||||
public void SetStrongState(bool state)
|
public void SetStrongState(bool state)
|
||||||
{
|
{
|
||||||
if (SelectedItems.OfType<Hit>().All(h => h.IsStrong == state))
|
if (SelectedItems.OfType<TaikoStrongableHitObject>().All(h => h.IsStrong == state))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
EditorBeatmap.PerformOnSelection(h =>
|
EditorBeatmap.PerformOnSelection(h =>
|
||||||
{
|
{
|
||||||
if (!(h is Hit taikoHit)) return;
|
if (h is not TaikoStrongableHitObject strongable) return;
|
||||||
|
|
||||||
if (taikoHit.IsStrong != state)
|
if (strongable.IsStrong != state)
|
||||||
{
|
{
|
||||||
taikoHit.IsStrong = state;
|
strongable.IsStrong = state;
|
||||||
EditorBeatmap.Update(taikoHit);
|
EditorBeatmap.Update(strongable);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
|
|
||||||
public override Quad ScreenSpaceDrawQuad => MainPiece.Drawable.ScreenSpaceDrawQuad;
|
public override Quad ScreenSpaceDrawQuad => MainPiece.Drawable.ScreenSpaceDrawQuad;
|
||||||
|
|
||||||
|
// done strictly for editor purposes.
|
||||||
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => MainPiece.Drawable.ReceivePositionalInputAt(screenSpacePos);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rolling number of tick hits. This increases for hits and decreases for misses.
|
/// Rolling number of tick hits. This increases for hits and decreases for misses.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -78,6 +81,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
{
|
{
|
||||||
base.RecreatePieces();
|
base.RecreatePieces();
|
||||||
updateColour();
|
updateColour();
|
||||||
|
Height = HitObject.IsStrong ? TaikoStrongableHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnFree()
|
protected override void OnFree()
|
||||||
|
@ -12,6 +12,7 @@ using osu.Game.Rulesets.Objects;
|
|||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||||
{
|
{
|
||||||
@ -44,6 +45,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
IsFirstTick.Value = HitObject.FirstTick;
|
IsFirstTick.Value = HitObject.FirstTick;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void RecreatePieces()
|
||||||
|
{
|
||||||
|
base.RecreatePieces();
|
||||||
|
Size = new Vector2(HitObject.IsStrong ? TaikoStrongableHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
{
|
{
|
||||||
if (!userTriggered)
|
if (!userTriggered)
|
||||||
|
@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
|||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||||
{
|
{
|
||||||
@ -63,6 +64,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
{
|
{
|
||||||
updateActionsFromType();
|
updateActionsFromType();
|
||||||
base.RecreatePieces();
|
base.RecreatePieces();
|
||||||
|
Size = new Vector2(HitObject.IsStrong ? TaikoStrongableHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnFree()
|
protected override void OnFree()
|
||||||
|
@ -19,6 +19,7 @@ using osu.Game.Rulesets.Objects;
|
|||||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||||
{
|
{
|
||||||
@ -34,6 +35,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private const double ring_appear_offset = 100;
|
private const double ring_appear_offset = 100;
|
||||||
|
|
||||||
|
private Vector2 baseSize;
|
||||||
|
|
||||||
private readonly Container<DrawableSwellTick> ticks;
|
private readonly Container<DrawableSwellTick> ticks;
|
||||||
private readonly Container bodyContainer;
|
private readonly Container bodyContainer;
|
||||||
private readonly CircularContainer targetRing;
|
private readonly CircularContainer targetRing;
|
||||||
@ -141,6 +144,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
protected override void RecreatePieces()
|
||||||
|
{
|
||||||
|
base.RecreatePieces();
|
||||||
|
Size = baseSize = new Vector2(TaikoHitObject.DEFAULT_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnFree()
|
protected override void OnFree()
|
||||||
{
|
{
|
||||||
base.OnFree();
|
base.OnFree();
|
||||||
@ -269,7 +278,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
Size = BaseSize * Parent!.RelativeChildSize;
|
Size = baseSize * Parent!.RelativeChildSize;
|
||||||
|
|
||||||
// Make the swell stop at the hit target
|
// Make the swell stop at the hit target
|
||||||
X = Math.Max(0, X);
|
X = Math.Max(0, X);
|
||||||
|
@ -130,7 +130,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
|
|
||||||
public new TObject HitObject => (TObject)base.HitObject;
|
public new TObject HitObject => (TObject)base.HitObject;
|
||||||
|
|
||||||
protected Vector2 BaseSize;
|
|
||||||
protected SkinnableDrawable MainPiece;
|
protected SkinnableDrawable MainPiece;
|
||||||
|
|
||||||
protected DrawableTaikoHitObject([CanBeNull] TObject hitObject)
|
protected DrawableTaikoHitObject([CanBeNull] TObject hitObject)
|
||||||
@ -152,8 +151,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
|
|
||||||
protected virtual void RecreatePieces()
|
protected virtual void RecreatePieces()
|
||||||
{
|
{
|
||||||
Size = BaseSize = new Vector2(TaikoHitObject.DEFAULT_SIZE);
|
|
||||||
|
|
||||||
if (MainPiece != null)
|
if (MainPiece != null)
|
||||||
Content.Remove(MainPiece, true);
|
Content.Remove(MainPiece, true);
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||||
{
|
{
|
||||||
@ -44,13 +43,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
isStrong.UnbindEvents();
|
isStrong.UnbindEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void RecreatePieces()
|
|
||||||
{
|
|
||||||
base.RecreatePieces();
|
|
||||||
if (HitObject.IsStrong)
|
|
||||||
Size = BaseSize = new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
base.AddNestedHitObject(hitObject);
|
base.AddNestedHitObject(hitObject);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -9,6 +10,7 @@ using osu.Framework.Graphics.Sprites;
|
|||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||||
@ -19,13 +21,22 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var headDrawQuad = headCircle.ScreenSpaceDrawQuad;
|
// the reason why this calculation is so involved is that the head & tail sprites have different sizes/radii.
|
||||||
var tailDrawQuad = tailCircle.ScreenSpaceDrawQuad;
|
// therefore naively taking the SSDQs of them and making a quad out of them results in a trapezoid shape and not a box.
|
||||||
|
var headCentre = headCircle.ScreenSpaceDrawQuad.Centre;
|
||||||
|
var tailCentre = (tailCircle.ScreenSpaceDrawQuad.TopLeft + tailCircle.ScreenSpaceDrawQuad.BottomLeft) / 2;
|
||||||
|
|
||||||
return new Quad(headDrawQuad.TopLeft, tailDrawQuad.TopRight, headDrawQuad.BottomLeft, tailDrawQuad.BottomRight);
|
float headRadius = headCircle.ScreenSpaceDrawQuad.Height / 2;
|
||||||
|
float tailRadius = tailCircle.ScreenSpaceDrawQuad.Height / 2;
|
||||||
|
float radius = Math.Max(headRadius, tailRadius);
|
||||||
|
|
||||||
|
var rectangle = new RectangleF(headCentre.X, headCentre.Y, tailCentre.X - headCentre.X, 0).Inflate(radius);
|
||||||
|
return new Quad(rectangle.TopLeft, rectangle.TopRight, rectangle.BottomLeft, rectangle.BottomRight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => ScreenSpaceDrawQuad.Contains(screenSpacePos);
|
||||||
|
|
||||||
private LegacyCirclePiece headCircle = null!;
|
private LegacyCirclePiece headCircle = null!;
|
||||||
|
|
||||||
private Sprite body = null!;
|
private Sprite body = null!;
|
||||||
|
@ -241,8 +241,8 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
|
|
||||||
metadataLookup.Update(beatmapSet, preferOnlineFetch);
|
metadataLookup.Update(beatmapSet, preferOnlineFetch);
|
||||||
|
|
||||||
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
|
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
|
||||||
Assert.That(beatmap.OnlineID, Is.EqualTo(-1));
|
Assert.That(beatmap.OnlineID, Is.EqualTo(654321));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -273,34 +273,6 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
Assert.That(beatmap.OnlineID, Is.EqualTo(654321));
|
Assert.That(beatmap.OnlineID, Is.EqualTo(654321));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestMetadataLookupForBeatmapWithoutPopulatedIDAndIncorrectHash([Values] bool preferOnlineFetch)
|
|
||||||
{
|
|
||||||
var lookupResult = new OnlineBeatmapMetadata
|
|
||||||
{
|
|
||||||
BeatmapID = 654321,
|
|
||||||
BeatmapStatus = BeatmapOnlineStatus.Ranked,
|
|
||||||
MD5Hash = @"cafebabe",
|
|
||||||
};
|
|
||||||
|
|
||||||
var targetMock = preferOnlineFetch ? apiMetadataSourceMock : localCachedMetadataSourceMock;
|
|
||||||
targetMock.Setup(src => src.Available).Returns(true);
|
|
||||||
targetMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out lookupResult))
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
var beatmap = new BeatmapInfo
|
|
||||||
{
|
|
||||||
MD5Hash = @"deadbeef"
|
|
||||||
};
|
|
||||||
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
|
|
||||||
beatmap.BeatmapSet = beatmapSet;
|
|
||||||
|
|
||||||
metadataLookup.Update(beatmapSet, preferOnlineFetch);
|
|
||||||
|
|
||||||
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
|
|
||||||
Assert.That(beatmap.OnlineID, Is.EqualTo(-1));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestReturnedMetadataHasDifferentHash([Values] bool preferOnlineFetch)
|
public void TestReturnedMetadataHasDifferentHash([Values] bool preferOnlineFetch)
|
||||||
{
|
{
|
||||||
@ -383,58 +355,5 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
|
|
||||||
Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.None));
|
Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestPartiallyMaliciousSet([Values] bool preferOnlineFetch)
|
|
||||||
{
|
|
||||||
var firstResult = new OnlineBeatmapMetadata
|
|
||||||
{
|
|
||||||
BeatmapID = 654321,
|
|
||||||
BeatmapStatus = BeatmapOnlineStatus.Ranked,
|
|
||||||
BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
|
|
||||||
MD5Hash = @"cafebabe"
|
|
||||||
};
|
|
||||||
var secondResult = new OnlineBeatmapMetadata
|
|
||||||
{
|
|
||||||
BeatmapStatus = BeatmapOnlineStatus.Ranked,
|
|
||||||
BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
|
|
||||||
MD5Hash = @"dededede"
|
|
||||||
};
|
|
||||||
|
|
||||||
var targetMock = preferOnlineFetch ? apiMetadataSourceMock : localCachedMetadataSourceMock;
|
|
||||||
targetMock.Setup(src => src.Available).Returns(true);
|
|
||||||
targetMock.Setup(src => src.TryLookup(It.Is<BeatmapInfo>(bi => bi.OnlineID == 654321), out firstResult))
|
|
||||||
.Returns(true);
|
|
||||||
targetMock.Setup(src => src.TryLookup(It.Is<BeatmapInfo>(bi => bi.OnlineID == 666666), out secondResult))
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
var firstBeatmap = new BeatmapInfo
|
|
||||||
{
|
|
||||||
OnlineID = 654321,
|
|
||||||
MD5Hash = @"cafebabe",
|
|
||||||
};
|
|
||||||
var secondBeatmap = new BeatmapInfo
|
|
||||||
{
|
|
||||||
OnlineID = 666666,
|
|
||||||
MD5Hash = @"deadbeef"
|
|
||||||
};
|
|
||||||
var beatmapSet = new BeatmapSetInfo(new[]
|
|
||||||
{
|
|
||||||
firstBeatmap,
|
|
||||||
secondBeatmap
|
|
||||||
});
|
|
||||||
firstBeatmap.BeatmapSet = beatmapSet;
|
|
||||||
secondBeatmap.BeatmapSet = beatmapSet;
|
|
||||||
|
|
||||||
metadataLookup.Update(beatmapSet, preferOnlineFetch);
|
|
||||||
|
|
||||||
Assert.That(firstBeatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
|
|
||||||
Assert.That(firstBeatmap.OnlineID, Is.EqualTo(654321));
|
|
||||||
|
|
||||||
Assert.That(secondBeatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
|
|
||||||
Assert.That(secondBeatmap.OnlineID, Is.EqualTo(-1));
|
|
||||||
|
|
||||||
Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.None));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -120,11 +120,11 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
private void compareBeatmaps((IBeatmap beatmap, TestLegacySkin skin) expected, (IBeatmap beatmap, TestLegacySkin skin) actual)
|
private void compareBeatmaps((IBeatmap beatmap, TestLegacySkin skin) expected, (IBeatmap beatmap, TestLegacySkin skin) actual)
|
||||||
{
|
{
|
||||||
// Check all control points that are still considered to be at a global level.
|
// Check all control points that are still considered to be at a global level.
|
||||||
Assert.That(expected.beatmap.ControlPointInfo.TimingPoints.Serialize(), Is.EqualTo(actual.beatmap.ControlPointInfo.TimingPoints.Serialize()));
|
Assert.That(actual.beatmap.ControlPointInfo.TimingPoints.Serialize(), Is.EqualTo(expected.beatmap.ControlPointInfo.TimingPoints.Serialize()));
|
||||||
Assert.That(expected.beatmap.ControlPointInfo.EffectPoints.Serialize(), Is.EqualTo(actual.beatmap.ControlPointInfo.EffectPoints.Serialize()));
|
Assert.That(actual.beatmap.ControlPointInfo.EffectPoints.Serialize(), Is.EqualTo(expected.beatmap.ControlPointInfo.EffectPoints.Serialize()));
|
||||||
|
|
||||||
// Check all hitobjects.
|
// Check all hitobjects.
|
||||||
Assert.That(expected.beatmap.HitObjects.Serialize(), Is.EqualTo(actual.beatmap.HitObjects.Serialize()));
|
Assert.That(actual.beatmap.HitObjects.Serialize(), Is.EqualTo(expected.beatmap.HitObjects.Serialize()));
|
||||||
|
|
||||||
// Check skin.
|
// Check skin.
|
||||||
Assert.IsTrue(areComboColoursEqual(expected.skin.Configuration, actual.skin.Configuration));
|
Assert.IsTrue(areComboColoursEqual(expected.skin.Configuration, actual.skin.Configuration));
|
||||||
|
@ -716,7 +716,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
foreach (var entry in zip.Entries.ToArray())
|
foreach (var entry in zip.Entries.ToArray())
|
||||||
{
|
{
|
||||||
if (entry.Key.EndsWith(".osu", StringComparison.InvariantCulture))
|
if (entry.Key!.EndsWith(".osu", StringComparison.InvariantCulture))
|
||||||
zip.RemoveEntry(entry);
|
zip.RemoveEntry(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
161
osu.Game.Tests/Editing/TimingSectionAdjustmentsTest.cs
Normal file
161
osu.Game.Tests/Editing/TimingSectionAdjustmentsTest.cs
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Screens.Edit.Timing;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Editing
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class TimingSectionAdjustmentsTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestOffsetAdjustment()
|
||||||
|
{
|
||||||
|
var controlPoints = new ControlPointInfo();
|
||||||
|
|
||||||
|
controlPoints.Add(100, new TimingControlPoint { BeatLength = 100 });
|
||||||
|
controlPoints.Add(50_000, new TimingControlPoint { BeatLength = 200 });
|
||||||
|
controlPoints.Add(100_000, new TimingControlPoint { BeatLength = 50 });
|
||||||
|
|
||||||
|
var beatmap = new Beatmap
|
||||||
|
{
|
||||||
|
ControlPointInfo = controlPoints,
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0 },
|
||||||
|
new HitCircle { StartTime = 200 },
|
||||||
|
new HitCircle { StartTime = 49_900 },
|
||||||
|
new HitCircle { StartTime = 50_000 },
|
||||||
|
new HitCircle { StartTime = 50_200 },
|
||||||
|
new HitCircle { StartTime = 99_800 },
|
||||||
|
new HitCircle { StartTime = 100_000 },
|
||||||
|
new HitCircle { StartTime = 100_050 },
|
||||||
|
new HitCircle { StartTime = 100_550 },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
moveTimingPoint(beatmap, 100, -50);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(beatmap.HitObjects[0].StartTime, Is.EqualTo(-50));
|
||||||
|
Assert.That(beatmap.HitObjects[1].StartTime, Is.EqualTo(150));
|
||||||
|
Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(49_850));
|
||||||
|
Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(50_000));
|
||||||
|
});
|
||||||
|
|
||||||
|
moveTimingPoint(beatmap, 50_000, 1_000);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(49_850));
|
||||||
|
Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(51_000));
|
||||||
|
Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(51_200));
|
||||||
|
Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(100_800));
|
||||||
|
Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(100_000));
|
||||||
|
});
|
||||||
|
|
||||||
|
moveTimingPoint(beatmap, 100_000, 10_000);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(51_200));
|
||||||
|
Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(110_800));
|
||||||
|
Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(110_000));
|
||||||
|
Assert.That(beatmap.HitObjects[7].StartTime, Is.EqualTo(110_050));
|
||||||
|
Assert.That(beatmap.HitObjects[8].StartTime, Is.EqualTo(110_550));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBPMAdjustment()
|
||||||
|
{
|
||||||
|
var controlPoints = new ControlPointInfo();
|
||||||
|
|
||||||
|
controlPoints.Add(100, new TimingControlPoint { BeatLength = 100 });
|
||||||
|
controlPoints.Add(50_000, new TimingControlPoint { BeatLength = 200 });
|
||||||
|
controlPoints.Add(100_000, new TimingControlPoint { BeatLength = 50 });
|
||||||
|
|
||||||
|
var beatmap = new Beatmap
|
||||||
|
{
|
||||||
|
ControlPointInfo = controlPoints,
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0 },
|
||||||
|
new HitCircle { StartTime = 200 },
|
||||||
|
new Spinner { StartTime = 500, EndTime = 1000 },
|
||||||
|
new HitCircle { StartTime = 49_900 },
|
||||||
|
new HitCircle { StartTime = 50_000 },
|
||||||
|
new HitCircle { StartTime = 50_200 },
|
||||||
|
new HitCircle { StartTime = 99_800 },
|
||||||
|
new HitCircle { StartTime = 100_000 },
|
||||||
|
new HitCircle { StartTime = 100_050 },
|
||||||
|
new HitCircle { StartTime = 100_550 },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
adjustBeatLength(beatmap, 100, 50);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(beatmap.HitObjects[0].StartTime, Is.EqualTo(50));
|
||||||
|
Assert.That(beatmap.HitObjects[1].StartTime, Is.EqualTo(150));
|
||||||
|
Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(300));
|
||||||
|
Assert.That(beatmap.HitObjects[2].GetEndTime(), Is.EqualTo(550));
|
||||||
|
Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(25_000));
|
||||||
|
Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(50_000));
|
||||||
|
});
|
||||||
|
|
||||||
|
adjustBeatLength(beatmap, 50_000, 400);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(300));
|
||||||
|
Assert.That(beatmap.HitObjects[2].GetEndTime(), Is.EqualTo(550));
|
||||||
|
Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(25_000));
|
||||||
|
Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(50_000));
|
||||||
|
Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(50_400));
|
||||||
|
Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(149_600));
|
||||||
|
Assert.That(beatmap.HitObjects[7].StartTime, Is.EqualTo(100_000));
|
||||||
|
});
|
||||||
|
|
||||||
|
adjustBeatLength(beatmap, 100_000, 100);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(50_400));
|
||||||
|
Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(199_200));
|
||||||
|
Assert.That(beatmap.HitObjects[7].StartTime, Is.EqualTo(100_000));
|
||||||
|
Assert.That(beatmap.HitObjects[8].StartTime, Is.EqualTo(100_100));
|
||||||
|
Assert.That(beatmap.HitObjects[9].StartTime, Is.EqualTo(101_100));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void moveTimingPoint(IBeatmap beatmap, double originalTime, double adjustment)
|
||||||
|
{
|
||||||
|
var controlPoints = beatmap.ControlPointInfo;
|
||||||
|
var controlPointGroup = controlPoints.GroupAt(originalTime);
|
||||||
|
var timingPoint = controlPointGroup.ControlPoints.OfType<TimingControlPoint>().Single();
|
||||||
|
controlPoints.RemoveGroup(controlPointGroup);
|
||||||
|
TimingSectionAdjustments.AdjustHitObjectOffset(beatmap, timingPoint, adjustment);
|
||||||
|
controlPoints.Add(originalTime - adjustment, timingPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void adjustBeatLength(IBeatmap beatmap, double groupTime, double newBeatLength)
|
||||||
|
{
|
||||||
|
var controlPoints = beatmap.ControlPointInfo;
|
||||||
|
var controlPointGroup = controlPoints.GroupAt(groupTime);
|
||||||
|
var timingPoint = controlPointGroup.ControlPoints.OfType<TimingControlPoint>().Single();
|
||||||
|
double oldBeatLength = timingPoint.BeatLength;
|
||||||
|
timingPoint.BeatLength = newBeatLength;
|
||||||
|
TimingSectionAdjustments.SetHitObjectBPM(beatmap, timingPoint, oldBeatLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -627,6 +627,87 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
Assert.AreEqual(DateTimeOffset.MinValue.AddMilliseconds(1), filterCriteria.LastPlayed.Min);
|
Assert.AreEqual(DateTimeOffset.MinValue.AddMilliseconds(1), filterCriteria.LastPlayed.Min);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static DateTimeOffset dateTimeOffsetFromDateOnly(int year, int month, int day) =>
|
||||||
|
new DateTimeOffset(year, month, day, 0, 0, 0, TimeSpan.Zero);
|
||||||
|
|
||||||
|
private static readonly object[] ranked_date_valid_test_cases =
|
||||||
|
{
|
||||||
|
new object[] { "ranked<2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked<2012.03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked<2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked<2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
|
||||||
|
new object[] { "ranked<=2012", dateTimeOffsetFromDateOnly(2013, 1, 1), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked<=2012.03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked<=2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked<=2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
|
||||||
|
new object[] { "ranked>2012", dateTimeOffsetFromDateOnly(2013, 1, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked>2012.03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked>2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked>2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
|
||||||
|
new object[] { "ranked>=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked>=2012.03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked>=2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked>=2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
|
||||||
|
new object[] { "ranked=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked=2012-03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked=2012-03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
new object[] { "ranked=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min },
|
||||||
|
new object[] { "ranked=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max },
|
||||||
|
};
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCaseSource(nameof(ranked_date_valid_test_cases))]
|
||||||
|
public void TestValidRankedDateQueries(string query, DateTimeOffset expected, Func<FilterCriteria, DateTimeOffset?> f)
|
||||||
|
{
|
||||||
|
var filterCriteria = new FilterCriteria();
|
||||||
|
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||||
|
Assert.AreEqual(true, filterCriteria.DateRanked.HasFilter);
|
||||||
|
Assert.AreEqual(expected, f(filterCriteria));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly object[] ranked_date_invalid_test_cases =
|
||||||
|
{
|
||||||
|
new object[] { "ranked<0" },
|
||||||
|
new object[] { "ranked=99999" },
|
||||||
|
new object[] { "ranked>=2012-03-05-04" },
|
||||||
|
};
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCaseSource(nameof(ranked_date_invalid_test_cases))]
|
||||||
|
public void TestInvalidRankedDateQueries(string query)
|
||||||
|
{
|
||||||
|
var filterCriteria = new FilterCriteria();
|
||||||
|
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||||
|
Assert.AreEqual(false, filterCriteria.DateRanked.HasFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly object[] submitted_date_test_cases =
|
||||||
|
{
|
||||||
|
new object[] { "submitted<2012", true },
|
||||||
|
new object[] { "submitted<2012.03", true },
|
||||||
|
new object[] { "submitted<2012/03/05", true },
|
||||||
|
new object[] { "submitted<2012-3-5", true },
|
||||||
|
|
||||||
|
new object[] { "submitted<0", false },
|
||||||
|
new object[] { "submitted=99999", false },
|
||||||
|
new object[] { "submitted>=2012-03-05-04", false },
|
||||||
|
new object[] { "submitted>=2012/03.05-04", false },
|
||||||
|
};
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCaseSource(nameof(submitted_date_test_cases))]
|
||||||
|
public void TestInvalidRankedDateQueries(string query, bool expected)
|
||||||
|
{
|
||||||
|
var filterCriteria = new FilterCriteria();
|
||||||
|
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||||
|
Assert.AreEqual(expected, filterCriteria.DateSubmitted.HasFilter);
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly object[] played_query_tests =
|
private static readonly object[] played_query_tests =
|
||||||
{
|
{
|
||||||
new object[] { "0", DateTimeOffset.MinValue, true },
|
new object[] { "0", DateTimeOffset.MinValue, true },
|
||||||
|
@ -96,6 +96,7 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
|
|
||||||
public override IAdjustableAudioComponent Audio { get; }
|
public override IAdjustableAudioComponent Audio { get; }
|
||||||
public override Playfield Playfield { get; }
|
public override Playfield Playfield { get; }
|
||||||
|
public override PlayfieldAdjustmentContainer PlayfieldAdjustmentContainer { get; }
|
||||||
public override Container Overlays { get; }
|
public override Container Overlays { get; }
|
||||||
public override Container FrameStableComponents { get; }
|
public override Container FrameStableComponents { get; }
|
||||||
public override IFrameStableClock FrameStableClock { get; }
|
public override IFrameStableClock FrameStableClock { get; }
|
||||||
|
39
osu.Game.Tests/Resources/mania-0-01-sv.osu
Normal file
39
osu.Game.Tests/Resources/mania-0-01-sv.osu
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
osu file format v14
|
||||||
|
|
||||||
|
[General]
|
||||||
|
SampleSet: Normal
|
||||||
|
StackLeniency: 0.7
|
||||||
|
Mode: 3
|
||||||
|
|
||||||
|
[Difficulty]
|
||||||
|
HPDrainRate:3
|
||||||
|
CircleSize:5
|
||||||
|
OverallDifficulty:8
|
||||||
|
ApproachRate:8
|
||||||
|
SliderMultiplier:3.59999990463257
|
||||||
|
SliderTickRate:2
|
||||||
|
|
||||||
|
[TimingPoints]
|
||||||
|
24,352.941176470588,4,1,1,100,1,0
|
||||||
|
6376,-10000,4,1,1,100,0,0
|
||||||
|
|
||||||
|
[HitObjects]
|
||||||
|
51,192,24,1,0,0:0:0:0:
|
||||||
|
153,192,200,1,0,0:0:0:0:
|
||||||
|
358,192,376,1,0,0:0:0:0:
|
||||||
|
460,192,553,1,0,0:0:0:0:
|
||||||
|
460,192,729,128,0,1435:0:0:0:0:
|
||||||
|
358,192,906,128,0,1612:0:0:0:0:
|
||||||
|
256,192,1082,128,0,1788:0:0:0:0:
|
||||||
|
153,192,1259,128,0,1965:0:0:0:0:
|
||||||
|
51,192,1435,128,0,2141:0:0:0:0:
|
||||||
|
51,192,2318,1,12,0:0:0:0:
|
||||||
|
153,192,2318,1,4,0:0:0:0:
|
||||||
|
256,192,2318,1,6,0:0:0:0:
|
||||||
|
358,192,2318,1,14,0:0:0:0:
|
||||||
|
460,192,2318,1,0,0:0:0:0:
|
||||||
|
51,192,2494,128,0,2582:0:0:0:0:
|
||||||
|
153,192,2494,128,14,2582:0:0:0:0:
|
||||||
|
256,192,2494,128,6,2582:0:0:0:0:
|
||||||
|
358,192,2494,128,4,2582:0:0:0:0:
|
||||||
|
460,192,2494,128,12,2582:0:0:0:0:
|
@ -6,10 +6,12 @@ using System.Linq;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
|
using osu.Game.Localisation;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.Metadata;
|
using osu.Game.Online.Metadata;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using osu.Game.Tests.Visual.Metadata;
|
using osu.Game.Tests.Visual.Metadata;
|
||||||
@ -81,6 +83,38 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
|||||||
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
||||||
AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
|
AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
|
||||||
AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null);
|
AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null);
|
||||||
|
AddAssert("notification posted", () => notificationOverlay.AllNotifications.OfType<SimpleNotification>().Any(n => n.Text == DailyChallengeStrings.ChallengeEndedNotification));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestConclusionNotificationDoesNotFireOnDisconnect()
|
||||||
|
{
|
||||||
|
var room = new Room
|
||||||
|
{
|
||||||
|
RoomID = { Value = 1234 },
|
||||||
|
Name = { Value = "Daily Challenge: June 4, 2024" },
|
||||||
|
Playlist =
|
||||||
|
{
|
||||||
|
new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First())
|
||||||
|
{
|
||||||
|
RequiredMods = [new APIMod(new OsuModTraceable())],
|
||||||
|
AllowedMods = [new APIMod(new OsuModDoubleTime())]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
EndDate = { Value = DateTimeOffset.Now.AddHours(12) },
|
||||||
|
Category = { Value = RoomCategory.DailyChallenge }
|
||||||
|
};
|
||||||
|
|
||||||
|
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
|
||||||
|
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
|
||||||
|
|
||||||
|
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
|
||||||
|
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
||||||
|
AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
|
||||||
|
AddStep("disconnect from metadata server", () => metadataClient.Disconnect());
|
||||||
|
AddUntilStep("wait for disconnection", () => metadataClient.DailyChallengeInfo.Value, () => Is.Null);
|
||||||
|
AddAssert("no notification posted", () => notificationOverlay.AllNotifications, () => Is.Empty);
|
||||||
|
AddStep("reconnect to metadata server", () => metadataClient.Reconnect());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -362,6 +362,12 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddStep("add whistle addition", () =>
|
||||||
|
{
|
||||||
|
foreach (var h in EditorBeatmap.HitObjects)
|
||||||
|
h.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT));
|
||||||
|
});
|
||||||
|
|
||||||
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||||
|
|
||||||
hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
|
hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
|
||||||
@ -374,8 +380,10 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||||
});
|
});
|
||||||
|
|
||||||
hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
|
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL);
|
||||||
hitObjectHasSampleBank(1, HitSampleInfo.BANK_NORMAL);
|
hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_NORMAL);
|
||||||
|
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
|
||||||
|
hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_SOFT);
|
||||||
|
|
||||||
AddStep("Press drum bank shortcut", () =>
|
AddStep("Press drum bank shortcut", () =>
|
||||||
{
|
{
|
||||||
@ -384,8 +392,10 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||||
});
|
});
|
||||||
|
|
||||||
hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
|
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
|
||||||
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
|
hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_DRUM);
|
||||||
|
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
|
||||||
|
hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_SOFT);
|
||||||
|
|
||||||
AddStep("Press auto bank shortcut", () =>
|
AddStep("Press auto bank shortcut", () =>
|
||||||
{
|
{
|
||||||
@ -395,8 +405,47 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Should be a noop.
|
// Should be a noop.
|
||||||
hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
|
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
|
||||||
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
|
hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_DRUM);
|
||||||
|
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
|
||||||
|
hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_SOFT);
|
||||||
|
|
||||||
|
AddStep("Press addition normal bank shortcut", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.AltLeft);
|
||||||
|
InputManager.Key(Key.W);
|
||||||
|
InputManager.ReleaseKey(Key.AltLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
|
||||||
|
hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_DRUM);
|
||||||
|
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_NORMAL);
|
||||||
|
hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_NORMAL);
|
||||||
|
|
||||||
|
AddStep("Press addition drum bank shortcut", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.AltLeft);
|
||||||
|
InputManager.Key(Key.R);
|
||||||
|
InputManager.ReleaseKey(Key.AltLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
|
||||||
|
hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_DRUM);
|
||||||
|
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_DRUM);
|
||||||
|
hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_DRUM);
|
||||||
|
|
||||||
|
AddStep("Press auto bank shortcut", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.AltLeft);
|
||||||
|
InputManager.Key(Key.Q);
|
||||||
|
InputManager.ReleaseKey(Key.AltLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should be a noop.
|
||||||
|
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
|
||||||
|
hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_DRUM);
|
||||||
|
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_DRUM);
|
||||||
|
hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_DRUM);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -414,7 +463,21 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||||
});
|
});
|
||||||
|
|
||||||
checkPlacementSample(HitSampleInfo.BANK_NORMAL);
|
AddStep("Press soft addition bank shortcut", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.AltLeft);
|
||||||
|
InputManager.Key(Key.E);
|
||||||
|
InputManager.ReleaseKey(Key.AltLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
checkPlacementSampleBank(HitSampleInfo.BANK_NORMAL);
|
||||||
|
|
||||||
|
AddStep("Press finish sample shortcut", () =>
|
||||||
|
{
|
||||||
|
InputManager.Key(Key.E);
|
||||||
|
});
|
||||||
|
|
||||||
|
checkPlacementSampleAdditionBank(HitSampleInfo.BANK_SOFT);
|
||||||
|
|
||||||
AddStep("Press drum bank shortcut", () =>
|
AddStep("Press drum bank shortcut", () =>
|
||||||
{
|
{
|
||||||
@ -423,7 +486,18 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||||
});
|
});
|
||||||
|
|
||||||
checkPlacementSample(HitSampleInfo.BANK_DRUM);
|
checkPlacementSampleBank(HitSampleInfo.BANK_DRUM);
|
||||||
|
checkPlacementSampleAdditionBank(HitSampleInfo.BANK_SOFT);
|
||||||
|
|
||||||
|
AddStep("Press drum addition bank shortcut", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.AltLeft);
|
||||||
|
InputManager.Key(Key.R);
|
||||||
|
InputManager.ReleaseKey(Key.AltLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
checkPlacementSampleBank(HitSampleInfo.BANK_DRUM);
|
||||||
|
checkPlacementSampleAdditionBank(HitSampleInfo.BANK_DRUM);
|
||||||
|
|
||||||
AddStep("Press auto bank shortcut", () =>
|
AddStep("Press auto bank shortcut", () =>
|
||||||
{
|
{
|
||||||
@ -432,15 +506,29 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||||
});
|
});
|
||||||
|
|
||||||
checkPlacementSample(HitSampleInfo.BANK_NORMAL);
|
checkPlacementSampleBank(HitSampleInfo.BANK_NORMAL);
|
||||||
|
checkPlacementSampleAdditionBank(HitSampleInfo.BANK_DRUM);
|
||||||
|
|
||||||
|
AddStep("Press auto addition bank shortcut", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.AltLeft);
|
||||||
|
InputManager.Key(Key.Q);
|
||||||
|
InputManager.ReleaseKey(Key.AltLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
checkPlacementSampleBank(HitSampleInfo.BANK_NORMAL);
|
||||||
|
checkPlacementSampleAdditionBank(HitSampleInfo.BANK_NORMAL);
|
||||||
|
|
||||||
AddStep("Move after second object", () => EditorClock.Seek(750));
|
AddStep("Move after second object", () => EditorClock.Seek(750));
|
||||||
checkPlacementSample(HitSampleInfo.BANK_SOFT);
|
checkPlacementSampleBank(HitSampleInfo.BANK_SOFT);
|
||||||
|
checkPlacementSampleAdditionBank(HitSampleInfo.BANK_SOFT);
|
||||||
|
|
||||||
AddStep("Move to first object", () => EditorClock.Seek(0));
|
AddStep("Move to first object", () => EditorClock.Seek(0));
|
||||||
checkPlacementSample(HitSampleInfo.BANK_NORMAL);
|
checkPlacementSampleBank(HitSampleInfo.BANK_NORMAL);
|
||||||
|
checkPlacementSampleAdditionBank(HitSampleInfo.BANK_NORMAL);
|
||||||
|
|
||||||
void checkPlacementSample(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First().Bank, () => Is.EqualTo(expected));
|
void checkPlacementSampleBank(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name == HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected));
|
||||||
|
void checkPlacementSampleAdditionBank(string expected) => AddAssert($"Placement sample addition is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name != HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -585,7 +673,29 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL);
|
hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL);
|
||||||
hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM);
|
hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM);
|
||||||
hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL);
|
hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL);
|
||||||
hitObjectNodeHasSampleBank(2, 1, HitSampleInfo.BANK_DRUM);
|
hitObjectNodeHasSampleNormalBank(2, 1, HitSampleInfo.BANK_DRUM);
|
||||||
|
hitObjectNodeHasSampleAdditionBank(2, 1, HitSampleInfo.BANK_SOFT);
|
||||||
|
hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
|
||||||
|
|
||||||
|
AddStep("set normal addition bank", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.LAlt);
|
||||||
|
InputManager.Key(Key.W);
|
||||||
|
InputManager.ReleaseKey(Key.LAlt);
|
||||||
|
});
|
||||||
|
|
||||||
|
hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
|
||||||
|
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL);
|
||||||
|
|
||||||
|
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
|
||||||
|
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL);
|
||||||
|
|
||||||
|
hitObjectHasSampleBank(2, HitSampleInfo.BANK_DRUM);
|
||||||
|
hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL);
|
||||||
|
hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM);
|
||||||
|
hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL);
|
||||||
|
hitObjectNodeHasSampleNormalBank(2, 1, HitSampleInfo.BANK_DRUM);
|
||||||
|
hitObjectNodeHasSampleAdditionBank(2, 1, HitSampleInfo.BANK_NORMAL);
|
||||||
hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
|
hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -629,20 +739,37 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
InputManager.ReleaseKey(Key.LShift);
|
InputManager.ReleaseKey(Key.LShift);
|
||||||
});
|
});
|
||||||
|
|
||||||
hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
|
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_SOFT);
|
||||||
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
|
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
|
||||||
hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_SOFT);
|
hitObjectNodeHasSampleNormalBank(0, 0, HitSampleInfo.BANK_SOFT);
|
||||||
hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
|
hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
|
||||||
hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT);
|
hitObjectNodeHasSampleNormalBank(0, 1, HitSampleInfo.BANK_SOFT);
|
||||||
hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
|
hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
|
||||||
|
|
||||||
AddStep("unify whistle addition", () => InputManager.Key(Key.W));
|
AddStep("unify whistle addition", () => InputManager.Key(Key.W));
|
||||||
|
|
||||||
hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
|
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_SOFT);
|
||||||
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
|
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
|
||||||
hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_SOFT);
|
hitObjectNodeHasSampleNormalBank(0, 0, HitSampleInfo.BANK_SOFT);
|
||||||
hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_WHISTLE);
|
hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_WHISTLE);
|
||||||
hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT);
|
hitObjectNodeHasSampleNormalBank(0, 1, HitSampleInfo.BANK_SOFT);
|
||||||
|
hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
|
||||||
|
|
||||||
|
AddStep("set drum addition bank", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.LAlt);
|
||||||
|
InputManager.Key(Key.R);
|
||||||
|
InputManager.ReleaseKey(Key.LAlt);
|
||||||
|
});
|
||||||
|
|
||||||
|
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_SOFT);
|
||||||
|
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_DRUM);
|
||||||
|
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
|
||||||
|
hitObjectNodeHasSampleNormalBank(0, 0, HitSampleInfo.BANK_SOFT);
|
||||||
|
hitObjectNodeHasSampleAdditionBank(0, 0, HitSampleInfo.BANK_DRUM);
|
||||||
|
hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_WHISTLE);
|
||||||
|
hitObjectNodeHasSampleNormalBank(0, 1, HitSampleInfo.BANK_SOFT);
|
||||||
|
hitObjectNodeHasSampleAdditionBank(0, 1, HitSampleInfo.BANK_DRUM);
|
||||||
hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
|
hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,7 +165,9 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddStep("enable automatic bank assignment", () =>
|
AddStep("enable automatic bank assignment", () =>
|
||||||
{
|
{
|
||||||
InputManager.PressKey(Key.LShift);
|
InputManager.PressKey(Key.LShift);
|
||||||
|
InputManager.PressKey(Key.LAlt);
|
||||||
InputManager.Key(Key.Q);
|
InputManager.Key(Key.Q);
|
||||||
|
InputManager.ReleaseKey(Key.LAlt);
|
||||||
InputManager.ReleaseKey(Key.LShift);
|
InputManager.ReleaseKey(Key.LShift);
|
||||||
});
|
});
|
||||||
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
|
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
|
||||||
@ -228,7 +230,9 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddStep("select drum bank", () =>
|
AddStep("select drum bank", () =>
|
||||||
{
|
{
|
||||||
InputManager.PressKey(Key.LShift);
|
InputManager.PressKey(Key.LShift);
|
||||||
|
InputManager.PressKey(Key.LAlt);
|
||||||
InputManager.Key(Key.R);
|
InputManager.Key(Key.R);
|
||||||
|
InputManager.ReleaseKey(Key.LAlt);
|
||||||
InputManager.ReleaseKey(Key.LShift);
|
InputManager.ReleaseKey(Key.LShift);
|
||||||
});
|
});
|
||||||
AddStep("enable clap addition", () => InputManager.Key(Key.R));
|
AddStep("enable clap addition", () => InputManager.Key(Key.R));
|
||||||
|
@ -155,7 +155,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
var api = (DummyAPIAccess)API;
|
var api = (DummyAPIAccess)API;
|
||||||
|
|
||||||
api.Friends.Clear();
|
api.Friends.Clear();
|
||||||
api.Friends.Add(friend);
|
api.Friends.Add(new APIRelation
|
||||||
|
{
|
||||||
|
Mutual = true,
|
||||||
|
RelationType = RelationType.Friend,
|
||||||
|
TargetID = friend.OnlineID,
|
||||||
|
TargetUser = friend
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
int playerNumber = 1;
|
int playerNumber = 1;
|
||||||
|
@ -284,6 +284,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
public override IAdjustableAudioComponent Audio { get; }
|
public override IAdjustableAudioComponent Audio { get; }
|
||||||
public override Playfield Playfield { get; }
|
public override Playfield Playfield { get; }
|
||||||
|
public override PlayfieldAdjustmentContainer PlayfieldAdjustmentContainer { get; }
|
||||||
public override Container Overlays { get; }
|
public override Container Overlays { get; }
|
||||||
public override Container FrameStableComponents { get; }
|
public override Container FrameStableComponents { get; }
|
||||||
public override IFrameStableClock FrameStableClock { get; }
|
public override IFrameStableClock FrameStableClock { get; }
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Moq;
|
using Moq;
|
||||||
@ -36,15 +34,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
private readonly Bindable<BeatmapAvailability> beatmapAvailability = new Bindable<BeatmapAvailability>();
|
private readonly Bindable<BeatmapAvailability> beatmapAvailability = new Bindable<BeatmapAvailability>();
|
||||||
private readonly Bindable<Room> room = new Bindable<Room>();
|
private readonly Bindable<Room> room = new Bindable<Room>();
|
||||||
|
|
||||||
private MultiplayerRoom multiplayerRoom;
|
private MultiplayerRoom multiplayerRoom = null!;
|
||||||
private MultiplayerRoomUser localUser;
|
private MultiplayerRoomUser localUser = null!;
|
||||||
private OngoingOperationTracker ongoingOperationTracker;
|
private OngoingOperationTracker ongoingOperationTracker = null!;
|
||||||
|
|
||||||
private PopoverContainer content;
|
private PopoverContainer content = null!;
|
||||||
private MatchStartControl control;
|
private MatchStartControl control = null!;
|
||||||
|
|
||||||
private OsuButton readyButton => control.ChildrenOfType<OsuButton>().Single();
|
private OsuButton readyButton => control.ChildrenOfType<OsuButton>().Single();
|
||||||
|
|
||||||
|
[Cached(typeof(IBindable<PlaylistItem>))]
|
||||||
|
private readonly Bindable<PlaylistItem> currentItem = new Bindable<PlaylistItem>();
|
||||||
|
|
||||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
|
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
|
||||||
new CachedModelDependencyContainer<Room>(base.CreateChildDependencies(parent)) { Model = { BindTarget = room } };
|
new CachedModelDependencyContainer<Room>(base.CreateChildDependencies(parent)) { Model = { BindTarget = room } };
|
||||||
|
|
||||||
@ -112,15 +113,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable();
|
beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable();
|
||||||
|
|
||||||
var playlistItem = new PlaylistItem(Beatmap.Value.BeatmapInfo)
|
currentItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
|
||||||
{
|
{
|
||||||
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
|
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
|
||||||
};
|
};
|
||||||
|
|
||||||
room.Value = new Room
|
room.Value = new Room
|
||||||
{
|
{
|
||||||
Playlist = { playlistItem },
|
Playlist = { currentItem.Value },
|
||||||
CurrentPlaylistItem = { Value = playlistItem }
|
CurrentPlaylistItem = { BindTarget = currentItem }
|
||||||
};
|
};
|
||||||
|
|
||||||
localUser = new MultiplayerRoomUser(API.LocalUser.Value.Id) { User = API.LocalUser.Value };
|
localUser = new MultiplayerRoomUser(API.LocalUser.Value.Id) { User = API.LocalUser.Value };
|
||||||
@ -129,7 +130,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
Playlist =
|
Playlist =
|
||||||
{
|
{
|
||||||
TestMultiplayerClient.CreateMultiplayerPlaylistItem(playlistItem),
|
TestMultiplayerClient.CreateMultiplayerPlaylistItem(currentItem.Value),
|
||||||
},
|
},
|
||||||
Users = { localUser },
|
Users = { localUser },
|
||||||
Host = localUser,
|
Host = localUser,
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// 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;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Cursor;
|
using osu.Framework.Graphics.Cursor;
|
||||||
|
using osu.Game.Online.Rooms;
|
||||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Multiplayer
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
{
|
{
|
||||||
public partial class TestSceneMultiplayerMatchFooter : MultiplayerTestScene
|
public partial class TestSceneMultiplayerMatchFooter : MultiplayerTestScene
|
||||||
{
|
{
|
||||||
|
[Cached(typeof(IBindable<PlaylistItem>))]
|
||||||
|
private readonly Bindable<PlaylistItem> currentItem = new Bindable<PlaylistItem>();
|
||||||
|
|
||||||
public override void SetUpSteps()
|
public override void SetUpSteps()
|
||||||
{
|
{
|
||||||
base.SetUpSteps();
|
base.SetUpSteps();
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
@ -15,6 +16,7 @@ using osu.Framework.Platform;
|
|||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
using osu.Game.Overlays.Mods;
|
using osu.Game.Overlays.Mods;
|
||||||
@ -42,6 +44,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
private Live<BeatmapSetInfo> importedBeatmapSet;
|
private Live<BeatmapSetInfo> importedBeatmapSet;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuConfigManager configManager { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(GameHost host, AudioManager audio)
|
private void load(GameHost host, AudioManager audio)
|
||||||
{
|
{
|
||||||
@ -57,10 +62,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
Add(detachedBeatmapStore);
|
Add(detachedBeatmapStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void SetUpSteps()
|
private void setUp()
|
||||||
{
|
{
|
||||||
base.SetUpSteps();
|
|
||||||
|
|
||||||
AddStep("reset", () =>
|
AddStep("reset", () =>
|
||||||
{
|
{
|
||||||
Ruleset.Value = new OsuRuleset().RulesetInfo;
|
Ruleset.Value = new OsuRuleset().RulesetInfo;
|
||||||
@ -75,6 +78,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestSelectFreeMods()
|
public void TestSelectFreeMods()
|
||||||
{
|
{
|
||||||
|
setUp();
|
||||||
|
|
||||||
AddStep("set some freemods", () => songSelect.FreeMods.Value = new OsuRuleset().GetModsFor(ModType.Fun).ToArray());
|
AddStep("set some freemods", () => songSelect.FreeMods.Value = new OsuRuleset().GetModsFor(ModType.Fun).ToArray());
|
||||||
AddStep("set all freemods", () => songSelect.FreeMods.Value = new OsuRuleset().CreateAllMods().ToArray());
|
AddStep("set all freemods", () => songSelect.FreeMods.Value = new OsuRuleset().CreateAllMods().ToArray());
|
||||||
AddStep("set no freemods", () => songSelect.FreeMods.Value = Array.Empty<Mod>());
|
AddStep("set no freemods", () => songSelect.FreeMods.Value = Array.Empty<Mod>());
|
||||||
@ -85,6 +90,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
BeatmapInfo selectedBeatmap = null;
|
BeatmapInfo selectedBeatmap = null;
|
||||||
|
|
||||||
|
setUp();
|
||||||
|
|
||||||
AddStep("change ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo);
|
AddStep("change ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo);
|
||||||
AddStep("select beatmap",
|
AddStep("select beatmap",
|
||||||
() => songSelect.Carousel.SelectBeatmap(selectedBeatmap = beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == new TaikoRuleset().LegacyID)));
|
() => songSelect.Carousel.SelectBeatmap(selectedBeatmap = beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == new TaikoRuleset().LegacyID)));
|
||||||
@ -107,6 +114,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
[TestCase(typeof(OsuModHidden), typeof(OsuModTraceable))] // Incompatible.
|
[TestCase(typeof(OsuModHidden), typeof(OsuModTraceable))] // Incompatible.
|
||||||
public void TestAllowedModDeselectedWhenRequired(Type allowedMod, Type requiredMod)
|
public void TestAllowedModDeselectedWhenRequired(Type allowedMod, Type requiredMod)
|
||||||
{
|
{
|
||||||
|
setUp();
|
||||||
|
|
||||||
AddStep("change ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo);
|
AddStep("change ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo);
|
||||||
AddStep($"select {allowedMod.ReadableName()} as allowed", () => songSelect.FreeMods.Value = new[] { (Mod)Activator.CreateInstance(allowedMod) });
|
AddStep($"select {allowedMod.ReadableName()} as allowed", () => songSelect.FreeMods.Value = new[] { (Mod)Activator.CreateInstance(allowedMod) });
|
||||||
AddStep($"select {requiredMod.ReadableName()} as required", () => songSelect.Mods.Value = new[] { (Mod)Activator.CreateInstance(requiredMod) });
|
AddStep($"select {requiredMod.ReadableName()} as required", () => songSelect.Mods.Value = new[] { (Mod)Activator.CreateInstance(requiredMod) });
|
||||||
@ -120,6 +129,30 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
assertFreeModNotShown(requiredMod);
|
assertFreeModNotShown(requiredMod);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestChangeRulesetImmediatelyAfterLoadComplete()
|
||||||
|
{
|
||||||
|
AddStep("reset", () =>
|
||||||
|
{
|
||||||
|
configManager.SetValue(OsuSetting.ShowConvertedBeatmaps, false);
|
||||||
|
Beatmap.SetDefault();
|
||||||
|
SelectedMods.SetDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("create song select", () =>
|
||||||
|
{
|
||||||
|
SelectedRoom.Value.Playlist.Single().RulesetID = 2;
|
||||||
|
songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value, SelectedRoom.Value.Playlist.Single());
|
||||||
|
songSelect.OnLoadComplete += _ => Ruleset.Value = new TaikoRuleset().RulesetInfo;
|
||||||
|
LoadScreen(songSelect);
|
||||||
|
});
|
||||||
|
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded);
|
||||||
|
|
||||||
|
AddStep("confirm selection", () => songSelect.FinaliseSelection());
|
||||||
|
AddAssert("beatmap is taiko", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(1));
|
||||||
|
AddAssert("ruleset is taiko", () => Ruleset.Value.OnlineID, () => Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
private void assertFreeModNotShown(Type type)
|
private void assertFreeModNotShown(Type type)
|
||||||
{
|
{
|
||||||
AddAssert($"{type.ReadableName()} not displayed in freemod overlay",
|
AddAssert($"{type.ReadableName()} not displayed in freemod overlay",
|
||||||
@ -138,8 +171,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
public new BeatmapCarousel Carousel => base.Carousel;
|
public new BeatmapCarousel Carousel => base.Carousel;
|
||||||
|
|
||||||
public TestMultiplayerMatchSongSelect(Room room)
|
public TestMultiplayerMatchSongSelect(Room room, [CanBeNull] PlaylistItem itemToEdit = null)
|
||||||
: base(room)
|
: base(room, itemToEdit)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -165,11 +165,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
AddUntilStep("wait for room join", () => RoomJoined);
|
AddUntilStep("wait for room join", () => RoomJoined);
|
||||||
|
|
||||||
AddStep("join other user (ready)", () =>
|
AddStep("join other user", void () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID }));
|
||||||
{
|
AddUntilStep("wait for user populated", () => MultiplayerClient.ClientRoom!.Users.Single(u => u.UserID == PLAYER_1_ID).User, () => Is.Not.Null);
|
||||||
MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID });
|
AddStep("other user ready", () => MultiplayerClient.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready));
|
||||||
MultiplayerClient.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready);
|
|
||||||
});
|
|
||||||
|
|
||||||
ClickButtonWhenEnabled<MultiplayerSpectateButton>();
|
ClickButtonWhenEnabled<MultiplayerSpectateButton>();
|
||||||
|
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
@ -29,10 +28,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
public partial class TestSceneMultiplayerPlaylist : MultiplayerTestScene
|
public partial class TestSceneMultiplayerPlaylist : MultiplayerTestScene
|
||||||
{
|
{
|
||||||
private MultiplayerPlaylist list;
|
[Cached(typeof(IBindable<PlaylistItem>))]
|
||||||
private BeatmapManager beatmaps;
|
private readonly Bindable<PlaylistItem> currentItem = new Bindable<PlaylistItem>();
|
||||||
private BeatmapSetInfo importedSet;
|
|
||||||
private BeatmapInfo importedBeatmap;
|
private MultiplayerPlaylist list = null!;
|
||||||
|
private BeatmapManager beatmaps = null!;
|
||||||
|
private BeatmapSetInfo importedSet = null!;
|
||||||
|
private BeatmapInfo importedBeatmap = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(GameHost host, AudioManager audio)
|
private void load(GameHost host, AudioManager audio)
|
||||||
@ -198,7 +200,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
addItemStep();
|
addItemStep();
|
||||||
addItemStep();
|
addItemStep();
|
||||||
|
|
||||||
DrawableRoomPlaylistItem[] drawableItems = null;
|
DrawableRoomPlaylistItem[] drawableItems = null!;
|
||||||
AddStep("get drawable items", () => drawableItems = this.ChildrenOfType<DrawableRoomPlaylistItem>().ToArray());
|
AddStep("get drawable items", () => drawableItems = this.ChildrenOfType<DrawableRoomPlaylistItem>().ToArray());
|
||||||
|
|
||||||
// Add 1 item for another user.
|
// Add 1 item for another user.
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -28,13 +26,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
public partial class TestSceneMultiplayerSpectateButton : MultiplayerTestScene
|
public partial class TestSceneMultiplayerSpectateButton : MultiplayerTestScene
|
||||||
{
|
{
|
||||||
private MultiplayerSpectateButton spectateButton;
|
[Cached(typeof(IBindable<PlaylistItem>))]
|
||||||
private MatchStartControl startControl;
|
private readonly Bindable<PlaylistItem> currentItem = new Bindable<PlaylistItem>();
|
||||||
|
|
||||||
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
|
private MultiplayerSpectateButton spectateButton = null!;
|
||||||
|
private MatchStartControl startControl = null!;
|
||||||
|
|
||||||
private BeatmapSetInfo importedSet;
|
private BeatmapSetInfo importedSet = null!;
|
||||||
private BeatmapManager beatmaps;
|
private BeatmapManager beatmaps = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(GameHost host, AudioManager audio)
|
private void load(GameHost host, AudioManager audio)
|
||||||
@ -52,14 +51,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
AddStep("create button", () =>
|
AddStep("create button", () =>
|
||||||
{
|
{
|
||||||
AvailabilityTracker.SelectedItem.BindTo(selectedItem);
|
AvailabilityTracker.SelectedItem.BindTo(currentItem);
|
||||||
|
|
||||||
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
|
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
|
||||||
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
|
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
|
||||||
selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
|
|
||||||
{
|
currentItem.Value = SelectedRoom.Value.Playlist.First();
|
||||||
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID,
|
|
||||||
};
|
|
||||||
|
|
||||||
Child = new PopoverContainer
|
Child = new PopoverContainer
|
||||||
{
|
{
|
||||||
|
@ -648,6 +648,34 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
AddUntilStep("Info message displayed", () => channelManager.CurrentChannel.Value.Messages.Last(), () => Is.InstanceOf(typeof(InfoMessage)));
|
AddUntilStep("Info message displayed", () => channelManager.CurrentChannel.Value.Messages.Last(), () => Is.InstanceOf(typeof(InfoMessage)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestFiltering()
|
||||||
|
{
|
||||||
|
AddStep("Show overlay", () => chatOverlay.Show());
|
||||||
|
joinTestChannel(1);
|
||||||
|
joinTestChannel(3);
|
||||||
|
joinTestChannel(5);
|
||||||
|
joinChannel(new Channel(new APIUser { Id = 2001, Username = "alice" }));
|
||||||
|
joinChannel(new Channel(new APIUser { Id = 2002, Username = "bob" }));
|
||||||
|
joinChannel(new Channel(new APIUser { Id = 2003, Username = "charley the plant" }));
|
||||||
|
|
||||||
|
AddStep("filter to \"c\"", () => chatOverlay.ChildrenOfType<SearchTextBox>().Single().Text = "c");
|
||||||
|
AddUntilStep("bob filtered out", () => chatOverlay.ChildrenOfType<ChannelListItem>().Count(i => i.Alpha > 0), () => Is.EqualTo(5));
|
||||||
|
|
||||||
|
AddStep("filter to \"channel\"", () => chatOverlay.ChildrenOfType<SearchTextBox>().Single().Text = "channel");
|
||||||
|
AddUntilStep("only public channels left", () => chatOverlay.ChildrenOfType<ChannelListItem>().Count(i => i.Alpha > 0), () => Is.EqualTo(3));
|
||||||
|
|
||||||
|
AddStep("commit textbox", () =>
|
||||||
|
{
|
||||||
|
chatOverlay.ChildrenOfType<SearchTextBox>().Single().TakeFocus();
|
||||||
|
Schedule(() => InputManager.PressKey(Key.Enter));
|
||||||
|
});
|
||||||
|
AddUntilStep("#channel-2 active", () => channelManager.CurrentChannel.Value.Name, () => Is.EqualTo("#channel-2"));
|
||||||
|
|
||||||
|
AddStep("filter to \"channel-3\"", () => chatOverlay.ChildrenOfType<SearchTextBox>().Single().Text = "channel-3");
|
||||||
|
AddUntilStep("no channels left", () => chatOverlay.ChildrenOfType<ChannelListItem>().Count(i => i.Alpha > 0), () => Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
private void joinTestChannel(int i)
|
private void joinTestChannel(int i)
|
||||||
{
|
{
|
||||||
AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i]));
|
AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i]));
|
||||||
|
@ -30,14 +30,20 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
if (supportLevel > 3)
|
if (supportLevel > 3)
|
||||||
supportLevel = 0;
|
supportLevel = 0;
|
||||||
|
|
||||||
((DummyAPIAccess)API).Friends.Add(new APIUser
|
((DummyAPIAccess)API).Friends.Add(new APIRelation
|
||||||
{
|
{
|
||||||
Username = @"peppy",
|
TargetID = 2,
|
||||||
Id = 2,
|
RelationType = RelationType.Friend,
|
||||||
Colour = "99EB47",
|
Mutual = true,
|
||||||
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
TargetUser = new APIUser
|
||||||
IsSupporter = supportLevel > 0,
|
{
|
||||||
SupportLevel = supportLevel
|
Username = @"peppy",
|
||||||
|
Id = 2,
|
||||||
|
Colour = "99EB47",
|
||||||
|
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||||
|
IsSupporter = supportLevel > 0,
|
||||||
|
SupportLevel = supportLevel
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,11 +8,13 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions.LocalisationExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
@ -90,6 +92,48 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
AddAssert("no best score displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 1);
|
AddAssert("no best score displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHitResultsWithSameNameAreGrouped()
|
||||||
|
{
|
||||||
|
AddStep("Load scores without user best", () =>
|
||||||
|
{
|
||||||
|
var allScores = createScores();
|
||||||
|
allScores.UserScore = null;
|
||||||
|
scoresContainer.Scores = allScores;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType<ScoreTableRowBackground>().Any());
|
||||||
|
AddAssert("only one column for slider end", () =>
|
||||||
|
{
|
||||||
|
ScoreTable scoreTable = scoresContainer.ChildrenOfType<ScoreTable>().First();
|
||||||
|
return scoreTable.Columns.Count(c => c.Header.Equals("slider end")) == 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("all rows show non-zero slider ends", () =>
|
||||||
|
{
|
||||||
|
ScoreTable scoreTable = scoresContainer.ChildrenOfType<ScoreTable>().First();
|
||||||
|
int sliderEndColumnIndex = Array.FindIndex(scoreTable.Columns, c => c != null && c.Header.Equals("slider end"));
|
||||||
|
bool sliderEndFilledInEachRow = true;
|
||||||
|
|
||||||
|
for (int i = 0; i < scoreTable.Content?.GetLength(0); i++)
|
||||||
|
{
|
||||||
|
switch (scoreTable.Content[i, sliderEndColumnIndex])
|
||||||
|
{
|
||||||
|
case OsuSpriteText text:
|
||||||
|
if (text.Text.Equals(0.0d.ToLocalisableString(@"N0")))
|
||||||
|
sliderEndFilledInEachRow = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
sliderEndFilledInEachRow = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sliderEndFilledInEachRow;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestUserBest()
|
public void TestUserBest()
|
||||||
{
|
{
|
||||||
@ -103,6 +147,18 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType<ScoreTableRowBackground>().Any());
|
AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType<ScoreTableRowBackground>().Any());
|
||||||
AddAssert("best score displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 2);
|
AddAssert("best score displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 2);
|
||||||
|
|
||||||
|
AddStep("Load scores with personal best FC", () =>
|
||||||
|
{
|
||||||
|
var allScores = createScores();
|
||||||
|
allScores.UserScore = createUserBest();
|
||||||
|
allScores.UserScore.Score.Accuracy = 1;
|
||||||
|
scoresContainer.Beatmap.Value.MaxCombo = allScores.UserScore.Score.MaxCombo = 1337;
|
||||||
|
scoresContainer.Scores = allScores;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType<ScoreTableRowBackground>().Any());
|
||||||
|
AddAssert("best score displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 2);
|
||||||
|
|
||||||
AddStep("Load scores with personal best (null position)", () =>
|
AddStep("Load scores with personal best (null position)", () =>
|
||||||
{
|
{
|
||||||
var allScores = createScores();
|
var allScores = createScores();
|
||||||
@ -287,13 +343,17 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
|
|
||||||
const int initial_great_count = 2000;
|
const int initial_great_count = 2000;
|
||||||
const int initial_tick_count = 100;
|
const int initial_tick_count = 100;
|
||||||
|
const int initial_slider_end_count = 500;
|
||||||
|
|
||||||
int greatCount = initial_great_count;
|
int greatCount = initial_great_count;
|
||||||
int tickCount = initial_tick_count;
|
int tickCount = initial_tick_count;
|
||||||
|
int sliderEndCount = initial_slider_end_count;
|
||||||
|
|
||||||
foreach (var s in scores.Scores)
|
foreach (var (score, index) in scores.Scores.Select((s, i) => (s, i)))
|
||||||
{
|
{
|
||||||
s.Statistics = new Dictionary<HitResult, int>
|
HitResult sliderEndResult = index % 2 == 0 ? HitResult.SliderTailHit : HitResult.SmallTickHit;
|
||||||
|
|
||||||
|
score.Statistics = new Dictionary<HitResult, int>
|
||||||
{
|
{
|
||||||
{ HitResult.Great, greatCount },
|
{ HitResult.Great, greatCount },
|
||||||
{ HitResult.LargeTickHit, tickCount },
|
{ HitResult.LargeTickHit, tickCount },
|
||||||
@ -301,10 +361,19 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{ HitResult.Meh, RNG.Next(100) },
|
{ HitResult.Meh, RNG.Next(100) },
|
||||||
{ HitResult.Miss, initial_great_count - greatCount },
|
{ HitResult.Miss, initial_great_count - greatCount },
|
||||||
{ HitResult.LargeTickMiss, initial_tick_count - tickCount },
|
{ HitResult.LargeTickMiss, initial_tick_count - tickCount },
|
||||||
|
{ sliderEndResult, sliderEndCount },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Some hit results, including SliderTailHit and SmallTickHit, are only displayed
|
||||||
|
// when the maximum number is known
|
||||||
|
score.MaximumStatistics = new Dictionary<HitResult, int>
|
||||||
|
{
|
||||||
|
{ sliderEndResult, initial_slider_end_count },
|
||||||
};
|
};
|
||||||
|
|
||||||
greatCount -= 100;
|
greatCount -= 100;
|
||||||
tickCount -= RNG.Next(1, 5);
|
tickCount -= RNG.Next(1, 5);
|
||||||
|
sliderEndCount -= 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
return scores;
|
return scores;
|
||||||
|
@ -3,15 +3,20 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics.Containers;
|
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.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Profile;
|
using osu.Game.Overlays.Profile;
|
||||||
|
using osu.Game.Overlays.Profile.Header.Components;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
|
||||||
@ -22,6 +27,10 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Cached]
|
[Cached]
|
||||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
|
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
|
||||||
|
|
||||||
|
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||||
|
|
||||||
|
private readonly ManualResetEventSlim requestLock = new ManualResetEventSlim();
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuConfigManager configManager { get; set; } = null!;
|
private OsuConfigManager configManager { get; set; } = null!;
|
||||||
|
|
||||||
@ -400,5 +409,97 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
}
|
}
|
||||||
}, new OsuRuleset().RulesetInfo));
|
}, new OsuRuleset().RulesetInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private APIUser nonFriend => new APIUser
|
||||||
|
{
|
||||||
|
Id = 727,
|
||||||
|
Username = "Whatever",
|
||||||
|
};
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAddFriend()
|
||||||
|
{
|
||||||
|
AddStep("Setup request", () =>
|
||||||
|
{
|
||||||
|
requestLock.Reset();
|
||||||
|
|
||||||
|
dummyAPI.HandleRequest = request =>
|
||||||
|
{
|
||||||
|
if (request is not AddFriendRequest req)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (req.TargetId != nonFriend.OnlineID)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var apiRelation = new APIRelation
|
||||||
|
{
|
||||||
|
TargetID = nonFriend.OnlineID,
|
||||||
|
Mutual = true,
|
||||||
|
RelationType = RelationType.Friend,
|
||||||
|
TargetUser = nonFriend
|
||||||
|
};
|
||||||
|
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
requestLock.Wait(3000);
|
||||||
|
dummyAPI.Friends.Add(apiRelation);
|
||||||
|
req.TriggerSuccess(new AddFriendResponse
|
||||||
|
{
|
||||||
|
UserRelation = apiRelation
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
AddStep("clear friend list", () => dummyAPI.Friends.Clear());
|
||||||
|
AddStep("Show non-friend user", () => header.User.Value = new UserProfileData(nonFriend, new OsuRuleset().RulesetInfo));
|
||||||
|
AddStep("Click followers button", () => this.ChildrenOfType<FollowersButton>().First().TriggerClick());
|
||||||
|
AddStep("Complete request", () => requestLock.Set());
|
||||||
|
AddUntilStep("Friend added", () => API.Friends.Any(f => f.TargetID == nonFriend.OnlineID));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAddFriendNonMutual()
|
||||||
|
{
|
||||||
|
AddStep("Setup request", () =>
|
||||||
|
{
|
||||||
|
requestLock.Reset();
|
||||||
|
|
||||||
|
dummyAPI.HandleRequest = request =>
|
||||||
|
{
|
||||||
|
if (request is not AddFriendRequest req)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (req.TargetId != nonFriend.OnlineID)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var apiRelation = new APIRelation
|
||||||
|
{
|
||||||
|
TargetID = nonFriend.OnlineID,
|
||||||
|
Mutual = false,
|
||||||
|
RelationType = RelationType.Friend,
|
||||||
|
TargetUser = nonFriend
|
||||||
|
};
|
||||||
|
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
requestLock.Wait(3000);
|
||||||
|
dummyAPI.Friends.Add(apiRelation);
|
||||||
|
req.TriggerSuccess(new AddFriendResponse
|
||||||
|
{
|
||||||
|
UserRelation = apiRelation
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
AddStep("clear friend list", () => dummyAPI.Friends.Clear());
|
||||||
|
AddStep("Show non-friend user", () => header.User.Value = new UserProfileData(nonFriend, new OsuRuleset().RulesetInfo));
|
||||||
|
AddStep("Click followers button", () => this.ChildrenOfType<FollowersButton>().First().TriggerClick());
|
||||||
|
AddStep("Complete request", () => requestLock.Set());
|
||||||
|
AddUntilStep("Friend added", () => API.Friends.Any(f => f.TargetID == nonFriend.OnlineID));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -414,11 +414,7 @@ namespace osu.Game.Tests.Visual.Settings
|
|||||||
});
|
});
|
||||||
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre));
|
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre));
|
||||||
scrollToAndStartBinding("Left (centre)");
|
scrollToAndStartBinding("Left (centre)");
|
||||||
AddStep("clear binding", () =>
|
clearBinding();
|
||||||
{
|
|
||||||
var row = panel.ChildrenOfType<KeyBindingRow>().First(r => r.ChildrenOfType<OsuSpriteText>().Any(s => s.Text.ToString() == "Left (centre)"));
|
|
||||||
row.ChildrenOfType<KeyBindingRow.ClearButton>().Single().TriggerClick();
|
|
||||||
});
|
|
||||||
scrollToAndStartBinding("Left (rim)");
|
scrollToAndStartBinding("Left (rim)");
|
||||||
AddStep("bind M1", () => InputManager.Click(MouseButton.Left));
|
AddStep("bind M1", () => InputManager.Click(MouseButton.Left));
|
||||||
|
|
||||||
@ -431,6 +427,45 @@ namespace osu.Game.Tests.Visual.Settings
|
|||||||
AddUntilStep("conflict popover not shown", () => panel.ChildrenOfType<KeyBindingConflictPopover>().SingleOrDefault(), () => Is.Null);
|
AddUntilStep("conflict popover not shown", () => panel.ChildrenOfType<KeyBindingConflictPopover>().SingleOrDefault(), () => Is.Null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestResettingRowCannotConflictWithItself()
|
||||||
|
{
|
||||||
|
AddStep("reset taiko section to default", () =>
|
||||||
|
{
|
||||||
|
var section = panel.ChildrenOfType<VariantBindingsSubsection>().First(section => new TaikoRuleset().RulesetInfo.Equals(section.Ruleset));
|
||||||
|
section.ChildrenOfType<ResetButton>().Single().TriggerClick();
|
||||||
|
});
|
||||||
|
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre));
|
||||||
|
|
||||||
|
scrollToAndStartBinding("Left (centre)");
|
||||||
|
clearBinding();
|
||||||
|
scrollToAndStartBinding("Left (centre)", 1);
|
||||||
|
clearBinding();
|
||||||
|
|
||||||
|
scrollToAndStartBinding("Left (centre)");
|
||||||
|
AddStep("bind F", () => InputManager.Key(Key.F));
|
||||||
|
scrollToAndStartBinding("Left (centre)", 1);
|
||||||
|
AddStep("bind M1", () => InputManager.Click(MouseButton.Left));
|
||||||
|
|
||||||
|
AddStep("revert row to default", () =>
|
||||||
|
{
|
||||||
|
var row = panel.ChildrenOfType<KeyBindingRow>().First(r => r.ChildrenOfType<OsuSpriteText>().Any(s => s.Text.ToString() == "Left (centre)"));
|
||||||
|
InputManager.MoveMouseTo(row.ChildrenOfType<RevertToDefaultButton<bool>>().Single());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
AddWaitStep("wait a bit", 3);
|
||||||
|
AddUntilStep("conflict popover not shown", () => panel.ChildrenOfType<KeyBindingConflictPopover>().SingleOrDefault(), () => Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearBinding()
|
||||||
|
{
|
||||||
|
AddStep("clear binding", () =>
|
||||||
|
{
|
||||||
|
var row = panel.ChildrenOfType<KeyBindingRow>().First(r => r.ChildrenOfType<OsuSpriteText>().Any(s => s.Text.ToString() == "Left (centre)"));
|
||||||
|
row.ChildrenOfType<KeyBindingRow.ClearButton>().Single().TriggerClick();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void checkBinding(string name, string keyName)
|
private void checkBinding(string name, string keyName)
|
||||||
{
|
{
|
||||||
AddAssert($"Check {name} is bound to {keyName}", () =>
|
AddAssert($"Check {name} is bound to {keyName}", () =>
|
||||||
@ -442,23 +477,23 @@ namespace osu.Game.Tests.Visual.Settings
|
|||||||
}, () => Is.EqualTo(keyName));
|
}, () => Is.EqualTo(keyName));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scrollToAndStartBinding(string name)
|
private void scrollToAndStartBinding(string name, int bindingIndex = 0)
|
||||||
{
|
{
|
||||||
KeyBindingRow.KeyButton firstButton = null;
|
KeyBindingRow.KeyButton targetButton = null;
|
||||||
|
|
||||||
AddStep($"Scroll to {name}", () =>
|
AddStep($"Scroll to {name}", () =>
|
||||||
{
|
{
|
||||||
var firstRow = panel.ChildrenOfType<KeyBindingRow>().First(r => r.ChildrenOfType<OsuSpriteText>().Any(s => s.Text.ToString() == name));
|
var firstRow = panel.ChildrenOfType<KeyBindingRow>().First(r => r.ChildrenOfType<OsuSpriteText>().Any(s => s.Text.ToString() == name));
|
||||||
firstButton = firstRow.ChildrenOfType<KeyBindingRow.KeyButton>().First();
|
targetButton = firstRow.ChildrenOfType<KeyBindingRow.KeyButton>().ElementAt(bindingIndex);
|
||||||
|
|
||||||
panel.ChildrenOfType<SettingsPanel.SettingsSectionsContainer>().First().ScrollTo(firstButton);
|
panel.ChildrenOfType<SettingsPanel.SettingsSectionsContainer>().First().ScrollTo(targetButton);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddWaitStep("wait for scroll", 5);
|
AddWaitStep("wait for scroll", 5);
|
||||||
|
|
||||||
AddStep("click to bind", () =>
|
AddStep("click to bind", () =>
|
||||||
{
|
{
|
||||||
InputManager.MoveMouseTo(firstButton);
|
InputManager.MoveMouseTo(targetButton);
|
||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,12 @@ using System.Linq;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Extensions;
|
using osu.Game.Extensions;
|
||||||
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Catch;
|
using osu.Game.Rulesets.Catch;
|
||||||
using osu.Game.Rulesets.Mania;
|
using osu.Game.Rulesets.Mania;
|
||||||
@ -191,8 +194,39 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
{
|
{
|
||||||
AddStep("present beatmap", () => Game.PresentBeatmap(getImport()));
|
AddStep("present beatmap", () => Game.PresentBeatmap(getImport()));
|
||||||
|
|
||||||
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect);
|
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded);
|
||||||
AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.MatchesOnlineID(getImport().Beatmaps[expectedDiff - 1]));
|
AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.MatchesOnlineID(getImport().Beatmaps[expectedDiff - 1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override TestOsuGame CreateTestGame() => new NoBeatmapUpdateGame(LocalStorage, API);
|
||||||
|
|
||||||
|
private partial class NoBeatmapUpdateGame : TestOsuGame
|
||||||
|
{
|
||||||
|
public NoBeatmapUpdateGame(Storage storage, IAPIProvider api, string[] args = null)
|
||||||
|
: base(storage, api, args)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IBeatmapUpdater CreateBeatmapUpdater() => new TestBeatmapUpdater();
|
||||||
|
|
||||||
|
private class TestBeatmapUpdater : IBeatmapUpdater
|
||||||
|
{
|
||||||
|
public void Queue(Live<BeatmapSetInfo> beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProcessObjectCounts(BeatmapInfo beatmapInfo, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,234 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Models;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Difficulty;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Skinning.Components;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
|
{
|
||||||
|
public partial class TestSceneBeatmapAttributeText : OsuTestScene
|
||||||
|
{
|
||||||
|
private readonly BeatmapAttributeText text;
|
||||||
|
|
||||||
|
public TestSceneBeatmapAttributeText()
|
||||||
|
{
|
||||||
|
Child = text = new BeatmapAttributeText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup() => Schedule(() =>
|
||||||
|
{
|
||||||
|
SelectedMods.SetDefault();
|
||||||
|
Ruleset.Value = new OsuRuleset().RulesetInfo;
|
||||||
|
Beatmap.Value = CreateWorkingBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
|
||||||
|
{
|
||||||
|
BeatmapInfo =
|
||||||
|
{
|
||||||
|
BPM = 100,
|
||||||
|
DifficultyName = "_Difficulty",
|
||||||
|
Status = BeatmapOnlineStatus.Loved,
|
||||||
|
Metadata =
|
||||||
|
{
|
||||||
|
Title = "_Title",
|
||||||
|
TitleUnicode = "_Title",
|
||||||
|
Artist = "_Artist",
|
||||||
|
ArtistUnicode = "_Artist",
|
||||||
|
Author = new RealmUser { Username = "_Creator" },
|
||||||
|
Source = "_Source",
|
||||||
|
},
|
||||||
|
Difficulty =
|
||||||
|
{
|
||||||
|
CircleSize = 1,
|
||||||
|
DrainRate = 2,
|
||||||
|
OverallDifficulty = 3,
|
||||||
|
ApproachRate = 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
[TestCase(BeatmapAttribute.CircleSize, "Circle Size: 1.00")]
|
||||||
|
[TestCase(BeatmapAttribute.HPDrain, "HP Drain: 2.00")]
|
||||||
|
[TestCase(BeatmapAttribute.Accuracy, "Accuracy: 3.00")]
|
||||||
|
[TestCase(BeatmapAttribute.ApproachRate, "Approach Rate: 4.00")]
|
||||||
|
[TestCase(BeatmapAttribute.Title, "Title: _Title")]
|
||||||
|
[TestCase(BeatmapAttribute.Artist, "Artist: _Artist")]
|
||||||
|
[TestCase(BeatmapAttribute.Creator, "Creator: _Creator")]
|
||||||
|
[TestCase(BeatmapAttribute.DifficultyName, "Difficulty: _Difficulty")]
|
||||||
|
[TestCase(BeatmapAttribute.Source, "Source: _Source")]
|
||||||
|
[TestCase(BeatmapAttribute.RankedStatus, "Beatmap Status: Loved")]
|
||||||
|
public void TestAttributeDisplay(BeatmapAttribute attribute, string expectedText)
|
||||||
|
{
|
||||||
|
AddStep($"set attribute: {attribute}", () => text.Attribute.Value = attribute);
|
||||||
|
AddAssert("check correct text", getText, () => Is.EqualTo(expectedText));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestChangeBeatmap()
|
||||||
|
{
|
||||||
|
AddStep("set title attribute", () => text.Attribute.Value = BeatmapAttribute.Title);
|
||||||
|
AddAssert("check initial title", getText, () => Is.EqualTo("Title: _Title"));
|
||||||
|
|
||||||
|
AddStep("change to beatmap with another title", () => Beatmap.Value = CreateWorkingBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
|
||||||
|
{
|
||||||
|
BeatmapInfo =
|
||||||
|
{
|
||||||
|
Metadata =
|
||||||
|
{
|
||||||
|
Title = "Another"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddAssert("check new title", getText, () => Is.EqualTo("Title: Another"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestWithMods()
|
||||||
|
{
|
||||||
|
AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
|
||||||
|
{
|
||||||
|
BeatmapInfo =
|
||||||
|
{
|
||||||
|
BPM = 100,
|
||||||
|
Length = 30000,
|
||||||
|
Difficulty =
|
||||||
|
{
|
||||||
|
ApproachRate = 10,
|
||||||
|
CircleSize = 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
test(BeatmapAttribute.BPM, new OsuModDoubleTime(), "BPM: 100.00", "BPM: 150.00");
|
||||||
|
test(BeatmapAttribute.Length, new OsuModDoubleTime(), "Length: 00:30", "Length: 00:20");
|
||||||
|
test(BeatmapAttribute.ApproachRate, new OsuModDoubleTime(), "Approach Rate: 10.00", "Approach Rate: 11.00");
|
||||||
|
test(BeatmapAttribute.CircleSize, new OsuModHardRock(), "Circle Size: 9.00", "Circle Size: 10.00");
|
||||||
|
|
||||||
|
void test(BeatmapAttribute attribute, Mod mod, string before, string after)
|
||||||
|
{
|
||||||
|
AddStep($"set attribute: {attribute}", () => text.Attribute.Value = attribute);
|
||||||
|
AddAssert("check text is correct", getText, () => Is.EqualTo(before));
|
||||||
|
|
||||||
|
AddStep("add DT mod", () => SelectedMods.Value = new[] { mod });
|
||||||
|
AddAssert("check text is correct", getText, () => Is.EqualTo(after));
|
||||||
|
AddStep("clear mods", () => SelectedMods.SetDefault());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestStarRating()
|
||||||
|
{
|
||||||
|
AddStep("set test ruleset", () => Ruleset.Value = new TestRuleset().RulesetInfo);
|
||||||
|
AddStep("set star rating attribute", () => text.Attribute.Value = BeatmapAttribute.StarRating);
|
||||||
|
AddAssert("check star rating is 0", getText, () => Is.EqualTo("Star Rating: 0.00"));
|
||||||
|
|
||||||
|
// Adding mod
|
||||||
|
TestMod mod = null!;
|
||||||
|
AddStep("add mod with difficulty 1", () => SelectedMods.Value = new[] { mod = new TestMod { Difficulty = { Value = 1 } } });
|
||||||
|
AddUntilStep("check star rating is 1", getText, () => Is.EqualTo("Star Rating: 1.00"));
|
||||||
|
|
||||||
|
// Changing mod setting
|
||||||
|
AddStep("change mod difficulty to 2", () => mod.Difficulty.Value = 2);
|
||||||
|
AddUntilStep("check star rating is 2", getText, () => Is.EqualTo("Star Rating: 2.00"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string getText() => text.ChildrenOfType<SpriteText>().Single().Text.ToString();
|
||||||
|
|
||||||
|
private class TestRuleset : Ruleset
|
||||||
|
{
|
||||||
|
public override IEnumerable<Mod> GetModsFor(ModType type) => new[]
|
||||||
|
{
|
||||||
|
new TestMod()
|
||||||
|
};
|
||||||
|
|
||||||
|
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap)
|
||||||
|
=> new OsuRuleset().CreateBeatmapConverter(beatmap);
|
||||||
|
|
||||||
|
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap)
|
||||||
|
=> new TestDifficultyCalculator(new TestRuleset().RulesetInfo, beatmap);
|
||||||
|
|
||||||
|
public override PerformanceCalculator CreatePerformanceCalculator()
|
||||||
|
=> new TestPerformanceCalculator(new TestRuleset());
|
||||||
|
|
||||||
|
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)
|
||||||
|
=> null!;
|
||||||
|
|
||||||
|
public override string Description => string.Empty;
|
||||||
|
public override string ShortName => string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestDifficultyCalculator : DifficultyCalculator
|
||||||
|
{
|
||||||
|
public TestDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
|
: base(ruleset, beatmap)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
|
=> new DifficultyAttributes(mods, mods.OfType<TestMod>().SingleOrDefault()?.Difficulty.Value ?? 0);
|
||||||
|
|
||||||
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
|
=> Array.Empty<DifficultyHitObject>();
|
||||||
|
|
||||||
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||||
|
=> Array.Empty<Skill>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestPerformanceCalculator : PerformanceCalculator
|
||||||
|
{
|
||||||
|
public TestPerformanceCalculator(Ruleset ruleset)
|
||||||
|
: base(ruleset)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
|
||||||
|
=> new PerformanceAttributes { Total = score.Mods.OfType<TestMod>().SingleOrDefault()?.Performance.Value ?? 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestMod : Mod
|
||||||
|
{
|
||||||
|
[SettingSource("difficulty")]
|
||||||
|
public BindableDouble Difficulty { get; } = new BindableDouble(0);
|
||||||
|
|
||||||
|
[SettingSource("performance")]
|
||||||
|
public BindableDouble Performance { get; } = new BindableDouble(0);
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public TestMod()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string Name => string.Empty;
|
||||||
|
public override LocalisableString Description => string.Empty;
|
||||||
|
public override double ScoreMultiplier => 1.0;
|
||||||
|
public override string Acronym => "Test";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="DeepEqual" Version="4.2.1" />
|
<PackageReference Include="DeepEqual" Version="4.2.1" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
|
||||||
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
|
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
|
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -60,12 +60,18 @@ namespace osu.Game.Audio
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int Volume { get; }
|
public int Volume { get; }
|
||||||
|
|
||||||
public HitSampleInfo(string name, string bank = SampleControlPoint.DEFAULT_BANK, string? suffix = null, int volume = 100)
|
/// <summary>
|
||||||
|
/// Whether this sample should automatically assign the bank of the normal sample whenever it is set in the editor.
|
||||||
|
/// </summary>
|
||||||
|
public bool EditorAutoBank { get; }
|
||||||
|
|
||||||
|
public HitSampleInfo(string name, string bank = SampleControlPoint.DEFAULT_BANK, string? suffix = null, int volume = 100, bool editorAutoBank = true)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
Bank = bank;
|
Bank = bank;
|
||||||
Suffix = suffix;
|
Suffix = suffix;
|
||||||
Volume = volume;
|
Volume = volume;
|
||||||
|
EditorAutoBank = editorAutoBank;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -92,9 +98,10 @@ namespace osu.Game.Audio
|
|||||||
/// <param name="newBank">An optional new sample bank.</param>
|
/// <param name="newBank">An optional new sample bank.</param>
|
||||||
/// <param name="newSuffix">An optional new lookup suffix.</param>
|
/// <param name="newSuffix">An optional new lookup suffix.</param>
|
||||||
/// <param name="newVolume">An optional new volume.</param>
|
/// <param name="newVolume">An optional new volume.</param>
|
||||||
|
/// <param name="newEditorAutoBank">An optional new editor auto bank flag.</param>
|
||||||
/// <returns>The new <see cref="HitSampleInfo"/>.</returns>
|
/// <returns>The new <see cref="HitSampleInfo"/>.</returns>
|
||||||
public virtual HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default)
|
public virtual HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default)
|
||||||
=> new HitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume));
|
=> new HitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume), newEditorAutoBank.GetOr(EditorAutoBank));
|
||||||
|
|
||||||
public virtual bool Equals(HitSampleInfo? other)
|
public virtual bool Equals(HitSampleInfo? other)
|
||||||
=> other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix;
|
=> other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix;
|
||||||
|
@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||||
|
|
||||||
var req = new GetBeatmapRequest(beatmapInfo);
|
var req = new GetBeatmapRequest(md5Hash: beatmapInfo.MD5Hash, filename: beatmapInfo.Path);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -62,7 +62,7 @@ namespace osu.Game.Beatmaps
|
|||||||
}
|
}
|
||||||
|
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
private BeatmapInfo()
|
protected BeatmapInfo()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,7 +285,8 @@ namespace osu.Game.Beatmaps
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="query">The query.</param>
|
/// <param name="query">The query.</param>
|
||||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
||||||
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => Realm.Run(r => r.All<BeatmapInfo>().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach());
|
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => Realm.Run(r =>
|
||||||
|
r.All<BeatmapInfo>().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach());
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
|
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
|
||||||
@ -313,6 +314,23 @@ namespace osu.Game.Beatmaps
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ResetAllOffsets()
|
||||||
|
{
|
||||||
|
const string reset_complete_message = "All offsets have been reset!";
|
||||||
|
Realm.Write(r =>
|
||||||
|
{
|
||||||
|
var items = r.All<BeatmapInfo>();
|
||||||
|
|
||||||
|
foreach (var beatmap in items)
|
||||||
|
{
|
||||||
|
if (beatmap.UserSettings.Offset != 0)
|
||||||
|
beatmap.UserSettings.Offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PostNotification?.Invoke(new ProgressCompletionNotification { Text = reset_complete_message });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public void Delete(Expression<Func<BeatmapSetInfo, bool>>? filter = null, bool silent = false)
|
public void Delete(Expression<Func<BeatmapSetInfo, bool>>? filter = null, bool silent = false)
|
||||||
{
|
{
|
||||||
Realm.Run(r =>
|
Realm.Run(r =>
|
||||||
|
@ -13,11 +13,11 @@ namespace osu.Game.Beatmaps
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class BeatmapOnlineChangeIngest : Component
|
public partial class BeatmapOnlineChangeIngest : Component
|
||||||
{
|
{
|
||||||
private readonly BeatmapUpdater beatmapUpdater;
|
private readonly IBeatmapUpdater beatmapUpdater;
|
||||||
private readonly RealmAccess realm;
|
private readonly RealmAccess realm;
|
||||||
private readonly MetadataClient metadataClient;
|
private readonly MetadataClient metadataClient;
|
||||||
|
|
||||||
public BeatmapOnlineChangeIngest(BeatmapUpdater beatmapUpdater, RealmAccess realm, MetadataClient metadataClient)
|
public BeatmapOnlineChangeIngest(IBeatmapUpdater beatmapUpdater, RealmAccess realm, MetadataClient metadataClient)
|
||||||
{
|
{
|
||||||
this.beatmapUpdater = beatmapUpdater;
|
this.beatmapUpdater = beatmapUpdater;
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -15,10 +14,7 @@ using osu.Game.Rulesets.Objects.Types;
|
|||||||
|
|
||||||
namespace osu.Game.Beatmaps
|
namespace osu.Game.Beatmaps
|
||||||
{
|
{
|
||||||
/// <summary>
|
public class BeatmapUpdater : IBeatmapUpdater
|
||||||
/// Handles all processing required to ensure a local beatmap is in a consistent state with any changes.
|
|
||||||
/// </summary>
|
|
||||||
public class BeatmapUpdater : IDisposable
|
|
||||||
{
|
{
|
||||||
private readonly IWorkingBeatmapCache workingBeatmapCache;
|
private readonly IWorkingBeatmapCache workingBeatmapCache;
|
||||||
|
|
||||||
@ -38,11 +34,6 @@ namespace osu.Game.Beatmaps
|
|||||||
metadataLookup = new BeatmapUpdaterMetadataLookup(api, storage);
|
metadataLookup = new BeatmapUpdaterMetadataLookup(api, storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Queue a beatmap for background processing.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="beatmapSet">The managed beatmap set to update. A transaction will be opened to apply changes.</param>
|
|
||||||
/// <param name="lookupScope">The preferred scope to use for metadata lookup.</param>
|
|
||||||
public void Queue(Live<BeatmapSetInfo> beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
|
public void Queue(Live<BeatmapSetInfo> beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
|
||||||
{
|
{
|
||||||
Logger.Log($"Queueing change for local beatmap {beatmapSet}");
|
Logger.Log($"Queueing change for local beatmap {beatmapSet}");
|
||||||
@ -50,55 +41,56 @@ namespace osu.Game.Beatmaps
|
|||||||
updateScheduler);
|
updateScheduler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
|
||||||
/// Run all processing on a beatmap immediately.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="beatmapSet">The managed beatmap set to update. A transaction will be opened to apply changes.</param>
|
|
||||||
/// <param name="lookupScope">The preferred scope to use for metadata lookup.</param>
|
|
||||||
public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapSet.Realm!.Write(_ =>
|
|
||||||
{
|
{
|
||||||
// Before we use below, we want to invalidate.
|
beatmapSet.Realm!.Write(_ =>
|
||||||
workingBeatmapCache.Invalidate(beatmapSet);
|
|
||||||
|
|
||||||
if (lookupScope != MetadataLookupScope.None)
|
|
||||||
metadataLookup.Update(beatmapSet, lookupScope == MetadataLookupScope.OnlineFirst);
|
|
||||||
|
|
||||||
foreach (var beatmap in beatmapSet.Beatmaps)
|
|
||||||
{
|
{
|
||||||
difficultyCache.Invalidate(beatmap);
|
// Before we use below, we want to invalidate.
|
||||||
|
workingBeatmapCache.Invalidate(beatmapSet);
|
||||||
|
|
||||||
var working = workingBeatmapCache.GetWorkingBeatmap(beatmap);
|
if (lookupScope != MetadataLookupScope.None)
|
||||||
var ruleset = working.BeatmapInfo.Ruleset.CreateInstance();
|
metadataLookup.Update(beatmapSet, lookupScope == MetadataLookupScope.OnlineFirst);
|
||||||
|
|
||||||
Debug.Assert(ruleset != null);
|
foreach (var beatmap in beatmapSet.Beatmaps)
|
||||||
|
{
|
||||||
|
difficultyCache.Invalidate(beatmap);
|
||||||
|
|
||||||
var calculator = ruleset.CreateDifficultyCalculator(working);
|
var working = workingBeatmapCache.GetWorkingBeatmap(beatmap);
|
||||||
|
var ruleset = working.BeatmapInfo.Ruleset.CreateInstance();
|
||||||
|
|
||||||
beatmap.StarRating = calculator.Calculate().StarRating;
|
Debug.Assert(ruleset != null);
|
||||||
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.
|
var calculator = ruleset.CreateDifficultyCalculator(working);
|
||||||
workingBeatmapCache.Invalidate(beatmapSet);
|
|
||||||
});
|
|
||||||
|
|
||||||
public void ProcessObjectCounts(BeatmapInfo beatmapInfo, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapInfo.Realm!.Write(_ =>
|
beatmap.StarRating = calculator.Calculate().StarRating;
|
||||||
|
beatmap.Length = working.Beatmap.CalculatePlayableLength();
|
||||||
|
beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength();
|
||||||
|
beatmap.EndTimeObjectCount = working.Beatmap.HitObjects.Count(h => h is IHasDuration);
|
||||||
|
beatmap.TotalObjectCount = working.Beatmap.HitObjects.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
|
||||||
|
workingBeatmapCache.Invalidate(beatmapSet);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProcessObjectCounts(BeatmapInfo beatmapInfo, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
|
||||||
{
|
{
|
||||||
// Before we use below, we want to invalidate.
|
beatmapInfo.Realm!.Write(_ =>
|
||||||
workingBeatmapCache.Invalidate(beatmapInfo);
|
{
|
||||||
|
// Before we use below, we want to invalidate.
|
||||||
|
workingBeatmapCache.Invalidate(beatmapInfo);
|
||||||
|
|
||||||
var working = workingBeatmapCache.GetWorkingBeatmap(beatmapInfo);
|
var working = workingBeatmapCache.GetWorkingBeatmap(beatmapInfo);
|
||||||
var beatmap = working.Beatmap;
|
var beatmap = working.Beatmap;
|
||||||
|
|
||||||
beatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration);
|
beatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration);
|
||||||
beatmapInfo.TotalObjectCount = beatmap.HitObjects.Count;
|
beatmapInfo.TotalObjectCount = beatmap.HitObjects.Count;
|
||||||
|
|
||||||
// And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
|
// And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
|
||||||
workingBeatmapCache.Invalidate(beatmapInfo);
|
workingBeatmapCache.Invalidate(beatmapInfo);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#region Implementation of IDisposable
|
#region Implementation of IDisposable
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Logging;
|
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
|
|
||||||
@ -44,10 +43,19 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
foreach (var beatmapInfo in beatmapSet.Beatmaps)
|
foreach (var beatmapInfo in beatmapSet.Beatmaps)
|
||||||
{
|
{
|
||||||
|
// note that these lookups DO NOT ACTUALLY FULLY GUARANTEE that the beatmap is what it claims it is,
|
||||||
|
// i.e. the correctness of this lookup should be treated as APPROXIMATE AT WORST.
|
||||||
|
// this is because the beatmap filename is used as a fallback in some scenarios where the MD5 of the beatmap may mismatch.
|
||||||
|
// this is considered to be an acceptable casualty so that things can continue to work as expected for users in some rare scenarios
|
||||||
|
// (stale beatmap files in beatmap packs, beatmap mirror desyncs).
|
||||||
|
// however, all this means that other places such as score submission ARE EXPECTED TO VERIFY THE MD5 OF THE BEATMAP AGAINST THE ONLINE ONE EXPLICITLY AGAIN.
|
||||||
|
//
|
||||||
|
// additionally note that the online ID stored to the map is EXPLICITLY NOT USED because some users in a silly attempt to "fix" things for themselves on stable
|
||||||
|
// would reuse online IDs of already submitted beatmaps, which means that information is pretty much expected to be bogus in a nonzero number of beatmapsets.
|
||||||
if (!tryLookup(beatmapInfo, preferOnlineFetch, out var res))
|
if (!tryLookup(beatmapInfo, preferOnlineFetch, out var res))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (res == null || shouldDiscardLookupResult(res, beatmapInfo))
|
if (res == null)
|
||||||
{
|
{
|
||||||
beatmapInfo.ResetOnlineInfo();
|
beatmapInfo.ResetOnlineInfo();
|
||||||
lookupResults.Add(null); // mark lookup failure
|
lookupResults.Add(null); // mark lookup failure
|
||||||
@ -83,23 +91,6 @@ namespace osu.Game.Beatmaps
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool shouldDiscardLookupResult(OnlineBeatmapMetadata result, BeatmapInfo beatmapInfo)
|
|
||||||
{
|
|
||||||
if (beatmapInfo.OnlineID > 0 && result.BeatmapID != beatmapInfo.OnlineID)
|
|
||||||
{
|
|
||||||
Logger.Log($"Discarding metadata lookup result due to mismatching online ID (expected: {beatmapInfo.OnlineID} actual: {result.BeatmapID})", LoggingTarget.Database);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (beatmapInfo.OnlineID == -1 && result.MD5Hash != beatmapInfo.MD5Hash)
|
|
||||||
{
|
|
||||||
Logger.Log($"Discarding metadata lookup result due to mismatching hash (expected: {beatmapInfo.MD5Hash} actual: {result.MD5Hash})", LoggingTarget.Database);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to retrieve the <see cref="OnlineBeatmapMetadata"/> for the given <paramref name="beatmapInfo"/>.
|
/// Attempts to retrieve the <see cref="OnlineBeatmapMetadata"/> for the given <paramref name="beatmapInfo"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -183,7 +183,17 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
if (scrollSpeedEncodedAsSliderVelocity)
|
if (scrollSpeedEncodedAsSliderVelocity)
|
||||||
{
|
{
|
||||||
foreach (var point in legacyControlPoints.EffectPoints)
|
foreach (var point in legacyControlPoints.EffectPoints)
|
||||||
legacyControlPoints.Add(point.Time, new DifficultyControlPoint { SliderVelocity = point.ScrollSpeed });
|
{
|
||||||
|
legacyControlPoints.Add(point.Time, new DifficultyControlPoint
|
||||||
|
{
|
||||||
|
SliderVelocityBindable =
|
||||||
|
{
|
||||||
|
MinValue = point.ScrollSpeedBindable.MinValue,
|
||||||
|
MaxValue = point.ScrollSpeedBindable.MaxValue,
|
||||||
|
Value = point.ScrollSpeedBindable.Value,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LegacyControlPointProperties lastControlPointProperties = new LegacyControlPointProperties();
|
LegacyControlPointProperties lastControlPointProperties = new LegacyControlPointProperties();
|
||||||
@ -539,7 +549,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
private string getSampleBank(IList<HitSampleInfo> samples, bool banksOnly = false)
|
private string getSampleBank(IList<HitSampleInfo> samples, bool banksOnly = false)
|
||||||
{
|
{
|
||||||
LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank);
|
LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank);
|
||||||
LegacySampleBank addBank = toLegacySampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name) && s.Name != HitSampleInfo.HIT_NORMAL)?.Bank);
|
LegacySampleBank addBank = toLegacySampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name) && s.Name != HitSampleInfo.HIT_NORMAL && !s.EditorAutoBank)?.Bank);
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
35
osu.Game/Beatmaps/IBeatmapUpdater.cs
Normal file
35
osu.Game/Beatmaps/IBeatmapUpdater.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Game.Database;
|
||||||
|
|
||||||
|
namespace osu.Game.Beatmaps
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles all processing required to ensure a local beatmap is in a consistent state with any changes.
|
||||||
|
/// </summary>
|
||||||
|
public interface IBeatmapUpdater : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Queue a beatmap for background processing.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="beatmapSet">The managed beatmap set to update. A transaction will be opened to apply changes.</param>
|
||||||
|
/// <param name="lookupScope">The preferred scope to use for metadata lookup.</param>
|
||||||
|
void Queue(Live<BeatmapSetInfo> beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Run all processing on a beatmap immediately.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="beatmapSet">The managed beatmap set to update. A transaction will be opened to apply changes.</param>
|
||||||
|
/// <param name="lookupScope">The preferred scope to use for metadata lookup.</param>
|
||||||
|
void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs a subset of processing focused on updating any cached beatmap object counts.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="beatmapInfo">The managed beatmap to update. A transaction will be opened to apply changes.</param>
|
||||||
|
/// <param name="lookupScope">The preferred scope to use for metadata lookup.</param>
|
||||||
|
void ProcessObjectCounts(BeatmapInfo beatmapInfo, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst);
|
||||||
|
}
|
||||||
|
}
|
@ -90,8 +90,7 @@ namespace osu.Game.Beatmaps
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(beatmapInfo.MD5Hash)
|
if (string.IsNullOrEmpty(beatmapInfo.MD5Hash)
|
||||||
&& string.IsNullOrEmpty(beatmapInfo.Path)
|
&& string.IsNullOrEmpty(beatmapInfo.Path))
|
||||||
&& beatmapInfo.OnlineID <= 0)
|
|
||||||
{
|
{
|
||||||
onlineMetadata = null;
|
onlineMetadata = null;
|
||||||
return false;
|
return false;
|
||||||
@ -240,10 +239,9 @@ namespace osu.Game.Beatmaps
|
|||||||
using var cmd = db.CreateCommand();
|
using var cmd = db.CreateCommand();
|
||||||
|
|
||||||
cmd.CommandText =
|
cmd.CommandText =
|
||||||
@"SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path";
|
@"SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR filename = @Path";
|
||||||
|
|
||||||
cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash));
|
cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash));
|
||||||
cmd.Parameters.Add(new SqliteParameter(@"@OnlineID", beatmapInfo.OnlineID));
|
|
||||||
cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path));
|
cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path));
|
||||||
|
|
||||||
using var reader = cmd.ExecuteReader();
|
using var reader = cmd.ExecuteReader();
|
||||||
@ -281,11 +279,10 @@ namespace osu.Game.Beatmaps
|
|||||||
SELECT `b`.`beatmapset_id`, `b`.`beatmap_id`, `b`.`approved`, `b`.`user_id`, `b`.`checksum`, `b`.`last_update`, `s`.`submit_date`, `s`.`approved_date`
|
SELECT `b`.`beatmapset_id`, `b`.`beatmap_id`, `b`.`approved`, `b`.`user_id`, `b`.`checksum`, `b`.`last_update`, `s`.`submit_date`, `s`.`approved_date`
|
||||||
FROM `osu_beatmaps` AS `b`
|
FROM `osu_beatmaps` AS `b`
|
||||||
JOIN `osu_beatmapsets` AS `s` ON `s`.`beatmapset_id` = `b`.`beatmapset_id`
|
JOIN `osu_beatmapsets` AS `s` ON `s`.`beatmapset_id` = `b`.`beatmapset_id`
|
||||||
WHERE `b`.`checksum` = @MD5Hash OR `b`.`beatmap_id` = @OnlineID OR `b`.`filename` = @Path
|
WHERE `b`.`checksum` = @MD5Hash OR `b`.`filename` = @Path
|
||||||
""";
|
""";
|
||||||
|
|
||||||
cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash));
|
cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash));
|
||||||
cmd.Parameters.Add(new SqliteParameter(@"@OnlineID", beatmapInfo.OnlineID));
|
|
||||||
cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path));
|
cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path));
|
||||||
|
|
||||||
using var reader = cmd.ExecuteReader();
|
using var reader = cmd.ExecuteReader();
|
||||||
|
@ -17,6 +17,7 @@ using osu.Game.Localisation;
|
|||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Mods.Input;
|
using osu.Game.Overlays.Mods.Input;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osu.Game.Screens.OnlinePlay.Lounge.Components;
|
using osu.Game.Screens.OnlinePlay.Lounge.Components;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
using osu.Game.Screens.Select.Filter;
|
using osu.Game.Screens.Select.Filter;
|
||||||
@ -193,6 +194,9 @@ namespace osu.Game.Configuration
|
|||||||
SetDefault(OsuSetting.EditorAutoSeekOnPlacement, true);
|
SetDefault(OsuSetting.EditorAutoSeekOnPlacement, true);
|
||||||
SetDefault(OsuSetting.EditorLimitedDistanceSnap, false);
|
SetDefault(OsuSetting.EditorLimitedDistanceSnap, false);
|
||||||
SetDefault(OsuSetting.EditorShowSpeedChanges, false);
|
SetDefault(OsuSetting.EditorShowSpeedChanges, false);
|
||||||
|
SetDefault(OsuSetting.EditorScaleOrigin, EditorOrigin.GridCentre);
|
||||||
|
SetDefault(OsuSetting.EditorRotationOrigin, EditorOrigin.GridCentre);
|
||||||
|
SetDefault(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges, true);
|
||||||
|
|
||||||
SetDefault(OsuSetting.HideCountryFlags, false);
|
SetDefault(OsuSetting.HideCountryFlags, false);
|
||||||
|
|
||||||
@ -204,8 +208,11 @@ namespace osu.Game.Configuration
|
|||||||
SetDefault<UserStatus?>(OsuSetting.UserOnlineStatus, null);
|
SetDefault<UserStatus?>(OsuSetting.UserOnlineStatus, null);
|
||||||
|
|
||||||
SetDefault(OsuSetting.EditorTimelineShowTimingChanges, true);
|
SetDefault(OsuSetting.EditorTimelineShowTimingChanges, true);
|
||||||
|
SetDefault(OsuSetting.EditorTimelineShowBreaks, true);
|
||||||
SetDefault(OsuSetting.EditorTimelineShowTicks, true);
|
SetDefault(OsuSetting.EditorTimelineShowTicks, true);
|
||||||
|
|
||||||
|
SetDefault(OsuSetting.EditorContractSidebars, false);
|
||||||
|
|
||||||
SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false);
|
SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -431,6 +438,11 @@ namespace osu.Game.Configuration
|
|||||||
HideCountryFlags,
|
HideCountryFlags,
|
||||||
EditorTimelineShowTimingChanges,
|
EditorTimelineShowTimingChanges,
|
||||||
EditorTimelineShowTicks,
|
EditorTimelineShowTicks,
|
||||||
AlwaysShowHoldForMenuButton
|
AlwaysShowHoldForMenuButton,
|
||||||
|
EditorContractSidebars,
|
||||||
|
EditorScaleOrigin,
|
||||||
|
EditorRotationOrigin,
|
||||||
|
EditorTimelineShowBreaks,
|
||||||
|
EditorAdjustExistingObjectsOnTimingChanges,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ namespace osu.Game.Database
|
|||||||
private RealmAccess realmAccess { get; set; } = null!;
|
private RealmAccess realmAccess { get; set; } = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private BeatmapUpdater beatmapUpdater { get; set; } = null!;
|
private IBeatmapUpdater beatmapUpdater { get; set; } = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IBindable<WorkingBeatmap> gameBeatmap { get; set; } = null!;
|
private IBindable<WorkingBeatmap> gameBeatmap { get; set; } = null!;
|
||||||
|
@ -93,8 +93,9 @@ namespace osu.Game.Database
|
|||||||
/// 40 2023-12-21 Add ScoreInfo.Version to keep track of which build scores were set on.
|
/// 40 2023-12-21 Add ScoreInfo.Version to keep track of which build scores were set on.
|
||||||
/// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances.
|
/// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances.
|
||||||
/// 42 2024-08-07 Update mania key bindings to reflect changes to ManiaAction
|
/// 42 2024-08-07 Update mania key bindings to reflect changes to ManiaAction
|
||||||
|
/// 43 2024-10-14 Reset keybind for toggling FPS display to avoid conflict with "convert to stream" in the editor, if not already changed by user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int schema_version = 42;
|
private const int schema_version = 43;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||||
@ -375,10 +376,6 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
foreach (var beatmap in beatmapSet.Beatmaps)
|
foreach (var beatmap in beatmapSet.Beatmaps)
|
||||||
{
|
{
|
||||||
// Cascade delete related scores, else they will have a null beatmap against the model's spec.
|
|
||||||
foreach (var score in beatmap.Scores)
|
|
||||||
realm.Remove(score);
|
|
||||||
|
|
||||||
realm.Remove(beatmap.Metadata);
|
realm.Remove(beatmap.Metadata);
|
||||||
realm.Remove(beatmap);
|
realm.Remove(beatmap);
|
||||||
}
|
}
|
||||||
@ -1192,6 +1189,21 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 43:
|
||||||
|
{
|
||||||
|
// Clear default bindings for "Toggle FPS Display",
|
||||||
|
// as it conflicts with "Convert to Stream" in the editor.
|
||||||
|
// Only apply change if set to the conflicting bind
|
||||||
|
// i.e. has been manually rebound by the user.
|
||||||
|
var keyBindings = migration.NewRealm.All<RealmKeyBinding>();
|
||||||
|
|
||||||
|
var toggleFpsBind = keyBindings.FirstOrDefault(bind => bind.ActionInt == (int)GlobalAction.ToggleFPSDisplay);
|
||||||
|
if (toggleFpsBind != null && toggleFpsBind.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Shift, InputKey.Control, InputKey.F }))
|
||||||
|
migration.NewRealm.Remove(toggleFpsBind);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
|
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
|
||||||
|
@ -528,7 +528,7 @@ namespace osu.Game.Database
|
|||||||
/// <param name="model">The new model proposed for import.</param>
|
/// <param name="model">The new model proposed for import.</param>
|
||||||
/// <param name="realm">The current realm context.</param>
|
/// <param name="realm">The current realm context.</param>
|
||||||
/// <returns>An existing model which matches the criteria to skip importing, else null.</returns>
|
/// <returns>An existing model which matches the criteria to skip importing, else null.</returns>
|
||||||
protected TModel? CheckForExisting(TModel model, Realm realm) => string.IsNullOrEmpty(model.Hash) ? null : realm.All<TModel>().FirstOrDefault(b => b.Hash == model.Hash);
|
protected TModel? CheckForExisting(TModel model, Realm realm) => string.IsNullOrEmpty(model.Hash) ? null : realm.All<TModel>().OrderBy(b => b.DeletePending).FirstOrDefault(b => b.Hash == model.Hash);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether import can be skipped after finding an existing import early in the process.
|
/// Whether import can be skipped after finding an existing import early in the process.
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user