mirror of
https://github.com/ppy/osu
synced 2025-02-20 04:17:06 +00:00
Merge remote-tracking branch 'upstream/master' into scoredatabase
This commit is contained in:
commit
aa7a665317
@ -63,6 +63,8 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
public override string[] HandledExtensions => new[] { ".osz" };
|
public override string[] HandledExtensions => new[] { ".osz" };
|
||||||
|
|
||||||
|
protected override string[] HashableFileTypes => new[] { ".osu" };
|
||||||
|
|
||||||
protected override string ImportFromStablePath => "Songs";
|
protected override string ImportFromStablePath => "Songs";
|
||||||
|
|
||||||
private readonly RulesetStore rulesets;
|
private readonly RulesetStore rulesets;
|
||||||
@ -129,19 +131,6 @@ namespace osu.Game.Beatmaps
|
|||||||
beatmaps.ForEach(b => b.OnlineBeatmapID = null);
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Downloads a beatmap.
|
/// Downloads a beatmap.
|
||||||
/// This will post notifications tracking progress.
|
/// This will post notifications tracking progress.
|
||||||
@ -317,20 +306,6 @@ namespace osu.Game.Beatmaps
|
|||||||
/// <returns>Results from the provided query.</returns>
|
/// <returns>Results from the provided query.</returns>
|
||||||
public IQueryable<BeatmapInfo> QueryBeatmaps(Expression<Func<BeatmapInfo, bool>> query) => beatmaps.Beatmaps.AsNoTracking().Where(query);
|
public IQueryable<BeatmapInfo> QueryBeatmaps(Expression<Func<BeatmapInfo, bool>> query) => beatmaps.Beatmaps.AsNoTracking().Where(query);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a SHA-2 hash from the provided archive based on contained beatmap (.osu) file content.
|
|
||||||
/// </summary>
|
|
||||||
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)
|
protected override BeatmapSetInfo CreateModel(ArchiveReader reader)
|
||||||
{
|
{
|
||||||
// let's make sure there are actually .osu files to import.
|
// let's make sure there are actually .osu files to import.
|
||||||
@ -349,7 +324,6 @@ namespace osu.Game.Beatmaps
|
|||||||
{
|
{
|
||||||
OnlineBeatmapSetID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID,
|
OnlineBeatmapSetID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID,
|
||||||
Beatmaps = new List<BeatmapInfo>(),
|
Beatmaps = new List<BeatmapInfo>(),
|
||||||
Hash = computeBeatmapSetHash(reader),
|
|
||||||
Metadata = beatmap.Metadata,
|
Metadata = beatmap.Metadata,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.IO.File;
|
using osu.Framework.IO.File;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
@ -205,7 +206,12 @@ namespace osu.Game.Database
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var model = CreateModel(archive);
|
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)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -214,6 +220,27 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
protected abstract string[] HashableFileTypes { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a SHA-2 hash from the provided archive based on file content of all files matching <see cref="HashableFileTypes"/>.
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Import an item from a <see cref="TModel"/>.
|
/// Import an item from a <see cref="TModel"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -237,6 +264,7 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
if (existing != null)
|
if (existing != null)
|
||||||
{
|
{
|
||||||
|
Undelete(existing);
|
||||||
Logger.Log($"Found existing {typeof(TModel)} for {item} (ID {existing.ID}). Skipping import.", LoggingTarget.Database);
|
Logger.Log($"Found existing {typeof(TModel)} for {item} (ID {existing.ID}). Skipping import.", LoggingTarget.Database);
|
||||||
handleEvent(() => ItemAdded?.Invoke(existing, true));
|
handleEvent(() => ItemAdded?.Invoke(existing, true));
|
||||||
return existing;
|
return existing;
|
||||||
@ -474,7 +502,12 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual TModel CheckForExisting(TModel model) => null;
|
/// <summary>
|
||||||
|
/// Check whether an existing model already exists for a new import item.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="model">The new model proposed for import. Note that <see cref="Populate"/> has not yet been run on this model.</param>
|
||||||
|
/// <returns>An existing model which matches the criteria to skip importing, else null.</returns>
|
||||||
|
protected virtual TModel CheckForExisting(TModel model) => ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash);
|
||||||
|
|
||||||
private DbSet<TModel> queryModel() => ContextFactory.Get().Set<TModel>();
|
private DbSet<TModel> queryModel() => ContextFactory.Get().Set<TModel>();
|
||||||
|
|
||||||
|
@ -13,5 +13,7 @@ namespace osu.Game.Database
|
|||||||
where TFile : INamedFileInfo
|
where TFile : INamedFileInfo
|
||||||
{
|
{
|
||||||
List<TFile> Files { get; set; }
|
List<TFile> Files { get; set; }
|
||||||
|
|
||||||
|
string Hash { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,6 +107,9 @@ namespace osu.Game.Database
|
|||||||
modelBuilder.Entity<BeatmapSetInfo>().HasIndex(b => b.DeletePending);
|
modelBuilder.Entity<BeatmapSetInfo>().HasIndex(b => b.DeletePending);
|
||||||
modelBuilder.Entity<BeatmapSetInfo>().HasIndex(b => b.Hash).IsUnique();
|
modelBuilder.Entity<BeatmapSetInfo>().HasIndex(b => b.Hash).IsUnique();
|
||||||
|
|
||||||
|
modelBuilder.Entity<SkinInfo>().HasIndex(b => b.Hash).IsUnique();
|
||||||
|
modelBuilder.Entity<SkinInfo>().HasIndex(b => b.DeletePending);
|
||||||
|
|
||||||
modelBuilder.Entity<DatabasedKeyBinding>().HasIndex(b => new { b.RulesetID, b.Variant });
|
modelBuilder.Entity<DatabasedKeyBinding>().HasIndex(b => new { b.RulesetID, b.Variant });
|
||||||
modelBuilder.Entity<DatabasedKeyBinding>().HasIndex(b => b.IntAction);
|
modelBuilder.Entity<DatabasedKeyBinding>().HasIndex(b => b.IntAction);
|
||||||
|
|
||||||
|
387
osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs
generated
Normal file
387
osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs
generated
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
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<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<float>("ApproachRate");
|
||||||
|
|
||||||
|
b.Property<float>("CircleSize");
|
||||||
|
|
||||||
|
b.Property<float>("DrainRate");
|
||||||
|
|
||||||
|
b.Property<float>("OverallDifficulty");
|
||||||
|
|
||||||
|
b.Property<double>("SliderMultiplier");
|
||||||
|
|
||||||
|
b.Property<double>("SliderTickRate");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.ToTable("BeatmapDifficulty");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<int>("AudioLeadIn");
|
||||||
|
|
||||||
|
b.Property<int>("BaseDifficultyID");
|
||||||
|
|
||||||
|
b.Property<int>("BeatDivisor");
|
||||||
|
|
||||||
|
b.Property<int>("BeatmapSetInfoID");
|
||||||
|
|
||||||
|
b.Property<bool>("Countdown");
|
||||||
|
|
||||||
|
b.Property<double>("DistanceSpacing");
|
||||||
|
|
||||||
|
b.Property<int>("GridSize");
|
||||||
|
|
||||||
|
b.Property<string>("Hash");
|
||||||
|
|
||||||
|
b.Property<bool>("Hidden");
|
||||||
|
|
||||||
|
b.Property<bool>("LetterboxInBreaks");
|
||||||
|
|
||||||
|
b.Property<string>("MD5Hash");
|
||||||
|
|
||||||
|
b.Property<int?>("MetadataID");
|
||||||
|
|
||||||
|
b.Property<int?>("OnlineBeatmapID");
|
||||||
|
|
||||||
|
b.Property<string>("Path");
|
||||||
|
|
||||||
|
b.Property<int>("RulesetID");
|
||||||
|
|
||||||
|
b.Property<bool>("SpecialStyle");
|
||||||
|
|
||||||
|
b.Property<float>("StackLeniency");
|
||||||
|
|
||||||
|
b.Property<double>("StarDifficulty");
|
||||||
|
|
||||||
|
b.Property<int>("Status");
|
||||||
|
|
||||||
|
b.Property<string>("StoredBookmarks");
|
||||||
|
|
||||||
|
b.Property<double>("TimelineZoom");
|
||||||
|
|
||||||
|
b.Property<string>("Version");
|
||||||
|
|
||||||
|
b.Property<bool>("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<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("Artist");
|
||||||
|
|
||||||
|
b.Property<string>("ArtistUnicode");
|
||||||
|
|
||||||
|
b.Property<string>("AudioFile");
|
||||||
|
|
||||||
|
b.Property<string>("AuthorString")
|
||||||
|
.HasColumnName("Author");
|
||||||
|
|
||||||
|
b.Property<string>("BackgroundFile");
|
||||||
|
|
||||||
|
b.Property<int>("PreviewTime");
|
||||||
|
|
||||||
|
b.Property<string>("Source");
|
||||||
|
|
||||||
|
b.Property<string>("Tags");
|
||||||
|
|
||||||
|
b.Property<string>("Title");
|
||||||
|
|
||||||
|
b.Property<string>("TitleUnicode");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.ToTable("BeatmapMetadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<int>("BeatmapSetInfoID");
|
||||||
|
|
||||||
|
b.Property<int>("FileInfoID");
|
||||||
|
|
||||||
|
b.Property<string>("Filename")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("BeatmapSetInfoID");
|
||||||
|
|
||||||
|
b.HasIndex("FileInfoID");
|
||||||
|
|
||||||
|
b.ToTable("BeatmapSetFileInfo");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<bool>("DeletePending");
|
||||||
|
|
||||||
|
b.Property<string>("Hash");
|
||||||
|
|
||||||
|
b.Property<int?>("MetadataID");
|
||||||
|
|
||||||
|
b.Property<int?>("OnlineBeatmapSetID");
|
||||||
|
|
||||||
|
b.Property<bool>("Protected");
|
||||||
|
|
||||||
|
b.Property<int>("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<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<int>("IntKey")
|
||||||
|
.HasColumnName("Key");
|
||||||
|
|
||||||
|
b.Property<int?>("RulesetID");
|
||||||
|
|
||||||
|
b.Property<string>("StringValue")
|
||||||
|
.HasColumnName("Value");
|
||||||
|
|
||||||
|
b.Property<int?>("Variant");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("RulesetID", "Variant");
|
||||||
|
|
||||||
|
b.ToTable("Settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<int>("IntAction")
|
||||||
|
.HasColumnName("Action");
|
||||||
|
|
||||||
|
b.Property<string>("KeysString")
|
||||||
|
.HasColumnName("Keys");
|
||||||
|
|
||||||
|
b.Property<int?>("RulesetID");
|
||||||
|
|
||||||
|
b.Property<int?>("Variant");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("IntAction");
|
||||||
|
|
||||||
|
b.HasIndex("RulesetID", "Variant");
|
||||||
|
|
||||||
|
b.ToTable("KeyBinding");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("Hash");
|
||||||
|
|
||||||
|
b.Property<int>("ReferenceCount");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Hash")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("ReferenceCount");
|
||||||
|
|
||||||
|
b.ToTable("FileInfo");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<int?>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<bool>("Available");
|
||||||
|
|
||||||
|
b.Property<string>("InstantiationInfo");
|
||||||
|
|
||||||
|
b.Property<string>("Name");
|
||||||
|
|
||||||
|
b.Property<string>("ShortName");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Available");
|
||||||
|
|
||||||
|
b.HasIndex("ShortName")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("RulesetInfo");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<int>("FileInfoID");
|
||||||
|
|
||||||
|
b.Property<string>("Filename")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Property<int>("SkinInfoID");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("FileInfoID");
|
||||||
|
|
||||||
|
b.HasIndex("SkinInfoID");
|
||||||
|
|
||||||
|
b.ToTable("SkinFileInfo");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("Creator");
|
||||||
|
|
||||||
|
b.Property<bool>("DeletePending");
|
||||||
|
|
||||||
|
b.Property<string>("Hash");
|
||||||
|
|
||||||
|
b.Property<string>("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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs
Normal file
41
osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs
Normal file
@ -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<string>(
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -384,10 +384,17 @@ namespace osu.Game.Migrations
|
|||||||
|
|
||||||
b.Property<bool>("DeletePending");
|
b.Property<bool>("DeletePending");
|
||||||
|
|
||||||
|
b.Property<string>("Hash");
|
||||||
|
|
||||||
b.Property<string>("Name");
|
b.Property<string>("Name");
|
||||||
|
|
||||||
b.HasKey("ID");
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("DeletePending");
|
||||||
|
|
||||||
|
b.HasIndex("Hash")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("SkinInfo");
|
b.ToTable("SkinInfo");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ using osu.Framework.Timing;
|
|||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Edit.Components
|
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;
|
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()
|
private void togglePause()
|
||||||
{
|
{
|
||||||
if (adjustableClock.IsRunning)
|
if (adjustableClock.IsRunning)
|
||||||
|
@ -20,6 +20,7 @@ using osu.Game.Screens.Edit.Components;
|
|||||||
using osu.Game.Screens.Edit.Components.Menus;
|
using osu.Game.Screens.Edit.Components.Menus;
|
||||||
using osu.Game.Screens.Edit.Compose;
|
using osu.Game.Screens.Edit.Compose;
|
||||||
using osu.Game.Screens.Edit.Design;
|
using osu.Game.Screens.Edit.Design;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Edit
|
namespace osu.Game.Screens.Edit
|
||||||
{
|
{
|
||||||
@ -157,29 +158,19 @@ namespace osu.Game.Screens.Edit
|
|||||||
bottomBackground.Colour = colours.Gray2;
|
bottomBackground.Colour = colours.Gray2;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exportBeatmap()
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
{
|
{
|
||||||
host.OpenFileExternally(Beatmap.Value.Save());
|
switch (e.Key)
|
||||||
|
{
|
||||||
|
case Key.Left:
|
||||||
|
seek(e, -1);
|
||||||
|
return true;
|
||||||
|
case Key.Right:
|
||||||
|
seek(e, 1);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onModeChanged(EditorScreenMode mode)
|
return base.OnKeyDown(e);
|
||||||
{
|
|
||||||
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 double scrollAccumulation;
|
private double scrollAccumulation;
|
||||||
@ -193,9 +184,9 @@ namespace osu.Game.Screens.Edit
|
|||||||
while (Math.Abs(scrollAccumulation) > precision)
|
while (Math.Abs(scrollAccumulation) > precision)
|
||||||
{
|
{
|
||||||
if (scrollAccumulation > 0)
|
if (scrollAccumulation > 0)
|
||||||
clock.SeekBackward(!clock.IsRunning);
|
seek(e, -1);
|
||||||
else
|
else
|
||||||
clock.SeekForward(!clock.IsRunning);
|
seek(e, 1);
|
||||||
|
|
||||||
scrollAccumulation = scrollAccumulation < 0 ? Math.Min(0, scrollAccumulation + precision) : Math.Max(0, scrollAccumulation - precision);
|
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.Tempo.Value = 1;
|
||||||
Beatmap.Value.Track.Start();
|
Beatmap.Value.Track.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.OnExiting(next);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,16 +68,20 @@ namespace osu.Game.Screens.Edit
|
|||||||
/// Seeks backwards by one beat length.
|
/// Seeks backwards by one beat length.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="snapped">Whether to snap to the closest beat after seeking.</param>
|
/// <param name="snapped">Whether to snap to the closest beat after seeking.</param>
|
||||||
public void SeekBackward(bool snapped = false) => seek(-1, snapped);
|
/// <param name="amount">The relative amount (magnitude) which should be seeked.</param>
|
||||||
|
public void SeekBackward(bool snapped = false, double amount = 1) => seek(-1, snapped, amount);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Seeks forwards by one beat length.
|
/// Seeks forwards by one beat length.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="snapped">Whether to snap to the closest beat after seeking.</param>
|
/// <param name="snapped">Whether to snap to the closest beat after seeking.</param>
|
||||||
public void SeekForward(bool snapped = false) => seek(1, snapped);
|
/// <param name="amount">The relative amount (magnitude) which should be seeked.</param>
|
||||||
|
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);
|
var timingPoint = ControlPointInfo.TimingPointAt(CurrentTime);
|
||||||
if (direction < 0 && timingPoint.Time == CurrentTime)
|
if (direction < 0 && timingPoint.Time == CurrentTime)
|
||||||
{
|
{
|
||||||
@ -87,7 +91,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
timingPoint = ControlPointInfo.TimingPoints[--activeIndex];
|
timingPoint = ControlPointInfo.TimingPoints[--activeIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
double seekAmount = timingPoint.BeatLength / beatDivisor;
|
double seekAmount = timingPoint.BeatLength / beatDivisor * amount;
|
||||||
double seekTime = CurrentTime + seekAmount * direction;
|
double seekTime = CurrentTime + seekAmount * direction;
|
||||||
|
|
||||||
if (!snapped || ControlPointInfo.TimingPoints.Count == 0)
|
if (!snapped || ControlPointInfo.TimingPoints.Count == 0)
|
||||||
|
@ -13,16 +13,20 @@ namespace osu.Game.Skinning
|
|||||||
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public string Hash { get; set; }
|
||||||
|
|
||||||
public string Creator { get; set; }
|
public string Creator { get; set; }
|
||||||
|
|
||||||
public List<SkinFileInfo> Files { get; set; }
|
public List<SkinFileInfo> Files { get; set; }
|
||||||
|
|
||||||
public bool DeletePending { 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 static SkinInfo Default { get; } = new SkinInfo { Name = "osu!lazer", Creator = "team osu!" };
|
||||||
|
|
||||||
public bool Equals(SkinInfo other) => other != null && ID == other.ID;
|
public bool Equals(SkinInfo other) => other != null && ID == other.ID;
|
||||||
|
|
||||||
public override string ToString() => $"\"{Name}\" by {Creator}";
|
public override string ToString() => FullName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,32 @@ namespace osu.Game.Skinning
|
|||||||
|
|
||||||
public override string[] HandledExtensions => new[] { ".osk" };
|
public override string[] HandledExtensions => new[] { ".osk" };
|
||||||
|
|
||||||
|
protected override string[] HashableFileTypes => new[] { ".ini" };
|
||||||
|
|
||||||
protected override string ImportFromStablePath => "Skins";
|
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();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a list of all usable <see cref="SkinInfo"/>s. Includes the special default skin plus all skins from <see cref="GetAllUserSkins"/>.
|
/// Returns a list of all usable <see cref="SkinInfo"/>s. Includes the special default skin plus all skins from <see cref="GetAllUserSkins"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -45,24 +69,13 @@ namespace osu.Game.Skinning
|
|||||||
/// <returns>A list of available <see cref="SkinInfo"/>.</returns>
|
/// <returns>A list of available <see cref="SkinInfo"/>.</returns>
|
||||||
public List<SkinInfo> GetAllUserSkins() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList();
|
public List<SkinInfo> GetAllUserSkins() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList();
|
||||||
|
|
||||||
protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo
|
protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name };
|
||||||
{
|
|
||||||
Name = archive.Name
|
|
||||||
};
|
|
||||||
|
|
||||||
protected override void Populate(SkinInfo model, ArchiveReader archive)
|
protected override void Populate(SkinInfo model, ArchiveReader archive)
|
||||||
{
|
{
|
||||||
base.Populate(model, archive);
|
base.Populate(model, archive);
|
||||||
populate(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
Skin reference = getSkin(model);
|
||||||
/// Populate a <see cref="SkinInfo"/> from its <see cref="SkinConfiguration"/> (if possible).
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="model"></param>
|
|
||||||
private void populate(SkinInfo model)
|
|
||||||
{
|
|
||||||
Skin reference = GetSkin(model);
|
|
||||||
if (!string.IsNullOrEmpty(reference.Configuration.SkinInfo.Name))
|
if (!string.IsNullOrEmpty(reference.Configuration.SkinInfo.Name))
|
||||||
{
|
{
|
||||||
model.Name = reference.Configuration.SkinInfo.Name;
|
model.Name = reference.Configuration.SkinInfo.Name;
|
||||||
@ -80,7 +93,7 @@ namespace osu.Game.Skinning
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="skinInfo">The skin to lookup.</param>
|
/// <param name="skinInfo">The skin to lookup.</param>
|
||||||
/// <returns>A <see cref="Skin"/> instance correlating to the provided <see cref="SkinInfo"/>.</returns>
|
/// <returns>A <see cref="Skin"/> instance correlating to the provided <see cref="SkinInfo"/>.</returns>
|
||||||
public Skin GetSkin(SkinInfo skinInfo)
|
private Skin getSkin(SkinInfo skinInfo)
|
||||||
{
|
{
|
||||||
if (skinInfo == SkinInfo.Default)
|
if (skinInfo == SkinInfo.Default)
|
||||||
return new DefaultSkin();
|
return new DefaultSkin();
|
||||||
@ -88,28 +101,6 @@ namespace osu.Game.Skinning
|
|||||||
return new LegacySkin(skinInfo, Files.Store, audio);
|
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();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Perform a lookup query on available <see cref="SkinInfo"/>s.
|
/// Perform a lookup query on available <see cref="SkinInfo"/>s.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
Loading…
Reference in New Issue
Block a user