mirror of
https://github.com/ppy/osu.git
synced 2024-12-15 05:02:55 +08:00
Merge branch 'master' into multi-spectator-better-start-time
This commit is contained in:
commit
785d2ed9b1
@ -9,6 +9,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -25,6 +26,22 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
private readonly List<Stage> stages = new List<Stage>();
|
||||
|
||||
public override Quad SkinnableComponentScreenSpaceDrawQuad
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Stages.Count == 1)
|
||||
return Stages.First().ScreenSpaceDrawQuad;
|
||||
|
||||
RectangleF area = RectangleF.Empty;
|
||||
|
||||
foreach (var stage in Stages)
|
||||
area = RectangleF.Union(area, stage.ScreenSpaceDrawQuad.AABBFloat);
|
||||
|
||||
return area;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos));
|
||||
|
||||
public ManiaPlayfield(List<StageDefinition> stageDefinitions)
|
||||
|
@ -125,13 +125,13 @@ namespace osu.Game.Tests.Visual.Background
|
||||
createFakeStoryboard();
|
||||
AddStep("Enable Storyboard", () =>
|
||||
{
|
||||
player.ReplacesBackground.Value = true;
|
||||
player.StoryboardReplacesBackground.Value = true;
|
||||
player.StoryboardEnabled.Value = true;
|
||||
});
|
||||
AddUntilStep("Background is invisible, storyboard is visible", () => songSelect.IsBackgroundInvisible() && player.IsStoryboardVisible);
|
||||
AddUntilStep("Background is black, storyboard is visible", () => songSelect.IsBackgroundVisible() && songSelect.IsBackgroundBlack() && player.IsStoryboardVisible);
|
||||
AddStep("Disable Storyboard", () =>
|
||||
{
|
||||
player.ReplacesBackground.Value = false;
|
||||
player.StoryboardReplacesBackground.Value = false;
|
||||
player.StoryboardEnabled.Value = false;
|
||||
});
|
||||
AddUntilStep("Background is visible, storyboard is invisible", () => songSelect.IsBackgroundVisible() && !player.IsStoryboardVisible);
|
||||
@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
createFakeStoryboard();
|
||||
AddStep("Enable Storyboard", () =>
|
||||
{
|
||||
player.ReplacesBackground.Value = true;
|
||||
player.StoryboardReplacesBackground.Value = true;
|
||||
player.StoryboardEnabled.Value = true;
|
||||
});
|
||||
AddStep("Enable user dim", () => player.DimmableStoryboard.IgnoreUserSettings.Value = false);
|
||||
@ -188,7 +188,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
{
|
||||
performFullSetup();
|
||||
createFakeStoryboard();
|
||||
AddStep("Enable replacing background", () => player.ReplacesBackground.Value = true);
|
||||
AddStep("Enable replacing background", () => player.StoryboardReplacesBackground.Value = true);
|
||||
|
||||
AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible);
|
||||
AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible());
|
||||
@ -199,11 +199,11 @@ namespace osu.Game.Tests.Visual.Background
|
||||
player.DimmableStoryboard.IgnoreUserSettings.Value = true;
|
||||
});
|
||||
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
|
||||
AddUntilStep("Background is invisible", () => songSelect.IsBackgroundInvisible());
|
||||
AddUntilStep("Background is dimmed", () => songSelect.IsBackgroundVisible() && songSelect.IsBackgroundBlack());
|
||||
|
||||
AddStep("Disable background replacement", () => player.ReplacesBackground.Value = false);
|
||||
AddStep("Disable background replacement", () => player.StoryboardReplacesBackground.Value = false);
|
||||
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
|
||||
AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible());
|
||||
AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible() && !songSelect.IsBackgroundBlack());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -257,7 +257,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
private void createFakeStoryboard() => AddStep("Create storyboard", () =>
|
||||
{
|
||||
player.StoryboardEnabled.Value = false;
|
||||
player.ReplacesBackground.Value = false;
|
||||
player.StoryboardReplacesBackground.Value = false;
|
||||
player.DimmableStoryboard.Add(new OsuSpriteText
|
||||
{
|
||||
Size = new Vector2(500, 50),
|
||||
@ -323,6 +323,8 @@ namespace osu.Game.Tests.Visual.Background
|
||||
config.BindWith(OsuSetting.BlurLevel, BlurLevel);
|
||||
}
|
||||
|
||||
public bool IsBackgroundBlack() => background.CurrentColour == OsuColour.Gray(0);
|
||||
|
||||
public bool IsBackgroundDimmed() => background.CurrentColour == OsuColour.Gray(1f - background.CurrentDim);
|
||||
|
||||
public bool IsBackgroundUndimmed() => background.CurrentColour == Color4.White;
|
||||
@ -331,8 +333,6 @@ namespace osu.Game.Tests.Visual.Background
|
||||
|
||||
public bool IsUserBlurDisabled() => background.CurrentBlur == new Vector2(0);
|
||||
|
||||
public bool IsBackgroundInvisible() => background.CurrentAlpha == 0;
|
||||
|
||||
public bool IsBackgroundVisible() => background.CurrentAlpha == 1;
|
||||
|
||||
public bool IsBackgroundBlur() => Precision.AlmostEquals(background.CurrentBlur, new Vector2(BACKGROUND_BLUR), 0.1f);
|
||||
@ -367,7 +367,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
{
|
||||
base.OnEntering(e);
|
||||
|
||||
ApplyToBackground(b => ReplacesBackground.BindTo(b.StoryboardReplacesBackground));
|
||||
ApplyToBackground(b => StoryboardReplacesBackground.BindTo(b.StoryboardReplacesBackground));
|
||||
}
|
||||
|
||||
public new DimmableStoryboard DimmableStoryboard => base.DimmableStoryboard;
|
||||
@ -376,7 +376,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
public bool BlockLoad;
|
||||
|
||||
public Bindable<bool> StoryboardEnabled;
|
||||
public readonly Bindable<bool> ReplacesBackground = new Bindable<bool>();
|
||||
public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>();
|
||||
public readonly Bindable<bool> IsPaused = new Bindable<bool>();
|
||||
|
||||
public LoadBlockingTestPlayer(bool allowPause = true)
|
||||
|
@ -8,6 +8,7 @@ using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.SkinEditor;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
@ -19,7 +20,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestToggleEditor()
|
||||
{
|
||||
AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox
|
||||
var skinComponentsContainer = new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.SongSelect));
|
||||
|
||||
AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox(skinComponentsContainer, null)
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
|
@ -124,7 +124,7 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (Source != null && Source is not IAdjustableClock && Source.CurrentTime < decoupledClock.CurrentTime)
|
||||
if (Source != null && Source is not IAdjustableClock && Source.CurrentTime < decoupledClock.CurrentTime - 100)
|
||||
{
|
||||
// InterpolatingFramedClock won't interpolate backwards unless its source has an ElapsedFrameTime.
|
||||
// See https://github.com/ppy/osu-framework/blob/ba1385330cc501f34937e08257e586c84e35d772/osu.Framework/Timing/InterpolatingFramedClock.cs#L91-L93
|
||||
|
@ -24,15 +24,13 @@ namespace osu.Game.Graphics.Containers
|
||||
public const double BACKGROUND_FADE_DURATION = 800;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not user-configured settings relating to brightness of elements should be ignored
|
||||
/// Whether or not user-configured settings relating to brightness of elements should be ignored.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For best or worst, this also bypasses storyboard disable. Not sure this is correct but leaving it as to not break anything.
|
||||
/// </remarks>
|
||||
public readonly Bindable<bool> IgnoreUserSettings = new Bindable<bool>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the storyboard loaded should completely hide the background behind it.
|
||||
/// </summary>
|
||||
public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether player is in break time.
|
||||
/// Must be bound to <see cref="BreakTracker.IsBreakTime"/> to allow for dim adjustments in gameplay.
|
||||
@ -57,7 +55,7 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
private float breakLightening => LightenDuringBreaks.Value && IsBreakTime.Value ? BREAK_LIGHTEN_AMOUNT : 0;
|
||||
|
||||
protected float DimLevel => Math.Max(!IgnoreUserSettings.Value ? (float)UserDimLevel.Value - breakLightening : DimWhenUserSettingsIgnored.Value, 0);
|
||||
protected virtual float DimLevel => Math.Max(!IgnoreUserSettings.Value ? (float)UserDimLevel.Value - breakLightening : DimWhenUserSettingsIgnored.Value, 0);
|
||||
|
||||
protected override Container<Drawable> Content => dimContent;
|
||||
|
||||
@ -83,7 +81,6 @@ namespace osu.Game.Graphics.Containers
|
||||
LightenDuringBreaks.ValueChanged += _ => UpdateVisuals();
|
||||
IsBreakTime.ValueChanged += _ => UpdateVisuals();
|
||||
ShowStoryboard.ValueChanged += _ => UpdateVisuals();
|
||||
StoryboardReplacesBackground.ValueChanged += _ => UpdateVisuals();
|
||||
IgnoreUserSettings.ValueChanged += _ => UpdateVisuals();
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ namespace osu.Game.IO.Archives
|
||||
{
|
||||
ZipArchiveEntry entry = archive.Entries.SingleOrDefault(e => e.Key == name);
|
||||
if (entry == null)
|
||||
throw new FileNotFoundException();
|
||||
return null;
|
||||
|
||||
var owner = MemoryAllocator.Default.Allocate<byte>((int)entry.Size);
|
||||
|
||||
|
@ -13,6 +13,7 @@ using osu.Framework.Threading;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Edit.Components;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
@ -23,14 +24,22 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
public Action<Type>? RequestPlacement;
|
||||
|
||||
private readonly SkinComponentsContainer? target;
|
||||
private readonly SkinComponentsContainer target;
|
||||
|
||||
private readonly RulesetInfo? ruleset;
|
||||
|
||||
private FillFlowContainer fill = null!;
|
||||
|
||||
public SkinComponentToolbox(SkinComponentsContainer? target = null)
|
||||
: base(target?.Lookup.Ruleset == null ? SkinEditorStrings.Components : LocalisableString.Interpolate($"{SkinEditorStrings.Components} ({target.Lookup.Ruleset.Name})"))
|
||||
/// <summary>
|
||||
/// Create a new component toolbox for the specified taget.
|
||||
/// </summary>
|
||||
/// <param name="target">The target. This is mainly used as a dependency source to find candidate components.</param>
|
||||
/// <param name="ruleset">A ruleset to filter components by. If null, only components which are not ruleset-specific will be included.</param>
|
||||
public SkinComponentToolbox(SkinComponentsContainer target, RulesetInfo? ruleset)
|
||||
: base(ruleset == null ? SkinEditorStrings.Components : LocalisableString.Interpolate($"{SkinEditorStrings.Components} ({ruleset.Name})"))
|
||||
{
|
||||
this.target = target;
|
||||
this.ruleset = ruleset;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -51,7 +60,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
fill.Clear();
|
||||
|
||||
var skinnableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables(target?.Lookup.Ruleset);
|
||||
var skinnableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables(ruleset);
|
||||
foreach (var type in skinnableTypes)
|
||||
attemptAddComponent(type);
|
||||
}
|
||||
|
@ -366,14 +366,14 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
// If the new target has a ruleset, let's show ruleset-specific items at the top, and the rest below.
|
||||
if (target.NewValue.Ruleset != null)
|
||||
{
|
||||
componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer)
|
||||
componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer, target.NewValue.Ruleset)
|
||||
{
|
||||
RequestPlacement = requestPlacement
|
||||
});
|
||||
}
|
||||
|
||||
// Remove the ruleset from the lookup to get base components.
|
||||
componentsSidebar.Add(new SkinComponentToolbox(getTarget(new SkinComponentsContainerLookup(target.NewValue.Target)))
|
||||
componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer, null)
|
||||
{
|
||||
RequestPlacement = requestPlacement
|
||||
});
|
||||
|
@ -23,6 +23,7 @@ using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osu.Game.Rulesets.Objects.Pooling;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
@ -94,6 +95,16 @@ namespace osu.Game.Rulesets.UI
|
||||
/// </summary>
|
||||
public readonly BindableBool DisplayJudgements = new BindableBool(true);
|
||||
|
||||
/// <summary>
|
||||
/// A screen space draw quad which resembles the edges of the playfield for skinning purposes.
|
||||
/// This will allow users / components to snap objects to the "edge" of the playfield.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rulesets which reduce the visible area further than the full relative playfield space itself
|
||||
/// should retarget this to the ScreenSpaceDrawQuad of the appropriate container.
|
||||
/// </remarks>
|
||||
public virtual Quad SkinnableComponentScreenSpaceDrawQuad => ScreenSpaceDrawQuad;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
protected IReadOnlyList<Mod> Mods { get; private set; }
|
||||
|
@ -36,6 +36,9 @@ namespace osu.Game.Screens.Backgrounds
|
||||
/// </remarks>
|
||||
public readonly Bindable<bool> IgnoreUserSettings = new Bindable<bool>(true);
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the storyboard loaded should completely hide the background behind it.
|
||||
/// </summary>
|
||||
public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>();
|
||||
|
||||
/// <summary>
|
||||
@ -60,12 +63,11 @@ namespace osu.Game.Screens.Backgrounds
|
||||
|
||||
InternalChild = dimmable = CreateFadeContainer();
|
||||
|
||||
dimmable.StoryboardReplacesBackground.BindTo(StoryboardReplacesBackground);
|
||||
dimmable.IgnoreUserSettings.BindTo(IgnoreUserSettings);
|
||||
dimmable.IsBreakTime.BindTo(IsBreakTime);
|
||||
dimmable.BlurAmount.BindTo(BlurAmount);
|
||||
dimmable.DimWhenUserSettingsIgnored.BindTo(DimWhenUserSettingsIgnored);
|
||||
|
||||
StoryboardReplacesBackground.BindTo(dimmable.StoryboardReplacesBackground);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -144,6 +146,8 @@ namespace osu.Game.Screens.Backgrounds
|
||||
/// </remarks>
|
||||
public readonly Bindable<float> BlurAmount = new BindableFloat();
|
||||
|
||||
public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>();
|
||||
|
||||
public Background Background
|
||||
{
|
||||
get => background;
|
||||
@ -187,11 +191,19 @@ namespace osu.Game.Screens.Backgrounds
|
||||
|
||||
userBlurLevel.ValueChanged += _ => UpdateVisuals();
|
||||
BlurAmount.ValueChanged += _ => UpdateVisuals();
|
||||
StoryboardReplacesBackground.ValueChanged += _ => UpdateVisuals();
|
||||
}
|
||||
|
||||
protected override bool ShowDimContent
|
||||
// The background needs to be hidden in the case of it being replaced by the storyboard
|
||||
=> (!ShowStoryboard.Value && !IgnoreUserSettings.Value) || !StoryboardReplacesBackground.Value;
|
||||
protected override float DimLevel
|
||||
{
|
||||
get
|
||||
{
|
||||
if ((IgnoreUserSettings.Value || ShowStoryboard.Value) && StoryboardReplacesBackground.Value)
|
||||
return 1;
|
||||
|
||||
return base.DimLevel;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateVisuals()
|
||||
{
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
@ -69,7 +70,9 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public Bindable<bool> ShowHealthBar = new Bindable<bool>(true);
|
||||
|
||||
[CanBeNull]
|
||||
private readonly DrawableRuleset drawableRuleset;
|
||||
|
||||
private readonly IReadOnlyList<Mod> mods;
|
||||
|
||||
/// <summary>
|
||||
@ -103,10 +106,11 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private readonly List<Drawable> hideTargets;
|
||||
|
||||
public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, bool alwaysShowLeaderboard = true)
|
||||
private readonly Drawable playfieldComponents;
|
||||
|
||||
public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, bool alwaysShowLeaderboard = true)
|
||||
{
|
||||
Drawable rulesetComponents;
|
||||
|
||||
this.drawableRuleset = drawableRuleset;
|
||||
this.mods = mods;
|
||||
|
||||
@ -123,6 +127,9 @@ namespace osu.Game.Screens.Play
|
||||
rulesetComponents = drawableRuleset != null
|
||||
? new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, }
|
||||
: Empty(),
|
||||
playfieldComponents = drawableRuleset != null
|
||||
? new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, }
|
||||
: Empty(),
|
||||
topRightElements = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
@ -162,7 +169,7 @@ namespace osu.Game.Screens.Play
|
||||
},
|
||||
};
|
||||
|
||||
hideTargets = new List<Drawable> { mainComponents, rulesetComponents, topRightElements };
|
||||
hideTargets = new List<Drawable> { mainComponents, rulesetComponents, playfieldComponents, topRightElements };
|
||||
|
||||
if (!alwaysShowLeaderboard)
|
||||
hideTargets.Add(LeaderboardFlow);
|
||||
@ -230,6 +237,16 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (drawableRuleset != null)
|
||||
{
|
||||
Quad playfieldScreenSpaceDrawQuad = drawableRuleset.Playfield.SkinnableComponentScreenSpaceDrawQuad;
|
||||
|
||||
playfieldComponents.Position = ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft);
|
||||
playfieldComponents.Width = (ToLocalSpace(playfieldScreenSpaceDrawQuad.TopRight) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length;
|
||||
playfieldComponents.Height = (ToLocalSpace(playfieldScreenSpaceDrawQuad.BottomLeft) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length;
|
||||
playfieldComponents.Rotation = drawableRuleset.Playfield.Rotation;
|
||||
}
|
||||
|
||||
float? lowestTopScreenSpaceLeft = null;
|
||||
float? lowestTopScreenSpaceRight = null;
|
||||
|
||||
|
@ -1049,8 +1049,6 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime);
|
||||
|
||||
DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground);
|
||||
|
||||
storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable;
|
||||
|
||||
foreach (var mod in GameplayState.Mods.OfType<IApplicableToPlayer>())
|
||||
|
@ -68,7 +68,10 @@ namespace osu.Game.Skinning
|
||||
MainHUDComponents,
|
||||
|
||||
[Description("Song select")]
|
||||
SongSelect
|
||||
SongSelect,
|
||||
|
||||
[Description("Playfield")]
|
||||
Playfield
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user