191 Commits

Author SHA1 Message Date
65f275106e update license information in various places (and readme) 2025-08-31 22:47:36 +03:00
6e374762fd synchronize with github 2025-08-31 21:26:21 +03:00
80646a166c add settings toggle to not delete imported archives 2025-08-31 19:39:13 +03:00
8f0510d903 made beatmap anonymization code a bit less awful 2025-08-31 15:06:24 +03:00
02e7000ee4 update editor beatmap anonymization code to update not just first diff 2025-08-30 22:56:01 +03:00
Dean Herbert
c26e669fc5 Merge pull request #34837 from smoogipoo/more-testable-footers
Add footer to `ScreenTestScene`
2025-08-30 21:52:30 +09:00
081355864e add beatmap editor option to remove online data of a map 2025-08-29 20:19:39 +03:00
6435a835d1 fix background category selection (this time for real) 2025-08-29 18:48:13 +03:00
Dean Herbert
e621eed0ba Merge pull request #34841 from peppy/interpolate-more
Adjust interpolation workaround to catch-up slightly smoother
2025-08-29 22:04:20 +09:00
Dean Herbert
41b8033ebd Adjust interpolation workaround to catch-up slightly smoother 2025-08-29 21:24:23 +09:00
Dean Herbert
1d9de88aaa Merge pull request #34836 from bdach/online-lookups-on-reenter
Fix song select not performing online lookup on re-enter
2025-08-29 21:21:34 +09:00
Dean Herbert
3ca5e20e70 Merge branch 'master' into online-lookups-on-reenter 2025-08-29 19:03:41 +09:00
Dean Herbert
404044e8d7 Merge pull request #34822 from bdach/carousel-multi-grouping-clean 2025-08-29 19:02:58 +09:00
Dan Balasescu
d5575b4037 Merge pull request #34830 from minetoblend/fix/hitobject-lifetime-delay
Fix hitobject drawables becoming visible 1 frame too late
2025-08-29 18:38:24 +09:00
Dan Balasescu
04ba5aa575 Move footer to ScreenTestScene 2025-08-29 17:49:48 +09:00
Bartłomiej Dach
51ed19cb99 Merge branch 'master' into carousel-multi-grouping-clean 2025-08-29 10:41:46 +02:00
Dean Herbert
0a408a3ac4 Fix tournament test failure due to control change 2025-08-29 17:12:24 +09:00
Dean Herbert
22ba956f25 Merge pull request #34833 from peppy/fix-mod-cosmetics
Fix some mods showing tooltips when settings are default
2025-08-29 17:09:01 +09:00
Bartłomiej Dach
526ee32268 Apply suggested rename 2025-08-29 09:58:56 +02:00
Bartłomiej Dach
df6d6edaca Fix song select not performing online lookup on re-enter
Closes https://github.com/ppy/osu/issues/34825.

Root cause is

	24ec43b3b6/osu.Game/Screens/SelectV2/SongSelect.cs (L345-L356)

not specifying `(..., true)`, therefore the fetch doesn't happen on
enter if song select doesn't change the global beatmap as a side effect
of the enter, which is the case on re-entering.
2025-08-29 09:31:20 +02:00
Bartłomiej Dach
24ec43b3b6 Merge pull request #34834 from peppy/tournament-warmup-better
Use switches for warmup/chat toggles in tournament interface
2025-08-29 09:09:20 +02:00
Dean Herbert
9e77a5b050 Fix obviously incorrect conditional
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
2025-08-29 16:01:49 +09:00
Dean Herbert
12832e9fef Use switches for warmup/chat toggles in tournament interface
As proposed in https://github.com/ppy/osu/discussions/32515.
2025-08-29 14:35:42 +09:00
Dean Herbert
e83f3d5e77 Fix some mods showing tooltips when settings are default 2025-08-29 14:08:18 +09:00
Dean Herbert
f2f5cf19a2 Return early to avoid creating mod description strings unnecessarily 2025-08-29 14:08:06 +09:00
marvin
bb9f9e4d35 Fix operations in PooledDrawableWithLifetimeContainer.CheckChildrenLife being in wrong order
Previously CompositeDrawable.CheckChildrenLife() would be run before lifetimeManager.Update() which lead to the new drawables being inserted into the container but not being made alive immediately, leading to the drawable not becoming visibile until the next update loop.
2025-08-28 23:36:29 +02:00
Bartłomiej Dach
6e1316241a Merge pull request #34774 from genskyff/feat/long-note-ratio
Add long note percentage filter for mania mode
2025-08-28 14:32:52 +02:00
Bartłomiej Dach
3eaa5314ac Merge pull request #34808 from peppy/fix-difficulty-churn
Fix beatmap carousel triggering full filters more often than it needs to
2025-08-28 14:26:25 +02:00
Bartłomiej Dach
6ba72fa481 Adjust tests to new beatmap set model usage in carousel 2025-08-28 13:09:11 +02:00
Bartłomiej Dach
8dd131f17e Support beatmap sets being split apart by the active group mode in beatmap carousel 2025-08-28 13:09:08 +02:00
Dean Herbert
7e109add96 Ensure filtering also runs after local gameplay LastPlayed changes 2025-08-28 19:10:20 +09:00
Dean Herbert
f953d58922 Merge pull request #34740 from bdach/maximised-player-audio
Always use audio from maximised player if there is one in multiplayer spectator
2025-08-28 18:27:56 +09:00
Bartłomiej Dach
47164c61b4 Add failing test coverage of splitting beatmap sets apart 2025-08-28 11:00:23 +02:00
Bartłomiej Dach
311c75aa53 Adjust test after allowing grouping modes to split beatmap sets apart 2025-08-28 11:00:23 +02:00
Dean Herbert
0b40f1d0db Merge pull request #34818 from smoogipoo/completion-handler-override
Preserve pre-post notification completion target
2025-08-28 14:10:59 +09:00
Dan Balasescu
e831d1b6fa Preserve pre-post notification completion target 2025-08-28 13:07:05 +09:00
Dean Herbert
ed15e1fb88 Merge branch 'master' into fix-difficulty-churn 2025-08-28 02:38:10 +09:00
Dean Herbert
33b99a51b1 Merge pull request #34812 from bdach/failed-at-judgement
Fix `HealthProcessor` potentially incorrectly reverting failed state
2025-08-28 02:37:44 +09:00
Dean Herbert
8a6c857719 Fix hidden beatmap state not being reflected immediately 2025-08-28 02:33:18 +09:00
Dean Herbert
5abd93eda7 Merge pull request #34809 from peppy/fix-star-rating-weird-mod-double-handling
Fix beatmap panels locally handling mod and ruleset changes unnecessarily
2025-08-28 01:27:31 +09:00
Bartłomiej Dach
4030383276 Allow grouping modes that apply max aggregate to split beatmap sets apart 2025-08-27 14:45:20 +02:00
Dean Herbert
f9c1b24df4 Apply in more places 2025-08-27 19:59:44 +09:00
Bartłomiej Dach
197c318180 Fix HealthProcessor potentially incorrectly reverting failed state
This stems from me looking into `TestSceneFailAnimation` failures
(https://github.com/ppy/osu/runs/48663953318). As it turns out, I should
not have been mad by CI, and rather should have been mad at myself for
failing to read.

`FailedAtJudgement` in fact does not mean "this judgement, and only this
judgement, triggered failure". If any further judgements occur
post-fail, they will also have `FailedAtJudgement` set to true. It is
essentially a *dump* of the state of `HealthProcessor.Failed` prior to
applying the judgement.

	ec21685c25/osu.Game/Rulesets/Scoring/HealthProcessor.cs (L49-L57)

Because of this, reverting several judgements which occur post-fail
could lead to failed state reverting earlier than intended, and thus
potentially trigger a second fail, thus tripping the `Player` assertion.
2025-08-27 12:27:11 +02:00
Dean Herbert
fda40d7fd5 Fix beatmap panels locally handling mod changes unnecessarily
The `BeatmapDifficultyCache` handles mod changes, so handling locally is
unnecessary. By handling locally, it creates a visual issue when
adjusting mods often. Test using Ctrl +/- at song select and observing
that without this change, the star rating will flicker back to the
default due to the local re-fetch.
2025-08-27 18:31:50 +09:00
Dean Herbert
be6fb9aa77 Fix beatmap carousel re-filtering when it doesn't need to
Local rules ensure we only handle callbacks when we need to.
2025-08-27 18:21:19 +09:00
Dean Herbert
0e57ee9ba6 Avoid triggering changes when add operations are empty
Only seems to happen in tests. I think.
2025-08-27 18:13:13 +09:00
Dean Herbert
043235fed2 Add test coverage ensuring filtering does not occur on unnecessary updates 2025-08-27 18:13:12 +09:00
Dean Herbert
ec21685c25 Merge pull request #34803 from peppy/update-framework-please-no-clock-breakage
Update framework
2025-08-27 13:23:09 +09:00
628181a883 also update colour on load 2025-08-27 01:11:57 +03:00
835329efd3 synchronize with github 2025-08-27 00:59:45 +03:00
5399943118 update logo colour only when changing setting value 2025-08-27 00:58:26 +03:00
d07f82f6f4 refactor custom seasonal background code
some of it may be trauma-inducing, but I don't know how to make it
better
2025-08-27 00:43:26 +03:00
Dean Herbert
244bad07c7 Update framework 2025-08-26 21:43:09 +09:00
Binwalker
149f18c3f5 test(ManiaFilterCriteriaTest): simplify test case 2025-08-26 21:36:20 +09:00
Binwalker
6a82b7331f refactor(ManiaFilterCriteria): exclude converted beatmaps from long note filter 2025-08-26 21:36:20 +09:00
Binwalker
65253708d8 test(ManiaFilterCriteriaTest): fix some test case for ln filter 2025-08-26 21:36:20 +09:00
Binwalker
556c2469bf fix(ManiaFilterCriteria): converted beatmaps are not included 2025-08-26 21:36:20 +09:00
Binwalker
f7b0e114a9 test(ManiaFilterCriteriaTest): add some testcase 2025-08-26 21:36:20 +09:00
Binwalker
68677200f3 feat(ManiaFilterCriteria): add long note ratio filter for mania 2025-08-26 21:36:19 +09:00
Dean Herbert
2bea59e65f Merge pull request #34802 from bdach/hack-around-carousel-panel-refresh
Work around excessive refreshes of carousel beatmap set panel backgrounds
2025-08-26 21:13:28 +09:00
Bartłomiej Dach
c0fd5637de Work around excessive refreshes of carousel beatmap set panel backgrounds
Closes https://github.com/ppy/osu/issues/34511 I guess.
2025-08-26 13:27:54 +02:00
Bartłomiej Dach
5e7a99c97f Merge pull request #34801 from peppy/replay-player-null
Fix crash on exiting `ReplayPlayer` is beatmap was not loaded successfully
2025-08-26 12:11:27 +02:00
Bartłomiej Dach
8f628d16ae Merge pull request #34800 from peppy/fix-daily-challenge-leaderboard-skip
Fix daily challenge / playlist leaderboard sometimes showing incorrect default state
2025-08-26 12:07:41 +02:00
Dean Herbert
2ccb65aa65 Add test coverage and fix one more fail case 2025-08-26 18:41:14 +09:00
Dean Herbert
4d851f2527 Fix crash on exiting ReplayPlayer is beatmap was not loaded successfully
Closes https://github.com/ppy/osu/issues/34763.
2025-08-26 18:31:42 +09:00
Dean Herbert
4bafbfb9e4 Apply NRT to ReplayPlayer for good measure 2025-08-26 18:30:12 +09:00
Dean Herbert
3f179e3903 Sort scores immediately for good measure 2025-08-26 17:51:14 +09:00
Dean Herbert
196b28115e Fix playlist leaderboard provider potentially inserting local user in wrong order
Due to `Perform` being used from a BDL method in conjunction with
`Success` (which is scheduled to the *update* thread), there was a
chance that the order of execution would be not quite as intended.

To rectify, let's not use `Success` and just continue with synchronous
flow.
2025-08-26 17:51:02 +09:00
Dean Herbert
7660a9ba8e Merge pull request #34794 from bdach/fix-aim-meter
Fix aim error meter applying incorrect scaling constant in normalised mode
2025-08-26 15:16:15 +09:00
Bartłomiej Dach
e908b80359 Fix aim error meter applying incorrect scaling constant in relative mode
Closes https://github.com/ppy/osu/issues/34769

Visible (and easiest to check) in test scene.
2025-08-25 14:20:05 +02:00
Bartłomiej Dach
a2bf8e3988 Fix copy-paste fail in log message 2025-08-25 13:43:03 +02:00
5b186bb740 potentially make logo look less weird (untested)
uh, yeah, I accidentally flipped the colors around in the UpdateColour() method (which I should've probably make private or protected), and it's the reason why the logo overall looked dimmer than it should've

anyhow, this should *probably* look a bit better, don't have any means to test it yet though
2025-08-25 12:46:27 +02:00
Bartłomiej Dach
6e8246b539 Merge pull request #34761 from frenzibyte/fix-flashlight
Fix flashlight not always matching gameplay scaling
2025-08-25 12:03:58 +02:00
6cb99c13c2 added cookie color customization (which shouldn't have been done) 2025-08-24 19:46:36 +03:00
Salman Alshamrani
3cca458c21 Fix xmldoc error and reword 2025-08-24 18:55:45 +03:00
Salman Alshamrani
bc59270f3e Fix flashlight not handling internal playfield sizing changes
Note that this does not handle sizing/scaling changes applied directly
to `Playfield`, but it handles any changes within the layers inside
`PlayfieldAdjustmentContainer`.
2025-08-24 17:51:05 +03:00
96008e06ab added settings toggle for song select v1 2025-08-24 05:42:59 +03:00
590b0a8028 welcome back select v1 2025-08-24 04:55:25 +03:00
70f7f09a83 synchronize with github (ppy/osu) 2025-08-23 18:11:07 +03:00
Dean Herbert
16343fd7d6 Merge pull request #34766 from bdach/remove-double-lookup
Pull up online beatmap set lookup to song select level to avoid two components doing the same fetch independently
2025-08-23 20:50:17 +09:00
Dean Herbert
acafc06bcc Merge pull request #34757 from bdach/new-mod-icons
Update mod icons
2025-08-23 19:55:10 +09:00
Salman Alshamrani
c0c3690908 Remove no longer valid test 2025-08-23 09:28:14 +03:00
490137405f added installer build script, adapted autoupdates (not functional yet) 2025-08-22 22:26:17 +03:00
f3c6f53f70 added version to 'experimental version' banner 2025-08-22 20:19:18 +03:00
8cb5c682b4 make seasonal bg config strings localisable 2025-08-22 19:07:15 +03:00
c3d79295d3 made seasonal background notifications transient 2025-08-22 16:07:28 +03:00
Bartłomiej Dach
5292d4a04e Fix song select favourite button potentially showing stale data from (un)favourite request callback 2025-08-22 14:25:25 +02:00
Bartłomiej Dach
d3ae20dd88 Pull up online beatmap set lookup to song select level to avoid two components doing the same fetch independently 2025-08-22 14:25:21 +02:00
Dean Herbert
c852e5854c Merge pull request #34723 from bdach/status-updates-are-great-arent-they
Refresh realm before performing song select refetches following an online metadata lookup
2025-08-22 20:25:36 +09:00
Dan Balasescu
0756c45d70 No longer download iOS simulator
https://github.com/actions/runner-images/issues/12862#issuecomment-3209787203
2025-08-22 13:29:46 +09:00
f31d310135 small fixes (vibe-sleeping) 2025-08-22 04:17:25 +03:00
26029de27d Added customs wallpaper in a menu. 2025-08-22 03:10:55 +03:00
c37f72f567 jvnkosu initial bringup
Some checks failed
Continuous Integration / Code Quality (push) Has been cancelled
Continuous Integration / Test (map[fullname:ubuntu-latest prettyname:Linux], MultiThreaded) (push) Has been cancelled
Continuous Integration / Test (map[fullname:ubuntu-latest prettyname:Linux], SingleThread) (push) Has been cancelled
Continuous Integration / Test (map[fullname:windows-latest prettyname:Windows], MultiThreaded) (push) Has been cancelled
Continuous Integration / Test (map[fullname:windows-latest prettyname:Windows], SingleThread) (push) Has been cancelled
Continuous Integration / Build only (Android) (push) Has been cancelled
Continuous Integration / Build only (iOS) (push) Has been cancelled
2025-08-21 22:12:07 +03:00
Salman Alshamrani
73624e4e25 Add visual test setup for taiko flashlight 2025-08-21 19:03:43 +03:00
Salman Alshamrani
f374af7ce7 Fix taiko flashlight applying aspect ratio twice 2025-08-21 19:03:43 +03:00
Salman Alshamrani
7530ad1a7b Adjust default flashlight size on osu! & osu!catch
Because the flashlight is made to be scaled by playfield, there are
constant scale factors applied somewhere in the
`PlayfieldAdjustmentContainer` which needs to be reflected in the
flashlight size to keep the size the same.

The factor is specifically 1.6x, computed in {Osu,Catch}PlayfieldAdjustmentContainer.ScalingContainer`.

More generally, I've deduced these factors by logging the difference
between the `flashlightSize` before and after b78abe2f.
2025-08-21 19:03:43 +03:00
Salman Alshamrani
a049f5065d Fix flashlight not correctly scaled to match playfield 2025-08-21 19:03:43 +03:00
Bartłomiej Dach
4627c8a859 Update resources 2025-08-21 14:44:43 +02:00
Dean Herbert
30f7da8f71 Merge pull request #34759 from bdach/BACKGROUND-STUCK-PLEASE-I-BEG-YOU
Fix song select background being stuck in revealed state
2025-08-21 21:01:59 +09:00
Dean Herbert
4b8ff481fd Merge pull request #34752 from bdach/avoid-endless-futile-backpopulation
Fix submission & rank date backpopulation failing every launch for some users
2025-08-21 20:37:03 +09:00
Bartłomiej Dach
a7f1795f98 Fix song select background being stuck in revealed state
Closes https://github.com/ppy/osu/issues/34731.

The failure scenario here is as follows:

- User holds down left mouse button for >200ms to reveal the background.
- User presses down another mouse button and releases it in <200ms.
- User releases left mouse button. Song select does not return.

The timing here is key because what is happening here is that the second
mouse button press is overwriting the `revealingBackground` scheduled
delegate. Releasing that same mouse button within 200ms leads to that
scheduled delegate being cancelled and cleared, and thus the release of
left mouse wrongly decides there is nothing left to do.

One thing I'm not entirely sure about is the release behaviour even with
this change; as things stand, the first release of any mouse button will
bring song select back, even if it was not the button that was initially
held down to reveal the background. That's probably easily fixed if
deemed required, but I'm most interested in fixing the bad breakage.
2025-08-21 11:31:37 +02:00
Bartłomiej Dach
c053cfbf9b Adjust icon sizings in mod display to match new assets 2025-08-21 09:00:48 +02:00
Bartłomiej Dach
e47a60f303 Add test steps to mod icon test scene for exercising all rulesets 2025-08-21 09:00:46 +02:00
Bartłomiej Dach
92016a7d9b Add and use new mod icon assets 2025-08-21 09:00:44 +02:00
Dean Herbert
41885c0fc0 Merge pull request #34643 from frenzibyte/leaderboard-resize
Fix leaderboard not resizing correctly
2025-08-21 13:33:24 +09:00
Dean Herbert
e75a6b4010 Log bass issues for more than one frame 2025-08-21 13:27:14 +09:00
Dean Herbert
ddce11fbc8 Adjust bass invalid data threshold 2025-08-21 13:27:13 +09:00
Bartłomiej Dach
c894969d17 Fix submission & rank date failing every launch for some users
Addresses https://github.com/ppy/osu/discussions/34705, I suppose.

The cagey tone of that statement is because this change merely papers
over the issue. The issue in question for the user that reported this is
that they have a bunch of very old beatmaps, whose md5 hashes do not
match the online hashes, that need updating. The submission/rank date
population was running every single time for these, and failing every
time, because there is really not much useful that the lookup *can* do.

Because mappers have made `OnlineID` essentially useless for determining
the provenance of a beatmap due to reusing them to "fix" beatmap
submission failures, online IDs have been explicitly disallowed from use
in any sort of beatmap lookup flow. The only things that are allowed to
be used are: md5 of the beatmap, and filename as a fallback for very old
beatmaps / beatmap packs.

If the user has local beatmaps with md5 not matching online, chances are
that any metadata lookups are likely to fail or return bogus data. At
that point my personal feeling is that backpopulation flows should leave
such beatmaps well alone and the user should just go update the beatmap
themselves.

I am aware that updating 124 individual beatmap sets would - in the
current state of things - would probably be a ridiculously onerous thing
to do, and that people have been asking multiple times for a facility to
update all local beatmaps at once, but that discussion is out of scope
at this stage.
2025-08-20 09:21:35 +02:00
Bartłomiej Dach
47fecfb669 Merge pull request #34606 from cl8n/sliderpoint
Add skin support for sliderpoint10 and sliderpoint30
2025-08-19 15:06:20 +02:00
Bartłomiej Dach
3b49673e83 Merge pull request #34667 from Hiviexd/verify/exclude-audio-from-hs-check
Exclude all beatmap audios from the hitsounds format check
2025-08-19 14:33:35 +02:00
Bartłomiej Dach
b1296b0c83 Merge pull request #34666 from Hiviexd/verify/check-inconsistent-audio
Add verify check for inconsistent audio usage
2025-08-19 14:33:13 +02:00
Bartłomiej Dach
33df7dc5e5 Add test coverage 2025-08-19 13:53:18 +02:00
Bartłomiej Dach
ad6c0c272d Fix leaderboard score text never showing if leaderboard starts collapsed
Only seems to reproduce in gameplay for whatever reason. Can't justify
spending time to chase down why really because the previous code looked
obviously wrong on closer inspection anyway (`rightLayer` has transforms
applied to it on collapse/expand).
2025-08-19 13:32:01 +02:00
Dean Herbert
7c3249c24c Update resources 2025-08-19 20:01:06 +09:00
Bartłomiej Dach
083365f332 Always use audio from maximised player if there is one in multiplayer spectator 2025-08-19 12:03:35 +02:00
Bartłomiej Dach
62b4999184 Add failing test case 2025-08-19 12:03:12 +02:00
Dean Herbert
35ab30e83f Merge pull request #34710 from LumpBloom7/SSV2-beatmap-panel-missing-ruleset-icon
Use fallback icon in `PanelBeatmap` if ruleset is not found
2025-08-19 17:48:17 +09:00
Dean Herbert
aba160fb62 Merge pull request #25716 from cdwcgt/hitposition
Add `AimErrorMeter`
2025-08-19 17:30:47 +09:00
Dean Herbert
ceb8a621ff Adjust marker style description to look more correct in dropdown 2025-08-19 16:43:55 +09:00
Dean Herbert
cf38bdfb04 Merge pull request #34721 from bdach/branch-2
Fix even more issues with replay fail indicator
2025-08-18 23:34:11 +09:00
Bartłomiej Dach
a337c8bb99 Adjust weighted average to 90/10 to match bar error meter 2025-08-18 14:43:24 +02:00
Bartłomiej Dach
807ba111fd Remove unnecessary condition 2025-08-18 14:40:50 +02:00
Bartłomiej Dach
fde2887068 Fix average marker not moving to first hit position 2025-08-18 14:40:07 +02:00
Bartłomiej Dach
49bb157fb8 Remove undesirable switch syntax 2025-08-18 14:14:05 +02:00
Bartłomiej Dach
b2dbd4a9dc Make extracted helper more comprehensible 2025-08-18 14:12:56 +02:00
Bartłomiej Dach
8dd349fd17 Rewrite incomprehensible comments 2025-08-18 13:54:18 +02:00
Bartłomiej Dach
df210241fc Fix manual click test being broken 2025-08-18 13:46:06 +02:00
Bartłomiej Dach
d696ac99d4 Improve test coverage somewhat 2025-08-18 13:20:38 +02:00
Bartłomiej Dach
777ab61143 Rename everything to start with 2025-08-18 12:46:53 +02:00
Salman Alshamrani
bb5933ef80 Add test for scores with long score/combo numbers 2025-08-18 13:46:51 +03:00
Salman Alshamrani
62548244bc Hide right-side numbers when not enough space is available 2025-08-18 13:46:08 +03:00
Bartłomiej Dach
fe612d465b Merge branch 'master' into hitposition 2025-08-18 12:20:51 +02:00
Bartłomiej Dach
a393b3c6b1 Refresh realm before performing song select refetches following an online metadata lookup
Probably closes https://github.com/ppy/osu/issues/34716

Can't see any other cause, can reproduce the issue on master using
manual db modifications via realm studio and it is not a consistent
reproduction, so seems like an open-and-shut lack of refresh.
2025-08-18 09:48:32 +02:00
Bartłomiej Dach
d26f31b71d Unapply replay playback speed when going to results
closes https://github.com/ppy/osu/issues/34700
2025-08-18 09:09:34 +02:00
Bartłomiej Dach
59ec6ed2eb Stop fail sample when rewinding to before it in replay
closes https://github.com/ppy/osu/issues/34688

I originally wrote it this way semi-intentionally because I thought
cutting out the sample was worse than letting it play out, but I also
forgot that people use like seventy hour long fail samples.
2025-08-18 09:02:49 +02:00
Bartłomiej Dach
5b1b22cb66 Fix replay fail indicator "go to results" button being clickable while invisible
closes https://github.com/ppy/osu/issues/34685
2025-08-18 08:54:45 +02:00
Derrick Timmermans
a1fb7acef3 Use fallback icon if ruleset is not found 2025-08-17 16:11:42 +02:00
clayton
e77fb987a9 Don't switch on array index in slidertick test 2025-08-16 09:32:31 -07:00
clayton
c4163e33e5 Don't use switch expression 2025-08-16 05:08:30 -07:00
clayton
a96d00a55f Fix Slider case of test 2025-08-16 04:57:05 -07:00
clayton
6a16200314 Dedupe switch returns 2025-08-16 04:55:49 -07:00
clayton
4d1ecab4e3 Map sliderpoint textures directly to HitResult types 2025-08-16 04:54:34 -07:00
clayton
f2839c7b65 Rename class to be more consistent with other skins' judgement pieces 2025-08-16 04:09:40 -07:00
Hivie
0bcf29304b more correct hashset usage 2025-08-15 14:41:59 +01:00
Hivie
7b455efe34 exclude all beatmap audios from the check
- prevents false positives on maps with multiple audios
2025-08-15 00:11:16 +01:00
Hivie
14530fe894 add tests 2025-08-15 00:09:03 +01:00
Hivie
148bc4ac34 add check for inconsistent audio usage 2025-08-15 00:08:55 +01:00
Salman Alshamrani
a3443f76be Limit leaderboard size to sane minimum values 2025-08-13 12:52:08 +03:00
Salman Alshamrani
d998847271 Fix leaderboard not resizing correctly 2025-08-13 12:51:51 +03:00
Salman Alshamrani
62803af1de Add ability to resize leaderboard in tests 2025-08-13 12:51:09 +03:00
clayton
375da52a34 Fix judgement position when not supplied a drawable hit object 2025-08-10 07:16:57 -07:00
clayton
cd7a304640 Add tick judgement gallery test scene 2025-08-10 07:16:18 -07:00
clayton
bcc9bc4498 Remove hit lighting for tick hits 2025-08-10 07:15:37 -07:00
clayton
5fc5d0bd5f Fix tick hits in non-legacy skins 2025-08-10 07:15:20 -07:00
clayton
18803fbec0 Always show slider head judgement
Because it may display sliderpoints in classic behaviour
2025-08-10 07:13:47 -07:00
clayton
9542e77d16 Add sliderpoint10 and sliderpoint30 support 2025-08-10 07:12:59 -07:00
cdwcgt
0fa0568f13 Merge remote-tracking branch 'upstream/master' into hitposition 2025-08-03 00:35:54 +08:00
cdwcgt
771081b9a7 Merge remote-tracking branch 'upstream/master' into hitposition 2024-08-10 18:33:15 +08:00
cdwcgt
20921577d7 handle IApplicableToDifficulty for CS change. 2024-08-10 18:14:45 +08:00
cdwcgt
84c0bd0052 Merge branch 'master' into hitposition 2024-06-07 20:07:20 +09:00
cdwcgt
398ac1b98d improve testing
can change aim meter style in test
2024-06-07 20:05:27 +09:00
Dean Herbert
00fd22841d Merge branch 'master' into hitposition 2024-05-28 10:35:46 +09:00
cdwcgt
e487f20f1b Merge branch 'master' into hitposition 2024-04-06 11:23:28 +09:00
cdwcgt
33ab00ecd8 code format 2024-03-01 13:15:29 +09:00
cdwcgt
098ade2cfa Merge branch 'master' into hitposition 2024-02-20 19:36:32 +09:00
cdwcgt
0b5251fcf4 Remove dependencies on ScoreProcesser 2024-02-20 19:34:45 +09:00
cdwcgt
55bc043719 Merge branch 'master' into hitposition 2024-01-26 15:27:17 +09:00
cdwcgt
bf70552186 add comment, fix some 2024-01-26 14:32:52 +09:00
cdwcgt
9e3c7e2ca9 Rewrite some text and names 2024-01-26 00:43:37 +09:00
cdwcgt
d22435b55f rename param 2024-01-25 22:57:43 +09:00
cdwcgt
4e0dca69ed move AimErrorMeter to HUD 2024-01-25 17:45:56 +09:00
cdwcgt
5ffb92b638 Add new hit position style, change appearance
1. round the hit position that it will not beyond meter range.
2. add relative position style.
- in relative style, rotation can be apply that adjust the relative direction, in Absolute will use `UprightAspectMaintainingContai`ner to prevent rotate because it will cause confusing and meaningless.
3. use the cross-style in https://github.com/ppy/osu/pull/25716#issuecomment-1848974233
2024-01-25 17:45:02 +09:00
cdwcgt
1cd2331d28 expose FindRelativeHitPosition method 2024-01-25 17:27:22 +09:00
cdwcgt
814f39058e fix prefix in AimErrorMeterStrings not corrently 2023-12-13 19:07:39 +08:00
cdwcgt
f61cb3caa7 clear transforms and returned to pool after Clear()
from #25747
2023-12-13 19:06:35 +08:00
cdwcgt
07d81c0824 move AimErrorMeterStrings to HUD 2023-12-13 19:00:24 +08:00
cdwcgt
3195681805 fix getkey isn't match the name 2023-12-13 18:59:03 +08:00
cdwcgt
77a2ac8f42 remove dot at the end of label string 2023-12-11 07:58:41 +09:00
cdwcgt
6eda09aff4 Judgment -> Judgement 2023-12-11 07:57:32 +09:00
cdwcgt
380c3d0444 Zoom animation 2023-12-10 21:18:52 +09:00
cdwcgt
ad1fdc631d use FadeInFromZero to avoid sudden transition 2023-12-10 21:05:05 +09:00
cdwcgt
c16ef5eac3 cleanup 2023-12-10 20:55:05 +09:00
cdwcgt
f9076183d0 rename Hit position to Aim error
used in danser and I think this is better
2023-12-10 20:54:30 +09:00
cdwcgt
6fa6de7c27 remove empty line 2023-12-10 14:29:43 +09:00
cdwcgt
daff00300a add settings for hit position meter 2023-12-10 14:16:22 +09:00
cdwcgt
72d97f4ad6 + to x 2023-12-10 12:56:31 +09:00
cdwcgt
3e56633882 adjust object scale 2023-12-10 12:56:23 +09:00
cdwcgt
18d3e9154f add color for miss
but miss HitEvent have no position?
2023-12-10 12:47:21 +09:00
cdwcgt
eae4227c5a add gameObject to test 2023-12-10 12:47:02 +09:00
cdwcgt
de65e90abf typo, add border, remove usage of scoreprocessor 2023-12-10 10:26:07 +08:00
cdwcgt
01815de675 add HitPositionMeter and basic test 2023-12-10 10:25:37 +09:00
184 changed files with 3832 additions and 1055 deletions

View File

@@ -148,9 +148,7 @@ jobs:
# https://github.com/dotnet/macios/issues/19157
# https://github.com/actions/runner-images/issues/12758
- name: Use Xcode 16.4
run: |
sudo xcode-select -switch /Applications/Xcode_16.4.app
xcodebuild -downloadPlatform iOS
run: sudo xcode-select -switch /Applications/Xcode_16.4.app
- name: Build
run: dotnet build -c Debug osu.iOS.slnf

1
.gitignore vendored
View File

@@ -19,6 +19,7 @@ bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Pp]ub/
# Visual Studio 2015 cache/options directory
.vs/

36
.vscode/launch.json vendored
View File

@@ -7,9 +7,9 @@
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/osu.Desktop/bin/Debug/net8.0/osu!.dll"
"${workspaceFolder}/osu.Desktop/bin/Debug/net8.0/osu!.dll"
],
"cwd": "${workspaceRoot}",
"cwd": "${workspaceFolder}",
"preLaunchTask": "Build osu! (Debug)",
"console": "internalConsole"
},
@@ -19,9 +19,9 @@
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/osu.Desktop/bin/Release/net8.0/osu!.dll"
"${workspaceFolder}/osu.Desktop/bin/Release/net8.0/osu!.dll"
],
"cwd": "${workspaceRoot}",
"cwd": "${workspaceFolder}",
"preLaunchTask": "Build osu! (Release)",
"console": "internalConsole"
},
@@ -31,9 +31,9 @@
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/osu.Game.Tests/bin/Debug/net8.0/osu.Game.Tests.dll"
"${workspaceFolder}/osu.Game.Tests/bin/Debug/net8.0/osu.Game.Tests.dll"
],
"cwd": "${workspaceRoot}",
"cwd": "${workspaceFolder}",
"preLaunchTask": "Build tests (Debug)",
"console": "internalConsole"
},
@@ -43,9 +43,9 @@
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/osu.Game.Tests/bin/Release/net8.0/osu.Game.Tests.dll"
"${workspaceFolder}/osu.Game.Tests/bin/Release/net8.0/osu.Game.Tests.dll"
],
"cwd": "${workspaceRoot}",
"cwd": "${workspaceFolder}",
"preLaunchTask": "Build tests (Release)",
"console": "internalConsole"
},
@@ -55,10 +55,10 @@
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/osu.Desktop/bin/Debug/net8.0/osu!.dll",
"${workspaceFolder}/osu.Desktop/bin/Debug/net8.0/osu!.dll",
"--tournament"
],
"cwd": "${workspaceRoot}",
"cwd": "${workspaceFolder}",
"preLaunchTask": "Build osu! (Debug)",
"console": "internalConsole"
},
@@ -68,10 +68,10 @@
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/osu.Desktop/bin/Release/net8.0/osu!.dll",
"${workspaceFolder}/osu.Desktop/bin/Release/net8.0/osu!.dll",
"--tournament"
],
"cwd": "${workspaceRoot}",
"cwd": "${workspaceFolder}",
"preLaunchTask": "Build osu! (Release)",
"console": "internalConsole"
},
@@ -81,10 +81,10 @@
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/net8.0/osu.Game.Tournament.Tests.dll",
"${workspaceFolder}/osu.Game.Tournament.Tests/bin/Debug/net8.0/osu.Game.Tournament.Tests.dll",
"--tournament"
],
"cwd": "${workspaceRoot}",
"cwd": "${workspaceFolder}",
"preLaunchTask": "Build tournament tests (Debug)",
"console": "internalConsole"
},
@@ -94,10 +94,10 @@
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/net8.0/osu.Game.Tournament.Tests.dll",
"${workspaceFolder}/osu.Game.Tournament.Tests/bin/Debug/net8.0/osu.Game.Tournament.Tests.dll",
"--tournament"
],
"cwd": "${workspaceRoot}",
"cwd": "${workspaceFolder}",
"preLaunchTask": "Build tournament tests (Release)",
"console": "internalConsole"
},
@@ -105,12 +105,12 @@
"name": "Benchmark",
"type": "coreclr",
"request": "launch",
"program": "${workspaceRoot}/osu.Game.Benchmarks/bin/Release/net8.0/osu.Game.Benchmarks.dll",
"program": "${workspaceFolder}/osu.Game.Benchmarks/bin/Release/net8.0/osu.Game.Benchmarks.dll",
"args": [
"--filter",
"*"
],
"cwd": "${workspaceRoot}",
"cwd": "${workspaceFolder}",
"preLaunchTask": "Build benchmarks",
"console": "internalConsole"
}

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"dotnet.defaultSolution": "osu.Desktop.slnf"
}

View File

@@ -49,7 +49,7 @@
<PackageProjectUrl>https://github.com/ppy/osu</PackageProjectUrl>
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<Company>ppy Pty Ltd</Company>
<Company>ppy Pty Ltd, jvnkosu! team</Company>
<Copyright>Copyright (c) 2025 ppy Pty Ltd</Copyright>
<PackageTags>osu game</PackageTags>
</PropertyGroup>

19
MakeInstaller.ps1 Normal file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env powershell
param (
[string]$Version,
[string]$BuildConfig = "Release"
)
if ($Version -eq "") {
Write-Host "Usage: .\MakeInstaller.ps1 <VERSION_NUMBER> [-BuildConfig <BUILD_CONFIG>]"
Write-Host "Example: .\MakeInstaller.ps1 2025.823.0 -BuildConfig Debug"
exit
}
$tmpPub = ".\pub"
if (-not (Test-Path -Path $tmpPub)) {
New-Item -ItemType Directory -path $tmpPub
}
dotnet publish -c $BuildConfig osu.Desktop --self-contained -r win-x64 -o $tmpPub -verbosity:m /p:Version=$Version
vpk pack --packId jvnkosu.Client --packTitle "jvnkosu!lazer" --packVersion $Version --packDir ./pub --mainExe="osu!.exe"

149
README.md
View File

@@ -1,147 +1,40 @@
<p align="center">
<img width="500" alt="osu! logo" src="assets/lazer.png">
</p>
# jvnkosu! client
# osu!
A free-to-win rhythm game based on osu!(lazer). Click is just a *rhythm* away!
[![Build status](https://github.com/ppy/osu/actions/workflows/ci.yml/badge.svg?branch=master&event=push)](https://github.com/ppy/osu/actions/workflows/ci.yml)
[![GitHub release](https://img.shields.io/github/release/ppy/osu.svg)](https://github.com/ppy/osu/releases/latest)
[![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu)
[![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/osu-web/localized.svg)](https://crowdin.com/project/osu-web)
## Disclaimer
A free-to-win rhythm game. Rhythm is just a *click* away!
*osu!* is a registered trademark of ppy Pty Ltd.
jvnkosu! is not affiliated with, or endorsed by ppy Pty Ltd., but makes use of its open-source components and resources.
This is the future and final iteration of the [osu!](https://osu.ppy.sh) game client which marks the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge.
## License
Client source code is licensed under the MIT license, see the [LICENCE](LICENCE) file in repository root for more info.
## Status
Game assets are included as a NuGet package and licensed under the CC BY-NC 4.0, which prohibits commercial use. See [ppy/osu-resources](https://github.com/ppy/osu-resources) for more info.
This project is under constant development, but we do our best to keep things in a stable state. Players are encouraged to install from a release alongside their stable *osu!* client. This project will continue to evolve until we eventually reach the point where most users prefer it over the previous "osu!stable" release.
Registered trademarks "osu!" and "ppy" are property of ppy Pty Ltd., and protected by trademark law.
A few resources are available as starting points to getting involved and understanding the project:
## Compiling from source
Building jvnkosu! from source is pretty much possible (and welcome here).
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
- You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management).
- Track our current efforts [towards improving the game](https://github.com/orgs/ppy/projects/7/views/6).
First, you must have a desktop platform with [.NET Core SDK 8](https://dotnet.microsoft.com/download) installed. Windows, Linux, macOS should work well. You can check if you have the SDK installed by running `dotnet --version` in your command prompt/terminal.
## Running osu!
If you are just looking to give the game a whirl, you can grab the latest release for your platform:
### Latest release:
| [Windows 10+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 12+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 13.4+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) |
|--------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------- | ------------- | ------------- |
You can also generally download a version for your current device from the [osu! site](https://osu.ppy.sh/home/download).
If your platform is unsupported or not listed above, there is still a chance you can run the release or manually build it by following the instructions below.
**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores very soon so we don't have to live with this limitation.
## Developing a custom ruleset
osu! is designed to allow user-created gameplay variations, called "rulesets". Building one of these allows a developer to harness the power of the osu! beatmap library, game engine, and general UX for a new style of gameplay. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates).
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/discussions/13096).
## Developing osu!
### Prerequisites
Please make sure you have the following prerequisites:
- A desktop platform with the [.NET 8.0 SDK](https://dotnet.microsoft.com/download) installed.
When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Visual Studio Code](https://code.visualstudio.com/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) plugin installed.
### Downloading the source code
Clone the repository:
```shell
git clone https://github.com/ppy/osu
cd osu
Then, download the source code. You may download it as an archive and unzip it, but using [Git](https://git-scm.com/) instead is recommended:
```
git clone https://gitea.jvnko.boats/jvnkosu/client
```
To update the source code to the latest commit, run the following command inside the `osu` directory:
```shell
git pull
To **run** the project, switch to project's directory and run the following:
```
### Building
#### From an IDE
You should load the solution via one of the platform-specific `.slnf` files, rather than the main `.sln`. This will reduce dependencies and hide platforms that you don't care about. Valid `.slnf` files are:
- `osu.Desktop.slnf` (most common)
- `osu.Android.slnf`
- `osu.iOS.slnf`
Run configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `osu! (Tests)` project/configuration. More information on this is provided [below](#contributing).
To build for mobile platforms, you will likely need to run `sudo dotnet workload restore` if you haven't done so previously. This will install Android/iOS tooling required to complete the build.
#### From CLI
You can also build and run *osu!* from the command-line with a single command:
```shell
dotnet run --project osu.Desktop
```
When running locally to do any kind of performance testing, make sure to add `-c Release` to the build command, as the overhead of running with the default `Debug` configuration can be large (especially when testing with local framework modifications as below).
If the build fails, try to restore NuGet packages with `dotnet restore`.
### Testing with resource/framework modifications
Sometimes it may be necessary to cross-test changes in [osu-resources](https://github.com/ppy/osu-resources) or [osu-framework](https://github.com/ppy/osu-framework). This can be quickly achieved using included commands:
Windows:
```ps
UseLocalFramework.ps1
UseLocalResources.ps1
To **compile**:
```
dotnet build osu.Desktop
```
macOS / Linux:
To reduce performance overhead in custom builds, it's recommended to build with the `-c Release` flag, that will use the release profile and remove possibly unneeded debugging code.
```ps
UseLocalFramework.sh
UseLocalResources.sh
```
Note that these commands assume you have the relevant project(s) checked out in adjacent directories:
```
|- osu // this repository
|- osu-framework
|- osu-resources
```
### Code analysis
Before committing your code, please run a code formatter. This can be achieved by running `dotnet format` in the command line, or using the `Format code` command in your IDE.
We have adopted some cross-platform, compiler integrated analyzers. They can provide warnings when you are editing, building inside IDE or from command line, as-if they are provided by the compiler itself.
JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it from PowerShell with `.\InspectCode.ps1`. Alternatively, you can install ReSharper or use Rider to get inline support in your IDE of choice.
## Contributing
When it comes to contributing to the project, the two main things you can do to help out are reporting issues and submitting pull requests. Please refer to the [contributing guidelines](CONTRIBUTING.md) to understand how to help in the most effective way possible.
If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web).
We love to reward quality contributions. If you have made a large contribution, or are a regular contributor, you are welcome to [submit an expense via opencollective](https://opencollective.com/ppy/expenses/new). If you have any questions, feel free to [reach out to peppy](mailto:pe@ppy.sh) before doing so.
## Licence
*osu!*'s code and framework are licensed under the [MIT licence](https://opensource.org/licenses/MIT). Please see [the licence file](LICENCE) for more information. [tl;dr](https://tldrlegal.com/license/mit-license) you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source.
Please note that this *does not cover* the usage of the "osu!" or "ppy" branding in any software, resources, advertising or promotion, as this is protected by trademark law.
Please also note that game resources are covered by a separate licence. Please see the [ppy/osu-resources](https://github.com/ppy/osu-resources) repository for clarifications.
### See the [original readme](README.original.md) for more info.

147
README.original.md Normal file
View File

@@ -0,0 +1,147 @@
<p align="center">
<img width="500" alt="osu! logo" src="assets/lazer.png">
</p>
# osu!
[![Build status](https://github.com/ppy/osu/actions/workflows/ci.yml/badge.svg?branch=master&event=push)](https://github.com/ppy/osu/actions/workflows/ci.yml)
[![GitHub release](https://img.shields.io/github/release/ppy/osu.svg)](https://github.com/ppy/osu/releases/latest)
[![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu)
[![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/osu-web/localized.svg)](https://crowdin.com/project/osu-web)
A free-to-win rhythm game. Rhythm is just a *click* away!
This is the future and final iteration of the [osu!](https://osu.ppy.sh) game client which marks the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge.
## Status
This project is under constant development, but we do our best to keep things in a stable state. Players are encouraged to install from a release alongside their stable *osu!* client. This project will continue to evolve until we eventually reach the point where most users prefer it over the previous "osu!stable" release.
A few resources are available as starting points to getting involved and understanding the project:
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
- You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management).
- Track our current efforts [towards improving the game](https://github.com/orgs/ppy/projects/7/views/6).
## Running osu!
If you are just looking to give the game a whirl, you can grab the latest release for your platform:
### Latest release:
| [Windows 10+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 12+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 13.4+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) |
|--------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------- | ------------- | ------------- |
You can also generally download a version for your current device from the [osu! site](https://osu.ppy.sh/home/download).
If your platform is unsupported or not listed above, there is still a chance you can run the release or manually build it by following the instructions below.
**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores very soon so we don't have to live with this limitation.
## Developing a custom ruleset
osu! is designed to allow user-created gameplay variations, called "rulesets". Building one of these allows a developer to harness the power of the osu! beatmap library, game engine, and general UX for a new style of gameplay. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates).
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/discussions/13096).
## Developing osu!
### Prerequisites
Please make sure you have the following prerequisites:
- A desktop platform with the [.NET 8.0 SDK](https://dotnet.microsoft.com/download) installed.
When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Visual Studio Code](https://code.visualstudio.com/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) plugin installed.
### Downloading the source code
Clone the repository:
```shell
git clone https://github.com/ppy/osu
cd osu
```
To update the source code to the latest commit, run the following command inside the `osu` directory:
```shell
git pull
```
### Building
#### From an IDE
You should load the solution via one of the platform-specific `.slnf` files, rather than the main `.sln`. This will reduce dependencies and hide platforms that you don't care about. Valid `.slnf` files are:
- `osu.Desktop.slnf` (most common)
- `osu.Android.slnf`
- `osu.iOS.slnf`
Run configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `osu! (Tests)` project/configuration. More information on this is provided [below](#contributing).
To build for mobile platforms, you will likely need to run `sudo dotnet workload restore` if you haven't done so previously. This will install Android/iOS tooling required to complete the build.
#### From CLI
You can also build and run *osu!* from the command-line with a single command:
```shell
dotnet run --project osu.Desktop
```
When running locally to do any kind of performance testing, make sure to add `-c Release` to the build command, as the overhead of running with the default `Debug` configuration can be large (especially when testing with local framework modifications as below).
If the build fails, try to restore NuGet packages with `dotnet restore`.
### Testing with resource/framework modifications
Sometimes it may be necessary to cross-test changes in [osu-resources](https://github.com/ppy/osu-resources) or [osu-framework](https://github.com/ppy/osu-framework). This can be quickly achieved using included commands:
Windows:
```ps
UseLocalFramework.ps1
UseLocalResources.ps1
```
macOS / Linux:
```ps
UseLocalFramework.sh
UseLocalResources.sh
```
Note that these commands assume you have the relevant project(s) checked out in adjacent directories:
```
|- osu // this repository
|- osu-framework
|- osu-resources
```
### Code analysis
Before committing your code, please run a code formatter. This can be achieved by running `dotnet format` in the command line, or using the `Format code` command in your IDE.
We have adopted some cross-platform, compiler integrated analyzers. They can provide warnings when you are editing, building inside IDE or from command line, as-if they are provided by the compiler itself.
JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it from PowerShell with `.\InspectCode.ps1`. Alternatively, you can install ReSharper or use Rider to get inline support in your IDE of choice.
## Contributing
When it comes to contributing to the project, the two main things you can do to help out are reporting issues and submitting pull requests. Please refer to the [contributing guidelines](CONTRIBUTING.md) to understand how to help in the most effective way possible.
If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web).
We love to reward quality contributions. If you have made a large contribution, or are a regular contributor, you are welcome to [submit an expense via opencollective](https://opencollective.com/ppy/expenses/new). If you have any questions, feel free to [reach out to peppy](mailto:pe@ppy.sh) before doing so.
## Licence
*osu!*'s code and framework are licensed under the [MIT licence](https://opensource.org/licenses/MIT). Please see [the licence file](LICENCE) for more information. [tl;dr](https://tldrlegal.com/license/mit-license) you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source.
Please note that this *does not cover* the usage of the "osu!" or "ppy" branding in any software, resources, advertising or promotion, as this is protected by trademark law.
Please also note that game resources are covered by a separate licence. Please see the [ppy/osu-resources](https://github.com/ppy/osu-resources) repository for clarifications.

View File

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

View File

@@ -115,10 +115,12 @@ namespace osu.Desktop
if (IsFirstRun)
LocalConfig.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer);
if (IsPackageManaged)
return new NoActionUpdateManager();
// if (IsPackageManaged)
// return new NoActionUpdateManager();
return new VelopackUpdateManager();
// return new VelopackUpdateManager();
return new NoActionUpdateManager(); // for now, APIs are useless for actually downloading the releases. TODO: adapt UpdateManager for gitea
}
public override bool RestartAppWhenExited()

View File

@@ -21,9 +21,9 @@ namespace osu.Desktop
public static class Program
{
#if DEBUG
private const string base_game_name = @"osu-development";
private const string base_game_name = @"jvnkosu-development";
#else
private const string base_game_name = @"osu";
private const string base_game_name = @"jvnkosu";
#endif
private static LegacyTcpIpcProvider? legacyIpc;

View File

@@ -3,11 +3,11 @@
<TargetFramework>net8.0</TargetFramework>
<OutputType>WinExe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Description>A free-to-win rhythm game. Rhythm is just a *click* away!</Description>
<Description>A free-to-win rhythm game based on osu!(lazer). Click is just a *rhythm* away!</Description>
<AssemblyName>osu!</AssemblyName>
<AssemblyTitle>osu!(lazer)</AssemblyTitle>
<Title>osu!</Title>
<Product>osu!(lazer)</Product>
<AssemblyTitle>jvnkosu!</AssemblyTitle>
<Title>jvnkosu!</Title>
<Product>jvnkosu!</Product>
<ApplicationIcon>lazer.ico</ApplicationIcon>
<Version>0.0.0</Version>
<FileVersion>0.0.0</FileVersion>

View File

@@ -3,16 +3,19 @@
<metadata>
<id>osulazer</id>
<version>0.0.0</version>
<title>osu!</title>
<authors>ppy Pty Ltd</authors>
<title>jvnkosu!</title>
<authors>ppy Pty Ltd., jvnkosu! team</authors>
<owners>Dean Herbert</owners>
<projectUrl>https://osu.ppy.sh/</projectUrl>
<projectUrl>https://osu.jvnko.boats/</projectUrl>
<iconUrl>https://github.com/ppy/osu/blob/master/assets/lazer-nuget.png?raw=true</iconUrl>
<icon>icon.png</icon>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>A free-to-win rhythm game. Rhythm is just a *click* away!</description>
<description>A free-to-win rhythm game based on osu!(lazer). Click is just a *rhythm* away!</description>
<releaseNotes>testing</releaseNotes>
<copyright>Copyright (c) 2025 ppy Pty Ltd</copyright>
<copyright>
Copyright (c) 2025 ppy Pty Ltd
Copyright (c) 2025 jvnkosu! team
</copyright>
<language>en-AU</language>
</metadata>
<files>

View File

@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
public override float DefaultFlashlightSize => 325;
public override float DefaultFlashlightSize => 203.125f;
protected override Flashlight CreateFlashlight() => new CatchFlashlight(this, playfield);

View File

@@ -3,6 +3,7 @@
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
@@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public override string Acronym => "FF";
public override LocalisableString Description => "The fruits are... floating?";
public override double ScoreMultiplier => 1;
public override IconUsage? Icon => FontAwesome.Solid.Cloud;
public override IconUsage? Icon => OsuIcon.ModFloatingFruits;
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
{

View File

@@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public override LocalisableString Description => "Dashing by default, slow down!";
public override ModType Type => ModType.Fun;
public override double ScoreMultiplier => 1;
public override IconUsage? Icon => FontAwesome.Solid.Running;
public override IconUsage? Icon => OsuIcon.ModMovingFast;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax) };
private DrawableCatchRuleset drawableRuleset = null!;

View File

@@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}
[TestCase]
public void TestFilterIntersection()
public void TestKeysFilterIntersection()
{
var criteria = new ManiaFilterCriteria();
criteria.TryParseCustomKeywordCriteria("keys", Operator.Greater, "4");
@@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}
[TestCase]
public void TestInvalidFilters()
public void TestInvalidKeysFilters()
{
var criteria = new ManiaFilterCriteria();
@@ -183,5 +183,132 @@ namespace osu.Game.Rulesets.Mania.Tests
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "4,some text"));
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4,5,6"));
}
[TestCase]
public void TestLnsEqual()
{
var criteria = new ManiaFilterCriteria();
var filterCriteria = new FilterCriteria
{
Ruleset = new ManiaRuleset().RulesetInfo
};
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0");
BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 0,
EndTimeObjectCount = 0
};
Assert.True(criteria.Matches(beatmapInfo1, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0");
BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 100,
EndTimeObjectCount = 0
};
Assert.True(criteria.Matches(beatmapInfo2, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "100");
BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 100,
EndTimeObjectCount = 100
};
Assert.True(criteria.Matches(beatmapInfo3, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "1");
BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 100,
EndTimeObjectCount = 1
};
Assert.True(criteria.Matches(beatmapInfo4, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0.1");
BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 1000,
EndTimeObjectCount = 1
};
Assert.True(criteria.Matches(beatmapInfo5, filterCriteria));
}
[TestCase]
public void TestLnsGreaterOrEqual()
{
var criteria = new ManiaFilterCriteria();
var filterCriteria = new FilterCriteria
{
Ruleset = new ManiaRuleset().RulesetInfo
};
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0");
BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 0,
EndTimeObjectCount = 0
};
Assert.True(criteria.Matches(beatmapInfo1, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0");
BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 100,
EndTimeObjectCount = 0
};
Assert.True(criteria.Matches(beatmapInfo2, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "100");
BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 100,
EndTimeObjectCount = 100
};
Assert.True(criteria.Matches(beatmapInfo3, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1");
BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 100,
EndTimeObjectCount = 1
};
Assert.True(criteria.Matches(beatmapInfo4, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0.1");
BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
{
TotalObjectCount = 1000,
EndTimeObjectCount = 1
};
Assert.True(criteria.Matches(beatmapInfo5, filterCriteria));
}
[TestCase]
public void TestLnsNotManiaRuleset()
{
var criteria = new ManiaFilterCriteria();
var filterCriteria = new FilterCriteria
{
Ruleset = new ManiaRuleset().RulesetInfo
};
criteria.TryParseCustomKeywordCriteria("lns", Operator.LessOrEqual, "100");
BeatmapInfo beatmapInfo = new BeatmapInfo
{
TotalObjectCount = 100,
EndTimeObjectCount = 50
};
Assert.False(criteria.Matches(beatmapInfo, filterCriteria));
}
[TestCase]
public void TestInvalidLnsFilters()
{
var criteria = new ManiaFilterCriteria();
Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "some text"));
Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1some text"));
}
}
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
@@ -19,12 +20,16 @@ namespace osu.Game.Rulesets.Mania
public class ManiaFilterCriteria : IRulesetFilterCriteria
{
private readonly HashSet<int> includedKeyCounts = Enumerable.Range(1, LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT).ToHashSet();
private FilterCriteria.OptionalRange<float> longNotePercentage;
public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria)
{
int keyCount = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods);
return includedKeyCounts.Contains(keyCount);
bool keyCountMatch = includedKeyCounts.Contains(keyCount);
bool longNotePercentageMatch = !longNotePercentage.HasFilter || (!isConvertedBeatmap(beatmapInfo) && longNotePercentage.IsInRange(calculateLongNotePercentage(beatmapInfo)));
return keyCountMatch && longNotePercentageMatch;
}
public bool TryParseCustomKeywordCriteria(string key, Operator op, string strValues)
@@ -84,6 +89,10 @@ namespace osu.Game.Rulesets.Mania
return false;
}
}
case "ln":
case "lns":
return FilterQueryParser.TryUpdateCriteriaRange(ref longNotePercentage, op, strValues);
}
return false;
@@ -103,5 +112,18 @@ namespace osu.Game.Rulesets.Mania
return false;
}
private static bool isConvertedBeatmap(BeatmapInfo beatmapInfo)
{
return !beatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo);
}
private static float calculateLongNotePercentage(BeatmapInfo beatmapInfo)
{
int holdNotes = beatmapInfo.EndTimeObjectCount;
int totalNotes = Math.Max(1, beatmapInfo.TotalObjectCount);
return holdNotes / (float)totalNotes * 100;
}
}
}

View File

@@ -4,6 +4,7 @@
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
@@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override LocalisableString Description => "No more tricky speed changes!";
public override IconUsage? Icon => FontAwesome.Solid.Equals;
public override IconUsage? Icon => OsuIcon.ModConstantSpeed;
public override ModType Type => ModType.Conversion;

View File

@@ -4,8 +4,10 @@
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Mods
@@ -14,6 +16,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public override string Name => "Cover";
public override string Acronym => "CO";
public override IconUsage? Icon => OsuIcon.ModCover;
public override LocalisableString Description => @"Decrease the playfield's viewing area.";

View File

@@ -1,8 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mods;
@@ -13,6 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Name => "Dual Stages";
public override string Acronym => "DS";
public override LocalisableString Description => @"Double the stages, double the fun!";
public override IconUsage? Icon => OsuIcon.ModDualStages;
public override ModType Type => ModType.Conversion;
public override double ScoreMultiplier => 1;

View File

@@ -3,7 +3,9 @@
using System;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Mods
@@ -12,6 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public override string Name => "Fade In";
public override string Acronym => "FI";
public override IconUsage? Icon => OsuIcon.ModFadeIn;
public override LocalisableString Description => @"Keys appear out of nowhere!";
public override double ScoreMultiplier => 1;
public override bool ValidForFreestyleAsRequiredMod => false;

View File

@@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods;
using osu.Framework.Graphics.Sprites;
using System.Collections.Generic;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
namespace osu.Game.Rulesets.Mania.Mods
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override LocalisableString Description => @"Replaces all hold notes with normal notes.";
public override IconUsage? Icon => FontAwesome.Solid.DotCircle;
public override IconUsage? Icon => OsuIcon.ModHoldOff;
public override ModType Type => ModType.Conversion;

View File

@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override LocalisableString Description => "Hold the keys. To the beat.";
public override IconUsage? Icon => FontAwesome.Solid.YinYang;
public override IconUsage? Icon => OsuIcon.ModInvert;
public override ModType Type => ModType.Conversion;

View File

@@ -1,7 +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.
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 1;
public override string Name => "One Key";
public override string Acronym => "1K";
public override IconUsage? Icon => OsuIcon.ModOneKey;
public override LocalisableString Description => @"Play with one key.";
public override bool Ranked => false;
}

View File

@@ -1,7 +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.
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 10;
public override string Name => "Ten Keys";
public override string Acronym => "10K";
public override IconUsage? Icon => OsuIcon.ModTenKeys;
public override LocalisableString Description => @"Play with ten keys.";
public override bool Ranked => false;
}

View File

@@ -1,7 +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.
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 2;
public override string Name => "Two Keys";
public override string Acronym => "2K";
public override IconUsage? Icon => OsuIcon.ModTwoKeys;
public override LocalisableString Description => @"Play with two keys.";
public override bool Ranked => false;
}

View File

@@ -1,7 +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.
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 3;
public override string Name => "Three Keys";
public override string Acronym => "3K";
public override IconUsage? Icon => OsuIcon.ModThreeKeys;
public override LocalisableString Description => @"Play with three keys.";
public override bool Ranked => false;
}

View File

@@ -1,7 +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.
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 4;
public override string Name => "Four Keys";
public override string Acronym => "4K";
public override IconUsage? Icon => OsuIcon.ModFourKeys;
public override LocalisableString Description => @"Play with four keys.";
}
}

View File

@@ -1,7 +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.
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 5;
public override string Name => "Five Keys";
public override string Acronym => "5K";
public override IconUsage? Icon => OsuIcon.ModFiveKeys;
public override LocalisableString Description => @"Play with five keys.";
}
}

View File

@@ -1,7 +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.
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 6;
public override string Name => "Six Keys";
public override string Acronym => "6K";
public override IconUsage? Icon => OsuIcon.ModSixKeys;
public override LocalisableString Description => @"Play with six keys.";
}
}

View File

@@ -1,7 +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.
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 7;
public override string Name => "Seven Keys";
public override string Acronym => "7K";
public override IconUsage? Icon => OsuIcon.ModSevenKeys;
public override LocalisableString Description => @"Play with seven keys.";
}
}

View File

@@ -1,7 +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.
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 8;
public override string Name => "Eight Keys";
public override string Acronym => "8K";
public override IconUsage? Icon => OsuIcon.ModEightKeys;
public override LocalisableString Description => @"Play with eight keys.";
}
}

View File

@@ -1,7 +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.
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 9;
public override string Name => "Nine Keys";
public override string Acronym => "9K";
public override IconUsage? Icon => OsuIcon.ModNineKeys;
public override LocalisableString Description => @"Play with nine keys.";
}
}

View File

@@ -4,8 +4,10 @@
using System;
using System.Linq;
using System.Threading;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
@@ -26,6 +28,8 @@ namespace osu.Game.Rulesets.Mania.Mods
public override double ScoreMultiplier => 0.9;
public override IconUsage? Icon => OsuIcon.ModNoRelease;
public override ModType Type => ModType.DifficultyReduction;
public override Type[] IncompatibleMods => new[] { typeof(ManiaModHoldOff) };

View File

@@ -32,26 +32,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
[Test]
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
[Test]
public void TestPlayfieldBasedSize()
{
OsuModFlashlight flashlight;
CreateModTest(new ModTestData
{
Mods = [flashlight = new OsuModFlashlight(), new OsuModBarrelRoll()],
PassCondition = () =>
{
var flashlightOverlay = Player.DrawableRuleset.Overlays
.ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>()
.First();
// the combo check is here because the flashlight radius decreases for the first time at 100 combo
// and hardcoding it here eliminates the need to meddle in flashlight internals further by e.g. exposing `GetComboScaleFor()`
return flashlightOverlay.GetSize() < flashlight.DefaultFlashlightSize && Player.GameplayState.ScoreProcessor.Combo.Value < 100;
}
});
}
[Test]
public void TestSliderDimsOnlyAfterStartTime()
{

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,162 @@
// 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.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Scoring;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using NUnit.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Utils;
using osu.Framework.Threading;
using osu.Game.Rulesets.Osu.HUD;
namespace osu.Game.Rulesets.Osu.Tests
{
public partial class TestSceneAimErrorMeter : OsuManualInputManagerTestScene
{
private DependencyProvidingContainer dependencyContainer = null!;
private ScoreProcessor scoreProcessor = null!;
private TestAimErrorMeter aimErrorMeter = null!;
private CircularContainer gameObject = null!;
private ScheduledDelegate? automaticAdditionDelegate;
protected override void LoadComplete()
{
base.LoadComplete();
AddSliderStep("Hit marker size", 0f, 12f, 7f, t =>
{
if (aimErrorMeter.IsNotNull())
aimErrorMeter.HitMarkerSize.Value = t;
});
AddSliderStep("Average position marker size", 1f, 25f, 7f, t =>
{
if (aimErrorMeter.IsNotNull())
aimErrorMeter.AverageMarkerSize.Value = t;
});
}
[SetUpSteps]
public void SetupSteps() => AddStep("Create components", () =>
{
automaticAdditionDelegate?.Cancel();
automaticAdditionDelegate = null;
var ruleset = new OsuRuleset();
scoreProcessor = new ScoreProcessor(ruleset);
Child = dependencyContainer = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
(typeof(ScoreProcessor), scoreProcessor)
}
};
dependencyContainer.Children = new Drawable[]
{
aimErrorMeter = new TestAimErrorMeter
{
Margin = new MarginPadding
{
Top = 100
},
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Scale = new Vector2(2),
},
gameObject = new CircularContainer
{
Size = new Vector2(2 * OsuHitObject.OBJECT_RADIUS),
Position = new Vector2(256, 192),
Colour = Color4.Yellow,
Masking = true,
BorderThickness = 2,
BorderColour = Color4.White,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
},
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(4),
}
}
}
};
});
protected override bool OnMouseDown(MouseDownEvent e)
{
// the division by 2 is because CS=5 applies a 0.5x (plus fudge) multiplier to `OBJECT_RADIUS`
aimErrorMeter.AddPoint((gameObject.ToLocalSpace(e.ScreenSpaceMouseDownPosition) - new Vector2(OsuHitObject.OBJECT_RADIUS)) / 2);
return true;
}
[Test]
public void TestManyHitPointsAutomatic()
{
AddStep("add scheduled delegate", () =>
{
automaticAdditionDelegate = Scheduler.AddDelayed(() =>
{
var randomPos = new Vector2(
RNG.NextSingle(0, 2 * OsuHitObject.OBJECT_RADIUS),
RNG.NextSingle(0, 2 * OsuHitObject.OBJECT_RADIUS));
aimErrorMeter.AddPoint(randomPos - new Vector2(OsuHitObject.OBJECT_RADIUS));
InputManager.MoveMouseTo(gameObject.ToScreenSpace(randomPos));
}, 1, true);
});
AddWaitStep("wait for some hit points", 10);
}
[Test]
public void TestDisplayStyles()
{
AddStep("Switch hit position marker style to +", () => aimErrorMeter.HitMarkerStyle.Value = AimErrorMeter.MarkerStyle.Plus);
AddStep("Switch hit position marker style to x", () => aimErrorMeter.HitMarkerStyle.Value = AimErrorMeter.MarkerStyle.X);
AddStep("Switch average position marker style to +", () => aimErrorMeter.AverageMarkerStyle.Value = AimErrorMeter.MarkerStyle.Plus);
AddStep("Switch average position marker style to x", () => aimErrorMeter.AverageMarkerStyle.Value = AimErrorMeter.MarkerStyle.X);
AddStep("Switch position display to absolute", () => aimErrorMeter.PositionDisplayStyle.Value = AimErrorMeter.PositionDisplay.Absolute);
AddStep("Switch position display to relative", () => aimErrorMeter.PositionDisplayStyle.Value = AimErrorMeter.PositionDisplay.Normalised);
}
[Test]
public void TestManualPlacement()
{
AddStep("return user input", () => InputManager.UseParentInput = true);
}
private partial class TestAimErrorMeter : AimErrorMeter
{
public void AddPoint(Vector2 position)
{
OnNewJudgement(new OsuHitCircleJudgementResult(new HitCircle(), new OsuJudgement())
{
CursorPositionAtHit = position
});
}
}
}
}

View File

@@ -0,0 +1,160 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public partial class TestSceneDrawableJudgementSliderTicks : OsuSkinnableTestScene
{
private bool classic;
private readonly JudgementPooler<DrawableOsuJudgement>[] judgementPools;
public TestSceneDrawableJudgementSliderTicks()
{
judgementPools = new JudgementPooler<DrawableOsuJudgement>[Rows * Cols];
}
protected override void LoadComplete()
{
base.LoadComplete();
int cellIndex = 0;
SetContents(_ =>
{
var container = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
judgementPools[cellIndex] = new JudgementPooler<DrawableOsuJudgement>(new[]
{
HitResult.Great,
HitResult.Miss,
HitResult.LargeTickHit,
HitResult.SliderTailHit,
HitResult.LargeTickMiss,
HitResult.IgnoreMiss,
}),
new GridContainer
{
Padding = new MarginPadding { Top = 26f },
RelativeSizeAxes = Axes.Both,
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
Content =
new[]
{
new[]
{
Empty(),
new OsuSpriteText
{
Text = "hit",
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
},
new OsuSpriteText
{
Text = "miss",
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
},
},
}.Concat(new[]
{
"head",
"tick",
"repeat",
"tail",
"slider",
}.Select(label => new Drawable[]
{
new OsuSpriteText
{
Text = label,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
},
new Container<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both },
new Container<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both },
})).ToArray(),
},
},
};
cellIndex++;
return container;
});
AddToggleStep("Toggle classic behaviour", c => classic = c);
AddStep("Show judgements", createAllJudgements);
}
private void createAllJudgements()
{
for (int cellIndex = 0; cellIndex < Rows * Cols; cellIndex++)
{
var slider = new Slider { StartTime = Time.Current, ClassicSliderBehaviour = classic };
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableHitObjects = new DrawableOsuHitObject[]
{
new DrawableSliderHead(new SliderHeadCircle { StartTime = Time.Current, ClassicSliderBehaviour = classic }),
new DrawableSliderTick(new SliderTick { StartTime = Time.Current }),
new DrawableSliderRepeat(new SliderRepeat(slider) { StartTime = Time.Current }),
new DrawableSliderTail(new SliderTailCircle(slider) { StartTime = Time.Current, ClassicSliderBehaviour = classic }),
new DrawableSlider(slider),
};
var containers = Cell(cellIndex).ChildrenOfType<Container<DrawableOsuJudgement>>().ToArray();
for (int i = 0; i < drawableHitObjects.Length; i++)
{
createJudgement(judgementPools[cellIndex], containers[i * 2], drawableHitObjects[i], true);
createJudgement(judgementPools[cellIndex], containers[i * 2 + 1], drawableHitObjects[i], false);
}
}
}
private void createJudgement(JudgementPooler<DrawableOsuJudgement> pool, Container<DrawableOsuJudgement> container, DrawableOsuHitObject drawableHitObject, bool hit)
{
container.Clear(false);
if (!drawableHitObject.DisplayResult)
return;
var hitObject = drawableHitObject.HitObject;
var result = new OsuJudgementResult(hitObject, hitObject.Judgement)
{
Type = hit ? hitObject.Judgement.MaxResult : hitObject.Judgement.MinResult,
};
var judgement = pool.Get(result.Type, d =>
{
d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre;
d.Scale = new Vector2(0.7f);
d.Apply(result, null);
});
if (judgement != null)
container.Add(judgement);
}
}
}

View File

@@ -0,0 +1,475 @@
// 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.ComponentModel;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Localisation.HUD;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Statistics;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osuTK;
using osuTK.Graphics;
using Container = osu.Framework.Graphics.Containers.Container;
namespace osu.Game.Rulesets.Osu.HUD
{
[Cached]
public partial class AimErrorMeter : HitErrorMeter
{
[SettingSource(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.HitMarkerSize), nameof(AimErrorMeterStrings.HitMarkerSizeDescription))]
public BindableNumber<float> HitMarkerSize { get; } = new BindableNumber<float>(7f)
{
MinValue = 0f,
MaxValue = 12f,
Precision = 1f
};
[SettingSource(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.HitMarkerStyle), nameof(AimErrorMeterStrings.HitMarkerStyleDescription))]
public Bindable<MarkerStyle> HitMarkerStyle { get; } = new Bindable<MarkerStyle>();
[SettingSource(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.AverageMarkerSize), nameof(AimErrorMeterStrings.AverageMarkerSizeDescription))]
public BindableNumber<float> AverageMarkerSize { get; } = new BindableNumber<float>(12f)
{
MinValue = 7f,
MaxValue = 25f,
Precision = 1f
};
[SettingSource(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.AverageMarkerStyle), nameof(AimErrorMeterStrings.AverageMarkerStyleDescription))]
public Bindable<MarkerStyle> AverageMarkerStyle { get; } = new Bindable<MarkerStyle>(MarkerStyle.Plus);
[SettingSource(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.PositionDisplayStyle), nameof(AimErrorMeterStrings.PositionDisplayStyleDescription))]
public Bindable<PositionDisplay> PositionDisplayStyle { get; } = new Bindable<PositionDisplay>();
// used for calculate relative position.
private Vector2? lastObjectPosition;
private Container averagePositionMarker = null!;
private Container averagePositionMarkerRotationContainer = null!;
private Vector2? averagePosition;
private readonly DrawablePool<HitPositionMarker> hitPositionPool = new DrawablePool<HitPositionMarker>(30);
private Container hitPositionMarkerContainer = null!;
private Container arrowBackgroundContainer = null!;
private UprightAspectMaintainingContainer rotateFixedContainer = null!;
private Container mainContainer = null!;
private float objectRadius;
private const int max_concurrent_judgements = 30;
private const float line_thickness = 2;
private const float inner_portion = 0.85f;
[Resolved]
private OsuColour colours { get; set; } = null!;
public AimErrorMeter()
{
AutoSizeAxes = Axes.Both;
AlwaysPresent = true;
}
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, ScoreProcessor processor)
{
InternalChild = new Container
{
Height = 100,
Width = 100,
Children = new Drawable[]
{
hitPositionPool,
rotateFixedContainer = new UprightAspectMaintainingContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
}
};
mainContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
BorderColour = Colour4.White,
Masking = true,
BorderThickness = 2,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(inner_portion),
Child = new Box
{
Colour = Colour4.Gray,
Alpha = 0.3f,
RelativeSizeAxes = Axes.Both
},
},
arrowBackgroundContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Name = "Arrow Background",
RelativeSizeAxes = Axes.Both,
Rotation = 45,
Alpha = 0f,
Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Height = inner_portion + 0.2f,
Width = line_thickness / 2,
},
new Circle
{
Height = 5f,
Width = line_thickness / 2,
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
Margin = new MarginPadding(-line_thickness / 4),
RelativePositionAxes = Axes.Both,
Y = -(inner_portion + 0.2f) / 2,
Rotation = -45
},
new Circle
{
Height = 5f,
Width = line_thickness / 2,
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
Margin = new MarginPadding(-line_thickness / 4),
RelativePositionAxes = Axes.Both,
Y = -(inner_portion + 0.2f) / 2,
Rotation = 45
}
}
},
new Container
{
Name = "Cross Background",
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0.5f,
Width = line_thickness,
Height = inner_portion * 0.9f
},
new Circle
{
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0.5f,
Width = line_thickness,
Height = inner_portion * 0.9f,
Rotation = 90
},
new Circle
{
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0.2f,
Width = line_thickness / 2,
Height = inner_portion * 0.9f,
Rotation = 45
},
new Circle
{
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0.2f,
Width = line_thickness / 2,
Height = inner_portion * 0.9f,
Rotation = 135
},
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
hitPositionMarkerContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
averagePositionMarker = new UprightAspectMaintainingContainer
{
RelativePositionAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = averagePositionMarkerRotationContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.25f,
},
new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.25f,
Rotation = 90
}
}
}
}
}
}
}
};
// handle IApplicableToDifficulty for CS change.
BeatmapDifficulty newDifficulty = new BeatmapDifficulty();
beatmap.Value.Beatmap.Difficulty.CopyTo(newDifficulty);
var mods = processor.Mods.Value;
foreach (var mod in mods.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(newDifficulty);
objectRadius = OsuHitObject.OBJECT_RADIUS * LegacyRulesetExtensions.CalculateScaleFromCircleSize(newDifficulty.CircleSize, true);
AverageMarkerSize.BindValueChanged(size => averagePositionMarker.Size = new Vector2(size.NewValue), true);
AverageMarkerStyle.BindValueChanged(style => averagePositionMarkerRotationContainer.Rotation = style.NewValue == MarkerStyle.Plus ? 0 : 45, true);
PositionDisplayStyle.BindValueChanged(s =>
{
Clear();
if (s.NewValue == PositionDisplay.Normalised)
{
arrowBackgroundContainer.FadeIn(100);
rotateFixedContainer.Remove(mainContainer, false);
AddInternal(mainContainer);
}
else
{
arrowBackgroundContainer.FadeOut(100);
// when in absolute mode, rotation of the aim error meter as a whole should not affect how the component is displayed
RemoveInternal(mainContainer, false);
rotateFixedContainer.Add(mainContainer);
}
}, true);
}
protected override void OnNewJudgement(JudgementResult judgement)
{
if (judgement is not OsuHitCircleJudgementResult circleJudgement) return;
if (circleJudgement.CursorPositionAtHit == null) return;
if (hitPositionMarkerContainer.Count > max_concurrent_judgements)
{
const double quick_fade_time = 300;
// check with a bit of lenience to avoid precision error in comparison.
var old = hitPositionMarkerContainer.FirstOrDefault(j => j.LifetimeEnd > Clock.CurrentTime + quick_fade_time * 1.1);
if (old != null)
{
old.ClearTransforms();
old.FadeOut(quick_fade_time).Expire();
}
}
Vector2 hitPosition;
if (PositionDisplayStyle.Value == PositionDisplay.Normalised && lastObjectPosition != null)
{
hitPosition = AccuracyHeatmap.FindRelativeHitPosition(lastObjectPosition.Value, ((OsuHitObject)circleJudgement.HitObject).StackedEndPosition,
circleJudgement.CursorPositionAtHit.Value, objectRadius, 45) * (inner_portion / 2);
}
else
{
// get relative position between mouse position and current object.
hitPosition = (circleJudgement.CursorPositionAtHit.Value - ((OsuHitObject)circleJudgement.HitObject).StackedPosition) / objectRadius / 2 * inner_portion;
}
hitPosition = Vector2.Clamp(hitPosition, new Vector2(-0.5f), new Vector2(0.5f));
hitPositionPool.Get(drawableHit =>
{
drawableHit.X = hitPosition.X;
drawableHit.Y = hitPosition.Y;
drawableHit.Colour = getColourForPosition(hitPosition);
hitPositionMarkerContainer.Add(drawableHit);
});
var newAveragePosition = 0.1f * hitPosition + 0.9f * (averagePosition ?? hitPosition);
averagePositionMarker.MoveTo(newAveragePosition, 800, Easing.OutQuint);
averagePosition = newAveragePosition;
lastObjectPosition = ((OsuHitObject)circleJudgement.HitObject).StackedPosition;
}
private Color4 getColourForPosition(Vector2 position)
{
float distance = Vector2.Distance(position, Vector2.Zero);
if (distance >= 0.5f * inner_portion)
return colours.Red;
if (distance >= 0.35f * inner_portion)
return colours.Yellow;
if (distance >= 0.2f * inner_portion)
return colours.Green;
return colours.Blue;
}
public override void Clear()
{
averagePosition = null;
averagePositionMarker.MoveTo(Vector2.Zero, 800, Easing.OutQuint);
lastObjectPosition = null;
foreach (var h in hitPositionMarkerContainer)
{
h.ClearTransforms();
h.Expire();
}
}
private partial class HitPositionMarker : PoolableDrawable
{
[Resolved]
private AimErrorMeter aimErrorMeter { get; set; } = null!;
public readonly BindableNumber<float> MarkerSize = new BindableFloat();
public readonly Bindable<MarkerStyle> Style = new Bindable<MarkerStyle>();
private readonly Container content;
public HitPositionMarker()
{
RelativePositionAxes = Axes.Both;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
InternalChild = new UprightAspectMaintainingContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = content = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.25f,
Rotation = -45
},
new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.25f,
Rotation = 45
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
MarkerSize.BindTo(aimErrorMeter.HitMarkerSize);
MarkerSize.BindValueChanged(size => Size = new Vector2(size.NewValue), true);
Style.BindTo(aimErrorMeter.HitMarkerStyle);
Style.BindValueChanged(style => content.Rotation = style.NewValue == MarkerStyle.X ? 0 : 45, true);
}
protected override void PrepareForUse()
{
base.PrepareForUse();
const int judgement_fade_in_duration = 100;
const int judgement_fade_out_duration = 5000;
this
.ResizeTo(new Vector2(0))
.FadeInFromZero(judgement_fade_in_duration, Easing.OutQuint)
.ResizeTo(new Vector2(MarkerSize.Value), judgement_fade_in_duration, Easing.OutQuint)
.Then()
.FadeOut(judgement_fade_out_duration)
.Expire();
}
}
public enum MarkerStyle
{
[Description("x")]
X,
[Description("+")]
Plus,
}
public enum PositionDisplay
{
[LocalisableDescription(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.Absolute))]
Absolute,
[LocalisableDescription(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.Normalised))]
Normalised,
}
}
}

View File

@@ -5,6 +5,7 @@ using System;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Name => @"Alternate";
public override string Acronym => @"AL";
public override LocalisableString Description => @"Don't use the same key twice in a row!";
public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
public override IconUsage? Icon => OsuIcon.ModAlternate;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray();
protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction != action;

View File

@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
@@ -19,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "AD";
public override LocalisableString Description => "Never trust the approach circles...";
public override double ScoreMultiplier => 1;
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
public override IconUsage? Icon => OsuIcon.ModApproachDifferent;
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModFreezeFrame) };

View File

@@ -11,6 +11,7 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
@@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override LocalisableString Description => "Play with blinds on your screen.";
public override string Acronym => "BL";
public override IconUsage? Icon => FontAwesome.Solid.Adjust;
public override IconUsage? Icon => OsuIcon.ModBlinds;
public override ModType Type => ModType.DifficultyIncrease;
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;

View File

@@ -3,9 +3,11 @@
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
@@ -21,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => "Bloom";
public override string Acronym => "BM";
public override IconUsage? Icon => OsuIcon.ModBloom;
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "The cursor blooms into.. a larger cursor!";
public override double ScoreMultiplier => 1;

View File

@@ -11,7 +11,9 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@@ -34,6 +36,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1;
public override IconUsage? Icon => OsuIcon.ModBubbles;
public override ModType Type => ModType.Fun;
// Compatibility with these seems potentially feasible in the future, blocked for now because they don't work as one would expect

View File

@@ -4,6 +4,7 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "DF";
public override IconUsage? Icon => FontAwesome.Solid.CompressArrowsAlt;
public override IconUsage? Icon => OsuIcon.ModDeflate;
public override LocalisableString Description => "Hit them at the right size!";

View File

@@ -7,6 +7,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => "Depth";
public override string Acronym => "DP";
public override IconUsage? Icon => FontAwesome.Solid.Cube;
public override IconUsage? Icon => OsuIcon.ModDepth;
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "3D. Almost.";
public override double ScoreMultiplier => 1;

View File

@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
public override float DefaultFlashlightSize => 200;
public override float DefaultFlashlightSize => 125;
private OsuFlashlight flashlight = null!;

View File

@@ -4,8 +4,10 @@
using System;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@@ -19,6 +21,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "FR";
public override IconUsage? Icon => OsuIcon.ModFreezeFrame;
public override double ScoreMultiplier => 1;
public override LocalisableString Description => "Burn the notes into your memory.";

View File

@@ -4,6 +4,7 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "GR";
public override IconUsage? Icon => FontAwesome.Solid.ArrowsAltV;
public override IconUsage? Icon => OsuIcon.ModGrow;
public override LocalisableString Description => "Hit them at the right size!";

View File

@@ -9,6 +9,7 @@ using osu.Framework.Localisation;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => "Magnetised";
public override string Acronym => "MG";
public override IconUsage? Icon => FontAwesome.Solid.Magnet;
public override IconUsage? Icon => OsuIcon.ModMagnetised;
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "No need to chase the circles your cursor is a magnet!";
public override double ScoreMultiplier => 0.5;

View File

@@ -4,10 +4,12 @@
using System;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@@ -23,6 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => "Repel";
public override string Acronym => "RP";
public override IconUsage? Icon => OsuIcon.ModRepel;
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "Hit objects run away!";
public override double ScoreMultiplier => 1;

View File

@@ -3,7 +3,9 @@
using System;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -11,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => @"Single Tap";
public override string Acronym => @"SG";
public override IconUsage? Icon => OsuIcon.ModSingleTap;
public override LocalisableString Description => @"You must only use one key!";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAlternate) }).ToArray();

View File

@@ -5,6 +5,7 @@ using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => "Spin In";
public override string Acronym => "SI";
public override IconUsage? Icon => FontAwesome.Solid.Undo;
public override IconUsage? Icon => OsuIcon.ModSpinIn;
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "Circles spin in. No approach circles.";
public override double ScoreMultiplier => 1;

View File

@@ -4,8 +4,10 @@
using System;
using System.Linq;
using System.Threading;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@@ -24,6 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => @"Strict Tracking";
public override string Acronym => @"ST";
public override IconUsage? Icon => OsuIcon.ModStrictTracking;
public override ModType Type => ModType.DifficultyIncrease;
public override LocalisableString Description => @"Once you start a slider, follow precisely or get a miss.";
public override double ScoreMultiplier => 1.0;

View File

@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Name => "Target Practice";
public override string Acronym => "TP";
public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => OsuIcon.ModTarget;
public override IconUsage? Icon => OsuIcon.ModTargetPractice;
public override LocalisableString Description => @"Practice keeping up with the beat of the song.";
public override double ScoreMultiplier => 0.1;

View File

@@ -4,7 +4,9 @@
using System;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@@ -18,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => "Traceable";
public override string Acronym => "TC";
public override IconUsage? Icon => OsuIcon.ModTraceable;
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "Put your faith in the approach circles...";
public override double ScoreMultiplier => 1;

View File

@@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => "Transform";
public override string Acronym => "TR";
public override IconUsage? Icon => FontAwesome.Solid.ArrowsAlt;
public override IconUsage? Icon => OsuIcon.ModTransform;
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1;

View File

@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
@@ -19,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => "Wiggle";
public override string Acronym => "WG";
public override IconUsage? Icon => FontAwesome.Solid.Certificate;
public override IconUsage? Icon => OsuIcon.ModWiggle;
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "They just won't stay still...";
public override double ScoreMultiplier => 1;

View File

@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
[Resolved]
private OsuConfigManager config { get; set; } = null!;
private Vector2 screenSpacePosition;
private Vector2? screenSpacePosition;
[BackgroundDependencyLoader]
private void load()
@@ -65,7 +65,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Lighting.ResetAnimation();
Lighting.SetColourFrom(this, Result);
Position = Parent!.ToLocalSpace(screenSpacePosition);
if (screenSpacePosition != null)
Position = Parent!.ToLocalSpace(screenSpacePosition.Value);
}
protected override void ApplyHitAnimations()
@@ -87,7 +89,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.ApplyHitAnimations();
}
protected override Drawable CreateDefaultJudgement(HitResult result) => new OsuJudgementPiece(result);
protected override Drawable CreateDefaultJudgement(HitResult result) =>
// Tick hits don't show a judgement by default
result.IsHit() && result.IsTick() ? Empty() : new OsuJudgementPiece(result);
private partial class OsuJudgementPiece : DefaultJudgementPiece
{

View File

@@ -16,17 +16,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
public override bool DisplayResult
{
get
{
if (HitObject?.ClassicSliderBehaviour == true)
return false;
return base.DisplayResult;
}
}
private readonly IBindable<int> pathVersion = new Bindable<int>();
protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle;

View File

@@ -3,6 +3,7 @@
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK.Graphics;
@@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (targetJudgement == null || targetResult == null)
Colour = Color4.White;
else
Colour = targetResult.IsHit ? targetJudgement.AccentColour : Color4.Transparent;
Colour = targetResult.IsHit && !targetResult.Type.IsTick() ? targetJudgement.AccentColour : Color4.Transparent;
}
}
}

View File

@@ -29,6 +29,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
switch (result)
{
case HitResult.LargeTickHit:
case HitResult.SliderTailHit:
return null;
case HitResult.IgnoreMiss:
case HitResult.LargeTickMiss:
return new ArgonJudgementPieceSliderTickMiss(result);

View File

@@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
public partial class LegacyJudgementPieceSliderTickHit : Sprite, IAnimatableJudgement
{
public void PlayAnimation()
{
// https://github.com/peppy/osu-stable-reference/blob/0e91e49bc83fe8b21c3ba5f1eb2d5d06456eae84/osu!/GameModes/Play/Rulesets/Ruleset.cs#L804-L806
this.MoveToOffset(new Vector2(0, -10), 300, Easing.Out)
.Then()
.FadeOut(60);
}
public Drawable GetAboveHitObjectsProxiedContent() => CreateProxy();
}
}

View File

@@ -5,7 +5,9 @@ using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK;
@@ -115,6 +117,39 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null;
case SkinComponentLookup<HitResult> resultComponent:
switch (resultComponent.Component)
{
case HitResult.LargeTickHit:
case HitResult.SliderTailHit:
if (getSliderPointTexture(resultComponent.Component) is Texture texture)
return new LegacyJudgementPieceSliderTickHit { Texture = texture };
break;
// If the corresponding hit result displays a judgement and the miss texture isn't provided by this skin, don't look up the miss texture from any further skins.
case HitResult.LargeTickMiss:
case HitResult.IgnoreMiss:
if (getSliderPointTexture(resultComponent.Component == HitResult.LargeTickMiss
? HitResult.LargeTickHit
: HitResult.SliderTailHit) != null)
return base.GetDrawableComponent(lookup) ?? Drawable.Empty();
break;
}
return base.GetDrawableComponent(lookup);
Texture? getSliderPointTexture(HitResult result)
{
// https://github.com/peppy/osu-stable-reference/blob/0e91e49bc83fe8b21c3ba5f1eb2d5d06456eae84/osu!/GameModes/Play/Rulesets/Ruleset.cs#L799
if (GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value < 2m)
// Note that osu!stable used sliderpoint30 for heads and repeats, and sliderpoint10 for ticks, but the mapping is intentionally changed here so that each texture represents one type of HitResult.
return GetTexture(result == HitResult.LargeTickHit ? "sliderpoint30" : "sliderpoint10");
return null;
}
case OsuSkinComponentLookup osuComponent:
switch (osuComponent.Component)
{

View File

@@ -232,10 +232,47 @@ namespace osu.Game.Rulesets.Osu.Statistics
if (pointGrid.Content.Count == 0)
return;
double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point.
double angle2 = Math.Atan2(end.Y - start.Y, start.X - end.X); // Angle between the end point and the start point.
Vector2 relativePosition = FindRelativeHitPosition(start, end, hitPoint, radius, rotation);
var localCentre = new Vector2(points_per_dimension - 1) / 2;
float localRadius = localCentre.X * inner_portion;
var localPoint = localCentre + localRadius * relativePosition;
// Find the most relevant hit point.
int r = (int)Math.Round(localPoint.Y);
int c = (int)Math.Round(localPoint.X);
if (r < 0 || r >= points_per_dimension || c < 0 || c >= points_per_dimension)
return;
PeakValue = Math.Max(PeakValue, ((GridPoint)pointGrid.Content[r][c]).Increment());
bufferedGrid.ForceRedraw();
}
/// <summary>
/// Normalises the position of a hit on a circle such that it is relative to the movement that was performed to arrive at said circle.
/// </summary>
/// <param name="previousObjectPosition">The position of the object prior to the one getting hit.</param>
/// <param name="nextObjectPosition">The position of the object which is getting hit.</param>
/// <param name="hitPoint">The point at which the user hit.</param>
/// <param name="objectRadius">The radius of <paramref name="previousObjectPosition"/> and <paramref name="nextObjectPosition"/>.</param>
/// <param name="rotation">
/// The rotation of the axis which is to be considered in the same direction as the vector
/// leading from <paramref name="previousObjectPosition"/> to <paramref name="nextObjectPosition"/>.
/// </param>
/// <returns>
/// A 2D vector representing the <paramref name="hitPoint"/> as relative to the movement between <paramref name="previousObjectPosition"/> and <paramref name="nextObjectPosition"/>
/// and relative to the <paramref name="objectRadius"/>.
/// If the object was hit perfectly in the middle, the return value will be <see cref="Vector2.Zero"/>.
/// If the object was hit perfectly at its edge, the returned vector will have a magnitude of 1.
/// </returns>
public static Vector2 FindRelativeHitPosition(Vector2 previousObjectPosition, Vector2 nextObjectPosition, Vector2 hitPoint, float objectRadius, float rotation)
{
double angle1 = Math.Atan2(nextObjectPosition.Y - hitPoint.Y, hitPoint.X - nextObjectPosition.X); // Angle between the end point and the hit point.
double angle2 = Math.Atan2(nextObjectPosition.Y - previousObjectPosition.Y, previousObjectPosition.X - nextObjectPosition.X); // Angle between the end point and the start point.
double finalAngle = angle2 - angle1; // Angle between start, end, and hit points.
float normalisedDistance = Vector2.Distance(hitPoint, end) / radius;
float normalisedDistance = Vector2.Distance(hitPoint, nextObjectPosition) / objectRadius; // Distance between the hit point and the end point.
// Consider two objects placed horizontally, with the start on the left and the end on the right.
// The above calculated the angle between {end, start}, and the angle between {end, hitPoint}, in the form:
@@ -254,22 +291,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
//
// We also need to apply the anti-clockwise rotation.
double rotatedAngle = finalAngle - float.DegreesToRadians(rotation);
var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle));
Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2;
float localRadius = localCentre.X * inner_portion * normalisedDistance;
Vector2 localPoint = localCentre + localRadius * rotatedCoordinate;
// Find the most relevant hit point.
int r = (int)Math.Round(localPoint.Y);
int c = (int)Math.Round(localPoint.X);
if (r < 0 || r >= points_per_dimension || c < 0 || c >= points_per_dimension)
return;
PeakValue = Math.Max(PeakValue, ((GridPoint)pointGrid.Content[r][c]).Increment());
bufferedGrid.ForceRedraw();
return -normalisedDistance * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle));
}
private abstract partial class GridPoint : CompositeDrawable

View File

@@ -81,6 +81,8 @@ namespace osu.Game.Rulesets.Osu.UI
HitResult.Ok,
HitResult.Meh,
HitResult.Miss,
HitResult.LargeTickHit,
HitResult.SliderTailHit,
HitResult.LargeTickMiss,
HitResult.IgnoreMiss,
}, onJudgementLoaded));

View File

@@ -3,7 +3,10 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.UI;
using osuTK;
@@ -12,6 +15,34 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods
{
public partial class TestSceneTaikoModFlashlight : TaikoModTestScene
{
[Test]
public void TestAspectRatios([Values] bool withClassicMod)
{
if (withClassicMod)
CreateModTest(new ModTestData { Mods = new Mod[] { new TaikoModFlashlight(), new TaikoModClassic() }, PassCondition = () => true });
else
CreateModTest(new ModTestData { Mod = new TaikoModFlashlight(), PassCondition = () => true });
AddStep("clear dim", () => LocalConfig.SetValue(OsuSetting.DimLevel, 0.0));
AddStep("reset", () => Stack.FillMode = FillMode.Stretch);
AddStep("set to 16:9", () =>
{
Stack.FillAspectRatio = 16 / 9f;
Stack.FillMode = FillMode.Fit;
});
AddStep("set to 4:3", () =>
{
Stack.FillAspectRatio = 4 / 3f;
Stack.FillMode = FillMode.Fit;
});
AddSliderStep("aspect ratio", 0.01f, 5f, 1f, v =>
{
Stack.FillAspectRatio = v;
Stack.FillMode = FillMode.Fit;
});
}
[TestCase(1f)]
[TestCase(0.5f)]
[TestCase(1.25f)]

View File

@@ -4,6 +4,7 @@
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.Mods;
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public override string Acronym => "CS";
public override double ScoreMultiplier => 0.9;
public override LocalisableString Description => "No more tricky speed changes!";
public override IconUsage? Icon => FontAwesome.Solid.Equals;
public override IconUsage? Icon => OsuIcon.ModConstantSpeed;
public override ModType Type => ModType.Conversion;
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)

View File

@@ -47,28 +47,15 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
this.taikoPlayfield = taikoPlayfield;
FlashlightSize = adjustSizeForPlayfieldAspectRatio(GetSize());
FlashlightSize = new Vector2(0, GetSize());
FlashlightSmoothness = 1.4f;
AddLayout(flashlightProperties);
}
/// <summary>
/// Returns the aspect ratio-adjusted size of the flashlight.
/// This ensures that the size of the flashlight remains independent of taiko-specific aspect ratio adjustments.
/// </summary>
/// <param name="size">
/// The size of the flashlight.
/// The value provided here should always come from <see cref="ModFlashlight{T}.Flashlight.GetSize"/>.
/// </param>
private Vector2 adjustSizeForPlayfieldAspectRatio(float size)
{
return new Vector2(0, size * taikoPlayfield.Parent!.Scale.Y);
}
protected override void UpdateFlashlightSize(float size)
{
this.TransformTo(nameof(FlashlightSize), adjustSizeForPlayfieldAspectRatio(size), FLASHLIGHT_FADE_DURATION);
this.TransformTo(nameof(FlashlightSize), new Vector2(0, size), FLASHLIGHT_FADE_DURATION);
}
protected override string FragmentShader => "CircularFlashlight";
@@ -82,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
FlashlightPosition = ToLocalSpace(taikoPlayfield.HitTarget.ScreenSpaceDrawQuad.Centre);
ClearTransforms(targetMember: nameof(FlashlightSize));
FlashlightSize = adjustSizeForPlayfieldAspectRatio(GetSize());
FlashlightSize = new Vector2(0, GetSize());
flashlightProperties.Validate();
}

View File

@@ -5,10 +5,12 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
@@ -21,6 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public override string Acronym => "SR";
public override double ScoreMultiplier => 0.6;
public override LocalisableString Description => "Simplify tricky rhythms!";
public override IconUsage? Icon => OsuIcon.ModSimplifiedRhythm;
public override ModType Type => ModType.DifficultyReduction;
[SettingSource("1/3 to 1/2 conversion", "Converts 1/3 patterns to 1/2 rhythm.")]

View File

@@ -6,9 +6,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Objects;
@@ -24,6 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public override string Name => @"Single Tap";
public override string Acronym => @"SG";
public override IconUsage? Icon => OsuIcon.ModSingleTap;
public override LocalisableString Description => @"One key for dons, one key for kats.";
public override double ScoreMultiplier => 1.0;

View File

@@ -3,8 +3,10 @@
using System;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
@@ -16,6 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public override string Name => "Swap";
public override string Acronym => "SW";
public override LocalisableString Description => @"Dons become kats, kats become dons";
public override IconUsage? Icon => OsuIcon.ModSwap;
public override ModType Type => ModType.Conversion;
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModRandom)).ToArray();

View File

@@ -117,6 +117,52 @@ namespace osu.Game.Tests.Editing.Checks
}
}
[Test]
public void TestBeatmapAudioTracksExemptedFromCheck()
{
using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
{
var beatmapSet = new BeatmapSetInfo
{
Files =
{
CheckTestHelpers.CreateMockFile("wav"),
CheckTestHelpers.CreateMockFile("mp3")
}
};
var firstPlayable = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = beatmapSet,
Metadata = new BeatmapMetadata { AudioFile = beatmapSet.Files[0].Filename }
}
};
var firstWorking = new Mock<TestWorkingBeatmap>(firstPlayable, null, null);
firstWorking.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
var secondPlayable = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = beatmapSet,
Metadata = new BeatmapMetadata { AudioFile = beatmapSet.Files[1].Filename }
}
};
var secondWorking = new Mock<TestWorkingBeatmap>(secondPlayable, null, null);
secondWorking.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
var context = new BeatmapVerifierContext(
new BeatmapVerifierContext.VerifiedBeatmap(firstWorking.Object, firstPlayable),
[new BeatmapVerifierContext.VerifiedBeatmap(secondWorking.Object, secondPlayable)],
DifficultyRating.ExpertPlus);
var issues = check.Run(context).ToList();
Assert.That(issues, Is.Empty);
}
}
private BeatmapVerifierContext getContext(Stream? resourceStream)
{
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);

View File

@@ -0,0 +1,151 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Models;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckInconsistentAudioTest
{
private CheckInconsistentAudio check = null!;
[SetUp]
public void Setup()
{
check = new CheckInconsistentAudio();
}
[Test]
public void TestConsistentAudio()
{
var beatmaps = createBeatmapSetWithAudio("audio.mp3", "audio.mp3");
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
Assert.That(check.Run(context), Is.Empty);
}
[Test]
public void TestInconsistentAudio()
{
var beatmaps = createBeatmapSetWithAudio("audio1.mp3", "audio2.mp3");
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckInconsistentAudio.IssueTemplateInconsistentAudio);
Assert.That(issues.Single().ToString(), Contains.Substring("audio1.mp3"));
Assert.That(issues.Single().ToString(), Contains.Substring("audio2.mp3"));
}
[Test]
public void TestInconsistentAudioWithNull()
{
var beatmaps = createBeatmapSetWithAudio("audio.mp3", null);
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckInconsistentAudio.IssueTemplateInconsistentAudio);
Assert.That(issues.Single().ToString(), Contains.Substring("audio.mp3"));
Assert.That(issues.Single().ToString(), Contains.Substring("not set"));
}
[Test]
public void TestInconsistentAudioWithEmptyString()
{
var beatmaps = createBeatmapSetWithAudio("audio.mp3", "");
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckInconsistentAudio.IssueTemplateInconsistentAudio);
Assert.That(issues.Single().ToString(), Contains.Substring("audio.mp3"));
Assert.That(issues.Single().ToString(), Contains.Substring("not set"));
}
[Test]
public void TestBothAudioNotSet()
{
var beatmaps = createBeatmapSetWithAudio("", "");
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
Assert.That(check.Run(context), Is.Empty);
}
[Test]
public void TestMultipleInconsistencies()
{
var beatmaps = createBeatmapSetWithAudio("audio1.mp3", "audio2.mp3", "audio3.mp3");
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.All(issue => issue.Template is CheckInconsistentAudio.IssueTemplateInconsistentAudio));
}
[Test]
public void TestSingleDifficulty()
{
var beatmaps = createBeatmapSetWithAudio("audio.mp3");
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
Assert.That(check.Run(context), Is.Empty);
}
private IBeatmap createBeatmapWithAudio(string audioFile, RealmNamedFileUsage? file)
{
var beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata { AudioFile = audioFile },
BeatmapSet = new BeatmapSetInfo()
}
};
if (file != null)
beatmap.BeatmapInfo.BeatmapSet!.Files.Add(file);
return beatmap;
}
private IBeatmap[] createBeatmapSetWithAudio(params string?[] audioFiles)
{
var beatmapSet = new BeatmapSetInfo();
var beatmaps = new IBeatmap[audioFiles.Length];
for (int i = 0; i < audioFiles.Length; i++)
{
string? audioFile = audioFiles[i];
var file = !string.IsNullOrEmpty(audioFile) ? CheckTestHelpers.CreateMockFile("mp3") : null;
beatmaps[i] = createBeatmapWithAudio(audioFile ?? "", file);
beatmaps[i].BeatmapInfo.BeatmapSet = beatmapSet;
beatmaps[i].BeatmapInfo.DifficultyName = $"Difficulty {i + 1}";
beatmapSet.Beatmaps.Add(beatmaps[i].BeatmapInfo);
}
return beatmaps;
}
private BeatmapVerifierContext createContextWithMultipleDifficulties(IBeatmap currentBeatmap, IBeatmap[] allDifficulties)
{
var verifiedCurrentBeatmap = new BeatmapVerifierContext.VerifiedBeatmap(new TestWorkingBeatmap(currentBeatmap), currentBeatmap);
var verifiedOtherBeatmaps = allDifficulties.Select(b => new BeatmapVerifierContext.VerifiedBeatmap(new TestWorkingBeatmap(b), b)).ToList();
return new BeatmapVerifierContext(verifiedCurrentBeatmap, verifiedOtherBeatmaps, DifficultyRating.ExpertPlus);
}
}
}

View File

@@ -66,6 +66,18 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("toggle black background", () => blackBackground?.FadeTo(1 - blackBackground.Alpha, 300, Easing.OutQuint));
AddSliderStep("leaderboard width", 0, 800, 300, v =>
{
if (leaderboard.IsNotNull())
leaderboard.Width = v;
});
AddSliderStep("leaderboard height", 0, 1000, 300, v =>
{
if (leaderboard.IsNotNull())
leaderboard.Height = v;
});
AddSliderStep("set player score", 50, 1_000_000, 700_000, v => gameplayState.ScoreProcessor.TotalScore.Value = v);
}
@@ -108,6 +120,45 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for 1st spot", () => leaderboard.TrackedScore!.ScorePosition.Value, () => Is.EqualTo(1));
}
[Test]
public void TestLongScores()
{
AddStep("set scores", () =>
{
var friend = new APIUser { Username = "Friend", Id = 1337 };
var api = (DummyAPIAccess)API;
api.Friends.Clear();
api.Friends.Add(new APIRelation
{
Mutual = true,
RelationType = RelationType.Friend,
TargetID = friend.OnlineID,
TargetUser = friend
});
// this is dodgy but anything less dodgy is a lot of work
((Bindable<LeaderboardScores?>)leaderboardManager.Scores).Value = LeaderboardScores.Success(new[]
{
new ScoreInfo { User = new APIUser { Username = "Top", Id = 2 }, TotalScore = 900_000_000, Accuracy = 0.99, MaxCombo = 999999 },
new ScoreInfo { User = new APIUser { Username = "Second", Id = 14 }, TotalScore = 800_000_000, Accuracy = 0.9, MaxCombo = 888888 },
new ScoreInfo { User = friend, TotalScore = 700_000_000, Accuracy = 0.88, MaxCombo = 777777 },
}, 3, null);
});
createLeaderboard();
AddStep("set score to 650k", () => gameplayState.ScoreProcessor.TotalScore.Value = 650_000_000);
AddUntilStep("wait for 4th spot", () => leaderboard.TrackedScore!.ScorePosition.Value, () => Is.EqualTo(4));
AddStep("set score to 750k", () => gameplayState.ScoreProcessor.TotalScore.Value = 750_000_000);
AddUntilStep("wait for 3rd spot", () => leaderboard.TrackedScore!.ScorePosition.Value, () => Is.EqualTo(3));
AddStep("set score to 850k", () => gameplayState.ScoreProcessor.TotalScore.Value = 850_000_000);
AddUntilStep("wait for 2nd spot", () => leaderboard.TrackedScore!.ScorePosition.Value, () => Is.EqualTo(2));
AddStep("set score to 950k", () => gameplayState.ScoreProcessor.TotalScore.Value = 950_000_000);
AddUntilStep("wait for 1st spot", () => leaderboard.TrackedScore!.ScorePosition.Value, () => Is.EqualTo(1));
}
[Test]
public void TestLayoutWithManyScores()
{

View File

@@ -24,6 +24,14 @@ namespace osu.Game.Tests.Visual.Gameplay
{
protected TestReplayPlayer Player = null!;
[Test]
public void TestFailedBeatmapLoad()
{
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo, withHitObjects: false));
AddUntilStep("wait for exit", () => Player.IsCurrentScreen());
}
[Test]
public void TestPauseViaSpace()
{

View File

@@ -28,6 +28,7 @@ using osu.Game.Storyboards;
using osu.Game.Tests.Beatmaps.IO;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -302,9 +303,69 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddWaitStep("wait a bit", 10);
}
[Test]
[Explicit("Test relies on timing of arriving frames to exercise assertions which doesn't work headless.")]
public void TestMaximisedUserIsAudioSource()
{
start(new[] { PLAYER_1_ID, PLAYER_2_ID });
loadSpectateScreen();
// With no frames, the synchronisation state will be TooFarAhead.
// In this state, all players should be muted.
assertMuted(PLAYER_1_ID, true);
assertMuted(PLAYER_2_ID, true);
// Send frames for both players.
sendFrames(PLAYER_1_ID, 20);
sendFrames(PLAYER_2_ID, 40);
waitUntilRunning(PLAYER_1_ID);
AddStep("maximise player 1", () =>
{
InputManager.MoveMouseTo(getInstance(PLAYER_1_ID));
InputManager.Click(MouseButton.Left);
});
assertMuted(PLAYER_1_ID, false);
assertMuted(PLAYER_2_ID, true);
waitUntilPaused(PLAYER_1_ID);
assertMuted(PLAYER_1_ID, false);
assertMuted(PLAYER_2_ID, true);
AddStep("minimise player 1", () =>
{
InputManager.MoveMouseTo(getInstance(PLAYER_1_ID));
InputManager.Click(MouseButton.Left);
});
assertMuted(PLAYER_1_ID, true);
assertMuted(PLAYER_2_ID, false);
AddStep("maximise player 2", () =>
{
InputManager.MoveMouseTo(getInstance(PLAYER_2_ID));
InputManager.Click(MouseButton.Left);
});
assertMuted(PLAYER_1_ID, true);
assertMuted(PLAYER_2_ID, false);
waitUntilPaused(PLAYER_2_ID);
sendFrames(PLAYER_1_ID, 60);
assertMuted(PLAYER_1_ID, true);
assertMuted(PLAYER_2_ID, false);
AddStep("minimise player 2", () =>
{
InputManager.MoveMouseTo(getInstance(PLAYER_2_ID));
InputManager.Click(MouseButton.Left);
});
assertMuted(PLAYER_1_ID, false);
assertMuted(PLAYER_2_ID, true);
}
[Test]
[FlakyTest]
public void TestMostInSyncUserIsAudioSource()
public void TestMostInSyncUserIsAudioSourceIfNoneMaximised()
{
start(new[] { PLAYER_1_ID, PLAYER_2_ID });
loadSpectateScreen();

View File

@@ -181,7 +181,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("beatmap in song select", () =>
{
var songSelect = (SoloSongSelect)Game.ScreenStack.CurrentScreen;
return songSelect.ChildrenOfType<BeatmapCarousel>().Single().GetCarouselItems()!.Any(i => i.Model is BeatmapSetInfo bsi && bsi.MatchesOnlineID(getImport()));
return songSelect.ChildrenOfType<BeatmapCarousel>().Single().GetCarouselItems()!.Any(i => i.Model is GroupedBeatmapSet gbs && gbs.BeatmapSet.MatchesOnlineID(getImport()));
});
}

View File

@@ -133,6 +133,8 @@ namespace osu.Game.Tests.Visual.Settings
{
public Bindable<Vector2> AreaOffset { get; } = new Bindable<Vector2>();
public Bindable<Vector2> AreaSize { get; } = new Bindable<Vector2>();
public Bindable<Vector2> OutputAreaOffset { get; } = new Bindable<Vector2>();
public Bindable<Vector2> OutputAreaSize { get; } = new Bindable<Vector2>();
public Bindable<float> Rotation { get; } = new Bindable<float>();

View File

@@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
];
var results = await runGrouping(GroupMode.None, beatmapSets);
Assert.That(results.Select(r => r.Model).OfType<BeatmapSetInfo>(), Is.EquivalentTo(beatmapSets));
Assert.That(results.Select(r => r.Model).OfType<GroupedBeatmapSet>().Select(groupedSet => groupedSet.BeatmapSet), Is.EquivalentTo(beatmapSets));
Assert.That(results.Select(r => r.Model).OfType<BeatmapInfo>(), Is.EquivalentTo(allBeatmaps));
assertTotal(results, beatmapSets.Count + allBeatmaps.Length);
}
@@ -74,11 +74,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2
addBeatmapSet(applyBeatmap('_'), beatmapSets, out var underscoreBeatmap);
var results = await runGrouping(mode, beatmapSets);
assertGroup(results, 0, "0-9", new[] { fiveBeatmap, fourBeatmap }, ref total);
assertGroup(results, 1, "A", new[] { aBeatmap }, ref total);
assertGroup(results, 2, "F", new[] { fBeatmap }, ref total);
assertGroup(results, 3, "Z", new[] { zBeatmap }, ref total);
assertGroup(results, 4, "Other", new[] { dashBeatmap, underscoreBeatmap }, ref total);
assertGroup(results, 0, "0-9", fiveBeatmap.Beatmaps.Concat(fourBeatmap.Beatmaps), ref total);
assertGroup(results, 1, "A", aBeatmap.Beatmaps, ref total);
assertGroup(results, 2, "F", fBeatmap.Beatmaps, ref total);
assertGroup(results, 3, "Z", zBeatmap.Beatmaps, ref total);
assertGroup(results, 4, "Other", dashBeatmap.Beatmaps.Concat(underscoreBeatmap.Beatmaps), ref total);
assertTotal(results, total);
}
@@ -115,12 +115,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2
addBeatmapSet(s => s.DateAdded = DateTimeOffset.Now.AddMonths(-2).AddDays(-3), beatmapSets, out var twoMonthsAgoBeatmap);
var results = await runGrouping(GroupMode.DateAdded, beatmapSets);
assertGroup(results, 0, "Today", new[] { todayBeatmap }, ref total);
assertGroup(results, 1, "Yesterday", new[] { yesterdayBeatmap }, ref total);
assertGroup(results, 2, "Last week", new[] { lastWeekBeatmap }, ref total);
assertGroup(results, 3, "Last month", new[] { lastMonthBeatmap }, ref total);
assertGroup(results, 4, "1 month ago", new[] { oneMonthAgoBeatmap }, ref total);
assertGroup(results, 5, "2 months ago", new[] { twoMonthsAgoBeatmap }, ref total);
assertGroup(results, 0, "Today", todayBeatmap.Beatmaps, ref total);
assertGroup(results, 1, "Yesterday", yesterdayBeatmap.Beatmaps, ref total);
assertGroup(results, 2, "Last week", lastWeekBeatmap.Beatmaps, ref total);
assertGroup(results, 3, "Last month", lastMonthBeatmap.Beatmaps, ref total);
assertGroup(results, 4, "1 month ago", oneMonthAgoBeatmap.Beatmaps, ref total);
assertGroup(results, 5, "2 months ago", twoMonthsAgoBeatmap.Beatmaps, ref total);
assertTotal(results, total);
}
@@ -139,13 +139,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2
addBeatmapSet(applyLastPlayed(null), beatmapSets, out var neverBeatmap);
var results = await runGrouping(GroupMode.LastPlayed, beatmapSets);
assertGroup(results, 0, "Today", new[] { todayBeatmap }, ref total);
assertGroup(results, 1, "Yesterday", new[] { yesterdayBeatmap }, ref total);
assertGroup(results, 2, "Last week", new[] { lastWeekBeatmap }, ref total);
assertGroup(results, 3, "Last month", new[] { lastMonthBeatmap }, ref total);
assertGroup(results, 4, "1 month ago", new[] { oneMonthAgoBeatmap }, ref total);
assertGroup(results, 5, "2 months ago", new[] { twoMonthsBeatmap }, ref total);
assertGroup(results, 6, "Never", new[] { neverBeatmap }, ref total);
assertGroup(results, 0, "Today", todayBeatmap.Beatmaps, ref total);
assertGroup(results, 1, "Yesterday", yesterdayBeatmap.Beatmaps, ref total);
assertGroup(results, 2, "Last week", lastWeekBeatmap.Beatmaps, ref total);
assertGroup(results, 3, "Last month", lastMonthBeatmap.Beatmaps, ref total);
assertGroup(results, 4, "1 month ago", oneMonthAgoBeatmap.Beatmaps, ref total);
assertGroup(results, 5, "2 months ago", twoMonthsBeatmap.Beatmaps, ref total);
assertGroup(results, 6, "Never", neverBeatmap.Beatmaps, ref total);
assertTotal(results, total);
}
@@ -162,7 +162,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
var results = await runGrouping(GroupMode.LastPlayed, beatmapSets);
int total = 0;
assertGroup(results, 0, "Today", new[] { set }, ref total);
assertGroup(results, 0, "Today", [set.Beatmaps[2]], ref total);
assertGroup(results, 1, "Never", [set.Beatmaps[0], set.Beatmaps[1]], ref total);
assertTotal(results, total);
}
@@ -176,8 +177,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
var results = await runGrouping(GroupMode.LastPlayed, beatmapSets);
int total = 0;
assertGroup(results, 0, "Over 5 months ago", new[] { overFiveMonthsBeatmap }, ref total);
assertGroup(results, 1, "Never", new[] { neverBeatmap }, ref total);
assertGroup(results, 0, "Over 5 months ago", overFiveMonthsBeatmap.Beatmaps, ref total);
assertGroup(results, 1, "Never", neverBeatmap.Beatmaps, ref total);
assertTotal(results, total);
}
@@ -207,14 +208,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2
addBeatmapSet(s => s.Status = BeatmapOnlineStatus.LocallyModified, beatmapSets, out var localBeatmap);
var results = await runGrouping(GroupMode.RankedStatus, beatmapSets);
assertGroup(results, 0, "Ranked", new[] { rankedBeatmap, approvedBeatmap }, ref total);
assertGroup(results, 1, "Qualified", new[] { qualifiedBeatmap }, ref total);
assertGroup(results, 2, "WIP", new[] { wipBeatmap }, ref total);
assertGroup(results, 3, "Pending", new[] { pendingBeatmap }, ref total);
assertGroup(results, 4, "Graveyard", new[] { graveyardBeatmap }, ref total);
assertGroup(results, 5, "Local", new[] { localBeatmap }, ref total);
assertGroup(results, 6, "Unknown", new[] { noneBeatmap }, ref total);
assertGroup(results, 7, "Loved", new[] { lovedBeatmap }, ref total);
assertGroup(results, 0, "Ranked", rankedBeatmap.Beatmaps.Concat(approvedBeatmap.Beatmaps), ref total);
assertGroup(results, 1, "Qualified", qualifiedBeatmap.Beatmaps, ref total);
assertGroup(results, 2, "WIP", wipBeatmap.Beatmaps, ref total);
assertGroup(results, 3, "Pending", pendingBeatmap.Beatmaps, ref total);
assertGroup(results, 4, "Graveyard", graveyardBeatmap.Beatmaps, ref total);
assertGroup(results, 5, "Local", localBeatmap.Beatmaps, ref total);
assertGroup(results, 6, "Unknown", noneBeatmap.Beatmaps, ref total);
assertGroup(results, 7, "Loved", lovedBeatmap.Beatmaps, ref total);
assertTotal(results, total);
}
@@ -240,12 +241,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2
addBeatmapSet(applyBPM(330), beatmapSets, out var beatmap330);
var results = await runGrouping(GroupMode.BPM, beatmapSets);
assertGroup(results, 0, "Under 60 BPM", new[] { beatmap30 }, ref total);
assertGroup(results, 1, "60 - 70 BPM", new[] { beatmap59, beatmap60 }, ref total);
assertGroup(results, 2, "90 - 100 BPM", new[] { beatmap90, beatmap95 }, ref total);
assertGroup(results, 3, "270 - 280 BPM", new[] { beatmap269, beatmap270 }, ref total);
assertGroup(results, 4, "290 - 300 BPM", new[] { beatmap299 }, ref total);
assertGroup(results, 5, "Over 300 BPM", new[] { beatmap300, beatmap330 }, ref total);
assertGroup(results, 0, "Under 60 BPM", beatmap30.Beatmaps, ref total);
assertGroup(results, 1, "60 - 70 BPM", (beatmap59.Beatmaps.Concat(beatmap60.Beatmaps)), ref total);
assertGroup(results, 2, "90 - 100 BPM", (beatmap90.Beatmaps.Concat(beatmap95.Beatmaps)), ref total);
assertGroup(results, 3, "270 - 280 BPM", (beatmap269.Beatmaps.Concat(beatmap270.Beatmaps)), ref total);
assertGroup(results, 4, "290 - 300 BPM", beatmap299.Beatmaps, ref total);
assertGroup(results, 5, "Over 300 BPM", (beatmap300.Beatmaps.Concat(beatmap330.Beatmaps)), ref total);
assertTotal(results, total);
}
@@ -272,10 +273,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2
addBeatmapSet(applyStars(7), beatmapSets, out var beatmap7);
var results = await runGrouping(GroupMode.Difficulty, beatmapSets);
assertGroup(results, 0, "Below 1 Star", new[] { beatmapBelow1 }, ref total);
assertGroup(results, 1, "1 Star", new[] { beatmapAbove1, beatmapAlmost2 }, ref total);
assertGroup(results, 2, "2 Stars", new[] { beatmap2, beatmapAbove2 }, ref total);
assertGroup(results, 3, "7 Stars", new[] { beatmap7 }, ref total);
assertGroup(results, 0, "Below 1 Star", beatmapBelow1.Beatmaps, ref total);
assertGroup(results, 1, "1 Star", (beatmapAbove1.Beatmaps.Concat(beatmapAlmost2.Beatmaps)), ref total);
assertGroup(results, 2, "2 Stars", (beatmap2.Beatmaps.Concat(beatmapAbove2.Beatmaps)), ref total);
assertGroup(results, 3, "7 Stars", beatmap7.Beatmaps, ref total);
assertTotal(results, total);
}
@@ -304,11 +305,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2
addBeatmapSet(applyLength(630_000), beatmapSets, out var beatmap10Min30Sec);
var results = await runGrouping(GroupMode.Length, beatmapSets);
assertGroup(results, 0, "1 minute or less", new[] { beatmap30Sec, beatmap1Min }, ref total);
assertGroup(results, 1, "2 minutes or less", new[] { beatmap1Min30Sec, beatmap2Min }, ref total);
assertGroup(results, 2, "5 minutes or less", new[] { beatmap5Min }, ref total);
assertGroup(results, 3, "10 minutes or less", new[] { beatmap6Min, beatmap10Min }, ref total);
assertGroup(results, 4, "Over 10 minutes", new[] { beatmap10Min30Sec }, ref total);
assertGroup(results, 0, "1 minute or less", (beatmap30Sec.Beatmaps.Concat(beatmap1Min.Beatmaps)), ref total);
assertGroup(results, 1, "2 minutes or less", (beatmap1Min30Sec.Beatmaps.Concat(beatmap2Min.Beatmaps)), ref total);
assertGroup(results, 2, "5 minutes or less", beatmap5Min.Beatmaps, ref total);
assertGroup(results, 3, "10 minutes or less", (beatmap6Min.Beatmaps.Concat(beatmap10Min.Beatmaps)), ref total);
assertGroup(results, 4, "Over 10 minutes", beatmap10Min30Sec.Beatmaps, ref total);
assertTotal(results, total);
}
@@ -334,10 +335,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2
addBeatmapSet(s => s.DateRanked = null, beatmapSets, out var beatmapUnranked);
var results = await runGrouping(GroupMode.DateRanked, beatmapSets);
assertGroup(results, 0, "2025", new[] { beatmap2025 }, ref total);
assertGroup(results, 1, "2010", new[] { beatmap2010 }, ref total);
assertGroup(results, 2, "2007", new[] { beatmapOct2007, beatmapDec2007 }, ref total);
assertGroup(results, 3, "Unranked", new[] { beatmapUnranked }, ref total);
assertGroup(results, 0, "2025", beatmap2025.Beatmaps, ref total);
assertGroup(results, 1, "2010", beatmap2010.Beatmaps, ref total);
assertGroup(results, 2, "2007", (beatmapOct2007.Beatmaps.Concat(beatmapDec2007.Beatmaps)), ref total);
assertGroup(results, 3, "Unranked", beatmapUnranked.Beatmaps, ref total);
assertTotal(results, total);
}
@@ -357,9 +358,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = string.Empty, beatmapSets, out var beatmapUnsourced);
var results = await runGrouping(GroupMode.Source, beatmapSets);
assertGroup(results, 0, "Cool Game", new[] { beatmapCoolGame, beatmapCoolGameB }, ref total);
assertGroup(results, 1, "Nice Movie", new[] { beatmapNiceMovie }, ref total);
assertGroup(results, 2, "Unsourced", new[] { beatmapUnsourced }, ref total);
assertGroup(results, 0, "Cool Game", (beatmapCoolGame.Beatmaps.Concat(beatmapCoolGameB.Beatmaps)), ref total);
assertGroup(results, 1, "Nice Movie", beatmapNiceMovie.Beatmaps, ref total);
assertGroup(results, 2, "Unsourced", beatmapUnsourced.Beatmaps, ref total);
assertTotal(results, total);
}
@@ -375,7 +376,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
return await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None);
}
private static void assertGroup(List<CarouselItem> items, int index, string expectedTitle, IEnumerable<BeatmapSetInfo> expectedBeatmapSets, ref int totalItems)
private static void assertGroup(List<CarouselItem> items, int index, string expectedTitle, IEnumerable<BeatmapInfo> expectedBeatmaps, ref int totalItems)
{
var groupItem = items.Where(i => i.Model is GroupDefinition).ElementAtOrDefault(index);
@@ -390,7 +391,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
var groupModel = (GroupDefinition)groupItem.Model;
Assert.That(groupModel.Title, Is.EqualTo(expectedTitle));
Assert.That(itemsInGroup.Select(i => i.Model).OfType<BeatmapInfo>(), Is.EquivalentTo(expectedBeatmapSets.SelectMany(bs => bs.Beatmaps)));
Assert.That(itemsInGroup.Select(i => i.Model).OfType<BeatmapInfo>(), Is.EquivalentTo(expectedBeatmaps));
totalItems += itemsInGroup.Count() + 1;
}

View File

@@ -237,7 +237,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
// Using groupingFilter.SetItems.Count alone doesn't work.
// When sorting by difficulty, there can be more than one set panel for the same set displayed.
return groupingFilter.SetItems.Sum(s => s.Value.Count(i => i.Model is BeatmapSetInfo));
return groupingFilter.SetItems.Sum(s => s.Value.Count(i => i.Model is GroupedBeatmapSet));
}, () => Is.EqualTo(expected));
}
@@ -440,7 +440,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
public BeatmapInfo? SelectedBeatmapInfo => CurrentSelection as BeatmapInfo;
public BeatmapSetInfo? SelectedBeatmapSet => SelectedBeatmapInfo?.BeatmapSet;
public new BeatmapSetInfo? ExpandedBeatmapSet => base.ExpandedBeatmapSet;
public new GroupedBeatmapSet? ExpandedBeatmapSet => base.ExpandedBeatmapSet;
public new GroupDefinition? ExpandedGroup => base.ExpandedGroup;
public TestBeatmapCarousel()

View File

@@ -22,8 +22,6 @@ using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
@@ -43,9 +41,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
protected Screens.SelectV2.SongSelect SongSelect { get; private set; } = null!;
protected BeatmapCarousel Carousel => SongSelect.ChildrenOfType<BeatmapCarousel>().Single();
[Cached]
protected readonly ScreenFooter Footer;
[Cached]
private readonly OsuLogo logo;
@@ -72,10 +67,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
State = { Value = Visibility.Visible },
},
Footer = new ScreenFooter
{
BackButtonPressed = () => Stack.CurrentScreen.Exit(),
},
logo = new OsuLogo
{
Alpha = 0f,
@@ -111,14 +102,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
Add(beatmapStore);
}
protected override void LoadComplete()
{
base.LoadComplete();
Stack.ScreenPushed += updateFooter;
Stack.ScreenExited += updateFooter;
}
public override void SetUpSteps()
{
base.SetUpSteps();
@@ -207,38 +190,5 @@ namespace osu.Game.Tests.Visual.SongSelectV2
}
protected void WaitForSuspension() => AddUntilStep("wait for not current", () => !SongSelect.AsNonNull().IsCurrentScreen());
private void updateFooter(IScreen? _, IScreen? newScreen)
{
if (newScreen is OsuScreen osuScreen && osuScreen.ShowFooter)
{
Footer.Show();
if (osuScreen.IsLoaded)
updateFooterButtons();
else
{
// ensure the current buttons are immediately disabled on screen change (so they can't be pressed).
Footer.SetButtons(Array.Empty<ScreenFooterButton>());
osuScreen.OnLoadComplete += _ => updateFooterButtons();
}
void updateFooterButtons()
{
var buttons = osuScreen.CreateFooterButtons();
osuScreen.LoadComponentsAgainstScreenDependencies(buttons);
Footer.SetButtons(buttons);
Footer.Show();
}
}
else
{
Footer.Hide();
Footer.SetButtons(Array.Empty<ScreenFooterButton>());
}
}
}
}

View File

@@ -396,7 +396,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
ApplyToFilterAndWaitForFilter("filter first away", c => c.UserStarDifficulty.Min = 3);
SelectNextPanel();
AddAssert("keyboard selected is first set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.First()));
AddAssert("keyboard selected is first set",
() => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet,
() => Is.EqualTo(BeatmapSets.First()));
}
[Test]
@@ -413,7 +415,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
ApplyToFilterAndWaitForFilter("filter first away", c => c.UserStarDifficulty.Min = 3);
SelectPrevPanel();
AddAssert("keyboard selected is last set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.Last()));
AddAssert("keyboard selected is last set",
() => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet,
() => Is.EqualTo(BeatmapSets.Last()));
}
[Test]
@@ -428,7 +432,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
ApplyToFilterAndWaitForFilter("filter last set away", c => c.SearchText = BeatmapSets.First().Metadata.Title);
SelectPrevPanel();
AddAssert("keyboard selected is first set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.First()));
AddAssert("keyboard selected is first set",
() => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet,
() => Is.EqualTo(BeatmapSets.First()));
}
[Test]
@@ -444,7 +450,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
// Single result is automatically selected for us, so we iterate once backwards to the set header.
SelectPrevPanel();
AddAssert("keyboard selected is second set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.Last()));
AddAssert("keyboard selected is second set",
() => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet,
() => Is.EqualTo(BeatmapSets.Last()));
}
}
}

View File

@@ -0,0 +1,120 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelectV2
{
[TestFixture]
public partial class TestSceneBeatmapCarouselSetsSplitApart : BeatmapCarouselTestScene
{
[SetUpSteps]
public void SetUpSteps()
{
RemoveAllBeatmaps();
CreateCarousel();
SortAndGroupBy(SortMode.Title, GroupMode.Length);
}
[Test]
public void TestSetTraversal()
{
AddBeatmaps(3, splitApart: true);
AddBeatmaps(3, splitApart: false);
WaitForDrawablePanels();
SelectNextSet();
WaitForSetSelection(set: 0, diff: 0);
SelectNextSet();
WaitForSetSelection(set: 1, diff: 0);
SelectPrevSet();
WaitForSetSelection(set: 0, diff: 0);
SelectPrevSet();
WaitForSetSelection(set: 5, diff: 0);
SelectPrevSet();
SelectPrevSet();
SelectPrevSet();
WaitForSetSelection(set: 2, diff: 4);
AddAssert("only two beatmap panels visible", () => GetVisiblePanels<PanelBeatmap>().Count(), () => Is.EqualTo(2));
}
[Test]
public void TestBeatmapTraversal()
{
AddBeatmaps(3, splitApart: true);
AddBeatmaps(3, splitApart: false);
WaitForDrawablePanels();
SelectNextSet();
WaitForSetSelection(set: 0, diff: 0);
SelectNextPanel();
WaitForSetSelection(set: 0, diff: 1);
SelectNextPanel(); // header of set 1 in group 0
Select();
WaitForSetSelection(set: 1, diff: 0);
SelectPrevPanel(); // header of set 1 in group 0
SelectPrevPanel(); // header of set 0 in group 0
Select();
WaitForSetSelection(set: 0, diff: 0);
SelectPrevPanel(); // header of set 0 in group 0
SelectPrevPanel(); // header of group 0
SelectPrevPanel(); // header of group 2
Select();
SelectNextPanel(); // header of set 0 in group 2
Select();
WaitForSetSelection(set: 0, diff: 4);
}
[Test]
public void TestRandomStaysInGroup()
{
AddBeatmaps(2, splitApart: false);
AddBeatmaps(1, splitApart: true);
WaitForDrawablePanels();
SelectPrevSet();
SelectPrevSet();
WaitForSetSelection(set: 1);
WaitForExpandedGroup(2);
AddStep("select next random", () => Carousel.NextRandom());
WaitForExpandedGroup(2);
AddStep("select next random", () => Carousel.NextRandom());
WaitForExpandedGroup(2);
}
protected void AddBeatmaps(int count, bool splitApart) => AddStep($"add {count} beatmaps ({(splitApart ? "" : "not ")}split apart)", () =>
{
var beatmapSets = new List<BeatmapSetInfo>();
for (int i = 0; i < count; i++)
{
var beatmapSet = CreateTestBeatmapSetInfo(6, false);
for (int j = 0; j < beatmapSet.Beatmaps.Count; j++)
{
beatmapSet.Beatmaps[j].Length = splitApart ? 30_000 * (j + 1) : 180_000;
}
beatmapSets.Add(beatmapSet);
}
BeatmapSets.AddRange(beatmapSets);
});
}
}

View File

@@ -22,12 +22,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
private BeatmapSetInfo baseTestBeatmap = null!;
private const int initial_filter_count = 3;
[SetUpSteps]
public void SetUpSteps()
{
RemoveAllBeatmaps();
CreateCarousel();
WaitForFiltering();
AddBeatmaps(1, 3);
WaitForFiltering();
AddStep("generate and add test beatmap", () =>
{
baseTestBeatmap = TestResources.CreateTestBeatmapSetInfo(3);
@@ -42,8 +46,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
b.Metadata = metadata;
BeatmapSets.Add(baseTestBeatmap);
});
WaitForFiltering();
AddAssert("filter count correct", () => Carousel.FilterCount, () => Is.EqualTo(initial_filter_count));
}
[Test]
@@ -81,12 +86,18 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddUntilStep("is scrolled to end", () => Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single().IsScrolledToEnd());
updateBeatmap(b => b.Metadata = new BeatmapMetadata
updateBeatmap(b =>
{
Artist = "updated test",
Title = $"beatmap {RNG.Next().ToString()}"
// hash will be updated when important metadata changes, such as title, difficulty, author etc.
b.Hash = "new hash";
b.Metadata = new BeatmapMetadata
{
Artist = "updated test",
Title = $"beatmap {RNG.Next().ToString()}"
};
});
assertDidFilter();
WaitForFiltering();
AddAssert("scroll is still at end", () => Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single().IsScrolledToEnd());
@@ -113,8 +124,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddStep("find panel", () => panel = Carousel.ChildrenOfType<PanelBeatmapSet>().Single(p => p.ChildrenOfType<OsuSpriteText>().Any(t => t.Text.ToString() == "beatmap")));
updateBeatmap(b => b.Metadata = metadata);
updateBeatmap(b =>
{
b.Metadata = metadata;
// hash will be updated when important metadata changes, such as title, difficulty, author etc.
b.Hash = "new hash";
});
assertDidFilter();
WaitForFiltering();
AddAssert("drawables unchanged", () => Carousel.ChildrenOfType<Panel>(), () => Is.EqualTo(originalDrawables));
@@ -123,7 +140,41 @@ namespace osu.Game.Tests.Visual.SongSelectV2
}
[Test]
public void TestSelectionHeld()
public void TestOnlineStatusUpdated()
{
List<Panel> originalDrawables = new List<Panel>();
AddStep("store drawable references", () =>
{
originalDrawables.Clear();
originalDrawables.AddRange(Carousel.ChildrenOfType<Panel>().ToList());
});
updateBeatmap(b => b.Status = BeatmapOnlineStatus.Graveyard);
assertDidFilter();
WaitForFiltering();
AddAssert("drawables unchanged", () => Carousel.ChildrenOfType<Panel>(), () => Is.EqualTo(originalDrawables));
}
[Test]
public void TestNoUpdateTriggeredOnUserTagsChange()
{
var metadata = new BeatmapMetadata
{
Artist = "updated test",
Title = "new beatmap title",
UserTags = { "hi" }
};
updateBeatmap(b => b.Metadata = metadata);
assertDidNotFilter();
}
[TestCase(false)]
[TestCase(true)]
public void TestSelectionHeld(bool hashChanged)
{
SelectNextSet();
@@ -131,7 +182,17 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
updateBeatmap();
updateBeatmap(b =>
{
if (hashChanged)
b.Hash = "new hash";
});
if (hashChanged)
assertDidFilter();
else
assertDidNotFilter();
WaitForFiltering();
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
@@ -148,6 +209,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
updateBeatmap(b => b.DifficultyName = "new name");
assertDidFilter();
WaitForFiltering();
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
@@ -164,6 +226,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
updateBeatmap(b => b.OnlineID = b.OnlineID + 1);
assertDidFilter();
WaitForFiltering();
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
@@ -339,6 +402,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("Order didn't change", () => Carousel.PostFilterBeatmaps.Select(b => b.ID), () => Is.EqualTo(originalOrder));
}
private void assertDidFilter() => AddAssert("did filter", () => Carousel.FilterCount, () => Is.EqualTo(initial_filter_count + 1));
private void assertDidNotFilter() => AddAssert("did not filter", () => Carousel.FilterCount, () => Is.EqualTo(initial_filter_count));
private void updateBeatmap(Action<BeatmapInfo>? updateBeatmap = null, Action<BeatmapSetInfo>? updateSet = null)
{
AddStep("update beatmap with different reference", () =>

View File

@@ -7,6 +7,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
@@ -42,7 +43,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
private DialogOverlay dialogOverlay = null!;
private LeaderboardManager leaderboardManager = null!;
private RealmPopulatingOnlineLookupSource lookupSource = null!;
private readonly IBindable<Screens.SelectV2.SongSelect.BeatmapSetLookupResult?> onlineLookupResult = new Bindable<Screens.SelectV2.SongSelect.BeatmapSetLookupResult?>();
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
@@ -52,7 +54,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API));
dependencies.Cache(leaderboardManager = new LeaderboardManager());
dependencies.Cache(lookupSource = new RealmPopulatingOnlineLookupSource());
dependencies.CacheAs(onlineLookupResult);
Dependencies.Cache(Realm);
@@ -68,7 +70,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
});
LoadComponent(leaderboardManager);
LoadComponent(lookupSource);
Child = contentContainer = new OsuContextMenuContainer
{

View File

@@ -4,13 +4,12 @@
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Models;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.SelectV2;
@@ -18,64 +17,25 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneBeatmapMetadataWedge : SongSelectComponentsTestScene
{
private APIBeatmapSet? currentOnlineSet;
private BeatmapMetadataWedge wedge = null!;
[Cached(typeof(IBindable<Screens.SelectV2.SongSelect.BeatmapSetLookupResult?>))]
private Bindable<Screens.SelectV2.SongSelect.BeatmapSetLookupResult?> onlineLookupResult = new Bindable<Screens.SelectV2.SongSelect.BeatmapSetLookupResult?>();
protected override void LoadComplete()
{
base.LoadComplete();
var lookupSource = new RealmPopulatingOnlineLookupSource();
Child = new DependencyProvidingContainer
Child = wedge = new BeatmapMetadataWedge
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = [(typeof(RealmPopulatingOnlineLookupSource), lookupSource)],
Children =
[
lookupSource,
wedge = new BeatmapMetadataWedge
{
State = { Value = Visibility.Visible },
}
]
State = { Value = Visibility.Visible },
};
}
[SetUpSteps]
public override void SetUpSteps()
{
AddStep("register request handling", () =>
{
((DummyAPIAccess)API).HandleRequest = request =>
{
switch (request)
{
case GetBeatmapSetRequest set:
if (set.ID == currentOnlineSet?.OnlineID)
{
set.TriggerSuccess(currentOnlineSet);
return true;
}
return false;
default:
return false;
}
};
});
}
[Test]
public void TestShowHide()
{
AddStep("all metrics", () =>
{
var (working, onlineSet) = createTestBeatmap();
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddStep("all metrics", () => (Beatmap.Value, onlineLookupResult.Value) = createTestBeatmap());
AddStep("hide wedge", () => wedge.Hide());
AddStep("show wedge", () => wedge.Show());
@@ -84,67 +44,63 @@ namespace osu.Game.Tests.Visual.SongSelectV2
[Test]
public void TestVariousMetrics()
{
AddStep("all metrics", () =>
{
var (working, onlineSet) = createTestBeatmap();
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddStep("all metrics", () => (Beatmap.Value, onlineLookupResult.Value) = createTestBeatmap());
AddStep("null beatmap", () => Beatmap.SetDefault());
AddStep("no source", () =>
{
var (working, onlineSet) = createTestBeatmap();
var (working, online) = createTestBeatmap();
working.Metadata.Source = string.Empty;
currentOnlineSet = onlineSet;
onlineLookupResult.Value = online;
Beatmap.Value = working;
});
AddStep("no success rate", () =>
{
var (working, onlineSet) = createTestBeatmap();
var (working, online) = createTestBeatmap();
onlineSet.Beatmaps.Single().PlayCount = 0;
onlineSet.Beatmaps.Single().PassCount = 0;
online.Result!.Beatmaps.Single().PlayCount = 0;
online.Result!.Beatmaps.Single().PassCount = 0;
currentOnlineSet = onlineSet;
onlineLookupResult.Value = online;
Beatmap.Value = working;
});
AddStep("no user ratings", () =>
{
var (working, onlineSet) = createTestBeatmap();
var (working, online) = createTestBeatmap();
onlineSet.Ratings = Array.Empty<int>();
online.Result!.Ratings = Array.Empty<int>();
currentOnlineSet = onlineSet;
onlineLookupResult.Value = online;
Beatmap.Value = working;
});
AddStep("no fail times", () =>
{
var (working, onlineSet) = createTestBeatmap();
var (working, online) = createTestBeatmap();
onlineSet.Beatmaps.Single().FailTimes = null;
online.Result!.Beatmaps.Single().FailTimes = null;
currentOnlineSet = onlineSet;
onlineLookupResult.Value = online;
Beatmap.Value = working;
});
AddStep("no metrics", () =>
{
var (working, onlineSet) = createTestBeatmap();
var (working, online) = createTestBeatmap();
onlineSet.Ratings = Array.Empty<int>();
onlineSet.Beatmaps.Single().FailTimes = null;
online.Result!.Ratings = Array.Empty<int>();
online.Result!.Beatmaps.Single().FailTimes = null;
currentOnlineSet = onlineSet;
onlineLookupResult.Value = online;
Beatmap.Value = working;
});
AddStep("local beatmap", () =>
{
var (working, onlineSet) = createTestBeatmap();
var (working, _) = createTestBeatmap();
working.BeatmapInfo.OnlineID = 0;
currentOnlineSet = onlineSet;
onlineLookupResult.Value = null;
Beatmap.Value = working;
});
}
@@ -154,16 +110,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
AddStep("long text", () =>
{
var (working, onlineSet) = createTestBeatmap();
var (working, online) = createTestBeatmap();
working.BeatmapInfo.Metadata.Author = new RealmUser { Username = "Verrrrryyyy llooonngggggg author" };
working.BeatmapInfo.Metadata.Source = "Verrrrryyyy llooonngggggg source";
working.BeatmapInfo.Metadata.Tags = string.Join(' ', Enumerable.Repeat(working.BeatmapInfo.Metadata.Tags, 3));
onlineSet.Genre = new BeatmapSetOnlineGenre { Id = 12, Name = "Verrrrryyyy llooonngggggg genre" };
onlineSet.Language = new BeatmapSetOnlineLanguage { Id = 12, Name = "Verrrrryyyy llooonngggggg language" };
onlineSet.Beatmaps.Single().TopTags = Enumerable.Repeat(onlineSet.Beatmaps.Single().TopTags, 3).SelectMany(t => t!).ToArray();
online.Result!.Genre = new BeatmapSetOnlineGenre { Id = 12, Name = "Verrrrryyyy llooonngggggg genre" };
online.Result!.Language = new BeatmapSetOnlineLanguage { Id = 12, Name = "Verrrrryyyy llooonngggggg language" };
online.Result!.Beatmaps.Single().TopTags = Enumerable.Repeat(online.Result!.Beatmaps.Single().TopTags, 3).SelectMany(t => t!).ToArray();
currentOnlineSet = onlineSet;
onlineLookupResult.Value = online;
Beatmap.Value = working;
});
}
@@ -171,22 +127,17 @@ namespace osu.Game.Tests.Visual.SongSelectV2
[Test]
public void TestOnlineAvailability()
{
AddStep("online beatmapset", () =>
{
var (working, onlineSet) = createTestBeatmap();
AddStep("online beatmapset", () => (Beatmap.Value, onlineLookupResult.Value) = createTestBeatmap());
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddUntilStep("rating wedge visible", () => wedge.RatingsVisible);
AddUntilStep("fail time wedge visible", () => wedge.FailRetryVisible);
AddStep("online beatmapset with local diff", () =>
{
var (working, onlineSet) = createTestBeatmap();
var (working, lookupResult) = createTestBeatmap();
working.BeatmapInfo.ResetOnlineInfo();
currentOnlineSet = onlineSet;
onlineLookupResult.Value = lookupResult;
Beatmap.Value = working;
});
AddUntilStep("rating wedge hidden", () => !wedge.RatingsVisible);
@@ -195,7 +146,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
var (working, _) = createTestBeatmap();
currentOnlineSet = null;
onlineLookupResult.Value = null;
Beatmap.Value = working;
});
AddAssert("rating wedge still hidden", () => !wedge.RatingsVisible);
@@ -205,21 +156,17 @@ namespace osu.Game.Tests.Visual.SongSelectV2
[Test]
public void TestUserTags()
{
AddStep("user tags", () =>
{
var (working, onlineSet) = createTestBeatmap();
AddStep("user tags", () => (Beatmap.Value, onlineLookupResult.Value) = createTestBeatmap());
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddStep("no user tags", () =>
{
var (working, onlineSet) = createTestBeatmap();
var (working, online) = createTestBeatmap();
onlineSet.Beatmaps.Single().TopTags = null;
onlineSet.RelatedTags = null;
online.Result!.Beatmaps.Single().TopTags = null;
online.Result!.RelatedTags = null;
working.BeatmapSetInfo.Beatmaps.Single().Metadata.UserTags.Clear();
currentOnlineSet = onlineSet;
onlineLookupResult.Value = online;
Beatmap.Value = working;
});
}
@@ -227,72 +174,60 @@ namespace osu.Game.Tests.Visual.SongSelectV2
[Test]
public void TestLoading()
{
AddStep("override request handling", () =>
{
currentOnlineSet = null;
((DummyAPIAccess)API).HandleRequest = request =>
{
switch (request)
{
case GetBeatmapSetRequest set:
Scheduler.AddDelayed(() => set.TriggerSuccess(currentOnlineSet!), 500);
return true;
default:
return false;
}
};
});
AddStep("set beatmap", () =>
{
var (working, onlineSet) = createTestBeatmap();
var (working, online) = createTestBeatmap();
currentOnlineSet = onlineSet;
onlineLookupResult.Value = Screens.SelectV2.SongSelect.BeatmapSetLookupResult.InProgress();
Scheduler.AddDelayed(() => onlineLookupResult.Value = online, 500);
Beatmap.Value = working;
});
AddWaitStep("wait", 5);
AddStep("set beatmap", () =>
{
var (working, onlineSet) = createTestBeatmap();
var (working, online) = createTestBeatmap();
onlineSet.RelatedTags![0].Name = "other/tag";
onlineSet.RelatedTags[1].Name = "another/tag";
onlineSet.RelatedTags[2].Name = "some/tag";
online.Result!.RelatedTags![0].Name = "other/tag";
online.Result!.RelatedTags[1].Name = "another/tag";
online.Result!.RelatedTags[2].Name = "some/tag";
currentOnlineSet = onlineSet;
onlineLookupResult.Value = Screens.SelectV2.SongSelect.BeatmapSetLookupResult.InProgress();
Scheduler.AddDelayed(() => onlineLookupResult.Value = online, 500);
Beatmap.Value = working;
});
AddWaitStep("wait", 5);
AddStep("no user tags", () =>
{
var (working, onlineSet) = createTestBeatmap();
var (working, online) = createTestBeatmap();
onlineSet.Beatmaps.Single().TopTags = null;
onlineSet.RelatedTags = null;
online.Result!.Beatmaps.Single().TopTags = null;
online.Result!.RelatedTags = null;
working.BeatmapSetInfo.Beatmaps.Single().Metadata.UserTags.Clear();
currentOnlineSet = onlineSet;
onlineLookupResult.Value = Screens.SelectV2.SongSelect.BeatmapSetLookupResult.InProgress();
Scheduler.AddDelayed(() => onlineLookupResult.Value = online, 500);
Beatmap.Value = working;
});
AddWaitStep("wait", 5);
AddStep("no user tags", () =>
{
var (working, onlineSet) = createTestBeatmap();
var (working, online) = createTestBeatmap();
onlineSet.Beatmaps.Single().TopTags = null;
onlineSet.RelatedTags = null;
online.Result!.Beatmaps.Single().TopTags = null;
online.Result!.RelatedTags = null;
working.BeatmapSetInfo.Beatmaps.Single().Metadata.UserTags.Clear();
currentOnlineSet = onlineSet;
onlineLookupResult.Value = Screens.SelectV2.SongSelect.BeatmapSetLookupResult.InProgress();
Scheduler.AddDelayed(() => onlineLookupResult.Value = online, 500);
Beatmap.Value = working;
});
AddWaitStep("wait", 5);
}
private (WorkingBeatmap, APIBeatmapSet) createTestBeatmap()
private (WorkingBeatmap, Screens.SelectV2.SongSelect.BeatmapSetLookupResult) createTestBeatmap()
{
var working = CreateWorkingBeatmap(Ruleset.Value);
var onlineSet = new APIBeatmapSet
@@ -346,7 +281,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
working.BeatmapSetInfo.DateSubmitted = DateTimeOffset.Now;
working.BeatmapSetInfo.DateRanked = DateTimeOffset.Now;
return (working, onlineSet);
working.Metadata.UserTags.AddRange(onlineSet.RelatedTags.Select(t => t.Name));
return (working, Screens.SelectV2.SongSelect.BeatmapSetLookupResult.Completed(onlineSet));
}
}
}

View File

@@ -11,6 +11,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
@@ -41,10 +42,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
private BeatmapTitleWedge titleWedge = null!;
private BeatmapTitleWedge.DifficultyDisplay difficultyDisplay => titleWedge.ChildrenOfType<BeatmapTitleWedge.DifficultyDisplay>().Single();
private APIBeatmapSet? currentOnlineSet;
[Cached]
private RealmPopulatingOnlineLookupSource lookupSource = new RealmPopulatingOnlineLookupSource();
[Cached(typeof(IBindable<Screens.SelectV2.SongSelect.BeatmapSetLookupResult?>))]
private Bindable<Screens.SelectV2.SongSelect.BeatmapSetLookupResult?> onlineLookupResult = new Bindable<Screens.SelectV2.SongSelect.BeatmapSetLookupResult?>();
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
@@ -58,7 +57,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddRange(new Drawable[]
{
lookupSource,
new Container
{
RelativeSizeAxes = Axes.Both,
@@ -142,44 +140,18 @@ namespace osu.Game.Tests.Visual.SongSelectV2
[Test]
public void TestOnlineAvailability()
{
AddStep("set up request handler", () =>
{
((DummyAPIAccess)API).HandleRequest = request =>
{
switch (request)
{
case GetBeatmapSetRequest set:
if (set.ID == currentOnlineSet?.OnlineID)
{
set.TriggerSuccess(currentOnlineSet);
return true;
}
AddStep("online beatmapset", () => (Beatmap.Value, onlineLookupResult.Value) = createTestBeatmap());
return false;
default:
return false;
}
};
});
AddStep("online beatmapset", () =>
{
var (working, onlineSet) = createTestBeatmap();
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddUntilStep("play count is 10000", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(0).Text.ToString(), () => Is.EqualTo("10,000"));
AddUntilStep("favourites count is 2345", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().Text.ToString(), () => Is.EqualTo("2,345"));
AddStep("online beatmapset with local diff", () =>
{
var (working, onlineSet) = createTestBeatmap();
var (working, lookupResult) = createTestBeatmap();
working.BeatmapInfo.ResetOnlineInfo();
currentOnlineSet = onlineSet;
Beatmap.Value = working;
onlineLookupResult.Value = lookupResult;
});
AddUntilStep("play count is -", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(0).Text.ToString(), () => Is.EqualTo("-"));
AddUntilStep("favourites count is 2345", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().Text.ToString(), () => Is.EqualTo("2,345"));
@@ -187,8 +159,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
var (working, _) = createTestBeatmap();
currentOnlineSet = null;
Beatmap.Value = working;
onlineLookupResult.Value = Screens.SelectV2.SongSelect.BeatmapSetLookupResult.Completed(null);
});
AddUntilStep("play count is -", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(0).Text.ToString(), () => Is.EqualTo("-"));
AddUntilStep("favourites count is -", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().Text.ToString(), () => Is.EqualTo("-"));
@@ -205,15 +177,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
switch (request)
{
case GetBeatmapSetRequest set:
if (set.ID == currentOnlineSet?.OnlineID)
{
set.TriggerSuccess(currentOnlineSet);
return true;
}
return false;
case PostBeatmapFavouriteRequest favourite:
Task.Run(() =>
{
@@ -228,13 +191,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
};
});
AddStep("online beatmapset", () =>
{
var (working, onlineSet) = createTestBeatmap();
AddStep("online beatmapset", () => (Beatmap.Value, onlineLookupResult.Value) = createTestBeatmap());
currentOnlineSet = onlineSet;
Beatmap.Value = working;
});
AddUntilStep("play count is 10000", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(0).Text.ToString(), () => Is.EqualTo("10,000"));
AddUntilStep("favourites count is 2345", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().Text.ToString(), () => Is.EqualTo("2,345"));
@@ -251,13 +209,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddStep("click favourite button", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().TriggerClick());
AddStep("change to another beatmap", () =>
{
var (working, onlineSet) = createTestBeatmap();
onlineSet.FavouriteCount = 9999;
onlineSet.HasFavourited = true;
working.BeatmapSetInfo.OnlineID = onlineSet.OnlineID = 99999;
var (working, online) = createTestBeatmap();
online.Result!.FavouriteCount = 9999;
online.Result!.HasFavourited = true;
working.BeatmapSetInfo.OnlineID = online.Result!.OnlineID = 99999;
currentOnlineSet = onlineSet;
Beatmap.Value = working;
onlineLookupResult.Value = online;
});
AddStep("allow request to complete", () => resetEvent.Set());
AddUntilStep("favourites count is 9999", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().Text.ToString(), () => Is.EqualTo("9,999"));
@@ -268,15 +226,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
switch (request)
{
case GetBeatmapSetRequest set:
if (set.ID == currentOnlineSet?.OnlineID)
{
set.TriggerSuccess(currentOnlineSet);
return true;
}
return false;
case PostBeatmapFavouriteRequest favourite:
Task.Run(() =>
{
@@ -350,7 +299,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
});
}
private (WorkingBeatmap, APIBeatmapSet) createTestBeatmap()
private (WorkingBeatmap, Screens.SelectV2.SongSelect.BeatmapSetLookupResult) createTestBeatmap()
{
var working = CreateWorkingBeatmap(Ruleset.Value);
var onlineSet = new APIBeatmapSet
@@ -371,7 +320,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
working.BeatmapSetInfo.DateSubmitted = DateTimeOffset.Now;
working.BeatmapSetInfo.DateRanked = DateTimeOffset.Now;
return (working, onlineSet);
return (working, Screens.SelectV2.SongSelect.BeatmapSetLookupResult.Completed(onlineSet));
}
private class TestHitObject : ConvertHitObject;

View File

@@ -75,21 +75,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
new PanelBeatmapSet
{
Item = new CarouselItem(beatmapSet)
Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet))
},
new PanelBeatmapSet
{
Item = new CarouselItem(beatmapSet),
Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet)),
KeyboardSelected = { Value = true }
},
new PanelBeatmapSet
{
Item = new CarouselItem(beatmapSet),
Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet)),
Expanded = { Value = true }
},
new PanelBeatmapSet
{
Item = new CarouselItem(beatmapSet),
Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet)),
KeyboardSelected = { Value = true },
Expanded = { Value = true }
},

View File

@@ -23,7 +23,9 @@ using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
using osuTK.Input;
using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel;
using FooterButtonMods = osu.Game.Screens.SelectV2.FooterButtonMods;
using FooterButtonOptions = osu.Game.Screens.SelectV2.FooterButtonOptions;
using FooterButtonRandom = osu.Game.Screens.SelectV2.FooterButtonRandom;
@@ -302,6 +304,28 @@ namespace osu.Game.Tests.Visual.SongSelectV2
});
}
/// <summary>
/// Last played and rank achieved may have changed, so we want to make sure filtering runs on resume to song select.
/// </summary>
[Test]
public void TestFilteringRunsAfterReturningFromGameplay()
{
AddStep("import actual beatmap", () => Beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()));
LoadSongSelect();
AddUntilStep("wait for filtered", () => SongSelect.ChildrenOfType<BeatmapCarousel>().Single().FilterCount, () => Is.EqualTo(1));
AddStep("enter gameplay", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for player", () => Stack.CurrentScreen is Player);
AddUntilStep("wait for fail", () => ((Player)Stack.CurrentScreen).GameplayState.HasFailed);
AddStep("exit gameplay", () => InputManager.Key(Key.Escape));
AddStep("exit gameplay", () => InputManager.Key(Key.Escape));
AddUntilStep("wait for filtered", () => SongSelect.ChildrenOfType<BeatmapCarousel>().Single().FilterCount, () => Is.EqualTo(2));
}
[Test]
public void TestAutoplayShortcut()
{

View File

@@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("no-collection group present", () =>
{
var group = grouping.GroupItems.Single(g => g.Key.Title == "Not in collection");
return group.Value.Select(i => i.Model).OfType<BeatmapSetInfo>().Single().Equals(beatmapSet);
return group.Value.Select(i => i.Model).OfType<GroupedBeatmapSet>().Single().BeatmapSet.Equals(beatmapSet);
});
AddStep("add beatmap to collection", () =>

View File

@@ -4,11 +4,13 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
@@ -22,6 +24,9 @@ namespace osu.Game.Tests.Visual.UserInterface
private FillFlowContainer spreadOutFlow = null!;
private ModDisplay modDisplay = null!;
[Resolved]
private RulesetStore rulesetStore { get; set; } = null!;
[SetUpSteps]
public void SetUpSteps()
{
@@ -70,9 +75,26 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestShowAllMods()
{
AddStep("create mod icons", () =>
createModIconsForRuleset(0);
createModIconsForRuleset(1);
createModIconsForRuleset(2);
createModIconsForRuleset(3);
AddStep("toggle selected", () =>
{
addRange(Ruleset.Value.CreateInstance().CreateAllMods().Select(m =>
foreach (var icon in this.ChildrenOfType<ModIcon>())
icon.Selected.Toggle();
});
}
private void createModIconsForRuleset(int rulesetId)
{
AddStep($"create mod icons for ruleset {rulesetId}", () =>
{
spreadOutFlow.Clear();
modDisplay.Current.Value = [];
addRange(rulesetStore.GetRuleset(rulesetId)!.CreateInstance().CreateAllMods().Select(m =>
{
if (m is OsuModFlashlight fl)
fl.FollowDelay.Value = 1245;
@@ -89,12 +111,6 @@ namespace osu.Game.Tests.Visual.UserInterface
return m;
}));
});
AddStep("toggle selected", () =>
{
foreach (var icon in this.ChildrenOfType<ModIcon>())
icon.Selected.Toggle();
});
}
[Test]

View File

@@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Tournament.Components;
using osu.Game.Tournament.IPC;
using osu.Game.Tournament.Screens.Gameplay;
@@ -66,6 +67,6 @@ namespace osu.Game.Tournament.Tests.Screens
() => this.ChildrenOfType<TeamScore>().All(score => score.Alpha == (visible ? 1 : 0)));
private void toggleWarmup()
=> AddStep("toggle warmup", () => this.ChildrenOfType<TourneyButton>().First().TriggerClick());
=> AddStep("toggle warmup", () => this.ChildrenOfType<LabelledSwitchButton>().First().ChildrenOfType<SwitchButton>().First().TriggerClick());
}
}

View File

@@ -7,7 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays.Settings;
using osu.Game.Tournament.Components;
using osu.Game.Tournament.IPC;
@@ -24,7 +24,6 @@ namespace osu.Game.Tournament.Screens.Gameplay
private readonly BindableBool warmup = new BindableBool();
public readonly Bindable<TourneyState> State = new Bindable<TourneyState>();
private OsuButton warmupButton = null!;
private MatchIPCInfo ipc = null!;
[Resolved]
@@ -40,6 +39,8 @@ namespace osu.Game.Tournament.Screens.Gameplay
{
this.ipc = ipc;
LabelledSwitchButton chatToggle;
AddRangeInternal(new Drawable[]
{
new TourneyVideo("gameplay")
@@ -95,17 +96,14 @@ namespace osu.Game.Tournament.Screens.Gameplay
{
Children = new Drawable[]
{
warmupButton = new TourneyButton
new LabelledSwitchButton
{
RelativeSizeAxes = Axes.X,
Text = "Toggle warmup",
Action = () => warmup.Toggle()
Label = "Warmup",
Current = warmup,
},
new TourneyButton
chatToggle = new LabelledSwitchButton
{
RelativeSizeAxes = Axes.X,
Text = "Toggle chat",
Action = () => { State.Value = State.Value == TourneyState.Idle ? TourneyState.Playing : TourneyState.Idle; }
Label = "Show chat",
},
new SettingsSlider<int>
{
@@ -123,13 +121,12 @@ namespace osu.Game.Tournament.Screens.Gameplay
}
});
State.BindValueChanged(state => chatToggle.Current.Value = State.Value == TourneyState.Idle, true);
chatToggle.Current.BindValueChanged(v => State.Value = v.NewValue ? TourneyState.Idle : TourneyState.Playing);
LadderInfo.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true);
warmup.BindValueChanged(w =>
{
warmupButton.Alpha = !w.NewValue ? 0.5f : 1;
header.ShowScores = !w.NewValue;
}, true);
warmup.BindValueChanged(w => header.ShowScores = !w.NewValue, true);
}
protected override void LoadComplete()

View File

@@ -13,6 +13,7 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps.Formats;
using osu.Game.Configuration;
using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Extensions;
@@ -36,8 +37,8 @@ namespace osu.Game.Beatmaps
public ProcessBeatmapDelegate? ProcessBeatmap { private get; set; }
public BeatmapImporter(Storage storage, RealmAccess realm)
: base(storage, realm)
public BeatmapImporter(Storage storage, RealmAccess realm, OsuConfigManager? config)
: base(storage, realm, config)
{
}

View File

@@ -17,6 +17,7 @@ using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.IO.Archives;
@@ -59,7 +60,7 @@ namespace osu.Game.Beatmaps
}
public BeatmapManager(Storage storage, RealmAccess realm, IAPIProvider? api, AudioManager audioManager, IResourceStore<byte[]> gameResources, GameHost? host = null,
WorkingBeatmap? defaultBeatmap = null, BeatmapDifficultyCache? difficultyCache = null, bool performOnlineLookups = false)
WorkingBeatmap? defaultBeatmap = null, BeatmapDifficultyCache? difficultyCache = null, bool performOnlineLookups = false, OsuConfigManager? config = null)
: base(storage, realm)
{
if (performOnlineLookups)
@@ -75,7 +76,7 @@ namespace osu.Game.Beatmaps
BeatmapTrackStore = audioManager.GetTrackStore(userResources);
beatmapImporter = CreateBeatmapImporter(storage, realm);
beatmapImporter = CreateBeatmapImporter(storage, realm, config);
beatmapImporter.ProcessBeatmap = (beatmapSet, scope) => ProcessBeatmap?.Invoke(beatmapSet, scope);
beatmapImporter.PostNotification = obj => PostNotification?.Invoke(obj);
@@ -98,7 +99,7 @@ namespace osu.Game.Beatmaps
return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host);
}
protected virtual BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm) => new BeatmapImporter(storage, realm);
protected virtual BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm, OsuConfigManager? config = null) => new BeatmapImporter(storage, realm, config);
/// <summary>
/// Create a new beatmap set, backed by a <see cref="BeatmapSetInfo"/> model,

View File

@@ -59,7 +59,10 @@ namespace osu.Game.Beatmaps
// An interpolating clock is used to ensure precise time values even when the host audio subsystem is not reporting
// high precision times (on windows there's generally only 5-10ms reporting intervals, as an example).
interpolatedTrack = new InterpolatingFramedClock(decoupledTrack);
interpolatedTrack = new InterpolatingFramedClock(decoupledTrack)
{
DriftRecoveryHalfLife = 80,
};
if (applyOffsets)
{

View File

@@ -8,6 +8,7 @@ using osu.Framework.Configuration;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Game.Beatmaps.Drawables.Cards;
@@ -41,6 +42,8 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.Ruleset, string.Empty);
SetDefault(OsuSetting.Skin, SkinInfo.ARGON_SKIN.ToString());
SetDefault(OsuSetting.MenuCookieColor, Colour4.FromHex(@"ff66ba"));
SetDefault(OsuSetting.BeatmapDetailTab, BeatmapDetailTab.Local);
SetDefault(OsuSetting.BeatmapLeaderboardSortMode, LeaderboardSortMode.Score);
SetDefault(OsuSetting.BeatmapDetailModsFilter, false);
@@ -65,6 +68,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.ToolbarClockDisplayMode, ToolbarClockDisplayMode.Full);
SetDefault(OsuSetting.ForceLegacySongSelect, false);
SetDefault(OsuSetting.SongSelectBackgroundBlur, false);
// Online settings
@@ -92,7 +96,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.ExternalLinkWarning, true);
SetDefault(OsuSetting.PreferNoVideo, false);
SetDefault(OsuSetting.BackgroundCategory, "Default");
SetDefault(OsuSetting.ShowOnlineExplicitContent, false);
SetDefault(OsuSetting.NotifyOnUsernameMentioned, true);
@@ -193,10 +197,13 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.IntroSequence, IntroSequence.Triangles);
SetDefault(OsuSetting.MenuBackgroundSource, BackgroundSource.Skin);
SetDefault(OsuSetting.SeasonalBackgroundMode, SeasonalBackgroundMode.Sometimes);
SetDefault(OsuSetting.SeasonalBackgroundMode, SeasonalBackgroundMode.Never);
SetDefault(OsuSetting.UseSeasonalBackgroundsV2, true);
SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full);
SetDefault(OsuSetting.DeleteImportedArchives, true);
SetDefault(OsuSetting.EditorDim, 0.25f, 0f, 0.75f, 0.25f);
SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f, 0f, 1f, 0.25f);
SetDefault(OsuSetting.EditorShowHitMarkers, true);
@@ -405,6 +412,7 @@ namespace osu.Game.Configuration
ChatDisplayHeight,
BeatmapListingCardSize,
ToolbarClockDisplayMode,
ForceLegacySongSelect,
SongSelectBackgroundBlur,
Version,
ShowFirstRunSetup,
@@ -412,6 +420,7 @@ namespace osu.Game.Configuration
Skin,
ScreenshotFormat,
ScreenshotCaptureMenuCursor,
MenuCookieColor,
BeatmapSkins,
BeatmapColours,
BeatmapHitsounds,
@@ -436,10 +445,13 @@ namespace osu.Game.Configuration
MenuBackgroundSource,
GameplayDisableWinKey,
SeasonalBackgroundMode,
UseSeasonalBackgroundsV2, // TODO: add migrations
BackgroundCategory,
EditorWaveformOpacity,
EditorShowHitMarkers,
EditorAutoSeekOnPlacement,
DiscordRichPresence,
DeleteImportedArchives,
ShowOnlineExplicitContent,
LastProcessedMetadataId,

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