1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-05 09:42:54 +08:00

Merge branch 'master' into stat_acc_anti_rake

This commit is contained in:
Givikap120 2024-11-15 01:29:42 +02:00
commit 2c8c31876d
119 changed files with 1851 additions and 1311 deletions

View File

@ -0,0 +1,228 @@
name: "🔒diffcalc (do not use)"
on:
workflow_call:
inputs:
id:
type: string
head-sha:
type: string
pr-url:
type: string
pr-text:
type: string
dispatch-inputs:
type: string
outputs:
target:
description: The comparison target.
value: ${{ jobs.generator.outputs.target }}
sheet:
description: The comparison spreadsheet.
value: ${{ jobs.generator.outputs.sheet }}
secrets:
DIFFCALC_GOOGLE_CREDENTIALS:
required: true
env:
GENERATOR_DIR: ${{ github.workspace }}/${{ inputs.id }}
GENERATOR_ENV: ${{ github.workspace }}/${{ inputs.id }}/.env
defaults:
run:
shell: bash -euo pipefail {0}
jobs:
generator:
name: Run
runs-on: self-hosted
timeout-minutes: 720
outputs:
target: ${{ steps.run.outputs.target }}
sheet: ${{ steps.run.outputs.sheet }}
steps:
- name: Checkout diffcalc-sheet-generator
uses: actions/checkout@v4
with:
path: ${{ inputs.id }}
repository: 'smoogipoo/diffcalc-sheet-generator'
- name: Add base environment
env:
GOOGLE_CREDS_FILE: ${{ github.workspace }}/${{ inputs.id }}/google-credentials.json
VARS_JSON: ${{ (vars != null && toJSON(vars)) || '' }}
run: |
# Required by diffcalc-sheet-generator
cp '${{ env.GENERATOR_DIR }}/.env.sample' "${{ env.GENERATOR_ENV }}"
# Add Google credentials
echo '${{ secrets.DIFFCALC_GOOGLE_CREDENTIALS }}' | base64 -d > "${{ env.GOOGLE_CREDS_FILE }}"
# Add repository variables
echo "${VARS_JSON}" | jq -c '. | to_entries | .[]' | while read -r line; do
opt=$(jq -r '.key' <<< ${line})
val=$(jq -r '.value' <<< ${line})
if [[ "${opt}" =~ ^DIFFCALC_ ]]; then
optNoPrefix=$(echo "${opt}" | cut -d '_' -f2-)
sed -i "s;^${optNoPrefix}=.*$;${optNoPrefix}=${val};" "${{ env.GENERATOR_ENV }}"
fi
done
- name: Add HEAD environment
run: |
sed -i "s;^OSU_A=.*$;OSU_A=${{ inputs.head-sha }};" "${{ env.GENERATOR_ENV }}"
- name: Add pull-request environment
if: ${{ inputs.pr-url != '' }}
run: |
sed -i "s;^OSU_B=.*$;OSU_B=${{ inputs.pr-url }};" "${{ env.GENERATOR_ENV }}"
- name: Add comment environment
if: ${{ inputs.pr-text != '' }}
env:
PR_TEXT: ${{ inputs.pr-text }}
run: |
# Add comment environment
echo "${PR_TEXT}" | sed -r 's/\r$//' | { grep -E '^\w+=' || true; } | while read -r line; do
opt=$(echo "${line}" | cut -d '=' -f1)
sed -i "s;^${opt}=.*$;${line};" "${{ env.GENERATOR_ENV }}"
done
- name: Add dispatch environment
if: ${{ inputs.dispatch-inputs != '' }}
env:
DISPATCH_INPUTS_JSON: ${{ inputs.dispatch-inputs }}
run: |
function get_input() {
echo "${DISPATCH_INPUTS_JSON}" | jq -r ".\"$1\""
}
osu_a=$(get_input 'osu-a')
osu_b=$(get_input 'osu-b')
ruleset=$(get_input 'ruleset')
generators=$(get_input 'generators')
difficulty_calculator_a=$(get_input 'difficulty-calculator-a')
difficulty_calculator_b=$(get_input 'difficulty-calculator-b')
score_processor_a=$(get_input 'score-processor-a')
score_processor_b=$(get_input 'score-processor-b')
converts=$(get_input 'converts')
ranked_only=$(get_input 'ranked-only')
sed -i "s;^OSU_B=.*$;OSU_B=${osu_b};" "${{ env.GENERATOR_ENV }}"
sed -i "s/^RULESET=.*$/RULESET=${ruleset}/" "${{ env.GENERATOR_ENV }}"
sed -i "s/^GENERATORS=.*$/GENERATORS=${generators}/" "${{ env.GENERATOR_ENV }}"
if [[ "${osu_a}" != 'latest' ]]; then
sed -i "s;^OSU_A=.*$;OSU_A=${osu_a};" "${{ env.GENERATOR_ENV }}"
fi
if [[ "${difficulty_calculator_a}" != 'latest' ]]; then
sed -i "s;^DIFFICULTY_CALCULATOR_A=.*$;DIFFICULTY_CALCULATOR_A=${difficulty_calculator_a};" "${{ env.GENERATOR_ENV }}"
fi
if [[ "${difficulty_calculator_b}" != 'latest' ]]; then
sed -i "s;^DIFFICULTY_CALCULATOR_B=.*$;DIFFICULTY_CALCULATOR_B=${difficulty_calculator_b};" "${{ env.GENERATOR_ENV }}"
fi
if [[ "${score_processor_a}" != 'latest' ]]; then
sed -i "s;^SCORE_PROCESSOR_A=.*$;SCORE_PROCESSOR_A=${score_processor_a};" "${{ env.GENERATOR_ENV }}"
fi
if [[ "${score_processor_b}" != 'latest' ]]; then
sed -i "s;^SCORE_PROCESSOR_B=.*$;SCORE_PROCESSOR_B=${score_processor_b};" "${{ env.GENERATOR_ENV }}"
fi
if [[ "${converts}" == 'true' ]]; then
sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=0/' "${{ env.GENERATOR_ENV }}"
else
sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=1/' "${{ env.GENERATOR_ENV }}"
fi
if [[ "${ranked_only}" == 'true' ]]; then
sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=1/' "${{ env.GENERATOR_ENV }}"
else
sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=0/' "${{ env.GENERATOR_ENV }}"
fi
- name: Query latest scores
id: query-scores
run: |
ruleset=$(cat ${{ env.GENERATOR_ENV }} | grep -E '^RULESET=' | cut -d '=' -f2-)
performance_data_name=$(curl -s "https://data.ppy.sh/" | grep "performance_${ruleset}_top_1000\b" | tail -1 | awk -F "'" '{print $2}' | sed 's/\.tar\.bz2//g')
echo "TARGET_DIR=${{ env.GENERATOR_DIR }}/sql/${ruleset}" >> "${GITHUB_OUTPUT}"
echo "DATA_NAME=${performance_data_name}" >> "${GITHUB_OUTPUT}"
echo "DATA_PKG=${performance_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}"
- name: Restore score cache
id: restore-score-cache
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
with:
path: ${{ steps.query-scores.outputs.DATA_PKG }}
key: ${{ steps.query-scores.outputs.DATA_NAME }}
- name: Download scores
if: steps.restore-score-cache.outputs.cache-hit != 'true'
run: |
wget -q -O "${{ steps.query-scores.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query-scores.outputs.DATA_PKG }}"
- name: Extract scores
run: |
tar -I lbzip2 -xf "${{ steps.query-scores.outputs.DATA_PKG }}"
rm -r "${{ steps.query-scores.outputs.TARGET_DIR }}"
mv "${{ steps.query-scores.outputs.DATA_NAME }}" "${{ steps.query-scores.outputs.TARGET_DIR }}"
- name: Query latest beatmaps
id: query-beatmaps
run: |
beatmaps_data_name=$(curl -s "https://data.ppy.sh/" | grep "osu_files" | tail -1 | awk -F "'" '{print $2}' | sed 's/\.tar\.bz2//g')
echo "TARGET_DIR=${{ env.GENERATOR_DIR }}/beatmaps" >> "${GITHUB_OUTPUT}"
echo "DATA_NAME=${beatmaps_data_name}" >> "${GITHUB_OUTPUT}"
echo "DATA_PKG=${beatmaps_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}"
- name: Restore beatmap cache
id: restore-beatmap-cache
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
with:
path: ${{ steps.query-beatmaps.outputs.DATA_PKG }}
key: ${{ steps.query-beatmaps.outputs.DATA_NAME }}
- name: Download beatmap
if: steps.restore-beatmap-cache.outputs.cache-hit != 'true'
run: |
wget -q -O "${{ steps.query-beatmaps.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query-beatmaps.outputs.DATA_PKG }}"
- name: Extract beatmap
run: |
tar -I lbzip2 -xf "${{ steps.query-beatmaps.outputs.DATA_PKG }}"
rm -r "${{ steps.query-beatmaps.outputs.TARGET_DIR }}"
mv "${{ steps.query-beatmaps.outputs.DATA_NAME }}" "${{ steps.query-beatmaps.outputs.TARGET_DIR }}"
- name: Run
id: run
run: |
# Add the GitHub token. This needs to be done here because it's unique per-job.
sed -i 's/^GH_TOKEN=.*$/GH_TOKEN=${{ github.token }}/' "${{ env.GENERATOR_ENV }}"
cd "${{ env.GENERATOR_DIR }}"
docker compose up --build --detach
docker compose logs --follow &
docker compose wait generator
link=$(docker compose logs --tail 10 generator | grep 'http' | sed -E 's/^.*(http.*)$/\1/')
target=$(cat "${{ env.GENERATOR_ENV }}" | grep -E '^OSU_B=' | cut -d '=' -f2-)
echo "target=${target}" >> "${GITHUB_OUTPUT}"
echo "sheet=${link}" >> "${GITHUB_OUTPUT}"
- name: Shutdown
if: ${{ always() }}
run: |
cd "${{ env.GENERATOR_DIR }}"
docker compose down --volumes
rm -rf "${{ env.GENERATOR_DIR }}"

View File

@ -103,27 +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
if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.comment.body, '!diffcalc') }}
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
@ -139,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:
@ -154,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 }}
@ -405,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 }}

View File

@ -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'

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1025.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1112.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -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
}
}

View File

@ -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" />

View File

@ -5,6 +5,7 @@ using System;
using osu.Framework.Utils;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods;
@ -73,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
// 0.0 +--------+-+---------------> Release Difference / ms
// release_threshold
if (isOverlapping)
holdAddition = 1 / (1 + Math.Exp(0.27 * (release_threshold - closestEndTime)));
holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold);
// Decay and increase individualStrains in own column
individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base);

View File

@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Mania.Edit
base.Update();
if (screenWithTimeline?.TimelineArea.Timeline != null)
drawableRuleset.TimelineTimeRange = EditorClock.TrackLength / screenWithTimeline.TimelineArea.Timeline.CurrentZoom / 2;
drawableRuleset.TimelineTimeRange = EditorClock.TrackLength / screenWithTimeline.TimelineArea.Timeline.CurrentZoom.Value / 2;
}
}
}

View File

@ -54,7 +54,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
},
columnBackgrounds = new ColumnFlow<Drawable>(stageDefinition)
{
RelativeSizeAxes = Axes.Y
RelativeSizeAxes = Axes.Y,
Masking = false,
},
new HitTargetInsetContainer
{
@ -126,8 +127,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
},
new Container
{
X = isLastColumn ? -0.16f : 0,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.Y,
Width = rightLineWidth,
Scale = new Vector2(0.740f, 1),

View File

@ -28,6 +28,12 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly FillFlowContainer<Container<TContent>> columns;
private readonly StageDefinition stageDefinition;
public new bool Masking
{
get => base.Masking;
set => base.Masking = value;
}
public ColumnFlow(StageDefinition stageDefinition)
{
this.stageDefinition = stageDefinition;

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.RadioButtons;
@ -24,38 +25,57 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
[Test]
public void TestTouchInputAfterTouchingComposeArea()
public void TestTouchInputPlaceHitCircleDirectly()
{
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "HitCircle")));
// this input is just for interacting with compose area
AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single()));
AddStep("move current time", () => InputManager.Key(Key.Right));
AddStep("tap to place circle", () => tap(this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(10, 10))));
AddStep("tap to place circle", () => tap(this.ChildrenOfType<Playfield>().Single()));
AddAssert("circle placed correctly", () =>
{
var circle = (HitCircle)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate);
Assert.Multiple(() =>
{
Assert.That(circle.Position.X, Is.EqualTo(10f).Within(0.01f));
Assert.That(circle.Position.Y, Is.EqualTo(10f).Within(0.01f));
Assert.That(circle.Position.X, Is.EqualTo(256f).Within(0.01f));
Assert.That(circle.Position.Y, Is.EqualTo(192f).Within(0.01f));
});
return true;
});
}
AddStep("tap slider", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "Slider")));
[Test]
public void TestTouchInputPlaceCircleAfterTouchingComposeArea()
{
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "HitCircle")));
// this input is just for interacting with compose area
AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single()));
AddAssert("circle placed", () => EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate) is HitCircle);
AddStep("move current time", () => InputManager.Key(Key.Right));
AddStep("move forward", () => InputManager.Key(Key.Right));
AddStep("tap to place circle", () => tap(this.ChildrenOfType<Playfield>().Single()));
AddAssert("circle placed correctly", () =>
{
var circle = (HitCircle)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate);
Assert.Multiple(() =>
{
Assert.That(circle.Position.X, Is.EqualTo(256f).Within(0.01f));
Assert.That(circle.Position.Y, Is.EqualTo(192f).Within(0.01f));
});
return true;
});
}
[Test]
public void TestTouchInputPlaceSliderDirectly()
{
AddStep("tap slider", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "Slider")));
AddStep("hold to draw slider", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(50, 20)))));
AddStep("drag to draw", () => InputManager.MoveTouchTo(new Touch(TouchSource.Touch1, this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(200, 50)))));
AddAssert("selection not initiated", () => this.ChildrenOfType<DragBox>().All(d => d.State == Visibility.Hidden));
AddAssert("blueprint visible", () => this.ChildrenOfType<SliderPlacementBlueprint>().Single().Alpha > 0);
AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value)));
AddAssert("slider placed correctly", () =>
{
@ -76,12 +96,55 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
}
private void tap(Drawable drawable) => tap(drawable.ScreenSpaceDrawQuad.Centre);
[Test]
public void TestTouchInputPlaceSliderAfterTouchingComposeArea()
{
AddStep("tap slider", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "Slider")));
AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single()));
AddStep("tap and hold another spot", () => hold(this.ChildrenOfType<Playfield>().Single(), new Vector2(50, 0)));
AddUntilStep("wait for slider placement", () => EditorBeatmap.HitObjects.SingleOrDefault(h => h.StartTime == EditorClock.CurrentTimeAccurate) is Slider);
AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value)));
AddStep("move forward", () => InputManager.Key(Key.Right));
AddStep("hold to draw slider", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(50, 20)))));
AddStep("drag to draw", () => InputManager.MoveTouchTo(new Touch(TouchSource.Touch1, this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(200, 50)))));
AddAssert("selection not initiated", () => this.ChildrenOfType<DragBox>().All(d => d.State == Visibility.Hidden));
AddAssert("blueprint visible", () => this.ChildrenOfType<SliderPlacementBlueprint>().Single().IsPresent);
AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value)));
AddAssert("slider placed correctly", () =>
{
var slider = (Slider)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate);
Assert.Multiple(() =>
{
Assert.That(slider.Position.X, Is.EqualTo(50f).Within(0.01f));
Assert.That(slider.Position.Y, Is.EqualTo(20f).Within(0.01f));
Assert.That(slider.Path.ControlPoints.Count, Is.EqualTo(2));
Assert.That(slider.Path.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
// the final position may be slightly off from the mouse position when drawing, account for that.
Assert.That(slider.Path.ControlPoints[1].Position.X, Is.EqualTo(150).Within(5));
Assert.That(slider.Path.ControlPoints[1].Position.Y, Is.EqualTo(30).Within(5));
});
return true;
});
}
private void tap(Drawable drawable, Vector2 offset = default) => tap(drawable.ToScreenSpace(drawable.LayoutRectangle.Centre + offset));
private void tap(Vector2 position)
{
InputManager.BeginTouch(new Touch(TouchSource.Touch1, position));
hold(position);
InputManager.EndTouch(new Touch(TouchSource.Touch1, position));
}
private void hold(Drawable drawable, Vector2 offset = default) => hold(drawable.ToScreenSpace(drawable.LayoutRectangle.Centre + offset));
private void hold(Vector2 position)
{
InputManager.BeginTouch(new Touch(TouchSource.Touch1, position));
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;
@ -33,6 +34,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
var osuLastObj = (OsuDifficultyHitObject)current.Previous(0);
var osuLastLastObj = (OsuDifficultyHitObject)current.Previous(1);
const int radius = OsuDifficultyHitObject.NORMALISED_RADIUS;
const int diameter = OsuDifficultyHitObject.NORMALISED_DIAMETER;
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
@ -77,14 +81,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
wideAngleBonus = calcWideAngleBonus(currAngle);
acuteAngleBonus = calcAcuteAngleBonus(currAngle);
if (osuCurrObj.StrainTime > 100) // Only buff deltaTime exceeding 300 bpm 1/2.
if (DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2) < 300) // Only buff deltaTime exceeding 300 bpm 1/2.
acuteAngleBonus = 0;
else
{
acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
* Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
* Math.Min(angleBonus, diameter * 1.25 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter).
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, radius, diameter) - radius) / radius), 2); // Buff distance exceeding radius up to diameter.
}
// Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
@ -104,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2);
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
double overlapVelocityBuff = Math.Min(125 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity));
double overlapVelocityBuff = Math.Min(diameter * 1.25 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity));
velocityChangeBonus = overlapVelocityBuff * distRatio;

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;
@ -120,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
islandCount.Count++;
// repeated island (ex: triplet -> triplet)
double power = logistic(island.Delta, 2.75, 0.24, 14);
double power = DifficultyCalculationUtils.Logistic(island.Delta, maxValue: 2.75, multiplier: 0.24, midpointOffset: 58.33);
effectiveRatio *= Math.Min(3.0 / islandCount.Count, Math.Pow(1.0 / islandCount.Count, power));
islandCounts[countIndex] = (islandCount.Island, islandCount.Count);
@ -172,8 +173,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though)
}
private static double logistic(double x, double maxValue, double multiplier, double offset) => (maxValue / (1 + Math.Pow(Math.E, offset - (multiplier * x))));
private class Island : IEquatable<Island>
{
private readonly double deltaDifferenceEpsilon;

View File

@ -3,6 +3,7 @@
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;
@ -10,8 +11,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
{
public static class SpeedEvaluator
{
private const double single_spacing_threshold = 125; // 1.25 circles distance between centers
private const double min_speed_bonus = 75; // ~200BPM
private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers
private const double min_speed_bonus = 200; // 200 BPM 1/4th
private const double speed_balancing_factor = 40;
private const double distance_multiplier = 0.94;
@ -43,8 +44,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
double speedBonus = 0.0;
// Add additional scaling bonus for streams/bursts higher than 200bpm
if (strainTime < min_speed_bonus)
speedBonus = 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
if (DifficultyCalculationUtils.MillisecondsToBPM(strainTime) > min_speed_bonus)
speedBonus = 0.75 * Math.Pow((DifficultyCalculationUtils.BPMToMilliseconds(min_speed_bonus) - strainTime) / speed_balancing_factor, 2);
double travelDistance = osuPrevObj?.TravelDistance ?? 0;
double distance = travelDistance + osuCurrObj.MinimumJumpDistance;

View File

@ -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))
{

View File

@ -196,7 +196,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;

View File

@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
/// </summary>
public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
public const int NORMALISED_DIAMETER = NORMALISED_RADIUS * 2;
public const int MIN_DELTA_TIME = 25;
private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f;

View File

@ -34,7 +34,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
currentStrain *= strainDecay(current.DeltaTime);
currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier;
ObjectStrains.Add(currentStrain);
return currentStrain;
}

View File

@ -23,9 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// </summary>
protected virtual double ReducedStrainBaseline => 0.75;
protected List<double> ObjectStrains = new List<double>();
protected double Difficulty;
protected OsuStrainSkill(Mod[] mods)
: base(mods)
{
@ -33,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
public override double DifficultyValue()
{
Difficulty = 0;
double difficulty = 0;
double weight = 1;
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
@ -53,25 +50,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
// We're sorting from highest to lowest strain.
foreach (double strain in strains.OrderDescending())
{
Difficulty += strain * weight;
difficulty += strain * weight;
weight *= DecayWeight;
}
return Difficulty;
}
/// <summary>
/// Returns the number of strains weighted against the top strain.
/// The result is scaled by clock rate as it affects the total number of strains.
/// </summary>
public double CountDifficultStrains()
{
if (Difficulty == 0)
return 0.0;
double consistentTopStrain = Difficulty / 10; // What would the top strain be if all strain values were identical
// Use a weighted sum of all strains. Constants are arbitrary and give nice values
return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88))));
return difficulty;
}
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;

View File

@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
double totalStrain = currentStrain * currentRhythm;
ObjectStrains.Add(totalStrain);
return totalStrain;
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osuTK;
@ -31,12 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
public override void EndPlacement(bool commit)
{
if (!commit && PlacementActive != PlacementState.Finished)
{
gridToolboxGroup.StartPosition.Value = originalOrigin;
gridToolboxGroup.Spacing.Value = originalSpacing;
if (!gridToolboxGroup.GridLinesRotation.Disabled)
gridToolboxGroup.GridLinesRotation.Value = originalRotation;
}
resetGridState();
base.EndPlacement(commit);
@ -103,6 +99,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
public override void UpdateTimeAndPosition(SnapResult result)
{
if (State.Value == Visibility.Hidden)
return;
var pos = ToLocalSpace(result.ScreenSpacePosition);
if (PlacementActive != PlacementState.Active)
@ -122,5 +121,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
}
}
}
protected override void PopOut()
{
base.PopOut();
resetGridState();
}
private void resetGridState()
{
gridToolboxGroup.StartPosition.Value = originalOrigin;
gridToolboxGroup.Spacing.Value = originalSpacing;
if (!gridToolboxGroup.GridLinesRotation.Disabled)
gridToolboxGroup.GridLinesRotation.Value = originalRotation;
}
}
}

View File

@ -156,6 +156,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return true;
}
// this allows sliders to be drawn outside compose area (after starting from a point within the compose area).
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || PlacementActive == PlacementState.Active;
// ReceivePositionalInputAtSubTree generally always returns true when masking is disabled, but we don't want that,
// otherwise a slider path tooltip will be displayed anywhere in the editor (outside compose area).
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => ReceivePositionalInputAt(screenSpacePos);
private void beginNewSegment(PathControlPoint lastPoint)
{
segmentStart = lastPoint;

View File

@ -50,9 +50,14 @@ namespace osu.Game.Rulesets.Osu.Edit
[Resolved]
private HitObjectComposer composer { get; set; } = null!;
private Bindable<TernaryState> newComboState = null!;
[BackgroundDependencyLoader]
private void load()
{
var selectionHandler = (EditorSelectionHandler)composer.BlueprintContainer.SelectionHandler;
newComboState = selectionHandler.SelectionNewComboState.GetBoundCopy();
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
Child = new FillFlowContainer
@ -120,10 +125,11 @@ namespace osu.Game.Rulesets.Osu.Edit
changeHandler?.BeginChange();
began = true;
distanceSnapInput.Current.BindValueChanged(_ => tryCreatePolygon());
offsetAngleInput.Current.BindValueChanged(_ => tryCreatePolygon());
repeatCountInput.Current.BindValueChanged(_ => tryCreatePolygon());
pointInput.Current.BindValueChanged(_ => tryCreatePolygon());
distanceSnapInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon));
offsetAngleInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon));
repeatCountInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon));
pointInput.Current.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon));
newComboState.BindValueChanged(_ => Scheduler.AddOnce(tryCreatePolygon));
tryCreatePolygon();
}
@ -138,39 +144,69 @@ namespace osu.Game.Rulesets.Osu.Edit
double length = distanceSnapInput.Current.Value * velocity * timeSpacing;
float polygonRadius = (float)(length / (2 * Math.Sin(double.Pi / pointInput.Current.Value)));
editorBeatmap.RemoveRange(insertedCircles);
insertedCircles.Clear();
int totalPoints = pointInput.Current.Value * repeatCountInput.Current.Value;
var selectionHandler = (EditorSelectionHandler)composer.BlueprintContainer.SelectionHandler;
bool first = true;
for (int i = 1; i <= pointInput.Current.Value * repeatCountInput.Current.Value; ++i)
if (insertedCircles.Count > totalPoints)
{
float angle = float.DegreesToRadians(offsetAngleInput.Current.Value) + i * (2 * float.Pi / pointInput.Current.Value);
var position = OsuPlayfield.BASE_SIZE / 2 + new Vector2(polygonRadius * float.Cos(angle), polygonRadius * float.Sin(angle));
editorBeatmap.RemoveRange(insertedCircles.GetRange(totalPoints, insertedCircles.Count - totalPoints));
insertedCircles.RemoveRange(totalPoints, insertedCircles.Count - totalPoints);
}
var circle = new HitCircle
var newlyAdded = new List<HitCircle>();
for (int i = 0; i < totalPoints; ++i)
{
float angle = float.DegreesToRadians(offsetAngleInput.Current.Value) + (i + 1) * (2 * float.Pi / pointInput.Current.Value);
var position = OsuPlayfield.BASE_SIZE / 2 + new Vector2(polygonRadius * float.Cos(angle), polygonRadius * float.Sin(angle));
bool newCombo = i == 0 && newComboState.Value == TernaryState.True;
HitCircle circle;
if (i < insertedCircles.Count)
{
Position = position,
StartTime = startTime,
NewCombo = first && selectionHandler.SelectionNewComboState.Value == TernaryState.True,
};
// TODO: probably ensure samples also follow current ternary status (not trivial)
circle.Samples.Add(circle.CreateHitSampleInfo());
circle = insertedCircles[i];
circle.Position = position;
circle.StartTime = startTime;
circle.NewCombo = newCombo;
editorBeatmap.Update(circle);
}
else
{
circle = new HitCircle
{
Position = position,
StartTime = startTime,
NewCombo = newCombo,
};
newlyAdded.Add(circle);
// TODO: probably ensure samples also follow current ternary status (not trivial)
circle.Samples.Add(circle.CreateHitSampleInfo());
}
if (position.X < 0 || position.Y < 0 || position.X > OsuPlayfield.BASE_SIZE.X || position.Y > OsuPlayfield.BASE_SIZE.Y)
{
commitButton.Enabled.Value = false;
editorBeatmap.RemoveRange(insertedCircles);
insertedCircles.Clear();
return;
}
insertedCircles.Add(circle);
startTime = beatSnapProvider.SnapTime(startTime + timeSpacing);
first = false;
}
editorBeatmap.AddRange(insertedCircles);
var previousNewComboState = newComboState.Value;
insertedCircles.AddRange(newlyAdded);
editorBeatmap.AddRange(newlyAdded);
// When adding new hitObjects, newCombo state will get reset to false when no objects are selected.
// Since this is the case when this popover is showing, we need to restore the previous newCombo state
newComboState.Value = previousNewComboState;
commitButton.Enabled.Value = true;
}

View File

@ -14,6 +14,7 @@ using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Shaders.Types;
using osu.Framework.Graphics.Textures;
using osu.Framework.Graphics.Visualisation;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Timing;
@ -23,6 +24,7 @@ using osuTK.Graphics.ES30;
namespace osu.Game.Rulesets.Osu.UI.Cursor
{
[DrawVisualiserHidden]
public partial class CursorTrail : Drawable, IRequireHighFrequencyMousePosition
{
private const int max_sprites = 2048;

View File

@ -3,6 +3,7 @@
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data;
@ -11,26 +12,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
{
public class ColourEvaluator
{
/// <summary>
/// A sigmoid function. It gives a value between (middle - height/2) and (middle + height/2).
/// </summary>
/// <param name="val">The input value.</param>
/// <param name="center">The center of the sigmoid, where the largest gradient occurs and value is equal to middle.</param>
/// <param name="width">The radius of the sigmoid, outside of which values are near the minimum/maximum.</param>
/// <param name="middle">The middle of the sigmoid output.</param>
/// <param name="height">The height of the sigmoid output. This will be equal to max value - min value.</param>
private static double sigmoid(double val, double center, double width, double middle, double height)
{
double sigmoid = Math.Tanh(Math.E * -(val - center) / width);
return sigmoid * (height / 2) + middle;
}
/// <summary>
/// Evaluate the difficulty of the first note of a <see cref="MonoStreak"/>.
/// </summary>
public static double EvaluateDifficultyOf(MonoStreak monoStreak)
{
return sigmoid(monoStreak.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5;
return DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5;
}
/// <summary>
@ -38,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
/// </summary>
public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern)
{
return sigmoid(alternatingMonoPattern.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(alternatingMonoPattern.Parent);
return DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * EvaluateDifficultyOf(alternatingMonoPattern.Parent);
}
/// <summary>
@ -46,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
/// </summary>
public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern)
{
return 2 * (1 - sigmoid(repeatingHitPattern.RepetitionInterval, 2, 2, 0.5, 1));
return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E));
}
public static double EvaluateDifficultyOf(DifficultyHitObject hitObject)

View File

@ -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);
}
});
}

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -0,0 +1,63 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.IO;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual;
using MemoryStream = System.IO.MemoryStream;
namespace osu.Game.Tests.Beatmaps.IO
{
[HeadlessTest]
public partial class LegacyBeatmapExporterTest : OsuTestScene
{
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
[Test]
public void TestObjectsSnappedAfterTruncatingExport()
{
IWorkingBeatmap beatmap = null!;
MemoryStream outStream = null!;
// Ensure importer encoding is correct
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"decimal-timing-beatmap.olz"));
AddAssert("timing point has decimal offset", () => beatmap.Beatmap.ControlPointInfo.TimingPoints[0].Time, () => Is.EqualTo(284.725).Within(0.001));
AddAssert("kiai has decimal offset", () => beatmap.Beatmap.ControlPointInfo.EffectPoints[0].Time, () => Is.EqualTo(28520.019).Within(0.001));
AddAssert("hit object has decimal offset", () => beatmap.Beatmap.HitObjects[0].StartTime, () => Is.EqualTo(28520.019).Within(0.001));
// Ensure exporter legacy conversion is correct
AddStep("export", () =>
{
outStream = new MemoryStream();
new LegacyBeatmapExporter(LocalStorage)
.ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
});
AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
AddAssert("timing point has truncated offset", () => beatmap.Beatmap.ControlPointInfo.TimingPoints[0].Time, () => Is.EqualTo(284).Within(0.001));
AddAssert("kiai is snapped", () => beatmap.Beatmap.ControlPointInfo.EffectPoints[0].Time, () => Is.EqualTo(28519).Within(0.001));
AddAssert("hit object is snapped", () => beatmap.Beatmap.HitObjects[0].StartTime, () => Is.EqualTo(28519).Within(0.001));
}
private IWorkingBeatmap importBeatmapFromStream(Stream stream)
{
var imported = beatmaps.Import(new ImportTask(stream, "filename.osz")).GetResultSafely();
return imported.AsNonNull().PerformRead(s => beatmaps.GetWorkingBeatmap(s.Beatmaps[0]));
}
private IWorkingBeatmap importBeatmapFromArchives(string filename)
{
var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();
return imported.AsNonNull().PerformRead(s => beatmaps.GetWorkingBeatmap(s.Beatmaps[0]));
}
}
}

View File

@ -0,0 +1,161 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Timing;
namespace osu.Game.Tests.Editing
{
[TestFixture]
public class TimingSectionAdjustmentsTest
{
[Test]
public void TestOffsetAdjustment()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(100, new TimingControlPoint { BeatLength = 100 });
controlPoints.Add(50_000, new TimingControlPoint { BeatLength = 200 });
controlPoints.Add(100_000, new TimingControlPoint { BeatLength = 50 });
var beatmap = new Beatmap
{
ControlPointInfo = controlPoints,
HitObjects = new List<HitObject>
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 200 },
new HitCircle { StartTime = 49_900 },
new HitCircle { StartTime = 50_000 },
new HitCircle { StartTime = 50_200 },
new HitCircle { StartTime = 99_800 },
new HitCircle { StartTime = 100_000 },
new HitCircle { StartTime = 100_050 },
new HitCircle { StartTime = 100_550 },
}
};
moveTimingPoint(beatmap, 100, -50);
Assert.Multiple(() =>
{
Assert.That(beatmap.HitObjects[0].StartTime, Is.EqualTo(-50));
Assert.That(beatmap.HitObjects[1].StartTime, Is.EqualTo(150));
Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(49_850));
Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(50_000));
});
moveTimingPoint(beatmap, 50_000, 1_000);
Assert.Multiple(() =>
{
Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(49_850));
Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(51_000));
Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(51_200));
Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(100_800));
Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(100_000));
});
moveTimingPoint(beatmap, 100_000, 10_000);
Assert.Multiple(() =>
{
Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(51_200));
Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(110_800));
Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(110_000));
Assert.That(beatmap.HitObjects[7].StartTime, Is.EqualTo(110_050));
Assert.That(beatmap.HitObjects[8].StartTime, Is.EqualTo(110_550));
});
}
[Test]
public void TestBPMAdjustment()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(100, new TimingControlPoint { BeatLength = 100 });
controlPoints.Add(50_000, new TimingControlPoint { BeatLength = 200 });
controlPoints.Add(100_000, new TimingControlPoint { BeatLength = 50 });
var beatmap = new Beatmap
{
ControlPointInfo = controlPoints,
HitObjects = new List<HitObject>
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 200 },
new Spinner { StartTime = 500, EndTime = 1000 },
new HitCircle { StartTime = 49_900 },
new HitCircle { StartTime = 50_000 },
new HitCircle { StartTime = 50_200 },
new HitCircle { StartTime = 99_800 },
new HitCircle { StartTime = 100_000 },
new HitCircle { StartTime = 100_050 },
new HitCircle { StartTime = 100_550 },
}
};
adjustBeatLength(beatmap, 100, 50);
Assert.Multiple(() =>
{
Assert.That(beatmap.HitObjects[0].StartTime, Is.EqualTo(50));
Assert.That(beatmap.HitObjects[1].StartTime, Is.EqualTo(150));
Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(300));
Assert.That(beatmap.HitObjects[2].GetEndTime(), Is.EqualTo(550));
Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(25_000));
Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(50_000));
});
adjustBeatLength(beatmap, 50_000, 400);
Assert.Multiple(() =>
{
Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(300));
Assert.That(beatmap.HitObjects[2].GetEndTime(), Is.EqualTo(550));
Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(25_000));
Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(50_000));
Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(50_400));
Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(149_600));
Assert.That(beatmap.HitObjects[7].StartTime, Is.EqualTo(100_000));
});
adjustBeatLength(beatmap, 100_000, 100);
Assert.Multiple(() =>
{
Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(50_400));
Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(199_200));
Assert.That(beatmap.HitObjects[7].StartTime, Is.EqualTo(100_000));
Assert.That(beatmap.HitObjects[8].StartTime, Is.EqualTo(100_100));
Assert.That(beatmap.HitObjects[9].StartTime, Is.EqualTo(101_100));
});
}
private static void moveTimingPoint(IBeatmap beatmap, double originalTime, double adjustment)
{
var controlPoints = beatmap.ControlPointInfo;
var controlPointGroup = controlPoints.GroupAt(originalTime);
var timingPoint = controlPointGroup.ControlPoints.OfType<TimingControlPoint>().Single();
controlPoints.RemoveGroup(controlPointGroup);
TimingSectionAdjustments.AdjustHitObjectOffset(beatmap, timingPoint, adjustment);
controlPoints.Add(originalTime - adjustment, timingPoint);
}
private static void adjustBeatLength(IBeatmap beatmap, double groupTime, double newBeatLength)
{
var controlPoints = beatmap.ControlPointInfo;
var controlPointGroup = controlPoints.GroupAt(groupTime);
var timingPoint = controlPointGroup.ControlPoints.OfType<TimingControlPoint>().Single();
double oldBeatLength = timingPoint.BeatLength;
timingPoint.BeatLength = newBeatLength;
TimingSectionAdjustments.SetHitObjectBPM(beatmap, timingPoint, oldBeatLength);
}
}
}

View File

@ -205,7 +205,9 @@ namespace osu.Game.Tests.Visual.Collections
AddStep("click first delete button", () =>
{
InputManager.MoveMouseTo(dialog.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().First(), new Vector2(5, 0));
InputManager.MoveMouseTo(dialog
.ChildrenOfType<DrawableCollectionListItem>().Single(i => i.Model.Value.Name == "1")
.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().Single(), new Vector2(5, 0));
InputManager.Click(MouseButton.Left);
});
@ -213,9 +215,11 @@ namespace osu.Game.Tests.Visual.Collections
assertCollectionCount(1);
assertCollectionName(0, "2");
AddStep("click first delete button", () =>
AddStep("click second delete button", () =>
{
InputManager.MoveMouseTo(dialog.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().First(), new Vector2(5, 0));
InputManager.MoveMouseTo(dialog
.ChildrenOfType<DrawableCollectionListItem>().Single(i => i.Model.Value.Name == "2")
.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().Single(), new Vector2(5, 0));
InputManager.Click(MouseButton.Left);
});
@ -261,6 +265,7 @@ namespace osu.Game.Tests.Visual.Collections
}
[Test]
[Solo]
public void TestCollectionRenamedExternal()
{
BeatmapCollection first = null!;
@ -310,7 +315,7 @@ namespace osu.Game.Tests.Visual.Collections
AddStep("focus first collection", () =>
{
InputManager.MoveMouseTo(firstItem = dialog.ChildrenOfType<DrawableCollectionListItem>().First());
InputManager.MoveMouseTo(firstItem = dialog.ChildrenOfType<DrawableCollectionListItem>().Single(i => i.Model.Value.Name == "1"));
InputManager.Click(MouseButton.Left);
});
@ -337,6 +342,6 @@ namespace osu.Game.Tests.Visual.Collections
=> AddUntilStep($"{count} collections shown", () => dialog.ChildrenOfType<DrawableCollectionListItem>().Count() == count + 1); // +1 for placeholder
private void assertCollectionName(int index, string name)
=> AddUntilStep($"item {index + 1} has correct name", () => dialog.ChildrenOfType<DrawableCollectionListItem>().ElementAt(index).ChildrenOfType<TextBox>().First().Text == name);
=> AddUntilStep($"item {index + 1} has correct name", () => dialog.ChildrenOfType<DrawableCollectionList>().Single().OrderedItems.ElementAt(index).ChildrenOfType<TextBox>().First().Text == name);
}
}

View File

@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
Add(metadataClient);
// add button to observe for daily challenge changes and perform its logic.
Add(new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D));
Add(new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D));
}
[Test]

View File

@ -19,6 +19,7 @@ using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Components.TernaryButtons;
@ -82,6 +83,45 @@ namespace osu.Game.Tests.Visual.Editing
});
}
[Test]
public void TestPlacementOutsideComposeScreen()
{
AddStep("clear all control points and hitobjects", () =>
{
editorBeatmap.ControlPointInfo.Clear();
editorBeatmap.Clear();
});
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddStep("select circle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").TriggerClick());
AddStep("move mouse to compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType<HitObjectContainer>().Single()));
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("circle placed", () => editorBeatmap.HitObjects.Count == 1);
AddStep("move mouse outside compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType<HitObjectContainer>().Single().ScreenSpaceDrawQuad.TopLeft - new Vector2(0f, 20f)));
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("no circle placed", () => editorBeatmap.HitObjects.Count == 1);
}
[Test]
public void TestDragSliderOutsideComposeScreen()
{
AddStep("clear all control points and hitobjects", () =>
{
editorBeatmap.ControlPointInfo.Clear();
editorBeatmap.Clear();
});
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddStep("select slider", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "Slider").TriggerClick());
AddStep("move mouse to compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType<HitObjectContainer>().Single()));
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
AddStep("move mouse outside compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType<HitObjectContainer>().Single().ScreenSpaceDrawQuad.TopLeft - new Vector2(0f, 80f)));
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("slider placed", () => editorBeatmap.HitObjects.Count == 1);
}
[Test]
public void TestPlacementOnlyWorksWithTiming()
{

View File

@ -523,7 +523,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("restart completed", () => getCurrentPlayer() != null && getCurrentPlayer() != previousPlayer);
AddStep("release quick retry key", () => InputManager.ReleaseKey(Key.Tilde));
AddUntilStep("wait for player", () => getCurrentPlayer()?.LoadState == LoadState.Ready);
AddUntilStep("wait for player", () => getCurrentPlayer()?.LoadState >= LoadState.Ready);
AddUntilStep("time reached zero", () => getCurrentPlayer()?.GameplayClockContainer.CurrentTime > 0);
AddUntilStep("skip button not visible", () => !checkSkipButtonVisible());

View File

@ -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)
{
}
}

View File

@ -55,21 +55,21 @@ namespace osu.Game.Tests.Visual.Online
{
Username = @"flyte",
Id = 3103765,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
}),
new UserBrickPanel(new APIUser
{
Username = @"peppy",
Id = 2,
Colour = "99EB47",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
}),
new UserGridPanel(new APIUser
{
Username = @"flyte",
Id = 3103765,
CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg",
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
Status = { Value = UserStatus.Online }
}) { Width = 300 },
boundPanel1 = new UserGridPanel(new APIUser
@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online
Username = @"peppy",
Id = 2,
CountryCode = CountryCode.AU,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
IsSupporter = true,
SupportLevel = 3,
}) { Width = 300 },
@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Online
Username = @"flyte",
Id = 3103765,
CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg",
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 }
}) { Width = 300 },
new UserRankPanel(new APIUser
@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 2,
Colour = "99EB47",
CountryCode = CountryCode.AU,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
Statistics = new UserStatistics { GlobalRank = null, CountryRank = null }
}) { Width = 300 }
}
@ -168,6 +168,14 @@ namespace osu.Game.Tests.Visual.Online
CountryRank = RNG.Next(100000)
});
});
AddStep("set statistics to something big", () =>
{
API.UpdateStatistics(new UserStatistics
{
GlobalRank = RNG.Next(1_000_000, 100_000_000),
CountryRank = RNG.Next(1_000_000, 100_000_000)
});
});
AddStep("set statistics to empty", () =>
{
API.UpdateStatistics(new UserStatistics());

View File

@ -40,6 +40,13 @@ namespace osu.Game.Tests.Visual.Ranking
AddSliderStep("height", 0.0f, 1000.0f, height.Value, height.Set);
}
[Test]
public void TestZeroEvents()
{
createTest(new List<HitEvent>());
AddStep("update offset", () => graph.UpdateOffset(10));
}
[Test]
public void TestManyDistributedEventsOffset()
{

View File

@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@ -23,7 +20,6 @@ using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko;
@ -35,15 +31,11 @@ namespace osu.Game.Tests.Visual.SongSelect
[TestFixture]
public partial class TestSceneBeatmapInfoWedge : OsuTestScene
{
private RulesetStore rulesets;
private TestBeatmapInfoWedge infoWedge;
private readonly List<IBeatmap> beatmaps = new List<IBeatmap>();
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
this.rulesets = rulesets;
}
private TestBeatmapInfoWedge infoWedge = null!;
private readonly List<IBeatmap> beatmaps = new List<IBeatmap>();
protected override void LoadComplete()
{
@ -156,7 +148,7 @@ namespace osu.Game.Tests.Visual.SongSelect
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm });
OsuModDoubleTime doubleTime = null;
OsuModDoubleTime doubleTime = null!;
selectBeatmap(beatmap);
checkDisplayedBPM($"{bpm}");
@ -173,7 +165,7 @@ namespace osu.Game.Tests.Visual.SongSelect
[TestCase(120, 120.4, null, "120")]
[TestCase(120, 120.6, "DT", "180-182 (mostly 180)")]
[TestCase(120, 120.4, "DT", "180")]
public void TestVaryingBPM(double commonBpm, double otherBpm, string mod, string expectedDisplay)
public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay)
{
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
@ -203,7 +195,7 @@ namespace osu.Game.Tests.Visual.SongSelect
double drain = beatmap.CalculateDrainLength();
beatmap.BeatmapInfo.Length = drain;
OsuModDoubleTime doubleTime = null;
OsuModDoubleTime doubleTime = null!;
selectBeatmap(beatmap);
checkDisplayedLength(drain);
@ -221,14 +213,15 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep($"check map drain ({displayedLength})", () =>
{
var label = infoWedge.DisplayedContent.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsTotalLength(displayedLength));
var label = infoWedge.DisplayedContent.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>()
.Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsTotalLength(displayedLength));
return label.Statistic.Content == displayedLength.ToString();
});
}
private void setRuleset(RulesetInfo rulesetInfo)
{
Container containerBefore = null;
Container? containerBefore = null;
AddStep("set ruleset", () =>
{
@ -242,9 +235,9 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
}
private void selectBeatmap([CanBeNull] IBeatmap b)
private void selectBeatmap(IBeatmap? b)
{
Container containerBefore = null;
Container? containerBefore = null;
AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
{
@ -307,11 +300,6 @@ namespace osu.Game.Tests.Visual.SongSelect
public new WedgeInfoText Info => base.Info;
}
private class TestHitObject : ConvertHitObject, IHasPosition
{
public float X => 0;
public float Y => 0;
public Vector2 Position { get; } = Vector2.Zero;
}
private class TestHitObject : ConvertHitObject;
}
}

View File

@ -14,9 +14,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Select;
using osuTK;
namespace osu.Game.Tests.Visual.SongSelectV2
{
@ -209,11 +207,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
public new WedgeInfoText? Info => base.Info;
}
private class TestHitObject : ConvertHitObject, IHasPosition
{
public float X => 0;
public float Y => 0;
public Vector2 Position { get; } = Vector2.Zero;
}
private class TestHitObject : ConvertHitObject;
}
}

View File

@ -159,6 +159,23 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("check star rating is 2", getText, () => Is.EqualTo("Star Rating: 2.00"));
}
[Test]
public void TestMaxPp()
{
AddStep("set test ruleset", () => Ruleset.Value = new TestRuleset().RulesetInfo);
AddStep("set max pp attribute", () => text.Attribute.Value = BeatmapAttribute.MaxPP);
AddAssert("check max pp is 0", getText, () => Is.EqualTo("Max PP: 0"));
// Adding mod
TestMod mod = null!;
AddStep("add mod with pp 1", () => SelectedMods.Value = new[] { mod = new TestMod { Performance = { Value = 1 } } });
AddUntilStep("check max pp is 1", getText, () => Is.EqualTo("Max PP: 1"));
// Changing mod setting
AddStep("change mod pp to 2", () => mod.Performance.Value = 2);
AddUntilStep("check max pp is 2", getText, () => Is.EqualTo("Max PP: 2"));
}
private string getText() => text.ChildrenOfType<SpriteText>().Single().Text.ToString();
private class TestRuleset : Ruleset

View File

@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.UserInterface
break;
case Key.Q:
buttons.OnExit = action;
buttons.OnExit = _ => action();
break;
case Key.O:

View File

@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestStandardButton()
{
AddStep("add button", () => Child = new MainMenuButton(
ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => { }, 0, Key.P)
ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.P)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)],
Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D)
Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -161,7 +161,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)],
Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D)
Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -4,12 +4,15 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Textures;
using osu.Framework.Lists;
using osu.Framework.Logging;
using osu.Framework.Threading;
@ -18,7 +21,11 @@ using osu.Game.Database;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osu.Game.Storyboards;
namespace osu.Game.Beatmaps
{
@ -237,10 +244,37 @@ namespace osu.Game.Beatmaps
var ruleset = rulesetInfo.CreateInstance();
Debug.Assert(ruleset != null);
var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo));
var attributes = calculator.Calculate(key.OrderedMods, cancellationToken);
PlayableCachedWorkingBeatmap workingBeatmap = new PlayableCachedWorkingBeatmap(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo));
IBeatmap playableBeatmap = workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo, key.OrderedMods, cancellationToken);
return new StarDifficulty(attributes);
var difficulty = ruleset.CreateDifficultyCalculator(workingBeatmap).Calculate(key.OrderedMods, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
var performanceCalculator = ruleset.CreatePerformanceCalculator();
if (performanceCalculator == null)
return new StarDifficulty(difficulty, new PerformanceAttributes());
ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = key.OrderedMods;
scoreProcessor.ApplyBeatmap(playableBeatmap);
cancellationToken.ThrowIfCancellationRequested();
ScoreInfo perfectScore = new ScoreInfo(key.BeatmapInfo, ruleset.RulesetInfo)
{
Passed = true,
Accuracy = 1,
Mods = key.OrderedMods,
MaxCombo = scoreProcessor.MaximumCombo,
Combo = scoreProcessor.MaximumCombo,
TotalScore = scoreProcessor.MaximumTotalScore,
Statistics = scoreProcessor.MaximumStatistics,
MaximumStatistics = scoreProcessor.MaximumStatistics
};
var performance = performanceCalculator.Calculate(perfectScore, difficulty);
cancellationToken.ThrowIfCancellationRequested();
return new StarDifficulty(difficulty, performance);
}
catch (OperationCanceledException)
{
@ -276,7 +310,6 @@ namespace osu.Game.Beatmaps
{
public readonly BeatmapInfo BeatmapInfo;
public readonly RulesetInfo Ruleset;
public readonly Mod[] OrderedMods;
public DifficultyCacheLookup(BeatmapInfo beatmapInfo, RulesetInfo? ruleset, IEnumerable<Mod>? mods)
@ -317,5 +350,42 @@ namespace osu.Game.Beatmaps
CancellationToken = cancellationToken;
}
}
/// <summary>
/// A working beatmap that caches its playable representation.
/// This is intended as single-use for when it is guaranteed that the playable beatmap can be reused.
/// </summary>
private class PlayableCachedWorkingBeatmap : IWorkingBeatmap
{
private readonly IWorkingBeatmap working;
private IBeatmap? playable;
public PlayableCachedWorkingBeatmap(IWorkingBeatmap working)
{
this.working = working;
}
public IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList<Mod> mods)
=> playable ??= working.GetPlayableBeatmap(ruleset, mods);
public IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList<Mod> mods, CancellationToken cancellationToken)
=> playable ??= working.GetPlayableBeatmap(ruleset, mods, cancellationToken);
IBeatmapInfo IWorkingBeatmap.BeatmapInfo => working.BeatmapInfo;
bool IWorkingBeatmap.BeatmapLoaded => working.BeatmapLoaded;
bool IWorkingBeatmap.TrackLoaded => working.TrackLoaded;
IBeatmap IWorkingBeatmap.Beatmap => working.Beatmap;
Texture IWorkingBeatmap.GetBackground() => working.GetBackground();
Texture IWorkingBeatmap.GetPanelBackground() => working.GetPanelBackground();
Waveform IWorkingBeatmap.Waveform => working.Waveform;
Storyboard IWorkingBeatmap.Storyboard => working.Storyboard;
ISkin IWorkingBeatmap.Skin => working.Skin;
Track IWorkingBeatmap.Track => working.Track;
Track IWorkingBeatmap.LoadTrack() => working.LoadTrack();
Stream IWorkingBeatmap.GetStream(string storagePath) => working.GetStream(storagePath);
void IWorkingBeatmap.BeginAsyncLoad() => working.BeginAsyncLoad();
void IWorkingBeatmap.CancelAsyncLoad() => working.CancelAsyncLoad();
void IWorkingBeatmap.PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint) => working.PrepareTrackForPreview(looping, offsetFromPreviewPoint);
}
}
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
@ -146,7 +147,8 @@ namespace osu.Game.Beatmaps.Drawables
approachRate.Text = @" AR: " + adjustedDifficulty.ApproachRate.ToString(@"0.##");
overallDifficulty.Text = @" OD: " + adjustedDifficulty.OverallDifficulty.ToString(@"0.##");
length.Text = "Length: " + TimeSpan.FromMilliseconds(displayedContent.BeatmapInfo.Length / rate).ToString(@"mm\:ss");
TimeSpan lengthTimeSpan = TimeSpan.FromMilliseconds(displayedContent.BeatmapInfo.Length / rate);
length.Text = "Length: " + lengthTimeSpan.ToFormattedDuration();
bpm.Text = " BPM: " + Math.Round(bpmAdjusted, 0);
}

View File

@ -38,8 +38,7 @@ namespace osu.Game.Beatmaps.Formats
internal static RulesetStore? RulesetStore;
private Beatmap beatmap = null!;
private ConvertHitObjectParser? parser;
private ConvertHitObjectParser parser = null!;
private LegacySampleBank defaultSampleBank;
private int defaultSampleVolume = 100;
@ -80,6 +79,7 @@ namespace osu.Game.Beatmaps.Formats
{
this.beatmap = beatmap;
this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion;
parser = new ConvertHitObjectParser(getOffsetTime(), FormatVersion);
applyLegacyDefaults(this.beatmap.BeatmapInfo);
@ -162,7 +162,8 @@ namespace osu.Game.Beatmaps.Formats
{
if (hitObject is IHasRepeats hasRepeats)
{
SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.StartTime + CONTROL_POINT_LENIENCY + 1) ?? SampleControlPoint.DEFAULT;
SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.StartTime + CONTROL_POINT_LENIENCY + 1)
?? SampleControlPoint.DEFAULT;
hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList();
for (int i = 0; i < hasRepeats.NodeSamples.Count; i++)
@ -175,7 +176,8 @@ namespace osu.Game.Beatmaps.Formats
}
else
{
SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + CONTROL_POINT_LENIENCY) ?? SampleControlPoint.DEFAULT;
SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + CONTROL_POINT_LENIENCY)
?? SampleControlPoint.DEFAULT;
hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList();
}
}
@ -263,29 +265,7 @@ namespace osu.Game.Beatmaps.Formats
break;
case @"Mode":
int rulesetID = Parsing.ParseInt(pair.Value);
beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(rulesetID) ?? throw new ArgumentException("Ruleset is not available locally.");
switch (rulesetID)
{
case 0:
parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
break;
case 1:
parser = new Rulesets.Objects.Legacy.Taiko.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
break;
case 2:
parser = new Rulesets.Objects.Legacy.Catch.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
break;
case 3:
parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
break;
}
beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(Parsing.ParseInt(pair.Value)) ?? throw new ArgumentException("Ruleset is not available locally.");
break;
case @"LetterboxInBreaks":
@ -617,17 +597,10 @@ namespace osu.Game.Beatmaps.Formats
private void handleHitObject(string line)
{
// If the ruleset wasn't specified, assume the osu!standard ruleset.
parser ??= new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
var obj = parser.Parse(line);
obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
if (obj != null)
{
obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
beatmap.HitObjects.Add(obj);
}
beatmap.HitObjects.Add(obj);
}
private int getOffsetTime(int time) => time + (ApplyOffsets ? offset : 0);

View File

@ -6,7 +6,7 @@ using System;
namespace osu.Game.Beatmaps.Legacy
{
[Flags]
internal enum LegacyHitObjectType
public enum LegacyHitObjectType
{
Circle = 1,
Slider = 1 << 1,

View File

@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using JetBrains.Annotations;
using osu.Framework.Utils;
using osu.Game.Rulesets.Difficulty;
@ -25,30 +22,34 @@ namespace osu.Game.Beatmaps
/// The difficulty attributes computed for the given beatmap.
/// Might not be available if the star difficulty is associated with a beatmap that's not locally available.
/// </summary>
[CanBeNull]
public readonly DifficultyAttributes Attributes;
public readonly DifficultyAttributes? DifficultyAttributes;
/// <summary>
/// Creates a <see cref="StarDifficulty"/> structure based on <see cref="DifficultyAttributes"/> computed
/// by a <see cref="DifficultyCalculator"/>.
/// The performance attributes computed for a perfect score on the given beatmap.
/// Might not be available if the star difficulty is associated with a beatmap that's not locally available.
/// </summary>
public StarDifficulty([NotNull] DifficultyAttributes attributes)
public readonly PerformanceAttributes? PerformanceAttributes;
/// <summary>
/// Creates a <see cref="StarDifficulty"/> structure.
/// </summary>
public StarDifficulty(DifficultyAttributes difficulty, PerformanceAttributes performance)
{
Stars = double.IsFinite(attributes.StarRating) ? attributes.StarRating : 0;
MaxCombo = attributes.MaxCombo;
Attributes = attributes;
Stars = double.IsFinite(difficulty.StarRating) ? difficulty.StarRating : 0;
MaxCombo = difficulty.MaxCombo;
DifficultyAttributes = difficulty;
PerformanceAttributes = performance;
// Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...)
}
/// <summary>
/// Creates a <see cref="StarDifficulty"/> structure with a pre-populated star difficulty and max combo
/// in scenarios where computing <see cref="DifficultyAttributes"/> is not feasible (i.e. when working with online sources).
/// in scenarios where computing <see cref="Rulesets.Difficulty.DifficultyAttributes"/> is not feasible (i.e. when working with online sources).
/// </summary>
public StarDifficulty(double starDifficulty, int maxCombo)
{
Stars = double.IsFinite(starDifficulty) ? starDifficulty : 0;
MaxCombo = maxCombo;
Attributes = null;
}
public DifficultyRating DifficultyRating => GetDifficultyRating(Stars);

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
@ -29,7 +30,11 @@ namespace osu.Game.Collections
private IDisposable? realmSubscription;
protected override FillFlowContainer<RearrangeableListItem<Live<BeatmapCollection>>> CreateListFillFlowContainer() => new Flow
private Flow flow = null!;
public IEnumerable<Drawable> OrderedItems => flow.FlowingChildren;
protected override FillFlowContainer<RearrangeableListItem<Live<BeatmapCollection>>> CreateListFillFlowContainer() => flow = new Flow
{
DragActive = { BindTarget = DragActive }
};
@ -43,8 +48,25 @@ namespace osu.Game.Collections
private void collectionsChanged(IRealmCollection<BeatmapCollection> collections, ChangeSet? changes)
{
Items.Clear();
Items.AddRange(collections.AsEnumerable().Select(c => c.ToLive(realm)));
if (changes == null)
{
Items.AddRange(collections.AsEnumerable().Select(c => c.ToLive(realm)));
return;
}
foreach (int i in changes.DeletedIndices.OrderDescending())
Items.RemoveAt(i);
foreach (int i in changes.InsertedIndices)
Items.Insert(i, collections[i].ToLive(realm));
foreach (int i in changes.NewModifiedIndices)
{
var updatedItem = collections[i];
Items.RemoveAt(i);
Items.Insert(i, updatedItem.ToLive(realm));
}
}
protected override OsuRearrangeableListItem<Live<BeatmapCollection>> CreateOsuDrawable(Live<BeatmapCollection> item)
@ -123,12 +145,37 @@ namespace osu.Game.Collections
var previous = PlaceholderItem;
placeholderContainer.Clear(false);
placeholderContainer.Add(PlaceholderItem = new DrawableCollectionListItem(new BeatmapCollection().ToLiveUnmanaged(), false));
placeholderContainer.Add(PlaceholderItem = new NewCollectionEntryItem());
return previous;
}
}
private partial class NewCollectionEntryItem : DrawableCollectionListItem
{
[Resolved]
private RealmAccess realm { get; set; } = null!;
public NewCollectionEntryItem()
: base(new BeatmapCollection().ToLiveUnmanaged(), false)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
TextBox.OnCommit += (sender, newText) =>
{
if (string.IsNullOrEmpty(TextBox.Text))
return;
realm.Write(r => r.Add(new BeatmapCollection(TextBox.Text)));
TextBox.Text = string.Empty;
};
}
}
/// <summary>
/// The flow of <see cref="DrawableCollectionListItem"/>. Disables layout easing unless a drag is in progress.
/// </summary>

View File

@ -28,6 +28,10 @@ namespace osu.Game.Collections
private const float item_height = 35;
private const float button_width = item_height * 0.75f;
protected TextBox TextBox => content.TextBox;
private ItemContent content = null!;
/// <summary>
/// Creates a new <see cref="DrawableCollectionListItem"/>.
/// </summary>
@ -48,7 +52,7 @@ namespace osu.Game.Collections
CornerRadius = item_height / 2;
}
protected override Drawable CreateContent() => new ItemContent(Model);
protected override Drawable CreateContent() => content = new ItemContent(Model);
/// <summary>
/// The main content of the <see cref="DrawableCollectionListItem"/>.
@ -57,10 +61,7 @@ namespace osu.Game.Collections
{
private readonly Live<BeatmapCollection> collection;
private ItemTextBox textBox = null!;
[Resolved]
private RealmAccess realm { get; set; } = null!;
public ItemTextBox TextBox { get; private set; } = null!;
public ItemContent(Live<BeatmapCollection> collection)
{
@ -80,7 +81,7 @@ namespace osu.Game.Collections
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
IsTextBoxHovered = v => textBox.ReceivePositionalInputAt(v)
IsTextBoxHovered = v => TextBox.ReceivePositionalInputAt(v)
}
: Empty(),
new Container
@ -89,7 +90,7 @@ namespace osu.Game.Collections
Padding = new MarginPadding { Right = collection.IsManaged ? button_width : 0 },
Children = new Drawable[]
{
textBox = new ItemTextBox
TextBox = new ItemTextBox
{
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
@ -107,18 +108,14 @@ namespace osu.Game.Collections
base.LoadComplete();
// Bind late, as the collection name may change externally while still loading.
textBox.Current.Value = collection.PerformRead(c => c.IsValid ? c.Name : string.Empty);
textBox.OnCommit += onCommit;
TextBox.Current.Value = collection.PerformRead(c => c.IsValid ? c.Name : string.Empty);
TextBox.OnCommit += onCommit;
}
private void onCommit(TextBox sender, bool newText)
{
if (collection.IsManaged)
collection.PerformWrite(c => c.Name = textBox.Current.Value);
else if (!string.IsNullOrEmpty(textBox.Current.Value))
realm.Write(r => r.Add(new BeatmapCollection(textBox.Current.Value)));
textBox.Text = string.Empty;
if (collection.IsManaged && collection.Value.Name != TextBox.Current.Value)
collection.PerformWrite(c => c.Name = TextBox.Current.Value);
}
}

View File

@ -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,
}
}

View File

@ -59,7 +59,25 @@ namespace osu.Game.Database
};
// Convert beatmap elements to be compatible with legacy format
// So we truncate time and position values to integers, and convert paths with multiple segments to bezier curves
// So we truncate time and position values to integers, and convert paths with multiple segments to Bézier curves
// We must first truncate all timing points and move all objects in the timing section with it to ensure everything stays snapped
for (int i = 0; i < playableBeatmap.ControlPointInfo.TimingPoints.Count; i++)
{
var timingPoint = playableBeatmap.ControlPointInfo.TimingPoints[i];
double offset = Math.Floor(timingPoint.Time) - timingPoint.Time;
double nextTimingPointTime = i + 1 < playableBeatmap.ControlPointInfo.TimingPoints.Count
? playableBeatmap.ControlPointInfo.TimingPoints[i + 1].Time
: double.PositiveInfinity;
// Offset all control points in the timing section (including the current one)
foreach (var controlPoint in playableBeatmap.ControlPointInfo.AllControlPoints.Where(o => o.Time >= timingPoint.Time && o.Time < nextTimingPointTime))
controlPoint.Time += offset;
// Offset all hit objects in the timing section
foreach (var hitObject in playableBeatmap.HitObjects.Where(o => o.StartTime >= timingPoint.Time && o.StartTime < nextTimingPointTime))
hitObject.StartTime += offset;
}
foreach (var controlPoint in playableBeatmap.ControlPointInfo.AllControlPoints)
controlPoint.Time = Math.Floor(controlPoint.Time);

View File

@ -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>

View File

@ -12,23 +12,28 @@ namespace osu.Game.Localisation.SkinComponents
/// <summary>
/// "Attribute"
/// </summary>
public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), "Attribute");
public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), @"Attribute");
/// <summary>
/// "The attribute to be displayed."
/// </summary>
public static LocalisableString AttributeDescription => new TranslatableString(getKey(@"attribute_description"), "The attribute to be displayed.");
public static LocalisableString AttributeDescription => new TranslatableString(getKey(@"attribute_description"), @"The attribute to be displayed.");
/// <summary>
/// "Template"
/// </summary>
public static LocalisableString Template => new TranslatableString(getKey(@"template"), "Template");
public static LocalisableString Template => new TranslatableString(getKey(@"template"), @"Template");
/// <summary>
/// "Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values)."
/// </summary>
public static LocalisableString TemplateDescription => new TranslatableString(getKey(@"template_description"), @"Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values).");
private static string getKey(string key) => $"{prefix}:{key}";
/// <summary>
/// "Max PP"
/// </summary>
public static LocalisableString MaxPP => new TranslatableString(getKey(@"max_pp"), @"Max PP");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -1,121 +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 System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Difficulty
{
public class PerformanceBreakdownCalculator
{
private readonly IBeatmap playableBeatmap;
private readonly BeatmapDifficultyCache difficultyCache;
public PerformanceBreakdownCalculator(IBeatmap playableBeatmap, BeatmapDifficultyCache difficultyCache)
{
this.playableBeatmap = playableBeatmap;
this.difficultyCache = difficultyCache;
}
[ItemCanBeNull]
public async Task<PerformanceBreakdown> CalculateAsync(ScoreInfo score, CancellationToken cancellationToken = default)
{
var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator();
// Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value.
if (attributes?.Attributes == null || performanceCalculator == null)
return null;
cancellationToken.ThrowIfCancellationRequested();
PerformanceAttributes[] performanceArray = await Task.WhenAll(
// compute actual performance
performanceCalculator.CalculateAsync(score, attributes.Value.Attributes, cancellationToken),
// compute performance for perfect play
getPerfectPerformance(score, cancellationToken)
).ConfigureAwait(false);
return new PerformanceBreakdown(performanceArray[0] ?? new PerformanceAttributes(), performanceArray[1] ?? new PerformanceAttributes());
}
[ItemCanBeNull]
private Task<PerformanceAttributes> getPerfectPerformance(ScoreInfo score, CancellationToken cancellationToken = default)
{
return Task.Run(async () =>
{
Ruleset ruleset = score.Ruleset.CreateInstance();
ScoreInfo perfectPlay = score.DeepClone();
perfectPlay.Accuracy = 1;
perfectPlay.Passed = true;
// calculate max combo
// todo: Get max combo from difficulty calculator instead when diffcalc properly supports lazer-first scores
perfectPlay.MaxCombo = calculateMaxCombo(playableBeatmap);
// create statistics assuming all hit objects have perfect hit result
var statistics = playableBeatmap.HitObjects
.SelectMany(getPerfectHitResults)
.GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count()))
.ToDictionary(pair => pair.hitResult, pair => pair.count);
perfectPlay.Statistics = statistics;
perfectPlay.MaximumStatistics = statistics;
// calculate total score
ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = perfectPlay.Mods;
scoreProcessor.ApplyBeatmap(playableBeatmap);
perfectPlay.TotalScore = scoreProcessor.MaximumTotalScore;
// compute rank achieved
// default to SS, then adjust the rank with mods
perfectPlay.Rank = ScoreRank.X;
foreach (IApplicableToScoreProcessor mod in perfectPlay.Mods.OfType<IApplicableToScoreProcessor>())
{
perfectPlay.Rank = mod.AdjustRank(perfectPlay.Rank, 1);
}
// calculate performance for this perfect score
var difficulty = await difficultyCache.GetDifficultyAsync(
playableBeatmap.BeatmapInfo,
score.Ruleset,
score.Mods,
cancellationToken
).ConfigureAwait(false);
var performanceCalculator = ruleset.CreatePerformanceCalculator();
if (performanceCalculator == null || difficulty == null)
return null;
return await performanceCalculator.CalculateAsync(perfectPlay, difficulty.Value.Attributes.AsNonNull(), cancellationToken).ConfigureAwait(false);
}, cancellationToken);
}
private int calculateMaxCombo(IBeatmap beatmap)
{
return beatmap.HitObjects.SelectMany(getPerfectHitResults).Count(r => r.AffectsCombo());
}
private IEnumerable<HitResult> getPerfectHitResults(HitObject hitObject)
{
foreach (HitObject nested in hitObject.NestedHitObjects)
yield return nested.Judgement.MaxResult;
yield return hitObject.Judgement.MaxResult;
}
}
}

View File

@ -26,10 +26,10 @@ namespace osu.Game.Rulesets.Difficulty.Skills
protected virtual int SectionLength => 400;
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)
@ -57,7 +57,29 @@ 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
if (consistentTopStrain == 0)
return ObjectStrains.Count;
// 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>

View File

@ -0,0 +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.
using System;
namespace osu.Game.Rulesets.Difficulty.Utils
{
public static class DifficultyCalculationUtils
{
/// <summary>
/// Converts BPM value into milliseconds
/// </summary>
/// <param name="bpm">Beats per minute</param>
/// <param name="delimiter">Which rhythm delimiter to use, default is 1/4</param>
/// <returns>BPM conveted to milliseconds</returns>
public static double BPMToMilliseconds(double bpm, int delimiter = 4)
{
return 60000.0 / delimiter / bpm;
}
/// <summary>
/// Converts milliseconds value into a BPM value
/// </summary>
/// <param name="ms">Milliseconds</param>
/// <param name="delimiter">Which rhythm delimiter to use, default is 1/4</param>
/// <returns>Milliseconds conveted to beats per minute</returns>
public static double MillisecondsToBPM(double ms, int delimiter = 4)
{
return 60000.0 / (ms * delimiter);
}
/// <summary>
/// Calculates a S-shaped logistic function (https://en.wikipedia.org/wiki/Logistic_function)
/// </summary>
/// <param name="x">Value to calculate the function for</param>
/// <param name="maxValue">Maximum value returnable by the function</param>
/// <param name="multiplier">Growth rate of the function</param>
/// <param name="midpointOffset">How much the function midpoint is offset from zero <paramref name="x"/></param>
/// <returns>The output of logistic function of <paramref name="x"/></returns>
public static double Logistic(double x, double midpointOffset, double multiplier, double maxValue = 1) => maxValue / (1 + Math.Exp(multiplier * (midpointOffset - x)));
/// <summary>
/// Calculates a S-shaped logistic function (https://en.wikipedia.org/wiki/Logistic_function)
/// </summary>
/// <param name="maxValue">Maximum value returnable by the function</param>
/// <param name="exponent">Exponent</param>
/// <returns>The output of logistic function</returns>
public static double Logistic(double exponent, double maxValue = 1) => maxValue / (1 + Math.Exp(exponent));
}
}

View File

@ -537,22 +537,23 @@ namespace osu.Game.Rulesets.Edit
#region IPlacementHandler
public void BeginPlacement(HitObject hitObject)
public void ShowPlacement(HitObject hitObject)
{
EditorBeatmap.PlacementObject.Value = hitObject;
}
public void EndPlacement(HitObject hitObject, bool commit)
public void HidePlacement()
{
EditorBeatmap.PlacementObject.Value = null;
}
if (commit)
{
EditorBeatmap.Add(hitObject);
public void CommitPlacement(HitObject hitObject)
{
EditorBeatmap.PlacementObject.Value = null;
EditorBeatmap.Add(hitObject);
if (autoSeekOnPlacement.Value && EditorClock.CurrentTime < hitObject.StartTime)
EditorClock.SeekSmoothlyTo(hitObject.StartTime);
}
if (autoSeekOnPlacement.Value && EditorClock.CurrentTime < hitObject.StartTime)
EditorClock.SeekSmoothlyTo(hitObject.StartTime);
}
public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject);

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@ -63,18 +64,26 @@ namespace osu.Game.Rulesets.Edit
startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true);
}
private bool placementBegun;
protected override void BeginPlacement(bool commitStart = false)
{
base.BeginPlacement(commitStart);
placementHandler.BeginPlacement(HitObject);
if (State.Value == Visibility.Visible)
placementHandler.ShowPlacement(HitObject);
placementBegun = true;
}
public override void EndPlacement(bool commit)
{
base.EndPlacement(commit);
placementHandler.EndPlacement(HitObject, IsValidForPlacement && commit);
if (IsValidForPlacement && commit)
placementHandler.CommitPlacement(HitObject);
else
placementHandler.HidePlacement();
}
/// <summary>
@ -127,5 +136,19 @@ namespace osu.Game.Rulesets.Edit
/// refreshing <see cref="Objects.HitObject.NestedHitObjects"/> and parameters for the <see cref="HitObject"/>.
/// </summary>
protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
protected override void PopIn()
{
base.PopIn();
if (placementBegun)
placementHandler.ShowPlacement(HitObject);
}
protected override void PopOut()
{
base.PopOut();
placementHandler.HidePlacement();
}
}
}

View File

@ -7,7 +7,6 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Objects;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Edit
@ -15,7 +14,7 @@ namespace osu.Game.Rulesets.Edit
/// <summary>
/// A blueprint which governs the placement of something.
/// </summary>
public abstract partial class PlacementBlueprint : CompositeDrawable, IKeyBindingHandler<GlobalAction>
public abstract partial class PlacementBlueprint : VisibilityContainer, IKeyBindingHandler<GlobalAction>
{
/// <summary>
/// Whether the <see cref="HitObject"/> is currently mid-placement, but has not necessarily finished being placed.
@ -31,12 +30,17 @@ namespace osu.Game.Rulesets.Edit
/// </remarks>
protected virtual bool IsValidForPlacement => true;
// the blueprint should still be considered for input even if it is hidden,
// especially when such input is the reason for making the blueprint become visible.
public override bool PropagatePositionalInputSubTree => true;
public override bool PropagateNonPositionalInputSubTree => true;
protected PlacementBlueprint()
{
RelativeSizeAxes = Axes.Both;
// This is required to allow the blueprint's position to be updated via OnMouseMove/Handle
// on the same frame it is made visible via a PlacementState change.
// the blueprint should still be considered for input even if it is hidden,
// especially when such input is the reason for making the blueprint become visible.
AlwaysPresent = true;
}
@ -104,8 +108,6 @@ namespace osu.Game.Rulesets.Edit
{
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
protected override bool Handle(UIEvent e)
{
base.Handle(e);
@ -127,6 +129,9 @@ namespace osu.Game.Rulesets.Edit
}
}
protected override void PopIn() => this.FadeIn();
protected override void PopOut() => this.FadeOut();
public enum PlacementState
{
Waiting,

View File

@ -1,20 +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.
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Objects.Legacy.Catch
{
/// <summary>
/// Legacy osu!catch Hit-type, used for parsing Beatmaps.
/// </summary>
internal sealed class ConvertHit : ConvertHitObject, IHasPosition
{
public float X => Position.X;
public float Y => Position.Y;
public Vector2 Position { get; set; }
}
}

View File

@ -1,63 +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 osuTK;
using osu.Game.Audio;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Objects.Legacy.Catch
{
/// <summary>
/// A HitObjectParser to parse legacy osu!catch Beatmaps.
/// </summary>
public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
{
private ConvertHitObject lastObject;
public ConvertHitObjectParser(double offset, int formatVersion)
: base(offset, formatVersion)
{
}
protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
{
return lastObject = new ConvertHit
{
Position = position,
NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo,
ComboOffset = newCombo ? comboOffset : 0
};
}
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
IList<IList<HitSampleInfo>> nodeSamples)
{
return lastObject = new ConvertSlider
{
Position = position,
NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo,
ComboOffset = newCombo ? comboOffset : 0,
Path = new SliderPath(controlPoints, length),
NodeSamples = nodeSamples,
RepeatCount = repeatCount
};
}
protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration)
{
return lastObject = new ConvertSpinner
{
Duration = duration,
NewCombo = newCombo
// Spinners cannot have combo offset.
};
}
protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration)
{
return lastObject = null;
}
}
}

View File

@ -1,20 +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.
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Objects.Legacy.Catch
{
/// <summary>
/// Legacy osu!catch Slider-type, used for parsing Beatmaps.
/// </summary>
internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition
{
public float X => Position.X;
public float Y => Position.Y;
public Vector2 Position { get; set; }
}
}

View File

@ -0,0 +1,13 @@
// 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.
namespace osu.Game.Rulesets.Objects.Legacy
{
/// <summary>
/// Legacy "HitCircle" hit object type.
/// </summary>
/// <remarks>
/// Only used for parsing beatmaps and not gameplay.
/// </remarks>
internal sealed class ConvertHitCircle : ConvertHitObject;
}

View File

@ -1,21 +1,34 @@
// 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.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Objects.Legacy
{
/// <summary>
/// A hit object only used for conversion, not actual gameplay.
/// Represents a legacy hit object.
/// </summary>
internal abstract class ConvertHitObject : HitObject, IHasCombo
/// <remarks>
/// Only used for parsing beatmaps and not gameplay.
/// </remarks>
internal abstract class ConvertHitObject : HitObject, IHasCombo, IHasPosition, IHasLegacyHitObjectType
{
public bool NewCombo { get; set; }
public int ComboOffset { get; set; }
public float X => Position.X;
public float Y => Position.Y;
public Vector2 Position { get; set; }
public LegacyHitObjectType LegacyType { get; set; }
public override Judgement CreateJudgement() => new IgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;

View File

@ -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 osuTK;
using osu.Game.Rulesets.Objects.Types;
using System;
@ -11,7 +9,6 @@ using System.IO;
using osu.Game.Beatmaps.Formats;
using osu.Game.Audio;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
@ -24,24 +21,32 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// <summary>
/// A HitObjectParser to parse legacy Beatmaps.
/// </summary>
public abstract class ConvertHitObjectParser : HitObjectParser
public class ConvertHitObjectParser : HitObjectParser
{
/// <summary>
/// The offset to apply to all time values.
/// </summary>
protected readonly double Offset;
private readonly double offset;
/// <summary>
/// The .osu format (beatmap) version.
/// </summary>
protected readonly int FormatVersion;
private readonly int formatVersion;
protected bool FirstObject { get; private set; } = true;
/// <summary>
/// Whether the current hitobject is the first hitobject in the beatmap.
/// </summary>
private bool firstObject = true;
protected ConvertHitObjectParser(double offset, int formatVersion)
/// <summary>
/// The last parsed hitobject.
/// </summary>
private ConvertHitObject? lastObject;
internal ConvertHitObjectParser(double offset, int formatVersion)
{
Offset = offset;
FormatVersion = formatVersion;
this.offset = offset;
this.formatVersion = formatVersion;
}
public override HitObject Parse(string text)
@ -49,11 +54,11 @@ namespace osu.Game.Rulesets.Objects.Legacy
string[] split = text.Split(',');
Vector2 pos =
FormatVersion >= LegacyBeatmapEncoder.FIRST_LAZER_VERSION
formatVersion >= LegacyBeatmapEncoder.FIRST_LAZER_VERSION
? new Vector2(Parsing.ParseFloat(split[0], Parsing.MAX_COORDINATE_VALUE), Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE))
: new Vector2((int)Parsing.ParseFloat(split[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE));
double startTime = Parsing.ParseDouble(split[2]) + Offset;
double startTime = Parsing.ParseDouble(split[2]) + offset;
LegacyHitObjectType type = (LegacyHitObjectType)Parsing.ParseInt(split[3]);
@ -66,11 +71,11 @@ namespace osu.Game.Rulesets.Objects.Legacy
var soundType = (LegacyHitSoundType)Parsing.ParseInt(split[4]);
var bankInfo = new SampleBankInfo();
HitObject result = null;
ConvertHitObject? result = null;
if (type.HasFlag(LegacyHitObjectType.Circle))
{
result = CreateHit(pos, combo, comboOffset);
result = createHitCircle(pos, combo, comboOffset);
if (split.Length > 5)
readCustomSampleBanks(split[5], bankInfo);
@ -145,13 +150,13 @@ namespace osu.Game.Rulesets.Objects.Legacy
for (int i = 0; i < nodes; i++)
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
result = CreateSlider(pos, combo, comboOffset, convertPathString(split[5], pos), length, repeatCount, nodeSamples);
result = createSlider(pos, combo, comboOffset, convertPathString(split[5], pos), length, repeatCount, nodeSamples);
}
else if (type.HasFlag(LegacyHitObjectType.Spinner))
{
double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + Offset - startTime);
double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + offset - startTime);
result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, duration);
result = createSpinner(new Vector2(512, 384) / 2, combo, duration);
if (split.Length > 6)
readCustomSampleBanks(split[6], bankInfo);
@ -169,18 +174,19 @@ namespace osu.Game.Rulesets.Objects.Legacy
readCustomSampleBanks(string.Join(':', ss.Skip(1)), bankInfo);
}
result = CreateHold(pos, combo, comboOffset, endTime + Offset - startTime);
result = createHold(pos, endTime + offset - startTime);
}
if (result == null)
throw new InvalidDataException($"Unknown hit object type: {split[3]}");
result.StartTime = startTime;
result.LegacyType = type;
if (result.Samples.Count == 0)
result.Samples = convertSoundType(soundType, bankInfo);
FirstObject = false;
firstObject = false;
return result;
}
@ -200,10 +206,11 @@ namespace osu.Game.Rulesets.Objects.Legacy
if (!Enum.IsDefined(addBank))
addBank = LegacySampleBank.Normal;
string stringBank = bank.ToString().ToLowerInvariant();
string? stringBank = bank.ToString().ToLowerInvariant();
string? stringAddBank = addBank.ToString().ToLowerInvariant();
if (stringBank == @"none")
stringBank = null;
string stringAddBank = addBank.ToString().ToLowerInvariant();
if (stringAddBank == @"none")
{
@ -357,7 +364,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
{
int endPointLength = endPoint == null ? 0 : 1;
if (FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION)
if (formatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION)
{
if (vertices.Length + endPointLength != 3)
type = PathType.BEZIER;
@ -393,7 +400,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
// Legacy CATMULL sliders don't support multiple segments, so adjacent CATMULL segments should be treated as a single one.
// Importantly, this is not applied to the first control point, which may duplicate the slider path's position
// resulting in a duplicate (0,0) control point in the resultant list.
if (type == PathType.CATMULL && endIndex > 1 && FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION)
if (type == PathType.CATMULL && endIndex > 1 && formatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION)
continue;
// The last control point of each segment is not allowed to start a new implicit segment.
@ -442,7 +449,15 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// <param name="newCombo">Whether the hit object creates a new combo.</param>
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
/// <returns>The hit object.</returns>
protected abstract HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset);
private ConvertHitObject createHitCircle(Vector2 position, bool newCombo, int comboOffset)
{
return lastObject = new ConvertHitCircle
{
Position = position,
NewCombo = firstObject || lastObject is ConvertSpinner || newCombo,
ComboOffset = newCombo ? comboOffset : 0
};
}
/// <summary>
/// Creats a legacy Slider-type hit object.
@ -455,27 +470,51 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// <param name="repeatCount">The slider repeat count.</param>
/// <param name="nodeSamples">The samples to be played when the slider nodes are hit. This includes the head and tail of the slider.</param>
/// <returns>The hit object.</returns>
protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
IList<IList<HitSampleInfo>> nodeSamples);
private ConvertHitObject createSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
IList<IList<HitSampleInfo>> nodeSamples)
{
return lastObject = new ConvertSlider
{
Position = position,
NewCombo = firstObject || lastObject is ConvertSpinner || newCombo,
ComboOffset = newCombo ? comboOffset : 0,
Path = new SliderPath(controlPoints, length),
NodeSamples = nodeSamples,
RepeatCount = repeatCount
};
}
/// <summary>
/// Creates a legacy Spinner-type hit object.
/// </summary>
/// <param name="position">The position of the hit object.</param>
/// <param name="newCombo">Whether the hit object creates a new combo.</param>
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
/// <param name="duration">The spinner duration.</param>
/// <returns>The hit object.</returns>
protected abstract HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration);
private ConvertHitObject createSpinner(Vector2 position, bool newCombo, double duration)
{
return lastObject = new ConvertSpinner
{
Position = position,
Duration = duration,
NewCombo = newCombo
// Spinners cannot have combo offset.
};
}
/// <summary>
/// Creates a legacy Hold-type hit object.
/// </summary>
/// <param name="position">The position of the hit object.</param>
/// <param name="newCombo">Whether the hit object creates a new combo.</param>
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
/// <param name="duration">The hold duration.</param>
protected abstract HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration);
private ConvertHitObject createHold(Vector2 position, double duration)
{
return lastObject = new ConvertHold
{
Position = position,
Duration = duration
};
}
private List<HitSampleInfo> convertSoundType(LegacyHitSoundType type, SampleBankInfo bankInfo)
{
@ -511,21 +550,19 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// <summary>
/// An optional overriding filename which causes all bank/sample specifications to be ignored.
/// </summary>
public string Filename;
public string? Filename;
/// <summary>
/// The bank identifier to use for the base ("hitnormal") sample.
/// Transferred to <see cref="HitSampleInfo.Bank"/> when appropriate.
/// </summary>
[CanBeNull]
public string BankForNormal;
public string? BankForNormal;
/// <summary>
/// The bank identifier to use for additions ("hitwhistle", "hitfinish", "hitclap").
/// Transferred to <see cref="HitSampleInfo.Bank"/> when appropriate.
/// </summary>
[CanBeNull]
public string BankForAdditions;
public string? BankForAdditions;
/// <summary>
/// Hit sample volume (0-100).
@ -548,8 +585,6 @@ namespace osu.Game.Rulesets.Objects.Legacy
public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone();
}
#nullable enable
public class LegacyHitSampleInfo : HitSampleInfo, IEquatable<LegacyHitSampleInfo>
{
public readonly int CustomSampleBank;
@ -577,13 +612,14 @@ namespace osu.Game.Rulesets.Objects.Legacy
IsLayered = isLayered;
}
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default)
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default,
Optional<bool> newEditorAutoBank = default)
=> With(newName, newBank, newVolume, newEditorAutoBank);
public virtual LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default,
Optional<int> newCustomSampleBank = default,
Optional<bool> newIsLayered = default)
=> new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newEditorAutoBank.GetOr(EditorAutoBank), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered));
public virtual LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default,
Optional<bool> newEditorAutoBank = default, Optional<int> newCustomSampleBank = default, Optional<bool> newIsLayered = default)
=> new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newEditorAutoBank.GetOr(EditorAutoBank), newCustomSampleBank.GetOr(CustomSampleBank),
newIsLayered.GetOr(IsLayered));
public bool Equals(LegacyHitSampleInfo? other)
// The additions to equality checks here are *required* to ensure that pooling works correctly.
@ -615,9 +651,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
Path.ChangeExtension(Filename, null)
};
public sealed override LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default,
Optional<int> newCustomSampleBank = default,
Optional<bool> newIsLayered = default)
public sealed override LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default,
Optional<bool> newEditorAutoBank = default, Optional<int> newCustomSampleBank = default, Optional<bool> newIsLayered = default)
=> new FileHitSampleInfo(Filename, newVolume.GetOr(Volume));
public bool Equals(FileHitSampleInfo? other)

View File

@ -3,17 +3,18 @@
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects.Legacy.Catch
namespace osu.Game.Rulesets.Objects.Legacy
{
/// <summary>
/// Legacy osu!catch Spinner-type, used for parsing Beatmaps.
/// Legacy "Hold" hit object type. Generally only valid in the mania ruleset.
/// </summary>
internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition
/// <remarks>
/// Only used for parsing beatmaps and not gameplay.
/// </remarks>
internal sealed class ConvertHold : ConvertHitObject, IHasDuration
{
public double EndTime => StartTime + Duration;
public double Duration { get; set; }
public float X => 256; // Required for CatchBeatmapConverter
public double EndTime => StartTime + Duration;
}
}

View File

@ -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 osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic;
using Newtonsoft.Json;
@ -13,7 +11,13 @@ using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Objects.Legacy
{
internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasSliderVelocity
/// <summary>
/// Legacy "Slider" hit object type.
/// </summary>
/// <remarks>
/// Only used for parsing beatmaps and not gameplay.
/// </remarks>
internal class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasSliderVelocity, IHasGenerateTicks
{
/// <summary>
/// Scoring distance with a speed-adjusted beat length of 1 second.
@ -50,6 +54,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
set => SliderVelocityMultiplierBindable.Value = value;
}
public bool GenerateTicks { get; set; } = true;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);

View File

@ -3,11 +3,14 @@
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects.Legacy.Taiko
namespace osu.Game.Rulesets.Objects.Legacy
{
/// <summary>
/// Legacy osu!taiko Spinner-type, used for parsing Beatmaps.
/// Legacy "Spinner" hit object type.
/// </summary>
/// <remarks>
/// Only used for parsing beatmaps and not gameplay.
/// </remarks>
internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration
{
public double Duration { get; set; }

View File

@ -0,0 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps.Legacy;
namespace osu.Game.Rulesets.Objects.Legacy
{
/// <summary>
/// A hit object from a legacy beatmap representation.
/// </summary>
public interface IHasLegacyHitObjectType
{
/// <summary>
/// The hit object type.
/// </summary>
LegacyHitObjectType LegacyType { get; }
}
}

View File

@ -1,15 +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.
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects.Legacy.Mania
{
/// <summary>
/// Legacy osu!mania Hit-type, used for parsing Beatmaps.
/// </summary>
internal sealed class ConvertHit : ConvertHitObject, IHasXPosition
{
public float X { get; set; }
}
}

View File

@ -1,58 +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.
using osuTK;
using osu.Game.Audio;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Objects.Legacy.Mania
{
/// <summary>
/// A HitObjectParser to parse legacy osu!mania Beatmaps.
/// </summary>
public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
{
public ConvertHitObjectParser(double offset, int formatVersion)
: base(offset, formatVersion)
{
}
protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
{
return new ConvertHit
{
X = position.X
};
}
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
IList<IList<HitSampleInfo>> nodeSamples)
{
return new ConvertSlider
{
X = position.X,
Path = new SliderPath(controlPoints, length),
NodeSamples = nodeSamples,
RepeatCount = repeatCount
};
}
protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration)
{
return new ConvertSpinner
{
X = position.X,
Duration = duration
};
}
protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration)
{
return new ConvertHold
{
X = position.X,
Duration = duration
};
}
}
}

View File

@ -1,16 +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.
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects.Legacy.Mania
{
internal sealed class ConvertHold : ConvertHitObject, IHasXPosition, IHasDuration
{
public float X { get; set; }
public double Duration { get; set; }
public double EndTime => StartTime + Duration;
}
}

View File

@ -1,15 +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.
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects.Legacy.Mania
{
/// <summary>
/// Legacy osu!mania Slider-type, used for parsing Beatmaps.
/// </summary>
internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasXPosition
{
public float X { get; set; }
}
}

View File

@ -1,19 +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.
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects.Legacy.Mania
{
/// <summary>
/// Legacy osu!mania Spinner-type, used for parsing Beatmaps.
/// </summary>
internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition
{
public double Duration { get; set; }
public double EndTime => StartTime + Duration;
public float X { get; set; }
}
}

View File

@ -1,20 +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.
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Objects.Legacy.Osu
{
/// <summary>
/// Legacy osu! Hit-type, used for parsing Beatmaps.
/// </summary>
internal sealed class ConvertHit : ConvertHitObject, IHasPosition
{
public Vector2 Position { get; set; }
public float X => Position.X;
public float Y => Position.Y;
}
}

View File

@ -1,64 +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 osuTK;
using System.Collections.Generic;
using osu.Game.Audio;
namespace osu.Game.Rulesets.Objects.Legacy.Osu
{
/// <summary>
/// A HitObjectParser to parse legacy osu! Beatmaps.
/// </summary>
public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
{
private ConvertHitObject lastObject;
public ConvertHitObjectParser(double offset, int formatVersion)
: base(offset, formatVersion)
{
}
protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
{
return lastObject = new ConvertHit
{
Position = position,
NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo,
ComboOffset = newCombo ? comboOffset : 0
};
}
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
IList<IList<HitSampleInfo>> nodeSamples)
{
return lastObject = new ConvertSlider
{
Position = position,
NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo,
ComboOffset = newCombo ? comboOffset : 0,
Path = new SliderPath(controlPoints, length),
NodeSamples = nodeSamples,
RepeatCount = repeatCount
};
}
protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration)
{
return lastObject = new ConvertSpinner
{
Position = position,
Duration = duration,
NewCombo = newCombo
// Spinners cannot have combo offset.
};
}
protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration)
{
return lastObject = null;
}
}
}

View File

@ -1,22 +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.
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Objects.Legacy.Osu
{
/// <summary>
/// Legacy osu! Slider-type, used for parsing Beatmaps.
/// </summary>
internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition, IHasGenerateTicks
{
public Vector2 Position { get; set; }
public float X => Position.X;
public float Y => Position.Y;
public bool GenerateTicks { get; set; } = true;
}
}

View File

@ -1,24 +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.
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Objects.Legacy.Osu
{
/// <summary>
/// Legacy osu! Spinner-type, used for parsing Beatmaps.
/// </summary>
internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasPosition
{
public double Duration { get; set; }
public double EndTime => StartTime + Duration;
public Vector2 Position { get; set; }
public float X => Position.X;
public float Y => Position.Y;
}
}

View File

@ -1,12 +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.
namespace osu.Game.Rulesets.Objects.Legacy.Taiko
{
/// <summary>
/// Legacy osu!taiko Hit-type, used for parsing Beatmaps.
/// </summary>
internal sealed class ConvertHit : ConvertHitObject
{
}
}

View File

@ -1,51 +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 osuTK;
using System.Collections.Generic;
using osu.Game.Audio;
namespace osu.Game.Rulesets.Objects.Legacy.Taiko
{
/// <summary>
/// A HitObjectParser to parse legacy osu!taiko Beatmaps.
/// </summary>
public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
{
public ConvertHitObjectParser(double offset, int formatVersion)
: base(offset, formatVersion)
{
}
protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
{
return new ConvertHit();
}
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
IList<IList<HitSampleInfo>> nodeSamples)
{
return new ConvertSlider
{
Path = new SliderPath(controlPoints, length),
NodeSamples = nodeSamples,
RepeatCount = repeatCount
};
}
protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration)
{
return new ConvertSpinner
{
Duration = duration
};
}
protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration)
{
return null;
}
}
}

View File

@ -1,12 +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.
namespace osu.Game.Rulesets.Objects.Legacy.Taiko
{
/// <summary>
/// Legacy osu!taiko Slider-type, used for parsing Beatmaps.
/// </summary>
internal sealed class ConvertSlider : Legacy.ConvertSlider
{
}
}

View File

@ -119,6 +119,11 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
public long MaximumTotalScore { get; private set; }
/// <summary>
/// The maximum achievable combo.
/// </summary>
public int MaximumCombo { get; private set; }
/// <summary>
/// The maximum sum of accuracy-affecting judgements at the current point in time.
/// </summary>
@ -423,6 +428,7 @@ namespace osu.Game.Rulesets.Scoring
MaximumResultCounts.AddRange(ScoreResultCounts);
MaximumTotalScore = TotalScore.Value;
MaximumCombo = HighestCombo.Value;
}
ScoreResultCounts.Clear();

View File

@ -316,9 +316,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary>
/// Refreshes the current placement tool.
/// </summary>
private void refreshTool()
private void refreshPlacement()
{
removePlacement();
CurrentPlacement?.EndPlacement(false);
CurrentPlacement?.Expire();
CurrentPlacement = null;
ensurePlacementCreated();
}
@ -344,21 +347,24 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
case PlacementBlueprint.PlacementState.Waiting:
if (!Composer.CursorInPlacementArea)
removePlacement();
CurrentPlacement.Hide();
else
CurrentPlacement.Show();
break;
case PlacementBlueprint.PlacementState.Active:
CurrentPlacement.Show();
break;
case PlacementBlueprint.PlacementState.Finished:
removePlacement();
refreshPlacement();
break;
}
}
if (Composer.CursorInPlacementArea)
ensurePlacementCreated();
// updates the placement with the latest editor clock time.
if (CurrentPlacement != null)
// updates the placement with the latest editor clock time.
updatePlacementTimeAndPosition();
}
}
protected override bool OnMouseMove(MouseMoveEvent e)
@ -386,7 +392,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void hitObjectAdded(HitObject obj)
{
// refresh the tool to handle the case of placement completing.
refreshTool();
refreshPlacement();
// on successful placement, the new combo button should be reset as this is the most common user interaction.
if (Beatmap.SelectedHitObjects.Count == 0)
@ -415,14 +421,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
public void CommitIfPlacementActive()
{
CurrentPlacement?.EndPlacement(CurrentPlacement.PlacementActive == PlacementBlueprint.PlacementState.Active);
removePlacement();
}
private void removePlacement()
{
CurrentPlacement?.EndPlacement(false);
CurrentPlacement?.Expire();
CurrentPlacement = null;
refreshPlacement();
}
private CompositionTool currentTool;

View File

@ -258,6 +258,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void resetTernaryStates()
{
if (SelectionNewComboState.Value == TernaryState.Indeterminate)
SelectionNewComboState.Value = TernaryState.False;
AutoSelectionBankEnabled.Value = true;
SelectionAdditionBanksEnabled.Value = true;
SelectionBankStates[HIT_BANK_AUTO].Value = TernaryState.True;
@ -269,7 +271,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
protected virtual void UpdateTernaryStates()
{
SelectionNewComboState.Value = GetStateFromSelection(SelectedItems.OfType<IHasComboInformation>(), h => h.NewCombo);
if (SelectedItems.Any())
SelectionNewComboState.Value = GetStateFromSelection(SelectedItems.OfType<IHasComboInformation>(), h => h.NewCombo);
AutoSelectionBankEnabled.Value = SelectedItems.Count == 0;
var samplesInSelection = SelectedItems.SelectMany(enumerateAllSamples).ToArray();

View File

@ -17,6 +17,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
protected OsuSpriteText Label { get; private set; }
protected Container LabelContainer { get; private set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
@ -26,7 +28,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
InternalChildren = new Drawable[]
{
new Container
LabelContainer = new Container
{
AutoSizeAxes = Axes.X,
Height = 16,

View File

@ -16,6 +16,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
@ -40,6 +41,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved]
private Editor? editor { get; set; }
[Resolved]
private TimelineBlueprintContainer? timelineBlueprintContainer { get; set; }
public SamplePointPiece(HitObject hitObject)
{
HitObject = hitObject;
@ -53,15 +57,45 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected virtual double GetTime() => HitObject is IHasRepeats r ? HitObject.StartTime + r.Duration / r.SpanCount() / 2 : HitObject.StartTime;
[BackgroundDependencyLoader]
private void load()
private void load(OsuConfigManager config)
{
HitObject.DefaultsApplied += _ => updateText();
Label.AllowMultiline = false;
LabelContainer.AutoSizeAxes = Axes.None;
updateText();
if (editor != null)
editor.ShowSampleEditPopoverRequested += onShowSampleEditPopoverRequested;
}
private readonly Bindable<bool> contracted = new Bindable<bool>();
protected override void LoadComplete()
{
base.LoadComplete();
if (timelineBlueprintContainer != null)
contracted.BindTo(timelineBlueprintContainer.SamplePointContracted);
contracted.BindValueChanged(v =>
{
if (v.NewValue)
{
Label.FadeOut(200, Easing.OutQuint);
LabelContainer.ResizeTo(new Vector2(12), 200, Easing.OutQuint);
LabelContainer.CornerRadius = 6;
}
else
{
Label.FadeIn(200, Easing.OutQuint);
LabelContainer.ResizeTo(new Vector2(Label.Width, 16), 200, Easing.OutQuint);
LabelContainer.CornerRadius = 8;
}
}, true);
FinishTransforms();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
@ -87,6 +121,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updateText()
{
Label.Text = $"{abbreviateBank(GetBankValue(GetSamples()))} {GetVolumeValue(GetSamples())}";
if (!contracted.Value)
LabelContainer.ResizeWidthTo(Label.Width, 200, Easing.OutQuint);
}
private static string? abbreviateBank(string? bank)

View File

@ -19,17 +19,22 @@ using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
[Cached]
internal partial class TimelineBlueprintContainer : EditorBlueprintContainer
{
[Resolved(CanBeNull = true)]
private Timeline timeline { get; set; }
[Resolved(CanBeNull = true)]
private EditorClock editorClock { get; set; }
private Bindable<HitObject> placement;
private SelectionBlueprint<HitObject> placementBlueprint;
@ -118,9 +123,53 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
base.Update();
updateSamplePointContractedState();
updateStacking();
}
public Bindable<bool> SamplePointContracted = new Bindable<bool>();
private void updateSamplePointContractedState()
{
const double minimum_gap = 28;
if (timeline == null || editorClock == null)
return;
// Find the smallest time gap between any two sample point pieces
double smallestTimeGap = double.PositiveInfinity;
double lastTime = double.PositiveInfinity;
// The blueprints are ordered in reverse chronological order
foreach (var selectionBlueprint in SelectionBlueprints)
{
var hitObject = selectionBlueprint.Item;
// Only check the hit objects which are visible in the timeline
// SelectionBlueprints can contain hit objects which are not visible in the timeline due to selection keeping them alive
if (hitObject.StartTime > editorClock.CurrentTime + timeline.VisibleRange / 2)
continue;
if (hitObject.GetEndTime() < editorClock.CurrentTime - timeline.VisibleRange / 2)
break;
if (hitObject is IHasRepeats hasRepeats)
smallestTimeGap = Math.Min(smallestTimeGap, hasRepeats.Duration / hasRepeats.SpanCount() / 2);
double gap = lastTime - hitObject.GetEndTime();
// If the gap is less than 1ms, we can assume that the objects are stacked on top of each other
// Contracting doesn't make sense in this case
if (gap > 1 && gap < smallestTimeGap)
smallestTimeGap = gap;
lastTime = hitObject.StartTime;
}
double smallestAbsoluteGap = ((TimelineSelectionBlueprintContainer)SelectionBlueprints).ContentRelativeToAbsoluteFactor.X * smallestTimeGap;
SamplePointContracted.Value = smallestAbsoluteGap < minimum_gap;
}
private readonly Stack<HitObject> currentConcurrentObjects = new Stack<HitObject>();
private void updateStacking()
@ -291,6 +340,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
protected override HitObjectOrderedSelectionContainer Content { get; }
public Vector2 ContentRelativeToAbsoluteFactor => Content.RelativeToAbsoluteFactor;
public TimelineSelectionBlueprintContainer()
{
AddInternal(new TimelinePart<SelectionBlueprint<HitObject>>(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });

View File

@ -3,6 +3,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Transforms;
@ -32,10 +33,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override Container<Drawable> Content => zoomedContent;
/// <summary>
/// The current zoom level of <see cref="ZoomableScrollContainer"/>.
/// The current (final) zoom level of <see cref="ZoomableScrollContainer"/>.
/// It may differ from <see cref="Zoom"/> during transitions.
/// </summary>
public float CurrentZoom { get; private set; } = 1;
public BindableFloat CurrentZoom { get; private set; } = new BindableFloat(1);
private bool isZoomSetUp;
@ -98,7 +99,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
minZoom = minimum;
maxZoom = maximum;
CurrentZoom = zoomTarget = initial;
CurrentZoom.Value = zoomTarget = initial;
zoomedContentWidthCache.Invalidate();
isZoomSetUp = true;
@ -124,7 +125,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (IsLoaded)
setZoomTarget(newZoom, ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), zoomedContent).X);
else
CurrentZoom = zoomTarget = newZoom;
CurrentZoom.Value = zoomTarget = newZoom;
}
protected override void UpdateAfterChildren()
@ -154,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updateZoomedContentWidth()
{
zoomedContent.Width = DrawWidth * CurrentZoom;
zoomedContent.Width = DrawWidth * CurrentZoom.Value;
zoomedContentWidthCache.Validate();
}
@ -238,7 +239,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
float expectedWidth = d.DrawWidth * newZoom;
float targetOffset = expectedWidth * (focusPoint / contentSize) - focusOffset;
d.CurrentZoom = newZoom;
d.CurrentZoom.Value = newZoom;
d.updateZoomedContentWidth();
// Temporarily here to make sure ScrollTo gets the correct DrawSize for scrollable area.
@ -247,7 +248,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
d.ScrollTo(targetOffset, false);
}
protected override void ReadIntoStartValue(ZoomableScrollContainer d) => StartValue = d.CurrentZoom;
protected override void ReadIntoStartValue(ZoomableScrollContainer d) => StartValue = d.CurrentZoom.Value;
}
}
}

View File

@ -10,17 +10,21 @@ namespace osu.Game.Screens.Edit.Compose
public interface IPlacementHandler
{
/// <summary>
/// Notifies that a placement has begun.
/// Notifies that a placement blueprint became visible on the screen.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> being placed.</param>
void BeginPlacement(HitObject hitObject);
/// <param name="hitObject">The <see cref="HitObject"/> representing the placement.</param>
void ShowPlacement(HitObject hitObject);
/// <summary>
/// Notifies that a placement has finished.
/// Notifies that a visible placement blueprint has been hidden.
/// </summary>
void HidePlacement();
/// <summary>
/// Notifies that a placement has been committed.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> that has been placed.</param>
/// <param name="commit">Whether the object should be committed.</param>
void EndPlacement(HitObject hitObject, bool commit);
void CommitPlacement(HitObject hitObject);
/// <summary>
/// Deletes a <see cref="HitObject"/>.

View File

@ -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),
}
}
}

View File

@ -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);

View File

@ -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

View File

@ -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 =>

View File

@ -0,0 +1,55 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.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;
}
}
}
}

View File

@ -41,7 +41,7 @@ namespace osu.Game.Screens.Menu
public Action? OnEditBeatmap;
public Action? OnEditSkin;
public Action? OnExit;
public Action<UIEvent>? OnExit;
public Action? OnBeatmapListing;
public Action? OnSolo;
public Action? OnSettings;
@ -104,11 +104,11 @@ namespace osu.Game.Screens.Menu
buttonArea.AddRange(new Drawable[]
{
new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), _ => OnSettings?.Invoke(), Key.O, Key.S)
new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), (_, _) => OnSettings?.Invoke(), Key.O, Key.S)
{
Padding = new MarginPadding { Right = WEDGE_WIDTH },
},
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), _ => State = ButtonSystemState.TopLevel)
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), (_, _) => State = ButtonSystemState.TopLevel)
{
Padding = new MarginPadding { Right = WEDGE_WIDTH },
VisibleStateMin = ButtonSystemState.Play,
@ -132,7 +132,7 @@ namespace osu.Game.Screens.Menu
[BackgroundDependencyLoader]
private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host)
{
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => OnSolo?.Invoke(), Key.P)
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), (_, _) => OnSolo?.Invoke(), Key.P)
{
Padding = new MarginPadding { Left = WEDGE_WIDTH },
});
@ -141,22 +141,22 @@ namespace osu.Game.Screens.Menu
buttonsPlay.Add(new DailyChallengeButton(@"button-daily-select", new Color4(94, 63, 186, 255), onDailyChallenge, Key.D));
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), _ => OnEditBeatmap?.Invoke(), Key.B, Key.E)
buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), (_, _) => OnEditBeatmap?.Invoke(), Key.B, Key.E)
{
Padding = new MarginPadding { Left = WEDGE_WIDTH },
});
buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), _ => OnEditSkin?.Invoke(), Key.S));
buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), (_, _) => OnEditSkin?.Invoke(), Key.S));
buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit);
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), _ => State = ButtonSystemState.Play, Key.P, Key.M, Key.L)
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), (_, _) => State = ButtonSystemState.Play, Key.P, Key.M, Key.L)
{
Padding = new MarginPadding { Left = WEDGE_WIDTH },
});
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), _ => State = ButtonSystemState.Edit, Key.E));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), _ => OnBeatmapListing?.Invoke(), Key.B, Key.D));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), (_, _) => State = ButtonSystemState.Edit, Key.E));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), (_, _) => OnBeatmapListing?.Invoke(), Key.B, Key.D));
if (host.CanExit)
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), _ => OnExit?.Invoke(), Key.Q));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), (_, e) => OnExit?.Invoke(e), Key.Q));
buttonArea.AddRange(buttonsPlay);
buttonArea.AddRange(buttonsEdit);
@ -179,7 +179,7 @@ namespace osu.Game.Screens.Menu
sampleLogoSwoosh = audio.Samples.Get(@"Menu/osu-logo-swoosh");
}
private void onMultiplayer(MainMenuButton _)
private void onMultiplayer(MainMenuButton mainMenuButton, UIEvent uiEvent)
{
if (api.State.Value != APIState.Online)
{
@ -190,7 +190,7 @@ namespace osu.Game.Screens.Menu
OnMultiplayer?.Invoke();
}
private void onPlaylists(MainMenuButton _)
private void onPlaylists(MainMenuButton mainMenuButton, UIEvent uiEvent)
{
if (api.State.Value != APIState.Online)
{
@ -201,7 +201,7 @@ namespace osu.Game.Screens.Menu
OnPlaylists?.Invoke();
}
private void onDailyChallenge(MainMenuButton button)
private void onDailyChallenge(MainMenuButton button, UIEvent uiEvent)
{
if (api.State.Value != APIState.Online)
{

Some files were not shown because too many files have changed in this diff Show More