mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 15:22:55 +08:00
Merge branch 'master' into mod-overlay/full-screen
This commit is contained in:
commit
9c91e6d8f5
@ -51,7 +51,7 @@
|
|||||||
<Reference Include="Java.Interop" />
|
<Reference Include="Java.Interop" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.405.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.405.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.405.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Transitive Dependencies">
|
<ItemGroup Label="Transitive Dependencies">
|
||||||
|
@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
|
||||||
|
|
||||||
[TestCase(4.0505463516206195d, "diffcalc-test")]
|
[TestCase(4.0505463516206195d, 127, "diffcalc-test")]
|
||||||
public void Test(double expected, string name)
|
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> base.Test(expected, name);
|
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||||
|
|
||||||
[TestCase(5.1696411260785498d, "diffcalc-test")]
|
[TestCase(5.1696411260785498d, 127, "diffcalc-test")]
|
||||||
public void TestClockRateAdjusted(double expected, string name)
|
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expected, name, new CatchModDoubleTime());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new CatchModDoubleTime());
|
||||||
|
|
||||||
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset().RulesetInfo, beatmap);
|
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset().RulesetInfo, beatmap);
|
||||||
|
|
||||||
|
@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
|
||||||
|
|
||||||
[TestCase(2.3449735700206298d, "diffcalc-test")]
|
[TestCase(2.3449735700206298d, 151, "diffcalc-test")]
|
||||||
public void Test(double expected, string name)
|
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> base.Test(expected, name);
|
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||||
|
|
||||||
[TestCase(2.7879104989252959d, "diffcalc-test")]
|
[TestCase(2.7879104989252959d, 151, "diffcalc-test")]
|
||||||
public void TestClockRateAdjusted(double expected, string name)
|
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expected, name, new ManiaModDoubleTime());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime());
|
||||||
|
|
||||||
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset().RulesetInfo, beatmap);
|
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset().RulesetInfo, beatmap);
|
||||||
|
|
||||||
|
@ -15,15 +15,20 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
|
||||||
|
|
||||||
[TestCase(6.6972307565739273d, "diffcalc-test")]
|
[TestCase(6.6972307565739273d, 206, "diffcalc-test")]
|
||||||
[TestCase(1.4484754139145539d, "zero-length-sliders")]
|
[TestCase(1.4484754139145539d, 45, "zero-length-sliders")]
|
||||||
public void Test(double expected, string name)
|
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> base.Test(expected, name);
|
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||||
|
|
||||||
[TestCase(8.9382559208689809d, "diffcalc-test")]
|
[TestCase(8.9382559208689809d, 206, "diffcalc-test")]
|
||||||
[TestCase(1.7548875851757628d, "zero-length-sliders")]
|
[TestCase(1.7548875851757628d, 45, "zero-length-sliders")]
|
||||||
public void TestClockRateAdjusted(double expected, string name)
|
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expected, name, new OsuModDoubleTime());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
||||||
|
|
||||||
|
[TestCase(6.6972307218715166d, 239, "diffcalc-test")]
|
||||||
|
[TestCase(1.4484754139145537d, 54, "zero-length-sliders")]
|
||||||
|
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
|
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
||||||
|
|
||||||
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap);
|
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap);
|
||||||
|
|
||||||
|
@ -61,10 +61,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||||
double drainRate = beatmap.Difficulty.DrainRate;
|
double drainRate = beatmap.Difficulty.DrainRate;
|
||||||
|
int maxCombo = beatmap.GetMaxCombo();
|
||||||
int maxCombo = beatmap.HitObjects.Count;
|
|
||||||
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
|
|
||||||
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
|
|
||||||
|
|
||||||
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
|
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
|
||||||
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
||||||
|
@ -14,15 +14,15 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
||||||
|
|
||||||
[TestCase(2.2420075288523802d, "diffcalc-test")]
|
[TestCase(2.2420075288523802d, 200, "diffcalc-test")]
|
||||||
[TestCase(2.2420075288523802d, "diffcalc-test-strong")]
|
[TestCase(2.2420075288523802d, 200, "diffcalc-test-strong")]
|
||||||
public void Test(double expected, string name)
|
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> base.Test(expected, name);
|
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||||
|
|
||||||
[TestCase(3.134084469440479d, "diffcalc-test")]
|
[TestCase(3.134084469440479d, 200, "diffcalc-test")]
|
||||||
[TestCase(3.134084469440479d, "diffcalc-test-strong")]
|
[TestCase(3.134084469440479d, 200, "diffcalc-test-strong")]
|
||||||
public void TestClockRateAdjusted(double expected, string name)
|
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expected, name, new TaikoModDoubleTime());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
|
||||||
|
|
||||||
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset().RulesetInfo, beatmap);
|
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset().RulesetInfo, beatmap);
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default size of a drawable taiko hit object.
|
/// Default size of a drawable taiko hit object.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const float DEFAULT_SIZE = 0.45f;
|
public const float DEFAULT_SIZE = 0.475f;
|
||||||
|
|
||||||
public override Judgement CreateJudgement() => new TaikoJudgement();
|
public override Judgement CreateJudgement() => new TaikoJudgement();
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Scale multiplier for a strong drawable taiko hit object.
|
/// Scale multiplier for a strong drawable taiko hit object.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const float STRONG_SCALE = 1.4f;
|
public const float STRONG_SCALE = 1 / 0.65f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default size of a strong drawable taiko hit object.
|
/// Default size of a strong drawable taiko hit object.
|
||||||
|
@ -11,6 +11,7 @@ using osu.Game.Beatmaps.ControlPoints;
|
|||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Backgrounds;
|
using osu.Game.Graphics.Backgrounds;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Skinning.Default
|
namespace osu.Game.Rulesets.Taiko.Skinning.Default
|
||||||
@ -24,8 +25,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class CirclePiece : BeatSyncedContainer, IHasAccentColour
|
public abstract class CirclePiece : BeatSyncedContainer, IHasAccentColour
|
||||||
{
|
{
|
||||||
public const float SYMBOL_SIZE = 0.45f;
|
public const float SYMBOL_SIZE = TaikoHitObject.DEFAULT_SIZE;
|
||||||
public const float SYMBOL_BORDER = 8;
|
public const float SYMBOL_BORDER = 8;
|
||||||
|
|
||||||
private const double pre_beat_transition_time = 80;
|
private const double pre_beat_transition_time = 80;
|
||||||
|
|
||||||
private Color4 accentColour;
|
private Color4 accentColour;
|
||||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
new Sprite
|
new Sprite
|
||||||
{
|
{
|
||||||
Texture = skin.GetTexture("approachcircle"),
|
Texture = skin.GetTexture("approachcircle"),
|
||||||
Scale = new Vector2(0.73f),
|
Scale = new Vector2(0.83f),
|
||||||
Alpha = 0.47f, // eyeballed to match stable
|
Alpha = 0.47f, // eyeballed to match stable
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
new Sprite
|
new Sprite
|
||||||
{
|
{
|
||||||
Texture = skin.GetTexture("taikobigcircle"),
|
Texture = skin.GetTexture("taikobigcircle"),
|
||||||
Scale = new Vector2(0.7f),
|
Scale = new Vector2(0.8f),
|
||||||
Alpha = 0.22f, // eyeballed to match stable
|
Alpha = 0.22f, // eyeballed to match stable
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
|
@ -13,6 +13,7 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
@ -183,14 +184,41 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
assertItemInHistoryListStep(2, 0);
|
assertItemInHistoryListStep(2, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInsertedItemDoesNotRefreshAllOthers()
|
||||||
|
{
|
||||||
|
AddStep("change to round robin queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayersRoundRobin }).WaitSafely());
|
||||||
|
|
||||||
|
// Add a few items for the local user.
|
||||||
|
addItemStep();
|
||||||
|
addItemStep();
|
||||||
|
addItemStep();
|
||||||
|
addItemStep();
|
||||||
|
addItemStep();
|
||||||
|
|
||||||
|
DrawableRoomPlaylistItem[] drawableItems = null;
|
||||||
|
AddStep("get drawable items", () => drawableItems = this.ChildrenOfType<DrawableRoomPlaylistItem>().ToArray());
|
||||||
|
|
||||||
|
// Add 1 item for another user.
|
||||||
|
AddStep("join second user", () => MultiplayerClient.AddUser(new APIUser { Id = 10 }));
|
||||||
|
addItemStep(userId: 10);
|
||||||
|
|
||||||
|
// New item inserted towards the top of the list.
|
||||||
|
assertItemInQueueListStep(7, 1);
|
||||||
|
AddAssert("all previous playlist items remained", () => drawableItems.All(this.ChildrenOfType<DrawableRoomPlaylistItem>().Contains));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a step to create a new playlist item.
|
/// Adds a step to create a new playlist item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void addItemStep(bool expired = false) => AddStep("add item", () => MultiplayerClient.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)
|
private void addItemStep(bool expired = false, int? userId = null) => AddStep("add item", () =>
|
||||||
|
{
|
||||||
|
MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)
|
||||||
{
|
{
|
||||||
Expired = expired,
|
Expired = expired,
|
||||||
PlayedAt = DateTimeOffset.Now
|
PlayedAt = DateTimeOffset.Now
|
||||||
})));
|
})).WaitSafely();
|
||||||
|
});
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asserts the position of a given playlist item in the queue list.
|
/// Asserts the position of a given playlist item in the queue list.
|
||||||
|
@ -44,9 +44,6 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
|
|
||||||
private BeatmapInfo beatmapInfo;
|
private BeatmapInfo beatmapInfo;
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private RealmAccess realm { get; set; }
|
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private readonly DialogOverlay dialogOverlay;
|
private readonly DialogOverlay dialogOverlay;
|
||||||
|
|
||||||
@ -92,6 +89,12 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get<RulesetStore>(), () => beatmapManager, LocalStorage, Realm, Scheduler));
|
dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get<RulesetStore>(), () => beatmapManager, LocalStorage, Realm, Scheduler));
|
||||||
Dependencies.Cache(Realm);
|
Dependencies.Cache(Realm);
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load() => Schedule(() =>
|
||||||
|
{
|
||||||
var imported = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely();
|
var imported = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely();
|
||||||
|
|
||||||
imported?.PerformRead(s =>
|
imported?.PerformRead(s =>
|
||||||
@ -115,26 +118,26 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
importedScores.Add(scoreManager.Import(score).Value);
|
importedScores.Add(scoreManager.Import(score).Value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return dependencies;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SetUp]
|
|
||||||
public void Setup() => Schedule(() =>
|
|
||||||
{
|
|
||||||
realm.Run(r =>
|
|
||||||
{
|
|
||||||
// Due to soft deletions, we can re-use deleted scores between test runs
|
|
||||||
scoreManager.Undelete(r.All<ScoreInfo>().Where(s => s.DeletePending).ToList());
|
|
||||||
});
|
|
||||||
|
|
||||||
leaderboard.BeatmapInfo = beatmapInfo;
|
|
||||||
leaderboard.RefetchScores(); // Required in the case that the beatmap hasn't changed
|
|
||||||
});
|
});
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetupSteps()
|
public void SetupSteps()
|
||||||
{
|
{
|
||||||
|
AddUntilStep("ensure scores imported", () => importedScores.Count == 50);
|
||||||
|
AddStep("undelete scores", () =>
|
||||||
|
{
|
||||||
|
Realm.Run(r =>
|
||||||
|
{
|
||||||
|
// Due to soft deletions, we can re-use deleted scores between test runs
|
||||||
|
scoreManager.Undelete(r.All<ScoreInfo>().Where(s => s.DeletePending).ToList());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
AddStep("set up leaderboard", () =>
|
||||||
|
{
|
||||||
|
leaderboard.BeatmapInfo = beatmapInfo;
|
||||||
|
leaderboard.RefetchScores(); // Required in the case that the beatmap hasn't changed
|
||||||
|
});
|
||||||
|
|
||||||
// Ensure the leaderboard items have finished showing up
|
// Ensure the leaderboard items have finished showing up
|
||||||
AddStep("finish transforms", () => leaderboard.FinishTransforms(true));
|
AddStep("finish transforms", () => leaderboard.FinishTransforms(true));
|
||||||
AddUntilStep("wait for drawables", () => leaderboard.ChildrenOfType<LeaderboardScore>().Any());
|
AddUntilStep("wait for drawables", () => leaderboard.ChildrenOfType<LeaderboardScore>().Any());
|
||||||
@ -169,11 +172,14 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
AddStep("click delete button", () =>
|
AddStep("click delete button", () =>
|
||||||
{
|
{
|
||||||
InputManager.MoveMouseTo(dialogOverlay.ChildrenOfType<DialogButton>().First());
|
InputManager.MoveMouseTo(dialogOverlay.ChildrenOfType<DialogButton>().First());
|
||||||
InputManager.Click(MouseButton.Left);
|
InputManager.PressButton(MouseButton.Left);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
|
AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
|
||||||
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != scoreBeingDeleted.OnlineID));
|
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != scoreBeingDeleted.OnlineID));
|
||||||
|
|
||||||
|
// "Clean up"
|
||||||
|
AddStep("release left mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -5,6 +5,7 @@ using System.Collections.Generic;
|
|||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Beatmaps.Timing;
|
using osu.Game.Beatmaps.Timing;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps
|
namespace osu.Game.Beatmaps
|
||||||
{
|
{
|
||||||
@ -70,4 +71,27 @@ namespace osu.Game.Beatmaps
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
new IReadOnlyList<T> HitObjects { get; }
|
new IReadOnlyList<T> HitObjects { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class BeatmapExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Finds the maximum achievable combo by hitting all <see cref="HitObject"/>s in a beatmap.
|
||||||
|
/// </summary>
|
||||||
|
public static int GetMaxCombo(this IBeatmap beatmap)
|
||||||
|
{
|
||||||
|
int combo = 0;
|
||||||
|
foreach (var h in beatmap.HitObjects)
|
||||||
|
addCombo(h, ref combo);
|
||||||
|
return combo;
|
||||||
|
|
||||||
|
static void addCombo(HitObject hitObject, ref int combo)
|
||||||
|
{
|
||||||
|
if (hitObject.CreateJudgement().MaxResult.AffectsCombo())
|
||||||
|
combo++;
|
||||||
|
|
||||||
|
foreach (var nested in hitObject.NestedHitObjects)
|
||||||
|
addCombo(nested, ref combo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,6 +88,7 @@ namespace osu.Game.Configuration
|
|||||||
throw new InvalidOperationException($"{nameof(SettingSourceAttribute)} had an unsupported custom control type ({controlType.ReadableName()})");
|
throw new InvalidOperationException($"{nameof(SettingSourceAttribute)} had an unsupported custom control type ({controlType.ReadableName()})");
|
||||||
|
|
||||||
var control = (Drawable)Activator.CreateInstance(controlType);
|
var control = (Drawable)Activator.CreateInstance(controlType);
|
||||||
|
controlType.GetProperty(nameof(SettingsItem<object>.SettingSourceObject))?.SetValue(control, obj);
|
||||||
controlType.GetProperty(nameof(SettingsItem<object>.LabelText))?.SetValue(control, attr.Label);
|
controlType.GetProperty(nameof(SettingsItem<object>.LabelText))?.SetValue(control, attr.Label);
|
||||||
controlType.GetProperty(nameof(SettingsItem<object>.TooltipText))?.SetValue(control, attr.Description);
|
controlType.GetProperty(nameof(SettingsItem<object>.TooltipText))?.SetValue(control, attr.Description);
|
||||||
controlType.GetProperty(nameof(SettingsItem<object>.Current))?.SetValue(control, value);
|
controlType.GetProperty(nameof(SettingsItem<object>.Current))?.SetValue(control, value);
|
||||||
|
@ -11,6 +11,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Online.Rooms
|
namespace osu.Game.Online.Rooms
|
||||||
{
|
{
|
||||||
@ -84,6 +85,19 @@ namespace osu.Game.Online.Rooms
|
|||||||
Beatmap = beatmap;
|
Beatmap = beatmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PlaylistItem(MultiplayerPlaylistItem item)
|
||||||
|
: this(new APIBeatmap { OnlineID = item.BeatmapID })
|
||||||
|
{
|
||||||
|
ID = item.ID;
|
||||||
|
OwnerID = item.OwnerID;
|
||||||
|
RulesetID = item.RulesetID;
|
||||||
|
Expired = item.Expired;
|
||||||
|
PlaylistOrder = item.PlaylistOrder;
|
||||||
|
PlayedAt = item.PlayedAt;
|
||||||
|
RequiredMods = item.RequiredMods.ToArray();
|
||||||
|
AllowedMods = item.AllowedMods.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
public void MarkInvalid() => valid.Value = false;
|
public void MarkInvalid() => valid.Value = false;
|
||||||
|
|
||||||
#region Newtonsoft.Json implicit ShouldSerialize() methods
|
#region Newtonsoft.Json implicit ShouldSerialize() methods
|
||||||
@ -101,13 +115,13 @@ namespace osu.Game.Online.Rooms
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public PlaylistItem With(IBeatmapInfo beatmap) => new PlaylistItem(beatmap)
|
public PlaylistItem With(Optional<IBeatmapInfo> beatmap = default, Optional<ushort?> playlistOrder = default) => new PlaylistItem(beatmap.GetOr(Beatmap))
|
||||||
{
|
{
|
||||||
ID = ID,
|
ID = ID,
|
||||||
OwnerID = OwnerID,
|
OwnerID = OwnerID,
|
||||||
RulesetID = RulesetID,
|
RulesetID = RulesetID,
|
||||||
Expired = Expired,
|
Expired = Expired,
|
||||||
PlaylistOrder = PlaylistOrder,
|
PlaylistOrder = playlistOrder.GetOr(PlaylistOrder),
|
||||||
PlayedAt = PlayedAt,
|
PlayedAt = PlayedAt,
|
||||||
AllowedMods = AllowedMods,
|
AllowedMods = AllowedMods,
|
||||||
RequiredMods = RequiredMods,
|
RequiredMods = RequiredMods,
|
||||||
@ -119,6 +133,7 @@ namespace osu.Game.Online.Rooms
|
|||||||
&& Beatmap.OnlineID == other.Beatmap.OnlineID
|
&& Beatmap.OnlineID == other.Beatmap.OnlineID
|
||||||
&& RulesetID == other.RulesetID
|
&& RulesetID == other.RulesetID
|
||||||
&& Expired == other.Expired
|
&& Expired == other.Expired
|
||||||
|
&& PlaylistOrder == other.PlaylistOrder
|
||||||
&& AllowedMods.SequenceEqual(other.AllowedMods)
|
&& AllowedMods.SequenceEqual(other.AllowedMods)
|
||||||
&& RequiredMods.SequenceEqual(other.RequiredMods);
|
&& RequiredMods.SequenceEqual(other.RequiredMods);
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
|||||||
HeaderText = @"Confirm deletion of";
|
HeaderText = @"Confirm deletion of";
|
||||||
Buttons = new PopupDialogButton[]
|
Buttons = new PopupDialogButton[]
|
||||||
{
|
{
|
||||||
new PopupDialogOkButton
|
new PopupDialogDangerousButton
|
||||||
{
|
{
|
||||||
Text = @"Yes. Go for it.",
|
Text = @"Yes. Go for it.",
|
||||||
Action = deleteAction
|
Action = deleteAction
|
||||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics.Cursor;
|
|||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
@ -24,6 +25,11 @@ namespace osu.Game.Overlays.Settings
|
|||||||
|
|
||||||
protected Drawable Control { get; }
|
protected Drawable Control { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The source component if this <see cref="SettingsItem{T}"/> was created via <see cref="SettingSourceAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
public object SettingSourceObject { get; internal set; }
|
||||||
|
|
||||||
private IHasCurrentValue<T> controlWithCurrent => Control as IHasCurrentValue<T>;
|
private IHasCurrentValue<T> controlWithCurrent => Control as IHasCurrentValue<T>;
|
||||||
|
|
||||||
protected override Container<Drawable> Content => FlowContent;
|
protected override Container<Drawable> Content => FlowContent;
|
||||||
|
@ -190,7 +190,7 @@ namespace osu.Game.Overlays.Toolbar
|
|||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
{
|
{
|
||||||
if (e.Action == Hotkey)
|
if (e.Action == Hotkey && !e.Repeat)
|
||||||
{
|
{
|
||||||
TriggerClick();
|
TriggerClick();
|
||||||
return true;
|
return true;
|
||||||
|
@ -15,6 +15,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
|
|||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Difficulty
|
namespace osu.Game.Rulesets.Difficulty
|
||||||
{
|
{
|
||||||
@ -119,15 +120,23 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap.
|
/// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This can only be used to compute difficulties for legacy mod combinations.
|
||||||
|
/// </remarks>
|
||||||
/// <returns>A collection of structures describing the difficulty of the beatmap for each mod combination.</returns>
|
/// <returns>A collection of structures describing the difficulty of the beatmap for each mod combination.</returns>
|
||||||
public IEnumerable<DifficultyAttributes> CalculateAll(CancellationToken cancellationToken = default)
|
public IEnumerable<DifficultyAttributes> CalculateAllLegacyCombinations(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
var rulesetInstance = ruleset.CreateInstance();
|
||||||
|
|
||||||
foreach (var combination in CreateDifficultyAdjustmentModCombinations())
|
foreach (var combination in CreateDifficultyAdjustmentModCombinations())
|
||||||
{
|
{
|
||||||
if (combination is MultiMod multi)
|
Mod classicMod = rulesetInstance.CreateAllMods().SingleOrDefault(m => m is ModClassic);
|
||||||
yield return Calculate(multi.Mods, cancellationToken);
|
|
||||||
else
|
var finalCombination = ModUtils.FlattenMod(combination);
|
||||||
yield return Calculate(combination.Yield(), cancellationToken);
|
if (classicMod != null)
|
||||||
|
finalCombination = finalCombination.Append(classicMod);
|
||||||
|
|
||||||
|
yield return Calculate(finalCombination.ToArray(), cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Audio.Sample;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
@ -27,6 +29,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
|||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
private MultiplayerRoom room => multiplayerClient.Room;
|
private MultiplayerRoom room => multiplayerClient.Room;
|
||||||
|
|
||||||
|
private Sample countdownTickSample;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(AudioManager audio)
|
||||||
|
{
|
||||||
|
countdownTickSample = audio.Samples.Get(@"Multiplayer/countdown-tick");
|
||||||
|
// disabled for now pending further work on sound effect
|
||||||
|
// countdownTickFinalSample = audio.Samples.Get(@"Multiplayer/countdown-tick-final");
|
||||||
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
@ -36,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
|||||||
}
|
}
|
||||||
|
|
||||||
private MultiplayerCountdown countdown;
|
private MultiplayerCountdown countdown;
|
||||||
private DateTimeOffset countdownChangeTime;
|
private double countdownChangeTime;
|
||||||
private ScheduledDelegate countdownUpdateDelegate;
|
private ScheduledDelegate countdownUpdateDelegate;
|
||||||
|
|
||||||
private void onRoomUpdated() => Scheduler.AddOnce(() =>
|
private void onRoomUpdated() => Scheduler.AddOnce(() =>
|
||||||
@ -44,20 +56,55 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
|||||||
if (countdown != room?.Countdown)
|
if (countdown != room?.Countdown)
|
||||||
{
|
{
|
||||||
countdown = room?.Countdown;
|
countdown = room?.Countdown;
|
||||||
countdownChangeTime = DateTimeOffset.Now;
|
countdownChangeTime = Time.Current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scheduleNextCountdownUpdate();
|
||||||
|
|
||||||
|
updateButtonText();
|
||||||
|
updateButtonColour();
|
||||||
|
});
|
||||||
|
|
||||||
|
private void scheduleNextCountdownUpdate()
|
||||||
|
{
|
||||||
|
countdownUpdateDelegate?.Cancel();
|
||||||
|
|
||||||
if (countdown != null)
|
if (countdown != null)
|
||||||
countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 100, true);
|
{
|
||||||
|
// The remaining time on a countdown may be at a fractional portion between two seconds.
|
||||||
|
// We want to align certain audio/visual cues to the point at which integer seconds change.
|
||||||
|
// To do so, we schedule to the next whole second. Note that scheduler invocation isn't
|
||||||
|
// guaranteed to be accurate, so this may still occur slightly late, but even in such a case
|
||||||
|
// the next invocation will be roughly correct.
|
||||||
|
double timeToNextSecond = countdownTimeRemaining.TotalMilliseconds % 1000;
|
||||||
|
|
||||||
|
countdownUpdateDelegate = Scheduler.AddDelayed(onCountdownTick, timeToNextSecond);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
countdownUpdateDelegate?.Cancel();
|
countdownUpdateDelegate?.Cancel();
|
||||||
countdownUpdateDelegate = null;
|
countdownUpdateDelegate = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onCountdownTick()
|
||||||
|
{
|
||||||
updateButtonText();
|
updateButtonText();
|
||||||
updateButtonColour();
|
|
||||||
});
|
int secondsRemaining = countdownTimeRemaining.Seconds;
|
||||||
|
|
||||||
|
playTickSound(secondsRemaining);
|
||||||
|
|
||||||
|
if (secondsRemaining > 0)
|
||||||
|
scheduleNextCountdownUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playTickSound(int secondsRemaining)
|
||||||
|
{
|
||||||
|
if (secondsRemaining < 10) countdownTickSample?.Play();
|
||||||
|
// disabled for now pending further work on sound effect
|
||||||
|
// if (secondsRemaining <= 3) countdownTickFinalSample?.Play();
|
||||||
|
}
|
||||||
|
|
||||||
private void updateButtonText()
|
private void updateButtonText()
|
||||||
{
|
{
|
||||||
@ -75,15 +122,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
|||||||
|
|
||||||
if (countdown != null)
|
if (countdown != null)
|
||||||
{
|
{
|
||||||
TimeSpan timeElapsed = DateTimeOffset.Now - countdownChangeTime;
|
string countdownText = $"Starting in {countdownTimeRemaining:mm\\:ss}";
|
||||||
TimeSpan countdownRemaining;
|
|
||||||
|
|
||||||
if (timeElapsed > countdown.TimeRemaining)
|
|
||||||
countdownRemaining = TimeSpan.Zero;
|
|
||||||
else
|
|
||||||
countdownRemaining = countdown.TimeRemaining - timeElapsed;
|
|
||||||
|
|
||||||
string countdownText = $"Starting in {countdownRemaining:mm\\:ss}";
|
|
||||||
|
|
||||||
switch (localUser?.State)
|
switch (localUser?.State)
|
||||||
{
|
{
|
||||||
@ -116,6 +155,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TimeSpan countdownTimeRemaining
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
double timeElapsed = Time.Current - countdownChangeTime;
|
||||||
|
TimeSpan remaining;
|
||||||
|
|
||||||
|
if (timeElapsed > countdown.TimeRemaining.TotalMilliseconds)
|
||||||
|
remaining = TimeSpan.Zero;
|
||||||
|
else
|
||||||
|
remaining = countdown.TimeRemaining - TimeSpan.FromMilliseconds(timeElapsed);
|
||||||
|
|
||||||
|
return remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void updateButtonColour()
|
private void updateButtonColour()
|
||||||
{
|
{
|
||||||
if (room == null)
|
if (room == null)
|
||||||
|
@ -117,9 +117,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
|||||||
{
|
{
|
||||||
base.PlaylistItemChanged(item);
|
base.PlaylistItemChanged(item);
|
||||||
|
|
||||||
|
var newApiItem = Playlist.SingleOrDefault(i => i.ID == item.ID);
|
||||||
|
var existingApiItemInQueue = queueList.Items.SingleOrDefault(i => i.ID == item.ID);
|
||||||
|
|
||||||
|
// Test if the only change between the two playlist items is the order.
|
||||||
|
if (newApiItem != null && existingApiItemInQueue != null && existingApiItemInQueue.With(playlistOrder: newApiItem.PlaylistOrder).Equals(newApiItem))
|
||||||
|
{
|
||||||
|
// Set the new playlist order directly without refreshing the DrawablePlaylistItem.
|
||||||
|
existingApiItemInQueue.PlaylistOrder = newApiItem.PlaylistOrder;
|
||||||
|
|
||||||
|
// The following isn't really required, but is here for safety and explicitness.
|
||||||
|
// MultiplayerQueueList internally binds to changes in Playlist to invalidate its own layout, which is mutated on every playlist operation.
|
||||||
|
queueList.Invalidate();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
removeItemFromLists(item.ID);
|
removeItemFromLists(item.ID);
|
||||||
addItemToLists(item);
|
addItemToLists(item);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void addItemToLists(MultiplayerPlaylistItem item)
|
private void addItemToLists(MultiplayerPlaylistItem item)
|
||||||
{
|
{
|
||||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Screens.Select
|
|||||||
HeaderText = @"Confirm deletion of";
|
HeaderText = @"Confirm deletion of";
|
||||||
Buttons = new PopupDialogButton[]
|
Buttons = new PopupDialogButton[]
|
||||||
{
|
{
|
||||||
new PopupDialogOkButton
|
new PopupDialogDangerousButton
|
||||||
{
|
{
|
||||||
Text = @"Yes. Totally. Delete it.",
|
Text = @"Yes. Totally. Delete it.",
|
||||||
Action = () => manager?.Delete(beatmap),
|
Action = () => manager?.Delete(beatmap),
|
||||||
|
@ -174,7 +174,7 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
{
|
{
|
||||||
if (e.Action == Hotkey)
|
if (e.Action == Hotkey && !e.Repeat)
|
||||||
{
|
{
|
||||||
TriggerClick();
|
TriggerClick();
|
||||||
return true;
|
return true;
|
||||||
|
@ -38,7 +38,7 @@ namespace osu.Game.Screens.Select
|
|||||||
HeaderText = "Confirm deletion of local score";
|
HeaderText = "Confirm deletion of local score";
|
||||||
Buttons = new PopupDialogButton[]
|
Buttons = new PopupDialogButton[]
|
||||||
{
|
{
|
||||||
new PopupDialogOkButton
|
new PopupDialogDangerousButton
|
||||||
{
|
{
|
||||||
Text = "Yes. Please.",
|
Text = "Yes. Please.",
|
||||||
Action = () => scoreManager?.Delete(score)
|
Action = () => scoreManager?.Delete(score)
|
||||||
|
@ -8,6 +8,7 @@ using osu.Framework.Audio.Sample;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.OpenGL.Textures;
|
using osu.Framework.Graphics.OpenGL.Textures;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Beatmaps.Formats;
|
using osu.Game.Beatmaps.Formats;
|
||||||
@ -46,13 +47,13 @@ namespace osu.Game.Skinning
|
|||||||
this.resources = resources;
|
this.resources = resources;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
|
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => Textures?.Get(componentName, wrapModeS, wrapModeT);
|
||||||
|
|
||||||
public override ISample GetSample(ISampleInfo sampleInfo)
|
public override ISample GetSample(ISampleInfo sampleInfo)
|
||||||
{
|
{
|
||||||
foreach (string lookup in sampleInfo.LookupNames)
|
foreach (string lookup in sampleInfo.LookupNames)
|
||||||
{
|
{
|
||||||
var sample = resources.AudioManager.Samples.Get(lookup);
|
var sample = Samples?.Get(lookup) ?? resources.AudioManager.Samples.Get(lookup);
|
||||||
if (sample != null)
|
if (sample != null)
|
||||||
return sample;
|
return sample;
|
||||||
}
|
}
|
||||||
@ -157,6 +158,16 @@ namespace osu.Game.Skinning
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (component.LookupName)
|
||||||
|
{
|
||||||
|
// Temporary until default skin has a valid hit lighting.
|
||||||
|
case @"lighting":
|
||||||
|
return Drawable.Empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GetTexture(component.LookupName) is Texture t)
|
||||||
|
return new Sprite { Texture = t };
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,9 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -11,6 +13,7 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.Cursor;
|
using osu.Game.Graphics.Cursor;
|
||||||
@ -22,7 +25,7 @@ using osu.Game.Screens.Edit.Components.Menus;
|
|||||||
namespace osu.Game.Skinning.Editor
|
namespace osu.Game.Skinning.Editor
|
||||||
{
|
{
|
||||||
[Cached(typeof(SkinEditor))]
|
[Cached(typeof(SkinEditor))]
|
||||||
public class SkinEditor : VisibilityContainer
|
public class SkinEditor : VisibilityContainer, ICanAcceptFiles
|
||||||
{
|
{
|
||||||
public const double TRANSITION_DURATION = 500;
|
public const double TRANSITION_DURATION = 500;
|
||||||
|
|
||||||
@ -36,12 +39,18 @@ namespace osu.Game.Skinning.Editor
|
|||||||
|
|
||||||
private Bindable<Skin> currentSkin;
|
private Bindable<Skin> currentSkin;
|
||||||
|
|
||||||
|
[Resolved(canBeNull: true)]
|
||||||
|
private OsuGame game { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private SkinManager skins { get; set; }
|
private SkinManager skins { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuColour colours { get; set; }
|
private OsuColour colours { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private RealmAccess realm { get; set; }
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private SkinEditorOverlay skinEditorOverlay { get; set; }
|
private SkinEditorOverlay skinEditorOverlay { get; set; }
|
||||||
|
|
||||||
@ -171,6 +180,8 @@ namespace osu.Game.Skinning.Editor
|
|||||||
|
|
||||||
Show();
|
Show();
|
||||||
|
|
||||||
|
game?.RegisterImportHandler(this);
|
||||||
|
|
||||||
// as long as the skin editor is loaded, let's make sure we can modify the current skin.
|
// as long as the skin editor is loaded, let's make sure we can modify the current skin.
|
||||||
currentSkin = skins.CurrentSkin.GetBoundCopy();
|
currentSkin = skins.CurrentSkin.GetBoundCopy();
|
||||||
|
|
||||||
@ -229,21 +240,29 @@ namespace osu.Game.Skinning.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void placeComponent(Type type)
|
private void placeComponent(Type type)
|
||||||
|
{
|
||||||
|
if (!(Activator.CreateInstance(type) is ISkinnableDrawable component))
|
||||||
|
throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISkinnableDrawable)}.");
|
||||||
|
|
||||||
|
placeComponent(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void placeComponent(ISkinnableDrawable component, bool applyDefaults = true)
|
||||||
{
|
{
|
||||||
var targetContainer = getFirstTarget();
|
var targetContainer = getFirstTarget();
|
||||||
|
|
||||||
if (targetContainer == null)
|
if (targetContainer == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!(Activator.CreateInstance(type) is ISkinnableDrawable component))
|
|
||||||
throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISkinnableDrawable)}.");
|
|
||||||
|
|
||||||
var drawableComponent = (Drawable)component;
|
var drawableComponent = (Drawable)component;
|
||||||
|
|
||||||
|
if (applyDefaults)
|
||||||
|
{
|
||||||
// give newly added components a sane starting location.
|
// give newly added components a sane starting location.
|
||||||
drawableComponent.Origin = Anchor.TopCentre;
|
drawableComponent.Origin = Anchor.TopCentre;
|
||||||
drawableComponent.Anchor = Anchor.TopCentre;
|
drawableComponent.Anchor = Anchor.TopCentre;
|
||||||
drawableComponent.Y = targetContainer.DrawSize.Y / 2;
|
drawableComponent.Y = targetContainer.DrawSize.Y / 2;
|
||||||
|
}
|
||||||
|
|
||||||
targetContainer.Add(component);
|
targetContainer.Add(component);
|
||||||
|
|
||||||
@ -313,5 +332,54 @@ namespace osu.Game.Skinning.Editor
|
|||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item);
|
availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Drag & drop import handling
|
||||||
|
|
||||||
|
public Task Import(params string[] paths)
|
||||||
|
{
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
var file = new FileInfo(paths.First());
|
||||||
|
|
||||||
|
// import to skin
|
||||||
|
currentSkin.Value.SkinInfo.PerformWrite(skinInfo =>
|
||||||
|
{
|
||||||
|
using (var contents = file.OpenRead())
|
||||||
|
skins.AddFile(skinInfo, contents, file.Name);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Even though we are 100% on an update thread, we need to wait for realm callbacks to fire (to correctly invalidate caches in RealmBackedResourceStore).
|
||||||
|
// See https://github.com/realm/realm-dotnet/discussions/2634#discussioncomment-2483573 for further discussion.
|
||||||
|
// This is the best we can do for now.
|
||||||
|
realm.Run(r => r.Refresh());
|
||||||
|
|
||||||
|
// place component
|
||||||
|
var sprite = new SkinnableSprite
|
||||||
|
{
|
||||||
|
SpriteName = { Value = file.Name },
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Position = getFirstTarget().ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position),
|
||||||
|
};
|
||||||
|
|
||||||
|
placeComponent(sprite, false);
|
||||||
|
|
||||||
|
SkinSelectionHandler.ApplyClosestAnchor(sprite);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Import(params ImportTask[] tasks) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public IEnumerable<string> HandledExtensions => new[] { ".jpg", ".jpeg", ".png" };
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
game?.UnregisterImportHandler(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -157,13 +157,13 @@ namespace osu.Game.Skinning.Editor
|
|||||||
|
|
||||||
if (item.UsesFixedAnchor) continue;
|
if (item.UsesFixedAnchor) continue;
|
||||||
|
|
||||||
applyClosestAnchor(drawable);
|
ApplyClosestAnchor(drawable);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void applyClosestAnchor(Drawable drawable) => applyAnchor(drawable, getClosestAnchor(drawable));
|
public static void ApplyClosestAnchor(Drawable drawable) => applyAnchor(drawable, getClosestAnchor(drawable));
|
||||||
|
|
||||||
protected override void OnSelectionChanged()
|
protected override void OnSelectionChanged()
|
||||||
{
|
{
|
||||||
@ -252,7 +252,7 @@ namespace osu.Game.Skinning.Editor
|
|||||||
|
|
||||||
if (item.UsesFixedAnchor) continue;
|
if (item.UsesFixedAnchor) continue;
|
||||||
|
|
||||||
applyClosestAnchor(drawable);
|
ApplyClosestAnchor(drawable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,7 +279,7 @@ namespace osu.Game.Skinning.Editor
|
|||||||
foreach (var item in SelectedItems)
|
foreach (var item in SelectedItems)
|
||||||
{
|
{
|
||||||
item.UsesFixedAnchor = false;
|
item.UsesFixedAnchor = false;
|
||||||
applyClosestAnchor((Drawable)item);
|
ApplyClosestAnchor((Drawable)item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ using osu.Framework.IO.Stores;
|
|||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.Formats;
|
using osu.Game.Beatmaps.Formats;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
using osu.Game.Rulesets.Objects.Legacy;
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
@ -37,11 +38,11 @@ namespace osu.Game.Skinning
|
|||||||
|
|
||||||
private static IResourceStore<byte[]> createRealmBackedStore(BeatmapInfo beatmapInfo, IStorageResourceProvider? resources)
|
private static IResourceStore<byte[]> createRealmBackedStore(BeatmapInfo beatmapInfo, IStorageResourceProvider? resources)
|
||||||
{
|
{
|
||||||
if (resources == null)
|
if (resources == null || beatmapInfo.BeatmapSet == null)
|
||||||
// should only ever be used in tests.
|
// should only ever be used in tests.
|
||||||
return new ResourceStore<byte[]>();
|
return new ResourceStore<byte[]>();
|
||||||
|
|
||||||
return new RealmBackedResourceStore(beatmapInfo.BeatmapSet, resources.Files, new[] { @"ogg" });
|
return new RealmBackedResourceStore<BeatmapSetInfo>(beatmapInfo.BeatmapSet.ToLive(resources.RealmAccess), resources.Files, resources.RealmAccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Drawable? GetDrawableComponent(ISkinComponent component)
|
public override Drawable? GetDrawableComponent(ISkinComponent component)
|
||||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Skinning
|
|||||||
/// The <see cref="ISkin"/> which is being transformed.
|
/// The <see cref="ISkin"/> which is being transformed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[NotNull]
|
[NotNull]
|
||||||
protected ISkin Skin { get; }
|
protected internal ISkin Skin { get; }
|
||||||
|
|
||||||
protected LegacySkinTransformer([NotNull] ISkin skin)
|
protected LegacySkinTransformer([NotNull] ISkin skin)
|
||||||
{
|
{
|
||||||
|
@ -1,51 +1,77 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.IO.Stores;
|
using osu.Framework.IO.Stores;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Extensions;
|
using osu.Game.Extensions;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
namespace osu.Game.Skinning
|
namespace osu.Game.Skinning
|
||||||
{
|
{
|
||||||
public class RealmBackedResourceStore : ResourceStore<byte[]>
|
public class RealmBackedResourceStore<T> : ResourceStore<byte[]>
|
||||||
|
where T : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, string> fileToStoragePathMapping = new Dictionary<string, string>();
|
private Lazy<Dictionary<string, string>> fileToStoragePathMapping;
|
||||||
|
|
||||||
public RealmBackedResourceStore(IHasRealmFiles source, IResourceStore<byte[]> underlyingStore, string[] extensions = null)
|
private readonly Live<T> liveSource;
|
||||||
|
private readonly IDisposable? realmSubscription;
|
||||||
|
|
||||||
|
public RealmBackedResourceStore(Live<T> source, IResourceStore<byte[]> underlyingStore, RealmAccess? realm)
|
||||||
: base(underlyingStore)
|
: base(underlyingStore)
|
||||||
{
|
{
|
||||||
// Must be initialised before the file cache.
|
liveSource = source;
|
||||||
if (extensions != null)
|
|
||||||
{
|
invalidateCache();
|
||||||
foreach (string extension in extensions)
|
Debug.Assert(fileToStoragePathMapping != null);
|
||||||
AddExtension(extension);
|
|
||||||
|
realmSubscription = realm?.RegisterForNotifications(r => r.All<T>().Where(s => s.ID == source.ID), skinChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
initialiseFileCache(source);
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
realmSubscription?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initialiseFileCache(IHasRealmFiles source)
|
private void skinChanged(IRealmCollection<T> sender, ChangeSet changes, Exception error) => invalidateCache();
|
||||||
{
|
|
||||||
fileToStoragePathMapping.Clear();
|
|
||||||
foreach (var f in source.Files)
|
|
||||||
fileToStoragePathMapping[f.Filename.ToLowerInvariant()] = f.File.GetStoragePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IEnumerable<string> GetFilenames(string name)
|
protected override IEnumerable<string> GetFilenames(string name)
|
||||||
{
|
{
|
||||||
foreach (string filename in base.GetFilenames(name))
|
foreach (string filename in base.GetFilenames(name))
|
||||||
{
|
{
|
||||||
string path = getPathForFile(filename.ToStandardisedPath());
|
string? path = getPathForFile(filename.ToStandardisedPath());
|
||||||
if (path != null)
|
if (path != null)
|
||||||
yield return path;
|
yield return path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string getPathForFile(string filename) =>
|
private string? getPathForFile(string filename)
|
||||||
fileToStoragePathMapping.TryGetValue(filename.ToLower(), out string path) ? path : null;
|
{
|
||||||
|
if (fileToStoragePathMapping.Value.TryGetValue(filename.ToLowerInvariant(), out string path))
|
||||||
|
return path;
|
||||||
|
|
||||||
public override IEnumerable<string> GetAvailableResources() => fileToStoragePathMapping.Keys;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invalidateCache() => fileToStoragePathMapping = new Lazy<Dictionary<string, string>>(initialiseFileCache);
|
||||||
|
|
||||||
|
private Dictionary<string, string> initialiseFileCache() => liveSource.PerformRead(source =>
|
||||||
|
{
|
||||||
|
var dictionary = new Dictionary<string, string>();
|
||||||
|
dictionary.Clear();
|
||||||
|
foreach (var f in source.Files)
|
||||||
|
dictionary[f.Filename.ToLowerInvariant()] = f.File.GetStoragePath();
|
||||||
|
|
||||||
|
return dictionary;
|
||||||
|
});
|
||||||
|
|
||||||
|
public override IEnumerable<string> GetAvailableResources() => fileToStoragePathMapping.Value.Keys;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,6 +54,8 @@ namespace osu.Game.Skinning
|
|||||||
where TLookup : notnull
|
where TLookup : notnull
|
||||||
where TValue : notnull;
|
where TValue : notnull;
|
||||||
|
|
||||||
|
private readonly RealmBackedResourceStore<SkinInfo>? realmBackedStorage;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Construct a new skin.
|
/// Construct a new skin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -67,7 +69,9 @@ namespace osu.Game.Skinning
|
|||||||
{
|
{
|
||||||
SkinInfo = skin.ToLive(resources.RealmAccess);
|
SkinInfo = skin.ToLive(resources.RealmAccess);
|
||||||
|
|
||||||
storage ??= new RealmBackedResourceStore(skin, resources.Files, new[] { @"ogg" });
|
storage ??= realmBackedStorage = new RealmBackedResourceStore<SkinInfo>(SkinInfo, resources.Files, resources.RealmAccess);
|
||||||
|
|
||||||
|
(storage as ResourceStore<byte[]>)?.AddExtension("ogg");
|
||||||
|
|
||||||
var samples = resources.AudioManager?.GetSampleStore(storage);
|
var samples = resources.AudioManager?.GetSampleStore(storage);
|
||||||
if (samples != null)
|
if (samples != null)
|
||||||
@ -191,6 +195,8 @@ namespace osu.Game.Skinning
|
|||||||
|
|
||||||
Textures?.Dispose();
|
Textures?.Dispose();
|
||||||
Samples?.Dispose();
|
Samples?.Dispose();
|
||||||
|
|
||||||
|
realmBackedStorage?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@ -23,6 +24,7 @@ using osu.Game.Audio;
|
|||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
using osu.Game.IO.Archives;
|
using osu.Game.IO.Archives;
|
||||||
|
using osu.Game.Models;
|
||||||
using osu.Game.Overlays.Notifications;
|
using osu.Game.Overlays.Notifications;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
|
|
||||||
@ -36,7 +38,7 @@ namespace osu.Game.Skinning
|
|||||||
/// For gameplay components, see <see cref="RulesetSkinProvidingContainer"/> which adds extra legacy and toggle logic that may affect the lookup process.
|
/// For gameplay components, see <see cref="RulesetSkinProvidingContainer"/> which adds extra legacy and toggle logic that may affect the lookup process.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[ExcludeFromDynamicCompile]
|
[ExcludeFromDynamicCompile]
|
||||||
public class SkinManager : ISkinSource, IStorageResourceProvider, IModelImporter<SkinInfo>
|
public class SkinManager : ISkinSource, IStorageResourceProvider, IModelImporter<SkinInfo>, IModelManager<SkinInfo>, IModelFileManager<SkinInfo, RealmNamedFileUsage>
|
||||||
{
|
{
|
||||||
private readonly AudioManager audio;
|
private readonly AudioManager audio;
|
||||||
|
|
||||||
@ -96,7 +98,10 @@ namespace osu.Game.Skinning
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin);
|
CurrentSkinInfo.ValueChanged += skin =>
|
||||||
|
{
|
||||||
|
CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin);
|
||||||
|
};
|
||||||
|
|
||||||
CurrentSkin.Value = DefaultSkin;
|
CurrentSkin.Value = DefaultSkin;
|
||||||
CurrentSkin.ValueChanged += skin =>
|
CurrentSkin.ValueChanged += skin =>
|
||||||
@ -313,5 +318,45 @@ namespace osu.Game.Skinning
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
public bool Delete(SkinInfo item)
|
||||||
|
{
|
||||||
|
return skinModelManager.Delete(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Delete(List<SkinInfo> items, bool silent = false)
|
||||||
|
{
|
||||||
|
skinModelManager.Delete(items, silent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Undelete(List<SkinInfo> items, bool silent = false)
|
||||||
|
{
|
||||||
|
skinModelManager.Undelete(items, silent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Undelete(SkinInfo item)
|
||||||
|
{
|
||||||
|
skinModelManager.Undelete(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsAvailableLocally(SkinInfo model)
|
||||||
|
{
|
||||||
|
return skinModelManager.IsAvailableLocally(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReplaceFile(SkinInfo model, RealmNamedFileUsage file, Stream contents)
|
||||||
|
{
|
||||||
|
skinModelManager.ReplaceFile(model, file, contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteFile(SkinInfo model, RealmNamedFileUsage file)
|
||||||
|
{
|
||||||
|
skinModelManager.DeleteFile(model, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddFile(SkinInfo model, Stream contents, string filename)
|
||||||
|
{
|
||||||
|
skinModelManager.AddFile(model, contents, filename);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ namespace osu.Game.Skinning
|
|||||||
set => base.AutoSizeAxes = value;
|
set => base.AutoSizeAxes = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly ISkinComponent component;
|
protected readonly ISkinComponent Component;
|
||||||
|
|
||||||
private readonly ConfineMode confineMode;
|
private readonly ConfineMode confineMode;
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ namespace osu.Game.Skinning
|
|||||||
|
|
||||||
protected SkinnableDrawable(ISkinComponent component, ConfineMode confineMode = ConfineMode.NoScaling)
|
protected SkinnableDrawable(ISkinComponent component, ConfineMode confineMode = ConfineMode.NoScaling)
|
||||||
{
|
{
|
||||||
this.component = component;
|
Component = component;
|
||||||
this.confineMode = confineMode;
|
this.confineMode = confineMode;
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
@ -75,13 +75,13 @@ namespace osu.Game.Skinning
|
|||||||
|
|
||||||
protected override void SkinChanged(ISkinSource skin)
|
protected override void SkinChanged(ISkinSource skin)
|
||||||
{
|
{
|
||||||
Drawable = skin.GetDrawableComponent(component);
|
Drawable = skin.GetDrawableComponent(Component);
|
||||||
|
|
||||||
isDefault = false;
|
isDefault = false;
|
||||||
|
|
||||||
if (Drawable == null)
|
if (Drawable == null)
|
||||||
{
|
{
|
||||||
Drawable = CreateDefault(component);
|
Drawable = CreateDefault(Component);
|
||||||
isDefault = true;
|
isDefault = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,26 +1,56 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Overlays.Settings;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Skinning
|
namespace osu.Game.Skinning
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A skinnable element which uses a stable sprite and can therefore share implementation logic.
|
/// A skinnable element which uses a single texture backing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SkinnableSprite : SkinnableDrawable
|
public class SkinnableSprite : SkinnableDrawable, ISkinnableDrawable
|
||||||
{
|
{
|
||||||
protected override bool ApplySizeRestrictionsToDefault => true;
|
protected override bool ApplySizeRestrictionsToDefault => true;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private TextureStore textures { get; set; }
|
private TextureStore textures { get; set; }
|
||||||
|
|
||||||
|
[SettingSource("Sprite name", "The filename of the sprite", SettingControlType = typeof(SpriteSelectorControl))]
|
||||||
|
public Bindable<string> SpriteName { get; } = new Bindable<string>(string.Empty);
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private ISkinSource source { get; set; }
|
||||||
|
|
||||||
public SkinnableSprite(string textureName, ConfineMode confineMode = ConfineMode.NoScaling)
|
public SkinnableSprite(string textureName, ConfineMode confineMode = ConfineMode.NoScaling)
|
||||||
: base(new SpriteComponent(textureName), confineMode)
|
: base(new SpriteComponent(textureName), confineMode)
|
||||||
{
|
{
|
||||||
|
SpriteName.Value = textureName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SkinnableSprite()
|
||||||
|
: base(new SpriteComponent(string.Empty), ConfineMode.NoScaling)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.None;
|
||||||
|
AutoSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
SpriteName.BindValueChanged(name =>
|
||||||
|
{
|
||||||
|
((SpriteComponent)Component).LookupName = name.NewValue ?? string.Empty;
|
||||||
|
if (IsLoaded)
|
||||||
|
SkinChanged(CurrentSkin);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Drawable CreateDefault(ISkinComponent component)
|
protected override Drawable CreateDefault(ISkinComponent component)
|
||||||
@ -28,19 +58,85 @@ namespace osu.Game.Skinning
|
|||||||
var texture = textures.Get(component.LookupName);
|
var texture = textures.Get(component.LookupName);
|
||||||
|
|
||||||
if (texture == null)
|
if (texture == null)
|
||||||
return null;
|
return new SpriteNotFound(component.LookupName);
|
||||||
|
|
||||||
return new Sprite { Texture = texture };
|
return new Sprite { Texture = texture };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool UsesFixedAnchor { get; set; }
|
||||||
|
|
||||||
private class SpriteComponent : ISkinComponent
|
private class SpriteComponent : ISkinComponent
|
||||||
{
|
{
|
||||||
|
public string LookupName { get; set; }
|
||||||
|
|
||||||
public SpriteComponent(string textureName)
|
public SpriteComponent(string textureName)
|
||||||
{
|
{
|
||||||
LookupName = textureName;
|
LookupName = textureName;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public string LookupName { get; }
|
public class SpriteSelectorControl : SettingsDropdown<string>
|
||||||
|
{
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
// Round-about way of getting the user's skin to find available resources.
|
||||||
|
// In the future we'll probably want to allow access to resources from the fallbacks, or potentially other skins
|
||||||
|
// but that requires further thought.
|
||||||
|
var highestPrioritySkin = getHighestPriorityUserSkin(((SkinnableSprite)SettingSourceObject).source.AllSources) as Skin;
|
||||||
|
|
||||||
|
string[] availableFiles = highestPrioritySkin?.SkinInfo.PerformRead(s => s.Files
|
||||||
|
.Where(f => f.Filename.EndsWith(".png", StringComparison.Ordinal)
|
||||||
|
|| f.Filename.EndsWith(".jpg", StringComparison.Ordinal))
|
||||||
|
.Select(f => f.Filename).Distinct()).ToArray();
|
||||||
|
|
||||||
|
if (availableFiles?.Length > 0)
|
||||||
|
Items = availableFiles;
|
||||||
|
|
||||||
|
static ISkin getHighestPriorityUserSkin(IEnumerable<ISkin> skins)
|
||||||
|
{
|
||||||
|
foreach (var skin in skins)
|
||||||
|
{
|
||||||
|
if (skin is LegacySkinTransformer transformer && isUserSkin(transformer.Skin))
|
||||||
|
return transformer.Skin;
|
||||||
|
|
||||||
|
if (isUserSkin(skin))
|
||||||
|
return skin;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporarily used to exclude undesirable ISkin implementations
|
||||||
|
static bool isUserSkin(ISkin skin)
|
||||||
|
=> skin.GetType() == typeof(DefaultSkin)
|
||||||
|
|| skin.GetType() == typeof(DefaultLegacySkin)
|
||||||
|
|| skin.GetType() == typeof(LegacySkin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SpriteNotFound : CompositeDrawable
|
||||||
|
{
|
||||||
|
public SpriteNotFound(string lookup)
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
new SpriteIcon
|
||||||
|
{
|
||||||
|
Size = new Vector2(50),
|
||||||
|
Icon = FontAwesome.Solid.QuestionCircle
|
||||||
|
},
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Position = new Vector2(25, 50),
|
||||||
|
Text = $"missing: {lookup}",
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,10 +22,13 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
|
|
||||||
protected abstract string ResourceAssembly { get; }
|
protected abstract string ResourceAssembly { get; }
|
||||||
|
|
||||||
protected void Test(double expected, string name, params Mod[] mods)
|
protected void Test(double expectedStarRating, int expectedMaxCombo, string name, params Mod[] mods)
|
||||||
{
|
{
|
||||||
|
var attributes = CreateDifficultyCalculator(getBeatmap(name)).Calculate(mods);
|
||||||
|
|
||||||
// Platform-dependent math functions (Pow, Cbrt, Exp, etc) may result in minute differences.
|
// Platform-dependent math functions (Pow, Cbrt, Exp, etc) may result in minute differences.
|
||||||
Assert.That(CreateDifficultyCalculator(getBeatmap(name)).Calculate(mods).StarRating, Is.EqualTo(expected).Within(0.00001));
|
Assert.That(attributes.StarRating, Is.EqualTo(expectedStarRating).Within(0.00001));
|
||||||
|
Assert.That(attributes.MaxCombo, Is.EqualTo(expectedMaxCombo));
|
||||||
}
|
}
|
||||||
|
|
||||||
private IWorkingBeatmap getBeatmap(string name)
|
private IWorkingBeatmap getBeatmap(string name)
|
||||||
|
@ -31,7 +31,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
public override IBindable<bool> IsConnected => isConnected;
|
public override IBindable<bool> IsConnected => isConnected;
|
||||||
private readonly Bindable<bool> isConnected = new Bindable<bool>(true);
|
private readonly Bindable<bool> isConnected = new Bindable<bool>(true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The local client's <see cref="Room"/>. This is not always equivalent to the server-side room.
|
||||||
|
/// </summary>
|
||||||
public new Room? APIRoom => base.APIRoom;
|
public new Room? APIRoom => base.APIRoom;
|
||||||
|
|
||||||
public Action<MultiplayerRoom>? RoomSetupAction;
|
public Action<MultiplayerRoom>? RoomSetupAction;
|
||||||
|
|
||||||
public bool RoomJoined { get; private set; }
|
public bool RoomJoined { get; private set; }
|
||||||
@ -46,6 +50,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly List<MultiplayerPlaylistItem> serverSidePlaylist = new List<MultiplayerPlaylistItem>();
|
private readonly List<MultiplayerPlaylistItem> serverSidePlaylist = new List<MultiplayerPlaylistItem>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Guaranteed up-to-date API room.
|
||||||
|
/// </summary>
|
||||||
|
private Room? serverSideAPIRoom;
|
||||||
|
|
||||||
private MultiplayerPlaylistItem? currentItem => Room?.Playlist[currentIndex];
|
private MultiplayerPlaylistItem? currentItem => Room?.Playlist[currentIndex];
|
||||||
private int currentIndex;
|
private int currentIndex;
|
||||||
private long lastPlaylistItemId;
|
private long lastPlaylistItemId;
|
||||||
@ -192,13 +201,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
protected override async Task<MultiplayerRoom> JoinRoom(long roomId, string? password = null)
|
protected override async Task<MultiplayerRoom> JoinRoom(long roomId, string? password = null)
|
||||||
{
|
{
|
||||||
var apiRoom = roomManager.ServerSideRooms.Single(r => r.RoomID.Value == roomId);
|
serverSideAPIRoom = roomManager.ServerSideRooms.Single(r => r.RoomID.Value == roomId);
|
||||||
|
|
||||||
if (password != apiRoom.Password.Value)
|
if (password != serverSideAPIRoom.Password.Value)
|
||||||
throw new InvalidOperationException("Invalid password.");
|
throw new InvalidOperationException("Invalid password.");
|
||||||
|
|
||||||
serverSidePlaylist.Clear();
|
serverSidePlaylist.Clear();
|
||||||
serverSidePlaylist.AddRange(apiRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item)));
|
serverSidePlaylist.AddRange(serverSideAPIRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item)));
|
||||||
lastPlaylistItemId = serverSidePlaylist.Max(item => item.ID);
|
lastPlaylistItemId = serverSidePlaylist.Max(item => item.ID);
|
||||||
|
|
||||||
var localUser = new MultiplayerRoomUser(api.LocalUser.Value.Id)
|
var localUser = new MultiplayerRoomUser(api.LocalUser.Value.Id)
|
||||||
@ -210,11 +219,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
Settings =
|
Settings =
|
||||||
{
|
{
|
||||||
Name = apiRoom.Name.Value,
|
Name = serverSideAPIRoom.Name.Value,
|
||||||
MatchType = apiRoom.Type.Value,
|
MatchType = serverSideAPIRoom.Type.Value,
|
||||||
Password = password,
|
Password = password,
|
||||||
QueueMode = apiRoom.QueueMode.Value,
|
QueueMode = serverSideAPIRoom.QueueMode.Value,
|
||||||
AutoStartDuration = apiRoom.AutoStartDuration.Value
|
AutoStartDuration = serverSideAPIRoom.AutoStartDuration.Value
|
||||||
},
|
},
|
||||||
Playlist = serverSidePlaylist.ToList(),
|
Playlist = serverSidePlaylist.ToList(),
|
||||||
Users = { localUser },
|
Users = { localUser },
|
||||||
@ -449,8 +458,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
public async Task EditUserPlaylistItem(int userId, MultiplayerPlaylistItem item)
|
public async Task EditUserPlaylistItem(int userId, MultiplayerPlaylistItem item)
|
||||||
{
|
{
|
||||||
Debug.Assert(Room != null);
|
Debug.Assert(Room != null);
|
||||||
Debug.Assert(APIRoom != null);
|
|
||||||
Debug.Assert(currentItem != null);
|
Debug.Assert(currentItem != null);
|
||||||
|
Debug.Assert(serverSideAPIRoom != null);
|
||||||
|
|
||||||
item.OwnerID = userId;
|
item.OwnerID = userId;
|
||||||
|
|
||||||
@ -469,6 +478,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
item.PlaylistOrder = existingItem.PlaylistOrder;
|
item.PlaylistOrder = existingItem.PlaylistOrder;
|
||||||
|
|
||||||
serverSidePlaylist[serverSidePlaylist.IndexOf(existingItem)] = item;
|
serverSidePlaylist[serverSidePlaylist.IndexOf(existingItem)] = item;
|
||||||
|
serverSideAPIRoom.Playlist[serverSideAPIRoom.Playlist.IndexOf(serverSideAPIRoom.Playlist.Single(i => i.ID == item.ID))] = new PlaylistItem(item);
|
||||||
|
|
||||||
await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false);
|
await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@ -479,6 +489,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
Debug.Assert(Room != null);
|
Debug.Assert(Room != null);
|
||||||
Debug.Assert(APIRoom != null);
|
Debug.Assert(APIRoom != null);
|
||||||
|
Debug.Assert(serverSideAPIRoom != null);
|
||||||
|
|
||||||
var item = serverSidePlaylist.Find(i => i.ID == playlistItemId);
|
var item = serverSidePlaylist.Find(i => i.ID == playlistItemId);
|
||||||
|
|
||||||
@ -495,6 +506,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
throw new InvalidOperationException("Attempted to remove an item which has already been played.");
|
throw new InvalidOperationException("Attempted to remove an item which has already been played.");
|
||||||
|
|
||||||
serverSidePlaylist.Remove(item);
|
serverSidePlaylist.Remove(item);
|
||||||
|
serverSideAPIRoom.Playlist.RemoveAll(i => i.ID == item.ID);
|
||||||
await ((IMultiplayerClient)this).PlaylistItemRemoved(playlistItemId).ConfigureAwait(false);
|
await ((IMultiplayerClient)this).PlaylistItemRemoved(playlistItemId).ConfigureAwait(false);
|
||||||
|
|
||||||
await updateCurrentItem(Room).ConfigureAwait(false);
|
await updateCurrentItem(Room).ConfigureAwait(false);
|
||||||
@ -576,10 +588,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
private async Task addItem(MultiplayerPlaylistItem item)
|
private async Task addItem(MultiplayerPlaylistItem item)
|
||||||
{
|
{
|
||||||
Debug.Assert(Room != null);
|
Debug.Assert(Room != null);
|
||||||
|
Debug.Assert(serverSideAPIRoom != null);
|
||||||
|
|
||||||
item.ID = ++lastPlaylistItemId;
|
item.ID = ++lastPlaylistItemId;
|
||||||
|
|
||||||
serverSidePlaylist.Add(item);
|
serverSidePlaylist.Add(item);
|
||||||
|
serverSideAPIRoom.Playlist.Add(new PlaylistItem(item));
|
||||||
await ((IMultiplayerClient)this).PlaylistItemAdded(item).ConfigureAwait(false);
|
await ((IMultiplayerClient)this).PlaylistItemAdded(item).ConfigureAwait(false);
|
||||||
|
|
||||||
await updatePlaylistOrder(Room).ConfigureAwait(false);
|
await updatePlaylistOrder(Room).ConfigureAwait(false);
|
||||||
@ -603,6 +617,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
private async Task updatePlaylistOrder(MultiplayerRoom room)
|
private async Task updatePlaylistOrder(MultiplayerRoom room)
|
||||||
{
|
{
|
||||||
|
Debug.Assert(serverSideAPIRoom != null);
|
||||||
|
|
||||||
List<MultiplayerPlaylistItem> orderedActiveItems;
|
List<MultiplayerPlaylistItem> orderedActiveItems;
|
||||||
|
|
||||||
switch (room.Settings.QueueMode)
|
switch (room.Settings.QueueMode)
|
||||||
@ -648,6 +664,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false);
|
await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also ensure that the API room's playlist is correct.
|
||||||
|
foreach (var item in serverSideAPIRoom.Playlist)
|
||||||
|
item.PlaylistOrder = serverSidePlaylist.Single(i => i.ID == item.ID).PlaylistOrder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Realm" Version="10.10.0" />
|
<PackageReference Include="Realm" Version="10.10.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2022.405.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2022.405.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.405.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.14.1" />
|
<PackageReference Include="Sentry" Version="3.14.1" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.30.1" />
|
<PackageReference Include="SharpCompress" Version="0.30.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||||
|
@ -62,7 +62,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.405.0" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.405.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.405.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
|
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
Loading…
Reference in New Issue
Block a user