1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-22 11:27:19 +08:00

Merge branch 'master' into mania-shadow-icon-changes

This commit is contained in:
Bartłomiej Dach 2023-03-20 19:33:52 +01:00 committed by GitHub
commit b5e04791e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 659 additions and 156 deletions

View File

@ -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>

View File

@ -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" />

View File

@ -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");

View File

@ -71,12 +71,23 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
return base.GetDrawableComponent(lookup);
}
private static readonly Color4 colour_special_column = new Color4(169, 106, 255, 255);
private const int total_colours = 6;
private static readonly Color4 colour_yellow = new Color4(255, 197, 40, 255);
private static readonly Color4 colour_orange = new Color4(252, 109, 1, 255);
private static readonly Color4 colour_pink = new Color4(213, 35, 90, 255);
private static readonly Color4 colour_purple = new Color4(203, 60, 236, 255);
private static readonly Color4 colour_cyan = new Color4(72, 198, 255, 255);
private static readonly Color4 colour_green = new Color4(100, 192, 92, 255);
public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
{
if (lookup is ManiaSkinConfigurationLookup maniaLookup)
{
int column = maniaLookup.ColumnIndex ?? 0;
var stage = beatmap.GetStageForColumnIndex(column);
int columnIndex = maniaLookup.ColumnIndex ?? 0;
var stage = beatmap.GetStageForColumnIndex(columnIndex);
switch (maniaLookup.Lookup)
{
@ -89,53 +100,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case LegacyManiaSkinConfigurationLookups.ColumnWidth:
return SkinUtils.As<TValue>(new Bindable<float>(
stage.IsSpecialColumn(column) ? 120 : 60
stage.IsSpecialColumn(columnIndex) ? 120 : 60
));
case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:
Color4 colour;
const int total_colours = 7;
if (stage.IsSpecialColumn(column))
colour = new Color4(159, 101, 255, 255);
else
{
switch (column % total_colours)
{
case 0:
colour = new Color4(240, 216, 0, 255);
break;
case 1:
colour = new Color4(240, 101, 0, 255);
break;
case 2:
colour = new Color4(240, 0, 130, 255);
break;
case 3:
colour = new Color4(192, 0, 240, 255);
break;
case 4:
colour = new Color4(0, 96, 240, 255);
break;
case 5:
colour = new Color4(0, 226, 240, 255);
break;
case 6:
colour = new Color4(0, 240, 96, 255);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
var colour = getColourForLayout(columnIndex, stage);
return SkinUtils.As<TValue>(new Bindable<Color4>(colour));
}
@ -143,5 +113,203 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
return base.GetConfig<TLookup, TValue>(lookup);
}
private Color4 getColourForLayout(int columnIndex, StageDefinition stage)
{
// Account for cases like dual-stage (assume that all stages have the same column count for now).
columnIndex %= stage.Columns;
// For now, these are defined per column count as per https://user-images.githubusercontent.com/50823728/218038463-b450f46c-ef21-4551-b133-f866be59970c.png
// See https://github.com/ppy/osu/discussions/21996 for discussion.
switch (stage.Columns)
{
case 1:
return colour_yellow;
case 2:
switch (columnIndex)
{
case 0: return colour_green;
case 1: return colour_cyan;
default: throw new ArgumentOutOfRangeException();
}
case 3:
switch (columnIndex)
{
case 0: return colour_pink;
case 1: return colour_orange;
case 2: return colour_yellow;
default: throw new ArgumentOutOfRangeException();
}
case 4:
switch (columnIndex)
{
case 0: return colour_yellow;
case 1: return colour_orange;
case 2: return colour_pink;
case 3: return colour_purple;
default: throw new ArgumentOutOfRangeException();
}
case 5:
switch (columnIndex)
{
case 0: return colour_pink;
case 1: return colour_orange;
case 2: return colour_yellow;
case 3: return colour_green;
case 4: return colour_cyan;
default: throw new ArgumentOutOfRangeException();
}
case 6:
switch (columnIndex)
{
case 0: return colour_pink;
case 1: return colour_orange;
case 2: return colour_yellow;
case 3: return colour_cyan;
case 4: return colour_purple;
case 5: return colour_pink;
default: throw new ArgumentOutOfRangeException();
}
case 7:
switch (columnIndex)
{
case 0: return colour_pink;
case 1: return colour_cyan;
case 2: return colour_pink;
case 3: return colour_special_column;
case 4: return colour_green;
case 5: return colour_cyan;
case 6: return colour_green;
default: throw new ArgumentOutOfRangeException();
}
case 8:
switch (columnIndex)
{
case 0: return colour_purple;
case 1: return colour_pink;
case 2: return colour_orange;
case 3: return colour_yellow;
case 4: return colour_yellow;
case 5: return colour_orange;
case 6: return colour_pink;
case 7: return colour_purple;
default: throw new ArgumentOutOfRangeException();
}
case 9:
switch (columnIndex)
{
case 0: return colour_purple;
case 1: return colour_pink;
case 2: return colour_orange;
case 3: return colour_yellow;
case 4: return colour_special_column;
case 5: return colour_yellow;
case 6: return colour_orange;
case 7: return colour_pink;
case 8: return colour_purple;
default: throw new ArgumentOutOfRangeException();
}
case 10:
switch (columnIndex)
{
case 0: return colour_purple;
case 1: return colour_pink;
case 2: return colour_orange;
case 3: return colour_yellow;
case 4: return colour_cyan;
case 5: return colour_green;
case 6: return colour_yellow;
case 7: return colour_orange;
case 8: return colour_pink;
case 9: return colour_purple;
default: throw new ArgumentOutOfRangeException();
}
}
// fallback for unhandled scenarios
if (stage.IsSpecialColumn(columnIndex))
return colour_special_column;
switch (columnIndex % total_colours)
{
case 0: return colour_yellow;
case 1: return colour_orange;
case 2: return colour_pink;
case 3: return colour_purple;
case 4: return colour_cyan;
case 5: return colour_green;
default: throw new ArgumentOutOfRangeException();
}
}
}
}

View File

@ -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;
}
}

View File

@ -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">

View File

@ -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,

View File

@ -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; }

View File

@ -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 =>
{

View File

@ -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();
}
}
}
}

View File

@ -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

View File

@ -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),

View File

@ -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;
}
}

View 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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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");

View File

@ -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;
}

View File

@ -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!;

View File

@ -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

View File

@ -242,7 +242,6 @@ namespace osu.Game.Screens.Play.HUD
{
length = value;
mask.Width = value * DrawWidth;
fill.Width = value * DrawWidth;
}
}

View File

@ -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,35 +755,6 @@ namespace osu.Game.Screens.Play
progressToResults(true);
}
/// <summary>
/// Asynchronously run score preparation operations (database import, online submission etc.).
/// </summary>
/// <returns>The final score.</returns>
private async Task<ScoreInfo> prepareAndImportScore()
{
var scoreCopy = Score.DeepClone();
try
{
await PrepareScoreForResultsAsync(scoreCopy).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Error(ex, @"Score preparation failed!");
}
try
{
await ImportScore(scoreCopy).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Error(ex, @"Score import failed!");
}
return scoreCopy.ScoreInfo;
}
/// <summary>
/// Queue the results screen for display.
/// </summary>
@ -800,12 +770,25 @@ namespace osu.Game.Screens.Play
resultsDisplayDelegate = new ScheduledDelegate(() =>
{
if (prepareScoreForDisplayTask?.IsCompleted != true)
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;
@ -816,6 +799,51 @@ namespace osu.Game.Screens.Play
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>
[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();
try
{
await PrepareScoreForResultsAsync(scoreCopy).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Error(ex, @"Score preparation failed!");
}
try
{
await ImportScore(scoreCopy).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Error(ex, @"Score import failed!");
}
return scoreCopy.ScoreInfo;
});
}
protected override bool OnScroll(ScrollEvent e)
{
// During pause, allow global volume adjust regardless of settings.

View File

@ -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);

View File

@ -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);

View File

@ -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}]";
}

View File

@ -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" />

View File

@ -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>