diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 24c68d392b..8728d776d0 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -63,6 +63,8 @@ namespace osu.Game.Beatmaps
public override string[] HandledExtensions => new[] { ".osz" };
+ protected override string[] HashableFileTypes => new[] { ".osu" };
+
protected override string ImportFromStablePath => "Songs";
private readonly RulesetStore rulesets;
@@ -129,19 +131,6 @@ namespace osu.Game.Beatmaps
beatmaps.ForEach(b => b.OnlineBeatmapID = null);
}
- protected override BeatmapSetInfo CheckForExisting(BeatmapSetInfo model)
- {
- // check if this beatmap has already been imported and exit early if so
- var existingHashMatch = beatmaps.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash);
- if (existingHashMatch != null)
- {
- Undelete(existingHashMatch);
- return existingHashMatch;
- }
-
- return null;
- }
-
///
/// Downloads a beatmap.
/// This will post notifications tracking progress.
@@ -317,20 +306,6 @@ namespace osu.Game.Beatmaps
/// Results from the provided query.
public IQueryable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query);
- ///
- /// Create a SHA-2 hash from the provided archive based on contained beatmap (.osu) file content.
- ///
- private string computeBeatmapSetHash(ArchiveReader reader)
- {
- // for now, concatenate all .osu files in the set to create a unique hash.
- MemoryStream hashable = new MemoryStream();
- foreach (string file in reader.Filenames.Where(f => f.EndsWith(".osu")))
- using (Stream s = reader.GetStream(file))
- s.CopyTo(hashable);
-
- return hashable.ComputeSHA2Hash();
- }
-
protected override BeatmapSetInfo CreateModel(ArchiveReader reader)
{
// let's make sure there are actually .osu files to import.
@@ -349,7 +324,6 @@ namespace osu.Game.Beatmaps
{
OnlineBeatmapSetID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID,
Beatmaps = new List(),
- Hash = computeBeatmapSetHash(reader),
Metadata = beatmap.Metadata,
};
}
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index ec87df3d71..a7c2aad260 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -8,6 +8,7 @@ using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
+using osu.Framework.Extensions;
using osu.Framework.IO.File;
using osu.Framework.Logging;
using osu.Framework.Platform;
@@ -205,7 +206,12 @@ namespace osu.Game.Database
try
{
var model = CreateModel(archive);
- return model == null ? null : Import(model, archive);
+
+ if (model == null) return null;
+
+ model.Hash = computeHash(archive);
+
+ return Import(model, archive);
}
catch (Exception e)
{
@@ -214,6 +220,27 @@ namespace osu.Game.Database
}
}
+ ///
+ /// Any file extensions which should be included in hash creation.
+ /// Generally should include all file types which determine the file's uniqueness.
+ /// Large files should be avoided if possible.
+ ///
+ protected abstract string[] HashableFileTypes { get; }
+
+ ///
+ /// Create a SHA-2 hash from the provided archive based on file content of all files matching .
+ ///
+ private string computeHash(ArchiveReader reader)
+ {
+ // for now, concatenate all .osu files in the set to create a unique hash.
+ MemoryStream hashable = new MemoryStream();
+ foreach (string file in reader.Filenames.Where(f => HashableFileTypes.Any(f.EndsWith)))
+ using (Stream s = reader.GetStream(file))
+ s.CopyTo(hashable);
+
+ return hashable.ComputeSHA2Hash();
+ }
+
///
/// Import an item from a .
///
@@ -237,6 +264,7 @@ namespace osu.Game.Database
if (existing != null)
{
+ Undelete(existing);
Logger.Log($"Found existing {typeof(TModel)} for {item} (ID {existing.ID}). Skipping import.", LoggingTarget.Database);
handleEvent(() => ItemAdded?.Invoke(existing, true));
return existing;
@@ -474,7 +502,12 @@ namespace osu.Game.Database
{
}
- protected virtual TModel CheckForExisting(TModel model) => null;
+ ///
+ /// Check whether an existing model already exists for a new import item.
+ ///
+ /// The new model proposed for import. Note that has not yet been run on this model.
+ /// An existing model which matches the criteria to skip importing, else null.
+ protected virtual TModel CheckForExisting(TModel model) => ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash);
private DbSet queryModel() => ContextFactory.Get().Set();
diff --git a/osu.Game/Database/IHasFiles.cs b/osu.Game/Database/IHasFiles.cs
index 6ccb1f5722..b5ac22efc9 100644
--- a/osu.Game/Database/IHasFiles.cs
+++ b/osu.Game/Database/IHasFiles.cs
@@ -13,5 +13,7 @@ namespace osu.Game.Database
where TFile : INamedFileInfo
{
List Files { get; set; }
+
+ string Hash { get; set; }
}
}
diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs
index 7ffde8bf0e..db5a2771d4 100644
--- a/osu.Game/Database/OsuDbContext.cs
+++ b/osu.Game/Database/OsuDbContext.cs
@@ -107,6 +107,9 @@ namespace osu.Game.Database
modelBuilder.Entity().HasIndex(b => b.DeletePending);
modelBuilder.Entity().HasIndex(b => b.Hash).IsUnique();
+ modelBuilder.Entity().HasIndex(b => b.Hash).IsUnique();
+ modelBuilder.Entity().HasIndex(b => b.DeletePending);
+
modelBuilder.Entity().HasIndex(b => new { b.RulesetID, b.Variant });
modelBuilder.Entity().HasIndex(b => b.IntAction);
diff --git a/osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs
new file mode 100644
index 0000000000..120674671a
--- /dev/null
+++ b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs
@@ -0,0 +1,387 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using osu.Game.Database;
+
+namespace osu.Game.Migrations
+{
+ [DbContext(typeof(OsuDbContext))]
+ [Migration("20181128100659_AddSkinInfoHash")]
+ partial class AddSkinInfoHash
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "2.1.4-rtm-31024");
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("ApproachRate");
+
+ b.Property("CircleSize");
+
+ b.Property("DrainRate");
+
+ b.Property("OverallDifficulty");
+
+ b.Property("SliderMultiplier");
+
+ b.Property("SliderTickRate");
+
+ b.HasKey("ID");
+
+ b.ToTable("BeatmapDifficulty");
+ });
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("AudioLeadIn");
+
+ b.Property("BaseDifficultyID");
+
+ b.Property("BeatDivisor");
+
+ b.Property("BeatmapSetInfoID");
+
+ b.Property("Countdown");
+
+ b.Property("DistanceSpacing");
+
+ b.Property("GridSize");
+
+ b.Property("Hash");
+
+ b.Property("Hidden");
+
+ b.Property("LetterboxInBreaks");
+
+ b.Property("MD5Hash");
+
+ b.Property("MetadataID");
+
+ b.Property("OnlineBeatmapID");
+
+ b.Property("Path");
+
+ b.Property("RulesetID");
+
+ b.Property("SpecialStyle");
+
+ b.Property("StackLeniency");
+
+ b.Property("StarDifficulty");
+
+ b.Property("Status");
+
+ b.Property("StoredBookmarks");
+
+ b.Property("TimelineZoom");
+
+ b.Property("Version");
+
+ b.Property("WidescreenStoryboard");
+
+ b.HasKey("ID");
+
+ b.HasIndex("BaseDifficultyID");
+
+ b.HasIndex("BeatmapSetInfoID");
+
+ b.HasIndex("Hash");
+
+ b.HasIndex("MD5Hash");
+
+ b.HasIndex("MetadataID");
+
+ b.HasIndex("OnlineBeatmapID")
+ .IsUnique();
+
+ b.HasIndex("RulesetID");
+
+ b.ToTable("BeatmapInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("Artist");
+
+ b.Property("ArtistUnicode");
+
+ b.Property("AudioFile");
+
+ b.Property("AuthorString")
+ .HasColumnName("Author");
+
+ b.Property("BackgroundFile");
+
+ b.Property("PreviewTime");
+
+ b.Property("Source");
+
+ b.Property("Tags");
+
+ b.Property("Title");
+
+ b.Property("TitleUnicode");
+
+ b.HasKey("ID");
+
+ b.ToTable("BeatmapMetadata");
+ });
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("BeatmapSetInfoID");
+
+ b.Property("FileInfoID");
+
+ b.Property("Filename")
+ .IsRequired();
+
+ b.HasKey("ID");
+
+ b.HasIndex("BeatmapSetInfoID");
+
+ b.HasIndex("FileInfoID");
+
+ b.ToTable("BeatmapSetFileInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("DeletePending");
+
+ b.Property("Hash");
+
+ b.Property("MetadataID");
+
+ b.Property("OnlineBeatmapSetID");
+
+ b.Property("Protected");
+
+ b.Property("Status");
+
+ b.HasKey("ID");
+
+ b.HasIndex("DeletePending");
+
+ b.HasIndex("Hash")
+ .IsUnique();
+
+ b.HasIndex("MetadataID");
+
+ b.HasIndex("OnlineBeatmapSetID")
+ .IsUnique();
+
+ b.ToTable("BeatmapSetInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("IntKey")
+ .HasColumnName("Key");
+
+ b.Property("RulesetID");
+
+ b.Property("StringValue")
+ .HasColumnName("Value");
+
+ b.Property("Variant");
+
+ b.HasKey("ID");
+
+ b.HasIndex("RulesetID", "Variant");
+
+ b.ToTable("Settings");
+ });
+
+ modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("IntAction")
+ .HasColumnName("Action");
+
+ b.Property("KeysString")
+ .HasColumnName("Keys");
+
+ b.Property("RulesetID");
+
+ b.Property("Variant");
+
+ b.HasKey("ID");
+
+ b.HasIndex("IntAction");
+
+ b.HasIndex("RulesetID", "Variant");
+
+ b.ToTable("KeyBinding");
+ });
+
+ modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("Hash");
+
+ b.Property("ReferenceCount");
+
+ b.HasKey("ID");
+
+ b.HasIndex("Hash")
+ .IsUnique();
+
+ b.HasIndex("ReferenceCount");
+
+ b.ToTable("FileInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("Available");
+
+ b.Property("InstantiationInfo");
+
+ b.Property("Name");
+
+ b.Property("ShortName");
+
+ b.HasKey("ID");
+
+ b.HasIndex("Available");
+
+ b.HasIndex("ShortName")
+ .IsUnique();
+
+ b.ToTable("RulesetInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("FileInfoID");
+
+ b.Property("Filename")
+ .IsRequired();
+
+ b.Property("SkinInfoID");
+
+ b.HasKey("ID");
+
+ b.HasIndex("FileInfoID");
+
+ b.HasIndex("SkinInfoID");
+
+ b.ToTable("SkinFileInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("Creator");
+
+ b.Property("DeletePending");
+
+ b.Property("Hash");
+
+ b.Property("Name");
+
+ b.HasKey("ID");
+
+ b.HasIndex("DeletePending");
+
+ b.HasIndex("Hash")
+ .IsUnique();
+
+ b.ToTable("SkinInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
+ {
+ b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
+ .WithMany()
+ .HasForeignKey("BaseDifficultyID")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
+ .WithMany("Beatmaps")
+ .HasForeignKey("BeatmapSetInfoID")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
+ .WithMany("Beatmaps")
+ .HasForeignKey("MetadataID");
+
+ b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
+ .WithMany()
+ .HasForeignKey("RulesetID")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
+ {
+ b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
+ .WithMany("Files")
+ .HasForeignKey("BeatmapSetInfoID")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
+ .WithMany()
+ .HasForeignKey("FileInfoID")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
+ {
+ b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
+ .WithMany("BeatmapSets")
+ .HasForeignKey("MetadataID");
+ });
+
+ modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
+ {
+ b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
+ .WithMany()
+ .HasForeignKey("FileInfoID")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.HasOne("osu.Game.Skinning.SkinInfo")
+ .WithMany("Files")
+ .HasForeignKey("SkinInfoID")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs
new file mode 100644
index 0000000000..860264a7dd
--- /dev/null
+++ b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs
@@ -0,0 +1,41 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace osu.Game.Migrations
+{
+ public partial class AddSkinInfoHash : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "Hash",
+ table: "SkinInfo",
+ nullable: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_SkinInfo_DeletePending",
+ table: "SkinInfo",
+ column: "DeletePending");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_SkinInfo_Hash",
+ table: "SkinInfo",
+ column: "Hash",
+ unique: true);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "IX_SkinInfo_DeletePending",
+ table: "SkinInfo");
+
+ migrationBuilder.DropIndex(
+ name: "IX_SkinInfo_Hash",
+ table: "SkinInfo");
+
+ migrationBuilder.DropColumn(
+ name: "Hash",
+ table: "SkinInfo");
+ }
+ }
+}
diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs
index abd6c43499..ba45fa9e36 100644
--- a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs
+++ b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs
@@ -384,10 +384,17 @@ namespace osu.Game.Migrations
b.Property("DeletePending");
+ b.Property("Hash");
+
b.Property("Name");
b.HasKey("ID");
+ b.HasIndex("DeletePending");
+
+ b.HasIndex("Hash")
+ .IsUnique();
+
b.ToTable("SkinInfo");
});
diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs
index 7d68f32495..d2e51d1f57 100644
--- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs
+++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs
@@ -13,6 +13,7 @@ using osu.Framework.Timing;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
+using osuTK.Input;
namespace osu.Game.Screens.Edit.Components
{
@@ -63,6 +64,18 @@ namespace osu.Game.Screens.Edit.Components
tabs.Current.ValueChanged += newValue => Beatmap.Value.Track.Tempo.Value = newValue;
}
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ switch (e.Key)
+ {
+ case Key.Space:
+ togglePause();
+ return true;
+ }
+
+ return base.OnKeyDown(e);
+ }
+
private void togglePause()
{
if (adjustableClock.IsRunning)
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index c4fb9dc419..9bccefc508 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -20,6 +20,7 @@ using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Design;
+using osuTK.Input;
namespace osu.Game.Screens.Edit
{
@@ -157,29 +158,19 @@ namespace osu.Game.Screens.Edit
bottomBackground.Colour = colours.Gray2;
}
- private void exportBeatmap()
+ protected override bool OnKeyDown(KeyDownEvent e)
{
- host.OpenFileExternally(Beatmap.Value.Save());
- }
-
- private void onModeChanged(EditorScreenMode mode)
- {
- currentScreen?.Exit();
-
- switch (mode)
+ switch (e.Key)
{
- case EditorScreenMode.Compose:
- currentScreen = new ComposeScreen();
- break;
- case EditorScreenMode.Design:
- currentScreen = new DesignScreen();
- break;
- default:
- currentScreen = new EditorScreen();
- break;
+ case Key.Left:
+ seek(e, -1);
+ return true;
+ case Key.Right:
+ seek(e, 1);
+ return true;
}
- LoadComponentAsync(currentScreen, screenContainer.Add);
+ return base.OnKeyDown(e);
}
private double scrollAccumulation;
@@ -193,9 +184,9 @@ namespace osu.Game.Screens.Edit
while (Math.Abs(scrollAccumulation) > precision)
{
if (scrollAccumulation > 0)
- clock.SeekBackward(!clock.IsRunning);
+ seek(e, -1);
else
- clock.SeekForward(!clock.IsRunning);
+ seek(e, 1);
scrollAccumulation = scrollAccumulation < 0 ? Math.Min(0, scrollAccumulation + precision) : Math.Max(0, scrollAccumulation - precision);
}
@@ -224,7 +215,40 @@ namespace osu.Game.Screens.Edit
Beatmap.Value.Track.Tempo.Value = 1;
Beatmap.Value.Track.Start();
}
+
return base.OnExiting(next);
}
+
+ private void exportBeatmap() => host.OpenFileExternally(Beatmap.Value.Save());
+
+ private void onModeChanged(EditorScreenMode mode)
+ {
+ currentScreen?.Exit();
+
+ switch (mode)
+ {
+ case EditorScreenMode.Compose:
+ currentScreen = new ComposeScreen();
+ break;
+ case EditorScreenMode.Design:
+ currentScreen = new DesignScreen();
+ break;
+ default:
+ currentScreen = new EditorScreen();
+ break;
+ }
+
+ LoadComponentAsync(currentScreen, screenContainer.Add);
+ }
+
+ private void seek(UIEvent e, int direction)
+ {
+ double amount = e.ShiftPressed ? 2 : 1;
+
+ if (direction < 1)
+ clock.SeekBackward(!clock.IsRunning, amount);
+ else
+ clock.SeekForward(!clock.IsRunning, amount);
+ }
}
}
diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs
index 465e3a43e2..aa30b1a9f5 100644
--- a/osu.Game/Screens/Edit/EditorClock.cs
+++ b/osu.Game/Screens/Edit/EditorClock.cs
@@ -68,16 +68,20 @@ namespace osu.Game.Screens.Edit
/// Seeks backwards by one beat length.
///
/// Whether to snap to the closest beat after seeking.
- public void SeekBackward(bool snapped = false) => seek(-1, snapped);
+ /// The relative amount (magnitude) which should be seeked.
+ public void SeekBackward(bool snapped = false, double amount = 1) => seek(-1, snapped, amount);
///
/// Seeks forwards by one beat length.
///
/// Whether to snap to the closest beat after seeking.
- public void SeekForward(bool snapped = false) => seek(1, snapped);
+ /// The relative amount (magnitude) which should be seeked.
+ public void SeekForward(bool snapped = false, double amount = 1) => seek(1, snapped, amount);
- private void seek(int direction, bool snapped)
+ private void seek(int direction, bool snapped, double amount = 1)
{
+ if (amount <= 0) throw new ArgumentException("Value should be greater than zero", nameof(amount));
+
var timingPoint = ControlPointInfo.TimingPointAt(CurrentTime);
if (direction < 0 && timingPoint.Time == CurrentTime)
{
@@ -87,7 +91,7 @@ namespace osu.Game.Screens.Edit
timingPoint = ControlPointInfo.TimingPoints[--activeIndex];
}
- double seekAmount = timingPoint.BeatLength / beatDivisor;
+ double seekAmount = timingPoint.BeatLength / beatDivisor * amount;
double seekTime = CurrentTime + seekAmount * direction;
if (!snapped || ControlPointInfo.TimingPoints.Count == 0)
diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs
index 423e7c0780..60162ed224 100644
--- a/osu.Game/Skinning/SkinInfo.cs
+++ b/osu.Game/Skinning/SkinInfo.cs
@@ -13,16 +13,20 @@ namespace osu.Game.Skinning
public string Name { get; set; }
+ public string Hash { get; set; }
+
public string Creator { get; set; }
public List Files { get; set; }
public bool DeletePending { get; set; }
+ public string FullName => $"\"{Name}\" by {Creator}";
+
public static SkinInfo Default { get; } = new SkinInfo { Name = "osu!lazer", Creator = "team osu!" };
public bool Equals(SkinInfo other) => other != null && ID == other.ID;
- public override string ToString() => $"\"{Name}\" by {Creator}";
+ public override string ToString() => FullName;
}
}
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index bd694e443a..ce179d43ef 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -26,8 +26,32 @@ namespace osu.Game.Skinning
public override string[] HandledExtensions => new[] { ".osk" };
+ protected override string[] HashableFileTypes => new[] { ".ini" };
+
protected override string ImportFromStablePath => "Skins";
+ public SkinManager(Storage storage, DatabaseContextFactory contextFactory, IIpcHost importHost, AudioManager audio)
+ : base(storage, contextFactory, new SkinStore(contextFactory, storage), importHost)
+ {
+ this.audio = audio;
+
+ ItemRemoved += removedInfo =>
+ {
+ // check the removed skin is not the current user choice. if it is, switch back to default.
+ if (removedInfo.ID == CurrentSkinInfo.Value.ID)
+ CurrentSkinInfo.Value = SkinInfo.Default;
+ };
+
+ CurrentSkinInfo.ValueChanged += info => CurrentSkin.Value = getSkin(info);
+ CurrentSkin.ValueChanged += skin =>
+ {
+ if (skin.SkinInfo != CurrentSkinInfo.Value)
+ throw new InvalidOperationException($"Setting {nameof(CurrentSkin)}'s value directly is not supported. Use {nameof(CurrentSkinInfo)} instead.");
+
+ SourceChanged?.Invoke();
+ };
+ }
+
///
/// Returns a list of all usable s. Includes the special default skin plus all skins from .
///
@@ -45,24 +69,13 @@ namespace osu.Game.Skinning
/// A list of available .
public List GetAllUserSkins() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList();
- protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo
- {
- Name = archive.Name
- };
+ protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name };
protected override void Populate(SkinInfo model, ArchiveReader archive)
{
base.Populate(model, archive);
- populate(model);
- }
- ///
- /// Populate a from its (if possible).
- ///
- ///
- private void populate(SkinInfo model)
- {
- Skin reference = GetSkin(model);
+ Skin reference = getSkin(model);
if (!string.IsNullOrEmpty(reference.Configuration.SkinInfo.Name))
{
model.Name = reference.Configuration.SkinInfo.Name;
@@ -80,7 +93,7 @@ namespace osu.Game.Skinning
///
/// The skin to lookup.
/// A instance correlating to the provided .
- public Skin GetSkin(SkinInfo skinInfo)
+ private Skin getSkin(SkinInfo skinInfo)
{
if (skinInfo == SkinInfo.Default)
return new DefaultSkin();
@@ -88,28 +101,6 @@ namespace osu.Game.Skinning
return new LegacySkin(skinInfo, Files.Store, audio);
}
- public SkinManager(Storage storage, DatabaseContextFactory contextFactory, IIpcHost importHost, AudioManager audio)
- : base(storage, contextFactory, new SkinStore(contextFactory, storage), importHost)
- {
- this.audio = audio;
-
- ItemRemoved += removedInfo =>
- {
- // check the removed skin is not the current user choice. if it is, switch back to default.
- if (removedInfo.ID == CurrentSkinInfo.Value.ID)
- CurrentSkinInfo.Value = SkinInfo.Default;
- };
-
- CurrentSkinInfo.ValueChanged += info => CurrentSkin.Value = GetSkin(info);
- CurrentSkin.ValueChanged += skin =>
- {
- if (skin.SkinInfo != CurrentSkinInfo.Value)
- throw new InvalidOperationException($"Setting {nameof(CurrentSkin)}'s value directly is not supported. Use {nameof(CurrentSkinInfo)} instead.");
-
- SourceChanged?.Invoke();
- };
- }
-
///
/// Perform a lookup query on available s.
///