mirror of
https://github.com/osukey/osukey.git
synced 2025-08-04 23:24:04 +09:00
Merge branch 'master' into fruit-piece-in-place
This commit is contained in:
@ -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
|
||||
|
@ -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();
|
||||
|
28
osu.Game.Rulesets.Catch/Judgements/CatchJudgementResult.cs
Normal file
28
osu.Game.Rulesets.Catch/Judgements/CatchJudgementResult.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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());
|
||||
});
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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++)
|
||||
{
|
||||
|
@ -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>();
|
||||
|
@ -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())
|
||||
|
@ -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>
|
||||
|
75
osu.Game/Database/ImportTask.cs
Normal file
75
osu.Game/Database/ImportTask.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
17
osu.Game/Screens/Multi/Lounge/Components/RoomStatusFilter.cs
Normal file
17
osu.Game/Screens/Multi/Lounge/Components/RoomStatusFilter.cs
Normal 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,
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -135,6 +135,7 @@ namespace osu.Game.Screens.Multi.Match.Components
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
TabbableContentContainer = this,
|
||||
LengthLimit = 100
|
||||
},
|
||||
},
|
||||
new Section("Duration")
|
||||
|
@ -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 =>
|
||||
{
|
||||
|
Reference in New Issue
Block a user