1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 18:07:24 +08:00

Merge branch 'master' into direct-stats-offset

This commit is contained in:
Dan Balasescu 2019-03-12 16:17:36 +09:00 committed by GitHub
commit e6e0cf1957
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 452 additions and 274 deletions

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
[TestCase(4.2038001515546597d, "diffcalc-test")]
[TestCase(4.2058561036909863d, "diffcalc-test")]
public void Test(double expected, string name)
=> base.Test(expected, name);

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Audio;
@ -25,6 +24,11 @@ namespace osu.Game.Rulesets.Catch.Objects
public double Velocity;
public double TickDistance;
/// <summary>
/// The length of one span of this <see cref="JuiceStream"/>.
/// </summary>
public double SpanDuration => Duration / this.SpanCount();
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
@ -41,19 +45,6 @@ namespace osu.Game.Rulesets.Catch.Objects
protected override void CreateNestedHitObjects()
{
base.CreateNestedHitObjects();
createTicks();
}
private void createTicks()
{
if (TickDistance == 0)
return;
var length = Path.Distance;
var tickDistance = Math.Min(TickDistance, length);
var spanDuration = length / Velocity;
var minDistanceFromEnd = Velocity * 0.01;
var tickSamples = Samples.Select(s => new SampleInfo
{
@ -62,81 +53,59 @@ namespace osu.Game.Rulesets.Catch.Objects
Volume = s.Volume
}).ToList();
AddNested(new Fruit
SliderEventDescriptor? lastEvent = null;
foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset))
{
Samples = Samples,
StartTime = StartTime,
X = X
});
double lastTickTime = StartTime;
for (int span = 0; span < this.SpanCount(); span++)
{
var spanStartTime = StartTime + span * spanDuration;
var reversed = span % 2 == 1;
for (double d = tickDistance;; d += tickDistance)
// generate tiny droplets since the last point
if (lastEvent != null)
{
bool isLastTick = false;
if (d + minDistanceFromEnd >= length)
double sinceLastTick = e.Time - lastEvent.Value.Time;
if (sinceLastTick > 80)
{
d = length;
isLastTick = true;
}
double timeBetweenTiny = sinceLastTick;
while (timeBetweenTiny > 100)
timeBetweenTiny /= 2;
var timeProgress = d / length;
var distanceProgress = reversed ? 1 - timeProgress : timeProgress;
double time = spanStartTime + timeProgress * spanDuration;
if (LegacyLastTickOffset != null)
{
// If we're the last tick, apply the legacy offset
if (span == this.SpanCount() - 1 && isLastTick)
time = Math.Max(StartTime + Duration / 2, time - LegacyLastTickOffset.Value);
}
int tinyTickCount = 1;
double tinyTickInterval = time - lastTickTime;
while (tinyTickInterval > 100 && tinyTickCount < 10000)
{
tinyTickInterval /= 2;
tinyTickCount *= 2;
}
for (int tinyTickIndex = 0; tinyTickIndex < tinyTickCount - 1; tinyTickIndex++)
{
var t = lastTickTime + (tinyTickIndex + 1) * tinyTickInterval;
double progress = reversed ? 1 - (t - spanStartTime) / spanDuration : (t - spanStartTime) / spanDuration;
AddNested(new TinyDroplet
for (double t = timeBetweenTiny; t < sinceLastTick; t += timeBetweenTiny)
{
StartTime = t,
X = X + Path.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH,
Samples = tickSamples
});
AddNested(new TinyDroplet
{
Samples = tickSamples,
StartTime = t + lastEvent.Value.Time,
X = X + Path.PositionAt(
lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X / CatchPlayfield.BASE_WIDTH,
});
}
}
lastTickTime = time;
if (isLastTick)
break;
AddNested(new Droplet
{
StartTime = time,
X = X + Path.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH,
Samples = tickSamples
});
}
AddNested(new Fruit
// this also includes LegacyLastTick and this is used for TinyDroplet generation above.
// this means that the final segment of TinyDroplets are increasingly mistimed where LegacyLastTickOffset is being applied.
lastEvent = e;
switch (e.Type)
{
Samples = Samples,
StartTime = spanStartTime + spanDuration,
X = X + Path.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH
});
case SliderEventType.Tick:
AddNested(new Droplet
{
Samples = tickSamples,
StartTime = e.Time,
X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH,
});
break;
case SliderEventType.Head:
case SliderEventType.Tail:
case SliderEventType.Repeat:
AddNested(new Fruit
{
Samples = Samples,
StartTime = e.Time,
X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH,
});
break;
}
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class TestCaseOsuPlayer : Game.Tests.Visual.TestCasePlayer
{
public TestCaseOsuPlayer()
: base(new OsuRuleset())
{
}
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osuTK;
using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic;
@ -155,116 +154,76 @@ namespace osu.Game.Rulesets.Osu.Objects
{
base.CreateNestedHitObjects();
createSliderEnds();
createTicks();
createRepeatPoints();
if (LegacyLastTickOffset != null)
TailCircle.StartTime = Math.Max(StartTime + Duration / 2, TailCircle.StartTime - LegacyLastTickOffset.Value);
}
private void createSliderEnds()
{
HeadCircle = new SliderCircle
foreach (var e in
SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset))
{
StartTime = StartTime,
Position = Position,
Samples = getNodeSamples(0),
SampleControlPoint = SampleControlPoint,
IndexInCurrentCombo = IndexInCurrentCombo,
ComboIndex = ComboIndex,
};
var firstSample = Samples.Find(s => s.Name == SampleInfo.HIT_NORMAL)
?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
var sampleList = new List<SampleInfo>();
TailCircle = new SliderTailCircle(this)
{
StartTime = EndTime,
Position = EndPosition,
IndexInCurrentCombo = IndexInCurrentCombo,
ComboIndex = ComboIndex,
};
AddNested(HeadCircle);
AddNested(TailCircle);
}
private void createTicks()
{
// A very lenient maximum length of a slider for ticks to be generated.
// This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage.
const double max_length = 100000;
var length = Math.Min(max_length, Path.Distance);
var tickDistance = MathHelper.Clamp(TickDistance, 0, length);
if (tickDistance == 0) return;
var minDistanceFromEnd = Velocity * 10;
var spanCount = this.SpanCount();
for (var span = 0; span < spanCount; span++)
{
var spanStartTime = StartTime + span * SpanDuration;
var reversed = span % 2 == 1;
for (var d = tickDistance; d <= length; d += tickDistance)
{
if (d > length - minDistanceFromEnd)
break;
var distanceProgress = d / length;
var timeProgress = reversed ? 1 - distanceProgress : distanceProgress;
var firstSample = Samples.Find(s => s.Name == SampleInfo.HIT_NORMAL)
?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
var sampleList = new List<SampleInfo>();
if (firstSample != null)
sampleList.Add(new SampleInfo
{
Bank = firstSample.Bank,
Volume = firstSample.Volume,
Name = @"slidertick",
});
AddNested(new SliderTick
if (firstSample != null)
sampleList.Add(new SampleInfo
{
SpanIndex = span,
SpanStartTime = spanStartTime,
StartTime = spanStartTime + timeProgress * SpanDuration,
Position = Position + Path.PositionAt(distanceProgress),
StackHeight = StackHeight,
Scale = Scale,
Samples = sampleList
Bank = firstSample.Bank,
Volume = firstSample.Volume,
Name = @"slidertick",
});
switch (e.Type)
{
case SliderEventType.Tick:
AddNested(new SliderTick
{
SpanIndex = e.SpanIndex,
SpanStartTime = e.SpanStartTime,
StartTime = e.Time,
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
Scale = Scale,
Samples = sampleList
});
break;
case SliderEventType.Head:
AddNested(HeadCircle = new SliderCircle
{
StartTime = e.Time,
Position = Position,
Samples = getNodeSamples(0),
SampleControlPoint = SampleControlPoint,
IndexInCurrentCombo = IndexInCurrentCombo,
ComboIndex = ComboIndex,
});
break;
case SliderEventType.LegacyLastTick:
// we need to use the LegacyLastTick here for compatibility reasons (difficulty).
// it is *okay* to use this because the TailCircle is not used for any meaningful purpose in gameplay.
// if this is to change, we should revisit this.
AddNested(TailCircle = new SliderTailCircle(this)
{
StartTime = e.Time,
Position = EndPosition,
IndexInCurrentCombo = IndexInCurrentCombo,
ComboIndex = ComboIndex,
});
break;
case SliderEventType.Repeat:
AddNested(new RepeatPoint
{
RepeatIndex = e.SpanIndex,
SpanDuration = SpanDuration,
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
Scale = Scale,
Samples = getNodeSamples(e.SpanIndex + 1)
});
break;
}
}
}
private void createRepeatPoints()
{
for (int repeatIndex = 0, repeat = 1; repeatIndex < RepeatCount; repeatIndex++, repeat++)
{
AddNested(new RepeatPoint
{
RepeatIndex = repeatIndex,
SpanDuration = SpanDuration,
StartTime = StartTime + repeat * SpanDuration,
Position = Position + Path.PositionAt(repeat % 2),
StackHeight = StackHeight,
Scale = Scale,
Samples = getNodeSamples(1 + repeatIndex)
});
}
}
private List<SampleInfo> getNodeSamples(int nodeIndex)
{
if (nodeIndex < NodeSamples.Count)
return NodeSamples[nodeIndex];
return Samples;
}
private List<SampleInfo> getNodeSamples(int nodeIndex) =>
nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;
public override Judgement CreateJudgement() => new OsuJudgement();
}

View File

@ -8,6 +8,10 @@ using osu.Game.Rulesets.Osu.Judgements;
namespace osu.Game.Rulesets.Osu.Objects
{
/// <summary>
/// Note that this should not be used for timing correctness.
/// See <see cref="SliderEventType.LegacyLastTick"/> usage in <see cref="Slider"/> for more information.
/// </summary>
public class SliderTailCircle : SliderCircle
{
private readonly IBindable<SliderPath> pathBindable = new Bindable<SliderPath>();

View File

@ -207,6 +207,41 @@ namespace osu.Game.Tests.Beatmaps.IO
}
}
[TestCase(true)]
[TestCase(false)]
public void TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set)
{
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"TestImportThenDeleteThenImport-{set}"))
{
try
{
var osu = loadOsu(host);
var imported = LoadOszIntoOsu(osu);
if (set)
imported.OnlineBeatmapSetID = 1234;
else
imported.Beatmaps.First().OnlineBeatmapID = 1234;
osu.Dependencies.Get<BeatmapManager>().Update(imported);
deleteBeatmapSet(imported, osu);
var importedSecondTime = LoadOszIntoOsu(osu);
// check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched)
Assert.IsTrue(imported.ID != importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID);
}
finally
{
host.Exit();
}
}
}
[Test]
[NonParallelizable]
[Ignore("Binding IPC on Appveyor isn't working (port in use). Need to figure out why")]

View File

@ -102,11 +102,14 @@ namespace osu.Game.Beatmaps
b.BeatmapSet = beatmapSet;
}
validateOnlineIds(beatmapSet.Beatmaps);
validateOnlineIds(beatmapSet);
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
fetchAndPopulateOnlineValues(b, beatmapSet.Beatmaps);
}
protected override void PreImport(BeatmapSetInfo beatmapSet)
{
// check if a set already exists with the same online id, delete if it does.
if (beatmapSet.OnlineBeatmapSetID != null)
{
@ -120,14 +123,30 @@ namespace osu.Game.Beatmaps
}
}
private void validateOnlineIds(List<BeatmapInfo> beatmaps)
private void validateOnlineIds(BeatmapSetInfo beatmapSet)
{
var beatmapIds = beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList();
var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList();
// ensure all IDs are unique in this set and none match existing IDs in the local beatmap store.
if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1) || QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).Any())
// remove all online IDs if any problems were found.
beatmaps.ForEach(b => b.OnlineBeatmapID = null);
// ensure all IDs are unique
if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1))
{
resetIds();
return;
}
// find any existing beatmaps in the database that have matching online ids
var existingBeatmaps = QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).ToList();
if (existingBeatmaps.Count > 0)
{
// reset the import ids (to force a re-fetch) *unless* they match the candidate CheckForExisting set.
// we can ignore the case where the new ids are contained by the CheckForExisting set as it will either be used (import skipped) or deleted.
var existing = CheckForExisting(beatmapSet);
if (existing == null || existingBeatmaps.Any(b => !existing.Beatmaps.Contains(b)))
resetIds();
}
void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null);
}
/// <summary>
@ -254,6 +273,18 @@ namespace osu.Game.Beatmaps
/// <returns>The first result for the provided query, or null if no results were found.</returns>
public BeatmapSetInfo QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query);
protected override bool CanUndelete(BeatmapSetInfo existing, BeatmapSetInfo import)
{
if (!base.CanUndelete(existing, import))
return false;
var existingIds = existing.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i);
var importIds = import.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i);
// force re-import if we are not in a sane state.
return existing.OnlineBeatmapSetID == import.OnlineBeatmapSetID && existingIds.SequenceEqual(importIds);
}
/// <summary>
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
/// </summary>

View File

@ -45,24 +45,7 @@ namespace osu.Game.Beatmaps.Drawables
protected override Drawable CreateDrawable(BeatmapInfo model)
{
Drawable drawable;
var localBeatmap = beatmaps.GetWorkingBeatmap(model);
if (model?.BeatmapSet?.OnlineInfo != null)
{
drawable = new BeatmapSetCover(model.BeatmapSet, beatmapSetCoverType);
}
else if (localBeatmap.BeatmapInfo.ID != 0)
{
// Fall back to local background if one exists
drawable = new BeatmapBackgroundSprite(localBeatmap);
}
else
{
// Use the default background if somehow an online set does not exist and we don't have a local copy.
drawable = new BeatmapBackgroundSprite(beatmaps.DefaultBeatmap);
}
Drawable drawable = getDrawableForModel(model);
drawable.RelativeSizeAxes = Axes.Both;
drawable.Anchor = Anchor.Centre;
@ -72,5 +55,16 @@ namespace osu.Game.Beatmaps.Drawables
return drawable;
}
private Drawable getDrawableForModel(BeatmapInfo model)
{
// prefer online cover where available.
if (model?.BeatmapSet?.OnlineInfo != null)
return new BeatmapSetCover(model.BeatmapSet, beatmapSetCoverType);
return model?.ID > 0
? new BeatmapBackgroundSprite(beatmaps.GetWorkingBeatmap(model))
: new BeatmapBackgroundSprite(beatmaps.DefaultBeatmap);
}
}
}

View File

@ -300,21 +300,31 @@ namespace osu.Game.Database
{
if (!write.IsTransactionLeader) throw new InvalidOperationException($"Ensure there is no parent transaction so errors can correctly be handled by {this}");
var existing = CheckForExisting(item);
if (existing != null)
{
Undelete(existing);
Logger.Log($"Found existing {typeof(TModel)} for {item} (ID {existing.ID}). Skipping import.", LoggingTarget.Database);
handleEvent(() => ItemAdded?.Invoke(existing, true));
return existing;
}
if (archive != null)
item.Files = createFileInfos(archive, Files);
Populate(item, archive);
var existing = CheckForExisting(item);
if (existing != null)
{
if (CanUndelete(existing, item))
{
Undelete(existing);
Logger.Log($"Found existing {typeof(TModel)} for {item} (ID {existing.ID}). Skipping import.", LoggingTarget.Database);
handleEvent(() => ItemAdded?.Invoke(existing, true));
return existing;
}
else
{
Delete(existing);
ModelStore.PurgeDeletable(s => s.ID == existing.ID);
}
}
PreImport(item);
// import to store
ModelStore.Add(item);
}
@ -542,12 +552,29 @@ namespace osu.Game.Database
{
}
/// <summary>
/// Perform any final actions before the import to database executes.
/// </summary>
/// <param name="model">The model prepared for import.</param>
protected virtual void PreImport(TModel model)
{
}
/// <summary>
/// Check whether an existing model already exists for a new import item.
/// </summary>
/// <param name="model">The new model proposed for import. Note that <see cref="Populate"/> has not yet been run on this model.</param>
/// <param name="model">The new model proposed for import.
/// <returns>An existing model which matches the criteria to skip importing, else null.</returns>
protected virtual TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash);
protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash);
/// <summary>
/// After an existing <see cref="TModel"/> is found during an import process, the default behaviour is to restore the existing
/// item and skip the import. This method allows changing that behaviour.
/// </summary>
/// <param name="existing">The existing model.</param>
/// <param name="import">The newly imported model.</param>
/// <returns>Whether the existing model should be restored and used. Returning false will delete the existing a force a re-import.</returns>
protected virtual bool CanUndelete(TModel existing, TModel import) => true;
private DbSet<TModel> queryModel() => ContextFactory.Get().Set<TModel>();

View File

@ -320,6 +320,8 @@ namespace osu.Game.Overlays
this.MoveToY(Height, transition_length, Easing.InSine);
this.FadeOut(transition_length, Easing.InSine);
channelSelectionOverlay.State = Visibility.Hidden;
textbox.HoldFocus = false;
base.PopOut();
}

View File

@ -134,9 +134,9 @@ namespace osu.Game.Overlays
Filter.Tabs.Current.Value = DirectSortCriteria.Ranked;
}
};
((FilterControl)Filter).Ruleset.ValueChanged += _ => Scheduler.AddOnce(updateSearch);
((FilterControl)Filter).Ruleset.ValueChanged += _ => queueUpdateSearch();
Filter.DisplayStyleControl.DisplayStyle.ValueChanged += style => recreatePanels(style.NewValue);
Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => Scheduler.AddOnce(updateSearch);
Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => queueUpdateSearch();
Header.Tabs.Current.ValueChanged += tab =>
{
@ -144,24 +144,11 @@ namespace osu.Game.Overlays
{
currentQuery.Value = string.Empty;
Filter.Tabs.Current.Value = (DirectSortCriteria)Header.Tabs.Current.Value;
Scheduler.AddOnce(updateSearch);
queueUpdateSearch();
}
};
currentQuery.ValueChanged += text =>
{
queryChangedDebounce?.Cancel();
if (string.IsNullOrEmpty(text.NewValue))
Scheduler.AddOnce(updateSearch);
else
{
BeatmapSets = null;
ResultAmounts = null;
queryChangedDebounce = Scheduler.AddDelayed(updateSearch, 500);
}
};
currentQuery.ValueChanged += text => queueUpdateSearch(!string.IsNullOrEmpty(text.NewValue));
currentQuery.BindTo(Filter.Search.Current);
@ -170,7 +157,7 @@ namespace osu.Game.Overlays
if (Header.Tabs.Current.Value != DirectTab.Search && tab.NewValue != (DirectSortCriteria)Header.Tabs.Current.Value)
Header.Tabs.Current.Value = DirectTab.Search;
Scheduler.AddOnce(updateSearch);
queueUpdateSearch();
};
updateResultCounts();
@ -242,37 +229,42 @@ namespace osu.Game.Overlays
// Queries are allowed to be run only on the first pop-in
if (getSetsRequest == null)
Scheduler.AddOnce(updateSearch);
queueUpdateSearch();
}
private SearchBeatmapSetsRequest getSetsRequest;
private readonly Bindable<string> currentQuery = new Bindable<string>();
private readonly Bindable<string> currentQuery = new Bindable<string>(string.Empty);
private ScheduledDelegate queryChangedDebounce;
private PreviewTrackManager previewTrackManager;
private void queueUpdateSearch(bool queryTextChanged = false)
{
BeatmapSets = null;
ResultAmounts = null;
getSetsRequest?.Cancel();
queryChangedDebounce?.Cancel();
queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100);
}
private void updateSearch()
{
queryChangedDebounce?.Cancel();
if (!IsLoaded)
return;
if (State == Visibility.Hidden)
return;
BeatmapSets = null;
ResultAmounts = null;
getSetsRequest?.Cancel();
if (api == null)
return;
previewTrackManager.StopAnyPlaying(this);
getSetsRequest = new SearchBeatmapSetsRequest(currentQuery.Value ?? string.Empty,
getSetsRequest = new SearchBeatmapSetsRequest(
currentQuery.Value,
((FilterControl)Filter).Ruleset.Value,
Filter.DisplayStyleControl.Dropdown.Current.Value,
Filter.Tabs.Current.Value); //todo: sort direction (?)

View File

@ -0,0 +1,148 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osuTK;
namespace osu.Game.Rulesets.Objects
{
public static class SliderEventGenerator
{
public static IEnumerable<SliderEventDescriptor> Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, double? legacyLastTickOffset)
{
// A very lenient maximum length of a slider for ticks to be generated.
// This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage.
const double max_length = 100000;
var length = Math.Min(max_length, totalDistance);
tickDistance = MathHelper.Clamp(tickDistance, 0, length);
var minDistanceFromEnd = velocity * 10;
yield return new SliderEventDescriptor
{
Type = SliderEventType.Head,
SpanIndex = 0,
SpanStartTime = startTime,
Time = startTime,
PathProgress = 0,
};
if (tickDistance != 0)
{
for (var span = 0; span < spanCount; span++)
{
var spanStartTime = startTime + span * spanDuration;
var reversed = span % 2 == 1;
for (var d = tickDistance; d <= length; d += tickDistance)
{
if (d > length - minDistanceFromEnd)
break;
var pathProgress = d / length;
var timeProgress = reversed ? 1 - pathProgress : pathProgress;
yield return new SliderEventDescriptor
{
Type = SliderEventType.Tick,
SpanIndex = span,
SpanStartTime = spanStartTime,
Time = spanStartTime + timeProgress * spanDuration,
PathProgress = pathProgress,
};
}
if (span < spanCount - 1)
{
yield return new SliderEventDescriptor
{
Type = SliderEventType.Repeat,
SpanIndex = span,
SpanStartTime = startTime + span * spanDuration,
Time = spanStartTime + spanDuration,
PathProgress = (span + 1) % 2,
};
}
}
}
double totalDuration = spanCount * spanDuration;
// Okay, I'll level with you. I made a mistake. It was 2007.
// Times were simpler. osu! was but in its infancy and sliders were a new concept.
// A hack was made, which has unfortunately lived through until this day.
//
// This legacy tick is used for some calculations and judgements where audio output is not required.
// Generally we are keeping this around just for difficulty compatibility.
// Optimistically we do not want to ever use this for anything user-facing going forwards.
int finalSpanIndex = spanCount - 1;
double finalSpanStartTime = startTime + finalSpanIndex * spanDuration;
double finalSpanEndTime = Math.Max(startTime + totalDuration / 2, (finalSpanStartTime + spanDuration) - (legacyLastTickOffset ?? 0));
double finalProgress = (finalSpanEndTime - finalSpanStartTime) / spanDuration;
if (spanCount % 2 == 0) finalProgress = 1 - finalProgress;
yield return new SliderEventDescriptor
{
Type = SliderEventType.LegacyLastTick,
SpanIndex = finalSpanIndex,
SpanStartTime = finalSpanStartTime,
Time = finalSpanEndTime,
PathProgress = finalProgress,
};
yield return new SliderEventDescriptor
{
Type = SliderEventType.Tail,
SpanIndex = finalSpanIndex,
SpanStartTime = startTime + (spanCount - 1) * spanDuration,
Time = startTime + totalDuration,
PathProgress = spanCount % 2,
};
}
}
/// <summary>
/// Describes a point in time on a slider given special meaning.
/// Should be used by rulesets to visualise the slider.
/// </summary>
public struct SliderEventDescriptor
{
/// <summary>
/// The type of event.
/// </summary>
public SliderEventType Type;
/// <summary>
/// The time of this event.
/// </summary>
public double Time;
/// <summary>
/// The zero-based index of the span. In the case of repeat sliders, this will increase after each <see cref="SliderEventType.Repeat"/>.
/// </summary>
public int SpanIndex;
/// <summary>
/// The time at which the contained <see cref="SpanIndex"/> begins.
/// </summary>
public double SpanStartTime;
/// <summary>
/// The progress along the slider's <see cref="SliderPath"/> at which this event occurs.
/// </summary>
public double PathProgress;
}
public enum SliderEventType
{
Tick,
LegacyLastTick,
Head,
Tail,
Repeat
}
}

View File

@ -92,30 +92,6 @@ namespace osu.Game.Screens.Play.HUD
public Action HoverGained;
public Action HoverLost;
public bool OnPressed(GlobalAction action)
{
switch (action)
{
case GlobalAction.Back:
BeginConfirm();
return true;
}
return false;
}
public bool OnReleased(GlobalAction action)
{
switch (action)
{
case GlobalAction.Back:
AbortConfirm();
return true;
}
return false;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
@ -178,7 +154,7 @@ namespace osu.Game.Screens.Play.HUD
// avoid starting a new confirm call until we finish animating.
pendingAnimation = true;
Progress.Value = 0;
AbortConfirm();
overlayCircle.ScaleTo(0, 100)
.Then().FadeOut().ScaleTo(1).FadeIn(500)
@ -207,6 +183,31 @@ namespace osu.Game.Screens.Play.HUD
base.OnHoverLost(e);
}
public bool OnPressed(GlobalAction action)
{
switch (action)
{
case GlobalAction.Back:
if (!pendingAnimation)
BeginConfirm();
return true;
}
return false;
}
public bool OnReleased(GlobalAction action)
{
switch (action)
{
case GlobalAction.Back:
AbortConfirm();
return true;
}
return false;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (!pendingAnimation && e.CurrentState.Mouse.Buttons.Count() == 1)