#nullable enable using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; using Content.Shared.Preferences; using Microsoft.EntityFrameworkCore; using Robust.Shared.Maths; using Robust.Shared.Network; using Robust.Shared.Localization.Macros; namespace Content.Server.Database { public abstract class ServerDbBase { public async Task GetPlayerPreferencesAsync(NetUserId userId) { await using var db = await GetDb(); var prefs = await db.DbContext .Preference .Include(p => p.Profiles).ThenInclude(h => h.Jobs) .Include(p => p.Profiles).ThenInclude(h => h.Antags) .SingleOrDefaultAsync(p => p.UserId == userId.UserId); if (prefs is null) return null; var maxSlot = prefs.Profiles.Max(p => p.Slot) + 1; var profiles = new Dictionary(maxSlot); foreach (var profile in prefs.Profiles) { profiles[profile.Slot] = ConvertProfiles(profile); } return new PlayerPreferences(profiles, prefs.SelectedCharacterSlot); } public async Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index) { await using var db = await GetDb(); await SetSelectedCharacterSlotAsync(userId, index, db.DbContext); await db.DbContext.SaveChangesAsync(); } public async Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot) { await using var db = await GetDb(); if (profile is null) { DeleteCharacterSlot(db.DbContext, userId, slot); await db.DbContext.SaveChangesAsync(); return; } if (profile is not HumanoidCharacterProfile humanoid) { // TODO: Handle other ICharacterProfile implementations properly throw new NotImplementedException(); } var entity = ConvertProfiles(humanoid, slot); var prefs = await db.DbContext .Preference .Include(p => p.Profiles) .SingleAsync(p => p.UserId == userId.UserId); var oldProfile = prefs .Profiles .SingleOrDefault(h => h.Slot == entity.Slot); if (oldProfile is not null) { prefs.Profiles.Remove(oldProfile); } prefs.Profiles.Add(entity); await db.DbContext.SaveChangesAsync(); } private static void DeleteCharacterSlot(ServerDbContext db, NetUserId userId, int slot) { db.Preference .Single(p => p.UserId == userId.UserId) .Profiles .RemoveAll(h => h.Slot == slot); } public async Task InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile) { await using var db = await GetDb(); var profile = ConvertProfiles((HumanoidCharacterProfile) defaultProfile, 0); var prefs = new Preference { UserId = userId.UserId, SelectedCharacterSlot = 0 }; prefs.Profiles.Add(profile); db.DbContext.Preference.Add(prefs); await db.DbContext.SaveChangesAsync(); return new PlayerPreferences(new[] {new KeyValuePair(0, defaultProfile)}, 0); } public async Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot) { await using var db = await GetDb(); DeleteCharacterSlot(db.DbContext, userId, deleteSlot); await SetSelectedCharacterSlotAsync(userId, newSlot, db.DbContext); await db.DbContext.SaveChangesAsync(); } private static async Task SetSelectedCharacterSlotAsync(NetUserId userId, int newSlot, ServerDbContext db) { var prefs = await db.Preference.SingleAsync(p => p.UserId == userId.UserId); prefs.SelectedCharacterSlot = newSlot; } private static HumanoidCharacterProfile ConvertProfiles(Profile profile) { var jobs = profile.Jobs.ToDictionary(j => j.JobName, j => (JobPriority) j.Priority); var antags = profile.Antags.Select(a => a.AntagName); var sex = Sex.Male; if (Enum.TryParse(profile.Sex, true, out var sexVal)) sex = sexVal; var clothing = ClothingPreference.Jumpsuit; if (Enum.TryParse(profile.Clothing, true, out var clothingVal)) clothing = clothingVal; var backpack = BackpackPreference.Backpack; if (Enum.TryParse(profile.Backpack, true, out var backpackVal)) backpack = backpackVal; var gender = sex == Sex.Male ? Gender.Male : Gender.Female; if (Enum.TryParse(profile.Gender, true, out var genderVal)) gender = genderVal; return new HumanoidCharacterProfile( profile.CharacterName, profile.Age, sex, gender, new HumanoidCharacterAppearance ( profile.HairName, Color.FromHex(profile.HairColor), profile.FacialHairName, Color.FromHex(profile.FacialHairColor), Color.FromHex(profile.EyeColor), Color.FromHex(profile.SkinColor) ), clothing, backpack, jobs, (PreferenceUnavailableMode) profile.PreferenceUnavailable, antags.ToList() ); } private static Profile ConvertProfiles(HumanoidCharacterProfile humanoid, int slot) { var appearance = (HumanoidCharacterAppearance) humanoid.CharacterAppearance; var entity = new Profile { CharacterName = humanoid.Name, Age = humanoid.Age, Sex = humanoid.Sex.ToString(), Gender = humanoid.Gender.ToString(), HairName = appearance.HairStyleName, HairColor = appearance.HairColor.ToHex(), FacialHairName = appearance.FacialHairStyleName, FacialHairColor = appearance.FacialHairColor.ToHex(), EyeColor = appearance.EyeColor.ToHex(), SkinColor = appearance.SkinColor.ToHex(), Clothing = humanoid.Clothing.ToString(), Backpack = humanoid.Backpack.ToString(), Slot = slot, PreferenceUnavailable = (DbPreferenceUnavailableMode) humanoid.PreferenceUnavailable }; entity.Jobs.AddRange( humanoid.JobPriorities .Where(j => j.Value != JobPriority.Never) .Select(j => new Job {JobName = j.Key, Priority = (DbJobPriority) j.Value}) ); entity.Antags.AddRange( humanoid.AntagPreferences .Select(a => new Antag {AntagName = a}) ); return entity; } public async Task GetAssignedUserIdAsync(string name) { await using var db = await GetDb(); var assigned = await db.DbContext.AssignedUserId.SingleOrDefaultAsync(p => p.UserName == name); return assigned?.UserId is { } g ? new NetUserId(g) : default(NetUserId?); } public async Task AssignUserIdAsync(string name, NetUserId netUserId) { await using var db = await GetDb(); db.DbContext.AssignedUserId.Add(new AssignedUserId { UserId = netUserId.UserId, UserName = name }); await db.DbContext.SaveChangesAsync(); } /* * BAN STUFF */ public abstract Task GetServerBanAsync(IPAddress? address, NetUserId? userId); public abstract Task AddServerBanAsync(ServerBanDef serverBan); /* * PLAYER RECORDS */ public abstract Task UpdatePlayerRecord(NetUserId userId, string userName, IPAddress address); public abstract Task GetPlayerRecordByUserName(string userName, CancellationToken cancel); public abstract Task GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel); /* * CONNECTION LOG */ public abstract Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address); /* * ADMIN STUFF */ public async Task GetAdminDataForAsync(NetUserId userId, CancellationToken cancel) { await using var db = await GetDb(); return await db.DbContext.Admin .Include(p => p.Flags) .Include(p => p.AdminRank) .ThenInclude(p => p!.Flags) .SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel); } public abstract Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync(CancellationToken cancel); public async Task GetAdminRankDataForAsync(int id, CancellationToken cancel = default) { await using var db = await GetDb(); return await db.DbContext.AdminRank .Include(r => r.Flags) .SingleOrDefaultAsync(r => r.Id == id, cancel); } public async Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel) { await using var db = await GetDb(); var admin = await db.DbContext.Admin.SingleAsync(a => a.UserId == userId.UserId, cancel); db.DbContext.Admin.Remove(admin); await db.DbContext.SaveChangesAsync(cancel); } public async Task AddAdminAsync(Admin admin, CancellationToken cancel) { await using var db = await GetDb(); db.DbContext.Admin.Add(admin); await db.DbContext.SaveChangesAsync(cancel); } public async Task UpdateAdminAsync(Admin admin, CancellationToken cancel) { await using var db = await GetDb(); var existing = await db.DbContext.Admin.Include(a => a.Flags).SingleAsync(a => a.UserId == admin.UserId, cancel); existing.Flags = admin.Flags; existing.Title = admin.Title; existing.AdminRankId = admin.AdminRankId; await db.DbContext.SaveChangesAsync(cancel); } public async Task RemoveAdminRankAsync(int rankId, CancellationToken cancel) { await using var db = await GetDb(); var admin = await db.DbContext.AdminRank.SingleAsync(a => a.Id == rankId, cancel); db.DbContext.AdminRank.Remove(admin); await db.DbContext.SaveChangesAsync(cancel); } public async Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel) { await using var db = await GetDb(); db.DbContext.AdminRank.Add(rank); await db.DbContext.SaveChangesAsync(cancel); } public async Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel) { await using var db = await GetDb(); var existing = await db.DbContext.AdminRank .Include(r => r.Flags) .SingleAsync(a => a.Id == rank.Id, cancel); existing.Flags = rank.Flags; existing.Name = rank.Name; await db.DbContext.SaveChangesAsync(cancel); } protected abstract Task GetDb(); protected abstract class DbGuard : IAsyncDisposable { public abstract ServerDbContext DbContext { get; } public abstract ValueTask DisposeAsync(); } } }