mirror of
https://github.com/ppy/osu.git
synced 2024-11-08 02:59:48 +08:00
Merge branch 'master' into update-mania-argon-colours
This commit is contained in:
commit
e30e312efb
@ -1,7 +1,7 @@
|
||||
<!-- Contains required properties for osu!framework projects. -->
|
||||
<Project>
|
||||
<PropertyGroup Label="C#">
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<LangVersion>10.0</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
@ -11,7 +11,7 @@
|
||||
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.228.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.314.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />
|
||||
|
@ -5,7 +5,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
@ -139,7 +138,17 @@ namespace osu.Desktop
|
||||
|
||||
desktopWindow.CursorState |= CursorState.Hidden;
|
||||
desktopWindow.Title = Name;
|
||||
desktopWindow.DragDrop += f => fileDrop(new[] { f });
|
||||
desktopWindow.DragDrop += f =>
|
||||
{
|
||||
// on macOS, URL associations are handled via SDL_DROPFILE events.
|
||||
if (f.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal))
|
||||
{
|
||||
HandleLink(f);
|
||||
return;
|
||||
}
|
||||
|
||||
fileDrop(new[] { f });
|
||||
};
|
||||
}
|
||||
|
||||
protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo();
|
||||
@ -151,10 +160,6 @@ namespace osu.Desktop
|
||||
{
|
||||
lock (importableFiles)
|
||||
{
|
||||
string firstExtension = Path.GetExtension(filePaths.First());
|
||||
|
||||
if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return;
|
||||
|
||||
importableFiles.AddRange(filePaths);
|
||||
|
||||
Logger.Log($"Adding {filePaths.Length} files for import");
|
||||
|
@ -13,6 +13,7 @@ using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
using osu.Framework.Graphics.Shaders.Types;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
@ -255,15 +256,23 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
Source.parts.CopyTo(parts, 0);
|
||||
}
|
||||
|
||||
private IUniformBuffer<CursorTrailParameters> cursorTrailParameters;
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
|
||||
vertexBatch ??= renderer.CreateQuadBatch<TexturedTrailVertex>(max_sprites, 1);
|
||||
|
||||
cursorTrailParameters ??= renderer.CreateUniformBuffer<CursorTrailParameters>();
|
||||
cursorTrailParameters.Data = cursorTrailParameters.Data with
|
||||
{
|
||||
FadeClock = time,
|
||||
FadeExponent = fadeExponent
|
||||
};
|
||||
|
||||
shader.Bind();
|
||||
shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time);
|
||||
shader.GetUniform<float>("g_FadeExponent").UpdateValue(ref fadeExponent);
|
||||
shader.BindUniformBlock("m_CursorTrailParameters", cursorTrailParameters);
|
||||
|
||||
texture.Bind();
|
||||
|
||||
@ -323,6 +332,15 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
vertexBatch?.Dispose();
|
||||
cursorTrailParameters?.Dispose();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
private record struct CursorTrailParameters
|
||||
{
|
||||
public UniformFloat FadeClock;
|
||||
public UniformFloat FadeExponent;
|
||||
private readonly UniformPadding8 pad1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
<OutputType>Library</OutputType>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Description>click the circles. to the beat.</Description>
|
||||
<LangVersion>10</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Nuget">
|
||||
|
212
osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSingleTap.cs
Normal file
212
osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSingleTap.cs
Normal file
@ -0,0 +1,212 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Replays;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneTaikoModSingleTap : TaikoModTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestInputAlternate() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModSingleTap(),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Hit
|
||||
{
|
||||
StartTime = 100,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
StartTime = 300,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
StartTime = 500,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
StartTime = 700,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(100, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(120),
|
||||
new TaikoReplayFrame(300, TaikoAction.LeftRim),
|
||||
new TaikoReplayFrame(320),
|
||||
new TaikoReplayFrame(500, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(520),
|
||||
new TaikoReplayFrame(700, TaikoAction.LeftRim),
|
||||
new TaikoReplayFrame(720),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 1
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestInputSameKey() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModSingleTap(),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Hit
|
||||
{
|
||||
StartTime = 100,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
StartTime = 300,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
StartTime = 500,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
StartTime = 700,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(100, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(120),
|
||||
new TaikoReplayFrame(300, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(320),
|
||||
new TaikoReplayFrame(500, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(520),
|
||||
new TaikoReplayFrame(700, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(720),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 4
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestInputIntro() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModSingleTap(),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Hit
|
||||
{
|
||||
StartTime = 100,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(20),
|
||||
new TaikoReplayFrame(100, TaikoAction.LeftRim),
|
||||
new TaikoReplayFrame(120),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestInputStrong() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModSingleTap(),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Hit
|
||||
{
|
||||
StartTime = 100,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
StartTime = 300,
|
||||
Type = HitType.Rim,
|
||||
IsStrong = true
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
StartTime = 500,
|
||||
Type = HitType.Rim,
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(100, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(120),
|
||||
new TaikoReplayFrame(300, TaikoAction.LeftRim),
|
||||
new TaikoReplayFrame(320),
|
||||
new TaikoReplayFrame(500, TaikoAction.LeftRim),
|
||||
new TaikoReplayFrame(520),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestInputBreaks() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModSingleTap(),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
Breaks = new List<BreakPeriod>
|
||||
{
|
||||
new BreakPeriod(100, 1600),
|
||||
},
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Hit
|
||||
{
|
||||
StartTime = 100,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
StartTime = 2000,
|
||||
Type = HitType.Rim,
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(100, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(120),
|
||||
// Press different key after break but before hit object.
|
||||
new TaikoReplayFrame(1900, TaikoAction.LeftRim),
|
||||
new TaikoReplayFrame(1820),
|
||||
// Press original key at second hitobject and ensure it has been hit.
|
||||
new TaikoReplayFrame(2000, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(2020),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2
|
||||
});
|
||||
}
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Replays;
|
||||
@ -12,5 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
=> new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" });
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModSingleTap) }).ToArray();
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
@ -13,5 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
=> new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" });
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModSingleTap) }).ToArray();
|
||||
}
|
||||
}
|
||||
|
@ -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.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
@ -9,5 +11,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
public class TaikoModRelax : ModRelax
|
||||
{
|
||||
public override LocalisableString Description => @"No ninja-like spinners, demanding drumrolls or unexpected katus.";
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModSingleTap) }).ToArray();
|
||||
}
|
||||
}
|
||||
|
127
osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs
Normal file
127
osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs
Normal file
@ -0,0 +1,127 @@
|
||||
// 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.Localisation;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Utils;
|
||||
using osu.Game.Rulesets.Taiko.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public partial class TaikoModSingleTap : Mod, IApplicableToDrawableRuleset<TaikoHitObject>, IUpdatableByPlayfield
|
||||
{
|
||||
public override string Name => @"Single Tap";
|
||||
public override string Acronym => @"SG";
|
||||
public override LocalisableString Description => @"One key for dons, one key for kats.";
|
||||
|
||||
public override double ScoreMultiplier => 1.0;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(TaikoModCinema) };
|
||||
public override ModType Type => ModType.Conversion;
|
||||
|
||||
private DrawableTaikoRuleset ruleset = null!;
|
||||
|
||||
private TaikoPlayfield playfield { get; set; } = null!;
|
||||
|
||||
private TaikoAction? lastAcceptedCentreAction { get; set; }
|
||||
private TaikoAction? lastAcceptedRimAction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A tracker for periods where single tap should not be enforced (i.e. non-gameplay periods).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is different from <see cref="Player.IsBreakTime"/> in that the periods here end strictly at the first object after the break, rather than the break's end time.
|
||||
/// </remarks>
|
||||
private PeriodTracker nonGameplayPeriods = null!;
|
||||
|
||||
private IFrameStableClock gameplayClock = null!;
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
|
||||
{
|
||||
ruleset = (DrawableTaikoRuleset)drawableRuleset;
|
||||
ruleset.KeyBindingInputManager.Add(new InputInterceptor(this));
|
||||
playfield = (TaikoPlayfield)ruleset.Playfield;
|
||||
|
||||
var periods = new List<Period>();
|
||||
|
||||
if (drawableRuleset.Objects.Any())
|
||||
{
|
||||
periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
|
||||
|
||||
foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
|
||||
periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
|
||||
|
||||
static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
|
||||
}
|
||||
|
||||
nonGameplayPeriods = new PeriodTracker(periods);
|
||||
|
||||
gameplayClock = drawableRuleset.FrameStableClock;
|
||||
}
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
if (!nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime)) return;
|
||||
|
||||
lastAcceptedCentreAction = null;
|
||||
lastAcceptedRimAction = null;
|
||||
}
|
||||
|
||||
private bool checkCorrectAction(TaikoAction action)
|
||||
{
|
||||
if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
|
||||
return true;
|
||||
|
||||
// If next hit object is strong, allow usage of all actions. Strong drumrolls are ignored in this check.
|
||||
if (playfield.HitObjectContainer.AliveObjects.FirstOrDefault(h => h.Result?.HasResult != true)?.HitObject is TaikoStrongableHitObject hitObject
|
||||
&& hitObject.IsStrong
|
||||
&& hitObject is not DrumRoll)
|
||||
return true;
|
||||
|
||||
if ((action == TaikoAction.LeftCentre || action == TaikoAction.RightCentre)
|
||||
&& (lastAcceptedCentreAction == null || lastAcceptedCentreAction == action))
|
||||
{
|
||||
lastAcceptedCentreAction = action;
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((action == TaikoAction.LeftRim || action == TaikoAction.RightRim)
|
||||
&& (lastAcceptedRimAction == null || lastAcceptedRimAction == action))
|
||||
{
|
||||
lastAcceptedRimAction = action;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private partial class InputInterceptor : Component, IKeyBindingHandler<TaikoAction>
|
||||
{
|
||||
private readonly TaikoModSingleTap mod;
|
||||
|
||||
public InputInterceptor(TaikoModSingleTap mod)
|
||||
{
|
||||
this.mod = mod;
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
|
||||
// if the pressed action is incorrect, block it from reaching gameplay.
|
||||
=> !mod.checkCorrectAction(e.Action);
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<TaikoAction> e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -158,6 +158,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
new TaikoModDifficultyAdjust(),
|
||||
new TaikoModClassic(),
|
||||
new TaikoModSwap(),
|
||||
new TaikoModSingleTap(),
|
||||
};
|
||||
|
||||
case ModType.Automation:
|
||||
|
@ -33,6 +33,8 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true);
|
||||
|
||||
public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
|
||||
|
||||
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
|
||||
|
||||
protected override bool UserScrollSpeedAdjustment => false;
|
||||
|
125
osu.Game.Tests/Database/LegacyExporterTest.cs
Normal file
125
osu.Game.Tests/Database/LegacyExporterTest.cs
Normal file
@ -0,0 +1,125 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
|
||||
namespace osu.Game.Tests.Database
|
||||
{
|
||||
[TestFixture]
|
||||
public class LegacyExporterTest
|
||||
{
|
||||
private TestLegacyExporter legacyExporter = null!;
|
||||
private TemporaryNativeStorage storage = null!;
|
||||
|
||||
private const string short_filename = "normal file name";
|
||||
|
||||
private const string long_filename =
|
||||
"some file with super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name";
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
storage = new TemporaryNativeStorage("export-storage");
|
||||
legacyExporter = new TestLegacyExporter(storage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExportFileWithNormalNameTest()
|
||||
{
|
||||
var item = new TestPathInfo(short_filename);
|
||||
|
||||
Assert.That(item.Filename.Length, Is.LessThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
|
||||
|
||||
exportItemAndAssert(item, short_filename);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExportFileWithNormalNameMultipleTimesTest()
|
||||
{
|
||||
var item = new TestPathInfo(short_filename);
|
||||
|
||||
Assert.That(item.Filename.Length, Is.LessThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
|
||||
|
||||
//Export multiple times
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
string expectedFileName = i == 0 ? short_filename : $"{short_filename} ({i})";
|
||||
exportItemAndAssert(item, expectedFileName);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExportFileWithSuperLongNameTest()
|
||||
{
|
||||
int expectedLength = TestLegacyExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length);
|
||||
string expectedName = long_filename.Remove(expectedLength);
|
||||
|
||||
var item = new TestPathInfo(long_filename);
|
||||
|
||||
Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
|
||||
exportItemAndAssert(item, expectedName);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExportFileWithSuperLongNameMultipleTimesTest()
|
||||
{
|
||||
int expectedLength = TestLegacyExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length);
|
||||
string expectedName = long_filename.Remove(expectedLength);
|
||||
|
||||
var item = new TestPathInfo(long_filename);
|
||||
|
||||
Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
|
||||
|
||||
//Export multiple times
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
string expectedFilename = i == 0 ? expectedName : $"{expectedName} ({i})";
|
||||
exportItemAndAssert(item, expectedFilename);
|
||||
}
|
||||
}
|
||||
|
||||
private void exportItemAndAssert(IHasNamedFiles item, string expectedName)
|
||||
{
|
||||
Assert.DoesNotThrow(() => legacyExporter.Export(item));
|
||||
Assert.That(storage.Exists($"exports/{expectedName}{legacyExporter.GetExtension()}"), Is.True);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (storage.IsNotNull())
|
||||
storage.Dispose();
|
||||
}
|
||||
|
||||
private class TestPathInfo : IHasNamedFiles
|
||||
{
|
||||
public string Filename { get; }
|
||||
|
||||
public IEnumerable<INamedFileUsage> Files { get; } = new List<INamedFileUsage>();
|
||||
|
||||
public TestPathInfo(string filename)
|
||||
{
|
||||
Filename = filename;
|
||||
}
|
||||
|
||||
public override string ToString() => Filename;
|
||||
}
|
||||
|
||||
private class TestLegacyExporter : LegacyExporter<IHasNamedFiles>
|
||||
{
|
||||
public TestLegacyExporter(Storage storage)
|
||||
: base(storage)
|
||||
{
|
||||
}
|
||||
|
||||
public string GetExtension() => FileExtension;
|
||||
|
||||
protected override string FileExtension => ".test";
|
||||
}
|
||||
}
|
||||
}
|
@ -176,6 +176,7 @@ namespace osu.Game.Tests.Resources
|
||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
},
|
||||
BeatmapInfo = beatmap,
|
||||
BeatmapHash = beatmap.Hash,
|
||||
Ruleset = beatmap.Ruleset,
|
||||
Mods = new Mod[] { new TestModHardRock(), new TestModDoubleTime() },
|
||||
TotalScore = 2845370,
|
||||
|
@ -80,7 +80,7 @@ namespace osu.Game.Tests.Rulesets
|
||||
|
||||
dependencies.CacheAs<TextureStore>(ParentTextureStore = new TestTextureStore(parent.Get<GameHost>().Renderer));
|
||||
dependencies.CacheAs<ISampleStore>(ParentSampleStore = new TestSampleStore());
|
||||
dependencies.CacheAs<ShaderManager>(ParentShaderManager = new TestShaderManager(parent.Get<GameHost>().Renderer));
|
||||
dependencies.CacheAs<ShaderManager>(ParentShaderManager = new TestShaderManager(parent.Get<GameHost>().Renderer, parent.Get<ShaderManager>()));
|
||||
|
||||
return new DrawableRulesetDependencies(new OsuRuleset(), dependencies);
|
||||
}
|
||||
@ -156,12 +156,15 @@ namespace osu.Game.Tests.Rulesets
|
||||
|
||||
private class TestShaderManager : ShaderManager
|
||||
{
|
||||
public TestShaderManager(IRenderer renderer)
|
||||
private readonly ShaderManager parentManager;
|
||||
|
||||
public TestShaderManager(IRenderer renderer, ShaderManager parentManager)
|
||||
: base(renderer, new ResourceStore<byte[]>())
|
||||
{
|
||||
this.parentManager = parentManager;
|
||||
}
|
||||
|
||||
public override byte[] LoadRaw(string name) => null;
|
||||
public override byte[] LoadRaw(string name) => parentManager.LoadRaw(name);
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
|
@ -133,6 +133,25 @@ namespace osu.Game.Tests.Skins.IO
|
||||
assertImportedOnce(import1, import2);
|
||||
});
|
||||
|
||||
[Test]
|
||||
public Task TestImportExportedNonAsciiSkinFilename() => runSkinTest(async osu =>
|
||||
{
|
||||
MemoryStream exportStream = new MemoryStream();
|
||||
|
||||
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 『1』", "author 1"), "custom.osk"));
|
||||
assertCorrectMetadata(import1, "name 『1』 [custom]", "author 1", osu);
|
||||
|
||||
import1.PerformRead(s =>
|
||||
{
|
||||
new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportModelTo(s, exportStream);
|
||||
});
|
||||
|
||||
string exportFilename = import1.GetDisplayString().GetValidFilename();
|
||||
|
||||
var import2 = await loadSkinIntoOsu(osu, new ImportTask(exportStream, $"{exportFilename}.osk"));
|
||||
assertCorrectMetadata(import2, "name 『1』 [custom]", "author 1", osu);
|
||||
});
|
||||
|
||||
[Test]
|
||||
public Task TestSameMetadataNameSameFolderName([Values] bool batchImport) => runSkinTest(async osu =>
|
||||
{
|
||||
|
@ -9,6 +9,7 @@ using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Background
|
||||
{
|
||||
@ -97,15 +98,29 @@ namespace osu.Game.Tests.Visual.Background
|
||||
texelSize = Source.texelSize;
|
||||
}
|
||||
|
||||
private IUniformBuffer<TriangleBorderData>? borderDataBuffer;
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
{
|
||||
TextureShader.GetUniform<float>("thickness").UpdateValue(ref thickness);
|
||||
TextureShader.GetUniform<float>("texelSize").UpdateValue(ref texelSize);
|
||||
borderDataBuffer ??= renderer.CreateUniformBuffer<TriangleBorderData>();
|
||||
borderDataBuffer.Data = borderDataBuffer.Data with
|
||||
{
|
||||
Thickness = thickness,
|
||||
TexelSize = texelSize
|
||||
};
|
||||
|
||||
TextureShader.BindUniformBlock("m_BorderData", borderDataBuffer);
|
||||
|
||||
base.Draw(renderer);
|
||||
}
|
||||
|
||||
protected override bool CanDrawOpaqueInterior => false;
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
borderDataBuffer?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,16 +84,80 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
});
|
||||
|
||||
clearScores();
|
||||
checkCount(0);
|
||||
checkDisplayedCount(0);
|
||||
|
||||
loadMoreScores(() => beatmapInfo);
|
||||
checkCount(10);
|
||||
importMoreScores(() => beatmapInfo);
|
||||
checkDisplayedCount(10);
|
||||
|
||||
loadMoreScores(() => beatmapInfo);
|
||||
checkCount(20);
|
||||
importMoreScores(() => beatmapInfo);
|
||||
checkDisplayedCount(20);
|
||||
|
||||
clearScores();
|
||||
checkCount(0);
|
||||
checkDisplayedCount(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalScoresDisplayOnBeatmapEdit()
|
||||
{
|
||||
BeatmapInfo beatmapInfo = null!;
|
||||
string originalHash = string.Empty;
|
||||
|
||||
AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local);
|
||||
|
||||
AddStep(@"Import beatmap", () =>
|
||||
{
|
||||
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
|
||||
|
||||
leaderboard.BeatmapInfo = beatmapInfo;
|
||||
});
|
||||
|
||||
clearScores();
|
||||
checkDisplayedCount(0);
|
||||
|
||||
AddStep(@"Perform initial save to guarantee stable hash", () =>
|
||||
{
|
||||
IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap;
|
||||
beatmapManager.Save(beatmapInfo, beatmap);
|
||||
|
||||
originalHash = beatmapInfo.Hash;
|
||||
});
|
||||
|
||||
importMoreScores(() => beatmapInfo);
|
||||
|
||||
checkDisplayedCount(10);
|
||||
checkStoredCount(10);
|
||||
|
||||
AddStep(@"Save with changes", () =>
|
||||
{
|
||||
IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap;
|
||||
beatmap.Difficulty.ApproachRate = 12;
|
||||
beatmapManager.Save(beatmapInfo, beatmap);
|
||||
});
|
||||
|
||||
AddAssert("Hash changed", () => beatmapInfo.Hash, () => Is.Not.EqualTo(originalHash));
|
||||
checkDisplayedCount(0);
|
||||
checkStoredCount(10);
|
||||
|
||||
importMoreScores(() => beatmapInfo);
|
||||
importMoreScores(() => beatmapInfo);
|
||||
checkDisplayedCount(20);
|
||||
checkStoredCount(30);
|
||||
|
||||
AddStep(@"Revert changes", () =>
|
||||
{
|
||||
IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap;
|
||||
beatmap.Difficulty.ApproachRate = 8;
|
||||
beatmapManager.Save(beatmapInfo, beatmap);
|
||||
});
|
||||
|
||||
AddAssert("Hash restored", () => beatmapInfo.Hash, () => Is.EqualTo(originalHash));
|
||||
checkDisplayedCount(10);
|
||||
checkStoredCount(30);
|
||||
|
||||
clearScores();
|
||||
checkDisplayedCount(0);
|
||||
checkStoredCount(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -162,9 +226,9 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
});
|
||||
}
|
||||
|
||||
private void loadMoreScores(Func<BeatmapInfo> beatmapInfo)
|
||||
private void importMoreScores(Func<BeatmapInfo> beatmapInfo)
|
||||
{
|
||||
AddStep(@"Load new scores via manager", () =>
|
||||
AddStep(@"Import new scores", () =>
|
||||
{
|
||||
foreach (var score in generateSampleScores(beatmapInfo()))
|
||||
scoreManager.Import(score);
|
||||
@ -176,8 +240,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddStep("Clear all scores", () => scoreManager.Delete());
|
||||
}
|
||||
|
||||
private void checkCount(int expected) =>
|
||||
AddUntilStep("Correct count displayed", () => leaderboard.ChildrenOfType<LeaderboardScore>().Count() == expected);
|
||||
private void checkDisplayedCount(int expected) =>
|
||||
AddUntilStep($"{expected} scores displayed", () => leaderboard.ChildrenOfType<LeaderboardScore>().Count(), () => Is.EqualTo(expected));
|
||||
|
||||
private void checkStoredCount(int expected) =>
|
||||
AddUntilStep($"Total scores stored is {expected}", () => Realm.Run(r => r.All<ScoreInfo>().Count(s => !s.DeletePending)), () => Is.EqualTo(expected));
|
||||
|
||||
private static ScoreInfo[] generateSampleScores(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
@ -210,6 +277,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
},
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
BeatmapInfo = beatmapInfo,
|
||||
BeatmapHash = beatmapInfo.Hash,
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 6602580,
|
||||
@ -226,6 +294,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
Date = DateTime.Now.AddSeconds(-30),
|
||||
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
|
||||
BeatmapInfo = beatmapInfo,
|
||||
BeatmapHash = beatmapInfo.Hash,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
User = new APIUser
|
||||
{
|
||||
@ -243,6 +312,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
Date = DateTime.Now.AddSeconds(-70),
|
||||
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
|
||||
BeatmapInfo = beatmapInfo,
|
||||
BeatmapHash = beatmapInfo.Hash,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
|
||||
User = new APIUser
|
||||
@ -261,6 +331,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
Date = DateTime.Now.AddMinutes(-40),
|
||||
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
|
||||
BeatmapInfo = beatmapInfo,
|
||||
BeatmapHash = beatmapInfo.Hash,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
|
||||
User = new APIUser
|
||||
@ -279,6 +350,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
Date = DateTime.Now.AddHours(-2),
|
||||
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
|
||||
BeatmapInfo = beatmapInfo,
|
||||
BeatmapHash = beatmapInfo.Hash,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
|
||||
User = new APIUser
|
||||
@ -297,6 +369,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
Date = DateTime.Now.AddHours(-25),
|
||||
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
|
||||
BeatmapInfo = beatmapInfo,
|
||||
BeatmapHash = beatmapInfo.Hash,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
|
||||
User = new APIUser
|
||||
@ -315,6 +388,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
Date = DateTime.Now.AddHours(-50),
|
||||
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
|
||||
BeatmapInfo = beatmapInfo,
|
||||
BeatmapHash = beatmapInfo.Hash,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
|
||||
User = new APIUser
|
||||
@ -333,6 +407,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
Date = DateTime.Now.AddHours(-72),
|
||||
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
|
||||
BeatmapInfo = beatmapInfo,
|
||||
BeatmapHash = beatmapInfo.Hash,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
|
||||
User = new APIUser
|
||||
@ -351,6 +426,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
Date = DateTime.Now.AddMonths(-3),
|
||||
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
|
||||
BeatmapInfo = beatmapInfo,
|
||||
BeatmapHash = beatmapInfo.Hash,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
|
||||
User = new APIUser
|
||||
@ -369,6 +445,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
Date = DateTime.Now.AddYears(-2),
|
||||
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
|
||||
BeatmapInfo = beatmapInfo,
|
||||
BeatmapHash = beatmapInfo.Hash,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
|
||||
User = new APIUser
|
||||
|
@ -94,6 +94,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
OnlineID = i,
|
||||
BeatmapInfo = beatmapInfo,
|
||||
BeatmapHash = beatmapInfo.Hash,
|
||||
Accuracy = RNG.NextDouble(),
|
||||
TotalScore = RNG.Next(1, 1000000),
|
||||
MaxCombo = RNG.Next(1, 1000),
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -19,6 +20,19 @@ namespace osu.Game.Database
|
||||
public abstract class LegacyExporter<TModel>
|
||||
where TModel : class, IHasNamedFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Max length of filename (including extension).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The filename limit for most OSs is 255. This actual usable length is smaller because <see cref="Storage.CreateFileSafely(string)"/> adds an additional "_<see cref="Guid"/>" to the end of the path.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For more information see <see href="https://www.ibm.com/docs/en/spectrum-protect/8.1.9?topic=parameters-file-specification-syntax">file specification syntax</see>, <seealso href="https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits">file systems limitations</seealso>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public const int MAX_FILENAME_LENGTH = 255 - (32 + 4 + 2 + 5); //max path - (Guid + Guid "D" format chars + Storage.CreateFileSafely chars + account for ' (99)' suffix)
|
||||
|
||||
/// <summary>
|
||||
/// The file extension for exports (including the leading '.').
|
||||
/// </summary>
|
||||
@ -44,12 +58,16 @@ namespace osu.Game.Database
|
||||
{
|
||||
string itemFilename = GetFilename(item).GetValidFilename();
|
||||
|
||||
if (itemFilename.Length > MAX_FILENAME_LENGTH - FileExtension.Length)
|
||||
itemFilename = itemFilename.Remove(MAX_FILENAME_LENGTH - FileExtension.Length);
|
||||
|
||||
IEnumerable<string> existingExports =
|
||||
exportStorage
|
||||
.GetFiles(string.Empty, $"{itemFilename}*{FileExtension}")
|
||||
.Concat(exportStorage.GetDirectories(string.Empty));
|
||||
|
||||
string filename = NamingUtils.GetNextBestFilename(existingExports, $"{itemFilename}{FileExtension}");
|
||||
|
||||
using (var stream = exportStorage.CreateFileSafely(filename))
|
||||
ExportModelTo(item, stream);
|
||||
|
||||
|
@ -70,8 +70,9 @@ namespace osu.Game.Database
|
||||
/// 23 2022-08-01 Added LastLocalUpdate to BeatmapInfo.
|
||||
/// 24 2022-08-22 Added MaximumStatistics to ScoreInfo.
|
||||
/// 25 2022-09-18 Remove skins to add with new naming.
|
||||
/// 26 2023-02-05 Added BeatmapHash to ScoreInfo.
|
||||
/// </summary>
|
||||
private const int schema_version = 25;
|
||||
private const int schema_version = 26;
|
||||
|
||||
/// <summary>
|
||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||
@ -866,6 +867,15 @@ namespace osu.Game.Database
|
||||
// Remove the default skins so they can be added back by SkinManager with updated naming.
|
||||
migration.NewRealm.RemoveRange(migration.NewRealm.All<SkinInfo>().Where(s => s.Protected));
|
||||
break;
|
||||
|
||||
case 26:
|
||||
// Add ScoreInfo.BeatmapHash property to ensure scores correspond to the correct version of beatmap.
|
||||
var scores = migration.NewRealm.All<ScoreInfo>();
|
||||
|
||||
foreach (var score in scores)
|
||||
score.BeatmapHash = score.BeatmapInfo.Hash;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
16
osu.Game/Graphics/Backgrounds/TriangleBorderData.cs
Normal file
16
osu.Game/Graphics/Backgrounds/TriangleBorderData.cs
Normal file
@ -0,0 +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 System.Runtime.InteropServices;
|
||||
using osu.Framework.Graphics.Shaders.Types;
|
||||
|
||||
namespace osu.Game.Graphics.Backgrounds
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public record struct TriangleBorderData
|
||||
{
|
||||
public UniformFloat Thickness;
|
||||
public UniformFloat TexelSize;
|
||||
private readonly UniformPadding8 pad1;
|
||||
}
|
||||
}
|
@ -252,7 +252,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
|
||||
private class TrianglesDrawNode : DrawNode
|
||||
{
|
||||
private float fill = 1f;
|
||||
private const float fill = 1f;
|
||||
|
||||
protected new Triangles Source => (Triangles)base.Source;
|
||||
|
||||
@ -284,6 +284,8 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
parts.AddRange(Source.parts);
|
||||
}
|
||||
|
||||
private IUniformBuffer<TriangleBorderData> borderDataBuffer;
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
@ -294,14 +296,17 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
vertexBatch = renderer.CreateQuadBatch<TexturedVertex2D>(Source.AimCount, 1);
|
||||
}
|
||||
|
||||
// Due to triangles having various sizes we would need to set a different "texelSize" value for each of them, which is insanely expensive, thus we should use one single value.
|
||||
// texelSize computed for an average triangle (size 100) will result in big triangles becoming blurry, so we may just use 0 for all of them.
|
||||
// But we still need to specify at least something, because otherwise other shader usages will override this value.
|
||||
float texelSize = 0f;
|
||||
borderDataBuffer ??= renderer.CreateUniformBuffer<TriangleBorderData>();
|
||||
borderDataBuffer.Data = borderDataBuffer.Data with
|
||||
{
|
||||
Thickness = fill,
|
||||
// Due to triangles having various sizes we would need to set a different "TexelSize" value for each of them, which is insanely expensive, thus we should use one single value.
|
||||
// TexelSize computed for an average triangle (size 100) will result in big triangles becoming blurry, so we may just use 0 for all of them.
|
||||
TexelSize = 0
|
||||
};
|
||||
|
||||
shader.Bind();
|
||||
shader.GetUniform<float>("thickness").UpdateValue(ref fill);
|
||||
shader.GetUniform<float>("texelSize").UpdateValue(ref texelSize);
|
||||
shader.BindUniformBlock("m_BorderData", borderDataBuffer);
|
||||
|
||||
foreach (TriangleParticle particle in parts)
|
||||
{
|
||||
@ -352,6 +357,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
vertexBatch?.Dispose();
|
||||
borderDataBuffer?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -226,6 +226,8 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
parts.AddRange(Source.parts);
|
||||
}
|
||||
|
||||
private IUniformBuffer<TriangleBorderData>? borderDataBuffer;
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
@ -239,9 +241,15 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
vertexBatch = renderer.CreateQuadBatch<TexturedVertex2D>(Source.AimCount, 1);
|
||||
}
|
||||
|
||||
borderDataBuffer ??= renderer.CreateUniformBuffer<TriangleBorderData>();
|
||||
borderDataBuffer.Data = borderDataBuffer.Data with
|
||||
{
|
||||
Thickness = thickness,
|
||||
TexelSize = texelSize
|
||||
};
|
||||
|
||||
shader.Bind();
|
||||
shader.GetUniform<float>("thickness").UpdateValue(ref thickness);
|
||||
shader.GetUniform<float>("texelSize").UpdateValue(ref texelSize);
|
||||
shader.BindUniformBlock("m_BorderData", borderDataBuffer);
|
||||
|
||||
Vector2 relativeSize = Vector2.Divide(triangleSize, size);
|
||||
|
||||
@ -289,6 +297,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
vertexBatch?.Dispose();
|
||||
borderDataBuffer?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,11 +3,18 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
using osu.Framework.Graphics.Shaders.Types;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Graphics.ES30;
|
||||
|
||||
namespace osu.Game.Graphics.Sprites
|
||||
{
|
||||
@ -16,7 +23,7 @@ namespace osu.Game.Graphics.Sprites
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ShaderManager shaders)
|
||||
{
|
||||
TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LogoAnimation");
|
||||
TextureShader = shaders.Load(@"LogoAnimation", @"LogoAnimation");
|
||||
}
|
||||
|
||||
private float animationProgress;
|
||||
@ -41,11 +48,22 @@ namespace osu.Game.Graphics.Sprites
|
||||
{
|
||||
private LogoAnimation source => (LogoAnimation)Source;
|
||||
|
||||
private readonly Action<TexturedVertex2D> addVertexAction;
|
||||
|
||||
private float progress;
|
||||
|
||||
public LogoAnimationDrawNode(LogoAnimation source)
|
||||
: base(source)
|
||||
{
|
||||
addVertexAction = v =>
|
||||
{
|
||||
animationVertexBatch!.Add(new LogoAnimationVertex
|
||||
{
|
||||
Position = v.Position,
|
||||
Colour = v.Colour,
|
||||
TexturePosition = v.TexturePosition,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
public override void ApplyState()
|
||||
@ -55,14 +73,62 @@ namespace osu.Game.Graphics.Sprites
|
||||
progress = source.animationProgress;
|
||||
}
|
||||
|
||||
private IUniformBuffer<AnimationData> animationDataBuffer;
|
||||
private IVertexBatch<LogoAnimationVertex> animationVertexBatch;
|
||||
|
||||
protected override void Blit(IRenderer renderer)
|
||||
{
|
||||
TextureShader.GetUniform<float>("progress").UpdateValue(ref progress);
|
||||
if (DrawRectangle.Width == 0 || DrawRectangle.Height == 0)
|
||||
return;
|
||||
|
||||
base.Blit(renderer);
|
||||
animationDataBuffer ??= renderer.CreateUniformBuffer<AnimationData>();
|
||||
animationVertexBatch ??= renderer.CreateQuadBatch<LogoAnimationVertex>(1, 2);
|
||||
|
||||
animationDataBuffer.Data = animationDataBuffer.Data with { Progress = progress };
|
||||
|
||||
TextureShader.BindUniformBlock("m_AnimationData", animationDataBuffer);
|
||||
|
||||
renderer.DrawQuad(
|
||||
Texture,
|
||||
ScreenSpaceDrawQuad,
|
||||
DrawColourInfo.Colour,
|
||||
inflationPercentage: new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height),
|
||||
textureCoords: TextureCoords,
|
||||
vertexAction: addVertexAction);
|
||||
}
|
||||
|
||||
protected override bool CanDrawOpaqueInterior => false;
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
animationDataBuffer?.Dispose();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
private record struct AnimationData
|
||||
{
|
||||
public UniformFloat Progress;
|
||||
private readonly UniformPadding12 pad1;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct LogoAnimationVertex : IEquatable<LogoAnimationVertex>, IVertex
|
||||
{
|
||||
[VertexMember(2, VertexAttribPointerType.Float)]
|
||||
public Vector2 Position;
|
||||
|
||||
[VertexMember(4, VertexAttribPointerType.Float)]
|
||||
public Color4 Colour;
|
||||
|
||||
[VertexMember(2, VertexAttribPointerType.Float)]
|
||||
public Vector2 TexturePosition;
|
||||
|
||||
public readonly bool Equals(LogoAnimationVertex other) =>
|
||||
Position.Equals(other.Position)
|
||||
&& TexturePosition.Equals(other.TexturePosition)
|
||||
&& Colour.Equals(other.Colour);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
@ -39,7 +40,7 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Right = -3 }, // Compensate for scrollbar margin
|
||||
Child = new OsuScrollContainer
|
||||
Child = new SidebarScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new Container
|
||||
@ -74,5 +75,30 @@ namespace osu.Game.Overlays
|
||||
|
||||
[NotNull]
|
||||
protected virtual Drawable CreateContent() => Empty();
|
||||
|
||||
private partial class SidebarScrollContainer : OsuScrollContainer
|
||||
{
|
||||
protected override bool OnScroll(ScrollEvent e)
|
||||
{
|
||||
if (e.ScrollDelta.Y > 0 && IsScrolledToStart())
|
||||
return false;
|
||||
|
||||
if (e.ScrollDelta.Y < 0 && IsScrolledToEnd())
|
||||
return false;
|
||||
|
||||
return base.OnScroll(e);
|
||||
}
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
if (e.Delta.Y > 0 && IsScrolledToStart())
|
||||
return false;
|
||||
|
||||
if (e.Delta.Y < 0 && IsScrolledToEnd())
|
||||
return false;
|
||||
|
||||
return base.OnDragStart(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,15 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osuTK;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Framework.Localisation;
|
||||
@ -52,7 +49,6 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
|
||||
{
|
||||
private readonly OsuSpriteText valueText;
|
||||
protected readonly LinkFlowContainer DescriptionText;
|
||||
private readonly Box lineBackground;
|
||||
|
||||
public new int Count
|
||||
{
|
||||
@ -63,25 +59,14 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Padding = new MarginPadding { Top = 10, Bottom = 20 };
|
||||
Padding = new MarginPadding { Bottom = 20 };
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 5),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new CircularContainer
|
||||
{
|
||||
Masking = true,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 2,
|
||||
Child = lineBackground = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = header,
|
||||
@ -91,7 +76,6 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
|
||||
{
|
||||
Text = "0",
|
||||
Font = OsuFont.GetFont(size: 40, weight: FontWeight.Light),
|
||||
UseFullGlyphHeight = false,
|
||||
},
|
||||
DescriptionText = new LinkFlowContainer(t => t.Font = t.Font.With(size: 14))
|
||||
{
|
||||
@ -101,12 +85,6 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
lineBackground.Colour = colourProvider.Highlight1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Wiki.Markdown
|
||||
{
|
||||
@ -19,24 +20,30 @@ namespace osu.Game.Overlays.Wiki.Markdown
|
||||
{
|
||||
private readonly bool isOutdated;
|
||||
private readonly bool needsCleanup;
|
||||
private readonly bool isStub;
|
||||
|
||||
public WikiNoticeContainer(YamlFrontMatterBlock yamlFrontMatterBlock)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Direction = FillDirection.Vertical;
|
||||
Spacing = new Vector2(10);
|
||||
|
||||
foreach (object line in yamlFrontMatterBlock.Lines)
|
||||
{
|
||||
switch (line.ToString())
|
||||
{
|
||||
case "outdated: true":
|
||||
case @"outdated: true":
|
||||
isOutdated = true;
|
||||
break;
|
||||
|
||||
case "needs_cleanup: true":
|
||||
case @"needs_cleanup: true":
|
||||
needsCleanup = true;
|
||||
break;
|
||||
|
||||
case @"stub: true":
|
||||
isStub = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -60,6 +67,14 @@ namespace osu.Game.Overlays.Wiki.Markdown
|
||||
Text = WikiStrings.ShowNeedsCleanupOrRewrite,
|
||||
});
|
||||
}
|
||||
|
||||
if (isStub)
|
||||
{
|
||||
Add(new NoticeBox
|
||||
{
|
||||
Text = WikiStrings.ShowStub,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private partial class NoticeBox : Container
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -9,6 +10,7 @@ using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
using osu.Framework.Graphics.Shaders.Types;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
@ -245,6 +247,8 @@ namespace osu.Game.Rulesets.Mods
|
||||
flashlightSmoothness = Source.flashlightSmoothness;
|
||||
}
|
||||
|
||||
private IUniformBuffer<FlashlightParameters>? flashlightParametersBuffer;
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
@ -259,12 +263,17 @@ namespace osu.Game.Rulesets.Mods
|
||||
});
|
||||
}
|
||||
|
||||
shader.Bind();
|
||||
flashlightParametersBuffer ??= renderer.CreateUniformBuffer<FlashlightParameters>();
|
||||
flashlightParametersBuffer.Data = flashlightParametersBuffer.Data with
|
||||
{
|
||||
Position = flashlightPosition,
|
||||
Size = flashlightSize,
|
||||
Dim = flashlightDim,
|
||||
Smoothness = flashlightSmoothness
|
||||
};
|
||||
|
||||
shader.GetUniform<Vector2>("flashlightPos").UpdateValue(ref flashlightPosition);
|
||||
shader.GetUniform<Vector2>("flashlightSize").UpdateValue(ref flashlightSize);
|
||||
shader.GetUniform<float>("flashlightDim").UpdateValue(ref flashlightDim);
|
||||
shader.GetUniform<float>("flashlightSmoothness").UpdateValue(ref flashlightSmoothness);
|
||||
shader.Bind();
|
||||
shader.BindUniformBlock("m_FlashlightParameters", flashlightParametersBuffer);
|
||||
|
||||
renderer.DrawQuad(renderer.WhitePixel, screenSpaceDrawQuad, DrawColourInfo.Colour, vertexAction: addAction);
|
||||
|
||||
@ -275,6 +284,17 @@ namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
quadBatch?.Dispose();
|
||||
flashlightParametersBuffer?.Dispose();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
private record struct FlashlightParameters
|
||||
{
|
||||
public UniformVector2 Position;
|
||||
public UniformVector2 Size;
|
||||
public UniformFloat Dim;
|
||||
public UniformFloat Smoothness;
|
||||
private readonly UniformPadding8 pad1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,9 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value == recorder)
|
||||
return;
|
||||
|
||||
if (value != null && recorder != null)
|
||||
throw new InvalidOperationException("Cannot attach more than one recorder");
|
||||
|
||||
|
@ -123,6 +123,7 @@ namespace osu.Game.Scoring.Legacy
|
||||
// before returning for database import, we must restore the database-sourced BeatmapInfo.
|
||||
// if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception.
|
||||
score.ScoreInfo.BeatmapInfo = workingBeatmap.BeatmapInfo;
|
||||
score.ScoreInfo.BeatmapHash = workingBeatmap.BeatmapInfo.Hash;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
@ -22,6 +22,9 @@ using Realms;
|
||||
|
||||
namespace osu.Game.Scoring
|
||||
{
|
||||
/// <summary>
|
||||
/// A realm model containing metadata for a single score.
|
||||
/// </summary>
|
||||
[ExcludeFromDynamicCompile]
|
||||
[MapTo("Score")]
|
||||
public class ScoreInfo : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable<ScoreInfo>, IScoreInfo
|
||||
@ -29,8 +32,19 @@ namespace osu.Game.Scoring
|
||||
[PrimaryKey]
|
||||
public Guid ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="BeatmapInfo"/> this score was made against.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When setting this, make sure to also set <see cref="BeatmapHash"/> to allow relational consistency when a beatmap is potentially changed.
|
||||
/// </remarks>
|
||||
public BeatmapInfo BeatmapInfo { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="osu.Game.Beatmaps.BeatmapInfo.Hash"/> at the point in time when the score was set.
|
||||
/// </summary>
|
||||
public string BeatmapHash { get; set; } = string.Empty;
|
||||
|
||||
public RulesetInfo Ruleset { get; set; } = null!;
|
||||
|
||||
public IList<RealmNamedFileUsage> Files { get; } = null!;
|
||||
|
@ -114,7 +114,7 @@ namespace osu.Game.Screens.Play
|
||||
Anchor = Anchor.Centre,
|
||||
FillMode = FillMode.Fill,
|
||||
},
|
||||
loading = new LoadingLayer(true)
|
||||
loading = new LoadingLayer(dimBackground: true, blockInput: false)
|
||||
}
|
||||
},
|
||||
versionFlow = new FillFlowContainer
|
||||
|
@ -242,7 +242,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
length = value;
|
||||
mask.Width = value * DrawWidth;
|
||||
fill.Width = value * DrawWidth;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -248,6 +248,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
// ensure the score is in a consistent state with the current player.
|
||||
Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo;
|
||||
Score.ScoreInfo.BeatmapHash = Beatmap.Value.BeatmapInfo.Hash;
|
||||
Score.ScoreInfo.Ruleset = ruleset.RulesetInfo;
|
||||
Score.ScoreInfo.Mods = gameplayMods;
|
||||
|
||||
@ -276,7 +277,7 @@ namespace osu.Game.Screens.Play
|
||||
},
|
||||
FailOverlay = new FailOverlay
|
||||
{
|
||||
SaveReplay = prepareAndImportScore,
|
||||
SaveReplay = async () => await prepareAndImportScoreAsync(true).ConfigureAwait(false),
|
||||
OnRetry = () => Restart(),
|
||||
OnQuit = () => PerformExit(true),
|
||||
},
|
||||
@ -613,6 +614,9 @@ namespace osu.Game.Screens.Play
|
||||
// if an exit has been requested, cancel any pending completion (the user has shown intention to exit).
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
|
||||
// import current score if possible.
|
||||
prepareAndImportScoreAsync();
|
||||
|
||||
// The actual exit is performed if
|
||||
// - the pause / fail dialog was not requested
|
||||
// - the pause / fail dialog was requested but is already displayed (user showing intention to exit).
|
||||
@ -735,14 +739,9 @@ namespace osu.Game.Screens.Play
|
||||
// is no chance that a user could return to the (already completed) Player instance from a child screen.
|
||||
ValidForResume = false;
|
||||
|
||||
// Ensure we are not writing to the replay any more, as we are about to consume and store the score.
|
||||
DrawableRuleset.SetRecordTarget(null);
|
||||
|
||||
if (!Configuration.ShowResults)
|
||||
return;
|
||||
|
||||
prepareScoreForDisplayTask ??= Task.Run(prepareAndImportScore);
|
||||
|
||||
bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
|
||||
|
||||
if (storyboardHasOutro)
|
||||
@ -756,11 +755,70 @@ namespace osu.Game.Screens.Play
|
||||
progressToResults(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue the results screen for display.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A final display will only occur once all work is completed in <see cref="PrepareScoreForResultsAsync"/>. This means that even after calling this method, the results screen will never be shown until <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="true"/>.
|
||||
/// </remarks>
|
||||
/// <param name="withDelay">Whether a minimum delay (<see cref="RESULTS_DISPLAY_DELAY"/>) should be added before the screen is displayed.</param>
|
||||
private void progressToResults(bool withDelay)
|
||||
{
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
|
||||
double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0;
|
||||
|
||||
resultsDisplayDelegate = new ScheduledDelegate(() =>
|
||||
{
|
||||
if (prepareScoreForDisplayTask == null)
|
||||
{
|
||||
// Try importing score since the task hasn't been invoked yet.
|
||||
prepareAndImportScoreAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!prepareScoreForDisplayTask.IsCompleted)
|
||||
// If the asynchronous preparation has not completed, keep repeating this delegate.
|
||||
return;
|
||||
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
|
||||
if (prepareScoreForDisplayTask.GetResultSafely() == null)
|
||||
{
|
||||
// If score import did not occur, we do not want to show the results screen.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.IsCurrentScreen())
|
||||
// This player instance may already be in the process of exiting.
|
||||
return;
|
||||
|
||||
this.Push(CreateResults(prepareScoreForDisplayTask.GetResultSafely()));
|
||||
}, Time.Current + delay, 50);
|
||||
|
||||
Scheduler.Add(resultsDisplayDelegate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously run score preparation operations (database import, online submission etc.).
|
||||
/// </summary>
|
||||
/// <param name="forceImport">Whether the score should be imported even if non-passing (or the current configuration doesn't allow for it).</param>
|
||||
/// <returns>The final score.</returns>
|
||||
private async Task<ScoreInfo> prepareAndImportScore()
|
||||
[ItemCanBeNull]
|
||||
private Task<ScoreInfo> prepareAndImportScoreAsync(bool forceImport = false)
|
||||
{
|
||||
// Ensure we are not writing to the replay any more, as we are about to consume and store the score.
|
||||
DrawableRuleset.SetRecordTarget(null);
|
||||
|
||||
if (prepareScoreForDisplayTask != null)
|
||||
return prepareScoreForDisplayTask;
|
||||
|
||||
// We do not want to import the score in cases where we don't show results
|
||||
bool canShowResults = Configuration.ShowResults && ScoreProcessor.HasCompleted.Value && GameplayState.HasPassed;
|
||||
if (!canShowResults && !forceImport)
|
||||
return Task.FromResult<ScoreInfo>(null);
|
||||
|
||||
return prepareScoreForDisplayTask = Task.Run(async () =>
|
||||
{
|
||||
var scoreCopy = Score.DeepClone();
|
||||
|
||||
@ -783,37 +841,7 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
|
||||
return scoreCopy.ScoreInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue the results screen for display.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A final display will only occur once all work is completed in <see cref="PrepareScoreForResultsAsync"/>. This means that even after calling this method, the results screen will never be shown until <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="true"/>.
|
||||
/// </remarks>
|
||||
/// <param name="withDelay">Whether a minimum delay (<see cref="RESULTS_DISPLAY_DELAY"/>) should be added before the screen is displayed.</param>
|
||||
private void progressToResults(bool withDelay)
|
||||
{
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
|
||||
double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0;
|
||||
|
||||
resultsDisplayDelegate = new ScheduledDelegate(() =>
|
||||
{
|
||||
if (prepareScoreForDisplayTask?.IsCompleted != true)
|
||||
// If the asynchronous preparation has not completed, keep repeating this delegate.
|
||||
return;
|
||||
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
|
||||
if (!this.IsCurrentScreen())
|
||||
// This player instance may already be in the process of exiting.
|
||||
return;
|
||||
|
||||
this.Push(CreateResults(prepareScoreForDisplayTask.GetResultSafely()));
|
||||
}, Time.Current + delay, 50);
|
||||
|
||||
Scheduler.Add(resultsDisplayDelegate);
|
||||
});
|
||||
}
|
||||
|
||||
protected override bool OnScroll(ScrollEvent e)
|
||||
|
@ -65,6 +65,7 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
r.All<ScoreInfo>()
|
||||
.Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0"
|
||||
+ $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1"
|
||||
+ $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}"
|
||||
+ $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2"
|
||||
+ $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmapInfo.ID, ruleset.Value.ShortName),
|
||||
localScoresChanged);
|
||||
|
@ -191,6 +191,7 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
|
||||
scoreSubscription = realm.RegisterForNotifications(r =>
|
||||
r.All<ScoreInfo>().Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0"
|
||||
+ $" AND {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}"
|
||||
+ $" AND {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1"
|
||||
+ $" AND {nameof(ScoreInfo.DeletePending)} == false"
|
||||
, beatmapInfo.ID, ruleset.Value.ShortName), localScoresChanged);
|
||||
|
@ -101,7 +101,8 @@ namespace osu.Game.Skinning
|
||||
// In both of these cases, the expectation from the user is that the filename or folder name is displayed somewhere to identify the skin.
|
||||
if (archiveName != item.Name
|
||||
// lazer exports use this format
|
||||
&& archiveName != item.GetDisplayString())
|
||||
// GetValidFilename accounts for skins with non-ASCII characters in the name that have been exported by lazer.
|
||||
&& archiveName != item.GetDisplayString().GetValidFilename())
|
||||
item.Name = @$"{item.Name} [{archiveName}]";
|
||||
}
|
||||
|
||||
|
@ -67,6 +67,7 @@ namespace osu.Game.Users
|
||||
{
|
||||
username.Anchor = Anchor.CentreLeft;
|
||||
username.Origin = Anchor.CentreLeft;
|
||||
username.UseFullGlyphHeight = false;
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -95,13 +96,23 @@ namespace osu.Game.Users
|
||||
}
|
||||
};
|
||||
|
||||
if (User.Groups != null)
|
||||
{
|
||||
details.Add(new GroupBadgeFlow
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
User = { Value = User }
|
||||
});
|
||||
}
|
||||
|
||||
if (User.IsSupporter)
|
||||
{
|
||||
details.Add(new SupporterIcon
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Height = 20,
|
||||
Height = 16,
|
||||
SupportLevel = User.SupportLevel
|
||||
});
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<OutputType>Library</OutputType>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<LangVersion>10</LangVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Nuget">
|
||||
<Title>osu!</Title>
|
||||
@ -35,8 +36,8 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="10.20.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2023.228.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.228.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2023.314.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.320.0" />
|
||||
<PackageReference Include="Sentry" Version="3.28.1" />
|
||||
<PackageReference Include="SharpCompress" Version="0.32.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
|
@ -16,6 +16,6 @@
|
||||
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.228.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.314.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
Loading…
Reference in New Issue
Block a user