Merge pull request #2103 from peppy/skin-database

Implement skin import, database and storage
This commit is contained in:
Dan Balasescu 2018-02-23 13:35:46 +09:00 committed by GitHub
commit ce7d212c3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 675 additions and 2 deletions

View File

@ -14,6 +14,8 @@ protected override void InitialiseDefaults()
{
// UI/selection defaults
Set(OsuSetting.Ruleset, 0, 0, int.MaxValue);
Set(OsuSetting.Skin, 0, 0, int.MaxValue);
Set(OsuSetting.BeatmapDetailTab, BeatmapDetailTab.Details);
Set(OsuSetting.ShowConvertedBeatmaps, true);
@ -122,6 +124,7 @@ public enum OsuSetting
ChatDisplayHeight,
Version,
ShowConvertedBeatmaps,
SpeedChangeVisualisation
SpeedChangeVisualisation,
Skin
}
}

View File

@ -13,6 +13,7 @@
using osu.Game.Rulesets;
using DatabasedKeyBinding = osu.Game.Input.Bindings.DatabasedKeyBinding;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
using osu.Game.Skinning;
namespace osu.Game.Database
{
@ -26,6 +27,7 @@ public class OsuDbContext : DbContext
public DbSet<DatabasedSetting> DatabasedSetting { get; set; }
public DbSet<FileInfo> FileInfo { get; set; }
public DbSet<RulesetInfo> RulesetInfo { get; set; }
public DbSet<SkinInfo> SkinInfo { get; set; }
private readonly string connectionString;

View File

@ -0,0 +1,379 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using osu.Game.Database;
using System;
namespace osu.Game.Migrations
{
[DbContext(typeof(OsuDbContext))]
[Migration("20180219060912_AddSkins")]
partial class AddSkins
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
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<float>("SliderMultiplier");
b.Property<float>("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<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")
.IsUnique();
b.HasIndex("MD5Hash")
.IsUnique();
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.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>("Name");
b.HasKey("ID");
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
}
}
}

View File

@ -0,0 +1,73 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace osu.Game.Migrations
{
public partial class AddSkins : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SkinInfo",
columns: table => new
{
ID = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Creator = table.Column<string>(type: "TEXT", nullable: true),
DeletePending = table.Column<bool>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_SkinInfo", x => x.ID);
});
migrationBuilder.CreateTable(
name: "SkinFileInfo",
columns: table => new
{
ID = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
FileInfoID = table.Column<int>(type: "INTEGER", nullable: false),
Filename = table.Column<string>(type: "TEXT", nullable: false),
SkinInfoID = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SkinFileInfo", x => x.ID);
table.ForeignKey(
name: "FK_SkinFileInfo_FileInfo_FileInfoID",
column: x => x.FileInfoID,
principalTable: "FileInfo",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_SkinFileInfo_SkinInfo_SkinInfoID",
column: x => x.SkinInfoID,
principalTable: "SkinInfo",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_SkinFileInfo_FileInfoID",
table: "SkinFileInfo",
column: "FileInfoID");
migrationBuilder.CreateIndex(
name: "IX_SkinFileInfo_SkinInfoID",
table: "SkinFileInfo",
column: "SkinInfoID");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SkinFileInfo");
migrationBuilder.DropTable(
name: "SkinInfo");
}
}
}

View File

@ -281,6 +281,43 @@ protected override void BuildModel(ModelBuilder modelBuilder)
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>("Name");
b.HasKey("ID");
b.ToTable("SkinInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
@ -322,6 +359,19 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.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
}
}

View File

@ -30,6 +30,7 @@
using osu.Game.Screens.Play;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Mods;
using osu.Game.Skinning;
using OpenTK.Graphics;
namespace osu.Game
@ -79,6 +80,8 @@ private Intro intro
private Bindable<int> configRuleset;
public Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
private Bindable<int> configSkin;
private readonly string[] args;
private SettingsOverlay settings;
@ -122,10 +125,18 @@ private void load(FrameworkConfigManager frameworkConfig)
dependencies.CacheAs(this);
// bind config int to database RulesetInfo
configRuleset = LocalConfig.GetBindable<int>(OsuSetting.Ruleset);
Ruleset.Value = RulesetStore.GetRuleset(configRuleset.Value) ?? RulesetStore.AvailableRulesets.First();
Ruleset.ValueChanged += r => configRuleset.Value = r.ID ?? 0;
// bind config int to database SkinInfo
configSkin = LocalConfig.GetBindable<int>(OsuSetting.Skin);
SkinManager.CurrentSkinInfo.ValueChanged += s => configSkin.Value = s.ID;
configSkin.ValueChanged += id => SkinManager.CurrentSkinInfo.Value = SkinManager.Query(s => s.ID == id) ?? SkinInfo.Default;
configSkin.TriggerChange();
LocalConfig.BindWith(OsuSetting.VolumeInactive, inactiveVolumeAdjust);
}
@ -187,7 +198,9 @@ protected override void LoadComplete()
CursorOverrideContainer.CanShowCursor = currentScreen?.CursorVisible ?? false;
// hook up notifications to components.
SkinManager.PostNotification = n => notifications?.Post(n);
BeatmapManager.PostNotification = n => notifications?.Post(n);
BeatmapManager.GetStableStorage = GetStorageForStableInstall;
AddRange(new Drawable[]

View File

@ -30,6 +30,7 @@
using osu.Game.IO;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
namespace osu.Game
{
@ -39,6 +40,8 @@ public class OsuGameBase : Framework.Game, IOnlineComponent, ICanAcceptFiles
protected BeatmapManager BeatmapManager;
protected SkinManager SkinManager;
protected RulesetStore RulesetStore;
protected FileStore FileStore;
@ -103,6 +106,8 @@ private void load()
runMigrations();
dependencies.Cache(SkinManager = new SkinManager(Host.Storage, contextFactory, Host));
dependencies.Cache(API = new APIAccess
{
Username = LocalConfig.Get<string>(OsuSetting.Username),
@ -120,6 +125,7 @@ private void load()
fileImporters.Add(BeatmapManager);
fileImporters.Add(ScoreStore);
fileImporters.Add(SkinManager);
//this completely overrides the framework default. will need to change once we make a proper FontStore.
dependencies.Cache(Fonts = new FontStore { ScaleAdjust = 100 });

View File

@ -1,26 +1,33 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Skinning;
using OpenTK;
namespace osu.Game.Overlays.Settings.Sections
{
public class SkinSection : SettingsSection
{
private SettingsDropdown<int> skinDropdown;
public override string Header => "Skin";
public override FontAwesome Icon => FontAwesome.fa_paint_brush;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
private void load(OsuConfigManager config, SkinManager skins)
{
FlowContent.Spacing = new Vector2(0, 5);
Children = new Drawable[]
{
skinDropdown = new SettingsDropdown<int>(),
new SettingsSlider<double, SizeSlider>
{
LabelText = "Menu cursor size",
@ -39,6 +46,14 @@ private void load(OsuConfigManager config)
Bindable = config.GetBindable<bool>(OsuSetting.AutoCursorSize)
},
};
void reloadSkins() => skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair<string, int>(s.Name, s.ID));
skins.ItemAdded += _ => reloadSkins();
skins.ItemRemoved += _ => reloadSkins();
reloadSkins();
skinDropdown.Bindable = config.GetBindable<int>(OsuSetting.Skin);
}
private class SizeSlider : OsuSliderBar<double>

View File

@ -0,0 +1,25 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using osu.Game.Database;
using osu.Game.IO;
namespace osu.Game.Skinning
{
public class SkinFileInfo : INamedFileInfo
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
public int SkinInfoID { get; set; }
public int FileInfoID { get; set; }
public FileInfo FileInfo { get; set; }
[Required]
public string Filename { get; set; }
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using osu.Game.Database;
namespace osu.Game.Skinning
{
public class SkinInfo : IHasFiles<SkinFileInfo>, IEquatable<SkinInfo>, IHasPrimaryKey, ISoftDelete
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
public string Name { get; set; }
public string Creator { get; set; }
public List<SkinFileInfo> Files { get; set; }
public bool DeletePending { get; set; }
public static SkinInfo Default { get; } = new SkinInfo { Name = "osu!lazer", Creator = "team osu!" };
public bool Equals(SkinInfo other) => other != null && ID == other.ID;
}
}

View File

@ -0,0 +1,49 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Configuration;
using osu.Framework.Platform;
using osu.Game.Database;
using osu.Game.IO.Archives;
namespace osu.Game.Skinning
{
public class SkinManager : ArchiveModelManager<SkinInfo, SkinFileInfo>
{
public readonly Bindable<SkinInfo> CurrentSkinInfo = new Bindable<SkinInfo>(SkinInfo.Default) { Default = SkinInfo.Default };
public override string[] HandledExtensions => new[] { ".osk" };
/// <summary>
/// Returns a list of all usable <see cref="SkinInfo"/>s.
/// </summary>
/// <returns>A list of available <see cref="SkinInfo"/>.</returns>
public List<SkinInfo> GetAllUsableSkins()
{
var userSkins = ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList();
userSkins.Insert(0, SkinInfo.Default);
return userSkins;
}
protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name };
private SkinStore store;
public SkinManager(Storage storage, DatabaseContextFactory contextFactory, IIpcHost importHost)
: base(storage, contextFactory, new SkinStore(contextFactory, storage), importHost)
{
}
/// <summary>
/// Perform a lookup query on available <see cref="SkinInfo"/>s.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The first result for the provided query, or null if no results were found.</returns>
public SkinInfo Query(Expression<Func<SkinInfo, bool>> query) => ModelStore.ConsumableItems.AsNoTracking().FirstOrDefault(query);
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Linq;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Platform;
using osu.Game.Database;
namespace osu.Game.Skinning
{
public class SkinStore : MutableDatabaseBackedStore<SkinInfo>
{
public SkinStore(DatabaseContextFactory contextFactory, Storage storage = null)
: base(contextFactory, storage)
{
}
protected override IQueryable<SkinInfo> AddIncludesForConsumption(IQueryable<SkinInfo> query) =>
base.AddIncludesForConsumption(query)
.Include(s => s.Files).ThenInclude(f => f.FileInfo);
}
}

View File

@ -322,6 +322,10 @@
<Compile Include="Migrations\20171209034410_AddRulesetInfoShortName.Designer.cs">
<DependentUpon>20171209034410_AddRulesetInfoShortName.cs</DependentUpon>
</Compile>
<Compile Include="Migrations\20180219060912_AddSkins.cs" />
<Compile Include="Migrations\20180219060912_AddSkins.designer.cs">
<DependentUpon>20180219060912_AddSkins.cs</DependentUpon>
</Compile>
<Compile Include="Migrations\OsuDbContextModelSnapshot.cs" />
<Compile Include="Online\API\Requests\GetBeatmapSetRequest.cs" />
<Compile Include="Online\Chat\DrawableLinkCompiler.cs" />
@ -850,6 +854,10 @@
<Compile Include="Screens\Tournament\Teams\DrawingsTeam.cs" />
<Compile Include="Screens\Tournament\Teams\ITeamList.cs" />
<Compile Include="Screens\Tournament\Teams\StorageBackedTeamList.cs" />
<Compile Include="Skinning\SkinFileInfo.cs" />
<Compile Include="Skinning\SkinInfo.cs" />
<Compile Include="Skinning\SkinManager.cs" />
<Compile Include="Skinning\SkinStore.cs" />
<Compile Include="Storyboards\CommandLoop.cs" />
<Compile Include="Storyboards\CommandTimeline.cs" />
<Compile Include="Storyboards\CommandTimelineGroup.cs" />