mirror of
https://github.com/ppy/osu.git
synced 2025-01-13 19:04:06 +08:00
Merge branch 'master' into dropdown-refresh
This commit is contained in:
commit
d2115824fa
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1 +1,2 @@
|
|||||||
|
github: ppy
|
||||||
custom: https://osu.ppy.sh/home/support
|
custom: https://osu.ppy.sh/home/support
|
||||||
|
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@ -79,9 +79,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
|
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
|
||||||
# FIXME: Suppress warnings from templates project
|
# FIXME: Suppress warnings from templates project
|
||||||
dotnet codefilesanity | while read -r line; do
|
exit_code=0
|
||||||
echo "::warning::$line"
|
while read -r line; do
|
||||||
done
|
if [[ ! -z "$line" ]]; then
|
||||||
|
echo "::error::$line"
|
||||||
|
exit_code=1
|
||||||
|
fi
|
||||||
|
done <<< $(dotnet codefilesanity)
|
||||||
|
exit $exit_code
|
||||||
|
|
||||||
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
|
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
|
||||||
# - name: .NET Format (Dry Run)
|
# - name: .NET Format (Dry Run)
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1012.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1013.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Transitive Dependencies">
|
<ItemGroup Label="Transitive Dependencies">
|
||||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||||
|
@ -45,8 +45,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
|||||||
{
|
{
|
||||||
new Spinner
|
new Spinner
|
||||||
{
|
{
|
||||||
Duration = 2000,
|
Duration = 6000,
|
||||||
Position = OsuPlayfield.BASE_SIZE / 2
|
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -30,6 +30,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
public class TestSceneSpinnerRotation : TestSceneOsuPlayer
|
public class TestSceneSpinnerRotation : TestSceneOsuPlayer
|
||||||
{
|
{
|
||||||
|
private const double spinner_start_time = 100;
|
||||||
|
private const double spinner_duration = 6000;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private AudioManager audioManager { get; set; }
|
private AudioManager audioManager { get; set; }
|
||||||
|
|
||||||
@ -77,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
double finalTrackerRotation = 0, trackerRotationTolerance = 0;
|
double finalTrackerRotation = 0, trackerRotationTolerance = 0;
|
||||||
double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
|
double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
|
||||||
|
|
||||||
addSeekStep(5000);
|
addSeekStep(spinner_start_time + 5000);
|
||||||
AddStep("retrieve disc rotation", () =>
|
AddStep("retrieve disc rotation", () =>
|
||||||
{
|
{
|
||||||
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
|
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
|
||||||
@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
});
|
});
|
||||||
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation);
|
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation);
|
||||||
|
|
||||||
addSeekStep(2500);
|
addSeekStep(spinner_start_time + 2500);
|
||||||
AddAssert("disc rotation rewound",
|
AddAssert("disc rotation rewound",
|
||||||
// we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
|
// we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
|
||||||
// due to the exponential damping applied we're allowing a larger margin of error of about 10%
|
// due to the exponential damping applied we're allowing a larger margin of error of about 10%
|
||||||
@ -102,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
|
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
|
||||||
() => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100));
|
() => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100));
|
||||||
|
|
||||||
addSeekStep(5000);
|
addSeekStep(spinner_start_time + 5000);
|
||||||
AddAssert("is disc rotation almost same",
|
AddAssert("is disc rotation almost same",
|
||||||
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance));
|
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance));
|
||||||
AddAssert("is symbol rotation almost same",
|
AddAssert("is symbol rotation almost same",
|
||||||
@ -140,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestSpinnerNormalBonusRewinding()
|
public void TestSpinnerNormalBonusRewinding()
|
||||||
{
|
{
|
||||||
addSeekStep(1000);
|
addSeekStep(spinner_start_time + 1000);
|
||||||
|
|
||||||
AddAssert("player score matching expected bonus score", () =>
|
AddAssert("player score matching expected bonus score", () =>
|
||||||
{
|
{
|
||||||
@ -201,24 +204,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
|
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
|
|
||||||
{
|
|
||||||
Frames = scoreReplay
|
|
||||||
.Frames
|
|
||||||
.Cast<OsuReplayFrame>()
|
|
||||||
.Select(replayFrame =>
|
|
||||||
{
|
|
||||||
var adjustedTime = replayFrame.Time * rate;
|
|
||||||
return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray());
|
|
||||||
})
|
|
||||||
.Cast<ReplayFrame>()
|
|
||||||
.ToList()
|
|
||||||
};
|
|
||||||
|
|
||||||
private void addSeekStep(double time)
|
private void addSeekStep(double time)
|
||||||
{
|
{
|
||||||
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
|
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
|
||||||
|
|
||||||
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
|
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,7 +229,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
new Spinner
|
new Spinner
|
||||||
{
|
{
|
||||||
Position = new Vector2(256, 192),
|
Position = new Vector2(256, 192),
|
||||||
EndTime = 6000,
|
StartTime = spinner_start_time,
|
||||||
|
Duration = spinner_duration
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
114
osu.Game.Tests/Database/FileStoreTests.cs
Normal file
114
osu.Game.Tests/Database/FileStoreTests.cs
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// 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.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Game.Models;
|
||||||
|
using osu.Game.Stores;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Database
|
||||||
|
{
|
||||||
|
public class FileStoreTests : RealmTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestImportFile()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
var realm = realmFactory.Context;
|
||||||
|
var files = new RealmFileStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 });
|
||||||
|
|
||||||
|
realm.Write(() => files.Add(testData, realm));
|
||||||
|
|
||||||
|
Assert.True(files.Storage.Exists("0/05/054edec1d0211f624fed0cbca9d4f9400b0e491c43742af2c5b0abebf0c990d8"));
|
||||||
|
Assert.True(files.Storage.Exists(realm.All<RealmFile>().First().StoragePath));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImportSameFileTwice()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
var realm = realmFactory.Context;
|
||||||
|
var files = new RealmFileStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 });
|
||||||
|
|
||||||
|
realm.Write(() => files.Add(testData, realm));
|
||||||
|
realm.Write(() => files.Add(testData, realm));
|
||||||
|
|
||||||
|
Assert.AreEqual(1, realm.All<RealmFile>().Count());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDontPurgeReferenced()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
var realm = realmFactory.Context;
|
||||||
|
var files = new RealmFileStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm));
|
||||||
|
|
||||||
|
var timer = new Stopwatch();
|
||||||
|
timer.Start();
|
||||||
|
|
||||||
|
realm.Write(() =>
|
||||||
|
{
|
||||||
|
// attach the file to an arbitrary beatmap
|
||||||
|
var beatmapSet = CreateBeatmapSet(CreateRuleset());
|
||||||
|
|
||||||
|
beatmapSet.Files.Add(new RealmNamedFileUsage(file, "arbitrary.resource"));
|
||||||
|
|
||||||
|
realm.Add(beatmapSet);
|
||||||
|
});
|
||||||
|
|
||||||
|
Logger.Log($"Import complete at {timer.ElapsedMilliseconds}");
|
||||||
|
|
||||||
|
string path = file.StoragePath;
|
||||||
|
|
||||||
|
Assert.True(realm.All<RealmFile>().Any());
|
||||||
|
Assert.True(files.Storage.Exists(path));
|
||||||
|
|
||||||
|
files.Cleanup();
|
||||||
|
Logger.Log($"Cleanup complete at {timer.ElapsedMilliseconds}");
|
||||||
|
|
||||||
|
Assert.True(realm.All<RealmFile>().Any());
|
||||||
|
Assert.True(file.IsValid);
|
||||||
|
Assert.True(files.Storage.Exists(path));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPurgeUnreferenced()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
var realm = realmFactory.Context;
|
||||||
|
var files = new RealmFileStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm));
|
||||||
|
|
||||||
|
string path = file.StoragePath;
|
||||||
|
|
||||||
|
Assert.True(realm.All<RealmFile>().Any());
|
||||||
|
Assert.True(files.Storage.Exists(path));
|
||||||
|
|
||||||
|
files.Cleanup();
|
||||||
|
|
||||||
|
Assert.False(realm.All<RealmFile>().Any());
|
||||||
|
Assert.False(file.IsValid);
|
||||||
|
Assert.False(files.Storage.Exists(path));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,6 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
213
osu.Game.Tests/Database/RealmLiveTests.cs
Normal file
213
osu.Game.Tests/Database/RealmLiveTests.cs
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
// 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.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Models;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Database
|
||||||
|
{
|
||||||
|
public class RealmLiveTests : RealmTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestLiveCastability()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realmFactory, _) =>
|
||||||
|
{
|
||||||
|
RealmLive<RealmBeatmap> beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive();
|
||||||
|
|
||||||
|
ILive<IBeatmapInfo> iBeatmap = beatmap;
|
||||||
|
|
||||||
|
Assert.AreEqual(0, iBeatmap.Value.Length);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestValueAccessWithOpenContext()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realmFactory, _) =>
|
||||||
|
{
|
||||||
|
RealmLive<RealmBeatmap>? liveBeatmap = null;
|
||||||
|
Task.Factory.StartNew(() =>
|
||||||
|
{
|
||||||
|
using (var threadContext = realmFactory.CreateContext())
|
||||||
|
{
|
||||||
|
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
|
||||||
|
|
||||||
|
liveBeatmap = beatmap.ToLive();
|
||||||
|
}
|
||||||
|
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||||
|
|
||||||
|
Debug.Assert(liveBeatmap != null);
|
||||||
|
|
||||||
|
Task.Factory.StartNew(() =>
|
||||||
|
{
|
||||||
|
Assert.DoesNotThrow(() =>
|
||||||
|
{
|
||||||
|
using (realmFactory.CreateContext())
|
||||||
|
{
|
||||||
|
var resolved = liveBeatmap.Value;
|
||||||
|
|
||||||
|
Assert.IsTrue(resolved.Realm.IsClosed);
|
||||||
|
Assert.IsTrue(resolved.IsValid);
|
||||||
|
|
||||||
|
// can access properties without a crash.
|
||||||
|
Assert.IsFalse(resolved.Hidden);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScopedReadWithoutContext()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realmFactory, _) =>
|
||||||
|
{
|
||||||
|
RealmLive<RealmBeatmap>? liveBeatmap = null;
|
||||||
|
Task.Factory.StartNew(() =>
|
||||||
|
{
|
||||||
|
using (var threadContext = realmFactory.CreateContext())
|
||||||
|
{
|
||||||
|
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
|
||||||
|
|
||||||
|
liveBeatmap = beatmap.ToLive();
|
||||||
|
}
|
||||||
|
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||||
|
|
||||||
|
Debug.Assert(liveBeatmap != null);
|
||||||
|
|
||||||
|
Task.Factory.StartNew(() =>
|
||||||
|
{
|
||||||
|
liveBeatmap.PerformRead(beatmap =>
|
||||||
|
{
|
||||||
|
Assert.IsTrue(beatmap.IsValid);
|
||||||
|
Assert.IsFalse(beatmap.Hidden);
|
||||||
|
});
|
||||||
|
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScopedWriteWithoutContext()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realmFactory, _) =>
|
||||||
|
{
|
||||||
|
RealmLive<RealmBeatmap>? liveBeatmap = null;
|
||||||
|
Task.Factory.StartNew(() =>
|
||||||
|
{
|
||||||
|
using (var threadContext = realmFactory.CreateContext())
|
||||||
|
{
|
||||||
|
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
|
||||||
|
|
||||||
|
liveBeatmap = beatmap.ToLive();
|
||||||
|
}
|
||||||
|
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||||
|
|
||||||
|
Debug.Assert(liveBeatmap != null);
|
||||||
|
|
||||||
|
Task.Factory.StartNew(() =>
|
||||||
|
{
|
||||||
|
liveBeatmap.PerformWrite(beatmap => { beatmap.Hidden = true; });
|
||||||
|
liveBeatmap.PerformRead(beatmap => { Assert.IsTrue(beatmap.Hidden); });
|
||||||
|
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestValueAccessWithoutOpenContextFails()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realmFactory, _) =>
|
||||||
|
{
|
||||||
|
RealmLive<RealmBeatmap>? liveBeatmap = null;
|
||||||
|
Task.Factory.StartNew(() =>
|
||||||
|
{
|
||||||
|
using (var threadContext = realmFactory.CreateContext())
|
||||||
|
{
|
||||||
|
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
|
||||||
|
|
||||||
|
liveBeatmap = beatmap.ToLive();
|
||||||
|
}
|
||||||
|
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||||
|
|
||||||
|
Debug.Assert(liveBeatmap != null);
|
||||||
|
|
||||||
|
Task.Factory.StartNew(() =>
|
||||||
|
{
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
{
|
||||||
|
var unused = liveBeatmap.Value;
|
||||||
|
});
|
||||||
|
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestLiveAssumptions()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realmFactory, _) =>
|
||||||
|
{
|
||||||
|
int changesTriggered = 0;
|
||||||
|
|
||||||
|
using (var updateThreadContext = realmFactory.CreateContext())
|
||||||
|
{
|
||||||
|
updateThreadContext.All<RealmBeatmap>().SubscribeForNotifications(gotChange);
|
||||||
|
RealmLive<RealmBeatmap>? liveBeatmap = null;
|
||||||
|
|
||||||
|
Task.Factory.StartNew(() =>
|
||||||
|
{
|
||||||
|
using (var threadContext = realmFactory.CreateContext())
|
||||||
|
{
|
||||||
|
var ruleset = CreateRuleset();
|
||||||
|
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
|
||||||
|
|
||||||
|
// add a second beatmap to ensure that a full refresh occurs below.
|
||||||
|
// not just a refresh from the resolved Live.
|
||||||
|
threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
|
||||||
|
|
||||||
|
liveBeatmap = beatmap.ToLive();
|
||||||
|
}
|
||||||
|
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||||
|
|
||||||
|
Debug.Assert(liveBeatmap != null);
|
||||||
|
|
||||||
|
// not yet seen by main context
|
||||||
|
Assert.AreEqual(0, updateThreadContext.All<RealmBeatmap>().Count());
|
||||||
|
Assert.AreEqual(0, changesTriggered);
|
||||||
|
|
||||||
|
var resolved = liveBeatmap.Value;
|
||||||
|
|
||||||
|
// retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
|
||||||
|
Assert.AreEqual(2, updateThreadContext.All<RealmBeatmap>().Count());
|
||||||
|
Assert.AreEqual(1, changesTriggered);
|
||||||
|
|
||||||
|
// even though the realm that this instance was resolved for was closed, it's still valid.
|
||||||
|
Assert.IsTrue(resolved.Realm.IsClosed);
|
||||||
|
Assert.IsTrue(resolved.IsValid);
|
||||||
|
|
||||||
|
// can access properties without a crash.
|
||||||
|
Assert.IsFalse(resolved.Hidden);
|
||||||
|
|
||||||
|
updateThreadContext.Write(r =>
|
||||||
|
{
|
||||||
|
// can use with the main context.
|
||||||
|
r.Remove(resolved);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void gotChange(IRealmCollection<RealmBeatmap> sender, ChangeSet changes, Exception error)
|
||||||
|
{
|
||||||
|
changesTriggered++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,10 +5,12 @@ using System;
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Models;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
@ -70,6 +72,46 @@ namespace osu.Game.Tests.Database
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static RealmBeatmapSet CreateBeatmapSet(RealmRuleset ruleset)
|
||||||
|
{
|
||||||
|
RealmFile createRealmFile() => new RealmFile { Hash = Guid.NewGuid().ToString().ComputeSHA2Hash() };
|
||||||
|
|
||||||
|
var metadata = new RealmBeatmapMetadata
|
||||||
|
{
|
||||||
|
Title = "My Love",
|
||||||
|
Artist = "Kuba Oms"
|
||||||
|
};
|
||||||
|
|
||||||
|
var beatmapSet = new RealmBeatmapSet
|
||||||
|
{
|
||||||
|
Beatmaps =
|
||||||
|
{
|
||||||
|
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Easy", },
|
||||||
|
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Normal", },
|
||||||
|
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Hard", },
|
||||||
|
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Insane", }
|
||||||
|
},
|
||||||
|
Files =
|
||||||
|
{
|
||||||
|
new RealmNamedFileUsage(createRealmFile(), "test [easy].osu"),
|
||||||
|
new RealmNamedFileUsage(createRealmFile(), "test [normal].osu"),
|
||||||
|
new RealmNamedFileUsage(createRealmFile(), "test [hard].osu"),
|
||||||
|
new RealmNamedFileUsage(createRealmFile(), "test [insane].osu"),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
beatmapSet.Files.Add(new RealmNamedFileUsage(createRealmFile(), $"hitsound{i}.mp3"));
|
||||||
|
|
||||||
|
foreach (var b in beatmapSet.Beatmaps)
|
||||||
|
b.BeatmapSet = beatmapSet;
|
||||||
|
|
||||||
|
return beatmapSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static RealmRuleset CreateRuleset() =>
|
||||||
|
new RealmRuleset(0, "osu!", "osu", true);
|
||||||
|
|
||||||
private class RealmTestGame : Framework.Game
|
private class RealmTestGame : Framework.Game
|
||||||
{
|
{
|
||||||
public RealmTestGame(Func<Task> work)
|
public RealmTestGame(Func<Task> work)
|
||||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Tests
|
|||||||
protected virtual TestOsuGameBase LoadOsuIntoHost(GameHost host, bool withBeatmap = false)
|
protected virtual TestOsuGameBase LoadOsuIntoHost(GameHost host, bool withBeatmap = false)
|
||||||
{
|
{
|
||||||
var osu = new TestOsuGameBase(withBeatmap);
|
var osu = new TestOsuGameBase(withBeatmap);
|
||||||
Task.Run(() => host.Run(osu))
|
Task.Factory.StartNew(() => host.Run(osu), TaskCreationOptions.LongRunning)
|
||||||
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
|
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
|
||||||
|
|
||||||
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
|
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
|
||||||
|
@ -6,6 +6,7 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
using osu.Framework.Audio.Track;
|
using osu.Framework.Audio.Track;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
@ -18,16 +19,19 @@ namespace osu.Game.Tests.Visual.Audio
|
|||||||
{
|
{
|
||||||
public class TestSceneAudioFilter : OsuTestScene
|
public class TestSceneAudioFilter : OsuTestScene
|
||||||
{
|
{
|
||||||
private OsuSpriteText lowpassText;
|
private OsuSpriteText lowPassText;
|
||||||
private AudioFilter lowpassFilter;
|
private AudioFilter lowPassFilter;
|
||||||
|
|
||||||
private OsuSpriteText highpassText;
|
private OsuSpriteText highPassText;
|
||||||
private AudioFilter highpassFilter;
|
private AudioFilter highPassFilter;
|
||||||
|
|
||||||
private Track track;
|
private Track track;
|
||||||
|
|
||||||
private WaveformTestBeatmap beatmap;
|
private WaveformTestBeatmap beatmap;
|
||||||
|
|
||||||
|
private OsuSliderBar<int> lowPassSlider;
|
||||||
|
private OsuSliderBar<int> highPassSlider;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(AudioManager audio)
|
private void load(AudioManager audio)
|
||||||
{
|
{
|
||||||
@ -38,53 +42,89 @@ namespace osu.Game.Tests.Visual.Audio
|
|||||||
{
|
{
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
lowpassFilter = new AudioFilter(audio.TrackMixer),
|
lowPassFilter = new AudioFilter(audio.TrackMixer),
|
||||||
highpassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
|
highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
|
||||||
lowpassText = new OsuSpriteText
|
lowPassText = new OsuSpriteText
|
||||||
{
|
{
|
||||||
Padding = new MarginPadding(20),
|
Padding = new MarginPadding(20),
|
||||||
Text = $"Low Pass: {lowpassFilter.Cutoff.Value}hz",
|
Text = $"Low Pass: {lowPassFilter.Cutoff}hz",
|
||||||
Font = new FontUsage(size: 40)
|
Font = new FontUsage(size: 40)
|
||||||
},
|
},
|
||||||
new OsuSliderBar<int>
|
lowPassSlider = new OsuSliderBar<int>
|
||||||
{
|
{
|
||||||
Width = 500,
|
Width = 500,
|
||||||
Height = 50,
|
Height = 50,
|
||||||
Padding = new MarginPadding(20),
|
Padding = new MarginPadding(20),
|
||||||
Current = { BindTarget = lowpassFilter.Cutoff }
|
Current = new BindableInt
|
||||||
|
{
|
||||||
|
MinValue = 0,
|
||||||
|
MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
highpassText = new OsuSpriteText
|
highPassText = new OsuSpriteText
|
||||||
{
|
{
|
||||||
Padding = new MarginPadding(20),
|
Padding = new MarginPadding(20),
|
||||||
Text = $"High Pass: {highpassFilter.Cutoff.Value}hz",
|
Text = $"High Pass: {highPassFilter.Cutoff}hz",
|
||||||
Font = new FontUsage(size: 40)
|
Font = new FontUsage(size: 40)
|
||||||
},
|
},
|
||||||
new OsuSliderBar<int>
|
highPassSlider = new OsuSliderBar<int>
|
||||||
{
|
{
|
||||||
Width = 500,
|
Width = 500,
|
||||||
Height = 50,
|
Height = 50,
|
||||||
Padding = new MarginPadding(20),
|
Padding = new MarginPadding(20),
|
||||||
Current = { BindTarget = highpassFilter.Cutoff }
|
Current = new BindableInt
|
||||||
|
{
|
||||||
|
MinValue = 0,
|
||||||
|
MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
lowpassFilter.Cutoff.ValueChanged += e => lowpassText.Text = $"Low Pass: {e.NewValue}hz";
|
|
||||||
highpassFilter.Cutoff.ValueChanged += e => highpassText.Text = $"High Pass: {e.NewValue}hz";
|
lowPassSlider.Current.ValueChanged += e =>
|
||||||
|
{
|
||||||
|
lowPassText.Text = $"Low Pass: {e.NewValue}hz";
|
||||||
|
lowPassFilter.Cutoff = e.NewValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
highPassSlider.Current.ValueChanged += e =>
|
||||||
|
{
|
||||||
|
highPassText.Text = $"High Pass: {e.NewValue}hz";
|
||||||
|
highPassFilter.Cutoff = e.NewValue;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Overrides of Drawable
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
highPassSlider.Current.Value = highPassFilter.Cutoff;
|
||||||
|
lowPassSlider.Current.Value = lowPassFilter.Cutoff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUpSteps()
|
public void SetUpSteps()
|
||||||
{
|
{
|
||||||
AddStep("Play Track", () => track.Start());
|
AddStep("Play Track", () => track.Start());
|
||||||
|
|
||||||
|
AddStep("Reset filters", () =>
|
||||||
|
{
|
||||||
|
lowPassFilter.Cutoff = AudioFilter.MAX_LOWPASS_CUTOFF;
|
||||||
|
highPassFilter.Cutoff = 0;
|
||||||
|
});
|
||||||
|
|
||||||
waitTrackPlay();
|
waitTrackPlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestLowPass()
|
public void TestLowPassSweep()
|
||||||
{
|
{
|
||||||
AddStep("Filter Sweep", () =>
|
AddStep("Filter Sweep", () =>
|
||||||
{
|
{
|
||||||
lowpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
|
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
|
||||||
.CutoffTo(0, 2000, Easing.OutCubic);
|
.CutoffTo(0, 2000, Easing.OutCubic);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -92,7 +132,7 @@ namespace osu.Game.Tests.Visual.Audio
|
|||||||
|
|
||||||
AddStep("Filter Sweep (reverse)", () =>
|
AddStep("Filter Sweep (reverse)", () =>
|
||||||
{
|
{
|
||||||
lowpassFilter.CutoffTo(0).Then()
|
lowPassFilter.CutoffTo(0).Then()
|
||||||
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
|
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -101,11 +141,11 @@ namespace osu.Game.Tests.Visual.Audio
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestHighPass()
|
public void TestHighPassSweep()
|
||||||
{
|
{
|
||||||
AddStep("Filter Sweep", () =>
|
AddStep("Filter Sweep", () =>
|
||||||
{
|
{
|
||||||
highpassFilter.CutoffTo(0).Then()
|
highPassFilter.CutoffTo(0).Then()
|
||||||
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
|
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -113,7 +153,7 @@ namespace osu.Game.Tests.Visual.Audio
|
|||||||
|
|
||||||
AddStep("Filter Sweep (reverse)", () =>
|
AddStep("Filter Sweep (reverse)", () =>
|
||||||
{
|
{
|
||||||
highpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
|
highPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
|
||||||
.CutoffTo(0, 2000, Easing.OutCubic);
|
.CutoffTo(0, 2000, Easing.OutCubic);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -103,6 +103,30 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
checkFrameCount(0);
|
checkFrameCount(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRatePreservedWhenTimeNotProgressing()
|
||||||
|
{
|
||||||
|
AddStep("set manual clock rate", () => manualClock.Rate = 1);
|
||||||
|
seekManualTo(5000);
|
||||||
|
createStabilityContainer();
|
||||||
|
checkRate(1);
|
||||||
|
|
||||||
|
seekManualTo(10000);
|
||||||
|
checkRate(1);
|
||||||
|
|
||||||
|
AddWaitStep("wait some", 3);
|
||||||
|
checkRate(1);
|
||||||
|
|
||||||
|
seekManualTo(5000);
|
||||||
|
checkRate(-1);
|
||||||
|
|
||||||
|
AddWaitStep("wait some", 3);
|
||||||
|
checkRate(-1);
|
||||||
|
|
||||||
|
seekManualTo(10000);
|
||||||
|
checkRate(1);
|
||||||
|
}
|
||||||
|
|
||||||
private const int max_frames_catchup = 50;
|
private const int max_frames_catchup = 50;
|
||||||
|
|
||||||
private void createStabilityContainer(double gameplayStartTime = double.MinValue) => AddStep("create container", () =>
|
private void createStabilityContainer(double gameplayStartTime = double.MinValue) => AddStep("create container", () =>
|
||||||
@ -116,6 +140,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
private void checkFrameCount(int frames) =>
|
private void checkFrameCount(int frames) =>
|
||||||
AddAssert($"elapsed frames is {frames}", () => consumer.ElapsedFrames == frames);
|
AddAssert($"elapsed frames is {frames}", () => consumer.ElapsedFrames == frames);
|
||||||
|
|
||||||
|
private void checkRate(double rate) =>
|
||||||
|
AddAssert($"clock rate is {rate}", () => consumer.Clock.Rate == rate);
|
||||||
|
|
||||||
public class ClockConsumingChild : CompositeDrawable
|
public class ClockConsumingChild : CompositeDrawable
|
||||||
{
|
{
|
||||||
private readonly OsuSpriteText text;
|
private readonly OsuSpriteText text;
|
||||||
|
@ -90,8 +90,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
CreateTest(() =>
|
CreateTest(() =>
|
||||||
{
|
{
|
||||||
AddStep("fail on first judgement", () => currentFailConditions = (_, __) => true);
|
AddStep("fail on first judgement", () => currentFailConditions = (_, __) => true);
|
||||||
AddStep("set storyboard duration to 1.3s", () => currentStoryboardDuration = 1300);
|
|
||||||
|
// Fail occurs at 164ms with the provided beatmap.
|
||||||
|
// Fail animation runs for 2.5s realtime but the gameplay time change is *variable* due to the frequency transform being applied, so we need a bit of lenience.
|
||||||
|
AddStep("set storyboard duration to 0.6s", () => currentStoryboardDuration = 600);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("wait for fail", () => Player.HasFailed);
|
AddUntilStep("wait for fail", () => Player.HasFailed);
|
||||||
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
|
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
|
||||||
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
|
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
|
||||||
|
31
osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs
Normal file
31
osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// 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.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Navigation
|
||||||
|
{
|
||||||
|
public class TestSceneStartupImport : OsuGameTestScene
|
||||||
|
{
|
||||||
|
private string importFilename;
|
||||||
|
|
||||||
|
protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { importFilename });
|
||||||
|
|
||||||
|
public override void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("Prepare import beatmap", () => importFilename = TestResources.GetTestBeatmapForImport());
|
||||||
|
|
||||||
|
base.SetUpSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImportCreatedNotification()
|
||||||
|
{
|
||||||
|
AddUntilStep("Import notification was presented", () => Game.Notifications.ChildrenOfType<ImportProgressNotification>().Count() == 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
|
|||||||
public static TournamentGameBase LoadTournament(GameHost host, TournamentGameBase tournament = null)
|
public static TournamentGameBase LoadTournament(GameHost host, TournamentGameBase tournament = null)
|
||||||
{
|
{
|
||||||
tournament ??= new TournamentGameBase();
|
tournament ??= new TournamentGameBase();
|
||||||
Task.Run(() => host.Run(tournament))
|
Task.Factory.StartNew(() => host.Run(tournament), TaskCreationOptions.LongRunning)
|
||||||
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
|
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
|
||||||
WaitForOrAssert(() => tournament.IsLoaded, @"osu! failed to start in a reasonable amount of time");
|
WaitForOrAssert(() => tournament.IsLoaded, @"osu! failed to start in a reasonable amount of time");
|
||||||
return tournament;
|
return tournament;
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using ManagedBass.Fx;
|
using ManagedBass.Fx;
|
||||||
using osu.Framework.Audio.Mixing;
|
using osu.Framework.Audio.Mixing;
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Audio.Effects
|
namespace osu.Game.Audio.Effects
|
||||||
@ -21,10 +20,25 @@ namespace osu.Game.Audio.Effects
|
|||||||
private readonly BQFParameters filter;
|
private readonly BQFParameters filter;
|
||||||
private readonly BQFType type;
|
private readonly BQFType type;
|
||||||
|
|
||||||
|
private bool isAttached;
|
||||||
|
|
||||||
|
private int cutoff;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The current cutoff of this filter.
|
/// The cutoff frequency of this filter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public BindableNumber<int> Cutoff { get; }
|
public int Cutoff
|
||||||
|
{
|
||||||
|
get => cutoff;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == cutoff)
|
||||||
|
return;
|
||||||
|
|
||||||
|
cutoff = value;
|
||||||
|
updateFilter(cutoff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A Component that implements a BASS FX BiQuad Filter Effect.
|
/// A Component that implements a BASS FX BiQuad Filter Effect.
|
||||||
@ -36,102 +50,96 @@ namespace osu.Game.Audio.Effects
|
|||||||
this.mixer = mixer;
|
this.mixer = mixer;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
|
|
||||||
int initialCutoff;
|
|
||||||
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case BQFType.HighPass:
|
|
||||||
initialCutoff = 1;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BQFType.LowPass:
|
|
||||||
initialCutoff = MAX_LOWPASS_CUTOFF;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
initialCutoff = 500; // A default that should ensure audio remains audible for other filters.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Cutoff = new BindableNumber<int>(initialCutoff)
|
|
||||||
{
|
|
||||||
MinValue = 1,
|
|
||||||
MaxValue = MAX_LOWPASS_CUTOFF
|
|
||||||
};
|
|
||||||
|
|
||||||
filter = new BQFParameters
|
filter = new BQFParameters
|
||||||
{
|
{
|
||||||
lFilter = type,
|
lFilter = type,
|
||||||
fCenter = initialCutoff,
|
|
||||||
fBandwidth = 0,
|
fBandwidth = 0,
|
||||||
fQ = 0.7f // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0)
|
// This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0)
|
||||||
|
fQ = 0.7f
|
||||||
};
|
};
|
||||||
|
|
||||||
// Don't start attached if this is low-pass or high-pass filter (as they have special auto-attach/detach logic)
|
Cutoff = getInitialCutoff(type);
|
||||||
if (type != BQFType.LowPass && type != BQFType.HighPass)
|
|
||||||
attachFilter();
|
|
||||||
|
|
||||||
Cutoff.ValueChanged += updateFilter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void attachFilter()
|
private int getInitialCutoff(BQFType type)
|
||||||
{
|
{
|
||||||
Debug.Assert(!mixer.Effects.Contains(filter));
|
switch (type)
|
||||||
mixer.Effects.Add(filter);
|
{
|
||||||
|
case BQFType.HighPass:
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
case BQFType.LowPass:
|
||||||
|
return MAX_LOWPASS_CUTOFF;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 500; // A default that should ensure audio remains audible for other filters.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void detachFilter()
|
private void updateFilter(int newValue)
|
||||||
{
|
{
|
||||||
Debug.Assert(mixer.Effects.Contains(filter));
|
switch (type)
|
||||||
mixer.Effects.Remove(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateFilter(ValueChangedEvent<int> cutoff)
|
|
||||||
{
|
{
|
||||||
|
case BQFType.LowPass:
|
||||||
// Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz.
|
// Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz.
|
||||||
if (type == BQFType.LowPass)
|
if (newValue >= MAX_LOWPASS_CUTOFF)
|
||||||
{
|
{
|
||||||
if (cutoff.NewValue >= MAX_LOWPASS_CUTOFF)
|
ensureDetached();
|
||||||
{
|
|
||||||
detachFilter();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cutoff.OldValue >= MAX_LOWPASS_CUTOFF && cutoff.NewValue < MAX_LOWPASS_CUTOFF)
|
break;
|
||||||
attachFilter();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz.
|
// Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz.
|
||||||
if (type == BQFType.HighPass)
|
case BQFType.HighPass:
|
||||||
|
if (newValue <= 1)
|
||||||
{
|
{
|
||||||
if (cutoff.NewValue <= 1)
|
ensureDetached();
|
||||||
{
|
|
||||||
detachFilter();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cutoff.OldValue <= 1 && cutoff.NewValue > 1)
|
break;
|
||||||
attachFilter();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensureAttached();
|
||||||
|
|
||||||
var filterIndex = mixer.Effects.IndexOf(filter);
|
var filterIndex = mixer.Effects.IndexOf(filter);
|
||||||
|
|
||||||
if (filterIndex < 0) return;
|
if (filterIndex < 0) return;
|
||||||
|
|
||||||
if (mixer.Effects[filterIndex] is BQFParameters existingFilter)
|
if (mixer.Effects[filterIndex] is BQFParameters existingFilter)
|
||||||
{
|
{
|
||||||
existingFilter.fCenter = cutoff.NewValue;
|
existingFilter.fCenter = newValue;
|
||||||
|
|
||||||
// required to update effect with new parameters.
|
// required to update effect with new parameters.
|
||||||
mixer.Effects[filterIndex] = existingFilter;
|
mixer.Effects[filterIndex] = existingFilter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ensureAttached()
|
||||||
|
{
|
||||||
|
if (isAttached)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Debug.Assert(!mixer.Effects.Contains(filter));
|
||||||
|
mixer.Effects.Add(filter);
|
||||||
|
isAttached = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureDetached()
|
||||||
|
{
|
||||||
|
if (!isAttached)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Debug.Assert(mixer.Effects.Contains(filter));
|
||||||
|
mixer.Effects.Remove(filter);
|
||||||
|
isAttached = false;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
ensureDetached();
|
||||||
if (mixer.Effects.Contains(filter))
|
|
||||||
detachFilter();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Transforms;
|
using osu.Framework.Graphics.Transforms;
|
||||||
|
|
||||||
@ -12,7 +11,7 @@ namespace osu.Game.Audio.Effects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The filter cutoff.
|
/// The filter cutoff.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
BindableNumber<int> Cutoff { get; }
|
int Cutoff { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class FilterableAudioComponentExtensions
|
public static class FilterableAudioComponentExtensions
|
||||||
@ -40,7 +39,7 @@ namespace osu.Game.Audio.Effects
|
|||||||
public static TransformSequence<T> CutoffTo<T, TEasing>(this T component, int newCutoff, double duration, TEasing easing)
|
public static TransformSequence<T> CutoffTo<T, TEasing>(this T component, int newCutoff, double duration, TEasing easing)
|
||||||
where T : class, ITransformableFilter, IDrawable
|
where T : class, ITransformableFilter, IDrawable
|
||||||
where TEasing : IEasingFunction
|
where TEasing : IEasingFunction
|
||||||
=> component.TransformBindableTo(component.Cutoff, newCutoff, duration, easing);
|
=> component.TransformTo(nameof(component.Cutoff), newCutoff, duration, easing);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Smoothly adjusts filter cutoff over time.
|
/// Smoothly adjusts filter cutoff over time.
|
||||||
@ -49,6 +48,6 @@ namespace osu.Game.Audio.Effects
|
|||||||
public static TransformSequence<T> CutoffTo<T, TEasing>(this TransformSequence<T> sequence, int newCutoff, double duration, TEasing easing)
|
public static TransformSequence<T> CutoffTo<T, TEasing>(this TransformSequence<T> sequence, int newCutoff, double duration, TEasing easing)
|
||||||
where T : class, ITransformableFilter, IDrawable
|
where T : class, ITransformableFilter, IDrawable
|
||||||
where TEasing : IEasingFunction
|
where TEasing : IEasingFunction
|
||||||
=> sequence.Append(o => o.TransformBindableTo(o.Cutoff, newCutoff, duration, easing));
|
=> sequence.Append(o => o.TransformTo(nameof(o.Cutoff), newCutoff, duration, easing));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,7 +116,7 @@ namespace osu.Game.Database
|
|||||||
/// <param name="paths">One or more archive locations on disk.</param>
|
/// <param name="paths">One or more archive locations on disk.</param>
|
||||||
public Task Import(params string[] paths)
|
public Task Import(params string[] paths)
|
||||||
{
|
{
|
||||||
var notification = new ProgressNotification { State = ProgressNotificationState.Active };
|
var notification = new ImportProgressNotification();
|
||||||
|
|
||||||
PostNotification?.Invoke(notification);
|
PostNotification?.Invoke(notification);
|
||||||
|
|
||||||
@ -125,7 +125,7 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
public Task Import(params ImportTask[] tasks)
|
public Task Import(params ImportTask[] tasks)
|
||||||
{
|
{
|
||||||
var notification = new ProgressNotification { State = ProgressNotificationState.Active };
|
var notification = new ImportProgressNotification();
|
||||||
|
|
||||||
PostNotification?.Invoke(notification);
|
PostNotification?.Invoke(notification);
|
||||||
|
|
||||||
|
20
osu.Game/Database/IHasRealmFiles.cs
Normal file
20
osu.Game/Database/IHasRealmFiles.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// 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.Collections.Generic;
|
||||||
|
using osu.Game.Models;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Database
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A model that contains a list of files it is responsible for.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHasRealmFiles
|
||||||
|
{
|
||||||
|
IList<RealmNamedFileUsage> Files { get; }
|
||||||
|
|
||||||
|
string Hash { get; set; }
|
||||||
|
}
|
||||||
|
}
|
19
osu.Game/Database/INamedFile.cs
Normal file
19
osu.Game/Database/INamedFile.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// 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.Game.Models;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Database
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a join model which gives a filename and scope to a <see cref="File"/>.
|
||||||
|
/// </summary>
|
||||||
|
public interface INamedFile
|
||||||
|
{
|
||||||
|
string Filename { get; set; }
|
||||||
|
|
||||||
|
RealmFile File { get; set; }
|
||||||
|
}
|
||||||
|
}
|
15
osu.Game/Database/ImportProgressNotification.cs
Normal file
15
osu.Game/Database/ImportProgressNotification.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// 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.Game.Overlays.Notifications;
|
||||||
|
|
||||||
|
namespace osu.Game.Database
|
||||||
|
{
|
||||||
|
public class ImportProgressNotification : ProgressNotification
|
||||||
|
{
|
||||||
|
public ImportProgressNotification()
|
||||||
|
{
|
||||||
|
State = ProgressNotificationState.Active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
111
osu.Game/Database/RealmLive.cs
Normal file
111
osu.Game/Database/RealmLive.cs
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
// 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.Threading;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Database
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a method of working with realm objects over longer application lifetimes.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The underlying object type.</typeparam>
|
||||||
|
public class RealmLive<T> : ILive<T> where T : RealmObject, IHasGuidPrimaryKey
|
||||||
|
{
|
||||||
|
public Guid ID { get; }
|
||||||
|
|
||||||
|
private readonly SynchronizationContext? fetchedContext;
|
||||||
|
private readonly int fetchedThreadId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The original live data used to create this instance.
|
||||||
|
/// </summary>
|
||||||
|
private readonly T data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Construct a new instance of live realm data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The realm data.</param>
|
||||||
|
public RealmLive(T data)
|
||||||
|
{
|
||||||
|
this.data = data;
|
||||||
|
|
||||||
|
fetchedContext = SynchronizationContext.Current;
|
||||||
|
fetchedThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||||
|
|
||||||
|
ID = data.ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Perform a read operation on this live object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="perform">The action to perform.</param>
|
||||||
|
public void PerformRead(Action<T> perform)
|
||||||
|
{
|
||||||
|
if (originalDataValid)
|
||||||
|
{
|
||||||
|
perform(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var realm = Realm.GetInstance(data.Realm.Config))
|
||||||
|
perform(realm.Find<T>(ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Perform a read operation on this live object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="perform">The action to perform.</param>
|
||||||
|
public TReturn PerformRead<TReturn>(Func<T, TReturn> perform)
|
||||||
|
{
|
||||||
|
if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn)))
|
||||||
|
throw new InvalidOperationException($"Realm live objects should not exit the scope of {nameof(PerformRead)}.");
|
||||||
|
|
||||||
|
if (originalDataValid)
|
||||||
|
return perform(data);
|
||||||
|
|
||||||
|
using (var realm = Realm.GetInstance(data.Realm.Config))
|
||||||
|
return perform(realm.Find<T>(ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Perform a write operation on this live object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="perform">The action to perform.</param>
|
||||||
|
public void PerformWrite(Action<T> perform) =>
|
||||||
|
PerformRead(t =>
|
||||||
|
{
|
||||||
|
var transaction = t.Realm.BeginWrite();
|
||||||
|
perform(t);
|
||||||
|
transaction.Commit();
|
||||||
|
});
|
||||||
|
|
||||||
|
public T Value
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (originalDataValid)
|
||||||
|
return data;
|
||||||
|
|
||||||
|
T retrieved;
|
||||||
|
|
||||||
|
using (var realm = Realm.GetInstance(data.Realm.Config))
|
||||||
|
retrieved = realm.Find<T>(ID);
|
||||||
|
|
||||||
|
if (!retrieved.IsValid)
|
||||||
|
throw new InvalidOperationException("Attempted to access value without an open context");
|
||||||
|
|
||||||
|
return retrieved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool originalDataValid => isCorrectThread && data.IsValid;
|
||||||
|
|
||||||
|
// this matches realm's internal thread validation (see https://github.com/realm/realm-dotnet/blob/903b4d0b304f887e37e2d905384fb572a6496e70/Realm/Realm/Native/SynchronizationContextScheduler.cs#L72)
|
||||||
|
private bool isCorrectThread
|
||||||
|
=> (fetchedContext != null && SynchronizationContext.Current == fetchedContext) || fetchedThreadId == Thread.CurrentThread.ManagedThreadId;
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
using Realms;
|
using Realms;
|
||||||
@ -47,5 +48,17 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
return mapper.Map<T>(item);
|
return mapper.Map<T>(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<RealmLive<T>> ToLive<T>(this IEnumerable<T> realmList)
|
||||||
|
where T : RealmObject, IHasGuidPrimaryKey
|
||||||
|
{
|
||||||
|
return realmList.Select(l => new RealmLive<T>(l)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RealmLive<T> ToLive<T>(this T realmObject)
|
||||||
|
where T : RealmObject, IHasGuidPrimaryKey
|
||||||
|
{
|
||||||
|
return new RealmLive<T>(realmObject);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
117
osu.Game/Models/RealmBeatmap.cs
Normal file
117
osu.Game/Models/RealmBeatmap.cs
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
// 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 JetBrains.Annotations;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A single beatmap difficulty.
|
||||||
|
/// </summary>
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
|
[Serializable]
|
||||||
|
[MapTo("Beatmap")]
|
||||||
|
public class RealmBeatmap : RealmObject, IHasGuidPrimaryKey, IBeatmapInfo
|
||||||
|
{
|
||||||
|
[PrimaryKey]
|
||||||
|
public Guid ID { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public string DifficultyName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public RealmRuleset Ruleset { get; set; } = null!;
|
||||||
|
|
||||||
|
public RealmBeatmapDifficulty Difficulty { get; set; } = null!;
|
||||||
|
|
||||||
|
public RealmBeatmapMetadata Metadata { get; set; } = null!;
|
||||||
|
|
||||||
|
public RealmBeatmapSet? BeatmapSet { get; set; }
|
||||||
|
|
||||||
|
public BeatmapSetOnlineStatus Status
|
||||||
|
{
|
||||||
|
get => (BeatmapSetOnlineStatus)StatusInt;
|
||||||
|
set => StatusInt = (int)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MapTo(nameof(Status))]
|
||||||
|
public int StatusInt { get; set; }
|
||||||
|
|
||||||
|
public int? OnlineID { get; set; }
|
||||||
|
|
||||||
|
public double Length { get; set; }
|
||||||
|
|
||||||
|
public double BPM { get; set; }
|
||||||
|
|
||||||
|
public string Hash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public double StarRating { get; set; }
|
||||||
|
|
||||||
|
public string MD5Hash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool Hidden { get; set; }
|
||||||
|
|
||||||
|
public RealmBeatmap(RealmRuleset ruleset, RealmBeatmapDifficulty difficulty, RealmBeatmapMetadata metadata)
|
||||||
|
{
|
||||||
|
Ruleset = ruleset;
|
||||||
|
Difficulty = difficulty;
|
||||||
|
Metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
private RealmBeatmap()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Properties we may not want persisted (but also maybe no harm?)
|
||||||
|
|
||||||
|
public double AudioLeadIn { get; set; }
|
||||||
|
|
||||||
|
public float StackLeniency { get; set; } = 0.7f;
|
||||||
|
|
||||||
|
public bool SpecialStyle { get; set; }
|
||||||
|
|
||||||
|
public bool LetterboxInBreaks { get; set; }
|
||||||
|
|
||||||
|
public bool WidescreenStoryboard { get; set; }
|
||||||
|
|
||||||
|
public bool EpilepsyWarning { get; set; }
|
||||||
|
|
||||||
|
public bool SamplesMatchPlaybackRate { get; set; }
|
||||||
|
|
||||||
|
public double DistanceSpacing { get; set; }
|
||||||
|
|
||||||
|
public int BeatDivisor { get; set; }
|
||||||
|
|
||||||
|
public int GridSize { get; set; }
|
||||||
|
|
||||||
|
public double TimelineZoom { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public bool AudioEquals(RealmBeatmap? other) => other != null
|
||||||
|
&& BeatmapSet != null
|
||||||
|
&& other.BeatmapSet != null
|
||||||
|
&& BeatmapSet.Hash == other.BeatmapSet.Hash
|
||||||
|
&& Metadata.AudioFile == other.Metadata.AudioFile;
|
||||||
|
|
||||||
|
public bool BackgroundEquals(RealmBeatmap? other) => other != null
|
||||||
|
&& BeatmapSet != null
|
||||||
|
&& other.BeatmapSet != null
|
||||||
|
&& BeatmapSet.Hash == other.BeatmapSet.Hash
|
||||||
|
&& Metadata.BackgroundFile == other.Metadata.BackgroundFile;
|
||||||
|
|
||||||
|
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
|
||||||
|
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
|
||||||
|
IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;
|
||||||
|
IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => Difficulty;
|
||||||
|
}
|
||||||
|
}
|
45
osu.Game/Models/RealmBeatmapDifficulty.cs
Normal file
45
osu.Game/Models/RealmBeatmapDifficulty.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// 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.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Models
|
||||||
|
{
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
|
[MapTo("BeatmapDifficulty")]
|
||||||
|
public class RealmBeatmapDifficulty : EmbeddedObject, IBeatmapDifficultyInfo
|
||||||
|
{
|
||||||
|
public float DrainRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
|
||||||
|
public float CircleSize { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
|
||||||
|
public float OverallDifficulty { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
|
||||||
|
public float ApproachRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
|
||||||
|
|
||||||
|
public double SliderMultiplier { get; set; } = 1;
|
||||||
|
public double SliderTickRate { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a shallow-clone of this <see cref="RealmBeatmapDifficulty"/>.
|
||||||
|
/// </summary>
|
||||||
|
public RealmBeatmapDifficulty Clone()
|
||||||
|
{
|
||||||
|
var diff = new RealmBeatmapDifficulty();
|
||||||
|
CopyTo(diff);
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CopyTo(RealmBeatmapDifficulty difficulty)
|
||||||
|
{
|
||||||
|
difficulty.ApproachRate = ApproachRate;
|
||||||
|
difficulty.DrainRate = DrainRate;
|
||||||
|
difficulty.CircleSize = CircleSize;
|
||||||
|
difficulty.OverallDifficulty = OverallDifficulty;
|
||||||
|
|
||||||
|
difficulty.SliderMultiplier = SliderMultiplier;
|
||||||
|
difficulty.SliderTickRate = SliderTickRate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
osu.Game/Models/RealmBeatmapMetadata.cs
Normal file
45
osu.Game/Models/RealmBeatmapMetadata.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// 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 Newtonsoft.Json;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Models
|
||||||
|
{
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
|
[Serializable]
|
||||||
|
[MapTo("BeatmapMetadata")]
|
||||||
|
public class RealmBeatmapMetadata : RealmObject, IBeatmapMetadataInfo
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty("title_unicode")]
|
||||||
|
public string TitleUnicode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Artist { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty("artist_unicode")]
|
||||||
|
public string ArtistUnicode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Author { get; set; } = string.Empty; // eventually should be linked to a persisted User.
|
||||||
|
|
||||||
|
public string Source { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty(@"tags")]
|
||||||
|
public string Tags { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The time in milliseconds to begin playing the track for preview purposes.
|
||||||
|
/// If -1, the track should begin playing at 40% of its length.
|
||||||
|
/// </summary>
|
||||||
|
public int PreviewTime { get; set; }
|
||||||
|
|
||||||
|
public string AudioFile { get; set; } = string.Empty;
|
||||||
|
public string BackgroundFile { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
78
osu.Game/Models/RealmBeatmapSet.cs
Normal file
78
osu.Game/Models/RealmBeatmapSet.cs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Models
|
||||||
|
{
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
|
[MapTo("BeatmapSet")]
|
||||||
|
public class RealmBeatmapSet : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable<RealmBeatmapSet>, IBeatmapSetInfo
|
||||||
|
{
|
||||||
|
[PrimaryKey]
|
||||||
|
public Guid ID { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public int? OnlineID { get; set; }
|
||||||
|
|
||||||
|
public DateTimeOffset DateAdded { get; set; }
|
||||||
|
|
||||||
|
public IBeatmapMetadataInfo? Metadata => Beatmaps.FirstOrDefault()?.Metadata;
|
||||||
|
|
||||||
|
public IList<RealmBeatmap> Beatmaps { get; } = null!;
|
||||||
|
|
||||||
|
public IList<RealmNamedFileUsage> Files { get; } = null!;
|
||||||
|
|
||||||
|
public bool DeletePending { get; set; }
|
||||||
|
|
||||||
|
public string Hash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether deleting this beatmap set should be prohibited (due to it being a system requirement to be present).
|
||||||
|
/// </summary>
|
||||||
|
public bool Protected { get; set; }
|
||||||
|
|
||||||
|
public double MaxStarDifficulty => Beatmaps.Max(b => b.StarRating);
|
||||||
|
|
||||||
|
public double MaxLength => Beatmaps.Max(b => b.Length);
|
||||||
|
|
||||||
|
public double MaxBPM => Beatmaps.Max(b => b.BPM);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null.
|
||||||
|
/// The path returned is relative to the user file storage.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">The name of the file to get the storage path of.</param>
|
||||||
|
public string? GetPathForFile(string filename) => Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.StoragePath;
|
||||||
|
|
||||||
|
public override string ToString() => Metadata?.ToString() ?? base.ToString();
|
||||||
|
|
||||||
|
public bool Equals(RealmBeatmapSet? other)
|
||||||
|
{
|
||||||
|
if (other == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (IsManaged && other.IsManaged)
|
||||||
|
return ID == other.ID;
|
||||||
|
|
||||||
|
if (OnlineID.HasValue && other.OnlineID.HasValue)
|
||||||
|
return OnlineID == other.OnlineID;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash))
|
||||||
|
return Hash == other.Hash;
|
||||||
|
|
||||||
|
return ReferenceEquals(this, other);
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerable<IBeatmapInfo> IBeatmapSetInfo.Beatmaps => Beatmaps;
|
||||||
|
|
||||||
|
IEnumerable<INamedFileUsage> IBeatmapSetInfo.Files => Files;
|
||||||
|
}
|
||||||
|
}
|
22
osu.Game/Models/RealmFile.cs
Normal file
22
osu.Game/Models/RealmFile.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.IO;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Models
|
||||||
|
{
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
|
[MapTo("File")]
|
||||||
|
public class RealmFile : RealmObject, IFileInfo
|
||||||
|
{
|
||||||
|
[PrimaryKey]
|
||||||
|
public string Hash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string StoragePath => Path.Combine(Hash.Remove(1), Hash.Remove(2), Hash);
|
||||||
|
}
|
||||||
|
}
|
34
osu.Game/Models/RealmNamedFileUsage.cs
Normal file
34
osu.Game/Models/RealmNamedFileUsage.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// 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.Framework.Testing;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.IO;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Models
|
||||||
|
{
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
|
public class RealmNamedFileUsage : EmbeddedObject, INamedFile, INamedFileUsage
|
||||||
|
{
|
||||||
|
public RealmFile File { get; set; } = null!;
|
||||||
|
|
||||||
|
public string Filename { get; set; } = null!;
|
||||||
|
|
||||||
|
public RealmNamedFileUsage(RealmFile file, string filename)
|
||||||
|
{
|
||||||
|
File = file;
|
||||||
|
Filename = filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
private RealmNamedFileUsage()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
IFileInfo INamedFileUsage.File => File;
|
||||||
|
}
|
||||||
|
}
|
63
osu.Game/Models/RealmRuleset.cs
Normal file
63
osu.Game/Models/RealmRuleset.cs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// 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 JetBrains.Annotations;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Models
|
||||||
|
{
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
|
[MapTo("Ruleset")]
|
||||||
|
public class RealmRuleset : RealmObject, IEquatable<RealmRuleset>, IRulesetInfo
|
||||||
|
{
|
||||||
|
[PrimaryKey]
|
||||||
|
public string ShortName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public int? OnlineID { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string InstantiationInfo { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public RealmRuleset(string shortName, string name, string instantiationInfo, int? onlineID = null)
|
||||||
|
{
|
||||||
|
ShortName = shortName;
|
||||||
|
Name = name;
|
||||||
|
InstantiationInfo = instantiationInfo;
|
||||||
|
OnlineID = onlineID;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
private RealmRuleset()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmRuleset(int? onlineID, string name, string shortName, bool available)
|
||||||
|
{
|
||||||
|
OnlineID = onlineID;
|
||||||
|
Name = name;
|
||||||
|
ShortName = shortName;
|
||||||
|
Available = available;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Available { get; set; }
|
||||||
|
|
||||||
|
public bool Equals(RealmRuleset? other) => other != null && OnlineID == other.OnlineID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo;
|
||||||
|
|
||||||
|
public override string ToString() => Name;
|
||||||
|
|
||||||
|
public RealmRuleset Clone() => new RealmRuleset
|
||||||
|
{
|
||||||
|
OnlineID = OnlineID,
|
||||||
|
Name = Name,
|
||||||
|
ShortName = ShortName,
|
||||||
|
InstantiationInfo = InstantiationInfo,
|
||||||
|
Available = Available
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -130,12 +130,6 @@ namespace osu.Game.Online.Rooms
|
|||||||
set => MaxAttempts.Value = value;
|
set => MaxAttempts.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The position of this <see cref="Room"/> in the list. This is not read from or written to the API.
|
|
||||||
/// </summary>
|
|
||||||
[JsonIgnore]
|
|
||||||
public readonly Bindable<long> Position = new Bindable<long>(-1); // Todo: This does not need to exist.
|
|
||||||
|
|
||||||
public Room()
|
public Room()
|
||||||
{
|
{
|
||||||
Password.BindValueChanged(p => HasPassword.Value = !string.IsNullOrEmpty(p.NewValue));
|
Password.BindValueChanged(p => HasPassword.Value = !string.IsNullOrEmpty(p.NewValue));
|
||||||
@ -192,8 +186,6 @@ namespace osu.Game.Online.Rooms
|
|||||||
RecentParticipants.Clear();
|
RecentParticipants.Clear();
|
||||||
RecentParticipants.AddRange(other.RecentParticipants);
|
RecentParticipants.AddRange(other.RecentParticipants);
|
||||||
}
|
}
|
||||||
|
|
||||||
Position.Value = other.Position.Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveExpiredPlaylistItems()
|
public void RemoveExpiredPlaylistItems()
|
||||||
|
@ -211,13 +211,6 @@ namespace osu.Game
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
if (args?.Length > 0)
|
|
||||||
{
|
|
||||||
var paths = args.Where(a => !a.StartsWith('-')).ToArray();
|
|
||||||
if (paths.Length > 0)
|
|
||||||
Task.Run(() => Import(paths));
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies.CacheAs(this);
|
dependencies.CacheAs(this);
|
||||||
|
|
||||||
dependencies.Cache(SentryLogger);
|
dependencies.Cache(SentryLogger);
|
||||||
@ -867,6 +860,19 @@ namespace osu.Game
|
|||||||
{
|
{
|
||||||
if (mode.NewValue != OverlayActivation.All) CloseAllOverlays();
|
if (mode.NewValue != OverlayActivation.All) CloseAllOverlays();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Importantly, this should be run after binding PostNotification to the import handlers so they can present the import after game startup.
|
||||||
|
handleStartupImport();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleStartupImport()
|
||||||
|
{
|
||||||
|
if (args?.Length > 0)
|
||||||
|
{
|
||||||
|
var paths = args.Where(a => !a.StartsWith('-')).ToArray();
|
||||||
|
if (paths.Length > 0)
|
||||||
|
Task.Run(() => Import(paths));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays)
|
private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays)
|
||||||
|
@ -55,7 +55,10 @@ namespace osu.Game.Rulesets.UI
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The current direction of playback to be exposed to frame stable children.
|
/// The current direction of playback to be exposed to frame stable children.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private int direction;
|
/// <remarks>
|
||||||
|
/// Initially it is presumed that playback will proceed in the forward direction.
|
||||||
|
/// </remarks>
|
||||||
|
private int direction = 1;
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(GameplayClock clock, ISamplePlaybackDisabler sampleDisabler)
|
private void load(GameplayClock clock, ISamplePlaybackDisabler sampleDisabler)
|
||||||
@ -139,7 +142,9 @@ namespace osu.Game.Rulesets.UI
|
|||||||
state = PlaybackState.NotValid;
|
state = PlaybackState.NotValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state == PlaybackState.Valid)
|
// if the proposed time is the same as the current time, assume that the clock will continue progressing in the same direction as previously.
|
||||||
|
// this avoids spurious flips in direction from -1 to 1 during rewinds.
|
||||||
|
if (state == PlaybackState.Valid && proposedTime != manualClock.CurrentTime)
|
||||||
direction = proposedTime >= manualClock.CurrentTime ? 1 : -1;
|
direction = proposedTime >= manualClock.CurrentTime ? 1 : -1;
|
||||||
|
|
||||||
double timeBehind = Math.Abs(proposedTime - parentGameplayClock.CurrentTime);
|
double timeBehind = Math.Abs(proposedTime - parentGameplayClock.CurrentTime);
|
||||||
|
@ -116,8 +116,6 @@ namespace osu.Game.Screens.OnlinePlay.Components
|
|||||||
if (ignoredRooms.Contains(room.RoomID.Value.Value))
|
if (ignoredRooms.Contains(room.RoomID.Value.Value))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
room.Position.Value = -room.RoomID.Value.Value;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var pi in room.Playlist)
|
foreach (var pi in room.Playlist)
|
||||||
|
@ -129,7 +129,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
|||||||
private void updateSorting()
|
private void updateSorting()
|
||||||
{
|
{
|
||||||
foreach (var room in roomFlow)
|
foreach (var room in roomFlow)
|
||||||
roomFlow.SetLayoutPosition(room, room.Room.Position.Value);
|
roomFlow.SetLayoutPosition(room, -(room.Room.RoomID.Value ?? 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnClick(ClickEvent e)
|
protected override bool OnClick(ClickEvent e)
|
||||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Screens.Play
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manage the animation to be applied when a player fails.
|
/// Manage the animation to be applied when a player fails.
|
||||||
/// Single file; automatically disposed after use.
|
/// Single use and automatically disposed after use.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class FailAnimation : CompositeDrawable
|
public class FailAnimation : CompositeDrawable
|
||||||
{
|
{
|
||||||
|
@ -35,6 +35,8 @@ namespace osu.Game.Screens.Play
|
|||||||
{
|
{
|
||||||
protected const float BACKGROUND_BLUR = 15;
|
protected const float BACKGROUND_BLUR = 15;
|
||||||
|
|
||||||
|
private const double content_out_duration = 300;
|
||||||
|
|
||||||
public override bool HideOverlaysOnEnter => hideOverlays;
|
public override bool HideOverlaysOnEnter => hideOverlays;
|
||||||
|
|
||||||
public override bool DisallowExternalBeatmapRulesetChanges => true;
|
public override bool DisallowExternalBeatmapRulesetChanges => true;
|
||||||
@ -135,7 +137,9 @@ namespace osu.Game.Screens.Play
|
|||||||
muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce);
|
muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce);
|
||||||
batteryWarningShownOnce = sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce);
|
batteryWarningShownOnce = sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce);
|
||||||
|
|
||||||
InternalChild = (content = new LogoTrackingContainer
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
(content = new LogoTrackingContainer
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@ -163,8 +167,9 @@ namespace osu.Game.Screens.Play
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
idleTracker = new IdleTracker(750),
|
idleTracker = new IdleTracker(750),
|
||||||
|
}),
|
||||||
lowPassFilter = new AudioFilter(audio.TrackMixer)
|
lowPassFilter = new AudioFilter(audio.TrackMixer)
|
||||||
});
|
};
|
||||||
|
|
||||||
if (Beatmap.Value.BeatmapInfo.EpilepsyWarning)
|
if (Beatmap.Value.BeatmapInfo.EpilepsyWarning)
|
||||||
{
|
{
|
||||||
@ -195,7 +200,6 @@ namespace osu.Game.Screens.Play
|
|||||||
epilepsyWarning.DimmableBackground = b;
|
epilepsyWarning.DimmableBackground = b;
|
||||||
});
|
});
|
||||||
|
|
||||||
lowPassFilter.CutoffTo(500, 100, Easing.OutCubic);
|
|
||||||
Beatmap.Value.Track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
|
Beatmap.Value.Track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
|
||||||
|
|
||||||
content.ScaleTo(0.7f);
|
content.ScaleTo(0.7f);
|
||||||
@ -240,15 +244,15 @@ namespace osu.Game.Screens.Play
|
|||||||
public override bool OnExiting(IScreen next)
|
public override bool OnExiting(IScreen next)
|
||||||
{
|
{
|
||||||
cancelLoad();
|
cancelLoad();
|
||||||
|
contentOut();
|
||||||
|
|
||||||
content.ScaleTo(0.7f, 150, Easing.InQuint);
|
// Ensure the screen doesn't expire until all the outwards fade operations have completed.
|
||||||
this.FadeOut(150);
|
this.Delay(content_out_duration).FadeOut();
|
||||||
|
|
||||||
ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
|
ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
|
||||||
|
|
||||||
BackgroundBrightnessReduction = false;
|
BackgroundBrightnessReduction = false;
|
||||||
Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
|
Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
|
||||||
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
|
|
||||||
|
|
||||||
return base.OnExiting(next);
|
return base.OnExiting(next);
|
||||||
}
|
}
|
||||||
@ -344,6 +348,7 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
content.FadeInFromZero(400);
|
content.FadeInFromZero(400);
|
||||||
content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer);
|
content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer);
|
||||||
|
lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint);
|
||||||
|
|
||||||
ApplyToBackground(b => b?.FadeColour(Color4.White, 800, Easing.OutQuint));
|
ApplyToBackground(b => b?.FadeColour(Color4.White, 800, Easing.OutQuint));
|
||||||
}
|
}
|
||||||
@ -353,8 +358,9 @@ namespace osu.Game.Screens.Play
|
|||||||
// Ensure the logo is no longer tracking before we scale the content
|
// Ensure the logo is no longer tracking before we scale the content
|
||||||
content.StopTracking();
|
content.StopTracking();
|
||||||
|
|
||||||
content.ScaleTo(0.7f, 300, Easing.InQuint);
|
content.ScaleTo(0.7f, content_out_duration * 2, Easing.OutQuint);
|
||||||
content.FadeOut(250);
|
content.FadeOut(content_out_duration, Easing.OutQuint);
|
||||||
|
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, content_out_duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void pushWhenLoaded()
|
private void pushWhenLoaded()
|
||||||
@ -381,7 +387,7 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
contentOut();
|
contentOut();
|
||||||
|
|
||||||
TransformSequence<PlayerLoader> pushSequence = this.Delay(250);
|
TransformSequence<PlayerLoader> pushSequence = this.Delay(content_out_duration);
|
||||||
|
|
||||||
// only show if the warning was created (i.e. the beatmap needs it)
|
// only show if the warning was created (i.e. the beatmap needs it)
|
||||||
// and this is not a restart of the map (the warning expires after first load).
|
// and this is not a restart of the map (the warning expires after first load).
|
||||||
@ -400,6 +406,11 @@ namespace osu.Game.Screens.Play
|
|||||||
})
|
})
|
||||||
.Delay(EpilepsyWarning.FADE_DURATION);
|
.Delay(EpilepsyWarning.FADE_DURATION);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This goes hand-in-hand with the restoration of low pass filter in contentOut().
|
||||||
|
this.TransformBindableTo(volumeAdjustment, 0, content_out_duration, Easing.OutCubic);
|
||||||
|
}
|
||||||
|
|
||||||
pushSequence.Schedule(() =>
|
pushSequence.Schedule(() =>
|
||||||
{
|
{
|
||||||
|
116
osu.Game/Stores/RealmFileStore.cs
Normal file
116
osu.Game/Stores/RealmFileStore.cs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
// 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.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Framework.IO.Stores;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Framework.Platform;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Models;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Stores
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the storing of files to the file system (and database) backing.
|
||||||
|
/// </summary>
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
|
public class RealmFileStore
|
||||||
|
{
|
||||||
|
private readonly RealmContextFactory realmFactory;
|
||||||
|
|
||||||
|
public readonly IResourceStore<byte[]> Store;
|
||||||
|
|
||||||
|
public readonly Storage Storage;
|
||||||
|
|
||||||
|
public RealmFileStore(RealmContextFactory realmFactory, Storage storage)
|
||||||
|
{
|
||||||
|
this.realmFactory = realmFactory;
|
||||||
|
|
||||||
|
Storage = storage.GetStorageForDirectory(@"files");
|
||||||
|
Store = new StorageBackedResourceStore(Storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a new file to the game-wide database, copying it to permanent storage if not already present.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The file data stream.</param>
|
||||||
|
/// <param name="realm">The realm instance to add to. Should already be in a transaction.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public RealmFile Add(Stream data, Realm realm)
|
||||||
|
{
|
||||||
|
string hash = data.ComputeSHA2Hash();
|
||||||
|
|
||||||
|
var existing = realm.Find<RealmFile>(hash);
|
||||||
|
|
||||||
|
var file = existing ?? new RealmFile { Hash = hash };
|
||||||
|
|
||||||
|
if (!checkFileExistsAndMatchesHash(file))
|
||||||
|
copyToStore(file, data);
|
||||||
|
|
||||||
|
if (!file.IsManaged)
|
||||||
|
realm.Add(file);
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyToStore(RealmFile file, Stream data)
|
||||||
|
{
|
||||||
|
data.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
using (var output = Storage.GetStream(file.StoragePath, FileAccess.Write))
|
||||||
|
data.CopyTo(output);
|
||||||
|
|
||||||
|
data.Seek(0, SeekOrigin.Begin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool checkFileExistsAndMatchesHash(RealmFile file)
|
||||||
|
{
|
||||||
|
string path = file.StoragePath;
|
||||||
|
|
||||||
|
// we may be re-adding a file to fix missing store entries.
|
||||||
|
if (!Storage.Exists(path))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// even if the file already exists, check the existing checksum for safety.
|
||||||
|
using (var stream = Storage.GetStream(path))
|
||||||
|
return stream.ComputeSHA2Hash() == file.Hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cleanup()
|
||||||
|
{
|
||||||
|
var realm = realmFactory.Context;
|
||||||
|
|
||||||
|
// can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal.
|
||||||
|
using (var transaction = realm.BeginWrite())
|
||||||
|
{
|
||||||
|
// TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707)
|
||||||
|
var files = realm.All<RealmFile>().ToList();
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
if (file.BacklinksCount > 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Storage.Delete(file.StoragePath);
|
||||||
|
realm.Remove(file);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Error(e, $@"Could not delete databased file {file.Hash}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -78,9 +78,11 @@ namespace osu.Game.Tests.Visual
|
|||||||
|
|
||||||
protected void CreateGame()
|
protected void CreateGame()
|
||||||
{
|
{
|
||||||
AddGame(Game = new TestOsuGame(LocalStorage, API));
|
AddGame(Game = CreateTestGame());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API);
|
||||||
|
|
||||||
protected void PushAndConfirm(Func<Screen> newScreen)
|
protected void PushAndConfirm(Func<Screen> newScreen)
|
||||||
{
|
{
|
||||||
Screen screen = null;
|
Screen screen = null;
|
||||||
@ -135,7 +137,8 @@ namespace osu.Game.Tests.Visual
|
|||||||
|
|
||||||
public new void PerformFromScreen(Action<IScreen> action, IEnumerable<Type> validScreens = null) => base.PerformFromScreen(action, validScreens);
|
public new void PerformFromScreen(Action<IScreen> action, IEnumerable<Type> validScreens = null) => base.PerformFromScreen(action, validScreens);
|
||||||
|
|
||||||
public TestOsuGame(Storage storage, IAPIProvider api)
|
public TestOsuGame(Storage storage, IAPIProvider api, string[] args = null)
|
||||||
|
: base(args)
|
||||||
{
|
{
|
||||||
Storage = storage;
|
Storage = storage;
|
||||||
API = api;
|
API = api;
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Realm" Version="10.6.0" />
|
<PackageReference Include="Realm" Version="10.6.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2021.1012.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2021.1013.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.9.4" />
|
<PackageReference Include="Sentry" Version="3.9.4" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.29.0" />
|
<PackageReference Include="SharpCompress" Version="0.29.0" />
|
||||||
|
@ -70,7 +70,7 @@
|
|||||||
<Reference Include="System.Net.Http" />
|
<Reference Include="System.Net.Http" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.1012.0" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.1013.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
||||||
@ -93,7 +93,7 @@
|
|||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2021.1012.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2021.1013.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.28.3" />
|
<PackageReference Include="SharpCompress" Version="0.28.3" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||||
|
Loading…
Reference in New Issue
Block a user