1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 10:33:30 +08:00

Merge branch 'master' into osu-game-base-cleanup

This commit is contained in:
Dan Balasescu 2021-05-31 13:32:09 +09:00 committed by GitHub
commit 1ba607b3ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 737 additions and 229 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.524.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.528.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -161,13 +161,13 @@ namespace osu.Game.Rulesets.Catch
switch (result) switch (result)
{ {
case HitResult.LargeTickHit: case HitResult.LargeTickHit:
return "large droplet"; return "Large droplet";
case HitResult.SmallTickHit: case HitResult.SmallTickHit:
return "small droplet"; return "Small droplet";
case HitResult.LargeBonus: case HitResult.LargeBonus:
return "banana"; return "Banana";
} }
return base.GetDisplayNameForHitResult(result); return base.GetDisplayNameForHitResult(result);

View File

@ -28,53 +28,56 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{ {
case HUDSkinComponents.ComboCounter: case HUDSkinComponents.ComboCounter:
// catch may provide its own combo counter; hide the default. // catch may provide its own combo counter; hide the default.
return providesComboCounter ? Drawable.Empty() : null; if (providesComboCounter)
return Drawable.Empty();
break;
} }
} }
if (!(component is CatchSkinComponent catchSkinComponent)) if (component is CatchSkinComponent catchSkinComponent)
return null;
switch (catchSkinComponent.Component)
{ {
case CatchSkinComponents.Fruit: switch (catchSkinComponent.Component)
if (GetTexture("fruit-pear") != null) {
return new LegacyFruitPiece(); case CatchSkinComponents.Fruit:
if (GetTexture("fruit-pear") != null)
return new LegacyFruitPiece();
break; return null;
case CatchSkinComponents.Banana: case CatchSkinComponents.Banana:
if (GetTexture("fruit-bananas") != null) if (GetTexture("fruit-bananas") != null)
return new LegacyBananaPiece(); return new LegacyBananaPiece();
break; return null;
case CatchSkinComponents.Droplet: case CatchSkinComponents.Droplet:
if (GetTexture("fruit-drop") != null) if (GetTexture("fruit-drop") != null)
return new LegacyDropletPiece(); return new LegacyDropletPiece();
break; return null;
case CatchSkinComponents.CatcherIdle: case CatchSkinComponents.CatcherIdle:
return this.GetAnimation("fruit-catcher-idle", true, true, true) ?? return this.GetAnimation("fruit-catcher-idle", true, true, true) ??
this.GetAnimation("fruit-ryuuta", true, true, true); this.GetAnimation("fruit-ryuuta", true, true, true);
case CatchSkinComponents.CatcherFail: case CatchSkinComponents.CatcherFail:
return this.GetAnimation("fruit-catcher-fail", true, true, true) ?? return this.GetAnimation("fruit-catcher-fail", true, true, true) ??
this.GetAnimation("fruit-ryuuta", true, true, true); this.GetAnimation("fruit-ryuuta", true, true, true);
case CatchSkinComponents.CatcherKiai: case CatchSkinComponents.CatcherKiai:
return this.GetAnimation("fruit-catcher-kiai", true, true, true) ?? return this.GetAnimation("fruit-catcher-kiai", true, true, true) ??
this.GetAnimation("fruit-ryuuta", true, true, true); this.GetAnimation("fruit-ryuuta", true, true, true);
case CatchSkinComponents.CatchComboCounter: case CatchSkinComponents.CatchComboCounter:
if (providesComboCounter) if (providesComboCounter)
return new LegacyCatchComboCounter(Source); return new LegacyCatchComboCounter(Source);
break; return null;
}
} }
return null; return Source.GetDrawableComponent(component);
} }
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)

View File

@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
break; break;
} }
return null; return Source.GetDrawableComponent(component);
} }
private Drawable getResult(HitResult result) private Drawable getResult(HitResult result)

View File

@ -34,90 +34,90 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
public override Drawable GetDrawableComponent(ISkinComponent component) public override Drawable GetDrawableComponent(ISkinComponent component)
{ {
if (!(component is OsuSkinComponent osuComponent)) if (component is OsuSkinComponent osuComponent)
return null;
switch (osuComponent.Component)
{ {
case OsuSkinComponents.FollowPoint: switch (osuComponent.Component)
return this.GetAnimation(component.LookupName, true, false, true, startAtCurrentTime: false); {
case OsuSkinComponents.FollowPoint:
return this.GetAnimation(component.LookupName, true, false, true, startAtCurrentTime: false);
case OsuSkinComponents.SliderFollowCircle: case OsuSkinComponents.SliderFollowCircle:
var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true); var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true);
if (followCircle != null) if (followCircle != null)
// follow circles are 2x the hitcircle resolution in legacy skins (since they are scaled down from >1x // follow circles are 2x the hitcircle resolution in legacy skins (since they are scaled down from >1x
followCircle.Scale *= 0.5f; followCircle.Scale *= 0.5f;
return followCircle; return followCircle;
case OsuSkinComponents.SliderBall: case OsuSkinComponents.SliderBall:
var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: ""); var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "");
// todo: slider ball has a custom frame delay based on velocity // todo: slider ball has a custom frame delay based on velocity
// Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME); // Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME);
if (sliderBallContent != null) if (sliderBallContent != null)
return new LegacySliderBall(sliderBallContent); return new LegacySliderBall(sliderBallContent);
return null;
case OsuSkinComponents.SliderBody:
if (hasHitCircle.Value)
return new LegacySliderBody();
return null;
case OsuSkinComponents.SliderTailHitCircle:
if (hasHitCircle.Value)
return new LegacyMainCirclePiece("sliderendcircle", false);
return null;
case OsuSkinComponents.SliderHeadHitCircle:
if (hasHitCircle.Value)
return new LegacyMainCirclePiece("sliderstartcircle");
return null;
case OsuSkinComponents.HitCircle:
if (hasHitCircle.Value)
return new LegacyMainCirclePiece();
return null;
case OsuSkinComponents.Cursor:
if (Source.GetTexture("cursor") != null)
return new LegacyCursor();
return null;
case OsuSkinComponents.CursorTrail:
if (Source.GetTexture("cursortrail") != null)
return new LegacyCursorTrail();
return null;
case OsuSkinComponents.HitCircleText:
if (!this.HasFont(LegacyFont.HitCircle))
return null; return null;
return new LegacySpriteText(LegacyFont.HitCircle) case OsuSkinComponents.SliderBody:
{ if (hasHitCircle.Value)
// stable applies a blanket 0.8x scale to hitcircle fonts return new LegacySliderBody();
Scale = new Vector2(0.8f),
};
case OsuSkinComponents.SpinnerBody: return null;
bool hasBackground = Source.GetTexture("spinner-background") != null;
if (Source.GetTexture("spinner-top") != null && !hasBackground) case OsuSkinComponents.SliderTailHitCircle:
return new LegacyNewStyleSpinner(); if (hasHitCircle.Value)
else if (hasBackground) return new LegacyMainCirclePiece("sliderendcircle", false);
return new LegacyOldStyleSpinner();
return null; return null;
case OsuSkinComponents.SliderHeadHitCircle:
if (hasHitCircle.Value)
return new LegacyMainCirclePiece("sliderstartcircle");
return null;
case OsuSkinComponents.HitCircle:
if (hasHitCircle.Value)
return new LegacyMainCirclePiece();
return null;
case OsuSkinComponents.Cursor:
if (Source.GetTexture("cursor") != null)
return new LegacyCursor();
return null;
case OsuSkinComponents.CursorTrail:
if (Source.GetTexture("cursortrail") != null)
return new LegacyCursorTrail();
return null;
case OsuSkinComponents.HitCircleText:
if (!this.HasFont(LegacyFont.HitCircle))
return null;
return new LegacySpriteText(LegacyFont.HitCircle)
{
// stable applies a blanket 0.8x scale to hitcircle fonts
Scale = new Vector2(0.8f),
};
case OsuSkinComponents.SpinnerBody:
bool hasBackground = Source.GetTexture("spinner-background") != null;
if (Source.GetTexture("spinner-top") != null && !hasBackground)
return new LegacyNewStyleSpinner();
else if (hasBackground)
return new LegacyOldStyleSpinner();
return null;
}
} }
return null; return Source.GetDrawableComponent(component);
} }
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)

View File

@ -38,98 +38,98 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
return Drawable.Empty().With(d => d.Expire()); return Drawable.Empty().With(d => d.Expire());
} }
if (!(component is TaikoSkinComponent taikoComponent)) if (component is TaikoSkinComponent taikoComponent)
return null;
switch (taikoComponent.Component)
{ {
case TaikoSkinComponents.DrumRollBody: switch (taikoComponent.Component)
if (GetTexture("taiko-roll-middle") != null) {
return new LegacyDrumRoll(); case TaikoSkinComponents.DrumRollBody:
if (GetTexture("taiko-roll-middle") != null)
return new LegacyDrumRoll();
return null; return null;
case TaikoSkinComponents.InputDrum: case TaikoSkinComponents.InputDrum:
if (GetTexture("taiko-bar-left") != null) if (GetTexture("taiko-bar-left") != null)
return new LegacyInputDrum(); return new LegacyInputDrum();
return null; return null;
case TaikoSkinComponents.CentreHit: case TaikoSkinComponents.CentreHit:
case TaikoSkinComponents.RimHit: case TaikoSkinComponents.RimHit:
if (GetTexture("taikohitcircle") != null) if (GetTexture("taikohitcircle") != null)
return new LegacyHit(taikoComponent.Component); return new LegacyHit(taikoComponent.Component);
return null; return null;
case TaikoSkinComponents.DrumRollTick: case TaikoSkinComponents.DrumRollTick:
return this.GetAnimation("sliderscorepoint", false, false); return this.GetAnimation("sliderscorepoint", false, false);
case TaikoSkinComponents.HitTarget: case TaikoSkinComponents.HitTarget:
if (GetTexture("taikobigcircle") != null) if (GetTexture("taikobigcircle") != null)
return new TaikoLegacyHitTarget(); return new TaikoLegacyHitTarget();
return null; return null;
case TaikoSkinComponents.PlayfieldBackgroundRight: case TaikoSkinComponents.PlayfieldBackgroundRight:
if (GetTexture("taiko-bar-right") != null) if (GetTexture("taiko-bar-right") != null)
return new TaikoLegacyPlayfieldBackgroundRight(); return new TaikoLegacyPlayfieldBackgroundRight();
return null; return null;
case TaikoSkinComponents.PlayfieldBackgroundLeft: case TaikoSkinComponents.PlayfieldBackgroundLeft:
// This is displayed inside LegacyInputDrum. It is required to be there for layout purposes (can be seen on legacy skins). // This is displayed inside LegacyInputDrum. It is required to be there for layout purposes (can be seen on legacy skins).
if (GetTexture("taiko-bar-right") != null) if (GetTexture("taiko-bar-right") != null)
return Drawable.Empty(); return Drawable.Empty();
return null; return null;
case TaikoSkinComponents.BarLine: case TaikoSkinComponents.BarLine:
if (GetTexture("taiko-barline") != null) if (GetTexture("taiko-barline") != null)
return new LegacyBarLine(); return new LegacyBarLine();
return null; return null;
case TaikoSkinComponents.TaikoExplosionMiss: case TaikoSkinComponents.TaikoExplosionMiss:
var missSprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false); var missSprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
if (missSprite != null) if (missSprite != null)
return new LegacyHitExplosion(missSprite); return new LegacyHitExplosion(missSprite);
return null; return null;
case TaikoSkinComponents.TaikoExplosionOk: case TaikoSkinComponents.TaikoExplosionOk:
case TaikoSkinComponents.TaikoExplosionGreat: case TaikoSkinComponents.TaikoExplosionGreat:
var hitName = getHitName(taikoComponent.Component); var hitName = getHitName(taikoComponent.Component);
var hitSprite = this.GetAnimation(hitName, true, false); var hitSprite = this.GetAnimation(hitName, true, false);
if (hitSprite != null) if (hitSprite != null)
{ {
var strongHitSprite = this.GetAnimation($"{hitName}k", true, false); var strongHitSprite = this.GetAnimation($"{hitName}k", true, false);
return new LegacyHitExplosion(hitSprite, strongHitSprite); return new LegacyHitExplosion(hitSprite, strongHitSprite);
} }
return null; return null;
case TaikoSkinComponents.TaikoExplosionKiai: case TaikoSkinComponents.TaikoExplosionKiai:
// suppress the default kiai explosion if the skin brings its own sprites. // suppress the default kiai explosion if the skin brings its own sprites.
// the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield. // the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield.
if (hasExplosion.Value) if (hasExplosion.Value)
return Drawable.Empty().With(d => d.Expire()); return Drawable.Empty().With(d => d.Expire());
return null; return null;
case TaikoSkinComponents.Scroller: case TaikoSkinComponents.Scroller:
if (GetTexture("taiko-slider") != null) if (GetTexture("taiko-slider") != null)
return new LegacyTaikoScroller(); return new LegacyTaikoScroller();
return null; return null;
case TaikoSkinComponents.Mascot: case TaikoSkinComponents.Mascot:
return new DrawableTaikoMascot(); return new DrawableTaikoMascot();
}
} }
return Source.GetDrawableComponent(component); return Source.GetDrawableComponent(component);

View File

@ -169,6 +169,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
protected override Track GetBeatmapTrack() => throw new NotImplementedException(); protected override Track GetBeatmapTrack() => throw new NotImplementedException();
protected override ISkin GetSkin() => throw new NotImplementedException();
public override Stream GetStream(string storagePath) => throw new NotImplementedException(); public override Stream GetStream(string storagePath) => throw new NotImplementedException();
} }
} }

View File

@ -0,0 +1,130 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Lists;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osu.Game.Storyboards;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneBeatmapSkinFallbacks : OsuPlayerTestScene
{
private ISkin currentBeatmapSkin;
[Resolved]
private SkinManager skinManager { get; set; }
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor();
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
protected override bool HasCustomSteps => true;
[Test]
public void TestEmptyLegacyBeatmapSkinFallsBack()
{
CreateSkinTest(SkinInfo.Default, () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null));
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value));
}
protected void CreateSkinTest(SkinInfo gameCurrentSkin, Func<ISkin> getBeatmapSkin)
{
CreateTest(() =>
{
AddStep("setup skins", () =>
{
skinManager.CurrentSkinInfo.Value = gameCurrentSkin;
currentBeatmapSkin = getBeatmapSkin();
});
});
}
protected bool AssertComponentsFromExpectedSource(SkinnableTarget target, ISkin expectedSource)
{
var actualComponentsContainer = Player.ChildrenOfType<SkinnableTargetContainer>().First(s => s.Target == target)
.ChildrenOfType<SkinnableTargetComponentsContainer>().SingleOrDefault();
if (actualComponentsContainer == null)
return false;
var actualInfo = actualComponentsContainer.CreateSkinnableInfo();
var expectedComponentsContainer = (SkinnableTargetComponentsContainer)expectedSource.GetDrawableComponent(new SkinnableTargetComponent(target));
if (expectedComponentsContainer == null)
return false;
var expectedComponentsAdjustmentContainer = new Container
{
Position = actualComponentsContainer.Parent.ToSpaceOfOtherDrawable(actualComponentsContainer.DrawPosition, Content),
Size = actualComponentsContainer.DrawSize,
Child = expectedComponentsContainer,
};
Add(expectedComponentsAdjustmentContainer);
expectedComponentsAdjustmentContainer.UpdateSubTree();
var expectedInfo = expectedComponentsContainer.CreateSkinnableInfo();
Remove(expectedComponentsAdjustmentContainer);
return almostEqual(actualInfo, expectedInfo);
static bool almostEqual(SkinnableInfo info, SkinnableInfo other) =>
other != null
&& info.Type == other.Type
&& info.Anchor == other.Anchor
&& info.Origin == other.Origin
&& Precision.AlmostEquals(info.Position, other.Position)
&& Precision.AlmostEquals(info.Scale, other.Scale)
&& Precision.AlmostEquals(info.Rotation, other.Rotation)
&& info.Children.SequenceEqual(other.Children, new FuncEqualityComparer<SkinnableInfo>(almostEqual));
}
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
=> new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, Audio, currentBeatmapSkin);
protected override Ruleset CreatePlayerRuleset() => new TestOsuRuleset();
private class CustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap
{
private readonly ISkin beatmapSkin;
public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio, ISkin beatmapSkin)
: base(beatmap, storyboard, referenceClock, audio)
{
this.beatmapSkin = beatmapSkin;
}
protected override ISkin GetSkin() => beatmapSkin;
}
private class TestOsuRuleset : OsuRuleset
{
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new TestOsuLegacySkinTransformer(source);
private class TestOsuLegacySkinTransformer : OsuLegacySkinTransformer
{
public TestOsuLegacySkinTransformer(ISkinSource source)
: base(source)
{
}
}
}
}
}

View File

@ -0,0 +1,43 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays;
using osu.Game.Overlays.Wiki;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneWikiMainPage : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Orange);
public TestSceneWikiMainPage()
{
Children = new Drawable[]
{
new Box
{
Colour = overlayColour.Background5,
RelativeSizeAxes = Axes.Both,
},
new BasicScrollContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(20),
Child = new WikiMainPage
{
Markdown = main_page_markdown
}
}
};
}
// From https://osu.ppy.sh/api/v2/wiki/en/Main_Page
private const string main_page_markdown =
"---\nlayout: main_page\n---\n\n<!-- Do not add any empty lines inside this div. -->\n\n<div class=\"wiki-main-page__blurb\">\nWelcome to the osu! wiki, a project containing a wide range of osu! related information.\n</div>\n\n<div class=\"wiki-main-page__panels\">\n<div class=\"wiki-main-page-panel wiki-main-page-panel--full\">\n\n# Getting started\n\n[Welcome](/wiki/Welcome) • [Installation](/wiki/Installation) • [Registration](/wiki/Registration) • [Help Centre](/wiki/Help_Centre) • [FAQ](/wiki/FAQ)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# Game client\n\n[Interface](/wiki/Interface) • [Options](/wiki/Options) • [Visual settings](/wiki/Visual_Settings) • [Shortcut key reference](/wiki/Shortcut_key_reference) • [Configuration file](/wiki/osu!_Program_Files/User_Configuration_File) • [Program files](/wiki/osu!_Program_Files)\n\n[File formats](/wiki/osu!_File_Formats): [.osz](/wiki/osu!_File_Formats/Osz_(file_format)) • [.osk](/wiki/osu!_File_Formats/Osk_(file_format)) • [.osr](/wiki/osu!_File_Formats/Osr_(file_format)) • [.osu](/wiki/osu!_File_Formats/Osu_(file_format)) • [.osb](/wiki/osu!_File_Formats/Osb_(file_format)) • [.db](/wiki/osu!_File_Formats/Db_(file_format))\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# Gameplay\n\n[Game modes](/wiki/Game_mode): [osu!](/wiki/Game_mode/osu!) • [osu!taiko](/wiki/Game_mode/osu!taiko) • [osu!catch](/wiki/Game_mode/osu!catch) • [osu!mania](/wiki/Game_mode/osu!mania)\n\n[Beatmap](/wiki/Beatmap) • [Hit object](/wiki/Hit_object) • [Mods](/wiki/Game_modifier) • [Score](/wiki/Score) • [Replay](/wiki/Replay) • [Multi](/wiki/Multi)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# [Beatmap editor](/wiki/Beatmap_Editor)\n\nSections: [Compose](/wiki/Beatmap_Editor/Compose) • [Design](/wiki/Beatmap_Editor/Design) • [Timing](/wiki/Beatmap_Editor/Timing) • [Song setup](/wiki/Beatmap_Editor/Song_Setup)\n\nComponents: [AiMod](/wiki/Beatmap_Editor/AiMod) • [Beat snap divisor](/wiki/Beatmap_Editor/Beat_Snap_Divisor) • [Distance snap](/wiki/Beatmap_Editor/Distance_Snap) • [Menu](/wiki/Beatmap_Editor/Menu) • [SB load](/wiki/Beatmap_Editor/SB_Load) • [Timelines](/wiki/Beatmap_Editor/Timelines)\n\n[Beatmapping](/wiki/Beatmapping) • [Difficulty](/wiki/Beatmap/Difficulty) • [Mapping techniques](/wiki/Mapping_Techniques) • [Storyboarding](/wiki/Storyboarding)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# Beatmap submission and ranking\n\n[Submission](/wiki/Submission) • [Modding](/wiki/Modding) • [Ranking procedure](/wiki/Beatmap_ranking_procedure) • [Mappers' Guild](/wiki/Mappers_Guild) • [Project Loved](/wiki/Project_Loved)\n\n[Ranking criteria](/wiki/Ranking_Criteria): [osu!](/wiki/Ranking_Criteria/osu!) • [osu!taiko](/wiki/Ranking_Criteria/osu!taiko) • [osu!catch](/wiki/Ranking_Criteria/osu!catch) • [osu!mania](/wiki/Ranking_Criteria/osu!mania)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# Community\n\n[Tournaments](/wiki/Tournaments) • [Skinning](/wiki/Skinning) • [Projects](/wiki/Projects) • [Guides](/wiki/Guides) • [osu!dev Discord server](/wiki/osu!dev_Discord_server) • [How you can help](/wiki/How_You_Can_Help!) • [Glossary](/wiki/Glossary)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# People\n\n[The Team](/wiki/People/The_Team): [Developers](/wiki/People/The_Team/Developers) • [Global Moderation Team](/wiki/People/The_Team/Global_Moderation_Team) • [Support Team](/wiki/People/The_Team/Support_Team) • [Nomination Assessment Team](/wiki/People/The_Team/Nomination_Assessment_Team) • [Beatmap Nominators](/wiki/People/The_Team/Beatmap_Nominators) • [osu! Alumni](/wiki/People/The_Team/osu!_Alumni) • [Project Loved Team](/wiki/People/The_Team/Project_Loved_Team)\n\nOrganisations: [osu! UCI](/wiki/Organisations/osu!_UCI)\n\n[Community Contributors](/wiki/People/Community_Contributors) • [Users with unique titles](/wiki/People/Users_with_unique_titles)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# For developers\n\n[API](/wiki/osu!api) • [Bot account](/wiki/Bot_account) • [Brand identity guidelines](/wiki/Brand_identity_guidelines)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# About the wiki\n\n[Sitemap](/wiki/Sitemap) • [Contribution guide](/wiki/osu!_wiki_Contribution_Guide) • [Article styling criteria](/wiki/Article_Styling_Criteria) • [News styling criteria](/wiki/News_Styling_Criteria)\n\n</div>\n</div>\n";
}
}

View File

@ -358,7 +358,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target)); AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target));
// this is an important check, to make sure updateComponentFromBeatmap() was actually run // this is an important check, to make sure updateComponentFromBeatmap() was actually run
AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo == target); AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo.Equals(target));
} }
[Test] [Test]
@ -390,7 +390,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("has correct ruleset", () => Ruleset.Value.ID == 0); AddUntilStep("has correct ruleset", () => Ruleset.Value.ID == 0);
// this is an important check, to make sure updateComponentFromBeatmap() was actually run // this is an important check, to make sure updateComponentFromBeatmap() was actually run
AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo == target); AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo.Equals(target));
} }
[Test] [Test]
@ -781,7 +781,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3); AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3);
AddAssert("Check first item in group selected", () => Beatmap.Value.BeatmapInfo == groupIcon.Items.First().Beatmap); AddAssert("Check first item in group selected", () => Beatmap.Value.BeatmapInfo.Equals(groupIcon.Items.First().Beatmap));
} }
[Test] [Test]

View File

@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
@ -11,13 +13,20 @@ namespace osu.Game.Tests.Visual.UserInterface
[TestFixture] [TestFixture]
public class TestSceneDialogOverlay : OsuTestScene public class TestSceneDialogOverlay : OsuTestScene
{ {
public TestSceneDialogOverlay() private DialogOverlay overlay;
[SetUpSteps]
public void SetUpSteps()
{ {
DialogOverlay overlay; AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
}
Add(overlay = new DialogOverlay()); [Test]
public void TestBasic()
{
TestPopupDialog dialog = null;
AddStep("dialog #1", () => overlay.Push(new TestPopupDialog AddStep("dialog #1", () => overlay.Push(dialog = new TestPopupDialog
{ {
Icon = FontAwesome.Regular.TrashAlt, Icon = FontAwesome.Regular.TrashAlt,
HeaderText = @"Confirm deletion of", HeaderText = @"Confirm deletion of",
@ -37,7 +46,9 @@ namespace osu.Game.Tests.Visual.UserInterface
}, },
})); }));
AddStep("dialog #2", () => overlay.Push(new TestPopupDialog AddAssert("first dialog displayed", () => overlay.CurrentDialog == dialog);
AddStep("dialog #2", () => overlay.Push(dialog = new TestPopupDialog
{ {
Icon = FontAwesome.Solid.Cog, Icon = FontAwesome.Solid.Cog,
HeaderText = @"What do you want to do with", HeaderText = @"What do you want to do with",
@ -70,6 +81,42 @@ namespace osu.Game.Tests.Visual.UserInterface
}, },
}, },
})); }));
AddAssert("second dialog displayed", () => overlay.CurrentDialog == dialog);
}
[Test]
public void TestDismissBeforePush()
{
AddStep("dismissed dialog push", () =>
{
overlay.Push(new TestPopupDialog
{
State = { Value = Visibility.Hidden }
});
});
AddAssert("no dialog pushed", () => overlay.CurrentDialog == null);
}
[Test]
public void TestDismissBeforePushViaButtonPress()
{
AddStep("dismissed dialog push", () =>
{
TestPopupDialog dialog;
overlay.Push(dialog = new TestPopupDialog
{
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton { Text = @"OK" },
},
});
dialog.PerformOkAction();
});
AddAssert("no dialog pushed", () => overlay.CurrentDialog == null);
} }
private class TestPopupDialog : PopupDialog private class TestPopupDialog : PopupDialog

View File

@ -11,6 +11,7 @@ using osu.Game.Beatmaps;
using osu.Game.IO.Archives; using osu.Game.IO.Archives;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Skinning;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
@ -52,6 +53,8 @@ namespace osu.Game.Tests
protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile)); protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile));
protected override ISkin GetSkin() => null;
public override Stream GetStream(string storagePath) => null; public override Stream GetStream(string storagePath) => null;
protected override Track GetBeatmapTrack() => trackStore.Get(firstAudioFile); protected override Track GetBeatmapTrack() => trackStore.Get(firstAudioFile);

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
@ -244,6 +245,8 @@ namespace osu.Game.Beatmaps
{ {
var setInfo = info.BeatmapSet; var setInfo = info.BeatmapSet;
Debug.Assert(setInfo.Files != null);
using (var stream = new MemoryStream()) using (var stream = new MemoryStream())
{ {
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
@ -290,7 +293,9 @@ namespace osu.Game.Beatmaps
if (beatmapInfo?.BeatmapSet == null || beatmapInfo == DefaultBeatmap?.BeatmapInfo) if (beatmapInfo?.BeatmapSet == null || beatmapInfo == DefaultBeatmap?.BeatmapInfo)
return DefaultBeatmap; return DefaultBeatmap;
if (beatmapInfo.BeatmapSet.Files == null) // force a re-query if files are not in a state which looks like the model has
// full database information present.
if (beatmapInfo.BeatmapSet.Files == null || beatmapInfo.BeatmapSet.Files.Count == 0)
{ {
var info = beatmapInfo; var info = beatmapInfo;
beatmapInfo = QueryBeatmap(b => b.ID == info.ID); beatmapInfo = QueryBeatmap(b => b.ID == info.ID);
@ -526,6 +531,7 @@ namespace osu.Game.Beatmaps
protected override IBeatmap GetBeatmap() => beatmap; protected override IBeatmap GetBeatmap() => beatmap;
protected override Texture GetBackground() => null; protected override Texture GetBackground() => null;
protected override Track GetBeatmapTrack() => null; protected override Track GetBeatmapTrack() => null;
protected override ISkin GetSkin() => null;
public override Stream GetStream(string storagePath) => null; public override Stream GetStream(string storagePath) => null;
} }
} }

View File

@ -15,6 +15,7 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
@ -49,6 +50,8 @@ namespace osu.Game.Beatmaps
protected override Track GetBeatmapTrack() => GetVirtualTrack(); protected override Track GetBeatmapTrack() => GetVirtualTrack();
protected override ISkin GetSkin() => null;
public override Stream GetStream(string storagePath) => null; public override Stream GetStream(string storagePath) => null;
private class DummyRulesetInfo : RulesetInfo private class DummyRulesetInfo : RulesetInfo

View File

@ -324,7 +324,7 @@ namespace osu.Game.Beatmaps
public bool SkinLoaded => skin.IsResultAvailable; public bool SkinLoaded => skin.IsResultAvailable;
public ISkin Skin => skin.Value; public ISkin Skin => skin.Value;
protected virtual ISkin GetSkin() => new DefaultSkin(null); protected abstract ISkin GetSkin();
private readonly RecyclableLazy<ISkin> skin; private readonly RecyclableLazy<ISkin> skin;
public abstract Stream GetStream(string storagePath); public abstract Stream GetStream(string storagePath);

View File

@ -235,6 +235,9 @@ namespace osu.Game.Online.Spectator
{ {
Debug.Assert(ThreadSafety.IsUpdateThread); Debug.Assert(ThreadSafety.IsUpdateThread);
if (!IsPlaying)
return;
if (frame is IConvertibleReplayFrame convertible) if (frame is IConvertibleReplayFrame convertible)
pendingFrames.Enqueue(convertible.ToLegacy(currentBeatmap)); pendingFrames.Enqueue(convertible.ToLegacy(currentBeatmap));

View File

@ -95,6 +95,10 @@ namespace osu.Game.Overlays.Dialog
} }
} }
// We always want dialogs to show their appear animation, so we request they start hidden.
// Normally this would not be required, but is here due to the manual Show() call that occurs before LoadComplete().
protected override bool StartHidden => true;
protected PopupDialog() protected PopupDialog()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -205,8 +209,17 @@ namespace osu.Game.Overlays.Dialog
}, },
}, },
}; };
// It's important we start in a visible state so our state fires on hide, even before load.
// This is used by the DialogOverlay to know when the dialog was dismissed.
Show();
} }
/// <summary>
/// Programmatically clicks the first <see cref="PopupDialogOkButton"/>.
/// </summary>
public void PerformOkAction() => Buttons.OfType<PopupDialogOkButton>().First().Click();
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)
{ {
if (e.Repeat) return false; if (e.Repeat) return false;

View File

@ -35,15 +35,16 @@ namespace osu.Game.Overlays
public void Push(PopupDialog dialog) public void Push(PopupDialog dialog)
{ {
if (dialog == CurrentDialog) return; if (dialog == CurrentDialog || dialog.State.Value != Visibility.Visible) return;
// if any existing dialog is being displayed, dismiss it before showing a new one.
CurrentDialog?.Hide(); CurrentDialog?.Hide();
CurrentDialog = dialog; CurrentDialog = dialog;
CurrentDialog.State.ValueChanged += state => onDialogOnStateChanged(dialog, state.NewValue);
dialogContainer.Add(CurrentDialog); dialogContainer.Add(CurrentDialog);
CurrentDialog.Show();
CurrentDialog.State.ValueChanged += state => onDialogOnStateChanged(dialog, state.NewValue);
Show(); Show();
} }

View File

@ -0,0 +1,104 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using HtmlAgilityPack;
using osu.Framework.Allocation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Overlays.Wiki
{
public class WikiMainPage : FillFlowContainer
{
public string Markdown;
public WikiMainPage()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
}
[BackgroundDependencyLoader]
private void load()
{
var html = new HtmlDocument();
html.LoadHtml(Markdown);
var panels = createPanels(html).ToArray();
Children = new Drawable[]
{
createBlurb(html),
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
RowDimensions = Enumerable.Repeat(new Dimension(GridSizeMode.AutoSize), panels.Length).ToArray(),
Content = panels,
},
};
}
private Container createBlurb(HtmlDocument html)
{
var blurbNode = html.DocumentNode.SelectSingleNode("//div[contains(@class, 'wiki-main-page__blurb')]");
return new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
{
Vertical = 30,
},
Child = new OsuSpriteText
{
Text = blurbNode.InnerText,
Font = OsuFont.GetFont(size: 12),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
}
};
}
private IEnumerable<Drawable[]> createPanels(HtmlDocument html)
{
var panelsNode = html.DocumentNode.SelectNodes("//div[contains(@class, 'wiki-main-page-panel')]").ToArray();
Debug.Assert(panelsNode.Length > 1);
var i = 0;
while (i < panelsNode.Length)
{
var isFullWidth = panelsNode[i].HasClass("wiki-main-page-panel--full");
if (isFullWidth)
{
yield return new Drawable[]
{
new WikiPanelContainer(panelsNode[i++].InnerText, true)
{
// This is required to fill up the space of "null" drawable below.
Width = 2,
},
null,
};
}
else
{
yield return new Drawable[]
{
new WikiPanelContainer(panelsNode[i++].InnerText),
i < panelsNode.Length ? new WikiPanelContainer(panelsNode[i++].InnerText) : null,
};
}
}
}
}
}

View 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;
using Markdig.Syntax;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers.Markdown;
using osu.Game.Overlays.Wiki.Markdown;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Wiki
{
public class WikiPanelContainer : Container
{
private WikiPanelMarkdownContainer panelContainer;
private readonly string text;
private readonly bool isFullWidth;
public WikiPanelContainer(string text, bool isFullWidth = false)
{
this.text = text;
this.isFullWidth = isFullWidth;
RelativeSizeAxes = Axes.X;
Padding = new MarginPadding(3);
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 4,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(25),
Offset = new Vector2(0, 1),
Radius = 3,
},
Child = new Box
{
Colour = colourProvider.Background4,
RelativeSizeAxes = Axes.Both,
},
},
panelContainer = new WikiPanelMarkdownContainer(isFullWidth)
{
Text = text,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
};
}
protected override void Update()
{
base.Update();
Height = Math.Max(panelContainer.Height, Parent.DrawHeight);
}
private class WikiPanelMarkdownContainer : WikiMarkdownContainer
{
private readonly bool isFullWidth;
public WikiPanelMarkdownContainer(bool isFullWidth)
{
this.isFullWidth = isFullWidth;
LineSpacing = 0;
DocumentPadding = new MarginPadding(30);
DocumentMargin = new MarginPadding(0);
}
public override SpriteText CreateSpriteText() => base.CreateSpriteText().With(t => t.Font = t.Font.With(weight: FontWeight.Bold));
public override MarkdownTextFlowContainer CreateTextFlow() => base.CreateTextFlow().With(f => f.TextAnchor = Anchor.TopCentre);
protected override MarkdownParagraph CreateParagraph(ParagraphBlock paragraphBlock, int level)
=> base.CreateParagraph(paragraphBlock, level).With(p => p.Margin = new MarginPadding { Bottom = 10 });
protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new WikiPanelHeading(headingBlock)
{
IsFullWidth = isFullWidth,
};
}
private class WikiPanelHeading : OsuMarkdownHeading
{
public bool IsFullWidth;
public WikiPanelHeading(HeadingBlock headingBlock)
: base(headingBlock)
{
Margin = new MarginPadding { Bottom = 40 };
}
public override MarkdownTextFlowContainer CreateTextFlow() => base.CreateTextFlow().With(f =>
{
f.Anchor = Anchor.TopCentre;
f.Origin = Anchor.TopCentre;
f.TextAnchor = Anchor.TopCentre;
});
protected override FontWeight GetFontWeightByLevel(int level) => FontWeight.Light;
protected override float GetFontSizeByLevel(int level) => base.GetFontSizeByLevel(IsFullWidth ? level : 3);
}
}
}

View File

@ -11,7 +11,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Performance;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Audio; using osu.Game.Audio;
@ -445,9 +444,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <summary> /// <summary>
/// Apply (generally fade-in) transforms leading into the <see cref="HitObject"/> start time. /// Apply (generally fade-in) transforms leading into the <see cref="HitObject"/> start time.
/// The local drawable hierarchy is recursively delayed to <see cref="LifetimeEntry.LifetimeStart"/> for convenience. /// By default, this will fade in the object from zero with no duration.
///
/// By default this will fade in the object from zero with no duration.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This is called once before every <see cref="UpdateStateTransforms"/>. This is to ensure a good state in the case /// This is called once before every <see cref="UpdateStateTransforms"/>. This is to ensure a good state in the case
@ -623,17 +620,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
protected internal new ScheduledDelegate Schedule(Action action) => base.Schedule(action); protected internal new ScheduledDelegate Schedule(Action action) => base.Schedule(action);
/// <summary> /// <summary>
/// A safe offset prior to the start time of <see cref="HitObject"/> at which this <see cref="DrawableHitObject"/> may begin displaying contents. /// An offset prior to the start time of <see cref="HitObject"/> at which this <see cref="DrawableHitObject"/> may begin displaying contents.
/// By default, <see cref="DrawableHitObject"/>s are assumed to display their contents within 10 seconds prior to the start time of <see cref="HitObject"/>. /// By default, <see cref="DrawableHitObject"/>s are assumed to display their contents within 10 seconds prior to the start time of <see cref="HitObject"/>.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This is only used as an optimisation to delay the initial update of this <see cref="DrawableHitObject"/> and may be tuned more aggressively if required. /// The initial transformation (<see cref="UpdateInitialTransforms"/>) starts at this offset before the start time of <see cref="HitObject"/>.
/// It is indirectly used to decide the automatic transform offset provided to <see cref="UpdateInitialTransforms"/>.
/// A more accurate <see cref="LifetimeEntry.LifetimeStart"/> should be set for further optimisation (in <see cref="LoadComplete"/>, for example).
/// <para>
/// Only has an effect if this <see cref="DrawableHitObject"/> is not being pooled.
/// For pooled <see cref="DrawableHitObject"/>s, use <see cref="HitObjectLifetimeEntry.InitialLifetimeOffset"/> instead.
/// </para>
/// </remarks> /// </remarks>
protected virtual double InitialLifetimeOffset => 10000; protected virtual double InitialLifetimeOffset => 10000;

View File

@ -82,9 +82,8 @@ namespace osu.Game.Rulesets.Objects
/// By default, <see cref="HitObject"/>s are assumed to display their contents within 10 seconds prior to their start time. /// By default, <see cref="HitObject"/>s are assumed to display their contents within 10 seconds prior to their start time.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This is only used as an optimisation to delay the initial update of the <see cref="HitObject"/> and may be tuned more aggressively if required. /// This is only used as an optimisation to delay the initial application of the <see cref="HitObject"/> to a <see cref="DrawableHitObject"/>.
/// It is indirectly used to decide the automatic transform offset provided to <see cref="DrawableHitObject.UpdateInitialTransforms"/>. /// A more accurate <see cref="LifetimeEntry.LifetimeStart"/> should be set on the hit object application, for further optimisation.
/// A more accurate <see cref="LifetimeEntry.LifetimeStart"/> should be set for further optimisation (in <see cref="DrawableHitObject.LoadComplete"/>, for example).
/// </remarks> /// </remarks>
protected virtual double InitialLifetimeOffset => 10000; protected virtual double InitialLifetimeOffset => 10000;

View File

@ -473,25 +473,23 @@ namespace osu.Game.Screens.Edit
{ {
if (!exitConfirmed) if (!exitConfirmed)
{ {
// if the confirm dialog is already showing (or we can't show it, ie. in tests) exit without save. // dialog overlay may not be available in visual tests.
if (dialogOverlay == null || dialogOverlay.CurrentDialog is PromptForSaveDialog) if (dialogOverlay == null)
{ {
confirmExit(); confirmExit();
return base.OnExiting(next); return true;
}
// if the dialog is already displayed, confirm exit with no save.
if (dialogOverlay.CurrentDialog is PromptForSaveDialog saveDialog)
{
saveDialog.PerformOkAction();
return true;
} }
if (isNewBeatmap || HasUnsavedChanges) if (isNewBeatmap || HasUnsavedChanges)
{ {
dialogOverlay?.Push(new PromptForSaveDialog(() => dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave));
{
confirmExit();
this.Exit();
}, () =>
{
confirmExitWithSave();
this.Exit();
}));
return true; return true;
} }
} }
@ -499,15 +497,23 @@ namespace osu.Game.Screens.Edit
ApplyToBackground(b => b.FadeColour(Color4.White, 500)); ApplyToBackground(b => b.FadeColour(Color4.White, 500));
resetTrack(); resetTrack();
Beatmap.Value = beatmapManager.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo); // To update the game-wide beatmap with any changes, perform a re-fetch on exit.
// This is required as the editor makes its local changes via EditorBeatmap
// (which are not propagated outwards to a potentially cached WorkingBeatmap).
var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo);
if (!(refetchedBeatmap is DummyWorkingBeatmap))
Beatmap.Value = refetchedBeatmap;
return base.OnExiting(next); return base.OnExiting(next);
} }
private void confirmExitWithSave() private void confirmExitWithSave()
{ {
exitConfirmed = true;
Save(); Save();
exitConfirmed = true;
this.Exit();
} }
private void confirmExit() private void confirmExit()
@ -529,6 +535,7 @@ namespace osu.Game.Screens.Edit
} }
exitConfirmed = true; exitConfirmed = true;
this.Exit();
} }
private readonly Bindable<string> clipboard = new Bindable<string>(); private readonly Bindable<string> clipboard = new Bindable<string>();

View File

@ -11,6 +11,7 @@ using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Skinning;
using Decoder = osu.Game.Beatmaps.Formats.Decoder; using Decoder = osu.Game.Beatmaps.Formats.Decoder;
namespace osu.Game.Screens.Edit namespace osu.Game.Screens.Edit
@ -117,6 +118,8 @@ namespace osu.Game.Screens.Edit
protected override Track GetBeatmapTrack() => throw new NotImplementedException(); protected override Track GetBeatmapTrack() => throw new NotImplementedException();
protected override ISkin GetSkin() => throw new NotImplementedException();
public override Stream GetStream(string storagePath) => throw new NotImplementedException(); public override Stream GetStream(string storagePath) => throw new NotImplementedException();
} }
} }

View File

@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -22,6 +19,8 @@ using osu.Game.Screens.Edit;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Menu namespace osu.Game.Screens.Menu
{ {
@ -120,7 +119,7 @@ namespace osu.Game.Screens.Menu
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
Margin = new MarginPadding { Right = 15, Top = 5 } Margin = new MarginPadding { Right = 15, Top = 5 }
}, },
exitConfirmOverlay?.CreateProxy() ?? Drawable.Empty() exitConfirmOverlay?.CreateProxy() ?? Empty()
}); });
buttons.StateChanged += state => buttons.StateChanged += state =>
@ -270,15 +269,11 @@ namespace osu.Game.Screens.Menu
if (!exitConfirmed && dialogOverlay != null) if (!exitConfirmed && dialogOverlay != null)
{ {
if (dialogOverlay.CurrentDialog is ConfirmExitDialog exitDialog) if (dialogOverlay.CurrentDialog is ConfirmExitDialog exitDialog)
{ exitDialog.PerformOkAction();
exitConfirmed = true;
exitDialog.Buttons.First().Click();
}
else else
{
dialogOverlay.Push(new ConfirmExitDialog(confirmAndExit, () => exitConfirmOverlay.Abort())); dialogOverlay.Push(new ConfirmExitDialog(confirmAndExit, () => exitConfirmOverlay.Abort()));
return true;
} return true;
} }
buttons.State = ButtonSystemState.Exit; buttons.State = ButtonSystemState.Exit;

View File

@ -522,7 +522,10 @@ namespace osu.Game.Screens.Play
if (!this.IsCurrentScreen()) if (!this.IsCurrentScreen())
{ {
ValidForResume = false; ValidForResume = false;
this.MakeCurrent();
// in the potential case that this instance has already been exited, this is required to avoid a crash.
if (this.GetChildScreen() != null)
this.MakeCurrent();
return; return;
} }

View File

@ -3,6 +3,7 @@
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -23,6 +24,25 @@ namespace osu.Game.Skinning
Configuration.AllowDefaultComboColoursFallback = false; Configuration.AllowDefaultComboColoursFallback = false;
} }
public override Drawable GetDrawableComponent(ISkinComponent component)
{
if (component is SkinnableTargetComponent targetComponent)
{
switch (targetComponent.Target)
{
case SkinnableTarget.MainHUDComponents:
// this should exist in LegacySkin instead, but there isn't a fallback skin for LegacySkins yet.
// therefore keep the check here until fallback default legacy skin is supported.
if (!this.HasFont(LegacyFont.Score))
return null;
break;
}
}
return base.GetDrawableComponent(component);
}
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{ {
switch (lookup) switch (lookup)
@ -51,6 +71,6 @@ namespace osu.Game.Skinning
} }
private static SkinInfo createSkinInfo(BeatmapInfo beatmap) => private static SkinInfo createSkinInfo(BeatmapInfo beatmap) =>
new SkinInfo { Name = beatmap.ToString(), Creator = beatmap.Metadata.Author.ToString() }; new SkinInfo { Name = beatmap.ToString(), Creator = beatmap.Metadata?.AuthorString };
} }
} }

View File

@ -17,6 +17,7 @@ using osu.Game.IO;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Skinning;
namespace osu.Game.Tests.Beatmaps namespace osu.Game.Tests.Beatmaps
{ {
@ -216,6 +217,8 @@ namespace osu.Game.Tests.Beatmaps
protected override Track GetBeatmapTrack() => throw new NotImplementedException(); protected override Track GetBeatmapTrack() => throw new NotImplementedException();
protected override ISkin GetSkin() => throw new NotImplementedException();
public override Stream GetStream(string storagePath) => throw new NotImplementedException(); public override Stream GetStream(string storagePath) => throw new NotImplementedException();
protected override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) protected override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)

View File

@ -6,6 +6,7 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Skinning;
using osu.Game.Storyboards; using osu.Game.Storyboards;
namespace osu.Game.Tests.Beatmaps namespace osu.Game.Tests.Beatmaps
@ -36,6 +37,8 @@ namespace osu.Game.Tests.Beatmaps
protected override Storyboard GetStoryboard() => storyboard ?? base.GetStoryboard(); protected override Storyboard GetStoryboard() => storyboard ?? base.GetStoryboard();
protected override ISkin GetSkin() => null;
public override Stream GetStream(string storagePath) => null; public override Stream GetStream(string storagePath) => null;
protected override Texture GetBackground() => null; protected override Texture GetBackground() => null;

View File

@ -19,6 +19,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="DiffPlex" Version="1.7.0" /> <PackageReference Include="DiffPlex" Version="1.7.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.33" />
<PackageReference Include="Humanizer" Version="2.8.26" /> <PackageReference Include="Humanizer" Version="2.8.26" />
<PackageReference Include="MessagePack" Version="2.2.85" /> <PackageReference Include="MessagePack" Version="2.2.85" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.5" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.5" />
@ -33,7 +34,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="ppy.osu.Framework" Version="2021.524.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.528.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" />
<PackageReference Include="Sentry" Version="3.3.4" /> <PackageReference Include="Sentry" Version="3.3.4" />
<PackageReference Include="SharpCompress" Version="0.28.2" /> <PackageReference Include="SharpCompress" Version="0.28.2" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.524.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.528.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -93,7 +93,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2021.524.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.528.0" />
<PackageReference Include="SharpCompress" Version="0.28.2" /> <PackageReference Include="SharpCompress" Version="0.28.2" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />