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

Merge branch 'master' into muted-notification

This commit is contained in:
Dean Herbert 2019-09-28 20:33:32 +08:00 committed by GitHub
commit 02c1f490f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 807 additions and 173 deletions

View File

@ -1,11 +1,11 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.0)
addressable (2.6.0)
public_suffix (>= 2.0.2, < 4.0)
CFPropertyList (3.0.1)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
babosa (1.0.2)
babosa (1.0.3)
claide (1.0.3)
colored (1.2)
colored2 (3.1.2)
@ -26,8 +26,8 @@ GEM
http-cookie (~> 1.0.0)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
fastimage (2.1.5)
fastlane (2.129.0)
fastimage (2.1.7)
fastlane (2.131.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
babosa (>= 1.0.2, < 2.0.0)
@ -77,9 +77,9 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.9)
google-cloud-core (1.3.0)
google-cloud-core (1.3.1)
google-cloud-env (~> 1.0)
google-cloud-env (1.2.0)
google-cloud-env (1.2.1)
faraday (~> 0.11)
google-cloud-storage (1.16.0)
digest-crc (~> 0.4)
@ -100,9 +100,9 @@ GEM
json (2.2.0)
jwt (2.1.0)
memoist (0.16.0)
mime-types (3.2.2)
mime-types (3.3)
mime-types-data (~> 3.2015)
mime-types-data (3.2019.0331)
mime-types-data (3.2019.0904)
mini_magick (4.9.5)
mini_portile2 (2.4.0)
multi_json (1.13.1)
@ -121,14 +121,14 @@ GEM
uber (< 0.2.0)
retriable (3.1.2)
rouge (2.0.7)
rubyzip (1.2.3)
rubyzip (1.2.4)
security (0.1.3)
signet (0.11.0)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.5)
simctl (1.6.6)
CFPropertyList
naturally
slack-notifier (2.3.2)

View File

@ -31,12 +31,10 @@ If you are not interested in developing the game, you can still consume our [bin
**Latest build:**
| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) |
| ------------- | ------------- |
| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [iOS(iOS 10+)](https://testflight.apple.com/join/2tLcjWlF) | [Android (5+)](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
| ------------- | ------------- | ------------- | ------------- |
- **Linux** users are recommended to self-compile until we have official deployment in place.
- **iOS** users can join the [TestFlight beta program](https://testflight.apple.com/join/2tLcjWlF) (note that due to high demand this is regularly full).
- **Android** users can self-compile, and expect a public beta soon.
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.

View File

@ -1,5 +1,83 @@
update_fastlane
platform :android do
desc 'Deploy to play store'
lane :beta do |options|
update_version(
version: options[:version],
build: options[:build],
)
build(options)
supply(
apk: './osu.Android/bin/Release/sh.ppy.osulazer-Signed.apk',
package_name: 'sh.ppy.osulazer',
track: 'alpha', # upload to alpha, we can promote it later
json_key: options[:json_key],
)
end
desc 'Deploy to github release'
lane :build_github do |options|
update_version(
version: options[:version],
build: options[:build],
)
build(options)
client = HTTPClient.new
changelog = client.get_content 'https://gist.githubusercontent.com/peppy/aaa2ec1a323554b619671cac6dbbb776/raw'
changelog.gsub!('$BUILD_ID', options[:build])
set_github_release(
repository_name: "ppy/osu",
api_token: ENV["GITHUB_TOKEN"],
name: options[:build],
tag_name: options[:build],
is_draft: true,
description: changelog,
commitish: "master",
upload_assets: ["osu.Android/bin/Release/sh.ppy.osulazer.apk"]
)
end
desc 'Compile the project'
lane :build do |options|
nuget_restore(
project_path: 'osu.Android.sln'
)
souyuz(
build_configuration: 'Release',
solution_path: 'osu.Android.sln',
platform: "android",
output_path: "osu.Android/bin/Release/",
keystore_path: options[:keystore_path],
keystore_alias: options[:keystore_alias],
keystore_password: ENV["KEYSTORE_PASSWORD"]
)
end
lane :update_version do |options|
split = options[:build].split('.')
split[1] = split[1].to_s.rjust(4, '0')
android_build = split.join('')
app_version(
solution_path: 'osu.Android.sln',
version: options[:version],
build: android_build,
)
end
end
platform :ios do
desc 'Deploy to testflight'
lane :beta do |options|

View File

@ -15,6 +15,30 @@ Install _fastlane_ using
or alternatively using `brew cask install fastlane`
# Available Actions
## Android
### android beta
```
fastlane android beta
```
Deploy to play store
### android build_github
```
fastlane android build_github
```
Deploy to github release
### android build
```
fastlane android build
```
Compile the project
### android update_version
```
fastlane android update_version
```
----
## iOS
### ios beta
```

View File

@ -4,11 +4,37 @@
using System;
using Android.App;
using osu.Game;
using osu.Game.Updater;
namespace osu.Android
{
public class OsuGameAndroid : OsuGame
{
public override Version AssemblyVersion => new Version(Application.Context.ApplicationContext.PackageManager.GetPackageInfo(Application.Context.ApplicationContext.PackageName, 0).VersionName);
public override Version AssemblyVersion
{
get
{
var packageInfo = Application.Context.ApplicationContext.PackageManager.GetPackageInfo(Application.Context.ApplicationContext.PackageName, 0);
try
{
string versionName = packageInfo.VersionCode.ToString();
// undo play store version garbling
return new Version(int.Parse(versionName.Substring(0, 4)), int.Parse(versionName.Substring(4, 4)), int.Parse(versionName.Substring(8, 1)));
}
catch
{
}
return new Version(packageInfo.VersionName);
}
}
protected override void LoadComplete()
{
base.LoadComplete();
Add(new SimpleUpdateManager());
}
}
}
}

View File

@ -1,6 +1,7 @@
// 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.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Beatmaps;
@ -37,9 +38,21 @@ namespace osu.Game.Rulesets.Catch.Objects
public int ComboOffset { get; set; }
public int IndexInCurrentCombo { get; set; }
public Bindable<int> IndexInCurrentComboBindable { get; } = new Bindable<int>();
public int ComboIndex { get; set; }
public int IndexInCurrentCombo
{
get => IndexInCurrentComboBindable.Value;
set => IndexInCurrentComboBindable.Value = value;
}
public Bindable<int> ComboIndexBindable { get; } = new Bindable<int>();
public int ComboIndex
{
get => ComboIndexBindable.Value;
set => ComboIndexBindable.Value = value;
}
/// <summary>
/// Difference between the distance to the next object
@ -48,10 +61,16 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary>
public float DistanceToHyperDash { get; set; }
public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>();
/// <summary>
/// The next fruit starts a new combo. Used for explodey.
/// </summary>
public virtual bool LastInCombo { get; set; }
public virtual bool LastInCombo
{
get => LastInComboBindable.Value;
set => LastInComboBindable.Value = value;
}
public float Scale { get; set; } = 1;

View File

@ -0,0 +1,26 @@
// 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.Bindables;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneHitCircleComboChange : TestSceneHitCircle
{
private readonly Bindable<int> comboIndex = new Bindable<int>();
protected override void LoadComplete()
{
base.LoadComplete();
Scheduler.AddDelayed(() => comboIndex.Value++, 250, true);
}
protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto)
{
circle.ComboIndexBindable.BindTo(comboIndex);
circle.IndexInCurrentComboBindable.BindTo(comboIndex);
return base.CreateDrawableHitCircle(circle, auto);
}
}
}

View File

@ -297,11 +297,7 @@ namespace osu.Game.Rulesets.Osu.Tests
slider.ApplyDefaults(cpi, new BeatmapDifficulty { CircleSize = circleSize, SliderTickRate = 3 });
var drawable = new DrawableSlider(slider)
{
Anchor = Anchor.Centre,
Depth = depthIndex++
};
var drawable = CreateDrawableSlider(slider);
foreach (var mod in Mods.Value.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawable });
@ -311,6 +307,12 @@ namespace osu.Game.Rulesets.Osu.Tests
return drawable;
}
protected virtual DrawableSlider CreateDrawableSlider(Slider slider) => new DrawableSlider(slider)
{
Anchor = Anchor.Centre,
Depth = depthIndex++
};
private float judgementOffsetDirection = 1;
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)

View File

@ -0,0 +1,28 @@
// 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.Bindables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneSliderComboChange : TestSceneSlider
{
private readonly Bindable<int> comboIndex = new Bindable<int>();
protected override void LoadComplete()
{
base.LoadComplete();
Scheduler.AddDelayed(() => comboIndex.Value++, 250, true);
}
protected override DrawableSlider CreateDrawableSlider(Slider slider)
{
slider.ComboIndexBindable.BindTo(comboIndex);
slider.IndexInCurrentComboBindable.BindTo(comboIndex);
return base.CreateDrawableSlider(slider);
}
}
}

View File

@ -0,0 +1,96 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.MathUtils;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
using osuTK;
using System.Collections.Generic;
using System.Linq;
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneSpinnerRotation : TestSceneOsuPlayer
{
[Resolved]
private AudioManager audioManager { get; set; }
private TrackVirtualManual track;
protected override bool Autoplay => true;
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap)
{
var working = new ClockBackedTestWorkingBeatmap(beatmap, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
track = (TrackVirtualManual)working.Track;
return working;
}
private DrawableSpinner drawableSpinner;
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddUntilStep("wait for track to start running", () => track.IsRunning);
AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)((TestPlayer)Player).DrawableRuleset.Playfield.AllHitObjects.First());
}
[Test]
public void TestSpinnerRewindingRotation()
{
addSeekStep(5000);
AddAssert("is rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, 0, 100));
addSeekStep(0);
AddAssert("is rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, 0, 100));
}
[Test]
public void TestSpinnerMiddleRewindingRotation()
{
double estimatedRotation = 0;
addSeekStep(5000);
AddStep("retrieve rotation", () => estimatedRotation = drawableSpinner.Disc.RotationAbsolute);
addSeekStep(2500);
addSeekStep(5000);
AddAssert("is rotation absolute almost same", () => Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, estimatedRotation, 100));
}
private void addSeekStep(double time)
{
AddStep($"seek to {time}", () => track.Seek(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, ((TestPlayer)Player).DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
{
HitObjects = new List<HitObject>
{
new Spinner
{
Position = new Vector2(256, 192),
EndTime = 5000,
},
// placeholder object to avoid hitting the results screen
new HitObject
{
StartTime = 99999,
}
}
};
}
}

View File

@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return true;
},
},
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.HitCircle), _ => new MainCirclePiece(HitObject.IndexInCurrentCombo)),
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.HitCircle), _ => new MainCirclePiece()),
ApproachCircle = new ApproachCircle
{
Alpha = 0,

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private readonly NumberPiece number;
private readonly GlowPiece glow;
public MainCirclePiece(int index)
public MainCirclePiece()
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
@ -31,10 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
glow = new GlowPiece(),
circle = new CirclePiece(),
number = new NumberPiece
{
Text = (index + 1).ToString(),
},
number = new NumberPiece(),
ring = new RingPiece(),
flash = new FlashPiece(),
explode = new ExplodePiece(),
@ -42,12 +39,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
private readonly IBindable<ArmedState> state = new Bindable<ArmedState>();
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject)
{
OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject;
state.BindTo(drawableObject.State);
state.BindValueChanged(updateState, true);
@ -58,6 +57,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
glow.Colour = colour.NewValue;
circle.Colour = colour.NewValue;
}, true);
indexInCurrentCombo.BindTo(osuObject.IndexInCurrentComboBindable);
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
}
private void updateState(ValueChangedEvent<ArmedState> state)

View File

@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
lastAngle -= 360;
currentRotation += thisAngle - lastAngle;
RotationAbsolute += Math.Abs(thisAngle - lastAngle);
RotationAbsolute += Math.Abs(thisAngle - lastAngle) * Math.Sign(Clock.ElapsedFrameTime);
}
lastAngle = thisAngle;

View File

@ -58,13 +58,37 @@ namespace osu.Game.Rulesets.Osu.Objects
public virtual bool NewCombo { get; set; }
public int ComboOffset { get; set; }
public readonly Bindable<int> ComboOffsetBindable = new Bindable<int>();
public virtual int IndexInCurrentCombo { get; set; }
public int ComboOffset
{
get => ComboOffsetBindable.Value;
set => ComboOffsetBindable.Value = value;
}
public virtual int ComboIndex { get; set; }
public Bindable<int> IndexInCurrentComboBindable { get; } = new Bindable<int>();
public bool LastInCombo { get; set; }
public virtual int IndexInCurrentCombo
{
get => IndexInCurrentComboBindable.Value;
set => IndexInCurrentComboBindable.Value = value;
}
public Bindable<int> ComboIndexBindable { get; } = new Bindable<int>();
public virtual int ComboIndex
{
get => ComboIndexBindable.Value;
set => ComboIndexBindable.Value = value;
}
public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>();
public bool LastInCombo
{
get => LastInComboBindable.Value;
set => LastInComboBindable.Value = value;
}
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{

View File

@ -33,28 +33,6 @@ namespace osu.Game.Rulesets.Osu.Objects
public Vector2 StackedPositionAt(double t) => StackedPosition + this.CurvePositionAt(t);
public override int ComboIndex
{
get => base.ComboIndex;
set
{
base.ComboIndex = value;
foreach (var n in NestedHitObjects.OfType<IHasComboInformation>())
n.ComboIndex = value;
}
}
public override int IndexInCurrentCombo
{
get => base.IndexInCurrentCombo;
set
{
base.IndexInCurrentCombo = value;
foreach (var n in NestedHitObjects.OfType<IHasComboInformation>())
n.IndexInCurrentCombo = value;
}
}
public readonly Bindable<SliderPath> PathBindable = new Bindable<SliderPath>();
public SliderPath Path
@ -192,8 +170,6 @@ namespace osu.Game.Rulesets.Osu.Objects
Position = Position,
Samples = getNodeSamples(0),
SampleControlPoint = SampleControlPoint,
IndexInCurrentCombo = IndexInCurrentCombo,
ComboIndex = ComboIndex,
});
break;
@ -205,8 +181,6 @@ namespace osu.Game.Rulesets.Osu.Objects
{
StartTime = e.Time,
Position = EndPosition,
IndexInCurrentCombo = IndexInCurrentCombo,
ComboIndex = ComboIndex,
});
break;

View File

@ -9,7 +9,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Skinning;
using osuTK;
@ -25,13 +24,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
}
private readonly IBindable<ArmedState> state = new Bindable<ArmedState>();
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject, ISkinSource skin)
{
OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject;
Sprite hitCircleSprite;
SkinnableSpriteText hitCircleText;
InternalChildren = new Drawable[]
{
@ -42,14 +44,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
{
Font = OsuFont.Numeric.With(size: 40),
UseFullGlyphHeight = false,
}, confineMode: ConfineMode.NoScaling)
{
Text = (((IHasComboInformation)drawableObject.HitObject).IndexInCurrentCombo + 1).ToString()
},
}, confineMode: ConfineMode.NoScaling),
new Sprite
{
Texture = skin.GetTexture("hitcircleoverlay"),
@ -63,6 +62,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
accentColour.BindTo(drawableObject.AccentColour);
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true);
indexInCurrentCombo.BindTo(osuObject.IndexInCurrentComboBindable);
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
}
private void updateState(ValueChangedEvent<ArmedState> state)

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -115,6 +116,32 @@ namespace osu.Game.Tests.Visual.Gameplay
assertPosition(4, 1f);
}
[Test]
public void TestSliderMultiplierDoesNotAffectRelativeBeatLength()
{
var beatmap = createBeatmap(new TimingControlPoint { BeatLength = time_range });
beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2;
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
AddStep("adjust time range", () => drawableRuleset.TimeRange.Value = 5000);
for (int i = 0; i < 5; i++)
assertPosition(i, i / 5f);
}
[Test]
public void TestSliderMultiplierAffectsNonRelativeBeatLength()
{
var beatmap = createBeatmap(new TimingControlPoint { BeatLength = time_range });
beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2;
createTest(beatmap);
AddStep("adjust time range", () => drawableRuleset.TimeRange.Value = 2000);
assertPosition(0, 0);
assertPosition(1, 1);
}
private void assertPosition(int index, float relativeY) => AddAssert($"hitobject {index} at {relativeY}",
() => Precision.AlmostEquals(drawableRuleset.Playfield.AllHitObjects.ElementAt(index).DrawPosition.Y, drawableRuleset.Playfield.HitObjectContainer.DrawHeight * relativeY));
@ -193,6 +220,8 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
public new Bindable<double> TimeRange => base.TimeRange;
public TestDrawableScrollingRuleset(Ruleset ruleset, IWorkingBeatmap beatmap, IReadOnlyList<Mod> mods)
: base(ruleset, beatmap, mods)
{

View File

@ -0,0 +1,202 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
using IntroSequence = osu.Game.Configuration.IntroSequence;
namespace osu.Game.Tests.Visual.Menus
{
public class TestSceneScreenNavigation : ManualInputManagerTestScene
{
private const float click_padding = 25;
private GameHost host;
private TestOsuGame osuGame;
private Vector2 backButtonPosition => osuGame.ToScreenSpace(new Vector2(click_padding, osuGame.LayoutRectangle.Bottom - click_padding));
private Vector2 optionsButtonPosition => osuGame.ToScreenSpace(new Vector2(click_padding, click_padding));
[BackgroundDependencyLoader]
private void load(GameHost host)
{
this.host = host;
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
};
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Create new game instance", () =>
{
if (osuGame != null)
{
Remove(osuGame);
osuGame.Dispose();
}
osuGame = new TestOsuGame(LocalStorage, API);
osuGame.SetHost(host);
// todo: this can be removed once we can run audio trakcs without a device present
// see https://github.com/ppy/osu/issues/1302
osuGame.LocalConfig.Set(OsuSetting.IntroSequence, IntroSequence.Circles);
Add(osuGame);
});
AddUntilStep("Wait for load", () => osuGame.IsLoaded);
AddUntilStep("Wait for intro", () => osuGame.ScreenStack.CurrentScreen is IntroScreen);
confirmAtMainMenu();
}
[Test]
public void TestExitSongSelectWithEscape()
{
TestSongSelect songSelect = null;
pushAndConfirm(() => songSelect = new TestSongSelect());
AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
AddStep("Press escape", () => pressAndRelease(Key.Escape));
AddAssert("Overlay was hidden", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden);
exitViaEscapeAndConfirm();
}
[Test]
public void TestExitSongSelectWithClick()
{
TestSongSelect songSelect = null;
pushAndConfirm(() => songSelect = new TestSongSelect());
AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
AddStep("Move mouse to backButton", () => InputManager.MoveMouseTo(backButtonPosition));
// BackButton handles hover using its child button, so this checks whether or not any of BackButton's children are hovered.
AddUntilStep("Back button is hovered", () => InputManager.HoveredDrawables.Any(d => d.Parent == osuGame.BackButton));
AddStep("Click back button", () => InputManager.Click(MouseButton.Left));
AddUntilStep("Overlay was hidden", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden);
exitViaBackButtonAndConfirm();
}
[Test]
public void TestExitMultiWithEscape()
{
pushAndConfirm(() => new Screens.Multi.Multiplayer());
exitViaEscapeAndConfirm();
}
[Test]
public void TestExitMultiWithBackButton()
{
pushAndConfirm(() => new Screens.Multi.Multiplayer());
exitViaBackButtonAndConfirm();
}
[Test]
public void TestOpenOptionsAndExitWithEscape()
{
AddUntilStep("Wait for options to load", () => osuGame.Settings.IsLoaded);
AddStep("Enter menu", () => pressAndRelease(Key.Enter));
AddStep("Move mouse to options overlay", () => InputManager.MoveMouseTo(optionsButtonPosition));
AddStep("Click options overlay", () => InputManager.Click(MouseButton.Left));
AddAssert("Options overlay was opened", () => osuGame.Settings.State.Value == Visibility.Visible);
AddStep("Hide options overlay using escape", () => pressAndRelease(Key.Escape));
AddAssert("Options overlay was closed", () => osuGame.Settings.State.Value == Visibility.Hidden);
}
private void pushAndConfirm(Func<Screen> newScreen)
{
Screen screen = null;
AddStep("Push new screen", () => osuGame.ScreenStack.Push(screen = newScreen()));
AddUntilStep("Wait for new screen", () => osuGame.ScreenStack.CurrentScreen == screen && screen.IsLoaded);
}
private void exitViaEscapeAndConfirm()
{
AddStep("Press escape", () => pressAndRelease(Key.Escape));
confirmAtMainMenu();
}
private void exitViaBackButtonAndConfirm()
{
AddStep("Move mouse to backButton", () => InputManager.MoveMouseTo(backButtonPosition));
AddStep("Click back button", () => InputManager.Click(MouseButton.Left));
confirmAtMainMenu();
}
private void confirmAtMainMenu() => AddUntilStep("Wait for main menu", () => osuGame.ScreenStack.CurrentScreen is MainMenu menu && menu.IsLoaded);
private void pressAndRelease(Key key)
{
InputManager.PressKey(key);
InputManager.ReleaseKey(key);
}
private class TestOsuGame : OsuGame
{
public new ScreenStack ScreenStack => base.ScreenStack;
public new BackButton BackButton => base.BackButton;
public new SettingsPanel Settings => base.Settings;
public new OsuConfigManager LocalConfig => base.LocalConfig;
protected override Loader CreateLoader() => new TestLoader();
public TestOsuGame(Storage storage, IAPIProvider api)
{
Storage = storage;
API = api;
}
protected override void LoadComplete()
{
base.LoadComplete();
API.Login("Rhythm Champion", "osu!");
}
}
private class TestSongSelect : PlaySongSelect
{
public ModSelectOverlay ModSelectOverlay => ModSelect;
}
private class TestLoader : Loader
{
protected override ShaderPrecompiler CreateShaderPrecompiler() => new TestShaderPrecompiler();
private class TestShaderPrecompiler : ShaderPrecompiler
{
protected override bool AllLoaded => true;
}
}
}
}

View File

@ -22,6 +22,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public TestSceneBackButton()
{
BackButton button;
BackButton.Receptor receptor = new BackButton.Receptor();
Child = new Container
{
@ -31,12 +32,13 @@ namespace osu.Game.Tests.Visual.UserInterface
Masking = true,
Children = new Drawable[]
{
receptor,
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray
},
button = new BackButton
button = new BackButton(receptor)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Beatmaps
@ -45,25 +44,6 @@ namespace osu.Game.Beatmaps
public virtual void PostProcess()
{
void updateNestedCombo(HitObject obj, int comboIndex, int indexInCurrentCombo)
{
if (obj is IHasComboInformation objectComboInfo)
{
objectComboInfo.ComboIndex = comboIndex;
objectComboInfo.IndexInCurrentCombo = indexInCurrentCombo;
foreach (var nestedObject in obj.NestedHitObjects)
updateNestedCombo(nestedObject, comboIndex, indexInCurrentCombo);
}
}
foreach (var hitObject in Beatmap.HitObjects)
{
if (hitObject is IHasComboInformation objectComboInfo)
{
foreach (var nested in hitObject.NestedHitObjects)
updateNestedCombo(nested, objectComboInfo.ComboIndex, objectComboInfo.IndexInCurrentCombo);
}
}
}
}
}

View File

@ -0,0 +1,22 @@
// 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.Configuration;
namespace osu.Game.Configuration
{
public class InMemoryConfigManager<T> : ConfigManager<T>
where T : struct
{
public InMemoryConfigManager()
{
InitialiseDefaults();
}
protected override void PerformLoad()
{
}
protected override bool PerformSave() => true;
}
}

View File

@ -0,0 +1,21 @@
// 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.
namespace osu.Game.Configuration
{
/// <summary>
/// Stores global per-session statics. These will not be stored after exiting the game.
/// </summary>
public class SessionStatics : InMemoryConfigManager<Static>
{
protected override void InitialiseDefaults()
{
Set(Static.LoginOverlayDisplayed, false);
}
}
public enum Static
{
LoginOverlayDisplayed,
}
}

View File

@ -10,14 +10,16 @@ using osu.Game.Input.Bindings;
namespace osu.Game.Graphics.UserInterface
{
public class BackButton : VisibilityContainer, IKeyBindingHandler<GlobalAction>
public class BackButton : VisibilityContainer
{
public Action Action;
private readonly TwoLayerButton button;
public BackButton()
public BackButton(Receptor receptor)
{
receptor.OnBackPressed = () => Action?.Invoke();
Size = TwoLayerButton.SIZE_EXTENDED;
Child = button = new TwoLayerButton
@ -37,19 +39,6 @@ namespace osu.Game.Graphics.UserInterface
button.HoverColour = colours.PinkDark;
}
public bool OnPressed(GlobalAction action)
{
if (action == GlobalAction.Back)
{
Action?.Invoke();
return true;
}
return false;
}
public bool OnReleased(GlobalAction action) => action == GlobalAction.Back;
protected override void PopIn()
{
button.MoveToX(0, 400, Easing.OutQuint);
@ -61,5 +50,24 @@ namespace osu.Game.Graphics.UserInterface
button.MoveToX(-TwoLayerButton.SIZE_EXTENDED.X / 2, 400, Easing.OutQuint);
button.FadeOut(400, Easing.OutQuint);
}
public class Receptor : Drawable, IKeyBindingHandler<GlobalAction>
{
public Action OnBackPressed;
public bool OnPressed(GlobalAction action)
{
switch (action)
{
case GlobalAction.Back:
OnBackPressed?.Invoke();
return true;
}
return false;
}
public bool OnReleased(GlobalAction action) => action == GlobalAction.Back;
}
}
}

View File

@ -64,7 +64,10 @@ namespace osu.Game.Online.API
public void Perform(IAPIProvider api)
{
if (!(api is APIAccess apiAccess))
throw new NotSupportedException($"A {nameof(APIAccess)} is required to perform requests.");
{
Fail(new NotSupportedException($"A {nameof(APIAccess)} is required to perform requests."));
return;
}
API = apiAccess;

View File

@ -81,10 +81,14 @@ namespace osu.Game
public readonly Bindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>();
private OsuScreenStack screenStack;
protected OsuScreenStack ScreenStack;
protected BackButton BackButton;
protected SettingsPanel Settings;
private VolumeOverlay volume;
private OsuLogo osuLogo;
private BackButton backButton;
private MainMenu menuScreen;
@ -96,8 +100,6 @@ namespace osu.Game
private readonly string[] args;
private SettingsPanel settings;
private readonly List<OverlayContainer> overlays = new List<OverlayContainer>();
private readonly List<OverlayContainer> toolbarElements = new List<OverlayContainer>();
@ -318,6 +320,8 @@ namespace osu.Game
}, $"watch {databasedScoreInfo}", bypassScreenAllowChecks: true);
}
protected virtual Loader CreateLoader() => new Loader();
#region Beatmap progression
private void beatmapChanged(ValueChangedEvent<WorkingBeatmap> beatmap)
@ -356,7 +360,7 @@ namespace osu.Game
performFromMainMenuTask?.Cancel();
// if the current screen does not allow screen changing, give the user an option to try again later.
if (!bypassScreenAllowChecks && (screenStack.CurrentScreen as IOsuScreen)?.AllowExternalScreenChange == false)
if (!bypassScreenAllowChecks && (ScreenStack.CurrentScreen as IOsuScreen)?.AllowExternalScreenChange == false)
{
notifications.Post(new SimpleNotification
{
@ -374,7 +378,7 @@ namespace osu.Game
CloseAllOverlays(false);
// we may already be at the target screen type.
if (targetScreen != null && screenStack.CurrentScreen?.GetType() == targetScreen)
if (targetScreen != null && ScreenStack.CurrentScreen?.GetType() == targetScreen)
{
action();
return;
@ -421,6 +425,7 @@ namespace osu.Game
ScoreManager.PresentImport = items => PresentScore(items.First());
Container logoContainer;
BackButton.Receptor receptor;
dependencies.CacheAs(idleTracker = new GameIdleTracker(6000));
@ -437,15 +442,16 @@ namespace osu.Game
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
screenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both },
backButton = new BackButton
receptor = new BackButton.Receptor(),
ScreenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both },
BackButton = new BackButton(receptor)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Action = () =>
{
if ((screenStack.CurrentScreen as IOsuScreen)?.AllowBackButton == true)
screenStack.Exit();
if ((ScreenStack.CurrentScreen as IOsuScreen)?.AllowBackButton == true)
ScreenStack.Exit();
}
},
logoContainer = new Container { RelativeSizeAxes = Axes.Both },
@ -458,18 +464,15 @@ namespace osu.Game
idleTracker
});
screenStack.ScreenPushed += screenPushed;
screenStack.ScreenExited += screenExited;
ScreenStack.ScreenPushed += screenPushed;
ScreenStack.ScreenExited += screenExited;
loadComponentSingleFile(osuLogo, logo =>
{
logoContainer.Add(logo);
// Loader has to be created after the logo has finished loading as Loader performs logo transformations on entering.
screenStack.Push(new Loader
{
RelativeSizeAxes = Axes.Both
});
ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both));
});
loadComponentSingleFile(Toolbar = new Toolbar
@ -505,7 +508,7 @@ namespace osu.Game
loadComponentSingleFile(social = new SocialOverlay(), overlayContent.Add, true);
loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true);
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
loadComponentSingleFile(settings = new SettingsOverlay { GetToolbarHeight = () => ToolbarOffset }, leftFloatingOverlayContent.Add, true);
loadComponentSingleFile(Settings = new SettingsOverlay { GetToolbarHeight = () => ToolbarOffset }, leftFloatingOverlayContent.Add, true);
var changelogOverlay = loadComponentSingleFile(new ChangelogOverlay(), overlayContent.Add, true);
loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true);
loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true);
@ -536,7 +539,7 @@ namespace osu.Game
Add(externalLinkOpener = new ExternalLinkOpener());
var singleDisplaySideOverlays = new OverlayContainer[] { settings, notifications };
var singleDisplaySideOverlays = new OverlayContainer[] { Settings, notifications };
overlays.AddRange(singleDisplaySideOverlays);
foreach (var overlay in singleDisplaySideOverlays)
@ -589,7 +592,7 @@ namespace osu.Game
{
float offset = 0;
if (settings.State.Value == Visibility.Visible)
if (Settings.State.Value == Visibility.Visible)
offset += ToolbarButton.WIDTH / 2;
if (notifications.State.Value == Visibility.Visible)
offset -= ToolbarButton.WIDTH / 2;
@ -597,7 +600,7 @@ namespace osu.Game
screenContainer.MoveToX(offset, SettingsPanel.TRANSITION_LENGTH, Easing.OutQuint);
}
settings.State.ValueChanged += _ => updateScreenOffset();
Settings.State.ValueChanged += _ => updateScreenOffset();
notifications.State.ValueChanged += _ => updateScreenOffset();
}
@ -742,7 +745,7 @@ namespace osu.Game
return true;
case GlobalAction.ToggleSettings:
settings.ToggleVisibility();
Settings.ToggleVisibility();
return true;
case GlobalAction.ToggleDirect:
@ -789,13 +792,13 @@ namespace osu.Game
protected override bool OnExiting()
{
if (screenStack.CurrentScreen is Loader)
if (ScreenStack.CurrentScreen is Loader)
return false;
if (introScreen == null)
return true;
if (!introScreen.DidLoadMenu || !(screenStack.CurrentScreen is IntroScreen))
if (!introScreen.DidLoadMenu || !(ScreenStack.CurrentScreen is IntroScreen))
{
Scheduler.Add(introScreen.MakeCurrent);
return true;
@ -823,7 +826,7 @@ namespace osu.Game
screenContainer.Padding = new MarginPadding { Top = ToolbarOffset };
overlayContent.Padding = new MarginPadding { Top = ToolbarOffset };
MenuCursorContainer.CanShowCursor = (screenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false;
MenuCursorContainer.CanShowCursor = (ScreenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false;
}
protected virtual void ScreenChanged(IScreen current, IScreen newScreen)
@ -849,9 +852,9 @@ namespace osu.Game
Toolbar.Show();
if (newOsuScreen.AllowBackButton)
backButton.Show();
BackButton.Show();
else
backButton.Hide();
BackButton.Hide();
}
}

View File

@ -65,7 +65,7 @@ namespace osu.Game
protected RulesetConfigCache RulesetConfigCache;
protected APIAccess API;
protected IAPIProvider API;
protected MenuCursorContainer MenuCursorContainer;
@ -73,6 +73,8 @@ namespace osu.Game
protected override Container<Drawable> Content => content;
protected Storage Storage { get; set; }
private Bindable<WorkingBeatmap> beatmap; // cached via load() method
[Cached]
@ -123,7 +125,7 @@ namespace osu.Game
{
Resources.AddStore(new DllResourceStore(@"osu.Game.Resources.dll"));
dependencies.Cache(contextFactory = new DatabaseContextFactory(Host.Storage));
dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage));
var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures")));
largeStore.AddStore(Host.CreateTextureLoaderStore(new OnlineStore()));
@ -158,21 +160,21 @@ namespace osu.Game
runMigrations();
dependencies.Cache(SkinManager = new SkinManager(Host.Storage, contextFactory, Host, Audio, new NamespacedResourceStore<byte[]>(Resources, "Skins/Legacy")));
dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Audio, new NamespacedResourceStore<byte[]>(Resources, "Skins/Legacy")));
dependencies.CacheAs<ISkinSource>(SkinManager);
API = new APIAccess(LocalConfig);
if (API == null) API = new APIAccess(LocalConfig);
dependencies.CacheAs<IAPIProvider>(API);
dependencies.CacheAs(API);
var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures);
dependencies.Cache(RulesetStore = new RulesetStore(contextFactory));
dependencies.Cache(FileStore = new FileStore(contextFactory, Host.Storage));
dependencies.Cache(FileStore = new FileStore(contextFactory, Storage));
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Host.Storage, API, contextFactory, Host));
dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory, RulesetStore, API, Audio, Host, defaultBeatmap));
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Host, defaultBeatmap));
// this should likely be moved to ArchiveModelManager when another case appers where it is necessary
// to have inter-dependent model managers. this could be obtained with an IHasForeign<T> interface to
@ -189,6 +191,7 @@ namespace osu.Game
dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore));
dependencies.Cache(SettingsStore = new SettingsStore(contextFactory));
dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore));
dependencies.Cache(new SessionStatics());
dependencies.Cache(new OsuColour());
fileImporters.Add(BeatmapManager);
@ -206,7 +209,8 @@ namespace osu.Game
FileStore.Cleanup();
AddInternal(API);
if (API is APIAccess apiAcces)
AddInternal(apiAcces);
AddInternal(RulesetConfigCache);
GlobalActionContainer globalBinding;
@ -266,9 +270,13 @@ namespace osu.Game
public override void SetHost(GameHost host)
{
if (LocalConfig == null)
LocalConfig = new OsuConfigManager(host.Storage);
base.SetHost(host);
if (Storage == null)
Storage = host.Storage;
if (LocalConfig == null)
LocalConfig = new OsuConfigManager(Storage);
}
private readonly List<ICanAcceptFiles> fileImporters = new List<ICanAcceptFiles>();

View File

@ -57,7 +57,7 @@ namespace osu.Game.Overlays
protected override void LoadComplete()
{
beatmap.BindValueChanged(beatmapChanged, true);
mods.BindValueChanged(_ => updateAudioAdjustments(), true);
mods.BindValueChanged(_ => ResetTrackAdjustments(), true);
base.LoadComplete();
}
@ -213,12 +213,12 @@ namespace osu.Game.Overlays
current = beatmap.NewValue;
TrackChanged?.Invoke(current, direction);
updateAudioAdjustments();
ResetTrackAdjustments();
queuedDirection = null;
}
private void updateAudioAdjustments()
public void ResetTrackAdjustments()
{
var track = current?.Track;
if (track == null)

View File

@ -76,6 +76,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// </summary>
public JudgementResult Result { get; private set; }
private Bindable<int> comboIndexBindable;
public override bool RemoveWhenNotAlive => false;
public override bool RemoveCompletedTransforms => false;
protected override bool RequiresChildrenUpdate => true;
@ -122,6 +124,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
protected override void LoadComplete()
{
base.LoadComplete();
if (HitObject is IHasComboInformation combo)
{
comboIndexBindable = combo.ComboIndexBindable.GetBoundCopy();
comboIndexBindable.BindValueChanged(_ => updateAccentColour());
}
updateState(ArmedState.Idle, true);
}
@ -244,12 +253,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
{
base.SkinChanged(skin, allowFallback);
if (HitObject is IHasComboInformation combo)
{
var comboColours = skin.GetConfig<GlobalSkinConfiguration, List<Color4>>(GlobalSkinConfiguration.ComboColours)?.Value;
AccentColour.Value = comboColours?.Count > 0 ? comboColours[combo.ComboIndex % comboColours.Count] : Color4.White;
}
updateAccentColour();
ApplySkin(skin, allowFallback);
@ -257,6 +261,15 @@ namespace osu.Game.Rulesets.Objects.Drawables
updateState(State.Value, true);
}
private void updateAccentColour()
{
if (HitObject is IHasComboInformation combo)
{
var comboColours = CurrentSkin.GetConfig<GlobalSkinConfiguration, List<Color4>>(GlobalSkinConfiguration.ComboColours)?.Value;
AccentColour.Value = comboColours?.Count > 0 ? comboColours[combo.ComboIndex % comboColours.Count] : Color4.White;
}
}
/// <summary>
/// Called when a change is made to the skin.
/// </summary>
@ -316,8 +329,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
get => lifetimeStart ?? (HitObject.StartTime - InitialLifetimeOffset);
set
{
base.LifetimeStart = value;
lifetimeStart = value;
base.LifetimeStart = value;
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Game.Audio;
@ -82,6 +83,15 @@ namespace osu.Game.Rulesets.Objects
CreateNestedHitObjects();
if (this is IHasComboInformation hasCombo)
{
foreach (var n in NestedHitObjects.OfType<IHasComboInformation>())
{
n.ComboIndexBindable.BindTo(hasCombo.ComboIndexBindable);
n.IndexInCurrentComboBindable.BindTo(hasCombo.IndexInCurrentComboBindable);
}
}
nestedHitObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
foreach (var h in nestedHitObjects)

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
namespace osu.Game.Rulesets.Objects.Types
{
/// <summary>
@ -8,16 +10,22 @@ namespace osu.Game.Rulesets.Objects.Types
/// </summary>
public interface IHasComboInformation : IHasCombo
{
Bindable<int> IndexInCurrentComboBindable { get; }
/// <summary>
/// The offset of this hitobject in the current combo.
/// </summary>
int IndexInCurrentCombo { get; set; }
Bindable<int> ComboIndexBindable { get; }
/// <summary>
/// The offset of this combo in relation to the beatmap.
/// </summary>
int ComboIndex { get; set; }
Bindable<bool> LastInComboBindable { get; }
/// <summary>
/// Whether this is the last object in the current combo.
/// </summary>

View File

@ -131,7 +131,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
if (duration > maxDuration)
{
maxDuration = duration;
baseBeatLength = timingPoints[i].BeatLength;
// The slider multiplier is post-multiplied to determine the final velocity, but for relative scale beat lengths
// the multiplier should not affect the effective timing point (the longest in the beatmap), so it is factored out here
baseBeatLength = timingPoints[i].BeatLength / Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier;
}
}
}

View File

@ -77,6 +77,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
if (!initialStateCache.IsValid)
{
foreach (var cached in hitObjectInitialStateCache.Values)
cached.Invalidate();
switch (direction.Value)
{
case ScrollingDirection.Up:

View File

@ -63,13 +63,15 @@ namespace osu.Game.Screens.Menu
protected override BackgroundScreen CreateBackground() => background;
private Bindable<int> holdDelay;
private Bindable<bool> loginDisplayed;
private ExitConfirmOverlay exitConfirmOverlay;
[BackgroundDependencyLoader(true)]
private void load(DirectOverlay direct, SettingsOverlay settings, OsuConfigManager config)
private void load(DirectOverlay direct, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics)
{
holdDelay = config.GetBindable<int>(OsuSetting.UIHoldActivationDelay);
loginDisplayed = statics.GetBindable<bool>(Static.LoginOverlayDisplayed);
if (host.CanExit)
{
@ -170,7 +172,6 @@ namespace osu.Game.Screens.Menu
Beatmap.ValueChanged += beatmap_ValueChanged;
}
private bool loginDisplayed;
private bool exitConfirmed;
protected override void LogoArriving(OsuLogo logo, bool resuming)
@ -198,10 +199,10 @@ namespace osu.Game.Screens.Menu
bool displayLogin()
{
if (!loginDisplayed)
if (!loginDisplayed.Value)
{
Scheduler.AddDelayed(() => login?.Show(), 500);
loginDisplayed = true;
loginDisplayed.Value = true;
}
return true;

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.MathUtils;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@ -45,7 +46,6 @@ namespace osu.Game.Screens.Play.HUD
{
text = new OsuSpriteText
{
Text = "hold for menu",
Font = OsuFont.GetFont(weight: FontWeight.Bold),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
@ -60,9 +60,23 @@ namespace osu.Game.Screens.Play.HUD
AutoSizeAxes = Axes.Both;
}
[Resolved]
private OsuConfigManager config { get; set; }
private Bindable<int> activationDelay;
protected override void LoadComplete()
{
activationDelay = config.GetBindable<int>(OsuSetting.UIHoldActivationDelay);
activationDelay.BindValueChanged(v =>
{
text.Text = v.NewValue > 0
? "hold for menu"
: "press for menu";
}, true);
text.FadeInFromZero(500, Easing.OutQuint).Delay(1500).FadeOut(500, Easing.OutQuint);
base.LoadComplete();
}

View File

@ -490,6 +490,7 @@ namespace osu.Game.Screens.Select
BeatmapDetails.Leaderboard.RefreshScores();
Beatmap.Value.Track.Looping = true;
music?.ResetTrackAdjustments();
if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending)
{

View File

@ -12,13 +12,17 @@ namespace osu.Game.Skinning
/// </summary>
public abstract class SkinReloadableDrawable : CompositeDrawable
{
/// <summary>
/// The current skin source.
/// </summary>
protected ISkinSource CurrentSkin { get; private set; }
private readonly Func<ISkinSource, bool> allowFallback;
private ISkinSource skin;
/// <summary>
/// Whether fallback to default skin should be allowed if the custom skin is missing this resource.
/// </summary>
private bool allowDefaultFallback => allowFallback == null || allowFallback.Invoke(skin);
private bool allowDefaultFallback => allowFallback == null || allowFallback.Invoke(CurrentSkin);
/// <summary>
/// Create a new <see cref="SkinReloadableDrawable"/>
@ -32,19 +36,19 @@ namespace osu.Game.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource source)
{
skin = source;
skin.SourceChanged += onChange;
CurrentSkin = source;
CurrentSkin.SourceChanged += onChange;
}
private void onChange() =>
// schedule required to avoid calls after disposed.
// note that this has the side-effect of components only performing a skin change when they are alive.
Scheduler.AddOnce(() => SkinChanged(skin, allowDefaultFallback));
Scheduler.AddOnce(() => SkinChanged(CurrentSkin, allowDefaultFallback));
protected override void LoadAsyncComplete()
{
base.LoadAsyncComplete();
SkinChanged(skin, allowDefaultFallback);
SkinChanged(CurrentSkin, allowDefaultFallback);
}
/// <summary>
@ -60,8 +64,8 @@ namespace osu.Game.Skinning
{
base.Dispose(isDisposing);
if (skin != null)
skin.SourceChanged -= onChange;
if (CurrentSkin != null)
CurrentSkin.SourceChanged -= onChange;
}
}
}

View File

@ -78,7 +78,8 @@ namespace osu.Game.Updater
break;
case RuntimeInfo.Platform.Android:
bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".apk"));
// on our testing device this causes the download to magically disappear.
//bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".apk"));
break;
}

View File

@ -21,7 +21,7 @@
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Humanizer" Version="2.7.2" />
<PackageReference Include="Humanizer" Version="2.7.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />

View File

@ -113,7 +113,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Humanizer" Version="2.7.2" />
<PackageReference Include="Humanizer" Version="2.7.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />