Merge branch 'master' into roompanel

This commit is contained in:
Dean Herbert 2017-05-02 18:04:48 +09:00 committed by GitHub
commit 5a8a8acbc8
77 changed files with 2173 additions and 1047 deletions

View File

@ -1,10 +1,6 @@
# osu! [![Build status](https://ci.appveyor.com/api/projects/status/u2p01nx7l6og8buh?svg=true)](https://ci.appveyor.com/project/peppy/osu)
# osu! [![Build status](https://ci.appveyor.com/api/projects/status/u2p01nx7l6og8buh?svg=true)](https://ci.appveyor.com/project/peppy/osu) [![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu) [![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)
[osu! on the web](https://osu.ppy.sh) | [dev chat](https://discord.gg/ppy)
Rhythm is just a *click* away. The future of osu! and the beginning of an open era!
Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the beginning of an open era!
# Status
@ -12,14 +8,14 @@ This is still heavily under development and is not intended for end-user use. Th
# Requirements
- A desktop platform which can compile .NET 4.5.
- Visual Studio or MonoDevelop is recommended.
- A desktop platform which can compile .NET 4.5 (tested on macOS, linux and windows). We recommend using [Visual Studio Code](https://code.visualstudio.com/) (all platforms) or [Visual Studio Community Edition](https://www.visualstudio.com/) (windows only), both of which are free.
- Make sure you initialise and keep submodules up-to-date.
# Contributing
We welcome all contributions, but keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention on having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time, to ensure no effort is wasted.
Contributions can be made via pull requests to this repository. We hope to credit and reward larger contributions via a [bounty system](https://goo.gl/nFdoyI). If you're unsure of what you can help with, check out the [list](https://github.com/ppy/osu/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3Abounty) of available issues with bounty.
Contributions can be made via pull requests to this repository. We hope to credit and reward larger contributions via a [bounty system](https://www.bountysource.com/teams/ppy). If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu-framework/issues).
Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured; with any libraries we are using; with any processes involved with contributing, *please* bring it up. I welcome all feedback so we can make contributing to this project as pain-free as possible.

@ -1 +1 @@
Subproject commit 9204b838504a51ffe7577c103b91270a2687bfb8
Subproject commit 58c108309f72462d8dc45aa488ab78cd6db08cf1

View File

@ -38,9 +38,9 @@ namespace osu.Desktop.VisualTests.Tests
Origin = Anchor.TopLeft,
});
AddStep("Toggle Bar", progress.ToggleBar);
AddStep("Toggle Bar", () => progress.AllowSeeking = !progress.AllowSeeking);
AddWaitStep(5);
AddStep("Toggle Bar", progress.ToggleBar);
AddStep("Toggle Bar", () => progress.AllowSeeking = !progress.AllowSeeking);
AddWaitStep(2);
AddRepeatStep("New Values", displayNewValues, 5);

View File

@ -3,6 +3,7 @@
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using System;
@ -95,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.Mods
protected override Score CreateReplayScore(Beatmap<OsuHitObject> beatmap) => new Score
{
Replay = new OsuAutoReplay(beatmap)
Replay = new OsuAutoGenerator(beatmap).Generate()
};
}

View File

@ -12,16 +12,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
public class FollowPoint : Container
{
public double StartTime;
public double EndTime;
public Vector2 EndPosition;
private const float width = 8;
public FollowPoint()
{
Origin = Anchor.Centre;
Alpha = 0;
Masking = true;
AutoSizeAxes = Axes.Both;
@ -45,22 +40,5 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Delay(StartTime);
FadeIn(DrawableOsuHitObject.TIME_FADEIN);
ScaleTo(1.5f);
ScaleTo(1, DrawableOsuHitObject.TIME_FADEIN, EasingTypes.Out);
MoveTo(EndPosition, DrawableOsuHitObject.TIME_FADEIN, EasingTypes.Out);
Delay(EndTime - StartTime);
FadeOut(DrawableOsuHitObject.TIME_FADEIN);
Delay(DrawableOsuHitObject.TIME_FADEIN);
Expire(true);
}
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using OpenTK;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
@ -80,14 +81,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
double fadeOutTime = startTime + fraction * duration;
double fadeInTime = fadeOutTime - PreEmpt;
Add(new FollowPoint
FollowPoint fp;
Add(fp = new FollowPoint
{
StartTime = fadeInTime,
EndTime = fadeOutTime,
Position = pointStartPosition,
EndPosition = pointEndPosition,
Rotation = rotation,
Alpha = 0,
Scale = new Vector2(1.5f),
});
using (fp.BeginAbsoluteSequence(fadeInTime))
{
fp.FadeIn(DrawableOsuHitObject.TIME_FADEIN);
fp.ScaleTo(1, DrawableOsuHitObject.TIME_FADEIN, EasingTypes.Out);
fp.MoveTo(pointEndPosition, DrawableOsuHitObject.TIME_FADEIN, EasingTypes.Out);
fp.Delay(fadeOutTime - fadeInTime);
fp.FadeOut(DrawableOsuHitObject.TIME_FADEIN);
}
fp.Expire(true);
}
}
prevHitObject = currHitObject;

View File

@ -104,10 +104,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ApproachCircle.ScaleTo(1.1f, TIME_PREEMPT);
}
protected override void UpdateState(ArmedState state)
protected override void UpdateCurrentState(ArmedState state)
{
base.UpdateState(state);
ApproachCircle.FadeOut();
double endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime;

View File

@ -21,17 +21,23 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override OsuJudgement CreateJudgement() => new OsuJudgement { MaxScore = OsuScoreResult.Hit300 };
protected override void UpdateState(ArmedState state)
protected sealed override void UpdateState(ArmedState state)
{
Flush();
UpdateInitialState();
Delay(HitObject.StartTime - Time.Current - TIME_PREEMPT + Judgement.TimeOffset, true);
using (BeginAbsoluteSequence(HitObject.StartTime - TIME_PREEMPT, true))
{
UpdatePreemptState();
UpdatePreemptState();
using (BeginDelayedSequence(TIME_PREEMPT + Judgement.TimeOffset, true))
UpdateCurrentState(state);
}
}
Delay(TIME_PREEMPT, true);
protected virtual void UpdateCurrentState(ArmedState state)
{
}
protected virtual void UpdatePreemptState()

View File

@ -158,10 +158,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ball.Alpha = 0;
}
protected override void UpdateState(ArmedState state)
protected override void UpdateCurrentState(ArmedState state)
{
base.UpdateState(state);
ball.FadeIn();
Delay(slider.Duration, true);
@ -181,4 +179,4 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
void UpdateProgress(double progress, int repeat);
}
}
}

View File

@ -72,10 +72,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Delay(-animIn);
}
protected override void UpdateState(ArmedState state)
protected override void UpdateCurrentState(ArmedState state)
{
base.UpdateState(state);
switch (state)
{
case ArmedState.Idle:
@ -93,4 +91,4 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
}
}
}

View File

@ -132,10 +132,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
disc.FadeIn(200);
}
protected override void UpdateState(ArmedState state)
protected override void UpdateCurrentState(ArmedState state)
{
base.UpdateState(state);
Delay(spinner.Duration, true);
FadeOut(160);

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Objects
set { Curve.Distance = value; }
}
public List<List<SampleInfo>> RepeatSamples { get; set; } = new List<List<SampleInfo>>();
public List<SampleInfoList> RepeatSamples { get; set; } = new List<SampleInfoList>();
public int RepeatCount { get; set; } = 1;
private int stackHeight;
@ -117,12 +117,12 @@ namespace osu.Game.Rulesets.Osu.Objects
StackHeight = StackHeight,
Scale = Scale,
ComboColour = ComboColour,
Samples = Samples.Select(s => new SampleInfo
Samples = new SampleInfoList(Samples.Select(s => new SampleInfo
{
Bank = s.Bank,
Name = @"slidertick",
Volume = s.Volume
}).ToList()
}))
};
}
}

View File

@ -1,315 +0,0 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using osu.Framework.MathUtils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Replays;
using osu.Game.Users;
namespace osu.Game.Rulesets.Osu
{
public class OsuAutoReplay : Replay
{
private static readonly Vector2 spinner_centre = new Vector2(256, 192);
private const float spin_radius = 50;
private readonly Beatmap<OsuHitObject> beatmap;
public OsuAutoReplay(Beatmap<OsuHitObject> beatmap)
{
this.beatmap = beatmap;
User = new User
{
Username = @"Autoplay",
};
createAutoReplay();
}
private class ReplayFrameComparer : IComparer<ReplayFrame>
{
public int Compare(ReplayFrame f1, ReplayFrame f2)
{
if (f1 == null) throw new NullReferenceException($@"{nameof(f1)} cannot be null");
if (f2 == null) throw new NullReferenceException($@"{nameof(f2)} cannot be null");
return f1.Time.CompareTo(f2.Time);
}
}
private static readonly IComparer<ReplayFrame> replay_frame_comparer = new ReplayFrameComparer();
private int findInsertionIndex(ReplayFrame frame)
{
int index = Frames.BinarySearch(frame, replay_frame_comparer);
if (index < 0)
{
index = ~index;
}
else
{
// Go to the first index which is actually bigger
while (index < Frames.Count && frame.Time == Frames[index].Time)
{
++index;
}
}
return index;
}
private void addFrameToReplay(ReplayFrame frame) => Frames.Insert(findInsertionIndex(frame), frame);
private static Vector2 circlePosition(double t, double radius) => new Vector2((float)(Math.Cos(t) * radius), (float)(Math.Sin(t) * radius));
private double applyModsToTime(double v) => v;
private double applyModsToRate(double v) => v;
public bool DelayedMovements; // ModManager.CheckActive(Mods.Relax2);
private void createAutoReplay()
{
int buttonIndex = 0;
EasingTypes preferredEasing = DelayedMovements ? EasingTypes.InOutCubic : EasingTypes.Out;
addFrameToReplay(new ReplayFrame(-100000, 256, 500, ReplayButtonState.None));
addFrameToReplay(new ReplayFrame(beatmap.HitObjects[0].StartTime - 1500, 256, 500, ReplayButtonState.None));
addFrameToReplay(new ReplayFrame(beatmap.HitObjects[0].StartTime - 1000, 256, 192, ReplayButtonState.None));
// We are using ApplyModsToRate and not ApplyModsToTime to counteract the speed up / slow down from HalfTime / DoubleTime so that we remain at a constant framerate of 60 fps.
float frameDelay = (float)applyModsToRate(1000.0 / 60.0);
// Already superhuman, but still somewhat realistic
int reactionTime = (int)applyModsToRate(100);
for (int i = 0; i < beatmap.HitObjects.Count; i++)
{
OsuHitObject h = beatmap.HitObjects[i];
//if (h.EndTime < InputManager.ReplayStartTime)
//{
// h.IsHit = true;
// continue;
//}
int endDelay = h is Spinner ? 1 : 0;
if (DelayedMovements && i > 0)
{
OsuHitObject last = beatmap.HitObjects[i - 1];
double endTime = (last as IHasEndTime)?.EndTime ?? last.StartTime;
//Make the cursor stay at a hitObject as long as possible (mainly for autopilot).
if (h.StartTime - h.HitWindowFor(OsuScoreResult.Miss) > endTime + h.HitWindowFor(OsuScoreResult.Hit50) + 50)
{
if (!(last is Spinner) && h.StartTime - endTime < 1000) addFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit50), last.EndPosition.X, last.EndPosition.Y, ReplayButtonState.None));
if (!(h is Spinner)) addFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Miss), h.Position.X, h.Position.Y, ReplayButtonState.None));
}
else if (h.StartTime - h.HitWindowFor(OsuScoreResult.Hit50) > endTime + h.HitWindowFor(OsuScoreResult.Hit50) + 50)
{
if (!(last is Spinner) && h.StartTime - endTime < 1000) addFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit50), last.EndPosition.X, last.EndPosition.Y, ReplayButtonState.None));
if (!(h is Spinner)) addFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Hit50), h.Position.X, h.Position.Y, ReplayButtonState.None));
}
else if (h.StartTime - h.HitWindowFor(OsuScoreResult.Hit100) > endTime + h.HitWindowFor(OsuScoreResult.Hit100) + 50)
{
if (!(last is Spinner) && h.StartTime - endTime < 1000) addFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit100), last.EndPosition.X, last.EndPosition.Y, ReplayButtonState.None));
if (!(h is Spinner)) addFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Hit100), h.Position.X, h.Position.Y, ReplayButtonState.None));
}
}
Vector2 targetPosition = h.Position;
EasingTypes easing = preferredEasing;
float spinnerDirection = -1;
if (h is Spinner)
{
targetPosition = Frames[Frames.Count - 1].Position;
Vector2 difference = spinner_centre - targetPosition;
float differenceLength = difference.Length;
float newLength = (float)Math.Sqrt(differenceLength * differenceLength - spin_radius * spin_radius);
if (differenceLength > spin_radius)
{
float angle = (float)Math.Asin(spin_radius / differenceLength);
if (angle > 0)
{
spinnerDirection = -1;
}
else
{
spinnerDirection = 1;
}
difference.X = difference.X * (float)Math.Cos(angle) - difference.Y * (float)Math.Sin(angle);
difference.Y = difference.X * (float)Math.Sin(angle) + difference.Y * (float)Math.Cos(angle);
difference.Normalize();
difference *= newLength;
targetPosition += difference;
easing = EasingTypes.In;
}
else if (difference.Length > 0)
{
targetPosition = spinner_centre - difference * (spin_radius / difference.Length);
}
else
{
targetPosition = spinner_centre + new Vector2(0, -spin_radius);
}
}
// Do some nice easing for cursor movements
if (Frames.Count > 0)
{
ReplayFrame lastFrame = Frames[Frames.Count - 1];
// Wait until Auto could "see and react" to the next note.
double waitTime = h.StartTime - Math.Max(0.0, DrawableOsuHitObject.TIME_PREEMPT - reactionTime);
if (waitTime > lastFrame.Time)
{
lastFrame = new ReplayFrame(waitTime, lastFrame.MouseX, lastFrame.MouseY, lastFrame.ButtonState);
addFrameToReplay(lastFrame);
}
Vector2 lastPosition = lastFrame.Position;
double timeDifference = applyModsToTime(h.StartTime - lastFrame.Time);
// Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up.
if (timeDifference > 0 && // Sanity checks
((lastPosition - targetPosition).Length > h.Radius * (1.5 + 100.0 / timeDifference) || // Either the distance is big enough
timeDifference >= 266)) // ... or the beats are slow enough to tap anyway.
{
// Perform eased movement
for (double time = lastFrame.Time + frameDelay; time < h.StartTime; time += frameDelay)
{
Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPosition, lastFrame.Time, h.StartTime, easing);
addFrameToReplay(new ReplayFrame((int)time, currentPosition.X, currentPosition.Y, lastFrame.ButtonState));
}
buttonIndex = 0;
}
else
{
buttonIndex++;
}
}
ReplayButtonState button = buttonIndex % 2 == 0 ? ReplayButtonState.Left1 : ReplayButtonState.Right1;
double hEndTime = ((h as IHasEndTime)?.EndTime ?? h.StartTime) + KEY_UP_DELAY;
ReplayFrame newFrame = new ReplayFrame(h.StartTime, targetPosition.X, targetPosition.Y, button);
ReplayFrame endFrame = new ReplayFrame(hEndTime + endDelay, h.EndPosition.X, h.EndPosition.Y, ReplayButtonState.None);
// Decrement because we want the previous frame, not the next one
int index = findInsertionIndex(newFrame) - 1;
// Do we have a previous frame? No need to check for < replay.Count since we decremented!
if (index >= 0)
{
ReplayFrame previousFrame = Frames[index];
var previousButton = previousFrame.ButtonState;
// If a button is already held, then we simply alternate
if (previousButton != ReplayButtonState.None)
{
Debug.Assert(previousButton != (ReplayButtonState.Left1 | ReplayButtonState.Right1));
// Force alternation if we have the same button. Otherwise we can just keep the naturally to us assigned button.
if (previousButton == button)
{
button = (ReplayButtonState.Left1 | ReplayButtonState.Right1) & ~button;
newFrame.ButtonState = button;
}
// We always follow the most recent slider / spinner, so remove any other frames that occur while it exists.
int endIndex = findInsertionIndex(endFrame);
if (index < Frames.Count - 1)
Frames.RemoveRange(index + 1, Math.Max(0, endIndex - (index + 1)));
// After alternating we need to keep holding the other button in the future rather than the previous one.
for (int j = index + 1; j < Frames.Count; ++j)
{
// Don't affect frames which stop pressing a button!
if (j < Frames.Count - 1 || Frames[j].ButtonState == previousButton)
Frames[j].ButtonState = button;
}
}
}
addFrameToReplay(newFrame);
// We add intermediate frames for spinning / following a slider here.
if (h is Spinner)
{
Spinner s = h as Spinner;
Vector2 difference = targetPosition - spinner_centre;
float radius = difference.Length;
float angle = radius == 0 ? 0 : (float)Math.Atan2(difference.Y, difference.X);
double t;
for (double j = h.StartTime + frameDelay; j < s.EndTime; j += frameDelay)
{
t = applyModsToTime(j - h.StartTime) * spinnerDirection;
Vector2 pos = spinner_centre + circlePosition(t / 20 + angle, spin_radius);
addFrameToReplay(new ReplayFrame((int)j, pos.X, pos.Y, button));
}
t = applyModsToTime(s.EndTime - h.StartTime) * spinnerDirection;
Vector2 endPosition = spinner_centre + circlePosition(t / 20 + angle, spin_radius);
addFrameToReplay(new ReplayFrame(s.EndTime, endPosition.X, endPosition.Y, button));
endFrame.MouseX = endPosition.X;
endFrame.MouseY = endPosition.Y;
}
else if (h is Slider)
{
Slider s = h as Slider;
for (double j = frameDelay; j < s.Duration; j += frameDelay)
{
Vector2 pos = s.PositionAt(j / s.Duration);
addFrameToReplay(new ReplayFrame(h.StartTime + j, pos.X, pos.Y, button));
}
addFrameToReplay(new ReplayFrame(s.EndTime, s.EndPosition.X, s.EndPosition.Y, button));
}
// We only want to let go of our button if we are at the end of the current replay. Otherwise something is still going on after us so we need to keep the button pressed!
if (Frames[Frames.Count - 1].Time <= endFrame.Time)
addFrameToReplay(endFrame);
}
//Player.currentScore.Replay = InputManager.ReplayScore.Replay;
//Player.currentScore.PlayerName = "osu!";
}
}
}

View File

@ -2,10 +2,7 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Input;
using osu.Game.Configuration;
using osu.Game.Screens.Play;
using OpenTK.Input;
using KeyboardState = osu.Framework.Input.KeyboardState;
@ -17,13 +14,6 @@ namespace osu.Game.Rulesets.Osu
{
private bool leftViaKeyboard;
private bool rightViaKeyboard;
private Bindable<bool> mouseDisabled;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
mouseDisabled = config.GetBindable<bool>(OsuConfig.MouseDisableButtons);
}
protected override void TransformState(InputState state)
{
@ -40,12 +30,6 @@ namespace osu.Game.Rulesets.Osu
if (mouse != null)
{
if (mouseDisabled.Value)
{
mouse.SetPressed(MouseButton.Left, false);
mouse.SetPressed(MouseButton.Right, false);
}
if (leftViaKeyboard)
mouse.SetPressed(MouseButton.Left, true);
if (rightViaKeyboard)

View File

@ -0,0 +1,332 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using osu.Framework.MathUtils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using System;
using System.Diagnostics;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Osu.Replays
{
public class OsuAutoGenerator : OsuAutoGeneratorBase
{
#region Parameters
/// <summary>
/// If delayed movements should be used, causing the cursor to stay on each hitobject for as long as possible.
/// Mainly for Autopilot.
/// </summary>
public bool DelayedMovements; // ModManager.CheckActive(Mods.Relax2);
#endregion
#region Constants
/// <summary>
/// The "reaction time" in ms between "seeing" a new hit object and moving to "react" to it.
/// </summary>
private readonly double reactionTime;
/// <summary>
/// What easing to use when moving between hitobjects
/// </summary>
private EasingTypes preferredEasing => DelayedMovements ? EasingTypes.InOutCubic : EasingTypes.Out;
#endregion
#region Construction / Initialisation
public OsuAutoGenerator(Beatmap<OsuHitObject> beatmap)
: base(beatmap)
{
// Already superhuman, but still somewhat realistic
reactionTime = ApplyModsToRate(100);
}
#endregion
#region Generator
/// <summary>
/// Which button (left or right) to use for the current hitobject.
/// Even means LMB will be used to click, odd means RMB will be used.
/// This keeps track of the button previously used for alt/singletap logic.
/// </summary>
private int buttonIndex;
public override Replay Generate()
{
buttonIndex = 0;
AddFrameToReplay(new ReplayFrame(-100000, 256, 500, ReplayButtonState.None));
AddFrameToReplay(new ReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, 256, 500, ReplayButtonState.None));
AddFrameToReplay(new ReplayFrame(Beatmap.HitObjects[0].StartTime - 1000, 256, 192, ReplayButtonState.None));
for (int i = 0; i < Beatmap.HitObjects.Count; i++)
{
OsuHitObject h = Beatmap.HitObjects[i];
if (DelayedMovements && i > 0)
{
OsuHitObject prev = Beatmap.HitObjects[i - 1];
addDelayedMovements(h, prev);
}
addHitObjectReplay(h);
}
return Replay;
}
private void addDelayedMovements(OsuHitObject h, OsuHitObject prev)
{
double endTime = (prev as IHasEndTime)?.EndTime ?? prev.StartTime;
// Make the cursor stay at a hitObject as long as possible (mainly for autopilot).
if (h.StartTime - h.HitWindowFor(OsuScoreResult.Miss) > endTime + h.HitWindowFor(OsuScoreResult.Hit50) + 50)
{
if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit50), prev.EndPosition.X, prev.EndPosition.Y, ReplayButtonState.None));
if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Miss), h.Position.X, h.Position.Y, ReplayButtonState.None));
}
else if (h.StartTime - h.HitWindowFor(OsuScoreResult.Hit50) > endTime + h.HitWindowFor(OsuScoreResult.Hit50) + 50)
{
if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit50), prev.EndPosition.X, prev.EndPosition.Y, ReplayButtonState.None));
if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Hit50), h.Position.X, h.Position.Y, ReplayButtonState.None));
}
else if (h.StartTime - h.HitWindowFor(OsuScoreResult.Hit100) > endTime + h.HitWindowFor(OsuScoreResult.Hit100) + 50)
{
if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit100), prev.EndPosition.X, prev.EndPosition.Y, ReplayButtonState.None));
if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Hit100), h.Position.X, h.Position.Y, ReplayButtonState.None));
}
}
private void addHitObjectReplay(OsuHitObject h)
{
// Default values for circles/sliders
Vector2 startPosition = h.Position;
EasingTypes easing = preferredEasing;
float spinnerDirection = -1;
// The startPosition for the slider should not be its .Position, but the point on the circle whose tangent crosses the current cursor position
// We also modify spinnerDirection so it spins in the direction it enters the spin circle, to make a smooth transition.
// TODO: Shouldn't the spinner always spin in the same direction?
if (h is Spinner)
{
calcSpinnerStartPosAndDirection(Frames[Frames.Count - 1].Position, out startPosition, out spinnerDirection);
Vector2 spinCentreOffset = SPINNER_CENTRE - Frames[Frames.Count - 1].Position;
if (spinCentreOffset.Length > SPIN_RADIUS)
{
// If moving in from the outside, don't ease out (default eases out). This means auto will "start" spinning immediately after moving into position.
easing = EasingTypes.In;
}
}
// Do some nice easing for cursor movements
if (Frames.Count > 0)
{
moveToHitObject(h.StartTime, startPosition, h.Radius, easing);
}
// Add frames to click the hitobject
addHitObjectClickFrames(h, startPosition, spinnerDirection);
}
#endregion
#region Helper subroutines
private static void calcSpinnerStartPosAndDirection(Vector2 prevPos, out Vector2 startPosition, out float spinnerDirection)
{
Vector2 spinCentreOffset = SPINNER_CENTRE - prevPos;
float distFromCentre = spinCentreOffset.Length;
float distToTangentPoint = (float)Math.Sqrt(distFromCentre * distFromCentre - SPIN_RADIUS * SPIN_RADIUS);
if (distFromCentre > SPIN_RADIUS)
{
// Previous cursor position was outside spin circle, set startPosition to the tangent point.
// Angle between centre offset and tangent point offset.
float angle = (float)Math.Asin(SPIN_RADIUS / distFromCentre);
if (angle > 0)
{
spinnerDirection = -1;
}
else
{
spinnerDirection = 1;
}
// Rotate by angle so it's parallel to tangent line
spinCentreOffset.X = spinCentreOffset.X * (float)Math.Cos(angle) - spinCentreOffset.Y * (float)Math.Sin(angle);
spinCentreOffset.Y = spinCentreOffset.X * (float)Math.Sin(angle) + spinCentreOffset.Y * (float)Math.Cos(angle);
// Set length to distToTangentPoint
spinCentreOffset.Normalize();
spinCentreOffset *= distToTangentPoint;
// Move along the tangent line, now startPosition is at the tangent point.
startPosition = prevPos + spinCentreOffset;
}
else if (spinCentreOffset.Length > 0)
{
// Previous cursor position was inside spin circle, set startPosition to the nearest point on spin circle.
startPosition = SPINNER_CENTRE - spinCentreOffset * (SPIN_RADIUS / spinCentreOffset.Length);
spinnerDirection = 1;
}
else
{
// Degenerate case where cursor position is exactly at the centre of the spin circle.
startPosition = SPINNER_CENTRE + new Vector2(0, -SPIN_RADIUS);
spinnerDirection = 1;
}
}
private void moveToHitObject(double targetTime, Vector2 targetPos, double hitObjectRadius, EasingTypes easing)
{
ReplayFrame lastFrame = Frames[Frames.Count - 1];
// Wait until Auto could "see and react" to the next note.
double waitTime = targetTime - Math.Max(0.0, DrawableOsuHitObject.TIME_PREEMPT - reactionTime);
if (waitTime > lastFrame.Time)
{
lastFrame = new ReplayFrame(waitTime, lastFrame.MouseX, lastFrame.MouseY, lastFrame.ButtonState);
AddFrameToReplay(lastFrame);
}
Vector2 lastPosition = lastFrame.Position;
double timeDifference = ApplyModsToTime(targetTime - lastFrame.Time);
// Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up.
if (timeDifference > 0 && // Sanity checks
((lastPosition - targetPos).Length > hitObjectRadius * (1.5 + 100.0 / timeDifference) || // Either the distance is big enough
timeDifference >= 266)) // ... or the beats are slow enough to tap anyway.
{
// Perform eased movement
for (double time = lastFrame.Time + FrameDelay; time < targetTime; time += FrameDelay)
{
Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPos, lastFrame.Time, targetTime, easing);
AddFrameToReplay(new ReplayFrame((int)time, currentPosition.X, currentPosition.Y, lastFrame.ButtonState));
}
buttonIndex = 0;
}
else
{
buttonIndex++;
}
}
// Add frames to click the hitobject
private void addHitObjectClickFrames(OsuHitObject h, Vector2 startPosition, float spinnerDirection)
{
// Time to insert the first frame which clicks the object
// Here we mainly need to determine which button to use
ReplayButtonState button = buttonIndex % 2 == 0 ? ReplayButtonState.Left1 : ReplayButtonState.Right1;
ReplayFrame startFrame = new ReplayFrame(h.StartTime, startPosition.X, startPosition.Y, button);
// TODO: Why do we delay 1 ms if the object is a spinner? There already is KEY_UP_DELAY from hEndTime.
double hEndTime = ((h as IHasEndTime)?.EndTime ?? h.StartTime) + KEY_UP_DELAY;
int endDelay = h is Spinner ? 1 : 0;
ReplayFrame endFrame = new ReplayFrame(hEndTime + endDelay, h.EndPosition.X, h.EndPosition.Y, ReplayButtonState.None);
// Decrement because we want the previous frame, not the next one
int index = FindInsertionIndex(startFrame) - 1;
// If the previous frame has a button pressed, force alternation.
// If there are frames ahead, modify those to use the new button press.
// Do we have a previous frame? No need to check for < replay.Count since we decremented!
if (index >= 0)
{
ReplayFrame previousFrame = Frames[index];
var previousButton = previousFrame.ButtonState;
// If a button is already held, then we simply alternate
if (previousButton != ReplayButtonState.None)
{
Debug.Assert(previousButton != (ReplayButtonState.Left1 | ReplayButtonState.Right1), "Previous button state was not Left1 nor Right1 despite only using those two states.");
// Force alternation if we have the same button. Otherwise we can just keep the naturally to us assigned button.
if (previousButton == button)
{
button = (ReplayButtonState.Left1 | ReplayButtonState.Right1) & ~button;
startFrame.ButtonState = button;
}
// We always follow the most recent slider / spinner, so remove any other frames that occur while it exists.
int endIndex = FindInsertionIndex(endFrame);
if (index < Frames.Count - 1)
Frames.RemoveRange(index + 1, Math.Max(0, endIndex - (index + 1)));
// After alternating we need to keep holding the other button in the future rather than the previous one.
for (int j = index + 1; j < Frames.Count; ++j)
{
// Don't affect frames which stop pressing a button!
if (j < Frames.Count - 1 || Frames[j].ButtonState == previousButton)
Frames[j].ButtonState = button;
}
}
}
AddFrameToReplay(startFrame);
// We add intermediate frames for spinning / following a slider here.
if (h is Spinner)
{
Spinner s = h as Spinner;
Vector2 difference = startPosition - SPINNER_CENTRE;
float radius = difference.Length;
float angle = radius == 0 ? 0 : (float)Math.Atan2(difference.Y, difference.X);
double t;
for (double j = h.StartTime + FrameDelay; j < s.EndTime; j += FrameDelay)
{
t = ApplyModsToTime(j - h.StartTime) * spinnerDirection;
Vector2 pos = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS);
AddFrameToReplay(new ReplayFrame((int)j, pos.X, pos.Y, button));
}
t = ApplyModsToTime(s.EndTime - h.StartTime) * spinnerDirection;
Vector2 endPosition = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS);
AddFrameToReplay(new ReplayFrame(s.EndTime, endPosition.X, endPosition.Y, button));
endFrame.MouseX = endPosition.X;
endFrame.MouseY = endPosition.Y;
}
else if (h is Slider)
{
Slider s = h as Slider;
for (double j = FrameDelay; j < s.Duration; j += FrameDelay)
{
Vector2 pos = s.PositionAt(j / s.Duration);
AddFrameToReplay(new ReplayFrame(h.StartTime + j, pos.X, pos.Y, button));
}
AddFrameToReplay(new ReplayFrame(s.EndTime, s.EndPosition.X, s.EndPosition.Y, button));
}
// We only want to let go of our button if we are at the end of the current replay. Otherwise something is still going on after us so we need to keep the button pressed!
if (Frames[Frames.Count - 1].Time <= endFrame.Time)
AddFrameToReplay(endFrame);
}
#endregion
}
}

View File

@ -0,0 +1,96 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using System;
using System.Collections.Generic;
using osu.Game.Rulesets.Replays;
using osu.Game.Users;
namespace osu.Game.Rulesets.Osu.Replays
{
public abstract class OsuAutoGeneratorBase : AutoGenerator<OsuHitObject>
{
#region Constants
/// <summary>
/// Constants (for spinners).
/// </summary>
protected static readonly Vector2 SPINNER_CENTRE = new Vector2(256, 192);
protected const float SPIN_RADIUS = 50;
/// <summary>
/// The time in ms between each ReplayFrame.
/// </summary>
protected readonly double FrameDelay;
#endregion
#region Construction / Initialisation
protected Replay Replay;
protected List<ReplayFrame> Frames => Replay.Frames;
protected OsuAutoGeneratorBase(Beatmap<OsuHitObject> beatmap)
: base(beatmap)
{
Replay = new Replay
{
User = new User
{
Username = @"Autoplay",
}
};
// We are using ApplyModsToRate and not ApplyModsToTime to counteract the speed up / slow down from HalfTime / DoubleTime so that we remain at a constant framerate of 60 fps.
FrameDelay = ApplyModsToRate(1000.0 / 60.0);
}
#endregion
#region Utilities
protected double ApplyModsToTime(double v) => v;
protected double ApplyModsToRate(double v) => v;
private class ReplayFrameComparer : IComparer<ReplayFrame>
{
public int Compare(ReplayFrame f1, ReplayFrame f2)
{
if (f1 == null) throw new NullReferenceException($@"{nameof(f1)} cannot be null");
if (f2 == null) throw new NullReferenceException($@"{nameof(f2)} cannot be null");
return f1.Time.CompareTo(f2.Time);
}
}
private static readonly IComparer<ReplayFrame> replay_frame_comparer = new ReplayFrameComparer();
protected int FindInsertionIndex(ReplayFrame frame)
{
int index = Frames.BinarySearch(frame, replay_frame_comparer);
if (index < 0)
{
index = ~index;
}
else
{
// Go to the first index which is actually bigger
while (index < Frames.Count && frame.Time == Frames[index].Time)
{
++index;
}
}
return index;
}
protected void AddFrameToReplay(ReplayFrame frame) => Frames.Insert(FindInsertionIndex(frame), frame);
protected static Vector2 CirclePosition(double t, double radius) => new Vector2((float)(Math.Cos(t) * radius), (float)(Math.Sin(t) * radius));
#endregion
}
}

View File

@ -69,7 +69,6 @@
<Compile Include="Objects\Drawables\Pieces\SliderBody.cs" />
<Compile Include="Objects\OsuHitObjectDifficulty.cs" />
<Compile Include="Objects\SliderTick.cs" />
<Compile Include="OsuAutoReplay.cs" />
<Compile Include="OsuDifficultyCalculator.cs" />
<Compile Include="OsuKeyConversionInputManager.cs" />
<Compile Include="Scoring\OsuScoreProcessor.cs" />
@ -83,6 +82,8 @@
<Compile Include="Objects\Spinner.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Mods\OsuMod.cs" />
<Compile Include="Replays\OsuAutoGenerator.cs" />
<Compile Include="Replays\OsuAutoGeneratorBase.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu-framework\osu.Framework\osu.Framework.csproj">
@ -102,6 +103,9 @@
<None Include="packages.config" />
</ItemGroup>
<ItemGroup />
<ItemGroup>
<Folder Include="Replays\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

View File

@ -66,9 +66,10 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
var distanceData = obj as IHasDistance;
var repeatsData = obj as IHasRepeats;
var endTimeData = obj as IHasEndTime;
var curveData = obj as IHasCurve;
// Old osu! used hit sounding to determine various hit type information
List<SampleInfo> samples = obj.Samples;
SampleInfoList samples = obj.Samples;
bool strong = samples.Any(s => s.Name == SampleInfo.HIT_FINISH);
@ -102,16 +103,35 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
if (tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength)
{
List<SampleInfoList> allSamples = curveData != null ? curveData.RepeatSamples : new List<SampleInfoList>(new[] { samples });
int i = 0;
for (double j = obj.StartTime; j <= obj.StartTime + taikoDuration + tickSpacing / 8; j += tickSpacing)
{
// Todo: This should generate different type of hits (including strongs)
// depending on hitobject sound additions (not implemented fully yet)
yield return new CentreHit
SampleInfoList currentSamples = allSamples[i];
bool isRim = currentSamples.Any(s => s.Name == SampleInfo.HIT_CLAP || s.Name == SampleInfo.HIT_WHISTLE);
strong = currentSamples.Any(s => s.Name == SampleInfo.HIT_FINISH);
if (isRim)
{
StartTime = j,
Samples = obj.Samples,
IsStrong = strong,
};
yield return new RimHit
{
StartTime = j,
Samples = currentSamples,
IsStrong = strong
};
}
else
{
yield return new CentreHit
{
StartTime = j,
Samples = currentSamples,
IsStrong = strong,
};
}
i = (i + 1) % allSamples.Count;
}
}
else

View File

@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
protected override Score CreateReplayScore(Beatmap<TaikoHitObject> beatmap) => new Score
{
User = new User { Username = "mekkadosu!" },
Replay = new TaikoAutoReplay(beatmap)
Replay = new TaikoAutoReplay(beatmap).Generate(),
};
}
}

View File

@ -148,9 +148,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
var completion = (float)userHits / HitObject.RequiredHits;
expandingRing.FadeTo(expandingRing.Alpha + MathHelper.Clamp(completion / 16, 0.1f, 0.6f), 50);
expandingRing.Delay(50);
expandingRing.FadeTo(completion / 8, 2000, EasingTypes.OutQuint);
expandingRing.DelayReset();
using (expandingRing.BeginDelayedSequence(50))
expandingRing.FadeTo(completion / 8, 2000, EasingTypes.OutQuint);
symbol.RotateTo((float)(completion * HitObject.Duration / 8), 4000, EasingTypes.OutQuint);

View File

@ -82,12 +82,12 @@ namespace osu.Game.Rulesets.Taiko.Objects
TickSpacing = tickSpacing,
StartTime = t,
IsStrong = IsStrong,
Samples = Samples.Select(s => new SampleInfo
Samples = new SampleInfoList(Samples.Select(s => new SampleInfo
{
Bank = s.Bank,
Name = @"slidertick",
Volume = s.Volume
}).ToList()
}))
});
first = false;

View File

@ -0,0 +1,127 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
namespace osu.Game.Rulesets.Taiko.Objects
{
internal class TaikoHitObjectDifficulty
{
/// <summary>
/// Factor by how much individual / overall strain decays per second.
/// </summary>
/// <remarks>
/// These values are results of tweaking a lot and taking into account general feedback.
/// </remarks>
internal const double DECAY_BASE = 0.30;
private const double type_change_bonus = 0.75;
private const double rhythm_change_bonus = 1.0;
private const double rhythm_change_base_threshold = 0.2;
private const double rhythm_change_base = 2.0;
internal TaikoHitObject BaseHitObject;
/// <summary>
/// Measures note density in a way
/// </summary>
internal double Strain = 1;
private double timeElapsed;
private int sameTypeSince = 1;
private bool isRim => BaseHitObject is RimHit;
public TaikoHitObjectDifficulty(TaikoHitObject baseHitObject)
{
BaseHitObject = baseHitObject;
}
internal void CalculateStrains(TaikoHitObjectDifficulty previousHitObject, double timeRate)
{
// Rather simple, but more specialized things are inherently inaccurate due to the big difference playstyles and opinions make.
// See Taiko feedback thread.
timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate;
double decay = Math.Pow(DECAY_BASE, timeElapsed / 1000);
double addition = 1;
// Only if we are no slider or spinner we get an extra addition
if (previousHitObject.BaseHitObject is Hit && BaseHitObject is Hit
&& BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime < 1000) // And we only want to check out hitobjects which aren't so far in the past
{
addition += typeChangeAddition(previousHitObject);
addition += rhythmChangeAddition(previousHitObject);
}
double additionFactor = 1.0;
// Scale AdditionFactor linearly from 0.4 to 1 for TimeElapsed from 0 to 50
if (timeElapsed < 50.0)
additionFactor = 0.4 + 0.6 * timeElapsed / 50.0;
Strain = previousHitObject.Strain * decay + addition * additionFactor;
}
private TypeSwitch lastTypeSwitchEven = TypeSwitch.None;
private double typeChangeAddition(TaikoHitObjectDifficulty previousHitObject)
{
// If we don't have the same hit type, trigger a type change!
if (previousHitObject.isRim != isRim)
{
lastTypeSwitchEven = previousHitObject.sameTypeSince % 2 == 0 ? TypeSwitch.Even : TypeSwitch.Odd;
// We only want a bonus if the parity of the type switch changes!
switch (previousHitObject.lastTypeSwitchEven)
{
case TypeSwitch.Even:
if (lastTypeSwitchEven == TypeSwitch.Odd)
return type_change_bonus;
break;
case TypeSwitch.Odd:
if (lastTypeSwitchEven == TypeSwitch.Even)
return type_change_bonus;
break;
}
}
// No type change? Increment counter and keep track of last type switch
else
{
lastTypeSwitchEven = previousHitObject.lastTypeSwitchEven;
sameTypeSince = previousHitObject.sameTypeSince + 1;
}
return 0;
}
private double rhythmChangeAddition(TaikoHitObjectDifficulty previousHitObject)
{
// We don't want a division by zero if some random mapper decides to put 2 HitObjects at the same time.
if (timeElapsed == 0 || previousHitObject.timeElapsed == 0)
return 0;
double timeElapsedRatio = Math.Max(previousHitObject.timeElapsed / timeElapsed, timeElapsed / previousHitObject.timeElapsed);
if (timeElapsedRatio >= 8)
return 0;
double difference = Math.Log(timeElapsedRatio, rhythm_change_base) % 1.0;
if (isWithinChangeThreshold(difference))
return rhythm_change_bonus;
return 0;
}
private bool isWithinChangeThreshold(double value)
{
return value > rhythm_change_base_threshold && value < 1 - rhythm_change_base_threshold;
}
private enum TypeSwitch
{
None,
Even,
Odd
}
}
}

View File

@ -1,7 +1,8 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Taiko.Objects;
@ -9,27 +10,28 @@ using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Taiko.Replays
{
public class TaikoAutoReplay : Replay
public class TaikoAutoReplay : AutoGenerator<TaikoHitObject>
{
private readonly Beatmap<TaikoHitObject> beatmap;
private const double swell_hit_speed = 50;
public TaikoAutoReplay(Beatmap<TaikoHitObject> beatmap)
: base(beatmap)
{
this.beatmap = beatmap;
createAutoReplay();
}
private void createAutoReplay()
protected Replay Replay;
protected List<ReplayFrame> Frames => Replay.Frames;
public override Replay Generate()
{
bool hitButton = true;
Frames.Add(new ReplayFrame(-100000, null, null, ReplayButtonState.None));
Frames.Add(new ReplayFrame(beatmap.HitObjects[0].StartTime - 1000, null, null, ReplayButtonState.None));
Frames.Add(new ReplayFrame(Beatmap.HitObjects[0].StartTime - 1000, null, null, ReplayButtonState.None));
for (int i = 0; i < beatmap.HitObjects.Count; i++)
for (int i = 0; i < Beatmap.HitObjects.Count; i++)
{
TaikoHitObject h = beatmap.HitObjects[i];
TaikoHitObject h = Beatmap.HitObjects[i];
ReplayButtonState button;
@ -45,12 +47,13 @@ namespace osu.Game.Rulesets.Taiko.Replays
int d = 0;
int count = 0;
int req = swell.RequiredHits;
double hitRate = swell.Duration / req;
double hitRate = Math.Min(swell_hit_speed, swell.Duration / req);
for (double j = h.StartTime; j < endTime; j += hitRate)
{
switch (d)
{
default:
case 0:
button = ReplayButtonState.Left1;
break;
case 1:
@ -66,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Replays
Frames.Add(new ReplayFrame(j, null, null, button));
d = (d + 1) % 4;
if (++count > req)
if (++count == req)
break;
}
}
@ -102,15 +105,17 @@ namespace osu.Game.Rulesets.Taiko.Replays
Frames.Add(new ReplayFrame(endTime + KEY_UP_DELAY, null, null, ReplayButtonState.None));
if (i < beatmap.HitObjects.Count - 1)
if (i < Beatmap.HitObjects.Count - 1)
{
double waitTime = beatmap.HitObjects[i + 1].StartTime - 1000;
double waitTime = Beatmap.HitObjects[i + 1].StartTime - 1000;
if (waitTime > endTime)
Frames.Add(new ReplayFrame(waitTime, null, null, ReplayButtonState.None));
}
hitButton = !hitButton;
}
return Replay;
}
}
}

View File

@ -6,18 +6,133 @@ using osu.Game.Rulesets.Beatmaps;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
using System.Collections.Generic;
using System.Globalization;
using System;
namespace osu.Game.Rulesets.Taiko
{
public class TaikoDifficultyCalculator : DifficultyCalculator<TaikoHitObject>
internal class TaikoDifficultyCalculator : DifficultyCalculator<TaikoHitObject>
{
public TaikoDifficultyCalculator(Beatmap beatmap) : base(beatmap)
private const double star_scaling_factor = 0.04125;
/// <summary>
/// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size STRAIN_STEP.
/// This is to eliminate higher influence of stream over aim by simply having more HitObjects with high strain.
/// The higher this value, the less strains there will be, indirectly giving long beatmaps an advantage.
/// </summary>
private const double strain_step = 400;
/// <summary>
/// The weighting of each strain value decays to this number * it's previous value
/// </summary>
private const double decay_weight = 0.9;
/// <summary>
/// HitObjects are stored as a member variable.
/// </summary>
private readonly List<TaikoHitObjectDifficulty> difficultyHitObjects = new List<TaikoHitObjectDifficulty>();
public TaikoDifficultyCalculator(Beatmap beatmap)
: base(beatmap)
{
}
protected override double CalculateInternal(Dictionary<string, string> categoryDifficulty)
{
return 0;
// Fill our custom DifficultyHitObject class, that carries additional information
difficultyHitObjects.Clear();
foreach (var hitObject in Objects)
difficultyHitObjects.Add(new TaikoHitObjectDifficulty(hitObject));
// Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure.
difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime));
if (!calculateStrainValues()) return 0;
double starRating = calculateDifficulty() * star_scaling_factor;
if (categoryDifficulty != null)
{
categoryDifficulty.Add("Strain", starRating.ToString("0.00", CultureInfo.InvariantCulture));
categoryDifficulty.Add("Hit window 300", (35 /*HitObjectManager.HitWindow300*/ / TimeRate).ToString("0.00", CultureInfo.InvariantCulture));
}
return starRating;
}
private bool calculateStrainValues()
{
// Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment.
using (List<TaikoHitObjectDifficulty>.Enumerator hitObjectsEnumerator = difficultyHitObjects.GetEnumerator())
{
if (!hitObjectsEnumerator.MoveNext()) return false;
TaikoHitObjectDifficulty current = hitObjectsEnumerator.Current;
// First hitObject starts at strain 1. 1 is the default for strain values, so we don't need to set it here. See DifficultyHitObject.
while (hitObjectsEnumerator.MoveNext())
{
var next = hitObjectsEnumerator.Current;
next?.CalculateStrains(current, TimeRate);
current = next;
}
return true;
}
}
private double calculateDifficulty()
{
double actualStrainStep = strain_step * TimeRate;
// Find the highest strain value within each strain step
List<double> highestStrains = new List<double>();
double intervalEndTime = actualStrainStep;
double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval
TaikoHitObjectDifficulty previousHitObject = null;
foreach (var hitObject in difficultyHitObjects)
{
// While we are beyond the current interval push the currently available maximum to our strain list
while (hitObject.BaseHitObject.StartTime > intervalEndTime)
{
highestStrains.Add(maximumStrain);
// The maximum strain of the next interval is not zero by default! We need to take the last hitObject we encountered, take its strain and apply the decay
// until the beginning of the next interval.
if (previousHitObject == null)
{
maximumStrain = 0;
}
else
{
double decay = Math.Pow(TaikoHitObjectDifficulty.DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000);
maximumStrain = previousHitObject.Strain * decay;
}
// Go to the next time interval
intervalEndTime += actualStrainStep;
}
// Obtain maximum strain
maximumStrain = Math.Max(hitObject.Strain, maximumStrain);
previousHitObject = hitObject;
}
// Build the weighted sum over the highest strains for each interval
double difficulty = 0;
double weight = 1;
highestStrains.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
foreach (double strain in highestStrains)
{
difficulty += weight * strain;
weight *= decay_weight;
}
return difficulty;
}
protected override BeatmapConverter<TaikoHitObject> CreateBeatmapConverter() => new TaikoBeatmapConverter();

View File

@ -81,6 +81,7 @@
<Compile Include="Replays\TaikoFramedReplayInputHandler.cs" />
<Compile Include="Replays\TaikoAutoReplay.cs" />
<Compile Include="Objects\TaikoHitObject.cs" />
<Compile Include="Objects\TaikoHitObjectDifficulty.cs" />
<Compile Include="TaikoDifficultyCalculator.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Scoring\TaikoScoreProcessor.cs" />

View File

@ -26,15 +26,15 @@ namespace osu.Game.Tests.Beatmaps.IO
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new HeadlessGameHost())
{
var osu = loadOsu(host);
loadOsu(host);
var temp = prepareTempCopy(osz_path);
Assert.IsTrue(File.Exists(temp));
osu.Dependencies.Get<BeatmapDatabase>().Import(temp);
host.Dependencies.Get<BeatmapDatabase>().Import(temp);
ensureLoaded(osu);
ensureLoaded(host);
Assert.IsFalse(File.Exists(temp));
}
@ -49,7 +49,7 @@ namespace osu.Game.Tests.Beatmaps.IO
Assert.IsTrue(host.IsPrimaryInstance);
Assert.IsTrue(!client.IsPrimaryInstance);
var osu = loadOsu(host);
loadOsu(host);
var temp = prepareTempCopy(osz_path);
@ -59,7 +59,7 @@ namespace osu.Game.Tests.Beatmaps.IO
if (!importer.ImportAsync(temp).Wait(5000))
Assert.Fail(@"IPC took too long to send");
ensureLoaded(osu);
ensureLoaded(host);
Assert.IsFalse(File.Exists(temp));
}
@ -71,16 +71,16 @@ namespace osu.Game.Tests.Beatmaps.IO
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new HeadlessGameHost())
{
var osu = loadOsu(host);
loadOsu(host);
var temp = prepareTempCopy(osz_path);
Assert.IsTrue(File.Exists(temp));
using (File.OpenRead(temp))
osu.Dependencies.Get<BeatmapDatabase>().Import(temp);
host.Dependencies.Get<BeatmapDatabase>().Import(temp);
ensureLoaded(osu);
ensureLoaded(host);
Assert.IsTrue(File.Exists(temp));
@ -105,19 +105,19 @@ namespace osu.Game.Tests.Beatmaps.IO
Thread.Sleep(1);
//reset beatmap database (sqlite and storage backing)
osu.Dependencies.Get<RulesetDatabase>().Reset();
osu.Dependencies.Get<BeatmapDatabase>().Reset();
host.Dependencies.Get<RulesetDatabase>().Reset();
host.Dependencies.Get<BeatmapDatabase>().Reset();
return osu;
}
private void ensureLoaded(OsuGameBase osu, int timeout = 10000)
private void ensureLoaded(GameHost host, int timeout = 10000)
{
IEnumerable<BeatmapSetInfo> resultSets = null;
Action waitAction = () =>
{
while (!(resultSets = osu.Dependencies.Get<BeatmapDatabase>()
while (!(resultSets = host.Dependencies.Get<BeatmapDatabase>()
.Query<BeatmapSetInfo>().Where(s => s.OnlineBeatmapSetID == 241526)).Any())
Thread.Sleep(50);
};
@ -134,7 +134,7 @@ namespace osu.Game.Tests.Beatmaps.IO
//if we don't re-check here, the set will be inserted but the beatmaps won't be present yet.
waitAction = () =>
{
while ((resultBeatmaps = osu.Dependencies.Get<BeatmapDatabase>()
while ((resultBeatmaps = host.Dependencies.Get<BeatmapDatabase>()
.Query<BeatmapInfo>().Where(s => s.OnlineBeatmapSetID == 241526 && s.BaseDifficultyID > 0)).Count() != 12)
Thread.Sleep(50);
};
@ -143,7 +143,7 @@ namespace osu.Game.Tests.Beatmaps.IO
@"Beatmaps did not import to the database in allocated time");
//fetch children and check we can load from the post-storage path...
var set = osu.Dependencies.Get<BeatmapDatabase>().GetChildren(resultSets.First());
var set = host.Dependencies.Get<BeatmapDatabase>().GetChildren(resultSets.First());
Assert.IsTrue(set.Beatmaps.Count == resultBeatmaps.Count(),
$@"Incorrect database beatmap count post-import ({resultBeatmaps.Count()} but should be {set.Beatmaps.Count}).");
@ -153,16 +153,16 @@ namespace osu.Game.Tests.Beatmaps.IO
Assert.IsTrue(set.Beatmaps.Count > 0);
var beatmap = osu.Dependencies.Get<BeatmapDatabase>().GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 0))?.Beatmap;
var beatmap = host.Dependencies.Get<BeatmapDatabase>().GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 0))?.Beatmap;
Assert.IsTrue(beatmap?.HitObjects.Count > 0);
beatmap = osu.Dependencies.Get<BeatmapDatabase>().GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 1))?.Beatmap;
beatmap = host.Dependencies.Get<BeatmapDatabase>().GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 1))?.Beatmap;
Assert.IsTrue(beatmap?.HitObjects.Count > 0);
beatmap = osu.Dependencies.Get<BeatmapDatabase>().GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 2))?.Beatmap;
beatmap = host.Dependencies.Get<BeatmapDatabase>().GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 2))?.Beatmap;
Assert.IsTrue(beatmap?.HitObjects.Count > 0);
beatmap = osu.Dependencies.Get<BeatmapDatabase>().GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 3))?.Beatmap;
beatmap = host.Dependencies.Get<BeatmapDatabase>().GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 3))?.Beatmap;
Assert.IsTrue(beatmap?.HitObjects.Count > 0);
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
namespace osu.Game.Audio
{
public class SampleInfoList : List<SampleInfo>
{
public SampleInfoList()
{
}
public SampleInfoList(IEnumerable<SampleInfo> elements)
{
AddRange(elements);
}
}
}

View File

@ -35,6 +35,10 @@ namespace osu.Game.Beatmaps
protected DifficultyCalculator(Beatmap beatmap)
{
Objects = CreateBeatmapConverter().Convert(beatmap).HitObjects;
foreach (var h in Objects)
h.ApplyDefaults(beatmap.TimingInfo, beatmap.BeatmapInfo.Difficulty);
PreprocessHitObjects();
}

View File

@ -17,7 +17,7 @@ namespace osu.Game.Beatmaps.Drawables
/// <summary>
/// Fires when one of our difficulties was selected. Will fire on first expand.
/// </summary>
public Action<BeatmapGroup, BeatmapInfo> SelectionChanged;
public Action<BeatmapGroup, BeatmapPanel> SelectionChanged;
/// <summary>
/// Fires when one of our difficulties is clicked when already selected. Should start playing the map.
@ -89,8 +89,6 @@ namespace osu.Game.Beatmaps.Drawables
//we want to make sure one of our children is selected in the case none have been selected yet.
if (SelectedPanel == null)
BeatmapPanels.First().State = PanelSelectedState.Selected;
else
SelectionChanged?.Invoke(this, SelectedPanel.Beatmap);
}
private void panelGainedSelection(BeatmapPanel panel)
@ -106,7 +104,7 @@ namespace osu.Game.Beatmaps.Drawables
finally
{
State = BeatmapGroupState.Expanded;
SelectionChanged?.Invoke(this, panel.Beatmap);
SelectionChanged?.Invoke(this, SelectedPanel);
}
}
}

View File

@ -1,19 +1,18 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Beatmaps.Drawables
{
@ -23,8 +22,6 @@ namespace osu.Game.Beatmaps.Drawables
private readonly SpriteText title;
private readonly SpriteText artist;
private Bindable<bool> preferUnicode;
private readonly WorkingBeatmap beatmap;
private readonly FillFlowContainer difficultyIcons;
@ -82,15 +79,10 @@ namespace osu.Game.Beatmaps.Drawables
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
private void load(LocalisationEngine localisation)
{
preferUnicode = config.GetBindable<bool>(OsuConfig.ShowUnicode);
preferUnicode.ValueChanged += unicode =>
{
title.Text = unicode ? beatmap.BeatmapSetInfo.Metadata.TitleUnicode : beatmap.BeatmapSetInfo.Metadata.Title;
artist.Text = unicode ? beatmap.BeatmapSetInfo.Metadata.ArtistUnicode : beatmap.BeatmapSetInfo.Metadata.Artist;
};
preferUnicode.TriggerChange();
title.Current = localisation.GetUnicodePreference(beatmap.BeatmapSetInfo.Metadata.TitleUnicode, beatmap.BeatmapSetInfo.Metadata.Title);
artist.Current = localisation.GetUnicodePreference(beatmap.BeatmapSetInfo.Metadata.ArtistUnicode, beatmap.BeatmapSetInfo.Metadata.Artist);
}
private class PanelBackground : BufferedContainer
@ -112,7 +104,7 @@ namespace osu.Game.Beatmaps.Drawables
Depth = -1,
Direction = FillDirection.Horizontal,
RelativeSizeAxes = Axes.Both,
// This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle
// This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle
Shear = new Vector2(0.8f, 0),
Alpha = 0.5f,
Children = new[]

View File

@ -94,6 +94,9 @@ namespace osu.Game.Beatmaps
{
if (track != null && BeatmapInfo.AudioEquals(other.BeatmapInfo))
other.track = track;
if (background != null && BeatmapInfo.BackgroundEquals(other.BeatmapInfo))
other.background = background;
}
public virtual void Dispose()

View File

@ -25,7 +25,7 @@ namespace osu.Game.Configuration
Set(OsuConfig.MenuCursorSize, 1.0, 0.5f, 2);
Set(OsuConfig.GameplayCursorSize, 1.0, 0.5f, 2);
Set(OsuConfig.DimLevel, 30, 0, 100);
Set(OsuConfig.DimLevel, 0.3, 0, 1);
Set(OsuConfig.MouseDisableButtons, false);
Set(OsuConfig.MouseDisableWheel, false);
@ -35,12 +35,15 @@ namespace osu.Game.Configuration
Set(OsuConfig.MenuParallax, true);
Set(OsuConfig.MenuVoice, true);
Set(OsuConfig.MenuMusic, true);
Set(OsuConfig.BeatmapDetailTab, BeatmapDetailTab.Details);
Set(OsuConfig.ShowInterface, true);
Set(OsuConfig.KeyOverlay, false);
//todo: implement all settings below this line (remove the Disabled set when doing so).
//todo: implement all settings below this line (remove the Disabled set when doing so).
Set(OsuConfig.AudioOffset, 0, -500.0, 500.0);
Set(OsuConfig.MouseSpeed, 1.0).Disabled = true;
@ -89,7 +92,6 @@ namespace osu.Game.Configuration
Set(OsuConfig.IgnoreBeatmapSamples, false).Disabled = true;
Set(OsuConfig.IgnoreBeatmapSkins, false).Disabled = true;
Set(OsuConfig.IgnoreList, string.Empty).Disabled = true;
Set(OsuConfig.Language, @"unknown").Disabled = true;
Set(OsuConfig.AllowNowPlayingHighlights, false).Disabled = true;
Set(OsuConfig.LastVersion, string.Empty).Disabled = true;
Set(OsuConfig.LastVersionPermissionsFailed, string.Empty).Disabled = true;
@ -148,8 +150,6 @@ namespace osu.Game.Configuration
Set(OsuConfig.YahooIntegration, false).Disabled = true;
Set(OsuConfig.ForceFrameFlush, false).Disabled = true;
Set(OsuConfig.DetectPerformanceIssues, true).Disabled = true;
Set(OsuConfig.MenuMusic, true).Disabled = true;
Set(OsuConfig.MenuVoice, true).Disabled = true;
Set(OsuConfig.RawInput, false).Disabled = true;
Set(OsuConfig.AbsoluteToOsuWindow, Get<bool>(OsuConfig.RawInput)).Disabled = true;
Set(OsuConfig.ShowMenuTips, true).Disabled = true;
@ -162,16 +162,6 @@ namespace osu.Game.Configuration
Set(OsuConfig.UpdateFailCount, 0).Disabled = true;
//Set(OsuConfig.TreeSortMode, TreeGroupMode.Show_All).Disabled = true;
//Set(OsuConfig.TreeSortMode2, TreeSortMode.Title).Disabled = true;
bool unicodeDefault = false;
switch (Get<string>(OsuConfig.Language))
{
case @"zh":
case @"ja":
case @"ko":
unicodeDefault = true;
break;
}
Set(OsuConfig.ShowUnicode, unicodeDefault);
Set(OsuConfig.PermanentSongInfo, false).Disabled = true;
Set(OsuConfig.Ticker, false).Disabled = true;
Set(OsuConfig.CompatibilityContext, false).Disabled = true;
@ -179,7 +169,6 @@ namespace osu.Game.Configuration
Set(OsuConfig.ConfineMouse, Get<bool>(OsuConfig.ConfineMouseToFullscreen) ?
ConfineMouseMode.Fullscreen : ConfineMouseMode.Never).Disabled = true;
GetOriginalBindable<bool>(OsuConfig.SavePassword).ValueChanged += delegate
{
if (Get<bool>(OsuConfig.SavePassword)) Set(OsuConfig.SaveUsername, true);
@ -339,11 +328,9 @@ namespace osu.Game.Configuration
SaveUsername,
TreeSortMode,
TreeSortMode2,
ShowUnicode,
PermanentSongInfo,
Ticker,
CompatibilityContext,
CanForceOptimusCompatibility,
}
}

View File

@ -93,5 +93,9 @@ namespace osu.Game.Database
public bool AudioEquals(BeatmapInfo other) => other != null && BeatmapSet != null && other.BeatmapSet != null &&
BeatmapSet.Path == other.BeatmapSet.Path &&
(Metadata ?? BeatmapSet.Metadata).AudioFile == (other.Metadata ?? other.BeatmapSet.Metadata).AudioFile;
public bool BackgroundEquals(BeatmapInfo other) => other != null && BeatmapSet != null && other.BeatmapSet != null &&
BeatmapSet.Path == other.BeatmapSet.Path &&
(Metadata ?? BeatmapSet.Metadata).BackgroundFile == (other.Metadata ?? other.BeatmapSet.Metadata).BackgroundFile;
}
}

View File

@ -22,5 +22,15 @@ namespace osu.Game.Database
public int PreviewTime { get; set; }
public string AudioFile { get; set; }
public string BackgroundFile { get; set; }
public string[] SearchableTerms => new[]
{
Artist,
ArtistUnicode,
Title,
TitleUnicode,
Source,
Tags
};
}
}

View File

@ -123,6 +123,8 @@ namespace osu.Game.Graphics.UserInterface
set { label.Text = value; }
}
protected readonly TextAwesome Icon;
private Color4? accentColour;
public virtual Color4 AccentColour
{
@ -145,19 +147,19 @@ namespace osu.Game.Graphics.UserInterface
Foreground.Children = new Drawable[]
{
label = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
new TextAwesome
{
Icon = FontAwesome.fa_chevron_down,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Margin = new MarginPadding { Right = 4 },
TextSize = 20
}
label = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
Icon = new TextAwesome
{
Icon = FontAwesome.fa_chevron_down,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Margin = new MarginPadding { Right = 4 },
TextSize = 20
}
};
}

View File

@ -23,14 +23,14 @@ namespace osu.Game.Graphics.UserInterface
private readonly Box leftBox;
private readonly Box rightBox;
public string TooltipText
public virtual string TooltipText
{
get
{
var bindableDouble = CurrentNumber as BindableNumber<double>;
if (bindableDouble != null)
{
if (bindableDouble.MaxValue == 1 && bindableDouble.MinValue == 0)
if (bindableDouble.MaxValue == 1 && (bindableDouble.MinValue == 0 || bindableDouble.MinValue == -1))
return bindableDouble.Value.ToString(@"P0");
return bindableDouble.Value.ToString(@"n1");
}

View File

@ -131,14 +131,14 @@ namespace osu.Game.Graphics.UserInterface
foreach (var star in stars.Children)
{
star.ClearTransforms(true);
if (count <= newValue)
star.Delay(Math.Max(i - count, 0) * animationDelay, true);
else
star.Delay(Math.Max(count - 1 - i, 0) * animationDelay, true);
star.FadeTo(i < newValue ? 1.0f : minStarAlpha, fadingDuration);
star.Icon.ScaleTo(getStarScale(i, newValue), scalingDuration, scalingEasing);
star.DelayReset();
var delay = (count <= newValue ? Math.Max(i - count, 0) : Math.Max(count - 1 - i, 0)) * animationDelay;
using (BeginDelayedSequence(delay, true))
{
star.FadeTo(i < newValue ? 1.0f : minStarAlpha, fadingDuration);
star.Icon.ScaleTo(getStarScale(i, newValue), scalingDuration, scalingEasing);
}
i++;
}

View File

@ -6,7 +6,7 @@ using osu.Game.Database;
namespace osu.Game.Online.API.Requests
{
public class GetBeatmapDetailsRequest : APIRequest<GetBeatmapDeatilsResponse>
public class GetBeatmapDetailsRequest : APIRequest<GetBeatmapDetailsResponse>
{
private readonly BeatmapInfo beatmap;
@ -20,7 +20,7 @@ namespace osu.Game.Online.API.Requests
protected override string Target => $@"beatmaps/{lookupString}";
}
public class GetBeatmapDeatilsResponse : BeatmapMetrics
public class GetBeatmapDetailsResponse : BeatmapMetrics
{
//the online API returns some metrics as a nested object.
[JsonProperty(@"failtimes")]

View File

@ -0,0 +1,137 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Select;
using OpenTK;
using OpenTK.Graphics;
using System;
namespace osu.Game.Overlays.Music
{
internal class FilterControl : Container
{
public readonly FilterTextBox Search;
public FilterControl()
{
Children = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0f, 10f),
Children = new Drawable[]
{
Search = new FilterTextBox
{
RelativeSizeAxes = Axes.X,
Height = 40,
Exit = () => ExitRequested?.Invoke(),
},
new CollectionsDropdown<PlaylistCollection>
{
RelativeSizeAxes = Axes.X,
Items = new[] { new KeyValuePair<string, PlaylistCollection>(@"All", PlaylistCollection.All) },
}
},
},
};
Search.Current.ValueChanged += current_ValueChanged;
}
private void current_ValueChanged(string newValue) => FilterChanged?.Invoke(newValue);
public Action ExitRequested;
public Action<string> FilterChanged;
public class FilterTextBox : SearchTextBox
{
private Color4 backgroundColour;
protected override Color4 BackgroundUnfocused => backgroundColour;
protected override Color4 BackgroundFocused => backgroundColour;
public FilterTextBox()
{
Masking = true;
CornerRadius = 5;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
backgroundColour = colours.Gray2;
}
}
private class CollectionsDropdown<T> : OsuDropdown<T>
{
protected override DropdownHeader CreateHeader() => new CollectionsHeader { AccentColour = AccentColour };
protected override Menu CreateMenu() => new CollectionsMenu();
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AccentColour = colours.Gray6;
}
private class CollectionsHeader : OsuDropdownHeader
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
BackgroundColour = colours.Gray4;
}
public CollectionsHeader()
{
CornerRadius = 5;
Height = 30;
Icon.TextSize = 14;
Icon.Margin = new MarginPadding(0);
Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 10, Right = 10 };
EdgeEffect = new EdgeEffect
{
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(0.3f),
Radius = 3,
Offset = new Vector2(0f, 1f),
};
}
}
private class CollectionsMenu : OsuMenu
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Background.Colour = colours.Gray4;
}
public CollectionsMenu()
{
CornerRadius = 5;
EdgeEffect = new EdgeEffect
{
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(0.3f),
Radius = 3,
Offset = new Vector2(0f, 1f),
};
}
}
}
}
}

View File

@ -0,0 +1,137 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Localisation;
namespace osu.Game.Overlays.Music
{
internal class PlaylistItem : Container, IFilterable
{
private const float fade_duration = 100;
private Color4 hoverColour;
private TextAwesome handle;
private OsuSpriteText title;
public readonly BeatmapSetInfo BeatmapSetInfo;
public Action<BeatmapSetInfo> OnSelect;
private bool selected;
public bool Selected
{
get { return selected; }
set
{
if (value == selected) return;
selected = value;
Flush(true);
title.FadeColour(Selected ? hoverColour : Color4.White, fade_duration);
}
}
public PlaylistItem(BeatmapSetInfo setInfo)
{
BeatmapSetInfo = setInfo;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding { Top = 3, Bottom = 3 };
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, LocalisationEngine localisation)
{
BeatmapMetadata metadata = BeatmapSetInfo.Metadata;
FilterTerms = metadata.SearchableTerms;
Children = new Drawable[]
{
handle = new TextAwesome
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
TextSize = 12,
Colour = colours.Gray5,
Icon = FontAwesome.fa_bars,
Alpha = 0f,
Margin = new MarginPadding { Left = 5 },
Padding = new MarginPadding { Top = 2 },
},
new FillFlowContainer<OsuSpriteText>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = 20 },
Spacing = new Vector2(5f, 0f),
Children = new []
{
title = new OsuSpriteText
{
TextSize = 16,
Font = @"Exo2.0-Regular",
Current = localisation.GetUnicodePreference(metadata.TitleUnicode, metadata.Title),
},
new OsuSpriteText
{
TextSize = 14,
Font = @"Exo2.0-Bold",
Colour = colours.Gray9,
Padding = new MarginPadding { Top = 1 },
Current = localisation.GetUnicodePreference(metadata.ArtistUnicode, metadata.Artist),
}
}
},
};
hoverColour = colours.Yellow;
}
protected override bool OnHover(Framework.Input.InputState state)
{
handle.FadeIn(fade_duration);
return base.OnHover(state);
}
protected override void OnHoverLost(Framework.Input.InputState state)
{
handle.FadeOut(fade_duration);
}
protected override bool OnClick(Framework.Input.InputState state)
{
OnSelect?.Invoke(BeatmapSetInfo);
return true;
}
public string[] FilterTerms { get; private set; }
private bool matching = true;
public bool MatchingCurrentFilter
{
set
{
if (matching == value) return;
matching = value;
FadeTo(matching ? 1 : 0, 200);
}
}
}
}

View File

@ -0,0 +1,87 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Database;
namespace osu.Game.Overlays.Music
{
internal class PlaylistList : Container
{
private readonly FillFlowContainer<PlaylistItem> items;
public IEnumerable<BeatmapSetInfo> BeatmapSets
{
set
{
items.Children = value.Select(item => new PlaylistItem(item) { OnSelect = itemSelected }).ToList();
}
}
private void itemSelected(BeatmapSetInfo b)
{
OnSelect?.Invoke(b);
}
public Action<BeatmapSetInfo> OnSelect;
private readonly SearchContainer search;
public void Filter(string searchTerm) => search.SearchTerm = searchTerm;
public BeatmapSetInfo SelectedItem
{
get { return items.Children.FirstOrDefault(i => i.Selected)?.BeatmapSetInfo; }
set
{
foreach (PlaylistItem s in items.Children)
s.Selected = s.BeatmapSetInfo.ID == value?.ID;
}
}
public PlaylistList()
{
Children = new Drawable[]
{
new ScrollContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
search = new SearchContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
items = new ItemSearchContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
}
}
},
},
};
}
private class ItemSearchContainer : FillFlowContainer<PlaylistItem>, IHasFilterableChildren
{
public string[] FilterTerms => new string[] { };
public bool MatchingCurrentFilter { set { } }
public IEnumerable<IFilterable> FilterableChildren => Children;
public ItemSearchContainer()
{
LayoutDuration = 200;
LayoutEasing = EasingTypes.OutQuint;
}
}
}
}

View File

@ -0,0 +1,164 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Configuration;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics;
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Extensions;
namespace osu.Game.Overlays.Music
{
public class PlaylistOverlay : OverlayContainer
{
private const float transition_duration = 600;
private const float playlist_height = 510;
private FilterControl filter;
private PlaylistList list;
private TrackManager trackManager;
private BeatmapDatabase beatmaps;
private readonly Bindable<WorkingBeatmap> beatmapBacking = new Bindable<WorkingBeatmap>();
protected IEnumerable<BeatmapSetInfo> BeatmapSets;
[BackgroundDependencyLoader]
private void load(OsuGameBase game, BeatmapDatabase beatmaps, OsuColour colours)
{
this.beatmaps = beatmaps;
trackManager = game.Audio.Track;
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
CornerRadius = 5,
Masking = true,
EdgeEffect = new EdgeEffect
{
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(40),
Radius = 5,
},
Children = new Drawable[]
{
new Box
{
Colour = colours.Gray3,
RelativeSizeAxes = Axes.Both,
},
list = new PlaylistList
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 95, Bottom = 10, Right = 10 },
OnSelect = itemSelected,
},
filter = new FilterControl
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ExitRequested = () => State = Visibility.Hidden,
FilterChanged = search => list.Filter(search),
Padding = new MarginPadding(10),
},
},
},
};
list.BeatmapSets = BeatmapSets = beatmaps.GetAllWithChildren<BeatmapSetInfo>().ToList();
beatmapBacking.BindTo(game.Beatmap);
}
protected override void LoadComplete()
{
base.LoadComplete();
beatmapBacking.ValueChanged += b => list.SelectedItem = b?.BeatmapSetInfo;
beatmapBacking.TriggerChange();
}
protected override void PopIn()
{
filter.Search.HoldFocus = true;
Schedule(() => filter.Search.TriggerFocus());
ResizeTo(new Vector2(1, playlist_height), transition_duration, EasingTypes.OutQuint);
FadeIn(transition_duration, EasingTypes.OutQuint);
}
protected override void PopOut()
{
filter.Search.HoldFocus = false;
ResizeTo(new Vector2(1, 0), transition_duration, EasingTypes.OutQuint);
FadeOut(transition_duration);
}
private void itemSelected(BeatmapSetInfo set)
{
if (set.ID == (beatmapBacking.Value?.BeatmapSetInfo?.ID ?? -1))
{
beatmapBacking.Value?.Track?.Seek(0);
return;
}
playSpecified(set.Beatmaps[0]);
}
public void PlayPrevious()
{
var currentID = beatmapBacking.Value?.BeatmapSetInfo.ID ?? -1;
var available = BeatmapSets.Reverse();
var playable = available.SkipWhile(b => b.ID != currentID).Skip(1).FirstOrDefault() ?? available.FirstOrDefault();
if (playable != null)
playSpecified(playable.Beatmaps[0]);
}
public void PlayNext()
{
var currentID = beatmapBacking.Value?.BeatmapSetInfo.ID ?? -1;
var available = BeatmapSets;
var playable = available.SkipWhile(b => b.ID != currentID).Skip(1).FirstOrDefault() ?? available.FirstOrDefault();
if (playable != null)
playSpecified(playable.Beatmaps[0]);
}
private void playSpecified(BeatmapInfo info)
{
beatmapBacking.Value = beatmaps.GetWorkingBeatmap(info, beatmapBacking);
Task.Run(() =>
{
var track = beatmapBacking.Value.Track;
trackManager.SetExclusive(track);
track.Start();
}).ContinueWith(task => Schedule(task.ThrowIfFaulted), TaskContinuationOptions.OnlyOnFaulted);
}
}
//todo: placeholder
public enum PlaylistCollection
{
All
}
}

View File

@ -2,60 +2,59 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Configuration;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.MathUtils;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Game.Graphics.Sprites;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Threading;
using osu.Game.Overlays.Music;
namespace osu.Game.Overlays
{
public class MusicController : FocusedOverlayContainer
{
private Drawable currentBackground;
private DragBar progress;
private Button playButton;
private SpriteText title, artist;
private const float player_height = 130;
private List<BeatmapSetInfo> playList;
private readonly List<BeatmapInfo> playHistory = new List<BeatmapInfo>();
private int playListIndex;
private int playHistoryIndex = -1;
private TrackManager trackManager;
private Bindable<WorkingBeatmap> beatmapSource;
private Bindable<bool> preferUnicode;
private WorkingBeatmap current;
private BeatmapDatabase beatmaps;
private Container dragContainer;
private const float transition_length = 800;
private const float progress_height = 10;
private const float bottom_black_area_height = 55;
private Drawable currentBackground;
private DragBar progressBar;
private Button playButton;
private Button playlistButton;
private SpriteText title, artist;
private PlaylistOverlay playlist;
private LocalisationEngine localisation;
private readonly Bindable<WorkingBeatmap> beatmapBacking = new Bindable<WorkingBeatmap>();
private Container dragContainer;
private Container playerContainer;
public MusicController()
{
Width = 400;
Height = 130;
Margin = new MarginPadding(10);
}
@ -81,247 +80,228 @@ namespace osu.Game.Overlays
}
[BackgroundDependencyLoader]
private void load(OsuGameBase game, OsuConfigManager config, BeatmapDatabase beatmaps, OsuColour colours)
private void load(OsuGameBase game, OsuColour colours, LocalisationEngine localisation)
{
this.localisation = localisation;
Children = new Drawable[]
{
dragContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Masking = true,
CornerRadius = 5,
EdgeEffect = new EdgeEffect
{
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(40),
Radius = 5,
},
RelativeSizeAxes = Axes.Both,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
title = new OsuSpriteText
playlist = new PlaylistOverlay
{
Origin = Anchor.BottomCentre,
Anchor = Anchor.TopCentre,
Position = new Vector2(0, 40),
TextSize = 25,
Colour = Color4.White,
Text = @"Nothing to play",
Font = @"Exo2.0-MediumItalic"
},
artist = new OsuSpriteText
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Position = new Vector2(0, 45),
TextSize = 15,
Colour = Color4.White,
Text = @"Nothing to play",
Font = @"Exo2.0-BoldItalic"
},
new Container
{
Padding = new MarginPadding { Bottom = progress_height },
Height = bottom_black_area_height,
RelativeSizeAxes = Axes.X,
Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre,
Children = new Drawable[]
Y = player_height + 10,
},
playerContainer = new Container
{
RelativeSizeAxes = Axes.X,
Height = player_height,
Masking = true,
CornerRadius = 5,
EdgeEffect = new EdgeEffect
{
new FillFlowContainer<Button>
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(40),
Radius = 5,
},
Children = new[]
{
currentBackground = new Background(),
title = new OsuSpriteText
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Children = new[]
Origin = Anchor.BottomCentre,
Anchor = Anchor.TopCentre,
Position = new Vector2(0, 40),
TextSize = 25,
Colour = Color4.White,
Text = @"Nothing to play",
Font = @"Exo2.0-MediumItalic"
},
artist = new OsuSpriteText
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Position = new Vector2(0, 45),
TextSize = 15,
Colour = Color4.White,
Text = @"Nothing to play",
Font = @"Exo2.0-BoldItalic"
},
new Container
{
Padding = new MarginPadding { Bottom = progress_height },
Height = bottom_black_area_height,
RelativeSizeAxes = Axes.X,
Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre,
Children = new Drawable[]
{
new Button
new FillFlowContainer<Button>
{
Action = prev,
Icon = FontAwesome.fa_step_backward,
},
playButton = new Button
{
Scale = new Vector2(1.4f),
IconScale = new Vector2(1.4f),
Action = () =>
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Children = new[]
{
if (current?.Track == null) return;
if (current.Track.IsRunning)
current.Track.Stop();
else
current.Track.Start();
},
Icon = FontAwesome.fa_play_circle_o,
new Button
{
Action = prev,
Icon = FontAwesome.fa_step_backward,
},
playButton = new Button
{
Scale = new Vector2(1.4f),
IconScale = new Vector2(1.4f),
Action = play,
Icon = FontAwesome.fa_play_circle_o,
},
new Button
{
Action = next,
Icon = FontAwesome.fa_step_forward,
},
}
},
new Button
playlistButton = new Button
{
Action = next,
Icon = FontAwesome.fa_step_forward,
Origin = Anchor.Centre,
Anchor = Anchor.CentreRight,
Position = new Vector2(-bottom_black_area_height / 2, 0),
Icon = FontAwesome.fa_bars,
Action = () => playlist.ToggleVisibility(),
},
}
},
new Button
progressBar = new DragBar
{
Origin = Anchor.Centre,
Anchor = Anchor.CentreRight,
Position = new Vector2(-bottom_black_area_height / 2, 0),
Icon = FontAwesome.fa_bars,
},
}
Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre,
Height = progress_height,
Colour = colours.Yellow,
SeekRequested = seek
}
},
},
progress = new DragBar
{
Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre,
Height = progress_height,
Colour = colours.Yellow,
SeekRequested = seek
}
}
}
};
this.beatmaps = beatmaps;
trackManager = game.Audio.Track;
preferUnicode = config.GetBindable<bool>(OsuConfig.ShowUnicode);
preferUnicode.ValueChanged += unicode => updateDisplay(current, TransformDirection.None);
beatmapBacking.BindTo(game.Beatmap);
beatmapSource = game.Beatmap ?? new Bindable<WorkingBeatmap>();
playList = beatmaps.GetAllWithChildren<BeatmapSetInfo>();
currentBackground = new MusicControllerBackground();
dragContainer.Add(currentBackground);
playlist.StateChanged += (c, s) => playlistButton.FadeColour(s == Visibility.Visible ? colours.Yellow : Color4.White, 200, EasingTypes.OutQuint);
}
protected override void LoadComplete()
{
beatmapSource.ValueChanged += workingChanged;
beatmapSource.TriggerChange();
beatmapBacking.ValueChanged += beatmapChanged;
beatmapBacking.TriggerChange();
base.LoadComplete();
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
Height = dragContainer.Height;
}
protected override void Update()
{
base.Update();
if (pendingBeatmapSwitch != null)
{
pendingBeatmapSwitch();
pendingBeatmapSwitch = null;
}
if (current?.TrackLoaded ?? false)
{
progress.UpdatePosition(current.Track.Length == 0 ? 0 : (float)(current.Track.CurrentTime / current.Track.Length));
playButton.Icon = current.Track.IsRunning ? FontAwesome.fa_pause_circle_o : FontAwesome.fa_play_circle_o;
var track = current.Track;
if (current.Track.HasCompleted && !current.Track.Looping) next();
progressBar.UpdatePosition(track.Length == 0 ? 0 : (float)(track.CurrentTime / track.Length));
playButton.Icon = track.IsRunning ? FontAwesome.fa_pause_circle_o : FontAwesome.fa_play_circle_o;
if (track.HasCompleted && !track.Looping) next();
}
else
playButton.Icon = FontAwesome.fa_play_circle_o;
}
private void workingChanged(WorkingBeatmap beatmap)
private void play()
{
progress.IsEnabled = beatmap != null;
if (beatmap == current) return;
bool audioEquals = current?.BeatmapInfo?.AudioEquals(beatmap?.BeatmapInfo) ?? false;
current = beatmap;
updateDisplay(current, audioEquals ? TransformDirection.None : TransformDirection.Next);
appendToHistory(current?.BeatmapInfo);
}
var track = current?.Track;
private void appendToHistory(BeatmapInfo beatmap)
{
if (beatmap == null) return;
if (playHistoryIndex >= 0)
if (track == null)
{
if (beatmap.AudioEquals(playHistory[playHistoryIndex]))
return;
if (playHistoryIndex < playHistory.Count - 1)
playHistory.RemoveRange(playHistoryIndex + 1, playHistory.Count - playHistoryIndex - 1);
playlist.PlayNext();
return;
}
playHistory.Insert(++playHistoryIndex, beatmap);
if (track.IsRunning)
track.Stop();
else
track.Start();
}
private void prev()
{
if (playHistoryIndex > 0)
play(playHistory[--playHistoryIndex], false);
queuedDirection = TransformDirection.Prev;
playlist.PlayPrevious();
}
private void next()
{
if (playHistoryIndex < playHistory.Count - 1)
play(playHistory[++playHistoryIndex], true);
else
{
if (playList.Count == 0) return;
if (current != null && playList.Count == 1) return;
//shuffle
BeatmapInfo nextToPlay;
do
{
int j = RNG.Next(playListIndex, playList.Count);
if (j != playListIndex)
{
BeatmapSetInfo temp = playList[playListIndex];
playList[playListIndex] = playList[j];
playList[j] = temp;
}
nextToPlay = playList[playListIndex++].Beatmaps[0];
if (playListIndex == playList.Count) playListIndex = 0;
}
while (nextToPlay.AudioEquals(current?.BeatmapInfo));
play(nextToPlay, true);
appendToHistory(nextToPlay);
}
queuedDirection = TransformDirection.Next;
playlist.PlayNext();
}
private void play(BeatmapInfo info, bool isNext)
private WorkingBeatmap current;
private TransformDirection queuedDirection = TransformDirection.Next;
private void beatmapChanged(WorkingBeatmap beatmap)
{
current = beatmaps.GetWorkingBeatmap(info, current);
Task.Run(() =>
{
trackManager.SetExclusive(current.Track);
current.Track.Start();
beatmapSource.Value = current;
}).ContinueWith(task => Schedule(task.ThrowIfFaulted), TaskContinuationOptions.OnlyOnFaulted);
updateDisplay(current, isNext ? TransformDirection.Next : TransformDirection.Prev);
progressBar.IsEnabled = beatmap != null;
bool audioEquals = beatmapBacking.Value?.BeatmapInfo?.AudioEquals(current?.BeatmapInfo) ?? false;
current = beatmapBacking.Value;
updateDisplay(beatmapBacking, audioEquals ? TransformDirection.None : queuedDirection);
queuedDirection = TransformDirection.Next;
}
private Action pendingBeatmapSwitch;
private ScheduledDelegate pendingBeatmapSwitch;
private void updateDisplay(WorkingBeatmap beatmap, TransformDirection direction)
{
//we might be off-screen when this update comes in.
//rather than Scheduling, manually handle this to avoid possible memory contention.
pendingBeatmapSwitch = () =>
pendingBeatmapSwitch?.Cancel();
pendingBeatmapSwitch = Schedule(delegate
{
Task.Run(() =>
{
if (beatmap?.Beatmap == null)
if (beatmap?.Beatmap == null) //this is not needed if a placeholder exists
{
title.Current = null;
title.Text = @"Nothing to play";
artist.Current = null;
artist.Text = @"Nothing to play";
}
else
{
BeatmapMetadata metadata = beatmap.Beatmap.BeatmapInfo.Metadata;
title.Text = preferUnicode ? metadata.TitleUnicode : metadata.Title;
artist.Text = preferUnicode ? metadata.ArtistUnicode : metadata.Artist;
title.Current = localisation.GetUnicodePreference(metadata.TitleUnicode, metadata.Title);
artist.Current = localisation.GetUnicodePreference(metadata.ArtistUnicode, metadata.Artist);
}
});
dragContainer.Add(new AsyncLoadWrapper(new MusicControllerBackground(beatmap)
playerContainer.Add(new AsyncLoadWrapper(new Background(beatmap)
{
OnLoadComplete = d =>
{
@ -345,17 +325,15 @@ namespace osu.Game.Overlays
{
Depth = float.MaxValue,
});
};
});
}
private void seek(float position)
{
current?.Track?.Seek(current.Track.Length * position);
current?.Track?.Start();
var track = current?.Track;
track?.Seek(track.Length * position);
}
private const float transition_length = 800;
protected override void PopIn()
{
base.PopIn();
@ -372,14 +350,19 @@ namespace osu.Game.Overlays
dragContainer.ScaleTo(0.9f, transition_length, EasingTypes.OutQuint);
}
private enum TransformDirection { None, Next, Prev }
private enum TransformDirection
{
None,
Next,
Prev
}
private class MusicControllerBackground : BufferedContainer
private class Background : BufferedContainer
{
private readonly Sprite sprite;
private readonly WorkingBeatmap beatmap;
public MusicControllerBackground(WorkingBeatmap beatmap = null)
public Background(WorkingBeatmap beatmap = null)
{
this.beatmap = beatmap;
CacheDrawnFrameBuffer = true;

View File

@ -12,7 +12,11 @@ using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Options
{
public class OptionSlider<T> : FillFlowContainer where T : struct
public class OptionSlider<T> : OptionSlider<T, OsuSliderBar<T>> where T: struct
{
}
public class OptionSlider<T, U> : FillFlowContainer where T : struct where U : SliderBar<T>, new()
{
private readonly SliderBar<T> slider;
private readonly SpriteText text;
@ -50,7 +54,7 @@ namespace osu.Game.Overlays.Options
{
Alpha = 0,
},
slider = new OsuSliderBar<T>
slider = new U()
{
Margin = new MarginPadding { Top = 5, Bottom = 5 },
RelativeSizeAxes = Axes.X

View File

@ -2,7 +2,6 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
@ -18,10 +17,10 @@ namespace osu.Game.Overlays.Options.Sections.Audio
{
Children = new Drawable[]
{
new OptionSlider<double>
new OptionSlider<double, OffsetSlider>
{
LabelText = "Audio Offset",
Bindable = (BindableDouble)config.GetBindable<double>(OsuConfig.AudioOffset)
Bindable = config.GetBindable<double>(OsuConfig.AudioOffset)
},
new OsuButton
{
@ -30,5 +29,10 @@ namespace osu.Game.Overlays.Options.Sections.Audio
}
};
}
private class OffsetSlider : OsuSliderBar<double>
{
public override string TooltipText => Current.Value.ToString(@"0ms");
}
}
}

View File

@ -2,7 +2,6 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
@ -18,10 +17,10 @@ namespace osu.Game.Overlays.Options.Sections.Gameplay
{
Children = new Drawable[]
{
new OptionSlider<int>
new OptionSlider<double>
{
LabelText = "Background dim",
Bindable = (BindableInt)config.GetBindable<int>(OsuConfig.DimLevel)
Bindable = config.GetBindable<double>(OsuConfig.DimLevel)
},
new OptionEnumDropdown<ProgressBarType>
{
@ -36,7 +35,7 @@ namespace osu.Game.Overlays.Options.Sections.Gameplay
new OptionSlider<double>
{
LabelText = "Score meter size",
Bindable = (BindableDouble)config.GetBindable<double>(OsuConfig.ScoreMeterScale)
Bindable = config.GetBindable<double>(OsuConfig.ScoreMeterScale)
},
new OsuCheckbox
{

View File

@ -2,9 +2,9 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Options.Sections.Gameplay
{
@ -17,18 +17,23 @@ namespace osu.Game.Overlays.Options.Sections.Gameplay
{
Children = new Drawable[]
{
new OptionSlider<double>
new OptionSlider<double, StarSlider>
{
LabelText = "Display beatmaps from",
Bindable = (BindableDouble)config.GetBindable<double>(OsuConfig.DisplayStarsMinimum)
Bindable = config.GetBindable<double>(OsuConfig.DisplayStarsMinimum)
},
new OptionSlider<double>
new OptionSlider<double, StarSlider>
{
LabelText = "up to",
Bindable = (BindableDouble)config.GetBindable<double>(OsuConfig.DisplayStarsMaximum)
Bindable = config.GetBindable<double>(OsuConfig.DisplayStarsMaximum)
},
};
}
private class StarSlider : OsuSliderBar<double>
{
public override string TooltipText => Current.Value.ToString(@"0.## stars");
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
@ -13,7 +14,7 @@ namespace osu.Game.Overlays.Options.Sections.General
protected override string Header => "Language";
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
private void load(OsuConfigManager osuConfig, FrameworkConfigManager frameworkConfig)
{
Children = new Drawable[]
{
@ -21,12 +22,12 @@ namespace osu.Game.Overlays.Options.Sections.General
new OsuCheckbox
{
LabelText = "Prefer metadata in original language",
Bindable = config.GetBindable<bool>(OsuConfig.ShowUnicode)
Bindable = frameworkConfig.GetBindable<bool>(FrameworkConfig.ShowUnicode)
},
new OsuCheckbox
{
LabelText = "Use alternative font for chat display",
Bindable = config.GetBindable<bool>(OsuConfig.AlternativeChatFont)
Bindable = osuConfig.GetBindable<bool>(OsuConfig.AlternativeChatFont)
},
};
}

View File

@ -2,7 +2,6 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
@ -18,10 +17,10 @@ namespace osu.Game.Overlays.Options.Sections.Input
{
Children = new Drawable[]
{
new OptionSlider<double>
new OptionSlider<double, SensitivitySlider>
{
LabelText = "Sensitivity",
Bindable = (BindableDouble)config.GetBindable<double>(OsuConfig.MouseSpeed),
Bindable = config.GetBindable<double>(OsuConfig.MouseSpeed)
},
new OsuCheckbox
{
@ -55,5 +54,10 @@ namespace osu.Game.Overlays.Options.Sections.Input
},
};
}
private class SensitivitySlider : OsuSliderBar<double>
{
public override string TooltipText => Current.Value.ToString(@"0.##x");
}
}
}

View File

@ -2,7 +2,6 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Graphics;
@ -59,15 +58,15 @@ namespace osu.Game.Overlays.Options.Sections
LabelText = "Always use skin cursor",
Bindable = config.GetBindable<bool>(OsuConfig.UseSkinCursor)
},
new OptionSlider<double>
new OptionSlider<double, SizeSlider>
{
LabelText = "Menu cursor size",
Bindable = (BindableDouble)config.GetBindable<double>(OsuConfig.MenuCursorSize)
Bindable = config.GetBindable<double>(OsuConfig.MenuCursorSize)
},
new OptionSlider<double>
new OptionSlider<double, SizeSlider>
{
LabelText = "Gameplay cursor size",
Bindable = (BindableDouble)config.GetBindable<double>(OsuConfig.GameplayCursorSize)
Bindable = config.GetBindable<double>(OsuConfig.GameplayCursorSize)
},
new OsuCheckbox
{
@ -76,5 +75,10 @@ namespace osu.Game.Overlays.Options.Sections
},
};
}
private class SizeSlider : OsuSliderBar<double>
{
public override string TooltipText => Current.Value.ToString(@"0.##x");
}
}
}

View File

@ -5,7 +5,6 @@ using osu.Game.Audio;
using osu.Game.Beatmaps.Timing;
using osu.Game.Database;
using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Objects
{
@ -29,7 +28,7 @@ namespace osu.Game.Rulesets.Objects
/// and can be treated as the default samples for the hit object.
/// </para>
/// </summary>
public List<SampleInfo> Samples = new List<SampleInfo>();
public SampleInfoList Samples = new SampleInfoList();
/// <summary>
/// Applies default values to this HitObject.

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
};
}
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<SampleInfoList> repeatSamples)
{
return new ConvertSlider
{

View File

@ -123,7 +123,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
}
// Generate the final per-node samples
var nodeSamples = new List<List<SampleInfo>>(nodes);
var nodeSamples = new List<SampleInfoList>(nodes);
for (int i = 0; i <= repeatCount; i++)
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// <param name="repeatCount">The slider repeat count.</param>
/// <param name="repeatSamples">The samples to be played when the repeat nodes are hit. This includes the head and tail of the slider.</param>
/// <returns>The hit object.</returns>
protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples);
protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<SampleInfoList> repeatSamples);
/// <summary>
/// Creates a legacy Spinner-type hit object.
@ -214,9 +214,9 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// <returns>The hit object.</returns>
protected abstract HitObject CreateSpinner(Vector2 position, double endTime);
private List<SampleInfo> convertSoundType(LegacySoundType type, SampleBankInfo bankInfo)
private SampleInfoList convertSoundType(LegacySoundType type, SampleBankInfo bankInfo)
{
var soundTypes = new List<SampleInfo>
var soundTypes = new SampleInfoList
{
new SampleInfo
{

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
public CurveType CurveType { get; set; }
public double Distance { get; set; }
public List<List<SampleInfo>> RepeatSamples { get; set; }
public List<SampleInfoList> RepeatSamples { get; set; }
public int RepeatCount { get; set; } = 1;
public double EndTime { get; set; }

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
};
}
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<SampleInfoList> repeatSamples)
{
return new ConvertSlider
{

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
};
}
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<SampleInfoList> repeatSamples)
{
return new ConvertSlider
{

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
};
}
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<SampleInfoList> repeatSamples)
{
return new ConvertSlider
{

View File

@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Objects.Types
/// <summary>
/// The samples to be played when each repeat node is hit (0 -> first repeat node, 1 -> second repeat node, etc).
/// </summary>
List<List<SampleInfo>> RepeatSamples { get; }
List<SampleInfoList> RepeatSamples { get; }
}
}

View File

@ -0,0 +1,39 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Objects;
using osu.Game.Beatmaps;
namespace osu.Game.Rulesets.Replays
{
public abstract class AutoGenerator<T> : IAutoGenerator
where T : HitObject
{
/// <summary>
/// Creates the auto replay and returns it.
/// Every subclass of OsuAutoGeneratorBase should implement this!
/// </summary>
public abstract Replay Generate();
#region Parameters
/// <summary>
/// The beatmap we're making.
/// </summary>
protected Beatmap<T> Beatmap;
#endregion
protected AutoGenerator(Beatmap<T> beatmap)
{
Beatmap = beatmap;
}
#region Constants
// Shared amongst all modes
protected const double KEY_UP_DELAY = 50;
#endregion
}
}

View File

@ -0,0 +1,10 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Rulesets.Replays
{
public interface IAutoGenerator
{
Replay Generate();
}
}

View File

@ -8,8 +8,6 @@ namespace osu.Game.Rulesets.Replays
{
public class Replay
{
protected const double KEY_UP_DELAY = 50;
public User User;
public List<ReplayFrame> Frames = new List<ReplayFrame>();

View File

@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.UI
where TObject : HitObject
{
/// <summary>
/// The Beatmap
/// The Beatmap
/// </summary>
public Beatmap<TObject> Beatmap;

View File

@ -50,6 +50,7 @@ namespace osu.Game.Rulesets.UI
{
content = new Container
{
AlwaysReceiveInput = true,
RelativeSizeAxes = Axes.Both,
}
}

View File

@ -5,8 +5,10 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
using osu.Framework.Configuration;
using osu.Framework.Screens;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.Backgrounds;
using OpenTK.Graphics;
@ -56,25 +58,33 @@ namespace osu.Game.Screens.Menu
};
}
private Bindable<bool> menuVoice;
private Bindable<bool> menuMusic;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
private void load(AudioManager audio, OsuConfigManager config)
{
welcome = audio.Sample.Get(@"welcome");
seeya = audio.Sample.Get(@"seeya");
menuVoice = config.GetBindable<bool>(OsuConfig.MenuVoice);
menuMusic = config.GetBindable<bool>(OsuConfig.MenuMusic);
bgm = audio.Track.Get(@"circles");
bgm.Looping = true;
welcome = audio.Sample.Get(@"welcome");
seeya = audio.Sample.Get(@"seeya");
}
protected override void OnEntering(Screen last)
{
base.OnEntering(last);
welcome.Play();
if (menuVoice)
welcome.Play();
Scheduler.AddDelayed(delegate
{
bgm.Start();
if (menuMusic)
bgm.Start();
LoadComponentAsync(mainMenu = new MainMenu());
@ -109,15 +119,17 @@ namespace osu.Game.Screens.Menu
if (!(last is MainMenu))
Content.FadeIn(300);
double fadeOutTime = 2000;
//we also handle the exit transition.
seeya.Play();
if (menuVoice)
seeya.Play();
else
fadeOutTime = 500;
const double fade_out_time = 2000;
Scheduler.AddDelayed(Exit, fade_out_time);
Scheduler.AddDelayed(Exit, fadeOutTime);
//don't want to fade out completely else we will stop running updates and shit will hit the fan.
Game.FadeTo(0.01f, fade_out_time);
Game.FadeTo(0.01f, fadeOutTime);
base.OnResuming(last);
}

View File

@ -2,8 +2,14 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.MathUtils;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Charts;
@ -15,6 +21,9 @@ using osu.Game.Screens.Select;
using osu.Game.Screens.Tournament;
using osu.Framework.Input;
using OpenTK.Input;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
namespace osu.Game.Screens.Menu
{
@ -54,11 +63,27 @@ namespace osu.Game.Screens.Menu
};
}
private Bindable<bool> menuMusic;
private TrackManager trackManager;
private WorkingBeatmap song;
[BackgroundDependencyLoader]
private void load(OsuGame game)
private void load(OsuGame game, OsuConfigManager config, BeatmapDatabase beatmaps)
{
menuMusic = config.GetBindable<bool>(OsuConfig.MenuMusic);
LoadComponentAsync(background);
if (!menuMusic)
{
trackManager = game.Audio.Track;
List<BeatmapSetInfo> choosableBeatmapSets = beatmaps.Query<BeatmapSetInfo>().ToList();
if (choosableBeatmapSets.Count > 0)
{
song = beatmaps.GetWorkingBeatmap(beatmaps.GetWithChildren<BeatmapSetInfo>(choosableBeatmapSets[RNG.Next(0, choosableBeatmapSets.Count - 1)].ID).Beatmaps[0]);
Beatmap = song;
}
}
buttons.OnSettings = game.ToggleOptions;
preloadSongSelect();
@ -81,6 +106,17 @@ namespace osu.Game.Screens.Menu
{
base.OnEntering(last);
buttons.FadeInFromZero(500);
if (last is Intro && song != null)
{
Task.Run(() =>
{
trackManager.SetExclusive(song.Track);
song.Track.Seek(song.Beatmap.Metadata.PreviewTime);
if (song.Beatmap.Metadata.PreviewTime == -1)
song.Track.Seek(song.Track.Length * 0.4f);
song.Track.Start();
});
}
}
protected override void OnSuspending(Screen next)

View File

@ -38,7 +38,7 @@ namespace osu.Game.Screens.Play
public Action RestartRequested;
public bool IsPaused => !interpolatedSourceClock.IsRunning;
public bool IsPaused => !decoupledClock.IsRunning;
internal override bool AllowRulesetChange => false;
@ -51,9 +51,9 @@ namespace osu.Game.Screens.Play
private bool canPause => ValidForResume && !HasFailed && Time.Current >= lastPauseActionTime + pause_cooldown;
private IAdjustableClock sourceClock;
private OffsetClock offsetClock;
private IFrameBasedClock interpolatedSourceClock;
private IAdjustableClock adjustableSourceClock;
private FramedOffsetClock offsetClock;
private DecoupleableInterpolatingFramedClock decoupledClock;
private RulesetInfo ruleset;
@ -62,7 +62,7 @@ namespace osu.Game.Screens.Play
#region User Settings
private Bindable<int> dimLevel;
private Bindable<double> dimLevel;
private Bindable<bool> mouseWheelDisabled;
private Bindable<double> userAudioOffset;
@ -70,6 +70,8 @@ namespace osu.Game.Screens.Play
private SkipButton skipButton;
private Container hitRendererContainer;
private HudOverlay hudOverlay;
private PauseOverlay pauseOverlay;
private FailOverlay failOverlay;
@ -77,7 +79,7 @@ namespace osu.Game.Screens.Play
[BackgroundDependencyLoader(permitNulls: true)]
private void load(AudioManager audio, BeatmapDatabase beatmaps, OsuConfigManager config, OsuGame osu)
{
dimLevel = config.GetBindable<int>(OsuConfig.DimLevel);
dimLevel = config.GetBindable<double>(OsuConfig.DimLevel);
mouseWheelDisabled = config.GetBindable<bool>(OsuConfig.MouseDisableWheel);
Ruleset rulesetInstance;
@ -87,10 +89,7 @@ namespace osu.Game.Screens.Play
if (Beatmap == null)
Beatmap = beatmaps.GetWorkingBeatmap(BeatmapInfo, withStoryboard: true);
if ((Beatmap?.Beatmap?.HitObjects.Count ?? 0) == 0)
throw new Exception("No valid objects were found!");
if (Beatmap == null)
if (Beatmap?.Beatmap == null)
throw new Exception("Beatmap was not loaded");
ruleset = osu?.Ruleset.Value ?? Beatmap.BeatmapInfo.Ruleset;
@ -108,6 +107,9 @@ namespace osu.Game.Screens.Play
rulesetInstance = ruleset.CreateInstance();
HitRenderer = rulesetInstance.CreateHitRendererWith(Beatmap);
}
if (!HitRenderer.Objects.Any())
throw new Exception("Beatmap contains no hit objects!");
}
catch (Exception e)
{
@ -123,23 +125,31 @@ namespace osu.Game.Screens.Play
if (track != null)
{
audio.Track.SetExclusive(track);
sourceClock = track;
adjustableSourceClock = track;
}
sourceClock = (IAdjustableClock)track ?? new StopwatchClock();
offsetClock = new OffsetClock(sourceClock);
adjustableSourceClock = (IAdjustableClock)track ?? new StopwatchClock();
decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
var firstObjectTime = HitRenderer.Objects.First().StartTime;
decoupledClock.Seek(Math.Min(0, firstObjectTime - Math.Max(Beatmap.Beatmap.TimingInfo.BeatLengthAt(firstObjectTime) * 4, Beatmap.BeatmapInfo.AudioLeadIn)));
decoupledClock.ProcessFrame();
offsetClock = new FramedOffsetClock(decoupledClock);
userAudioOffset = config.GetBindable<double>(OsuConfig.AudioOffset);
userAudioOffset.ValueChanged += v => offsetClock.Offset = v;
userAudioOffset.TriggerChange();
interpolatedSourceClock = new InterpolatingFramedClock(offsetClock);
Schedule(() =>
{
sourceClock.Reset();
adjustableSourceClock.Reset();
foreach (var mod in Beatmap.Mods.Value.OfType<IApplicableToClock>())
mod.ApplyToClock(sourceClock);
mod.ApplyToClock(adjustableSourceClock);
decoupledClock.ChangeSource(adjustableSourceClock);
});
scoreProcessor = HitRenderer.CreateScoreProcessor();
@ -155,7 +165,9 @@ namespace osu.Game.Screens.Play
hudOverlay.BindHitRenderer(HitRenderer);
hudOverlay.Progress.Objects = HitRenderer.Objects;
hudOverlay.Progress.AudioClock = interpolatedSourceClock;
hudOverlay.Progress.AudioClock = decoupledClock;
hudOverlay.Progress.AllowSeeking = HitRenderer.HasReplayLoaded;
hudOverlay.Progress.OnSeek = pos => decoupledClock.Seek(pos);
//bind HitRenderer to ScoreProcessor and ourselves (for a pass situation)
HitRenderer.OnAllJudged += onCompletion;
@ -165,16 +177,20 @@ namespace osu.Game.Screens.Play
Children = new Drawable[]
{
new Container
hitRendererContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Clock = interpolatedSourceClock,
Children = new Drawable[]
{
HitRenderer,
skipButton = new SkipButton
new Container
{
Alpha = 0
RelativeSizeAxes = Axes.Both,
Clock = offsetClock,
Children = new Drawable[]
{
HitRenderer,
skipButton = new SkipButton { Alpha = 0 },
}
},
}
},
@ -224,7 +240,7 @@ namespace osu.Game.Screens.Play
skipButton.Action = () =>
{
sourceClock.Seek(firstHitObject - skip_required_cutoff - fade_time);
decoupledClock.Seek(firstHitObject - skip_required_cutoff - fade_time);
skipButton.Action = null;
};
@ -241,7 +257,7 @@ namespace osu.Game.Screens.Play
// we want to wait for the source clock to stop so we can be sure all components are in a stable state.
if (!IsPaused)
{
sourceClock.Stop();
decoupledClock.Stop();
Schedule(() => Pause(force));
return;
@ -268,7 +284,7 @@ namespace osu.Game.Screens.Play
hudOverlay.KeyCounter.IsCounting = true;
hudOverlay.Progress.Hide();
pauseOverlay.Hide();
sourceClock.Start();
decoupledClock.Start();
}
public void Restart()
@ -304,7 +320,7 @@ namespace osu.Game.Screens.Play
private void onFail()
{
sourceClock.Stop();
decoupledClock.Stop();
HasFailed = true;
failOverlay.Retries = RestartCount;
@ -316,11 +332,11 @@ namespace osu.Game.Screens.Play
base.OnEntering(last);
(Background as BackgroundScreenBeatmap)?.BlurTo(Vector2.Zero, 1500, EasingTypes.OutQuint);
Background?.FadeTo((100f - dimLevel) / 100, 1500, EasingTypes.OutQuint);
Background?.FadeTo(1 - (float)dimLevel, 1500, EasingTypes.OutQuint);
Content.Alpha = 0;
dimLevel.ValueChanged += newDim => Background?.FadeTo((100f - newDim) / 100, 800);
dimLevel.ValueChanged += newDim => Background?.FadeTo(1 - (float)newDim, 800);
Content.ScaleTo(0.7f);
@ -332,13 +348,12 @@ namespace osu.Game.Screens.Play
Delay(750);
Schedule(() =>
{
sourceClock.Start();
decoupledClock.Start();
initializeSkipButton();
});
//keep in mind this is using the interpolatedSourceClock so won't be run as early as we may expect.
HitRenderer.Alpha = 0;
HitRenderer.FadeIn(750, EasingTypes.OutQuint);
hitRendererContainer.Alpha = 0;
hitRendererContainer.FadeIn(750, EasingTypes.OutQuint);
}
protected override void OnSuspending(Screen next)

View File

@ -1,21 +1,28 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK.Input;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Input;
using osu.Framework.Timing;
using osu.Game.Configuration;
using osu.Game.Input.Handlers;
namespace osu.Game.Screens.Play
{
public class PlayerInputManager : PassThroughInputManager
{
private readonly ManualClock clock = new ManualClock();
private ManualClock clock;
private IFrameBasedClock parentClock;
private ReplayInputHandler replayInputHandler;
public ReplayInputHandler ReplayInputHandler
{
get { return replayInputHandler; }
get
{
return replayInputHandler;
}
set
{
if (replayInputHandler != null) RemoveHandler(replayInputHandler);
@ -28,12 +35,58 @@ namespace osu.Game.Screens.Play
}
}
private Bindable<bool> mouseDisabled;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
mouseDisabled = config.GetBindable<bool>(OsuConfig.MouseDisableButtons);
}
protected override void LoadComplete()
{
base.LoadComplete();
//our clock will now be our parent's clock, but we want to replace this to allow manual control.
parentClock = Clock;
Clock = new FramedClock(clock);
Clock = new FramedClock(clock = new ManualClock
{
CurrentTime = parentClock.CurrentTime,
Rate = parentClock.Rate,
});
}
/// <summary>
/// Whether we running up-to-date with our parent clock.
/// If not, we will need to keep processing children until we catch up.
/// </summary>
private bool requireMoreUpdateLoops;
/// <summary>
/// Whether we in a valid state (ie. should we keep processing children frames).
/// This should be set to false when the replay is, for instance, waiting for future frames to arrive.
/// </summary>
private bool validState;
protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && validState;
private bool isAttached => replayInputHandler != null && !UseParentState;
private const int max_catch_up_updates_per_frame = 50;
public override bool UpdateSubTree()
{
requireMoreUpdateLoops = true;
validState = true;
int loops = 0;
while (validState && requireMoreUpdateLoops && loops++ < max_catch_up_updates_per_frame)
if (!base.UpdateSubTree())
return false;
return true;
}
protected override void Update()
@ -43,27 +96,44 @@ namespace osu.Game.Screens.Play
clock.Rate = parentClock.Rate;
clock.IsRunning = parentClock.IsRunning;
//if a replayHandler is not attached, we should just pass-through.
if (UseParentState || replayInputHandler == null)
if (!isAttached)
{
clock.CurrentTime = parentClock.CurrentTime;
base.Update();
return;
}
while (true)
else
{
double? newTime = replayInputHandler.SetFrameFromTime(parentClock.CurrentTime);
if (newTime == null)
//we shouldn't execute for this time value
break;
if (clock.CurrentTime == parentClock.CurrentTime)
break;
{
// we shouldn't execute for this time value. probably waiting on more replay data.
validState = false;
return;
}
clock.CurrentTime = newTime.Value;
base.Update();
}
requireMoreUpdateLoops = clock.CurrentTime != parentClock.CurrentTime;
base.Update();
}
protected override void TransformState(InputState state)
{
base.TransformState(state);
// we don't want to transform the state if a replay is present (for now, at least).
if (replayInputHandler != null) return;
var mouse = state.Mouse as Framework.Input.MouseState;
if (mouse != null)
{
if (mouseDisabled.Value)
{
mouse.SetPressed(MouseButton.Left, false);
mouse.SetPressed(MouseButton.Right, false);
}
}
}
}

View File

@ -14,6 +14,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Menu;
using OpenTK;
using osu.Framework.Localisation;
namespace osu.Game.Screens.Play
{
@ -174,10 +175,17 @@ namespace osu.Game.Screens.Play
}
};
}
}
private readonly WorkingBeatmap beatmap;
public BeatmapMetadataDisplay(WorkingBeatmap beatmap)
{
this.beatmap = beatmap;
}
[BackgroundDependencyLoader]
private void load(LocalisationEngine localisation)
{
var metadata = beatmap?.BeatmapInfo?.Metadata ?? new BeatmapMetadata();
@ -194,7 +202,7 @@ namespace osu.Game.Screens.Play
{
new OsuSpriteText
{
Text = metadata.Title,
Current = localisation.GetUnicodePreference(metadata.TitleUnicode, metadata.Title),
TextSize = 36,
Font = @"Exo2.0-MediumItalic",
Origin = Anchor.TopCentre,
@ -202,7 +210,7 @@ namespace osu.Game.Screens.Play
},
new OsuSpriteText
{
Text = metadata.Artist,
Current = localisation.GetUnicodePreference(metadata.ArtistUnicode, metadata.Artist),
TextSize = 26,
Font = @"Exo2.0-MediumItalic",
Origin = Anchor.TopCentre,

View File

@ -35,6 +35,8 @@ namespace osu.Game.Screens.Play
private double lastHitTime => ((objects.Last() as IHasEndTime)?.EndTime ?? objects.Last().StartTime) + 1;
private double firstHitTime => objects.First().StartTime;
private IEnumerable<HitObject> objects;
public IEnumerable<HitObject> Objects
@ -75,7 +77,7 @@ namespace osu.Game.Screens.Play
Origin = Anchor.BottomLeft,
SeekRequested = delegate (float position)
{
OnSeek?.Invoke(position);
OnSeek?.Invoke(firstHitTime + position * (lastHitTime - firstHitTime));
},
},
};
@ -86,18 +88,28 @@ namespace osu.Game.Screens.Play
State = Visibility.Visible;
}
private bool barVisible;
private bool allowSeeking;
public void ToggleBar()
public bool AllowSeeking
{
barVisible = !barVisible;
updateBarVisibility();
get
{
return allowSeeking;
}
set
{
if (allowSeeking == value) return;
allowSeeking = value;
updateBarVisibility();
}
}
private void updateBarVisibility()
{
bar.FadeTo(barVisible ? 1 : 0, transition_duration, EasingTypes.In);
MoveTo(new Vector2(0, barVisible ? 0 : bottom_bar_height), transition_duration, EasingTypes.In);
bar.FadeTo(allowSeeking ? 1 : 0, transition_duration, EasingTypes.In);
MoveTo(new Vector2(0, allowSeeking ? 0 : bottom_bar_height), transition_duration, EasingTypes.In);
}
protected override void PopIn()
@ -118,7 +130,7 @@ namespace osu.Game.Screens.Play
if (objects == null)
return;
double progress = (AudioClock?.CurrentTime ?? Time.Current) / lastHitTime;
double progress = ((AudioClock?.CurrentTime ?? Time.Current) - firstHitTime) / lastHitTime;
bar.UpdatePosition((float)progress);
graph.Progress = (int)(graph.ColumnCount * progress);

View File

@ -70,24 +70,26 @@ namespace osu.Game.Screens.Ranking
circleOuterBackground.ScaleTo(1, transition_time, EasingTypes.OutQuint);
circleOuterBackground.FadeTo(1, transition_time, EasingTypes.OutQuint);
Content.Delay(transition_time * 0.25f, true);
using (BeginDelayedSequence(transition_time * 0.25f, true))
{
circleOuter.ScaleTo(1, transition_time, EasingTypes.OutQuint);
circleOuter.FadeTo(1, transition_time, EasingTypes.OutQuint);
circleOuter.ScaleTo(1, transition_time, EasingTypes.OutQuint);
circleOuter.FadeTo(1, transition_time, EasingTypes.OutQuint);
Content.Delay(transition_time * 0.3f, true);
using (BeginDelayedSequence(transition_time * 0.3f, true))
{
backgroundParallax.FadeIn(transition_time, EasingTypes.OutQuint);
backgroundParallax.FadeIn(transition_time, EasingTypes.OutQuint);
circleInner.ScaleTo(1, transition_time, EasingTypes.OutQuint);
circleInner.FadeTo(1, transition_time, EasingTypes.OutQuint);
circleInner.ScaleTo(1, transition_time, EasingTypes.OutQuint);
circleInner.FadeTo(1, transition_time, EasingTypes.OutQuint);
Content.Delay(transition_time * 0.4f, true);
modeChangeButtons.FadeIn(transition_time, EasingTypes.OutQuint);
currentPage.FadeIn(transition_time, EasingTypes.OutQuint);
Content.DelayReset();
using (BeginDelayedSequence(transition_time * 0.4f, true))
{
modeChangeButtons.FadeIn(transition_time, EasingTypes.OutQuint);
currentPage.FadeIn(transition_time, EasingTypes.OutQuint);
}
}
}
}
protected override bool OnExiting(Screen next)

View File

@ -1,30 +1,29 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Configuration;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Users;
using OpenTK;
using OpenTK.Graphics;
using System;
using System.Collections.Generic;
using osu.Framework.Extensions.Color4Extensions;
using osu.Game.Beatmaps;
using osu.Game.Screens.Play;
using osu.Game.Rulesets.Scoring;
using osu.Framework.Graphics.Colour;
using System.Linq;
namespace osu.Game.Screens.Ranking
{
@ -272,8 +271,6 @@ namespace osu.Game.Screens.Ranking
{
private readonly BeatmapInfo beatmap;
private Bindable<bool> preferUnicode;
private readonly OsuSpriteText title;
private readonly OsuSpriteText artist;
private readonly OsuSpriteText versionMapper;
@ -323,20 +320,14 @@ namespace osu.Game.Screens.Ranking
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, OsuConfigManager config)
private void load(OsuColour colours, LocalisationEngine localisation)
{
title.Colour = artist.Colour = colours.BlueDarker;
versionMapper.Colour = colours.Gray8;
versionMapper.Text = $"{beatmap.Version} - mapped by {beatmap.Metadata.Author}";
preferUnicode = config.GetBindable<bool>(OsuConfig.ShowUnicode);
preferUnicode.ValueChanged += unicode =>
{
title.Text = unicode ? beatmap.Metadata.TitleUnicode : beatmap.Metadata.Title;
artist.Text = unicode ? beatmap.Metadata.ArtistUnicode : beatmap.Metadata.Artist;
};
preferUnicode.TriggerChange();
title.Current = localisation.GetUnicodePreference(beatmap.Metadata.TitleUnicode, beatmap.Metadata.Title);
artist.Current = localisation.GetUnicodePreference(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist);
}
}

View File

@ -120,7 +120,7 @@ namespace osu.Game.Screens.Select
public void RemoveBeatmap(BeatmapSetInfo info) => removeGroup(groups.Find(b => b.BeatmapSet.ID == info.ID));
public Action<BeatmapGroup, BeatmapInfo> SelectionChanged;
public Action<BeatmapInfo> SelectionChanged;
public Action StartRequested;
@ -230,7 +230,7 @@ namespace osu.Game.Screens.Select
return new BeatmapGroup(beatmapSet, database)
{
SelectionChanged = SelectionChanged,
SelectionChanged = (g, p) => selectGroup(g, p),
StartRequested = b => StartRequested?.Invoke(),
State = BeatmapGroupState.Collapsed
};
@ -328,21 +328,33 @@ namespace osu.Game.Screens.Select
private void selectGroup(BeatmapGroup group, BeatmapPanel panel = null, bool animated = true)
{
if (panel == null)
panel = group.BeatmapPanels.First();
try
{
if (panel == null)
panel = group.BeatmapPanels.First();
Trace.Assert(group.BeatmapPanels.Contains(panel), @"Selected panel must be in provided group");
if (selectedPanel == panel) return;
if (selectedGroup != null && selectedGroup != group && selectedGroup.State != BeatmapGroupState.Hidden)
selectedGroup.State = BeatmapGroupState.Collapsed;
Trace.Assert(group.BeatmapPanels.Contains(panel), @"Selected panel must be in provided group");
group.State = BeatmapGroupState.Expanded;
selectedGroup = group;
panel.State = PanelSelectedState.Selected;
selectedPanel = panel;
if (selectedGroup != null && selectedGroup != group && selectedGroup.State != BeatmapGroupState.Hidden)
selectedGroup.State = BeatmapGroupState.Collapsed;
float selectedY = computeYPositions(animated);
ScrollTo(selectedY, animated);
group.State = BeatmapGroupState.Expanded;
panel.State = PanelSelectedState.Selected;
if (selectedPanel == panel) return;
selectedPanel = panel;
selectedGroup = group;
SelectionChanged?.Invoke(panel.Beatmap);
}
finally
{
float selectedY = computeYPositions(animated);
ScrollTo(selectedY, animated);
}
}
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)

View File

@ -74,156 +74,21 @@ namespace osu.Game.Screens.Select
}
State = Visibility.Visible;
var lastContainer = beatmapInfoContainer;
float newDepth = lastContainer?.Depth + 1 ?? 0;
BeatmapInfo beatmapInfo = beatmap.BeatmapInfo;
BeatmapMetadata metadata = beatmap.BeatmapInfo?.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
List<InfoLabel> labels = new List<InfoLabel>();
if (beatmap.Beatmap != null)
{
HitObject lastObject = beatmap.Beatmap.HitObjects.LastOrDefault();
double endTime = (lastObject as IHasEndTime)?.EndTime ?? lastObject?.StartTime ?? 0;
labels.Add(new InfoLabel(new BeatmapStatistic
{
Name = "Length",
Icon = FontAwesome.fa_clock_o,
Content = beatmap.Beatmap.HitObjects.Count == 0 ? "-" : TimeSpan.FromMilliseconds(endTime - beatmap.Beatmap.HitObjects.First().StartTime).ToString(@"m\:ss"),
}));
labels.Add(new InfoLabel(new BeatmapStatistic
{
Name = "BPM",
Icon = FontAwesome.fa_circle,
Content = getBPMRange(beatmap.Beatmap),
}));
//get statistics fromt he current ruleset.
labels.AddRange(beatmap.BeatmapInfo.Ruleset.CreateInstance().GetBeatmapStatistics(beatmap).Select(s => new InfoLabel(s)));
}
AlwaysPresent = true;
var lastContainer = beatmapInfoContainer;
float newDepth = lastContainer?.Depth + 1 ?? 0;
Add(beatmapInfoContainer = new AsyncLoadWrapper(
new BufferedContainer
new BufferedWedgeInfo(beatmap)
{
Shear = -Shear,
OnLoadComplete = d =>
{
FadeIn(250);
lastContainer?.FadeOut(250);
lastContainer?.Expire();
},
PixelSnapping = true,
CacheDrawnFrameBuffer = true,
Shear = -Shear,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
// We will create the white-to-black gradient by modulating transparency and having
// a black backdrop. This results in an sRGB-space gradient and not linear space,
// transitioning from white to black more perceptually uniformly.
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
// We use a container, such that we can set the colour gradient to go across the
// vertices of the masked container instead of the vertices of the (larger) sprite.
new Container
{
RelativeSizeAxes = Axes.Both,
ColourInfo = ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0.3f)),
Children = new[]
{
// Zoomed-in and cropped beatmap background
new BeatmapBackgroundSprite(beatmap)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fill,
},
},
},
new DifficultyColourBar(beatmap.BeatmapInfo)
{
RelativeSizeAxes = Axes.Y,
Width = 20,
},
new FillFlowContainer
{
Name = "Top-aligned metadata",
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Direction = FillDirection.Vertical,
Margin = new MarginPadding { Top = 10, Left = 25, Right = 10, Bottom = 20 },
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new OsuSpriteText
{
Font = @"Exo2.0-MediumItalic",
Text = beatmapInfo.Version,
TextSize = 24,
},
}
},
new FillFlowContainer
{
Name = "Bottom-aligned metadata",
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Direction = FillDirection.Vertical,
Margin = new MarginPadding { Top = 15, Left = 25, Right = 10, Bottom = 20 },
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new OsuSpriteText
{
Font = @"Exo2.0-MediumItalic",
Text = !string.IsNullOrEmpty(metadata.Source) ? metadata.Source + " — " + metadata.Title : metadata.Title,
TextSize = 28,
},
new OsuSpriteText
{
Font = @"Exo2.0-MediumItalic",
Text = metadata.Artist,
TextSize = 17,
},
new FillFlowContainer
{
Margin = new MarginPadding { Top = 10 },
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Children = new[]
{
new OsuSpriteText
{
Font = @"Exo2.0-Medium",
Text = "mapped by ",
TextSize = 15,
},
new OsuSpriteText
{
Font = @"Exo2.0-Bold",
Text = metadata.Author,
TextSize = 15,
},
}
},
new FillFlowContainer
{
Margin = new MarginPadding { Top = 20, Left = 10 },
Spacing = new Vector2(40, 0),
AutoSizeAxes = Axes.Both,
Children = labels
},
}
},
}
})
{
@ -231,23 +96,164 @@ namespace osu.Game.Screens.Select
});
}
private string getBPMRange(Beatmap beatmap)
public class BufferedWedgeInfo : BufferedContainer
{
double bpmMax = beatmap.TimingInfo.BPMMaximum;
double bpmMin = beatmap.TimingInfo.BPMMinimum;
if (Precision.AlmostEquals(bpmMin, bpmMax)) return Math.Round(bpmMin) + "bpm";
return Math.Round(bpmMin) + "-" + Math.Round(bpmMax) + "bpm (mostly " + Math.Round(beatmap.TimingInfo.BPMMode) + "bpm)";
}
public class InfoLabel : Container
{
public InfoLabel(BeatmapStatistic statistic)
public BufferedWedgeInfo(WorkingBeatmap beatmap)
{
AutoSizeAxes = Axes.Both;
BeatmapInfo beatmapInfo = beatmap.BeatmapInfo;
BeatmapMetadata metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
List<InfoLabel> labels = new List<InfoLabel>();
if (beatmap.Beatmap != null)
{
HitObject lastObject = beatmap.Beatmap.HitObjects.LastOrDefault();
double endTime = (lastObject as IHasEndTime)?.EndTime ?? lastObject?.StartTime ?? 0;
labels.Add(new InfoLabel(new BeatmapStatistic
{
Name = "Length",
Icon = FontAwesome.fa_clock_o,
Content = beatmap.Beatmap.HitObjects.Count == 0 ? "-" : TimeSpan.FromMilliseconds(endTime - beatmap.Beatmap.HitObjects.First().StartTime).ToString(@"m\:ss"),
}));
labels.Add(new InfoLabel(new BeatmapStatistic
{
Name = "BPM",
Icon = FontAwesome.fa_circle,
Content = getBPMRange(beatmap.Beatmap),
}));
//get statistics fromt he current ruleset.
labels.AddRange(beatmapInfo.Ruleset.CreateInstance().GetBeatmapStatistics(beatmap).Select(s => new InfoLabel(s)));
}
PixelSnapping = true;
CacheDrawnFrameBuffer = true;
RelativeSizeAxes = Axes.Both;
Children = new Drawable[]
{
// We will create the white-to-black gradient by modulating transparency and having
// a black backdrop. This results in an sRGB-space gradient and not linear space,
// transitioning from white to black more perceptually uniformly.
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
// We use a container, such that we can set the colour gradient to go across the
// vertices of the masked container instead of the vertices of the (larger) sprite.
new Container
{
RelativeSizeAxes = Axes.Both,
ColourInfo = ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0.3f)),
Children = new[]
{
// Zoomed-in and cropped beatmap background
new BeatmapBackgroundSprite(beatmap)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fill,
},
},
},
new DifficultyColourBar(beatmap.BeatmapInfo)
{
RelativeSizeAxes = Axes.Y,
Width = 20,
},
new FillFlowContainer
{
Name = "Top-aligned metadata",
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Direction = FillDirection.Vertical,
Margin = new MarginPadding { Top = 10, Left = 25, Right = 10, Bottom = 20 },
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new OsuSpriteText
{
Font = @"Exo2.0-MediumItalic",
Text = beatmapInfo.Version,
TextSize = 24,
},
}
},
new FillFlowContainer
{
Name = "Bottom-aligned metadata",
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Direction = FillDirection.Vertical,
Margin = new MarginPadding { Top = 15, Left = 25, Right = 10, Bottom = 20 },
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new OsuSpriteText
{
Font = @"Exo2.0-MediumItalic",
Text = !string.IsNullOrEmpty(metadata.Source) ? metadata.Source + " — " + metadata.Title : metadata.Title,
TextSize = 28,
},
new OsuSpriteText
{
Font = @"Exo2.0-MediumItalic",
Text = metadata.Artist,
TextSize = 17,
},
new FillFlowContainer
{
Margin = new MarginPadding { Top = 10 },
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Children = new[]
{
new OsuSpriteText
{
Font = @"Exo2.0-Medium",
Text = "mapped by ",
TextSize = 15,
},
new OsuSpriteText
{
Font = @"Exo2.0-Bold",
Text = metadata.Author,
TextSize = 15,
},
}
},
new FillFlowContainer
{
Margin = new MarginPadding { Top = 20, Left = 10 },
Spacing = new Vector2(40, 0),
AutoSizeAxes = Axes.Both,
Children = labels
},
}
},
};
}
private string getBPMRange(Beatmap beatmap)
{
double bpmMax = beatmap.TimingInfo.BPMMaximum;
double bpmMin = beatmap.TimingInfo.BPMMinimum;
if (Precision.AlmostEquals(bpmMin, bpmMax)) return Math.Round(bpmMin) + "bpm";
return Math.Round(bpmMin) + "-" + Math.Round(bpmMax) + "bpm (mostly " + Math.Round(beatmap.TimingInfo.BPMMode) + "bpm)";
}
public class InfoLabel : Container
{
public InfoLabel(BeatmapStatistic statistic)
{
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
new TextAwesome
{
Icon = FontAwesome.fa_square,
@ -273,23 +279,23 @@ namespace osu.Game.Screens.Select
TextSize = 17,
Origin = Anchor.CentreLeft
},
};
}
}
private class DifficultyColourBar : DifficultyColouredContainer
{
public DifficultyColourBar(BeatmapInfo beatmap) : base(beatmap)
{
};
}
}
[BackgroundDependencyLoader]
private void load()
private class DifficultyColourBar : DifficultyColouredContainer
{
const float full_opacity_ratio = 0.7f;
Children = new Drawable[]
public DifficultyColourBar(BeatmapInfo beatmap) : base(beatmap)
{
}
[BackgroundDependencyLoader]
private void load()
{
const float full_opacity_ratio = 0.7f;
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
@ -305,7 +311,8 @@ namespace osu.Game.Screens.Select
X = full_opacity_ratio,
Width = 1 - full_opacity_ratio,
}
};
};
}
}
}
}

View File

@ -27,13 +27,8 @@ namespace osu.Game.Screens.Select
bool match = hasCurrentMode;
match &= string.IsNullOrEmpty(SearchText)
|| (set.Metadata.Artist ?? string.Empty).IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) != -1
|| (set.Metadata.ArtistUnicode ?? string.Empty).IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) != -1
|| (set.Metadata.Title ?? string.Empty).IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) != -1
|| (set.Metadata.TitleUnicode ?? string.Empty).IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) != -1
|| (set.Metadata.Tags ?? string.Empty).IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) != -1
|| (set.Metadata.Source ?? string.Empty).IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) != -1;
if (!string.IsNullOrEmpty(SearchText))
match &= set.Metadata.SearchableTerms.Any(term => term.IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) >= 0);
switch (g.State)
{

View File

@ -1,7 +1,6 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Linq;
using System.Threading;
using OpenTK;
using OpenTK.Input;
@ -15,8 +14,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@ -197,6 +196,12 @@ namespace osu.Game.Screens.Select
private void raiseSelect()
{
var pendingSelection = selectionChangedDebounce;
selectionChangedDebounce = null;
pendingSelection?.RunTask();
pendingSelection?.Cancel(); // cancel the already scheduled task.
if (Beatmap == null) return;
OnSelected();
@ -297,25 +302,37 @@ namespace osu.Game.Screens.Select
carousel.SelectBeatmap(beatmap?.BeatmapInfo);
}
private ScheduledDelegate selectionChangedDebounce;
// We need to keep track of the last selected beatmap ignoring debounce to play the correct selection sounds.
private BeatmapInfo selectionChangeNoBounce;
/// <summary>
/// selection has been changed as the result of interaction with the carousel.
/// </summary>
private void selectionChanged(BeatmapGroup group, BeatmapInfo beatmap)
private void selectionChanged(BeatmapInfo beatmap)
{
bool beatmapSetChange = false;
if (!beatmap.Equals(Beatmap?.BeatmapInfo))
{
if (beatmap.BeatmapSetInfoID == Beatmap?.BeatmapInfo.BeatmapSetInfoID)
if (beatmap.BeatmapSetInfoID == selectionChangeNoBounce?.BeatmapSetInfoID)
sampleChangeDifficulty.Play();
else
{
sampleChangeBeatmap.Play();
beatmapSetChange = true;
}
Beatmap = database.GetWorkingBeatmap(beatmap, Beatmap);
}
ensurePlayingSelected(beatmapSetChange);
selectionChangeNoBounce = beatmap;
selectionChangedDebounce?.Cancel();
selectionChangedDebounce = Scheduler.AddDelayed(delegate
{
Beatmap = database.GetWorkingBeatmap(beatmap, Beatmap);
ensurePlayingSelected(beatmapSetChange);
}, 100);
}
private void ensurePlayingSelected(bool preview = false)
@ -331,11 +348,6 @@ namespace osu.Game.Screens.Select
}
}
private void selectBeatmap(BeatmapSetInfo beatmapSet = null)
{
carousel.SelectBeatmap(beatmapSet != null ? beatmapSet.Beatmaps.First() : Beatmap?.BeatmapInfo);
}
private void removeBeatmapSet(BeatmapSetInfo beatmapSet)
{
carousel.RemoveBeatmap(beatmapSet);

View File

@ -106,7 +106,7 @@ namespace osu.Game.Screens.Tournament
speedTo(0f, 2000);
tracker.FadeIn(200);
delayedStateChangeDelegate = Delay(2300).Schedule(() => scrollState = ScrollState.Stopped);
delayedStateChangeDelegate = Scheduler.AddDelayed(() => scrollState = ScrollState.Stopped, 2300);
break;
case ScrollState.Stopped:
// Find closest to center
@ -144,7 +144,7 @@ namespace osu.Game.Screens.Tournament
st.Selected = true;
OnSelected?.Invoke(st.Team);
delayedStateChangeDelegate = Delay(10000).Schedule(() => scrollState = ScrollState.Idle);
delayedStateChangeDelegate = Scheduler.AddDelayed(() => scrollState = ScrollState.Idle, 10000);
break;
case ScrollState.Idle:
resetSelected();
@ -295,11 +295,7 @@ namespace osu.Game.Screens.Tournament
}
}
private void speedTo(float value, double duration = 0, EasingTypes easing = EasingTypes.None)
{
DelayReset();
TransformTo(() => speed, value, duration, easing, new TransformScrollSpeed());
}
private void speedTo(float value, double duration = 0, EasingTypes easing = EasingTypes.None) => TransformTo(() => speed, value, duration, easing, new TransformScrollSpeed());
private enum ScrollState
{

View File

@ -71,9 +71,13 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Audio\SampleInfo.cs" />
<Compile Include="Audio\SampleInfoList.cs" />
<Compile Include="Beatmaps\Drawables\BeatmapBackgroundSprite.cs" />
<Compile Include="Beatmaps\DifficultyCalculator.cs" />
<Compile Include="Online\API\Requests\PostMessageRequest.cs" />
<Compile Include="Overlays\Music\FilterControl.cs" />
<Compile Include="Overlays\Music\PlaylistItem.cs" />
<Compile Include="Overlays\Music\PlaylistList.cs" />
<Compile Include="Overlays\Toolbar\ToolbarChatButton.cs" />
<Compile Include="Rulesets\Beatmaps\BeatmapConverter.cs" />
<Compile Include="Rulesets\Beatmaps\BeatmapProcessor.cs" />
@ -420,6 +424,9 @@
<Compile Include="Screens\Select\BeatmapDetailArea.cs" />
<Compile Include="Graphics\UserInterface\OsuTabControlCheckbox.cs" />
<Compile Include="Screens\Select\BeatmapDetailAreaTabControl.cs" />
<Compile Include="Overlays\Music\PlaylistOverlay.cs" />
<Compile Include="Rulesets\Replays\IAutoGenerator.cs" />
<Compile Include="Rulesets\Replays\AutoGenerator.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)\osu-framework\osu.Framework\osu.Framework.csproj">