Merge branch 'master' into fruit-piece-in-place

This commit is contained in:
Dan Balasescu
2020-12-08 17:42:34 +09:00
committed by GitHub
32 changed files with 541 additions and 433 deletions

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@ -12,8 +13,11 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests
@ -52,6 +56,53 @@ namespace osu.Game.Rulesets.Catch.Tests
};
});
[Test]
public void TestCatcherHyperStateReverted()
{
DrawableCatchHitObject drawableObject1 = null;
DrawableCatchHitObject drawableObject2 = null;
JudgementResult result1 = null;
JudgementResult result2 = null;
AddStep("catch hyper fruit", () =>
{
drawableObject1 = createDrawableObject(new Fruit { HyperDashTarget = new Fruit { X = 100 } });
result1 = attemptCatch(drawableObject1);
});
AddStep("catch normal fruit", () =>
{
drawableObject2 = createDrawableObject(new Fruit());
result2 = attemptCatch(drawableObject2);
});
AddStep("revert second result", () =>
{
catcher.OnRevertResult(drawableObject2, result2);
});
checkHyperDash(true);
AddStep("revert first result", () =>
{
catcher.OnRevertResult(drawableObject1, result1);
});
checkHyperDash(false);
}
[Test]
public void TestCatcherAnimationStateReverted()
{
DrawableCatchHitObject drawableObject = null;
JudgementResult result = null;
AddStep("catch kiai fruit", () =>
{
drawableObject = createDrawableObject(new TestKiaiFruit());
result = attemptCatch(drawableObject);
});
checkState(CatcherAnimationState.Kiai);
AddStep("revert result", () =>
{
catcher.OnRevertResult(drawableObject, result);
});
checkState(CatcherAnimationState.Idle);
}
[Test]
public void TestCatcherCatchWidth()
{
@ -166,10 +217,37 @@ namespace osu.Game.Rulesets.Catch.Tests
private void attemptCatch(CatchHitObject hitObject, int count = 1)
{
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
for (var i = 0; i < count; i++)
catcher.AttemptCatch(hitObject);
attemptCatch(createDrawableObject(hitObject));
}
private JudgementResult attemptCatch(DrawableCatchHitObject drawableObject)
{
drawableObject.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var result = new CatchJudgementResult(drawableObject.HitObject, drawableObject.HitObject.CreateJudgement())
{
Type = catcher.CanCatch(drawableObject.HitObject) ? HitResult.Great : HitResult.Miss
};
catcher.OnNewResult(drawableObject, result);
return result;
}
private DrawableCatchHitObject createDrawableObject(CatchHitObject hitObject)
{
switch (hitObject)
{
case Banana banana:
return new DrawableBanana(banana);
case Droplet droplet:
return new DrawableDroplet(droplet);
case Fruit fruit:
return new DrawableFruit(fruit);
default:
throw new ArgumentOutOfRangeException(nameof(hitObject));
}
}
public class TestCatcher : Catcher

View File

@ -15,7 +15,6 @@ using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Tests
@ -58,10 +57,9 @@ namespace osu.Game.Rulesets.Catch.Tests
Schedule(() =>
{
bool caught = area.AttemptCatch(fruit);
area.OnNewResult(drawable, new JudgementResult(fruit, new CatchJudgement())
area.OnNewResult(drawable, new CatchJudgementResult(fruit, new CatchJudgement())
{
Type = caught ? HitResult.Great : HitResult.Miss
Type = area.MovableCatcher.CanCatch(fruit) ? HitResult.Great : HitResult.Miss
});
drawable.Expire();

View File

@ -0,0 +1,28 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Catch.Judgements
{
public class CatchJudgementResult : JudgementResult
{
/// <summary>
/// The catcher animation state prior to this judgement.
/// </summary>
public CatcherAnimationState CatcherAnimationState;
/// <summary>
/// Whether the catcher was hyper dashing prior to this judgement.
/// </summary>
public bool CatcherHyperDash;
public CatchJudgementResult([NotNull] HitObject hitObject, [NotNull] Judgement judgement)
: base(hitObject, judgement)
{
}
}
}

View File

@ -5,7 +5,9 @@ using System;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Utils;
@ -52,6 +54,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public override bool RemoveWhenNotAlive => IsOnPlate;
protected override JudgementResult CreateResult(Judgement judgement) => new CatchJudgementResult(HitObject, judgement);
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (CheckPosition == null) return;

View File

@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Catch.UI
((DrawableCatchHitObject)d).CheckPosition = checkIfWeCanCatch;
}
private bool checkIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj);
private bool checkIfWeCanCatch(CatchHitObject obj) => CatcherArea.MovableCatcher.CanCatch(obj);
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
=> CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result);

View File

@ -14,9 +14,11 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Rulesets.Judgements;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@ -190,11 +192,9 @@ namespace osu.Game.Rulesets.Catch.UI
internal static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty));
/// <summary>
/// Let the catcher attempt to catch a fruit.
/// Determine if this catcher can catch a <see cref="CatchHitObject"/> in the current position.
/// </summary>
/// <param name="hitObject">The fruit to catch.</param>
/// <returns>Whether the catch is possible.</returns>
public bool AttemptCatch(CatchHitObject hitObject)
public bool CanCatch(CatchHitObject hitObject)
{
if (!(hitObject is PalpableCatchHitObject fruit))
return false;
@ -205,21 +205,29 @@ namespace osu.Game.Rulesets.Catch.UI
var catchObjectPosition = fruit.X;
var catcherPosition = Position.X;
var validCatch =
catchObjectPosition >= catcherPosition - halfCatchWidth &&
catchObjectPosition <= catcherPosition + halfCatchWidth;
return catchObjectPosition >= catcherPosition - halfCatchWidth &&
catchObjectPosition <= catcherPosition + halfCatchWidth;
}
if (validCatch)
placeCaughtObject(fruit);
public void OnNewResult(DrawableCatchHitObject drawableObject, JudgementResult result)
{
var catchResult = (CatchJudgementResult)result;
catchResult.CatcherAnimationState = CurrentState;
catchResult.CatcherHyperDash = HyperDashing;
if (!(drawableObject.HitObject is PalpableCatchHitObject hitObject)) return;
if (result.IsHit)
placeCaughtObject(hitObject);
// droplet doesn't affect the catcher state
if (fruit is TinyDroplet) return validCatch;
if (hitObject is TinyDroplet) return;
if (validCatch && fruit.HyperDash)
if (result.IsHit && hitObject.HyperDash)
{
var target = fruit.HyperDashTarget;
var timeDifference = target.StartTime - fruit.StartTime;
double positionDifference = target.X - catcherPosition;
var target = hitObject.HyperDashTarget;
var timeDifference = target.StartTime - hitObject.StartTime;
double positionDifference = target.X - X;
var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
SetHyperDashState(Math.Abs(velocity), target.X);
@ -227,12 +235,30 @@ namespace osu.Game.Rulesets.Catch.UI
else
SetHyperDashState();
if (validCatch)
updateState(fruit.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle);
else if (!(fruit is Banana))
if (result.IsHit)
updateState(hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle);
else if (!(hitObject is Banana))
updateState(CatcherAnimationState.Fail);
}
return validCatch;
public void OnRevertResult(DrawableCatchHitObject drawableObject, JudgementResult result)
{
var catchResult = (CatchJudgementResult)result;
if (CurrentState != catchResult.CatcherAnimationState)
updateState(catchResult.CatcherAnimationState);
if (HyperDashing != catchResult.CatcherHyperDash)
{
if (catchResult.CatcherHyperDash)
SetHyperDashState(2);
else
SetHyperDashState();
}
caughtFruitContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject);
droppedObjectTarget.RemoveAll(d => (d as DrawableCatchHitObject)?.HitObject == drawableObject.HitObject);
hitExplosionContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject);
}
/// <summary>
@ -464,6 +490,7 @@ namespace osu.Game.Rulesets.Catch.UI
if (!hitLighting.Value) return;
HitExplosion hitExplosion = hitExplosionPool.Get();
hitExplosion.HitObject = caughtObject.HitObject;
hitExplosion.X = caughtObject.X;
hitExplosion.Scale = new Vector2(caughtObject.HitObject.Scale);
hitExplosion.ObjectColour = caughtObject.AccentColour.Value;

View File

@ -5,7 +5,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Judgements;
@ -42,6 +41,8 @@ namespace osu.Game.Rulesets.Catch.UI
public void OnNewResult(DrawableCatchHitObject hitObject, JudgementResult result)
{
MovableCatcher.OnNewResult(hitObject, result);
if (!result.Type.IsScorable())
return;
@ -56,12 +57,10 @@ namespace osu.Game.Rulesets.Catch.UI
comboDisplay.OnNewResult(hitObject, result);
}
public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result)
=> comboDisplay.OnRevertResult(fruit, result);
public bool AttemptCatch(CatchHitObject obj)
public void OnRevertResult(DrawableCatchHitObject hitObject, JudgementResult result)
{
return MovableCatcher.AttemptCatch(obj);
comboDisplay.OnRevertResult(hitObject, result);
MovableCatcher.OnRevertResult(hitObject, result);
}
protected override void UpdateAfterChildren()

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects;
using osuTK;
using osuTK.Graphics;
@ -15,6 +16,7 @@ namespace osu.Game.Rulesets.Catch.UI
public class HitExplosion : PoolableDrawable
{
private Color4 objectColour;
public CatchHitObject HitObject;
public Color4 ObjectColour
{

View File

@ -99,16 +99,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (Attributes.MaxCombo > 0)
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
double approachRateFactor = 1.0;
double approachRateFactor = 0.0;
if (Attributes.ApproachRate > 10.33)
approachRateFactor += 0.3 * (Attributes.ApproachRate - 10.33);
approachRateFactor += 0.4 * (Attributes.ApproachRate - 10.33);
else if (Attributes.ApproachRate < 8.0)
{
approachRateFactor += 0.01 * (8.0 - Attributes.ApproachRate);
}
approachRateFactor += 0.1 * (8.0 - Attributes.ApproachRate);
aimValue *= approachRateFactor;
aimValue *= 1.0 + Math.Min(approachRateFactor, approachRateFactor * (totalHits / 1000.0));
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
if (mods.Any(h => h is OsuModHidden))
@ -137,8 +134,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedStrain / 0.0675) - 4.0, 3.0) / 100000.0;
// Longer maps are worth more
speedValue *= 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
speedValue *= lengthBonus;
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
speedValue *= Math.Pow(0.97, countMiss);
@ -147,11 +145,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (Attributes.MaxCombo > 0)
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
double approachRateFactor = 1.0;
double approachRateFactor = 0.0;
if (Attributes.ApproachRate > 10.33)
approachRateFactor += 0.3 * (Attributes.ApproachRate - 10.33);
approachRateFactor += 0.4 * (Attributes.ApproachRate - 10.33);
speedValue *= approachRateFactor;
speedValue *= 1.0 + Math.Min(approachRateFactor, approachRateFactor * (totalHits / 1000.0));
if (mods.Any(m => m is OsuModHidden))
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);

View File

@ -14,6 +14,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
@ -68,6 +69,42 @@ namespace osu.Game.Tests.Beatmaps.IO
}
}
[Test]
public async Task TestImportThenDeleteFromStream()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = LoadOsuIntoHost(host);
var tempPath = TestResources.GetTestBeatmapForImport();
var manager = osu.Dependencies.Get<BeatmapManager>();
BeatmapSetInfo importedSet;
using (var stream = File.OpenRead(tempPath))
{
importedSet = await manager.Import(new ImportTask(stream, Path.GetFileName(tempPath)));
ensureLoaded(osu);
}
Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing");
File.Delete(tempPath);
var imported = manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID);
deleteBeatmapSet(imported, osu);
}
finally
{
host.Exit();
}
}
}
[Test]
public async Task TestImportThenImport()
{
@ -127,7 +164,7 @@ namespace osu.Game.Tests.Beatmaps.IO
// zip files differ because different compression or encoder.
Assert.AreNotEqual(hashBefore, hashFile(temp));
var importedSecondTime = await osu.Dependencies.Get<BeatmapManager>().Import(temp);
var importedSecondTime = await osu.Dependencies.Get<BeatmapManager>().Import(new ImportTask(temp));
ensureLoaded(osu);
@ -184,7 +221,7 @@ namespace osu.Game.Tests.Beatmaps.IO
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
}
var importedSecondTime = await osu.Dependencies.Get<BeatmapManager>().Import(temp);
var importedSecondTime = await osu.Dependencies.Get<BeatmapManager>().Import(new ImportTask(temp));
ensureLoaded(osu);
@ -235,7 +272,7 @@ namespace osu.Game.Tests.Beatmaps.IO
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
}
var importedSecondTime = await osu.Dependencies.Get<BeatmapManager>().Import(temp);
var importedSecondTime = await osu.Dependencies.Get<BeatmapManager>().Import(new ImportTask(temp));
ensureLoaded(osu);
@ -351,7 +388,7 @@ namespace osu.Game.Tests.Beatmaps.IO
// this will trigger purging of the existing beatmap (online set id match) but should rollback due to broken osu.
try
{
await manager.Import(breakTemp);
await manager.Import(new ImportTask(breakTemp));
}
catch
{
@ -614,7 +651,7 @@ namespace osu.Game.Tests.Beatmaps.IO
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
}
var imported = await osu.Dependencies.Get<BeatmapManager>().Import(temp);
var imported = await osu.Dependencies.Get<BeatmapManager>().Import(new ImportTask(temp));
ensureLoaded(osu);
@ -667,7 +704,7 @@ namespace osu.Game.Tests.Beatmaps.IO
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
}
var imported = await osu.Dependencies.Get<BeatmapManager>().Import(temp);
var imported = await osu.Dependencies.Get<BeatmapManager>().Import(new ImportTask(temp));
ensureLoaded(osu);
@ -821,7 +858,7 @@ namespace osu.Game.Tests.Beatmaps.IO
var manager = osu.Dependencies.Get<BeatmapManager>();
var importedSet = await manager.Import(temp);
var importedSet = await manager.Import(new ImportTask(temp));
ensureLoaded(osu);

View File

@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Tests.Resources;
@ -52,7 +53,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("import beatmap with track", () =>
{
var setWithTrack = Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).Result;
var setWithTrack = Game.BeatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).Result;
Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(setWithTrack.Beatmaps.First());
});

View File

@ -6,14 +6,17 @@ using osu.Game.Screens.Multi.Lounge.Components;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneLoungeFilterControl : OsuTestScene
public class TestSceneTimeshiftFilterControl : OsuTestScene
{
public TestSceneLoungeFilterControl()
public TestSceneTimeshiftFilterControl()
{
Child = new FilterControl
Child = new TimeshiftFilterControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Width = 0.7f,
Height = 80,
};
}
}

View File

@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Online
ensureSoleilyRemoved();
createButtonWithBeatmap(createSoleily());
AddAssert("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded);
AddStep("import soleily", () => beatmaps.Import(new[] { TestResources.GetTestBeatmapForImport() }));
AddStep("import soleily", () => beatmaps.Import(TestResources.GetTestBeatmapForImport()));
AddUntilStep("wait for beatmap import", () => beatmaps.GetAllUsableBeatmapSets().Any(b => b.OnlineBeatmapSetID == 241526));
createButtonWithBeatmap(createSoleily());
AddAssert("button state downloaded", () => downloadButton.DownloadState == DownloadState.LocallyAvailable);

View File

@ -12,6 +12,7 @@ using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Leaderboards;
@ -83,7 +84,7 @@ namespace osu.Game.Tests.Visual.UserInterface
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory));
beatmap = beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Result.Beatmaps[0];
beatmap = beatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).Result.Beatmaps[0];
for (int i = 0; i < 50; i++)
{

View File

@ -21,9 +21,7 @@ using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.IPC;
using osu.Game.Overlays.Notifications;
using osu.Game.Utils;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using FileInfo = osu.Game.IO.FileInfo;
namespace osu.Game.Database
@ -114,10 +112,19 @@ namespace osu.Game.Database
PostNotification?.Invoke(notification);
return Import(notification, paths);
return Import(notification, paths.Select(p => new ImportTask(p)).ToArray());
}
protected async Task<IEnumerable<TModel>> Import(ProgressNotification notification, params string[] paths)
public Task Import(Stream stream, string filename)
{
var notification = new ProgressNotification { State = ProgressNotificationState.Active };
PostNotification?.Invoke(notification);
return Import(notification, new ImportTask(stream, filename));
}
protected async Task<IEnumerable<TModel>> Import(ProgressNotification notification, params ImportTask[] tasks)
{
notification.Progress = 0;
notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising...";
@ -126,13 +133,13 @@ namespace osu.Game.Database
var imported = new List<TModel>();
await Task.WhenAll(paths.Select(async path =>
await Task.WhenAll(tasks.Select(async task =>
{
notification.CancellationToken.ThrowIfCancellationRequested();
try
{
var model = await Import(path, notification.CancellationToken);
var model = await Import(task, notification.CancellationToken);
lock (imported)
{
@ -140,8 +147,8 @@ namespace osu.Game.Database
imported.Add(model);
current++;
notification.Text = $"Imported {current} of {paths.Length} {HumanisedModelName}s";
notification.Progress = (float)current / paths.Length;
notification.Text = $"Imported {current} of {tasks.Length} {HumanisedModelName}s";
notification.Progress = (float)current / tasks.Length;
}
}
catch (TaskCanceledException)
@ -150,7 +157,7 @@ namespace osu.Game.Database
}
catch (Exception e)
{
Logger.Error(e, $@"Could not import ({Path.GetFileName(path)})", LoggingTarget.Database);
Logger.Error(e, $@"Could not import ({task})", LoggingTarget.Database);
}
}));
@ -183,16 +190,17 @@ namespace osu.Game.Database
/// <summary>
/// Import one <typeparamref name="TModel"/> from the filesystem and delete the file on success.
/// Note that this bypasses the UI flow and should only be used for special cases or testing.
/// </summary>
/// <param name="path">The archive location on disk.</param>
/// <param name="task">The <see cref="ImportTask"/> containing data about the <typeparamref name="TModel"/> to import.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>The imported model, if successful.</returns>
public async Task<TModel> Import(string path, CancellationToken cancellationToken = default)
internal async Task<TModel> Import(ImportTask task, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
TModel import;
using (ArchiveReader reader = getReaderFrom(path))
using (ArchiveReader reader = task.GetReader())
import = await Import(reader, cancellationToken);
// We may or may not want to delete the file depending on where it is stored.
@ -201,12 +209,12 @@ namespace osu.Game.Database
// TODO: Add a check to prevent files from storage to be deleted.
try
{
if (import != null && File.Exists(path) && ShouldDeleteArchive(path))
File.Delete(path);
if (import != null && File.Exists(task.Path) && ShouldDeleteArchive(task.Path))
File.Delete(task.Path);
}
catch (Exception e)
{
LogForModel(import, $@"Could not delete original file after import ({Path.GetFileName(path)})", e);
LogForModel(import, $@"Could not delete original file after import ({task})", e);
}
return import;
@ -727,23 +735,6 @@ namespace osu.Game.Database
protected virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace("Info", "").ToLower()}";
/// <summary>
/// Creates an <see cref="ArchiveReader"/> from a valid storage path.
/// </summary>
/// <param name="path">A file or folder path resolving the archive content.</param>
/// <returns>A reader giving access to the archive's content.</returns>
private ArchiveReader getReaderFrom(string path)
{
if (ZipUtils.IsZipArchive(path))
return new ZipArchiveReader(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read), Path.GetFileName(path));
if (Directory.Exists(path))
return new LegacyDirectoryArchiveReader(path);
if (File.Exists(path))
return new LegacyFileArchiveReader(path);
throw new InvalidFormatException($"{path} is not a valid archive");
}
#region Event handling / delaying
private readonly List<Action> queuedEvents = new List<Action>();

View File

@ -82,7 +82,7 @@ namespace osu.Game.Database
Task.Factory.StartNew(async () =>
{
// This gets scheduled back to the update thread, but we want the import to run in the background.
var imported = await Import(notification, filename);
var imported = await Import(notification, new ImportTask(filename));
// for now a failed import will be marked as a failed download for simplicity.
if (!imported.Any())

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace osu.Game.Database
@ -17,6 +18,13 @@ namespace osu.Game.Database
/// <param name="paths">The files which should be imported.</param>
Task Import(params string[] paths);
/// <summary>
/// Import the provided stream as a simple item.
/// </summary>
/// <param name="stream">The stream to import files from. Should be in a supported archive format.</param>
/// <param name="filename">The filename of the archive being imported.</param>
Task Import(Stream stream, string filename);
/// <summary>
/// An array of accepted file extensions (in the standard format of ".abc").
/// </summary>

View File

@ -0,0 +1,75 @@
// 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.
#nullable enable
using System.IO;
using osu.Game.IO.Archives;
using osu.Game.Utils;
using SharpCompress.Common;
namespace osu.Game.Database
{
/// <summary>
/// An encapsulated import task to be imported to an <see cref="ArchiveModelManager{TModel,TFileModel}"/>.
/// </summary>
public class ImportTask
{
/// <summary>
/// The path to the file (or filename in the case a stream is provided).
/// </summary>
public string Path { get; }
/// <summary>
/// An optional stream which provides the file content.
/// </summary>
public Stream? Stream { get; }
/// <summary>
/// Construct a new import task from a path (on a local filesystem).
/// </summary>
public ImportTask(string path)
{
Path = path;
}
/// <summary>
/// Construct a new import task from a stream.
/// </summary>
public ImportTask(Stream stream, string filename)
{
Path = filename;
Stream = stream;
}
/// <summary>
/// Retrieve an archive reader from this task.
/// </summary>
public ArchiveReader GetReader()
{
if (Stream != null)
return new ZipArchiveReader(Stream, Path);
return getReaderFrom(Path);
}
/// <summary>
/// Creates an <see cref="ArchiveReader"/> from a valid storage path.
/// </summary>
/// <param name="path">A file or folder path resolving the archive content.</param>
/// <returns>A reader giving access to the archive's content.</returns>
private ArchiveReader getReaderFrom(string path)
{
if (ZipUtils.IsZipArchive(path))
return new ZipArchiveReader(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read), System.IO.Path.GetFileName(path));
if (Directory.Exists(path))
return new LegacyDirectoryArchiveReader(path);
if (File.Exists(path))
return new LegacyFileArchiveReader(path);
throw new InvalidFormatException($"{path} is not a valid archive");
}
public override string ToString() => System.IO.Path.GetFileName(Path);
}
}

View File

@ -2,14 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.SearchableList
namespace osu.Game.Graphics.UserInterface
{
public class SlimEnumDropdown<T> : OsuEnumDropdown<T>
where T : struct, Enum

View File

@ -11,24 +11,24 @@ namespace osu.Game.Online.Multiplayer
{
public class GetRoomsRequest : APIRequest<List<Room>>
{
private readonly RoomStatusFilter statusFilter;
private readonly RoomCategoryFilter categoryFilter;
private readonly RoomStatusFilter status;
private readonly string category;
public GetRoomsRequest(RoomStatusFilter statusFilter, RoomCategoryFilter categoryFilter)
public GetRoomsRequest(RoomStatusFilter status, string category)
{
this.statusFilter = statusFilter;
this.categoryFilter = categoryFilter;
this.status = status;
this.category = category;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
if (statusFilter != RoomStatusFilter.Open)
req.AddParameter("mode", statusFilter.ToString().Underscore().ToLowerInvariant());
if (status != RoomStatusFilter.Open)
req.AddParameter("mode", status.ToString().Underscore().ToLowerInvariant());
if (categoryFilter != RoomCategoryFilter.Any)
req.AddParameter("category", categoryFilter.ToString().Underscore().ToLowerInvariant());
if (!string.IsNullOrEmpty(category))
req.AddParameter("category", category);
return req;
}

View File

@ -395,6 +395,17 @@ namespace osu.Game
}
}
public async Task Import(Stream stream, string filename)
{
var extension = Path.GetExtension(filename)?.ToLowerInvariant();
foreach (var importer in fileImporters)
{
if (importer.HandledExtensions.Contains(extension))
await importer.Import(stream, Path.GetFileNameWithoutExtension(filename));
}
}
public IEnumerable<string> HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions);
protected override void Dispose(bool isDisposing)

View File

@ -1,84 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osuTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.SearchableList
{
public class DisplayStyleControl : CompositeDrawable
{
public readonly Bindable<PanelDisplayStyle> DisplayStyle = new Bindable<PanelDisplayStyle>();
public DisplayStyleControl()
{
AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5f, 0f),
Direction = FillDirection.Horizontal,
Children = new[]
{
new DisplayStyleToggleButton(FontAwesome.Solid.ThLarge, PanelDisplayStyle.Grid, DisplayStyle),
new DisplayStyleToggleButton(FontAwesome.Solid.ListUl, PanelDisplayStyle.List, DisplayStyle),
},
};
DisplayStyle.Value = PanelDisplayStyle.Grid;
}
private class DisplayStyleToggleButton : OsuClickableContainer
{
private readonly SpriteIcon icon;
private readonly PanelDisplayStyle style;
private readonly Bindable<PanelDisplayStyle> bindable;
public DisplayStyleToggleButton(IconUsage icon, PanelDisplayStyle style, Bindable<PanelDisplayStyle> bindable)
{
this.bindable = bindable;
this.style = style;
Size = new Vector2(25f);
Children = new Drawable[]
{
this.icon = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = icon,
Size = new Vector2(18),
Alpha = 0.5f,
},
};
bindable.ValueChanged += Bindable_ValueChanged;
Bindable_ValueChanged(new ValueChangedEvent<PanelDisplayStyle>(bindable.Value, bindable.Value));
Action = () => bindable.Value = this.style;
}
private void Bindable_ValueChanged(ValueChangedEvent<PanelDisplayStyle> e)
{
icon.FadeTo(e.NewValue == style ? 1.0f : 0.5f, 100);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
bindable.ValueChanged -= Bindable_ValueChanged;
}
}
}
public enum PanelDisplayStyle
{
Grid,
List,
}
}

View File

@ -1,29 +0,0 @@
// 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 osuTK.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.SearchableList
{
public class HeaderTabControl<T> : OsuTabControl<T>
{
protected override TabItem<T> CreateTabItem(T value) => new HeaderTabItem(value);
public HeaderTabControl()
{
Height = 26;
AccentColour = Color4.White;
}
private class HeaderTabItem : OsuTabItem
{
public HeaderTabItem(T value)
: base(value)
{
Text.Font = Text.Font.With(size: 16);
}
}
}
}

View File

@ -1,165 +0,0 @@
// 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.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Framework.Graphics.Shapes;
namespace osu.Game.Overlays.SearchableList
{
public abstract class SearchableListFilterControl<TTab, TCategory> : Container
where TTab : struct, Enum
where TCategory : struct, Enum
{
private const float padding = 10;
private readonly Drawable filterContainer;
private readonly Drawable rightFilterContainer;
private readonly Box tabStrip;
public readonly SearchTextBox Search;
public readonly PageTabControl<TTab> Tabs;
public readonly SlimEnumDropdown<TCategory> Dropdown;
public readonly DisplayStyleControl DisplayStyleControl;
protected abstract Color4 BackgroundColour { get; }
protected abstract TTab DefaultTab { get; }
protected abstract TCategory DefaultCategory { get; }
protected virtual Drawable CreateSupplementaryControls() => null;
/// <summary>
/// The amount of padding added to content (does not affect background or tab control strip).
/// </summary>
protected virtual float ContentHorizontalPadding => WaveOverlayContainer.WIDTH_PADDING;
protected SearchableListFilterControl()
{
RelativeSizeAxes = Axes.X;
var controls = CreateSupplementaryControls();
Container controlsContainer;
Children = new[]
{
filterContainer = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = BackgroundColour,
Alpha = 0.9f,
},
tabStrip = new Box
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = 1,
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
{
Top = padding,
Horizontal = ContentHorizontalPadding
},
Children = new Drawable[]
{
Search = new FilterSearchTextBox
{
RelativeSizeAxes = Axes.X,
},
controlsContainer = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = controls != null ? padding : 0 },
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Right = 225 },
Child = Tabs = new PageTabControl<TTab>
{
RelativeSizeAxes = Axes.X,
},
},
new Box // keep the tab strip part of autosize, but don't put it in the flow container
{
RelativeSizeAxes = Axes.X,
Height = 1,
Colour = Color4.White.Opacity(0),
},
},
},
},
},
rightFilterContainer = new FillFlowContainer
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
Dropdown = new SlimEnumDropdown<TCategory>
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.None,
Width = 160f,
},
DisplayStyleControl = new DisplayStyleControl
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
},
}
}
};
if (controls != null) controlsContainer.Children = new[] { controls };
Tabs.Current.Value = DefaultTab;
Tabs.Current.TriggerChange();
Dropdown.Current.Value = DefaultCategory;
Dropdown.Current.TriggerChange();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
tabStrip.Colour = colours.Yellow;
}
protected override void Update()
{
base.Update();
Height = filterContainer.Height;
rightFilterContainer.Margin = new MarginPadding { Top = filterContainer.Height - 30, Right = ContentHorizontalPadding };
}
private class FilterSearchTextBox : SearchTextBox
{
[BackgroundDependencyLoader]
private void load()
{
BackgroundUnfocused = OsuColour.Gray(0.06f);
BackgroundFocused = OsuColour.Gray(0.12f);
}
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -99,6 +100,8 @@ namespace osu.Game.Screens.Edit.Setup
return Task.CompletedTask;
}
Task ICanAcceptFiles.Import(Stream stream, string filename) => throw new NotImplementedException();
protected override void LoadComplete()
{
base.LoadComplete();

View File

@ -1,24 +1,23 @@
// 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.ComponentModel;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading;
using osu.Game.Overlays.SearchableList;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osuTK.Graphics;
namespace osu.Game.Screens.Multi.Lounge.Components
{
public class FilterControl : SearchableListFilterControl<RoomStatusFilter, RoomCategoryFilter>
public abstract class FilterControl : CompositeDrawable
{
protected override Color4 BackgroundColour => Color4.Black.Opacity(0.5f);
protected override RoomStatusFilter DefaultTab => RoomStatusFilter.Open;
protected override RoomCategoryFilter DefaultCategory => RoomCategoryFilter.Any;
protected override float ContentHorizontalPadding => base.ContentHorizontalPadding + OsuScreen.HORIZONTAL_OVERFLOW_PADDING;
protected const float VERTICAL_PADDING = 10;
protected const float HORIZONTAL_PADDING = 80;
[Resolved(CanBeNull = true)]
private Bindable<FilterCriteria> filter { get; set; }
@ -26,66 +25,109 @@ namespace osu.Game.Screens.Multi.Lounge.Components
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
public FilterControl()
private readonly Box tabStrip;
private readonly SearchTextBox search;
private readonly PageTabControl<RoomStatusFilter> tabs;
protected FilterControl()
{
DisplayStyleControl.Hide();
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.25f,
},
tabStrip = new Box
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = 1,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Top = VERTICAL_PADDING,
Horizontal = HORIZONTAL_PADDING
},
Children = new Drawable[]
{
search = new FilterSearchTextBox
{
RelativeSizeAxes = Axes.X,
},
tabs = new PageTabControl<RoomStatusFilter>
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
},
}
}
};
tabs.Current.Value = RoomStatusFilter.Open;
tabs.Current.TriggerChange();
}
[BackgroundDependencyLoader]
private void load()
private void load(OsuColour colours)
{
filter ??= new Bindable<FilterCriteria>();
tabStrip.Colour = colours.Yellow;
}
protected override void LoadComplete()
{
base.LoadComplete();
ruleset.BindValueChanged(_ => updateFilter());
Search.Current.BindValueChanged(_ => scheduleUpdateFilter());
Dropdown.Current.BindValueChanged(_ => updateFilter());
Tabs.Current.BindValueChanged(_ => updateFilter(), true);
search.Current.BindValueChanged(_ => updateFilterDebounced());
ruleset.BindValueChanged(_ => UpdateFilter());
tabs.Current.BindValueChanged(_ => UpdateFilter(), true);
}
private ScheduledDelegate scheduledFilterUpdate;
private void scheduleUpdateFilter()
private void updateFilterDebounced()
{
scheduledFilterUpdate?.Cancel();
scheduledFilterUpdate = Scheduler.AddDelayed(updateFilter, 200);
scheduledFilterUpdate = Scheduler.AddDelayed(UpdateFilter, 200);
}
private void updateFilter()
protected void UpdateFilter()
{
scheduledFilterUpdate?.Cancel();
if (filter == null)
return;
var criteria = CreateCriteria();
criteria.SearchString = search.Current.Value;
criteria.Status = tabs.Current.Value;
criteria.Ruleset = ruleset.Value;
filter.Value = new FilterCriteria
filter.Value = criteria;
}
protected virtual FilterCriteria CreateCriteria() => new FilterCriteria();
public bool HoldFocus
{
get => search.HoldFocus;
set => search.HoldFocus = value;
}
public void TakeFocus() => search.TakeFocus();
private class FilterSearchTextBox : SearchTextBox
{
[BackgroundDependencyLoader]
private void load()
{
SearchString = Search.Current.Value ?? string.Empty,
StatusFilter = Tabs.Current.Value,
RoomCategoryFilter = Dropdown.Current.Value,
Ruleset = ruleset.Value
};
BackgroundUnfocused = OsuColour.Gray(0.06f);
BackgroundFocused = OsuColour.Gray(0.12f);
}
}
}
public enum RoomStatusFilter
{
Open,
[Description("Recently Ended")]
Ended,
Participated,
Owned,
}
public enum RoomCategoryFilter
{
Any,
Normal,
Spotlight
}
}

View File

@ -8,8 +8,8 @@ namespace osu.Game.Screens.Multi.Lounge.Components
public class FilterCriteria
{
public string SearchString;
public RoomStatusFilter StatusFilter;
public RoomCategoryFilter RoomCategoryFilter;
public RoomStatusFilter Status;
public string Category;
public RulesetInfo Ruleset;
}
}

View File

@ -0,0 +1,17 @@
// 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.ComponentModel;
namespace osu.Game.Screens.Multi.Lounge.Components
{
public enum RoomStatusFilter
{
Open,
[Description("Recently Ended")]
Ended,
Participated,
Owned,
}
}

View File

@ -0,0 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Multi.Lounge.Components
{
public class TimeshiftFilterControl : FilterControl
{
private readonly Dropdown<TimeshiftCategory> dropdown;
public TimeshiftFilterControl()
{
AddInternal(dropdown = new SlimEnumDropdown<TimeshiftCategory>
{
Anchor = Anchor.BottomRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.None,
Width = 160,
X = -HORIZONTAL_PADDING,
Y = -30
});
}
protected override void LoadComplete()
{
base.LoadComplete();
dropdown.Current.BindValueChanged(_ => UpdateFilter());
}
protected override FilterCriteria CreateCriteria()
{
var criteria = base.CreateCriteria();
switch (dropdown.Current.Value)
{
case TimeshiftCategory.Normal:
criteria.Category = "normal";
break;
case TimeshiftCategory.Spotlight:
criteria.Category = "spotlight";
break;
}
return criteria;
}
private enum TimeshiftCategory
{
Any,
Normal,
Spotlight
}
}
}

View File

@ -48,7 +48,6 @@ namespace osu.Game.Screens.Multi.Lounge
InternalChildren = new Drawable[]
{
Filter = new FilterControl { Depth = -1 },
content = new Container
{
RelativeSizeAxes = Axes.Both,
@ -79,6 +78,11 @@ namespace osu.Game.Screens.Multi.Lounge
},
},
},
Filter = new TimeshiftFilterControl
{
RelativeSizeAxes = Axes.X,
Height = 80,
},
};
// scroll selected room into view on selection.
@ -112,7 +116,7 @@ namespace osu.Game.Screens.Multi.Lounge
protected override void OnFocus(FocusEvent e)
{
Filter.Search.TakeFocus();
Filter.TakeFocus();
}
public override void OnEntering(IScreen last)
@ -136,19 +140,19 @@ namespace osu.Game.Screens.Multi.Lounge
private void onReturning()
{
Filter.Search.HoldFocus = true;
Filter.HoldFocus = true;
}
public override bool OnExiting(IScreen next)
{
Filter.Search.HoldFocus = false;
Filter.HoldFocus = false;
return base.OnExiting(next);
}
public override void OnSuspending(IScreen next)
{
base.OnSuspending(next);
Filter.Search.HoldFocus = false;
Filter.HoldFocus = false;
}
private void joinRequested(Room room)

View File

@ -135,6 +135,7 @@ namespace osu.Game.Screens.Multi.Match.Components
{
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
LengthLimit = 100
},
},
new Section("Duration")

View File

@ -317,7 +317,7 @@ namespace osu.Game.Screens.Multi
var tcs = new TaskCompletionSource<bool>();
pollReq?.Cancel();
pollReq = new GetRoomsRequest(currentFilter.Value.StatusFilter, currentFilter.Value.RoomCategoryFilter);
pollReq = new GetRoomsRequest(currentFilter.Value.Status, currentFilter.Value.Category);
pollReq.Success += result =>
{