1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-26 07:29:53 +08:00

Compare commits

...

21 Commits

  • Increase minimum size of video/storyboard icons globally (#37866)
    They were shockingly small.
    
    | Before | After |
    | :---: | :---: |
    | <img width="1358" height="601" alt="osu! 2026-05-22 at 08 19 27"
    src="https://github.com/user-attachments/assets/4e3ea756-2970-4b7a-b59d-0e684d5bfe49"
    /> | <img width="1358" height="601" alt="osu! 2026-05-22 at 08 17 23"
    src="https://github.com/user-attachments/assets/f0ce6385-6da8-4cb3-837c-0199f0034529"
    /> |
    | <img width="1358" height="601" alt="osu! 2026-05-22 at 08 19 34"
    src="https://github.com/user-attachments/assets/97bb06ed-4563-4fbc-a897-490abb593151"
    /> | <img width="1358" height="601" alt="osu! 2026-05-22 at 08 17 16"
    src="https://github.com/user-attachments/assets/294a14f9-09b5-480a-a383-15d82bde1aa0"
    /> |
  • Handle background offset when encoding/decoding beatmaps (#37841)
    - Part of https://github.com/ppy/osu/issues/14238 (doesn't close,
    because the property doesn't do anything yet).
    - Supersedes / closes https://github.com/ppy/osu/pull/37467.
  • Add ability to add videos in editor (#37857)
    https://github.com/user-attachments/assets/bc329cca-dfa1-4149-9760-121626cf1cb4
    
    ---
    
    - Closes https://github.com/ppy/osu/issues/36326
    
    Currently lacks the ability to specify custom video offset that isn't
    manual .osu editing (defaults to 0) but I'm starting here and listening
    to requirements.
    
    ---------
    
    Co-authored-by: Dean Herbert <pe@ppy.sh>
  • Make experimental audio the new default (#37856)
    Internal offset adjust is based on [survey
    results](https://docs.google.com/forms/d/1bWdwN9LPB4dJsqh2NO4z0HBiMiod1k8-tJN5oca-1Iw/edit)
    results (mean: 17.95, median: 24.5) with slight skewing based on
    cherry-picking results and personal experiences.
    
    For users which have had the setting disabled:
    
    <img width="1380" height="774" alt="osu! 2026-05-21 at 09 19 06"
    src="https://github.com/user-attachments/assets/0526517a-fa0b-485b-ac1b-b61b2fccd2af"
    />
    
    For users which are already using it:
    
    <img width="1380" height="774" alt="osu! 2026-05-21 at 09 20 24"
    src="https://github.com/user-attachments/assets/1bd77b39-5d75-43e8-8fe1-b324064b25fa"
    />
    
    Note the button is intentionally hidden to avoid any confusion (it's
    inverse now, so some users may mistakenly click it). Assume if a user is
    already on the new engine, they are happy with it.
    
    Test migration dialog in startup game flow using:
    
    ```diff
    diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
    index 4bd5ab83a3..091af6d428 100644
    --- a/osu.Game/OsuGame.cs
    +++ b/osu.Game/OsuGame.cs
    @@ -1315,6 +1315,8 @@ protected override void LoadComplete()
             /// </remarks>
             private void applyConfigMigrations()
             {
    +            dialogOverlay.Push(new MigrateNewAudioDialog(true));
    +
                 // arrives as 2020.123.0-lazer
                 string rawVersion = LocalConfig.Get<string>(OsuSetting.Version);
     
    
    ```
    
    ---------
    
    Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
  • Add client-side support for slots in multiplayer rooms (#37741)
    - Part of https://github.com/ppy/osu-server-spectator/issues/405
    
    <img width="1624" height="900" alt="Screenshot 2026-05-13 at 12 40 07"
    src="https://github.com/user-attachments/assets/a7f36d54-4cc6-49c9-8e89-ee0d049bb637"
    />
    <img width="1624" height="900" alt="Screenshot 2026-05-13 at 12 31 40"
    src="https://github.com/user-attachments/assets/0c054dfd-addd-4d00-bbca-119b4c3ec3cb"
    />
    
    Will not work until relevant server-side support is in.
    
    ---
    
    I was in two minds whether to PR this all at once or to PR only
    https://github.com/ppy/osu/commit/693e4ef4b095042f7a171672224e10f9c4f47c1c
    to begin with to unblock server-side implementation. In the end I opted
    for one PR because usage informs the model, so I find everything else
    relevant as part of review of the model design. If there are concerns
    about this making it into a release without server-side support and
    therefore things looking broken I will split the commit out on request.
    
    I put in some effort to add relevant logic in test multiplayer client to
    simulate the server side but I may well have missed something.
    
    ---------
    
    Co-authored-by: Dean Herbert <pe@ppy.sh>
  • Move configuration migrations to OsuGame (#37839)
    As we go forward, migrations are going to likely become more complex,
    requiring access to more components and also at a point in time where
    they are ready.
    
    In the upcoming case, `DialogOverlay` and `Audio` are important. Access
    to `Audio` is a killer as migrations were run before the `GameHost` has
    a chance to initialise it.
    
    ---------
    
    Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
  • Fix popup dialogs not appearing if pushed when OverlayActivationMode is wrong (#37838)
    This change is a prerequisite for making a migration dialog which runs
    on startup to let users know that something hs changed.
    
    A few cases this could happen:
    
    - During start (intro still playing)
    - During gameplay
    
    Basically making dialogs get poofed without the user ever seeing them.
    
    Arguably, we should also change the way dialogs are still poofed when
    activation mode becomes not-`All` (deferring for later response rather
    than dismissing?).
  • Allow rulesets to override PlacementReplacesExisting (#37789)
    Custom rulesets may have different conditions for replacement, and may
    not even use the `IHasColumn` interface.
  • Add legacy storyboard encoder (#37790)
    - Closes https://github.com/ppy/osu/issues/37757
    
    Commit-by-commit reading is recommended. Commits will be split to PRs on
    request but I consider this to be the minimal viable functional
    increment.
    
    ## Done
    
    - This adds a first version of a full storyboard encoder
    (a66dc406f498e35d4e0c8f2a462e946a9a1aeccc). I expect there to be hiccups
    due to weird corners of the `.osb` format; this is only intended to be
    somewhat correct as a start to build upon. Storyboarders are asked to
    file issues as necessary.
    - Due to the fact that storyboard definitions can reside both in the
    `.osu` and the `.osb`, b60698a95c4de1bfeb36fbb159fd5a6028920832 adds the
    required storage to be able to tell which storyboard element lives
    where, so that it can be decoded properly later.
    - In c9d3e04a4135886b5b0943c85f3cc6f4fe99c84c, the storyboard decoder is
    weaved into the beatmap decoder to handle the `.osu` part of the
    storyboard, via the
    `LegacyStoryboardEncoder.Encode{General,Events}ToBeatmap()` methods. For
    `.osb`s, `LegacyStoryboardEncoder.EncodeStandaloneStoryboard()` is
    intended, but for now is not used outside tests.
    - Because of the above, dd1c4e43dc51154cd67860f096712f8b4f229661 removes
    `Beatmap.UnhandledEventLines` as no longer required.
    - 26ac417ed98a8937c42e5f52c4e15ef065a48902 adds tests. They are mostly
    handwritten to ensure basic encode-decode roundtripping. Using existing
    storyboards is difficult, see "Known issues" section as to why.
    - 5cc542366db7caac38eb0729260d884905a2c0d5 fixes a bug in the storyboard
    decoder where the trigger group number was not properly negated on
    decode (see inline comment reference to relevant stable code).
    
    ## Known issues
    
    - Any and all variables in the `[Variables]` section are inlined into
    their usages by `LegacyStoryboardDecoder`, and as such
    `LegacyStoryboardEncoder` will end up inlining them and discarding the
    `[Variables]` section. As far as I can tell stable will also do this.
    - `LegacyStoryboardDecoder` splits all `M` (move) commands into
    `MX`/`MY` commands. Therefore, `LegacyStoryboardEncoder` will write out
    things in the same split way. I did not put in effort to attempt to
    reconcile this, for reasons of part laziness, part not wanting to bloat
    this already-large diff.
    - Ordering of storyboard samples on decode may not match the order on
    decode. I'm crossing fingers this doesn't matter.
  • Add custom editor toolbox icons for taiko, mania, and catch (#37804)
    Requires https://github.com/ppy/osu-resources/pull/424.
    
    Replaces the editor toolbox icons for taiko, mania, and catch rulesets
    with new designs, courtesy of [Adarin](https://osu.ppy.sh/users/118360).
    
    | taiko | catch | mania |
    |--------|--------|--------|
    | <img width="369" height="401" alt="image"
    src="https://github.com/user-attachments/assets/25dc6259-02b4-4a23-9a80-529a366c270e"
    /> | <img width="369" height="401" alt="image"
    src="https://github.com/user-attachments/assets/4bf67bf9-8a45-4075-8d33-f0501cdb08cb"
    /> | <img width="369" height="317" alt="image"
    src="https://github.com/user-attachments/assets/d130a97e-c082-4b01-a88a-d3209c05e33b"
    /> |
    
    ---------
    
    Co-authored-by: Dean Herbert <pe@ppy.sh>
  • Add score multiplier calculator API (#37822)
    - Part of https://github.com/ppy/osu/issues/37818
    - Continued from
    https://github.com/ppy/osu/pull/37355#issuecomment-4449248107
    
    ## Overview
    
    This PR introduces an alternative API, `ScoreMultiplierCalculator`, to
    be used going forward for calculating mod multipliers.
    
    The reason for introducing this new API is that it has been requested
    that:
    - For any two given mods, it should be possible to have the combined mod
    multipliers of them in combination be *different* than the product of
    the individual mods' multipliers in isolation, i.e. $mult( \\{ A, B \\}
    ) \neq mult( \\{ A \\} ) \cdot mult( \\{ B \\} )$.
    - For an individual mod, it should be possible to have the mod
    multipliers depend on a quantity that is *not* the presence of another
    mod or the direct value of a setting on the mod.
    
    This capability is being demonstrated in this PR via the
    `osu.Game.Tests.Rulesets.Scoring.ScoreMultiplierCalculatorTest` test
    fixture.
    
    ## Parity with `Mod.ScoreMultiplier`
    
    This PR contains a `ScoreMultiplierCalculator` implementation for each
    of the built-in four rulesets.
    
    The abstract `osu.Game.Tests.Rulesets.RulesetScoreMultiplierTest` and
    its four derived ruleset-specific test fixtures were written to ensure
    that the new implementations do not diverge from the current state of
    affairs.
    
    `Mod.ScoreMultiplier` is not removed in this diff to keep size low. It
    will be removed as a follow-up.
    
    ## Performance
    
    This PR contains a benchmark comparing the current implementation via
    `Mod.ScoreMultiplier` and the new `ScoreMultiplierCalculator` API.
    Results below.
    
    <details>
    
    | Method | Times | Mods | Mean | Error | StdDev | Gen0 | Allocated |
    |---------------------- |------ |---------------------
    |--------------:|------------:|------------:|--------:|----------:|
    | ViaModScoreMultiplier | 1 | mods (...)tings [27] | 121.171 ns | 1.5284
    ns | 1.4297 ns | 0.0782 | 656 B |
    | ViaCalculator | 1 | mods (...)tings [27] | 248.509 ns | 1.9313 ns |
    1.6127 ns | 0.1364 | 1144 B |
    | ViaModScoreMultiplier | 1 | multiple mods | 128.357 ns | 0.4282 ns |
    0.4006 ns | 0.0782 | 656 B |
    | ViaCalculator | 1 | multiple mods | 252.953 ns | 1.2860 ns | 1.2029 ns
    | 0.1364 | 1144 B |
    | ViaModScoreMultiplier | 1 | no mods | 3.007 ns | 0.0345 ns | 0.0288 ns
    | - | - |
    | ViaCalculator | 1 | no mods | 14.802 ns | 0.0616 ns | 0.0576 ns |
    0.0134 | 112 B |
    | ViaModScoreMultiplier | 1 | single mod | 40.271 ns | 0.1238 ns |
    0.1098 ns | 0.0258 | 216 B |
    | ViaCalculator | 1 | single mod | 113.033 ns | 0.3140 ns | 0.2937 ns |
    0.0842 | 704 B |
    | ViaModScoreMultiplier | 1 | single mod 2 | 3.653 ns | 0.0384 ns |
    0.0359 ns | 0.0038 | 32 B |
    | ViaCalculator | 1 | single mod 2 | 78.172 ns | 0.0680 ns | 0.0603 ns |
    0.0621 | 520 B |
    | ViaModScoreMultiplier | 10 | mods (...)tings [27] | 1,169.609 ns |
    4.3058 ns | 4.0276 ns | 0.7839 | 6560 B |
    | ViaCalculator | 10 | mods (...)tings [27] | 2,575.264 ns | 21.2705 ns
    | 19.8964 ns | 1.3657 | 11440 B |
    | ViaModScoreMultiplier | 10 | multiple mods | 1,171.775 ns | 6.2332 ns
    | 5.2050 ns | 0.7839 | 6560 B |
    | ViaCalculator | 10 | multiple mods | 2,579.593 ns | 22.1010 ns |
    20.6733 ns | 1.3657 | 11440 B |
    | ViaModScoreMultiplier | 10 | no mods | 35.943 ns | 0.1665 ns | 0.1476
    ns | - | - |
    | ViaCalculator | 10 | no mods | 154.980 ns | 0.2381 ns | 0.1988 ns |
    0.1338 | 1120 B |
    | ViaModScoreMultiplier | 10 | single mod | 404.185 ns | 1.3190 ns |
    1.2338 ns | 0.2580 | 2160 B |
    | ViaCalculator | 10 | single mod | 1,167.279 ns | 6.1641 ns | 5.7659 ns
    | 0.8411 | 7040 B |
    | ViaModScoreMultiplier | 10 | single mod 2 | 42.128 ns | 0.2878 ns |
    0.2692 ns | 0.0382 | 320 B |
    | ViaCalculator | 10 | single mod 2 | 775.435 ns | 2.3318 ns | 2.1811 ns
    | 0.6208 | 5200 B |
    | ViaModScoreMultiplier | 100 | mods (...)tings [27] | 11,623.346 ns |
    51.7174 ns | 43.1863 ns | 7.8430 | 65600 B |
    | ViaCalculator | 100 | mods (...)tings [27] | 25,252.987 ns | 44.4352
    ns | 39.3906 ns | 13.6719 | 114400 B |
    | ViaModScoreMultiplier | 100 | multiple mods | 11,928.536 ns | 35.2079
    ns | 32.9334 ns | 7.8430 | 65600 B |
    | ViaCalculator | 100 | multiple mods | 25,399.378 ns | 152.4597 ns |
    127.3108 ns | 13.6719 | 114400 B |
    | ViaModScoreMultiplier | 100 | no mods | 328.158 ns | 0.5827 ns |
    0.5165 ns | - | - |
    | ViaCalculator | 100 | no mods | 1,517.485 ns | 10.2304 ns | 9.5695 ns
    | 1.3390 | 11200 B |
    | ViaModScoreMultiplier | 100 | single mod | 3,986.251 ns | 24.2523 ns |
    21.4991 ns | 2.5787 | 21600 B |
    | ViaCalculator | 100 | single mod | 11,479.514 ns | 23.3738 ns |
    20.7203 ns | 8.4076 | 70400 B |
    | ViaModScoreMultiplier | 100 | single mod 2 | 385.679 ns | 3.5190 ns |
    3.2917 ns | 0.3824 | 3200 B |
    | ViaCalculator | 100 | single mod 2 | 7,658.646 ns | 21.8274 ns |
    19.3494 ns | 6.2103 | 52000 B |
    
    </details>
    
    While the calculator is obviously slower, in my view it is not
    egregiously so. The main overheads both time- and memory-wise are
    collection allocations for the dictionary and the set which I consider
    to be directly caused by the requested additional complexity and as such
    I don't really consider them eliminable.
    
    I have tried and applied some micro-optimisations
    (e2469ce338,
    cb33abec17), albeit with negligible
    effect. I have also tried to key the mods by `Acronym` instead of by
    `Type` and the difference was basically nil.
    
    <details>
    <summary>patch for keying by acronym</summary>
    
    ```diff
    diff --git a/osu.Game/Rulesets/Scoring/ScoreMultiplierCalculator.cs b/osu.Game/Rulesets/Scoring/ScoreMultiplierCalculator.cs
    index 772f9d178b..7f5907cbda 100644
    --- a/osu.Game/Rulesets/Scoring/ScoreMultiplierCalculator.cs
    +++ b/osu.Game/Rulesets/Scoring/ScoreMultiplierCalculator.cs
    @@ -13,26 +13,26 @@ namespace osu.Game.Rulesets.Scoring
         /// </summary>
         public class ScoreMultiplierCalculator
         {
    -        private static readonly List<(Type[] mods, Func<Mod[], double> multiplier)> combination_multipliers = [];
    -        private static readonly Dictionary<Type, Func<Mod, ScoreMultiplierCalculator, double>> single_multipliers_with_context = [];
    -        private static readonly Dictionary<Type, Func<Mod, double>> single_multipliers = [];
    +        private static readonly List<(string[] modAcronyms, Func<Mod[], double> multiplier)> combination_multipliers = [];
    +        private static readonly Dictionary<string, Func<Mod, ScoreMultiplierCalculator, double>> single_multipliers_with_context = [];
    +        private static readonly Dictionary<string, Func<Mod, double>> single_multipliers = [];
     
             /// <summary>
             /// Defines a flat, setting-independent score multiplier for the given <typeparamref name="TMod"/>.
             /// </summary>
             public static void Single<TMod>(double hasMultiplier)
    -            where TMod : Mod
    +            where TMod : Mod, new()
             {
    -            single_multipliers[typeof(TMod)] = _ => hasMultiplier;
    +            single_multipliers[new TMod().Acronym] = _ => hasMultiplier;
             }
     
             /// <summary>
             /// Defines a setting-dependent score multiplier for the given <typeparamref name="TMod"/>.
             /// </summary>
             public static void Single<TMod>(Func<TMod, double> hasMultiplier)
    -            where TMod : Mod
    +            where TMod : Mod, new()
             {
    -            single_multipliers[typeof(TMod)] = mod => hasMultiplier.Invoke((TMod)mod);
    +            single_multipliers[new TMod().Acronym] = mod => hasMultiplier.Invoke((TMod)mod);
             }
     
             /// <summary>
    @@ -40,20 +40,20 @@ public static void Single<TMod>(Func<TMod, double> hasMultiplier)
             /// The multiplier calculation is given additional context to calculate the multiplier via the <typeparamref name="TContext"/> type instance.
             /// </summary>
             public static void Single<TMod, TContext>(Func<TMod, TContext, double> hasMultiplier)
    -            where TMod : Mod
    +            where TMod : Mod, new()
                 where TContext : ScoreMultiplierCalculator
             {
    -            single_multipliers_with_context[typeof(TMod)] = (mod, context) => hasMultiplier.Invoke((TMod)mod, (TContext)context);
    +            single_multipliers_with_context[new TMod().Acronym] = (mod, context) => hasMultiplier.Invoke((TMod)mod, (TContext)context);
             }
     
             /// <summary>
             /// Defines a score multiplier specific to when both <typeparamref name="T1"/> and <typeparamref name="T2"/> mods are present.
             /// </summary>
             public static void Combination<T1, T2>(Func<T1, T2, double> hasMultiplier)
    -            where T1 : Mod
    -            where T2 : Mod
    +            where T1 : Mod, new()
    +            where T2 : Mod, new()
             {
    -            combination_multipliers.Add(([typeof(T1), typeof(T2)], mods => hasMultiplier((T1)mods[0], (T2)mods[1])));
    +            combination_multipliers.Add(([new T1().Acronym, new T2().Acronym], mods => hasMultiplier((T1)mods[0], (T2)mods[1])));
             }
     
             /// <summary>
    @@ -61,7 +61,7 @@ public static void Combination<T1, T2>(Func<T1, T2, double> hasMultiplier)
             /// </summary>
             public double CalculateFor(IEnumerable<Mod> mods)
             {
    -            var allModsByType = mods.ToDictionary(m => m.GetType());
    +            var allModsByType = mods.ToDictionary(m => m.Acronym);
     
                 if (allModsByType.Count == 0)
                     return 1;
    @@ -83,7 +83,7 @@ public double CalculateFor(IEnumerable<Mod> mods)
                     }
                 }
     
    -            foreach (var modType in remainingModTypes)
    +            foreach (string modType in remainingModTypes)
                 {
                     if (single_multipliers.TryGetValue(modType, out var multiplier))
                         result *= multiplier(allModsByType[modType]);
    
    ```
    
    </details>
    
    One particular parallel thread that may warrant follow-up is that
    `Mod.UsesDefaultConfiguration` is disproportionately expensive due to
    calling into regexes via Humanizer internals.
    
    <img width="1517" height="517" alt="Screenshot_2026-05-19_at_10 58 30"
    src="https://github.com/user-attachments/assets/68309a8c-74e7-4f96-8ef9-62868eeca337"
    />
  • Add slider velocity control to toolbox (#37746)
    https://github.com/user-attachments/assets/2a511e0d-51f8-4abf-a3ab-de0992618b6b
    
    This implements a rather opinionated UX that's designed to be a middle
    ground between the previous lazer behaviour of unconditionally
    inheriting the last slider's velocity and just a textbox that you'd need
    to manually fiddle with every time.
    
    As to what that means, precisely:
    
    - By default, the control follows the last slider's velocity (updates on
    seeks as well as changes to existing objects).
    - When the slider control in the toolbox is manually adjusted, the
    control decouples from the last slider's velocity and instead uses the
    last manually-specified value.
    - There is a button that allows the user to couple back to the last
    slider's velocity if they consider to have made a mistake in adjusting
    it.
    - Upon successful placement of a slider, the control reverts to
    following the last slider's velocity.
    
    Of note, this control *only interacts with and affects the next placed
    slider*. It is in no way coupled to any selected objects. This may be
    confusing to users but was an intentional choice to limit complexity
    (what if there are multiple selected objects with multiple velocities?)
    
    For adjusting existing objects you can use the green pieces on the
    timeline, which notably do support changing multiple selected objects at
    once.
    
    ---
    
    - Closes https://github.com/ppy/osu/issues/36844
    - Supersedes / closes https://github.com/ppy/osu/pull/33707
  • Add editor hotkeys for beatmap submit and edit externally (#37782)
    Addresses https://github.com/ppy/osu/discussions/37777.
    
    Note: please don't merge until after the imminent release. Would rather
    not add new features at this point.
  • Stop applying UI scale to ranked play (#37779)
    We already [did this for quick
    play](https://github.com/ppy/osu/pull/36025) but it was never carried
    across. Some of the new UI *could* work with UI scale with more
    consideration, but for now, let's disable it globally to fix cases like
    the results screen which people completely unusable at higher scales.
    
    ---
    
    @ppy/team-client would hope to get this into today's build, if priority
    review could be given to it.
    
    Closes https://github.com/ppy/osu/issues/37407.
    
    - Note that this adds a UI scale slider to all `ScreenTestScenes`. In
    some testing, this seems to work just fine.
    - May be worth reading 6c8dd589f7384d46377620340d300e53aec7eed5 commit
    message for one future concern i have.
149 changed files with 3461 additions and 572 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.513.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.521.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
@@ -0,0 +1,88 @@
// 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 BenchmarkDotNet.Attributes;
using NUnit.Framework;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Benchmarks
{
public class BenchmarkScoreMultiplierCalculator : BenchmarkTest
{
private ScoreMultiplierCalculator calculator = null!;
[Params(1, 10, 100)]
public int Times { get; set; }
public record ModTestCase(string Description, IEnumerable<Mod> Mods)
{
public override string ToString() => Description;
}
public static IEnumerable<ModTestCase> ValuesForMods =>
[
new ModTestCase("no mods", []),
new ModTestCase("single mod", [new OsuModHardRock()]),
new ModTestCase("single mod 2", [new OsuModEasy()]),
new ModTestCase("multiple mods", [new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime()]),
new ModTestCase("mods with adjusted settings", [
new OsuModDoubleTime { SpeedChange = { Value = 2 } },
new OsuModHidden { OnlyFadeApproachCircles = { Value = true } },
new OsuModHardRock()
]),
];
[ParamsSource(nameof(ValuesForMods))]
public ModTestCase Mods { get; set; } = null!;
public override void SetUp()
{
base.SetUp();
calculator = new OsuRuleset().CreateScoreMultiplierCalculator();
}
[Benchmark]
public double ViaModScoreMultiplier() => viaModScoreMultiplier(Times, Mods);
[Test]
public void ViaModScoreMultiplier([Values(100)] int times, [ValueSource(nameof(ValuesForMods))] ModTestCase mods)
=> viaModScoreMultiplier(times, mods);
private double viaModScoreMultiplier(int times, ModTestCase mods)
{
double scoreMultiplier = 1;
for (int i = 0; i < times; ++i)
{
scoreMultiplier = 1;
foreach (var mod in mods.Mods)
scoreMultiplier *= mod.ScoreMultiplier;
}
return scoreMultiplier;
}
[Benchmark]
public double ViaCalculator()
=> viaCalculator(Times, Mods);
[Test]
public void ViaCalculator([Values(100)] int times, [ValueSource(nameof(ValuesForMods))] ModTestCase mods)
=> viaCalculator(times, mods);
private double viaCalculator(int times, ModTestCase mods)
{
double scoreMultiplier = 1;
for (int i = 0; i < times; ++i)
scoreMultiplier = calculator.CalculateFor(mods.Mods);
return scoreMultiplier;
}
}
}
@@ -0,0 +1,41 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Tests.Rulesets;
namespace osu.Game.Rulesets.Catch.Tests
{
public class CatchScoreMultiplierTest : RulesetScoreMultiplierTest
{
public CatchScoreMultiplierTest()
: base(new CatchRuleset())
{
}
[Test]
public void TestFlashlightOnNonDefaultSettings()
=> TestModCombination([new CatchModFlashlight { ComboBasedSize = { Value = false } }]);
[Test]
public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
=> TestModCombination([new CatchModHalfTime { SpeedChange = { Value = speedChange } }]);
[Test]
public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
=> TestModCombination([new CatchModDaycore { SpeedChange = { Value = speedChange } }]);
[Test]
public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
=> TestModCombination([new CatchModDoubleTime { SpeedChange = { Value = speedChange } }]);
[Test]
public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
=> TestModCombination([new CatchModNightcore { SpeedChange = { Value = speedChange } }]);
[Test]
public void TestMultiplicativeCombination()
=> TestModCombination([new CatchModHidden(), new CatchModHardRock()]);
}
}
+2
View File
@@ -169,6 +169,8 @@ namespace osu.Game.Rulesets.Catch
}
}
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new CatchScoreMultiplierCalculator();
public override string Description => "osu!catch";
public override string ShortName => SHORT_NAME;
@@ -2,9 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
@@ -13,11 +13,11 @@ namespace osu.Game.Rulesets.Catch.Edit
public class BananaShowerCompositionTool : CompositionTool
{
public BananaShowerCompositionTool()
: base(nameof(BananaShower))
: base("Banana shower")
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorBananaShower };
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
}
@@ -2,7 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Edit
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorFruit };
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
}
@@ -2,9 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
@@ -13,11 +13,11 @@ namespace osu.Game.Rulesets.Catch.Edit
public class JuiceStreamCompositionTool : CompositionTool
{
public JuiceStreamCompositionTool()
: base(nameof(JuiceStream))
: base("Juice stream")
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorJuiceStream };
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint();
}
@@ -0,0 +1,85 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Scoring
{
public class CatchScoreMultiplierCalculator : ScoreMultiplierCalculator
{
static CatchScoreMultiplierCalculator()
{
#region Difficulty Reduction
Single<CatchModEasy>(hasMultiplier: 0.5);
Single<CatchModNoFail>(hasMultiplier: 0.5);
Single<CatchModHalfTime>(hasMultiplier: halfTime => rateAdjustMultiplier(halfTime.SpeedChange.Value));
Single<CatchModDaycore>(hasMultiplier: daycore => rateAdjustMultiplier(daycore.SpeedChange.Value));
#endregion
#region Difficulty Increase
Single<CatchModHardRock>(hasMultiplier: hardRock => hardRock.UsesDefaultConfiguration ? 1.12 : 1);
// Sudden Death
// Perfect
Single<CatchModDoubleTime>(hasMultiplier: doubleTime => rateAdjustMultiplier(doubleTime.SpeedChange.Value));
Single<CatchModNightcore>(hasMultiplier: nightcore => rateAdjustMultiplier(nightcore.SpeedChange.Value));
Single<CatchModHidden>(hasMultiplier: hidden => hidden.UsesDefaultConfiguration ? 1.06 : 1);
Single<CatchModFlashlight>(hasMultiplier: flashlight => flashlight.UsesDefaultConfiguration ? 1.12 : 1);
// Accuracy Challenge
#endregion
#region Conversion
Single<CatchModDifficultyAdjust>(hasMultiplier: 0.5);
Single<CatchModClassic>(hasMultiplier: 0.96);
// Mirror
#endregion
#region Automation
// Autoplay
// Cinema
Single<CatchModRelax>(hasMultiplier: 0.1);
#endregion
#region Fun
Single<ModWindUp>(hasMultiplier: 0.5);
Single<ModWindDown>(hasMultiplier: 0.5);
// Floating Fruits
// Muted
// No Scope
// Moving Fast
Single<CatchModSynesthesia>(hasMultiplier: 0.8);
#endregion
#region System
// Score V2
#endregion
}
private static double rateAdjustMultiplier(double speedChange)
{
// Round to the nearest multiple of 0.1.
double value = (int)(speedChange * 10) / 10.0;
// Offset back to 0.
value -= 1;
if (speedChange >= 1)
return 1 + value / 5;
else
return 0.6 + value;
}
}
}
@@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Mania.Tests
var decoded = DecodeFromLegacy(beatmaps_resource_store.GetStream($"Resources/Testing/Beatmaps/{name}.osu"), beatmaps_resource_store, name);
var decodedAfterEncode = DecodeFromLegacy(EncodeToLegacy(decoded), beatmaps_resource_store, name);
Sort(decoded.beatmap);
Sort(decodedAfterEncode.beatmap);
Sort(decoded.Beatmap);
Sort(decodedAfterEncode.Beatmap);
CompareBeatmaps(decoded, decodedAfterEncode);
}
@@ -0,0 +1,37 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Rulesets;
namespace osu.Game.Rulesets.Mania.Tests
{
public class ManiaScoreMultiplierTest : RulesetScoreMultiplierTest
{
public ManiaScoreMultiplierTest()
: base(new ManiaRuleset())
{
}
[Test]
public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
=> TestModCombination([new ManiaModHalfTime { SpeedChange = { Value = speedChange } }]);
[Test]
public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
=> TestModCombination([new ManiaModDaycore { SpeedChange = { Value = speedChange } }]);
[Test]
public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
=> TestModCombination([new ManiaModDoubleTime { SpeedChange = { Value = speedChange } }]);
[Test]
public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
=> TestModCombination([new ManiaModNightcore { SpeedChange = { Value = speedChange } }]);
[Test]
public void TestMultiplicativeCombination()
=> TestModCombination([new ManiaModEasy(), new ManiaModKey4()]);
}
}
@@ -9,6 +9,8 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
using osuTK.Input;
@@ -90,5 +92,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
private float getNoteHeight(Column resultPlayfield) =>
resultPlayfield.ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y -
resultPlayfield.ToScreenSpace(Vector2.Zero).Y;
public override bool ReplacesExistingObject(HitObject existing)
=> base.ReplacesExistingObject(existing) && HitObject.Column == ((IHasColumn)existing).Column;
}
}
@@ -2,7 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
@@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.Edit
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorHoldNote };
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint();
}
@@ -2,7 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Edit
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorNote };
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint();
}
+2
View File
@@ -307,6 +307,8 @@ namespace osu.Game.Rulesets.Mania
}
}
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new ManiaScoreMultiplierCalculator();
public override string Description => "osu!mania";
public override string ShortName => SHORT_NAME;
@@ -0,0 +1,99 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Scoring
{
public class ManiaScoreMultiplierCalculator : ScoreMultiplierCalculator
{
static ManiaScoreMultiplierCalculator()
{
#region Difficulty Reduction
Single<ManiaModEasy>(hasMultiplier: 0.5);
Single<ManiaModNoFail>(hasMultiplier: 0.5);
Single<ManiaModHalfTime>(hasMultiplier: halfTime => rateAdjustMultiplier(halfTime.SpeedChange.Value));
Single<ManiaModDaycore>(hasMultiplier: daycore => rateAdjustMultiplier(daycore.SpeedChange.Value));
Single<ManiaModNoRelease>(hasMultiplier: 0.9);
#endregion
#region Difficulty Increase
// Hard Rock
// Sudden Death
// Perfect
// Double Time
// Nightcore
// Fade In
// Hidden
// Cover
// Flashlight
// Accuracy Challenge
#endregion
#region Conversion
// Random
// Dual Stages
// Mirror
Single<ManiaModDifficultyAdjust>(hasMultiplier: 0.5);
Single<ManiaModClassic>(hasMultiplier: 0.96);
// Invert
Single<ManiaModConstantSpeed>(hasMultiplier: 0.9);
Single<ManiaModHoldOff>(hasMultiplier: 0.9);
Single<ManiaModKey1>(hasMultiplier: 0.9);
Single<ManiaModKey2>(hasMultiplier: 0.9);
Single<ManiaModKey3>(hasMultiplier: 0.9);
Single<ManiaModKey4>(hasMultiplier: 0.9);
Single<ManiaModKey5>(hasMultiplier: 0.9);
Single<ManiaModKey6>(hasMultiplier: 0.9);
Single<ManiaModKey7>(hasMultiplier: 0.9);
Single<ManiaModKey8>(hasMultiplier: 0.9);
Single<ManiaModKey9>(hasMultiplier: 0.9);
Single<ManiaModKey10>(hasMultiplier: 0.9);
#endregion
#region Automation
// Autoplay
// Cinema
#endregion
#region Fun
Single<ModWindUp>(hasMultiplier: 0.5);
Single<ModWindDown>(hasMultiplier: 0.5);
// Muted
Single<ModAdaptiveSpeed>(hasMultiplier: 0.5);
#endregion
#region System
// Score V2
#endregion
}
private static double rateAdjustMultiplier(double speedChange)
{
// Round to the nearest multiple of 0.1.
double value = (int)(speedChange * 10) / 10.0;
// Offset back to 0.
value -= 1;
if (speedChange >= 1)
return 1 + value / 5;
else
return 0.6 + value;
}
}
}
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Test]
public void TestTouchInputPlaceHitCircleDirectly()
{
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "HitCircle")));
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "Hit circle")));
AddStep("tap to place circle", () => tap(this.ChildrenOfType<Playfield>().Single()));
AddAssert("circle placed correctly", () =>
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Test]
public void TestTouchInputPlaceCircleAfterTouchingComposeArea()
{
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "HitCircle")));
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "Hit circle")));
AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single()));
AddAssert("circle placed", () => EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate) is HitCircle);
@@ -7,6 +7,9 @@ using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit;
@@ -130,5 +133,74 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("slider has correct velocity", () => slider!.Velocity, () => Is.EqualTo(velocityBefore));
AddAssert("slider has correct duration", () => slider!.Duration, () => Is.EqualTo(durationBefore));
}
[Test]
public void TestVelocityToolbox()
{
ExpandableSlider<double> velocitySlider = null!;
ExpandableButton useLastSliderButton = null!;
AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader()));
AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true);
AddStep("retrieve controls", () =>
{
var toolbox = this.ChildrenOfType<OsuSliderVelocityToolboxGroup>().Single();
velocitySlider = toolbox.ChildrenOfType<ExpandableSlider<double>>().Single();
useLastSliderButton = toolbox.ChildrenOfType<ExpandableButton>().Single();
});
AddAssert("velocity slider at 1x", () => velocitySlider.Current.Value, () => Is.EqualTo(1));
AddStep("expand right toolbox", () => InputManager.MoveMouseTo(this.ChildrenOfType<ExpandingToolboxContainer>().Last()));
AddUntilStep("wait for expand", () => useLastSliderButton.Expanded.Value, () => Is.True);
AddAssert("use last slider button disabled", () => useLastSliderButton.Enabled.Value, () => Is.False);
AddStep("seek to 5000", () => editorClock.Seek(5000));
AddStep("set 2x velocity", () => velocitySlider.Current.Value = 2);
placeSlider();
AddAssert("placed slider has 2x velocity", () => editorBeatmap.HitObjects.OfType<Slider>().Last().SliderVelocityMultiplier, () => Is.EqualTo(2));
AddStep("expand right toolbox", () => InputManager.MoveMouseTo(this.ChildrenOfType<ExpandingToolboxContainer>().Last()));
AddUntilStep("wait for expand", () => useLastSliderButton.Expanded.Value, () => Is.True);
AddAssert("use last slider button enabled", () => useLastSliderButton.Enabled.Value, () => Is.True);
AddStep("seek to 6000", () => editorClock.Seek(6000));
placeSlider();
AddAssert("placed slider has 2x velocity", () => editorBeatmap.HitObjects.OfType<Slider>().Last().SliderVelocityMultiplier, () => Is.EqualTo(2));
AddStep("expand right toolbox", () => InputManager.MoveMouseTo(this.ChildrenOfType<ExpandingToolboxContainer>().Last()));
AddUntilStep("wait for expand", () => useLastSliderButton.Expanded.Value, () => Is.True);
AddAssert("use last slider button enabled", () => useLastSliderButton.Enabled.Value, () => Is.True);
AddStep("seek to 9000", () => editorClock.Seek(9000));
AddStep("set 3x velocity", () => velocitySlider.Current.Value = 3);
placeSlider();
AddAssert("placed slider has 3x velocity", () => editorBeatmap.HitObjects.OfType<Slider>().Last().SliderVelocityMultiplier, () => Is.EqualTo(3));
AddStep("expand right toolbox", () => InputManager.MoveMouseTo(this.ChildrenOfType<ExpandingToolboxContainer>().Last()));
AddUntilStep("wait for expand", () => useLastSliderButton.Expanded.Value, () => Is.True);
AddAssert("use last slider button enabled", () => useLastSliderButton.Enabled.Value, () => Is.True);
AddStep("seek to 10000", () => editorClock.Seek(10000));
AddStep("set 1x velocity", () => velocitySlider.Current.Value = 1);
AddStep("use last slider velocity instead", () => useLastSliderButton.TriggerClick());
placeSlider();
AddAssert("placed slider has 3x velocity", () => editorBeatmap.HitObjects.OfType<Slider>().Last().SliderVelocityMultiplier, () => Is.EqualTo(3));
AddStep("expand right toolbox", () => InputManager.MoveMouseTo(this.ChildrenOfType<ExpandingToolboxContainer>().Last()));
AddUntilStep("wait for expand", () => useLastSliderButton.Expanded.Value, () => Is.True);
AddAssert("use last slider button disabled", () => useLastSliderButton.Enabled.Value, () => Is.False);
AddStep("seek back to 7000", () => editorClock.Seek(7000));
placeSlider();
AddAssert("placed slider has 2x velocity", () => editorBeatmap.HitObjects.OfType<Slider>().ElementAt(2).SliderVelocityMultiplier, () => Is.EqualTo(2));
AddStep("expand right toolbox", () => InputManager.MoveMouseTo(this.ChildrenOfType<ExpandingToolboxContainer>().Last()));
AddUntilStep("wait for expand", () => useLastSliderButton.Expanded.Value, () => Is.True);
AddAssert("use last slider button disabled", () => useLastSliderButton.Enabled.Value, () => Is.False);
void placeSlider()
{
AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3));
AddStep("move mouse to top left", () => InputManager.MoveMouseTo(editor.ChildrenOfType<Playfield>().First().ScreenSpaceDrawQuad.TopLeft + new Vector2(50)));
AddStep("start placement", () => InputManager.Click(MouseButton.Left));
AddStep("move mouse to bottom right", () => InputManager.MoveMouseTo(editor.ChildrenOfType<Playfield>().First().ScreenSpaceDrawQuad.BottomRight - new Vector2(50)));
AddStep("end placement", () => InputManager.Click(MouseButton.Right));
}
}
}
}
@@ -0,0 +1,45 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Rulesets;
namespace osu.Game.Rulesets.Osu.Tests
{
public class OsuScoreMultiplierTest : RulesetScoreMultiplierTest
{
public OsuScoreMultiplierTest()
: base(new OsuRuleset())
{
}
[Test]
public void TestFlashlightOnNonDefaultSettings()
=> TestModCombination([new OsuModFlashlight { ComboBasedSize = { Value = false } }]);
[Test]
public void TestHiddenOnNonDefaultSettings()
=> TestModCombination([new OsuModHidden { OnlyFadeApproachCircles = { Value = true } }]);
[Test]
public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
=> TestModCombination([new OsuModHalfTime { SpeedChange = { Value = speedChange } }]);
[Test]
public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
=> TestModCombination([new OsuModDaycore { SpeedChange = { Value = speedChange } }]);
[Test]
public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
=> TestModCombination([new OsuModDoubleTime { SpeedChange = { Value = speedChange } }]);
[Test]
public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
=> TestModCombination([new OsuModNightcore { SpeedChange = { Value = speedChange } }]);
[Test]
public void TestMultiplicativeCombination()
=> TestModCombination([new OsuModHidden(), new OsuModHardRock()]);
}
}
@@ -792,7 +792,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
AddStep("export beatmap", () =>
{
var beatmapEncoder = new LegacyBeatmapEncoder(playableBeatmap, null);
var beatmapEncoder = new LegacyBeatmapEncoder(playableBeatmap, null, null);
using (var stream = File.Open(Path.Combine(exportLocation, $"{testCaseName}.osu"), FileMode.Create))
{
@@ -54,6 +54,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved]
private EditorClock? editorClock { get; set; }
[Resolved]
private OsuSliderVelocityToolboxGroup? sliderVelocityToolbox { get; set; }
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
@@ -111,9 +114,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
}
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{
var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime);
@@ -129,11 +129,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
case SliderPlacementState.Initial:
BeginPlacement();
double? nearestSliderVelocity = (editorBeatmap
.HitObjects
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocityMultiplier;
HitObject.SliderVelocityMultiplier = nearestSliderVelocity ?? 1;
HitObject.SliderVelocityMultiplier = sliderVelocityToolbox?.SliderVelocity.Value ?? 1;
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
// Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation.
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public partial class FreehandSliderToolboxGroup : EditorToolboxGroup
{
public FreehandSliderToolboxGroup()
: base("slider")
: base("freehand")
{
}
@@ -7,14 +7,13 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Edit
{
public class HitCircleCompositionTool : CompositionTool
{
public HitCircleCompositionTool()
: base(nameof(HitCircle))
: base("Hit circle")
{
}
@@ -75,6 +75,9 @@ namespace osu.Game.Rulesets.Osu.Edit
[Cached(typeof(IDistanceSnapProvider))]
public readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
[Cached]
private readonly OsuSliderVelocityToolboxGroup sliderVelocityToolboxGroup = new OsuSliderVelocityToolboxGroup();
[Cached]
protected readonly OsuGridToolboxGroup OsuGridToolboxGroup = new OsuGridToolboxGroup();
@@ -111,6 +114,7 @@ namespace osu.Game.Rulesets.Osu.Edit
RightToolbox.AddRange(new Drawable[]
{
sliderVelocityToolboxGroup,
OsuGridToolboxGroup,
new TransformToolboxGroup
{
@@ -0,0 +1,213 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuSliderVelocityToolboxGroup : EditorToolboxGroup
{
/// <summary>
/// Whether the last slider's velocity should be used (if available).
/// </summary>
private bool useLastSliderVelocity;
/// <summary>
/// The slider velocity to be used for new object placements.
/// </summary>
public IBindable<double> SliderVelocity => sliderVelocity;
private readonly BindableDouble sliderVelocity = new BindableDouble(1)
{
Precision = 0.01,
MinValue = 0.1,
MaxValue = 10,
};
private ExpandableSlider<double> slider = null!;
private ExpandableButton useLastSliderButton = null!;
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
[Resolved]
private EditorClock editorClock { get; set; } = null!;
private bool syncingBindables;
private double lastClockPosition = double.NegativeInfinity;
private readonly Cached<Slider?> sliderVelocitySourceObject = new Cached<Slider?>();
public OsuSliderVelocityToolboxGroup()
: base("velocity")
{
}
[BackgroundDependencyLoader]
private void load()
{
Spacing = new Vector2(5);
Children = new Drawable[]
{
slider = new ExpandableSlider<double>
{
ExpandedLabelText = "Slider velocity",
Current = new BindableDouble(1)
{
Precision = 0.01,
MinValue = 0.1,
MaxValue = 10,
},
KeyboardStep = 0.1f,
},
useLastSliderButton = new ExpandableButton
{
RelativeSizeAxes = Axes.X,
Action = () =>
{
useLastSliderVelocity = true;
sliderVelocitySourceObject.Invalidate();
},
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
// set unconditionally to true initially.
// if there is no object available to get the slider velocity from, the code in `Update()` will handle that.
useLastSliderVelocity = true;
sliderVelocity.BindValueChanged(_ => updateSliderFromVelocity(), true);
slider.Current.BindValueChanged(_ =>
{
updateVelocityFromSlider();
updateContractedText();
});
updateContractedText();
useLastSliderButton.Expanded.BindValueChanged(_ => sliderVelocitySourceObject.Invalidate());
editorBeatmap.HitObjectAdded += invalidateSliderVelocitySourceObject;
editorBeatmap.HitObjectUpdated += invalidateSliderVelocitySourceObject;
editorBeatmap.HitObjectRemoved += invalidateSliderVelocitySourceObject;
}
private void updateContractedText()
{
slider.ContractedLabelText = LocalisableString.Interpolate($@"SV: {slider.Current.Value.ToLocalisableString("N2")}x");
}
/// <summary>
/// Updates the displayed value of this toolbox's slider from a change to <see cref="SliderVelocity"/>
/// (which is the source-of-truth used for new object placements).
/// This is only relevant when <see cref="useLastSliderVelocity"/> is true,
/// in which case this code is responsible for propagating the velocity from <see cref="sliderVelocitySourceObject"/> to the slider.
/// </summary>
private void updateSliderFromVelocity()
{
if (syncingBindables)
return;
if (!useLastSliderVelocity)
return;
syncingBindables = true;
slider.Current.Value = sliderVelocity.Value;
syncingBindables = false;
}
/// <summary>
/// Updates the value of <see cref="SliderVelocity"/> from a change to the slider's state.
/// This change is assumed to be user-provoked, and therefore <see cref="useLastSliderVelocity"/> is switched unconditionally off
/// as the presumed intent is to override the velocity from <see cref="sliderVelocitySourceObject"/>.
/// </summary>
private void updateVelocityFromSlider()
{
if (syncingBindables)
return;
syncingBindables = true;
useLastSliderVelocity = false;
sliderVelocity.Value = slider.Current.Value;
syncingBindables = false;
sliderVelocitySourceObject.Invalidate();
}
private void invalidateSliderVelocitySourceObject(HitObject _) => sliderVelocitySourceObject.Invalidate();
protected override void Update()
{
base.Update();
if (editorClock.CurrentTime != lastClockPosition)
{
sliderVelocitySourceObject.Invalidate();
lastClockPosition = editorClock.CurrentTime;
}
// Three possible causes of invalidation:
// - The user seeked the clock, which means a different velocity source object needs to be used.
// - Some change to the beatmap was made, which means the previously-used velocity source object may no longer be the most relevant one.
// - The user is interacting with the toolbox in a way that requires a visual state update
// (hovered to expand it, clicked the button to use last slider's velocity, or dragged the manual velocity slider).
// This is a procedural one, because `sliderVelocitySourceObject` will have been pointing at the correct object already,
// but to decrease unnecessary work being done every frame, the invalidation is explicitly re-triggered to update the toolbox state.
if (!sliderVelocitySourceObject.IsValid)
{
var lastSlider = getLastSlider();
sliderVelocitySourceObject.Value = lastSlider;
if (lastSlider == null)
{
useLastSliderButton.Enabled.Value = false;
useLastSliderButton.ExpandedLabelText = "No sliders to get velocity from";
useLastSliderButton.ContractedLabelText = default;
}
else
{
useLastSliderButton.Enabled.Value = useLastSliderButton.Expanded.Value && !useLastSliderVelocity;
useLastSliderButton.ExpandedLabelText = useLastSliderVelocity
? "Using last slider's velocity"
: LocalisableString.Interpolate($@"Use last slider's velocity ({lastSlider.SliderVelocityMultiplier.ToLocalisableString("N2")}x)");
useLastSliderButton.ContractedLabelText = $@"current {lastSlider.SliderVelocityMultiplier.ToLocalisableString("N2")}x";
if (useLastSliderVelocity)
sliderVelocity.Value = lastSlider.SliderVelocityMultiplier;
}
}
}
private Slider? getLastSlider()
{
return editorBeatmap
.HitObjects
.OfType<Slider>()
.LastOrDefault(h => h.StartTime <= editorClock.CurrentTime);
}
protected override void Dispose(bool isDisposing)
{
if (editorBeatmap.IsNotNull())
{
editorBeatmap.HitObjectAdded -= invalidateSliderVelocitySourceObject;
editorBeatmap.HitObjectUpdated -= invalidateSliderVelocitySourceObject;
editorBeatmap.HitObjectRemoved -= invalidateSliderVelocitySourceObject;
}
base.Dispose(isDisposing);
}
}
}
+2
View File
@@ -234,6 +234,8 @@ namespace osu.Game.Rulesets.Osu
}
}
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new OsuScoreMultiplierCalculator();
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetOsu };
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(RulesetInfo, beatmap);
@@ -0,0 +1,100 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
{
public class OsuScoreMultiplierCalculator : ScoreMultiplierCalculator
{
static OsuScoreMultiplierCalculator()
{
#region Difficulty Reduction
Single<OsuModEasy>(hasMultiplier: 0.5);
Single<OsuModNoFail>(hasMultiplier: 0.5);
Single<OsuModHalfTime>(hasMultiplier: halfTime => rateAdjustMultiplier(halfTime.SpeedChange.Value));
Single<OsuModDaycore>(hasMultiplier: daycore => rateAdjustMultiplier(daycore.SpeedChange.Value));
#endregion
#region Difficulty Increase
Single<OsuModHardRock>(hasMultiplier: hardRock => hardRock.UsesDefaultConfiguration ? 1.06 : 1);
// Sudden Death
// Perfect
Single<OsuModDoubleTime>(hasMultiplier: doubleTime => rateAdjustMultiplier(doubleTime.SpeedChange.Value));
Single<OsuModNightcore>(hasMultiplier: nightcore => rateAdjustMultiplier(nightcore.SpeedChange.Value));
Single<OsuModHidden>(hasMultiplier: hidden => hidden.UsesDefaultConfiguration ? 1.06 : 1);
// Traceable
Single<OsuModFlashlight>(hasMultiplier: flashlight => flashlight.UsesDefaultConfiguration ? 1.12 : 1);
Single<OsuModBlinds>(hasMultiplier: blinds => blinds.UsesDefaultConfiguration ? 1.12 : 1);
// Strict Tracking
// Accuracy Challenge
#endregion
#region Conversion
Single<OsuModTargetPractice>(hasMultiplier: 0.1);
Single<OsuModDifficultyAdjust>(hasMultiplier: 0.5);
Single<OsuModClassic>(hasMultiplier: 0.96);
// Random
// Mirror
// Alternate
// Single Tap
#endregion
#region Automation
// Autoplay
// Cinema
Single<OsuModRelax>(hasMultiplier: 0.1);
Single<OsuModAutopilot>(hasMultiplier: 0.1);
Single<OsuModSpunOut>(hasMultiplier: 0.9);
#endregion
#region Fun
// Transform
// Wiggle
// Spin In
// Grow
// Deflate
Single<ModWindUp>(hasMultiplier: 0.5);
Single<ModWindDown>(hasMultiplier: 0.5);
// Barrel Roll
// Approach Different
// Muted
// No Scope
Single<OsuModMagnetised>(hasMultiplier: 0.5);
// Repel
Single<ModAdaptiveSpeed>(hasMultiplier: 0.5);
// Freeze Frame
// Bubbles
Single<OsuModSynesthesia>(hasMultiplier: 0.8);
// Depth
// Bloom
#endregion
}
private static double rateAdjustMultiplier(double speedChange)
{
// Round to the nearest multiple of 0.1.
double value = (int)(speedChange * 10) / 10.0;
// Offset back to 0.
value -= 1;
if (speedChange >= 1)
return 1 + value / 5;
else
return 0.6 + value;
}
}
}
@@ -0,0 +1,41 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Tests.Rulesets;
namespace osu.Game.Rulesets.Taiko.Tests
{
public class TaikoScoreMultiplierTest : RulesetScoreMultiplierTest
{
public TaikoScoreMultiplierTest()
: base(new TaikoRuleset())
{
}
[Test]
public void TestFlashlightOnNonDefaultSettings()
=> TestModCombination([new TaikoModFlashlight { ComboBasedSize = { Value = false } }]);
[Test]
public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
=> TestModCombination([new TaikoModHalfTime { SpeedChange = { Value = speedChange } }]);
[Test]
public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
=> TestModCombination([new TaikoModDaycore { SpeedChange = { Value = speedChange } }]);
[Test]
public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
=> TestModCombination([new TaikoModDoubleTime { SpeedChange = { Value = speedChange } }]);
[Test]
public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
=> TestModCombination([new TaikoModNightcore { SpeedChange = { Value = speedChange } }]);
[Test]
public void TestMultiplicativeCombination()
=> TestModCombination([new TaikoModHidden(), new TaikoModHardRock()]);
}
}
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
currentStoryboard = new Storyboard();
for (int i = 0; i < 10; i++)
currentStoryboard.GetLayer("Foreground").Add(new StoryboardSprite($"test{i}", Anchor.Centre, Vector2.Zero));
currentStoryboard.GetLayer("Foreground").Add(new StoryboardSprite(StoryboardElementSource.Beatmap, $"test{i}", Anchor.Centre, Vector2.Zero));
});
CreateTest();
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
currentStoryboard = new Storyboard();
for (int i = 0; i < 10; i++)
currentStoryboard.GetLayer("Overlay").Add(new StoryboardSprite($"test{i}", Anchor.Centre, Vector2.Zero));
currentStoryboard.GetLayer("Overlay").Add(new StoryboardSprite(StoryboardElementSource.Beatmap, $"test{i}", Anchor.Centre, Vector2.Zero));
});
CreateTest();
@@ -2,22 +2,22 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Edit
{
public class DrumRollCompositionTool : CompositionTool
{
public DrumRollCompositionTool()
: base(nameof(DrumRoll))
: base("Drum roll")
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorDrumRoll };
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint();
}
@@ -2,7 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Edit
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorHit };
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint();
}
@@ -2,7 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Edit
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorSwell };
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint();
}
@@ -0,0 +1,86 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Mods;
namespace osu.Game.Rulesets.Taiko.Scoring
{
public class TaikoScoreMultiplierCalculator : ScoreMultiplierCalculator
{
static TaikoScoreMultiplierCalculator()
{
#region Difficulty Reduction
Single<TaikoModEasy>(hasMultiplier: 0.5);
Single<TaikoModNoFail>(hasMultiplier: 0.5);
Single<TaikoModHalfTime>(hasMultiplier: halfTime => rateAdjustMultiplier(halfTime.SpeedChange.Value));
Single<TaikoModDaycore>(hasMultiplier: daycore => rateAdjustMultiplier(daycore.SpeedChange.Value));
Single<TaikoModSimplifiedRhythm>(hasMultiplier: 0.6);
#endregion
#region Difficulty Increase
Single<TaikoModHardRock>(hasMultiplier: hardRock => hardRock.UsesDefaultConfiguration ? 1.06 : 1);
// Sudden Death
// Perfect
Single<TaikoModDoubleTime>(hasMultiplier: doubleTime => rateAdjustMultiplier(doubleTime.SpeedChange.Value));
Single<TaikoModNightcore>(hasMultiplier: nightcore => rateAdjustMultiplier(nightcore.SpeedChange.Value));
Single<TaikoModHidden>(hasMultiplier: hidden => hidden.UsesDefaultConfiguration ? 1.06 : 1);
Single<TaikoModFlashlight>(hasMultiplier: flashlight => flashlight.UsesDefaultConfiguration ? 1.12 : 1);
// Accuracy Challenge
#endregion
#region Conversion
// Random
Single<TaikoModDifficultyAdjust>(hasMultiplier: 0.5);
Single<TaikoModClassic>(hasMultiplier: 0.96);
// Swap
// Single Tap
Single<TaikoModConstantSpeed>(hasMultiplier: 0.9);
#endregion
#region Automation
// Autoplay
// Cinema
Single<TaikoModRelax>(hasMultiplier: 0.1);
#endregion
#region Fun
Single<ModWindUp>(hasMultiplier: 0.5);
Single<ModWindDown>(hasMultiplier: 0.5);
// Muted
Single<ModAdaptiveSpeed>(hasMultiplier: 0.5);
#endregion
#region System
// Score V2
#endregion
}
private static double rateAdjustMultiplier(double speedChange)
{
// Round to the nearest multiple of 0.1.
double value = (int)(speedChange * 10) / 10.0;
// Offset back to 0.
value -= 1;
if (speedChange >= 1)
return 1 + value / 5;
else
return 0.6 + value;
}
}
}
+2
View File
@@ -188,6 +188,8 @@ namespace osu.Game.Rulesets.Taiko
}
}
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new TaikoScoreMultiplierCalculator();
public override string Description => "osu!taiko";
public override string ShortName => SHORT_NAME;
@@ -41,14 +41,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
private static IEnumerable<string> allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu", StringComparison.Ordinal));
public record BeatmapComponents(IBeatmap Beatmap, LegacySkin Skin, Storyboard Storyboard);
[Test]
public void TestUnsupportedStoryboardEvents()
public void TestStoryboardEvents()
{
const string name = "Resources/storyboard_only_video.osu";
var decoded = DecodeFromLegacy(beatmaps_resource_store.GetStream(name), beatmaps_resource_store, name);
Assert.That(decoded.beatmap.UnhandledEventLines.Count, Is.EqualTo(1));
Assert.That(decoded.beatmap.UnhandledEventLines.Single(), Is.EqualTo("Video,0,\"video.avi\""));
var memoryStream = EncodeToLegacy(decoded);
@@ -63,8 +63,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoded = DecodeFromLegacy(beatmaps_resource_store.GetStream(name), beatmaps_resource_store, name);
var decodedAfterEncode = DecodeFromLegacy(EncodeToLegacy(decoded), beatmaps_resource_store, name);
Sort(decoded.beatmap);
Sort(decodedAfterEncode.beatmap);
Sort(decoded.Beatmap);
Sort(decodedAfterEncode.Beatmap);
CompareBeatmaps(decoded, decodedAfterEncode);
}
@@ -76,10 +76,10 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decodedAfterEncode = DecodeFromLegacy(EncodeToLegacy(decoded), beatmaps_resource_store, name);
// run an extra convert. this is expected to be stable.
decodedAfterEncode.beatmap = convert(decodedAfterEncode.beatmap);
decodedAfterEncode = decodedAfterEncode with { Beatmap = convert(decodedAfterEncode.Beatmap) };
Sort(decoded.beatmap);
Sort(decodedAfterEncode.beatmap);
Sort(decoded.Beatmap);
Sort(decodedAfterEncode.Beatmap);
CompareBeatmaps(decoded, decodedAfterEncode);
}
@@ -91,7 +91,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
// we are testing that the transfer of relevant data to hitobjects (from legacy control points) sticks through encode/decode.
// before the encode step, the legacy information is removed here.
decoded.beatmap.ControlPointInfo = removeLegacyControlPointTypes(decoded.beatmap.ControlPointInfo);
decoded.Beatmap.ControlPointInfo = removeLegacyControlPointTypes(decoded.Beatmap.ControlPointInfo);
var decodedAfterEncode = DecodeFromLegacy(EncodeToLegacy(decoded), beatmaps_resource_store, name);
@@ -120,17 +120,21 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
public static void CompareBeatmaps((IBeatmap beatmap, TestLegacySkin skin) expected, (IBeatmap beatmap, TestLegacySkin skin) actual)
public static void CompareBeatmaps(BeatmapComponents expected, BeatmapComponents actual)
{
// Check all control points that are still considered to be at a global level.
Assert.That(actual.beatmap.ControlPointInfo.TimingPoints.Serialize(), Is.EqualTo(expected.beatmap.ControlPointInfo.TimingPoints.Serialize()));
Assert.That(actual.beatmap.ControlPointInfo.EffectPoints.Serialize(), Is.EqualTo(expected.beatmap.ControlPointInfo.EffectPoints.Serialize()));
Assert.That(actual.Beatmap.ControlPointInfo.TimingPoints.Serialize(), Is.EqualTo(expected.Beatmap.ControlPointInfo.TimingPoints.Serialize()));
Assert.That(actual.Beatmap.ControlPointInfo.EffectPoints.Serialize(), Is.EqualTo(expected.Beatmap.ControlPointInfo.EffectPoints.Serialize()));
// Check all hitobjects.
Assert.That(actual.beatmap.HitObjects.Serialize(), Is.EqualTo(expected.beatmap.HitObjects.Serialize()));
Assert.That(actual.Beatmap.HitObjects.Serialize(), Is.EqualTo(expected.Beatmap.HitObjects.Serialize()));
// Check skin.
ClassicAssert.True(areComboColoursEqual(expected.skin.Configuration, actual.skin.Configuration));
ClassicAssert.True(areComboColoursEqual(expected.Skin.Configuration, actual.Skin.Configuration));
// Do a rough pass on storyboard layers.
foreach (string layer in actual.Storyboard.Layers.Concat(expected.Storyboard.Layers).Select(l => l.Name).Distinct())
Assert.That(actual.Storyboard.GetLayer(layer).Elements.Count, Is.EqualTo(expected.Storyboard.GetLayer(layer).Elements.Count));
}
[Test]
@@ -153,9 +157,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
};
var encoded = EncodeToLegacy((beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty)));
var encoded = EncodeToLegacy(new BeatmapComponents(beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty), new Storyboard()));
var decodedAfterEncode = DecodeFromLegacy(encoded, beatmaps_resource_store, string.Empty);
var decodedSlider = (Slider)decodedAfterEncode.beatmap.HitObjects[0];
var decodedSlider = (Slider)decodedAfterEncode.Beatmap.HitObjects[0];
Assert.That(decodedSlider.Path.ControlPoints.Count, Is.EqualTo(4));
Assert.That(decodedSlider.Path.ControlPoints[0].Type, Is.EqualTo(PathType.BSpline(3)));
Assert.That(decodedSlider.Path.ControlPoints[2].Type, Is.EqualTo(PathType.BSpline(3)));
@@ -183,9 +187,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
};
var encoded = EncodeToLegacy((beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty)));
var encoded = EncodeToLegacy(new BeatmapComponents(beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty), new Storyboard()));
var decodedAfterEncode = DecodeFromLegacy(encoded, beatmaps_resource_store, string.Empty);
var decodedSlider = (Slider)decodedAfterEncode.beatmap.HitObjects[0];
var decodedSlider = (Slider)decodedAfterEncode.Beatmap.HitObjects[0];
Assert.That(decodedSlider.Path.ControlPoints.Count, Is.EqualTo(5));
}
@@ -211,9 +215,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
};
var encoded = EncodeToLegacy((new Beatmap(), beatmapSkin));
var encoded = EncodeToLegacy(new BeatmapComponents(new Beatmap(), beatmapSkin, new Storyboard()));
var decodedAfterEncode = DecodeFromLegacy(encoded, beatmaps_resource_store, string.Empty);
Assert.That(decodedAfterEncode.skin.Configuration.CustomComboColours, Has.Count.EqualTo(8));
Assert.That(decodedAfterEncode.Skin.Configuration.CustomComboColours, Has.Count.EqualTo(8));
}
[Test]
@@ -234,9 +238,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
HitObjects = { originalSlider }
};
var encoded = EncodeToLegacy((beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty)));
var encoded = EncodeToLegacy(new BeatmapComponents(beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty), new Storyboard()));
var decodedAfterEncode = DecodeFromLegacy(encoded, beatmaps_resource_store, string.Empty, version: LegacyBeatmapEncoder.FIRST_LAZER_VERSION);
var decodedSlider = (Slider)decodedAfterEncode.beatmap.HitObjects[0];
var decodedSlider = (Slider)decodedAfterEncode.Beatmap.HitObjects[0];
Assert.That(decodedSlider.Path.ControlPoints.Select(p => p.Position),
Is.EquivalentTo(originalSlider.Path.ControlPoints.Select(p => p.Position)));
}
@@ -254,17 +258,17 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
};
var encoded = EncodeToLegacy((beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty)));
var encoded = EncodeToLegacy(new BeatmapComponents(beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty), new Storyboard()));
var decodedAfterEncode = DecodeFromLegacy(encoded, beatmaps_resource_store, string.Empty);
Assert.That(decodedAfterEncode.beatmap.HitObjects[0].Samples[0].Suffix, Is.Null);
Assert.That(decodedAfterEncode.beatmap.HitObjects[0].Samples[0].UseBeatmapSamples, Is.False);
Assert.That(decodedAfterEncode.Beatmap.HitObjects[0].Samples[0].Suffix, Is.Null);
Assert.That(decodedAfterEncode.Beatmap.HitObjects[0].Samples[0].UseBeatmapSamples, Is.False);
Assert.That(decodedAfterEncode.beatmap.HitObjects[1].Samples[0].Suffix, Is.Null);
Assert.That(decodedAfterEncode.beatmap.HitObjects[1].Samples[0].UseBeatmapSamples, Is.True);
Assert.That(decodedAfterEncode.Beatmap.HitObjects[1].Samples[0].Suffix, Is.Null);
Assert.That(decodedAfterEncode.Beatmap.HitObjects[1].Samples[0].UseBeatmapSamples, Is.True);
Assert.That(decodedAfterEncode.beatmap.HitObjects[2].Samples[0].Suffix, Is.EqualTo("3"));
Assert.That(decodedAfterEncode.beatmap.HitObjects[2].Samples[0].UseBeatmapSamples, Is.True);
Assert.That(decodedAfterEncode.Beatmap.HitObjects[2].Samples[0].Suffix, Is.EqualTo("3"));
Assert.That(decodedAfterEncode.Beatmap.HitObjects[2].Samples[0].UseBeatmapSamples, Is.True);
}
private static bool areComboColoursEqual(IHasComboColours a, IHasComboColours b)
@@ -289,7 +293,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
public static (IBeatmap beatmap, TestLegacySkin skin) DecodeFromLegacy(Stream stream, IResourceStore<byte[]> beatmapsResourceStore, string name, int version = LegacyDecoder<Beatmap>.LATEST_VERSION)
public static BeatmapComponents DecodeFromLegacy(Stream stream, IResourceStore<byte[]> beatmapsResourceStore, string name, int version = LegacyDecoder<Beatmap>.LATEST_VERSION)
{
using (var reader = new LineBufferedReader(stream))
{
@@ -297,7 +301,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
var beatmapSkin = new TestLegacySkin(beatmapsResourceStore, name);
stream.Seek(0, SeekOrigin.Begin);
beatmapSkin.Configuration = new LegacySkinDecoder().Decode(reader);
return (convert(beatmap), beatmapSkin);
stream.Seek(0, SeekOrigin.Begin);
var storyboard = new LegacyStoryboardDecoder().Decode(reader);
return new BeatmapComponents(convert(beatmap), beatmapSkin, storyboard);
}
}
@@ -309,13 +315,13 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
public static MemoryStream EncodeToLegacy((IBeatmap beatmap, ISkin skin) fullBeatmap)
public static MemoryStream EncodeToLegacy(BeatmapComponents fullBeatmap)
{
var (beatmap, beatmapSkin) = fullBeatmap;
var (beatmap, beatmapSkin, storyboard) = fullBeatmap;
var stream = new MemoryStream();
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(beatmap, beatmapSkin).Encode(writer);
new LegacyBeatmapEncoder(beatmap, beatmapSkin, storyboard).Encode(writer);
stream.Position = 0;
@@ -0,0 +1,400 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.IO;
using System.Linq;
using System.Text;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.Storyboards;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Beatmaps.Formats
{
[TestFixture]
public class LegacyStoryboardEncoderTest
{
[Test]
public void TestBackground()
{
var initial = createComponents();
initial.Beatmap.BeatmapInfo.Metadata.BackgroundFile = "bg.jpg";
var encoded = encode(initial);
var decodedAfterEncode = decode(encoded);
Assert.That(decodedAfterEncode.Beatmap.BeatmapInfo.Metadata.BackgroundFile, Is.EqualTo("bg.jpg"));
}
[Test]
public void TestBackgroundOffset()
{
var initial = createComponents();
initial.Beatmap.BeatmapInfo.Metadata.BackgroundFile = "bg_offset.jpg";
initial.Storyboard.BackgroundOffset = new Vector2(0, 45);
var encoded = encode(initial);
var decodedAfterEncode = decode(encoded);
Assert.Multiple(() =>
{
Assert.That(decodedAfterEncode.Beatmap.BeatmapInfo.Metadata.BackgroundFile, Is.EqualTo("bg_offset.jpg"));
Assert.That(decodedAfterEncode.Storyboard.BackgroundOffset, Is.EqualTo(new Vector2(0, 45)));
});
}
[Test]
public void TestVideos()
{
var initial = createComponents();
initial.Storyboard.GetLayer("Video").Add(new StoryboardVideo(StoryboardElementSource.Beatmap, "video1.avi", 0));
initial.Storyboard.GetLayer("Video").Add(new StoryboardVideo(StoryboardElementSource.Shared, "video2.mp4", 1234));
var encoded = encode(initial);
var decodedAfterEncode = decode(encoded);
Assert.Multiple(() =>
{
var videoLayer = decodedAfterEncode.Storyboard.GetLayer("Video");
Assert.That(videoLayer.Elements, Has.Count.EqualTo(2));
Assert.That(videoLayer.Elements[0].Source, Is.EqualTo(StoryboardElementSource.Beatmap));
Assert.That(videoLayer.Elements[0].Path, Is.EqualTo("video1.avi"));
Assert.That(videoLayer.Elements[0].StartTime, Is.EqualTo(0));
Assert.That(videoLayer.Elements[1].Source, Is.EqualTo(StoryboardElementSource.Shared));
Assert.That(videoLayer.Elements[1].Path, Is.EqualTo("video2.mp4"));
Assert.That(videoLayer.Elements[1].StartTime, Is.EqualTo(1234));
});
}
[Test]
public void TestVideoWithCommands()
{
var initial = createComponents();
var video = new StoryboardVideo(StoryboardElementSource.Beatmap, "video1.avi", 0);
video.Commands.AddScale(Easing.None, 0, 0, 0.7f, 0.7f);
initial.Storyboard.GetLayer("Video").Add(video);
var encoded = encode(initial);
var decodedAfterEncode = decode(encoded);
Assert.Multiple(() =>
{
var decodedVideo = (StoryboardVideo)decodedAfterEncode.Storyboard.GetLayer("Video").Elements.Single();
Assert.That(decodedVideo.Source, Is.EqualTo(StoryboardElementSource.Beatmap));
Assert.That(decodedVideo.Path, Is.EqualTo("video1.avi"));
Assert.That(decodedVideo.StartTime, Is.EqualTo(0));
Assert.That(decodedVideo.Commands.Scale, Has.Count.EqualTo(1));
var scaleCommand = (decodedVideo.Commands.Scale.Single());
Assert.That(scaleCommand.Easing, Is.EqualTo(Easing.None));
Assert.That(scaleCommand.StartTime, Is.EqualTo(0));
Assert.That(scaleCommand.EndTime, Is.EqualTo(0));
Assert.That(scaleCommand.StartValue, Is.EqualTo(0.7f));
Assert.That(scaleCommand.EndValue, Is.EqualTo(0.7f));
});
}
[Test]
public void TestSpritesAndAnimations()
{
var initial = createComponents();
initial.Storyboard.GetLayer("Background").Add(new StoryboardSprite(StoryboardElementSource.Beatmap, "1.png", Anchor.TopLeft, new Vector2()));
initial.Storyboard.GetLayer("Fail").Add(new StoryboardSprite(StoryboardElementSource.Shared, "2.png", Anchor.Centre, new Vector2(-3)));
initial.Storyboard.GetLayer("Pass").Add(new StoryboardSprite(StoryboardElementSource.Shared, "3.png", Anchor.BottomRight, new Vector2(30, -30)));
initial.Storyboard.GetLayer("Foreground").Add(new StoryboardAnimation(StoryboardElementSource.Beatmap, "anim1", Anchor.CentreLeft, new Vector2(30), frameCount: 10, frameDelay: 30, AnimationLoopType.LoopForever));
initial.Storyboard.GetLayer("Overlay").Add(new StoryboardAnimation(StoryboardElementSource.Shared, "anim2", Anchor.CentreRight, new Vector2(30), frameCount: 4, frameDelay: 100, AnimationLoopType.LoopOnce));
var encoded = encode(initial);
var decodedAfterEncode = decode(encoded);
var sb = decodedAfterEncode.Storyboard;
Assert.Multiple(() =>
{
var backgroundSprite = (StoryboardSprite)sb.GetLayer("Background").Elements.Single();
Assert.That(backgroundSprite.Source, Is.EqualTo(StoryboardElementSource.Beatmap));
Assert.That(backgroundSprite.Path, Is.EqualTo("1.png"));
Assert.That(backgroundSprite.Origin, Is.EqualTo(Anchor.TopLeft));
Assert.That(backgroundSprite.InitialPosition, Is.EqualTo(new Vector2()));
var failSprite = (StoryboardSprite)sb.GetLayer("Fail").Elements.Single();
Assert.That(failSprite.Source, Is.EqualTo(StoryboardElementSource.Shared));
Assert.That(failSprite.Path, Is.EqualTo("2.png"));
Assert.That(failSprite.Origin, Is.EqualTo(Anchor.Centre));
Assert.That(failSprite.InitialPosition, Is.EqualTo(new Vector2(-3)));
var passSprite = (StoryboardSprite)sb.GetLayer("Pass").Elements.Single();
Assert.That(passSprite.Source, Is.EqualTo(StoryboardElementSource.Shared));
Assert.That(passSprite.Path, Is.EqualTo("3.png"));
Assert.That(passSprite.Origin, Is.EqualTo(Anchor.BottomRight));
Assert.That(passSprite.InitialPosition, Is.EqualTo(new Vector2(30, -30)));
var foregroundAnimation = (StoryboardAnimation)sb.GetLayer("Foreground").Elements.Single();
Assert.That(foregroundAnimation.Source, Is.EqualTo(StoryboardElementSource.Beatmap));
Assert.That(foregroundAnimation.Path, Is.EqualTo("anim1"));
Assert.That(foregroundAnimation.Origin, Is.EqualTo(Anchor.CentreLeft));
Assert.That(foregroundAnimation.InitialPosition, Is.EqualTo(new Vector2(30)));
Assert.That(foregroundAnimation.FrameCount, Is.EqualTo(10));
Assert.That(foregroundAnimation.FrameDelay, Is.EqualTo(30));
Assert.That(foregroundAnimation.LoopType, Is.EqualTo(AnimationLoopType.LoopForever));
var overlayAnimation = (StoryboardAnimation)sb.GetLayer("Overlay").Elements.Single();
Assert.That(overlayAnimation.Source, Is.EqualTo(StoryboardElementSource.Shared));
Assert.That(overlayAnimation.Path, Is.EqualTo("anim2"));
Assert.That(overlayAnimation.Origin, Is.EqualTo(Anchor.CentreRight));
Assert.That(overlayAnimation.InitialPosition, Is.EqualTo(new Vector2(30)));
Assert.That(overlayAnimation.FrameCount, Is.EqualTo(4));
Assert.That(overlayAnimation.FrameDelay, Is.EqualTo(100));
Assert.That(overlayAnimation.LoopType, Is.EqualTo(AnimationLoopType.LoopOnce));
});
}
[Test]
public void TestCommands()
{
var initial = createComponents();
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, "test.jpg", Anchor.Centre, new Vector2(300));
sprite.Commands.AddAlpha(Easing.InBack, 100, 200, 0, 1);
sprite.Commands.AddBlendingParameters(Easing.None, 300, 300, BlendingParameters.Additive, BlendingParameters.Additive);
sprite.Commands.AddColour(Easing.InCubic, 400, 500, Color4.White, Color4.Aquamarine);
sprite.Commands.AddFlipH(Easing.InOutQuad, 600, 600, true, true);
sprite.Commands.AddFlipV(Easing.InOutQuad, 800, 900, true, false);
sprite.Commands.AddRotation(Easing.OutSine, 1000, 1100, 0, 720);
sprite.Commands.AddScale(Easing.OutQuint, 1200, 1300, 1, 4);
sprite.Commands.AddVectorScale(Easing.InCirc, 1400, 1500, new Vector2(4), new Vector2(3, 1));
sprite.Commands.AddX(Easing.InOutQuad, 1600, 1700, 300, 500);
sprite.Commands.AddY(Easing.OutBounce, 1800, 1800, 300, 100);
initial.Storyboard.GetLayer("Background").Add(sprite);
var encoded = encode(initial);
var decodedAfterEncode = decode(encoded);
var decodedSprite = (StoryboardSprite)decodedAfterEncode.Storyboard.GetLayer("Background").Elements.Single();
Assert.Multiple(() =>
{
var alphaCommand = decodedSprite.Commands.Alpha.Single();
Assert.That(alphaCommand.Easing, Is.EqualTo(Easing.InBack));
Assert.That(alphaCommand.StartTime, Is.EqualTo(100));
Assert.That(alphaCommand.EndTime, Is.EqualTo(200));
Assert.That(alphaCommand.StartValue, Is.EqualTo(0));
Assert.That(alphaCommand.EndValue, Is.EqualTo(1));
var blendingCommand = decodedSprite.Commands.BlendingParameters.Single();
Assert.That(blendingCommand.Easing, Is.EqualTo(Easing.None));
Assert.That(blendingCommand.StartTime, Is.EqualTo(300));
Assert.That(blendingCommand.EndTime, Is.EqualTo(300));
Assert.That(blendingCommand.StartValue, Is.EqualTo(BlendingParameters.Additive));
Assert.That(blendingCommand.EndValue, Is.EqualTo(BlendingParameters.Additive));
var colourCommand = decodedSprite.Commands.Colour.Single();
Assert.That(colourCommand.Easing, Is.EqualTo(Easing.InCubic));
Assert.That(colourCommand.StartTime, Is.EqualTo(400));
Assert.That(colourCommand.EndTime, Is.EqualTo(500));
Assert.That(colourCommand.StartValue, Is.EqualTo(Color4.White));
Assert.That(colourCommand.EndValue, Is.EqualTo(Color4.Aquamarine));
var flipHCommand = decodedSprite.Commands.FlipH.Single();
Assert.That(flipHCommand.Easing, Is.EqualTo(Easing.InOutQuad));
Assert.That(flipHCommand.StartTime, Is.EqualTo(600));
Assert.That(flipHCommand.EndTime, Is.EqualTo(600));
Assert.That(flipHCommand.StartValue, Is.EqualTo(true));
Assert.That(flipHCommand.EndValue, Is.EqualTo(true));
var flipVCommand = decodedSprite.Commands.FlipV.Single();
Assert.That(flipVCommand.Easing, Is.EqualTo(Easing.InOutQuad));
Assert.That(flipVCommand.StartTime, Is.EqualTo(800));
Assert.That(flipVCommand.EndTime, Is.EqualTo(900));
Assert.That(flipVCommand.StartValue, Is.EqualTo(true));
Assert.That(flipVCommand.EndValue, Is.EqualTo(false));
var rotationCommand = decodedSprite.Commands.Rotation.Single();
Assert.That(rotationCommand.Easing, Is.EqualTo(Easing.OutSine));
Assert.That(rotationCommand.StartTime, Is.EqualTo(1000));
Assert.That(rotationCommand.EndTime, Is.EqualTo(1100));
Assert.That(rotationCommand.StartValue, Is.EqualTo(0));
Assert.That(rotationCommand.EndValue, Is.EqualTo(720));
var scaleCommand = decodedSprite.Commands.Scale.Single();
Assert.That(scaleCommand.Easing, Is.EqualTo(Easing.OutQuint));
Assert.That(scaleCommand.StartTime, Is.EqualTo(1200));
Assert.That(scaleCommand.EndTime, Is.EqualTo(1300));
Assert.That(scaleCommand.StartValue, Is.EqualTo(1));
Assert.That(scaleCommand.EndValue, Is.EqualTo(4));
var vectorScaleCommand = decodedSprite.Commands.VectorScale.Single();
Assert.That(vectorScaleCommand.Easing, Is.EqualTo(Easing.InCirc));
Assert.That(vectorScaleCommand.StartTime, Is.EqualTo(1400));
Assert.That(vectorScaleCommand.EndTime, Is.EqualTo(1500));
Assert.That(vectorScaleCommand.StartValue, Is.EqualTo(new Vector2(4)));
Assert.That(vectorScaleCommand.EndValue, Is.EqualTo(new Vector2(3, 1)));
var xCommand = decodedSprite.Commands.X.Single();
Assert.That(xCommand.Easing, Is.EqualTo(Easing.InOutQuad));
Assert.That(xCommand.StartTime, Is.EqualTo(1600));
Assert.That(xCommand.EndTime, Is.EqualTo(1700));
Assert.That(xCommand.StartValue, Is.EqualTo(300));
Assert.That(xCommand.EndValue, Is.EqualTo(500));
var yCommand = decodedSprite.Commands.Y.Single();
Assert.That(yCommand.Easing, Is.EqualTo(Easing.OutBounce));
Assert.That(yCommand.StartTime, Is.EqualTo(1800));
Assert.That(yCommand.EndTime, Is.EqualTo(1800));
Assert.That(yCommand.StartValue, Is.EqualTo(300));
Assert.That(yCommand.EndValue, Is.EqualTo(100));
});
}
[Test]
public void TestLoopingGroup()
{
var initial = createComponents();
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, "test.jpg", Anchor.Centre, new Vector2(300));
var loopingGroup = sprite.AddLoopingGroup(1000, 44);
loopingGroup.AddAlpha(Easing.OutQuint, 1000, 1500, 0, 1);
initial.Storyboard.GetLayer("Background").Add(sprite);
var encoded = encode(initial);
var decodedAfterEncode = decode(encoded);
var decodedSprite = (StoryboardSprite)decodedAfterEncode.Storyboard.GetLayer("Background").Elements.Single();
Assert.Multiple(() =>
{
Assert.That(decodedSprite.LoopingGroups, Has.Count.EqualTo(1));
var decodedLoopingGroup = decodedSprite.LoopingGroups.Single();
Assert.That(decodedLoopingGroup.StartTime, Is.EqualTo(1000));
Assert.That(decodedLoopingGroup.TotalIterations, Is.EqualTo(45));
var alphaCommand = decodedLoopingGroup.Alpha.Single();
Assert.That(alphaCommand.Easing, Is.EqualTo(Easing.OutQuint));
Assert.That(alphaCommand.StartTime, Is.EqualTo(1000));
Assert.That(alphaCommand.EndTime, Is.EqualTo(1500));
Assert.That(alphaCommand.StartValue, Is.EqualTo(0));
Assert.That(alphaCommand.EndValue, Is.EqualTo(1));
});
}
[Test]
public void TestTriggerGroup()
{
var initial = createComponents();
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, "test.jpg", Anchor.Centre, new Vector2(300));
var triggerGroup = sprite.AddTriggerGroup("Passing", 0, 100000, 33);
triggerGroup.AddAlpha(Easing.OutQuint, 0, 500, 0, 1);
initial.Storyboard.GetLayer("Background").Add(sprite);
var encoded = encode(initial);
var decodedAfterEncode = decode(encoded);
var decodedSprite = (StoryboardSprite)decodedAfterEncode.Storyboard.GetLayer("Background").Elements.Single();
Assert.Multiple(() =>
{
Assert.That(decodedSprite.TriggerGroups, Has.Count.EqualTo(1));
var decodedTriggerGroup = decodedSprite.TriggerGroups.Single();
Assert.That(decodedTriggerGroup.TriggerName, Is.EqualTo("Passing"));
Assert.That(decodedTriggerGroup.TriggerStartTime, Is.EqualTo(0));
Assert.That(decodedTriggerGroup.TriggerEndTime, Is.EqualTo(100000));
Assert.That(decodedTriggerGroup.GroupNumber, Is.EqualTo(33));
var alphaCommand = decodedTriggerGroup.Alpha.Single();
Assert.That(alphaCommand.Easing, Is.EqualTo(Easing.OutQuint));
Assert.That(alphaCommand.StartTime, Is.EqualTo(0));
Assert.That(alphaCommand.EndTime, Is.EqualTo(500));
Assert.That(alphaCommand.StartValue, Is.EqualTo(0));
Assert.That(alphaCommand.EndValue, Is.EqualTo(1));
});
}
[Test]
public void TestStoryboardSamples()
{
var initial = createComponents();
initial.Storyboard.GetLayer("Pass").Add(new StoryboardSampleInfo(StoryboardElementSource.Beatmap, "pass.wav", 4000, 85));
initial.Storyboard.GetLayer("Fail").Add(new StoryboardSampleInfo(StoryboardElementSource.Shared, "fail.wav", 4000, 100));
var encoded = encode(initial);
var decodedAfterEncode = decode(encoded);
Assert.Multiple(() =>
{
var passingSample = (StoryboardSampleInfo)decodedAfterEncode.Storyboard.GetLayer("Pass").Elements.Single();
Assert.That(passingSample.Source, Is.EqualTo(StoryboardElementSource.Beatmap));
Assert.That(passingSample.Path, Is.EqualTo("pass.wav"));
Assert.That(passingSample.StartTime, Is.EqualTo(4000));
Assert.That(passingSample.Volume, Is.EqualTo(85));
var failingSample = (StoryboardSampleInfo)decodedAfterEncode.Storyboard.GetLayer("Fail").Elements.Single();
Assert.That(failingSample.Source, Is.EqualTo(StoryboardElementSource.Shared));
Assert.That(failingSample.Path, Is.EqualTo("fail.wav"));
Assert.That(failingSample.StartTime, Is.EqualTo(4000));
Assert.That(failingSample.Volume, Is.EqualTo(100));
});
}
private record DecodedBeatmapComponents(IBeatmap Beatmap, Storyboard Storyboard);
private record EncodedBeatmapComponents(MemoryStream Beatmap, MemoryStream Storyboard);
private DecodedBeatmapComponents createComponents()
{
var beatmapInfo = new BeatmapInfo();
var beatmap = new Beatmap
{
BeatmapInfo = beatmapInfo
};
var storyboard = new Storyboard
{
Beatmap = beatmap,
BeatmapInfo = beatmapInfo
};
return new DecodedBeatmapComponents(beatmap, storyboard);
}
private EncodedBeatmapComponents encode(DecodedBeatmapComponents decoded)
{
var beatmapStream = new MemoryStream();
using (var beatmapWriter = new StreamWriter(beatmapStream, Encoding.UTF8, 1024, leaveOpen: true))
new LegacyBeatmapEncoder(decoded.Beatmap, null, decoded.Storyboard).Encode(beatmapWriter);
beatmapStream.Position = 0;
var storyboardStream = new MemoryStream();
using (var storyboardWriter = new StreamWriter(storyboardStream, Encoding.UTF8, 1024, leaveOpen: true))
new LegacyStoryboardEncoder(decoded.Storyboard).EncodeStandaloneStoryboard(storyboardWriter);
storyboardStream.Position = 0;
return new EncodedBeatmapComponents(beatmapStream, storyboardStream);
}
private DecodedBeatmapComponents decode(EncodedBeatmapComponents encoded)
{
using var beatmapReader = new LineBufferedReader(encoded.Beatmap, leaveOpen: true);
var beatmap = new LegacyBeatmapDecoder().Decode(beatmapReader);
encoded.Beatmap.Position = 0;
using var storyboardReader = new LineBufferedReader(encoded.Storyboard, leaveOpen: true);
var storyboard = new LegacyStoryboardDecoder().Decode(beatmapReader, storyboardReader);
encoded.Beatmap.Position = 0;
encoded.Storyboard.Position = 0;
return new DecodedBeatmapComponents(beatmap, storyboard);
}
}
}
@@ -87,7 +87,7 @@ namespace osu.Game.Tests.Editing.Checks
{
var storyboard = new Storyboard();
var layer = storyboard.GetLayer("Video");
layer.Add(new StoryboardVideo("abc123.mp4", 0));
layer.Add(new StoryboardVideo(StoryboardElementSource.Beatmap, "abc123.mp4", 0));
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null!, null!);
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
@@ -260,7 +260,7 @@ namespace osu.Game.Tests.Editing.Checks
if (hasStoryboard)
{
storyboard = new Storyboard();
storyboard.GetLayer("Background").Add(new StoryboardSprite("test.png", Anchor.Centre, Vector2.Zero));
storyboard.GetLayer("Background").Add(new StoryboardSprite(StoryboardElementSource.Beatmap, "test.png", Anchor.Centre, Vector2.Zero));
}
var verifiedCurrentBeatmap = new BeatmapVerifierContext.VerifiedBeatmap(new TestWorkingBeatmap(currentBeatmap, storyboard), currentBeatmap);
@@ -80,7 +80,7 @@ namespace osu.Game.Tests.Editing.Checks
{
var storyboard = new Storyboard();
var video = new StoryboardVideo("abc123.mp4", 0);
var video = new StoryboardVideo(StoryboardElementSource.Beatmap, "abc123.mp4", 0);
storyboard.GetLayer("Video").Add(video);
@@ -98,7 +98,7 @@ namespace osu.Game.Tests.Editing.Checks
{
var storyboard = new Storyboard();
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, "unknown", Anchor.TopLeft, Vector2.Zero);
storyboard.GetLayer("Background").Add(sprite);
@@ -77,7 +77,7 @@ namespace osu.Game.Tests.Editing.Checks
{
var storyboard = new Storyboard();
var layer = storyboard.GetLayer("Video");
layer.Add(new StoryboardVideo("abc123.mp4", 0));
layer.Add(new StoryboardVideo(StoryboardElementSource.Beatmap, "abc123.mp4", 0));
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null!, null!);
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
@@ -143,7 +143,7 @@ namespace osu.Game.Tests.Editing.Checks
};
var storyboard = new Storyboard();
storyboard.GetLayer("Video").Add(new StoryboardVideo(path, startTime));
storyboard.GetLayer("Video").Add(new StoryboardVideo(StoryboardElementSource.Beatmap, path, startTime));
var working = new TestWorkingBeatmap(beatmap, storyboard);
return new BeatmapVerifierContext.VerifiedBeatmap(working, beatmap);
@@ -362,7 +362,7 @@ namespace osu.Game.Tests.Editing
using (var encoded = new MemoryStream())
{
using (var sw = new StreamWriter(encoded))
new LegacyBeatmapEncoder(beatmap, null).Encode(sw);
new LegacyBeatmapEncoder(beatmap, null, null).Encode(sw);
return encoded.ToArray();
}
@@ -79,7 +79,7 @@ namespace osu.Game.Tests.Gameplay
{
Child = new FrameStabilityContainer
{
Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1))
Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(StoryboardElementSource.Beatmap, string.Empty, 0, 1))
}
});
});
@@ -109,7 +109,7 @@ namespace osu.Game.Tests.Gameplay
{
Child = new FrameStabilityContainer
{
Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1))
Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(StoryboardElementSource.Beatmap, string.Empty, 0, 1))
}
});
@@ -141,7 +141,7 @@ namespace osu.Game.Tests.Gameplay
Child = beatmapSkinSourceContainer
});
beatmapSkinSourceContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
beatmapSkinSourceContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(StoryboardElementSource.Beatmap, "test-sample", 1, 1))
{
Clock = gameplayContainer
});
@@ -126,7 +126,7 @@ namespace osu.Game.Tests.NonVisual
Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation));
osu.Migrate(customPath);
osu.MigrateUserData(customPath);
Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath));
@@ -183,16 +183,16 @@ namespace osu.Game.Tests.NonVisual
{
var osu = LoadOsuIntoHost(host);
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.DoesNotThrow(() => osu.MigrateUserData(customPath));
Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
Assert.DoesNotThrow(() => osu.Migrate(customPath2));
Assert.DoesNotThrow(() => osu.MigrateUserData(customPath2));
Assert.That(File.Exists(Path.Combine(customPath2, OsuGameBase.CLIENT_DATABASE_FILENAME)));
// some files may have been left behind for whatever reason, but that's not what we're testing here.
cleanupPath(customPath);
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.DoesNotThrow(() => osu.MigrateUserData(customPath));
Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
}
finally
@@ -212,8 +212,8 @@ namespace osu.Game.Tests.NonVisual
{
var osu = LoadOsuIntoHost(host);
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.Throws<ArgumentException>(() => osu.Migrate(customPath));
Assert.DoesNotThrow(() => osu.MigrateUserData(customPath));
Assert.Throws<ArgumentException>(() => osu.MigrateUserData(customPath));
}
finally
{
@@ -238,14 +238,14 @@ namespace osu.Game.Tests.NonVisual
string originalDirectory = storage.GetFullPath(".");
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.DoesNotThrow(() => osu.MigrateUserData(customPath));
Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
Directory.CreateDirectory(customPath2);
File.WriteAllText(Path.Combine(customPath2, OsuGameBase.CLIENT_DATABASE_FILENAME), "I am a text");
// Fails because file already exists.
Assert.Throws<ArgumentException>(() => osu.Migrate(customPath2));
Assert.Throws<ArgumentException>(() => osu.MigrateUserData(customPath2));
osuStorage?.ChangeDataPath(customPath2);
@@ -269,7 +269,7 @@ namespace osu.Game.Tests.NonVisual
{
var osu = LoadOsuIntoHost(host);
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.DoesNotThrow(() => osu.MigrateUserData(customPath));
string subFolder = Path.Combine(customPath, "sub");
@@ -278,7 +278,7 @@ namespace osu.Game.Tests.NonVisual
Directory.CreateDirectory(subFolder);
Assert.Throws<ArgumentException>(() => osu.Migrate(subFolder));
Assert.Throws<ArgumentException>(() => osu.MigrateUserData(subFolder));
}
finally
{
@@ -297,7 +297,7 @@ namespace osu.Game.Tests.NonVisual
{
var osu = LoadOsuIntoHost(host);
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.DoesNotThrow(() => osu.MigrateUserData(customPath));
string seeminglySubFolder = customPath + "sub";
@@ -306,7 +306,7 @@ namespace osu.Game.Tests.NonVisual
Directory.CreateDirectory(seeminglySubFolder);
osu.Migrate(seeminglySubFolder);
osu.MigrateUserData(seeminglySubFolder);
}
finally
{
@@ -0,0 +1,84 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Tests.Rulesets.Scoring
{
public class ScoreMultiplierCalculatorTest
{
[Test]
public void TestFlatMultiplier()
{
var calculator = new TestScoreMultiplierCalculator();
double multiplier = calculator.CalculateFor([new OsuModEasy()]);
Assert.That(multiplier, Is.EqualTo(0.15));
}
[Test]
public void TestSettingDependentMultiplier()
{
var calculator = new TestScoreMultiplierCalculator();
double multiplier = calculator.CalculateFor([new OsuModDaycore { SpeedChange = { Value = 0.6 } }]);
Assert.That(multiplier, Is.EqualTo(0.4));
}
[Test]
public void TestContextDependentMultiplier()
{
var calculator = new TestScoreMultiplierCalculator();
double multiplier;
Assert.Multiple(() =>
{
calculator.HardRockPenalty = false;
multiplier = calculator.CalculateFor([new OsuModHardRock()]);
Assert.That(multiplier, Is.EqualTo(1.4));
calculator.HardRockPenalty = true;
multiplier = calculator.CalculateFor([new OsuModHardRock()]);
Assert.That(multiplier, Is.EqualTo(1.2));
});
}
[Test]
public void TestCombinationMultiplier()
{
var calculator = new TestScoreMultiplierCalculator();
double multiplier = calculator.CalculateFor([new OsuModEasy(), new OsuModDaycore()]);
Assert.That(multiplier, Is.EqualTo(0.003));
}
[Test]
public void TestCombinationAndFlatMultipliers()
{
var calculator = new TestScoreMultiplierCalculator();
double multiplier = calculator.CalculateFor([new OsuModDaycore(), new OsuModHardRock(), new OsuModEasy()]);
Assert.That(multiplier, Is.EqualTo(0.003 * 1.4));
}
private class TestScoreMultiplierCalculator : ScoreMultiplierCalculator
{
static TestScoreMultiplierCalculator()
{
Single<OsuModEasy>(hasMultiplier: 0.15);
Single<OsuModDaycore>(hasMultiplier: daycore => (1 + daycore.SpeedChange.Value) / 4);
Single<OsuModHardRock, TestScoreMultiplierCalculator>(hasMultiplier: (_, ctx) => ctx.HardRockPenalty ? 1.2 : 1.4);
Combination<OsuModEasy, OsuModDaycore>(hasMultiplier: (_, _) => 0.003);
}
public bool HardRockPenalty { get; set; }
}
}
}
@@ -355,6 +355,7 @@ namespace osu.Game.Tests.Visual.Background
public double StartTime => double.MinValue;
public double EndTime => double.MaxValue;
public double EndTimeForDisplay => double.MaxValue;
public StoryboardElementSource Source => StoryboardElementSource.Beatmap;
public Drawable CreateDrawable() => new DrawableTestStoryboardElement();
}
@@ -93,7 +93,7 @@ namespace osu.Game.Tests.Visual.Editing
});
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddStep("select circle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").TriggerClick());
AddStep("select circle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "Hit circle").TriggerClick());
AddStep("move mouse to compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType<HitObjectContainer>().Single()));
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("circle placed", () => editorBeatmap.HitObjects.Count == 1);
@@ -128,10 +128,10 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("clear all control points", () => editorBeatmap.ControlPointInfo.Clear());
AddAssert("Tool is selection", () => hitObjectComposer.ChildrenOfType<ComposeBlueprintContainer>().First().CurrentTool is SelectTool);
AddAssert("Hitcircle button not clickable", () => !hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").Enabled.Value);
AddAssert("Hitcircle button not clickable", () => !hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "Hit circle").Enabled.Value);
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddAssert("Hitcircle button is clickable", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").Enabled.Value);
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").TriggerClick());
AddAssert("Hitcircle button is clickable", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "Hit circle").Enabled.Value);
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "Hit circle").TriggerClick());
AddAssert("Tool changed", () => hitObjectComposer.ChildrenOfType<ComposeBlueprintContainer>().First().CurrentTool is HitCircleCompositionTool);
}
@@ -146,7 +146,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").TriggerClick());
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "Hit circle").TriggerClick());
ExpandingToolboxContainer toolboxContainer = null!;
@@ -186,7 +186,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").TriggerClick());
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "Hit circle").TriggerClick());
AddStep("move mouse to scroll area", () =>
{
@@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
var layer = storyboard.GetLayer("Background");
var sprite = new StoryboardSprite(lookup_name, Anchor.TopLeft, new Vector2(256, 192));
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, lookup_name, Anchor.TopLeft, new Vector2(256, 192));
sprite.Commands.AddAlpha(Easing.None, 0, 2000, 0, 2);
layer.Elements.Clear();
@@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
var layer = storyboard.GetLayer("Video");
var sprite = new StoryboardVideo("Videos/test-video.mp4", Time.Current);
var sprite = new StoryboardVideo(StoryboardElementSource.Beatmap, "Videos/test-video.mp4", Time.Current);
if (scaleTransformProvided)
{
@@ -250,7 +250,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
var layer = storyboard.GetLayer("Background");
var sprite = new StoryboardSprite(lookupName, origin, initialPosition);
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, lookupName, origin, initialPosition);
var loop = sprite.AddLoopingGroup(Time.Current, 100);
loop.AddAlpha(Easing.None, 0, 10000, 1, 1);
@@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
var storyboard = new Storyboard();
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, "unknown", Anchor.TopLeft, Vector2.Zero);
sprite.Commands.AddAlpha(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1);
@@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.Gameplay
var storyboard = new Storyboard();
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, "unknown", Anchor.TopLeft, Vector2.Zero);
// these should be ignored as we have an alpha visibility blocker proceeding this command.
sprite.Commands.AddScale(Easing.None, loop_start_time, -18000, 0, 1);
@@ -201,7 +201,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
var layer = storyboard.GetLayer("Background");
var sprite = new StoryboardSprite(lookup_name, Anchor.Centre, new Vector2(320, 240));
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, lookup_name, Anchor.Centre, new Vector2(320, 240));
sprite.Commands.AddScale(Easing.None, 0, clock_limit, 0.5f, 0.5f);
sprite.Commands.AddAlpha(Easing.None, 0, clock_limit, 1, 1);
addCommands?.Invoke(sprite);
@@ -38,10 +38,10 @@ namespace osu.Game.Tests.Visual.Gameplay
storyboard = new Storyboard();
var backgroundLayer = storyboard.GetLayer("Background");
backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: -7000, volume: 20));
backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: -5000, volume: 20));
backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 0, volume: 20));
backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 2000, volume: 20));
backgroundLayer.Add(new StoryboardSampleInfo(StoryboardElementSource.Beatmap, "Intro/welcome.mp3", time: -7000, volume: 20));
backgroundLayer.Add(new StoryboardSampleInfo(StoryboardElementSource.Beatmap, "Intro/welcome.mp3", time: -5000, volume: 20));
backgroundLayer.Add(new StoryboardSampleInfo(StoryboardElementSource.Beatmap, "Intro/welcome.mp3", time: 0, volume: 20));
backgroundLayer.Add(new StoryboardSampleInfo(StoryboardElementSource.Beatmap, "Intro/welcome.mp3", time: 2000, volume: 20));
}
[SetUp]
@@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private Storyboard createStoryboard(double startTime)
{
var storyboard = new Storyboard();
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, "unknown", Anchor.TopLeft, Vector2.Zero);
sprite.Commands.AddAlpha(Easing.None, startTime, 0, 0, 1);
storyboard.GetLayer("Background").Add(sprite);
return storyboard;
@@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private Storyboard createStoryboard(double duration)
{
var storyboard = new Storyboard();
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, "unknown", Anchor.TopLeft, Vector2.Zero);
sprite.Commands.AddAlpha(Easing.None, 0, duration, 1, 0);
storyboard.GetLayer("Background").Add(sprite);
return storyboard;
@@ -493,7 +493,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestIntroStoryboardElement() => testLeadIn(b =>
{
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, "unknown", Anchor.TopLeft, Vector2.Zero);
sprite.Commands.AddAlpha(Easing.None, -2000, 0, 0, 1);
b.Storyboard.GetLayer("Background").Add(sprite);
});
@@ -1206,6 +1206,27 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("style selection screen closed", () => this.ChildrenOfType<MultiplayerMatchFreestyleSelect>().SingleOrDefault()?.IsCurrentScreen() != true);
}
[Test]
public void TestMaxParticipantsAndSlots()
{
createRoom(() => new Room
{
Name = "Test Room",
Password = "password",
Playlist =
[
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}
],
MaxParticipants = 10
});
AddStep("turn max participants off", () => multiplayerClient.ChangeSettings(maxParticipants: null));
AddStep("turn max participants back on", () => multiplayerClient.ChangeSettings(maxParticipants: 8));
}
private void enterGameplay()
{
pressReadyButton();
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
@@ -55,6 +56,68 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("two unique panels", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 2);
}
[Test]
public void TestSlots()
{
setUpList();
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 1);
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
{
Id = 3,
Username = "Second",
CoverUrl = TestResources.COVER_IMAGE_3,
}));
AddAssert("two unique panels", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 2);
AddStep("introduce slots", () => MultiplayerClient.ChangeMatchRoomState(new StandardMatchRoomState
{
Slots = [null, 3, null, null, 1001, null, null]
}).WaitSafely());
AddStep("click first slot", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ParticipantPanel>().First());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("slots changed", () => ((StandardMatchRoomState)MultiplayerClient.ClientRoom!.MatchState!).Slots,
() => Is.EquivalentTo(new int?[] { 1001, 3, null, null, null, null, null }));
AddStep("click second slot", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ParticipantPanel>().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
AddUntilStep("slots not changed", () => ((StandardMatchRoomState)MultiplayerClient.ClientRoom!.MatchState!).Slots,
() => Is.EquivalentTo(new int?[] { 1001, 3, null, null, null, null, null }));
AddStep("click last slot", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ParticipantPanel>().Last());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("slots changed", () => ((StandardMatchRoomState)MultiplayerClient.ClientRoom!.MatchState!).Slots,
() => Is.EquivalentTo(new int?[] { null, 3, null, null, null, null, 1001 }));
AddStep("shuffle slots", () => MultiplayerClient.ChangeMatchRoomState(new StandardMatchRoomState
{
Slots = [null, null, 1001, null, null, null, 3]
}).WaitSafely());
AddStep("remove slots", () => MultiplayerClient.ChangeMatchRoomState(new StandardMatchRoomState
{
Slots = [null, 3, null, 1001]
}).WaitSafely());
AddStep("add slots", () => MultiplayerClient.ChangeMatchRoomState(new StandardMatchRoomState
{
Slots = [null, null, 3, null, 1001, null]
}).WaitSafely());
AddStep("turn off slots", () => MultiplayerClient.ChangeMatchRoomState(new StandardMatchRoomState
{
Slots = null
}).WaitSafely());
}
[Test]
public void TestAddReferee()
{
@@ -86,7 +149,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("two unique panels", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 2);
AddStep("kick null user", () => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.User == null)
AddStep("kick null user", () => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.User?.User == null)
.ChildrenOfType<ParticipantPanel.KickButton>().Single().TriggerClick());
AddUntilStep("null user kicked", () => MultiplayerClient.ClientRoom.AsNonNull().Users.Count == 1);
@@ -111,7 +174,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("remove host", () => MultiplayerClient.RemoveUser(API.LocalUser.Value));
AddAssert("single panel is for second user", () => this.ChildrenOfType<ParticipantPanel>().Single().Current.Value.UserID == secondUser?.Id);
AddAssert("single panel is for second user", () => this.ChildrenOfType<ParticipantPanel>().Single().Current.Value.User?.UserID == secondUser?.Id);
}
[Test]
@@ -150,7 +213,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddRepeatStep("increment progress", () =>
{
float progress = this.ChildrenOfType<ParticipantPanel>().Single().Current.Value.BeatmapAvailability.DownloadProgress ?? 0;
float progress = this.ChildrenOfType<ParticipantPanel>().Single().Current.Value.User?.BeatmapAvailability.DownloadProgress ?? 0;
MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress + RNG.NextSingle(0.1f)));
}, 25);
@@ -195,16 +258,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
}));
AddUntilStep("first user crown visible",
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.UserID == 1001).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.User?.UserID == 1001).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
AddUntilStep("second user crown hidden",
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.UserID == 3).ChildrenOfType<SpriteIcon>().First().Alpha == 0);
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.User?.UserID == 3).ChildrenOfType<SpriteIcon>().First().Alpha == 0);
AddStep("make second user host", () => MultiplayerClient.TransferHost(3));
AddUntilStep("first user crown visible",
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.UserID == 1001).ChildrenOfType<SpriteIcon>().First().Alpha == 0);
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.User?.UserID == 1001).ChildrenOfType<SpriteIcon>().First().Alpha == 0);
AddUntilStep("second user crown hidden",
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.UserID == 3).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.User?.UserID == 3).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
}
[Test]
@@ -221,8 +284,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("make second user host", () => MultiplayerClient.TransferHost(3));
AddAssert("second user above first", () =>
{
var first = this.ChildrenOfType<ParticipantPanel>().Single(u => u.Current.Value.UserID == 1001);
var second = this.ChildrenOfType<ParticipantPanel>().Single(u => u.Current.Value.UserID == 3);
var first = this.ChildrenOfType<ParticipantPanel>().Single(u => u.Current.Value.User?.UserID == 1001);
var second = this.ChildrenOfType<ParticipantPanel>().Single(u => u.Current.Value.User?.UserID == 3);
return second.ScreenSpaceDrawQuad.TopLeft.Y < first.ScreenSpaceDrawQuad.TopLeft.Y;
});
}
@@ -178,6 +178,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType<RoomPanel.CornerIcon>().First().Alpha));
}
[Test]
public void TestSetAndUnsetMaxParticipants()
{
RoomPanel panel = null!;
Room room = null!;
AddStep("create room", () => Child = panel = createLoungeRoom(room = new Room
{
Name = "A room",
Type = MatchType.HeadToHead,
}));
AddUntilStep("wait for panel load", () => panel.ChildrenOfType<DrawableRoomParticipantsList>().Any());
AddStep("set max participants", () => room.MaxParticipants = 5);
AddStep("unset max participants", () => room.MaxParticipants = null);
}
[Test]
public void TestMultiplayerRooms()
{
@@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.RankedPlay
AddStep("set discard phase", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.CardDiscard).WaitSafely());
AddWaitStep("wait", 3);
AddUntilStep("wait until cards are present", () => this.ChildrenOfType<PlayerHandOfCards.PlayerHandCard>().Count() == 5);
for (int i = 0; i < 3; i++)
{
@@ -7,6 +7,7 @@ using System;
using System.Threading;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
@@ -16,10 +17,12 @@ using osu.Game.Overlays.Dialog;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public partial class TestSceneDialogOverlay : OsuTestScene
public partial class TestSceneDialogOverlay : OsuTestScene, IOverlayManager
{
private DialogOverlay overlay;
private readonly Bindable<OverlayActivation> overlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
[SetUpSteps]
public void SetUpSteps()
{
@@ -99,7 +102,8 @@ namespace osu.Game.Tests.Visual.UserInterface
{
Icon = FontAwesome.Regular.TrashAlt,
HeaderText = @"Confirm deletion ofConfirm deletion ofConfirm deletion ofConfirm deletion ofConfirm deletion ofConfirm deletion of",
BodyText = @"Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver. ",
BodyText =
@"Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver. ",
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton
@@ -116,6 +120,36 @@ namespace osu.Game.Tests.Visual.UserInterface
}));
}
[Test]
public void TestPushWhileOverlayActivationDisabled()
{
PopupDialog dialog = null;
AddStep("set activation mode disabled", () => overlayActivationMode.Value = OverlayActivation.Disabled);
AddStep("push dialog", () =>
{
overlay.Push(dialog = new TestPopupDialog
{
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton { Text = @"OK" },
},
});
});
AddUntilStep("overlay not visible", () => overlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("set activation mode enabled", () => overlayActivationMode.Value = OverlayActivation.All);
AddUntilStep("overlay visible", () => overlay.State.Value, () => Is.EqualTo(Visibility.Visible));
AddUntilStep("dialog displayed", () => dialog.State.Value, () => Is.EqualTo(Visibility.Visible));
AddStep("set activation mode disabled", () => overlayActivationMode.Value = OverlayActivation.Disabled);
AddUntilStep("dialog hidden", () => dialog.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddAssert("dialog dismissed", () => overlay.CurrentDialog, () => Is.Null);
}
[Test]
public void TestPushBeforeLoad()
{
@@ -194,5 +228,17 @@ namespace osu.Game.Tests.Visual.UserInterface
private partial class TestPopupDialog : PopupDialog
{
}
public IBindable<OverlayActivation> OverlayActivationMode => overlayActivationMode;
public IDisposable RegisterBlockingOverlay(OverlayContainer overlayContainer) => throw new NotImplementedException();
public void ShowBlockingOverlay(OverlayContainer overlay)
{
}
public void HideBlockingOverlay(OverlayContainer overlay)
{
}
}
}
@@ -188,6 +188,12 @@ namespace osu.Game.Tests.Visual.UserInterface
Caption = "File selector",
PlaceholderText = "Select a file",
},
new FormFileSelector
{
Caption = "File selector with deselection",
PlaceholderText = "Select a file",
AllowClear = true,
},
new FormBeatmapFileSelector(true)
{
Caption = "File selector with intermediate choice dialog",
@@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneMigrateAudioDialog : OsuManualInputManagerTestScene
{
private DialogOverlay overlay = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
}
[Test]
public void TestWasUsing()
{
AddStep("create dialog", () =>
{
overlay.Push(new MigrateNewAudioDialog(true));
});
}
[Test]
public void TestNotUsing()
{
AddStep("create dialog", () =>
{
overlay.Push(new MigrateNewAudioDialog(false));
});
}
}
}
-2
View File
@@ -65,8 +65,6 @@ namespace osu.Game.Beatmaps
public SortedList<BreakPeriod> Breaks { get; set; } = new SortedList<BreakPeriod>(Comparer<BreakPeriod>.Default);
public List<string> UnhandledEventLines { get; set; } = new List<string>();
[JsonIgnore]
public double TotalBreakTime => Breaks.Sum(b => b.Duration);
-1
View File
@@ -72,7 +72,6 @@ namespace osu.Game.Beatmaps
beatmap.ControlPointInfo = original.ControlPointInfo;
beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList();
beatmap.Breaks = original.Breaks;
beatmap.UnhandledEventLines = original.UnhandledEventLines;
beatmap.AudioLeadIn = original.AudioLeadIn;
beatmap.StackLeniency = original.StackLeniency;
beatmap.SpecialStyle = original.SpecialStyle;
+7 -5
View File
@@ -27,6 +27,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using osu.Game.Skinning;
using osu.Game.Storyboards;
using osu.Game.Utils;
using Realms;
@@ -216,7 +217,7 @@ namespace osu.Game.Beatmaps
targetBeatmapSet.Beatmaps.Add(newBeatmap.BeatmapInfo);
newBeatmap.BeatmapInfo.BeatmapSet = targetBeatmapSet;
save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin, transferCollections: false);
save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin, new Storyboard(), transferCollections: false);
workingBeatmapCache.Invalidate(targetBeatmapSet);
return GetWorkingBeatmap(newBeatmap.BeatmapInfo);
@@ -359,8 +360,9 @@ namespace osu.Game.Beatmaps
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param>
/// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param>
/// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param>
public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) =>
save(beatmapInfo, beatmapContent, beatmapSkin, transferCollections: true);
/// <param name="storyboard">The storyboard content to write, null if to be omitted.</param>
public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null, Storyboard? storyboard = null) =>
save(beatmapInfo, beatmapContent, beatmapSkin, storyboard, transferCollections: true);
public void DeleteAllVideos()
{
@@ -502,7 +504,7 @@ namespace osu.Game.Beatmaps
setInfo.Status = BeatmapOnlineStatus.LocallyModified;
}
private void save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin, bool transferCollections)
private void save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin, Storyboard? storyboard, bool transferCollections)
{
var setInfo = beatmapInfo.BeatmapSet;
Debug.Assert(setInfo != null);
@@ -526,7 +528,7 @@ namespace osu.Game.Beatmaps
{
using var stream = new MemoryStream();
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw);
new LegacyBeatmapEncoder(beatmapContent, beatmapSkin, storyboard).Encode(sw);
stream.Seek(0, SeekOrigin.Begin);
@@ -244,10 +244,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
});
if (BeatmapSet.HasVideo)
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) });
leftIconArea.Add(new VideoIconPill());
if (BeatmapSet.HasStoryboard)
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) });
leftIconArea.Add(new StoryboardIconPill());
if (BeatmapSet.FeaturedInSpotlight)
{
@@ -226,10 +226,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
});
if (BeatmapSet.HasVideo)
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) });
leftIconArea.Add(new VideoIconPill());
if (BeatmapSet.HasStoryboard)
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) });
leftIconArea.Add(new StoryboardIconPill());
if (BeatmapSet.FeaturedInSpotlight)
{
+2 -2
View File
@@ -19,11 +19,11 @@ namespace osu.Game.Beatmaps.Formats
{
var output = CreateTemplateObject();
foreach (LineBufferedReader stream in otherStreams.Prepend(primaryStream))
ParseStreamInto(stream, output);
ParseStreamInto(stream, stream == primaryStream, output);
return output;
}
protected abstract void ParseStreamInto(LineBufferedReader stream, TOutput output);
protected abstract void ParseStreamInto(LineBufferedReader stream, bool isPrimaryStream, TOutput output);
}
public abstract class Decoder
@@ -13,7 +13,7 @@ namespace osu.Game.Beatmaps.Formats
AddDecoder<Beatmap>("{", _ => new JsonBeatmapDecoder());
}
protected override void ParseStreamInto(LineBufferedReader stream, Beatmap output)
protected override void ParseStreamInto(LineBufferedReader stream, bool isPrimaryStream, Beatmap output)
{
stream.ReadToEnd().DeserializeInto(output);
@@ -81,7 +81,7 @@ namespace osu.Game.Beatmaps.Formats
return templateBeatmap;
}
protected override void ParseStreamInto(LineBufferedReader stream, Beatmap beatmap)
protected override void ParseStreamInto(LineBufferedReader stream, bool isPrimaryStream, Beatmap beatmap)
{
this.beatmap = beatmap;
this.beatmap.BeatmapVersion = FormatVersion;
@@ -89,7 +89,7 @@ namespace osu.Game.Beatmaps.Formats
ApplyLegacyDefaults(this.beatmap);
base.ParseStreamInto(stream, beatmap);
base.ParseStreamInto(stream, isPrimaryStream, beatmap);
applyDifficultyRestrictions(beatmap.Difficulty, beatmap);
@@ -204,7 +204,7 @@ namespace osu.Game.Beatmaps.Formats
beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(0) ?? beatmap.BeatmapInfo.Ruleset;
}
protected override void ParseLine(Beatmap beatmap, Section section, string line)
protected override void ParseLine(Beatmap beatmap, Section section, string line, bool isPrimaryStream)
{
switch (section)
{
@@ -237,7 +237,7 @@ namespace osu.Game.Beatmaps.Formats
return;
}
base.ParseLine(beatmap, section, line);
base.ParseLine(beatmap, section, line, isPrimaryStream);
}
private void handleGeneral(string line)
@@ -430,10 +430,6 @@ namespace osu.Game.Beatmaps.Formats
{
string[] split = line.Split(',');
// Until we have full storyboard encoder coverage, let's track any lines which aren't handled
// and store them to a temporary location such that they aren't lost on editor save / export.
bool lineSupportedByEncoder = false;
if (Enum.TryParse(split[0], out LegacyEventType type))
{
switch (type)
@@ -445,7 +441,6 @@ namespace osu.Game.Beatmaps.Formats
if (string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile))
{
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]);
lineSupportedByEncoder = true;
}
break;
@@ -459,14 +454,12 @@ namespace osu.Game.Beatmaps.Formats
if (!SupportedExtensions.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant()))
{
beatmap.BeatmapInfo.Metadata.BackgroundFile = filename;
lineSupportedByEncoder = true;
}
break;
case LegacyEventType.Background:
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]);
lineSupportedByEncoder = true;
break;
case LegacyEventType.Break:
@@ -474,13 +467,9 @@ namespace osu.Game.Beatmaps.Formats
double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2])));
beatmap.Breaks.Add(new BreakPeriod(start, end));
lineSupportedByEncoder = true;
break;
}
}
if (!lineSupportedByEncoder)
beatmap.UnhandledEventLines.Add(line);
}
private void handleTimingPoint(string line)
@@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
using osu.Game.Storyboards;
using osuTK;
using osuTK.Graphics;
@@ -24,8 +25,8 @@ namespace osu.Game.Beatmaps.Formats
public const int FIRST_LAZER_VERSION = 128;
private readonly IBeatmap beatmap;
private readonly ISkin? skin;
private readonly LegacyStoryboardEncoder? storyboardEncoder;
private readonly int onlineRulesetID;
@@ -34,11 +35,18 @@ namespace osu.Game.Beatmaps.Formats
/// </summary>
/// <param name="beatmap">The beatmap to encode.</param>
/// <param name="skin">The beatmap's skin, used for encoding combo colours.</param>
public LegacyBeatmapEncoder(IBeatmap beatmap, ISkin? skin)
/// <param name="storyboard">
/// The combined storyboard, loaded from both the <c>.osu</c> and the <c>.osz</c>.
/// Only elements from the <c>.osu</c> (marked via <see cref="StoryboardElementSource.Beatmap"/>) will be encoded to the beatmap.
/// </param>
public LegacyBeatmapEncoder(IBeatmap beatmap, ISkin? skin, Storyboard? storyboard)
{
this.beatmap = beatmap;
this.skin = skin;
if (storyboard != null)
storyboardEncoder = new LegacyStoryboardEncoder(storyboard);
onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID;
if (onlineRulesetID < 0 || onlineRulesetID > 3)
@@ -101,7 +109,12 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine(FormattableString.Invariant($@"CountdownOffset: {beatmap.CountdownOffset}"));
if (onlineRulesetID == 3)
writer.WriteLine(FormattableString.Invariant($"SpecialStyle: {(beatmap.SpecialStyle ? '1' : '0')}"));
writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.WidescreenStoryboard ? '1' : '0')}"));
if (storyboardEncoder != null)
storyboardEncoder.EncodeGeneralToBeatmap(writer);
else
writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.WidescreenStoryboard ? '1' : '0')}"));
if (beatmap.SamplesMatchPlaybackRate)
writer.WriteLine(@"SamplesMatchPlaybackRate: 1");
}
@@ -151,14 +164,19 @@ namespace osu.Game.Beatmaps.Formats
{
writer.WriteLine("[Events]");
if (!string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile))
writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Background},0,\"{beatmap.BeatmapInfo.Metadata.BackgroundFile}\",0,0"));
if (storyboardEncoder != null)
{
storyboardEncoder.EncodeEventsToBeatmap(writer);
}
else
{
if (!string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile))
writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Background},0,\"{beatmap.BeatmapInfo.Metadata.BackgroundFile}\",0,0"));
}
writer.WriteLine("// Break Periods");
foreach (var b in beatmap.Breaks)
writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Break},{b.StartTime},{b.EndTime}"));
foreach (string l in beatmap.UnhandledEventLines)
writer.WriteLine(l);
}
private void handleControlPoints(TextWriter writer)
+3 -3
View File
@@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps.Formats
FormatVersion = version;
}
protected override void ParseStreamInto(LineBufferedReader stream, T output)
protected override void ParseStreamInto(LineBufferedReader stream, bool isPrimaryStream, T output)
{
Section section = Section.General;
@@ -65,7 +65,7 @@ namespace osu.Game.Beatmaps.Formats
try
{
ParseLine(output, section, line);
ParseLine(output, section, line, isPrimaryStream);
}
catch (Exception e)
{
@@ -84,7 +84,7 @@ namespace osu.Game.Beatmaps.Formats
{
}
protected virtual void ParseLine(T output, Section section, string line)
protected virtual void ParseLine(T output, Section section, string line, bool isPrimaryStream)
{
switch (section)
{
@@ -49,13 +49,13 @@ namespace osu.Game.Beatmaps.Formats
return sb;
}
protected override void ParseStreamInto(LineBufferedReader stream, Storyboard storyboard)
protected override void ParseStreamInto(LineBufferedReader stream, bool isPrimaryStream, Storyboard storyboard)
{
this.storyboard = storyboard;
base.ParseStreamInto(stream, storyboard);
base.ParseStreamInto(stream, isPrimaryStream, storyboard);
}
protected override void ParseLine(Storyboard storyboard, Section section, string line)
protected override void ParseLine(Storyboard storyboard, Section section, string line, bool isPrimaryStream)
{
switch (section)
{
@@ -64,7 +64,7 @@ namespace osu.Game.Beatmaps.Formats
return;
case Section.Events:
handleEvents(line);
handleEvents(line, isPrimaryStream);
return;
case Section.Variables:
@@ -72,7 +72,7 @@ namespace osu.Game.Beatmaps.Formats
return;
}
base.ParseLine(storyboard, section, line);
base.ParseLine(storyboard, section, line, isPrimaryStream);
}
private void handleGeneral(Storyboard storyboard, string line)
@@ -91,7 +91,7 @@ namespace osu.Game.Beatmaps.Formats
}
}
private void handleEvents(string line)
private void handleEvents(string line, bool isPrimaryStream)
{
decodeVariables(ref line);
@@ -116,8 +116,24 @@ namespace osu.Game.Beatmaps.Formats
if (!Enum.TryParse(split[0], out LegacyEventType type))
throw new InvalidDataException($@"Unknown event type: {split[0]}");
var source = isPrimaryStream ? StoryboardElementSource.Beatmap : StoryboardElementSource.Shared;
switch (type)
{
case LegacyEventType.Background:
{
// the actual filename is handled in `LegacyBeatmapDecoder`.
// this only handles the background offset, because it does not logically belong in `Beatmap` or related classes.
if (split.Length > 4)
{
float x = Parsing.ParseFloat(split[3]);
float y = Parsing.ParseFloat(split[4]);
storyboard.BackgroundOffset = new Vector2(x, y);
}
break;
}
case LegacyEventType.Video:
{
int offset = Parsing.ParseInt(split[1]);
@@ -131,7 +147,7 @@ namespace osu.Game.Beatmaps.Formats
if (!SupportedExtensions.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path).ToLowerInvariant()))
break;
storyboard.GetLayer("Video").Add(storyboardSprite = new StoryboardVideo(path, offset));
storyboard.GetLayer("Video").Add(storyboardSprite = new StoryboardVideo(source, path, offset));
break;
}
@@ -142,7 +158,7 @@ namespace osu.Game.Beatmaps.Formats
string path = CleanFilename(split[3]);
float x = Parsing.ParseFloat(split[4], Parsing.MAX_COORDINATE_VALUE);
float y = Parsing.ParseFloat(split[5], Parsing.MAX_COORDINATE_VALUE);
storyboardSprite = new StoryboardSprite(path, origin, new Vector2(x, y));
storyboardSprite = new StoryboardSprite(source, path, origin, new Vector2(x, y));
storyboard.GetLayer(layer).Add(storyboardSprite);
break;
}
@@ -162,7 +178,7 @@ namespace osu.Game.Beatmaps.Formats
frameDelay = Math.Round(0.015 * frameDelay) * 1.186 * (1000 / 60f);
var loopType = split.Length > 8 ? parseAnimationLoopType(split[8]) : AnimationLoopType.LoopForever;
storyboardSprite = new StoryboardAnimation(path, origin, new Vector2(x, y), frameCount, frameDelay, loopType);
storyboardSprite = new StoryboardAnimation(source, path, origin, new Vector2(x, y), frameCount, frameDelay, loopType);
storyboard.GetLayer(layer).Add(storyboardSprite);
break;
}
@@ -173,7 +189,7 @@ namespace osu.Game.Beatmaps.Formats
string layer = parseLayer(split[2]);
string path = CleanFilename(split[3]);
float volume = split.Length > 4 ? Parsing.ParseFloat(split[4]) : 100;
storyboard.GetLayer(layer).Add(new StoryboardSampleInfo(path, time, (int)volume));
storyboard.GetLayer(layer).Add(new StoryboardSampleInfo(source, path, time, (int)volume));
break;
}
}
@@ -192,7 +208,8 @@ namespace osu.Game.Beatmaps.Formats
string triggerName = split[1];
double startTime = split.Length > 2 ? Parsing.ParseDouble(split[2]) : double.MinValue;
double endTime = split.Length > 3 ? Parsing.ParseDouble(split[3]) : double.MaxValue;
int groupNumber = split.Length > 4 ? Parsing.ParseInt(split[4]) : 0;
// negation as per https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L736
int groupNumber = split.Length > 4 ? -Parsing.ParseInt(split[4]) : 0;
currentCommandsGroup = storyboardSprite?.AddTriggerGroup(triggerName, startTime, endTime, groupNumber);
break;
}
@@ -0,0 +1,349 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Storyboards;
using osu.Game.Storyboards.Commands;
namespace osu.Game.Beatmaps.Formats
{
public class LegacyStoryboardEncoder
{
private readonly Storyboard storyboard;
public LegacyStoryboardEncoder(Storyboard storyboard)
{
this.storyboard = storyboard;
}
#region Storyboards embedded in beatmaps
public void EncodeGeneralToBeatmap(TextWriter writer)
{
writer.WriteLine(FormattableString.Invariant($@"UseSkinSprites: {(storyboard.UseSkinSprites ? '1' : '0')}"));
writer.WriteLine(FormattableString.Invariant($@"WidescreenStoryboard: {(storyboard.Beatmap.WidescreenStoryboard ? '1' : '0')}"));
}
public void EncodeEventsToBeatmap(TextWriter writer)
=> encodeEvents(writer, StoryboardElementSource.Beatmap);
#endregion
#region Standalone storyboards
public void EncodeStandaloneStoryboard(TextWriter writer)
{
writer.WriteLine(@"[Events]");
encodeEvents(writer, StoryboardElementSource.Shared);
}
#endregion
private void encodeEvents(TextWriter writer, StoryboardElementSource target)
{
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameModes/Edit/Modes/EditorModeDesign.cs#L189
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/Events/EventManager.cs#L368
writer.WriteLine(@"// Background and Video events");
if (target == StoryboardElementSource.Beatmap)
{
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1499
writer.WriteLine(string.Format(CultureInfo.InvariantCulture,
@"{0},{1},""{2}"",{3},{4}",
(int)LegacyEventType.Background, 0, storyboard.BeatmapInfo.Metadata.BackgroundFile, storyboard.BackgroundOffset.X, storyboard.BackgroundOffset.Y));
}
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1496
foreach (var video in storyboard.GetLayer(@"Video").Elements.OfType<StoryboardVideo>().Where(v => v.Source == target))
{
writer.WriteLine(string.Format(CultureInfo.InvariantCulture,
@"{0},{1},""{2}""",
nameof(LegacyEventType.Video), video.StartTime, video.Path));
encodeCommands(writer, video);
}
foreach (var legacyLayer in Enum.GetValues<LegacyStoryLayer>().Except(LegacyStoryLayer.Video.Yield()))
{
writer.WriteLine(string.Format(CultureInfo.InvariantCulture,
@"// Storyboard Layer {0} ({1})",
(int)legacyLayer,
legacyLayer));
string layerName = legacyLayer.ToString();
var layer = storyboard.GetLayer(layerName);
encodeSpritesFromLayer(writer, layer, target);
}
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1478-L1481
writer.WriteLine(@"// Storyboard Sound Samples");
foreach (var legacyLayer in Enum.GetValues<LegacyStoryLayer>().Except(LegacyStoryLayer.Video.Yield()))
{
string layerName = legacyLayer.ToString();
var layer = storyboard.GetLayer(layerName);
foreach (var sample in layer.Elements.OfType<StoryboardSampleInfo>().Where(s => s.Source == target))
{
writer.WriteLine(string.Format(CultureInfo.InvariantCulture,
@"{0},{1},{2},""{3}"",{4}",
nameof(LegacyEventType.Sample),
sample.StartTime,
(int)legacyLayer,
sample.Path,
sample.Volume));
}
}
}
private void encodeSpritesFromLayer(TextWriter writer, StoryboardLayer layer, StoryboardElementSource target)
{
foreach (var element in layer.Elements.Where(elem => elem.Source == target))
{
LegacyOrigins origin;
switch (element)
{
case StoryboardAnimation animation:
{
origin = convertOrigin(animation.Origin);
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1505-L1507
writer.WriteLine(string.Format(
CultureInfo.InvariantCulture,
@"{0},{1},{2},""{3}"",{4},{5},{6},{7},{8}",
nameof(LegacyEventType.Animation),
layer.Name,
origin,
animation.Path,
animation.InitialPosition.X,
animation.InitialPosition.Y,
animation.FrameCount,
animation.FrameDelay,
animation.LoopType));
encodeCommands(writer, animation);
break;
}
case StoryboardSprite sprite:
{
origin = convertOrigin(sprite.Origin);
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1502
writer.WriteLine(string.Format(
CultureInfo.InvariantCulture,
@"{0},{1},{2},""{3}"",{4},{5}",
nameof(LegacyEventType.Sprite),
layer.Name,
origin,
sprite.Path,
sprite.InitialPosition.X,
sprite.InitialPosition.Y));
encodeCommands(writer, sprite);
break;
}
}
}
}
private void encodeCommands(TextWriter writer, StoryboardSprite sprite)
{
foreach (var loopingGroup in sprite.LoopingGroups)
{
writer.WriteLine(string.Format(
CultureInfo.InvariantCulture,
@" L,{0},{1}",
loopingGroup.StartTime, loopingGroup.TotalIterations));
foreach (var command in loopingGroup.AllCommands)
// see `StoryboardLoopingCommand` ctor for why `relativeToTime` is passed
encodeCommand(writer, command, 2, relativeToTime: loopingGroup.StartTime);
}
foreach (var command in sprite.Commands.AllCommands)
encodeCommand(writer, command, 1);
foreach (var triggerGroup in sprite.TriggerGroups)
{
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1564-L1572
writer.Write(string.Format(
CultureInfo.InvariantCulture,
@" T,{0}",
triggerGroup.TriggerName));
if (triggerGroup.TriggerEndTime != 0)
{
writer.Write(string.Format(
CultureInfo.InvariantCulture,
@",{0},{1}",
triggerGroup.TriggerStartTime, triggerGroup.TriggerEndTime));
}
if (triggerGroup.GroupNumber != 0)
{
writer.Write(string.Format(CultureInfo.InvariantCulture, @",{0}", -triggerGroup.GroupNumber));
}
writer.WriteLine();
foreach (var command in triggerGroup.AllCommands)
encodeCommand(writer, command, 2);
}
}
private void encodeCommand(TextWriter writer, IStoryboardCommand command, int depth, double relativeToTime = 0)
{
for (int i = 0; i < depth; ++i)
writer.Write(' ');
string typeAcronym;
string details;
if (command is IStoryboardLoopingCommand loopingCommand)
command = loopingCommand.OriginalCommand;
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1546-L1550
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1690-L1730
switch (command)
{
case StoryboardVectorScaleCommand vectorScale:
typeAcronym = @"V";
details = vectorScale.StartValue == vectorScale.EndValue
? string.Format(CultureInfo.InvariantCulture, @"{0},{1}", vectorScale.StartValue.X, vectorScale.StartValue.Y)
: string.Format(CultureInfo.InvariantCulture,
@"{0},{1},{2},{3}",
vectorScale.StartValue.X, vectorScale.StartValue.Y, vectorScale.EndValue.X, vectorScale.EndValue.Y);
break;
case StoryboardAlphaCommand fade:
typeAcronym = @"F";
details = fade.StartValue == fade.EndValue
? fade.StartValue.ToString(CultureInfo.InvariantCulture)
: string.Format(CultureInfo.InvariantCulture,
@"{0},{1}",
fade.StartValue,
fade.EndValue);
break;
case StoryboardRotationCommand rotation:
typeAcronym = @"R";
details = rotation.StartValue == rotation.EndValue
? rotation.StartValue.ToString(CultureInfo.InvariantCulture)
: string.Format(CultureInfo.InvariantCulture,
@"{0},{1}",
float.DegreesToRadians(rotation.StartValue),
float.DegreesToRadians(rotation.EndValue));
break;
case StoryboardScaleCommand scale:
typeAcronym = @"S";
details = scale.StartValue == scale.EndValue
? scale.StartValue.ToString(CultureInfo.InvariantCulture)
: string.Format(CultureInfo.InvariantCulture,
@"{0},{1}",
scale.StartValue,
scale.EndValue);
break;
// stable has M commands that combine X and Y movement, but we decompose those into X/Y with no way to undo
case StoryboardXCommand movementX:
typeAcronym = @"MX";
details = movementX.StartValue == movementX.EndValue
? movementX.StartValue.ToString(CultureInfo.InvariantCulture)
: string.Format(CultureInfo.InvariantCulture,
@"{0},{1}",
movementX.StartValue,
movementX.EndValue);
break;
case StoryboardYCommand movementY:
typeAcronym = @"MY";
details = movementY.StartValue == movementY.EndValue
? movementY.StartValue.ToString(CultureInfo.InvariantCulture)
: string.Format(CultureInfo.InvariantCulture,
@"{0},{1}",
movementY.StartValue,
movementY.EndValue);
break;
case StoryboardColourCommand colour:
typeAcronym = @"C";
details = colour.StartValue == colour.EndValue
? string.Format(CultureInfo.InvariantCulture,
@"{0},{1},{2}",
(int)(colour.StartValue.R * 255), (int)(colour.StartValue.G * 255), (int)(colour.StartValue.B * 255))
: string.Format(CultureInfo.InvariantCulture,
@"{0},{1},{2},{3},{4},{5}",
(int)(colour.StartValue.R * 255), (int)(colour.StartValue.G * 255), (int)(colour.StartValue.B * 255),
(int)(colour.EndValue.R * 255), (int)(colour.EndValue.G * 255), (int)(colour.EndValue.B * 255));
break;
case StoryboardFlipHCommand:
typeAcronym = @"P";
details = @"H";
break;
case StoryboardFlipVCommand:
typeAcronym = @"P";
details = @"V";
break;
case StoryboardBlendingParametersCommand:
typeAcronym = @"P";
details = @"A";
break;
default:
return;
}
writer.WriteLine(string.Format(CultureInfo.InvariantCulture,
@"{0},{1},{2},{3},{4}",
typeAcronym,
(int)command.Easing,
command.StartTime - relativeToTime,
command.StartTime == command.EndTime ? null : command.EndTime - relativeToTime,
details));
}
private LegacyOrigins convertOrigin(Anchor anchor)
{
switch (anchor)
{
case Anchor.TopLeft:
return LegacyOrigins.TopLeft;
case Anchor.TopCentre:
return LegacyOrigins.TopCentre;
case Anchor.TopRight:
return LegacyOrigins.TopRight;
case Anchor.CentreLeft:
return LegacyOrigins.CentreLeft;
case Anchor.Centre:
return LegacyOrigins.Centre;
case Anchor.CentreRight:
return LegacyOrigins.CentreRight;
case Anchor.BottomLeft:
return LegacyOrigins.BottomLeft;
case Anchor.BottomCentre:
return LegacyOrigins.BottomCentre;
case Anchor.BottomRight:
return LegacyOrigins.BottomRight;
default:
return LegacyOrigins.TopLeft;
}
}
}
}
+43 -3
View File
@@ -5,6 +5,7 @@ using System;
using System.Diagnostics;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Timing;
@@ -49,6 +50,11 @@ namespace osu.Game.Beatmaps
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
[Resolved]
private AudioManager audioManager { get; set; } = null!;
private Bindable<bool> experimentalAudio = null!;
public bool IsRewinding { get; private set; }
public FramedBeatmapClock(bool applyOffsets, bool requireDecoupling, IClock? source = null)
@@ -66,9 +72,7 @@ namespace osu.Game.Beatmaps
if (applyOffsets)
{
// Audio timings in general with newer BASS versions don't match stable.
// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
platformOffsetClock = new OffsetCorrectionClock(interpolatedTrack) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 };
platformOffsetClock = new OffsetCorrectionClock(interpolatedTrack);
// User global offset (set in settings) should also be applied.
userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock);
@@ -94,6 +98,9 @@ namespace osu.Game.Beatmaps
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true);
experimentalAudio = audioManager.UseExperimentalWasapi.GetBoundCopy();
experimentalAudio.BindValueChanged(_ => updatePlatformOffset());
// TODO: this doesn't update when using ChangeSource() to change beatmap.
beatmapOffsetSubscription = realm.SubscribeToPropertyChanged(
r => r.Find<BeatmapInfo>(beatmap.Value.BeatmapInfo.ID)?.UserSettings,
@@ -105,6 +112,39 @@ namespace osu.Game.Beatmaps
}
}
/// <summary>
/// Audio timings in general with newer BASS versions don't match stable.
/// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
/// </summary>
public const double WINDOWS_BASE_AUDIO_OFFSET = 15;
/// <summary>
/// An additional offset applied to account for experimental mode being much better.
/// </summary>
public const double WINDOWS_EXPERIMENTAL_AUDIO_OFFSET = -25;
private void updatePlatformOffset()
{
if (!applyOffsets)
return;
Debug.Assert(platformOffsetClock != null);
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
platformOffsetClock.Offset = WINDOWS_BASE_AUDIO_OFFSET;
if (audioManager.UseExperimentalWasapi.Value)
platformOffsetClock.Offset += WINDOWS_EXPERIMENTAL_AUDIO_OFFSET;
return;
default:
platformOffsetClock.Offset = 0;
break;
}
}
protected override void Update()
{
base.Update();
-6
View File
@@ -44,12 +44,6 @@ namespace osu.Game.Beatmaps
/// </summary>
SortedList<BreakPeriod> Breaks { get; set; }
/// <summary>
/// All lines from the [Events] section which aren't handled in the encoding process yet.
/// These lines should be written out to the beatmap file on save or export.
/// </summary>
List<string> UnhandledEventLines { get; }
/// <summary>
/// Total amount of break time in the beatmap.
/// </summary>
@@ -9,8 +9,8 @@ namespace osu.Game.Configuration
{
protected override string Filename => base.Filename.Replace(".ini", ".dev.ini");
public DevelopmentOsuConfigManager(Storage storage, GameHost? host = null)
: base(storage, host)
public DevelopmentOsuConfigManager(Storage storage)
: base(storage)
{
}
}
@@ -0,0 +1,69 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.Settings.Sections.Audio;
namespace osu.Game.Configuration
{
public partial class MigrateNewAudioDialog : PopupDialog
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
public MigrateNewAudioDialog(bool wasAlreadyUsing)
{
Icon = FontAwesome.Regular.Bell;
if (wasAlreadyUsing)
{
HeaderText = @"New audio engine is now default!";
BodyText =
$"""
We recently added a new "Experimental Audio" backend for Windows users to reduce hitsound latency. Due to overwhelmingly positive feedback, this is now the default mode.
As you were already using this engine, your audio offset has been adjusted to account for an internal offset change (no intervention required).
If you have any issues, you can switch back to the legacy engine from settings via the "{AudioSettingsStrings.LegacyAudioLabel}" checkbox.
""";
}
else
{
HeaderText = @"New audio engine has been enabled";
BodyText =
$"""
We recently added a new "Experimental Audio" backend for Windows users to reduce hitsound latency. Due to overwhelmingly positive feedback, this is now the default mode.
If you have any issues, you can switch back to the legacy engine below, or at any time in settings via the "{AudioSettingsStrings.LegacyAudioLabel}" checkbox.
""";
MainContent.Add(new Container
{
Margin = new MarginPadding { Top = 20 },
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 400,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new LegacyAudioCheckbox(),
}
});
}
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton
{
Text = BeatmapOverlayStrings.UserContentConfirmButtonText,
},
};
}
}
}
+1 -45
View File
@@ -2,15 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Input.Handlers.Mouse;
using osu.Framework.Input.Handlers.Pen;
using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Game.Beatmaps.Drawables.Cards;
@@ -33,14 +30,9 @@ namespace osu.Game.Configuration
{
public class OsuConfigManager : IniConfigManager<OsuSetting>, IGameplaySettings
{
private readonly GameHost? host;
public OsuConfigManager(Storage storage, GameHost? host = null)
public OsuConfigManager(Storage storage)
: base(storage)
{
this.host = host;
Migrate();
}
protected override void InitialiseDefaults()
@@ -258,42 +250,6 @@ namespace osu.Game.Configuration
return false;
}
public void Migrate()
{
// arrives as 2020.123.0-lazer
string rawVersion = Get<string>(OsuSetting.Version);
if (rawVersion.Length < 6)
return;
string[] pieces = rawVersion.Split('.');
// on a fresh install or when coming from a non-release build, execution will end here.
// we don't want to run migrations in such cases.
if (!int.TryParse(pieces[0], out int year)) return;
if (!int.TryParse(pieces[1], out int monthDay)) return;
int combined = year * 10000 + monthDay;
if (combined < 20250214)
{
// UI scaling on mobile platforms has been internally adjusted such that 1x UI scale looks correctly zoomed in than before.
if (RuntimeInfo.IsMobile)
GetBindable<float>(OsuSetting.UIScale).SetDefault();
}
if (combined < 20250428)
{
// Pen tablet sensitivity is now separated from cursor sensitivity.
// Most users will want the default to be what they already had set on cursor sensitivity so let's transfer it.
var mouseHandler = host?.AvailableInputHandlers.OfType<MouseHandler>().SingleOrDefault();
var penHandler = host?.AvailableInputHandlers.OfType<PenHandler>().SingleOrDefault();
if (penHandler != null && mouseHandler != null && penHandler.Sensitivity.IsDefault)
penHandler.Sensitivity.Value = mouseHandler.Sensitivity.Value;
}
}
public override TrackedSettings CreateTrackedSettings()
{
return new TrackedSettings
+9 -1
View File
@@ -67,6 +67,14 @@ namespace osu.Game.Database
Configuration = new LegacySkinDecoder().Decode(skinStreamReader)
};
using var storyboardStream = base.GetFileContents(model, file);
if (storyboardStream == null)
return null;
using var storyboardStreamReader = new LineBufferedReader(storyboardStream);
var beatmapStoryboard = new LegacyStoryboardDecoder().Decode(storyboardStreamReader);
MutateBeatmap(model, playableBeatmap);
// Encode to legacy format
@@ -78,7 +86,7 @@ namespace osu.Game.Database
// If we don't do that, uploads to BSS may show changes where there are none.
sw.NewLine = "\r\n";
new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin).Encode(sw);
new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin, beatmapStoryboard).Encode(sw);
}
stream.Seek(0, SeekOrigin.Begin);
+32
View File
@@ -93,6 +93,14 @@ namespace osu.Game.Graphics
public static IconUsage EditorHitCircle => get(OsuIconMapping.EditorHitCircle);
public static IconUsage EditorSlider => get(OsuIconMapping.EditorSlider);
public static IconUsage EditorSpinner => get(OsuIconMapping.EditorSpinner);
public static IconUsage EditorHit => get(OsuIconMapping.EditorHit);
public static IconUsage EditorDrumRoll => get(OsuIconMapping.EditorDrumRoll);
public static IconUsage EditorSwell => get(OsuIconMapping.EditorSwell);
public static IconUsage EditorFruit => get(OsuIconMapping.EditorFruit);
public static IconUsage EditorJuiceStream => get(OsuIconMapping.EditorJuiceStream);
public static IconUsage EditorNote => get(OsuIconMapping.EditorNote);
public static IconUsage EditorHoldNote => get(OsuIconMapping.EditorHoldNote);
public static IconUsage EditorBananaShower => get(OsuIconMapping.EditorBananaShower);
public static IconUsage EditorGrid => get(OsuIconMapping.EditorGrid);
public static IconUsage EditorAddControlPoint => get(OsuIconMapping.EditorAddControlPoint);
public static IconUsage EditorConvertToStream => get(OsuIconMapping.EditorConvertToStream);
@@ -409,6 +417,30 @@ namespace osu.Game.Graphics
[Description(@"Editor/spinner")]
EditorSpinner,
[Description(@"Editor/hit")]
EditorHit,
[Description(@"Editor/drum-roll")]
EditorDrumRoll,
[Description(@"Editor/swell")]
EditorSwell,
[Description(@"Editor/fruit")]
EditorFruit,
[Description(@"Editor/juice-stream")]
EditorJuiceStream,
[Description(@"Editor/banana-shower")]
EditorBananaShower,
[Description(@"Editor/note")]
EditorNote,
[Description(@"Editor/hold-note")]
EditorHoldNote,
[Description(@"Editor/grid")]
EditorGrid,
@@ -21,9 +21,11 @@ using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Game.Database;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
namespace osu.Game.Graphics.UserInterfaceV2
{
@@ -67,6 +69,12 @@ namespace osu.Game.Graphics.UserInterfaceV2
/// </summary>
public LocalisableString PlaceholderText { get; init; }
/// <summary>
/// If set to <see langword="true"/>, the selector will display a button,
/// which when clicked, will change <see cref="Current"/>'s value to <see langword="null"/>.
/// </summary>
public bool AllowClear { get; init; }
public Container PreviewContainer { get; private set; } = null!;
private FormControlBackground background = null!;
@@ -180,7 +188,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
private void onFileSelected()
{
if (Current.Value != null)
if (Current.Value != null || AllowClear)
this.HidePopover();
initialChooserPath = Current.Value?.DirectoryName;
@@ -238,12 +246,12 @@ namespace osu.Game.Graphics.UserInterfaceV2
Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException();
protected virtual FileChooserPopover CreatePopover(string[] handledExtensions, Bindable<FileInfo?> current, string? chooserPath) =>
new FileChooserPopover(handledExtensions, current, chooserPath);
protected virtual FileChooserPopover CreatePopover(string[] handledExtensions, Bindable<FileInfo?> current, string? chooserPath, bool allowClear) =>
new FileChooserPopover(handledExtensions, current, chooserPath, allowClear);
public Popover GetPopover()
{
var popover = CreatePopover(handledExtensions, Current, initialChooserPath);
var popover = CreatePopover(handledExtensions, Current, initialChooserPath, AllowClear);
popoverState.UnbindBindings();
popoverState.BindTo(popover.State);
return popover;
@@ -258,7 +266,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected OsuFileSelector FileSelector;
public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> current, string? chooserPath)
public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> current, string? chooserPath, bool allowClear)
: base(false)
{
Child = new Container
@@ -267,9 +275,37 @@ namespace osu.Game.Graphics.UserInterfaceV2
// simplest solution to avoid underlying text to bleed through the bottom border
// https://github.com/ppy/osu/pull/30005#issuecomment-2378884430
Padding = new MarginPadding { Bottom = 1 },
Child = FileSelector = new OsuFileSelector(chooserPath, handledExtensions)
Children = new[]
{
RelativeSizeAxes = Axes.Both,
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = allowClear ? 50 : 0 },
Child = FileSelector = new OsuFileSelector(chooserPath, handledExtensions)
{
RelativeSizeAxes = Axes.Both,
},
},
allowClear
? new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Padding = new MarginPadding(5),
Child = new DangerousRoundedButton
{
Text = CommonStrings.ButtonsClear,
Action = () => OnFileSelected(null),
Enabled = { Value = current.Value != null },
Padding = new MarginPadding(5),
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Width = 60,
}
}
: Empty()
},
};
@@ -308,7 +344,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
};
}
protected virtual void OnFileSelected(FileInfo file) => current.Value = file;
protected virtual void OnFileSelected(FileInfo? file) => current.Value = file;
}
}
}
@@ -155,6 +155,8 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Alt, InputKey.Left }, GlobalAction.EditorSeekToPreviousBookmark),
new KeyBinding(new[] { InputKey.Alt, InputKey.Right }, GlobalAction.EditorSeekToNextBookmark),
new KeyBinding(new[] { InputKey.Control, InputKey.L }, GlobalAction.EditorDiscardUnsavedChanges),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.U }, GlobalAction.EditorSubmitBeatmap),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.O }, GlobalAction.EditorEditExternally),
};
private static IEnumerable<KeyBinding> editorTestPlayKeyBindings => new[]
@@ -528,6 +530,12 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.NextSkin))]
NextSkin,
[LocalisableDescription(typeof(EditorStrings), nameof(EditorStrings.SubmitBeatmap))]
EditorSubmitBeatmap,
[LocalisableDescription(typeof(EditorStrings), nameof(EditorStrings.EditExternally))]
EditorEditExternally
}
public enum GlobalActionCategory
@@ -100,19 +100,14 @@ namespace osu.Game.Localisation
public static LocalisableString AdjustBeatmapOffsetAutomaticallyTooltip => new TranslatableString(getKey(@"adjust_beatmap_offset_automatically_tooltip"), @"If enabled, the offset suggested from last play on a beatmap is automatically applied.");
/// <summary>
/// "Use experimental audio mode"
/// "Use legacy audio mode"
/// </summary>
public static LocalisableString WasapiLabel => new TranslatableString(getKey(@"wasapi_label"), @"Use experimental audio mode");
public static LocalisableString LegacyAudioLabel => new TranslatableString(getKey(@"legacy_audio_label"), @"Use legacy audio mode");
/// <summary>
/// "This will attempt to initialise the audio engine in a lower latency mode."
/// "Use this if you are experiencing audio issues. Note that audio latency will be higher when this is toggled on."
/// </summary>
public static LocalisableString WasapiTooltip => new TranslatableString(getKey(@"wasapi_tooltip"), @"This will attempt to initialise the audio engine in a lower latency mode.");
/// <summary>
/// "Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value."
/// </summary>
public static LocalisableString WasapiNotice => new TranslatableString(getKey(@"wasapi_notice"), @"Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value.");
public static LocalisableString LegacyAudioTooltip => new TranslatableString(getKey(@"legacy_audio_tooltip"), @"Use this if you are experiencing audio issues. Note that audio latency will be higher when this is toggled on.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
+31 -22
View File
@@ -42,8 +42,7 @@ namespace osu.Game.Localisation
/// <summary>
/// "If enabled, an &quot;Are you ready? 3, 2, 1, GO!&quot; countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so."
/// </summary>
public static LocalisableString CountdownDescription => new TranslatableString(getKey(@"countdown_description"),
@"If enabled, an ""Are you ready? 3, 2, 1, GO!"" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so.");
public static LocalisableString CountdownDescription => new TranslatableString(getKey(@"countdown_description"), @"If enabled, an ""Are you ready? 3, 2, 1, GO!"" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so.");
/// <summary>
/// "Countdown speed"
@@ -53,8 +52,7 @@ namespace osu.Game.Localisation
/// <summary>
/// "If the countdown sounds off-time, use this to make it appear one or more beats early."
/// </summary>
public static LocalisableString CountdownOffsetDescription =>
new TranslatableString(getKey(@"countdown_offset_description"), @"If the countdown sounds off-time, use this to make it appear one or more beats early.");
public static LocalisableString CountdownOffsetDescription => new TranslatableString(getKey(@"countdown_offset_description"), @"If the countdown sounds off-time, use this to make it appear one or more beats early.");
/// <summary>
/// "Countdown offset"
@@ -69,8 +67,7 @@ namespace osu.Game.Localisation
/// <summary>
/// "Allows storyboards to use the full screen space, rather than be confined to a 4:3 area."
/// </summary>
public static LocalisableString WidescreenSupportDescription =>
new TranslatableString(getKey(@"widescreen_support_description"), @"Allows storyboards to use the full screen space, rather than be confined to a 4:3 area.");
public static LocalisableString WidescreenSupportDescription => new TranslatableString(getKey(@"widescreen_support_description"), @"Allows storyboards to use the full screen space, rather than be confined to a 4:3 area.");
/// <summary>
/// "Epilepsy warning"
@@ -80,8 +77,7 @@ namespace osu.Game.Localisation
/// <summary>
/// "Recommended if the storyboard or video contain scenes with rapidly flashing colours."
/// </summary>
public static LocalisableString EpilepsyWarningDescription =>
new TranslatableString(getKey(@"epilepsy_warning_description"), @"Recommended if the storyboard or video contain scenes with rapidly flashing colours.");
public static LocalisableString EpilepsyWarningDescription => new TranslatableString(getKey(@"epilepsy_warning_description"), @"Recommended if the storyboard or video contain scenes with rapidly flashing colours.");
/// <summary>
/// "Letterbox during breaks"
@@ -91,8 +87,7 @@ namespace osu.Game.Localisation
/// <summary>
/// "Adds horizontal letterboxing to give a cinematic look during breaks."
/// </summary>
public static LocalisableString LetterboxDuringBreaksDescription =>
new TranslatableString(getKey(@"letterbox_during_breaks_description"), @"Adds horizontal letterboxing to give a cinematic look during breaks.");
public static LocalisableString LetterboxDuringBreaksDescription => new TranslatableString(getKey(@"letterbox_during_breaks_description"), @"Adds horizontal letterboxing to give a cinematic look during breaks.");
/// <summary>
/// "Samples match playback rate"
@@ -102,8 +97,7 @@ namespace osu.Game.Localisation
/// <summary>
/// "When enabled, all samples will speed up or slow down when rate-changing mods are enabled."
/// </summary>
public static LocalisableString SamplesMatchPlaybackRateDescription => new TranslatableString(getKey(@"samples_match_playback_rate_description"),
@"When enabled, all samples will speed up or slow down when rate-changing mods are enabled.");
public static LocalisableString SamplesMatchPlaybackRateDescription => new TranslatableString(getKey(@"samples_match_playback_rate_description"), @"When enabled, all samples will speed up or slow down when rate-changing mods are enabled.");
/// <summary>
/// "The size of all hit objects"
@@ -123,8 +117,7 @@ namespace osu.Game.Localisation
/// <summary>
/// "The harshness of hit windows and difficulty of special objects (ie. spinners)"
/// </summary>
public static LocalisableString OverallDifficultyDescription =>
new TranslatableString(getKey(@"overall_difficulty_description"), @"The harshness of hit windows and difficulty of special objects (ie. spinners)");
public static LocalisableString OverallDifficultyDescription => new TranslatableString(getKey(@"overall_difficulty_description"), @"The harshness of hit windows and difficulty of special objects (ie. spinners)");
/// <summary>
/// "Tick Rate"
@@ -134,8 +127,7 @@ namespace osu.Game.Localisation
/// <summary>
/// "Determines how many &quot;ticks&quot; are generated within long hit objects. A tick rate of 1 will generate ticks on each beat, 2 would be twice per beat, etc."
/// </summary>
public static LocalisableString TickRateDescription => new TranslatableString(getKey(@"tick_rate_description"),
@"Determines how many ""ticks"" are generated within long hit objects. A tick rate of 1 will generate ticks on each beat, 2 would be twice per beat, etc.");
public static LocalisableString TickRateDescription => new TranslatableString(getKey(@"tick_rate_description"), @"Determines how many ""ticks"" are generated within long hit objects. A tick rate of 1 will generate ticks on each beat, 2 would be twice per beat, etc.");
/// <summary>
/// "Base Velocity"
@@ -145,8 +137,7 @@ namespace osu.Game.Localisation
/// <summary>
/// "The base velocity of the beatmap, affecting things like slider velocity and scroll speed in some rulesets."
/// </summary>
public static LocalisableString BaseVelocityDescription => new TranslatableString(getKey(@"base_velocity_description"),
@"The base velocity of the beatmap, affecting things like slider velocity and scroll speed in some rulesets.");
public static LocalisableString BaseVelocityDescription => new TranslatableString(getKey(@"base_velocity_description"), @"The base velocity of the beatmap, affecting things like slider velocity and scroll speed in some rulesets.");
/// <summary>
/// "Metadata"
@@ -188,6 +179,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString AudioTrack => new TranslatableString(getKey(@"audio_track"), @"Audio Track");
/// <summary>
/// "Video"
/// </summary>
public static LocalisableString Video => new TranslatableString(getKey(@"video"), @"Video");
/// <summary>
/// "The video will be used instead of the static background, if present. Beatmap downloads are offered both with and without video, so if adding a video, a matching background should also be provided."
/// </summary>
public static LocalisableString VideoHint => new TranslatableString(getKey(@"video_hint"), @"The video will be used instead of the static background, if present. Beatmap downloads are offered both with and without video, so if adding a video, a matching background should also be provided.");
/// <summary>
/// "Custom sample sets"
/// </summary>
@@ -198,6 +199,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString ClickToSelectTrack => new TranslatableString(getKey(@"click_to_select_track"), @"Click to select a track");
/// <summary>
/// "Click to select a video"
/// </summary>
public static LocalisableString ClickToSelectVideo => new TranslatableString(getKey(@"click_to_select_video"), @"Click to select a video");
/// <summary>
/// "Click to select a background image"
/// </summary>
@@ -221,14 +227,12 @@ namespace osu.Game.Localisation
/// <summary>
/// "Sync metadata with all difficulties"
/// </summary>
public static LocalisableString SyncMetadataWithAllDifficulties =>
new TranslatableString(getKey(@"sync_metadata_with_all_difficulties"), @"Sync metadata with all difficulties");
public static LocalisableString SyncMetadataWithAllDifficulties => new TranslatableString(getKey(@"sync_metadata_with_all_difficulties"), @"Sync metadata with all difficulties");
/// <summary>
/// "Copies artist, title, source, and tags to all difficulties."
/// </summary>
public static LocalisableString SyncMetadataWithAllDifficultiesTooltip => new TranslatableString(getKey(@"sync_metadata_with_all_difficulties_tooltip"),
@"Copies artist, title, source, and tags to all difficulties.");
public static LocalisableString SyncMetadataWithAllDifficultiesTooltip => new TranslatableString(getKey(@"sync_metadata_with_all_difficulties_tooltip"), @"Copies artist, title, source, and tags to all difficulties.");
/// <summary>
/// "Ruleset ({0})"
@@ -260,6 +264,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString DragToSetBackground => new TranslatableString(getKey(@"drag_to_set_background"), @"Drag image here to set beatmap background!");
/// <summary>
/// "Drag video here to set beatmap video!"
/// </summary>
public static LocalisableString DragToSetVideo => new TranslatableString(getKey(@"drag_to_set_video"), @"Drag video here to set beatmap video!");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
@@ -0,0 +1,20 @@
// 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 MessagePack;
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// User requests to change their slot in the room.
/// </summary>
[MessagePackObject]
public class ChangeSlotRequest : MatchUserRequest
{
/// <summary>
/// The zero-based ID of the desired slot.
/// </summary>
[Key(0)]
public byte SlotID { get; set; }
}
}
@@ -18,6 +18,7 @@ namespace osu.Game.Online.Multiplayer
[Union(0, typeof(TeamVersusRoomState))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
[Union(1, typeof(MatchmakingRoomState))]
[Union(2, typeof(RankedPlayRoomState))]
[Union(3, typeof(StandardMatchRoomState))]
public abstract class MatchRoomState
{
}
@@ -7,22 +7,20 @@ using MessagePack;
namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus
{
[MessagePackObject]
public class TeamVersusRoomState : MatchRoomState
public class TeamVersusRoomState : StandardMatchRoomState
{
[Key(0)]
public List<MultiplayerTeam> Teams { get; set; } = new List<MultiplayerTeam>();
[Key(1)]
public bool Locked { get; set; }
public static TeamVersusRoomState CreateDefault() =>
public static TeamVersusRoomState CreateDefault(byte? maxParticipants = null) =>
new TeamVersusRoomState
{
Teams =
{
new MultiplayerTeam { ID = 0, Name = "Team Red" },
new MultiplayerTeam { ID = 1, Name = "Team Blue" },
}
},
Slots = maxParticipants == null ? null : new int?[maxParticipants.Value]
};
}
}
@@ -23,6 +23,7 @@ namespace osu.Game.Online.Multiplayer
[Union(4, typeof(RankedPlayCardHandReplayRequest))]
[Union(5, typeof(SetLockStateRequest))]
[Union(6, typeof(RollRequest))]
[Union(7, typeof(ChangeSlotRequest))]
public abstract class MatchUserRequest
{
}
@@ -403,8 +403,9 @@ namespace osu.Game.Online.Multiplayer
/// <param name="queueMode">The new queue mode, if any.</param>
/// <param name="autoStartDuration">The new auto-start countdown duration, if any.</param>
/// <param name="autoSkip">The new auto-skip setting.</param>
/// <param name="maxParticipants">The new participant count limit, if any.</param>
public Task ChangeSettings(Optional<string> name = default, Optional<string> password = default, Optional<MatchType> matchType = default, Optional<QueueMode> queueMode = default,
Optional<TimeSpan> autoStartDuration = default, Optional<bool> autoSkip = default)
Optional<TimeSpan> autoStartDuration = default, Optional<bool> autoSkip = default, Optional<byte?> maxParticipants = default)
{
if (Room == null)
throw new InvalidOperationException("Must be joined to a match to change settings.");
@@ -416,7 +417,8 @@ namespace osu.Game.Online.Multiplayer
MatchType = matchType.GetOr(Room.Settings.MatchType),
QueueMode = queueMode.GetOr(Room.Settings.QueueMode),
AutoStartDuration = autoStartDuration.GetOr(Room.Settings.AutoStartDuration),
AutoSkip = autoSkip.GetOr(Room.Settings.AutoSkip)
AutoSkip = autoSkip.GetOr(Room.Settings.AutoSkip),
MaxParticipants = maxParticipants.GetOr(Room.Settings.MaxParticipants),
});
}
@@ -32,6 +32,9 @@ namespace osu.Game.Online.Multiplayer
[Key(6)]
public bool AutoSkip { get; set; }
[Key(7)]
public byte? MaxParticipants { get; set; }
[IgnoreMember]
public bool AutoStartEnabled => AutoStartDuration != TimeSpan.Zero;
@@ -47,6 +50,7 @@ namespace osu.Game.Online.Multiplayer
QueueMode = room.QueueMode;
AutoStartDuration = room.AutoStartDuration;
AutoSkip = room.AutoSkip;
MaxParticipants = room.MaxParticipants;
}
public bool Equals(MultiplayerRoomSettings? other)
@@ -60,7 +64,8 @@ namespace osu.Game.Online.Multiplayer
&& MatchType == other.MatchType
&& QueueMode == other.QueueMode
&& AutoStartDuration == other.AutoStartDuration
&& AutoSkip == other.AutoSkip;
&& AutoSkip == other.AutoSkip
&& MaxParticipants == other.MaxParticipants;
}
public override string ToString() => $"Name:{Name}"
@@ -69,6 +74,7 @@ namespace osu.Game.Online.Multiplayer
+ $" Item:{PlaylistItemId}"
+ $" Queue:{QueueMode}"
+ $" Start:{AutoStartDuration}"
+ $" AutoSkip:{AutoSkip}";
+ $" AutoSkip:{AutoSkip}"
+ $" MaxParticipants:{MaxParticipants?.ToString() ?? "no limit"}";
}
}
@@ -10,14 +10,13 @@ namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// <para>
/// If <see langword="true"/>, <see cref="MultiplayerRoomUserRole.Player"/>s will not be able to change teams by themselves in the room,
/// If <see langword="true"/>, <see cref="MultiplayerRoomUserRole.Player"/>s will not be able to change teams and slots by themselves in the room,
/// only <see cref="MultiplayerRoomUserRole.Referee"/>s will be able to change teams for the <see cref="MultiplayerRoomUserRole.Player"/>s.
/// </para>
/// <para>
/// If <see langword="false"/>, any user can change their team in the room.
/// If <see langword="false"/>, any user can change their team and slot in the room.
/// </para>
/// </summary>
// TODO: mention slots as well when slots are reimplemented
[Key(0)]
public bool Locked { get; set; }
}
@@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using MessagePack;
namespace osu.Game.Online.Multiplayer
{
[MessagePackObject]
public class StandardMatchRoomState : MatchRoomState
{
/// <summary>
/// Whether the room is currently locked.
/// When locked, changes to slots (and teams, in team versus) cannot be performed by anyone but room referees.
/// </summary>
[Key(1)]
public bool Locked { get; set; }
/// <summary>
/// The state of slots in the room.
/// Linked to <see cref="MultiplayerRoomSettings.MaxParticipants"/>.
/// <list type="bullet">
/// <item>When <see cref="MultiplayerRoomSettings.MaxParticipants"/> is <see langword="null"/>, this property is also <see langword="null"/>.</item>
/// <item>
/// When <see cref="MultiplayerRoomSettings.MaxParticipants"/> is not <see langword="null"/>, this property is an array of that length.
/// The items of that array represent either an empty slot (represented by <see langword="null"/>),
/// or an user occupying that slot (represented by the ID of the relevant user).
/// </item>
/// </list>
/// </summary>
[Key(2)]
public int?[]? Slots { get; set; }
public static StandardMatchRoomState Create(byte? maxParticipants = null) =>
new StandardMatchRoomState
{
Slots = maxParticipants == null ? null : new int?[maxParticipants.Value]
};
}
}
+4 -3
View File
@@ -119,7 +119,7 @@ namespace osu.Game.Online.Rooms
/// <summary>
/// The maximum number of users allowed in the room.
/// </summary>
public int? MaxParticipants
public byte? MaxParticipants
{
get => maxParticipants;
set => SetField(ref maxParticipants, value);
@@ -297,8 +297,8 @@ namespace osu.Game.Online.Rooms
[JsonProperty("ends_at")]
private DateTimeOffset? endDate;
// Not yet serialised (not implemented).
private int? maxParticipants;
[JsonProperty("max_participants")]
private byte? maxParticipants;
[JsonProperty("participant_count")]
private int participantCount;
@@ -365,6 +365,7 @@ namespace osu.Game.Online.Rooms
QueueMode = room.Settings.QueueMode;
AutoStartDuration = room.Settings.AutoStartDuration;
AutoSkip = room.Settings.AutoSkip;
MaxParticipants = room.Settings.MaxParticipants;
Host = room.Host != null ? new APIUser { Id = room.Host.UserID } : null;
Playlist = room.Playlist.Select(p => new PlaylistItem(p)).ToArray();
}
@@ -24,6 +24,7 @@ namespace osu.Game.Online
internal static readonly IReadOnlyList<(Type derivedType, Type baseType)> BASE_TYPE_MAPPING = new[]
{
// multiplayer
(typeof(ChangeSlotRequest), typeof(MatchUserRequest)),
(typeof(ChangeTeamRequest), typeof(MatchUserRequest)),
(typeof(StartMatchCountdownRequest), typeof(MatchUserRequest)),
(typeof(StopCountdownRequest), typeof(MatchUserRequest)),
@@ -32,6 +33,7 @@ namespace osu.Game.Online
(typeof(CountdownStartedEvent), typeof(MatchServerEvent)),
(typeof(CountdownStoppedEvent), typeof(MatchServerEvent)),
(typeof(RollEvent), typeof(MatchServerEvent)),
(typeof(StandardMatchRoomState), typeof(MatchRoomState)),
(typeof(TeamVersusRoomState), typeof(MatchRoomState)),
(typeof(TeamVersusUserState), typeof(MatchUserState)),
(typeof(MatchStartCountdown), typeof(MultiplayerCountdown)),
+70 -1
View File
@@ -26,6 +26,8 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Input.Handlers.Mouse;
using osu.Framework.Input.Handlers.Pen;
using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Localisation;
using osu.Framework.Logging;
@@ -159,6 +161,8 @@ namespace osu.Game
private OnScreenDisplay onScreenDisplay;
private DialogOverlay dialogOverlay;
[Resolved]
private FrameworkConfigManager frameworkConfig { get; set; }
@@ -1045,6 +1049,7 @@ namespace osu.Game
{ FrameworkSetting.VolumeUniversal, 0.6 },
{ FrameworkSetting.VolumeMusic, 0.6 },
{ FrameworkSetting.VolumeEffect, 0.6 },
{ FrameworkSetting.AudioUseExperimentalWasapi, true },
};
}
@@ -1234,7 +1239,7 @@ namespace osu.Game
}, rightFloatingOverlayContent.Add, true);
loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile<IDialogOverlay>(new DialogOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile<IDialogOverlay>(dialogOverlay = new DialogOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile(new MedalOverlay(), topMostOverlayContent.Add);
loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);
@@ -1293,6 +1298,70 @@ namespace osu.Game
// Importantly, this should be run after binding PostNotification to the import handlers so they can present the import after game startup.
handleStartupImport();
applyConfigMigrations();
// finally, update the version stored to the configuration.
// this MUST happen after `applyConfigMigrations()` call, as it relies on comparing the previous version.
// debug / local compilations will reset to a non-release string.
LocalConfig.SetValue(OsuSetting.Version, Version);
}
/// <summary>
/// Apply any migrations to configuration.
/// </summary>
/// <remarks>
/// For database migrations, see <see cref="RealmAccess.applyMigrationsForVersion"/>.
/// </remarks>
private void applyConfigMigrations()
{
// arrives as 2020.123.0-lazer
string rawVersion = LocalConfig.Get<string>(OsuSetting.Version);
if (rawVersion.Length < 6)
return;
string[] pieces = rawVersion.Split('.');
// on a fresh install or when coming from a non-release build, execution will end here.
// we don't want to run migrations in such cases.
if (!int.TryParse(pieces[0], out int year)) return;
if (!int.TryParse(pieces[1], out int monthDay)) return;
int combined = year * 10000 + monthDay;
if (combined < 20250214)
{
// UI scaling on mobile platforms has been internally adjusted such that 1x UI scale looks correctly zoomed in than before.
if (RuntimeInfo.IsMobile)
LocalConfig.GetBindable<float>(OsuSetting.UIScale).SetDefault();
}
if (combined < 20260520)
{
// Pen tablet sensitivity is now separated from cursor sensitivity.
// Most users will want the default to be what they already had set on cursor sensitivity so let's transfer it.
var mouseHandler = Host?.AvailableInputHandlers.OfType<MouseHandler>().SingleOrDefault();
var penHandler = Host?.AvailableInputHandlers.OfType<PenHandler>().SingleOrDefault();
if (penHandler != null && mouseHandler != null && penHandler.Sensitivity.IsDefault)
penHandler.Sensitivity.Value = mouseHandler.Sensitivity.Value;
}
if (combined < 20260521 && RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
{
bool wasAlreadyUsing = Audio.UseExperimentalWasapi.Value;
// see application of FramedBeatmapClock.WINDOWS_EXPERIMENTAL_AUDIO_OFFSET in FramedBeatmapClock.
// this basically undoes this new offset assuming that users which have been using this setting for a while
// already have had things tuned.
if (wasAlreadyUsing)
LocalConfig.SetValue(OsuSetting.AudioOffset, LocalConfig.Get<double>(OsuSetting.AudioOffset) - FramedBeatmapClock.WINDOWS_EXPERIMENTAL_AUDIO_OFFSET);
Audio.UseExperimentalWasapi.Value = true;
dialogOverlay.Push(new MigrateNewAudioDialog(wasAlreadyUsing));
}
}
private void handleBackButton()
+3 -3
View File
@@ -541,8 +541,8 @@ namespace osu.Game
Storage ??= host.Storage;
LocalConfig ??= UseDevelopmentServer
? new DevelopmentOsuConfigManager(Storage, host)
: new OsuConfigManager(Storage, host);
? new DevelopmentOsuConfigManager(Storage)
: new OsuConfigManager(Storage);
host.ExceptionThrown += onExceptionThrown;
}
@@ -590,7 +590,7 @@ namespace osu.Game
/// <param name="path">The path to migrate to.</param>
/// <returns>Whether migration succeeded to completion. If <c>false</c>, some files were left behind.</returns>
/// <exception cref="TimeoutException"></exception>
public bool Migrate(string path)
public bool MigrateUserData(string path)
{
Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""...");
@@ -178,6 +178,7 @@ namespace osu.Game.Overlays.Dashboard.CurrentlyOnline
break;
case UserActivity.InSoloGame:
case UserActivity.PlayingDailyChallenge:
case UserActivity.InMultiplayerGame:
case UserActivity.InPlaylistGame:
userPanel.CanSpectate.Value = true;
+10 -2
View File
@@ -14,7 +14,6 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osuTK;
@@ -41,6 +40,8 @@ namespace osu.Game.Overlays.Dialog
private readonly TextFlowContainer header;
private readonly TextFlowContainer body;
public Container MainContent { get; private set; }
private bool actionInvoked;
public IconUsage Icon
@@ -222,6 +223,13 @@ namespace osu.Game.Overlays.Dialog
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 15 },
},
MainContent = new Container
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
buttonsContainer = new FillFlowContainer<PopupDialogButton>
{
Anchor = Anchor.TopCentre,
@@ -243,7 +251,7 @@ namespace osu.Game.Overlays.Dialog
}
[BackgroundDependencyLoader]
private void load(AudioManager audio, OsuColour colours)
private void load(AudioManager audio)
{
flashSample = audio.Samples.Get(@"UI/default-select-disabled");
}
+11 -2
View File
@@ -13,6 +13,7 @@ using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
namespace osu.Game.Overlays
{
@@ -28,8 +29,14 @@ namespace osu.Game.Overlays
public PopupDialog CurrentDialog { get; private set; }
public override bool IsPresent => Scheduler.HasPendingTasks
|| dialogContainer.Children.Count > 0;
public override bool IsPresent => (Scheduler.HasPendingTasks || dialogContainer.Children.Count > 0)
// The following line ensures that dialogs are not presented while the dialog overlay
// cannot be displayed. This is due to the `Schedule` usage inside `Push()`.
//
// Without this, a dialog pushed during disabled overlay activation mode would be presented,
// but immediately dismissed without ever being seen by the user (see
// https://github.com/ppy/osu/blob/ce5e54c9d27b17d460d99e774de502f9480fb710/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs#L131-L136).
&& OverlayActivationMode.Value == OverlayActivation.All;
[CanBeNull]
private IDisposable duckOperation;
@@ -77,6 +84,7 @@ namespace osu.Game.Overlays
return;
}
Logger.Log($"{nameof(DialogOverlay)}: Showing dialog {dialog}");
dialogContainer.Add(dialog);
Show();
@@ -98,6 +106,7 @@ namespace osu.Game.Overlays
// Handle the case where the dialog is the currently displayed dialog.
// In this scenario, the overlay itself should also be hidden.
Hide();
Logger.Log($"{nameof(DialogOverlay)}: Dismissing dialog {dialog}");
CurrentDialog = null;
}
}
@@ -24,9 +24,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
private AudioDeviceDropdown dropdown = null!;
private FormCheckBox? wasapiExperimental;
private readonly Bindable<SettingsNote.Data?> wasapiExperimentalNote = new Bindable<SettingsNote.Data?>();
private FormCheckBox? legacyAudio;
[BackgroundDependencyLoader]
private void load()
@@ -44,18 +42,12 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
{
Add(new SettingsItemV2(wasapiExperimental = new FormCheckBox
Add(new SettingsItemV2(legacyAudio = new LegacyAudioCheckbox())
{
Caption = AudioSettingsStrings.WasapiLabel,
HintText = AudioSettingsStrings.WasapiTooltip,
Current = audio.UseExperimentalWasapi,
})
{
Keywords = new[] { "wasapi", "latency", "exclusive" },
Note = { BindTarget = wasapiExperimentalNote },
Keywords = new[] { "wasapi", "latency", "exclusive", "legacy", "experimental" },
});
wasapiExperimental.Current.ValueChanged += _ => onDeviceChanged(string.Empty);
legacyAudio.Current.ValueChanged += _ => onDeviceChanged(string.Empty);
}
audio.OnNewDevice += onDeviceChanged;
@@ -65,18 +57,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
onDeviceChanged(string.Empty);
}
private void onDeviceChanged(string _)
{
updateItems();
if (wasapiExperimental != null)
{
if (wasapiExperimental.Current.Value)
wasapiExperimentalNote.Value = new SettingsNote.Data(AudioSettingsStrings.WasapiNotice, SettingsNote.Type.Warning);
else
wasapiExperimentalNote.Value = null;
}
}
private void onDeviceChanged(string _) => Scheduler.AddOnce(updateItems);
private void updateItems()
{
@@ -117,4 +98,37 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
=> string.IsNullOrEmpty(item) ? CommonStrings.Default : base.GenerateItemText(item);
}
}
public partial class LegacyAudioCheckbox : FormCheckBox
{
private Bindable<bool> configExperimentalAudio = null!;
public LegacyAudioCheckbox()
{
Caption = AudioSettingsStrings.LegacyAudioLabel;
HintText = AudioSettingsStrings.LegacyAudioTooltip;
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
configExperimentalAudio = audio.UseExperimentalWasapi.GetBoundCopy();
}
protected override void LoadComplete()
{
base.LoadComplete();
// Manual two-way binding because we're inverting what the framework exposes.
Current.ValueChanged += legacy =>
{
configExperimentalAudio.Value = !legacy.NewValue;
};
configExperimentalAudio.BindValueChanged(experimental =>
{
Current.Value = !experimental.NewValue;
}, true);
}
}
}
@@ -102,7 +102,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
});
}
protected virtual bool PerformMigration() => game?.Migrate(destination.FullName) != false;
protected virtual bool PerformMigration() => game?.MigrateUserData(destination.FullName) != false;
public override void OnEntering(ScreenTransitionEvent e)
{

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