136 Commits

Author SHA1 Message Date
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
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
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
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
140 changed files with 2815 additions and 621 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"
}

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"

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

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

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

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

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

@@ -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,7 +197,8 @@ 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);
@@ -405,6 +410,7 @@ namespace osu.Game.Configuration
ChatDisplayHeight,
BeatmapListingCardSize,
ToolbarClockDisplayMode,
ForceLegacySongSelect,
SongSelectBackgroundBlur,
Version,
ShowFirstRunSetup,
@@ -412,6 +418,7 @@ namespace osu.Game.Configuration
Skin,
ScreenshotFormat,
ScreenshotCaptureMenuCursor,
MenuCookieColor,
BeatmapSkins,
BeatmapColours,
BeatmapHitsounds,
@@ -436,6 +443,8 @@ namespace osu.Game.Configuration
MenuBackgroundSource,
GameplayDisableWinKey,
SeasonalBackgroundMode,
UseSeasonalBackgroundsV2, // TODO: add migrations
BackgroundCategory,
EditorWaveformOpacity,
EditorShowHitMarkers,
EditorAutoSeekOnPlacement,

View File

@@ -14,12 +14,6 @@ namespace osu.Game.Configuration
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.AlwaysSeasonalBackground))]
Always,
/// <summary>
/// Seasonal backgrounds are shown only during their corresponding season.
/// </summary>
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.SometimesSeasonalBackground))]
Sometimes,
/// <summary>
/// Seasonal backgrounds are never shown.
/// </summary>

View File

@@ -558,9 +558,15 @@ namespace osu.Game.Database
Logger.Log("Querying for beatmap sets that contain missing submission/rank date...");
// find all ranked beatmap sets with missing date ranked or date submitted that have at least one difficulty ranked as well.
// the reason for checking ranked status of the difficulties is that they can be locally modified or unknown too, and for those the lookup is likely to fail.
// this is because metadata lookups are primarily based on file hash, so they will fail to match if the beatmap does not match the online version
// (which is likely to be the case if the beatmap is locally modified or unknown).
// that said, one difficulty in ranked state is enough for the backpopulation to work.
HashSet<Guid> beatmapSetIds = realmAccess.Run(r => new HashSet<Guid>(
r.All<BeatmapSetInfo>()
.Where(b => b.StatusInt > 0 && (b.DateRanked == null || b.DateSubmitted == null))
.Filter($@"{nameof(BeatmapSetInfo.StatusInt)} > 0 && ({nameof(BeatmapSetInfo.DateRanked)} == null || {nameof(BeatmapSetInfo.DateSubmitted)} == null) "
+ $@"&& ANY {nameof(BeatmapSetInfo.Beatmaps)}.{nameof(BeatmapInfo.StatusInt)} > 0")
.AsEnumerable()
.Select(b => b.ID)));
@@ -591,11 +597,7 @@ namespace osu.Game.Database
{
BeatmapSetInfo beatmapSet = r.Find<BeatmapSetInfo>(id)!;
// we want any ranked representative of the set.
// the reason for checking ranked status of the difficulty is that it can be locally modified,
// at which point the lookup will fail - but there might still be another unmodified difficulty on which it will work.
if (beatmapSet.Beatmaps.FirstOrDefault(b => b.Status >= BeatmapOnlineStatus.Ranked) is not BeatmapInfo beatmap)
return false;
var beatmap = beatmapSet.Beatmaps.First(b => b.Status >= BeatmapOnlineStatus.Ranked);
bool lookupSucceeded = localMetadataSource.TryLookup(beatmap, out var result);
@@ -725,7 +727,7 @@ namespace osu.Game.Database
}
catch (Exception e)
{
Logger.Log(@$"Failed to update ranked/submitted dates for beatmap set {id}: {e}");
Logger.Log(@$"Failed to update user tags for beatmap {id}: {e}");
++failedCount;
}
}

View File

@@ -204,10 +204,11 @@ namespace osu.Game.Database
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
Filename += realm_extension;
#if DEBUG
// TODO: fix
// #if DEBUG
if (!DebugUtils.IsNUnitRunning)
applyFilenameSchemaSuffix(ref Filename);
#endif
// #endif
// `prepareFirstRealmAccess()` triggers the first `getRealmInstance` call, which will implicitly run realm migrations and bring the schema up-to-date.
using (var realm = prepareFirstRealmAccess())

View File

@@ -4,6 +4,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -19,79 +20,121 @@ namespace osu.Game.Graphics.Backgrounds
{
public partial class SeasonalBackgroundLoader : Component
{
public event Action<Exception> OnLoadFailure;
public event Action BackgroundChanged;
/// <summary>
/// Fired when background should be changed due to receiving backgrounds from API
/// or when the user setting is changed (as it might require unloading the seasonal background).
/// Fired when categories have been successfully refreshed from the server.
/// </summary>
public event Action SeasonalBackgroundChanged;
public event Action OnCategoriesRefreshed;
public readonly Bindable<IEnumerable<string>> AvailableCategories = new Bindable<IEnumerable<string>>(new List<string>());
[Resolved]
private IAPIProvider api { get; set; }
private Bindable<SeasonalBackgroundMode> seasonalBackgroundMode;
private Bindable<APISeasonalBackgrounds> seasonalBackgrounds;
private Bindable<bool> useSeasonalBackgrounds;
private Bindable<string> selectedCategory;
private Bindable<APISeasonalBackgrounds> currentBackgrounds;
private int current;
private int currentBackgroundIndex;
private bool shouldShowCustomBackgrounds => useSeasonalBackgrounds.Value;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, SessionStatics sessionStatics)
{
seasonalBackgroundMode = config.GetBindable<SeasonalBackgroundMode>(OsuSetting.SeasonalBackgroundMode);
seasonalBackgroundMode.BindValueChanged(_ => SeasonalBackgroundChanged?.Invoke());
useSeasonalBackgrounds = config.GetBindable<bool>(OsuSetting.UseSeasonalBackgroundsV2);
useSeasonalBackgrounds.BindValueChanged(_ => BackgroundChanged?.Invoke());
seasonalBackgrounds = sessionStatics.GetBindable<APISeasonalBackgrounds>(Static.SeasonalBackgrounds);
seasonalBackgrounds.BindValueChanged(_ =>
{
if (shouldShowSeasonal)
SeasonalBackgroundChanged?.Invoke();
});
selectedCategory = config.GetBindable<string>(OsuSetting.BackgroundCategory);
selectedCategory.BindValueChanged(_ => fetchBackgroundsForSelectedCategory());
fetchSeasonalBackgrounds();
currentBackgrounds = sessionStatics.GetBindable<APISeasonalBackgrounds>(Static.SeasonalBackgrounds);
if (shouldShowCustomBackgrounds)
fetchCategories(true);
}
private void fetchSeasonalBackgrounds()
/// <summary>
/// Public method to trigger a refresh of categories from the UI.
/// </summary>
public void RefreshCategories(bool ignoreSuccess = false)
{
if (seasonalBackgrounds.Value != null)
return;
fetchCategories(ignoreSuccess);
}
private void fetchCategories(bool ignoreSuccess = false)
{
if (!shouldShowCustomBackgrounds) return;
var request = new GetBackgroundCategoriesRequest();
var request = new GetSeasonalBackgroundsRequest();
request.Success += response =>
{
seasonalBackgrounds.Value = response;
current = RNG.Next(0, response.Backgrounds?.Count ?? 0);
var serverCategories = response.Categories ?? Enumerable.Empty<string>();
AvailableCategories.Value = serverCategories.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
if (!AvailableCategories.Value.Any())
{
selectedCategory.Value = "";
return; // we don't have any categories!!!
}
if (!AvailableCategories.Value.Contains(selectedCategory.Value))
selectedCategory.Value = AvailableCategories.Value.Contains("Default")
? "Default"
: AvailableCategories.Value.ElementAt(0);
fetchBackgroundsForSelectedCategory();
if (!ignoreSuccess)
OnCategoriesRefreshed?.Invoke();
};
request.Failure += exception =>
{
AvailableCategories.Value = Array.Empty<string>();
OnLoadFailure?.Invoke(exception);
};
api.PerformAsync(request);
}
public SeasonalBackground LoadNextBackground()
private void fetchBackgroundsForSelectedCategory()
{
if (!shouldShowSeasonal)
if (!shouldShowCustomBackgrounds) return;
string categoryToFetch = selectedCategory.Value == "Default" ? null : selectedCategory.Value;
var request = new GetSeasonalBackgroundsRequest(categoryToFetch);
request.Success += response =>
{
currentBackgrounds.Value = response;
currentBackgroundIndex = RNG.Next(0, response.Backgrounds?.Count ?? 0);
BackgroundChanged?.Invoke();
};
request.Failure += exception =>
{
OnLoadFailure?.Invoke(exception);
};
api.PerformAsync(request);
}
public Background LoadNextBackground()
{
if (!shouldShowCustomBackgrounds || currentBackgrounds.Value?.Backgrounds?.Any() != true)
return null;
var backgrounds = seasonalBackgrounds.Value.Backgrounds;
current = (current + 1) % backgrounds.Count;
string url = backgrounds[current].Url;
var backgrounds = currentBackgrounds.Value.Backgrounds;
currentBackgroundIndex = (currentBackgroundIndex + 1) % backgrounds.Count;
string url = backgrounds[currentBackgroundIndex].Url;
return new SeasonalBackground(url);
}
private bool shouldShowSeasonal
{
get
{
if (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Never)
return false;
if (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Sometimes && !isInSeason)
return false;
return seasonalBackgrounds.Value?.Backgrounds?.Any() == true;
}
}
private bool isInSeason => seasonalBackgrounds.Value != null && DateTimeOffset.Now < seasonalBackgrounds.Value.EndDate;
}
[LongRunningLoad]

View File

@@ -81,27 +81,6 @@ namespace osu.Game.Graphics
public static IconUsage InsaneMania => get(0xe027);
public static IconUsage ExpertMania => get(0xe028);
// mod icons
public static IconUsage ModPerfect => get(0xe049);
public static IconUsage ModAutopilot => get(0xe03a);
public static IconUsage ModAuto => get(0xe03b);
public static IconUsage ModCinema => get(0xe03c);
public static IconUsage ModDoubleTime => get(0xe03d);
public static IconUsage ModEasy => get(0xe03e);
public static IconUsage ModFlashlight => get(0xe03f);
public static IconUsage ModHalftime => get(0xe040);
public static IconUsage ModHardRock => get(0xe041);
public static IconUsage ModHidden => get(0xe042);
public static IconUsage ModNightcore => get(0xe043);
public static IconUsage ModNoFail => get(0xe044);
public static IconUsage ModRelax => get(0xe045);
public static IconUsage ModSpunOut => get(0xe046);
public static IconUsage ModSuddenDeath => get(0xe047);
public static IconUsage ModTarget => get(0xe048);
// Use "Icons/BeatmapDetails/mod-icon" instead
// public static IconUsage ModBg => Get(0xe04a);
#endregion
#region New single-file-based icons
@@ -181,6 +160,88 @@ namespace osu.Game.Graphics
public static IconUsage Tortoise => get(OsuIconMapping.Tortoise);
public static IconUsage Hare => get(OsuIconMapping.Hare);
// mod icons
public static IconUsage ModNoMod => get(OsuIconMapping.ModNoMod);
/*
can be regenerated semi-automatically using osu-web's mod database via
$ jq -r '.[].Mods[].Name' mods.json | sort | uniq | \
sed 's/ //g' | \
awk '{print "public static IconUsage Mod" $0 " => get(OsuIconMapping.Mod" $0 ");"}' | pbcopy
*/
public static IconUsage ModAccuracyChallenge => get(OsuIconMapping.ModAccuracyChallenge);
public static IconUsage ModAdaptiveSpeed => get(OsuIconMapping.ModAdaptiveSpeed);
public static IconUsage ModAlternate => get(OsuIconMapping.ModAlternate);
public static IconUsage ModApproachDifferent => get(OsuIconMapping.ModApproachDifferent);
public static IconUsage ModAutopilot => get(OsuIconMapping.ModAutopilot);
public static IconUsage ModAutoplay => get(OsuIconMapping.ModAutoplay);
public static IconUsage ModBarrelRoll => get(OsuIconMapping.ModBarrelRoll);
public static IconUsage ModBlinds => get(OsuIconMapping.ModBlinds);
public static IconUsage ModBloom => get(OsuIconMapping.ModBloom);
public static IconUsage ModBubbles => get(OsuIconMapping.ModBubbles);
public static IconUsage ModCinema => get(OsuIconMapping.ModCinema);
public static IconUsage ModClassic => get(OsuIconMapping.ModClassic);
public static IconUsage ModConstantSpeed => get(OsuIconMapping.ModConstantSpeed);
public static IconUsage ModCover => get(OsuIconMapping.ModCover);
public static IconUsage ModDaycore => get(OsuIconMapping.ModDaycore);
public static IconUsage ModDeflate => get(OsuIconMapping.ModDeflate);
public static IconUsage ModDepth => get(OsuIconMapping.ModDepth);
public static IconUsage ModDifficultyAdjust => get(OsuIconMapping.ModDifficultyAdjust);
public static IconUsage ModDoubleTime => get(OsuIconMapping.ModDoubleTime);
public static IconUsage ModDualStages => get(OsuIconMapping.ModDualStages);
public static IconUsage ModEasy => get(OsuIconMapping.ModEasy);
public static IconUsage ModEightKeys => get(OsuIconMapping.ModEightKeys);
public static IconUsage ModFadeIn => get(OsuIconMapping.ModFadeIn);
public static IconUsage ModFiveKeys => get(OsuIconMapping.ModFiveKeys);
public static IconUsage ModFlashlight => get(OsuIconMapping.ModFlashlight);
public static IconUsage ModFloatingFruits => get(OsuIconMapping.ModFloatingFruits);
public static IconUsage ModFourKeys => get(OsuIconMapping.ModFourKeys);
public static IconUsage ModFreezeFrame => get(OsuIconMapping.ModFreezeFrame);
public static IconUsage ModGrow => get(OsuIconMapping.ModGrow);
public static IconUsage ModHalfTime => get(OsuIconMapping.ModHalfTime);
public static IconUsage ModHardRock => get(OsuIconMapping.ModHardRock);
public static IconUsage ModHidden => get(OsuIconMapping.ModHidden);
public static IconUsage ModHoldOff => get(OsuIconMapping.ModHoldOff);
public static IconUsage ModInvert => get(OsuIconMapping.ModInvert);
public static IconUsage ModMagnetised => get(OsuIconMapping.ModMagnetised);
public static IconUsage ModMirror => get(OsuIconMapping.ModMirror);
public static IconUsage ModMovingFast => get(OsuIconMapping.ModMovingFast);
public static IconUsage ModMuted => get(OsuIconMapping.ModMuted);
public static IconUsage ModNightcore => get(OsuIconMapping.ModNightcore);
public static IconUsage ModNineKeys => get(OsuIconMapping.ModNineKeys);
public static IconUsage ModNoFail => get(OsuIconMapping.ModNoFail);
public static IconUsage ModNoRelease => get(OsuIconMapping.ModNoRelease);
public static IconUsage ModNoScope => get(OsuIconMapping.ModNoScope);
public static IconUsage ModOneKey => get(OsuIconMapping.ModOneKey);
public static IconUsage ModPerfect => get(OsuIconMapping.ModPerfect);
public static IconUsage ModRandom => get(OsuIconMapping.ModRandom);
public static IconUsage ModRelax => get(OsuIconMapping.ModRelax);
public static IconUsage ModRepel => get(OsuIconMapping.ModRepel);
public static IconUsage ModScoreV2 => get(OsuIconMapping.ModScoreV2);
public static IconUsage ModSevenKeys => get(OsuIconMapping.ModSevenKeys);
public static IconUsage ModSimplifiedRhythm => get(OsuIconMapping.ModSimplifiedRhythm);
public static IconUsage ModSingleTap => get(OsuIconMapping.ModSingleTap);
public static IconUsage ModSixKeys => get(OsuIconMapping.ModSixKeys);
public static IconUsage ModSpinIn => get(OsuIconMapping.ModSpinIn);
public static IconUsage ModSpunOut => get(OsuIconMapping.ModSpunOut);
public static IconUsage ModStrictTracking => get(OsuIconMapping.ModStrictTracking);
public static IconUsage ModSuddenDeath => get(OsuIconMapping.ModSuddenDeath);
public static IconUsage ModSwap => get(OsuIconMapping.ModSwap);
public static IconUsage ModSynesthesia => get(OsuIconMapping.ModSynesthesia);
public static IconUsage ModTargetPractice => get(OsuIconMapping.ModTargetPractice);
public static IconUsage ModTenKeys => get(OsuIconMapping.ModTenKeys);
public static IconUsage ModThreeKeys => get(OsuIconMapping.ModThreeKeys);
public static IconUsage ModTouchDevice => get(OsuIconMapping.ModTouchDevice);
public static IconUsage ModTraceable => get(OsuIconMapping.ModTraceable);
public static IconUsage ModTransform => get(OsuIconMapping.ModTransform);
public static IconUsage ModTwoKeys => get(OsuIconMapping.ModTwoKeys);
public static IconUsage ModWiggle => get(OsuIconMapping.ModWiggle);
public static IconUsage ModWindDown => get(OsuIconMapping.ModWindDown);
public static IconUsage ModWindUp => get(OsuIconMapping.ModWindUp);
private static IconUsage get(OsuIconMapping glyph) => new IconUsage((char)glyph, FONT_NAME);
private enum OsuIconMapping
@@ -400,6 +461,224 @@ namespace osu.Game.Graphics
[Description(@"hare")]
Hare,
// mod icons
[Description(@"Mods/mod-no-mod")]
ModNoMod,
/*
rest can be regenerated semi-automatically using osu-web's mod database via
$ jq -r '.[].Mods[].Name' mods.json | sort | uniq | \
awk '{kebab = $0; gsub(" ", "-", kebab); pascal = $0; gsub(" ", "", pascal); print "[Description(@\"Mods/mod-" tolower(kebab) "\")]\nMod" pascal ",\n" }' | pbcopy
*/
[Description(@"Mods/mod-accuracy-challenge")]
ModAccuracyChallenge,
[Description(@"Mods/mod-adaptive-speed")]
ModAdaptiveSpeed,
[Description(@"Mods/mod-alternate")]
ModAlternate,
[Description(@"Mods/mod-approach-different")]
ModApproachDifferent,
[Description(@"Mods/mod-autopilot")]
ModAutopilot,
[Description(@"Mods/mod-autoplay")]
ModAutoplay,
[Description(@"Mods/mod-barrel-roll")]
ModBarrelRoll,
[Description(@"Mods/mod-blinds")]
ModBlinds,
[Description(@"Mods/mod-bloom")]
ModBloom,
[Description(@"Mods/mod-bubbles")]
ModBubbles,
[Description(@"Mods/mod-cinema")]
ModCinema,
[Description(@"Mods/mod-classic")]
ModClassic,
[Description(@"Mods/mod-constant-speed")]
ModConstantSpeed,
[Description(@"Mods/mod-cover")]
ModCover,
[Description(@"Mods/mod-daycore")]
ModDaycore,
[Description(@"Mods/mod-deflate")]
ModDeflate,
[Description(@"Mods/mod-depth")]
ModDepth,
[Description(@"Mods/mod-difficulty-adjust")]
ModDifficultyAdjust,
[Description(@"Mods/mod-double-time")]
ModDoubleTime,
[Description(@"Mods/mod-dual-stages")]
ModDualStages,
[Description(@"Mods/mod-easy")]
ModEasy,
[Description(@"Mods/mod-eight-keys")]
ModEightKeys,
[Description(@"Mods/mod-fade-in")]
ModFadeIn,
[Description(@"Mods/mod-five-keys")]
ModFiveKeys,
[Description(@"Mods/mod-flashlight")]
ModFlashlight,
[Description(@"Mods/mod-floating-fruits")]
ModFloatingFruits,
[Description(@"Mods/mod-four-keys")]
ModFourKeys,
[Description(@"Mods/mod-freeze-frame")]
ModFreezeFrame,
[Description(@"Mods/mod-grow")]
ModGrow,
[Description(@"Mods/mod-half-time")]
ModHalfTime,
[Description(@"Mods/mod-hard-rock")]
ModHardRock,
[Description(@"Mods/mod-hidden")]
ModHidden,
[Description(@"Mods/mod-hold-off")]
ModHoldOff,
[Description(@"Mods/mod-invert")]
ModInvert,
[Description(@"Mods/mod-magnetised")]
ModMagnetised,
[Description(@"Mods/mod-mirror")]
ModMirror,
[Description(@"Mods/mod-moving-fast")]
ModMovingFast,
[Description(@"Mods/mod-muted")]
ModMuted,
[Description(@"Mods/mod-nightcore")]
ModNightcore,
[Description(@"Mods/mod-nine-keys")]
ModNineKeys,
[Description(@"Mods/mod-no-fail")]
ModNoFail,
[Description(@"Mods/mod-no-release")]
ModNoRelease,
[Description(@"Mods/mod-no-scope")]
ModNoScope,
[Description(@"Mods/mod-one-key")]
ModOneKey,
[Description(@"Mods/mod-perfect")]
ModPerfect,
[Description(@"Mods/mod-random")]
ModRandom,
[Description(@"Mods/mod-relax")]
ModRelax,
[Description(@"Mods/mod-repel")]
ModRepel,
[Description(@"Mods/mod-score-v2")]
ModScoreV2,
[Description(@"Mods/mod-seven-keys")]
ModSevenKeys,
[Description(@"Mods/mod-simplified-rhythm")]
ModSimplifiedRhythm,
[Description(@"Mods/mod-single-tap")]
ModSingleTap,
[Description(@"Mods/mod-six-keys")]
ModSixKeys,
[Description(@"Mods/mod-spin-in")]
ModSpinIn,
[Description(@"Mods/mod-spun-out")]
ModSpunOut,
[Description(@"Mods/mod-strict-tracking")]
ModStrictTracking,
[Description(@"Mods/mod-sudden-death")]
ModSuddenDeath,
[Description(@"Mods/mod-swap")]
ModSwap,
[Description(@"Mods/mod-synesthesia")]
ModSynesthesia,
[Description(@"Mods/mod-target-practice")]
ModTargetPractice,
[Description(@"Mods/mod-ten-keys")]
ModTenKeys,
[Description(@"Mods/mod-three-keys")]
ModThreeKeys,
[Description(@"Mods/mod-touch-device")]
ModTouchDevice,
[Description(@"Mods/mod-traceable")]
ModTraceable,
[Description(@"Mods/mod-transform")]
ModTransform,
[Description(@"Mods/mod-two-keys")]
ModTwoKeys,
[Description(@"Mods/mod-wiggle")]
ModWiggle,
[Description(@"Mods/mod-wind-down")]
ModWindDown,
[Description(@"Mods/mod-wind-up")]
ModWindUp,
}
public class OsuIconStore : ITextureStore, ITexturedGlyphLookupStore

View File

@@ -78,6 +78,20 @@ Your experience will not be perfect, and may even feel subpar compared to games
Please bear with us as we continue to improve the game for you!");
/// <summary>
/// "Welcome to jvnkosu!lazer!"
/// </summary>
public static LocalisableString GreetingNotification => new TranslatableString(getKey(@"greeting_notification"), @"Welcome to jvnkosu!lazer!");
/// <summary>
/// "Failed to load backgrounds!\nCheck your internet connection"
/// </summary>
public static LocalisableString SeasonalBackgroundsFail => new TranslatableString(getKey(@"seasonal_backgrounds_fail"), @"Failed to load backgrounds!\nCheck your internet connection"); // TODO: implement l10n in osu-resources
/// <summary>
/// "Successfully refreshed background categories!"
/// </summary>
public static LocalisableString SeasonalBackgroundsRefreshed => new TranslatableString(getKey(@"seasonal_backgrounds_refreshed"), @"Successfully refreshed background categories!");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@@ -0,0 +1,74 @@
// 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.Localisation;
namespace osu.Game.Localisation.HUD
{
public static class AimErrorMeterStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.HUD.AimErrorMeterStrings";
/// <summary>
/// "Hit marker size"
/// </summary>
public static LocalisableString HitMarkerSize => new TranslatableString(getKey(@"hit_marker_size"), @"Hit marker size");
/// <summary>
/// "Controls the size of the markers displayed after every hit."
/// </summary>
public static LocalisableString HitMarkerSizeDescription => new TranslatableString(getKey(@"hit_marker_size_description"), @"Controls the size of the markers displayed after every hit.");
/// <summary>
/// "Hit marker style"
/// </summary>
public static LocalisableString HitMarkerStyle => new TranslatableString(getKey(@"hit_marker_style"), @"Hit marker style");
/// <summary>
/// "The visual style of the hit markers."
/// </summary>
public static LocalisableString HitMarkerStyleDescription => new TranslatableString(getKey(@"hit_marker_style_description"), @"The visual style of the hit markers.");
/// <summary>
/// "Average position marker size"
/// </summary>
public static LocalisableString AverageMarkerSize => new TranslatableString(getKey(@"average_marker_size"), @"Average position marker size");
/// <summary>
/// "Controls the size of the marker showing average hit position."
/// </summary>
public static LocalisableString AverageMarkerSizeDescription => new TranslatableString(getKey(@"average_marker_size_description"), @"Controls the size of the marker showing average hit position.");
/// <summary>
/// "Average position marker style"
/// </summary>
public static LocalisableString AverageMarkerStyle => new TranslatableString(getKey(@"average_marker_style"), @"Average position marker style");
/// <summary>
/// "The visual style of the average position marker."
/// </summary>
public static LocalisableString AverageMarkerStyleDescription => new TranslatableString(getKey(@"average_marker_style_description"), @"The visual style of the average position marker.");
/// <summary>
/// "Position display style"
/// </summary>
public static LocalisableString PositionDisplayStyle => new TranslatableString(getKey(@"position_style"), @"Position display style");
/// <summary>
/// "Controls whether positions displayed on the meter are absolute (as seen on screen) or normalised (relative to the direction of movement from previous object)."
/// </summary>
public static LocalisableString PositionDisplayStyleDescription => new TranslatableString(getKey(@"position_style_description"), @"Controls whether positions displayed on the meter are absolute (as seen on screen) or normalised (relative to the direction of movement from previous object).");
/// <summary>
/// "Absolute"
/// </summary>
public static LocalisableString Absolute => new TranslatableString(getKey(@"absolute"), @"Absolute");
/// <summary>
/// "Normalised"
/// </summary>
public static LocalisableString Normalised => new TranslatableString(getKey(@"normalised"), @"Normalised");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@@ -64,11 +64,26 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString BackgroundSource => new TranslatableString(getKey(@"background_source"), @"Background source");
/// <summary>
/// "Use custom backgrounds from server"
/// </summary>
public static LocalisableString UseSeasonalBackgrounds => new TranslatableString(getKey(@"use_seasonal_backgrounds"), @"Use custom backgrounds from server");
/// <summary>
/// "Seasonal backgrounds"
/// </summary>
public static LocalisableString SeasonalBackgrounds => new TranslatableString(getKey(@"seasonal_backgrounds"), @"Seasonal backgrounds");
/// <summary>
/// "Background categories"
/// </summary>
public static LocalisableString SeasonalBackgroundsCategories => new TranslatableString(getKey(@"seasonal_backgrounds_categories"), @"Background categories");
/// <summary>
/// "Refresh categories"
/// </summary>
public static LocalisableString SeasonalBackgroundsRefresh => new TranslatableString(getKey(@"seasonal_backgrounds_refresh"), @"Refresh categories");
/// <summary>
/// "Changes to this setting will only apply with an active osu!supporter tag."
/// </summary>
@@ -169,6 +184,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString SelectedMods => new TranslatableString(getKey(@"selected_mods"), @"Selected Mods");
/// <summary>
/// "Use legacy song select (SelectV1)"
/// </summary>
public static LocalisableString ForceLegacySongSelect => new TranslatableString(getKey(@"force_select_v1"), @"Use legacy song select (SelectV1)");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
public class GetBackgroundCategoriesRequest : APIRequest<APIBackgroundCategories>
{
protected override string Target => @"seasonal-backgrounds-categories";
}
}

View File

@@ -8,7 +8,7 @@ namespace osu.Game.Online.API.Requests
public class GetMenuContentRequest : OsuJsonWebRequest<APIMenuContent>
{
public GetMenuContentRequest()
: base(@"https://assets.ppy.sh/menu-content.json")
: base(@"https://osu.jvnko.boats/uploads/menu-content.json") // TODO: backend
{
}
}

View File

@@ -1,12 +1,33 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System.Net;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
public class GetSeasonalBackgroundsRequest : APIRequest<APISeasonalBackgrounds>
{
protected override string Target => @"seasonal-backgrounds";
private readonly string? category;
public GetSeasonalBackgroundsRequest()
{
}
public GetSeasonalBackgroundsRequest(string? category)
{
this.category = category;
}
protected override string Target => getPath();
private string getPath()
{
if (string.IsNullOrEmpty(category))
return @"seasonal-backgrounds";
return $@"seasonal-backgrounds?category={WebUtility.UrlEncode(category)}";
}
}
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using Newtonsoft.Json;
namespace osu.Game.Online.API.Requests.Responses
{
public class APIBackgroundCategories
{
[JsonProperty("categories")]
public List<string>? Categories { get; set; }
}
}

View File

@@ -7,13 +7,13 @@ namespace osu.Game.Online
{
public DevelopmentEndpointConfiguration()
{
WebsiteUrl = APIUrl = @"https://dev.ppy.sh";
APIClientSecret = @"3LP2mhUrV89xxzD1YKNndXHEhWWCRLPNKioZ9ymT";
APIClientID = "5";
SpectatorUrl = $@"{APIUrl}/signalr/spectator";
MultiplayerUrl = $@"{APIUrl}/signalr/multiplayer";
MetadataUrl = $@"{APIUrl}/signalr/metadata";
BeatmapSubmissionServiceUrl = $@"{APIUrl}/beatmap-submission";
WebsiteUrl = APIUrl = @"https://osu.jvnko.boats";
APIClientSecret = @"ijBg9O6aULCYGnvEELYD3IdW7fqrYiFaoMdkzQNA";
APIClientID = "1";
SpectatorUrl = $@"https://osu-spec.jvnko.boats/spectator";
MultiplayerUrl = $@"https://osu-spec.jvnko.boats/multiplayer";
MetadataUrl = $@"https://osu-spec.jvnko.boats/metadata";
BeatmapSubmissionServiceUrl = $@"https://osu-bss.jvnko.boats";
}
}
}

View File

@@ -7,13 +7,13 @@ namespace osu.Game.Online
{
public ProductionEndpointConfiguration()
{
WebsiteUrl = APIUrl = @"https://osu.ppy.sh";
APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk";
APIClientID = "5";
SpectatorUrl = "https://spectator.ppy.sh/spectator";
MultiplayerUrl = "https://spectator.ppy.sh/multiplayer";
MetadataUrl = "https://spectator.ppy.sh/metadata";
BeatmapSubmissionServiceUrl = "https://bss.ppy.sh";
WebsiteUrl = APIUrl = @"https://osu.jvnko.boats";
APIClientSecret = @"ijBg9O6aULCYGnvEELYD3IdW7fqrYiFaoMdkzQNA";
APIClientID = "1";
SpectatorUrl = $@"https://osu-spec.jvnko.boats/spectator";
MultiplayerUrl = $@"https://osu-spec.jvnko.boats/multiplayer";
MetadataUrl = $@"https://osu-spec.jvnko.boats/metadata";
BeatmapSubmissionServiceUrl = $@"https://osu-bss.jvnko.boats";
}
}
}

View File

@@ -11,7 +11,7 @@ namespace osu.Game.Online
{
protected override string GetLookupUrl(string url)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) || !uri.Host.EndsWith(@".ppy.sh", StringComparison.OrdinalIgnoreCase))
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) || !uri.Host.EndsWith(@".jvnko.boats", StringComparison.OrdinalIgnoreCase))
{
Logger.Log($@"Blocking resource lookup from external website: {url}", LoggingTarget.Network, LogLevel.Important);
return string.Empty;

View File

@@ -38,6 +38,7 @@ using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
@@ -92,9 +93,9 @@ namespace osu.Game
{
#if DEBUG
// Different port allows running release and debug builds alongside each other.
public const string IPC_PIPE_NAME = "osu-lazer-debug";
public const string IPC_PIPE_NAME = "jvnkosu-debug";
#else
public const string IPC_PIPE_NAME = "osu-lazer";
public const string IPC_PIPE_NAME = "jvnkosu";
#endif
/// <summary>
@@ -169,6 +170,9 @@ namespace osu.Game
[Cached]
private readonly ScreenshotManager screenshotManager = new ScreenshotManager();
[Cached]
private SeasonalBackgroundLoader seasonalBackgroundLoader;
protected SentryLogger SentryLogger;
public virtual StableStorage GetStorageForStableInstall() => null;
@@ -263,6 +267,8 @@ namespace osu.Game
tabletLogNotifyOnError = true;
}, true);
});
initializeSeasonalBackgrounds();
}
#region IOverlayManager
@@ -927,7 +933,7 @@ namespace osu.Game
protected virtual Loader CreateLoader() => new Loader();
protected virtual UpdateManager CreateUpdateManager() => new UpdateManager();
protected virtual UpdateManager CreateUpdateManager() => new NoActionUpdateManager();
/// <summary>
/// Adjust the globally applied <see cref="DrawSizePreservingFillContainer.TargetDrawSize"/> in every <see cref="ScalingContainer"/>.
@@ -1186,8 +1192,8 @@ namespace osu.Game
Margin = new MarginPadding(5),
}, topMostOverlayContent.Add);
if (!IsDeployedBuild)
loadComponentSingleFile(devBuildBanner = new DevBuildBanner(), ScreenContainer.Add);
// if (!IsDeployedBuild) // we're going to have the "developer build" banner for a while
loadComponentSingleFile(devBuildBanner = new DevBuildBanner(), ScreenContainer.Add);
loadComponentSingleFile(osuLogo, _ =>
{
@@ -1232,6 +1238,8 @@ namespace osu.Game
loadComponentSingleFile(screenshotManager, Add);
loadComponentSingleFile(seasonalBackgroundLoader, Add);
// dependency on notification overlay, dependent by settings overlay
loadComponentSingleFile(CreateUpdateManager(), Add, true);
@@ -1469,6 +1477,40 @@ namespace osu.Game
}
}
private void initializeSeasonalBackgrounds()
{
seasonalBackgroundLoader = new SeasonalBackgroundLoader();
seasonalBackgroundLoader.OnCategoriesRefreshed += handleCategoriesRefreshed;
seasonalBackgroundLoader.OnLoadFailure += handleBackgroundLoadFailure;
}
private void handleCategoriesRefreshed()
{
Schedule(() =>
{
Notifications?.Post(new SimpleNotification
{
Text = ButtonSystemStrings.SeasonalBackgroundsRefreshed,
Icon = FontAwesome.Solid.CheckCircle,
Transient = true
});
});
}
private void handleBackgroundLoadFailure(Exception exception)
{
Schedule(() =>
{
Notifications?.Post(new SimpleErrorNotification
{
Text = ButtonSystemStrings.SeasonalBackgroundsFail,
Icon = FontAwesome.Solid.ExclamationTriangle,
Transient = true
});
});
}
private Task asyncLoadStream;
/// <summary>
@@ -1701,6 +1743,12 @@ namespace osu.Game
{
case IntroScreen intro:
introScreen = intro;
SimpleNotification notification = new SimpleNotification
{
Text = ButtonSystemStrings.GreetingNotification,
Transient = true,
};
Notifications?.Post(notification);
devBuildBanner?.Show();
break;

View File

@@ -76,12 +76,12 @@ namespace osu.Game
public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider
{
#if DEBUG
public const string GAME_NAME = "osu! (development)";
public const string GAME_NAME = "jvnkosu! (development)";
#else
public const string GAME_NAME = "osu!";
public const string GAME_NAME = "jvnkosu!";
#endif
public const string OSU_PROTOCOL = "osu://";
public const string OSU_PROTOCOL = "jnvkosu://";
/// <summary>
/// The filename of the main client database.

View File

@@ -30,9 +30,18 @@ namespace osu.Game.Overlays
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Font = OsuFont.Numeric.With(weight: FontWeight.Bold, size: 12),
Colour = colours.YellowDark,
Text = @"DEVELOPER BUILD",
Font = OsuFont.Torus.With(size: 12),
Colour = colours.GrayF,
Text = $@"jvnkosu! " + game.Version,
Y = -12,
},
new OsuSpriteText
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 15),
Colour = colours.Yellow,
Text = "Experimental version",
},
new Sprite
{

View File

@@ -8,6 +8,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
@@ -18,21 +19,55 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
{
protected override LocalisableString Header => UserInterfaceStrings.MainMenuHeader;
[Resolved]
private SeasonalBackgroundLoader backgroundLoader { get; set; }
private IBindable<APIUser> user;
private SettingsEnumDropdown<BackgroundSource> backgroundSourceDropdown;
private Bindable<bool> useSeasonalBackgrounds;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, IAPIProvider api)
{
user = api.LocalUser.GetBoundCopy();
useSeasonalBackgrounds = config.GetBindable<bool>(OsuSetting.UseSeasonalBackgroundsV2);
var backgroundToggle = new SettingsCheckbox
{
LabelText = UserInterfaceStrings.UseSeasonalBackgrounds,
Current = config.GetBindable<bool>(OsuSetting.UseSeasonalBackgroundsV2),
ClassicDefault = true
};
var categoryDropdown = new SettingsDropdown<string>
{
LabelText = UserInterfaceStrings.SeasonalBackgroundsCategories,
Current = config.GetBindable<string>(OsuSetting.BackgroundCategory)
};
var refreshButton = new SettingsButton
{
Text = UserInterfaceStrings.SeasonalBackgroundsRefresh,
Action = () => backgroundLoader.RefreshCategories()
};
// TODO: the category dropdown disappear if no backgrounds (e.g. when first enabling the setting)
refreshButton.CanBeShown.BindTo(useSeasonalBackgrounds);
categoryDropdown.CanBeShown.BindTo(useSeasonalBackgrounds);
useSeasonalBackgrounds.BindValueChanged(
_ => backgroundLoader.RefreshCategories(true)
);
backgroundLoader.AvailableCategories.BindValueChanged(categories => categoryDropdown.Items = categories.NewValue, true);
Children = new Drawable[]
{
new SettingsCheckbox
{
LabelText = UserInterfaceStrings.ShowMenuTips,
Current = config.GetBindable<bool>(OsuSetting.MenuTips)
Current = config.GetBindable<bool>(OsuSetting.MenuTips),
},
new SettingsCheckbox
{
@@ -56,11 +91,15 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
LabelText = UserInterfaceStrings.BackgroundSource,
Current = config.GetBindable<BackgroundSource>(OsuSetting.MenuBackgroundSource),
},
new SettingsEnumDropdown<SeasonalBackgroundMode>
backgroundToggle,
categoryDropdown,
refreshButton,
new SettingsColour
{
LabelText = UserInterfaceStrings.SeasonalBackgrounds,
Current = config.GetBindable<SeasonalBackgroundMode>(OsuSetting.SeasonalBackgroundMode),
}
LabelText = @"osu! logo colour",
Current = config.GetBindable<Colour4>(OsuSetting.MenuCookieColor),
ClassicDefault = Colour4.FromHex(@"ff66ba"),
},
};
}

View File

@@ -19,6 +19,12 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
{
Children = new Drawable[]
{
new SettingsCheckbox
{
LabelText = UserInterfaceStrings.ForceLegacySongSelect,
Current = config.GetBindable<bool>(OsuSetting.ForceLegacySongSelect),
ClassicDefault = false
},
new SettingsCheckbox
{
LabelText = UserInterfaceStrings.ShowConvertedBeatmaps,

View File

@@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Edit
new CheckDelayedHitsounds(),
new CheckSongFormat(),
new CheckHitsoundsFormat(),
new CheckInconsistentAudio(),
// Files
new CheckZeroByteFiles(),

View File

@@ -7,6 +7,7 @@ using ManagedBass;
using osu.Framework.Audio.Callbacks;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Models;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks
@@ -24,13 +25,22 @@ namespace osu.Game.Rulesets.Edit.Checks
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var beatmapSet = context.CurrentDifficulty.Playable.BeatmapInfo.BeatmapSet;
var audioFile = beatmapSet?.GetFile(context.CurrentDifficulty.Playable.Metadata.AudioFile);
if (beatmapSet == null) yield break;
// Collect all audio files from all difficulties to exclude them from the check, as they aren't hitsounds.
var audioFiles = new HashSet<RealmNamedFileUsage>(ReferenceEqualityComparer.Instance);
foreach (var difficulty in context.AllDifficulties)
{
var audioFile = beatmapSet.GetFile(difficulty.Playable.Metadata.AudioFile);
if (audioFile != null)
audioFiles.Add(audioFile);
}
foreach (var file in beatmapSet.Files)
{
if (audioFile != null && ReferenceEquals(file.File, audioFile.File)) continue;
if (audioFiles.Contains(file)) continue;
using (Stream data = context.CurrentDifficulty.Working.GetStream(file.File.GetStoragePath()))
{

View File

@@ -0,0 +1,53 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckInconsistentAudio : ICheck
{
public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Inconsistent audio files", CheckScope.BeatmapSet);
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateInconsistentAudio(this)
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
if (context.AllDifficulties.Count() <= 1)
yield break;
var referenceBeatmap = context.CurrentDifficulty.Playable;
string referenceAudioFile = referenceBeatmap.Metadata.AudioFile;
foreach (var beatmap in context.OtherDifficulties)
{
string currentAudioFile = beatmap.Playable.Metadata.AudioFile;
if (referenceAudioFile != currentAudioFile)
{
yield return new IssueTemplateInconsistentAudio(this).Create(
string.IsNullOrEmpty(referenceAudioFile) ? "not set" : referenceAudioFile,
beatmap.Playable.BeatmapInfo.DifficultyName,
string.IsNullOrEmpty(currentAudioFile) ? "not set" : currentAudioFile
);
}
}
}
public class IssueTemplateInconsistentAudio : IssueTemplate
{
public IssueTemplateInconsistentAudio(ICheck check)
: base(check, IssueType.Problem, "Inconsistent audio file between this difficulty ({0}) and \"{1}\" ({2}).")
{
}
public Issue Create(string referenceAudio, string otherDifficulty, string otherAudio)
=> new Issue(this, referenceAudio, otherDifficulty, otherAudio);
}
}
}

View File

@@ -6,8 +6,10 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Localisation.HUD;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Scoring;
@@ -24,6 +26,8 @@ namespace osu.Game.Rulesets.Mods
public override LocalisableString Description => "Fail if your accuracy drops too low!";
public override IconUsage? Icon => OsuIcon.ModAccuracyChallenge;
public override ModType Type => ModType.DifficultyIncrease;
public override double ScoreMultiplier => 1.0;

View File

@@ -6,10 +6,12 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
@@ -27,6 +29,8 @@ namespace osu.Game.Rulesets.Mods
public override LocalisableString Description => "Let track speed adapt to you.";
public override IconUsage? Icon => OsuIcon.ModAdaptiveSpeed;
public override ModType Type => ModType.Fun;
public override double ScoreMultiplier => 0.5;

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