1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 17:32:54 +08:00

Merge branch 'master' into extract-random-mod-logic-2

This commit is contained in:
Dan Balasescu 2022-03-31 13:42:47 +09:00 committed by GitHub
commit ff4745be59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
95 changed files with 1786 additions and 663 deletions

View File

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

View File

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

View File

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

View File

@ -1,24 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // 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 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 namespace osu.Game.Rulesets.EmptyScrolling.Mods
{ {
public class EmptyScrollingModAutoplay : ModAutoplay public class EmptyScrollingModAutoplay : ModAutoplay
{ {
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
{ => new ModReplayData(new EmptyScrollingAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "sample" });
ScoreInfo = new ScoreInfo
{
User = new APIUser { Username = "sample" },
},
Replay = new EmptyScrollingAutoGenerator(beatmap).Generate(),
};
} }
} }

View File

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

View File

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

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage) public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage)
// Bypass LegacySkinResourceStore to avoid returning null for retrieving files due to bad skin info (SkinInfo.Files = null). // 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 System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Catch.Mods namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModAutoplay : ModAutoplay public class CatchModAutoplay : ModAutoplay
{ {
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
{ => new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "osu!salad" });
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!salad" } },
Replay = new CatchAutoGenerator(beatmap).Generate(),
};
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -175,7 +175,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
private class TestLegacySkin : LegacySkin private class TestLegacySkin : LegacySkin
{ {
public TestLegacySkin(IResourceStore<byte[]> storage, string fileName) 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

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

View File

@ -148,7 +148,7 @@ namespace osu.Game.Tests.Gameplay
private class TestSkin : LegacySkin private class TestSkin : LegacySkin
{ {
public TestSkin(string resourceName, IStorageResourceProvider resources) 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); Assert.That(osuStorage, Is.Not.Null);
// In the following tests, realm files are ignored as // 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. // lead to unexpected behaviour.
// - in the case of checking the destination, the files may have already been recreated by the game // - in the case of checking the destination, the files may have already been recreated by the game
// as part of the standard migration flow. // as part of the standard migration flow.
foreach (string file in osuStorage.IgnoreFiles) 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(File.Exists(Path.Combine(originalDirectory, file)));
Assert.That(storage.Exists(file), Is.False, () => $"{file} exists in destination when it was expected to be ignored"); 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) 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(Directory.Exists(Path.Combine(originalDirectory, dir)));
Assert.That(storage.Exists(dir), Is.False, () => $"{dir} exists in destination when it was expected to be ignored"); 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); var osu = LoadOsuIntoHost(host);
const string database_filename = "client.realm";
Assert.DoesNotThrow(() => osu.Migrate(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)));
Assert.DoesNotThrow(() => osu.Migrate(customPath2)); 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. // some files may have been left behind for whatever reason, but that's not what we're testing here.
cleanupPath(customPath); cleanupPath(customPath);
Assert.DoesNotThrow(() => osu.Migrate(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 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] [Test]
public void TestMigrationToNestedTargetFails() public void TestMigrationToNestedTargetFails()
{ {

View File

@ -1,12 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Audio;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Skinning; using osu.Game.Skinning;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace osu.Game.Tests.NonVisual.Skinning namespace osu.Game.Tests.NonVisual.Skinning
{ {
@ -71,7 +80,7 @@ namespace osu.Game.Tests.NonVisual.Skinning
var texture = legacySkin.GetTexture(requestedComponent); var texture = legacySkin.GetTexture(requestedComponent);
Assert.IsNotNull(texture); Assert.IsNotNull(texture);
Assert.AreEqual(textureStore.Textures[expectedTexture], texture); Assert.AreEqual(textureStore.Textures[expectedTexture].Width, texture.Width);
Assert.AreEqual(expectedScale, texture.ScaleAdjust); Assert.AreEqual(expectedScale, texture.ScaleAdjust);
} }
@ -88,23 +97,50 @@ namespace osu.Game.Tests.NonVisual.Skinning
private class TestLegacySkin : LegacySkin private class TestLegacySkin : LegacySkin
{ {
public TestLegacySkin(TextureStore textureStore) public TestLegacySkin(IResourceStore<TextureUpload> textureStore)
: base(new SkinInfo(), null, null, string.Empty) : 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) 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 class BeatmapSkinSource : LegacyBeatmapSkin
{ {
public BeatmapSkinSource() 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 class BeatmapSkinSource : LegacyBeatmapSkin
{ {
public BeatmapSkinSource() 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 System.Linq;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.Break; using osu.Game.Screens.Play.Break;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Users.Drawables;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
@ -39,11 +41,18 @@ namespace osu.Game.Tests.Visual.Gameplay
seekToBreak(1); seekToBreak(1);
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); 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 combo", () => getResultsScreen().Score.Combo > 100);
AddAssert("score has no misses", () => getResultsScreen().Score.Statistics[HitResult.Miss] == 0); 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; ResultsScreen getResultsScreen() => Stack.CurrentScreen as ResultsScreen;
} }

View File

@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestEmptyLegacyBeatmapSkinFallsBack() 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)); AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded));
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value)); 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>()); 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() protected override void AddCheckSteps()

View File

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

View File

@ -0,0 +1,80 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing;
using osu.Game.Overlays.Toolbar;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Menus
{
[TestFixture]
public class TestSceneToolbarClock : OsuManualInputManagerTestScene
{
private readonly Container mainContainer;
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,
},
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));
}
[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 });
}
}
}

View File

@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(200, 50), Size = new Vector2(250, 50),
} }
}; };
}); });
@ -85,7 +85,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddAssert("countdown button not visible", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown()); AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad); AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad);
} }
@ -103,7 +102,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
ClickButtonWhenEnabled<MultiplayerReadyButton>(); 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()); AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
@ -128,63 +133,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
[Test] [Test]
public void TestCountdownButtonEnablementAndVisibilityWhileSpectating() public void TestCountdownWhileSpectating()
{ {
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == 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 is visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
AddAssert("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value); AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" })); AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }));
AddAssert("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value); AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready)); AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready));
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value); AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
} }
[Test]
public void TestSpectatingDuringCountdownWithNoReadyUsersCancelsCountdown()
{
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("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
AddUntilStep("match not started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.Open);
}
[Test]
public void TestReadyButtonEnabledWhileSpectatingDuringCountdown()
{
AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }));
AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready));
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("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
AddAssert("ready button enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value);
}
[Test] [Test]
public void TestBecomeHostDuringCountdownAndReady() public void TestBecomeHostDuringCountdownAndReady()
{ {
@ -205,6 +168,31 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null); 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] [Test]
public void TestDeletedBeatmapDisableReady() public void TestDeletedBeatmapDisableReady()
{ {

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); 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] [Test]
public void TestKickButtonOnlyPresentWhenHost() public void TestKickButtonOnlyPresentWhenHost()
{ {
@ -202,9 +221,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestManyUsers() public void TestManyUsers()
{ {
const int users_count = 20;
AddStep("add many users", () => AddStep("add many users", () =>
{ {
for (int i = 0; i < 20; i++) for (int i = 0; i < users_count; i++)
{ {
MultiplayerClient.AddUser(new APIUser 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] [Test]

View File

@ -3,7 +3,9 @@
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
@ -28,6 +30,23 @@ namespace osu.Game.Tests.Visual.Navigation
stream.CopyTo(outStream); 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] [Test]
public void TestMigration() public void TestMigration()
{ {

View File

@ -116,7 +116,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private void testBeatmapLabels(Ruleset ruleset) private void testBeatmapLabels(Ruleset ruleset)
{ {
AddAssert("check version", () => infoWedge.Info.VersionLabel.Current.Value == $"{ruleset.ShortName}Version"); 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 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")); 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", () => AddStep("reset defaults", () =>
{ {
Ruleset.Value = new OsuRuleset().RulesetInfo; Ruleset.Value = new OsuRuleset().RulesetInfo;
Beatmap.SetDefault(); Beatmap.SetDefault();
SelectedMods.SetDefault();
songSelect = null; songSelect = null;
}); });
@ -563,7 +565,7 @@ namespace osu.Game.Tests.Visual.SongSelect
} }
[Test] [Test]
public void TestAutoplayViaCtrlEnter() public void TestAutoplayShortcut()
{ {
addRulesetImportStep(0); addRulesetImportStep(0);
@ -580,11 +582,65 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader); 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()); 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] [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

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

View File

@ -44,6 +44,8 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f); SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f);
SetDefault(OsuSetting.ToolbarClockDisplayMode, ToolbarClockDisplayMode.Full);
// Online settings // Online settings
SetDefault(OsuSetting.Username, string.Empty); SetDefault(OsuSetting.Username, string.Empty);
SetDefault(OsuSetting.Token, string.Empty); SetDefault(OsuSetting.Token, string.Empty);
@ -295,6 +297,7 @@ namespace osu.Game.Configuration
RandomSelectAlgorithm, RandomSelectAlgorithm,
ShowFpsDisplay, ShowFpsDisplay,
ChatDisplayHeight, ChatDisplayHeight,
ToolbarClockDisplayMode,
Version, Version,
ShowConvertedBeatmaps, ShowConvertedBeatmaps,
Skin, Skin,

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. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Development; using osu.Framework.Development;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -14,6 +18,7 @@ using osu.Framework.Platform;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Models; using osu.Game.Models;
@ -29,8 +34,6 @@ using SharpCompress.Archives.Zip;
using SharpCompress.Common; using SharpCompress.Common;
using SharpCompress.Writers.Zip; using SharpCompress.Writers.Zip;
#nullable enable
namespace osu.Game.Database namespace osu.Game.Database
{ {
internal class EFToRealmMigrator : CompositeDrawable internal class EFToRealmMigrator : CompositeDrawable
@ -57,7 +60,7 @@ namespace osu.Game.Database
[Resolved] [Resolved]
private Storage storage { get; set; } = null!; private Storage storage { get; set; } = null!;
private readonly OsuSpriteText currentOperationText; private readonly OsuTextFlowContainer currentOperationText;
public EFToRealmMigrator() public EFToRealmMigrator()
{ {
@ -99,11 +102,13 @@ namespace osu.Game.Database
{ {
State = { Value = Visibility.Visible } State = { Value = Visibility.Visible }
}, },
currentOperationText = new OsuSpriteText currentOperationText = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 30))
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = 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!"); log("Migration successful!");
if (DebugUtils.IsDebugBuild) 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 else
{ {
log("Migration failed!"); log("Migration failed!");
Logger.Log(t.Exception.ToString(), LoggingTarget.Database); 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 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 = () => 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 attachment_filename = "attach_me.zip";
const string backup_folder = "backups"; const string backup_folder = "backups";

View File

@ -13,6 +13,7 @@ using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Development; using osu.Framework.Development;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
@ -211,7 +212,7 @@ namespace osu.Game.Database
if (realm.All<ScoreInfo>().Any()) if (realm.All<ScoreInfo>().Any())
{ {
Logger.Log(@"Recovery aborted as the existing database has scores set already.", LoggingTarget.Database); 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; return;
} }
} }
@ -293,7 +294,18 @@ namespace osu.Game.Database
/// Compact this realm. /// Compact this realm.
/// </summary> /// </summary>
/// <returns></returns> /// <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> /// <summary>
/// Run work on realm with a return value. /// Run work on realm with a return value.
@ -542,6 +554,11 @@ namespace osu.Game.Database
return Realm.GetInstance(getConfiguration()); 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 finally
{ {
if (tookSemaphoreLock) if (tookSemaphoreLock)

View File

@ -36,15 +36,15 @@ namespace osu.Game.IO
public override string[] IgnoreDirectories => new[] public override string[] IgnoreDirectories => new[]
{ {
"cache", "cache",
"client.realm.management" $"{OsuGameBase.CLIENT_DATABASE_FILENAME}.management",
}; };
public override string[] IgnoreFiles => new[] public override string[] IgnoreFiles => new[]
{ {
"framework.ini", "framework.ini",
"storage.ini", "storage.ini",
"client.realm.note", $"{OsuGameBase.CLIENT_DATABASE_FILENAME}.note",
"client.realm.lock", $"{OsuGameBase.CLIENT_DATABASE_FILENAME}.lock",
}; };
public OsuStorage(GameHost host, Storage defaultStorage) public OsuStorage(GameHost host, Storage defaultStorage)
@ -64,12 +64,22 @@ namespace osu.Game.IO
/// </summary> /// </summary>
public void ResetCustomStoragePath() public void ResetCustomStoragePath()
{ {
storageConfig.SetValue(StorageConfig.FullPath, string.Empty); ChangeDataPath(string.Empty);
storageConfig.Save();
ChangeTargetStorage(defaultStorage); 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> /// <summary>
/// Attempts to change to the user's custom storage path. /// Attempts to change to the user's custom storage path.
/// </summary> /// </summary>
@ -117,8 +127,7 @@ namespace osu.Game.IO
{ {
bool cleanupSucceeded = base.Migrate(newStorage); bool cleanupSucceeded = base.Migrate(newStorage);
storageConfig.SetValue(StorageConfig.FullPath, newStorage.GetFullPath(".")); ChangeDataPath(newStorage.GetFullPath("."));
storageConfig.Save();
return cleanupSucceeded; return cleanupSucceeded;
} }

View File

@ -15,6 +15,11 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonObject(MemberSerialization.OptIn)] [JsonObject(MemberSerialization.OptIn)]
public class APIUser : IEquatable<APIUser>, IUser 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")] [JsonProperty(@"id")]
public int Id { get; set; } = 1; public int Id { get; set; } = 1;
@ -238,7 +243,7 @@ namespace osu.Game.Online.API.Requests.Responses
/// </summary> /// </summary>
public static readonly APIUser SYSTEM_USER = new APIUser public static readonly APIUser SYSTEM_USER = new APIUser
{ {
Id = 0, Id = SYSTEM_USER_ID,
Username = "system", Username = "system",
Colour = @"9c0101", Colour = @"9c0101",
}; };

View File

@ -241,7 +241,9 @@ namespace osu.Game.Online.Multiplayer
/// <param name="password">The new password, if any.</param> /// <param name="password">The new password, if any.</param>
/// <param name="matchType">The type of the match, if any.</param> /// <param name="matchType">The type of the match, if any.</param>
/// <param name="queueMode">The new queue mode, 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) if (Room == null)
throw new InvalidOperationException("Must be joined to a match to change settings."); throw new InvalidOperationException("Must be joined to a match to change settings.");
@ -252,6 +254,7 @@ namespace osu.Game.Online.Multiplayer
Password = password.GetOr(Room.Settings.Password), Password = password.GetOr(Room.Settings.Password),
MatchType = matchType.GetOr(Room.Settings.MatchType), MatchType = matchType.GetOr(Room.Settings.MatchType),
QueueMode = queueMode.GetOr(Room.Settings.QueueMode), QueueMode = queueMode.GetOr(Room.Settings.QueueMode),
AutoStartDuration = autoStartDuration.GetOr(Room.Settings.AutoStartDuration),
}); });
} }
@ -745,6 +748,7 @@ namespace osu.Game.Online.Multiplayer
APIRoom.Password.Value = Room.Settings.Password; APIRoom.Password.Value = Room.Settings.Password;
APIRoom.Type.Value = Room.Settings.MatchType; APIRoom.Type.Value = Room.Settings.MatchType;
APIRoom.QueueMode.Value = Room.Settings.QueueMode; APIRoom.QueueMode.Value = Room.Settings.QueueMode;
APIRoom.AutoStartDuration.Value = Room.Settings.AutoStartDuration;
RoomUpdated?.Invoke(); RoomUpdated?.Invoke();
} }

View File

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

View File

@ -92,6 +92,16 @@ namespace osu.Game.Online.Rooms
set => QueueMode.Value = value; 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] [Cached]
public readonly Bindable<int?> MaxParticipants = new Bindable<int?>(); public readonly Bindable<int?> MaxParticipants = new Bindable<int?>();
@ -172,6 +182,7 @@ namespace osu.Game.Online.Rooms
EndDate.Value = other.EndDate.Value; EndDate.Value = other.EndDate.Value;
UserScore.Value = other.UserScore.Value; UserScore.Value = other.UserScore.Value;
QueueMode.Value = other.QueueMode.Value; QueueMode.Value = other.QueueMode.Value;
AutoStartDuration.Value = other.AutoStartDuration.Value;
DifficultyRange.Value = other.DifficultyRange.Value; DifficultyRange.Value = other.DifficultyRange.Value;
PlaylistItemStats.Value = other.PlaylistItemStats.Value; PlaylistItemStats.Value = other.PlaylistItemStats.Value;
CurrentPlaylistItem.Value = other.CurrentPlaylistItem.Value; CurrentPlaylistItem.Value = other.CurrentPlaylistItem.Value;

View File

@ -57,6 +57,11 @@ namespace osu.Game
public const string CLIENT_STREAM_NAME = @"lazer"; public const string CLIENT_STREAM_NAME = @"lazer";
/// <summary>
/// The filename of the main client database.
/// </summary>
public const string CLIENT_DATABASE_FILENAME = @"client.realm";
public const int SAMPLE_CONCURRENCY = 6; public const int SAMPLE_CONCURRENCY = 6;
/// <summary> /// <summary>
@ -200,7 +205,7 @@ namespace osu.Game
if (Storage.Exists(DatabaseContextFactory.DATABASE_NAME)) if (Storage.Exists(DatabaseContextFactory.DATABASE_NAME))
dependencies.Cache(EFContextFactory = new DatabaseContextFactory(Storage)); dependencies.Cache(EFContextFactory = new DatabaseContextFactory(Storage));
dependencies.Cache(realm = new RealmAccess(Storage, "client", Host.UpdateThread, EFContextFactory)); dependencies.Cache(realm = new RealmAccess(Storage, CLIENT_DATABASE_FILENAME, Host.UpdateThread, EFContextFactory));
dependencies.CacheAs<RulesetStore>(RulesetStore = new RealmRulesetStore(realm, Storage)); dependencies.CacheAs<RulesetStore>(RulesetStore = new RealmRulesetStore(realm, Storage));
dependencies.CacheAs<IRulesetStore>(RulesetStore); dependencies.CacheAs<IRulesetStore>(RulesetStore);

View File

@ -0,0 +1,176 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Overlays.Mods
{
public class ModSettingsArea : CompositeDrawable
{
public Bindable<IReadOnlyList<Mod>> SelectedMods { get; } = new Bindable<IReadOnlyList<Mod>>();
private readonly Box background;
private readonly FillFlowContainer modSettingsFlow;
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
public ModSettingsArea()
{
RelativeSizeAxes = Axes.X;
Height = 250;
Anchor = Anchor.BottomRight;
Origin = Anchor.BottomRight;
InternalChild = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 2,
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
new OsuScrollContainer(Direction.Horizontal)
{
RelativeSizeAxes = Axes.Both,
ScrollbarOverlapsContent = false,
Child = modSettingsFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Padding = new MarginPadding { Vertical = 7, Horizontal = 70 },
Spacing = new Vector2(7),
Direction = FillDirection.Horizontal
}
}
}
};
}
[BackgroundDependencyLoader]
private void load()
{
background.Colour = colourProvider.Dark3;
}
protected override void LoadComplete()
{
base.LoadComplete();
SelectedMods.BindValueChanged(_ => updateMods());
}
private void updateMods()
{
modSettingsFlow.Clear();
foreach (var mod in SelectedMods.Value.OrderBy(mod => mod.Type).ThenBy(mod => mod.Acronym))
{
var settings = mod.CreateSettingsControls().ToList();
if (settings.Count > 0)
{
if (modSettingsFlow.Any())
{
modSettingsFlow.Add(new Box
{
RelativeSizeAxes = Axes.Y,
Width = 2,
Colour = colourProvider.Dark4,
});
}
modSettingsFlow.Add(new ModSettingsColumn(mod, settings));
}
}
}
protected override bool OnMouseDown(MouseDownEvent e) => true;
protected override bool OnHover(HoverEvent e) => true;
private class ModSettingsColumn : CompositeDrawable
{
public ModSettingsColumn(Mod mod, IEnumerable<Drawable> settingsControls)
{
Width = 250;
RelativeSizeAxes = Axes.Y;
Padding = new MarginPadding { Bottom = 7 };
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 10),
new Dimension()
},
Content = new[]
{
new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(7),
Children = new Drawable[]
{
new ModSwitchTiny(mod)
{
Active = { Value = true },
Scale = new Vector2(0.6f),
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft
},
new OsuSpriteText
{
Text = mod.Name,
Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold),
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Margin = new MarginPadding { Bottom = 2 }
}
}
}
},
new[] { Empty() },
new Drawable[]
{
new OsuScrollContainer(Direction.Vertical)
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Right = 7 },
ChildrenEnumerable = settingsControls,
Spacing = new Vector2(0, 7)
}
}
}
}
};
}
}
}
}

View File

@ -3,11 +3,14 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.IO;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Overlays.Settings.Sections.Maintenance namespace osu.Game.Overlays.Settings.Sections.Maintenance
{ {
@ -16,6 +19,12 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
[Resolved] [Resolved]
private Storage storage { get; set; } private Storage storage { get; set; }
[Resolved]
private OsuGameBase game { get; set; }
[Resolved(canBeNull: true)]
private DialogOverlay dialogOverlay { get; set; }
protected override DirectoryInfo InitialPath => new DirectoryInfo(storage.GetFullPath(string.Empty)).Parent; protected override DirectoryInfo InitialPath => new DirectoryInfo(storage.GetFullPath(string.Empty)).Parent;
public override bool AllowExternalScreenChange => false; public override bool AllowExternalScreenChange => false;
@ -32,9 +41,30 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
try try
{ {
if (target.GetDirectories().Length > 0 || target.GetFiles().Length > 0) var directoryInfos = target.GetDirectories();
var fileInfos = target.GetFiles();
if (directoryInfos.Length > 0 || fileInfos.Length > 0)
{
// Quick test for whether there's already an osu! install at the target path.
if (fileInfos.Any(f => f.Name == OsuGameBase.CLIENT_DATABASE_FILENAME))
{
dialogOverlay.Push(new ConfirmDialog("The target directory already seems to have an osu! install. Use that data instead?", () =>
{
dialogOverlay.Push(new ConfirmDialog("To complete this operation, osu! will close. Please open it again to use the new data location.", () =>
{
(storage as OsuStorage)?.ChangeDataPath(target.FullName);
game.GracefullyExit();
}, () => { }));
},
() => { }));
return;
}
target = target.CreateSubdirectory("osu-lazer"); target = target.CreateSubdirectory("osu-lazer");
} }
}
catch (Exception e) catch (Exception e)
{ {
Logger.Log($"Error during migration: {e.Message}", level: LogLevel.Error); Logger.Log($"Error during migration: {e.Message}", level: LogLevel.Error);

View File

@ -0,0 +1,159 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Toolbar
{
public class AnalogClockDisplay : ClockDisplay
{
private const float hand_thickness = 2.4f;
private Drawable hour;
private Drawable minute;
private Drawable second;
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(22);
InternalChildren = new[]
{
new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 2,
BorderColour = Color4.White,
Child = new Box
{
AlwaysPresent = true,
Alpha = 0,
RelativeSizeAxes = Axes.Both
},
},
hour = new LargeHand(0.34f),
minute = new LargeHand(0.48f),
second = new SecondHand(),
new CentreCircle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
}
protected override void UpdateDisplay(DateTimeOffset now)
{
float secondFractional = now.Second / 60f;
float minuteFractional = (now.Minute + secondFractional) / 60f;
float hourFractional = ((minuteFractional + now.Hour) % 12) / 12f;
updateRotation(hour, hourFractional);
updateRotation(minute, minuteFractional);
updateRotation(second, secondFractional);
}
private void updateRotation(Drawable hand, float fraction)
{
const float duration = 320;
float rotation = fraction * 360 - 90;
if (Math.Abs(hand.Rotation - rotation) > 180)
hand.RotateTo(rotation);
else
hand.RotateTo(rotation, duration, Easing.OutElastic);
}
private class CentreCircle : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
InternalChildren = new Drawable[]
{
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(hand_thickness),
Colour = Color4.White,
},
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(hand_thickness * 0.7f),
Colour = colours.PinkLight,
},
};
}
}
private class SecondHand : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.X;
Width = 0.66f;
Height = hand_thickness * 0.7f;
Anchor = Anchor.Centre;
Origin = Anchor.Custom;
OriginPosition = new Vector2(Height * 2, Height / 2);
InternalChildren = new Drawable[]
{
new Circle
{
Colour = colours.PinkLight,
RelativeSizeAxes = Axes.Both,
},
};
}
}
private class LargeHand : CompositeDrawable
{
public LargeHand(float length)
{
Width = length;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Anchor = Anchor.Centre;
Origin = Anchor.Custom;
OriginPosition = new Vector2(hand_thickness / 2); // offset x also, to ensure the centre of the line is centered on the face.
Height = hand_thickness;
InternalChildren = new Drawable[]
{
new Circle
{
Colour = Color4.White,
RelativeSizeAxes = Axes.Both,
BorderThickness = 0.7f,
BorderColour = colours.Gray2,
},
};
RelativeSizeAxes = Axes.X;
}
}
}
}

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.
using System;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Overlays.Toolbar
{
public abstract class ClockDisplay : CompositeDrawable
{
private int? lastSecond;
protected override void Update()
{
base.Update();
var now = DateTimeOffset.Now;
if (now.Second != lastSecond)
{
lastSecond = now.Second;
UpdateDisplay(now);
}
}
protected abstract void UpdateDisplay(DateTimeOffset now);
}
}

View File

@ -0,0 +1,64 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Overlays.Toolbar
{
public class DigitalClockDisplay : ClockDisplay
{
private OsuSpriteText realTime;
private OsuSpriteText gameTime;
private bool showRuntime = true;
public bool ShowRuntime
{
get => showRuntime;
set
{
if (showRuntime == value)
return;
showRuntime = value;
updateMetrics();
}
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
realTime = new OsuSpriteText(),
gameTime = new OsuSpriteText
{
Y = 14,
Colour = colours.PinkLight,
Scale = new Vector2(0.6f)
}
};
updateMetrics();
}
protected override void UpdateDisplay(DateTimeOffset now)
{
realTime.Text = $"{now:HH:mm:ss}";
gameTime.Text = $"running {new TimeSpan(TimeSpan.TicksPerSecond * (int)(Clock.CurrentTime / 1000)):c}";
}
private void updateMetrics()
{
Width = showRuntime ? 66 : 45; // Allows for space for game time up to 99 days (in the padding area since this is quite rare).
gameTime.FadeTo(showRuntime ? 1 : 0);
}
}
}

View File

@ -104,6 +104,7 @@ namespace osu.Game.Overlays.Toolbar
// Icon = FontAwesome.Solid.search // Icon = FontAwesome.Solid.search
//}, //},
userButton = new ToolbarUserButton(), userButton = new ToolbarUserButton(),
new ToolbarClock(),
new ToolbarNotificationButton(), new ToolbarNotificationButton(),
} }
} }

View File

@ -0,0 +1,101 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osuTK;
namespace osu.Game.Overlays.Toolbar
{
public class ToolbarClock : CompositeDrawable
{
private Bindable<ToolbarClockDisplayMode> clockDisplayMode;
private DigitalClockDisplay digital;
private AnalogClockDisplay analog;
public ToolbarClock()
{
RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X;
Padding = new MarginPadding(10);
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
clockDisplayMode = config.GetBindable<ToolbarClockDisplayMode>(OsuSetting.ToolbarClockDisplayMode);
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new Drawable[]
{
analog = new AnalogClockDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
digital = new DigitalClockDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
clockDisplayMode.BindValueChanged(displayMode =>
{
bool showAnalog = displayMode.NewValue == ToolbarClockDisplayMode.Analog || displayMode.NewValue == ToolbarClockDisplayMode.Full;
bool showDigital = displayMode.NewValue != ToolbarClockDisplayMode.Analog;
bool showRuntime = displayMode.NewValue == ToolbarClockDisplayMode.DigitalWithRuntime || displayMode.NewValue == ToolbarClockDisplayMode.Full;
digital.FadeTo(showDigital ? 1 : 0);
digital.ShowRuntime = showRuntime;
analog.FadeTo(showAnalog ? 1 : 0);
}, true);
}
protected override bool OnClick(ClickEvent e)
{
cycleDisplayMode();
return true;
}
private void cycleDisplayMode()
{
switch (clockDisplayMode.Value)
{
case ToolbarClockDisplayMode.Analog:
clockDisplayMode.Value = ToolbarClockDisplayMode.Full;
break;
case ToolbarClockDisplayMode.Digital:
clockDisplayMode.Value = ToolbarClockDisplayMode.Analog;
break;
case ToolbarClockDisplayMode.DigitalWithRuntime:
clockDisplayMode.Value = ToolbarClockDisplayMode.Digital;
break;
case ToolbarClockDisplayMode.Full:
clockDisplayMode.Value = ToolbarClockDisplayMode.DigitalWithRuntime;
break;
}
}
}
}

View File

@ -41,7 +41,7 @@ namespace osu.Game.Overlays
public void ShowUser(IUser user) public void ShowUser(IUser user)
{ {
if (user == APIUser.SYSTEM_USER) if (user.OnlineID == APIUser.SYSTEM_USER_ID)
return; return;
Show(); Show();

View File

@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Edit
private void regenerateAutoplay() private void regenerateAutoplay()
{ {
var autoplayMod = drawableRuleset.Mods.OfType<ModAutoplay>().Single(); var autoplayMod = drawableRuleset.Mods.OfType<ModAutoplay>().Single();
drawableRuleset.SetReplayScore(autoplayMod.CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods)); drawableRuleset.SetReplayScore(autoplayMod.CreateScoreFromReplayData(drawableRuleset.Beatmap, drawableRuleset.Mods));
} }
private void addHitObject(HitObject hitObject) private void addHitObject(HitObject hitObject)

View File

@ -1,14 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Scoring; using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public interface ICreateReplay [Obsolete("Use ICreateReplayData instead")] // Can be removed 20220929
public interface ICreateReplay : ICreateReplayData
{ {
public Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods); public Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods);
ModReplayData ICreateReplayData.CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
{
var replayScore = CreateReplayScore(beatmap, mods);
return new ModReplayData(replayScore.Replay, new ModCreatedUser { Username = replayScore.ScoreInfo.User.Username });
}
} }
} }

View File

@ -0,0 +1,63 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Replays;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Rulesets.Mods
{
/// <summary>
/// A mod which creates full replay data, which is to be played back in place of a local user playing the game.
/// </summary>
public interface ICreateReplayData
{
/// <summary>
/// Create replay data.
/// </summary>
/// <param name="beatmap">The beatmap to create replay data for.</param>
/// <param name="mods">The mods to take into account when creating the replay data.</param>
/// <returns>A <see cref="ModReplayData"/> structure, containing the generated replay data.</returns>
/// <remarks>
/// For callers that want to receive a directly usable <see cref="Score"/> instance,
/// the <see cref="ModExtensions.CreateScoreFromReplayData"/> extension method is provided for convenience.
/// </remarks>
ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods);
}
/// <summary>
/// Data created by a mod that implements <see cref="ICreateReplayData"/>.
/// </summary>
public class ModReplayData
{
/// <summary>
/// The full replay data.
/// </summary>
public readonly Replay Replay;
/// <summary>
/// Placeholder user data to show in place of the local user when the associated mod is active.
/// </summary>
public readonly ModCreatedUser User;
public ModReplayData(Replay replay, ModCreatedUser user = null)
{
Replay = replay;
User = user ?? new ModCreatedUser();
}
}
/// <summary>
/// A user which is associated with a replay that was created by a mod (ie. autoplay or cinema).
/// </summary>
public class ModCreatedUser : IUser
{
public int OnlineID => APIUser.SYSTEM_USER_ID;
public bool IsBot => true;
public string Username { get; set; } = string.Empty;
}
}

View File

@ -11,7 +11,7 @@ using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModAutoplay : Mod, IApplicableFailOverride, ICreateReplay public abstract class ModAutoplay : Mod, IApplicableFailOverride, ICreateReplayData
{ {
public override string Name => "Autoplay"; public override string Name => "Autoplay";
public override string Acronym => "AT"; public override string Acronym => "AT";
@ -26,10 +26,20 @@ namespace osu.Game.Rulesets.Mods
public override bool UserPlayable => false; public override bool UserPlayable => false;
public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) }; public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) };
public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;
[Obsolete("Override CreateReplayData(IBeatmap, IReadOnlyList<Mod>) instead")] // Can be removed 20220929
public virtual Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score { Replay = new Replay() }; public virtual Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score { Replay = new Replay() };
public virtual ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
{
#pragma warning disable CS0618
var replayScore = CreateReplayScore(beatmap, mods);
#pragma warning restore CS0618
return new ModReplayData(replayScore.Replay, new ModCreatedUser { Username = replayScore.ScoreInfo.User.Username });
}
} }
} }

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mods
public override string Description => "The whole playfield is on a wheel!"; public override string Description => "The whole playfield is on a wheel!";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override string SettingDescription => $"{SpinSpeed.Value} rpm {Direction.Value.GetDescription().ToLowerInvariant()}"; public override string SettingDescription => $"{SpinSpeed.Value:N2} rpm {Direction.Value.GetDescription().ToLowerInvariant()}";
public void Update(Playfield playfield) public void Update(Playfield playfield)
{ {

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -14,8 +16,6 @@ namespace osu.Game.Rulesets.Mods
{ {
public virtual void ApplyToDrawableRuleset(DrawableRuleset<T> drawableRuleset) public virtual void ApplyToDrawableRuleset(DrawableRuleset<T> drawableRuleset)
{ {
drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods));
// AlwaysPresent required for hitsounds // AlwaysPresent required for hitsounds
drawableRuleset.AlwaysPresent = true; drawableRuleset.AlwaysPresent = true;
drawableRuleset.Hide(); drawableRuleset.Hide();
@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => OsuIcon.ModCinema; public override IconUsage? Icon => OsuIcon.ModCinema;
public override string Description => "Watch the video without visual distractions."; public override string Description => "Watch the video without visual distractions.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAutoplay)).ToArray();
public void ApplyToHUD(HUDOverlay overlay) public void ApplyToHUD(HUDOverlay overlay)
{ {
overlay.ShowHud.Value = false; overlay.ShowHud.Value = false;

View File

@ -0,0 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mods
{
public static class ModExtensions
{
public static Score CreateScoreFromReplayData(this ICreateReplayData mod, IBeatmap beatmap, IReadOnlyList<Mod> mods)
{
var replayData = mod.CreateReplayData(beatmap, mods);
return new Score
{
Replay = replayData.Replay,
ScoreInfo =
{
User = new APIUser
{
Id = APIUser.SYSTEM_USER_ID,
Username = replayData.User.Username,
}
}
};
}
}
}

View File

@ -90,12 +90,7 @@ namespace osu.Game.Scoring
/// </remarks> /// </remarks>
/// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param> /// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param>
/// <returns>The bindable containing the total score.</returns> /// <returns>The bindable containing the total score.</returns>
public Bindable<long> GetBindableTotalScore([NotNull] ScoreInfo score) public Bindable<long> GetBindableTotalScore([NotNull] ScoreInfo score) => new TotalScoreBindable(score, this, configManager);
{
var bindable = new TotalScoreBindable(score, this);
configManager?.BindWith(OsuSetting.ScoreDisplayMode, bindable.ScoringMode);
return bindable;
}
/// <summary> /// <summary>
/// Retrieves a bindable that represents the formatted total score string of a <see cref="ScoreInfo"/>. /// Retrieves a bindable that represents the formatted total score string of a <see cref="ScoreInfo"/>.
@ -118,7 +113,11 @@ namespace osu.Game.Scoring
public void GetTotalScore([NotNull] ScoreInfo score, [NotNull] Action<long> callback, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default) public void GetTotalScore([NotNull] ScoreInfo score, [NotNull] Action<long> callback, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default)
{ {
GetTotalScoreAsync(score, mode, cancellationToken) GetTotalScoreAsync(score, mode, cancellationToken)
.ContinueWith(task => scheduler.Add(() => callback(task.GetResultSafely())), TaskContinuationOptions.OnlyOnRanToCompletion); .ContinueWith(task => scheduler.Add(() =>
{
if (!cancellationToken.IsCancellationRequested)
callback(task.GetResultSafely());
}), TaskContinuationOptions.OnlyOnRanToCompletion);
} }
/// <summary> /// <summary>
@ -183,8 +182,7 @@ namespace osu.Game.Scoring
/// </summary> /// </summary>
private class TotalScoreBindable : Bindable<long> private class TotalScoreBindable : Bindable<long>
{ {
public readonly Bindable<ScoringMode> ScoringMode = new Bindable<ScoringMode>(); private readonly Bindable<ScoringMode> scoringMode = new Bindable<ScoringMode>();
private readonly ScoreInfo score; private readonly ScoreInfo score;
private readonly ScoreManager scoreManager; private readonly ScoreManager scoreManager;
@ -195,12 +193,14 @@ namespace osu.Game.Scoring
/// </summary> /// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to provide the total score of.</param> /// <param name="score">The <see cref="ScoreInfo"/> to provide the total score of.</param>
/// <param name="scoreManager">The <see cref="ScoreManager"/>.</param> /// <param name="scoreManager">The <see cref="ScoreManager"/>.</param>
public TotalScoreBindable(ScoreInfo score, ScoreManager scoreManager) /// <param name="configManager">The config.</param>
public TotalScoreBindable(ScoreInfo score, ScoreManager scoreManager, OsuConfigManager configManager)
{ {
this.score = score; this.score = score;
this.scoreManager = scoreManager; this.scoreManager = scoreManager;
ScoringMode.BindValueChanged(onScoringModeChanged, true); configManager?.BindWith(OsuSetting.ScoreDisplayMode, scoringMode);
scoringMode.BindValueChanged(onScoringModeChanged, true);
} }
private void onScoringModeChanged(ValueChangedEvent<ScoringMode> mode) private void onScoringModeChanged(ValueChangedEvent<ScoringMode> mode)

View File

@ -79,10 +79,10 @@ namespace osu.Game.Screens.Menu
private readonly ButtonArea buttonArea; private readonly ButtonArea buttonArea;
private readonly Button backButton; private readonly MainMenuButton backButton;
private readonly List<Button> buttonsTopLevel = new List<Button>(); private readonly List<MainMenuButton> buttonsTopLevel = new List<MainMenuButton>();
private readonly List<Button> buttonsPlay = new List<Button>(); private readonly List<MainMenuButton> buttonsPlay = new List<MainMenuButton>();
private Sample sampleBack; private Sample sampleBack;
@ -100,8 +100,8 @@ namespace osu.Game.Screens.Menu
buttonArea.AddRange(new Drawable[] buttonArea.AddRange(new Drawable[]
{ {
new Button(ButtonSystemStrings.Settings, string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O), new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O),
backButton = new Button(ButtonSystemStrings.Back, @"button-back-select", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH) backButton = new MainMenuButton(ButtonSystemStrings.Back, @"button-back-select", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH)
{ {
VisibleState = ButtonSystemState.Play, VisibleState = ButtonSystemState.Play,
}, },
@ -126,24 +126,24 @@ namespace osu.Game.Screens.Menu
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(AudioManager audio, IdleTracker idleTracker, GameHost host) private void load(AudioManager audio, IdleTracker idleTracker, GameHost host)
{ {
buttonsPlay.Add(new Button(ButtonSystemStrings.Solo, @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P));
buttonsPlay.Add(new Button(ButtonSystemStrings.Multi, @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M)); buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M));
buttonsPlay.Add(new Button(ButtonSystemStrings.Playlists, @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L)); buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L));
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
buttonsTopLevel.Add(new Button(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P));
buttonsTopLevel.Add(new Button(ButtonSystemStrings.Edit, @"button-edit-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-edit-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E));
buttonsTopLevel.Add(new Button(ButtonSystemStrings.Browse, @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.D)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.D));
if (host.CanExit) if (host.CanExit)
buttonsTopLevel.Add(new Button(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q));
buttonArea.AddRange(buttonsPlay); buttonArea.AddRange(buttonsPlay);
buttonArea.AddRange(buttonsTopLevel); buttonArea.AddRange(buttonsTopLevel);
buttonArea.ForEach(b => buttonArea.ForEach(b =>
{ {
if (b is Button) if (b is MainMenuButton)
{ {
b.Origin = Anchor.CentreLeft; b.Origin = Anchor.CentreLeft;
b.Anchor = Anchor.CentreLeft; b.Anchor = Anchor.CentreLeft;
@ -305,7 +305,7 @@ namespace osu.Game.Screens.Menu
{ {
buttonArea.ButtonSystemState = state; buttonArea.ButtonSystemState = state;
foreach (var b in buttonArea.Children.OfType<Button>()) foreach (var b in buttonArea.Children.OfType<MainMenuButton>())
b.ButtonSystemState = state; b.ButtonSystemState = state;
} }

View File

@ -176,7 +176,7 @@ namespace osu.Game.Screens.Menu
private static readonly Color4 transparent_white = Color4.White.Opacity(0.2f); private static readonly Color4 transparent_white = Color4.White.Opacity(0.2f);
private float[] audioData; private readonly float[] audioData = new float[256];
private readonly QuadBatch<TexturedVertex2D> vertexBatch = new QuadBatch<TexturedVertex2D>(100, 10); private readonly QuadBatch<TexturedVertex2D> vertexBatch = new QuadBatch<TexturedVertex2D>(100, 10);
@ -192,7 +192,8 @@ namespace osu.Game.Screens.Menu
shader = Source.shader; shader = Source.shader;
texture = Source.texture; texture = Source.texture;
size = Source.DrawSize.X; size = Source.DrawSize.X;
audioData = Source.frequencyAmplitudes;
Source.frequencyAmplitudes.AsSpan().CopyTo(audioData);
} }
public override void Draw(Action<TexturedVertex2D> vertexAction) public override void Draw(Action<TexturedVertex2D> vertexAction)

View File

@ -28,7 +28,7 @@ namespace osu.Game.Screens.Menu
/// Button designed specifically for the osu!next main menu. /// Button designed specifically for the osu!next main menu.
/// In order to correctly flow, we have to use a negative margin on the parent container (due to the parallelogram shape). /// In order to correctly flow, we have to use a negative margin on the parent container (due to the parallelogram shape).
/// </summary> /// </summary>
public class Button : BeatSyncedContainer, IStateful<ButtonState> public class MainMenuButton : BeatSyncedContainer, IStateful<ButtonState>
{ {
public event Action<ButtonState> StateChanged; public event Action<ButtonState> StateChanged;
@ -51,7 +51,7 @@ namespace osu.Game.Screens.Menu
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
public Button(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, Key triggerKey = Key.Unknown) public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, Key triggerKey = Key.Unknown)
{ {
this.sampleName = sampleName; this.sampleName = sampleName;
this.clickAction = clickAction; this.clickAction = clickAction;
@ -209,7 +209,7 @@ namespace osu.Game.Screens.Menu
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)
{ {
if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed) if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed || e.SuperPressed)
return false; return false;
if (TriggerKey == e.Key && TriggerKey != Key.Unknown) if (TriggerKey == e.Key && TriggerKey != Key.Unknown)

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
@ -93,7 +94,12 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
{ {
} }
protected class SectionContainer : FillFlowContainer<Section> /// <remarks>
/// <see cref="ReverseChildIDFillFlowContainer{T}"/> is used to ensure that if the nested <see cref="Section"/>s
/// use expanded overhanging content (like an <see cref="OsuDropdown{T}"/>'s dropdown),
/// then the overhanging content will be correctly Z-ordered.
/// </remarks>
protected class SectionContainer : ReverseChildIDFillFlowContainer<Section>
{ {
public SectionContainer() public SectionContainer()
{ {

View File

@ -30,7 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private Sample sampleReadyAll; private Sample sampleReadyAll;
private Sample sampleUnready; private Sample sampleUnready;
private readonly BindableBool enabled = new BindableBool(); private readonly MultiplayerReadyButton readyButton;
private readonly MultiplayerCountdownButton countdownButton; private readonly MultiplayerCountdownButton countdownButton;
private int countReady; private int countReady;
private ScheduledDelegate readySampleDelegate; private ScheduledDelegate readySampleDelegate;
@ -50,12 +50,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{ {
new Drawable[] new Drawable[]
{ {
new MultiplayerReadyButton readyButton = new MultiplayerReadyButton
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Size = Vector2.One, Size = Vector2.One,
Action = onReadyClick, Action = onReadyClick,
Enabled = { BindTarget = enabled },
}, },
countdownButton = new MultiplayerCountdownButton countdownButton = new MultiplayerCountdownButton
{ {
@ -63,7 +62,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Size = new Vector2(40, 1), Size = new Vector2(40, 1),
Alpha = 0, Alpha = 0,
Action = startCountdown, Action = startCountdown,
Enabled = { BindTarget = enabled } CancelAction = cancelCountdown
} }
} }
} }
@ -108,30 +107,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Debug.Assert(clickOperation == null); Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation(); clickOperation = ongoingOperationTracker.BeginOperation();
// Ensure the current user becomes ready before being able to do anything else (start match, stop countdown, unready). if (isReady() && Client.IsHost && Room.Countdown == null)
if (!isReady() || !Client.IsHost)
{
toggleReady();
return;
}
// Local user is the room host and is in a ready state.
// The only action they can take is to stop a countdown if one's currently running.
if (Room.Countdown != null)
{
stopCountdown();
return;
}
// And if a countdown isn't running, start the match.
startMatch(); startMatch();
else
toggleReady();
bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating; bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating;
void toggleReady() => Client.ToggleReady().ContinueWith(_ => endOperation()); void toggleReady() => Client.ToggleReady().ContinueWith(_ => endOperation());
void stopCountdown() => Client.SendMatchRequest(new StopCountdownRequest()).ContinueWith(_ => endOperation());
void startMatch() => Client.StartMatch().ContinueWith(t => void startMatch() => Client.StartMatch().ContinueWith(t =>
{ {
// accessing Exception here silences any potential errors from the antecedent task // accessing Exception here silences any potential errors from the antecedent task
@ -153,6 +137,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Client.SendMatchRequest(new StartMatchCountdownRequest { Duration = duration }).ContinueWith(_ => endOperation()); Client.SendMatchRequest(new StartMatchCountdownRequest { Duration = duration }).ContinueWith(_ => endOperation());
} }
private void cancelCountdown()
{
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
Client.SendMatchRequest(new StopCountdownRequest()).ContinueWith(_ => endOperation());
}
private void endOperation() private void endOperation()
{ {
clickOperation?.Dispose(); clickOperation?.Dispose();
@ -163,7 +155,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{ {
if (Room == null) if (Room == null)
{ {
enabled.Value = false; readyButton.Enabled.Value = false;
countdownButton.Enabled.Value = false;
return; return;
} }
@ -172,32 +165,33 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready); int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating); int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
if (Room.Countdown != null) if (!Client.IsHost || Room.Settings.AutoStartEnabled)
countdownButton.Alpha = 0; countdownButton.Hide();
else else
{ {
switch (localUser?.State) switch (localUser?.State)
{ {
default: default:
countdownButton.Alpha = 0; countdownButton.Hide();
break; break;
case MultiplayerUserState.Idle:
case MultiplayerUserState.Spectating: case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready: case MultiplayerUserState.Ready:
countdownButton.Alpha = Room.Host?.Equals(localUser) == true ? 1 : 0; countdownButton.Show();
break; break;
} }
} }
enabled.Value = readyButton.Enabled.Value = countdownButton.Enabled.Value =
Room.State == MultiplayerRoomState.Open Room.State == MultiplayerRoomState.Open
&& CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId && CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId
&& !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired && !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired
&& !operationInProgress.Value; && !operationInProgress.Value;
// When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready. // When the local user is the host and spectating the match, the ready button should be enabled only if any users are ready.
if (localUser?.State == MultiplayerUserState.Spectating) if (localUser?.State == MultiplayerUserState.Spectating)
enabled.Value &= Room.Host?.Equals(localUser) == true && newCountReady > 0; readyButton.Enabled.Value &= Client.IsHost && newCountReady > 0 && Room.Countdown == null;
if (newCountReady == countReady) if (newCountReady == countReady)
return; return;

View File

@ -14,6 +14,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.Multiplayer;
using osuTK; using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
@ -30,12 +31,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
public new Action<TimeSpan> Action; public new Action<TimeSpan> Action;
public Action CancelAction;
[Resolved]
private MultiplayerClient multiplayerClient { get; set; }
[Resolved]
private OsuColour colours { get; set; }
private readonly Drawable background; private readonly Drawable background;
public MultiplayerCountdownButton() public MultiplayerCountdownButton()
{ {
Icon = FontAwesome.Solid.CaretDown; Icon = FontAwesome.Regular.Clock;
IconScale = new Vector2(0.6f);
Add(background = new Box Add(background = new Box
{ {
@ -44,6 +52,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}); });
base.Action = this.ShowPopover; base.Action = this.ShowPopover;
TooltipText = "Countdown settings";
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -52,6 +62,38 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
background.Colour = colours.Green; background.Colour = colours.Green;
} }
protected override void LoadComplete()
{
base.LoadComplete();
multiplayerClient.RoomUpdated += onRoomUpdated;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
multiplayerClient.RoomUpdated -= onRoomUpdated;
}
private void onRoomUpdated() => Scheduler.AddOnce(() =>
{
bool countdownActive = multiplayerClient.Room?.Countdown != null;
if (countdownActive)
{
background
.FadeColour(colours.YellowLight, 100, Easing.In)
.Then()
.FadeColour(colours.YellowDark, 900, Easing.OutQuint)
.Loop();
}
else
{
background
.FadeColour(colours.Green, 200, Easing.OutQuint);
}
});
public Popover GetPopover() public Popover GetPopover()
{ {
var flow = new FillFlowContainer var flow = new FillFlowContainer
@ -68,7 +110,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Text = $"Start match in {duration.Humanize()}", Text = $"Start match in {duration.Humanize()}",
BackgroundColour = background.Colour, BackgroundColour = colours.Green,
Action = () => Action = () =>
{ {
Action(duration); Action(duration);
@ -77,6 +119,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}); });
} }
if (multiplayerClient.Room?.Countdown != null && multiplayerClient.IsHost)
{
flow.Add(new OsuButton
{
RelativeSizeAxes = Axes.X,
Text = "Stop countdown",
BackgroundColour = colours.Red,
Action = () =>
{
CancelAction();
this.HidePopover();
}
});
}
return new OsuPopover { Child = flow }; return new OsuPopover { Child = flow };
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -21,6 +22,7 @@ using osu.Game.Online.Rooms;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Match.Components;
using osuTK; using osuTK;
using Container = osu.Framework.Graphics.Containers.Container;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{ {
@ -56,7 +58,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
public Action SettingsApplied; public Action SettingsApplied;
public OsuTextBox NameField, MaxParticipantsField; public OsuTextBox NameField, MaxParticipantsField;
public RoomAvailabilityPicker AvailabilityPicker;
public MatchTypePicker TypePicker; public MatchTypePicker TypePicker;
public OsuEnumDropdown<QueueMode> QueueModeDropdown; public OsuEnumDropdown<QueueMode> QueueModeDropdown;
public OsuTextBox PasswordTextBox; public OsuTextBox PasswordTextBox;
@ -64,6 +65,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
public OsuSpriteText ErrorText; public OsuSpriteText ErrorText;
private OsuEnumDropdown<StartMode> startModeDropdown;
private OsuSpriteText typeLabel; private OsuSpriteText typeLabel;
private LoadingLayer loadingLayer; private LoadingLayer loadingLayer;
@ -163,14 +165,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
LengthLimit = 100, LengthLimit = 100,
}, },
}, },
new Section("Room visibility") // new Section("Room visibility")
{ // {
Alpha = disabled_alpha, // Alpha = disabled_alpha,
Child = AvailabilityPicker = new RoomAvailabilityPicker // Child = AvailabilityPicker = new RoomAvailabilityPicker
{ // {
Enabled = { Value = false } // Enabled = { Value = false }
}, // },
}, // },
new Section("Game type") new Section("Game type")
{ {
Child = new FillFlowContainer Child = new FillFlowContainer
@ -204,6 +206,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
RelativeSizeAxes = Axes.X RelativeSizeAxes = Axes.X
} }
} }
},
new Section("Auto start")
{
Child = new Container
{
RelativeSizeAxes = Axes.X,
Height = 40,
Child = startModeDropdown = new OsuEnumDropdown<StartMode>
{
RelativeSizeAxes = Axes.X
}
}
} }
}, },
}, },
@ -321,12 +335,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
TypePicker.Current.BindValueChanged(type => typeLabel.Text = type.NewValue.GetLocalisableDescription(), true); TypePicker.Current.BindValueChanged(type => typeLabel.Text = type.NewValue.GetLocalisableDescription(), true);
RoomName.BindValueChanged(name => NameField.Text = name.NewValue, true); RoomName.BindValueChanged(name => NameField.Text = name.NewValue, true);
Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true);
Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true); Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true);
MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true); MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true);
RoomID.BindValueChanged(roomId => playlistContainer.Alpha = roomId.NewValue == null ? 1 : 0, true); RoomID.BindValueChanged(roomId => playlistContainer.Alpha = roomId.NewValue == null ? 1 : 0, true);
Password.BindValueChanged(password => PasswordTextBox.Text = password.NewValue ?? string.Empty, true); Password.BindValueChanged(password => PasswordTextBox.Text = password.NewValue ?? string.Empty, true);
QueueMode.BindValueChanged(mode => QueueModeDropdown.Current.Value = mode.NewValue, true); QueueMode.BindValueChanged(mode => QueueModeDropdown.Current.Value = mode.NewValue, true);
AutoStartDuration.BindValueChanged(duration => startModeDropdown.Current.Value = (StartMode)(int)duration.NewValue.TotalSeconds, true);
operationInProgress.BindTo(ongoingOperationTracker.InProgress); operationInProgress.BindTo(ongoingOperationTracker.InProgress);
operationInProgress.BindValueChanged(v => operationInProgress.BindValueChanged(v =>
@ -363,6 +377,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Debug.Assert(applyingSettingsOperation == null); Debug.Assert(applyingSettingsOperation == null);
applyingSettingsOperation = ongoingOperationTracker.BeginOperation(); applyingSettingsOperation = ongoingOperationTracker.BeginOperation();
TimeSpan autoStartDuration = TimeSpan.FromSeconds((int)startModeDropdown.Current.Value);
// If the client is already in a room, update via the client. // If the client is already in a room, update via the client.
// Otherwise, update the room directly in preparation for it to be submitted to the API on match creation. // Otherwise, update the room directly in preparation for it to be submitted to the API on match creation.
if (client.Room != null) if (client.Room != null)
@ -371,7 +387,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
name: NameField.Text, name: NameField.Text,
password: PasswordTextBox.Text, password: PasswordTextBox.Text,
matchType: TypePicker.Current.Value, matchType: TypePicker.Current.Value,
queueMode: QueueModeDropdown.Current.Value) queueMode: QueueModeDropdown.Current.Value,
autoStartDuration: autoStartDuration)
.ContinueWith(t => Schedule(() => .ContinueWith(t => Schedule(() =>
{ {
if (t.IsCompletedSuccessfully) if (t.IsCompletedSuccessfully)
@ -383,10 +400,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
else else
{ {
room.Name.Value = NameField.Text; room.Name.Value = NameField.Text;
room.Availability.Value = AvailabilityPicker.Current.Value;
room.Type.Value = TypePicker.Current.Value; room.Type.Value = TypePicker.Current.Value;
room.Password.Value = PasswordTextBox.Current.Value; room.Password.Value = PasswordTextBox.Current.Value;
room.QueueMode.Value = QueueModeDropdown.Current.Value; room.QueueMode.Value = QueueModeDropdown.Current.Value;
room.AutoStartDuration.Value = autoStartDuration;
if (int.TryParse(MaxParticipantsField.Text, out int max)) if (int.TryParse(MaxParticipantsField.Text, out int max))
room.MaxParticipants.Value = max; room.MaxParticipants.Value = max;
@ -452,5 +469,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Triangles.ColourDark = colours.YellowDark; Triangles.ColourDark = colours.YellowDark;
} }
} }
private enum StartMode
{
[Description("Off")]
Off = 0,
[Description("30 seconds")]
Seconds_30 = 30,
[Description("1 minute")]
Seconds_60 = 60,
[Description("3 minutes")]
Seconds_180 = 180,
[Description("5 minutes")]
Seconds_300 = 300
}
} }
} }

View File

@ -36,18 +36,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
} }
private MultiplayerCountdown countdown; private MultiplayerCountdown countdown;
private DateTimeOffset countdownReceivedTime; private DateTimeOffset countdownChangeTime;
private ScheduledDelegate countdownUpdateDelegate; private ScheduledDelegate countdownUpdateDelegate;
private void onRoomUpdated() => Scheduler.AddOnce(() => private void onRoomUpdated() => Scheduler.AddOnce(() =>
{ {
if (countdown == null && room?.Countdown != null) if (countdown != room?.Countdown)
countdownReceivedTime = DateTimeOffset.Now; {
countdown = room?.Countdown; countdown = room?.Countdown;
countdownChangeTime = DateTimeOffset.Now;
}
if (room?.Countdown != null) if (countdown != null)
countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 1000, true); countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 100, true);
else else
{ {
countdownUpdateDelegate?.Cancel(); countdownUpdateDelegate?.Cancel();
@ -74,7 +75,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
if (countdown != null) if (countdown != null)
{ {
TimeSpan timeElapsed = DateTimeOffset.Now - countdownReceivedTime; TimeSpan timeElapsed = DateTimeOffset.Now - countdownChangeTime;
TimeSpan countdownRemaining; TimeSpan countdownRemaining;
if (timeElapsed > countdown.TimeRemaining) if (timeElapsed > countdown.TimeRemaining)
@ -168,7 +169,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{ {
get get
{ {
if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready) if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready && !room.Settings.AutoStartEnabled)
return "Cancel countdown"; return "Cancel countdown";
return base.TooltipText; return base.TooltipText;

View File

@ -80,6 +80,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{ {
Schedule(() => Schedule(() =>
{ {
// If an error or server side trigger occurred this screen may have already exited by external means.
if (!this.IsCurrentScreen())
return;
loadingLayer.Hide(); loadingLayer.Hide();
if (t.IsFaulted) if (t.IsFaulted)

View File

@ -198,15 +198,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
else else
userModsDisplay.FadeOut(fade_time); userModsDisplay.FadeOut(fade_time);
if (Client.IsHost && !User.Equals(Client.LocalUser)) kickButton.Alpha = Client.IsHost && !User.Equals(Client.LocalUser) ? 1 : 0;
kickButton.FadeIn(fade_time); crown.Alpha = Room.Host?.Equals(User) == true ? 1 : 0;
else
kickButton.FadeOut(fade_time);
if (Room.Host?.Equals(User) == true)
crown.FadeIn(fade_time);
else
crown.FadeOut(fade_time);
// If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187
// This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix.

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -15,6 +16,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{ {
private FillFlowContainer<ParticipantPanel> panels; private FillFlowContainer<ParticipantPanel> panels;
[CanBeNull]
private ParticipantPanel currentHostPanel;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -55,6 +59,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
// Add panels for all users new to the room. // Add panels for all users new to the room.
foreach (var user in Room.Users.Except(panels.Select(p => p.User))) foreach (var user in Room.Users.Except(panels.Select(p => p.User)))
panels.Add(new ParticipantPanel(user)); panels.Add(new ParticipantPanel(user));
if (currentHostPanel == null || !currentHostPanel.User.Equals(Room.Host))
{
// Reset position of previous host back to normal, if one existing.
if (currentHostPanel != null && panels.Contains(currentHostPanel))
panels.SetLayoutPosition(currentHostPanel, 0);
currentHostPanel = null;
// Change position of new host to display above all participants.
if (Room.Host != null)
{
currentHostPanel = panels.SingleOrDefault(u => u.User.Equals(Room.Host));
if (currentHostPanel != null)
panels.SetLayoutPosition(currentHostPanel, -1);
}
}
} }
} }
} }

View File

@ -81,6 +81,9 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(typeof(Room))] [Resolved(typeof(Room))]
protected Bindable<QueueMode> QueueMode { get; private set; } protected Bindable<QueueMode> QueueMode { get; private set; }
[Resolved(typeof(Room))]
protected Bindable<TimeSpan> AutoStartDuration { get; private set; }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IBindable<PlaylistItem> subScreenSelectedItem { get; set; } private IBindable<PlaylistItem> subScreenSelectedItem { get; set; }

View File

@ -21,7 +21,9 @@ namespace osu.Game.Screens.Play.HUD
private uint scheduledPopOutCurrentId; private uint scheduledPopOutCurrentId;
private const double pop_out_duration = 150; private const double big_pop_out_duration = 300;
private const double small_pop_out_duration = 100;
private const double fade_out_duration = 100; private const double fade_out_duration = 100;
@ -65,32 +67,28 @@ namespace osu.Game.Screens.Play.HUD
Margin = new MarginPadding(10); Margin = new MarginPadding(10);
Scale = new Vector2(1.2f); Scale = new Vector2(1.28f);
InternalChildren = new[] InternalChildren = new[]
{ {
counterContainer = new Container counterContainer = new Container
{ {
AutoSizeAxes = Axes.Both,
AlwaysPresent = true, AlwaysPresent = true,
Children = new[] Children = new[]
{ {
popOutCount = new LegacySpriteText(LegacyFont.Combo) popOutCount = new LegacySpriteText(LegacyFont.Combo)
{ {
Alpha = 0, Alpha = 0,
Margin = new MarginPadding(0.05f),
Blending = BlendingParameters.Additive, Blending = BlendingParameters.Additive,
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
BypassAutoSizeAxes = Axes.Both, BypassAutoSizeAxes = Axes.Both,
}, },
displayedCountSpriteText = new LegacySpriteText(LegacyFont.Combo) displayedCountSpriteText = new LegacySpriteText(LegacyFont.Combo)
{ {
// Initial text and AlwaysPresent allow the counter to have a size before it first displays a combo.
// This is useful for display in the skin editor.
Text = formatCount(0),
AlwaysPresent = true,
Alpha = 0, Alpha = 0,
AlwaysPresent = true,
Anchor = Anchor.BottomLeft,
BypassAutoSizeAxes = Axes.Both,
}, },
} }
} }
@ -130,8 +128,25 @@ namespace osu.Game.Screens.Play.HUD
base.LoadComplete(); base.LoadComplete();
((IHasText)displayedCountSpriteText).Text = formatCount(Current.Value); ((IHasText)displayedCountSpriteText).Text = formatCount(Current.Value);
((IHasText)popOutCount).Text = formatCount(Current.Value);
Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true); Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true);
updateLayout();
}
private void updateLayout()
{
const float font_height_ratio = 0.625f;
const float vertical_offset = 9;
displayedCountSpriteText.OriginPosition = new Vector2(0, font_height_ratio * displayedCountSpriteText.Height + vertical_offset);
displayedCountSpriteText.Position = new Vector2(0, -(1 - font_height_ratio) * displayedCountSpriteText.Height + vertical_offset);
popOutCount.OriginPosition = new Vector2(3, font_height_ratio * popOutCount.Height + vertical_offset); // In stable, the bigger pop out scales a bit to the left
popOutCount.Position = new Vector2(0, -(1 - font_height_ratio) * popOutCount.Height + vertical_offset);
counterContainer.Size = displayedCountSpriteText.Size;
} }
private void updateCount(bool rolling) private void updateCount(bool rolling)
@ -164,27 +179,31 @@ namespace osu.Game.Screens.Play.HUD
{ {
((IHasText)popOutCount).Text = formatCount(newValue); ((IHasText)popOutCount).Text = formatCount(newValue);
popOutCount.ScaleTo(1.6f); popOutCount.ScaleTo(1.56f)
popOutCount.FadeTo(0.75f); .ScaleTo(1, big_pop_out_duration);
popOutCount.MoveTo(Vector2.Zero);
popOutCount.ScaleTo(1, pop_out_duration); popOutCount.FadeTo(0.6f)
popOutCount.FadeOut(pop_out_duration); .FadeOut(big_pop_out_duration);
popOutCount.MoveTo(displayedCountSpriteText.Position, pop_out_duration);
} }
private void transformNoPopOut(int newValue) private void transformNoPopOut(int newValue)
{ {
((IHasText)displayedCountSpriteText).Text = formatCount(newValue); ((IHasText)displayedCountSpriteText).Text = formatCount(newValue);
counterContainer.Size = displayedCountSpriteText.Size;
displayedCountSpriteText.ScaleTo(1); displayedCountSpriteText.ScaleTo(1);
} }
private void transformPopOutSmall(int newValue) private void transformPopOutSmall(int newValue)
{ {
((IHasText)displayedCountSpriteText).Text = formatCount(newValue); ((IHasText)displayedCountSpriteText).Text = formatCount(newValue);
displayedCountSpriteText.ScaleTo(1.1f);
displayedCountSpriteText.ScaleTo(1, pop_out_duration); counterContainer.Size = displayedCountSpriteText.Size;
displayedCountSpriteText.ScaleTo(1).Then()
.ScaleTo(1.1f, small_pop_out_duration / 2, Easing.In).Then()
.ScaleTo(1, small_pop_out_duration / 2, Easing.Out);
} }
private void scheduledPopOutSmall(uint id) private void scheduledPopOutSmall(uint id)
@ -212,7 +231,7 @@ namespace osu.Game.Screens.Play.HUD
Scheduler.AddDelayed(delegate Scheduler.AddDelayed(delegate
{ {
scheduledPopOutSmall(newTaskId); scheduledPopOutSmall(newTaskId);
}, pop_out_duration); }, big_pop_out_duration - 140);
} }
private void onCountRolling(int currentValue, int newValue) private void onCountRolling(int currentValue, int newValue)

View File

@ -287,12 +287,14 @@ namespace osu.Game.Screens.Select
{ {
TitleLabel = new OsuSpriteText TitleLabel = new OsuSpriteText
{ {
Current = { BindTarget = titleBinding },
Font = OsuFont.GetFont(size: 28, italics: true), Font = OsuFont.GetFont(size: 28, italics: true),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Truncate = true, Truncate = true,
}, },
ArtistLabel = new OsuSpriteText ArtistLabel = new OsuSpriteText
{ {
Current = { BindTarget = artistBinding },
Font = OsuFont.GetFont(size: 17, italics: true), Font = OsuFont.GetFont(size: 17, italics: true),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Truncate = true, Truncate = true,
@ -314,9 +316,6 @@ namespace osu.Game.Screens.Select
} }
}; };
titleBinding.BindValueChanged(_ => setMetadata(metadata.Source));
artistBinding.BindValueChanged(_ => setMetadata(metadata.Source), true);
addInfoLabels(); addInfoLabels();
} }
@ -352,12 +351,6 @@ namespace osu.Game.Screens.Select
}, true); }, true);
} }
private void setMetadata(string source)
{
ArtistLabel.Text = artistBinding.Value;
TitleLabel.Text = string.IsNullOrEmpty(source) ? titleBinding.Value : source + " — " + titleBinding.Value;
}
private void addInfoLabels() private void addInfoLabels()
{ {
if (working.Beatmap?.HitObjects?.Any() != true) if (working.Beatmap?.HitObjects?.Any() != true)

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
@ -14,13 +15,13 @@ using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Users; using osu.Game.Users;
using osu.Game.Utils;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Screens.Select namespace osu.Game.Screens.Select
{ {
public class PlaySongSelect : SongSelect public class PlaySongSelect : SongSelect
{ {
private bool removeAutoModOnResume;
private OsuScreen playerLoader; private OsuScreen playerLoader;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
@ -43,25 +44,6 @@ namespace osu.Game.Screens.Select
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
private ModAutoplay getAutoplayMod() => Ruleset.Value.CreateInstance().GetAutoplayMod();
public override void OnResuming(IScreen last)
{
base.OnResuming(last);
playerLoader = null;
if (removeAutoModOnResume)
{
var autoType = getAutoplayMod()?.GetType();
if (autoType != null)
Mods.Value = Mods.Value.Where(m => m.GetType() != autoType).ToArray();
removeAutoModOnResume = false;
}
}
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)
{ {
switch (e.Key) switch (e.Key)
@ -77,10 +59,16 @@ namespace osu.Game.Screens.Select
return base.OnKeyDown(e); return base.OnKeyDown(e);
} }
private IReadOnlyList<Mod> modsAtGameplayStart;
private ModAutoplay getAutoplayMod() => Ruleset.Value.CreateInstance().GetAutoplayMod();
protected override bool OnStart() protected override bool OnStart()
{ {
if (playerLoader != null) return false; if (playerLoader != null) return false;
modsAtGameplayStart = Mods.Value;
// Ctrl+Enter should start map with autoplay enabled. // Ctrl+Enter should start map with autoplay enabled.
if (GetContainingInputManager().CurrentState?.Keyboard.ControlPressed == true) if (GetContainingInputManager().CurrentState?.Keyboard.ControlPressed == true)
{ {
@ -95,13 +83,12 @@ namespace osu.Game.Screens.Select
return false; return false;
} }
var mods = Mods.Value; var mods = Mods.Value.Append(autoInstance).ToArray();
if (mods.All(m => m.GetType() != autoInstance.GetType())) if (!ModUtils.CheckCompatibleSet(mods, out var invalid))
{ mods = mods.Except(invalid).Append(autoInstance).ToArray();
Mods.Value = mods.Append(autoInstance).ToArray();
removeAutoModOnResume = true; Mods.Value = mods;
}
} }
SampleConfirm?.Play(); SampleConfirm?.Play();
@ -111,12 +98,26 @@ namespace osu.Game.Screens.Select
Player createPlayer() Player createPlayer()
{ {
var replayGeneratingMod = Mods.Value.OfType<ICreateReplay>().FirstOrDefault(); var replayGeneratingMod = Mods.Value.OfType<ICreateReplayData>().FirstOrDefault();
if (replayGeneratingMod != null) if (replayGeneratingMod != null)
return new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateReplayScore(beatmap, mods)); {
return new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods));
}
return new SoloPlayer(); return new SoloPlayer();
} }
} }
public override void OnResuming(IScreen last)
{
base.OnResuming(last);
if (playerLoader != null)
{
Mods.Value = modsAtGameplayStart;
playerLoader = null;
}
}
} }
} }

View File

@ -30,11 +30,9 @@ namespace osu.Game.Skinning
public DefaultLegacySkin(SkinInfo skin, IStorageResourceProvider resources) public DefaultLegacySkin(SkinInfo skin, IStorageResourceProvider resources)
: base( : base(
skin, skin,
new NamespacedResourceStore<byte[]>(resources.Resources, "Skins/Legacy"),
resources, resources,
// A default legacy skin may still have a skin.ini if it is modified by the user. // In the case of the actual default legacy skin (ie. the fallback one, which a user hasn't applied any modifications to) we want to use the game provided resources.
// We must specify the stream directly as we are redirecting storage to the osu-resources location for other files. skin.Protected ? new NamespacedResourceStore<byte[]>(resources.Resources, "Skins/Legacy") : null
new LegacyDatabasedSkinResourceStore(skin, resources.Files).GetStream("skin.ini")
) )
{ {
Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255); Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255);

View File

@ -42,6 +42,9 @@ namespace osu.Game.Skinning.Editor
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
[Resolved(canBeNull: true)]
private SkinEditorOverlay skinEditorOverlay { get; set; }
[Cached] [Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
@ -107,7 +110,7 @@ namespace osu.Game.Skinning.Editor
new EditorMenuItem("Save", MenuItemType.Standard, Save), new EditorMenuItem("Save", MenuItemType.Standard, Save),
new EditorMenuItem("Revert to default", MenuItemType.Destructive, revert), new EditorMenuItem("Revert to default", MenuItemType.Destructive, revert),
new EditorMenuItemSpacer(), new EditorMenuItemSpacer(),
new EditorMenuItem("Exit", MenuItemType.Standard, Hide), new EditorMenuItem("Exit", MenuItemType.Standard, () => skinEditorOverlay?.Hide()),
}, },
}, },
} }

View File

@ -14,6 +14,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osuTK; using osuTK;
@ -94,7 +95,7 @@ namespace osu.Game.Skinning.Editor
var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod(); var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod();
if (replayGeneratingMod != null) if (replayGeneratingMod != null)
screen.Push(new PlayerLoader(() => new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateReplayScore(beatmap, mods)))); screen.Push(new PlayerLoader(() => new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods))));
}, new[] { typeof(Player), typeof(SongSelect) }) }, new[] { typeof(Player), typeof(SongSelect) })
}, },
} }

View File

@ -1,7 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations; #nullable enable
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -21,16 +22,14 @@ namespace osu.Game.Skinning
/// </summary> /// </summary>
/// <param name="component">The requested component.</param> /// <param name="component">The requested component.</param>
/// <returns>A drawable representation for the requested component, or null if unavailable.</returns> /// <returns>A drawable representation for the requested component, or null if unavailable.</returns>
[CanBeNull] Drawable? GetDrawableComponent(ISkinComponent component);
Drawable GetDrawableComponent(ISkinComponent component);
/// <summary> /// <summary>
/// Retrieve a <see cref="Texture"/>. /// Retrieve a <see cref="Texture"/>.
/// </summary> /// </summary>
/// <param name="componentName">The requested texture.</param> /// <param name="componentName">The requested texture.</param>
/// <returns>A matching texture, or null if unavailable.</returns> /// <returns>A matching texture, or null if unavailable.</returns>
[CanBeNull] Texture? GetTexture(string componentName) => GetTexture(componentName, default, default);
Texture GetTexture(string componentName) => GetTexture(componentName, default, default);
/// <summary> /// <summary>
/// Retrieve a <see cref="Texture"/>. /// Retrieve a <see cref="Texture"/>.
@ -39,23 +38,22 @@ namespace osu.Game.Skinning
/// <param name="wrapModeS">The texture wrap mode in horizontal direction.</param> /// <param name="wrapModeS">The texture wrap mode in horizontal direction.</param>
/// <param name="wrapModeT">The texture wrap mode in vertical direction.</param> /// <param name="wrapModeT">The texture wrap mode in vertical direction.</param>
/// <returns>A matching texture, or null if unavailable.</returns> /// <returns>A matching texture, or null if unavailable.</returns>
[CanBeNull] Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT);
Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT);
/// <summary> /// <summary>
/// Retrieve a <see cref="SampleChannel"/>. /// Retrieve a <see cref="SampleChannel"/>.
/// </summary> /// </summary>
/// <param name="sampleInfo">The requested sample.</param> /// <param name="sampleInfo">The requested sample.</param>
/// <returns>A matching sample channel, or null if unavailable.</returns> /// <returns>A matching sample channel, or null if unavailable.</returns>
[CanBeNull] ISample? GetSample(ISampleInfo sampleInfo);
ISample GetSample(ISampleInfo sampleInfo);
/// <summary> /// <summary>
/// Retrieve a configuration value. /// Retrieve a configuration value.
/// </summary> /// </summary>
/// <param name="lookup">The requested configuration value.</param> /// <param name="lookup">The requested configuration value.</param>
/// <returns>A matching value boxed in an <see cref="IBindable{TValue}"/>, or null if unavailable.</returns> /// <returns>A matching value boxed in an <see cref="IBindable{TValue}"/>, or null if unavailable.</returns>
[CanBeNull] IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup); where TLookup : notnull
where TValue : notnull;
} }
} }

View File

@ -1,8 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Game.Audio; using osu.Game.Audio;
@ -20,14 +23,28 @@ namespace osu.Game.Skinning
protected override bool AllowManiaSkin => false; protected override bool AllowManiaSkin => false;
protected override bool UseCustomSampleBanks => true; protected override bool UseCustomSampleBanks => true;
public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, IResourceStore<byte[]> storage, IStorageResourceProvider resources) /// <summary>
: base(createSkinInfo(beatmapInfo), new LegacySkinResourceStore(beatmapInfo.BeatmapSet, storage), resources, beatmapInfo.Path) /// Construct a new legacy beatmap skin instance.
/// </summary>
/// <param name="beatmapInfo">The model for this beatmap.</param>
/// <param name="resources">Access to raw game resources.</param>
public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, IStorageResourceProvider? resources)
: base(createSkinInfo(beatmapInfo), resources, createRealmBackedStore(beatmapInfo, resources), beatmapInfo.Path.AsNonNull())
{ {
// Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer) // Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer)
Configuration.AllowDefaultComboColoursFallback = false; Configuration.AllowDefaultComboColoursFallback = false;
} }
public override Drawable GetDrawableComponent(ISkinComponent component) private static IResourceStore<byte[]> createRealmBackedStore(BeatmapInfo beatmapInfo, IStorageResourceProvider? resources)
{
if (resources == null)
// should only ever be used in tests.
return new ResourceStore<byte[]>();
return new RealmBackedResourceStore(beatmapInfo.BeatmapSet, resources.Files, new[] { @"ogg" });
}
public override Drawable? GetDrawableComponent(ISkinComponent component)
{ {
if (component is SkinnableTargetComponent targetComponent) if (component is SkinnableTargetComponent targetComponent)
{ {
@ -46,7 +63,7 @@ namespace osu.Game.Skinning
return base.GetDrawableComponent(component); return base.GetDrawableComponent(component);
} }
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
{ {
switch (lookup) switch (lookup)
{ {
@ -62,10 +79,10 @@ namespace osu.Game.Skinning
return base.GetConfig<TLookup, TValue>(lookup); return base.GetConfig<TLookup, TValue>(lookup);
} }
protected override IBindable<Color4> GetComboColour(IHasComboColours source, int comboIndex, IHasComboInformation combo) protected override IBindable<Color4>? GetComboColour(IHasComboColours source, int comboIndex, IHasComboInformation combo)
=> base.GetComboColour(source, combo.ComboIndexWithOffsets, combo); => base.GetComboColour(source, combo.ComboIndexWithOffsets, combo);
public override ISample GetSample(ISampleInfo sampleInfo) public override ISample? GetSample(ISampleInfo sampleInfo)
{ {
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy && legacy.CustomSampleBank == 0) if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy && legacy.CustomSampleBank == 0)
{ {
@ -77,6 +94,10 @@ namespace osu.Game.Skinning
} }
private static SkinInfo createSkinInfo(BeatmapInfo beatmapInfo) => private static SkinInfo createSkinInfo(BeatmapInfo beatmapInfo) =>
new SkinInfo { Name = beatmapInfo.ToString(), Creator = beatmapInfo.Metadata.Author.Username ?? string.Empty }; new SkinInfo
{
Name = beatmapInfo.ToString(),
Creator = beatmapInfo.Metadata.Author.Username ?? string.Empty
};
} }
} }

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
@ -15,6 +17,7 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Database;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -27,12 +30,6 @@ namespace osu.Game.Skinning
{ {
public class LegacySkin : Skin public class LegacySkin : Skin
{ {
[CanBeNull]
protected TextureStore Textures;
[CanBeNull]
protected ISampleStore Samples;
/// <summary> /// <summary>
/// Whether texture for the keys exists. /// Whether texture for the keys exists.
/// Used to determine if the mania ruleset is skinned. /// Used to determine if the mania ruleset is skinned.
@ -51,7 +48,7 @@ namespace osu.Game.Skinning
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
public LegacySkin(SkinInfo skin, IStorageResourceProvider resources) public LegacySkin(SkinInfo skin, IStorageResourceProvider resources)
: this(skin, new LegacyDatabasedSkinResourceStore(skin, resources.Files), resources, "skin.ini") : this(skin, resources, null)
{ {
} }
@ -59,36 +56,12 @@ namespace osu.Game.Skinning
/// Construct a new legacy skin instance. /// Construct a new legacy skin instance.
/// </summary> /// </summary>
/// <param name="skin">The model for this skin.</param> /// <param name="skin">The model for this skin.</param>
/// <param name="storage">A storage for looking up files within this skin using user-facing filenames.</param>
/// <param name="resources">Access to raw game resources.</param> /// <param name="resources">Access to raw game resources.</param>
/// <param name="storage">An optional store which will be used for looking up skin resources. If null, one will be created from realm <see cref="IHasRealmFiles"/> pattern.</param>
/// <param name="configurationFilename">The user-facing filename of the configuration file to be parsed. Can accept an .osu or skin.ini file.</param> /// <param name="configurationFilename">The user-facing filename of the configuration file to be parsed. Can accept an .osu or skin.ini file.</param>
protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore<byte[]> storage, [CanBeNull] IStorageResourceProvider resources, string configurationFilename) protected LegacySkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? storage, string configurationFilename = @"skin.ini")
: this(skin, storage, resources, string.IsNullOrEmpty(configurationFilename) ? null : storage?.GetStream(configurationFilename)) : base(skin, resources, storage, configurationFilename)
{ {
}
/// <summary>
/// Construct a new legacy skin instance.
/// </summary>
/// <param name="skin">The model for this skin.</param>
/// <param name="storage">A storage for looking up files within this skin using user-facing filenames.</param>
/// <param name="resources">Access to raw game resources.</param>
/// <param name="configurationStream">An optional stream containing the contents of a skin.ini file.</param>
protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore<byte[]> storage, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] Stream configurationStream)
: base(skin, resources, configurationStream)
{
if (storage != null)
{
var samples = resources?.AudioManager?.GetSampleStore(storage);
if (samples != null)
samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY;
Samples = samples;
Textures = new TextureStore(resources?.CreateTextureLoaderStore(storage));
(storage as ResourceStore<byte[]>)?.AddExtension("ogg");
}
// todo: this shouldn't really be duplicated here (from ManiaLegacySkinTransformer). we need to come up with a better solution. // todo: this shouldn't really be duplicated here (from ManiaLegacySkinTransformer). we need to come up with a better solution.
hasKeyTexture = new Lazy<bool>(() => this.GetAnimation( hasKeyTexture = new Lazy<bool>(() => this.GetAnimation(
lookupForMania<string>(new LegacyManiaSkinConfigurationLookup(4, LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value ?? "mania-key1", true, lookupForMania<string>(new LegacyManiaSkinConfigurationLookup(4, LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value ?? "mania-key1", true,
@ -110,7 +83,7 @@ namespace osu.Game.Skinning
} }
} }
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
{ {
switch (lookup) switch (lookup)
{ {
@ -156,7 +129,7 @@ namespace osu.Game.Skinning
return null; return null;
} }
private IBindable<TValue> lookupForMania<TValue>(LegacyManiaSkinConfigurationLookup maniaLookup) private IBindable<TValue>? lookupForMania<TValue>(LegacyManiaSkinConfigurationLookup maniaLookup)
{ {
if (!maniaConfigurations.TryGetValue(maniaLookup.Keys, out var existing)) if (!maniaConfigurations.TryGetValue(maniaLookup.Keys, out var existing))
maniaConfigurations[maniaLookup.Keys] = existing = new LegacyManiaSkinConfiguration(maniaLookup.Keys); maniaConfigurations[maniaLookup.Keys] = existing = new LegacyManiaSkinConfiguration(maniaLookup.Keys);
@ -296,20 +269,20 @@ namespace osu.Game.Skinning
/// <param name="source">The source to retrieve the combo colours from.</param> /// <param name="source">The source to retrieve the combo colours from.</param>
/// <param name="colourIndex">The preferred index for retrieving the combo colour with.</param> /// <param name="colourIndex">The preferred index for retrieving the combo colour with.</param>
/// <param name="combo">Information on the combo whose using the returned colour.</param> /// <param name="combo">Information on the combo whose using the returned colour.</param>
protected virtual IBindable<Color4> GetComboColour(IHasComboColours source, int colourIndex, IHasComboInformation combo) protected virtual IBindable<Color4>? GetComboColour(IHasComboColours source, int colourIndex, IHasComboInformation combo)
{ {
var colour = source.ComboColours?[colourIndex % source.ComboColours.Count]; var colour = source.ComboColours?[colourIndex % source.ComboColours.Count];
return colour.HasValue ? new Bindable<Color4>(colour.Value) : null; return colour.HasValue ? new Bindable<Color4>(colour.Value) : null;
} }
private IBindable<Color4> getCustomColour(IHasCustomColours source, string lookup) private IBindable<Color4>? getCustomColour(IHasCustomColours source, string lookup)
=> source.CustomColours.TryGetValue(lookup, out var col) ? new Bindable<Color4>(col) : null; => source.CustomColours.TryGetValue(lookup, out var col) ? new Bindable<Color4>(col) : null;
private IBindable<string> getManiaImage(LegacyManiaSkinConfiguration source, string lookup) private IBindable<string>? getManiaImage(LegacyManiaSkinConfiguration source, string lookup)
=> source.ImageLookups.TryGetValue(lookup, out string image) ? new Bindable<string>(image) : null; => source.ImageLookups.TryGetValue(lookup, out string image) ? new Bindable<string>(image) : null;
[CanBeNull] private IBindable<TValue>? legacySettingLookup<TValue>(SkinConfiguration.LegacySetting legacySetting)
private IBindable<TValue> legacySettingLookup<TValue>(SkinConfiguration.LegacySetting legacySetting) where TValue : notnull
{ {
switch (legacySetting) switch (legacySetting)
{ {
@ -321,8 +294,9 @@ namespace osu.Game.Skinning
} }
} }
[CanBeNull] private IBindable<TValue>? genericLookup<TLookup, TValue>(TLookup lookup)
private IBindable<TValue> genericLookup<TLookup, TValue>(TLookup lookup) where TLookup : notnull
where TValue : notnull
{ {
try try
{ {
@ -345,7 +319,7 @@ namespace osu.Game.Skinning
return null; return null;
} }
public override Drawable GetDrawableComponent(ISkinComponent component) public override Drawable? GetDrawableComponent(ISkinComponent component)
{ {
if (base.GetDrawableComponent(component) is Drawable c) if (base.GetDrawableComponent(component) is Drawable c)
return c; return c;
@ -385,8 +359,7 @@ namespace osu.Game.Skinning
} }
}) })
{ {
Children = this.HasFont(LegacyFont.Score) Children = new Drawable[]
? new Drawable[]
{ {
new LegacyComboCounter(), new LegacyComboCounter(),
new LegacyScoreCounter(), new LegacyScoreCounter(),
@ -395,16 +368,6 @@ namespace osu.Game.Skinning
new SongProgress(), new SongProgress(),
new BarHitErrorMeter(), new BarHitErrorMeter(),
} }
: new Drawable[]
{
// TODO: these should fallback to using osu!classic skin textures, rather than doing this.
new DefaultComboCounter(),
new DefaultScoreCounter(),
new DefaultAccuracyCounter(),
new DefaultHealthDisplay(),
new SongProgress(),
new BarHitErrorMeter(),
}
}; };
return skinnableTargetWrapper; return skinnableTargetWrapper;
@ -414,7 +377,7 @@ namespace osu.Game.Skinning
case GameplaySkinComponent<HitResult> resultComponent: case GameplaySkinComponent<HitResult> resultComponent:
// TODO: this should be inside the judgement pieces. // TODO: this should be inside the judgement pieces.
Func<Drawable> createDrawable = () => getJudgementAnimation(resultComponent.Component); Func<Drawable?> createDrawable = () => getJudgementAnimation(resultComponent.Component);
// kind of wasteful that we throw this away, but should do for now. // kind of wasteful that we throw this away, but should do for now.
if (createDrawable() != null) if (createDrawable() != null)
@ -433,7 +396,7 @@ namespace osu.Game.Skinning
return this.GetAnimation(component.LookupName, false, false); return this.GetAnimation(component.LookupName, false, false);
} }
private Texture getParticleTexture(HitResult result) private Texture? getParticleTexture(HitResult result)
{ {
switch (result) switch (result)
{ {
@ -450,7 +413,7 @@ namespace osu.Game.Skinning
return null; return null;
} }
private Drawable getJudgementAnimation(HitResult result) private Drawable? getJudgementAnimation(HitResult result)
{ {
switch (result) switch (result)
{ {
@ -470,7 +433,7 @@ namespace osu.Game.Skinning
return null; return null;
} }
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{ {
foreach (string name in getFallbackNames(componentName)) foreach (string name in getFallbackNames(componentName))
{ {
@ -498,7 +461,7 @@ namespace osu.Game.Skinning
return null; return null;
} }
public override ISample GetSample(ISampleInfo sampleInfo) public override ISample? GetSample(ISampleInfo sampleInfo)
{ {
IEnumerable<string> lookupNames; IEnumerable<string> lookupNames;
@ -551,12 +514,5 @@ namespace osu.Game.Skinning
// Fall back to using the last piece for components coming from lazer (e.g. "Gameplay/osu/approachcircle" -> "approachcircle"). // Fall back to using the last piece for components coming from lazer (e.g. "Gameplay/osu/approachcircle" -> "approachcircle").
yield return componentName.Split('/').Last(); yield return componentName.Split('/').Last();
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
Textures?.Dispose();
Samples?.Dispose();
}
} }
} }

View File

@ -1,39 +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;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
using osu.Framework.IO.Stores;
using osu.Game.Database;
using osu.Game.Extensions;
namespace osu.Game.Skinning
{
public class LegacySkinResourceStore : ResourceStore<byte[]>
{
private readonly IHasNamedFiles source;
public LegacySkinResourceStore(IHasNamedFiles source, IResourceStore<byte[]> underlyingStore)
: base(underlyingStore)
{
this.source = source;
}
protected override IEnumerable<string> GetFilenames(string name)
{
foreach (string filename in base.GetFilenames(name))
{
string path = getPathForFile(filename.ToStandardisedPath());
if (path != null)
yield return path;
}
}
private string getPathForFile(string filename) =>
source.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath();
public override IEnumerable<string> GetAvailableResources() => source.Files.Select(f => f.Filename);
}
}

View File

@ -4,21 +4,29 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Game.Database;
using osu.Game.Extensions; using osu.Game.Extensions;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
public class LegacyDatabasedSkinResourceStore : ResourceStore<byte[]> public class RealmBackedResourceStore : ResourceStore<byte[]>
{ {
private readonly Dictionary<string, string> fileToStoragePathMapping = new Dictionary<string, string>(); private readonly Dictionary<string, string> fileToStoragePathMapping = new Dictionary<string, string>();
public LegacyDatabasedSkinResourceStore(SkinInfo source, IResourceStore<byte[]> underlyingStore) public RealmBackedResourceStore(IHasRealmFiles source, IResourceStore<byte[]> underlyingStore, string[] extensions = null)
: base(underlyingStore) : base(underlyingStore)
{ {
// Must be initialised before the file cache.
if (extensions != null)
{
foreach (string extension in extensions)
AddExtension(extension);
}
initialiseFileCache(source); initialiseFileCache(source);
} }
private void initialiseFileCache(SkinInfo source) private void initialiseFileCache(IHasRealmFiles source)
{ {
fileToStoragePathMapping.Clear(); fileToStoragePathMapping.Clear();
foreach (var f in source.Files) foreach (var f in source.Files)

View File

@ -46,7 +46,10 @@ namespace osu.Game.Skinning
return null; return null;
} }
public IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup) => null; public IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
where TLookup : notnull
where TValue : notnull
=> null;
public void Dispose() public void Dispose()
{ {

View File

@ -1,22 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using JetBrains.Annotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
@ -24,8 +26,17 @@ namespace osu.Game.Skinning
{ {
public abstract class Skin : IDisposable, ISkin public abstract class Skin : IDisposable, ISkin
{ {
/// <summary>
/// A texture store which can be used to perform user file lookups for this skin.
/// </summary>
protected TextureStore? Textures { get; }
/// <summary>
/// A sample store which can be used to perform user file lookups for this skin.
/// </summary>
protected ISampleStore? Samples { get; }
public readonly Live<SkinInfo> SkinInfo; public readonly Live<SkinInfo> SkinInfo;
private readonly IStorageResourceProvider resources;
public SkinConfiguration Configuration { get; set; } public SkinConfiguration Configuration { get; set; }
@ -33,46 +44,61 @@ namespace osu.Game.Skinning
private readonly Dictionary<SkinnableTarget, SkinnableInfo[]> drawableComponentInfo = new Dictionary<SkinnableTarget, SkinnableInfo[]>(); private readonly Dictionary<SkinnableTarget, SkinnableInfo[]> drawableComponentInfo = new Dictionary<SkinnableTarget, SkinnableInfo[]>();
public abstract ISample GetSample(ISampleInfo sampleInfo); public abstract ISample? GetSample(ISampleInfo sampleInfo);
public Texture GetTexture(string componentName) => GetTexture(componentName, default, default); public Texture? GetTexture(string componentName) => GetTexture(componentName, default, default);
public abstract Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT); public abstract Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT);
public abstract IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup); public abstract IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
where TLookup : notnull
where TValue : notnull;
protected Skin(SkinInfo skin, IStorageResourceProvider resources, [CanBeNull] Stream configurationStream = null) /// <summary>
/// Construct a new skin.
/// </summary>
/// <param name="skin">The skin's metadata. Usually a live realm object.</param>
/// <param name="resources">Access to game-wide resources.</param>
/// <param name="storage">An optional store which will *replace* all file lookups that are usually sourced from <paramref name="skin"/>.</param>
/// <param name="configurationFilename">An optional filename to read the skin configuration from. If not provided, the configuration will be retrieved from the storage using "skin.ini".</param>
protected Skin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? storage = null, string configurationFilename = @"skin.ini")
{ {
SkinInfo = resources?.RealmAccess != null if (resources != null)
? skin.ToLive(resources.RealmAccess) {
// This path should only be used in some tests. SkinInfo = skin.ToLive(resources.RealmAccess);
: skin.ToLiveUnmanaged();
this.resources = resources; storage ??= new RealmBackedResourceStore(skin, resources.Files, new[] { @"ogg" });
configurationStream ??= getConfigurationStream(); var samples = resources.AudioManager?.GetSampleStore(storage);
if (samples != null)
samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY;
Samples = samples;
Textures = new TextureStore(resources.CreateTextureLoaderStore(storage));
}
else
{
// Generally only used for tests.
SkinInfo = skin.ToLiveUnmanaged();
}
var configurationStream = storage?.GetStream(configurationFilename);
if (configurationStream != null) if (configurationStream != null)
{
// stream will be closed after use by LineBufferedReader. // stream will be closed after use by LineBufferedReader.
ParseConfigurationStream(configurationStream); ParseConfigurationStream(configurationStream);
Debug.Assert(Configuration != null);
}
else else
Configuration = new SkinConfiguration(); Configuration = new SkinConfiguration();
// skininfo files may be null for default skin. // skininfo files may be null for default skin.
SkinInfo.PerformRead(s =>
{
// we may want to move this to some kind of async operation in the future.
foreach (SkinnableTarget skinnableTarget in Enum.GetValues(typeof(SkinnableTarget))) foreach (SkinnableTarget skinnableTarget in Enum.GetValues(typeof(SkinnableTarget)))
{ {
string filename = $"{skinnableTarget}.json"; string filename = $"{skinnableTarget}.json";
// skininfo files may be null for default skin. byte[]? bytes = storage?.Get(filename);
var fileInfo = s.Files.FirstOrDefault(f => f.Filename == filename);
if (fileInfo == null)
continue;
byte[] bytes = resources?.Files.Get(fileInfo.File.GetStoragePath());
if (bytes == null) if (bytes == null)
continue; continue;
@ -92,7 +118,6 @@ namespace osu.Game.Skinning
Logger.Error(ex, "Failed to load skin configuration."); Logger.Error(ex, "Failed to load skin configuration.");
} }
} }
});
} }
protected virtual void ParseConfigurationStream(Stream stream) protected virtual void ParseConfigurationStream(Stream stream)
@ -101,16 +126,6 @@ namespace osu.Game.Skinning
Configuration = new LegacySkinDecoder().Decode(reader); Configuration = new LegacySkinDecoder().Decode(reader);
} }
private Stream getConfigurationStream()
{
string path = SkinInfo.PerformRead(s => s.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath());
if (string.IsNullOrEmpty(path))
return null;
return resources?.Files.GetStream(path);
}
/// <summary> /// <summary>
/// Remove all stored customisations for the provided target. /// Remove all stored customisations for the provided target.
/// </summary> /// </summary>
@ -129,7 +144,7 @@ namespace osu.Game.Skinning
DrawableComponentInfo[targetContainer.Target] = targetContainer.CreateSkinnableInfo().ToArray(); DrawableComponentInfo[targetContainer.Target] = targetContainer.CreateSkinnableInfo().ToArray();
} }
public virtual Drawable GetDrawableComponent(ISkinComponent component) public virtual Drawable? GetDrawableComponent(ISkinComponent component)
{ {
switch (component) switch (component)
{ {
@ -137,9 +152,23 @@ namespace osu.Game.Skinning
if (!DrawableComponentInfo.TryGetValue(target.Target, out var skinnableInfo)) if (!DrawableComponentInfo.TryGetValue(target.Target, out var skinnableInfo))
return null; return null;
var components = new List<Drawable>();
foreach (var i in skinnableInfo)
{
try
{
components.Add(i.CreateInstance());
}
catch (Exception e)
{
Logger.Error(e, $"Unable to create skin component {i.Type.Name}");
}
}
return new SkinnableTargetComponentsContainer return new SkinnableTargetComponentsContainer
{ {
ChildrenEnumerable = skinnableInfo.Select(i => i.CreateInstance()) Children = components,
}; };
} }
@ -168,6 +197,9 @@ namespace osu.Game.Skinning
return; return;
isDisposed = true; isDisposed = true;
Textures?.Dispose();
Samples?.Dispose();
} }
#endregion #endregion

View File

@ -96,11 +96,13 @@ namespace osu.Game.Tests.Beatmaps
AddStep("setup skins", () => AddStep("setup skins", () =>
{ {
userSkinInfo.Files.Clear(); userSkinInfo.Files.Clear();
if (!string.IsNullOrEmpty(userFile))
userSkinInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = userFile }, userFile)); userSkinInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = userFile }, userFile));
Debug.Assert(beatmapInfo.BeatmapSet != null); Debug.Assert(beatmapInfo.BeatmapSet != null);
beatmapInfo.BeatmapSet.Files.Clear(); beatmapInfo.BeatmapSet.Files.Clear();
if (!string.IsNullOrEmpty(beatmapFile))
beatmapInfo.BeatmapSet.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = beatmapFile }, beatmapFile)); beatmapInfo.BeatmapSet.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = beatmapFile }, beatmapFile));
// Need to refresh the cached skin source to refresh the skin resource store. // Need to refresh the cached skin source to refresh the skin resource store.
@ -191,22 +193,32 @@ namespace osu.Game.Tests.Beatmaps
} }
} }
private class TestWorkingBeatmap : ClockBackedTestWorkingBeatmap private class TestWorkingBeatmap : ClockBackedTestWorkingBeatmap, IStorageResourceProvider
{ {
private readonly BeatmapInfo skinBeatmapInfo; private readonly BeatmapInfo skinBeatmapInfo;
private readonly IResourceStore<byte[]> resourceStore;
private readonly IStorageResourceProvider resources; private readonly IStorageResourceProvider resources;
public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore<byte[]> resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, IStorageResourceProvider resources) public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore<byte[]> accessMarkingResourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock,
IStorageResourceProvider resources)
: base(beatmap, storyboard, referenceClock, resources.AudioManager) : base(beatmap, storyboard, referenceClock, resources.AudioManager)
{ {
this.skinBeatmapInfo = skinBeatmapInfo; this.skinBeatmapInfo = skinBeatmapInfo;
this.resourceStore = resourceStore; Files = accessMarkingResourceStore;
this.resources = resources; this.resources = resources;
} }
protected internal override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, resources); protected internal override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, this);
public AudioManager AudioManager => resources.AudioManager;
public IResourceStore<byte[]> Files { get; }
public IResourceStore<byte[]> Resources => resources.Resources;
public RealmAccess RealmAccess => resources.RealmAccess;
public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => resources.CreateTextureLoaderStore(underlyingStore);
} }
} }
} }

View File

@ -7,7 +7,6 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.IO.Stores;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -112,7 +111,7 @@ namespace osu.Game.Tests.Beatmaps
public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.DarkGoldenrod; public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.DarkGoldenrod;
public TestBeatmapSkin(BeatmapInfo beatmapInfo, bool hasColours) public TestBeatmapSkin(BeatmapInfo beatmapInfo, bool hasColours)
: base(beatmapInfo, new ResourceStore<byte[]>(), null) : base(beatmapInfo, null)
{ {
if (hasColours) if (hasColours)
{ {
@ -141,7 +140,7 @@ namespace osu.Game.Tests.Beatmaps
public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.LightCyan; public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.LightCyan;
public TestSkin(bool hasCustomColours) public TestSkin(bool hasCustomColours)
: base(new SkinInfo(), new ResourceStore<byte[]>(), null, string.Empty) : base(new SkinInfo(), null, null)
{ {
if (hasCustomColours) if (hasCustomColours)
{ {

View File

@ -119,6 +119,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
Debug.Assert(Room != null); Debug.Assert(Room != null);
((IMultiplayerClient)this).UserStateChanged(userId, newState); ((IMultiplayerClient)this).UserStateChanged(userId, newState);
updateRoomStateIfRequired();
}
private void updateRoomStateIfRequired()
{
Debug.Assert(Room != null);
Debug.Assert(APIRoom != null);
Schedule(() => Schedule(() =>
{ {
@ -126,13 +133,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
case MultiplayerRoomState.Open: case MultiplayerRoomState.Open:
// If there are no remaining ready users or the host is not ready, stop any existing countdown. // If there are no remaining ready users or the host is not ready, stop any existing countdown.
// Todo: When we have an "automatic start" mode, this should also start a new countdown if any users _are_ ready.
// Todo: This doesn't yet support non-match-start countdowns. // Todo: This doesn't yet support non-match-start countdowns.
bool shouldStopCountdown = Room.Users.All(u => u.State != MultiplayerUserState.Ready); if (Room.Settings.AutoStartEnabled)
shouldStopCountdown |= Room.Host?.State != MultiplayerUserState.Ready && Room.Host?.State != MultiplayerUserState.Spectating; {
bool shouldHaveCountdown = !APIRoom.Playlist.GetCurrentItem()!.Expired && Room.Users.Any(u => u.State == MultiplayerUserState.Ready);
if (shouldHaveCountdown && Room.Countdown == null)
startCountdown(new MatchStartCountdown { TimeRemaining = Room.Settings.AutoStartDuration }, StartMatch);
}
if (shouldStopCountdown)
countdownStopSource?.Cancel();
break; break;
case MultiplayerRoomState.WaitingForLoad: case MultiplayerRoomState.WaitingForLoad:
@ -204,7 +213,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
Name = apiRoom.Name.Value, Name = apiRoom.Name.Value,
MatchType = apiRoom.Type.Value, MatchType = apiRoom.Type.Value,
Password = password, Password = password,
QueueMode = apiRoom.QueueMode.Value QueueMode = apiRoom.QueueMode.Value,
AutoStartDuration = apiRoom.AutoStartDuration.Value
}, },
Playlist = serverSidePlaylist.ToList(), Playlist = serverSidePlaylist.ToList(),
Users = { localUser }, Users = { localUser },
@ -263,6 +273,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
ChangeUserState(user.UserID, MultiplayerUserState.Idle); ChangeUserState(user.UserID, MultiplayerUserState.Idle);
await changeMatchType(settings.MatchType).ConfigureAwait(false); await changeMatchType(settings.MatchType).ConfigureAwait(false);
updateRoomStateIfRequired();
} }
public override Task ChangeState(MultiplayerUserState newState) public override Task ChangeState(MultiplayerUserState newState)
@ -315,15 +326,42 @@ namespace osu.Game.Tests.Visual.Multiplayer
switch (request) switch (request)
{ {
case StartMatchCountdownRequest matchCountdownRequest: case StartMatchCountdownRequest matchCountdownRequest:
startCountdown(new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Duration }, StartMatch);
break;
case StopCountdownRequest _:
stopCountdown();
break;
case ChangeTeamRequest changeTeam:
TeamVersusRoomState roomState = (TeamVersusRoomState)Room.MatchState!;
TeamVersusUserState userState = (TeamVersusUserState)LocalUser.MatchState!;
var targetTeam = roomState.Teams.FirstOrDefault(t => t.ID == changeTeam.TeamID);
if (targetTeam != null)
{
userState.TeamID = targetTeam.ID;
await ((IMultiplayerClient)this).MatchUserStateChanged(LocalUser.UserID, userState).ConfigureAwait(false);
}
break;
}
}
private void startCountdown(MultiplayerCountdown countdown, Func<Task> continuation)
{
Debug.Assert(Room != null);
Debug.Assert(ThreadSafety.IsUpdateThread); Debug.Assert(ThreadSafety.IsUpdateThread);
countdownStopSource?.Cancel(); stopCountdown();
// Note that this will leak CTSs, however this is a test method and we haven't noticed foregoing disposal of non-linked CTSs to be detrimental. // Note that this will leak CTSs, however this is a test method and we haven't noticed foregoing disposal of non-linked CTSs to be detrimental.
// If necessary, this can be moved into the final schedule below, and the class-level fields be nulled out accordingly. // If necessary, this can be moved into the final schedule below, and the class-level fields be nulled out accordingly.
var stopSource = countdownStopSource = new CancellationTokenSource(); var stopSource = countdownStopSource = new CancellationTokenSource();
var skipSource = countdownSkipSource = new CancellationTokenSource(); var skipSource = countdownSkipSource = new CancellationTokenSource();
var countdown = new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Duration };
Task lastCountdownTask = countdownTask; Task lastCountdownTask = countdownTask;
countdownTask = start(); countdownTask = start();
@ -344,7 +382,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
try try
{ {
using (var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, skipSource.Token)) using (var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, skipSource.Token))
await Task.Delay(matchCountdownRequest.Duration, cancellationSource.Token).ConfigureAwait(false); await Task.Delay(countdown.TimeRemaining, cancellationSource.Token).ConfigureAwait(false);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@ -362,36 +400,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
if (stopSource.IsCancellationRequested) if (stopSource.IsCancellationRequested)
return; return;
StartMatch().WaitSafely(); continuation().WaitSafely();
}); });
} }
break;
case StopCountdownRequest _:
countdownStopSource?.Cancel();
Room.Countdown = null;
await MatchEvent(new CountdownChangedEvent { Countdown = Room.Countdown });
break;
case ChangeTeamRequest changeTeam:
TeamVersusRoomState roomState = (TeamVersusRoomState)Room.MatchState!;
TeamVersusUserState userState = (TeamVersusUserState)LocalUser.MatchState!;
var targetTeam = roomState.Teams.FirstOrDefault(t => t.ID == changeTeam.TeamID);
if (targetTeam != null)
{
userState.TeamID = targetTeam.ID;
await ((IMultiplayerClient)this).MatchUserStateChanged(LocalUser.UserID, userState).ConfigureAwait(false);
} }
break; private void stopCountdown() => countdownStopSource?.Cancel();
}
}
public override Task StartMatch() public override Task StartMatch()
{ {
@ -427,6 +441,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
await addItem(item).ConfigureAwait(false); await addItem(item).ConfigureAwait(false);
await updateCurrentItem(Room).ConfigureAwait(false); await updateCurrentItem(Room).ConfigureAwait(false);
updateRoomStateIfRequired();
} }
public override Task AddPlaylistItem(MultiplayerPlaylistItem item) => AddUserPlaylistItem(api.LocalUser.Value.OnlineID, item); public override Task AddPlaylistItem(MultiplayerPlaylistItem item) => AddUserPlaylistItem(api.LocalUser.Value.OnlineID, item);
@ -483,6 +498,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
await ((IMultiplayerClient)this).PlaylistItemRemoved(playlistItemId).ConfigureAwait(false); await ((IMultiplayerClient)this).PlaylistItemRemoved(playlistItemId).ConfigureAwait(false);
await updateCurrentItem(Room).ConfigureAwait(false); await updateCurrentItem(Room).ConfigureAwait(false);
updateRoomStateIfRequired();
} }
public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, playlistItemId); public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, playlistItemId);

View File

@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual
[TearDownSteps] [TearDownSteps]
public void TearDownSteps() public void TearDownSteps()
{ {
if (DebugUtils.IsNUnitRunning) if (DebugUtils.IsNUnitRunning && Game != null)
{ {
AddStep("exit game", () => Game.Exit()); AddStep("exit game", () => Game.Exit());
AddUntilStep("wait for game exit", () => Game.Parent == null); AddUntilStep("wait for game exit", () => Game.Parent == null);

View File

@ -115,11 +115,13 @@ namespace osu.Game.Tests.Visual
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{ {
headlessHostStorage = (parent.Get<GameHost>() as HeadlessGameHost)?.Storage; var host = parent.Get<GameHost>();
headlessHostStorage = (host as HeadlessGameHost)?.Storage;
Resources = parent.Get<OsuGameBase>().Resources; Resources = parent.Get<OsuGameBase>().Resources;
realm = new Lazy<RealmAccess>(() => new RealmAccess(LocalStorage, "client")); realm = new Lazy<RealmAccess>(() => new RealmAccess(LocalStorage, OsuGameBase.CLIENT_DATABASE_FILENAME, host.UpdateThread));
RecycleLocalStorage(false); RecycleLocalStorage(false);

View File

@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual
}, },
new OsuSpriteText new OsuSpriteText
{ {
Text = skin?.SkinInfo?.Value.Name ?? "none", Text = skin?.SkinInfo.Value.Name ?? "none",
Scale = new Vector2(1.5f), Scale = new Vector2(1.5f),
Padding = new MarginPadding(5), Padding = new MarginPadding(5),
}, },
@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual
private readonly bool extrapolateAnimations; private readonly bool extrapolateAnimations;
public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage, IStorageResourceProvider resources, bool extrapolateAnimations) public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage, IStorageResourceProvider resources, bool extrapolateAnimations)
: base(skin, storage, resources, "skin.ini") : base(skin, resources, storage)
{ {
this.extrapolateAnimations = extrapolateAnimations; this.extrapolateAnimations = extrapolateAnimations;
} }

View File

@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual
if (autoplayMod != null) if (autoplayMod != null)
{ {
DrawableRuleset?.SetReplayScore(autoplayMod.CreateReplayScore(GameplayState.Beatmap, Mods.Value)); DrawableRuleset?.SetReplayScore(autoplayMod.CreateScoreFromReplayData(GameplayState.Beatmap, Mods.Value));
return; return;
} }

View File

@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual
/// Instantiate a replay player that renders an autoplay mod. /// Instantiate a replay player that renders an autoplay mod.
/// </summary> /// </summary>
public TestReplayPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) public TestReplayPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false)
: base((beatmap, mods) => mods.OfType<ModAutoplay>().First().CreateReplayScore(beatmap, mods), new PlayerConfiguration : base((beatmap, mods) => mods.OfType<ModAutoplay>().First().CreateScoreFromReplayData(beatmap, mods), new PlayerConfiguration
{ {
AllowPause = allowPause, AllowPause = allowPause,
ShowResults = showResults ShowResults = showResults

View File

@ -21,7 +21,7 @@ namespace osu.Game.Users.Drawables
/// </summary> /// </summary>
public bool OpenOnClick public bool OpenOnClick
{ {
set => clickableArea.Enabled.Value = value; set => clickableArea.Enabled.Value = clickableArea.Action != null && value;
} }
/// <summary> /// <summary>
@ -52,8 +52,10 @@ namespace osu.Game.Users.Drawables
Add(clickableArea = new ClickableArea Add(clickableArea = new ClickableArea
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Action = openProfile
}); });
if (user?.Id != APIUser.SYSTEM_USER_ID)
clickableArea.Action = openProfile;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]