mirror of
https://github.com/ppy/osu.git
synced 2024-12-05 09:42:54 +08:00
Merge branch 'master' into multiplayer-remove-cmc-and-composite
This commit is contained in:
commit
6fba6de4dc
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 }}"
|
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -88,7 +88,7 @@ jobs:
|
||||
# Attempt to upload results even if test fails.
|
||||
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
|
||||
- name: Upload Test Results
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
|
||||
@ -114,7 +114,7 @@ jobs:
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
- name: Install .NET workloads
|
||||
run: dotnet workload install maui-android
|
||||
run: dotnet workload install android
|
||||
|
||||
- name: Compile
|
||||
run: dotnet build -c Debug osu.Android.slnf
|
||||
|
274
.github/workflows/diffcalc.yml
vendored
274
.github/workflows/diffcalc.yml
vendored
@ -103,26 +103,11 @@ permissions:
|
||||
env:
|
||||
EXECUTION_ID: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -euo pipefail {0}
|
||||
|
||||
jobs:
|
||||
master-environment:
|
||||
name: Save master environment
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
HEAD: ${{ steps.get-head.outputs.HEAD }}
|
||||
steps:
|
||||
- name: Checkout osu
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: master
|
||||
sparse-checkout: |
|
||||
README.md
|
||||
|
||||
- name: Get HEAD ref
|
||||
id: get-head
|
||||
run: |
|
||||
ref=$(git log -1 --format='%H')
|
||||
echo "HEAD=https://github.com/${{ github.repository }}/commit/${ref}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
check-permissions:
|
||||
name: Check permissions
|
||||
runs-on: ubuntu-latest
|
||||
@ -138,9 +123,23 @@ jobs:
|
||||
done
|
||||
exit 1
|
||||
|
||||
run-diffcalc:
|
||||
name: Run spreadsheet generator
|
||||
needs: check-permissions
|
||||
uses: ./.github/workflows/_diffcalc_processor.yml
|
||||
with:
|
||||
# Can't reference env... Why GitHub, WHY?
|
||||
id: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
head-sha: https://github.com/${{ github.repository }}/commit/${{ github.event.pull_request.head.sha || github.sha }}
|
||||
pr-url: ${{ github.event.issue.pull_request.html_url || '' }}
|
||||
pr-text: ${{ github.event.comment.body || '' }}
|
||||
dispatch-inputs: ${{ (github.event.type == 'workflow_dispatch' && toJSON(inputs)) || '' }}
|
||||
secrets:
|
||||
DIFFCALC_GOOGLE_CREDENTIALS: ${{ secrets.DIFFCALC_GOOGLE_CREDENTIALS }}
|
||||
|
||||
create-comment:
|
||||
name: Create PR comment
|
||||
needs: [ master-environment, check-permissions ]
|
||||
needs: check-permissions
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
||||
steps:
|
||||
@ -153,249 +152,34 @@ jobs:
|
||||
|
||||
*This comment will update on completion*
|
||||
|
||||
directory:
|
||||
name: Prepare directory
|
||||
needs: check-permissions
|
||||
runs-on: self-hosted
|
||||
outputs:
|
||||
GENERATOR_DIR: ${{ steps.set-outputs.outputs.GENERATOR_DIR }}
|
||||
GENERATOR_ENV: ${{ steps.set-outputs.outputs.GENERATOR_ENV }}
|
||||
GOOGLE_CREDS_FILE: ${{ steps.set-outputs.outputs.GOOGLE_CREDS_FILE }}
|
||||
steps:
|
||||
- name: Checkout diffcalc-sheet-generator
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: ${{ env.EXECUTION_ID }}
|
||||
repository: 'smoogipoo/diffcalc-sheet-generator'
|
||||
|
||||
- name: Set outputs
|
||||
id: set-outputs
|
||||
run: |
|
||||
echo "GENERATOR_DIR=${{ github.workspace }}/${{ env.EXECUTION_ID }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "GENERATOR_ENV=${{ github.workspace }}/${{ env.EXECUTION_ID }}/.env" >> "${GITHUB_OUTPUT}"
|
||||
echo "GOOGLE_CREDS_FILE=${{ github.workspace }}/${{ env.EXECUTION_ID }}/google-credentials.json" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
environment:
|
||||
name: Setup environment
|
||||
needs: [ master-environment, directory ]
|
||||
runs-on: self-hosted
|
||||
env:
|
||||
VARS_JSON: ${{ toJSON(vars) }}
|
||||
steps:
|
||||
- name: Add base environment
|
||||
run: |
|
||||
# Required by diffcalc-sheet-generator
|
||||
cp '${{ needs.directory.outputs.GENERATOR_DIR }}/.env.sample' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
|
||||
# Add Google credentials
|
||||
echo '${{ secrets.DIFFCALC_GOOGLE_CREDENTIALS }}' | base64 -d > "${{ needs.directory.outputs.GOOGLE_CREDS_FILE }}"
|
||||
|
||||
# Add repository variables
|
||||
echo "${VARS_JSON}" | jq -c '. | to_entries | .[]' | while read -r line; do
|
||||
opt=$(jq -r '.key' <<< ${line})
|
||||
val=$(jq -r '.value' <<< ${line})
|
||||
|
||||
if [[ "${opt}" =~ ^DIFFCALC_ ]]; then
|
||||
optNoPrefix=$(echo "${opt}" | cut -d '_' -f2-)
|
||||
sed -i "s;^${optNoPrefix}=.*$;${optNoPrefix}=${val};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Add master environment
|
||||
run: |
|
||||
sed -i "s;^OSU_A=.*$;OSU_A=${{ needs.master-environment.outputs.HEAD }};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
|
||||
- name: Add pull-request environment
|
||||
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
||||
run: |
|
||||
sed -i "s;^OSU_B=.*$;OSU_B=${{ github.event.issue.pull_request.html_url }};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
|
||||
- name: Add comment environment
|
||||
if: ${{ github.event_name == 'issue_comment' }}
|
||||
env:
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
run: |
|
||||
# Add comment environment
|
||||
echo "$COMMENT_BODY" | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do
|
||||
opt=$(echo "${line}" | cut -d '=' -f1)
|
||||
sed -i "s;^${opt}=.*$;${line};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
done
|
||||
|
||||
- name: Add dispatch environment
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
run: |
|
||||
sed -i 's;^OSU_B=.*$;OSU_B=${{ inputs.osu-b }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
sed -i 's/^RULESET=.*$/RULESET=${{ inputs.ruleset }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
sed -i 's/^GENERATORS=.*$/GENERATORS=${{ inputs.generators }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
|
||||
if [[ '${{ inputs.osu-a }}' != 'latest' ]]; then
|
||||
sed -i 's;^OSU_A=.*$;OSU_A=${{ inputs.osu-a }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
fi
|
||||
|
||||
if [[ '${{ inputs.difficulty-calculator-a }}' != 'latest' ]]; then
|
||||
sed -i 's;^DIFFICULTY_CALCULATOR_A=.*$;DIFFICULTY_CALCULATOR_A=${{ inputs.difficulty-calculator-a }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
fi
|
||||
|
||||
if [[ '${{ inputs.difficulty-calculator-b }}' != 'latest' ]]; then
|
||||
sed -i 's;^DIFFICULTY_CALCULATOR_B=.*$;DIFFICULTY_CALCULATOR_B=${{ inputs.difficulty-calculator-b }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
fi
|
||||
|
||||
if [[ '${{ inputs.score-processor-a }}' != 'latest' ]]; then
|
||||
sed -i 's;^SCORE_PROCESSOR_A=.*$;SCORE_PROCESSOR_A=${{ inputs.score-processor-a }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
fi
|
||||
|
||||
if [[ '${{ inputs.score-processor-b }}' != 'latest' ]]; then
|
||||
sed -i 's;^SCORE_PROCESSOR_B=.*$;SCORE_PROCESSOR_B=${{ inputs.score-processor-b }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
fi
|
||||
|
||||
if [[ '${{ inputs.converts }}' == 'true' ]]; then
|
||||
sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=0/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
else
|
||||
sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=1/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
fi
|
||||
|
||||
if [[ '${{ inputs.ranked-only }}' == 'true' ]]; then
|
||||
sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=1/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
else
|
||||
sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=0/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
fi
|
||||
|
||||
scores:
|
||||
name: Setup scores
|
||||
needs: [ directory, environment ]
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Query latest data
|
||||
id: query
|
||||
run: |
|
||||
ruleset=$(cat ${{ needs.directory.outputs.GENERATOR_ENV }} | grep -E '^RULESET=' | cut -d '=' -f2-)
|
||||
performance_data_name=$(curl -s "https://data.ppy.sh/" | grep "performance_${ruleset}_top_1000\b" | tail -1 | awk -F "'" '{print $2}' | sed 's/\.tar\.bz2//g')
|
||||
|
||||
echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/sql/${ruleset}" >> "${GITHUB_OUTPUT}"
|
||||
echo "DATA_NAME=${performance_data_name}" >> "${GITHUB_OUTPUT}"
|
||||
echo "DATA_PKG=${performance_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Restore cache
|
||||
id: restore-cache
|
||||
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
||||
with:
|
||||
path: ${{ steps.query.outputs.DATA_PKG }}
|
||||
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||
|
||||
- name: Download
|
||||
if: steps.restore-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
wget -q -O "${{ steps.query.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query.outputs.DATA_PKG }}"
|
||||
|
||||
- name: Extract
|
||||
run: |
|
||||
tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_PKG }}"
|
||||
rm -r "${{ steps.query.outputs.TARGET_DIR }}"
|
||||
mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}"
|
||||
|
||||
beatmaps:
|
||||
name: Setup beatmaps
|
||||
needs: directory
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Query latest data
|
||||
id: query
|
||||
run: |
|
||||
beatmaps_data_name=$(curl -s "https://data.ppy.sh/" | grep "osu_files" | tail -1 | awk -F "'" '{print $2}' | sed 's/\.tar\.bz2//g')
|
||||
|
||||
echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/beatmaps" >> "${GITHUB_OUTPUT}"
|
||||
echo "DATA_NAME=${beatmaps_data_name}" >> "${GITHUB_OUTPUT}"
|
||||
echo "DATA_PKG=${beatmaps_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Restore cache
|
||||
id: restore-cache
|
||||
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
||||
with:
|
||||
path: ${{ steps.query.outputs.DATA_PKG }}
|
||||
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||
|
||||
- name: Download
|
||||
if: steps.restore-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
wget -q -O "${{ steps.query.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query.outputs.DATA_PKG }}"
|
||||
|
||||
- name: Extract
|
||||
run: |
|
||||
tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_PKG }}"
|
||||
rm -r "${{ steps.query.outputs.TARGET_DIR }}"
|
||||
mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}"
|
||||
|
||||
generator:
|
||||
name: Run generator
|
||||
needs: [ directory, environment, scores, beatmaps ]
|
||||
runs-on: self-hosted
|
||||
timeout-minutes: 720
|
||||
outputs:
|
||||
TARGET: ${{ steps.run.outputs.TARGET }}
|
||||
SPREADSHEET_LINK: ${{ steps.run.outputs.SPREADSHEET_LINK }}
|
||||
steps:
|
||||
- name: Run
|
||||
id: run
|
||||
run: |
|
||||
# Add the GitHub token. This needs to be done here because it's unique per-job.
|
||||
sed -i 's/^GH_TOKEN=.*$/GH_TOKEN=${{ github.token }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
|
||||
cd "${{ needs.directory.outputs.GENERATOR_DIR }}"
|
||||
|
||||
docker compose up --build --detach
|
||||
docker compose logs --follow &
|
||||
docker compose wait generator
|
||||
|
||||
link=$(docker compose logs --tail 10 generator | grep 'http' | sed -E 's/^.*(http.*)$/\1/')
|
||||
target=$(cat "${{ needs.directory.outputs.GENERATOR_ENV }}" | grep -E '^OSU_B=' | cut -d '=' -f2-)
|
||||
|
||||
echo "TARGET=${target}" >> "${GITHUB_OUTPUT}"
|
||||
echo "SPREADSHEET_LINK=${link}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Shutdown
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
cd "${{ needs.directory.outputs.GENERATOR_DIR }}"
|
||||
docker compose down --volumes
|
||||
|
||||
output-cli:
|
||||
name: Output info
|
||||
needs: generator
|
||||
name: Info
|
||||
needs: run-diffcalc
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Output info
|
||||
run: |
|
||||
echo "Target: ${{ needs.generator.outputs.TARGET }}"
|
||||
echo "Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }}"
|
||||
|
||||
cleanup:
|
||||
name: Cleanup
|
||||
needs: [ directory, generator ]
|
||||
if: ${{ always() && needs.directory.result == 'success' }}
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Cleanup
|
||||
run: |
|
||||
rm -rf "${{ needs.directory.outputs.GENERATOR_DIR }}"
|
||||
echo "Target: ${{ needs.run-diffcalc.outputs.target }}"
|
||||
echo "Spreadsheet: ${{ needs.run-diffcalc.outputs.sheet }}"
|
||||
|
||||
update-comment:
|
||||
name: Update PR comment
|
||||
needs: [ create-comment, generator ]
|
||||
needs: [ create-comment, run-diffcalc ]
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ always() && needs.create-comment.result == 'success' }}
|
||||
steps:
|
||||
- name: Update comment on success
|
||||
if: ${{ needs.generator.result == 'success' }}
|
||||
if: ${{ needs.run-diffcalc.result == 'success' }}
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
mode: recreate
|
||||
message: |
|
||||
Target: ${{ needs.generator.outputs.TARGET }}
|
||||
Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }}
|
||||
Target: ${{ needs.run-diffcalc.outputs.target }}
|
||||
Spreadsheet: ${{ needs.run-diffcalc.outputs.sheet }}
|
||||
|
||||
- name: Update comment on failure
|
||||
if: ${{ needs.generator.result == 'failure' }}
|
||||
if: ${{ needs.run-diffcalc.result == 'failure' }}
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
@ -404,7 +188,7 @@ jobs:
|
||||
Difficulty calculation failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: Update comment on cancellation
|
||||
if: ${{ needs.generator.result == 'cancelled' }}
|
||||
if: ${{ needs.run-diffcalc.result == 'cancelled' }}
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
|
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
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Continuous Integration"]
|
||||
workflows: [ "Continuous Integration" ]
|
||||
types:
|
||||
- completed
|
||||
permissions: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
annotate:
|
||||
permissions:
|
||||
checks: write # to create checks (dorny/test-reporter)
|
||||
|
||||
name: Annotate CI run with test results
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- { prettyname: Windows }
|
||||
- { prettyname: macOS }
|
||||
- { prettyname: Linux }
|
||||
threadingMode: ['SingleThread', 'MultiThreaded']
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.repository.full_name }}
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
|
||||
- name: Download results
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: osu-test-results-*
|
||||
merge-multiple: true
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Annotate CI run with test results
|
||||
uses: dorny/test-reporter@v1.8.0
|
||||
with:
|
||||
artifact: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
|
||||
name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
|
||||
name: Results
|
||||
path: "*.trx"
|
||||
reporter: dotnet-trx
|
||||
list-suites: 'failed'
|
||||
|
@ -141,12 +141,12 @@ namespace osu.Desktop
|
||||
|
||||
// Make sure that this is a laptop.
|
||||
IntPtr[] gpus = new IntPtr[64];
|
||||
if (checkError(EnumPhysicalGPUs(gpus, out int gpuCount)))
|
||||
if (checkError(EnumPhysicalGPUs(gpus, out int gpuCount), nameof(EnumPhysicalGPUs)))
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < gpuCount; i++)
|
||||
{
|
||||
if (checkError(GetSystemType(gpus[i], out var type)))
|
||||
if (checkError(GetSystemType(gpus[i], out var type), nameof(GetSystemType)))
|
||||
return false;
|
||||
|
||||
if (type == NvSystemType.LAPTOP)
|
||||
@ -182,7 +182,7 @@ namespace osu.Desktop
|
||||
|
||||
bool success = setSetting(NvSettingID.OGL_THREAD_CONTROL_ID, (uint)value);
|
||||
|
||||
Logger.Log(success ? $"Threaded optimizations set to \"{value}\"!" : "Threaded optimizations set failed!");
|
||||
Logger.Log(success ? $"[NVAPI] Threaded optimizations set to \"{value}\"!" : "[NVAPI] Threaded optimizations set failed!");
|
||||
}
|
||||
}
|
||||
|
||||
@ -205,7 +205,7 @@ namespace osu.Desktop
|
||||
|
||||
uint numApps = profile.NumOfApps;
|
||||
|
||||
if (checkError(EnumApplications(sessionHandle, profileHandle, 0, ref numApps, applications)))
|
||||
if (checkError(EnumApplications(sessionHandle, profileHandle, 0, ref numApps, applications), nameof(EnumApplications)))
|
||||
return false;
|
||||
|
||||
for (uint i = 0; i < numApps; i++)
|
||||
@ -236,10 +236,10 @@ namespace osu.Desktop
|
||||
|
||||
isApplicationSpecific = true;
|
||||
|
||||
if (checkError(FindApplicationByName(sessionHandle, osu_filename, out profileHandle, ref application)))
|
||||
if (checkError(FindApplicationByName(sessionHandle, osu_filename, out profileHandle, ref application), nameof(FindApplicationByName)))
|
||||
{
|
||||
isApplicationSpecific = false;
|
||||
if (checkError(GetCurrentGlobalProfile(sessionHandle, out profileHandle)))
|
||||
if (checkError(GetCurrentGlobalProfile(sessionHandle, out profileHandle), nameof(GetCurrentGlobalProfile)))
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -258,12 +258,10 @@ namespace osu.Desktop
|
||||
Version = NvProfile.Stride,
|
||||
IsPredefined = 0,
|
||||
ProfileName = PROFILE_NAME,
|
||||
GPUSupport = new uint[32]
|
||||
GpuSupport = NvDrsGpuSupport.Geforce
|
||||
};
|
||||
|
||||
newProfile.GPUSupport[0] = 1;
|
||||
|
||||
if (checkError(CreateProfile(sessionHandle, ref newProfile, out profileHandle)))
|
||||
if (checkError(CreateProfile(sessionHandle, ref newProfile, out profileHandle), nameof(CreateProfile)))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
@ -284,7 +282,7 @@ namespace osu.Desktop
|
||||
SettingID = settingId
|
||||
};
|
||||
|
||||
if (checkError(GetSetting(sessionHandle, profileHandle, settingId, ref setting)))
|
||||
if (checkError(GetSetting(sessionHandle, profileHandle, settingId, ref setting), nameof(GetSetting)))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
@ -313,7 +311,7 @@ namespace osu.Desktop
|
||||
};
|
||||
|
||||
// Set the thread state
|
||||
if (checkError(SetSetting(sessionHandle, profileHandle, ref newSetting)))
|
||||
if (checkError(SetSetting(sessionHandle, profileHandle, ref newSetting), nameof(SetSetting)))
|
||||
return false;
|
||||
|
||||
// Get the profile (needed to check app count)
|
||||
@ -321,7 +319,7 @@ namespace osu.Desktop
|
||||
{
|
||||
Version = NvProfile.Stride
|
||||
};
|
||||
if (checkError(GetProfileInfo(sessionHandle, profileHandle, ref profile)))
|
||||
if (checkError(GetProfileInfo(sessionHandle, profileHandle, ref profile), nameof(GetProfileInfo)))
|
||||
return false;
|
||||
|
||||
if (!containsApplication(profileHandle, profile, out application))
|
||||
@ -332,12 +330,12 @@ namespace osu.Desktop
|
||||
application.AppName = osu_filename;
|
||||
application.UserFriendlyName = APPLICATION_NAME;
|
||||
|
||||
if (checkError(CreateApplication(sessionHandle, profileHandle, ref application)))
|
||||
if (checkError(CreateApplication(sessionHandle, profileHandle, ref application), nameof(CreateApplication)))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save!
|
||||
return !checkError(SaveSettings(sessionHandle));
|
||||
return !checkError(SaveSettings(sessionHandle), nameof(SaveSettings));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -346,20 +344,25 @@ namespace osu.Desktop
|
||||
/// <returns>If the operation succeeded.</returns>
|
||||
private static bool createSession()
|
||||
{
|
||||
if (checkError(CreateSession(out sessionHandle)))
|
||||
if (checkError(CreateSession(out sessionHandle), nameof(CreateSession)))
|
||||
return false;
|
||||
|
||||
// Load settings into session
|
||||
if (checkError(LoadSettings(sessionHandle)))
|
||||
if (checkError(LoadSettings(sessionHandle), nameof(LoadSettings)))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool checkError(NvStatus status)
|
||||
private static bool checkError(NvStatus status, string caller)
|
||||
{
|
||||
Status = status;
|
||||
return status != NvStatus.OK;
|
||||
|
||||
bool hasError = status != NvStatus.OK;
|
||||
if (hasError)
|
||||
Logger.Log($"[NVAPI] {caller} call failed with status code {status}");
|
||||
|
||||
return hasError;
|
||||
}
|
||||
|
||||
static NVAPI()
|
||||
@ -458,9 +461,7 @@ namespace osu.Desktop
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
|
||||
public string ProfileName;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
|
||||
public uint[] GPUSupport;
|
||||
|
||||
public NvDrsGpuSupport GpuSupport;
|
||||
public uint IsPredefined;
|
||||
public uint NumOfApps;
|
||||
public uint NumOfSettings;
|
||||
@ -606,6 +607,7 @@ namespace osu.Desktop
|
||||
SYNC_NOT_ACTIVE = -194, // The requested action cannot be performed without Sync being enabled.
|
||||
SYNC_MASTER_NOT_FOUND = -195, // The requested action cannot be performed without Sync Master being enabled.
|
||||
INVALID_SYNC_TOPOLOGY = -196, // Invalid displays passed in the NV_GSYNC_DISPLAY pointer.
|
||||
|
||||
ECID_SIGN_ALGO_UNSUPPORTED = -197, // The specified signing algorithm is not supported. Either an incorrect value was entered or the current installed driver/hardware does not support the input value.
|
||||
ECID_KEY_VERIFICATION_FAILED = -198, // The encrypted public key verification has failed.
|
||||
FIRMWARE_OUT_OF_DATE = -199, // The device's firmware is out of date.
|
||||
@ -744,4 +746,12 @@ namespace osu.Desktop
|
||||
OGL_THREAD_CONTROL_NUM_VALUES = 2,
|
||||
OGL_THREAD_CONTROL_DEFAULT = 0
|
||||
}
|
||||
|
||||
[Flags]
|
||||
internal enum NvDrsGpuSupport : uint
|
||||
{
|
||||
Geforce = 1 << 0,
|
||||
Quadro = 1 << 1,
|
||||
Nvs = 1 << 2
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="System.IO.Packaging" Version="8.0.1" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
<PackageReference Include="Velopack" Version="0.0.630-g9c52e40" />
|
||||
<PackageReference Include="Velopack" Version="0.0.915" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
<EmbeddedResource Include="lazer.ico" />
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
@ -106,7 +107,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
AddStep("start drag", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<Column>().First());
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<NoteSelectionBlueprint>().Single(blueprint => blueprint.IsSelected && blueprint.HitObject.StartTime == 0));
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
});
|
||||
AddStep("end drag", () =>
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
@ -73,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
// 0.0 +--------+-+---------------> Release Difference / ms
|
||||
// release_threshold
|
||||
if (isOverlapping)
|
||||
holdAddition = 1 / (1 + Math.Exp(0.27 * (release_threshold - closestEndTime)));
|
||||
holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold);
|
||||
|
||||
// Decay and increase individualStrains in own column
|
||||
individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base);
|
||||
|
@ -3,21 +3,39 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components
|
||||
{
|
||||
public partial class EditBodyPiece : DefaultBodyPiece
|
||||
public partial class EditBodyPiece : CompositeDrawable
|
||||
{
|
||||
private readonly Container border;
|
||||
|
||||
public EditBodyPiece()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
border = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderThickness = 3,
|
||||
Child = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
AccentColour.Value = colours.Yellow;
|
||||
|
||||
Background.Alpha = 0.5f;
|
||||
border.BorderColour = colours.YellowDarker;
|
||||
}
|
||||
|
||||
protected override Drawable CreateForeground() => base.CreateForeground().With(d => d.Alpha = 0);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
@ -26,10 +27,11 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components
|
||||
{
|
||||
Height = DefaultNotePiece.NOTE_HEIGHT;
|
||||
|
||||
CornerRadius = 5;
|
||||
Masking = true;
|
||||
|
||||
InternalChild = new DefaultNotePiece();
|
||||
InternalChild = new EditNotePiece
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 1,
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -60,19 +62,23 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components
|
||||
{
|
||||
base.OnDrag(e);
|
||||
Dragging?.Invoke(e.ScreenSpaceMousePosition);
|
||||
updateState();
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
{
|
||||
base.OnDragEnd(e);
|
||||
DragEnded?.Invoke();
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
InternalChild.Colour = Colour4.White;
|
||||
|
||||
var colour = colours.Yellow;
|
||||
|
||||
if (IsHovered)
|
||||
if (IsHovered || IsDragged)
|
||||
colour = colour.Lighten(1);
|
||||
|
||||
Colour = colour;
|
||||
|
@ -2,28 +2,63 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components
|
||||
{
|
||||
public partial class EditNotePiece : CompositeDrawable
|
||||
{
|
||||
private readonly Container border;
|
||||
private readonly Box box;
|
||||
|
||||
[Resolved]
|
||||
private Column? column { get; set; }
|
||||
|
||||
public EditNotePiece()
|
||||
{
|
||||
Height = DefaultNotePiece.NOTE_HEIGHT;
|
||||
|
||||
CornerRadius = 5;
|
||||
Masking = true;
|
||||
|
||||
InternalChild = new DefaultNotePiece();
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
border = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderThickness = 3,
|
||||
Child = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
},
|
||||
},
|
||||
box = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 3,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Colour = colours.Yellow;
|
||||
border.BorderColour = colours.YellowDark;
|
||||
box.Colour = colours.YellowLight;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (column != null)
|
||||
Scale = new Vector2(1, column.ScrollingInfo.Direction.Value == ScrollingDirection.Down ? 1 : -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,10 @@
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
@ -17,9 +19,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
public partial class HoldNotePlacementBlueprint : ManiaPlacementBlueprint<HoldNote>
|
||||
{
|
||||
private readonly EditBodyPiece bodyPiece;
|
||||
private readonly EditNotePiece headPiece;
|
||||
private readonly EditNotePiece tailPiece;
|
||||
private EditBodyPiece bodyPiece = null!;
|
||||
private Circle headPiece = null!;
|
||||
private Circle tailPiece = null!;
|
||||
|
||||
[Resolved]
|
||||
private IScrollingInfo scrollingInfo { get; set; } = null!;
|
||||
@ -28,14 +30,29 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
|
||||
public HoldNotePlacementBlueprint()
|
||||
: base(new HoldNote())
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
bodyPiece = new EditBodyPiece { Origin = Anchor.TopCentre },
|
||||
headPiece = new EditNotePiece { Origin = Anchor.Centre },
|
||||
tailPiece = new EditNotePiece { Origin = Anchor.Centre }
|
||||
headPiece = new Circle
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Colour = colours.Yellow,
|
||||
Height = 10
|
||||
},
|
||||
tailPiece = new Circle
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Colour = colours.Yellow,
|
||||
Height = 10
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -2,14 +2,14 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK;
|
||||
|
||||
@ -17,9 +17,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
public partial class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint<HoldNote>
|
||||
{
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
@ -29,9 +26,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
[Resolved]
|
||||
private IPositionSnapProvider? positionSnapProvider { get; set; }
|
||||
|
||||
private EditBodyPiece body = null!;
|
||||
private EditHoldNoteEndPiece head = null!;
|
||||
private EditHoldNoteEndPiece tail = null!;
|
||||
|
||||
protected new DrawableHoldNote DrawableObject => (DrawableHoldNote)base.DrawableObject;
|
||||
|
||||
public HoldNoteSelectionBlueprint(HoldNote hold)
|
||||
: base(hold)
|
||||
{
|
||||
@ -42,9 +42,17 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
body = new EditBodyPiece
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
},
|
||||
head = new EditHoldNoteEndPiece
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
DragStarted = () => changeHandler?.BeginChange(),
|
||||
Dragging = pos =>
|
||||
{
|
||||
@ -64,6 +72,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
tail = new EditHoldNoteEndPiece
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
DragStarted = () => changeHandler?.BeginChange(),
|
||||
Dragging = pos =>
|
||||
{
|
||||
@ -79,19 +89,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
},
|
||||
DragEnded = () => changeHandler?.EndChange(),
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderThickness = 1,
|
||||
BorderColour = colours.Yellow,
|
||||
Child = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -99,11 +96,23 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
base.Update();
|
||||
|
||||
head.Height = DrawableObject.Head.DrawHeight;
|
||||
head.Y = HitObjectContainer.PositionAtTime(HitObject.Head.StartTime, HitObject.StartTime);
|
||||
tail.Height = DrawableObject.Tail.DrawHeight;
|
||||
tail.Y = HitObjectContainer.PositionAtTime(HitObject.Tail.StartTime, HitObject.StartTime);
|
||||
Height = HitObjectContainer.LengthAtTime(HitObject.StartTime, HitObject.EndTime) + tail.DrawHeight;
|
||||
}
|
||||
|
||||
protected override void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||
{
|
||||
Origin = direction.NewValue == ScrollingDirection.Down ? Anchor.BottomCentre : Anchor.TopCentre;
|
||||
|
||||
foreach (var child in InternalChildren)
|
||||
child.Anchor = Origin;
|
||||
|
||||
head.Scale = tail.Scale = body.Scale = new Vector2(1, direction.NewValue == ScrollingDirection.Down ? 1 : -1);
|
||||
}
|
||||
|
||||
public override Quad SelectionQuad => ScreenSpaceDrawQuad;
|
||||
|
||||
public override Vector2 ScreenSpaceSelectionPoint => head.ScreenSpaceDrawQuad.Centre;
|
||||
|
@ -37,16 +37,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
directionBindable.BindValueChanged(onDirectionChanged, true);
|
||||
directionBindable.BindValueChanged(OnDirectionChanged, true);
|
||||
}
|
||||
|
||||
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||
{
|
||||
var anchor = direction.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
Anchor = Origin = anchor;
|
||||
foreach (var child in InternalChildren)
|
||||
child.Anchor = child.Origin = anchor;
|
||||
}
|
||||
protected abstract void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> direction);
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
|
@ -1,10 +1,12 @@
|
||||
// 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.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -12,14 +14,25 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
public partial class NotePlacementBlueprint : ManiaPlacementBlueprint<Note>
|
||||
{
|
||||
private readonly EditNotePiece piece;
|
||||
private Circle piece = null!;
|
||||
|
||||
public NotePlacementBlueprint()
|
||||
: base(new Note())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
InternalChild = piece = new EditNotePiece { Origin = Anchor.Centre };
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Masking = true;
|
||||
|
||||
InternalChild = piece = new Circle
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Colour = colours.Yellow,
|
||||
Height = 10
|
||||
};
|
||||
}
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
|
@ -1,18 +1,42 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
public partial class NoteSelectionBlueprint : ManiaSelectionBlueprint<Note>
|
||||
{
|
||||
private readonly EditNotePiece notePiece;
|
||||
|
||||
public NoteSelectionBlueprint(Note note)
|
||||
: base(note)
|
||||
{
|
||||
AddInternal(new EditNotePiece { RelativeSizeAxes = Axes.X });
|
||||
Anchor = Anchor.BottomCentre;
|
||||
Origin = Anchor.BottomCentre;
|
||||
AddInternal(notePiece = new EditNotePiece
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
notePiece.Height = DrawableObject.DrawHeight;
|
||||
}
|
||||
|
||||
protected override void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||
{
|
||||
notePiece.Scale = new Vector2(1, direction.NewValue == ScrollingDirection.Down ? 1 : -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
base.Update();
|
||||
|
||||
if (screenWithTimeline?.TimelineArea.Timeline != null)
|
||||
drawableRuleset.TimelineTimeRange = EditorClock.TrackLength / screenWithTimeline.TimelineArea.Timeline.CurrentZoom / 2;
|
||||
drawableRuleset.TimelineTimeRange = EditorClock.TrackLength / screenWithTimeline.TimelineArea.Timeline.CurrentZoom.Value / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
},
|
||||
columnBackgrounds = new ColumnFlow<Drawable>(stageDefinition)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Masking = false,
|
||||
},
|
||||
new HitTargetInsetContainer
|
||||
{
|
||||
@ -126,8 +127,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
},
|
||||
new Container
|
||||
{
|
||||
X = isLastColumn ? -0.16f : 0,
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = rightLineWidth,
|
||||
Scale = new Vector2(0.740f, 1),
|
||||
|
@ -28,6 +28,12 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
private readonly FillFlowContainer<Container<TContent>> columns;
|
||||
private readonly StageDefinition stageDefinition;
|
||||
|
||||
public new bool Masking
|
||||
{
|
||||
get => base.Masking;
|
||||
set => base.Masking = value;
|
||||
}
|
||||
|
||||
public ColumnFlow(StageDefinition stageDefinition)
|
||||
{
|
||||
this.stageDefinition = stageDefinition;
|
||||
|
@ -17,4 +17,8 @@
|
||||
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
||||
<PackageReference Include="Moq" Version="4.17.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||
@ -24,38 +25,57 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||
|
||||
[Test]
|
||||
public void TestTouchInputAfterTouchingComposeArea()
|
||||
public void TestTouchInputPlaceHitCircleDirectly()
|
||||
{
|
||||
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))));
|
||||
AddStep("tap to place circle", () => tap(this.ChildrenOfType<Playfield>().Single()));
|
||||
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));
|
||||
Assert.That(circle.Position.X, Is.EqualTo(256f).Within(0.01f));
|
||||
Assert.That(circle.Position.Y, Is.EqualTo(192f).Within(0.01f));
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
AddStep("tap slider", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "Slider")));
|
||||
[Test]
|
||||
public void TestTouchInputPlaceCircleAfterTouchingComposeArea()
|
||||
{
|
||||
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()));
|
||||
AddAssert("circle placed", () => EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate) is HitCircle);
|
||||
|
||||
AddStep("move current time", () => InputManager.Key(Key.Right));
|
||||
AddStep("move forward", () => InputManager.Key(Key.Right));
|
||||
|
||||
AddStep("tap to place circle", () => tap(this.ChildrenOfType<Playfield>().Single()));
|
||||
AddAssert("circle placed correctly", () =>
|
||||
{
|
||||
var circle = (HitCircle)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(circle.Position.X, Is.EqualTo(256f).Within(0.01f));
|
||||
Assert.That(circle.Position.Y, Is.EqualTo(192f).Within(0.01f));
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTouchInputPlaceSliderDirectly()
|
||||
{
|
||||
AddStep("tap slider", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "Slider")));
|
||||
|
||||
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));
|
||||
AddAssert("blueprint visible", () => this.ChildrenOfType<SliderPlacementBlueprint>().Single().Alpha > 0);
|
||||
AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value)));
|
||||
AddAssert("slider placed correctly", () =>
|
||||
{
|
||||
@ -76,12 +96,55 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
});
|
||||
}
|
||||
|
||||
private void tap(Drawable drawable) => tap(drawable.ScreenSpaceDrawQuad.Centre);
|
||||
[Test]
|
||||
public void TestTouchInputPlaceSliderAfterTouchingComposeArea()
|
||||
{
|
||||
AddStep("tap slider", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "Slider")));
|
||||
|
||||
AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single()));
|
||||
AddStep("tap and hold another spot", () => hold(this.ChildrenOfType<Playfield>().Single(), new Vector2(50, 0)));
|
||||
AddUntilStep("wait for slider placement", () => EditorBeatmap.HitObjects.SingleOrDefault(h => h.StartTime == EditorClock.CurrentTimeAccurate) is Slider);
|
||||
AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value)));
|
||||
|
||||
AddStep("move forward", () => 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));
|
||||
AddAssert("blueprint visible", () => this.ChildrenOfType<SliderPlacementBlueprint>().Single().IsPresent);
|
||||
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, Vector2 offset = default) => tap(drawable.ToScreenSpace(drawable.LayoutRectangle.Centre + offset));
|
||||
|
||||
private void tap(Vector2 position)
|
||||
{
|
||||
InputManager.BeginTouch(new Touch(TouchSource.Touch1, position));
|
||||
hold(position);
|
||||
InputManager.EndTouch(new Touch(TouchSource.Touch1, position));
|
||||
}
|
||||
|
||||
private void hold(Drawable drawable, Vector2 offset = default) => hold(drawable.ToScreenSpace(drawable.LayoutRectangle.Centre + offset));
|
||||
|
||||
private void hold(Vector2 position)
|
||||
{
|
||||
InputManager.BeginTouch(new Touch(TouchSource.Touch1, position));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
@ -33,6 +34,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
var osuLastObj = (OsuDifficultyHitObject)current.Previous(0);
|
||||
var osuLastLastObj = (OsuDifficultyHitObject)current.Previous(1);
|
||||
|
||||
const int radius = OsuDifficultyHitObject.NORMALISED_RADIUS;
|
||||
const int diameter = OsuDifficultyHitObject.NORMALISED_DIAMETER;
|
||||
|
||||
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
|
||||
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
|
||||
|
||||
@ -77,14 +81,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
wideAngleBonus = calcWideAngleBonus(currAngle);
|
||||
acuteAngleBonus = calcAcuteAngleBonus(currAngle);
|
||||
|
||||
if (osuCurrObj.StrainTime > 100) // Only buff deltaTime exceeding 300 bpm 1/2.
|
||||
if (DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2) < 300) // Only buff deltaTime exceeding 300 bpm 1/2.
|
||||
acuteAngleBonus = 0;
|
||||
else
|
||||
{
|
||||
acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
|
||||
* Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
|
||||
* Math.Min(angleBonus, diameter * 1.25 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
|
||||
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4
|
||||
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter).
|
||||
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, radius, diameter) - radius) / radius), 2); // Buff distance exceeding radius up to diameter.
|
||||
}
|
||||
|
||||
// Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
|
||||
@ -104,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2);
|
||||
|
||||
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
|
||||
double overlapVelocityBuff = Math.Min(125 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity));
|
||||
double overlapVelocityBuff = Math.Min(diameter * 1.25 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity));
|
||||
|
||||
velocityChangeBonus = overlapVelocityBuff * distRatio;
|
||||
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
@ -120,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
islandCount.Count++;
|
||||
|
||||
// repeated island (ex: triplet -> triplet)
|
||||
double power = logistic(island.Delta, 2.75, 0.24, 14);
|
||||
double power = DifficultyCalculationUtils.Logistic(island.Delta, maxValue: 2.75, multiplier: 0.24, midpointOffset: 58.33);
|
||||
effectiveRatio *= Math.Min(3.0 / islandCount.Count, Math.Pow(1.0 / islandCount.Count, power));
|
||||
|
||||
islandCounts[countIndex] = (islandCount.Island, islandCount.Count);
|
||||
@ -172,8 +173,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
||||
}
|
||||
|
||||
private static double logistic(double x, double maxValue, double multiplier, double offset) => (maxValue / (1 + Math.Pow(Math.E, offset - (multiplier * x))));
|
||||
|
||||
private class Island : IEquatable<Island>
|
||||
{
|
||||
private readonly double deltaDifferenceEpsilon;
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
@ -10,8 +11,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
{
|
||||
public static class SpeedEvaluator
|
||||
{
|
||||
private const double single_spacing_threshold = 125; // 1.25 circles distance between centers
|
||||
private const double min_speed_bonus = 75; // ~200BPM
|
||||
private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers
|
||||
private const double min_speed_bonus = 200; // 200 BPM 1/4th
|
||||
private const double speed_balancing_factor = 40;
|
||||
private const double distance_multiplier = 0.94;
|
||||
|
||||
@ -43,8 +44,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
double speedBonus = 0.0;
|
||||
|
||||
// Add additional scaling bonus for streams/bursts higher than 200bpm
|
||||
if (strainTime < min_speed_bonus)
|
||||
speedBonus = 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
|
||||
if (DifficultyCalculationUtils.MillisecondsToBPM(strainTime) > min_speed_bonus)
|
||||
speedBonus = 0.75 * Math.Pow((DifficultyCalculationUtils.BPMToMilliseconds(min_speed_bonus) - strainTime) / speed_balancing_factor, 2);
|
||||
|
||||
double travelDistance = osuPrevObj?.TravelDistance ?? 0;
|
||||
double distance = travelDistance + osuCurrObj.MinimumJumpDistance;
|
||||
|
@ -48,8 +48,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
|
||||
|
||||
double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountDifficultStrains();
|
||||
double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountDifficultStrains();
|
||||
double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountTopWeightedStrains();
|
||||
double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountTopWeightedStrains();
|
||||
|
||||
if (mods.Any(m => m is OsuModTouchDevice))
|
||||
{
|
||||
|
@ -59,6 +59,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
countSliderEndsDropped = osuAttributes.SliderCount - score.Statistics.GetValueOrDefault(HitResult.SliderTailHit);
|
||||
countSliderTickMiss = score.Statistics.GetValueOrDefault(HitResult.LargeTickMiss);
|
||||
effectiveMissCount = countMiss;
|
||||
|
||||
if (osuAttributes.SliderCount > 0)
|
||||
{
|
||||
@ -179,7 +180,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
// We add tick misses here since they too mean that the player didn't follow the slider properly
|
||||
// We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
|
||||
estimateImproperlyFollowedDifficultSliders = Math.Min(countSliderEndsDropped + countSliderTickMiss, estimateDifficultSliders);
|
||||
estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, estimateDifficultSliders);
|
||||
}
|
||||
|
||||
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / estimateDifficultSliders, 3) + attributes.SliderFactor;
|
||||
|
@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
/// </summary>
|
||||
public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
|
||||
|
||||
public const int NORMALISED_DIAMETER = NORMALISED_RADIUS * 2;
|
||||
|
||||
public const int MIN_DELTA_TIME = 25;
|
||||
|
||||
private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f;
|
||||
|
@ -34,7 +34,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
currentStrain *= strainDecay(current.DeltaTime);
|
||||
currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier;
|
||||
ObjectStrains.Add(currentStrain);
|
||||
|
||||
return currentStrain;
|
||||
}
|
||||
|
@ -23,9 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
/// </summary>
|
||||
protected virtual double ReducedStrainBaseline => 0.75;
|
||||
|
||||
protected List<double> ObjectStrains = new List<double>();
|
||||
protected double Difficulty;
|
||||
|
||||
protected OsuStrainSkill(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
@ -33,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
|
||||
public override double DifficultyValue()
|
||||
{
|
||||
Difficulty = 0;
|
||||
double difficulty = 0;
|
||||
double weight = 1;
|
||||
|
||||
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
||||
@ -53,25 +50,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
// We're sorting from highest to lowest strain.
|
||||
foreach (double strain in strains.OrderDescending())
|
||||
{
|
||||
Difficulty += strain * weight;
|
||||
difficulty += strain * weight;
|
||||
weight *= DecayWeight;
|
||||
}
|
||||
|
||||
return Difficulty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of strains weighted against the top strain.
|
||||
/// The result is scaled by clock rate as it affects the total number of strains.
|
||||
/// </summary>
|
||||
public double CountDifficultStrains()
|
||||
{
|
||||
if (Difficulty == 0)
|
||||
return 0.0;
|
||||
|
||||
double consistentTopStrain = Difficulty / 10; // What would the top strain be if all strain values were identical
|
||||
// Use a weighted sum of all strains. Constants are arbitrary and give nice values
|
||||
return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88))));
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||
|
@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
|
||||
|
||||
double totalStrain = currentStrain * currentRhythm;
|
||||
ObjectStrains.Add(totalStrain);
|
||||
|
||||
return totalStrain;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osuTK;
|
||||
@ -31,12 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
||||
public override void EndPlacement(bool commit)
|
||||
{
|
||||
if (!commit && PlacementActive != PlacementState.Finished)
|
||||
{
|
||||
gridToolboxGroup.StartPosition.Value = originalOrigin;
|
||||
gridToolboxGroup.Spacing.Value = originalSpacing;
|
||||
if (!gridToolboxGroup.GridLinesRotation.Disabled)
|
||||
gridToolboxGroup.GridLinesRotation.Value = originalRotation;
|
||||
}
|
||||
resetGridState();
|
||||
|
||||
base.EndPlacement(commit);
|
||||
|
||||
@ -103,6 +99,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
{
|
||||
if (State.Value == Visibility.Hidden)
|
||||
return;
|
||||
|
||||
var pos = ToLocalSpace(result.ScreenSpacePosition);
|
||||
|
||||
if (PlacementActive != PlacementState.Active)
|
||||
@ -122,5 +121,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
base.PopOut();
|
||||
resetGridState();
|
||||
}
|
||||
|
||||
private void resetGridState()
|
||||
{
|
||||
gridToolboxGroup.StartPosition.Value = originalOrigin;
|
||||
gridToolboxGroup.Spacing.Value = originalSpacing;
|
||||
if (!gridToolboxGroup.GridLinesRotation.Disabled)
|
||||
gridToolboxGroup.GridLinesRotation.Value = originalRotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -156,6 +156,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
return true;
|
||||
}
|
||||
|
||||
// this allows sliders to be drawn outside compose area (after starting from a point within the compose area).
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || PlacementActive == PlacementState.Active;
|
||||
|
||||
// ReceivePositionalInputAtSubTree generally always returns true when masking is disabled, but we don't want that,
|
||||
// otherwise a slider path tooltip will be displayed anywhere in the editor (outside compose area).
|
||||
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
private void beginNewSegment(PathControlPoint lastPoint)
|
||||
{
|
||||
segmentStart = lastPoint;
|
||||
|
@ -50,9 +50,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
[Resolved]
|
||||
private HitObjectComposer composer { get; set; } = null!;
|
||||
|
||||
private Bindable<TernaryState> newComboState = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
var selectionHandler = (EditorSelectionHandler)composer.BlueprintContainer.SelectionHandler;
|
||||
newComboState = selectionHandler.SelectionNewComboState.GetBoundCopy();
|
||||
|
||||
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
|
||||
|
||||
Child = new FillFlowContainer
|
||||
@ -120,10 +125,11 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
changeHandler?.BeginChange();
|
||||
began = true;
|
||||
|
||||
distanceSnapInput.Current.BindValueChanged(_ => tryCreatePolygon());
|
||||
offsetAngleInput.Current.BindValueChanged(_ => tryCreatePolygon());
|
||||
repeatCountInput.Current.BindValueChanged(_ => tryCreatePolygon());
|
||||
pointInput.Current.BindValueChanged(_ => tryCreatePolygon());
|
||||
distanceSnapInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon));
|
||||
offsetAngleInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon));
|
||||
repeatCountInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon));
|
||||
pointInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon));
|
||||
newComboState.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon));
|
||||
tryCreatePolygon();
|
||||
}
|
||||
|
||||
@ -138,39 +144,69 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
double length = distanceSnapInput.Current.Value * velocity * timeSpacing;
|
||||
float polygonRadius = (float)(length / (2 * Math.Sin(double.Pi / pointInput.Current.Value)));
|
||||
|
||||
editorBeatmap.RemoveRange(insertedCircles);
|
||||
insertedCircles.Clear();
|
||||
int totalPoints = pointInput.Current.Value * repeatCountInput.Current.Value;
|
||||
|
||||
var selectionHandler = (EditorSelectionHandler)composer.BlueprintContainer.SelectionHandler;
|
||||
bool first = true;
|
||||
|
||||
for (int i = 1; i <= pointInput.Current.Value * repeatCountInput.Current.Value; ++i)
|
||||
if (insertedCircles.Count > totalPoints)
|
||||
{
|
||||
float angle = float.DegreesToRadians(offsetAngleInput.Current.Value) + i * (2 * float.Pi / pointInput.Current.Value);
|
||||
var position = OsuPlayfield.BASE_SIZE / 2 + new Vector2(polygonRadius * float.Cos(angle), polygonRadius * float.Sin(angle));
|
||||
editorBeatmap.RemoveRange(insertedCircles.GetRange(totalPoints, insertedCircles.Count - totalPoints));
|
||||
insertedCircles.RemoveRange(totalPoints, insertedCircles.Count - totalPoints);
|
||||
}
|
||||
|
||||
var circle = new HitCircle
|
||||
var newlyAdded = new List<HitCircle>();
|
||||
|
||||
for (int i = 0; i < totalPoints; ++i)
|
||||
{
|
||||
float angle = float.DegreesToRadians(offsetAngleInput.Current.Value) + (i + 1) * (2 * float.Pi / pointInput.Current.Value);
|
||||
var position = OsuPlayfield.BASE_SIZE / 2 + new Vector2(polygonRadius * float.Cos(angle), polygonRadius * float.Sin(angle));
|
||||
bool newCombo = i == 0 && newComboState.Value == TernaryState.True;
|
||||
|
||||
HitCircle circle;
|
||||
|
||||
if (i < insertedCircles.Count)
|
||||
{
|
||||
Position = position,
|
||||
StartTime = startTime,
|
||||
NewCombo = first && selectionHandler.SelectionNewComboState.Value == TernaryState.True,
|
||||
};
|
||||
// TODO: probably ensure samples also follow current ternary status (not trivial)
|
||||
circle.Samples.Add(circle.CreateHitSampleInfo());
|
||||
circle = insertedCircles[i];
|
||||
|
||||
circle.Position = position;
|
||||
circle.StartTime = startTime;
|
||||
circle.NewCombo = newCombo;
|
||||
|
||||
editorBeatmap.Update(circle);
|
||||
}
|
||||
else
|
||||
{
|
||||
circle = new HitCircle
|
||||
{
|
||||
Position = position,
|
||||
StartTime = startTime,
|
||||
NewCombo = newCombo,
|
||||
};
|
||||
|
||||
newlyAdded.Add(circle);
|
||||
|
||||
// TODO: probably ensure samples also follow current ternary status (not trivial)
|
||||
circle.Samples.Add(circle.CreateHitSampleInfo());
|
||||
}
|
||||
|
||||
if (position.X < 0 || position.Y < 0 || position.X > OsuPlayfield.BASE_SIZE.X || position.Y > OsuPlayfield.BASE_SIZE.Y)
|
||||
{
|
||||
commitButton.Enabled.Value = false;
|
||||
editorBeatmap.RemoveRange(insertedCircles);
|
||||
insertedCircles.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
insertedCircles.Add(circle);
|
||||
startTime = beatSnapProvider.SnapTime(startTime + timeSpacing);
|
||||
|
||||
first = false;
|
||||
}
|
||||
|
||||
editorBeatmap.AddRange(insertedCircles);
|
||||
var previousNewComboState = newComboState.Value;
|
||||
|
||||
insertedCircles.AddRange(newlyAdded);
|
||||
editorBeatmap.AddRange(newlyAdded);
|
||||
|
||||
// When adding new hitObjects, newCombo state will get reset to false when no objects are selected.
|
||||
// Since this is the case when this popover is showing, we need to restore the previous newCombo state
|
||||
newComboState.Value = previousNewComboState;
|
||||
|
||||
commitButton.Enabled.Value = true;
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
@ -25,13 +26,17 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
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 EditorRadioButtonCollection rotationOrigin = null!;
|
||||
|
||||
private RadioButton gridCentreButton = null!;
|
||||
private RadioButton playfieldCentreButton = null!;
|
||||
private RadioButton selectionCentreButton = null!;
|
||||
|
||||
private Bindable<EditorOrigin> configRotationOrigin = null!;
|
||||
|
||||
public PreciseRotationPopover(SelectionRotationHandler rotationHandler, OsuGridToolboxGroup gridToolbox)
|
||||
{
|
||||
this.rotationHandler = rotationHandler;
|
||||
@ -41,8 +46,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
configRotationOrigin = config.GetBindable<EditorOrigin>(OsuSetting.EditorRotationOrigin);
|
||||
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Width = 220,
|
||||
@ -66,14 +73,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Items = new[]
|
||||
{
|
||||
new RadioButton("Grid centre",
|
||||
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.GridCentre },
|
||||
gridCentreButton = new RadioButton("Grid centre",
|
||||
() => rotationInfo.Value = rotationInfo.Value with { Origin = EditorOrigin.GridCentre },
|
||||
() => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }),
|
||||
new RadioButton("Playfield centre",
|
||||
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre },
|
||||
playfieldCentreButton = new RadioButton("Playfield centre",
|
||||
() => rotationInfo.Value = rotationInfo.Value with { Origin = EditorOrigin.PlayfieldCentre },
|
||||
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
||||
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 })
|
||||
}
|
||||
}
|
||||
@ -95,13 +102,63 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
angleInput.SelectAll();
|
||||
});
|
||||
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
|
||||
rotationOrigin.Items.First().Select();
|
||||
|
||||
rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e =>
|
||||
{
|
||||
selectionCentreButton.Selected.Disabled = !e.NewValue;
|
||||
}, 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 =>
|
||||
{
|
||||
rotationHandler.Update(rotation.NewValue.Degrees, getOriginPosition(rotation.NewValue));
|
||||
@ -111,9 +168,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
private Vector2? getOriginPosition(PreciseRotationInfo rotation) =>
|
||||
rotation.Origin switch
|
||||
{
|
||||
RotationOrigin.GridCentre => gridToolbox.StartPosition.Value,
|
||||
RotationOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2,
|
||||
RotationOrigin.SelectionCentre => null,
|
||||
EditorOrigin.GridCentre => gridToolbox.StartPosition.Value,
|
||||
EditorOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2,
|
||||
EditorOrigin.SelectionCentre => null,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(rotation))
|
||||
};
|
||||
|
||||
@ -143,12 +200,5 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
}
|
||||
}
|
||||
|
||||
public enum RotationOrigin
|
||||
{
|
||||
GridCentre,
|
||||
PlayfieldCentre,
|
||||
SelectionCentre
|
||||
}
|
||||
|
||||
public record PreciseRotationInfo(float Degrees, RotationOrigin Origin);
|
||||
public record PreciseRotationInfo(float Degrees, EditorOrigin Origin);
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Input.Bindings;
|
||||
@ -18,6 +19,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
@ -28,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
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 BindableNumber<float> scaleInputBindable = null!;
|
||||
@ -41,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
private OsuCheckbox xCheckBox = null!;
|
||||
private OsuCheckbox yCheckBox = null!;
|
||||
|
||||
private Bindable<EditorOrigin> configScaleOrigin = null!;
|
||||
|
||||
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
|
||||
|
||||
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler, OsuGridToolboxGroup gridToolbox)
|
||||
@ -52,10 +56,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(EditorBeatmap editorBeatmap)
|
||||
private void load(EditorBeatmap editorBeatmap, OsuConfigManager config)
|
||||
{
|
||||
selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
|
||||
|
||||
configScaleOrigin = config.GetBindable<EditorOrigin>(OsuSetting.EditorScaleOrigin);
|
||||
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Width = 220,
|
||||
@ -67,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
Current = scaleInputBindable = new BindableNumber<float>
|
||||
{
|
||||
MinValue = 0.5f,
|
||||
MinValue = 0.05f,
|
||||
MaxValue = 2,
|
||||
Precision = 0.001f,
|
||||
Value = 1,
|
||||
@ -82,13 +88,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
Items = new[]
|
||||
{
|
||||
gridCentreButton = new RadioButton("Grid centre",
|
||||
() => setOrigin(ScaleOrigin.GridCentre),
|
||||
() => setOrigin(EditorOrigin.GridCentre),
|
||||
() => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }),
|
||||
playfieldCentreButton = new RadioButton("Playfield centre",
|
||||
() => setOrigin(ScaleOrigin.PlayfieldCentre),
|
||||
() => setOrigin(EditorOrigin.PlayfieldCentre),
|
||||
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
||||
selectionCentreButton = new RadioButton("Selection centre",
|
||||
() => setOrigin(ScaleOrigin.SelectionCentre),
|
||||
() => setOrigin(EditorOrigin.SelectionCentre),
|
||||
() => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare })
|
||||
}
|
||||
},
|
||||
@ -165,7 +171,56 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.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 =>
|
||||
{
|
||||
@ -182,7 +237,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
private void updateAxisCheckBoxesEnabled()
|
||||
{
|
||||
if (scaleInfo.Value.Origin != ScaleOrigin.SelectionCentre)
|
||||
if (scaleInfo.Value.Origin != EditorOrigin.SelectionCentre)
|
||||
{
|
||||
toggleAxisAvailable(xCheckBox.Current, true);
|
||||
toggleAxisAvailable(yCheckBox.Current, true);
|
||||
@ -208,7 +263,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
if (!scaleHandler.OriginalSurroundingQuad.HasValue)
|
||||
return;
|
||||
|
||||
const float min_scale = 0.5f;
|
||||
const float min_scale = 0.05f;
|
||||
const float max_scale = 10;
|
||||
|
||||
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
|
||||
@ -230,7 +285,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
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 };
|
||||
updateMinMaxScale();
|
||||
@ -241,13 +296,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
switch (scale.Origin)
|
||||
{
|
||||
case ScaleOrigin.GridCentre:
|
||||
case EditorOrigin.GridCentre:
|
||||
return gridToolbox.StartPosition.Value;
|
||||
|
||||
case ScaleOrigin.PlayfieldCentre:
|
||||
case EditorOrigin.PlayfieldCentre:
|
||||
return OsuPlayfield.BASE_SIZE / 2;
|
||||
|
||||
case ScaleOrigin.SelectionCentre:
|
||||
case EditorOrigin.SelectionCentre:
|
||||
if (selectedItems.Count == 1 && selectedItems.First() is Slider slider)
|
||||
return slider.Position;
|
||||
|
||||
@ -271,7 +326,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
return result;
|
||||
}
|
||||
|
||||
private float getRotation(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.GridCentre ? gridToolbox.GridLinesRotation.Value : 0;
|
||||
private float getRotation(PreciseScaleInfo scale) => scale.Origin == EditorOrigin.GridCentre ? gridToolbox.GridLinesRotation.Value : 0;
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
@ -299,12 +354,5 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
}
|
||||
}
|
||||
|
||||
public enum ScaleOrigin
|
||||
{
|
||||
GridCentre,
|
||||
PlayfieldCentre,
|
||||
SelectionCentre
|
||||
}
|
||||
|
||||
public record PreciseScaleInfo(float Scale, ScaleOrigin Origin, bool XAxis, bool YAxis);
|
||||
public record PreciseScaleInfo(float Scale, EditorOrigin Origin, bool XAxis, bool YAxis);
|
||||
}
|
||||
|
85
osu.Game.Rulesets.Osu/Mods/OsuModBloom.cs
Normal file
85
osu.Game.Rulesets.Osu/Mods/OsuModBloom.cs
Normal file
@ -0,0 +1,85 @@
|
||||
// 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.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModBloom : Mod, IApplicableToScoreProcessor, IUpdatableByPlayfield, IApplicableToPlayer
|
||||
{
|
||||
public override string Name => "Bloom";
|
||||
public override string Acronym => "BM";
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override LocalisableString Description => "The cursor blooms into.. a larger cursor!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
protected const float MIN_SIZE = 1;
|
||||
protected const float TRANSITION_DURATION = 100;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight), typeof(OsuModNoScope), typeof(ModTouchDevice) };
|
||||
|
||||
protected readonly BindableNumber<int> CurrentCombo = new BindableInt();
|
||||
protected readonly IBindable<bool> IsBreakTime = new Bindable<bool>();
|
||||
|
||||
private float currentSize;
|
||||
|
||||
[SettingSource(
|
||||
"Max size at combo",
|
||||
"The combo count at which the cursor reaches its maximum size",
|
||||
SettingControlType = typeof(SettingsSlider<int, RoundedSliderBar<int>>)
|
||||
)]
|
||||
public BindableInt MaxSizeComboCount { get; } = new BindableInt(50)
|
||||
{
|
||||
MinValue = 5,
|
||||
MaxValue = 100,
|
||||
};
|
||||
|
||||
[SettingSource(
|
||||
"Final size multiplier",
|
||||
"The multiplier applied to cursor size when combo reaches maximum",
|
||||
SettingControlType = typeof(SettingsSlider<float, RoundedSliderBar<float>>)
|
||||
)]
|
||||
public BindableFloat MaxCursorSize { get; } = new BindableFloat(10f)
|
||||
{
|
||||
MinValue = 5f,
|
||||
MaxValue = 15f,
|
||||
Precision = 0.5f,
|
||||
};
|
||||
|
||||
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
|
||||
|
||||
public void ApplyToPlayer(Player player)
|
||||
{
|
||||
IsBreakTime.BindTo(player.IsBreakTime);
|
||||
}
|
||||
|
||||
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
|
||||
{
|
||||
CurrentCombo.BindTo(scoreProcessor.Combo);
|
||||
CurrentCombo.BindValueChanged(combo =>
|
||||
{
|
||||
currentSize = Math.Clamp(MaxCursorSize.Value * ((float)combo.NewValue / MaxSizeComboCount.Value), MIN_SIZE, MaxCursorSize.Value);
|
||||
}, true);
|
||||
}
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
OsuCursor cursor = (OsuCursor)(playfield.Cursor!.ActiveCursor);
|
||||
|
||||
if (IsBreakTime.Value)
|
||||
cursor.ModScaleAdjust.Value = 1;
|
||||
else
|
||||
cursor.ModScaleAdjust.Value = (float)Interpolation.Lerp(cursor.ModScaleAdjust.Value, currentSize, Math.Clamp(cursor.Time.Elapsed / TRANSITION_DURATION, 0, 1));
|
||||
}
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public partial class OsuModFlashlight : ModFlashlight<OsuHitObject>, IApplicableToDrawableHitObject
|
||||
{
|
||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModBlinds)).ToArray();
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModBloom), typeof(OsuModBlinds) }).ToArray();
|
||||
|
||||
private const double default_follow_delay = 120;
|
||||
|
||||
|
@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public override LocalisableString Description => "Where's the cursor?";
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModBloom) };
|
||||
|
||||
private PeriodTracker spinnerPeriods = null!;
|
||||
|
||||
public override BindableInt HiddenComboCount { get; } = new BindableInt(10)
|
||||
|
@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModTouchDevice : ModTouchDevice
|
||||
{
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray();
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModBloom) }).ToArray();
|
||||
public override bool Ranked => UsesDefaultConfiguration;
|
||||
}
|
||||
}
|
||||
|
@ -214,7 +214,8 @@ namespace osu.Game.Rulesets.Osu
|
||||
new OsuModFreezeFrame(),
|
||||
new OsuModBubbles(),
|
||||
new OsuModSynesthesia(),
|
||||
new OsuModDepth()
|
||||
new OsuModDepth(),
|
||||
new OsuModBloom()
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
|
@ -14,6 +14,7 @@ using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
using osu.Framework.Graphics.Shaders.Types;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Graphics.Visualisation;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Timing;
|
||||
@ -23,6 +24,7 @@ using osuTK.Graphics.ES30;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
{
|
||||
[DrawVisualiserHidden]
|
||||
public partial class CursorTrail : Drawable, IRequireHighFrequencyMousePosition
|
||||
{
|
||||
private const int max_sprites = 2048;
|
||||
|
@ -38,6 +38,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
public IBindable<float> CursorScale => cursorScale;
|
||||
|
||||
/// <summary>
|
||||
/// Mods which want to adjust cursor size should do so via this bindable.
|
||||
/// </summary>
|
||||
public readonly Bindable<float> ModScaleAdjust = new Bindable<float>(1);
|
||||
|
||||
private readonly Bindable<float> cursorScale = new BindableFloat(1);
|
||||
|
||||
private Bindable<float> userCursorScale = null!;
|
||||
@ -67,6 +72,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
autoCursorScale = config.GetBindable<bool>(OsuSetting.AutoCursorSize);
|
||||
autoCursorScale.ValueChanged += _ => cursorScale.Value = CalculateCursorScale();
|
||||
|
||||
ModScaleAdjust.ValueChanged += _ => cursorScale.Value = CalculateCursorScale();
|
||||
|
||||
cursorScale.BindValueChanged(e => cursorScaleContainer.Scale = new Vector2(e.NewValue), true);
|
||||
}
|
||||
|
||||
@ -90,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
protected virtual float CalculateCursorScale()
|
||||
{
|
||||
float scale = userCursorScale.Value;
|
||||
float scale = userCursorScale.Value * ModScaleAdjust.Value;
|
||||
|
||||
if (autoCursorScale.Value && state != null)
|
||||
{
|
||||
|
@ -13,7 +13,8 @@
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Project References">
|
||||
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
<ProjectReference Include="..\osu.Game.Tests\osu.Game.Tests.csproj" />
|
||||
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data;
|
||||
@ -11,26 +12,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
{
|
||||
public class ColourEvaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// A sigmoid function. It gives a value between (middle - height/2) and (middle + height/2).
|
||||
/// </summary>
|
||||
/// <param name="val">The input value.</param>
|
||||
/// <param name="center">The center of the sigmoid, where the largest gradient occurs and value is equal to middle.</param>
|
||||
/// <param name="width">The radius of the sigmoid, outside of which values are near the minimum/maximum.</param>
|
||||
/// <param name="middle">The middle of the sigmoid output.</param>
|
||||
/// <param name="height">The height of the sigmoid output. This will be equal to max value - min value.</param>
|
||||
private static double sigmoid(double val, double center, double width, double middle, double height)
|
||||
{
|
||||
double sigmoid = Math.Tanh(Math.E * -(val - center) / width);
|
||||
return sigmoid * (height / 2) + middle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluate the difficulty of the first note of a <see cref="MonoStreak"/>.
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(MonoStreak monoStreak)
|
||||
{
|
||||
return sigmoid(monoStreak.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5;
|
||||
return DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -38,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern)
|
||||
{
|
||||
return sigmoid(alternatingMonoPattern.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(alternatingMonoPattern.Parent);
|
||||
return DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * EvaluateDifficultyOf(alternatingMonoPattern.Parent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -46,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern)
|
||||
{
|
||||
return 2 * (1 - sigmoid(repeatingHitPattern.RepetitionInterval, 2, 2, 0.5, 1));
|
||||
return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E));
|
||||
}
|
||||
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject hitObject)
|
||||
|
@ -1,33 +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;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates the stamina coefficient of taiko difficulty.
|
||||
/// </summary>
|
||||
public class Stamina : StrainDecaySkill
|
||||
public class Stamina : StrainSkill
|
||||
{
|
||||
protected override double SkillMultiplier => 1.1;
|
||||
protected override double StrainDecayBase => 0.4;
|
||||
private double skillMultiplier => 1.1;
|
||||
private double strainDecayBase => 0.4;
|
||||
|
||||
private readonly bool singleColourStamina;
|
||||
|
||||
private double currentStrain;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Stamina"/> skill.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
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")]
|
||||
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>
|
||||
/// The difficulty corresponding to the rhythm skill.
|
||||
/// </summary>
|
||||
@ -60,6 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||
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)
|
||||
@ -69,6 +76,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
GreatHitWindow = values[ATTRIB_ID_GREAT_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 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);
|
||||
Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm);
|
||||
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 rhythmRating = rhythm.DifficultyValue() * rhythm_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 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.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
@ -95,6 +108,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
StarRating = starRating,
|
||||
Mods = mods,
|
||||
StaminaDifficulty = staminaRating,
|
||||
MonoStaminaFactor = monoStaminaFactor,
|
||||
RhythmDifficulty = rhythmRating,
|
||||
ColourDifficulty = colourRating,
|
||||
PeakDifficulty = combinedRating,
|
||||
|
@ -42,18 +42,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
if (totalSuccessfulHits > 0)
|
||||
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;
|
||||
|
||||
double multiplier = 1.13;
|
||||
|
||||
if (score.Mods.Any(m => m is ModHidden))
|
||||
if (score.Mods.Any(m => m is ModHidden) && !isConvert)
|
||||
multiplier *= 1.075;
|
||||
|
||||
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 totalValue =
|
||||
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;
|
||||
|
||||
@ -81,21 +81,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
difficultyValue *= Math.Pow(0.986, effectiveMissCount);
|
||||
|
||||
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;
|
||||
|
||||
if (score.Mods.Any(m => m is ModHardRock))
|
||||
difficultyValue *= 1.10;
|
||||
|
||||
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)
|
||||
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)
|
||||
|
@ -54,17 +54,17 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
|
||||
public void SetStrongState(bool state)
|
||||
{
|
||||
if (SelectedItems.OfType<Hit>().All(h => h.IsStrong == state))
|
||||
if (SelectedItems.OfType<TaikoStrongableHitObject>().All(h => h.IsStrong == state))
|
||||
return;
|
||||
|
||||
EditorBeatmap.PerformOnSelection(h =>
|
||||
{
|
||||
if (!(h is Hit taikoHit)) return;
|
||||
if (h is not TaikoStrongableHitObject strongable) return;
|
||||
|
||||
if (taikoHit.IsStrong != state)
|
||||
if (strongable.IsStrong != state)
|
||||
{
|
||||
taikoHit.IsStrong = state;
|
||||
EditorBeatmap.Update(taikoHit);
|
||||
strongable.IsStrong = state;
|
||||
EditorBeatmap.Update(strongable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -31,6 +31,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
public override Quad ScreenSpaceDrawQuad => MainPiece.Drawable.ScreenSpaceDrawQuad;
|
||||
|
||||
// done strictly for editor purposes.
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => MainPiece.Drawable.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
/// <summary>
|
||||
/// Rolling number of tick hits. This increases for hits and decreases for misses.
|
||||
/// </summary>
|
||||
@ -78,6 +81,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
base.RecreatePieces();
|
||||
updateColour();
|
||||
Height = HitObject.IsStrong ? TaikoStrongableHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE;
|
||||
}
|
||||
|
||||
protected override void OnFree()
|
||||
|
@ -12,6 +12,7 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
@ -44,6 +45,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
IsFirstTick.Value = HitObject.FirstTick;
|
||||
}
|
||||
|
||||
protected override void RecreatePieces()
|
||||
{
|
||||
base.RecreatePieces();
|
||||
Size = new Vector2(HitObject.IsStrong ? TaikoStrongableHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE);
|
||||
}
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
if (!userTriggered)
|
||||
|
@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
@ -63,6 +64,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
updateActionsFromType();
|
||||
base.RecreatePieces();
|
||||
Size = new Vector2(HitObject.IsStrong ? TaikoStrongableHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE);
|
||||
}
|
||||
|
||||
protected override void OnFree()
|
||||
|
@ -19,6 +19,7 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
@ -34,6 +35,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
/// </summary>
|
||||
private const double ring_appear_offset = 100;
|
||||
|
||||
private Vector2 baseSize;
|
||||
|
||||
private readonly Container<DrawableSwellTick> ticks;
|
||||
private readonly Container bodyContainer;
|
||||
private readonly CircularContainer targetRing;
|
||||
@ -141,6 +144,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
|
||||
protected override void RecreatePieces()
|
||||
{
|
||||
base.RecreatePieces();
|
||||
Size = baseSize = new Vector2(TaikoHitObject.DEFAULT_SIZE);
|
||||
}
|
||||
|
||||
protected override void OnFree()
|
||||
{
|
||||
base.OnFree();
|
||||
@ -269,7 +278,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Size = BaseSize * Parent!.RelativeChildSize;
|
||||
Size = baseSize * Parent!.RelativeChildSize;
|
||||
|
||||
// Make the swell stop at the hit target
|
||||
X = Math.Max(0, X);
|
||||
|
@ -130,7 +130,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
public new TObject HitObject => (TObject)base.HitObject;
|
||||
|
||||
protected Vector2 BaseSize;
|
||||
protected SkinnableDrawable MainPiece;
|
||||
|
||||
protected DrawableTaikoHitObject([CanBeNull] TObject hitObject)
|
||||
@ -152,8 +151,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
protected virtual void RecreatePieces()
|
||||
{
|
||||
Size = BaseSize = new Vector2(TaikoHitObject.DEFAULT_SIZE);
|
||||
|
||||
if (MainPiece != null)
|
||||
Content.Remove(MainPiece, true);
|
||||
|
||||
|
@ -8,7 +8,6 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
@ -44,13 +43,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
isStrong.UnbindEvents();
|
||||
}
|
||||
|
||||
protected override void RecreatePieces()
|
||||
{
|
||||
base.RecreatePieces();
|
||||
if (HitObject.IsStrong)
|
||||
Size = BaseSize = new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE);
|
||||
}
|
||||
|
||||
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||
{
|
||||
base.AddNestedHitObject(hitObject);
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -9,6 +10,7 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
@ -19,13 +21,22 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
{
|
||||
get
|
||||
{
|
||||
var headDrawQuad = headCircle.ScreenSpaceDrawQuad;
|
||||
var tailDrawQuad = tailCircle.ScreenSpaceDrawQuad;
|
||||
// the reason why this calculation is so involved is that the head & tail sprites have different sizes/radii.
|
||||
// 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 Sprite body = null!;
|
||||
|
@ -84,8 +84,11 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
protected virtual double ComputeTimeRange()
|
||||
{
|
||||
// Adjust when we're using constant algorithm to not be sluggish.
|
||||
double multiplier = VisualisationMethod == ScrollVisualisationMethod.Constant ? 4 * Beatmap.Difficulty.SliderMultiplier : 1;
|
||||
// Using the constant algorithm results in a sluggish scroll speed that's equal to 60 BPM.
|
||||
// We need to adjust it to the expected default scroll speed (BPM * base SV multiplier).
|
||||
double multiplier = VisualisationMethod == ScrollVisualisationMethod.Constant
|
||||
? (Beatmap.BeatmapInfo.BPM * Beatmap.Difficulty.SliderMultiplier) / 60
|
||||
: 1;
|
||||
return PlayfieldAdjustmentContainer.ComputeTimeRange() / multiplier;
|
||||
}
|
||||
|
||||
|
@ -120,11 +120,11 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
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.
|
||||
Assert.That(expected.beatmap.ControlPointInfo.TimingPoints.Serialize(), Is.EqualTo(actual.beatmap.ControlPointInfo.TimingPoints.Serialize()));
|
||||
Assert.That(expected.beatmap.ControlPointInfo.EffectPoints.Serialize(), Is.EqualTo(actual.beatmap.ControlPointInfo.EffectPoints.Serialize()));
|
||||
Assert.That(actual.beatmap.ControlPointInfo.TimingPoints.Serialize(), Is.EqualTo(expected.beatmap.ControlPointInfo.TimingPoints.Serialize()));
|
||||
Assert.That(actual.beatmap.ControlPointInfo.EffectPoints.Serialize(), Is.EqualTo(expected.beatmap.ControlPointInfo.EffectPoints.Serialize()));
|
||||
|
||||
// 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.
|
||||
Assert.IsTrue(areComboColoursEqual(expected.skin.Configuration, actual.skin.Configuration));
|
||||
|
63
osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs
Normal file
63
osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs
Normal file
@ -0,0 +1,63 @@
|
||||
// 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.IO;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual;
|
||||
using MemoryStream = System.IO.MemoryStream;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps.IO
|
||||
{
|
||||
[HeadlessTest]
|
||||
public partial class LegacyBeatmapExporterTest : OsuTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; } = null!;
|
||||
|
||||
[Test]
|
||||
public void TestObjectsSnappedAfterTruncatingExport()
|
||||
{
|
||||
IWorkingBeatmap beatmap = null!;
|
||||
MemoryStream outStream = null!;
|
||||
|
||||
// Ensure importer encoding is correct
|
||||
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"decimal-timing-beatmap.olz"));
|
||||
AddAssert("timing point has decimal offset", () => beatmap.Beatmap.ControlPointInfo.TimingPoints[0].Time, () => Is.EqualTo(284.725).Within(0.001));
|
||||
AddAssert("kiai has decimal offset", () => beatmap.Beatmap.ControlPointInfo.EffectPoints[0].Time, () => Is.EqualTo(28520.019).Within(0.001));
|
||||
AddAssert("hit object has decimal offset", () => beatmap.Beatmap.HitObjects[0].StartTime, () => Is.EqualTo(28520.019).Within(0.001));
|
||||
|
||||
// Ensure exporter legacy conversion is correct
|
||||
AddStep("export", () =>
|
||||
{
|
||||
outStream = new MemoryStream();
|
||||
|
||||
new LegacyBeatmapExporter(LocalStorage)
|
||||
.ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
|
||||
});
|
||||
|
||||
AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
|
||||
AddAssert("timing point has truncated offset", () => beatmap.Beatmap.ControlPointInfo.TimingPoints[0].Time, () => Is.EqualTo(284).Within(0.001));
|
||||
AddAssert("kiai is snapped", () => beatmap.Beatmap.ControlPointInfo.EffectPoints[0].Time, () => Is.EqualTo(28519).Within(0.001));
|
||||
AddAssert("hit object is snapped", () => beatmap.Beatmap.HitObjects[0].StartTime, () => Is.EqualTo(28519).Within(0.001));
|
||||
}
|
||||
|
||||
private IWorkingBeatmap importBeatmapFromStream(Stream stream)
|
||||
{
|
||||
var imported = beatmaps.Import(new ImportTask(stream, "filename.osz")).GetResultSafely();
|
||||
return imported.AsNonNull().PerformRead(s => beatmaps.GetWorkingBeatmap(s.Beatmaps[0]));
|
||||
}
|
||||
|
||||
private IWorkingBeatmap importBeatmapFromArchives(string filename)
|
||||
{
|
||||
var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();
|
||||
return imported.AsNonNull().PerformRead(s => beatmaps.GetWorkingBeatmap(s.Beatmaps[0]));
|
||||
}
|
||||
}
|
||||
}
|
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -148,6 +148,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
[TestCase("tags too", false)]
|
||||
[TestCase("version", false)]
|
||||
[TestCase("an auteur", true)]
|
||||
[TestCase("unit", false)]
|
||||
public void TestCriteriaMatchingTerms(string terms, bool filtered)
|
||||
{
|
||||
var exampleBeatmapInfo = getExampleBeatmap();
|
||||
@ -175,6 +176,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
[TestCase("\"Artist\"!", true)]
|
||||
[TestCase("\"The Artist\"!", false)]
|
||||
[TestCase("\"the artist\"!", false)]
|
||||
[TestCase("\"unit tests\"!", false)]
|
||||
[TestCase("\"\\\"", true)] // nasty case, covers properly escaping user input in underlying regex.
|
||||
public void TestCriteriaMatchingExactTerms(string terms, bool filtered)
|
||||
{
|
||||
|
@ -501,6 +501,18 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
Assert.That(visibleBeatmaps, Is.EqualTo(expectedBeatmapIndexes));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplySourceQueries()
|
||||
{
|
||||
const string query = "find me songs with source=\"unit tests\" please";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("find me songs with please", filterCriteria.SearchText.Trim());
|
||||
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
|
||||
Assert.AreEqual("unit tests", filterCriteria.Source.SearchTerm);
|
||||
Assert.That(filterCriteria.Source.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase));
|
||||
}
|
||||
|
||||
private class CustomFilterCriteria : IRulesetFilterCriteria
|
||||
{
|
||||
public string? CustomValue { get; set; }
|
||||
|
BIN
osu.Game.Tests/Resources/Archives/decimal-timing-beatmap.olz
Normal file
BIN
osu.Game.Tests/Resources/Archives/decimal-timing-beatmap.olz
Normal file
Binary file not shown.
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:
|
@ -12,6 +12,7 @@ using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Rulesets;
|
||||
@ -205,7 +206,9 @@ namespace osu.Game.Tests.Visual.Collections
|
||||
|
||||
AddStep("click first delete button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(dialog.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().First(), new Vector2(5, 0));
|
||||
InputManager.MoveMouseTo(dialog
|
||||
.ChildrenOfType<DrawableCollectionListItem>().Single(i => i.Model.Value.Name == "1")
|
||||
.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().Single(), new Vector2(5, 0));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
@ -213,9 +216,11 @@ namespace osu.Game.Tests.Visual.Collections
|
||||
assertCollectionCount(1);
|
||||
assertCollectionName(0, "2");
|
||||
|
||||
AddStep("click first delete button", () =>
|
||||
AddStep("click second delete button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(dialog.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().First(), new Vector2(5, 0));
|
||||
InputManager.MoveMouseTo(dialog
|
||||
.ChildrenOfType<DrawableCollectionListItem>().Single(i => i.Model.Value.Name == "2")
|
||||
.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().Single(), new Vector2(5, 0));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
@ -310,7 +315,7 @@ namespace osu.Game.Tests.Visual.Collections
|
||||
|
||||
AddStep("focus first collection", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(firstItem = dialog.ChildrenOfType<DrawableCollectionListItem>().First());
|
||||
InputManager.MoveMouseTo(firstItem = dialog.ChildrenOfType<DrawableCollectionListItem>().Single(i => i.Model.Value.Name == "1"));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
@ -333,10 +338,44 @@ namespace osu.Game.Tests.Visual.Collections
|
||||
AddUntilStep("collection has new name", () => first.Name == "First");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSearch()
|
||||
{
|
||||
BeatmapCollection first = null!;
|
||||
|
||||
AddStep("add two collections", () =>
|
||||
{
|
||||
Realm.Write(r =>
|
||||
{
|
||||
r.Add(new[]
|
||||
{
|
||||
first = new BeatmapCollection(name: "1"),
|
||||
new BeatmapCollection(name: "2"),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
assertCollectionName(0, "1");
|
||||
assertCollectionName(1, "2");
|
||||
|
||||
AddStep("search for 1", () => dialog.ChildrenOfType<SearchTextBox>().Single().Current.Value = "1");
|
||||
|
||||
assertCollectionCount(1);
|
||||
|
||||
AddStep("change first collection name", () => Realm.Write(_ => first.Name = "First"));
|
||||
|
||||
assertCollectionCount(0);
|
||||
|
||||
AddStep("search for first", () => dialog.ChildrenOfType<SearchTextBox>().Single().Current.Value = "firs");
|
||||
|
||||
assertCollectionCount(1);
|
||||
}
|
||||
|
||||
private void assertCollectionCount(int count)
|
||||
=> AddUntilStep($"{count} collections shown", () => dialog.ChildrenOfType<DrawableCollectionListItem>().Count() == count + 1); // +1 for placeholder
|
||||
=> AddUntilStep($"{count} collections shown", () => dialog.ChildrenOfType<DrawableCollectionListItem>().Count(i => i.IsPresent) == count + 1); // +1 for placeholder
|
||||
|
||||
private void assertCollectionName(int index, string name)
|
||||
=> AddUntilStep($"item {index + 1} has correct name", () => dialog.ChildrenOfType<DrawableCollectionListItem>().ElementAt(index).ChildrenOfType<TextBox>().First().Text == name);
|
||||
=> AddUntilStep($"item {index + 1} has correct name",
|
||||
() => dialog.ChildrenOfType<DrawableCollectionList>().Single().OrderedItems.ElementAt(index).ChildrenOfType<TextBox>().First().Text == name);
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
Add(metadataClient);
|
||||
|
||||
// add button to observe for daily challenge changes and perform its logic.
|
||||
Add(new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D));
|
||||
Add(new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -19,6 +19,7 @@ using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Edit;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||
@ -82,6 +83,45 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlacementOutsideComposeScreen()
|
||||
{
|
||||
AddStep("clear all control points and hitobjects", () =>
|
||||
{
|
||||
editorBeatmap.ControlPointInfo.Clear();
|
||||
editorBeatmap.Clear();
|
||||
});
|
||||
|
||||
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
|
||||
AddStep("select circle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").TriggerClick());
|
||||
AddStep("move mouse to compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType<HitObjectContainer>().Single()));
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("circle placed", () => editorBeatmap.HitObjects.Count == 1);
|
||||
|
||||
AddStep("move mouse outside compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType<HitObjectContainer>().Single().ScreenSpaceDrawQuad.TopLeft - new Vector2(0f, 20f)));
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("no circle placed", () => editorBeatmap.HitObjects.Count == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDragSliderOutsideComposeScreen()
|
||||
{
|
||||
AddStep("clear all control points and hitobjects", () =>
|
||||
{
|
||||
editorBeatmap.ControlPointInfo.Clear();
|
||||
editorBeatmap.Clear();
|
||||
});
|
||||
|
||||
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
|
||||
AddStep("select slider", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "Slider").TriggerClick());
|
||||
|
||||
AddStep("move mouse to compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType<HitObjectContainer>().Single()));
|
||||
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
|
||||
AddStep("move mouse outside compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType<HitObjectContainer>().Single().ScreenSpaceDrawQuad.TopLeft - new Vector2(0f, 80f)));
|
||||
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
AddAssert("slider placed", () => editorBeatmap.HitObjects.Count == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlacementOnlyWorksWithTiming()
|
||||
{
|
||||
|
@ -1,14 +1,18 @@
|
||||
// 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.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play.PlayerSettings;
|
||||
using osu.Game.Tests.Resources;
|
||||
@ -52,6 +56,39 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNotEnoughTimedHitEvents()
|
||||
{
|
||||
AddStep("Set short reference score", () =>
|
||||
{
|
||||
List<HitEvent> hitEvents =
|
||||
[
|
||||
// 10 events total. one of them (head circle) being timed / having hitwindows, rest having no hitwindows
|
||||
new HitEvent(30, 1, HitResult.LargeTickHit, new SliderHeadCircle { ClassicSliderBehaviour = true }, null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
];
|
||||
|
||||
foreach (var ev in hitEvents)
|
||||
ev.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
offsetControl.ReferenceScore.Value = new ScoreInfo
|
||||
{
|
||||
HitEvents = hitEvents,
|
||||
BeatmapInfo = Beatmap.Value.BeatmapInfo,
|
||||
};
|
||||
});
|
||||
|
||||
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScoreFromDifferentBeatmap()
|
||||
{
|
||||
|
@ -155,7 +155,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
var api = (DummyAPIAccess)API;
|
||||
|
||||
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;
|
||||
|
@ -523,7 +523,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddUntilStep("restart completed", () => getCurrentPlayer() != null && getCurrentPlayer() != previousPlayer);
|
||||
AddStep("release quick retry key", () => InputManager.ReleaseKey(Key.Tilde));
|
||||
|
||||
AddUntilStep("wait for player", () => getCurrentPlayer()?.LoadState == LoadState.Ready);
|
||||
AddUntilStep("wait for player", () => getCurrentPlayer()?.LoadState >= LoadState.Ready);
|
||||
|
||||
AddUntilStep("time reached zero", () => getCurrentPlayer()?.GameplayClockContainer.CurrentTime > 0);
|
||||
AddUntilStep("skip button not visible", () => !checkSkipButtonVisible());
|
||||
|
@ -223,7 +223,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
protected partial class OutroPlayer : TestPlayer
|
||||
{
|
||||
public void ExitViaPause() => PerformExit(true);
|
||||
public void ExitViaPause() => PerformExitWithConfirmation();
|
||||
|
||||
public new FailOverlay FailOverlay => base.FailOverlay;
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
@ -15,6 +16,7 @@ using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays.Mods;
|
||||
@ -42,6 +44,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
private Live<BeatmapSetInfo> importedBeatmapSet;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager configManager { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
{
|
||||
@ -57,10 +62,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Add(detachedBeatmapStore);
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
private void setUp()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("reset", () =>
|
||||
{
|
||||
Ruleset.Value = new OsuRuleset().RulesetInfo;
|
||||
@ -75,6 +78,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestSelectFreeMods()
|
||||
{
|
||||
setUp();
|
||||
|
||||
AddStep("set some freemods", () => songSelect.FreeMods.Value = new OsuRuleset().GetModsFor(ModType.Fun).ToArray());
|
||||
AddStep("set all freemods", () => songSelect.FreeMods.Value = new OsuRuleset().CreateAllMods().ToArray());
|
||||
AddStep("set no freemods", () => songSelect.FreeMods.Value = Array.Empty<Mod>());
|
||||
@ -85,6 +90,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
BeatmapInfo selectedBeatmap = null;
|
||||
|
||||
setUp();
|
||||
|
||||
AddStep("change ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo);
|
||||
AddStep("select beatmap",
|
||||
() => songSelect.Carousel.SelectBeatmap(selectedBeatmap = beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == new TaikoRuleset().LegacyID)));
|
||||
@ -107,6 +114,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[TestCase(typeof(OsuModHidden), typeof(OsuModTraceable))] // Incompatible.
|
||||
public void TestAllowedModDeselectedWhenRequired(Type allowedMod, Type requiredMod)
|
||||
{
|
||||
setUp();
|
||||
|
||||
AddStep("change ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo);
|
||||
AddStep($"select {allowedMod.ReadableName()} as allowed", () => songSelect.FreeMods.Value = new[] { (Mod)Activator.CreateInstance(allowedMod) });
|
||||
AddStep($"select {requiredMod.ReadableName()} as required", () => songSelect.Mods.Value = new[] { (Mod)Activator.CreateInstance(requiredMod) });
|
||||
@ -120,6 +129,30 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
assertFreeModNotShown(requiredMod);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeRulesetImmediatelyAfterLoadComplete()
|
||||
{
|
||||
AddStep("reset", () =>
|
||||
{
|
||||
configManager.SetValue(OsuSetting.ShowConvertedBeatmaps, false);
|
||||
Beatmap.SetDefault();
|
||||
SelectedMods.SetDefault();
|
||||
});
|
||||
|
||||
AddStep("create song select", () =>
|
||||
{
|
||||
SelectedRoom.Value.Playlist.Single().RulesetID = 2;
|
||||
songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value, SelectedRoom.Value.Playlist.Single());
|
||||
songSelect.OnLoadComplete += _ => Ruleset.Value = new TaikoRuleset().RulesetInfo;
|
||||
LoadScreen(songSelect);
|
||||
});
|
||||
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded);
|
||||
|
||||
AddStep("confirm selection", () => songSelect.FinaliseSelection());
|
||||
AddAssert("beatmap is taiko", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(1));
|
||||
AddAssert("ruleset is taiko", () => Ruleset.Value.OnlineID, () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
private void assertFreeModNotShown(Type type)
|
||||
{
|
||||
AddAssert($"{type.ReadableName()} not displayed in freemod overlay",
|
||||
@ -138,8 +171,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
public new BeatmapCarousel Carousel => base.Carousel;
|
||||
|
||||
public TestMultiplayerMatchSongSelect(Room room)
|
||||
: base(room)
|
||||
public TestMultiplayerMatchSongSelect(Room room, [CanBeNull] PlaylistItem itemToEdit = null)
|
||||
: base(room, itemToEdit)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -30,14 +30,20 @@ namespace osu.Game.Tests.Visual.Online
|
||||
if (supportLevel > 3)
|
||||
supportLevel = 0;
|
||||
|
||||
((DummyAPIAccess)API).Friends.Add(new APIUser
|
||||
((DummyAPIAccess)API).Friends.Add(new APIRelation
|
||||
{
|
||||
Username = @"peppy",
|
||||
Id = 2,
|
||||
Colour = "99EB47",
|
||||
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
IsSupporter = supportLevel > 0,
|
||||
SupportLevel = supportLevel
|
||||
TargetID = 2,
|
||||
RelationType = RelationType.Friend,
|
||||
Mutual = true,
|
||||
TargetUser = new APIUser
|
||||
{
|
||||
Username = @"peppy",
|
||||
Id = 2,
|
||||
Colour = "99EB47",
|
||||
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
IsSupporter = supportLevel > 0,
|
||||
SupportLevel = supportLevel
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -40,15 +40,13 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
AddStep("import score", () =>
|
||||
{
|
||||
using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
|
||||
{
|
||||
var importTask = new ImportTask(resourceStream, "replay.osr");
|
||||
var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr");
|
||||
var importTask = new ImportTask(resourceStream, "replay.osr");
|
||||
|
||||
Game.ScoreManager.Import(new[] { importTask });
|
||||
}
|
||||
Game.ScoreManager.Import(new[] { importTask });
|
||||
});
|
||||
|
||||
AddUntilStep("Replay missing notification show", () => Game.Notifications.ChildrenOfType<MissingBeatmapNotification>().Any());
|
||||
AddUntilStep("Replay missing notification shown", () => Game.Notifications.ChildrenOfType<MissingBeatmapNotification>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -58,15 +56,13 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
AddStep("import score", () =>
|
||||
{
|
||||
using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
|
||||
{
|
||||
var importTask = new ImportTask(resourceStream, "replay.osr");
|
||||
var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr");
|
||||
var importTask = new ImportTask(resourceStream, "replay.osr");
|
||||
|
||||
Game.ScoreManager.Import(new[] { importTask });
|
||||
}
|
||||
Game.ScoreManager.Import(new[] { importTask });
|
||||
});
|
||||
|
||||
AddUntilStep("Replay missing notification not show", () => !Game.Notifications.ChildrenOfType<MissingBeatmapNotification>().Any());
|
||||
AddUntilStep("Replay missing notification not shown", () => !Game.Notifications.ChildrenOfType<MissingBeatmapNotification>().Any());
|
||||
}
|
||||
|
||||
private void setupBeatmapResponse(APIBeatmap b)
|
||||
|
@ -55,21 +55,21 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
Username = @"flyte",
|
||||
Id = 3103765,
|
||||
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
|
||||
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
|
||||
}),
|
||||
new UserBrickPanel(new APIUser
|
||||
{
|
||||
Username = @"peppy",
|
||||
Id = 2,
|
||||
Colour = "99EB47",
|
||||
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
|
||||
}),
|
||||
new UserGridPanel(new APIUser
|
||||
{
|
||||
Username = @"flyte",
|
||||
Id = 3103765,
|
||||
CountryCode = CountryCode.JP,
|
||||
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg",
|
||||
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
|
||||
Status = { Value = UserStatus.Online }
|
||||
}) { Width = 300 },
|
||||
boundPanel1 = new UserGridPanel(new APIUser
|
||||
@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Username = @"peppy",
|
||||
Id = 2,
|
||||
CountryCode = CountryCode.AU,
|
||||
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
|
||||
IsSupporter = true,
|
||||
SupportLevel = 3,
|
||||
}) { Width = 300 },
|
||||
@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Username = @"flyte",
|
||||
Id = 3103765,
|
||||
CountryCode = CountryCode.JP,
|
||||
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg",
|
||||
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
|
||||
Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 }
|
||||
}) { Width = 300 },
|
||||
new UserRankPanel(new APIUser
|
||||
@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Id = 2,
|
||||
Colour = "99EB47",
|
||||
CountryCode = CountryCode.AU,
|
||||
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
|
||||
Statistics = new UserStatistics { GlobalRank = null, CountryRank = null }
|
||||
}) { Width = 300 }
|
||||
}
|
||||
@ -168,6 +168,14 @@ namespace osu.Game.Tests.Visual.Online
|
||||
CountryRank = RNG.Next(100000)
|
||||
});
|
||||
});
|
||||
AddStep("set statistics to something big", () =>
|
||||
{
|
||||
API.UpdateStatistics(new UserStatistics
|
||||
{
|
||||
GlobalRank = RNG.Next(1_000_000, 100_000_000),
|
||||
CountryRank = RNG.Next(1_000_000, 100_000_000)
|
||||
});
|
||||
});
|
||||
AddStep("set statistics to empty", () =>
|
||||
{
|
||||
API.UpdateStatistics(new UserStatistics());
|
||||
|
@ -3,15 +3,20 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Profile;
|
||||
using osu.Game.Overlays.Profile.Header.Components;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Users;
|
||||
|
||||
@ -22,6 +27,10 @@ namespace osu.Game.Tests.Visual.Online
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
|
||||
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
|
||||
private readonly ManualResetEventSlim requestLock = new ManualResetEventSlim();
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager configManager { get; set; } = null!;
|
||||
|
||||
@ -400,5 +409,97 @@ namespace osu.Game.Tests.Visual.Online
|
||||
}
|
||||
}, 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,13 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
AddSliderStep("height", 0.0f, 1000.0f, height.Value, height.Set);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestZeroEvents()
|
||||
{
|
||||
createTest(new List<HitEvent>());
|
||||
AddStep("update offset", () => graph.UpdateOffset(10));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManyDistributedEventsOffset()
|
||||
{
|
||||
|
@ -414,11 +414,7 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
});
|
||||
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre));
|
||||
scrollToAndStartBinding("Left (centre)");
|
||||
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();
|
||||
});
|
||||
clearBinding();
|
||||
scrollToAndStartBinding("Left (rim)");
|
||||
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);
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
AddAssert($"Check {name} is bound to {keyName}", () =>
|
||||
@ -442,23 +477,23 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
}, () => 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}", () =>
|
||||
{
|
||||
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);
|
||||
|
||||
AddStep("click to bind", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(firstButton);
|
||||
InputManager.MoveMouseTo(targetButton);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
}
|
||||
|
@ -1,12 +1,9 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
@ -23,7 +20,6 @@ using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
@ -35,15 +31,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[TestFixture]
|
||||
public partial class TestSceneBeatmapInfoWedge : OsuTestScene
|
||||
{
|
||||
private RulesetStore rulesets;
|
||||
private TestBeatmapInfoWedge infoWedge;
|
||||
private readonly List<IBeatmap> beatmaps = new List<IBeatmap>();
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RulesetStore rulesets)
|
||||
{
|
||||
this.rulesets = rulesets;
|
||||
}
|
||||
private TestBeatmapInfoWedge infoWedge = null!;
|
||||
private readonly List<IBeatmap> beatmaps = new List<IBeatmap>();
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
@ -156,7 +148,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
|
||||
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm });
|
||||
|
||||
OsuModDoubleTime doubleTime = null;
|
||||
OsuModDoubleTime doubleTime = null!;
|
||||
|
||||
selectBeatmap(beatmap);
|
||||
checkDisplayedBPM($"{bpm}");
|
||||
@ -173,7 +165,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[TestCase(120, 120.4, null, "120")]
|
||||
[TestCase(120, 120.6, "DT", "180-182 (mostly 180)")]
|
||||
[TestCase(120, 120.4, "DT", "180")]
|
||||
public void TestVaryingBPM(double commonBpm, double otherBpm, string mod, string expectedDisplay)
|
||||
public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay)
|
||||
{
|
||||
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
|
||||
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
|
||||
@ -203,7 +195,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
double drain = beatmap.CalculateDrainLength();
|
||||
beatmap.BeatmapInfo.Length = drain;
|
||||
|
||||
OsuModDoubleTime doubleTime = null;
|
||||
OsuModDoubleTime doubleTime = null!;
|
||||
|
||||
selectBeatmap(beatmap);
|
||||
checkDisplayedLength(drain);
|
||||
@ -221,14 +213,15 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
AddUntilStep($"check map drain ({displayedLength})", () =>
|
||||
{
|
||||
var label = infoWedge.DisplayedContent.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsTotalLength(displayedLength));
|
||||
var label = infoWedge.DisplayedContent.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>()
|
||||
.Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsTotalLength(displayedLength));
|
||||
return label.Statistic.Content == displayedLength.ToString();
|
||||
});
|
||||
}
|
||||
|
||||
private void setRuleset(RulesetInfo rulesetInfo)
|
||||
{
|
||||
Container containerBefore = null;
|
||||
Container? containerBefore = null;
|
||||
|
||||
AddStep("set ruleset", () =>
|
||||
{
|
||||
@ -242,9 +235,9 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
|
||||
}
|
||||
|
||||
private void selectBeatmap([CanBeNull] IBeatmap b)
|
||||
private void selectBeatmap(IBeatmap? b)
|
||||
{
|
||||
Container containerBefore = null;
|
||||
Container? containerBefore = null;
|
||||
|
||||
AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
|
||||
{
|
||||
@ -307,11 +300,6 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
public new WedgeInfoText Info => base.Info;
|
||||
}
|
||||
|
||||
private class TestHitObject : ConvertHitObject, IHasPosition
|
||||
{
|
||||
public float X => 0;
|
||||
public float Y => 0;
|
||||
public Vector2 Position { get; } = Vector2.Zero;
|
||||
}
|
||||
private class TestHitObject : ConvertHitObject;
|
||||
}
|
||||
}
|
||||
|
@ -10,9 +10,12 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
@ -191,8 +194,39 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
AddStep("present beatmap", () => Game.PresentBeatmap(getImport()));
|
||||
|
||||
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect);
|
||||
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded);
|
||||
AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.MatchesOnlineID(getImport().Beatmaps[expectedDiff - 1]));
|
||||
}
|
||||
|
||||
protected override TestOsuGame CreateTestGame() => new NoBeatmapUpdateGame(LocalStorage, API);
|
||||
|
||||
private partial class NoBeatmapUpdateGame : TestOsuGame
|
||||
{
|
||||
public NoBeatmapUpdateGame(Storage storage, IAPIProvider api, string[] args = null)
|
||||
: base(storage, api, args)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IBeatmapUpdater CreateBeatmapUpdater() => new TestBeatmapUpdater();
|
||||
|
||||
private class TestBeatmapUpdater : IBeatmapUpdater
|
||||
{
|
||||
public void Queue(Live<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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,9 +14,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Screens.Select;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
@ -209,11 +207,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
public new WedgeInfoText? Info => base.Info;
|
||||
}
|
||||
|
||||
private class TestHitObject : ConvertHitObject, IHasPosition
|
||||
{
|
||||
public float X => 0;
|
||||
public float Y => 0;
|
||||
public Vector2 Position { get; } = Vector2.Zero;
|
||||
}
|
||||
private class TestHitObject : ConvertHitObject;
|
||||
}
|
||||
}
|
||||
|
@ -159,6 +159,23 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddUntilStep("check star rating is 2", getText, () => Is.EqualTo("Star Rating: 2.00"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMaxPp()
|
||||
{
|
||||
AddStep("set test ruleset", () => Ruleset.Value = new TestRuleset().RulesetInfo);
|
||||
AddStep("set max pp attribute", () => text.Attribute.Value = BeatmapAttribute.MaxPP);
|
||||
AddAssert("check max pp is 0", getText, () => Is.EqualTo("Max PP: 0"));
|
||||
|
||||
// Adding mod
|
||||
TestMod mod = null!;
|
||||
AddStep("add mod with pp 1", () => SelectedMods.Value = new[] { mod = new TestMod { Performance = { Value = 1 } } });
|
||||
AddUntilStep("check max pp is 1", getText, () => Is.EqualTo("Max PP: 1"));
|
||||
|
||||
// Changing mod setting
|
||||
AddStep("change mod pp to 2", () => mod.Performance.Value = 2);
|
||||
AddUntilStep("check max pp is 2", getText, () => Is.EqualTo("Max PP: 2"));
|
||||
}
|
||||
|
||||
private string getText() => text.ChildrenOfType<SpriteText>().Single().Text.ToString();
|
||||
|
||||
private class TestRuleset : Ruleset
|
||||
|
@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
break;
|
||||
|
||||
case Key.Q:
|
||||
buttons.OnExit = action;
|
||||
buttons.OnExit = _ => action();
|
||||
break;
|
||||
|
||||
case Key.O:
|
||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
public void TestStandardButton()
|
||||
{
|
||||
AddStep("add button", () => Child = new MainMenuButton(
|
||||
ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => { }, 0, Key.P)
|
||||
ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.P)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)],
|
||||
Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D)
|
||||
Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -161,7 +161,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)],
|
||||
Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D)
|
||||
Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
|
1
osu.Game/.idea/.idea.osu.dir/.idea/.name
Normal file
1
osu.Game/.idea/.idea.osu.dir/.idea/.name
Normal file
@ -0,0 +1 @@
|
||||
osu
|
8
osu.Game/.idea/.idea.osu.dir/.idea/indexLayout.xml
Normal file
8
osu.Game/.idea/.idea.osu.dir/.idea/indexLayout.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
</project>
|
6
osu.Game/.idea/.idea.osu.dir/.idea/vcs.xml
Normal file
6
osu.Game/.idea/.idea.osu.dir/.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
22
osu.Game/.idea/.idea.osu.dir/.idea/workspace.xml
Normal file
22
osu.Game/.idea/.idea.osu.dir/.idea/workspace.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="0959115c-c7d5-4a9d-b138-d34f47e6713a" name="Changes" comment="" />
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo"><![CDATA[{
|
||||
"associatedIndex": 1
|
||||
}]]></component>
|
||||
<component name="ProjectId" id="2p6efoueruThVmDy8XXhD8Ibwab" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="autoscrollFromSource" value="true" />
|
||||
<option name="autoscrollToSource" value="true" />
|
||||
<option name="flattenPackages" value="true" />
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||
</project>
|
@ -4,12 +4,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Threading;
|
||||
@ -18,7 +21,11 @@ using osu.Game.Database;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Storyboards;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
@ -237,10 +244,37 @@ namespace osu.Game.Beatmaps
|
||||
var ruleset = rulesetInfo.CreateInstance();
|
||||
Debug.Assert(ruleset != null);
|
||||
|
||||
var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo));
|
||||
var attributes = calculator.Calculate(key.OrderedMods, cancellationToken);
|
||||
PlayableCachedWorkingBeatmap workingBeatmap = new PlayableCachedWorkingBeatmap(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo));
|
||||
IBeatmap playableBeatmap = workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo, key.OrderedMods, cancellationToken);
|
||||
|
||||
return new StarDifficulty(attributes);
|
||||
var difficulty = ruleset.CreateDifficultyCalculator(workingBeatmap).Calculate(key.OrderedMods, cancellationToken);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var performanceCalculator = ruleset.CreatePerformanceCalculator();
|
||||
if (performanceCalculator == null)
|
||||
return new StarDifficulty(difficulty, new PerformanceAttributes());
|
||||
|
||||
ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
|
||||
scoreProcessor.Mods.Value = key.OrderedMods;
|
||||
scoreProcessor.ApplyBeatmap(playableBeatmap);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
ScoreInfo perfectScore = new ScoreInfo(key.BeatmapInfo, ruleset.RulesetInfo)
|
||||
{
|
||||
Passed = true,
|
||||
Accuracy = 1,
|
||||
Mods = key.OrderedMods,
|
||||
MaxCombo = scoreProcessor.MaximumCombo,
|
||||
Combo = scoreProcessor.MaximumCombo,
|
||||
TotalScore = scoreProcessor.MaximumTotalScore,
|
||||
Statistics = scoreProcessor.MaximumStatistics,
|
||||
MaximumStatistics = scoreProcessor.MaximumStatistics
|
||||
};
|
||||
|
||||
var performance = performanceCalculator.Calculate(perfectScore, difficulty);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
return new StarDifficulty(difficulty, performance);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@ -276,7 +310,6 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
public readonly BeatmapInfo BeatmapInfo;
|
||||
public readonly RulesetInfo Ruleset;
|
||||
|
||||
public readonly Mod[] OrderedMods;
|
||||
|
||||
public DifficultyCacheLookup(BeatmapInfo beatmapInfo, RulesetInfo? ruleset, IEnumerable<Mod>? mods)
|
||||
@ -317,5 +350,42 @@ namespace osu.Game.Beatmaps
|
||||
CancellationToken = cancellationToken;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A working beatmap that caches its playable representation.
|
||||
/// This is intended as single-use for when it is guaranteed that the playable beatmap can be reused.
|
||||
/// </summary>
|
||||
private class PlayableCachedWorkingBeatmap : IWorkingBeatmap
|
||||
{
|
||||
private readonly IWorkingBeatmap working;
|
||||
private IBeatmap? playable;
|
||||
|
||||
public PlayableCachedWorkingBeatmap(IWorkingBeatmap working)
|
||||
{
|
||||
this.working = working;
|
||||
}
|
||||
|
||||
public IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList<Mod> mods)
|
||||
=> playable ??= working.GetPlayableBeatmap(ruleset, mods);
|
||||
|
||||
public IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList<Mod> mods, CancellationToken cancellationToken)
|
||||
=> playable ??= working.GetPlayableBeatmap(ruleset, mods, cancellationToken);
|
||||
|
||||
IBeatmapInfo IWorkingBeatmap.BeatmapInfo => working.BeatmapInfo;
|
||||
bool IWorkingBeatmap.BeatmapLoaded => working.BeatmapLoaded;
|
||||
bool IWorkingBeatmap.TrackLoaded => working.TrackLoaded;
|
||||
IBeatmap IWorkingBeatmap.Beatmap => working.Beatmap;
|
||||
Texture IWorkingBeatmap.GetBackground() => working.GetBackground();
|
||||
Texture IWorkingBeatmap.GetPanelBackground() => working.GetPanelBackground();
|
||||
Waveform IWorkingBeatmap.Waveform => working.Waveform;
|
||||
Storyboard IWorkingBeatmap.Storyboard => working.Storyboard;
|
||||
ISkin IWorkingBeatmap.Skin => working.Skin;
|
||||
Track IWorkingBeatmap.Track => working.Track;
|
||||
Track IWorkingBeatmap.LoadTrack() => working.LoadTrack();
|
||||
Stream IWorkingBeatmap.GetStream(string storagePath) => working.GetStream(storagePath);
|
||||
void IWorkingBeatmap.BeginAsyncLoad() => working.BeginAsyncLoad();
|
||||
void IWorkingBeatmap.CancelAsyncLoad() => working.CancelAsyncLoad();
|
||||
void IWorkingBeatmap.PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint) => working.PrepareTrackForPreview(looping, offsetFromPreviewPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,11 +13,11 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
public partial class BeatmapOnlineChangeIngest : Component
|
||||
{
|
||||
private readonly BeatmapUpdater beatmapUpdater;
|
||||
private readonly IBeatmapUpdater beatmapUpdater;
|
||||
private readonly RealmAccess realm;
|
||||
private readonly MetadataClient metadataClient;
|
||||
|
||||
public BeatmapOnlineChangeIngest(BeatmapUpdater beatmapUpdater, RealmAccess realm, MetadataClient metadataClient)
|
||||
public BeatmapOnlineChangeIngest(IBeatmapUpdater beatmapUpdater, RealmAccess realm, MetadataClient metadataClient)
|
||||
{
|
||||
this.beatmapUpdater = beatmapUpdater;
|
||||
this.realm = realm;
|
||||
|
@ -1,7 +1,6 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@ -15,10 +14,7 @@ using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles all processing required to ensure a local beatmap is in a consistent state with any changes.
|
||||
/// </summary>
|
||||
public class BeatmapUpdater : IDisposable
|
||||
public class BeatmapUpdater : IBeatmapUpdater
|
||||
{
|
||||
private readonly IWorkingBeatmapCache workingBeatmapCache;
|
||||
|
||||
@ -38,11 +34,6 @@ namespace osu.Game.Beatmaps
|
||||
metadataLookup = new BeatmapUpdaterMetadataLookup(api, storage);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
Logger.Log($"Queueing change for local beatmap {beatmapSet}");
|
||||
@ -50,55 +41,56 @@ namespace osu.Game.Beatmaps
|
||||
updateScheduler);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapSet.Realm!.Write(_ =>
|
||||
public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
|
||||
{
|
||||
// Before we use below, we want to invalidate.
|
||||
workingBeatmapCache.Invalidate(beatmapSet);
|
||||
|
||||
if (lookupScope != MetadataLookupScope.None)
|
||||
metadataLookup.Update(beatmapSet, lookupScope == MetadataLookupScope.OnlineFirst);
|
||||
|
||||
foreach (var beatmap in beatmapSet.Beatmaps)
|
||||
beatmapSet.Realm!.Write(_ =>
|
||||
{
|
||||
difficultyCache.Invalidate(beatmap);
|
||||
// Before we use below, we want to invalidate.
|
||||
workingBeatmapCache.Invalidate(beatmapSet);
|
||||
|
||||
var working = workingBeatmapCache.GetWorkingBeatmap(beatmap);
|
||||
var ruleset = working.BeatmapInfo.Ruleset.CreateInstance();
|
||||
if (lookupScope != MetadataLookupScope.None)
|
||||
metadataLookup.Update(beatmapSet, lookupScope == MetadataLookupScope.OnlineFirst);
|
||||
|
||||
Debug.Assert(ruleset != null);
|
||||
foreach (var beatmap in beatmapSet.Beatmaps)
|
||||
{
|
||||
difficultyCache.Invalidate(beatmap);
|
||||
|
||||
var calculator = ruleset.CreateDifficultyCalculator(working);
|
||||
var working = workingBeatmapCache.GetWorkingBeatmap(beatmap);
|
||||
var ruleset = working.BeatmapInfo.Ruleset.CreateInstance();
|
||||
|
||||
beatmap.StarRating = calculator.Calculate().StarRating;
|
||||
beatmap.Length = working.Beatmap.CalculatePlayableLength();
|
||||
beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength();
|
||||
beatmap.EndTimeObjectCount = working.Beatmap.HitObjects.Count(h => h is IHasDuration);
|
||||
beatmap.TotalObjectCount = working.Beatmap.HitObjects.Count;
|
||||
}
|
||||
Debug.Assert(ruleset != null);
|
||||
|
||||
// And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
|
||||
workingBeatmapCache.Invalidate(beatmapSet);
|
||||
});
|
||||
var calculator = ruleset.CreateDifficultyCalculator(working);
|
||||
|
||||
public void ProcessObjectCounts(BeatmapInfo beatmapInfo, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapInfo.Realm!.Write(_ =>
|
||||
beatmap.StarRating = calculator.Calculate().StarRating;
|
||||
beatmap.Length = working.Beatmap.CalculatePlayableLength();
|
||||
beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength();
|
||||
beatmap.EndTimeObjectCount = working.Beatmap.HitObjects.Count(h => h is IHasDuration);
|
||||
beatmap.TotalObjectCount = working.Beatmap.HitObjects.Count;
|
||||
}
|
||||
|
||||
// And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
|
||||
workingBeatmapCache.Invalidate(beatmapSet);
|
||||
});
|
||||
}
|
||||
|
||||
public void ProcessObjectCounts(BeatmapInfo beatmapInfo, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
|
||||
{
|
||||
// Before we use below, we want to invalidate.
|
||||
workingBeatmapCache.Invalidate(beatmapInfo);
|
||||
beatmapInfo.Realm!.Write(_ =>
|
||||
{
|
||||
// Before we use below, we want to invalidate.
|
||||
workingBeatmapCache.Invalidate(beatmapInfo);
|
||||
|
||||
var working = workingBeatmapCache.GetWorkingBeatmap(beatmapInfo);
|
||||
var beatmap = working.Beatmap;
|
||||
var working = workingBeatmapCache.GetWorkingBeatmap(beatmapInfo);
|
||||
var beatmap = working.Beatmap;
|
||||
|
||||
beatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration);
|
||||
beatmapInfo.TotalObjectCount = beatmap.HitObjects.Count;
|
||||
beatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration);
|
||||
beatmapInfo.TotalObjectCount = beatmap.HitObjects.Count;
|
||||
|
||||
// And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
|
||||
workingBeatmapCache.Invalidate(beatmapInfo);
|
||||
});
|
||||
// And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
|
||||
workingBeatmapCache.Invalidate(beatmapInfo);
|
||||
});
|
||||
}
|
||||
|
||||
#region Implementation of IDisposable
|
||||
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets;
|
||||
@ -146,7 +147,8 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
approachRate.Text = @" AR: " + adjustedDifficulty.ApproachRate.ToString(@"0.##");
|
||||
overallDifficulty.Text = @" OD: " + adjustedDifficulty.OverallDifficulty.ToString(@"0.##");
|
||||
|
||||
length.Text = "Length: " + TimeSpan.FromMilliseconds(displayedContent.BeatmapInfo.Length / rate).ToString(@"mm\:ss");
|
||||
TimeSpan lengthTimeSpan = TimeSpan.FromMilliseconds(displayedContent.BeatmapInfo.Length / rate);
|
||||
length.Text = "Length: " + lengthTimeSpan.ToFormattedDuration();
|
||||
bpm.Text = " BPM: " + Math.Round(bpmAdjusted, 0);
|
||||
}
|
||||
|
||||
|
@ -38,8 +38,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
internal static RulesetStore? RulesetStore;
|
||||
|
||||
private Beatmap beatmap = null!;
|
||||
|
||||
private ConvertHitObjectParser? parser;
|
||||
private ConvertHitObjectParser parser = null!;
|
||||
|
||||
private LegacySampleBank defaultSampleBank;
|
||||
private int defaultSampleVolume = 100;
|
||||
@ -80,6 +79,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion;
|
||||
parser = new ConvertHitObjectParser(getOffsetTime(), FormatVersion);
|
||||
|
||||
applyLegacyDefaults(this.beatmap.BeatmapInfo);
|
||||
|
||||
@ -162,7 +162,8 @@ namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
if (hitObject is IHasRepeats hasRepeats)
|
||||
{
|
||||
SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.StartTime + CONTROL_POINT_LENIENCY + 1) ?? SampleControlPoint.DEFAULT;
|
||||
SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.StartTime + CONTROL_POINT_LENIENCY + 1)
|
||||
?? SampleControlPoint.DEFAULT;
|
||||
hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList();
|
||||
|
||||
for (int i = 0; i < hasRepeats.NodeSamples.Count; i++)
|
||||
@ -175,7 +176,8 @@ namespace osu.Game.Beatmaps.Formats
|
||||
}
|
||||
else
|
||||
{
|
||||
SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + CONTROL_POINT_LENIENCY) ?? SampleControlPoint.DEFAULT;
|
||||
SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + CONTROL_POINT_LENIENCY)
|
||||
?? SampleControlPoint.DEFAULT;
|
||||
hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList();
|
||||
}
|
||||
}
|
||||
@ -263,29 +265,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
break;
|
||||
|
||||
case @"Mode":
|
||||
int rulesetID = Parsing.ParseInt(pair.Value);
|
||||
|
||||
beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(rulesetID) ?? throw new ArgumentException("Ruleset is not available locally.");
|
||||
|
||||
switch (rulesetID)
|
||||
{
|
||||
case 0:
|
||||
parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
parser = new Rulesets.Objects.Legacy.Taiko.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
parser = new Rulesets.Objects.Legacy.Catch.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
|
||||
break;
|
||||
}
|
||||
|
||||
beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(Parsing.ParseInt(pair.Value)) ?? throw new ArgumentException("Ruleset is not available locally.");
|
||||
break;
|
||||
|
||||
case @"LetterboxInBreaks":
|
||||
@ -617,17 +597,10 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
private void handleHitObject(string line)
|
||||
{
|
||||
// If the ruleset wasn't specified, assume the osu!standard ruleset.
|
||||
parser ??= new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
|
||||
|
||||
var obj = parser.Parse(line);
|
||||
obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
||||
|
||||
if (obj != null)
|
||||
{
|
||||
obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
||||
|
||||
beatmap.HitObjects.Add(obj);
|
||||
}
|
||||
beatmap.HitObjects.Add(obj);
|
||||
}
|
||||
|
||||
private int getOffsetTime(int time) => time + (ApplyOffsets ? offset : 0);
|
||||
|
@ -183,7 +183,17 @@ namespace osu.Game.Beatmaps.Formats
|
||||
if (scrollSpeedEncodedAsSliderVelocity)
|
||||
{
|
||||
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();
|
||||
|
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);
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ using System;
|
||||
namespace osu.Game.Beatmaps.Legacy
|
||||
{
|
||||
[Flags]
|
||||
internal enum LegacyHitObjectType
|
||||
public enum LegacyHitObjectType
|
||||
{
|
||||
Circle = 1,
|
||||
Slider = 1 << 1,
|
||||
|
@ -1,9 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
|
||||
@ -25,30 +22,34 @@ namespace osu.Game.Beatmaps
|
||||
/// The difficulty attributes computed for the given beatmap.
|
||||
/// Might not be available if the star difficulty is associated with a beatmap that's not locally available.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public readonly DifficultyAttributes Attributes;
|
||||
public readonly DifficultyAttributes? DifficultyAttributes;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="StarDifficulty"/> structure based on <see cref="DifficultyAttributes"/> computed
|
||||
/// by a <see cref="DifficultyCalculator"/>.
|
||||
/// The performance attributes computed for a perfect score on the given beatmap.
|
||||
/// Might not be available if the star difficulty is associated with a beatmap that's not locally available.
|
||||
/// </summary>
|
||||
public StarDifficulty([NotNull] DifficultyAttributes attributes)
|
||||
public readonly PerformanceAttributes? PerformanceAttributes;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="StarDifficulty"/> structure.
|
||||
/// </summary>
|
||||
public StarDifficulty(DifficultyAttributes difficulty, PerformanceAttributes performance)
|
||||
{
|
||||
Stars = double.IsFinite(attributes.StarRating) ? attributes.StarRating : 0;
|
||||
MaxCombo = attributes.MaxCombo;
|
||||
Attributes = attributes;
|
||||
Stars = double.IsFinite(difficulty.StarRating) ? difficulty.StarRating : 0;
|
||||
MaxCombo = difficulty.MaxCombo;
|
||||
DifficultyAttributes = difficulty;
|
||||
PerformanceAttributes = performance;
|
||||
// Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="StarDifficulty"/> structure with a pre-populated star difficulty and max combo
|
||||
/// in scenarios where computing <see cref="DifficultyAttributes"/> is not feasible (i.e. when working with online sources).
|
||||
/// in scenarios where computing <see cref="Rulesets.Difficulty.DifficultyAttributes"/> is not feasible (i.e. when working with online sources).
|
||||
/// </summary>
|
||||
public StarDifficulty(double starDifficulty, int maxCombo)
|
||||
{
|
||||
Stars = double.IsFinite(starDifficulty) ? starDifficulty : 0;
|
||||
MaxCombo = maxCombo;
|
||||
Attributes = null;
|
||||
}
|
||||
|
||||
public DifficultyRating DifficultyRating => GetDifficultyRating(Stars);
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
@ -20,6 +21,12 @@ namespace osu.Game.Collections
|
||||
/// </summary>
|
||||
public partial class DrawableCollectionList : OsuRearrangeableListContainer<Live<BeatmapCollection>>
|
||||
{
|
||||
public new MarginPadding Padding
|
||||
{
|
||||
get => base.Padding;
|
||||
set => base.Padding = value;
|
||||
}
|
||||
|
||||
protected override ScrollContainer<Drawable> CreateScrollContainer() => scroll = new Scroll();
|
||||
|
||||
[Resolved]
|
||||
@ -29,7 +36,17 @@ namespace osu.Game.Collections
|
||||
|
||||
private IDisposable? realmSubscription;
|
||||
|
||||
protected override FillFlowContainer<RearrangeableListItem<Live<BeatmapCollection>>> CreateListFillFlowContainer() => new Flow
|
||||
private Flow flow = null!;
|
||||
|
||||
public IEnumerable<Drawable> OrderedItems => flow.FlowingChildren;
|
||||
|
||||
public string SearchTerm
|
||||
{
|
||||
get => flow.SearchTerm;
|
||||
set => flow.SearchTerm = value;
|
||||
}
|
||||
|
||||
protected override FillFlowContainer<RearrangeableListItem<Live<BeatmapCollection>>> CreateListFillFlowContainer() => flow = new Flow
|
||||
{
|
||||
DragActive = { BindTarget = DragActive }
|
||||
};
|
||||
@ -41,10 +58,51 @@ namespace osu.Game.Collections
|
||||
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapCollection>().OrderBy(c => c.Name), collectionsChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When non-null, signifies that a new collection was created and should be presented to the user.
|
||||
/// </summary>
|
||||
private Guid? lastCreated;
|
||||
|
||||
protected override void OnItemsChanged()
|
||||
{
|
||||
base.OnItemsChanged();
|
||||
|
||||
if (lastCreated != null)
|
||||
{
|
||||
var createdItem = flow.Children.SingleOrDefault(item => item.Model.Value.ID == lastCreated);
|
||||
|
||||
if (createdItem != null)
|
||||
scroll.ScrollTo(createdItem);
|
||||
|
||||
lastCreated = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void collectionsChanged(IRealmCollection<BeatmapCollection> collections, ChangeSet? changes)
|
||||
{
|
||||
Items.Clear();
|
||||
Items.AddRange(collections.AsEnumerable().Select(c => c.ToLive(realm)));
|
||||
if (changes == null)
|
||||
{
|
||||
Items.AddRange(collections.AsEnumerable().Select(c => c.ToLive(realm)));
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (int i in changes.DeletedIndices.OrderDescending())
|
||||
Items.RemoveAt(i);
|
||||
|
||||
foreach (int i in changes.InsertedIndices)
|
||||
Items.Insert(i, collections[i].ToLive(realm));
|
||||
|
||||
if (changes.InsertedIndices.Length == 1)
|
||||
lastCreated = collections[changes.InsertedIndices[0]].ID;
|
||||
|
||||
foreach (int i in changes.NewModifiedIndices)
|
||||
|
||||
{
|
||||
var updatedItem = collections[i];
|
||||
|
||||
Items.RemoveAt(i);
|
||||
Items.Insert(i, updatedItem.ToLive(realm));
|
||||
}
|
||||
}
|
||||
|
||||
protected override OsuRearrangeableListItem<Live<BeatmapCollection>> CreateOsuDrawable(Live<BeatmapCollection> item)
|
||||
@ -82,8 +140,7 @@ namespace osu.Game.Collections
|
||||
|
||||
public Scroll()
|
||||
{
|
||||
ScrollbarVisible = false;
|
||||
Padding = new MarginPadding(10);
|
||||
ScrollbarOverlapsContent = false;
|
||||
|
||||
base.Content.Add(new FillFlowContainer
|
||||
{
|
||||
@ -111,7 +168,7 @@ namespace osu.Game.Collections
|
||||
base.Update();
|
||||
|
||||
// AutoSizeAxes cannot be used as the height should represent the post-layout-transform height at all times, so that the placeholder doesn't bounce around.
|
||||
content.Height = ((Flow)Child).Children.Sum(c => c.DrawHeight + 5);
|
||||
content.Height = ((Flow)Child).Children.Sum(c => c.IsPresent ? c.DrawHeight + 5 : 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -123,16 +180,41 @@ namespace osu.Game.Collections
|
||||
var previous = PlaceholderItem;
|
||||
|
||||
placeholderContainer.Clear(false);
|
||||
placeholderContainer.Add(PlaceholderItem = new DrawableCollectionListItem(new BeatmapCollection().ToLiveUnmanaged(), false));
|
||||
placeholderContainer.Add(PlaceholderItem = new NewCollectionEntryItem());
|
||||
|
||||
return previous;
|
||||
}
|
||||
}
|
||||
|
||||
private partial class NewCollectionEntryItem : DrawableCollectionListItem
|
||||
{
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
public NewCollectionEntryItem()
|
||||
: base(new BeatmapCollection().ToLiveUnmanaged(), false)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
TextBox.OnCommit += (sender, newText) =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(TextBox.Text))
|
||||
return;
|
||||
|
||||
realm.Write(r => r.Add(new BeatmapCollection(TextBox.Text)));
|
||||
TextBox.Text = string.Empty;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The flow of <see cref="DrawableCollectionListItem"/>. Disables layout easing unless a drag is in progress.
|
||||
/// </summary>
|
||||
private partial class Flow : FillFlowContainer<RearrangeableListItem<Live<BeatmapCollection>>>
|
||||
private partial class Flow : SearchContainer<RearrangeableListItem<Live<BeatmapCollection>>>
|
||||
{
|
||||
public readonly IBindable<bool> DragActive = new Bindable<bool>();
|
||||
|
||||
@ -140,6 +222,8 @@ namespace osu.Game.Collections
|
||||
{
|
||||
Spacing = new Vector2(0, 5);
|
||||
LayoutEasing = Easing.OutQuint;
|
||||
|
||||
Padding = new MarginPadding { Right = 5 };
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
@ -10,6 +11,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
@ -23,11 +25,15 @@ namespace osu.Game.Collections
|
||||
/// <summary>
|
||||
/// Visualises a <see cref="BeatmapCollection"/> inside a <see cref="DrawableCollectionList"/>.
|
||||
/// </summary>
|
||||
public partial class DrawableCollectionListItem : OsuRearrangeableListItem<Live<BeatmapCollection>>
|
||||
public partial class DrawableCollectionListItem : OsuRearrangeableListItem<Live<BeatmapCollection>>, IFilterable
|
||||
{
|
||||
private const float item_height = 35;
|
||||
private const float button_width = item_height * 0.75f;
|
||||
|
||||
protected TextBox TextBox => content.TextBox;
|
||||
|
||||
private ItemContent content = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="DrawableCollectionListItem"/>.
|
||||
/// </summary>
|
||||
@ -48,7 +54,7 @@ namespace osu.Game.Collections
|
||||
CornerRadius = item_height / 2;
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent() => new ItemContent(Model);
|
||||
protected override Drawable CreateContent() => content = new ItemContent(Model);
|
||||
|
||||
/// <summary>
|
||||
/// The main content of the <see cref="DrawableCollectionListItem"/>.
|
||||
@ -57,10 +63,7 @@ namespace osu.Game.Collections
|
||||
{
|
||||
private readonly Live<BeatmapCollection> collection;
|
||||
|
||||
private ItemTextBox textBox = null!;
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
public ItemTextBox TextBox { get; private set; } = null!;
|
||||
|
||||
public ItemContent(Live<BeatmapCollection> collection)
|
||||
{
|
||||
@ -80,7 +83,7 @@ namespace osu.Game.Collections
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
IsTextBoxHovered = v => textBox.ReceivePositionalInputAt(v)
|
||||
IsTextBoxHovered = v => TextBox.ReceivePositionalInputAt(v)
|
||||
}
|
||||
: Empty(),
|
||||
new Container
|
||||
@ -89,7 +92,7 @@ namespace osu.Game.Collections
|
||||
Padding = new MarginPadding { Right = collection.IsManaged ? button_width : 0 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
textBox = new ItemTextBox
|
||||
TextBox = new ItemTextBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = Vector2.One,
|
||||
@ -107,18 +110,14 @@ namespace osu.Game.Collections
|
||||
base.LoadComplete();
|
||||
|
||||
// Bind late, as the collection name may change externally while still loading.
|
||||
textBox.Current.Value = collection.PerformRead(c => c.IsValid ? c.Name : string.Empty);
|
||||
textBox.OnCommit += onCommit;
|
||||
TextBox.Current.Value = collection.PerformRead(c => c.IsValid ? c.Name : string.Empty);
|
||||
TextBox.OnCommit += onCommit;
|
||||
}
|
||||
|
||||
private void onCommit(TextBox sender, bool newText)
|
||||
{
|
||||
if (collection.IsManaged)
|
||||
collection.PerformWrite(c => c.Name = textBox.Current.Value);
|
||||
else if (!string.IsNullOrEmpty(textBox.Current.Value))
|
||||
realm.Write(r => r.Add(new BeatmapCollection(textBox.Current.Value)));
|
||||
|
||||
textBox.Text = string.Empty;
|
||||
if (collection.IsManaged && collection.Value.Name != TextBox.Current.Value)
|
||||
collection.PerformWrite(c => c.Name = TextBox.Current.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,5 +209,25 @@ namespace osu.Game.Collections
|
||||
|
||||
private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c));
|
||||
}
|
||||
|
||||
public IEnumerable<LocalisableString> FilterTerms => [(LocalisableString)Model.Value.Name];
|
||||
|
||||
private bool matchingFilter = true;
|
||||
|
||||
public bool MatchingFilter
|
||||
{
|
||||
get => matchingFilter;
|
||||
set
|
||||
{
|
||||
matchingFilter = value;
|
||||
|
||||
if (matchingFilter)
|
||||
this.FadeIn(200);
|
||||
else
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
|
||||
public bool FilteringActive { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Collections
|
||||
@ -26,6 +27,9 @@ namespace osu.Game.Collections
|
||||
|
||||
private IDisposable? duckOperation;
|
||||
|
||||
private BasicSearchTextBox searchTextBox = null!;
|
||||
private DrawableCollectionList list = null!;
|
||||
|
||||
[Resolved]
|
||||
private MusicController? musicController { get; set; }
|
||||
|
||||
@ -104,10 +108,31 @@ namespace osu.Game.Collections
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colours.GreySeaFoamDarker
|
||||
},
|
||||
new DrawableCollectionList
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
Padding = new MarginPadding(10),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
searchTextBox = new BasicSearchTextBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Y = 10,
|
||||
Height = 40,
|
||||
ReleaseFocusOnCommit = false,
|
||||
HoldFocus = true,
|
||||
PlaceholderText = HomeStrings.SearchPlaceholder,
|
||||
},
|
||||
list = new DrawableCollectionList
|
||||
{
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Top = 60,
|
||||
},
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -117,6 +142,16 @@ namespace osu.Game.Collections
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
searchTextBox.Current.BindValueChanged(_ =>
|
||||
{
|
||||
list.SearchTerm = searchTextBox.Current.Value;
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user