1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 13:22:55 +08:00

Merge branch 'master' into fix-spectator-seeks

This commit is contained in:
Dean Herbert 2022-04-06 12:58:43 +09:00 committed by GitHub
commit a3695c7e97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
184 changed files with 4811 additions and 1602 deletions

2
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,2 @@
# Normalize all the line endings
32a74f95a5c80a0ed18e693f13a47522099df5c3

View File

@ -9,7 +9,7 @@ body:
Important to note that your issue may have already been reported before. Please check:
- Pinned issues, at the top of https://github.com/ppy/osu/issues.
- Current open `priority:0` issues, filterable [here](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0).
- And most importantly, search for your issue. If you find that it already exists, respond with a reaction or add any further information that may be helpful.
- And most importantly, search for your issue both in the [issue listing](https://github.com/ppy/osu/issues) and the [Q&A discussion listing](https://github.com/ppy/osu/discussions/categories/q-a). If you find that it already exists, respond with a reaction or add any further information that may be helpful.
- type: dropdown
attributes:
@ -48,20 +48,28 @@ body:
Attaching log files is required for every reported bug. See instructions below on how to find them.
**Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead.
### Desktop platforms
If the game has not yet been closed since you found the bug:
1. Head on to game settings and click on "Open osu! folder"
2. Then open the `logs` folder located there
**Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead.
The default places to find the logs are as follows:
The default places to find the logs on desktop platforms are as follows:
- `%AppData%/osu/logs` *on Windows*
- `~/.local/share/osu/logs` *on Linux & macOS*
- `Android/data/sh.ppy.osulazer/files/logs` *on Android*
- *On iOS*, they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
If you have selected a custom location for the game files, you can find the `logs` folder there.
### Mobile platforms
The places to find the logs on mobile platforms are as follows:
- *On Android*, navigate to `Android/data/sh.ppy.osulazer/files/logs` using a file browser app.
- *On iOS*, connect your device to a PC and copy the `logs` directory from the app's document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
---
After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below.
- type: textarea

View File

@ -8,17 +8,17 @@ GEM
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.553.0)
aws-sdk-core (3.126.0)
aws-partitions (1.570.0)
aws-sdk-core (3.130.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.54.0)
aws-sdk-core (~> 3, >= 3.126.0)
aws-sdk-kms (1.55.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.112.0)
aws-sdk-core (~> 3, >= 3.126.0)
aws-sdk-s3 (1.113.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0)
@ -36,8 +36,8 @@ GEM
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
emoji_regex (3.2.3)
excon (0.91.0)
faraday (1.9.3)
excon (0.92.1)
faraday (1.10.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@ -66,7 +66,7 @@ GEM
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.204.2)
fastlane (2.205.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@ -130,10 +130,10 @@ GEM
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.5.0)
faraday (>= 0.17.3, < 2.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.36.0)
google-cloud-storage (1.36.1)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
@ -141,8 +141,8 @@ GEM
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.1.0)
faraday (>= 0.17.3, < 2.0)
googleauth (1.1.2)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
@ -152,7 +152,7 @@ GEM
http-cookie (1.0.4)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.5.0)
jmespath (1.6.1)
json (2.6.1)
jwt (2.3.0)
memoist (0.16.2)
@ -182,9 +182,9 @@ GEM
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.16.0)
signet (0.16.1)
addressable (~> 2.8)
faraday (>= 0.17.3, < 2.0)
faraday (>= 0.17.5, < 3.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
@ -205,7 +205,7 @@ GEM
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8)
unf_ext (0.0.8.1)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)

View File

@ -3,22 +3,14 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.EmptyFreeform.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.EmptyFreeform.Mods
{
public class EmptyFreeformModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo
{
User = new APIUser { Username = "sample" },
},
Replay = new EmptyFreeformAutoGenerator(beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new EmptyFreeformAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "sample" });
}
}

View File

@ -3,22 +3,14 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Pippidon.Replays;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Pippidon.Mods
{
public class PippidonModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo
{
User = new APIUser { Username = "sample" },
},
Replay = new PippidonAutoGenerator(beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new PippidonAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "sample" });
}
}

View File

@ -1,24 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.EmptyScrolling.Replays;
using osu.Game.Scoring;
using System.Collections.Generic;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.EmptyScrolling.Replays;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.EmptyScrolling.Mods
{
public class EmptyScrollingModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo
{
User = new APIUser { Username = "sample" },
},
Replay = new EmptyScrollingAutoGenerator(beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new EmptyScrollingAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "sample" });
}
}

View File

@ -3,22 +3,14 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Pippidon.Replays;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Pippidon.Mods
{
public class PippidonModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo
{
User = new APIUser { Username = "sample" },
},
Replay = new PippidonAutoGenerator(beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new PippidonAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "sample" });
}
}

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.304.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.314.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.405.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.404.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -27,7 +27,7 @@ namespace osu.Game.Benchmarks
storage = new TemporaryNativeStorage("realm-benchmark");
storage.DeleteDirectory(string.Empty);
realm = new RealmAccess(storage, "client");
realm = new RealmAccess(storage, OsuGameBase.CLIENT_DATABASE_FILENAME);
realm.Run(r =>
{

View File

@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
[TestCase(4.0505463516206195d, "diffcalc-test")]
public void Test(double expected, string name)
=> base.Test(expected, name);
[TestCase(4.0505463516206195d, 127, "diffcalc-test")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(5.1696411260785498d, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new CatchModDoubleTime());
[TestCase(5.1696411260785498d, 127, "diffcalc-test")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new CatchModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset().RulesetInfo, beatmap);

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage)
// Bypass LegacySkinResourceStore to avoid returning null for retrieving files due to bad skin info (SkinInfo.Files = null).
: base(skin, storage, null, "skin.ini")
: base(skin, null, storage)
{
}
}

View File

@ -3,19 +3,14 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!salad" } },
Replay = new CatchAutoGenerator(beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "osu!salad" });
}
}

View File

@ -3,20 +3,15 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModCinema : ModCinema<CatchHitObject>
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!salad" } },
Replay = new CatchAutoGenerator(beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "osu!salad" });
}
}

View File

@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Mania.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase(2.3449735700206298d, "diffcalc-test")]
public void Test(double expected, string name)
=> base.Test(expected, name);
[TestCase(2.3449735700206298d, 151, "diffcalc-test")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(2.7879104989252959d, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new ManiaModDoubleTime());
[TestCase(2.7879104989252959d, 151, "diffcalc-test")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset().RulesetInfo, beatmap);

View File

@ -3,20 +3,15 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!topus" } },
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), new ModCreatedUser { Username = "osu!topus" });
}
}

View File

@ -3,21 +3,16 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModCinema : ModCinema<ManiaHitObject>
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!topus" } },
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), new ModCreatedUser { Username = "osu!topus" });
}
}

View File

@ -6,18 +6,18 @@ using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModAimAssist : OsuModTestScene
public class TestSceneOsuModMagnetised : OsuModTestScene
{
[TestCase(0.1f)]
[TestCase(0.5f)]
[TestCase(1)]
public void TestAimAssist(float strength)
public void TestMagnetised(float strength)
{
CreateModTest(new ModTestData
{
Mod = new OsuModAimAssist
Mod = new OsuModMagnetised
{
AssistStrength = { Value = strength },
AttractionStrength = { Value = strength },
},
PassCondition = () => true,
Autoplay = false,

View File

@ -15,15 +15,20 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.6972307565739273d, "diffcalc-test")]
[TestCase(1.4484754139145539d, "zero-length-sliders")]
public void Test(double expected, string name)
=> base.Test(expected, name);
[TestCase(6.6972307565739273d, 206, "diffcalc-test")]
[TestCase(1.4484754139145539d, 45, "zero-length-sliders")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(8.9382559208689809d, "diffcalc-test")]
[TestCase(1.7548875851757628d, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new OsuModDoubleTime());
[TestCase(8.9382559208689809d, 206, "diffcalc-test")]
[TestCase(1.7548875851757628d, 45, "zero-length-sliders")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
[TestCase(6.6972307218715166d, 239, "diffcalc-test")]
[TestCase(1.4484754139145537d, 54, "zero-length-sliders")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap);

View File

@ -4,7 +4,6 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Beatmaps;
@ -13,7 +12,6 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Tests.Visual;
using osuTK;
@ -67,11 +65,8 @@ namespace osu.Game.Rulesets.Osu.Tests
private class TestAutoMod : OsuModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "Autoplay" } },
Replay = new MissingAutoGenerator(beatmap, mods).Generate()
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new MissingAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
}
private class MissingAutoGenerator : OsuAutoGeneratorBase

View File

@ -61,10 +61,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate;
int maxCombo = beatmap.HitObjects.Count;
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
int maxCombo = beatmap.GetMaxCombo();
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation;
public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModAimAssist) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised) };
public bool PerformFail() => false;

View File

@ -5,21 +5,16 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAutoplay : ModAutoplay
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "Autoplay" } },
Replay = new OsuAutoGenerator(beatmap, mods).Generate()
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
}
}

View File

@ -5,22 +5,17 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModCinema : ModCinema<OsuHitObject>
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "Autoplay" } },
Replay = new OsuAutoGenerator(beatmap, mods).Generate()
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
}
}

View File

@ -16,20 +16,20 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
internal class OsuModAimAssist : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
internal class OsuModMagnetised : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => "Aim Assist";
public override string Acronym => "AA";
public override IconUsage? Icon => FontAwesome.Solid.MousePointer;
public override string Name => "Magnetised";
public override string Acronym => "MG";
public override IconUsage? Icon => FontAwesome.Solid.Magnet;
public override ModType Type => ModType.Fun;
public override string Description => "No need to chase the circle the circle chases you!";
public override string Description => "No need to chase the circles your cursor is a magnet!";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax) };
private IFrameStableClock gameplayClock;
[SettingSource("Assist strength", "How much this mod will assist you.", 0)]
public BindableFloat AssistStrength { get; } = new BindableFloat(0.5f)
[SettingSource("Attraction strength", "How strong the pull is.", 0)]
public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f)
{
Precision = 0.05f,
MinValue = 0.05f,
@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Mods
private void easeTo(DrawableHitObject hitObject, Vector2 destination)
{
double dampLength = Interpolation.Lerp(3000, 40, AssistStrength.Value);
double dampLength = Interpolation.Lerp(3000, 40, AttractionStrength.Value);
float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime);
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime);

View File

@ -4,19 +4,14 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Osu.Utils;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
@ -28,12 +23,6 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "It never gets boring!";
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE / 2;
/// <summary>
/// Number of previous hitobjects to be shifted together when another object is being moved.
/// </summary>
private const int preceding_hitobjects_to_shift = 10;
private Random? rng;
@ -42,330 +31,33 @@ namespace osu.Game.Rulesets.Osu.Mods
if (!(beatmap is OsuBeatmap osuBeatmap))
return;
var hitObjects = osuBeatmap.HitObjects;
Seed.Value ??= RNG.Next();
rng = new Random((int)Seed.Value);
var randomObjects = randomiseObjects(hitObjects);
var positionInfos = OsuHitObjectGenerationUtils.GeneratePositionInfos(osuBeatmap.HitObjects);
applyRandomisation(hitObjects, randomObjects);
}
/// <summary>
/// Randomise the position of each hit object and return a list of <see cref="RandomObjectInfo"/>s describing how each hit object should be placed.
/// </summary>
/// <param name="hitObjects">A list of <see cref="OsuHitObject"/>s to have their positions randomised.</param>
/// <returns>A list of <see cref="RandomObjectInfo"/>s describing how each hit object should be placed.</returns>
private List<RandomObjectInfo> randomiseObjects(IEnumerable<OsuHitObject> hitObjects)
{
Debug.Assert(rng != null, $"{nameof(ApplyToBeatmap)} was not called before randomising objects");
var randomObjects = new List<RandomObjectInfo>();
RandomObjectInfo? previous = null;
float rateOfChangeMultiplier = 0;
foreach (OsuHitObject hitObject in hitObjects)
foreach (var positionInfo in positionInfos)
{
var current = new RandomObjectInfo(hitObject);
randomObjects.Add(current);
// rateOfChangeMultiplier only changes every 5 iterations in a combo
// to prevent shaky-line-shaped streams
if (hitObject.IndexInCurrentCombo % 5 == 0)
if (positionInfo.HitObject.IndexInCurrentCombo % 5 == 0)
rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1;
if (previous == null)
if (positionInfo == positionInfos.First())
{
current.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2);
current.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2);
positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
}
else
{
current.DistanceFromPrevious = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal);
// The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object)
// is proportional to the distance between the last and the current hit object
// to allow jumps and prevent too sharp turns during streams.
// Allow maximum jump angle when jump distance is more than half of playfield diagonal length
current.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, current.DistanceFromPrevious / (playfield_diagonal * 0.5f));
positionInfo.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, positionInfo.DistanceFromPrevious / (playfield_diagonal * 0.5f));
}
previous = current;
}
return randomObjects;
}
/// <summary>
/// Reposition the hit objects according to the information in <paramref name="randomObjects"/>.
/// </summary>
/// <param name="hitObjects">The hit objects to be repositioned.</param>
/// <param name="randomObjects">A list of <see cref="RandomObjectInfo"/> describing how each hit object should be placed.</param>
private void applyRandomisation(IReadOnlyList<OsuHitObject> hitObjects, IReadOnlyList<RandomObjectInfo> randomObjects)
{
RandomObjectInfo? previous = null;
for (int i = 0; i < hitObjects.Count; i++)
{
var hitObject = hitObjects[i];
var current = randomObjects[i];
if (hitObject is Spinner)
{
previous = null;
continue;
}
computeRandomisedPosition(current, previous, i > 1 ? randomObjects[i - 2] : null);
// Move hit objects back into the playfield if they are outside of it
Vector2 shift = Vector2.Zero;
switch (hitObject)
{
case HitCircle circle:
shift = clampHitCircleToPlayfield(circle, current);
break;
case Slider slider:
shift = clampSliderToPlayfield(slider, current);
break;
}
if (shift != Vector2.Zero)
{
var toBeShifted = new List<OsuHitObject>();
for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--)
{
// only shift hit circles
if (!(hitObjects[j] is HitCircle)) break;
toBeShifted.Add(hitObjects[j]);
}
if (toBeShifted.Count > 0)
applyDecreasingShift(toBeShifted, shift);
}
previous = current;
}
}
/// <summary>
/// Compute the randomised position of a hit object while attempting to keep it inside the playfield.
/// </summary>
/// <param name="current">The <see cref="RandomObjectInfo"/> representing the hit object to have the randomised position computed for.</param>
/// <param name="previous">The <see cref="RandomObjectInfo"/> representing the hit object immediately preceding the current one.</param>
/// <param name="beforePrevious">The <see cref="RandomObjectInfo"/> representing the hit object immediately preceding the <paramref name="previous"/> one.</param>
private void computeRandomisedPosition(RandomObjectInfo current, RandomObjectInfo? previous, RandomObjectInfo? beforePrevious)
{
float previousAbsoluteAngle = 0f;
if (previous != null)
{
Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
}
float absoluteAngle = previousAbsoluteAngle + current.RelativeAngle;
var posRelativeToPrev = new Vector2(
current.DistanceFromPrevious * (float)Math.Cos(absoluteAngle),
current.DistanceFromPrevious * (float)Math.Sin(absoluteAngle)
);
Vector2 lastEndPosition = previous?.EndPositionRandomised ?? playfield_centre;
posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
current.PositionRandomised = lastEndPosition + posRelativeToPrev;
}
/// <summary>
/// Move the randomised position of a hit circle so that it fits inside the playfield.
/// </summary>
/// <returns>The deviation from the original randomised position in order to fit within the playfield.</returns>
private Vector2 clampHitCircleToPlayfield(HitCircle circle, RandomObjectInfo objectInfo)
{
var previousPosition = objectInfo.PositionRandomised;
objectInfo.EndPositionRandomised = objectInfo.PositionRandomised = clampToPlayfieldWithPadding(
objectInfo.PositionRandomised,
(float)circle.Radius
);
circle.Position = objectInfo.PositionRandomised;
return objectInfo.PositionRandomised - previousPosition;
}
/// <summary>
/// Moves the <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already.
/// </summary>
/// <returns>The deviation from the original randomised position in order to fit within the playfield.</returns>
private Vector2 clampSliderToPlayfield(Slider slider, RandomObjectInfo objectInfo)
{
var possibleMovementBounds = calculatePossibleMovementBounds(slider);
var previousPosition = objectInfo.PositionRandomised;
// Clamp slider position to the placement area
// If the slider is larger than the playfield, force it to stay at the original position
float newX = possibleMovementBounds.Width < 0
? objectInfo.PositionOriginal.X
: Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
float newY = possibleMovementBounds.Height < 0
? objectInfo.PositionOriginal.Y
: Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
slider.Position = objectInfo.PositionRandomised = new Vector2(newX, newY);
objectInfo.EndPositionRandomised = slider.EndPosition;
shiftNestedObjects(slider, objectInfo.PositionRandomised - objectInfo.PositionOriginal);
return objectInfo.PositionRandomised - previousPosition;
}
/// <summary>
/// Decreasingly shift a list of <see cref="OsuHitObject"/>s by a specified amount.
/// The first item in the list is shifted by the largest amount, while the last item is shifted by the smallest amount.
/// </summary>
/// <param name="hitObjects">The list of hit objects to be shifted.</param>
/// <param name="shift">The amount to be shifted.</param>
private void applyDecreasingShift(IList<OsuHitObject> hitObjects, Vector2 shift)
{
for (int i = 0; i < hitObjects.Count; i++)
{
var hitObject = hitObjects[i];
// The first object is shifted by a vector slightly smaller than shift
// The last object is shifted by a vector slightly larger than zero
Vector2 position = hitObject.Position + shift * ((hitObjects.Count - i) / (float)(hitObjects.Count + 1));
hitObject.Position = clampToPlayfieldWithPadding(position, (float)hitObject.Radius);
}
}
/// <summary>
/// Calculates a <see cref="RectangleF"/> which contains all of the possible movements of the slider (in relative X/Y coordinates)
/// such that the entire slider is inside the playfield.
/// </summary>
/// <remarks>
/// If the slider is larger than the playfield, the returned <see cref="RectangleF"/> may have negative width/height.
/// </remarks>
private RectangleF calculatePossibleMovementBounds(Slider slider)
{
var pathPositions = new List<Vector2>();
slider.Path.GetPathToProgress(pathPositions, 0, 1);
float minX = float.PositiveInfinity;
float maxX = float.NegativeInfinity;
float minY = float.PositiveInfinity;
float maxY = float.NegativeInfinity;
// Compute the bounding box of the slider.
foreach (var pos in pathPositions)
{
minX = MathF.Min(minX, pos.X);
maxX = MathF.Max(maxX, pos.X);
minY = MathF.Min(minY, pos.Y);
maxY = MathF.Max(maxY, pos.Y);
}
// Take the circle radius into account.
float radius = (float)slider.Radius;
minX -= radius;
minY -= radius;
maxX += radius;
maxY += radius;
// Given the bounding box of the slider (via min/max X/Y),
// the amount that the slider can move to the left is minX (with the sign flipped, since positive X is to the right),
// and the amount that it can move to the right is WIDTH - maxX.
// Same calculation applies for the Y axis.
float left = -minX;
float right = OsuPlayfield.BASE_SIZE.X - maxX;
float top = -minY;
float bottom = OsuPlayfield.BASE_SIZE.Y - maxY;
return new RectangleF(left, top, right - left, bottom - top);
}
/// <summary>
/// Shifts all nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s by the specified shift.
/// </summary>
/// <param name="slider"><see cref="Slider"/> whose nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted</param>
/// <param name="shift">The <see cref="Vector2"/> the <see cref="Slider"/>'s nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted by</param>
private void shiftNestedObjects(Slider slider, Vector2 shift)
{
foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat))
{
if (!(hitObject is OsuHitObject osuHitObject))
continue;
osuHitObject.Position += shift;
}
}
/// <summary>
/// Clamp a position to playfield, keeping a specified distance from the edges.
/// </summary>
/// <param name="position">The position to be clamped.</param>
/// <param name="padding">The minimum distance allowed from playfield edges.</param>
/// <returns>The clamped position.</returns>
private Vector2 clampToPlayfieldWithPadding(Vector2 position, float padding)
{
return new Vector2(
Math.Clamp(position.X, padding, OsuPlayfield.BASE_SIZE.X - padding),
Math.Clamp(position.Y, padding, OsuPlayfield.BASE_SIZE.Y - padding)
);
}
private class RandomObjectInfo
{
/// <summary>
/// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle.
/// </summary>
/// <remarks>
/// <see cref="RelativeAngle"/> of the first hit object in a beatmap represents the absolute angle from playfield center to the object.
/// </remarks>
/// <example>
/// If <see cref="RelativeAngle"/> is 0, the player's cursor doesn't need to change its direction of movement when passing
/// the previous object to reach this one.
/// </example>
public float RelativeAngle { get; set; }
/// <summary>
/// The jump distance from the previous hit object to this one.
/// </summary>
/// <remarks>
/// <see cref="DistanceFromPrevious"/> of the first hit object in a beatmap is relative to the playfield center.
/// </remarks>
public float DistanceFromPrevious { get; set; }
public Vector2 PositionOriginal { get; }
public Vector2 PositionRandomised { get; set; }
public Vector2 EndPositionOriginal { get; }
public Vector2 EndPositionRandomised { get; set; }
public OsuHitObject HitObject { get; }
public RandomObjectInfo(OsuHitObject hitObject)
{
PositionRandomised = PositionOriginal = hitObject.Position;
EndPositionRandomised = EndPositionOriginal = hitObject.EndPosition;
HitObject = hitObject;
}
osuBeatmap.HitObjects = OsuHitObjectGenerationUtils.RepositionHitObjects(positionInfos);
}
}
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
{
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModAimAssist) }).ToArray();
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised) }).ToArray();
/// <summary>
/// How early before a hitobject's start time to trigger a hit.

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModAimAssist) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised) };
private float theta;

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "They just won't stay still...";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModAimAssist) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised) };
private const int wiggle_duration = 90; // (ms) Higher = fewer wiggles
private const int wiggle_strength = 10; // Higher = stronger wiggles

View File

@ -195,7 +195,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModApproachDifferent(),
new OsuModMuted(),
new OsuModNoScope(),
new OsuModAimAssist(),
new OsuModMagnetised(),
new ModAdaptiveSpeed()
};

View File

@ -11,7 +11,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Utils
{
public static class OsuHitObjectGenerationUtils
public static partial class OsuHitObjectGenerationUtils
{
// The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
// The closer the hit objects draw to the border, the sharper the turn

View File

@ -0,0 +1,340 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osuTK;
#nullable enable
namespace osu.Game.Rulesets.Osu.Utils
{
public static partial class OsuHitObjectGenerationUtils
{
/// <summary>
/// Number of previous hitobjects to be shifted together when an object is being moved.
/// </summary>
private const int preceding_hitobjects_to_shift = 10;
private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE / 2;
/// <summary>
/// Generate a list of <see cref="ObjectPositionInfo"/>s containing information for how the given list of
/// <see cref="OsuHitObject"/>s are positioned.
/// </summary>
/// <param name="hitObjects">A list of <see cref="OsuHitObject"/>s to process.</param>
/// <returns>A list of <see cref="ObjectPositionInfo"/>s describing how each hit object is positioned relative to the previous one.</returns>
public static List<ObjectPositionInfo> GeneratePositionInfos(IEnumerable<OsuHitObject> hitObjects)
{
var positionInfos = new List<ObjectPositionInfo>();
Vector2 previousPosition = playfield_centre;
float previousAngle = 0;
foreach (OsuHitObject hitObject in hitObjects)
{
Vector2 relativePosition = hitObject.Position - previousPosition;
float absoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
float relativeAngle = absoluteAngle - previousAngle;
positionInfos.Add(new ObjectPositionInfo(hitObject)
{
RelativeAngle = relativeAngle,
DistanceFromPrevious = relativePosition.Length
});
previousPosition = hitObject.EndPosition;
previousAngle = absoluteAngle;
}
return positionInfos;
}
/// <summary>
/// Reposition the hit objects according to the information in <paramref name="objectPositionInfos"/>.
/// </summary>
/// <param name="objectPositionInfos">Position information for each hit object.</param>
/// <returns>The repositioned hit objects.</returns>
public static List<OsuHitObject> RepositionHitObjects(IEnumerable<ObjectPositionInfo> objectPositionInfos)
{
List<WorkingObject> workingObjects = objectPositionInfos.Select(o => new WorkingObject(o)).ToList();
WorkingObject? previous = null;
for (int i = 0; i < workingObjects.Count; i++)
{
var current = workingObjects[i];
var hitObject = current.HitObject;
if (hitObject is Spinner)
{
previous = null;
continue;
}
computeModifiedPosition(current, previous, i > 1 ? workingObjects[i - 2] : null);
// Move hit objects back into the playfield if they are outside of it
Vector2 shift = Vector2.Zero;
switch (hitObject)
{
case HitCircle _:
shift = clampHitCircleToPlayfield(current);
break;
case Slider _:
shift = clampSliderToPlayfield(current);
break;
}
if (shift != Vector2.Zero)
{
var toBeShifted = new List<OsuHitObject>();
for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--)
{
// only shift hit circles
if (!(workingObjects[j].HitObject is HitCircle)) break;
toBeShifted.Add(workingObjects[j].HitObject);
}
if (toBeShifted.Count > 0)
applyDecreasingShift(toBeShifted, shift);
}
previous = current;
}
return workingObjects.Select(p => p.HitObject).ToList();
}
/// <summary>
/// Compute the modified position of a hit object while attempting to keep it inside the playfield.
/// </summary>
/// <param name="current">The <see cref="WorkingObject"/> representing the hit object to have the modified position computed for.</param>
/// <param name="previous">The <see cref="WorkingObject"/> representing the hit object immediately preceding the current one.</param>
/// <param name="beforePrevious">The <see cref="WorkingObject"/> representing the hit object immediately preceding the <paramref name="previous"/> one.</param>
private static void computeModifiedPosition(WorkingObject current, WorkingObject? previous, WorkingObject? beforePrevious)
{
float previousAbsoluteAngle = 0f;
if (previous != null)
{
Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
}
float absoluteAngle = previousAbsoluteAngle + current.PositionInfo.RelativeAngle;
var posRelativeToPrev = new Vector2(
current.PositionInfo.DistanceFromPrevious * (float)Math.Cos(absoluteAngle),
current.PositionInfo.DistanceFromPrevious * (float)Math.Sin(absoluteAngle)
);
Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre;
posRelativeToPrev = RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
current.PositionModified = lastEndPosition + posRelativeToPrev;
}
/// <summary>
/// Move the modified position of a <see cref="HitCircle"/> so that it fits inside the playfield.
/// </summary>
/// <returns>The deviation from the original modified position in order to fit within the playfield.</returns>
private static Vector2 clampHitCircleToPlayfield(WorkingObject workingObject)
{
var previousPosition = workingObject.PositionModified;
workingObject.EndPositionModified = workingObject.PositionModified = clampToPlayfieldWithPadding(
workingObject.PositionModified,
(float)workingObject.HitObject.Radius
);
workingObject.HitObject.Position = workingObject.PositionModified;
return workingObject.PositionModified - previousPosition;
}
/// <summary>
/// Moves the <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already.
/// </summary>
/// <returns>The deviation from the original modified position in order to fit within the playfield.</returns>
private static Vector2 clampSliderToPlayfield(WorkingObject workingObject)
{
var slider = (Slider)workingObject.HitObject;
var possibleMovementBounds = calculatePossibleMovementBounds(slider);
var previousPosition = workingObject.PositionModified;
// Clamp slider position to the placement area
// If the slider is larger than the playfield, force it to stay at the original position
float newX = possibleMovementBounds.Width < 0
? workingObject.PositionOriginal.X
: Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
float newY = possibleMovementBounds.Height < 0
? workingObject.PositionOriginal.Y
: Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
workingObject.EndPositionModified = slider.EndPosition;
shiftNestedObjects(slider, workingObject.PositionModified - workingObject.PositionOriginal);
return workingObject.PositionModified - previousPosition;
}
/// <summary>
/// Decreasingly shift a list of <see cref="OsuHitObject"/>s by a specified amount.
/// The first item in the list is shifted by the largest amount, while the last item is shifted by the smallest amount.
/// </summary>
/// <param name="hitObjects">The list of hit objects to be shifted.</param>
/// <param name="shift">The amount to be shifted.</param>
private static void applyDecreasingShift(IList<OsuHitObject> hitObjects, Vector2 shift)
{
for (int i = 0; i < hitObjects.Count; i++)
{
var hitObject = hitObjects[i];
// The first object is shifted by a vector slightly smaller than shift
// The last object is shifted by a vector slightly larger than zero
Vector2 position = hitObject.Position + shift * ((hitObjects.Count - i) / (float)(hitObjects.Count + 1));
hitObject.Position = clampToPlayfieldWithPadding(position, (float)hitObject.Radius);
}
}
/// <summary>
/// Calculates a <see cref="RectangleF"/> which contains all of the possible movements of the slider (in relative X/Y coordinates)
/// such that the entire slider is inside the playfield.
/// </summary>
/// <remarks>
/// If the slider is larger than the playfield, the returned <see cref="RectangleF"/> may have negative width/height.
/// </remarks>
private static RectangleF calculatePossibleMovementBounds(Slider slider)
{
var pathPositions = new List<Vector2>();
slider.Path.GetPathToProgress(pathPositions, 0, 1);
float minX = float.PositiveInfinity;
float maxX = float.NegativeInfinity;
float minY = float.PositiveInfinity;
float maxY = float.NegativeInfinity;
// Compute the bounding box of the slider.
foreach (var pos in pathPositions)
{
minX = MathF.Min(minX, pos.X);
maxX = MathF.Max(maxX, pos.X);
minY = MathF.Min(minY, pos.Y);
maxY = MathF.Max(maxY, pos.Y);
}
// Take the circle radius into account.
float radius = (float)slider.Radius;
minX -= radius;
minY -= radius;
maxX += radius;
maxY += radius;
// Given the bounding box of the slider (via min/max X/Y),
// the amount that the slider can move to the left is minX (with the sign flipped, since positive X is to the right),
// and the amount that it can move to the right is WIDTH - maxX.
// Same calculation applies for the Y axis.
float left = -minX;
float right = OsuPlayfield.BASE_SIZE.X - maxX;
float top = -minY;
float bottom = OsuPlayfield.BASE_SIZE.Y - maxY;
return new RectangleF(left, top, right - left, bottom - top);
}
/// <summary>
/// Shifts all nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s by the specified shift.
/// </summary>
/// <param name="slider"><see cref="Slider"/> whose nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted</param>
/// <param name="shift">The <see cref="Vector2"/> the <see cref="Slider"/>'s nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted by</param>
private static void shiftNestedObjects(Slider slider, Vector2 shift)
{
foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat))
{
if (!(hitObject is OsuHitObject osuHitObject))
continue;
osuHitObject.Position += shift;
}
}
/// <summary>
/// Clamp a position to playfield, keeping a specified distance from the edges.
/// </summary>
/// <param name="position">The position to be clamped.</param>
/// <param name="padding">The minimum distance allowed from playfield edges.</param>
/// <returns>The clamped position.</returns>
private static Vector2 clampToPlayfieldWithPadding(Vector2 position, float padding)
{
return new Vector2(
Math.Clamp(position.X, padding, OsuPlayfield.BASE_SIZE.X - padding),
Math.Clamp(position.Y, padding, OsuPlayfield.BASE_SIZE.Y - padding)
);
}
public class ObjectPositionInfo
{
/// <summary>
/// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle.
/// </summary>
/// <remarks>
/// <see cref="RelativeAngle"/> of the first hit object in a beatmap represents the absolute angle from playfield center to the object.
/// </remarks>
/// <example>
/// If <see cref="RelativeAngle"/> is 0, the player's cursor doesn't need to change its direction of movement when passing
/// the previous object to reach this one.
/// </example>
public float RelativeAngle { get; set; }
/// <summary>
/// The jump distance from the previous hit object to this one.
/// </summary>
/// <remarks>
/// <see cref="DistanceFromPrevious"/> of the first hit object in a beatmap is relative to the playfield center.
/// </remarks>
public float DistanceFromPrevious { get; set; }
/// <summary>
/// The hit object associated with this <see cref="ObjectPositionInfo"/>.
/// </summary>
public OsuHitObject HitObject { get; }
public ObjectPositionInfo(OsuHitObject hitObject)
{
HitObject = hitObject;
}
}
private class WorkingObject
{
public Vector2 PositionOriginal { get; }
public Vector2 PositionModified { get; set; }
public Vector2 EndPositionModified { get; set; }
public ObjectPositionInfo PositionInfo { get; }
public OsuHitObject HitObject => PositionInfo.HitObject;
public WorkingObject(ObjectPositionInfo positionInfo)
{
PositionInfo = positionInfo;
PositionModified = PositionOriginal = HitObject.Position;
EndPositionModified = HitObject.EndPosition;
}
}
}
}

View File

@ -14,15 +14,15 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
[TestCase(2.2420075288523802d, "diffcalc-test")]
[TestCase(2.2420075288523802d, "diffcalc-test-strong")]
public void Test(double expected, string name)
=> base.Test(expected, name);
[TestCase(2.2420075288523802d, 200, "diffcalc-test")]
[TestCase(2.2420075288523802d, 200, "diffcalc-test-strong")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(3.134084469440479d, "diffcalc-test")]
[TestCase(3.134084469440479d, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new TaikoModDoubleTime());
[TestCase(3.134084469440479d, 200, "diffcalc-test")]
[TestCase(3.134084469440479d, 200, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset().RulesetInfo, beatmap);

View File

@ -3,19 +3,14 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "mekkadosu!" } },
Replay = new TaikoAutoGenerator(beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" });
}
}

View File

@ -3,20 +3,15 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModCinema : ModCinema<TaikoHitObject>
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "mekkadosu!" } },
Replay = new TaikoAutoGenerator(beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" });
}
}

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// <summary>
/// Default size of a drawable taiko hit object.
/// </summary>
public const float DEFAULT_SIZE = 0.45f;
public const float DEFAULT_SIZE = 0.475f;
public override Judgement CreateJudgement() => new TaikoJudgement();

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// <summary>
/// Scale multiplier for a strong drawable taiko hit object.
/// </summary>
public const float STRONG_SCALE = 1.4f;
public const float STRONG_SCALE = 1 / 0.65f;
/// <summary>
/// Default size of a strong drawable taiko hit object.

View File

@ -11,6 +11,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Taiko.Objects;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.Skinning.Default
@ -24,8 +25,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
/// </summary>
public abstract class CirclePiece : BeatSyncedContainer, IHasAccentColour
{
public const float SYMBOL_SIZE = 0.45f;
public const float SYMBOL_SIZE = TaikoHitObject.DEFAULT_SIZE;
public const float SYMBOL_BORDER = 8;
private const double pre_beat_transition_time = 80;
private Color4 accentColour;

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
new Sprite
{
Texture = skin.GetTexture("approachcircle"),
Scale = new Vector2(0.73f),
Scale = new Vector2(0.83f),
Alpha = 0.47f, // eyeballed to match stable
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
new Sprite
{
Texture = skin.GetTexture("taikobigcircle"),
Scale = new Vector2(0.7f),
Scale = new Vector2(0.8f),
Alpha = 0.22f, // eyeballed to match stable
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -175,7 +175,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
private class TestLegacySkin : LegacySkin
{
public TestLegacySkin(IResourceStore<byte[]> storage, string fileName)
: base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, storage, null, fileName)
: base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, null, storage, fileName)
{
}
}

View File

@ -8,6 +8,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
@ -64,6 +65,62 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
[TestCase(3, true)]
[TestCase(6, false)]
[TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)]
public void TestLegacyBeatmapReplayOffsetsDecode(int beatmapVersion, bool offsetApplied)
{
const double first_frame_time = 48;
const double second_frame_time = 65;
var decoder = new TestLegacyScoreDecoder(beatmapVersion);
using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
{
var score = decoder.Parse(resourceStream);
Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0)));
Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0)));
}
}
[TestCase(3)]
[TestCase(6)]
[TestCase(LegacyBeatmapDecoder.LATEST_VERSION)]
public void TestLegacyBeatmapReplayOffsetsEncodeDecode(int beatmapVersion)
{
const double first_frame_time = 2000;
const double second_frame_time = 3000;
var ruleset = new OsuRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
var beatmap = new TestBeatmap(ruleset)
{
BeatmapInfo =
{
BeatmapVersion = beatmapVersion
}
};
var score = new Score
{
ScoreInfo = scoreInfo,
Replay = new Replay
{
Frames = new List<ReplayFrame>
{
new OsuReplayFrame(first_frame_time, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton),
new OsuReplayFrame(second_frame_time, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
}
}
};
var decodedAfterEncode = encodeThenDecode(beatmapVersion, score, beatmap);
Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(first_frame_time));
Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(second_frame_time));
}
[Test]
public void TestCultureInvariance()
{
@ -86,15 +143,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
// rather than the classic ASCII U+002D HYPHEN-MINUS.
CultureInfo.CurrentCulture = new CultureInfo("se");
var encodeStream = new MemoryStream();
var encoder = new LegacyScoreEncoder(score, beatmap);
encoder.Encode(encodeStream);
var decodeStream = new MemoryStream(encodeStream.GetBuffer());
var decoder = new TestLegacyScoreDecoder();
var decodedAfterEncode = decoder.Parse(decodeStream);
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
Assert.Multiple(() =>
{
@ -110,6 +159,20 @@ namespace osu.Game.Tests.Beatmaps.Formats
});
}
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
{
var encodeStream = new MemoryStream();
var encoder = new LegacyScoreEncoder(score, beatmap);
encoder.Encode(encodeStream);
var decodeStream = new MemoryStream(encodeStream.GetBuffer());
var decoder = new TestLegacyScoreDecoder(beatmapVersion);
var decodedAfterEncode = decoder.Parse(decodeStream);
return decodedAfterEncode;
}
[TearDown]
public void TearDown()
{
@ -118,6 +181,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
private class TestLegacyScoreDecoder : LegacyScoreDecoder
{
private readonly int beatmapVersion;
private static readonly Dictionary<int, Ruleset> rulesets = new Ruleset[]
{
new OsuRuleset(),
@ -126,6 +191,11 @@ namespace osu.Game.Tests.Beatmaps.Formats
new ManiaRuleset()
}.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID);
public TestLegacyScoreDecoder(int beatmapVersion = LegacyBeatmapDecoder.LATEST_VERSION)
{
this.beatmapVersion = beatmapVersion;
}
protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId];
protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap
@ -134,7 +204,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
{
MD5Hash = md5Hash,
Ruleset = new OsuRuleset().RulesetInfo,
Difficulty = new BeatmapDifficulty()
Difficulty = new BeatmapDifficulty(),
BeatmapVersion = beatmapVersion,
}
});
}

View File

@ -147,7 +147,10 @@ namespace osu.Game.Tests.Database
Live<BeatmapSetInfo>? imported;
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
{
imported = await importer.Import(reader);
EnsureLoaded(realm.Realm);
}
Assert.AreEqual(1, realm.Realm.All<BeatmapSetInfo>().Count());
@ -510,6 +513,8 @@ namespace osu.Game.Tests.Database
new ImportTask(zipStream, string.Empty)
);
realm.Run(r => r.Refresh());
checkBeatmapSetCount(realm.Realm, 0);
checkBeatmapCount(realm.Realm, 0);
@ -565,6 +570,8 @@ namespace osu.Game.Tests.Database
{
}
EnsureLoaded(realm.Realm);
checkBeatmapSetCount(realm.Realm, 1);
checkBeatmapCount(realm.Realm, 12);
@ -726,6 +733,8 @@ namespace osu.Game.Tests.Database
var imported = importer.Import(toImport);
realm.Run(r => r.Refresh());
Assert.NotNull(imported);
Debug.Assert(imported != null);
@ -891,6 +900,8 @@ namespace osu.Game.Tests.Database
string? temp = TestResources.GetTestBeatmapForImport();
await importer.Import(temp);
EnsureLoaded(realm.Realm);
// Update via the beatmap, not the beatmap info, to ensure correct linking
BeatmapSetInfo setToUpdate = realm.Realm.All<BeatmapSetInfo>().First();

View File

@ -39,7 +39,7 @@ namespace osu.Game.Tests.Database
// ReSharper disable once AccessToDisposedClosure
var testStorage = new OsuStorage(host, storage.GetStorageForDirectory(caller));
using (var realm = new RealmAccess(testStorage, "client"))
using (var realm = new RealmAccess(testStorage, OsuGameBase.CLIENT_DATABASE_FILENAME))
{
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realm.Filename)}");
testAction(realm, testStorage);
@ -62,7 +62,7 @@ namespace osu.Game.Tests.Database
{
var testStorage = storage.GetStorageForDirectory(caller);
using (var realm = new RealmAccess(testStorage, "client"))
using (var realm = new RealmAccess(testStorage, OsuGameBase.CLIENT_DATABASE_FILENAME))
{
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realm.Filename)}");
await testAction(realm, testStorage);

View File

@ -148,7 +148,7 @@ namespace osu.Game.Tests.Gameplay
private class TestSkin : LegacySkin
{
public TestSkin(string resourceName, IStorageResourceProvider resources)
: base(DefaultLegacySkin.CreateInfo(), new TestResourceStore(resourceName), resources, "skin.ini")
: base(DefaultLegacySkin.CreateInfo(), resources, new TestResourceStore(resourceName))
{
}
}

View File

@ -143,14 +143,14 @@ namespace osu.Game.Tests.NonVisual
Assert.That(osuStorage, Is.Not.Null);
// In the following tests, realm files are ignored as
// - in the case of checking the source, interacting with the pipe files (client.realm.note) may
// - in the case of checking the source, interacting with the pipe files (.realm.note) may
// lead to unexpected behaviour.
// - in the case of checking the destination, the files may have already been recreated by the game
// as part of the standard migration flow.
foreach (string file in osuStorage.IgnoreFiles)
{
if (!file.Contains("realm", StringComparison.Ordinal))
if (!file.Contains(".realm", StringComparison.Ordinal))
{
Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
Assert.That(storage.Exists(file), Is.False, () => $"{file} exists in destination when it was expected to be ignored");
@ -159,7 +159,7 @@ namespace osu.Game.Tests.NonVisual
foreach (string dir in osuStorage.IgnoreDirectories)
{
if (!dir.Contains("realm", StringComparison.Ordinal))
if (!dir.Contains(".realm", StringComparison.Ordinal))
{
Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir)));
Assert.That(storage.Exists(dir), Is.False, () => $"{dir} exists in destination when it was expected to be ignored");
@ -188,19 +188,17 @@ namespace osu.Game.Tests.NonVisual
{
var osu = LoadOsuIntoHost(host);
const string database_filename = "client.realm";
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.That(File.Exists(Path.Combine(customPath, database_filename)));
Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
Assert.DoesNotThrow(() => osu.Migrate(customPath2));
Assert.That(File.Exists(Path.Combine(customPath2, database_filename)));
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.That(File.Exists(Path.Combine(customPath, database_filename)));
Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
}
finally
{
@ -233,6 +231,46 @@ namespace osu.Game.Tests.NonVisual
}
}
[Test]
public void TestMigrationFailsOnExistingData()
{
string customPath = prepareCustomPath();
string customPath2 = prepareCustomPath();
using (var host = new CustomTestHeadlessGameHost())
{
try
{
var osu = LoadOsuIntoHost(host);
var storage = osu.Dependencies.Get<Storage>();
var osuStorage = storage as OsuStorage;
string originalDirectory = storage.GetFullPath(".");
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
Directory.CreateDirectory(customPath2);
File.Copy(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME), Path.Combine(customPath2, OsuGameBase.CLIENT_DATABASE_FILENAME));
// Fails because file already exists.
Assert.Throws<ArgumentException>(() => osu.Migrate(customPath2));
osuStorage?.ChangeDataPath(customPath2);
Assert.That(osuStorage?.CustomStoragePath, Is.EqualTo(customPath2));
Assert.That(new StreamReader(Path.Combine(originalDirectory, "storage.ini")).ReadToEnd().Contains($"FullPath = {customPath2}"));
}
finally
{
host.Exit();
cleanupPath(customPath);
cleanupPath(customPath2);
}
}
}
[Test]
public void TestMigrationToNestedTargetFails()
{

View File

@ -1,12 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Audio;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Skinning;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace osu.Game.Tests.NonVisual.Skinning
{
@ -71,7 +80,7 @@ namespace osu.Game.Tests.NonVisual.Skinning
var texture = legacySkin.GetTexture(requestedComponent);
Assert.IsNotNull(texture);
Assert.AreEqual(textureStore.Textures[expectedTexture], texture);
Assert.AreEqual(textureStore.Textures[expectedTexture].Width, texture.Width);
Assert.AreEqual(expectedScale, texture.ScaleAdjust);
}
@ -88,23 +97,50 @@ namespace osu.Game.Tests.NonVisual.Skinning
private class TestLegacySkin : LegacySkin
{
public TestLegacySkin(TextureStore textureStore)
: base(new SkinInfo(), null, null, string.Empty)
public TestLegacySkin(IResourceStore<TextureUpload> textureStore)
: base(new SkinInfo(), new TestResourceProvider(textureStore), null, string.Empty)
{
Textures = textureStore;
}
private class TestResourceProvider : IStorageResourceProvider
{
private readonly IResourceStore<TextureUpload> textureStore;
public TestResourceProvider(IResourceStore<TextureUpload> textureStore)
{
this.textureStore = textureStore;
}
public AudioManager AudioManager => null;
public IResourceStore<byte[]> Files => null;
public IResourceStore<byte[]> Resources => null;
public RealmAccess RealmAccess => null;
public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => textureStore;
}
}
private class TestTextureStore : TextureStore
private class TestTextureStore : IResourceStore<TextureUpload>
{
public readonly Dictionary<string, Texture> Textures;
public readonly Dictionary<string, TextureUpload> Textures;
public TestTextureStore(params string[] fileNames)
{
Textures = fileNames.ToDictionary(fileName => fileName, fileName => new Texture(1, 1));
// use an incrementing width to allow assertion matching on correct textures as they turn from uploads into actual textures.
int width = 1;
Textures = fileNames.ToDictionary(fileName => fileName, fileName => new TextureUpload(new Image<Rgba32>(width, width++)));
}
public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => Textures.GetValueOrDefault(name);
public TextureUpload Get(string name) => Textures.GetValueOrDefault(name);
public Task<TextureUpload> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => Task.FromResult(Get(name));
public Stream GetStream(string name) => throw new NotImplementedException();
public IEnumerable<string> GetAvailableResources() => throw new NotImplementedException();
public void Dispose()
{
}
}
}
}

View File

@ -77,7 +77,7 @@ namespace osu.Game.Tests.Skins
public class BeatmapSkinSource : LegacyBeatmapSkin
{
public BeatmapSkinSource()
: base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null, null)
: base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null)
{
}

View File

@ -202,7 +202,7 @@ namespace osu.Game.Tests.Skins
public class BeatmapSkinSource : LegacyBeatmapSkin
{
public BeatmapSkinSource()
: base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null, null)
: base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null)
{
}
}

View File

@ -5,12 +5,14 @@ using System.ComponentModel;
using System.Linq;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.Break;
using osu.Game.Screens.Ranking;
using osu.Game.Users.Drawables;
namespace osu.Game.Tests.Visual.Gameplay
{
@ -39,11 +41,18 @@ namespace osu.Game.Tests.Visual.Gameplay
seekToBreak(1);
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
AddUntilStep("results displayed", () => getResultsScreen() != null);
AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true);
AddAssert("score has combo", () => getResultsScreen().Score.Combo > 100);
AddAssert("score has no misses", () => getResultsScreen().Score.Statistics[HitResult.Miss] == 0);
AddUntilStep("avatar displayed", () => getAvatar() != null);
AddAssert("avatar not clickable", () => getAvatar().ChildrenOfType<OsuClickableContainer>().First().Action == null);
ClickableAvatar getAvatar() => getResultsScreen()
.ChildrenOfType<ClickableAvatar>().FirstOrDefault();
ResultsScreen getResultsScreen() => Stack.CurrentScreen as ResultsScreen;
}

View File

@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestEmptyLegacyBeatmapSkinFallsBack()
{
CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null));
CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null));
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded));
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value));
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
var beatmap = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Array.Empty<Mod>());
return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod()?.CreateReplayScore(beatmap, Array.Empty<Mod>()));
return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod()?.CreateScoreFromReplayData(beatmap, Array.Empty<Mod>()));
}
protected override void AddCheckSteps()

View File

@ -37,6 +37,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Player.ScaleTo(0.4f);
LoadComponentAsync(skinEditor = new SkinEditor(Player), Add);
});
AddUntilStep("wait for loaded", () => skinEditor.IsLoaded);
}
[Test]

View File

@ -0,0 +1,107 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing;
using osu.Game.Configuration;
using osu.Game.Overlays.Toolbar;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Menus
{
[TestFixture]
public class TestSceneToolbarClock : OsuManualInputManagerTestScene
{
private Bindable<ToolbarClockDisplayMode> clockDisplayMode;
private readonly Container mainContainer;
private readonly ToolbarClock toolbarClock;
public TestSceneToolbarClock()
{
Children = new Drawable[]
{
mainContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = Toolbar.HEIGHT,
Children = new Drawable[]
{
new Box
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new Box
{
Colour = Color4.DarkRed,
RelativeSizeAxes = Axes.Y,
Width = 2,
},
toolbarClock = new ToolbarClock(),
new Box
{
Colour = Color4.DarkRed,
RelativeSizeAxes = Axes.Y,
Width = 2,
},
}
},
}
},
};
AddSliderStep("scale", 0.5, 4, 1, scale => mainContainer.Scale = new Vector2((float)scale));
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
clockDisplayMode = config.GetBindable<ToolbarClockDisplayMode>(OsuSetting.ToolbarClockDisplayMode);
}
[Test]
public void TestRealGameTime()
{
AddStep("Set game time real", () => mainContainer.Clock = Clock);
}
[Test]
public void TestLongGameTime()
{
AddStep("Set game time long", () => mainContainer.Clock = new FramedOffsetClock(Clock, false) { Offset = 3600.0 * 24 * 1000 * 98 });
}
[Test]
public void TestDisplayModeChange()
{
AddStep("Set clock display mode", () => clockDisplayMode.Value = ToolbarClockDisplayMode.Full);
AddStep("Trigger click", () => toolbarClock.TriggerClick());
AddAssert("State is digital with runtime", () => clockDisplayMode.Value == ToolbarClockDisplayMode.DigitalWithRuntime);
AddStep("Trigger click", () => toolbarClock.TriggerClick());
AddAssert("State is digital", () => clockDisplayMode.Value == ToolbarClockDisplayMode.Digital);
AddStep("Trigger click", () => toolbarClock.TriggerClick());
AddAssert("State is analog", () => clockDisplayMode.Value == ToolbarClockDisplayMode.Analog);
AddStep("Trigger click", () => toolbarClock.TriggerClick());
AddAssert("State is full", () => clockDisplayMode.Value == ToolbarClockDisplayMode.Full);
}
}
}

View File

@ -0,0 +1,326 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMatchStartControl : MultiplayerTestScene
{
private MatchStartControl control;
private BeatmapSetInfo importedSet;
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
private BeatmapManager beatmaps;
private RulesetStore rulesets;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
}
[SetUp]
public new void Setup() => Schedule(() =>
{
AvailabilityTracker.SelectedItem.BindTo(selectedItem);
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
};
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = control = new MatchStartControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(250, 50),
}
};
});
[Test]
public void TestStartWithCountdown()
{
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the first countdown button", () =>
{
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
InputManager.MoveMouseTo(popoverButton);
InputManager.Click(MouseButton.Left);
});
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad);
}
[Test]
public void TestCancelCountdown()
{
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the first countdown button", () =>
{
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
InputManager.MoveMouseTo(popoverButton);
InputManager.Click(MouseButton.Left);
});
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the cancel button", () =>
{
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().Last();
InputManager.MoveMouseTo(popoverButton);
InputManager.Click(MouseButton.Left);
});
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
}
[Test]
public void TestReadyAndUnReadyDuringCountdown()
{
AddStep("add second user as host", () =>
{
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
MultiplayerClient.TransferHost(2);
});
AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(2) }).WaitSafely());
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
}
[Test]
public void TestCountdownWhileSpectating()
{
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
AddAssert("countdown button is visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }));
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready));
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
}
[Test]
public void TestBecomeHostDuringCountdownAndReady()
{
AddStep("add second user as host", () =>
{
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
MultiplayerClient.TransferHost(2);
});
AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely());
AddUntilStep("countdown started", () => MultiplayerClient.Room?.Countdown != null);
AddStep("transfer host to local user", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID));
AddUntilStep("local user is host", () => MultiplayerClient.Room?.Host?.Equals(MultiplayerClient.LocalUser) == true);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null);
}
[Test]
public void TestCountdownButtonVisibilityWithAutoStartEnablement()
{
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
AddUntilStep("countdown button visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }).WaitSafely());
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
AddUntilStep("countdown button not visible", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
}
[Test]
public void TestClickingReadyButtonUnReadiesDuringAutoStart()
{
AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }).WaitSafely());
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle);
}
[Test]
public void TestDeletedBeatmapDisableReady()
{
OsuButton readyButton = null;
AddUntilStep("ensure ready button enabled", () =>
{
readyButton = control.ChildrenOfType<OsuButton>().Single();
return readyButton.Enabled.Value;
});
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
AddUntilStep("ready button disabled", () => !readyButton.Enabled.Value);
AddStep("undelete beatmap", () => beatmaps.Undelete(importedSet));
AddUntilStep("ready button enabled back", () => readyButton.Enabled.Value);
}
[Test]
public void TestToggleStateWhenNotHost()
{
AddStep("add second user as host", () =>
{
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
MultiplayerClient.TransferHost(2);
});
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
}
[TestCase(true)]
[TestCase(false)]
public void TestToggleStateWhenHost(bool allReady)
{
AddStep("setup", () =>
{
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
if (!allReady)
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
});
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
verifyGameplayStartFlow();
}
[Test]
public void TestBecomeHostWhileReady()
{
AddStep("add host", () =>
{
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
MultiplayerClient.TransferHost(2);
});
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0));
verifyGameplayStartFlow();
}
[Test]
public void TestLoseHostWhileReady()
{
AddStep("setup", () =>
{
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
});
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0));
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
AddUntilStep("ready button enabled", () => control.ChildrenOfType<OsuButton>().Single().Enabled.Value);
}
[TestCase(true)]
[TestCase(false)]
public void TestManyUsersChangingState(bool isHost)
{
const int users = 10;
AddStep("setup", () =>
{
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
for (int i = 0; i < users; i++)
MultiplayerClient.AddUser(new APIUser { Id = i, Username = "Another user" });
});
if (!isHost)
AddStep("transfer host", () => MultiplayerClient.TransferHost(2));
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddRepeatStep("change user ready state", () =>
{
MultiplayerClient.ChangeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle);
}, 20);
AddRepeatStep("ready all users", () =>
{
var nextUnready = MultiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
if (nextUnready != null)
MultiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
}, users);
}
private void verifyGameplayStartFlow()
{
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
AddStep("finish gameplay", () =>
{
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.Loaded);
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay);
});
AddUntilStep("ready button enabled", () => control.ChildrenOfType<OsuButton>().Single().Enabled.Value);
}
}
}

View File

@ -41,6 +41,7 @@ using osu.Game.Screens.Ranking;
using osu.Game.Screens.Spectate;
using osu.Game.Tests.Resources;
using osuTK.Input;
using ReadyButton = osu.Game.Screens.OnlinePlay.Components.ReadyButton;
namespace osu.Game.Tests.Visual.Multiplayer
{
@ -424,7 +425,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != multiplayerClient.Room?.Playlist.First().BeatmapID);
AddStep("start match externally", () => multiplayerClient.StartMatch());
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
@ -462,7 +463,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("Ruleset doesn't match current item", () => Ruleset.Value.OnlineID != multiplayerClient.Room?.Playlist.First().RulesetID);
AddStep("start match externally", () => multiplayerClient.StartMatch());
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
@ -502,7 +503,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("Mods don't match current item",
() => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
AddStep("start match externally", () => multiplayerClient.StartMatch());
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
@ -538,7 +539,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
AddStep("start match externally", () => multiplayerClient.StartMatch());
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddAssert("play not started", () => multiplayerComponents.IsCurrentScreen());
}
@ -571,7 +572,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
AddStep("start match externally", () => multiplayerClient.StartMatch());
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddStep("restore beatmap", () =>
{
@ -921,7 +922,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("start match by other user", () =>
{
multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready);
multiplayerClient.StartMatch();
multiplayerClient.StartMatch().WaitSafely();
});
AddUntilStep("wait for loading", () => multiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad);

View File

@ -163,6 +163,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("second user crown visible", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(1).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
}
[Test]
public void TestHostGetsPinnedToTop()
{
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
{
Id = 3,
Username = "Second",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}));
AddStep("make second user host", () => MultiplayerClient.TransferHost(3));
AddAssert("second user above first", () =>
{
var first = this.ChildrenOfType<ParticipantPanel>().ElementAt(0);
var second = this.ChildrenOfType<ParticipantPanel>().ElementAt(1);
return second.Y < first.Y;
});
}
[Test]
public void TestKickButtonOnlyPresentWhenHost()
{
@ -202,9 +221,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestManyUsers()
{
const int users_count = 20;
AddStep("add many users", () =>
{
for (int i = 0; i < 20; i++)
for (int i = 0; i < users_count; i++)
{
MultiplayerClient.AddUser(new APIUser
{
@ -243,6 +264,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
}
});
AddRepeatStep("switch hosts", () => MultiplayerClient.TransferHost(RNG.Next(0, users_count)), 10);
AddStep("give host back", () => MultiplayerClient.TransferHost(API.LocalUser.Value.Id));
}
[Test]

View File

@ -13,6 +13,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
@ -183,14 +184,41 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertItemInHistoryListStep(2, 0);
}
[Test]
public void TestInsertedItemDoesNotRefreshAllOthers()
{
AddStep("change to round robin queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayersRoundRobin }).WaitSafely());
// Add a few items for the local user.
addItemStep();
addItemStep();
addItemStep();
addItemStep();
addItemStep();
DrawableRoomPlaylistItem[] drawableItems = null;
AddStep("get drawable items", () => drawableItems = this.ChildrenOfType<DrawableRoomPlaylistItem>().ToArray());
// Add 1 item for another user.
AddStep("join second user", () => MultiplayerClient.AddUser(new APIUser { Id = 10 }));
addItemStep(userId: 10);
// New item inserted towards the top of the list.
assertItemInQueueListStep(7, 1);
AddAssert("all previous playlist items remained", () => drawableItems.All(this.ChildrenOfType<DrawableRoomPlaylistItem>().Contains));
}
/// <summary>
/// Adds a step to create a new playlist item.
/// </summary>
private void addItemStep(bool expired = false) => AddStep("add item", () => MultiplayerClient.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)
private void addItemStep(bool expired = false, int? userId = null) => AddStep("add item", () =>
{
Expired = expired,
PlayedAt = DateTimeOffset.Now
})));
MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)
{
Expired = expired,
PlayedAt = DateTimeOffset.Now
})).WaitSafely();
});
/// <summary>
/// Asserts the position of a given playlist item in the queue list.

View File

@ -1,199 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Resources;
using osuTK;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerReadyButton : MultiplayerTestScene
{
private MultiplayerReadyButton button;
private BeatmapSetInfo importedSet;
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
private BeatmapManager beatmaps;
private RulesetStore rulesets;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
}
[SetUp]
public new void Setup() => Schedule(() =>
{
AvailabilityTracker.SelectedItem.BindTo(selectedItem);
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
};
if (button != null)
Remove(button);
Add(button = new MultiplayerReadyButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
});
});
[Test]
public void TestDeletedBeatmapDisableReady()
{
OsuButton readyButton = null;
AddUntilStep("ensure ready button enabled", () =>
{
readyButton = button.ChildrenOfType<OsuButton>().Single();
return readyButton.Enabled.Value;
});
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
AddUntilStep("ready button disabled", () => !readyButton.Enabled.Value);
AddStep("undelete beatmap", () => beatmaps.Undelete(importedSet));
AddUntilStep("ready button enabled back", () => readyButton.Enabled.Value);
}
[Test]
public void TestToggleStateWhenNotHost()
{
AddStep("add second user as host", () =>
{
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
MultiplayerClient.TransferHost(2);
});
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
}
[TestCase(true)]
[TestCase(false)]
public void TestToggleStateWhenHost(bool allReady)
{
AddStep("setup", () =>
{
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
if (!allReady)
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
});
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
verifyGameplayStartFlow();
}
[Test]
public void TestBecomeHostWhileReady()
{
AddStep("add host", () =>
{
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
MultiplayerClient.TransferHost(2);
});
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0));
verifyGameplayStartFlow();
}
[Test]
public void TestLoseHostWhileReady()
{
AddStep("setup", () =>
{
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
});
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0));
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
AddUntilStep("ready button enabled", () => button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
}
[TestCase(true)]
[TestCase(false)]
public void TestManyUsersChangingState(bool isHost)
{
const int users = 10;
AddStep("setup", () =>
{
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
for (int i = 0; i < users; i++)
MultiplayerClient.AddUser(new APIUser { Id = i, Username = "Another user" });
});
if (!isHost)
AddStep("transfer host", () => MultiplayerClient.TransferHost(2));
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddRepeatStep("change user ready state", () =>
{
MultiplayerClient.ChangeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle);
}, 20);
AddRepeatStep("ready all users", () =>
{
var nextUnready = MultiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
if (nextUnready != null)
MultiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
}, users);
}
private void verifyGameplayStartFlow()
{
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
AddStep("finish gameplay", () =>
{
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.Loaded);
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay);
});
AddUntilStep("ready button enabled", () => button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
}
}
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@ -26,7 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public class TestSceneMultiplayerSpectateButton : MultiplayerTestScene
{
private MultiplayerSpectateButton spectateButton;
private MultiplayerReadyButton readyButton;
private MatchStartControl startControl;
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
@ -56,23 +57,27 @@ namespace osu.Game.Tests.Visual.Multiplayer
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID,
};
Child = new FillFlowContainer
Child = new PopoverContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
spectateButton = new MultiplayerSpectateButton
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
},
readyButton = new MultiplayerReadyButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
spectateButton = new MultiplayerSpectateButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
},
startControl = new MatchStartControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
}
}
}
};
@ -141,6 +146,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
=> AddUntilStep($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled);
private void assertReadyButtonEnablement(bool shouldBeEnabled)
=> AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => readyButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled);
=> AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => startControl.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value == shouldBeEnabled);
}
}

View File

@ -3,7 +3,9 @@
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using NUnit.Framework;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Database;
@ -28,6 +30,23 @@ namespace osu.Game.Tests.Visual.Navigation
stream.CopyTo(outStream);
}
[SetUp]
public void SetUp()
{
if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && RuntimeInformation.OSArchitecture == Architecture.Arm64)
Assert.Ignore("EF-to-realm migrations are not supported on M1 ARM architectures.");
}
public override void SetUpSteps()
{
// base SetUpSteps are executed before the above SetUp, therefore early-return to allow ignoring test properly.
// attempting to ignore here would yield a TargetInvocationException instead.
if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && RuntimeInformation.OSArchitecture == Architecture.Arm64)
return;
base.SetUpSteps();
}
[Test]
public void TestMigration()
{

View File

@ -5,9 +5,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
@ -29,6 +31,14 @@ namespace osu.Game.Tests.Visual.Online
private BeatmapListingSearchControl searchControl => overlay.ChildrenOfType<BeatmapListingSearchControl>().Single();
private OsuConfigManager localConfig;
[BackgroundDependencyLoader]
private void load()
{
Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage));
}
[SetUpSteps]
public void SetUpSteps()
{
@ -61,6 +71,8 @@ namespace osu.Game.Tests.Visual.Online
Id = API.LocalUser.Value.Id + 1,
};
});
AddStep("reset size", () => localConfig.SetValue(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal));
}
[Test]
@ -121,23 +133,23 @@ namespace osu.Game.Tests.Visual.Online
}
[Test]
public void TestCardSizeSwitching()
public void TestCardSizeSwitching([Values] bool viaConfig)
{
AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
AddStep("show many results", () => fetchFor(Enumerable.Repeat(CreateAPIBeatmapSet(Ruleset.Value), 100).ToArray()));
assertAllCardsOfType<BeatmapCardNormal>(100);
setCardSize(BeatmapCardSize.Extra);
setCardSize(BeatmapCardSize.Extra, viaConfig);
assertAllCardsOfType<BeatmapCardExtra>(100);
setCardSize(BeatmapCardSize.Normal);
setCardSize(BeatmapCardSize.Normal, viaConfig);
assertAllCardsOfType<BeatmapCardNormal>(100);
AddStep("fetch for 0 beatmaps", () => fetchFor());
AddUntilStep("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true);
setCardSize(BeatmapCardSize.Extra);
setCardSize(BeatmapCardSize.Extra, viaConfig);
AddAssert("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true);
}
@ -361,7 +373,13 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("\"no maps found\" placeholder not shown", () => !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any(d => d.IsPresent));
}
private void setCardSize(BeatmapCardSize cardSize) => AddStep($"set card size to {cardSize}", () => overlay.ChildrenOfType<BeatmapListingCardSizeTabControl>().Single().Current.Value = cardSize);
private void setCardSize(BeatmapCardSize cardSize, bool viaConfig) => AddStep($"set card size to {cardSize}", () =>
{
if (viaConfig)
localConfig.SetValue(OsuSetting.BeatmapListingCardSize, cardSize);
else
overlay.ChildrenOfType<BeatmapListingCardSizeTabControl>().Single().Current.Value = cardSize;
});
private void assertAllCardsOfType<T>(int expectedCount)
where T : BeatmapCard =>
@ -370,5 +388,11 @@ namespace osu.Game.Tests.Visual.Online
int loadedCorrectCount = this.ChildrenOfType<BeatmapCard>().Count(card => card.IsLoaded && card.GetType() == typeof(T));
return loadedCorrectCount > 0 && loadedCorrectCount == expectedCount;
});
protected override void Dispose(bool isDisposing)
{
localConfig?.Dispose();
base.Dispose(isDisposing);
}
}
}

View File

@ -71,7 +71,9 @@ namespace osu.Game.Tests.Visual.Online
{
Fails = Enumerable.Range(1, 100).Select(_ => RNG.Next(10)).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(_ => RNG.Next(10)).ToArray(),
}
},
PassCount = RNG.Next(0, 999),
PlayCount = RNG.Next(1000, 1999),
};
}

View File

@ -0,0 +1,122 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
using osu.Game.Overlays.Chat;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public class TestSceneChatTextBox : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
[Cached]
private readonly Bindable<Channel> currentChannel = new Bindable<Channel>();
private OsuSpriteText commitText;
private OsuSpriteText searchText;
private ChatTextBar bar;
[SetUp]
public void SetUp()
{
Schedule(() =>
{
Child = new GridContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
RowDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 30),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(),
},
Content = new[]
{
new Drawable[]
{
commitText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Font = OsuFont.Default.With(size: 20),
},
searchText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Font = OsuFont.Default.With(size: 20),
},
},
},
},
},
new Drawable[]
{
bar = new ChatTextBar
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 0.99f,
},
},
},
};
bar.OnChatMessageCommitted += text =>
{
commitText.Text = $"{nameof(bar.OnChatMessageCommitted)}: {text}";
commitText.FadeOutFromOne(1000, Easing.InQuint);
};
bar.OnSearchTermsChanged += text =>
{
searchText.Text = $"{nameof(bar.OnSearchTermsChanged)}: {text}";
};
});
}
[Test]
public void TestVisual()
{
AddStep("Public Channel", () => currentChannel.Value = createPublicChannel("#osu"));
AddStep("Public Channel Long Name", () => currentChannel.Value = createPublicChannel("#public-channel-long-name"));
AddStep("Private Channel", () => currentChannel.Value = createPrivateChannel("peppy", 2));
AddStep("Private Long Name", () => currentChannel.Value = createPrivateChannel("test user long name", 3));
AddStep("Chat Mode Channel", () => bar.ShowSearch.Value = false);
AddStep("Chat Mode Search", () => bar.ShowSearch.Value = true);
}
private static Channel createPublicChannel(string name)
=> new Channel { Name = name, Type = ChannelType.Public, Id = 1234 };
private static Channel createPrivateChannel(string username, int id)
=> new Channel(new APIUser { Id = id, Username = username });
}
}

View File

@ -1,16 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings.Sections;
using osu.Game.Overlays.Settings.Sections.Input;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Settings
{
[TestFixture]
public class TestSceneSettingsPanel : OsuTestScene
public class TestSceneSettingsPanel : OsuManualInputManagerTestScene
{
private SettingsPanel settings;
private DialogOverlay dialogOverlay;
@ -33,7 +38,55 @@ namespace osu.Game.Tests.Visual.Settings
public void ToggleVisibility()
{
AddWaitStep("wait some", 5);
AddToggleStep("toggle editor visibility", visible => settings.ToggleVisibility());
AddToggleStep("toggle visibility", visible => settings.ToggleVisibility());
}
[Test]
public void TestTextboxFocusAfterNestedPanelBackButton()
{
AddUntilStep("sections loaded", () => settings.SectionsContainer.Children.Count > 0);
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
AddStep("open key binding subpanel", () =>
{
settings.SectionsContainer
.ChildrenOfType<InputSection>().FirstOrDefault()?
.ChildrenOfType<OsuButton>().FirstOrDefault()?
.TriggerClick();
});
AddUntilStep("binding panel textbox focused", () => settings
.ChildrenOfType<KeyBindingPanel>().FirstOrDefault()?
.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
AddStep("Press back", () => settings
.ChildrenOfType<KeyBindingPanel>().FirstOrDefault()?
.ChildrenOfType<SettingsSubPanel.BackButton>().FirstOrDefault()?.TriggerClick());
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
}
[Test]
public void TestTextboxFocusAfterNestedPanelEscape()
{
AddUntilStep("sections loaded", () => settings.SectionsContainer.Children.Count > 0);
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
AddStep("open key binding subpanel", () =>
{
settings.SectionsContainer
.ChildrenOfType<InputSection>().FirstOrDefault()?
.ChildrenOfType<OsuButton>().FirstOrDefault()?
.TriggerClick();
});
AddUntilStep("binding panel textbox focused", () => settings
.ChildrenOfType<KeyBindingPanel>().FirstOrDefault()?
.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
AddStep("Escape", () => InputManager.Key(Key.Escape));
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
}
[BackgroundDependencyLoader]

View File

@ -116,7 +116,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private void testBeatmapLabels(Ruleset ruleset)
{
AddAssert("check version", () => infoWedge.Info.VersionLabel.Current.Value == $"{ruleset.ShortName}Version");
AddAssert("check title", () => infoWedge.Info.TitleLabel.Current.Value == $"{ruleset.ShortName}Source — {ruleset.ShortName}Title");
AddAssert("check title", () => infoWedge.Info.TitleLabel.Current.Value == $"{ruleset.ShortName}Title");
AddAssert("check artist", () => infoWedge.Info.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist");
AddAssert("check author", () => infoWedge.Info.MapperContainer.ChildrenOfType<OsuSpriteText>().Any(s => s.Current.Value == $"{ruleset.ShortName}Author"));
}

View File

@ -68,7 +68,9 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("reset defaults", () =>
{
Ruleset.Value = new OsuRuleset().RulesetInfo;
Beatmap.SetDefault();
SelectedMods.SetDefault();
songSelect = null;
});
@ -563,7 +565,7 @@ namespace osu.Game.Tests.Visual.SongSelect
}
[Test]
public void TestAutoplayViaCtrlEnter()
public void TestAutoplayShortcut()
{
addRulesetImportStep(0);
@ -580,11 +582,65 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader);
AddAssert("autoplay enabled", () => songSelect.Mods.Value.FirstOrDefault() is ModAutoplay);
AddAssert("autoplay selected", () => songSelect.Mods.Value.Single() is ModAutoplay);
AddUntilStep("wait for return to ss", () => songSelect.IsCurrentScreen());
AddAssert("mod disabled", () => songSelect.Mods.Value.Count == 0);
AddAssert("no mods selected", () => songSelect.Mods.Value.Count == 0);
}
[Test]
public void TestAutoplayShortcutKeepsAutoplayIfSelectedAlready()
{
addRulesetImportStep(0);
createSongSelect();
AddUntilStep("wait for selection", () => !Beatmap.IsDefault);
changeMods(new OsuModAutoplay());
AddStep("press ctrl+enter", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Enter);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader);
AddAssert("autoplay selected", () => songSelect.Mods.Value.Single() is ModAutoplay);
AddUntilStep("wait for return to ss", () => songSelect.IsCurrentScreen());
AddAssert("autoplay still selected", () => songSelect.Mods.Value.Single() is ModAutoplay);
}
[Test]
public void TestAutoplayShortcutReturnsInitialModsOnExit()
{
addRulesetImportStep(0);
createSongSelect();
AddUntilStep("wait for selection", () => !Beatmap.IsDefault);
changeMods(new OsuModRelax());
AddStep("press ctrl+enter", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Enter);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader);
AddAssert("only autoplay selected", () => songSelect.Mods.Value.Single() is ModAutoplay);
AddUntilStep("wait for return to ss", () => songSelect.IsCurrentScreen());
AddAssert("relax returned", () => songSelect.Mods.Value.Single() is ModRelax);
}
[Test]

View File

@ -44,9 +44,6 @@ namespace osu.Game.Tests.Visual.UserInterface
private BeatmapInfo beatmapInfo;
[Resolved]
private RealmAccess realm { get; set; }
[Cached]
private readonly DialogOverlay dialogOverlay;
@ -92,6 +89,12 @@ namespace osu.Game.Tests.Visual.UserInterface
dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get<RulesetStore>(), () => beatmapManager, LocalStorage, Realm, Scheduler));
Dependencies.Cache(Realm);
return dependencies;
}
[BackgroundDependencyLoader]
private void load() => Schedule(() =>
{
var imported = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely();
imported?.PerformRead(s =>
@ -115,26 +118,26 @@ namespace osu.Game.Tests.Visual.UserInterface
importedScores.Add(scoreManager.Import(score).Value);
}
});
return dependencies;
}
[SetUp]
public void Setup() => Schedule(() =>
{
realm.Run(r =>
{
// Due to soft deletions, we can re-use deleted scores between test runs
scoreManager.Undelete(r.All<ScoreInfo>().Where(s => s.DeletePending).ToList());
});
leaderboard.BeatmapInfo = beatmapInfo;
leaderboard.RefetchScores(); // Required in the case that the beatmap hasn't changed
});
[SetUpSteps]
public void SetupSteps()
{
AddUntilStep("ensure scores imported", () => importedScores.Count == 50);
AddStep("undelete scores", () =>
{
Realm.Run(r =>
{
// Due to soft deletions, we can re-use deleted scores between test runs
scoreManager.Undelete(r.All<ScoreInfo>().Where(s => s.DeletePending).ToList());
});
});
AddStep("set up leaderboard", () =>
{
leaderboard.BeatmapInfo = beatmapInfo;
leaderboard.RefetchScores(); // Required in the case that the beatmap hasn't changed
});
// Ensure the leaderboard items have finished showing up
AddStep("finish transforms", () => leaderboard.FinishTransforms(true));
AddUntilStep("wait for drawables", () => leaderboard.ChildrenOfType<LeaderboardScore>().Any());
@ -169,11 +172,14 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("click delete button", () =>
{
InputManager.MoveMouseTo(dialogOverlay.ChildrenOfType<DialogButton>().First());
InputManager.Click(MouseButton.Left);
InputManager.PressButton(MouseButton.Left);
});
AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != scoreBeingDeleted.OnlineID));
// "Clean up"
AddStep("release left mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
}
[Test]

View File

@ -0,0 +1,40 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public class TestSceneModSettingsArea : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Test]
public void TestModToggleArea()
{
ModSettingsArea modSettingsArea = null;
AddStep("create content", () => Child = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = modSettingsArea = new ModSettingsArea()
});
AddStep("set DT", () => modSettingsArea.SelectedMods.Value = new[] { new OsuModDoubleTime() });
AddStep("set DA", () => modSettingsArea.SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() });
AddStep("set FL+WU+DA+AD", () => modSettingsArea.SelectedMods.Value = new Mod[] { new OsuModFlashlight(), new ModWindUp(), new OsuModDifficultyAdjust(), new OsuModApproachDifferent() });
AddStep("set empty", () => modSettingsArea.SelectedMods.Value = Array.Empty<Mod>());
}
}
}

View File

@ -0,0 +1,108 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public class TestSceneShearedToggleButton : OsuManualInputManagerTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Test]
public void TestShearedToggleButton()
{
ShearedToggleButton button = null;
AddStep("create button", () =>
{
Child = button = new ShearedToggleButton(200)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Toggle me",
};
});
AddToggleStep("toggle button", active => button.Active.Value = active);
AddToggleStep("toggle disabled", disabled => button.Active.Disabled = disabled);
}
[Test]
public void TestSizing()
{
ShearedToggleButton toggleButton = null;
AddStep("create fixed width button", () => Child = toggleButton = new ShearedToggleButton(200)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Fixed width"
});
AddStep("change text", () => toggleButton.Text = "New text");
AddStep("create auto-sizing button", () => Child = toggleButton = new ShearedToggleButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "This button autosizes to its text!"
});
AddStep("change text", () => toggleButton.Text = "New text");
}
[Test]
public void TestDisabledState()
{
ShearedToggleButton button = null;
AddStep("create button", () =>
{
Child = button = new ShearedToggleButton(200)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Toggle me",
};
});
clickToggle();
assertToggleState(true);
clickToggle();
assertToggleState(false);
setToggleDisabledState(true);
assertToggleState(false);
clickToggle();
assertToggleState(false);
setToggleDisabledState(false);
assertToggleState(false);
clickToggle();
assertToggleState(true);
setToggleDisabledState(true);
assertToggleState(true);
clickToggle();
assertToggleState(true);
void clickToggle() => AddStep("click toggle", () =>
{
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
void assertToggleState(bool active) => AddAssert($"toggle is {(active ? "" : "not ")}active", () => button.Active.Value == active);
void setToggleDisabledState(bool disabled) => AddStep($"{(disabled ? "disable" : "enable")} toggle", () => button.Active.Disabled = disabled);
}
}
}

View File

@ -1,9 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.IO;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Platform;
using osu.Game.Rulesets;
using osu.Game.Tests;
@ -12,6 +16,45 @@ namespace osu.Game.Tournament.Tests.NonVisual
{
public class DataLoadTest : TournamentHostTest
{
[Test]
public void TestRulesetGetsValidOnlineID()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = new TestTournament(runOnLoadComplete: () =>
{
// ReSharper disable once AccessToDisposedClosure
var storage = host.Storage.GetStorageForDirectory(Path.Combine("tournaments", "default"));
using (var stream = storage.GetStream("bracket.json", FileAccess.Write, FileMode.Create))
using (var writer = new StreamWriter(stream))
{
writer.Write(@"{
""Ruleset"": {
""ShortName"": ""taiko"",
""OnlineID"": -1,
""Name"": ""osu!taiko"",
""InstantiationInfo"": ""osu.Game.Rulesets.OsuTaiko.TaikoRuleset, osu.Game.Rulesets.Taiko"",
""Available"": true
} }");
}
});
LoadTournament(host, osu);
osu.BracketLoadTask.WaitSafely();
Assert.That(osu.Dependencies.Get<IBindable<RulesetInfo>>().Value.OnlineID, Is.EqualTo(1));
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestUnavailableRuleset()
{
@ -19,7 +62,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
{
try
{
var osu = new TestTournament();
var osu = new TestTournament(true);
LoadTournament(host, osu);
var storage = osu.Dependencies.Get<Storage>();
@ -35,10 +78,23 @@ namespace osu.Game.Tournament.Tests.NonVisual
public class TestTournament : TournamentGameBase
{
private readonly bool resetRuleset;
private readonly Action runOnLoadComplete;
public new Task BracketLoadTask => base.BracketLoadTask;
public TestTournament(bool resetRuleset = false, Action runOnLoadComplete = null)
{
this.resetRuleset = resetRuleset;
this.runOnLoadComplete = runOnLoadComplete;
}
protected override void LoadComplete()
{
runOnLoadComplete?.Invoke();
base.LoadComplete();
Ruleset.Value = new RulesetInfo(); // not available
if (resetRuleset)
Ruleset.Value = new RulesetInfo(); // not available
}
}
}

View File

@ -26,7 +26,7 @@ namespace osu.Game.Tournament.Screens.Setup
dropdown.Current.BindValueChanged(v => Button.Enabled.Value = v.NewValue != startupTournament, true);
Action = () => game.GracefullyExit();
folderButton.Action = storage.PresentExternally;
folderButton.Action = () => storage.PresentExternally();
ButtonText = "Close osu!";
}

View File

@ -64,6 +64,16 @@ namespace osu.Game.Tournament
Textures.AddStore(new TextureLoaderStore(new StorageBackedResourceStore(storage)));
dependencies.CacheAs(new StableInfo(storage));
}
protected override void LoadComplete()
{
MenuCursorContainer.Cursor.AlwaysPresent = true; // required for tooltip display
// we don't want to show the menu cursor as it would appear on stream output.
MenuCursorContainer.Cursor.Alpha = 0;
base.LoadComplete();
Task.Run(readBracket);
}
@ -81,10 +91,14 @@ namespace osu.Game.Tournament
ladder ??= new LadderInfo();
ladder.Ruleset.Value = ladder.Ruleset.Value != null
var resolvedRuleset = ladder.Ruleset.Value != null
? RulesetStore.GetRuleset(ladder.Ruleset.Value.ShortName)
: RulesetStore.AvailableRulesets.First();
// Must set to null initially to avoid the following re-fetch hitting `ShortName` based equality check.
ladder.Ruleset.Value = null;
ladder.Ruleset.Value = resolvedRuleset;
bool addedInfo = false;
// assign teams
@ -282,16 +296,6 @@ namespace osu.Game.Tournament
}
}
protected override void LoadComplete()
{
MenuCursorContainer.Cursor.AlwaysPresent = true; // required for tooltip display
// we don't want to show the menu cursor as it would appear on stream output.
MenuCursorContainer.Cursor.Alpha = 0;
base.LoadComplete();
}
protected virtual void SaveChanges()
{
if (!bracketLoadTaskCompletionSource.Task.IsCompletedSuccessfully)

View File

@ -7,8 +7,10 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Tournament.Components;
using osu.Game.Tournament.Screens;
using osu.Game.Tournament.Screens.Drawings;
@ -23,6 +25,7 @@ using osu.Game.Tournament.Screens.TeamIntro;
using osu.Game.Tournament.Screens.TeamWin;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tournament
{
@ -123,16 +126,16 @@ namespace osu.Game.Tournament
new ScreenButton(typeof(RoundEditorScreen)) { Text = "Rounds Editor", RequestSelection = SetScreen },
new ScreenButton(typeof(LadderEditorScreen)) { Text = "Bracket Editor", RequestSelection = SetScreen },
new Separator(),
new ScreenButton(typeof(ScheduleScreen)) { Text = "Schedule", RequestSelection = SetScreen },
new ScreenButton(typeof(LadderScreen)) { Text = "Bracket", RequestSelection = SetScreen },
new ScreenButton(typeof(ScheduleScreen), Key.S) { Text = "Schedule", RequestSelection = SetScreen },
new ScreenButton(typeof(LadderScreen), Key.B) { Text = "Bracket", RequestSelection = SetScreen },
new Separator(),
new ScreenButton(typeof(TeamIntroScreen)) { Text = "Team Intro", RequestSelection = SetScreen },
new ScreenButton(typeof(SeedingScreen)) { Text = "Seeding", RequestSelection = SetScreen },
new ScreenButton(typeof(TeamIntroScreen), Key.I) { Text = "Team Intro", RequestSelection = SetScreen },
new ScreenButton(typeof(SeedingScreen), Key.D) { Text = "Seeding", RequestSelection = SetScreen },
new Separator(),
new ScreenButton(typeof(MapPoolScreen)) { Text = "Map Pool", RequestSelection = SetScreen },
new ScreenButton(typeof(GameplayScreen)) { Text = "Gameplay", RequestSelection = SetScreen },
new ScreenButton(typeof(MapPoolScreen), Key.M) { Text = "Map Pool", RequestSelection = SetScreen },
new ScreenButton(typeof(GameplayScreen), Key.G) { Text = "Gameplay", RequestSelection = SetScreen },
new Separator(),
new ScreenButton(typeof(TeamWinScreen)) { Text = "Win", RequestSelection = SetScreen },
new ScreenButton(typeof(TeamWinScreen), Key.W) { Text = "Win", RequestSelection = SetScreen },
new Separator(),
new ScreenButton(typeof(DrawingsScreen)) { Text = "Drawings", RequestSelection = SetScreen },
new ScreenButton(typeof(ShowcaseScreen)) { Text = "Showcase", RequestSelection = SetScreen },
@ -231,13 +234,60 @@ namespace osu.Game.Tournament
{
public readonly Type Type;
public ScreenButton(Type type)
private readonly Key? shortcutKey;
public ScreenButton(Type type, Key? shortcutKey = null)
{
this.shortcutKey = shortcutKey;
Type = type;
BackgroundColour = OsuColour.Gray(0.2f);
Action = () => RequestSelection?.Invoke(type);
RelativeSizeAxes = Axes.X;
if (shortcutKey != null)
{
Add(new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(24),
Margin = new MarginPadding(5),
Masking = true,
CornerRadius = 4,
Alpha = 0.5f,
Blending = BlendingParameters.Additive,
Children = new Drawable[]
{
new Box
{
Colour = OsuColour.Gray(0.1f),
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Font = OsuFont.Default.With(size: 24),
Y = -2,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = shortcutKey.ToString(),
}
}
});
}
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Key == shortcutKey)
{
TriggerClick();
return true;
}
return base.OnKeyDown(e);
}
private bool isSelected;

View File

@ -19,6 +19,11 @@ namespace osu.Game.Beatmaps.Formats
{
public class LegacyBeatmapDecoder : LegacyDecoder<Beatmap>
{
/// <summary>
/// An offset which needs to be applied to old beatmaps (v4 and lower) to correct timing changes that were applied at a game client level.
/// </summary>
public const int EARLY_VERSION_TIMING_OFFSET = 24;
internal static RulesetStore RulesetStore;
private Beatmap beatmap;
@ -50,8 +55,7 @@ namespace osu.Game.Beatmaps.Formats
RulesetStore = new AssemblyRulesetStore();
}
// BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off)
offset = FormatVersion < 5 ? 24 : 0;
offset = FormatVersion < 5 ? EARLY_VERSION_TIMING_OFFSET : 0;
}
protected override Beatmap CreateTemplateObject()

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Beatmaps
{
@ -70,4 +71,27 @@ namespace osu.Game.Beatmaps
/// </summary>
new IReadOnlyList<T> HitObjects { get; }
}
public static class BeatmapExtensions
{
/// <summary>
/// Finds the maximum achievable combo by hitting all <see cref="HitObject"/>s in a beatmap.
/// </summary>
public static int GetMaxCombo(this IBeatmap beatmap)
{
int combo = 0;
foreach (var h in beatmap.HitObjects)
addCombo(h, ref combo);
return combo;
static void addCombo(HitObject hitObject, ref int combo)
{
if (hitObject.CreateJudgement().MaxResult.AffectsCombo())
combo++;
foreach (var nested in hitObject.NestedHitObjects)
addCombo(nested, ref combo);
}
}
}
}

View File

@ -225,7 +225,7 @@ namespace osu.Game.Beatmaps
{
try
{
return new LegacyBeatmapSkin(BeatmapInfo, resources.Files, resources);
return new LegacyBeatmapSkin(BeatmapInfo, resources);
}
catch (Exception e)
{

View File

@ -10,6 +10,7 @@ using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
@ -44,6 +45,10 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f);
SetDefault(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal);
SetDefault(OsuSetting.ToolbarClockDisplayMode, ToolbarClockDisplayMode.Full);
// Online settings
SetDefault(OsuSetting.Username, string.Empty);
SetDefault(OsuSetting.Token, string.Empty);
@ -295,6 +300,8 @@ namespace osu.Game.Configuration
RandomSelectAlgorithm,
ShowFpsDisplay,
ChatDisplayHeight,
BeatmapListingCardSize,
ToolbarClockDisplayMode,
Version,
ShowConvertedBeatmaps,
Skin,

View File

@ -88,6 +88,7 @@ namespace osu.Game.Configuration
throw new InvalidOperationException($"{nameof(SettingSourceAttribute)} had an unsupported custom control type ({controlType.ReadableName()})");
var control = (Drawable)Activator.CreateInstance(controlType);
controlType.GetProperty(nameof(SettingsItem<object>.SettingSourceObject))?.SetValue(control, obj);
controlType.GetProperty(nameof(SettingsItem<object>.LabelText))?.SetValue(control, attr.Label);
controlType.GetProperty(nameof(SettingsItem<object>.TooltipText))?.SetValue(control, attr.Description);
controlType.GetProperty(nameof(SettingsItem<object>.Current))?.SetValue(control, value);

View File

@ -0,0 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Configuration
{
public enum ToolbarClockDisplayMode
{
Analog,
Digital,
DigitalWithRuntime,
Full
}
}

View File

@ -1,10 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Graphics;
@ -14,6 +18,7 @@ using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Models;
@ -29,8 +34,6 @@ using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Writers.Zip;
#nullable enable
namespace osu.Game.Database
{
internal class EFToRealmMigrator : CompositeDrawable
@ -57,7 +60,7 @@ namespace osu.Game.Database
[Resolved]
private Storage storage { get; set; } = null!;
private readonly OsuSpriteText currentOperationText;
private readonly OsuTextFlowContainer currentOperationText;
public EFToRealmMigrator()
{
@ -99,11 +102,13 @@ namespace osu.Game.Database
{
State = { Value = Visibility.Visible }
},
currentOperationText = new OsuSpriteText
currentOperationText = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 30))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Default.With(size: 30)
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
TextAnchor = Anchor.TopCentre,
},
}
},
@ -147,19 +152,34 @@ namespace osu.Game.Database
log("Migration successful!");
if (DebugUtils.IsDebugBuild)
Logger.Log("Your development database has been fully migrated to realm. If you switch back to a pre-realm branch and need your previous database, rename the backup file back to \"client.db\".\n\nNote that doing this can potentially leave your file store in a bad state.", level: LogLevel.Important);
{
Logger.Log(
"Your development database has been fully migrated to realm. If you switch back to a pre-realm branch and need your previous database, rename the backup file back to \"client.db\".\n\nNote that doing this can potentially leave your file store in a bad state.",
level: LogLevel.Important);
}
}
else
{
log("Migration failed!");
Logger.Log(t.Exception.ToString(), LoggingTarget.Database);
if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && t.Exception.Flatten().InnerException is TypeInitializationException)
{
// Not guaranteed to be the only cause of exception, but let's roll with it for now.
log("Please download and run the intel version of osu! once\nto allow data migration to complete!");
efContextFactory.SetMigrationCompletion();
return;
}
notificationOverlay.Post(new SimpleErrorNotification
{
Text = "IMPORTANT: During data migration, some of your data could not be successfully migrated. The previous version has been backed up.\n\nFor further assistance, please open a discussion on github and attach your backup files (click to get started).",
Text =
"IMPORTANT: During data migration, some of your data could not be successfully migrated. The previous version has been backed up.\n\nFor further assistance, please open a discussion on github and attach your backup files (click to get started).",
Activated = () =>
{
game.OpenUrlExternally($@"https://github.com/ppy/osu/discussions/new?title=Realm%20migration%20issue ({t.Exception.Message})&body=Please%20drag%20the%20""attach_me.zip""%20file%20here!&category=q-a", true);
game.OpenUrlExternally(
$@"https://github.com/ppy/osu/discussions/new?title=Realm%20migration%20issue ({t.Exception.Message})&body=Please%20drag%20the%20""attach_me.zip""%20file%20here!&category=q-a",
true);
const string attachment_filename = "attach_me.zip";
const string backup_folder = "backups";

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Collections.Generic;
using System.ComponentModel;
@ -11,12 +13,14 @@ using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Input.Bindings;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Statistics;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Input.Bindings;
@ -28,8 +32,6 @@ using osu.Game.Stores;
using Realms;
using Realms.Exceptions;
#nullable enable
namespace osu.Game.Database
{
/// <summary>
@ -46,6 +48,8 @@ namespace osu.Game.Database
private readonly IDatabaseContextFactory? efContextFactory;
private readonly SynchronizationContext? updateThreadSyncContext;
/// <summary>
/// Version history:
/// 6 ~2021-10-18 First tracked version.
@ -143,12 +147,15 @@ namespace osu.Game.Database
/// </summary>
/// <param name="storage">The game storage which will be used to create the realm backing file.</param>
/// <param name="filename">The filename to use for the realm backing file. A ".realm" extension will be added automatically if not specified.</param>
/// <param name="updateThread">The game update thread, used to post realm operations into a thread-safe context.</param>
/// <param name="efContextFactory">An EF factory used only for migration purposes.</param>
public RealmAccess(Storage storage, string filename, IDatabaseContextFactory? efContextFactory = null)
public RealmAccess(Storage storage, string filename, GameThread? updateThread = null, IDatabaseContextFactory? efContextFactory = null)
{
this.storage = storage;
this.efContextFactory = efContextFactory;
updateThreadSyncContext = updateThread?.SynchronizationContext ?? SynchronizationContext.Current;
Filename = filename;
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
@ -205,7 +212,7 @@ namespace osu.Game.Database
if (realm.All<ScoreInfo>().Any())
{
Logger.Log(@"Recovery aborted as the existing database has scores set already.", LoggingTarget.Database);
Logger.Log(@"To perform recovery, delete client.realm while osu! is not running.", LoggingTarget.Database);
Logger.Log($@"To perform recovery, delete {OsuGameBase.CLIENT_DATABASE_FILENAME} while osu! is not running.", LoggingTarget.Database);
return;
}
}
@ -287,7 +294,18 @@ namespace osu.Game.Database
/// Compact this realm.
/// </summary>
/// <returns></returns>
public bool Compact() => Realm.Compact(getConfiguration());
public bool Compact()
{
try
{
return Realm.Compact(getConfiguration());
}
// Catch can be removed along with entity framework. Is specifically to allow a failure message to arrive to the user (see similar catches in EFToRealmMigrator).
catch (AggregateException ae) when (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && ae.Flatten().InnerException is TypeInitializationException)
{
return true;
}
}
/// <summary>
/// Run work on realm with a return value.
@ -379,9 +397,6 @@ namespace osu.Game.Database
public IDisposable RegisterForNotifications<T>(Func<Realm, IQueryable<T>> query, NotificationCallbackDelegate<T> callback)
where T : RealmObjectBase
{
if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread.");
lock (realmLock)
{
Func<Realm, IDisposable?> action = realm => query(realm).QueryAsyncWithNotifications(callback);
@ -459,23 +474,24 @@ namespace osu.Game.Database
/// <returns>An <see cref="IDisposable"/> which should be disposed to unsubscribe any inner subscription.</returns>
public IDisposable RegisterCustomSubscription(Func<Realm, IDisposable?> action)
{
if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread.");
var syncContext = SynchronizationContext.Current;
if (updateThreadSyncContext == null)
throw new InvalidOperationException("Attempted to register a realm subscription before update thread registration.");
total_subscriptions.Value++;
registerSubscription(action);
if (ThreadSafety.IsUpdateThread)
updateThreadSyncContext.Send(_ => registerSubscription(action), null);
else
updateThreadSyncContext.Post(_ => registerSubscription(action), null);
// This token is returned to the consumer.
// When disposed, it will cause the registration to be permanently ceased (unsubscribed with realm and unregistered by this class).
return new InvokeOnDisposal(() =>
{
if (ThreadSafety.IsUpdateThread)
syncContext.Send(_ => unsubscribe(), null);
updateThreadSyncContext.Send(_ => unsubscribe(), null);
else
syncContext.Post(_ => unsubscribe(), null);
updateThreadSyncContext.Post(_ => unsubscribe(), null);
void unsubscribe()
{
@ -538,6 +554,11 @@ namespace osu.Game.Database
return Realm.GetInstance(getConfiguration());
}
// Catch can be removed along with entity framework. Is specifically to allow a failure message to arrive to the user (see similar catches in EFToRealmMigrator).
catch (AggregateException ae) when (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && ae.Flatten().InnerException is TypeInitializationException)
{
return Realm.GetInstance();
}
finally
{
if (tookSemaphoreLock)

View File

@ -28,7 +28,7 @@ namespace osu.Game.Graphics.UserInterface
if (!allowImmediateFocus)
return;
Scheduler.Add(() => GetContainingInputManager().ChangeFocus(this), false);
Scheduler.Add(() => GetContainingInputManager().ChangeFocus(this));
}
public new void KillFocus() => base.KillFocus();

View File

@ -0,0 +1,177 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Graphics.UserInterface
{
public class ShearedToggleButton : OsuClickableContainer
{
public BindableBool Active { get; } = new BindableBool();
public LocalisableString Text
{
get => text.Text;
set => text.Text = value;
}
private readonly Box background;
private readonly OsuSpriteText text;
private Sample? sampleOff;
private Sample? sampleOn;
private const float shear = 0.2f;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
/// <summary>
/// Creates a new <see cref="ShearedToggleButton"/>
/// </summary>
/// <param name="width">
/// The width of the button.
/// <list type="bullet">
/// <item>If a non-<see langword="null"/> value is provided, this button will have a fixed width equal to the provided value.</item>
/// <item>If a <see langword="null"/> value is provided (or the argument is omitted entirely), the button will autosize in width to fit the text.</item>
/// </list>
/// </param>
public ShearedToggleButton(float? width = null)
{
Height = 50;
Padding = new MarginPadding { Horizontal = shear * 50 };
Content.CornerRadius = 7;
Content.Shear = new Vector2(shear, 0);
Content.Masking = true;
Content.BorderThickness = 2;
Content.Anchor = Content.Origin = Anchor.Centre;
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.TorusAlternate.With(size: 17),
Shear = new Vector2(-shear, 0)
}
};
if (width != null)
{
Width = width.Value;
}
else
{
AutoSizeAxes = Axes.X;
text.Margin = new MarginPadding { Horizontal = 15 };
}
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
sampleOn = audio.Samples.Get(@"UI/check-on");
sampleOff = audio.Samples.Get(@"UI/check-off");
}
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
protected override void LoadComplete()
{
base.LoadComplete();
Active.BindValueChanged(_ =>
{
updateState();
playSample();
});
Active.BindDisabledChanged(disabled =>
{
updateState();
Action = disabled ? (Action?)null : Active.Toggle;
}, true);
FinishTransforms(true);
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateState();
base.OnHoverLost(e);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
Content.ScaleTo(0.8f, 2000, Easing.OutQuint);
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
Content.ScaleTo(1, 1000, Easing.OutElastic);
base.OnMouseUp(e);
}
private void updateState()
{
var darkerColour = Active.Value ? colourProvider.Highlight1 : colourProvider.Background3;
var lighterColour = Active.Value ? colourProvider.Colour0 : colourProvider.Background1;
if (Active.Disabled)
{
darkerColour = darkerColour.Darken(0.3f);
lighterColour = lighterColour.Darken(0.3f);
}
else if (IsHovered)
{
darkerColour = darkerColour.Lighten(0.3f);
lighterColour = lighterColour.Lighten(0.3f);
}
background.FadeColour(darkerColour, 150, Easing.OutQuint);
Content.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(darkerColour, lighterColour), 150, Easing.OutQuint);
var textColour = Active.Value ? colourProvider.Background6 : colourProvider.Content1;
if (Active.Disabled)
textColour = textColour.Opacity(0.6f);
text.FadeColour(textColour, 150, Easing.OutQuint);
}
private void playSample()
{
if (Active.Value)
sampleOn?.Play();
else
sampleOff?.Play();
}
}
}

View File

@ -36,15 +36,15 @@ namespace osu.Game.IO
public override string[] IgnoreDirectories => new[]
{
"cache",
"client.realm.management"
$"{OsuGameBase.CLIENT_DATABASE_FILENAME}.management",
};
public override string[] IgnoreFiles => new[]
{
"framework.ini",
"storage.ini",
"client.realm.note",
"client.realm.lock",
$"{OsuGameBase.CLIENT_DATABASE_FILENAME}.note",
$"{OsuGameBase.CLIENT_DATABASE_FILENAME}.lock",
};
public OsuStorage(GameHost host, Storage defaultStorage)
@ -64,12 +64,22 @@ namespace osu.Game.IO
/// </summary>
public void ResetCustomStoragePath()
{
storageConfig.SetValue(StorageConfig.FullPath, string.Empty);
storageConfig.Save();
ChangeDataPath(string.Empty);
ChangeTargetStorage(defaultStorage);
}
/// <summary>
/// Updates the target data path without immediately switching.
/// This does NOT migrate any data.
/// The game should immediately be restarted after calling this.
/// </summary>
public void ChangeDataPath(string newPath)
{
storageConfig.SetValue(StorageConfig.FullPath, newPath);
storageConfig.Save();
}
/// <summary>
/// Attempts to change to the user's custom storage path.
/// </summary>
@ -117,8 +127,7 @@ namespace osu.Game.IO
{
bool cleanupSucceeded = base.Migrate(newStorage);
storageConfig.SetValue(StorageConfig.FullPath, newStorage.GetFullPath("."));
storageConfig.Save();
ChangeDataPath(newStorage.GetFullPath("."));
return cleanupSucceeded;
}

View File

@ -70,9 +70,9 @@ namespace osu.Game.IO
public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) =>
UnderlyingStorage.GetStream(MutatePath(path), access, mode);
public override void OpenFileExternally(string filename) => UnderlyingStorage.OpenFileExternally(MutatePath(filename));
public override bool OpenFileExternally(string filename) => UnderlyingStorage.OpenFileExternally(MutatePath(filename));
public override void PresentFileExternally(string filename) => UnderlyingStorage.PresentFileExternally(MutatePath(filename));
public override bool PresentFileExternally(string filename) => UnderlyingStorage.PresentFileExternally(MutatePath(filename));
public override Storage GetStorageForDirectory(string path)
{

View File

@ -15,6 +15,11 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonObject(MemberSerialization.OptIn)]
public class APIUser : IEquatable<APIUser>, IUser
{
/// <summary>
/// A user ID which can be used to represent any system user which is not attached to a user profile.
/// </summary>
public const int SYSTEM_USER_ID = 0;
[JsonProperty(@"id")]
public int Id { get; set; } = 1;
@ -238,7 +243,7 @@ namespace osu.Game.Online.API.Requests.Responses
/// </summary>
public static readonly APIUser SYSTEM_USER = new APIUser
{
Id = 0,
Id = SYSTEM_USER_ID,
Username = "system",
Colour = @"9c0101",
};

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using MessagePack;
namespace osu.Game.Online.Multiplayer.Countdown
{
/// <summary>
/// Indicates a change to the <see cref="MultiplayerRoom"/>'s countdown.
/// </summary>
[MessagePackObject]
public class CountdownChangedEvent : MatchServerEvent
{
/// <summary>
/// The new countdown.
/// </summary>
[Key(0)]
public MultiplayerCountdown? Countdown { get; set; }
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using MessagePack;
#nullable enable
namespace osu.Game.Online.Multiplayer.Countdown
{
/// <summary>
/// A request for a countdown to start the match.
/// </summary>
[MessagePackObject]
public class StartMatchCountdownRequest : MatchUserRequest
{
/// <summary>
/// How long the countdown should last.
/// </summary>
[Key(0)]
public TimeSpan Duration { get; set; }
}
}

View File

@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using MessagePack;
namespace osu.Game.Online.Multiplayer.Countdown
{
/// <summary>
/// Request to stop the current countdown.
/// </summary>
[MessagePackObject]
public class StopCountdownRequest : MatchUserRequest
{
}
}

View File

@ -1,8 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using MessagePack;
using osu.Game.Online.Multiplayer.Countdown;
namespace osu.Game.Online.Multiplayer
{
@ -11,6 +14,8 @@ namespace osu.Game.Online.Multiplayer
/// </summary>
[Serializable]
[MessagePackObject]
// IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
[Union(0, typeof(CountdownChangedEvent))]
public abstract class MatchServerEvent
{
}

View File

@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using MessagePack;
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// A <see cref="MultiplayerCountdown"/> which will start the match after ending.
/// </summary>
[MessagePackObject]
public class MatchStartCountdown : MultiplayerCountdown
{
}
}

View File

@ -7,6 +7,7 @@ using MessagePack;
namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus
{
[MessagePackObject]
public class ChangeTeamRequest : MatchUserRequest
{
[Key(0)]

View File

@ -3,6 +3,7 @@
using System;
using MessagePack;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
namespace osu.Game.Online.Multiplayer
@ -12,7 +13,10 @@ namespace osu.Game.Online.Multiplayer
/// </summary>
[Serializable]
[MessagePackObject]
[Union(0, typeof(ChangeTeamRequest))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
// IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
[Union(0, typeof(ChangeTeamRequest))]
[Union(1, typeof(StartMatchCountdownRequest))]
[Union(2, typeof(StopCountdownRequest))]
public abstract class MatchUserRequest
{
}

View File

@ -16,6 +16,7 @@ using osu.Framework.Logging;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Rulesets;
@ -170,6 +171,8 @@ namespace osu.Game.Online.Multiplayer
Room = joinedRoom;
APIRoom = room;
Debug.Assert(joinedRoom.Playlist.Count > 0);
APIRoom.Playlist.Clear();
APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem));
@ -238,7 +241,9 @@ namespace osu.Game.Online.Multiplayer
/// <param name="password">The new password, if any.</param>
/// <param name="matchType">The type of the match, if any.</param>
/// <param name="queueMode">The new queue mode, if any.</param>
public Task ChangeSettings(Optional<string> name = default, Optional<string> password = default, Optional<MatchType> matchType = default, Optional<QueueMode> queueMode = default)
/// <param name="autoStartDuration">The new auto-start countdown duration, 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)
{
if (Room == null)
throw new InvalidOperationException("Must be joined to a match to change settings.");
@ -249,6 +254,7 @@ namespace osu.Game.Online.Multiplayer
Password = password.GetOr(Room.Settings.Password),
MatchType = matchType.GetOr(Room.Settings.MatchType),
QueueMode = queueMode.GetOr(Room.Settings.QueueMode),
AutoStartDuration = autoStartDuration.GetOr(Room.Settings.AutoStartDuration),
});
}
@ -534,7 +540,24 @@ namespace osu.Game.Online.Multiplayer
public Task MatchEvent(MatchServerEvent e)
{
// not used by any match types just yet.
if (Room == null)
return Task.CompletedTask;
Scheduler.Add(() =>
{
if (Room == null)
return;
switch (e)
{
case CountdownChangedEvent countdownChangedEvent:
Room.Countdown = countdownChangedEvent.Countdown;
break;
}
RoomUpdated?.Invoke();
}, false);
return Task.CompletedTask;
}
@ -665,6 +688,8 @@ namespace osu.Game.Online.Multiplayer
Room.Playlist.Remove(Room.Playlist.Single(existing => existing.ID == playlistItemId));
APIRoom.Playlist.RemoveAll(existing => existing.ID == playlistItemId);
Debug.Assert(Room.Playlist.Count > 0);
ItemRemoved?.Invoke(playlistItemId);
RoomUpdated?.Invoke();
});
@ -723,6 +748,7 @@ namespace osu.Game.Online.Multiplayer
APIRoom.Password.Value = Room.Settings.Password;
APIRoom.Type.Value = Room.Settings.MatchType;
APIRoom.QueueMode.Value = Room.Settings.QueueMode;
APIRoom.AutoStartDuration.Value = Room.Settings.AutoStartDuration;
RoomUpdated?.Invoke();
}

View File

@ -0,0 +1,28 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using MessagePack;
using osu.Game.Online.Multiplayer.Countdown;
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// Describes the current countdown in a <see cref="MultiplayerRoom"/>.
/// </summary>
[MessagePackObject]
[Union(0, typeof(MatchStartCountdown))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
public abstract class MultiplayerCountdown
{
/// <summary>
/// The amount of time remaining in the countdown.
/// </summary>
/// <remarks>
/// This is only sent once from the server upon initial retrieval of the <see cref="MultiplayerRoom"/> or via a <see cref="CountdownChangedEvent"/>.
/// </remarks>
[Key(0)]
public TimeSpan TimeRemaining { get; set; }
}
}

View File

@ -54,6 +54,12 @@ namespace osu.Game.Online.Multiplayer
[Key(6)]
public IList<MultiplayerPlaylistItem> Playlist { get; set; } = new List<MultiplayerPlaylistItem>();
/// <summary>
/// The currently-running countdown.
/// </summary>
[Key(7)]
public MultiplayerCountdown? Countdown { get; set; }
[JsonConstructor]
[SerializationConstructor]
public MultiplayerRoom(long roomId)

View File

@ -28,6 +28,12 @@ namespace osu.Game.Online.Multiplayer
[Key(4)]
public QueueMode QueueMode { get; set; } = QueueMode.HostOnly;
[Key(5)]
public TimeSpan AutoStartDuration { get; set; }
[IgnoreMember]
public bool AutoStartEnabled => AutoStartDuration != TimeSpan.Zero;
public bool Equals(MultiplayerRoomSettings? other)
{
if (ReferenceEquals(this, other)) return true;
@ -37,13 +43,15 @@ namespace osu.Game.Online.Multiplayer
&& Name.Equals(other.Name, StringComparison.Ordinal)
&& PlaylistItemId == other.PlaylistItemId
&& MatchType == other.MatchType
&& QueueMode == other.QueueMode;
&& QueueMode == other.QueueMode
&& AutoStartDuration == other.AutoStartDuration;
}
public override string ToString() => $"Name:{Name}"
+ $" Password:{(string.IsNullOrEmpty(Password) ? "no" : "yes")}"
+ $" Type:{MatchType}"
+ $" Item:{PlaylistItemId}"
+ $" Queue:{QueueMode}";
+ $" Queue:{QueueMode}"
+ $" Start:{AutoStartDuration}";
}
}

View File

@ -11,6 +11,7 @@ using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Utils;
namespace osu.Game.Online.Rooms
{
@ -84,6 +85,19 @@ namespace osu.Game.Online.Rooms
Beatmap = beatmap;
}
public PlaylistItem(MultiplayerPlaylistItem item)
: this(new APIBeatmap { OnlineID = item.BeatmapID })
{
ID = item.ID;
OwnerID = item.OwnerID;
RulesetID = item.RulesetID;
Expired = item.Expired;
PlaylistOrder = item.PlaylistOrder;
PlayedAt = item.PlayedAt;
RequiredMods = item.RequiredMods.ToArray();
AllowedMods = item.AllowedMods.ToArray();
}
public void MarkInvalid() => valid.Value = false;
#region Newtonsoft.Json implicit ShouldSerialize() methods
@ -101,13 +115,13 @@ namespace osu.Game.Online.Rooms
#endregion
public PlaylistItem With(IBeatmapInfo beatmap) => new PlaylistItem(beatmap)
public PlaylistItem With(Optional<IBeatmapInfo> beatmap = default, Optional<ushort?> playlistOrder = default) => new PlaylistItem(beatmap.GetOr(Beatmap))
{
ID = ID,
OwnerID = OwnerID,
RulesetID = RulesetID,
Expired = Expired,
PlaylistOrder = PlaylistOrder,
PlaylistOrder = playlistOrder.GetOr(PlaylistOrder),
PlayedAt = PlayedAt,
AllowedMods = AllowedMods,
RequiredMods = RequiredMods,
@ -119,6 +133,7 @@ namespace osu.Game.Online.Rooms
&& Beatmap.OnlineID == other.Beatmap.OnlineID
&& RulesetID == other.RulesetID
&& Expired == other.Expired
&& PlaylistOrder == other.PlaylistOrder
&& AllowedMods.SequenceEqual(other.AllowedMods)
&& RequiredMods.SequenceEqual(other.RequiredMods);
}

View File

@ -92,6 +92,16 @@ namespace osu.Game.Online.Rooms
set => QueueMode.Value = value;
}
[Cached]
public readonly Bindable<TimeSpan> AutoStartDuration = new Bindable<TimeSpan>();
[JsonProperty("auto_start_duration")]
private ushort autoStartDuration
{
get => (ushort)AutoStartDuration.Value.TotalSeconds;
set => AutoStartDuration.Value = TimeSpan.FromSeconds(value);
}
[Cached]
public readonly Bindable<int?> MaxParticipants = new Bindable<int?>();
@ -172,6 +182,7 @@ namespace osu.Game.Online.Rooms
EndDate.Value = other.EndDate.Value;
UserScore.Value = other.UserScore.Value;
QueueMode.Value = other.QueueMode.Value;
AutoStartDuration.Value = other.AutoStartDuration.Value;
DifficultyRange.Value = other.DifficultyRange.Value;
PlaylistItemStats.Value = other.PlaylistItemStats.Value;
CurrentPlaylistItem.Value = other.CurrentPlaylistItem.Value;

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
namespace osu.Game.Online
@ -18,8 +19,12 @@ namespace osu.Game.Online
internal static readonly IReadOnlyList<(Type derivedType, Type baseType)> BASE_TYPE_MAPPING = new[]
{
(typeof(ChangeTeamRequest), typeof(MatchUserRequest)),
(typeof(StartMatchCountdownRequest), typeof(MatchUserRequest)),
(typeof(StopCountdownRequest), typeof(MatchUserRequest)),
(typeof(CountdownChangedEvent), typeof(MatchServerEvent)),
(typeof(TeamVersusRoomState), typeof(MatchRoomState)),
(typeof(TeamVersusUserState), typeof(MatchUserState)),
(typeof(MatchStartCountdown), typeof(MultiplayerCountdown))
};
}
}

View File

@ -1061,6 +1061,12 @@ namespace osu.Game
return true;
case GlobalAction.RandomSkin:
// Don't allow random skin selection while in the skin editor.
// This is mainly to stop many "osu! default (modified)" skins being created via the SkinManager.EnsureMutableSkin() path.
// If people want this to work we can potentially avoid selecting default skins when the editor is open, or allow a maximum of one mutable skin somehow.
if (skinEditor.State.Value == Visibility.Visible)
return false;
SkinManager.SelectRandomSkin();
return true;
}

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