mirror of
https://github.com/ppy/osu.git
synced 2026-05-30 01:10:58 +08:00
Merge branch 'master' into pp_refactoring_strainskill
This commit is contained in:
@@ -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 }}"
|
||||
+29
-245
@@ -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 }}
|
||||
|
||||
@@ -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'
|
||||
|
||||
+32
-22
@@ -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.869" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
<EmbeddedResource Include="lazer.ico" />
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -180,7 +180,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
// We add tick misses here since they too mean that the player didn't follow the slider properly
|
||||
// We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
|
||||
estimateImproperlyFollowedDifficultSliders = Math.Min(countSliderEndsDropped + countSliderTickMiss, estimateDifficultSliders);
|
||||
estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, estimateDifficultSliders);
|
||||
}
|
||||
|
||||
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / estimateDifficultSliders, 3) + attributes.SliderFactor;
|
||||
|
||||
@@ -26,7 +26,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)
|
||||
@@ -63,27 +62,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
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))));
|
||||
}
|
||||
|
||||
protected override double StrainValueAt(DifficultyHitObject current)
|
||||
{
|
||||
double strain = base.StrainValueAt(current);
|
||||
ObjectStrains.Add(strain);
|
||||
return strain;
|
||||
}
|
||||
|
||||
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
|
||||
|
||||
double totalStrain = CurrentStrain * currentRhythm;
|
||||
ObjectStrains.Add(totalStrain);
|
||||
|
||||
return totalStrain;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
base.RecreatePieces();
|
||||
updateColour();
|
||||
Height = HitObject.IsStrong ? TaikoStrongableHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE;
|
||||
}
|
||||
|
||||
protected override void OnFree()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +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 System;
|
||||
using System.Linq;
|
||||
using Moq;
|
||||
@@ -36,15 +34,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
private readonly Bindable<BeatmapAvailability> beatmapAvailability = new Bindable<BeatmapAvailability>();
|
||||
private readonly Bindable<Room> room = new Bindable<Room>();
|
||||
|
||||
private MultiplayerRoom multiplayerRoom;
|
||||
private MultiplayerRoomUser localUser;
|
||||
private OngoingOperationTracker ongoingOperationTracker;
|
||||
private MultiplayerRoom multiplayerRoom = null!;
|
||||
private MultiplayerRoomUser localUser = null!;
|
||||
private OngoingOperationTracker ongoingOperationTracker = null!;
|
||||
|
||||
private PopoverContainer content;
|
||||
private MatchStartControl control;
|
||||
private PopoverContainer content = null!;
|
||||
private MatchStartControl control = null!;
|
||||
|
||||
private OsuButton readyButton => control.ChildrenOfType<OsuButton>().Single();
|
||||
|
||||
[Cached(typeof(IBindable<PlaylistItem>))]
|
||||
private readonly Bindable<PlaylistItem> currentItem = new Bindable<PlaylistItem>();
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
|
||||
new CachedModelDependencyContainer<Room>(base.CreateChildDependencies(parent)) { Model = { BindTarget = room } };
|
||||
|
||||
@@ -112,15 +113,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable();
|
||||
|
||||
var playlistItem = new PlaylistItem(Beatmap.Value.BeatmapInfo)
|
||||
currentItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
|
||||
{
|
||||
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
|
||||
};
|
||||
|
||||
room.Value = new Room
|
||||
{
|
||||
Playlist = { playlistItem },
|
||||
CurrentPlaylistItem = { Value = playlistItem }
|
||||
Playlist = { currentItem.Value },
|
||||
CurrentPlaylistItem = { BindTarget = currentItem }
|
||||
};
|
||||
|
||||
localUser = new MultiplayerRoomUser(API.LocalUser.Value.Id) { User = API.LocalUser.Value };
|
||||
@@ -129,7 +130,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
Playlist =
|
||||
{
|
||||
TestMultiplayerClient.CreateMultiplayerPlaylistItem(playlistItem),
|
||||
TestMultiplayerClient.CreateMultiplayerPlaylistItem(currentItem.Value),
|
||||
},
|
||||
Users = { localUser },
|
||||
Host = localUser,
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public partial class TestSceneMultiplayerMatchFooter : MultiplayerTestScene
|
||||
{
|
||||
[Cached(typeof(IBindable<PlaylistItem>))]
|
||||
private readonly Bindable<PlaylistItem> currentItem = new Bindable<PlaylistItem>();
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Platform;
|
||||
@@ -29,10 +28,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public partial class TestSceneMultiplayerPlaylist : MultiplayerTestScene
|
||||
{
|
||||
private MultiplayerPlaylist list;
|
||||
private BeatmapManager beatmaps;
|
||||
private BeatmapSetInfo importedSet;
|
||||
private BeatmapInfo importedBeatmap;
|
||||
[Cached(typeof(IBindable<PlaylistItem>))]
|
||||
private readonly Bindable<PlaylistItem> currentItem = new Bindable<PlaylistItem>();
|
||||
|
||||
private MultiplayerPlaylist list = null!;
|
||||
private BeatmapManager beatmaps = null!;
|
||||
private BeatmapSetInfo importedSet = null!;
|
||||
private BeatmapInfo importedBeatmap = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
@@ -198,7 +200,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
addItemStep();
|
||||
addItemStep();
|
||||
|
||||
DrawableRoomPlaylistItem[] drawableItems = null;
|
||||
DrawableRoomPlaylistItem[] drawableItems = null!;
|
||||
AddStep("get drawable items", () => drawableItems = this.ChildrenOfType<DrawableRoomPlaylistItem>().ToArray());
|
||||
|
||||
// Add 1 item for another user.
|
||||
|
||||
@@ -1,8 +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 System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -28,13 +26,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public partial class TestSceneMultiplayerSpectateButton : MultiplayerTestScene
|
||||
{
|
||||
private MultiplayerSpectateButton spectateButton;
|
||||
private MatchStartControl startControl;
|
||||
[Cached(typeof(IBindable<PlaylistItem>))]
|
||||
private readonly Bindable<PlaylistItem> currentItem = new Bindable<PlaylistItem>();
|
||||
|
||||
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
|
||||
private MultiplayerSpectateButton spectateButton = null!;
|
||||
private MatchStartControl startControl = null!;
|
||||
|
||||
private BeatmapSetInfo importedSet;
|
||||
private BeatmapManager beatmaps;
|
||||
private BeatmapSetInfo importedSet = null!;
|
||||
private BeatmapManager beatmaps = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
@@ -52,14 +51,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("create button", () =>
|
||||
{
|
||||
AvailabilityTracker.SelectedItem.BindTo(selectedItem);
|
||||
AvailabilityTracker.SelectedItem.BindTo(currentItem);
|
||||
|
||||
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
|
||||
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
|
||||
selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
|
||||
{
|
||||
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID,
|
||||
};
|
||||
|
||||
currentItem.Value = SelectedRoom.Value.Playlist.First();
|
||||
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -196,6 +196,7 @@ namespace osu.Game.Configuration
|
||||
SetDefault(OsuSetting.EditorShowSpeedChanges, false);
|
||||
SetDefault(OsuSetting.EditorScaleOrigin, EditorOrigin.GridCentre);
|
||||
SetDefault(OsuSetting.EditorRotationOrigin, EditorOrigin.GridCentre);
|
||||
SetDefault(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges, true);
|
||||
|
||||
SetDefault(OsuSetting.HideCountryFlags, false);
|
||||
|
||||
@@ -442,5 +443,6 @@ namespace osu.Game.Configuration
|
||||
EditorScaleOrigin,
|
||||
EditorRotationOrigin,
|
||||
EditorTimelineShowBreaks,
|
||||
EditorAdjustExistingObjectsOnTimingChanges,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace osu.Game.Database
|
||||
private RealmAccess realmAccess { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapUpdater beatmapUpdater { get; set; } = null!;
|
||||
private IBeatmapUpdater beatmapUpdater { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> gameBeatmap { get; set; } = null!;
|
||||
|
||||
@@ -39,6 +39,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString SetPreviewPointToCurrent => new TranslatableString(getKey(@"set_preview_point_to_current"), @"Set preview point to current time");
|
||||
|
||||
/// <summary>
|
||||
/// "Move already placed objects when changing timing"
|
||||
/// </summary>
|
||||
public static LocalisableString AdjustExistingObjectsOnTimingChanges => new TranslatableString(getKey(@"adjust_existing_objects_on_timing_changes"), @"Move already placed objects when changing timing");
|
||||
|
||||
/// <summary>
|
||||
/// "For editing (.olz)"
|
||||
/// </summary>
|
||||
|
||||
@@ -202,7 +202,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
Debug.Assert(joinedRoom.Playlist.Count > 0);
|
||||
|
||||
APIRoom.Playlist.Clear();
|
||||
APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem));
|
||||
APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(item => new PlaylistItem(item)));
|
||||
APIRoom.CurrentPlaylistItem.Value = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId);
|
||||
|
||||
// The server will null out the end date upon the host joining the room, but the null value is never communicated to the client.
|
||||
@@ -734,7 +734,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
Debug.Assert(APIRoom != null);
|
||||
|
||||
Room.Playlist.Add(item);
|
||||
APIRoom.Playlist.Add(createPlaylistItem(item));
|
||||
APIRoom.Playlist.Add(new PlaylistItem(item));
|
||||
|
||||
ItemAdded?.Invoke(item);
|
||||
RoomUpdated?.Invoke();
|
||||
@@ -780,7 +780,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
int existingIndex = APIRoom.Playlist.IndexOf(APIRoom.Playlist.Single(existing => existing.ID == item.ID));
|
||||
|
||||
APIRoom.Playlist.RemoveAt(existingIndex);
|
||||
APIRoom.Playlist.Insert(existingIndex, createPlaylistItem(item));
|
||||
APIRoom.Playlist.Insert(existingIndex, new PlaylistItem(item));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -853,18 +853,6 @@ namespace osu.Game.Online.Multiplayer
|
||||
RoomUpdated?.Invoke();
|
||||
}
|
||||
|
||||
private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item) => new PlaylistItem(new APIBeatmap { OnlineID = item.BeatmapID, StarRating = item.StarRating })
|
||||
{
|
||||
ID = item.ID,
|
||||
OwnerID = item.OwnerID,
|
||||
RulesetID = item.RulesetID,
|
||||
Expired = item.Expired,
|
||||
PlaylistOrder = item.PlaylistOrder,
|
||||
PlayedAt = item.PlayedAt,
|
||||
RequiredMods = item.RequiredMods.ToArray(),
|
||||
AllowedMods = item.AllowedMods.ToArray()
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// For the provided user ID, update whether the user is included in <see cref="CurrentMatchPlayingUserIds"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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 osu.Game.Online.Rooms;
|
||||
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
public static class MultiplayerRoomExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all historical/expired items from the <paramref name="room"/>, in the order in which they were played.
|
||||
/// </summary>
|
||||
public static IEnumerable<MultiplayerPlaylistItem> GetHistoricalItems(this MultiplayerRoom room)
|
||||
=> room.Playlist.Where(item => item.Expired).OrderBy(item => item.PlayedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all non-expired items from the <paramref name="room"/>, in the order in which they are to be played.
|
||||
/// </summary>
|
||||
public static IEnumerable<MultiplayerPlaylistItem> GetUpcomingItems(this MultiplayerRoom room)
|
||||
=> room.Playlist.Where(item => !item.Expired).OrderBy(item => item.PlaylistOrder);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first non-expired <see cref="MultiplayerPlaylistItem"/> in playlist order from the supplied <paramref name="room"/>,
|
||||
/// or the last-played <see cref="MultiplayerPlaylistItem"/> if all items are expired,
|
||||
/// or <see langword="null"/> if <paramref name="room"/> was empty.
|
||||
/// </summary>
|
||||
public static MultiplayerPlaylistItem? GetCurrentItem(this MultiplayerRoom room)
|
||||
{
|
||||
if (room.Playlist.Count == 0)
|
||||
return null;
|
||||
|
||||
return room.Playlist.All(item => item.Expired)
|
||||
? GetHistoricalItems(room).Last()
|
||||
: GetUpcomingItems(room).First();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,7 +198,7 @@ namespace osu.Game
|
||||
public readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> AvailableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>(new Dictionary<ModType, IReadOnlyList<Mod>>());
|
||||
|
||||
private BeatmapDifficultyCache difficultyCache;
|
||||
private BeatmapUpdater beatmapUpdater;
|
||||
private IBeatmapUpdater beatmapUpdater;
|
||||
|
||||
private UserLookupCache userCache;
|
||||
private BeatmapLookupCache beatmapCache;
|
||||
@@ -324,7 +324,7 @@ namespace osu.Game
|
||||
base.Content.Add(difficultyCache);
|
||||
|
||||
// TODO: OsuGame or OsuGameBase?
|
||||
dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage));
|
||||
dependencies.CacheAs(beatmapUpdater = CreateBeatmapUpdater());
|
||||
dependencies.CacheAs(SpectatorClient = new OnlineSpectatorClient(endpoints));
|
||||
dependencies.CacheAs(MultiplayerClient = new OnlineMultiplayerClient(endpoints));
|
||||
dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));
|
||||
@@ -563,6 +563,8 @@ namespace osu.Game
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual IBeatmapUpdater CreateBeatmapUpdater() => new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage);
|
||||
|
||||
protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager();
|
||||
|
||||
protected virtual BatteryInfo CreateBatteryInfo() => null;
|
||||
|
||||
@@ -42,10 +42,10 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
||||
protected double CurrentStrain;
|
||||
|
||||
private double currentSectionPeak; // We also keep track of the peak strain level in the current section.
|
||||
|
||||
private double currentSectionEnd;
|
||||
|
||||
private readonly List<double> strainPeaks = new List<double>();
|
||||
protected readonly List<double> ObjectStrains = new List<double>(); // Store individual strains
|
||||
|
||||
protected StrainSkill(Mod[] mods)
|
||||
: base(mods)
|
||||
@@ -86,7 +86,25 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
||||
currentSectionEnd += SectionLength;
|
||||
}
|
||||
|
||||
currentSectionPeak = Math.Max(StrainValueAt(current), currentSectionPeak);
|
||||
double strain = StrainValueAt(current);
|
||||
currentSectionPeak = Math.Max(strain, currentSectionPeak);
|
||||
|
||||
// Store the strain value for the object
|
||||
ObjectStrains.Add(strain);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the number of strains weighted against the top strain.
|
||||
/// The result is scaled by clock rate as it affects the total number of strains.
|
||||
/// </summary>
|
||||
public virtual double CountTopWeightedStrains()
|
||||
{
|
||||
if (ObjectStrains.Count == 0)
|
||||
return 0.0;
|
||||
|
||||
double consistentTopStrain = DifficultyValue() / 10; // What would the top strain be if all strain values were identical
|
||||
// Use a weighted sum of all strains. Constants are arbitrary and give nice values
|
||||
return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -421,7 +421,7 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
Items = new MenuItem[]
|
||||
{
|
||||
new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime)
|
||||
new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osuTK;
|
||||
@@ -25,6 +26,9 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
[Resolved]
|
||||
protected EditorBeatmap Beatmap { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager configManager { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorClock clock { get; set; } = null!;
|
||||
|
||||
@@ -110,7 +114,16 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
Beatmap.ControlPointInfo.RemoveGroup(SelectedGroup.Value);
|
||||
|
||||
foreach (var cp in currentGroupItems)
|
||||
{
|
||||
// Only adjust hit object offsets if the group contains a timing control point
|
||||
if (cp is TimingControlPoint tp && configManager.Get<bool>(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges))
|
||||
{
|
||||
TimingSectionAdjustments.AdjustHitObjectOffset(Beatmap, tp, time - SelectedGroup.Value.Time);
|
||||
Beatmap.UpdateAllHitObjects();
|
||||
}
|
||||
|
||||
Beatmap.ControlPointInfo.Add(time, cp);
|
||||
}
|
||||
|
||||
// the control point might not necessarily exist yet, if currentGroupItems was empty.
|
||||
SelectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(time, true);
|
||||
|
||||
@@ -10,6 +10,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
@@ -26,6 +27,9 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
[Resolved]
|
||||
private EditorBeatmap beatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager configManager { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Bindable<ControlPointGroup> selectedGroup { get; set; } = null!;
|
||||
|
||||
@@ -202,15 +206,25 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
// VERY TEMPORARY
|
||||
var currentGroupItems = selectedGroup.Value.ControlPoints.ToArray();
|
||||
|
||||
beatmap.BeginChange();
|
||||
beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value);
|
||||
|
||||
double newOffset = selectedGroup.Value.Time + adjust;
|
||||
|
||||
foreach (var cp in currentGroupItems)
|
||||
{
|
||||
if (cp is TimingControlPoint tp)
|
||||
{
|
||||
TimingSectionAdjustments.AdjustHitObjectOffset(beatmap, tp, adjust);
|
||||
beatmap.UpdateAllHitObjects();
|
||||
}
|
||||
|
||||
beatmap.ControlPointInfo.Add(newOffset, cp);
|
||||
}
|
||||
|
||||
// the control point might not necessarily exist yet, if currentGroupItems was empty.
|
||||
selectedGroup.Value = beatmap.ControlPointInfo.GroupAt(newOffset, true);
|
||||
beatmap.EndChange();
|
||||
|
||||
if (!editorClock.IsRunning && wasAtStart)
|
||||
editorClock.Seek(newOffset);
|
||||
@@ -223,7 +237,16 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
if (timing == null)
|
||||
return;
|
||||
|
||||
double oldBeatLength = timing.BeatLength;
|
||||
timing.BeatLength = 60000 / (timing.BPM + adjust);
|
||||
|
||||
if (configManager.Get<bool>(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges))
|
||||
{
|
||||
beatmap.BeginChange();
|
||||
TimingSectionAdjustments.SetHitObjectBPM(beatmap, timing, oldBeatLength);
|
||||
beatmap.UpdateAllHitObjects();
|
||||
beatmap.EndChange();
|
||||
}
|
||||
}
|
||||
|
||||
private partial class InlineButton : OsuButton
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
@@ -15,11 +18,20 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
private LabelledSwitchButton omitBarLine = null!;
|
||||
private BPMTextBox bpmTextEntry = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager configManager { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Flow.AddRange(new Drawable[]
|
||||
{
|
||||
new LabelledSwitchButton
|
||||
{
|
||||
Label = EditorStrings.AdjustExistingObjectsOnTimingChanges,
|
||||
FixedLabelWidth = 220,
|
||||
Current = configManager.GetBindable<bool>(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges),
|
||||
},
|
||||
new TapTimingControl(),
|
||||
bpmTextEntry = new BPMTextBox(),
|
||||
timeSignature = new LabelledTimeSignature
|
||||
@@ -42,6 +54,17 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
if (!isRebinding) ChangeHandler?.SaveState();
|
||||
}
|
||||
|
||||
bpmTextEntry.OnCommit = (oldBeatLength, _) =>
|
||||
{
|
||||
if (!configManager.Get<bool>(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges) || ControlPoint.Value == null)
|
||||
return;
|
||||
|
||||
Beatmap.BeginChange();
|
||||
TimingSectionAdjustments.SetHitObjectBPM(Beatmap, ControlPoint.Value, oldBeatLength);
|
||||
Beatmap.UpdateAllHitObjects();
|
||||
Beatmap.EndChange();
|
||||
};
|
||||
}
|
||||
|
||||
private bool isRebinding;
|
||||
@@ -74,6 +97,8 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
private partial class BPMTextBox : LabelledTextBox
|
||||
{
|
||||
public new Action<double, double>? OnCommit { get; set; }
|
||||
|
||||
private readonly BindableNumber<double> beatLengthBindable = new TimingControlPoint().BeatLengthBindable;
|
||||
|
||||
public BPMTextBox()
|
||||
@@ -81,10 +106,12 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
Label = "BPM";
|
||||
SelectAllOnFocus = true;
|
||||
|
||||
OnCommit += (_, isNew) =>
|
||||
base.OnCommit += (_, isNew) =>
|
||||
{
|
||||
if (!isNew) return;
|
||||
|
||||
double oldBeatLength = beatLengthBindable.Value;
|
||||
|
||||
try
|
||||
{
|
||||
if (double.TryParse(Current.Value, out double doubleVal) && doubleVal > 0)
|
||||
@@ -98,6 +125,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
// This is run regardless of parsing success as the parsed number may not actually trigger a change
|
||||
// due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state.
|
||||
beatLengthBindable.TriggerChange();
|
||||
OnCommit?.Invoke(oldBeatLength, beatLengthBindable.Value);
|
||||
};
|
||||
|
||||
beatLengthBindable.BindValueChanged(val =>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
public static class TimingSectionAdjustments
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all objects from <paramref name="beatmap"/> which are affected by the supplied <paramref name="timingControlPoint"/>.
|
||||
/// </summary>
|
||||
public static List<HitObject> HitObjectsInTimingRange(IBeatmap beatmap, TimingControlPoint timingControlPoint)
|
||||
{
|
||||
// If the first group, we grab all hitobjects prior to the next, if the last group, we grab all remaining hitobjects
|
||||
double startTime = beatmap.ControlPointInfo.TimingPoints.Any(x => x.Time < timingControlPoint.Time) ? timingControlPoint.Time : double.MinValue;
|
||||
double endTime = beatmap.ControlPointInfo.TimingPoints.FirstOrDefault(x => x.Time > timingControlPoint.Time)?.Time ?? double.MaxValue;
|
||||
|
||||
return beatmap.HitObjects.Where(x => Precision.AlmostBigger(x.StartTime, startTime) && Precision.DefinitelyBigger(endTime, x.StartTime)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves all relevant objects after <paramref name="timingControlPoint"/>'s offset has been changed by <paramref name="adjustment"/>.
|
||||
/// </summary>
|
||||
public static void AdjustHitObjectOffset(IBeatmap beatmap, TimingControlPoint timingControlPoint, double adjustment)
|
||||
{
|
||||
foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap, timingControlPoint))
|
||||
{
|
||||
hitObject.StartTime += adjustment;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures all relevant objects are still snapped to the same beats after <paramref name="timingControlPoint"/>'s beat length / BPM has been changed.
|
||||
/// </summary>
|
||||
public static void SetHitObjectBPM(IBeatmap beatmap, TimingControlPoint timingControlPoint, double oldBeatLength)
|
||||
{
|
||||
foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap, timingControlPoint))
|
||||
{
|
||||
double beat = (hitObject.StartTime - timingControlPoint.Time) / oldBeatLength;
|
||||
|
||||
hitObject.StartTime = (beat * timingControlPoint.BeatLength) + timingControlPoint.Time;
|
||||
|
||||
if (hitObject is not IHasRepeats && hitObject is IHasDuration hitObjectWithDuration)
|
||||
hitObjectWithDuration.Duration *= timingControlPoint.BeatLength / oldBeatLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays;
|
||||
@@ -16,6 +17,9 @@ namespace osu.Game.Screens.Menu
|
||||
[Resolved]
|
||||
private IDialogOverlay dialogOverlay { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realmAccess { get; set; } = null!;
|
||||
|
||||
public StorageErrorDialog(OsuStorage storage, OsuStorageError error)
|
||||
{
|
||||
HeaderText = StorageErrorDialogStrings.StorageError;
|
||||
@@ -35,7 +39,15 @@ namespace osu.Game.Screens.Menu
|
||||
Text = StorageErrorDialogStrings.TryAgain,
|
||||
Action = () =>
|
||||
{
|
||||
if (!storage.TryChangeToCustomStorage(out var nextError))
|
||||
bool success;
|
||||
OsuStorageError nextError;
|
||||
|
||||
// blocking all operations has a side effect of closing & reopening the realm db,
|
||||
// which is desirable here since the restoration of the old storage - if it succeeds - means the realm db has moved.
|
||||
using (realmAccess.BlockAllOperations(@"restoration of previously unavailable storage"))
|
||||
success = storage.TryChangeToCustomStorage(out nextError);
|
||||
|
||||
if (!success)
|
||||
dialogOverlay.Push(new StorageErrorDialog(storage, nextError));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
{
|
||||
public partial class RankRangePill : MultiplayerRoomComposite
|
||||
public partial class RankRangePill : CompositeDrawable
|
||||
{
|
||||
private OsuTextFlowContainer rankFlow;
|
||||
private OsuTextFlowContainer rankFlow = null!;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
public RankRangePill()
|
||||
{
|
||||
@@ -55,20 +57,28 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnRoomUpdated()
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.OnRoomUpdated();
|
||||
base.LoadComplete();
|
||||
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void onRoomUpdated() => Scheduler.AddOnce(updateState);
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
rankFlow.Clear();
|
||||
|
||||
if (Room == null || Room.Users.All(u => u.User == null))
|
||||
if (client.Room == null || client.Room.Users.All(u => u.User == null))
|
||||
{
|
||||
rankFlow.AddText("-");
|
||||
return;
|
||||
}
|
||||
|
||||
int minRank = Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Min();
|
||||
int maxRank = Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Max();
|
||||
int minRank = client.Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Min();
|
||||
int maxRank = client.Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Max();
|
||||
|
||||
rankFlow.AddText("#");
|
||||
rankFlow.AddText(minRank.ToString("#,0"), s => s.Font = s.Font.With(weight: FontWeight.Bold));
|
||||
@@ -78,5 +88,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
rankFlow.AddText("#");
|
||||
rankFlow.AddText(maxRank.ToString("#,0"), s => s.Font = s.Font.With(weight: FontWeight.Bold));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
client.RoomUpdated -= onRoomUpdated;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,50 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.Countdown;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
{
|
||||
public partial class MatchStartControl : MultiplayerRoomComposite
|
||||
public partial class MatchStartControl : CompositeDrawable
|
||||
{
|
||||
[Resolved]
|
||||
private OngoingOperationTracker ongoingOperationTracker { get; set; }
|
||||
|
||||
[CanBeNull]
|
||||
private IDisposable clickOperation;
|
||||
private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private IDialogOverlay dialogOverlay { get; set; }
|
||||
private IDialogOverlay? dialogOverlay { get; set; }
|
||||
|
||||
private Sample sampleReady;
|
||||
private Sample sampleReadyAll;
|
||||
private Sample sampleUnready;
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<PlaylistItem?> currentItem { get; set; } = null!;
|
||||
|
||||
private readonly MultiplayerReadyButton readyButton;
|
||||
private readonly MultiplayerCountdownButton countdownButton;
|
||||
|
||||
private IBindable<bool> operationInProgress = null!;
|
||||
private ScheduledDelegate? readySampleDelegate;
|
||||
private IDisposable? clickOperation;
|
||||
private Sample? sampleReady;
|
||||
private Sample? sampleReadyAll;
|
||||
private Sample? sampleUnready;
|
||||
private int countReady;
|
||||
private ScheduledDelegate readySampleDelegate;
|
||||
private IBindable<bool> operationInProgress;
|
||||
|
||||
public MatchStartControl()
|
||||
{
|
||||
@@ -91,34 +94,29 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
CurrentPlaylistItem.BindValueChanged(_ => updateState());
|
||||
}
|
||||
|
||||
protected override void OnRoomUpdated()
|
||||
{
|
||||
base.OnRoomUpdated();
|
||||
currentItem.BindValueChanged(_ => updateState());
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
client.LoadRequested += onLoadRequested;
|
||||
updateState();
|
||||
}
|
||||
|
||||
protected override void OnRoomLoadRequested()
|
||||
{
|
||||
base.OnRoomLoadRequested();
|
||||
endOperation();
|
||||
}
|
||||
private void onRoomUpdated() => Scheduler.AddOnce(updateState);
|
||||
|
||||
private void onLoadRequested() => Scheduler.AddOnce(endOperation);
|
||||
|
||||
private void onReadyButtonClick()
|
||||
{
|
||||
if (Room == null)
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
Debug.Assert(clickOperation == null);
|
||||
clickOperation = ongoingOperationTracker.BeginOperation();
|
||||
|
||||
if (Client.IsHost)
|
||||
if (client.IsHost)
|
||||
{
|
||||
if (Room.State == MultiplayerRoomState.Open)
|
||||
if (client.Room.State == MultiplayerRoomState.Open)
|
||||
{
|
||||
if (isReady() && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
|
||||
if (isReady() && !client.Room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
|
||||
startMatch();
|
||||
else
|
||||
toggleReady();
|
||||
@@ -131,16 +129,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
dialogOverlay.Push(new ConfirmAbortDialog(abortMatch, endOperation));
|
||||
}
|
||||
}
|
||||
else if (Room.State != MultiplayerRoomState.Closed)
|
||||
else if (client.Room.State != MultiplayerRoomState.Closed)
|
||||
toggleReady();
|
||||
|
||||
bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating;
|
||||
bool isReady() => client.LocalUser?.State == MultiplayerUserState.Ready || client.LocalUser?.State == MultiplayerUserState.Spectating;
|
||||
|
||||
void toggleReady() => Client.ToggleReady().FireAndForget(
|
||||
void toggleReady() => client.ToggleReady().FireAndForget(
|
||||
onSuccess: endOperation,
|
||||
onError: _ => endOperation());
|
||||
|
||||
void startMatch() => Client.StartMatch().FireAndForget(onSuccess: () =>
|
||||
void startMatch() => client.StartMatch().FireAndForget(onSuccess: () =>
|
||||
{
|
||||
// gameplay is starting, the button will be unblocked on load requested.
|
||||
}, onError: _ =>
|
||||
@@ -149,7 +147,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
endOperation();
|
||||
});
|
||||
|
||||
void abortMatch() => Client.AbortMatch().FireAndForget(endOperation, _ => endOperation());
|
||||
void abortMatch() => client.AbortMatch().FireAndForget(endOperation, _ => endOperation());
|
||||
}
|
||||
|
||||
private void startCountdown(TimeSpan duration)
|
||||
@@ -157,19 +155,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
Debug.Assert(clickOperation == null);
|
||||
clickOperation = ongoingOperationTracker.BeginOperation();
|
||||
|
||||
Client.SendMatchRequest(new StartMatchCountdownRequest { Duration = duration }).ContinueWith(_ => endOperation());
|
||||
client.SendMatchRequest(new StartMatchCountdownRequest { Duration = duration }).ContinueWith(_ => endOperation());
|
||||
}
|
||||
|
||||
private void cancelCountdown()
|
||||
{
|
||||
if (Client.Room == null)
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
Debug.Assert(clickOperation == null);
|
||||
clickOperation = ongoingOperationTracker.BeginOperation();
|
||||
|
||||
MultiplayerCountdown countdown = Client.Room.ActiveCountdowns.Single(c => c is MatchStartCountdown);
|
||||
Client.SendMatchRequest(new StopCountdownRequest(countdown.ID)).ContinueWith(_ => endOperation());
|
||||
MultiplayerCountdown countdown = client.Room.ActiveCountdowns.Single(c => c is MatchStartCountdown);
|
||||
client.SendMatchRequest(new StopCountdownRequest(countdown.ID)).ContinueWith(_ => endOperation());
|
||||
}
|
||||
|
||||
private void endOperation()
|
||||
@@ -180,19 +178,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
if (Room == null)
|
||||
if (client.Room == null)
|
||||
{
|
||||
readyButton.Enabled.Value = false;
|
||||
countdownButton.Enabled.Value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var localUser = Client.LocalUser;
|
||||
var localUser = client.LocalUser;
|
||||
|
||||
int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
|
||||
int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
|
||||
int newCountReady = client.Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
|
||||
int newCountTotal = client.Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
|
||||
|
||||
if (!Client.IsHost || Room.Settings.AutoStartEnabled)
|
||||
if (!client.IsHost || client.Room.Settings.AutoStartEnabled)
|
||||
countdownButton.Hide();
|
||||
else
|
||||
{
|
||||
@@ -211,21 +209,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
}
|
||||
|
||||
readyButton.Enabled.Value = countdownButton.Enabled.Value =
|
||||
Room.State != MultiplayerRoomState.Closed
|
||||
&& CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId
|
||||
&& !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired
|
||||
client.Room.State != MultiplayerRoomState.Closed
|
||||
&& currentItem.Value?.ID == client.Room.Settings.PlaylistItemId
|
||||
&& !client.Room.Playlist.Single(i => i.ID == client.Room.Settings.PlaylistItemId).Expired
|
||||
&& !operationInProgress.Value;
|
||||
|
||||
// When the local user is the host and spectating the match, the ready button should be enabled only if any users are ready.
|
||||
if (localUser?.State == MultiplayerUserState.Spectating)
|
||||
readyButton.Enabled.Value &= Client.IsHost && newCountReady > 0 && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown);
|
||||
readyButton.Enabled.Value &= client.IsHost && newCountReady > 0 && !client.Room.ActiveCountdowns.Any(c => c is MatchStartCountdown);
|
||||
|
||||
// When the local user is not the host, the button should only be enabled when no match is in progress.
|
||||
if (!Client.IsHost)
|
||||
readyButton.Enabled.Value &= Room.State == MultiplayerRoomState.Open;
|
||||
if (!client.IsHost)
|
||||
readyButton.Enabled.Value &= client.Room.State == MultiplayerRoomState.Open;
|
||||
|
||||
// At all times, the countdown button should only be enabled when no match is in progress.
|
||||
countdownButton.Enabled.Value &= Room.State == MultiplayerRoomState.Open;
|
||||
countdownButton.Enabled.Value &= client.Room.State == MultiplayerRoomState.Open;
|
||||
|
||||
if (newCountReady == countReady)
|
||||
return;
|
||||
@@ -249,6 +247,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
{
|
||||
client.RoomUpdated -= onRoomUpdated;
|
||||
client.LoadRequested -= onLoadRequested;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class ConfirmAbortDialog : DangerousActionDialog
|
||||
{
|
||||
public ConfirmAbortDialog(Action abortMatch, Action cancel)
|
||||
|
||||
@@ -5,7 +5,9 @@ using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
@@ -17,7 +19,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
{
|
||||
public partial class MultiplayerSpectateButton : MultiplayerRoomComposite
|
||||
public partial class MultiplayerSpectateButton : CompositeDrawable
|
||||
{
|
||||
[Resolved]
|
||||
private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!;
|
||||
@@ -25,6 +27,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<PlaylistItem?> currentItem { get; set; } = null!;
|
||||
|
||||
private IBindable<bool> operationInProgress = null!;
|
||||
|
||||
private readonly RoundedButton button;
|
||||
@@ -44,7 +52,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
{
|
||||
var clickOperation = ongoingOperationTracker.BeginOperation();
|
||||
|
||||
Client.ToggleSpectate().ContinueWith(_ => endOperation());
|
||||
client.ToggleSpectate().ContinueWith(_ => endOperation());
|
||||
|
||||
void endOperation() => clickOperation?.Dispose();
|
||||
}
|
||||
@@ -63,19 +71,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
CurrentPlaylistItem.BindValueChanged(_ => Scheduler.AddOnce(checkForAutomaticDownload), true);
|
||||
}
|
||||
|
||||
protected override void OnRoomUpdated()
|
||||
{
|
||||
base.OnRoomUpdated();
|
||||
|
||||
currentItem.BindValueChanged(_ => Scheduler.AddOnce(checkForAutomaticDownload), true);
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void onRoomUpdated() => Scheduler.AddOnce(updateState);
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
switch (Client.LocalUser?.State)
|
||||
switch (client.LocalUser?.State)
|
||||
{
|
||||
default:
|
||||
button.Text = "Spectate";
|
||||
@@ -88,8 +93,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
break;
|
||||
}
|
||||
|
||||
button.Enabled.Value = Client.Room != null
|
||||
&& Client.Room.State != MultiplayerRoomState.Closed
|
||||
button.Enabled.Value = client.Room != null
|
||||
&& client.Room.State != MultiplayerRoomState.Closed
|
||||
&& !operationInProgress.Value;
|
||||
|
||||
Scheduler.AddOnce(checkForAutomaticDownload);
|
||||
@@ -112,11 +117,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
|
||||
private void checkForAutomaticDownload()
|
||||
{
|
||||
PlaylistItem? currentItem = CurrentPlaylistItem.Value;
|
||||
PlaylistItem? item = currentItem.Value;
|
||||
|
||||
downloadCheckCancellation?.Cancel();
|
||||
|
||||
if (currentItem == null)
|
||||
if (item == null)
|
||||
return;
|
||||
|
||||
if (!automaticallyDownload.Value)
|
||||
@@ -128,13 +133,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
//
|
||||
// Rather than over-complicating this flow, let's only auto-download when spectating for the time being.
|
||||
// A potential path forward would be to have a local auto-download checkbox above the playlist item list area.
|
||||
if (Client.LocalUser?.State != MultiplayerUserState.Spectating)
|
||||
if (client.LocalUser?.State != MultiplayerUserState.Spectating)
|
||||
return;
|
||||
|
||||
// In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes.
|
||||
// ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised.
|
||||
beatmapLookupCache
|
||||
.GetBeatmapAsync(currentItem.Beatmap.OnlineID, (downloadCheckCancellation = new CancellationTokenSource()).Token)
|
||||
.GetBeatmapAsync(item.Beatmap.OnlineID, (downloadCheckCancellation = new CancellationTokenSource()).Token)
|
||||
.ContinueWith(resolved => Schedule(() =>
|
||||
{
|
||||
var beatmapSet = resolved.GetResultSafely()?.BeatmapSet;
|
||||
@@ -150,5 +155,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
client.RoomUpdated -= onRoomUpdated;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +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 System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -17,18 +15,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
/// <summary>
|
||||
/// The multiplayer playlist, containing lists to show the items from a <see cref="MultiplayerRoom"/> in both gameplay-order and historical-order.
|
||||
/// </summary>
|
||||
public partial class MultiplayerPlaylist : MultiplayerRoomComposite
|
||||
public partial class MultiplayerPlaylist : CompositeDrawable
|
||||
{
|
||||
public readonly Bindable<MultiplayerPlaylistDisplayMode> DisplayMode = new Bindable<MultiplayerPlaylistDisplayMode>();
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when an item requests to be edited.
|
||||
/// </summary>
|
||||
public Action<PlaylistItem> RequestEdit;
|
||||
public Action<PlaylistItem>? RequestEdit;
|
||||
|
||||
private MultiplayerPlaylistTabControl playlistTabControl;
|
||||
private MultiplayerQueueList queueList;
|
||||
private MultiplayerHistoryList historyList;
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<PlaylistItem?> currentItem { get; set; } = null!;
|
||||
|
||||
private MultiplayerPlaylistTabControl playlistTabControl = null!;
|
||||
private MultiplayerQueueList queueList = null!;
|
||||
private MultiplayerHistoryList historyList = null!;
|
||||
private bool firstPopulation = true;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -54,14 +58,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
queueList = new MultiplayerQueueList
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
SelectedItem = { BindTarget = CurrentPlaylistItem },
|
||||
SelectedItem = { BindTarget = currentItem },
|
||||
RequestEdit = item => RequestEdit?.Invoke(item)
|
||||
},
|
||||
historyList = new MultiplayerHistoryList
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
SelectedItem = { BindTarget = CurrentPlaylistItem }
|
||||
SelectedItem = { BindTarget = currentItem }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +77,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
DisplayMode.BindValueChanged(onDisplayModeChanged, true);
|
||||
client.ItemAdded += playlistItemAdded;
|
||||
client.ItemRemoved += playlistItemRemoved;
|
||||
client.ItemChanged += playlistItemChanged;
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void onDisplayModeChanged(ValueChangedEvent<MultiplayerPlaylistDisplayMode> mode)
|
||||
@@ -82,11 +92,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
queueList.FadeTo(mode.NewValue == MultiplayerPlaylistDisplayMode.Queue ? 1 : 0, 100);
|
||||
}
|
||||
|
||||
protected override void OnRoomUpdated()
|
||||
{
|
||||
base.OnRoomUpdated();
|
||||
private void onRoomUpdated() => Scheduler.AddOnce(updateState);
|
||||
|
||||
if (Room == null)
|
||||
private void updateState()
|
||||
{
|
||||
if (client.Room == null)
|
||||
{
|
||||
historyList.Items.Clear();
|
||||
queueList.Items.Clear();
|
||||
@@ -96,34 +106,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
|
||||
if (firstPopulation)
|
||||
{
|
||||
foreach (var item in Room.Playlist)
|
||||
foreach (var item in client.Room.Playlist)
|
||||
addItemToLists(item);
|
||||
|
||||
firstPopulation = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void PlaylistItemAdded(MultiplayerPlaylistItem item)
|
||||
{
|
||||
base.PlaylistItemAdded(item);
|
||||
addItemToLists(item);
|
||||
}
|
||||
private void playlistItemAdded(MultiplayerPlaylistItem item) => Schedule(() => addItemToLists(item));
|
||||
|
||||
protected override void PlaylistItemRemoved(long item)
|
||||
{
|
||||
base.PlaylistItemRemoved(item);
|
||||
removeItemFromLists(item);
|
||||
}
|
||||
private void playlistItemRemoved(long item) => Schedule(() => removeItemFromLists(item));
|
||||
|
||||
protected override void PlaylistItemChanged(MultiplayerPlaylistItem item)
|
||||
private void playlistItemChanged(MultiplayerPlaylistItem item) => Schedule(() =>
|
||||
{
|
||||
base.PlaylistItemChanged(item);
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
var newApiItem = Playlist.SingleOrDefault(i => i.ID == item.ID);
|
||||
var newApiItem = new PlaylistItem(item);
|
||||
var existingApiItemInQueue = queueList.Items.SingleOrDefault(i => i.ID == item.ID);
|
||||
|
||||
// Test if the only change between the two playlist items is the order.
|
||||
if (newApiItem != null && existingApiItemInQueue != null && existingApiItemInQueue.With(playlistOrder: newApiItem.PlaylistOrder).Equals(newApiItem))
|
||||
if (existingApiItemInQueue != null && existingApiItemInQueue.With(playlistOrder: newApiItem.PlaylistOrder).Equals(newApiItem))
|
||||
{
|
||||
// Set the new playlist order directly without refreshing the DrawablePlaylistItem.
|
||||
existingApiItemInQueue.PlaylistOrder = newApiItem.PlaylistOrder;
|
||||
@@ -137,20 +140,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
removeItemFromLists(item.ID);
|
||||
addItemToLists(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
private void addItemToLists(MultiplayerPlaylistItem item)
|
||||
{
|
||||
var apiItem = Playlist.SingleOrDefault(i => i.ID == item.ID);
|
||||
var apiItem = client.Room?.Playlist.SingleOrDefault(i => i.ID == item.ID);
|
||||
|
||||
// Item could have been removed from the playlist while the local player was in gameplay.
|
||||
if (apiItem == null)
|
||||
return;
|
||||
|
||||
if (item.Expired)
|
||||
historyList.Items.Add(apiItem);
|
||||
historyList.Items.Add(new PlaylistItem(apiItem));
|
||||
else
|
||||
queueList.Items.Add(apiItem);
|
||||
queueList.Items.Add(new PlaylistItem(apiItem));
|
||||
}
|
||||
|
||||
private void removeItemFromLists(long item)
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
// 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.Allocation;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
public abstract partial class MultiplayerRoomComposite : OnlinePlayComposite
|
||||
{
|
||||
[CanBeNull]
|
||||
protected MultiplayerRoom Room => Client.Room;
|
||||
|
||||
[Resolved]
|
||||
protected MultiplayerClient Client { get; private set; }
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Client.RoomUpdated += invokeOnRoomUpdated;
|
||||
Client.LoadRequested += invokeOnRoomLoadRequested;
|
||||
Client.UserLeft += invokeUserLeft;
|
||||
Client.UserKicked += invokeUserKicked;
|
||||
Client.UserJoined += invokeUserJoined;
|
||||
Client.ItemAdded += invokeItemAdded;
|
||||
Client.ItemRemoved += invokeItemRemoved;
|
||||
Client.ItemChanged += invokeItemChanged;
|
||||
|
||||
OnRoomUpdated();
|
||||
}
|
||||
|
||||
private void invokeOnRoomUpdated() => Scheduler.AddOnce(OnRoomUpdated);
|
||||
private void invokeUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() => UserJoined(user));
|
||||
private void invokeUserKicked(MultiplayerRoomUser user) => Scheduler.Add(() => UserKicked(user));
|
||||
private void invokeUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() => UserLeft(user));
|
||||
private void invokeItemAdded(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemAdded(item));
|
||||
private void invokeItemRemoved(long item) => Schedule(() => PlaylistItemRemoved(item));
|
||||
private void invokeItemChanged(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemChanged(item));
|
||||
private void invokeOnRoomLoadRequested() => Scheduler.AddOnce(OnRoomLoadRequested);
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a user has joined the room.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
protected virtual void UserJoined(MultiplayerRoomUser user)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a user has been kicked from the room (including the local user).
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
protected virtual void UserKicked(MultiplayerRoomUser user)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a user has left the room.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
protected virtual void UserLeft(MultiplayerRoomUser user)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a playlist item is added to the room.
|
||||
/// </summary>
|
||||
/// <param name="item">The added playlist item.</param>
|
||||
protected virtual void PlaylistItemAdded(MultiplayerPlaylistItem item)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a playlist item is removed from the room.
|
||||
/// </summary>
|
||||
/// <param name="item">The ID of the removed playlist item.</param>
|
||||
protected virtual void PlaylistItemRemoved(long item)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a playlist item is changed in the room.
|
||||
/// </summary>
|
||||
/// <param name="item">The new playlist item, with an existing item's ID.</param>
|
||||
protected virtual void PlaylistItemChanged(MultiplayerPlaylistItem item)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when any change occurs to the multiplayer room.
|
||||
/// </summary>
|
||||
protected virtual void OnRoomUpdated()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the room requests the local user to load into gameplay.
|
||||
/// </summary>
|
||||
protected virtual void OnRoomLoadRequested()
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
if (Client != null)
|
||||
{
|
||||
Client.RoomUpdated -= invokeOnRoomUpdated;
|
||||
Client.LoadRequested -= invokeOnRoomLoadRequested;
|
||||
Client.UserLeft -= invokeUserLeft;
|
||||
Client.UserKicked -= invokeUserKicked;
|
||||
Client.UserJoined -= invokeUserJoined;
|
||||
Client.ItemAdded -= invokeItemAdded;
|
||||
Client.ItemRemoved -= invokeItemRemoved;
|
||||
Client.ItemChanged -= invokeItemChanged;
|
||||
}
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,26 @@
|
||||
// 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.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
public partial class MultiplayerRoomSounds : MultiplayerRoomComposite
|
||||
public partial class MultiplayerRoomSounds : CompositeDrawable
|
||||
{
|
||||
private Sample hostChangedSample;
|
||||
private Sample userJoinedSample;
|
||||
private Sample userLeftSample;
|
||||
private Sample userKickedSample;
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
private Sample? hostChangedSample;
|
||||
private Sample? userJoinedSample;
|
||||
private Sample? userLeftSample;
|
||||
private Sample? userKickedSample;
|
||||
private MultiplayerRoomUser? host;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
@@ -32,36 +35,47 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Host.BindValueChanged(hostChanged);
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
client.UserJoined += onUserJoined;
|
||||
client.UserLeft += onUserLeft;
|
||||
client.UserKicked += onUserKicked;
|
||||
updateState();
|
||||
}
|
||||
|
||||
protected override void UserJoined(MultiplayerRoomUser user)
|
||||
private void onRoomUpdated() => Scheduler.AddOnce(updateState);
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
base.UserJoined(user);
|
||||
if (EqualityComparer<MultiplayerRoomUser>.Default.Equals(host, client.Room?.Host))
|
||||
return;
|
||||
|
||||
Scheduler.AddOnce(() => userJoinedSample?.Play());
|
||||
}
|
||||
|
||||
protected override void UserLeft(MultiplayerRoomUser user)
|
||||
{
|
||||
base.UserLeft(user);
|
||||
|
||||
Scheduler.AddOnce(() => userLeftSample?.Play());
|
||||
}
|
||||
|
||||
protected override void UserKicked(MultiplayerRoomUser user)
|
||||
{
|
||||
base.UserKicked(user);
|
||||
|
||||
Scheduler.AddOnce(() => userKickedSample?.Play());
|
||||
}
|
||||
|
||||
private void hostChanged(ValueChangedEvent<APIUser> value)
|
||||
{
|
||||
// only play sound when the host changes from an already-existing host.
|
||||
if (value.OldValue == null) return;
|
||||
if (host != null)
|
||||
Scheduler.AddOnce(() => hostChangedSample?.Play());
|
||||
|
||||
Scheduler.AddOnce(() => hostChangedSample?.Play());
|
||||
host = client.Room?.Host;
|
||||
}
|
||||
|
||||
private void onUserJoined(MultiplayerRoomUser user)
|
||||
=> Scheduler.AddOnce(() => userJoinedSample?.Play());
|
||||
|
||||
private void onUserLeft(MultiplayerRoomUser user)
|
||||
=> Scheduler.AddOnce(() => userLeftSample?.Play());
|
||||
|
||||
private void onUserKicked(MultiplayerRoomUser user)
|
||||
=> Scheduler.AddOnce(() => userKickedSample?.Play());
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
{
|
||||
client.RoomUpdated -= onRoomUpdated;
|
||||
client.UserJoined -= onUserJoined;
|
||||
client.UserLeft -= onUserLeft;
|
||||
client.UserKicked -= onUserKicked;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@@ -30,7 +31,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
{
|
||||
public partial class ParticipantPanel : MultiplayerRoomComposite, IHasContextMenu
|
||||
public partial class ParticipantPanel : CompositeDrawable, IHasContextMenu
|
||||
{
|
||||
public readonly MultiplayerRoomUser User;
|
||||
|
||||
@@ -40,6 +41,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
[Resolved]
|
||||
private IRulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
private SpriteIcon crown = null!;
|
||||
|
||||
private OsuSpriteText userRankText = null!;
|
||||
@@ -171,23 +175,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
Origin = Anchor.Centre,
|
||||
Alpha = 0,
|
||||
Margin = new MarginPadding(4),
|
||||
Action = () => Client.KickUser(User.UserID).FireAndForget(),
|
||||
Action = () => client.KickUser(User.UserID).FireAndForget(),
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnRoomUpdated()
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.OnRoomUpdated();
|
||||
base.LoadComplete();
|
||||
|
||||
if (Room == null || Client.LocalUser == null)
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void onRoomUpdated() => Scheduler.AddOnce(updateState);
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
if (client.Room == null || client.LocalUser == null)
|
||||
return;
|
||||
|
||||
const double fade_time = 50;
|
||||
|
||||
var currentItem = Playlist.GetCurrentItem();
|
||||
MultiplayerPlaylistItem? currentItem = client.Room.GetCurrentItem();
|
||||
Ruleset? ruleset = currentItem != null ? rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance() : null;
|
||||
|
||||
int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null;
|
||||
@@ -200,8 +212,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
else
|
||||
userModsDisplay.FadeOut(fade_time);
|
||||
|
||||
kickButton.Alpha = Client.IsHost && !User.Equals(Client.LocalUser) ? 1 : 0;
|
||||
crown.Alpha = Room.Host?.Equals(User) == true ? 1 : 0;
|
||||
kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0;
|
||||
crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0;
|
||||
|
||||
// If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187
|
||||
// This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix.
|
||||
@@ -215,7 +227,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Room == null)
|
||||
if (client.Room == null)
|
||||
return null;
|
||||
|
||||
// If the local user is targetted.
|
||||
@@ -223,7 +235,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
return null;
|
||||
|
||||
// If the local user is not the host of the room.
|
||||
if (Room.Host?.UserID != api.LocalUser.Value.Id)
|
||||
if (client.Room.Host?.UserID != api.LocalUser.Value.Id)
|
||||
return null;
|
||||
|
||||
int targetUser = User.UserID;
|
||||
@@ -233,23 +245,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
new OsuMenuItem("Give host", MenuItemType.Standard, () =>
|
||||
{
|
||||
// Ensure the local user is still host.
|
||||
if (!Client.IsHost)
|
||||
if (!client.IsHost)
|
||||
return;
|
||||
|
||||
Client.TransferHost(targetUser).FireAndForget();
|
||||
client.TransferHost(targetUser).FireAndForget();
|
||||
}),
|
||||
new OsuMenuItem("Kick", MenuItemType.Destructive, () =>
|
||||
{
|
||||
// Ensure the local user is still host.
|
||||
if (!Client.IsHost)
|
||||
if (!client.IsHost)
|
||||
return;
|
||||
|
||||
Client.KickUser(targetUser).FireAndForget();
|
||||
client.KickUser(targetUser).FireAndForget();
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
client.RoomUpdated -= onRoomUpdated;
|
||||
}
|
||||
|
||||
public partial class KickButton : IconButton
|
||||
{
|
||||
public KickButton()
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
// 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.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
{
|
||||
public partial class ParticipantsList : MultiplayerRoomComposite
|
||||
public partial class ParticipantsList : CompositeDrawable
|
||||
{
|
||||
private FillFlowContainer<ParticipantPanel> panels;
|
||||
private FillFlowContainer<ParticipantPanel> panels = null!;
|
||||
private ParticipantPanel? currentHostPanel;
|
||||
|
||||
[CanBeNull]
|
||||
private ParticipantPanel currentHostPanel;
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
@@ -37,11 +37,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnRoomUpdated()
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.OnRoomUpdated();
|
||||
base.LoadComplete();
|
||||
|
||||
if (Room == null)
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void onRoomUpdated() => Scheduler.AddOnce(updateState);
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
if (client.Room == null)
|
||||
panels.Clear();
|
||||
else
|
||||
{
|
||||
@@ -49,15 +57,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
foreach (var p in panels)
|
||||
{
|
||||
// Note that we *must* use reference equality here, as this call is scheduled and a user may have left and joined since it was last run.
|
||||
if (Room.Users.All(u => !ReferenceEquals(p.User, u)))
|
||||
if (client.Room.Users.All(u => !ReferenceEquals(p.User, u)))
|
||||
p.Expire();
|
||||
}
|
||||
|
||||
// Add panels for all users new to the room.
|
||||
foreach (var user in Room.Users.Except(panels.Select(p => p.User)))
|
||||
foreach (var user in client.Room.Users.Except(panels.Select(p => p.User)))
|
||||
panels.Add(new ParticipantPanel(user));
|
||||
|
||||
if (currentHostPanel == null || !currentHostPanel.User.Equals(Room.Host))
|
||||
if (currentHostPanel == null || !currentHostPanel.User.Equals(client.Room.Host))
|
||||
{
|
||||
// Reset position of previous host back to normal, if one existing.
|
||||
if (currentHostPanel != null && panels.Contains(currentHostPanel))
|
||||
@@ -66,9 +74,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
currentHostPanel = null;
|
||||
|
||||
// Change position of new host to display above all participants.
|
||||
if (Room.Host != null)
|
||||
if (client.Room.Host != null)
|
||||
{
|
||||
currentHostPanel = panels.SingleOrDefault(u => u.User.Equals(Room.Host));
|
||||
currentHostPanel = panels.SingleOrDefault(u => u.User.Equals(client.Room.Host));
|
||||
|
||||
if (currentHostPanel != null)
|
||||
panels.SetLayoutPosition(currentHostPanel, -1);
|
||||
@@ -76,5 +84,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
client.RoomUpdated -= onRoomUpdated;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// 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.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@@ -20,27 +19,26 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
{
|
||||
internal partial class TeamDisplay : MultiplayerRoomComposite
|
||||
internal partial class TeamDisplay : CompositeDrawable
|
||||
{
|
||||
private readonly MultiplayerRoomUser user;
|
||||
|
||||
private Drawable box;
|
||||
|
||||
private Sample sampleTeamSwap;
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
private OsuClickableContainer clickableContent;
|
||||
private OsuClickableContainer clickableContent = null!;
|
||||
private Drawable box = null!;
|
||||
private Sample? sampleTeamSwap;
|
||||
|
||||
public TeamDisplay(MultiplayerRoomUser user)
|
||||
{
|
||||
this.user = user;
|
||||
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
|
||||
AutoSizeAxes = Axes.X;
|
||||
|
||||
Margin = new MarginPadding { Horizontal = 3 };
|
||||
}
|
||||
|
||||
@@ -71,7 +69,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
}
|
||||
};
|
||||
|
||||
if (Client.LocalUser?.Equals(user) == true)
|
||||
if (client.LocalUser?.Equals(user) == true)
|
||||
{
|
||||
clickableContent.Action = changeTeam;
|
||||
clickableContent.TooltipText = "Change team";
|
||||
@@ -80,23 +78,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
sampleTeamSwap = audio.Samples.Get(@"Multiplayer/team-swap");
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void changeTeam()
|
||||
{
|
||||
Client.SendMatchRequest(new ChangeTeamRequest
|
||||
client.SendMatchRequest(new ChangeTeamRequest
|
||||
{
|
||||
TeamID = ((Client.LocalUser?.MatchState as TeamVersusUserState)?.TeamID + 1) % 2 ?? 0,
|
||||
TeamID = ((client.LocalUser?.MatchState as TeamVersusUserState)?.TeamID + 1) % 2 ?? 0,
|
||||
}).FireAndForget();
|
||||
}
|
||||
|
||||
public int? DisplayedTeam { get; private set; }
|
||||
|
||||
protected override void OnRoomUpdated()
|
||||
{
|
||||
base.OnRoomUpdated();
|
||||
private void onRoomUpdated() => Scheduler.AddOnce(updateState);
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
// we don't have a way of knowing when an individual user's state has updated, so just handle on RoomUpdated for now.
|
||||
|
||||
var userRoomState = Room?.Users.FirstOrDefault(u => u.Equals(user))?.MatchState;
|
||||
var userRoomState = client.Room?.Users.FirstOrDefault(u => u.Equals(user))?.MatchState;
|
||||
|
||||
const double duration = 400;
|
||||
|
||||
@@ -138,5 +144,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
return colours.Blue;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
client.RoomUpdated -= onRoomUpdated;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ namespace osu.Game.Screens.Play.Break
|
||||
protected virtual LocalisableString Format(T count)
|
||||
{
|
||||
if (count is Enum countEnum)
|
||||
return countEnum.GetDescription();
|
||||
return countEnum.GetLocalisableDescription();
|
||||
|
||||
return count.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
@@ -300,7 +300,9 @@ namespace osu.Game.Screens.Play
|
||||
if (element is LegacyHealthDisplay)
|
||||
return;
|
||||
|
||||
float bottom = drawable.ScreenSpaceDrawQuad.BottomRight.Y;
|
||||
// AABB is used here because the drawable can be flipped/rotated arbitrarily,
|
||||
// so the "bottom right" corner of the raw SSDQ might not necessarily be where one expects it to be.
|
||||
float bottom = drawable.ScreenSpaceDrawQuad.AABBFloat.BottomRight.Y;
|
||||
|
||||
bool isRelativeX = drawable.RelativeSizeAxes == Axes.X;
|
||||
|
||||
@@ -319,7 +321,7 @@ namespace osu.Game.Screens.Play
|
||||
// and align bottom-right components with the top-edge of the highest bottom-anchored hud element.
|
||||
else if (drawable.Anchor.HasFlag(Anchor.BottomRight) || (drawable.Anchor.HasFlag(Anchor.y2) && drawable.RelativeSizeAxes == Axes.X))
|
||||
{
|
||||
var topLeft = element.ScreenSpaceDrawQuad.TopLeft;
|
||||
var topLeft = element.ScreenSpaceDrawQuad.AABBFloat.TopLeft;
|
||||
if (highestBottomScreenSpace == null || topLeft.Y < highestBottomScreenSpace.Value.Y)
|
||||
highestBottomScreenSpace = topLeft;
|
||||
}
|
||||
|
||||
@@ -216,18 +216,12 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private int visibleSetsCount;
|
||||
|
||||
public BeatmapCarousel(FilterCriteria initialCriterial)
|
||||
public BeatmapCarousel(FilterCriteria initialCriteria)
|
||||
{
|
||||
root = new CarouselRoot(this);
|
||||
InternalChild = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
// Avoid clash between scrollbar and osu! logo.
|
||||
Top = 10,
|
||||
Bottom = 100,
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
setPool,
|
||||
@@ -239,7 +233,7 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
};
|
||||
|
||||
activeCriteria = initialCriterial;
|
||||
activeCriteria = initialCriteria;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -1116,11 +1110,6 @@ namespace osu.Game.Screens.Select
|
||||
// adjusting the item's overall X position can cause it to become masked away when
|
||||
// child items (difficulties) are still visible.
|
||||
item.Header.X = offsetX(dist, visibleHalfHeight) - (parent?.X ?? 0);
|
||||
|
||||
// We are applying a multiplicative alpha (which is internally done by nesting an
|
||||
// additional container and setting that container's alpha) such that we can
|
||||
// layer alpha transformations on top.
|
||||
item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1));
|
||||
}
|
||||
|
||||
private enum PendingScrollOperation
|
||||
@@ -1271,6 +1260,38 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
return base.OnDragStart(e);
|
||||
}
|
||||
|
||||
protected override ScrollbarContainer CreateScrollbar(Direction direction)
|
||||
{
|
||||
return new PaddedScrollbar();
|
||||
}
|
||||
|
||||
protected partial class PaddedScrollbar : OsuScrollbar
|
||||
{
|
||||
public PaddedScrollbar()
|
||||
: base(Direction.Vertical)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private const float top_padding = 10;
|
||||
private const float bottom_padding = 70;
|
||||
|
||||
protected override float ToScrollbarPosition(float scrollPosition)
|
||||
{
|
||||
if (Precision.AlmostEquals(0, ScrollableExtent))
|
||||
return 0;
|
||||
|
||||
return top_padding + (ScrollbarMovementExtent - (top_padding + bottom_padding)) * (scrollPosition / ScrollableExtent);
|
||||
}
|
||||
|
||||
protected override float FromScrollbarPosition(float scrollbarPosition)
|
||||
{
|
||||
if (Precision.AlmostEquals(0, ScrollbarMovementExtent))
|
||||
return 0;
|
||||
|
||||
return ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - (top_padding + bottom_padding)));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
||||
@@ -86,8 +86,6 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
};
|
||||
}
|
||||
|
||||
public void SetMultiplicativeAlpha(float alpha) => Header.BorderContainer.Alpha = alpha;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
@@ -28,7 +28,6 @@ using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
@@ -97,8 +96,8 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = Color4.Black,
|
||||
Alpha = 0.8f,
|
||||
Colour = OsuColour.Gray(0.05f),
|
||||
Alpha = 0.96f,
|
||||
Width = 2,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
|
||||
@@ -6,12 +6,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
@@ -82,14 +81,15 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = Vector2.One,
|
||||
Colour = Color4.Black.Opacity(0.5f),
|
||||
Colour = OsuColour.Gray(0.1f),
|
||||
Alpha = 0.96f,
|
||||
},
|
||||
modeLight = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 3,
|
||||
Position = new Vector2(0, -3),
|
||||
Colour = Color4.Black,
|
||||
Colour = OsuColour.Gray(0.1f),
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
|
||||
@@ -711,12 +711,6 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
Carousel.AllowSelection = true;
|
||||
|
||||
if (pendingFilterApplication)
|
||||
{
|
||||
Carousel.Filter(FilterControl.CreateCriteria());
|
||||
pendingFilterApplication = false;
|
||||
}
|
||||
|
||||
BeatmapDetails.Refresh();
|
||||
|
||||
beginLooping();
|
||||
@@ -749,6 +743,17 @@ namespace osu.Game.Screens.Select
|
||||
FilterControl.Activate();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (Carousel.AllowSelection && pendingFilterApplication)
|
||||
{
|
||||
Carousel.Filter(FilterControl.CreateCriteria());
|
||||
pendingFilterApplication = false;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnSuspending(ScreenTransitionEvent e)
|
||||
{
|
||||
// Handle the case where FinaliseSelection is never called (ie. when a screen is pushed externally).
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||
<PackageReference Include="DiffPlex" Version="1.7.2" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.67" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.70" />
|
||||
<PackageReference Include="Humanizer" Version="2.14.1" />
|
||||
<PackageReference Include="MessagePack" Version="2.5.187" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.10" />
|
||||
@@ -35,9 +35,9 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="11.5.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2024.1025.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2024.1111.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.1106.0" />
|
||||
<PackageReference Include="Sentry" Version="4.12.1" />
|
||||
<PackageReference Include="Sentry" Version="4.13.0" />
|
||||
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||
<PackageReference Include="SharpCompress" Version="0.38.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
|
||||
Reference in New Issue
Block a user