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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,10 +5,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Osu.Mods
{
@ -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 Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "Autoplay" } },
Replay = new OsuAutoGenerator(beatmap, mods).Generate()
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
}
}

View File

@ -5,11 +5,9 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Osu.Mods
{
@ -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 Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "Autoplay" } },
Replay = new OsuAutoGenerator(beatmap, mods).Generate()
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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,
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);
});
AddAssert("countdown button not visible", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad);
}
@ -103,7 +102,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
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());
AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
@ -128,63 +133,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
[Test]
public void TestCountdownButtonEnablementAndVisibilityWhileSpectating()
public void TestCountdownWhileSpectating()
{
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
AddAssert("countdown button is visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
AddAssert("countdown button 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" }));
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));
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]
public void TestBecomeHostDuringCountdownAndReady()
{
@ -205,6 +168,31 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null);
}
[Test]
public void TestCountdownButtonVisibilityWithAutoStartEnablement()
{
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
AddUntilStep("countdown button visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }).WaitSafely());
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
AddUntilStep("countdown button not visible", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
}
[Test]
public void TestClickingReadyButtonUnReadiesDuringAutoStart()
{
AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }).WaitSafely());
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle);
}
[Test]
public void TestDeletedBeatmapDisableReady()
{

View File

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

View File

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

View File

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

View File

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

View File

@ -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
{
return new LegacyBeatmapSkin(BeatmapInfo, resources.Files, resources);
return new LegacyBeatmapSkin(BeatmapInfo, resources);
}
catch (Exception e)
{

View File

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

View File

@ -13,6 +13,7 @@ using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Input.Bindings;
@ -211,7 +212,7 @@ namespace osu.Game.Database
if (realm.All<ScoreInfo>().Any())
{
Logger.Log(@"Recovery aborted as the existing database has scores set already.", LoggingTarget.Database);
Logger.Log(@"To perform recovery, delete client.realm while osu! is not running.", LoggingTarget.Database);
Logger.Log($@"To perform recovery, delete {OsuGameBase.CLIENT_DATABASE_FILENAME} while osu! is not running.", LoggingTarget.Database);
return;
}
}
@ -293,7 +294,18 @@ namespace osu.Game.Database
/// Compact this realm.
/// </summary>
/// <returns></returns>
public bool Compact() => Realm.Compact(getConfiguration());
public bool Compact()
{
try
{
return Realm.Compact(getConfiguration());
}
// Catch can be removed along with entity framework. Is specifically to allow a failure message to arrive to the user (see similar catches in EFToRealmMigrator).
catch (AggregateException ae) when (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && ae.Flatten().InnerException is TypeInitializationException)
{
return true;
}
}
/// <summary>
/// Run work on realm with a return value.
@ -542,6 +554,11 @@ namespace osu.Game.Database
return Realm.GetInstance(getConfiguration());
}
// Catch can be removed along with entity framework. Is specifically to allow a failure message to arrive to the user (see similar catches in EFToRealmMigrator).
catch (AggregateException ae) when (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && ae.Flatten().InnerException is TypeInitializationException)
{
return Realm.GetInstance();
}
finally
{
if (tookSemaphoreLock)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -57,6 +57,11 @@ namespace osu.Game
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;
/// <summary>
@ -200,7 +205,7 @@ namespace osu.Game
if (Storage.Exists(DatabaseContextFactory.DATABASE_NAME))
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<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.IO;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.IO;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
@ -16,6 +19,12 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
[Resolved]
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;
public override bool AllowExternalScreenChange => false;
@ -32,8 +41,29 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
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");
}
}
catch (Exception e)
{

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
//},
userButton = new ToolbarUserButton(),
new ToolbarClock(),
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)
{
if (user == APIUser.SYSTEM_USER)
if (user.OnlineID == APIUser.SYSTEM_USER_ID)
return;
Show();

View File

@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Edit
private void regenerateAutoplay()
{
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)

View File

@ -1,14 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Scoring;
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);
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
{
public abstract class ModAutoplay : Mod, IApplicableFailOverride, ICreateReplay
public abstract class ModAutoplay : Mod, IApplicableFailOverride, ICreateReplayData
{
public override string Name => "Autoplay";
public override string Acronym => "AT";
@ -26,10 +26,20 @@ namespace osu.Game.Rulesets.Mods
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;
[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 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 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)
{

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects;
@ -14,8 +16,6 @@ namespace osu.Game.Rulesets.Mods
{
public virtual void ApplyToDrawableRuleset(DrawableRuleset<T> drawableRuleset)
{
drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods));
// AlwaysPresent required for hitsounds
drawableRuleset.AlwaysPresent = true;
drawableRuleset.Hide();
@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => OsuIcon.ModCinema;
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)
{
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>
/// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param>
/// <returns>The bindable containing the total score.</returns>
public Bindable<long> GetBindableTotalScore([NotNull] ScoreInfo score)
{
var bindable = new TotalScoreBindable(score, this);
configManager?.BindWith(OsuSetting.ScoreDisplayMode, bindable.ScoringMode);
return bindable;
}
public Bindable<long> GetBindableTotalScore([NotNull] ScoreInfo score) => new TotalScoreBindable(score, this, configManager);
/// <summary>
/// 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)
{
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>
@ -183,8 +182,7 @@ namespace osu.Game.Scoring
/// </summary>
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 ScoreManager scoreManager;
@ -195,12 +193,14 @@ namespace osu.Game.Scoring
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to provide the total score of.</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.scoreManager = scoreManager;
ScoringMode.BindValueChanged(onScoringModeChanged, true);
configManager?.BindWith(OsuSetting.ScoreDisplayMode, scoringMode);
scoringMode.BindValueChanged(onScoringModeChanged, true);
}
private void onScoringModeChanged(ValueChangedEvent<ScoringMode> mode)

View File

@ -79,10 +79,10 @@ namespace osu.Game.Screens.Menu
private readonly ButtonArea buttonArea;
private readonly Button backButton;
private readonly MainMenuButton backButton;
private readonly List<Button> buttonsTopLevel = new List<Button>();
private readonly List<Button> buttonsPlay = new List<Button>();
private readonly List<MainMenuButton> buttonsTopLevel = new List<MainMenuButton>();
private readonly List<MainMenuButton> buttonsPlay = new List<MainMenuButton>();
private Sample sampleBack;
@ -100,8 +100,8 @@ namespace osu.Game.Screens.Menu
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),
backButton = new Button(ButtonSystemStrings.Back, @"button-back-select", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH)
new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O),
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"button-back-select", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH)
{
VisibleState = ButtonSystemState.Play,
},
@ -126,24 +126,24 @@ namespace osu.Game.Screens.Menu
[BackgroundDependencyLoader(true)]
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 Button(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.Solo, @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P));
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 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);
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 Button(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.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P));
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 MainMenuButton(ButtonSystemStrings.Browse, @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.D));
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(buttonsTopLevel);
buttonArea.ForEach(b =>
{
if (b is Button)
if (b is MainMenuButton)
{
b.Origin = Anchor.CentreLeft;
b.Anchor = Anchor.CentreLeft;
@ -305,7 +305,7 @@ namespace osu.Game.Screens.Menu
{
buttonArea.ButtonSystemState = state;
foreach (var b in buttonArea.Children.OfType<Button>())
foreach (var b in buttonArea.Children.OfType<MainMenuButton>())
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 float[] audioData;
private readonly float[] audioData = new float[256];
private readonly QuadBatch<TexturedVertex2D> vertexBatch = new QuadBatch<TexturedVertex2D>(100, 10);
@ -192,7 +192,8 @@ namespace osu.Game.Screens.Menu
shader = Source.shader;
texture = Source.texture;
size = Source.DrawSize.X;
audioData = Source.frequencyAmplitudes;
Source.frequencyAmplitudes.AsSpan().CopyTo(audioData);
}
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.
/// In order to correctly flow, we have to use a negative margin on the parent container (due to the parallelogram shape).
/// </summary>
public class Button : BeatSyncedContainer, IStateful<ButtonState>
public class MainMenuButton : BeatSyncedContainer, IStateful<ButtonState>
{
public event Action<ButtonState> StateChanged;
@ -51,7 +51,7 @@ namespace osu.Game.Screens.Menu
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.clickAction = clickAction;
@ -209,7 +209,7 @@ namespace osu.Game.Screens.Menu
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;
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.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
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()
{

View File

@ -30,7 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private Sample sampleReadyAll;
private Sample sampleUnready;
private readonly BindableBool enabled = new BindableBool();
private readonly MultiplayerReadyButton readyButton;
private readonly MultiplayerCountdownButton countdownButton;
private int countReady;
private ScheduledDelegate readySampleDelegate;
@ -50,12 +50,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
new Drawable[]
{
new MultiplayerReadyButton
readyButton = new MultiplayerReadyButton
{
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
Action = onReadyClick,
Enabled = { BindTarget = enabled },
},
countdownButton = new MultiplayerCountdownButton
{
@ -63,7 +62,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Size = new Vector2(40, 1),
Alpha = 0,
Action = startCountdown,
Enabled = { BindTarget = enabled }
CancelAction = cancelCountdown
}
}
}
@ -108,30 +107,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Debug.Assert(clickOperation == null);
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)
{
if (isReady() && Client.IsHost && Room.Countdown == null)
startMatch();
else
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();
bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating;
void toggleReady() => Client.ToggleReady().ContinueWith(_ => endOperation());
void stopCountdown() => Client.SendMatchRequest(new StopCountdownRequest()).ContinueWith(_ => endOperation());
void startMatch() => Client.StartMatch().ContinueWith(t =>
{
// 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());
}
private void cancelCountdown()
{
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
Client.SendMatchRequest(new StopCountdownRequest()).ContinueWith(_ => endOperation());
}
private void endOperation()
{
clickOperation?.Dispose();
@ -163,7 +155,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
if (Room == null)
{
enabled.Value = false;
readyButton.Enabled.Value = false;
countdownButton.Enabled.Value = false;
return;
}
@ -172,32 +165,33 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
if (Room.Countdown != null)
countdownButton.Alpha = 0;
if (!Client.IsHost || Room.Settings.AutoStartEnabled)
countdownButton.Hide();
else
{
switch (localUser?.State)
{
default:
countdownButton.Alpha = 0;
countdownButton.Hide();
break;
case MultiplayerUserState.Idle:
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
countdownButton.Alpha = Room.Host?.Equals(localUser) == true ? 1 : 0;
countdownButton.Show();
break;
}
}
enabled.Value =
readyButton.Enabled.Value = countdownButton.Enabled.Value =
Room.State == MultiplayerRoomState.Open
&& CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId
&& !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired
&& !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)
enabled.Value &= Room.Host?.Equals(localUser) == true && newCountReady > 0;
readyButton.Enabled.Value &= Client.IsHost && newCountReady > 0 && Room.Countdown == null;
if (newCountReady == countReady)
return;

View File

@ -14,6 +14,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.Multiplayer;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
@ -30,12 +31,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
public new Action<TimeSpan> Action;
public Action CancelAction;
[Resolved]
private MultiplayerClient multiplayerClient { get; set; }
[Resolved]
private OsuColour colours { get; set; }
private readonly Drawable background;
public MultiplayerCountdownButton()
{
Icon = FontAwesome.Solid.CaretDown;
IconScale = new Vector2(0.6f);
Icon = FontAwesome.Regular.Clock;
Add(background = new Box
{
@ -44,6 +52,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
});
base.Action = this.ShowPopover;
TooltipText = "Countdown settings";
}
[BackgroundDependencyLoader]
@ -52,6 +62,38 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
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()
{
var flow = new FillFlowContainer
@ -68,7 +110,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
RelativeSizeAxes = Axes.X,
Text = $"Start match in {duration.Humanize()}",
BackgroundColour = background.Colour,
BackgroundColour = colours.Green,
Action = () =>
{
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 };
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.ComponentModel;
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Allocation;
@ -21,6 +22,7 @@ using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osuTK;
using Container = osu.Framework.Graphics.Containers.Container;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
@ -56,7 +58,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
public Action SettingsApplied;
public OsuTextBox NameField, MaxParticipantsField;
public RoomAvailabilityPicker AvailabilityPicker;
public MatchTypePicker TypePicker;
public OsuEnumDropdown<QueueMode> QueueModeDropdown;
public OsuTextBox PasswordTextBox;
@ -64,6 +65,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
public OsuSpriteText ErrorText;
private OsuEnumDropdown<StartMode> startModeDropdown;
private OsuSpriteText typeLabel;
private LoadingLayer loadingLayer;
@ -163,14 +165,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
LengthLimit = 100,
},
},
new Section("Room visibility")
{
Alpha = disabled_alpha,
Child = AvailabilityPicker = new RoomAvailabilityPicker
{
Enabled = { Value = false }
},
},
// new Section("Room visibility")
// {
// Alpha = disabled_alpha,
// Child = AvailabilityPicker = new RoomAvailabilityPicker
// {
// Enabled = { Value = false }
// },
// },
new Section("Game type")
{
Child = new FillFlowContainer
@ -204,6 +206,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
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);
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);
MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true);
RoomID.BindValueChanged(roomId => playlistContainer.Alpha = roomId.NewValue == null ? 1 : 0, true);
Password.BindValueChanged(password => PasswordTextBox.Text = password.NewValue ?? string.Empty, 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.BindValueChanged(v =>
@ -363,6 +377,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Debug.Assert(applyingSettingsOperation == null);
applyingSettingsOperation = ongoingOperationTracker.BeginOperation();
TimeSpan autoStartDuration = TimeSpan.FromSeconds((int)startModeDropdown.Current.Value);
// 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.
if (client.Room != null)
@ -371,7 +387,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
name: NameField.Text,
password: PasswordTextBox.Text,
matchType: TypePicker.Current.Value,
queueMode: QueueModeDropdown.Current.Value)
queueMode: QueueModeDropdown.Current.Value,
autoStartDuration: autoStartDuration)
.ContinueWith(t => Schedule(() =>
{
if (t.IsCompletedSuccessfully)
@ -383,10 +400,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
else
{
room.Name.Value = NameField.Text;
room.Availability.Value = AvailabilityPicker.Current.Value;
room.Type.Value = TypePicker.Current.Value;
room.Password.Value = PasswordTextBox.Current.Value;
room.QueueMode.Value = QueueModeDropdown.Current.Value;
room.AutoStartDuration.Value = autoStartDuration;
if (int.TryParse(MaxParticipantsField.Text, out int max))
room.MaxParticipants.Value = max;
@ -452,5 +469,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
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 DateTimeOffset countdownReceivedTime;
private DateTimeOffset countdownChangeTime;
private ScheduledDelegate countdownUpdateDelegate;
private void onRoomUpdated() => Scheduler.AddOnce(() =>
{
if (countdown == null && room?.Countdown != null)
countdownReceivedTime = DateTimeOffset.Now;
if (countdown != room?.Countdown)
{
countdown = room?.Countdown;
countdownChangeTime = DateTimeOffset.Now;
}
countdown = room?.Countdown;
if (room?.Countdown != null)
countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 1000, true);
if (countdown != null)
countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 100, true);
else
{
countdownUpdateDelegate?.Cancel();
@ -74,7 +75,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
if (countdown != null)
{
TimeSpan timeElapsed = DateTimeOffset.Now - countdownReceivedTime;
TimeSpan timeElapsed = DateTimeOffset.Now - countdownChangeTime;
TimeSpan countdownRemaining;
if (timeElapsed > countdown.TimeRemaining)
@ -168,7 +169,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
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 base.TooltipText;

View File

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

View File

@ -198,15 +198,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
else
userModsDisplay.FadeOut(fade_time);
if (Client.IsHost && !User.Equals(Client.LocalUser))
kickButton.FadeIn(fade_time);
else
kickButton.FadeOut(fade_time);
if (Room.Host?.Equals(User) == true)
crown.FadeIn(fade_time);
else
crown.FadeOut(fade_time);
kickButton.Alpha = Client.IsHost && !User.Equals(Client.LocalUser) ? 1 : 0;
crown.Alpha = Room.Host?.Equals(User) == true ? 1 : 0;
// 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.

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -15,6 +16,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
private FillFlowContainer<ParticipantPanel> panels;
[CanBeNull]
private ParticipantPanel currentHostPanel;
[BackgroundDependencyLoader]
private void load()
{
@ -55,6 +59,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
// Add panels for all users new to the room.
foreach (var user in Room.Users.Except(panels.Select(p => p.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))]
protected Bindable<QueueMode> QueueMode { get; private set; }
[Resolved(typeof(Room))]
protected Bindable<TimeSpan> AutoStartDuration { get; private set; }
[Resolved(CanBeNull = true)]
private IBindable<PlaylistItem> subScreenSelectedItem { get; set; }

View File

@ -21,7 +21,9 @@ namespace osu.Game.Screens.Play.HUD
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;
@ -65,32 +67,28 @@ namespace osu.Game.Screens.Play.HUD
Margin = new MarginPadding(10);
Scale = new Vector2(1.2f);
Scale = new Vector2(1.28f);
InternalChildren = new[]
{
counterContainer = new Container
{
AutoSizeAxes = Axes.Both,
AlwaysPresent = true,
Children = new[]
{
popOutCount = new LegacySpriteText(LegacyFont.Combo)
{
Alpha = 0,
Margin = new MarginPadding(0.05f),
Blending = BlendingParameters.Additive,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
BypassAutoSizeAxes = Axes.Both,
},
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,
AlwaysPresent = true,
Anchor = Anchor.BottomLeft,
BypassAutoSizeAxes = Axes.Both,
},
}
}
@ -130,8 +128,25 @@ namespace osu.Game.Screens.Play.HUD
base.LoadComplete();
((IHasText)displayedCountSpriteText).Text = formatCount(Current.Value);
((IHasText)popOutCount).Text = formatCount(Current.Value);
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)
@ -164,27 +179,31 @@ namespace osu.Game.Screens.Play.HUD
{
((IHasText)popOutCount).Text = formatCount(newValue);
popOutCount.ScaleTo(1.6f);
popOutCount.FadeTo(0.75f);
popOutCount.MoveTo(Vector2.Zero);
popOutCount.ScaleTo(1.56f)
.ScaleTo(1, big_pop_out_duration);
popOutCount.ScaleTo(1, pop_out_duration);
popOutCount.FadeOut(pop_out_duration);
popOutCount.MoveTo(displayedCountSpriteText.Position, pop_out_duration);
popOutCount.FadeTo(0.6f)
.FadeOut(big_pop_out_duration);
}
private void transformNoPopOut(int newValue)
{
((IHasText)displayedCountSpriteText).Text = formatCount(newValue);
counterContainer.Size = displayedCountSpriteText.Size;
displayedCountSpriteText.ScaleTo(1);
}
private void transformPopOutSmall(int 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)
@ -212,7 +231,7 @@ namespace osu.Game.Screens.Play.HUD
Scheduler.AddDelayed(delegate
{
scheduledPopOutSmall(newTaskId);
}, pop_out_duration);
}, big_pop_out_duration - 140);
}
private void onCountRolling(int currentValue, int newValue)

View File

@ -287,12 +287,14 @@ namespace osu.Game.Screens.Select
{
TitleLabel = new OsuSpriteText
{
Current = { BindTarget = titleBinding },
Font = OsuFont.GetFont(size: 28, italics: true),
RelativeSizeAxes = Axes.X,
Truncate = true,
},
ArtistLabel = new OsuSpriteText
{
Current = { BindTarget = artistBinding },
Font = OsuFont.GetFont(size: 17, italics: true),
RelativeSizeAxes = Axes.X,
Truncate = true,
@ -314,9 +316,6 @@ namespace osu.Game.Screens.Select
}
};
titleBinding.BindValueChanged(_ => setMetadata(metadata.Source));
artistBinding.BindValueChanged(_ => setMetadata(metadata.Source), true);
addInfoLabels();
}
@ -352,12 +351,6 @@ namespace osu.Game.Screens.Select
}, true);
}
private void setMetadata(string source)
{
ArtistLabel.Text = artistBinding.Value;
TitleLabel.Text = string.IsNullOrEmpty(source) ? titleBinding.Value : source + " — " + titleBinding.Value;
}
private void addInfoLabels()
{
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.
// 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.Graphics.Sprites;
@ -14,13 +15,13 @@ using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Users;
using osu.Game.Utils;
using osuTK.Input;
namespace osu.Game.Screens.Select
{
public class PlaySongSelect : SongSelect
{
private bool removeAutoModOnResume;
private OsuScreen playerLoader;
[Resolved(CanBeNull = true)]
@ -43,25 +44,6 @@ namespace osu.Game.Screens.Select
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)
{
switch (e.Key)
@ -77,10 +59,16 @@ namespace osu.Game.Screens.Select
return base.OnKeyDown(e);
}
private IReadOnlyList<Mod> modsAtGameplayStart;
private ModAutoplay getAutoplayMod() => Ruleset.Value.CreateInstance().GetAutoplayMod();
protected override bool OnStart()
{
if (playerLoader != null) return false;
modsAtGameplayStart = Mods.Value;
// Ctrl+Enter should start map with autoplay enabled.
if (GetContainingInputManager().CurrentState?.Keyboard.ControlPressed == true)
{
@ -95,13 +83,12 @@ namespace osu.Game.Screens.Select
return false;
}
var mods = Mods.Value;
var mods = Mods.Value.Append(autoInstance).ToArray();
if (mods.All(m => m.GetType() != autoInstance.GetType()))
{
Mods.Value = mods.Append(autoInstance).ToArray();
removeAutoModOnResume = true;
}
if (!ModUtils.CheckCompatibleSet(mods, out var invalid))
mods = mods.Except(invalid).Append(autoInstance).ToArray();
Mods.Value = mods;
}
SampleConfirm?.Play();
@ -111,12 +98,26 @@ namespace osu.Game.Screens.Select
Player createPlayer()
{
var replayGeneratingMod = Mods.Value.OfType<ICreateReplay>().FirstOrDefault();
var replayGeneratingMod = Mods.Value.OfType<ICreateReplayData>().FirstOrDefault();
if (replayGeneratingMod != null)
return new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateReplayScore(beatmap, mods));
{
return new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods));
}
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)
: base(
skin,
new NamespacedResourceStore<byte[]>(resources.Resources, "Skins/Legacy"),
resources,
// A default legacy skin may still have a skin.ini if it is modified by the user.
// We must specify the stream directly as we are redirecting storage to the osu-resources location for other files.
new LegacyDatabasedSkinResourceStore(skin, resources.Files).GetStream("skin.ini")
// 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.
skin.Protected ? new NamespacedResourceStore<byte[]>(resources.Resources, "Skins/Legacy") : null
)
{
Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255);

View File

@ -42,6 +42,9 @@ namespace osu.Game.Skinning.Editor
[Resolved]
private OsuColour colours { get; set; }
[Resolved(canBeNull: true)]
private SkinEditorOverlay skinEditorOverlay { get; set; }
[Cached]
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("Revert to default", MenuItemType.Destructive, revert),
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.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play;
using osu.Game.Screens.Select;
using osuTK;
@ -94,7 +95,7 @@ namespace osu.Game.Skinning.Editor
var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod();
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) })
},
}

View File

@ -1,7 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
#nullable enable
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -21,16 +22,14 @@ namespace osu.Game.Skinning
/// </summary>
/// <param name="component">The requested component.</param>
/// <returns>A drawable representation for the requested component, or null if unavailable.</returns>
[CanBeNull]
Drawable GetDrawableComponent(ISkinComponent component);
Drawable? GetDrawableComponent(ISkinComponent component);
/// <summary>
/// Retrieve a <see cref="Texture"/>.
/// </summary>
/// <param name="componentName">The requested texture.</param>
/// <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>
/// 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="wrapModeT">The texture wrap mode in vertical direction.</param>
/// <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>
/// Retrieve a <see cref="SampleChannel"/>.
/// </summary>
/// <param name="sampleInfo">The requested sample.</param>
/// <returns>A matching sample channel, or null if unavailable.</returns>
[CanBeNull]
ISample GetSample(ISampleInfo sampleInfo);
ISample? GetSample(ISampleInfo sampleInfo);
/// <summary>
/// Retrieve a configuration value.
/// </summary>
/// <param name="lookup">The requested configuration value.</param>
/// <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.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.IO.Stores;
using osu.Game.Audio;
@ -20,14 +23,28 @@ namespace osu.Game.Skinning
protected override bool AllowManiaSkin => false;
protected override bool UseCustomSampleBanks => true;
public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, IResourceStore<byte[]> storage, IStorageResourceProvider resources)
: base(createSkinInfo(beatmapInfo), new LegacySkinResourceStore(beatmapInfo.BeatmapSet, storage), resources, beatmapInfo.Path)
/// <summary>
/// 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)
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)
{
@ -46,7 +63,7 @@ namespace osu.Game.Skinning
return base.GetDrawableComponent(component);
}
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
{
switch (lookup)
{
@ -62,10 +79,10 @@ namespace osu.Game.Skinning
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);
public override ISample GetSample(ISampleInfo sampleInfo)
public override ISample? GetSample(ISampleInfo sampleInfo)
{
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy && legacy.CustomSampleBank == 0)
{
@ -77,6 +94,10 @@ namespace osu.Game.Skinning
}
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.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
@ -15,6 +17,7 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Game.Audio;
using osu.Game.Beatmaps.Formats;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
@ -27,12 +30,6 @@ namespace osu.Game.Skinning
{
public class LegacySkin : Skin
{
[CanBeNull]
protected TextureStore Textures;
[CanBeNull]
protected ISampleStore Samples;
/// <summary>
/// Whether texture for the keys exists.
/// Used to determine if the mania ruleset is skinned.
@ -51,7 +48,7 @@ namespace osu.Game.Skinning
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
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.
/// </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="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>
protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore<byte[]> storage, [CanBeNull] IStorageResourceProvider resources, string configurationFilename)
: this(skin, storage, resources, string.IsNullOrEmpty(configurationFilename) ? null : storage?.GetStream(configurationFilename))
protected LegacySkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? storage, string configurationFilename = @"skin.ini")
: 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.
hasKeyTexture = new Lazy<bool>(() => this.GetAnimation(
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)
{
@ -156,7 +129,7 @@ namespace osu.Game.Skinning
return null;
}
private IBindable<TValue> lookupForMania<TValue>(LegacyManiaSkinConfigurationLookup maniaLookup)
private IBindable<TValue>? lookupForMania<TValue>(LegacyManiaSkinConfigurationLookup maniaLookup)
{
if (!maniaConfigurations.TryGetValue(maniaLookup.Keys, out var existing))
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="colourIndex">The preferred index for retrieving the combo colour with.</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];
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;
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;
[CanBeNull]
private IBindable<TValue> legacySettingLookup<TValue>(SkinConfiguration.LegacySetting legacySetting)
private IBindable<TValue>? legacySettingLookup<TValue>(SkinConfiguration.LegacySetting legacySetting)
where TValue : notnull
{
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
{
@ -345,7 +319,7 @@ namespace osu.Game.Skinning
return null;
}
public override Drawable GetDrawableComponent(ISkinComponent component)
public override Drawable? GetDrawableComponent(ISkinComponent component)
{
if (base.GetDrawableComponent(component) is Drawable c)
return c;
@ -385,26 +359,15 @@ namespace osu.Game.Skinning
}
})
{
Children = this.HasFont(LegacyFont.Score)
? new Drawable[]
{
new LegacyComboCounter(),
new LegacyScoreCounter(),
new LegacyAccuracyCounter(),
new LegacyHealthDisplay(),
new SongProgress(),
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(),
}
Children = new Drawable[]
{
new LegacyComboCounter(),
new LegacyScoreCounter(),
new LegacyAccuracyCounter(),
new LegacyHealthDisplay(),
new SongProgress(),
new BarHitErrorMeter(),
}
};
return skinnableTargetWrapper;
@ -414,7 +377,7 @@ namespace osu.Game.Skinning
case GameplaySkinComponent<HitResult> resultComponent:
// 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.
if (createDrawable() != null)
@ -433,7 +396,7 @@ namespace osu.Game.Skinning
return this.GetAnimation(component.LookupName, false, false);
}
private Texture getParticleTexture(HitResult result)
private Texture? getParticleTexture(HitResult result)
{
switch (result)
{
@ -450,7 +413,7 @@ namespace osu.Game.Skinning
return null;
}
private Drawable getJudgementAnimation(HitResult result)
private Drawable? getJudgementAnimation(HitResult result)
{
switch (result)
{
@ -470,7 +433,7 @@ namespace osu.Game.Skinning
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))
{
@ -498,7 +461,7 @@ namespace osu.Game.Skinning
return null;
}
public override ISample GetSample(ISampleInfo sampleInfo)
public override ISample? GetSample(ISampleInfo sampleInfo)
{
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").
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 osu.Framework.Extensions;
using osu.Framework.IO.Stores;
using osu.Game.Database;
using osu.Game.Extensions;
namespace osu.Game.Skinning
{
public class LegacyDatabasedSkinResourceStore : ResourceStore<byte[]>
public class RealmBackedResourceStore : ResourceStore<byte[]>
{
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)
{
// Must be initialised before the file cache.
if (extensions != null)
{
foreach (string extension in extensions)
AddExtension(extension);
}
initialiseFileCache(source);
}
private void initialiseFileCache(SkinInfo source)
private void initialiseFileCache(IHasRealmFiles source)
{
fileToStoragePathMapping.Clear();
foreach (var f in source.Files)

View File

@ -46,7 +46,10 @@ namespace osu.Game.Skinning
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()
{

View File

@ -1,22 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
using osu.Game.Audio;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.IO;
using osu.Game.Screens.Play.HUD;
@ -24,8 +26,17 @@ namespace osu.Game.Skinning
{
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;
private readonly IStorageResourceProvider resources;
public SkinConfiguration Configuration { get; set; }
@ -33,66 +44,80 @@ namespace osu.Game.Skinning
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
? skin.ToLive(resources.RealmAccess)
// This path should only be used in some tests.
: skin.ToLiveUnmanaged();
if (resources != null)
{
SkinInfo = skin.ToLive(resources.RealmAccess);
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)
{
// stream will be closed after use by LineBufferedReader.
ParseConfigurationStream(configurationStream);
Debug.Assert(Configuration != null);
}
else
Configuration = new SkinConfiguration();
// skininfo files may be null for default skin.
SkinInfo.PerformRead(s =>
foreach (SkinnableTarget skinnableTarget in Enum.GetValues(typeof(SkinnableTarget)))
{
// we may want to move this to some kind of async operation in the future.
foreach (SkinnableTarget skinnableTarget in Enum.GetValues(typeof(SkinnableTarget)))
string filename = $"{skinnableTarget}.json";
byte[]? bytes = storage?.Get(filename);
if (bytes == null)
continue;
try
{
string filename = $"{skinnableTarget}.json";
string jsonContent = Encoding.UTF8.GetString(bytes);
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SkinnableInfo>>(jsonContent);
// skininfo files may be null for default skin.
var fileInfo = s.Files.FirstOrDefault(f => f.Filename == filename);
if (fileInfo == null)
if (deserializedContent == null)
continue;
byte[] bytes = resources?.Files.Get(fileInfo.File.GetStoragePath());
if (bytes == null)
continue;
try
{
string jsonContent = Encoding.UTF8.GetString(bytes);
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SkinnableInfo>>(jsonContent);
if (deserializedContent == null)
continue;
DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray();
}
catch (Exception ex)
{
Logger.Error(ex, "Failed to load skin configuration.");
}
DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray();
}
});
catch (Exception ex)
{
Logger.Error(ex, "Failed to load skin configuration.");
}
}
}
protected virtual void ParseConfigurationStream(Stream stream)
@ -101,16 +126,6 @@ namespace osu.Game.Skinning
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>
/// Remove all stored customisations for the provided target.
/// </summary>
@ -129,7 +144,7 @@ namespace osu.Game.Skinning
DrawableComponentInfo[targetContainer.Target] = targetContainer.CreateSkinnableInfo().ToArray();
}
public virtual Drawable GetDrawableComponent(ISkinComponent component)
public virtual Drawable? GetDrawableComponent(ISkinComponent component)
{
switch (component)
{
@ -137,9 +152,23 @@ namespace osu.Game.Skinning
if (!DrawableComponentInfo.TryGetValue(target.Target, out var skinnableInfo))
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
{
ChildrenEnumerable = skinnableInfo.Select(i => i.CreateInstance())
Children = components,
};
}
@ -168,6 +197,9 @@ namespace osu.Game.Skinning
return;
isDisposed = true;
Textures?.Dispose();
Samples?.Dispose();
}
#endregion

View File

@ -96,12 +96,14 @@ namespace osu.Game.Tests.Beatmaps
AddStep("setup skins", () =>
{
userSkinInfo.Files.Clear();
userSkinInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = userFile }, userFile));
if (!string.IsNullOrEmpty(userFile))
userSkinInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = userFile }, userFile));
Debug.Assert(beatmapInfo.BeatmapSet != null);
beatmapInfo.BeatmapSet.Files.Clear();
beatmapInfo.BeatmapSet.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = beatmapFile }, beatmapFile));
if (!string.IsNullOrEmpty(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.
dependencies.SkinSource = new SkinProvidingContainer(Skin = new LegacySkin(userSkinInfo, this));
@ -191,22 +193,32 @@ namespace osu.Game.Tests.Beatmaps
}
}
private class TestWorkingBeatmap : ClockBackedTestWorkingBeatmap
private class TestWorkingBeatmap : ClockBackedTestWorkingBeatmap, IStorageResourceProvider
{
private readonly BeatmapInfo skinBeatmapInfo;
private readonly IResourceStore<byte[]> resourceStore;
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)
{
this.skinBeatmapInfo = skinBeatmapInfo;
this.resourceStore = resourceStore;
Files = accessMarkingResourceStore;
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.Audio;
using osu.Framework.Bindables;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Skinning;
@ -112,7 +111,7 @@ namespace osu.Game.Tests.Beatmaps
public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.DarkGoldenrod;
public TestBeatmapSkin(BeatmapInfo beatmapInfo, bool hasColours)
: base(beatmapInfo, new ResourceStore<byte[]>(), null)
: base(beatmapInfo, null)
{
if (hasColours)
{
@ -141,7 +140,7 @@ namespace osu.Game.Tests.Beatmaps
public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.LightCyan;
public TestSkin(bool hasCustomColours)
: base(new SkinInfo(), new ResourceStore<byte[]>(), null, string.Empty)
: base(new SkinInfo(), null, null)
{
if (hasCustomColours)
{

View File

@ -119,6 +119,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
Debug.Assert(Room != null);
((IMultiplayerClient)this).UserStateChanged(userId, newState);
updateRoomStateIfRequired();
}
private void updateRoomStateIfRequired()
{
Debug.Assert(Room != null);
Debug.Assert(APIRoom != null);
Schedule(() =>
{
@ -126,13 +133,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
case MultiplayerRoomState.Open:
// 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.
bool shouldStopCountdown = Room.Users.All(u => u.State != MultiplayerUserState.Ready);
shouldStopCountdown |= Room.Host?.State != MultiplayerUserState.Ready && Room.Host?.State != MultiplayerUserState.Spectating;
if (Room.Settings.AutoStartEnabled)
{
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;
case MultiplayerRoomState.WaitingForLoad:
@ -204,7 +213,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
Name = apiRoom.Name.Value,
MatchType = apiRoom.Type.Value,
Password = password,
QueueMode = apiRoom.QueueMode.Value
QueueMode = apiRoom.QueueMode.Value,
AutoStartDuration = apiRoom.AutoStartDuration.Value
},
Playlist = serverSidePlaylist.ToList(),
Users = { localUser },
@ -263,6 +273,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
ChangeUserState(user.UserID, MultiplayerUserState.Idle);
await changeMatchType(settings.MatchType).ConfigureAwait(false);
updateRoomStateIfRequired();
}
public override Task ChangeState(MultiplayerUserState newState)
@ -315,64 +326,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
switch (request)
{
case StartMatchCountdownRequest matchCountdownRequest:
Debug.Assert(ThreadSafety.IsUpdateThread);
countdownStopSource?.Cancel();
// 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.
var stopSource = countdownStopSource = new CancellationTokenSource();
var skipSource = countdownSkipSource = new CancellationTokenSource();
var countdown = new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Duration };
Task lastCountdownTask = countdownTask;
countdownTask = start();
async Task start()
{
await lastCountdownTask;
Schedule(() =>
{
if (stopSource.IsCancellationRequested)
return;
Room.Countdown = countdown;
MatchEvent(new CountdownChangedEvent { Countdown = countdown });
});
try
{
using (var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, skipSource.Token))
await Task.Delay(matchCountdownRequest.Duration, cancellationSource.Token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Clients need to be notified of cancellations in the following code.
}
Schedule(() =>
{
if (Room.Countdown != countdown)
return;
Room.Countdown = null;
MatchEvent(new CountdownChangedEvent { Countdown = null });
if (stopSource.IsCancellationRequested)
return;
StartMatch().WaitSafely();
});
}
startCountdown(new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Duration }, StartMatch);
break;
case StopCountdownRequest _:
countdownStopSource?.Cancel();
Room.Countdown = null;
await MatchEvent(new CountdownChangedEvent { Countdown = Room.Countdown });
stopCountdown();
break;
case ChangeTeamRequest changeTeam:
@ -393,6 +351,62 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
}
private void startCountdown(MultiplayerCountdown countdown, Func<Task> continuation)
{
Debug.Assert(Room != null);
Debug.Assert(ThreadSafety.IsUpdateThread);
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.
// 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 skipSource = countdownSkipSource = new CancellationTokenSource();
Task lastCountdownTask = countdownTask;
countdownTask = start();
async Task start()
{
await lastCountdownTask;
Schedule(() =>
{
if (stopSource.IsCancellationRequested)
return;
Room.Countdown = countdown;
MatchEvent(new CountdownChangedEvent { Countdown = countdown });
});
try
{
using (var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, skipSource.Token))
await Task.Delay(countdown.TimeRemaining, cancellationSource.Token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Clients need to be notified of cancellations in the following code.
}
Schedule(() =>
{
if (Room.Countdown != countdown)
return;
Room.Countdown = null;
MatchEvent(new CountdownChangedEvent { Countdown = null });
if (stopSource.IsCancellationRequested)
return;
continuation().WaitSafely();
});
}
}
private void stopCountdown() => countdownStopSource?.Cancel();
public override Task StartMatch()
{
Debug.Assert(Room != null);
@ -427,6 +441,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
await addItem(item).ConfigureAwait(false);
await updateCurrentItem(Room).ConfigureAwait(false);
updateRoomStateIfRequired();
}
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 updateCurrentItem(Room).ConfigureAwait(false);
updateRoomStateIfRequired();
}
public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, playlistItemId);

View File

@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual
[TearDownSteps]
public void TearDownSteps()
{
if (DebugUtils.IsNUnitRunning)
if (DebugUtils.IsNUnitRunning && Game != null)
{
AddStep("exit game", () => Game.Exit());
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)
{
headlessHostStorage = (parent.Get<GameHost>() as HeadlessGameHost)?.Storage;
var host = parent.Get<GameHost>();
headlessHostStorage = (host as HeadlessGameHost)?.Storage;
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);

View File

@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual
},
new OsuSpriteText
{
Text = skin?.SkinInfo?.Value.Name ?? "none",
Text = skin?.SkinInfo.Value.Name ?? "none",
Scale = new Vector2(1.5f),
Padding = new MarginPadding(5),
},
@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual
private readonly 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;
}

View File

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

View File

@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual
/// Instantiate a replay player that renders an autoplay mod.
/// </summary>
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,
ShowResults = showResults

View File

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