Files
crystall-punk-14/Content.Server/Database/ServerDbBase.cs

1612 lines
60 KiB
C#
Raw Normal View History

2021-03-22 01:30:50 +01:00
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
2023-12-10 16:30:12 +01:00
using System.Runtime.CompilerServices;
using System.Text.Json;
2020-11-10 16:50:28 +01:00
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
Humanoid appearance refactor (#10882) * initial commit - species prototype modifications - markings points as its own file - shared humanoid component * adds a tool to convert sprite accessories to markings (in go) * removes a fmt call * converts sprite accessory to markings * adds hair and facial hair to marking categories * multiple changes - humanoid visualizer system - markings modifications for visualizer - modifications to shared humanoid component - lays out a base for humanoid system * hidden layers, ports some properties from appearance component, shrinks DefaultMarkings a little * squishes the initialize event calls into one function adds stuff to set species/skin color externally from a server message - currently laid out as if it a dirty call to a networked component, may be subject to change (server-side has not been implemented yet) * makes the sprite pipeline more obvious * apply all markings, hidden layer set replacement * ensures that markings are cleared when the new set is applied * starts refactoring markingsset (unfinished) * more additions to the markingset api * adds constructor logic to markingset * adds a method to filter out markings in a set based on a given species * fixes enumerators in markingset * adds validator into MarkingSet, fixes ForwardMarkingEnumerator * modifications to the humanoid visual system * ensuredefault in markingset * oop * fixes up data keys, populates OnAppearanceChange in visualizer * changes to humanoid component, markings marking equality is now more strict, humanoidcomponent is now implemented for client as a child of sharedhumanoidcomponent * markings are now applied the visualizer by diffing them * base sprites are now applied to humanoids from humanoidvisualizer * passes along base sprite settings to the marking application so that markings know to follow skin color/alpha or not (see: slimes) * custom base layers on humanoids * merges all data keys into one data class for humanoid visualizers * setappearance in sharedhumanoidsystem, removes custombaselayercolors * humanoidcomponent, system (empty) in server * adds some basic public API functions to HumanoidSystem * add marking, remove marking * changes appearance MarkingsSet to a List<Marking>, adds listener for PlayerSpawnCompleteEvent in HumanoidSystem * ensuredefaultmarkings, oninit for humanoids * markingmanager API changes * removes MarkingsSet * LoadProfile, adjusts randomization in humanoid appearance to account for species * base layer settings in humanoidsystem, eye color from profile * rearranges files to centralize under Humanoid namespace * more reorganization, deletes some stuff gotta break stuff to make other things work, right? goodbye SpriteAccessory... * fixes a good chunk of server-side issues still does not compile, yet * singlemarkingpicker xaml layout * singlemarkingpicker logic * magic mirror window (varying pieces of it, mostly client-oriented) * removes some imports, gives MagicMirror a BUI class (not filled in yet) * populates magic mirror BUI functionality / window callbacks * fixes up some errors in humanoidprofileeditor * changes to SingleMarkingPicker SingleMarkingPicker now accepts a List<Marking>, species, and total possible markings available in that marking category * fixes up hair pickers on humanoid profile editor * fixes the errors in markingpicker * markingsystem is now gone * fixes a bunch of build errors * so that's why i did it like that * namespace issues, adds robustxamlloader to singlemarkingpicker * another robustxamlloader * human, lizard sprites/points * prototype fixes, deletion of old spriteaccessory * component registration, fixes dwarf skin toning no, 'ReptilianToned' does not exist * removes component registration from abstract humanoid component * visualizer data now cloneable * serialize for visualizer key * zero-count edge case * missing semi-colon moment * setspecies in humanoidsystem * ensures that default markings, if empty, will cause ensuredefault to skip over that given category * tryadd instead of add * whoops * diff and apply should properly apply markings now * always ensure default, fixes double load for player spawning * apply skin color now sets the skin color property in humanoidcomponent * removes sprite from a few species prototypes * sprite changes for specific base layers based on humanoid sex * layer ordering fix, and a missing base layer should now disallow markings on that layer * anymarking base layer, adds the right leg/foot for humans * loading a profile will now clear all markings on that humanoid * adds missing layers for humans * separates species.yml into respective species prototype files * ensures that if layer visibility was changed, all markings have to be reapplied * server-side enforcement of hiding hair (and other head-related markings) when equipping things that hide hair * slime fix, clothingsystem now dictates layer visibility server side * sussy * layer settings should now ensure a marking should match the skin tone * whoops * skincolor static class and functions in UI * skin color validation in humanoidcharacterappearance * markingpicker now shows only the markings for the selected category in used * getter for slot in singlemarkingpicker now ensures slot is 0 if markings exists * FilterSpecies no longer attempts to do removal while iterating * expands for SingleMarkingPicker * humanoid base dummy has blank layers now (and snout/tail/headside/headtop) * fixes an issue with visualizer system if the marking count was different but the markings themselves were (somewhat) the same * whoops * adds edge case handlers for count differences in humanoid markings * preview now loads profile instead of directly setting appearance * moves marking set loading to update controls * clones a marking set in markingpicker by using the deep clone constructor * whoops (deep cloning a marking now copies the marking id) * adds replace function for markingset * points should now update after the markings are remove/added * merging base layer sprites into a humanoid should now clear them before merging * sets dirty range start to count only if the dirty range start was never set above 0 * fixes up some issues with singlemarkingpicker * color selector sliders in single marking picker should now expand * hair from hair pickers should now apply in profile loading (client-side) * category in singlemarkingpicker now sets the private category variable * slot selector should now populate * single marking picker buttons now have text, also shows the category name over all user-clickable elements * removes a comment * removing hair slots now sets it to bald, defaults to zero used slots if current hair is bald on hair/facial hair * random skin color, eye color * populate colors now checks if the marking count is greater than zero in singlemarkingpicker * hair/facial hair pickers now just get the first possible hair from the respective species list * different approach to random skin color * oh, that's why it wasn't working * randomize everything now just updates every single control * selecting a new marking in SingleMarkingPicker should attempt to copy over old colors, populate list now uses cache, * markingmanager now uses OnlyWhitelisted to populate by category and species * filterspecies now uses onlyWhitelist to filter markings based on whitelist or not * oops * ui fix for singlemarkingpicker, ensures that cache is not null if it is null when populatelist is called * order of operations for the horizontal expand for add/remove * hair pickers should now update when you add/remove the hair slot * fixes variable naming error in character appearance * loc string fix in singlemarkingpicker * lizards, vox now have onlyWhitelist, vox restriction for hair/facialhairs * having zero possible hairs should no longer cause an exception in randomization * setting species should now update hair pickers * ignore categories for marking picker * and a clear as well for the category button * places that functionality in its own function instead * adds eye base sprite, vox now also have their own custom eye sprites * loading a profile client-side should do FilterSpecies for markings now * client-side load profile does filter species after adding in the hairs now * magic mirror * callbacks now call the callback instead of adding it on construct * whoops * in removemarking too * adds missing synchronize calls * comments out an updateinterface call in magic mirror * magic mirror window title, minimum sizing * fixes minsize, adds warning for players who try to set their hair for species that have no hair * removes spaces in xaml * namespace changes/organization * whoopsie (merge conflicts) * re-enables identity from humanoid component * damagevisuals now uses the enum given to it instead of the layerstate given on that layer tied to the enum * removes commas from json * changes to visuals system so the change is consistent * chest * reptilian * visualizer system now handles body sprite setting/coloration, similar to how characterappearance did it not a big fan of this * adds a check in applybasesprites * adding/removing parts should now make them invisible on a humanoid * body part removal/adding now enumerates over sublayers instead * synchro now runs in bodycomponent startup * parts instead of slots * humanoidcompnent check * switches from rsi to actualrsi * removes all the body stuff (too slow) * cleans up resolves from humanoid visualizer system * merging sprites now checks if the base sprites have been modified or not (through things like species changes, or custom base sprite changes) * not forgetting that one again * merging now returns an actual dirty value * replaces the sequenceequal with a more accurate solution * permanent layers, layer visibility on add/remove part in body * should send all hidden layers over now * isdirty in visualizer system for base layers * isdirty checks count as well * ok, IsDirty should now set the base layers if the merged sprites are different * equals override in HumanoidSpritePrototypes.cs temporary until record prototypes :heck: * makes fields readonly, equates IDs instead * adds forced markings through marking picker * forced in humanoidsystem api, ignorespecies in markingpicker * marking bui * makes that serializable as well * ignore species/forced toggles now work * adds icon to modifier verb, interface and keys to humanoid bases * needs the actual enum value to open, no? * makes the key the actual key * actions now propagate upwards * ignore species when set now repopulates markingpicker * modifiable base layers in the markings window * oops! * layout changes * info box should now appear * adds ignorespecies for marking picker, collapsible for base layer section of appearance modification window * collapsible layout moment * if base layers have changed, all markings are now dirty (and if a base layer is missing, the marking is still 'applied' but it's now just invisible * small change to marking visibility * small changes to modifier UI * markings now match skin on zombification * zombie stuff * makes the line edit in marking modifier window more obvious * disables vox on round start * horizontal expand on the single label in base layer modifiers * humanoid profiles in prototypes * randomhumanoidappearance won't work if the humanoid has a profile already stored * removes unused code * documentation in humanoidsystem server-side * documentation in shared/client * whoops * converts accessory into marking in locale files (also adds marking loc string into single marking picker) * be gone, shared humanoid appearance system from the last upstream merge * species ignore on randomization (defaults to no ignored species) * more upstream merge parts that bypassed any errors before merge * addresses review (also just adds typeserializers in some places) * submodule moment * upstream merge issues
2022-09-22 15:19:00 -07:00
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Preferences;
Loadouts redux (#25715) * Loadouts redux * Loadout window mockup * More workout * rent * validation * Developments * bcs * More cleanup * Rebuild working * Fix model and loading * obsession * efcore * We got a stew goin * Cleanup * Optional + SeniorEngineering fix * Fixes * Update science.yml * add add * Automatic naming * Update nukeops * Coming together * Right now * stargate * rejig the UI * weh * Loadouts tweaks * Merge conflicts + ordering fix * yerba mate * chocolat * More updates * Add multi-selection support * test h * fikss * a * add tech assistant and hazard suit * huh * Latest changes * add medical loadouts * and science * finish security loadouts * cargo * service done * added wildcards * add command * Move restrictions * Finalising * Fix existing work * Localise next batch * clothing fix * Fix storage names * review * the scooping room * Test fixes * Xamlify * Xamlify this too * Update Resources/Prototypes/Loadouts/Jobs/Medical/paramedic.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/loadout_groups.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/loadout_groups.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/Jobs/Security/detective.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/loadout_groups.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * ben * Margins --------- Co-authored-by: Firewatch <54725557+musicmanvr@users.noreply.github.com> Co-authored-by: Mr. 27 <koolthunder019@gmail.com> Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
2024-04-16 22:57:43 +10:00
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Utility;
namespace Content.Server.Database
{
public abstract class ServerDbBase
{
2023-12-10 16:30:12 +01:00
private readonly ISawmill _opsLog;
/// <param name="opsLog">Sawmill to trace log database operations to.</param>
public ServerDbBase(ISawmill opsLog)
{
_opsLog = opsLog;
}
#region Preferences
public async Task<PlayerPreferences?> 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)
2022-09-10 17:40:06 +02:00
.Include(p => p.Profiles).ThenInclude(h => h.Traits)
Loadouts redux (#25715) * Loadouts redux * Loadout window mockup * More workout * rent * validation * Developments * bcs * More cleanup * Rebuild working * Fix model and loading * obsession * efcore * We got a stew goin * Cleanup * Optional + SeniorEngineering fix * Fixes * Update science.yml * add add * Automatic naming * Update nukeops * Coming together * Right now * stargate * rejig the UI * weh * Loadouts tweaks * Merge conflicts + ordering fix * yerba mate * chocolat * More updates * Add multi-selection support * test h * fikss * a * add tech assistant and hazard suit * huh * Latest changes * add medical loadouts * and science * finish security loadouts * cargo * service done * added wildcards * add command * Move restrictions * Finalising * Fix existing work * Localise next batch * clothing fix * Fix storage names * review * the scooping room * Test fixes * Xamlify * Xamlify this too * Update Resources/Prototypes/Loadouts/Jobs/Medical/paramedic.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/loadout_groups.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/loadout_groups.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/Jobs/Security/detective.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/loadout_groups.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * ben * Margins --------- Co-authored-by: Firewatch <54725557+musicmanvr@users.noreply.github.com> Co-authored-by: Mr. 27 <koolthunder019@gmail.com> Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
2024-04-16 22:57:43 +10:00
.Include(p => p.Profiles)
.ThenInclude(h => h.Loadouts)
.ThenInclude(l => l.Groups)
.ThenInclude(group => group.Loadouts)
.AsSingleQuery()
.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<int, ICharacterProfile>(maxSlot);
foreach (var profile in prefs.Profiles)
{
profiles[profile.Slot] = ConvertProfiles(profile);
}
return new PlayerPreferences(profiles, prefs.SelectedCharacterSlot, Color.FromHex(prefs.AdminOOCColor));
}
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)
{
await 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 oldProfile = db.DbContext.Profile
.Include(p => p.Preference)
.Where(p => p.Preference.UserId == userId.UserId)
.Include(p => p.Jobs)
.Include(p => p.Antags)
.Include(p => p.Traits)
Loadouts redux (#25715) * Loadouts redux * Loadout window mockup * More workout * rent * validation * Developments * bcs * More cleanup * Rebuild working * Fix model and loading * obsession * efcore * We got a stew goin * Cleanup * Optional + SeniorEngineering fix * Fixes * Update science.yml * add add * Automatic naming * Update nukeops * Coming together * Right now * stargate * rejig the UI * weh * Loadouts tweaks * Merge conflicts + ordering fix * yerba mate * chocolat * More updates * Add multi-selection support * test h * fikss * a * add tech assistant and hazard suit * huh * Latest changes * add medical loadouts * and science * finish security loadouts * cargo * service done * added wildcards * add command * Move restrictions * Finalising * Fix existing work * Localise next batch * clothing fix * Fix storage names * review * the scooping room * Test fixes * Xamlify * Xamlify this too * Update Resources/Prototypes/Loadouts/Jobs/Medical/paramedic.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/loadout_groups.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/loadout_groups.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/Jobs/Security/detective.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/loadout_groups.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * ben * Margins --------- Co-authored-by: Firewatch <54725557+musicmanvr@users.noreply.github.com> Co-authored-by: Mr. 27 <koolthunder019@gmail.com> Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
2024-04-16 22:57:43 +10:00
.Include(p => p.Loadouts)
.ThenInclude(l => l.Groups)
.ThenInclude(group => group.Loadouts)
.AsSplitQuery()
.SingleOrDefault(h => h.Slot == slot);
var newProfile = ConvertProfiles(humanoid, slot, oldProfile);
if (oldProfile == null)
{
var prefs = await db.DbContext
.Preference
.Include(p => p.Profiles)
.SingleAsync(p => p.UserId == userId.UserId);
prefs.Profiles.Add(newProfile);
}
await db.DbContext.SaveChangesAsync();
}
private static async Task DeleteCharacterSlot(ServerDbContext db, NetUserId userId, int slot)
{
var profile = await db.Profile.Include(p => p.Preference)
.Where(p => p.Preference.UserId == userId.UserId && p.Slot == slot)
.SingleOrDefaultAsync();
if (profile == null)
{
return;
}
db.Profile.Remove(profile);
}
public async Task<PlayerPreferences> 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,
AdminOOCColor = Color.Red.ToHex()
};
prefs.Profiles.Add(profile);
db.DbContext.Preference.Add(prefs);
await db.DbContext.SaveChangesAsync();
return new PlayerPreferences(new[] {new KeyValuePair<int, ICharacterProfile>(0, defaultProfile)}, 0, Color.FromHex(prefs.AdminOOCColor));
}
public async Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot)
{
await using var db = await GetDb();
await DeleteCharacterSlot(db.DbContext, userId, deleteSlot);
await SetSelectedCharacterSlotAsync(userId, newSlot, db.DbContext);
await db.DbContext.SaveChangesAsync();
}
public async Task SaveAdminOOCColorAsync(NetUserId userId, Color color)
{
await using var db = await GetDb();
var prefs = await db.DbContext
.Preference
.Include(p => p.Profiles)
.SingleAsync(p => p.UserId == userId.UserId);
prefs.AdminOOCColor = color.ToHex();
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);
2022-09-10 17:40:06 +02:00
var traits = profile.Traits.Select(t => t.TraitName);
var sex = Sex.Male;
if (Enum.TryParse<Sex>(profile.Sex, true, out var sexVal))
sex = sexVal;
var spawnPriority = (SpawnPriorityPreference) profile.SpawnPriority;
var gender = sex == Sex.Male ? Gender.Male : Gender.Female;
if (Enum.TryParse<Gender>(profile.Gender, true, out var genderVal))
gender = genderVal;
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
var markingsRaw = profile.Markings?.Deserialize<List<string>>();
List<Marking> markings = new();
if (markingsRaw != null)
{
foreach (var marking in markingsRaw)
{
var parsed = Marking.ParseFromDbString(marking);
if (parsed is null) continue;
markings.Add(parsed);
}
}
Loadouts redux (#25715) * Loadouts redux * Loadout window mockup * More workout * rent * validation * Developments * bcs * More cleanup * Rebuild working * Fix model and loading * obsession * efcore * We got a stew goin * Cleanup * Optional + SeniorEngineering fix * Fixes * Update science.yml * add add * Automatic naming * Update nukeops * Coming together * Right now * stargate * rejig the UI * weh * Loadouts tweaks * Merge conflicts + ordering fix * yerba mate * chocolat * More updates * Add multi-selection support * test h * fikss * a * add tech assistant and hazard suit * huh * Latest changes * add medical loadouts * and science * finish security loadouts * cargo * service done * added wildcards * add command * Move restrictions * Finalising * Fix existing work * Localise next batch * clothing fix * Fix storage names * review * the scooping room * Test fixes * Xamlify * Xamlify this too * Update Resources/Prototypes/Loadouts/Jobs/Medical/paramedic.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/loadout_groups.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/loadout_groups.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/Jobs/Security/detective.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/loadout_groups.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * ben * Margins --------- Co-authored-by: Firewatch <54725557+musicmanvr@users.noreply.github.com> Co-authored-by: Mr. 27 <koolthunder019@gmail.com> Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
2024-04-16 22:57:43 +10:00
var loadouts = new Dictionary<string, RoleLoadout>();
foreach (var role in profile.Loadouts)
{
var loadout = new RoleLoadout(role.RoleName);
foreach (var group in role.Groups)
{
var groupLoadouts = loadout.SelectedLoadouts.GetOrNew(group.GroupName);
foreach (var profLoadout in group.Loadouts)
{
groupLoadouts.Add(new Loadout()
{
Prototype = profLoadout.LoadoutName,
});
}
}
loadouts[role.RoleName] = loadout;
}
return new HumanoidCharacterProfile(
profile.CharacterName,
2022-05-14 08:58:45 +10:00
profile.FlavorText,
profile.Species,
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),
Humanoid appearance refactor (#10882) * initial commit - species prototype modifications - markings points as its own file - shared humanoid component * adds a tool to convert sprite accessories to markings (in go) * removes a fmt call * converts sprite accessory to markings * adds hair and facial hair to marking categories * multiple changes - humanoid visualizer system - markings modifications for visualizer - modifications to shared humanoid component - lays out a base for humanoid system * hidden layers, ports some properties from appearance component, shrinks DefaultMarkings a little * squishes the initialize event calls into one function adds stuff to set species/skin color externally from a server message - currently laid out as if it a dirty call to a networked component, may be subject to change (server-side has not been implemented yet) * makes the sprite pipeline more obvious * apply all markings, hidden layer set replacement * ensures that markings are cleared when the new set is applied * starts refactoring markingsset (unfinished) * more additions to the markingset api * adds constructor logic to markingset * adds a method to filter out markings in a set based on a given species * fixes enumerators in markingset * adds validator into MarkingSet, fixes ForwardMarkingEnumerator * modifications to the humanoid visual system * ensuredefault in markingset * oop * fixes up data keys, populates OnAppearanceChange in visualizer * changes to humanoid component, markings marking equality is now more strict, humanoidcomponent is now implemented for client as a child of sharedhumanoidcomponent * markings are now applied the visualizer by diffing them * base sprites are now applied to humanoids from humanoidvisualizer * passes along base sprite settings to the marking application so that markings know to follow skin color/alpha or not (see: slimes) * custom base layers on humanoids * merges all data keys into one data class for humanoid visualizers * setappearance in sharedhumanoidsystem, removes custombaselayercolors * humanoidcomponent, system (empty) in server * adds some basic public API functions to HumanoidSystem * add marking, remove marking * changes appearance MarkingsSet to a List<Marking>, adds listener for PlayerSpawnCompleteEvent in HumanoidSystem * ensuredefaultmarkings, oninit for humanoids * markingmanager API changes * removes MarkingsSet * LoadProfile, adjusts randomization in humanoid appearance to account for species * base layer settings in humanoidsystem, eye color from profile * rearranges files to centralize under Humanoid namespace * more reorganization, deletes some stuff gotta break stuff to make other things work, right? goodbye SpriteAccessory... * fixes a good chunk of server-side issues still does not compile, yet * singlemarkingpicker xaml layout * singlemarkingpicker logic * magic mirror window (varying pieces of it, mostly client-oriented) * removes some imports, gives MagicMirror a BUI class (not filled in yet) * populates magic mirror BUI functionality / window callbacks * fixes up some errors in humanoidprofileeditor * changes to SingleMarkingPicker SingleMarkingPicker now accepts a List<Marking>, species, and total possible markings available in that marking category * fixes up hair pickers on humanoid profile editor * fixes the errors in markingpicker * markingsystem is now gone * fixes a bunch of build errors * so that's why i did it like that * namespace issues, adds robustxamlloader to singlemarkingpicker * another robustxamlloader * human, lizard sprites/points * prototype fixes, deletion of old spriteaccessory * component registration, fixes dwarf skin toning no, 'ReptilianToned' does not exist * removes component registration from abstract humanoid component * visualizer data now cloneable * serialize for visualizer key * zero-count edge case * missing semi-colon moment * setspecies in humanoidsystem * ensures that default markings, if empty, will cause ensuredefault to skip over that given category * tryadd instead of add * whoops * diff and apply should properly apply markings now * always ensure default, fixes double load for player spawning * apply skin color now sets the skin color property in humanoidcomponent * removes sprite from a few species prototypes * sprite changes for specific base layers based on humanoid sex * layer ordering fix, and a missing base layer should now disallow markings on that layer * anymarking base layer, adds the right leg/foot for humans * loading a profile will now clear all markings on that humanoid * adds missing layers for humans * separates species.yml into respective species prototype files * ensures that if layer visibility was changed, all markings have to be reapplied * server-side enforcement of hiding hair (and other head-related markings) when equipping things that hide hair * slime fix, clothingsystem now dictates layer visibility server side * sussy * layer settings should now ensure a marking should match the skin tone * whoops * skincolor static class and functions in UI * skin color validation in humanoidcharacterappearance * markingpicker now shows only the markings for the selected category in used * getter for slot in singlemarkingpicker now ensures slot is 0 if markings exists * FilterSpecies no longer attempts to do removal while iterating * expands for SingleMarkingPicker * humanoid base dummy has blank layers now (and snout/tail/headside/headtop) * fixes an issue with visualizer system if the marking count was different but the markings themselves were (somewhat) the same * whoops * adds edge case handlers for count differences in humanoid markings * preview now loads profile instead of directly setting appearance * moves marking set loading to update controls * clones a marking set in markingpicker by using the deep clone constructor * whoops (deep cloning a marking now copies the marking id) * adds replace function for markingset * points should now update after the markings are remove/added * merging base layer sprites into a humanoid should now clear them before merging * sets dirty range start to count only if the dirty range start was never set above 0 * fixes up some issues with singlemarkingpicker * color selector sliders in single marking picker should now expand * hair from hair pickers should now apply in profile loading (client-side) * category in singlemarkingpicker now sets the private category variable * slot selector should now populate * single marking picker buttons now have text, also shows the category name over all user-clickable elements * removes a comment * removing hair slots now sets it to bald, defaults to zero used slots if current hair is bald on hair/facial hair * random skin color, eye color * populate colors now checks if the marking count is greater than zero in singlemarkingpicker * hair/facial hair pickers now just get the first possible hair from the respective species list * different approach to random skin color * oh, that's why it wasn't working * randomize everything now just updates every single control * selecting a new marking in SingleMarkingPicker should attempt to copy over old colors, populate list now uses cache, * markingmanager now uses OnlyWhitelisted to populate by category and species * filterspecies now uses onlyWhitelist to filter markings based on whitelist or not * oops * ui fix for singlemarkingpicker, ensures that cache is not null if it is null when populatelist is called * order of operations for the horizontal expand for add/remove * hair pickers should now update when you add/remove the hair slot * fixes variable naming error in character appearance * loc string fix in singlemarkingpicker * lizards, vox now have onlyWhitelist, vox restriction for hair/facialhairs * having zero possible hairs should no longer cause an exception in randomization * setting species should now update hair pickers * ignore categories for marking picker * and a clear as well for the category button * places that functionality in its own function instead * adds eye base sprite, vox now also have their own custom eye sprites * loading a profile client-side should do FilterSpecies for markings now * client-side load profile does filter species after adding in the hairs now * magic mirror * callbacks now call the callback instead of adding it on construct * whoops * in removemarking too * adds missing synchronize calls * comments out an updateinterface call in magic mirror * magic mirror window title, minimum sizing * fixes minsize, adds warning for players who try to set their hair for species that have no hair * removes spaces in xaml * namespace changes/organization * whoopsie (merge conflicts) * re-enables identity from humanoid component * damagevisuals now uses the enum given to it instead of the layerstate given on that layer tied to the enum * removes commas from json * changes to visuals system so the change is consistent * chest * reptilian * visualizer system now handles body sprite setting/coloration, similar to how characterappearance did it not a big fan of this * adds a check in applybasesprites * adding/removing parts should now make them invisible on a humanoid * body part removal/adding now enumerates over sublayers instead * synchro now runs in bodycomponent startup * parts instead of slots * humanoidcompnent check * switches from rsi to actualrsi * removes all the body stuff (too slow) * cleans up resolves from humanoid visualizer system * merging sprites now checks if the base sprites have been modified or not (through things like species changes, or custom base sprite changes) * not forgetting that one again * merging now returns an actual dirty value * replaces the sequenceequal with a more accurate solution * permanent layers, layer visibility on add/remove part in body * should send all hidden layers over now * isdirty in visualizer system for base layers * isdirty checks count as well * ok, IsDirty should now set the base layers if the merged sprites are different * equals override in HumanoidSpritePrototypes.cs temporary until record prototypes :heck: * makes fields readonly, equates IDs instead * adds forced markings through marking picker * forced in humanoidsystem api, ignorespecies in markingpicker * marking bui * makes that serializable as well * ignore species/forced toggles now work * adds icon to modifier verb, interface and keys to humanoid bases * needs the actual enum value to open, no? * makes the key the actual key * actions now propagate upwards * ignore species when set now repopulates markingpicker * modifiable base layers in the markings window * oops! * layout changes * info box should now appear * adds ignorespecies for marking picker, collapsible for base layer section of appearance modification window * collapsible layout moment * if base layers have changed, all markings are now dirty (and if a base layer is missing, the marking is still 'applied' but it's now just invisible * small change to marking visibility * small changes to modifier UI * markings now match skin on zombification * zombie stuff * makes the line edit in marking modifier window more obvious * disables vox on round start * horizontal expand on the single label in base layer modifiers * humanoid profiles in prototypes * randomhumanoidappearance won't work if the humanoid has a profile already stored * removes unused code * documentation in humanoidsystem server-side * documentation in shared/client * whoops * converts accessory into marking in locale files (also adds marking loc string into single marking picker) * be gone, shared humanoid appearance system from the last upstream merge * species ignore on randomization (defaults to no ignored species) * more upstream merge parts that bypassed any errors before merge * addresses review (also just adds typeserializers in some places) * submodule moment * upstream merge issues
2022-09-22 15:19:00 -07:00
markings
),
spawnPriority,
jobs,
(PreferenceUnavailableMode) profile.PreferenceUnavailable,
2022-09-10 17:40:06 +02:00
antags.ToList(),
Loadouts redux (#25715) * Loadouts redux * Loadout window mockup * More workout * rent * validation * Developments * bcs * More cleanup * Rebuild working * Fix model and loading * obsession * efcore * We got a stew goin * Cleanup * Optional + SeniorEngineering fix * Fixes * Update science.yml * add add * Automatic naming * Update nukeops * Coming together * Right now * stargate * rejig the UI * weh * Loadouts tweaks * Merge conflicts + ordering fix * yerba mate * chocolat * More updates * Add multi-selection support * test h * fikss * a * add tech assistant and hazard suit * huh * Latest changes * add medical loadouts * and science * finish security loadouts * cargo * service done * added wildcards * add command * Move restrictions * Finalising * Fix existing work * Localise next batch * clothing fix * Fix storage names * review * the scooping room * Test fixes * Xamlify * Xamlify this too * Update Resources/Prototypes/Loadouts/Jobs/Medical/paramedic.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/loadout_groups.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/loadout_groups.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/Jobs/Security/detective.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/loadout_groups.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * ben * Margins --------- Co-authored-by: Firewatch <54725557+musicmanvr@users.noreply.github.com> Co-authored-by: Mr. 27 <koolthunder019@gmail.com> Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
2024-04-16 22:57:43 +10:00
traits.ToList(),
loadouts
);
}
private static Profile ConvertProfiles(HumanoidCharacterProfile humanoid, int slot, Profile? profile = null)
{
profile ??= new Profile();
var appearance = (HumanoidCharacterAppearance) humanoid.CharacterAppearance;
List<string> markingStrings = new();
foreach (var marking in appearance.Markings)
{
markingStrings.Add(marking.ToString());
}
var markings = JsonSerializer.SerializeToDocument(markingStrings);
profile.CharacterName = humanoid.Name;
profile.FlavorText = humanoid.FlavorText;
profile.Species = humanoid.Species;
profile.Age = humanoid.Age;
profile.Sex = humanoid.Sex.ToString();
profile.Gender = humanoid.Gender.ToString();
profile.HairName = appearance.HairStyleId;
profile.HairColor = appearance.HairColor.ToHex();
profile.FacialHairName = appearance.FacialHairStyleId;
profile.FacialHairColor = appearance.FacialHairColor.ToHex();
profile.EyeColor = appearance.EyeColor.ToHex();
profile.SkinColor = appearance.SkinColor.ToHex();
profile.SpawnPriority = (int) humanoid.SpawnPriority;
profile.Markings = markings;
profile.Slot = slot;
profile.PreferenceUnavailable = (DbPreferenceUnavailableMode) humanoid.PreferenceUnavailable;
profile.Jobs.Clear();
profile.Jobs.AddRange(
humanoid.JobPriorities
.Where(j => j.Value != JobPriority.Never)
.Select(j => new Job {JobName = j.Key, Priority = (DbJobPriority) j.Value})
);
profile.Antags.Clear();
profile.Antags.AddRange(
humanoid.AntagPreferences
.Select(a => new Antag {AntagName = a})
);
profile.Traits.Clear();
profile.Traits.AddRange(
2022-09-10 17:40:06 +02:00
humanoid.TraitPreferences
.Select(t => new Trait {TraitName = t})
);
Loadouts redux (#25715) * Loadouts redux * Loadout window mockup * More workout * rent * validation * Developments * bcs * More cleanup * Rebuild working * Fix model and loading * obsession * efcore * We got a stew goin * Cleanup * Optional + SeniorEngineering fix * Fixes * Update science.yml * add add * Automatic naming * Update nukeops * Coming together * Right now * stargate * rejig the UI * weh * Loadouts tweaks * Merge conflicts + ordering fix * yerba mate * chocolat * More updates * Add multi-selection support * test h * fikss * a * add tech assistant and hazard suit * huh * Latest changes * add medical loadouts * and science * finish security loadouts * cargo * service done * added wildcards * add command * Move restrictions * Finalising * Fix existing work * Localise next batch * clothing fix * Fix storage names * review * the scooping room * Test fixes * Xamlify * Xamlify this too * Update Resources/Prototypes/Loadouts/Jobs/Medical/paramedic.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/loadout_groups.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/loadout_groups.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/Jobs/Security/detective.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * Update Resources/Prototypes/Loadouts/loadout_groups.yml Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com> * ben * Margins --------- Co-authored-by: Firewatch <54725557+musicmanvr@users.noreply.github.com> Co-authored-by: Mr. 27 <koolthunder019@gmail.com> Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
2024-04-16 22:57:43 +10:00
profile.Loadouts.Clear();
foreach (var (role, loadouts) in humanoid.Loadouts)
{
var dz = new ProfileRoleLoadout()
{
RoleName = role,
};
foreach (var (group, groupLoadouts) in loadouts.SelectedLoadouts)
{
var profileGroup = new ProfileLoadoutGroup()
{
GroupName = group,
};
foreach (var loadout in groupLoadouts)
{
profileGroup.Loadouts.Add(new ProfileLoadout()
{
LoadoutName = loadout.Prototype,
});
}
dz.Groups.Add(profileGroup);
}
profile.Loadouts.Add(dz);
}
return profile;
}
#endregion
2021-11-11 17:54:02 +01:00
#region User Ids
public async Task<NetUserId?> 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();
}
2021-11-11 17:54:02 +01:00
#endregion
2021-11-11 17:54:02 +01:00
#region Bans
/*
* BAN STUFF
*/
/// <summary>
/// Looks up a ban by id.
/// This will return a pardoned ban as well.
/// </summary>
/// <param name="id">The ban id to look for.</param>
/// <returns>The ban with the given id or null if none exist.</returns>
public abstract Task<ServerBanDef?> GetServerBanAsync(int id);
/// <summary>
/// Looks up an user's most recent received un-pardoned ban.
/// This will NOT return a pardoned ban.
/// One of <see cref="address"/> or <see cref="userId"/> need to not be null.
/// </summary>
/// <param name="address">The ip address of the user.</param>
/// <param name="userId">The id of the user.</param>
2021-03-22 01:30:50 +01:00
/// <param name="hwId">The HWId of the user.</param>
/// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns>
2021-03-22 01:30:50 +01:00
public abstract Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId);
/// <summary>
/// Looks up an user's ban history.
/// This will return pardoned bans as well.
/// One of <see cref="address"/> or <see cref="userId"/> need to not be null.
/// </summary>
/// <param name="address">The ip address of the user.</param>
/// <param name="userId">The id of the user.</param>
2021-03-22 01:30:50 +01:00
/// <param name="hwId">The HWId of the user.</param>
/// <param name="includeUnbanned">Include pardoned and expired bans.</param>
/// <returns>The user's ban history.</returns>
2021-03-22 01:30:50 +01:00
public abstract Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
bool includeUnbanned);
public abstract Task AddServerBanAsync(ServerBanDef serverBan);
public abstract Task AddServerUnbanAsync(ServerUnbanDef serverUnban);
2023-04-03 02:24:55 +02:00
public async Task EditServerBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt)
{
await using var db = await GetDb();
var ban = await db.DbContext.Ban.SingleOrDefaultAsync(b => b.Id == id);
if (ban is null)
return;
ban.Severity = severity;
ban.Reason = reason;
ban.ExpirationTime = expiration?.UtcDateTime;
ban.LastEditedById = editedBy;
ban.LastEditedAt = editedAt.UtcDateTime;
await db.DbContext.SaveChangesAsync();
}
2023-04-03 02:24:55 +02:00
protected static async Task<ServerBanExemptFlags?> GetBanExemptionCore(DbGuard db, NetUserId? userId)
{
if (userId == null)
return null;
var exemption = await db.DbContext.BanExemption
.SingleOrDefaultAsync(e => e.UserId == userId.Value.UserId);
return exemption?.Flags;
}
public async Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flags)
{
await using var db = await GetDb();
if (flags == 0)
{
// Delete whatever is there.
await db.DbContext.BanExemption.Where(u => u.UserId == userId.UserId).ExecuteDeleteAsync();
return;
}
var exemption = await db.DbContext.BanExemption.SingleOrDefaultAsync(u => u.UserId == userId.UserId);
if (exemption == null)
{
exemption = new ServerBanExemption
{
UserId = userId
};
db.DbContext.BanExemption.Add(exemption);
}
exemption.Flags = flags;
await db.DbContext.SaveChangesAsync();
}
public async Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId)
{
await using var db = await GetDb();
var flags = await GetBanExemptionCore(db, userId);
return flags ?? ServerBanExemptFlags.None;
}
2021-11-11 17:54:02 +01:00
#endregion
2022-02-21 14:11:39 -08:00
#region Role Bans
/*
* ROLE BANS
*/
/// <summary>
/// Looks up a role ban by id.
/// This will return a pardoned role ban as well.
/// </summary>
/// <param name="id">The role ban id to look for.</param>
/// <returns>The role ban with the given id or null if none exist.</returns>
public abstract Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id);
/// <summary>
/// Looks up an user's role ban history.
/// This will return pardoned role bans based on the <see cref="includeUnbanned"/> bool.
/// Requires one of <see cref="address"/>, <see cref="userId"/>, or <see cref="hwId"/> to not be null.
/// </summary>
/// <param name="address">The IP address of the user.</param>
/// <param name="userId">The NetUserId of the user.</param>
/// <param name="hwId">The Hardware Id of the user.</param>
/// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param>
/// <returns>The user's role ban history.</returns>
public abstract Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
bool includeUnbanned);
public abstract Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan);
2022-02-21 14:11:39 -08:00
public abstract Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban);
public async Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt)
{
await using var db = await GetDb();
var ban = await db.DbContext.RoleBan.SingleOrDefaultAsync(b => b.Id == id);
if (ban is null)
return;
ban.Severity = severity;
ban.Reason = reason;
ban.ExpirationTime = expiration?.UtcDateTime;
ban.LastEditedById = editedBy;
ban.LastEditedAt = editedAt.UtcDateTime;
await db.DbContext.SaveChangesAsync();
}
2022-02-21 14:11:39 -08:00
#endregion
#region Playtime
public async Task<List<PlayTime>> GetPlayTimes(Guid player)
{
await using var db = await GetDb();
return await db.DbContext.PlayTime
.Where(p => p.PlayerId == player)
.ToListAsync();
}
public async Task UpdatePlayTimes(IReadOnlyCollection<PlayTimeUpdate> updates)
{
await using var db = await GetDb();
// Ideally I would just be able to send a bunch of UPSERT commands, but EFCore is a pile of garbage.
// So... In the interest of not making this take forever at high update counts...
// Bulk-load play time objects for all players involved.
// This allows us to semi-efficiently load all entities we need in a single DB query.
// Then we can update & insert without further round-trips to the DB.
var players = updates.Select(u => u.User.UserId).Distinct().ToArray();
var dbTimes = (await db.DbContext.PlayTime
.Where(p => players.Contains(p.PlayerId))
.ToArrayAsync())
.GroupBy(p => p.PlayerId)
.ToDictionary(g => g.Key, g => g.ToDictionary(p => p.Tracker, p => p));
foreach (var (user, tracker, time) in updates)
{
if (dbTimes.TryGetValue(user.UserId, out var userTimes)
&& userTimes.TryGetValue(tracker, out var ent))
{
// Already have a tracker in the database, update it.
ent.TimeSpent = time;
continue;
}
// No tracker, make a new one.
var playTime = new PlayTime
{
Tracker = tracker,
PlayerId = user.UserId,
TimeSpent = time
};
db.DbContext.PlayTime.Add(playTime);
}
await db.DbContext.SaveChangesAsync();
}
#endregion
2021-11-11 17:54:02 +01:00
#region Player Records
/*
* PLAYER RECORDS
*/
2021-11-11 17:54:02 +01:00
public async Task UpdatePlayerRecord(
2021-03-22 01:30:50 +01:00
NetUserId userId,
string userName,
IPAddress address,
2021-11-11 17:54:02 +01:00
ImmutableArray<byte> hwId)
{
await using var db = await GetDb();
var record = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == userId.UserId);
if (record == null)
{
db.DbContext.Player.Add(record = new Player
{
FirstSeenTime = DateTime.UtcNow,
UserId = userId.UserId,
});
}
record.LastSeenTime = DateTime.UtcNow;
record.LastSeenAddress = address;
record.LastSeenUserName = userName;
record.LastSeenHWId = hwId.ToArray();
await db.DbContext.SaveChangesAsync();
}
2021-11-11 17:54:02 +01:00
public async Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel)
{
await using var db = await GetDb();
// Sort by descending last seen time.
// So if, due to account renames, we have two people with the same username in the DB,
// the most recent one is picked.
var record = await db.DbContext.Player
.OrderByDescending(p => p.LastSeenTime)
.FirstOrDefaultAsync(p => p.LastSeenUserName == userName, cancel);
return record == null ? null : MakePlayerRecord(record);
}
public async Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel)
{
await using var db = await GetDb();
var record = await db.DbContext.Player
.SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel);
return record == null ? null : MakePlayerRecord(record);
}
[return: NotNullIfNotNull(nameof(player))]
protected PlayerRecord? MakePlayerRecord(Player? player)
{
if (player == null)
return null;
return new PlayerRecord(
new NetUserId(player.UserId),
new DateTimeOffset(NormalizeDatabaseTime(player.FirstSeenTime)),
player.LastSeenUserName,
new DateTimeOffset(NormalizeDatabaseTime(player.LastSeenTime)),
player.LastSeenAddress,
player.LastSeenHWId?.ToImmutableArray());
}
2021-11-11 17:54:02 +01:00
#endregion
#region Connection Logs
/*
* CONNECTION LOG
*/
public abstract Task<int> AddConnectionLogAsync(
2021-03-22 01:30:50 +01:00
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ConnectionDenyReason? denied,
int serverId);
public async Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans)
{
await using var db = await GetDb();
foreach (var ban in bans)
{
db.DbContext.ServerBanHit.Add(new ServerBanHit
{
ConnectionId = connection, BanId = ban.Id!.Value
});
}
await db.DbContext.SaveChangesAsync();
}
2021-11-11 17:54:02 +01:00
#endregion
2021-11-11 17:54:02 +01:00
#region Admin Ranks
/*
2021-11-11 17:54:02 +01:00
* ADMIN RANKS
*/
2020-11-10 16:50:28 +01:00
public async Task<Admin?> 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)
2022-01-04 06:37:06 -07:00
.AsSplitQuery() // tests fail because of a random warning if you dont have this!
2020-11-10 16:50:28 +01:00
.SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel);
}
public abstract Task<((Admin, string? lastUserName)[] admins, AdminRank[])>
GetAllAdminAndRanksAsync(CancellationToken cancel);
public async Task<AdminRank?> 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();
2020-11-13 03:23:13 +01:00
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;
2020-11-10 16:50:28 +01:00
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);
}
Fix database round start date issues (#26838) How can ONE DATABASE COLUMN have so many cursed issues I don't know, but it certainly pissed off the devil in its previous life. The start_date column on round entities in the database was added by https://github.com/space-wizards/space-station-14/pull/21153. For some reason, this PR gave the column a nonsensical default value instead of making it nullable. This default value causes the code from #25280 to break. It actually trips an assert though that's not what the original issue report ran into. This didn't get noticed on wizden servers because we at some point backfilled the start_date column based on the stored admin logs. So I change the database model to make this column nullable, updated the C# code to match, and made the existing migration set the invalid values to be NULL instead. Cool. Wait how's SQLite handle in this scenario anyways? Well actually turns out the column was *completely broken* in the first place! The code for inserting into the round table was copy pasted between SQLite and PostgreSQL, with the only difference being that the SQLite key manually assigned the primary key instead of letting SQLite AUTOINCREMENT it. And then the code to give a start_date value was only added to the PostgreSQL version (which is actually in the base class already). So for SQLite that column's been filled up with the same invalid default the whole time. Why was the code manually assigning a PK? I checked the SQLite docs for AUTOINCREMENT[1], and the behavior seems appropriate. I removed the SQLite-specific code path and it just seems to work regardless. The migration just sets the old values to NULL too. BUT WAIT, THERE'S MORE! Turns out just doing the migration on SQLite is a pain in the ass! EF Core has to create a new table to apply the nullability change, because SQLite doesn't support proper ALTER COLUMN. This causes the generated SQL commands to be weird and the UPDATE for the migration goes BEFORE the nullability change... I ended up having to make TWO migrations for SQLite. Yay. Fixes #26800 [1]: https://www.sqlite.org/autoinc.html
2024-04-14 07:39:43 +02:00
public async Task<int> AddNewRound(Server server, params Guid[] playerIds)
{
await using var db = await GetDb();
var players = await db.DbContext.Player
.Where(player => playerIds.Contains(player.UserId))
.ToListAsync();
var round = new Round
{
StartDate = DateTime.UtcNow,
Players = players,
ServerId = server.Id
};
db.DbContext.Round.Add(round);
await db.DbContext.SaveChangesAsync();
return round.Id;
}
public async Task<Round> GetRound(int id)
{
await using var db = await GetDb();
var round = await db.DbContext.Round
.Include(round => round.Players)
.SingleAsync(round => round.Id == id);
return round;
}
public async Task AddRoundPlayers(int id, Guid[] playerIds)
{
await using var db = await GetDb();
// ReSharper disable once SuggestVarOrType_Elsewhere
Dictionary<Guid, int> players = await db.DbContext.Player
.Where(player => playerIds.Contains(player.UserId))
.ToDictionaryAsync(player => player.UserId, player => player.Id);
foreach (var player in playerIds)
{
await db.DbContext.Database.ExecuteSqlAsync($"""
INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}) ON CONFLICT DO NOTHING
""");
}
await db.DbContext.SaveChangesAsync();
}
[return: NotNullIfNotNull(nameof(round))]
protected RoundRecord? MakeRoundRecord(Round? round)
{
if (round == null)
return null;
return new RoundRecord(
round.Id,
NormalizeDatabaseTime(round.StartDate),
MakeServerRecord(round.Server));
}
2020-11-10 16:50:28 +01:00
public async Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel)
{
await using var db = await GetDb();
2020-11-13 03:23:13 +01:00
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;
2020-11-10 16:50:28 +01:00
await db.DbContext.SaveChangesAsync(cancel);
}
2021-11-11 17:54:02 +01:00
#endregion
#region Admin Logs
public async Task<(Server, bool existed)> AddOrGetServer(string serverName)
{
await using var db = await GetDb();
var server = await db.DbContext.Server
.Where(server => server.Name.Equals(serverName))
.SingleOrDefaultAsync();
if (server != default)
return (server, true);
server = new Server
{
Name = serverName
};
db.DbContext.Server.Add(server);
await db.DbContext.SaveChangesAsync();
return (server, false);
}
[return: NotNullIfNotNull(nameof(server))]
protected ServerRecord? MakeServerRecord(Server? server)
{
if (server == null)
return null;
return new ServerRecord(server.Id, server.Name);
}
public async Task AddAdminLogs(List<AdminLog> logs)
{
DebugTools.Assert(logs.All(x => x.RoundId > 0), "Adding logs with invalid round ids.");
await using var db = await GetDb();
db.DbContext.AdminLog.AddRange(logs);
await db.DbContext.SaveChangesAsync();
}
protected abstract IQueryable<AdminLog> StartAdminLogsQuery(ServerDbContext db, LogFilter? filter = null);
private IQueryable<AdminLog> GetAdminLogsQuery(ServerDbContext db, LogFilter? filter = null)
{
// Save me from SQLite
var query = StartAdminLogsQuery(db, filter);
if (filter == null)
{
return query.OrderBy(log => log.Date);
}
if (filter.Round != null)
{
query = query.Where(log => log.RoundId == filter.Round);
}
if (filter.Types != null)
{
query = query.Where(log => filter.Types.Contains(log.Type));
}
if (filter.Impacts != null)
{
query = query.Where(log => filter.Impacts.Contains(log.Impact));
}
if (filter.Before != null)
{
query = query.Where(log => log.Date < filter.Before);
}
if (filter.After != null)
{
query = query.Where(log => log.Date > filter.After);
}
if (filter.IncludePlayers)
{
if (filter.AnyPlayers != null)
{
query = query.Where(log =>
log.Players.Any(p => filter.AnyPlayers.Contains(p.PlayerUserId)) ||
log.Players.Count == 0 && filter.IncludeNonPlayers);
}
if (filter.AllPlayers != null)
{
query = query.Where(log =>
log.Players.All(p => filter.AllPlayers.Contains(p.PlayerUserId)) ||
log.Players.Count == 0 && filter.IncludeNonPlayers);
}
}
else
{
query = query.Where(log => log.Players.Count == 0);
}
if (filter.LastLogId != null)
{
query = filter.DateOrder switch
{
DateOrder.Ascending => query.Where(log => log.Id > filter.LastLogId),
DateOrder.Descending => query.Where(log => log.Id < filter.LastLogId),
_ => throw new ArgumentOutOfRangeException(nameof(filter),
$"Unknown {nameof(DateOrder)} value {filter.DateOrder}")
};
}
query = filter.DateOrder switch
{
DateOrder.Ascending => query.OrderBy(log => log.Date),
DateOrder.Descending => query.OrderByDescending(log => log.Date),
_ => throw new ArgumentOutOfRangeException(nameof(filter),
$"Unknown {nameof(DateOrder)} value {filter.DateOrder}")
};
2022-09-27 21:16:04 -07:00
const int hardLogLimit = 500_000;
if (filter.Limit != null)
{
2022-09-27 21:16:04 -07:00
query = query.Take(Math.Min(filter.Limit.Value, hardLogLimit));
}
else
{
query = query.Take(hardLogLimit);
}
return query;
}
public async IAsyncEnumerable<string> GetAdminLogMessages(LogFilter? filter = null)
{
await using var db = await GetDb();
var query = GetAdminLogsQuery(db.DbContext, filter);
await foreach (var log in query.Select(log => log.Message).AsAsyncEnumerable())
{
yield return log;
}
}
public async IAsyncEnumerable<SharedAdminLog> GetAdminLogs(LogFilter? filter = null)
{
await using var db = await GetDb();
var query = GetAdminLogsQuery(db.DbContext, filter);
query = query.Include(log => log.Players);
await foreach (var log in query.AsAsyncEnumerable())
{
var players = new Guid[log.Players.Count];
for (var i = 0; i < log.Players.Count; i++)
{
players[i] = log.Players[i].PlayerUserId;
}
yield return new SharedAdminLog(log.Id, log.Type, log.Impact, log.Date, log.Message, players);
}
}
public async IAsyncEnumerable<JsonDocument> GetAdminLogsJson(LogFilter? filter = null)
{
await using var db = await GetDb();
var query = GetAdminLogsQuery(db.DbContext, filter);
await foreach (var json in query.Select(log => log.Json).AsAsyncEnumerable())
{
yield return json;
}
}
public async Task<int> CountAdminLogs(int round)
{
await using var db = await GetDb();
return await db.DbContext.AdminLog.CountAsync(log => log.RoundId == round);
}
#endregion
2022-01-04 06:37:06 -07:00
#region Whitelist
public async Task<bool> GetWhitelistStatusAsync(NetUserId player)
{
await using var db = await GetDb();
return await db.DbContext.Whitelist.AnyAsync(w => w.UserId == player);
}
public async Task AddToWhitelistAsync(NetUserId player)
{
await using var db = await GetDb();
db.DbContext.Whitelist.Add(new Whitelist { UserId = player });
await db.DbContext.SaveChangesAsync();
}
public async Task RemoveFromWhitelistAsync(NetUserId player)
{
await using var db = await GetDb();
var entry = await db.DbContext.Whitelist.SingleAsync(w => w.UserId == player);
db.DbContext.Whitelist.Remove(entry);
await db.DbContext.SaveChangesAsync();
}
public async Task<DateTimeOffset?> GetLastReadRules(NetUserId player)
{
await using var db = await GetDb();
return NormalizeDatabaseTime(await db.DbContext.Player
.Where(dbPlayer => dbPlayer.UserId == player)
.Select(dbPlayer => dbPlayer.LastReadRules)
.SingleOrDefaultAsync());
}
public async Task SetLastReadRules(NetUserId player, DateTimeOffset date)
{
await using var db = await GetDb();
var dbPlayer = await db.DbContext.Player.Where(dbPlayer => dbPlayer.UserId == player).SingleOrDefaultAsync();
if (dbPlayer == null)
{
return;
}
dbPlayer.LastReadRules = date.UtcDateTime;
await db.DbContext.SaveChangesAsync();
}
2022-01-04 06:37:06 -07:00
#endregion
#region Uploaded Resources Logs
public async Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data)
{
await using var db = await GetDb();
db.DbContext.UploadedResourceLog.Add(new UploadedResourceLog() { UserId = user, Date = date.UtcDateTime, Path = path, Data = data });
await db.DbContext.SaveChangesAsync();
}
public async Task PurgeUploadedResourceLogAsync(int days)
{
await using var db = await GetDb();
var date = DateTime.UtcNow.Subtract(TimeSpan.FromDays(days));
await foreach (var log in db.DbContext.UploadedResourceLog
.Where(l => date > l.Date)
.AsAsyncEnumerable())
{
db.DbContext.UploadedResourceLog.Remove(log);
}
await db.DbContext.SaveChangesAsync();
}
#endregion
#region Admin Notes
public virtual async Task<int> AddAdminNote(AdminNote note)
{
await using var db = await GetDb();
db.DbContext.AdminNotes.Add(note);
await db.DbContext.SaveChangesAsync();
return note.Id;
}
public virtual async Task<int> AddAdminWatchlist(AdminWatchlist watchlist)
{
await using var db = await GetDb();
db.DbContext.AdminWatchlists.Add(watchlist);
await db.DbContext.SaveChangesAsync();
return watchlist.Id;
}
public virtual async Task<int> AddAdminMessage(AdminMessage message)
{
await using var db = await GetDb();
db.DbContext.AdminMessages.Add(message);
await db.DbContext.SaveChangesAsync();
return message.Id;
}
public async Task<AdminNoteRecord?> GetAdminNote(int id)
{
await using var db = await GetDb();
var entity = await db.DbContext.AdminNotes
.Where(note => note.Id == id)
.Include(note => note.Round)
.ThenInclude(r => r!.Server)
.Include(note => note.CreatedBy)
.Include(note => note.LastEditedBy)
.Include(note => note.DeletedBy)
.Include(note => note.Player)
.SingleOrDefaultAsync();
return entity == null ? null : MakeAdminNoteRecord(entity);
}
private AdminNoteRecord MakeAdminNoteRecord(AdminNote entity)
{
return new AdminNoteRecord(
entity.Id,
MakeRoundRecord(entity.Round),
MakePlayerRecord(entity.Player),
entity.PlaytimeAtNote,
entity.Message,
entity.Severity,
MakePlayerRecord(entity.CreatedBy),
NormalizeDatabaseTime(entity.CreatedAt),
MakePlayerRecord(entity.LastEditedBy),
NormalizeDatabaseTime(entity.LastEditedAt),
NormalizeDatabaseTime(entity.ExpirationTime),
entity.Deleted,
MakePlayerRecord(entity.DeletedBy),
NormalizeDatabaseTime(entity.DeletedAt),
entity.Secret);
}
public async Task<AdminWatchlistRecord?> GetAdminWatchlist(int id)
{
await using var db = await GetDb();
var entity = await db.DbContext.AdminWatchlists
.Where(note => note.Id == id)
.Include(note => note.Round)
.ThenInclude(r => r!.Server)
.Include(note => note.CreatedBy)
.Include(note => note.LastEditedBy)
.Include(note => note.DeletedBy)
.Include(note => note.Player)
.SingleOrDefaultAsync();
return entity == null ? null : MakeAdminWatchlistRecord(entity);
}
public async Task<AdminMessageRecord?> GetAdminMessage(int id)
{
await using var db = await GetDb();
var entity = await db.DbContext.AdminMessages
.Where(note => note.Id == id)
.Include(note => note.Round)
.ThenInclude(r => r!.Server)
.Include(note => note.CreatedBy)
.Include(note => note.LastEditedBy)
.Include(note => note.DeletedBy)
.Include(note => note.Player)
.SingleOrDefaultAsync();
return entity == null ? null : MakeAdminMessageRecord(entity);
}
private AdminMessageRecord MakeAdminMessageRecord(AdminMessage entity)
{
return new AdminMessageRecord(
entity.Id,
MakeRoundRecord(entity.Round),
MakePlayerRecord(entity.Player),
entity.PlaytimeAtNote,
entity.Message,
MakePlayerRecord(entity.CreatedBy),
NormalizeDatabaseTime(entity.CreatedAt),
MakePlayerRecord(entity.LastEditedBy),
NormalizeDatabaseTime(entity.LastEditedAt),
NormalizeDatabaseTime(entity.ExpirationTime),
entity.Deleted,
MakePlayerRecord(entity.DeletedBy),
NormalizeDatabaseTime(entity.DeletedAt),
entity.Seen,
entity.Dismissed);
}
public async Task<ServerBanNoteRecord?> GetServerBanAsNoteAsync(int id)
{
await using var db = await GetDb();
var ban = await db.DbContext.Ban
.Include(ban => ban.Unban)
.Include(ban => ban.Round)
.ThenInclude(r => r!.Server)
.Include(ban => ban.CreatedBy)
.Include(ban => ban.LastEditedBy)
.Include(ban => ban.Unban)
.SingleOrDefaultAsync(b => b.Id == id);
if (ban is null)
return null;
var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == ban.PlayerUserId);
return new ServerBanNoteRecord(
ban.Id,
MakeRoundRecord(ban.Round),
MakePlayerRecord(player),
ban.PlaytimeAtNote,
ban.Reason,
ban.Severity,
MakePlayerRecord(ban.CreatedBy),
ban.BanTime,
MakePlayerRecord(ban.LastEditedBy),
ban.LastEditedAt,
ban.ExpirationTime,
ban.Hidden,
MakePlayerRecord(ban.Unban?.UnbanningAdmin == null
? null
: await db.DbContext.Player.SingleOrDefaultAsync(p =>
p.UserId == ban.Unban.UnbanningAdmin.Value)),
ban.Unban?.UnbanTime);
}
public async Task<ServerRoleBanNoteRecord?> GetServerRoleBanAsNoteAsync(int id)
{
await using var db = await GetDb();
var ban = await db.DbContext.RoleBan
.Include(ban => ban.Unban)
.Include(ban => ban.Round)
.ThenInclude(r => r!.Server)
.Include(ban => ban.CreatedBy)
.Include(ban => ban.LastEditedBy)
.Include(ban => ban.Unban)
.SingleOrDefaultAsync(b => b.Id == id);
if (ban is null)
return null;
var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == ban.PlayerUserId);
var unbanningAdmin =
ban.Unban is null
? null
: await db.DbContext.Player.SingleOrDefaultAsync(b => b.UserId == ban.Unban.UnbanningAdmin);
return new ServerRoleBanNoteRecord(
ban.Id,
MakeRoundRecord(ban.Round),
MakePlayerRecord(player),
ban.PlaytimeAtNote,
ban.Reason,
ban.Severity,
MakePlayerRecord(ban.CreatedBy),
ban.BanTime,
MakePlayerRecord(ban.LastEditedBy),
ban.LastEditedAt,
ban.ExpirationTime,
ban.Hidden,
new [] { ban.RoleId.Replace(BanManager.JobPrefix, null) },
MakePlayerRecord(unbanningAdmin),
ban.Unban?.UnbanTime);
}
public async Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player)
{
await using var db = await GetDb();
List<IAdminRemarksRecord> notes = new();
notes.AddRange(
(await (from note in db.DbContext.AdminNotes
where note.PlayerUserId == player &&
!note.Deleted &&
(note.ExpirationTime == null || DateTime.UtcNow < note.ExpirationTime)
select note)
.Include(note => note.Round)
.ThenInclude(r => r!.Server)
.Include(note => note.CreatedBy)
.Include(note => note.LastEditedBy)
.Include(note => note.Player)
.ToListAsync()).Select(MakeAdminNoteRecord));
notes.AddRange(await GetActiveWatchlistsImpl(db, player));
notes.AddRange(await GetMessagesImpl(db, player));
notes.AddRange(await GetServerBansAsNotesForUser(db, player));
notes.AddRange(await GetGroupedServerRoleBansAsNotesForUser(db, player));
return notes;
}
public async Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime)
{
await using var db = await GetDb();
var note = await db.DbContext.AdminNotes.Where(note => note.Id == id).SingleAsync();
note.Message = message;
note.Severity = severity;
note.Secret = secret;
note.LastEditedById = editedBy;
note.LastEditedAt = editedAt.UtcDateTime;
note.ExpirationTime = expiryTime?.UtcDateTime;
await db.DbContext.SaveChangesAsync();
}
public async Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime)
{
await using var db = await GetDb();
var note = await db.DbContext.AdminWatchlists.Where(note => note.Id == id).SingleAsync();
note.Message = message;
note.LastEditedById = editedBy;
note.LastEditedAt = editedAt.UtcDateTime;
note.ExpirationTime = expiryTime?.UtcDateTime;
await db.DbContext.SaveChangesAsync();
}
public async Task EditAdminMessage(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime)
{
await using var db = await GetDb();
var note = await db.DbContext.AdminMessages.Where(note => note.Id == id).SingleAsync();
note.Message = message;
note.LastEditedById = editedBy;
note.LastEditedAt = editedAt.UtcDateTime;
note.ExpirationTime = expiryTime?.UtcDateTime;
await db.DbContext.SaveChangesAsync();
}
public async Task DeleteAdminNote(int id, Guid deletedBy, DateTimeOffset deletedAt)
{
await using var db = await GetDb();
var note = await db.DbContext.AdminNotes.Where(note => note.Id == id).SingleAsync();
note.Deleted = true;
note.DeletedById = deletedBy;
note.DeletedAt = deletedAt.UtcDateTime;
await db.DbContext.SaveChangesAsync();
}
public async Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTimeOffset deletedAt)
{
await using var db = await GetDb();
var watchlist = await db.DbContext.AdminWatchlists.Where(note => note.Id == id).SingleAsync();
watchlist.Deleted = true;
watchlist.DeletedById = deletedBy;
watchlist.DeletedAt = deletedAt.UtcDateTime;
await db.DbContext.SaveChangesAsync();
}
public async Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt)
{
await using var db = await GetDb();
var message = await db.DbContext.AdminMessages.Where(note => note.Id == id).SingleAsync();
message.Deleted = true;
message.DeletedById = deletedBy;
message.DeletedAt = deletedAt.UtcDateTime;
await db.DbContext.SaveChangesAsync();
}
public async Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt)
{
await using var db = await GetDb();
var ban = await db.DbContext.Ban.Where(ban => ban.Id == id).SingleAsync();
ban.Hidden = true;
ban.LastEditedById = deletedBy;
ban.LastEditedAt = deletedAt.UtcDateTime;
await db.DbContext.SaveChangesAsync();
}
public async Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt)
{
await using var db = await GetDb();
var roleBan = await db.DbContext.RoleBan.Where(roleBan => roleBan.Id == id).SingleAsync();
roleBan.Hidden = true;
roleBan.LastEditedById = deletedBy;
roleBan.LastEditedAt = deletedAt.UtcDateTime;
await db.DbContext.SaveChangesAsync();
}
public async Task<List<IAdminRemarksRecord>> GetVisibleAdminRemarks(Guid player)
{
await using var db = await GetDb();
List<IAdminRemarksRecord> notesCol = new();
notesCol.AddRange(
(await (from note in db.DbContext.AdminNotes
where note.PlayerUserId == player &&
!note.Secret &&
!note.Deleted &&
(note.ExpirationTime == null || DateTime.UtcNow < note.ExpirationTime)
select note)
.Include(note => note.Round)
.ThenInclude(r => r!.Server)
.Include(note => note.CreatedBy)
.Include(note => note.Player)
.ToListAsync()).Select(MakeAdminNoteRecord));
notesCol.AddRange(await GetMessagesImpl(db, player));
notesCol.AddRange(await GetServerBansAsNotesForUser(db, player));
notesCol.AddRange(await GetGroupedServerRoleBansAsNotesForUser(db, player));
return notesCol;
}
public async Task<List<AdminWatchlistRecord>> GetActiveWatchlists(Guid player)
{
await using var db = await GetDb();
return await GetActiveWatchlistsImpl(db, player);
}
protected async Task<List<AdminWatchlistRecord>> GetActiveWatchlistsImpl(DbGuard db, Guid player)
{
var entities = await (from watchlist in db.DbContext.AdminWatchlists
where watchlist.PlayerUserId == player &&
!watchlist.Deleted &&
(watchlist.ExpirationTime == null || DateTime.UtcNow < watchlist.ExpirationTime)
select watchlist)
.Include(note => note.Round)
.ThenInclude(r => r!.Server)
.Include(note => note.CreatedBy)
.Include(note => note.LastEditedBy)
.Include(note => note.Player)
.ToListAsync();
return entities.Select(MakeAdminWatchlistRecord).ToList();
}
private AdminWatchlistRecord MakeAdminWatchlistRecord(AdminWatchlist entity)
{
return new AdminWatchlistRecord(entity.Id, MakeRoundRecord(entity.Round), MakePlayerRecord(entity.Player), entity.PlaytimeAtNote, entity.Message, MakePlayerRecord(entity.CreatedBy), NormalizeDatabaseTime(entity.CreatedAt), MakePlayerRecord(entity.LastEditedBy), NormalizeDatabaseTime(entity.LastEditedAt), NormalizeDatabaseTime(entity.ExpirationTime), entity.Deleted, MakePlayerRecord(entity.DeletedBy), NormalizeDatabaseTime(entity.DeletedAt));
}
public async Task<List<AdminMessageRecord>> GetMessages(Guid player)
{
await using var db = await GetDb();
return await GetMessagesImpl(db, player);
}
protected async Task<List<AdminMessageRecord>> GetMessagesImpl(DbGuard db, Guid player)
{
var entities = await (from message in db.DbContext.AdminMessages
where message.PlayerUserId == player && !message.Deleted &&
(message.ExpirationTime == null || DateTime.UtcNow < message.ExpirationTime)
select message).Include(note => note.Round)
.ThenInclude(r => r!.Server)
.Include(note => note.CreatedBy)
.Include(note => note.LastEditedBy)
.Include(note => note.Player)
.ToListAsync();
return entities.Select(MakeAdminMessageRecord).ToList();
}
public async Task MarkMessageAsSeen(int id, bool dismissedToo)
{
await using var db = await GetDb();
var message = await db.DbContext.AdminMessages.SingleAsync(m => m.Id == id);
message.Seen = true;
if (dismissedToo)
message.Dismissed = true;
await db.DbContext.SaveChangesAsync();
}
// These two are here because they get converted into notes later
protected async Task<List<ServerBanNoteRecord>> GetServerBansAsNotesForUser(DbGuard db, Guid user)
{
// You can't group queries, as player will not always exist. When it doesn't, the
// whole query returns nothing
var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == user);
2023-07-21 15:43:47 +02:00
var bans = await db.DbContext.Ban
.Where(ban => ban.PlayerUserId == user && !ban.Hidden)
.Include(ban => ban.Unban)
.Include(ban => ban.Round)
.ThenInclude(r => r!.Server)
.Include(ban => ban.CreatedBy)
.Include(ban => ban.LastEditedBy)
.Include(ban => ban.Unban)
2023-07-21 15:43:47 +02:00
.ToArrayAsync();
var banNotes = new List<ServerBanNoteRecord>();
2023-07-21 15:43:47 +02:00
foreach (var ban in bans)
{
var banNote = new ServerBanNoteRecord(
ban.Id,
MakeRoundRecord(ban.Round),
MakePlayerRecord(player),
ban.PlaytimeAtNote,
ban.Reason,
ban.Severity,
MakePlayerRecord(ban.CreatedBy),
NormalizeDatabaseTime(ban.BanTime),
MakePlayerRecord(ban.LastEditedBy),
NormalizeDatabaseTime(ban.LastEditedAt),
NormalizeDatabaseTime(ban.ExpirationTime),
ban.Hidden,
MakePlayerRecord(ban.Unban?.UnbanningAdmin == null
2023-07-21 15:43:47 +02:00
? null
: await db.DbContext.Player.SingleOrDefaultAsync(
p => p.UserId == ban.Unban.UnbanningAdmin.Value)),
NormalizeDatabaseTime(ban.Unban?.UnbanTime));
2023-07-21 15:43:47 +02:00
banNotes.Add(banNote);
}
return banNotes;
}
protected async Task<List<ServerRoleBanNoteRecord>> GetGroupedServerRoleBansAsNotesForUser(DbGuard db, Guid user)
{
// Server side query
2023-07-21 15:43:47 +02:00
var bansQuery = await db.DbContext.RoleBan
.Where(ban => ban.PlayerUserId == user && !ban.Hidden)
.Include(ban => ban.Unban)
.Include(ban => ban.Round)
.ThenInclude(r => r!.Server)
.Include(ban => ban.CreatedBy)
.Include(ban => ban.LastEditedBy)
.Include(ban => ban.Unban)
2023-07-21 15:43:47 +02:00
.ToArrayAsync();
// Client side query, as EF can't do groups yet
2023-07-21 15:43:47 +02:00
var bansEnumerable = bansQuery
2023-07-22 20:27:29 +03:00
.GroupBy(ban => new { ban.BanTime, CreatedBy = (Player?)ban.CreatedBy, ban.Reason, Unbanned = ban.Unban == null })
2023-07-21 15:43:47 +02:00
.Select(banGroup => banGroup)
.ToArray();
List<ServerRoleBanNoteRecord> bans = new();
var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == user);
2023-07-21 15:43:47 +02:00
foreach (var banGroup in bansEnumerable)
{
2023-07-21 15:43:47 +02:00
var firstBan = banGroup.First();
Player? unbanningAdmin = null;
2023-07-21 15:43:47 +02:00
if (firstBan.Unban?.UnbanningAdmin is not null)
unbanningAdmin = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == firstBan.Unban.UnbanningAdmin.Value);
2023-07-21 15:43:47 +02:00
bans.Add(new ServerRoleBanNoteRecord(
firstBan.Id,
MakeRoundRecord(firstBan.Round),
MakePlayerRecord(player),
firstBan.PlaytimeAtNote,
firstBan.Reason,
firstBan.Severity,
MakePlayerRecord(firstBan.CreatedBy),
NormalizeDatabaseTime(firstBan.BanTime),
MakePlayerRecord(firstBan.LastEditedBy),
NormalizeDatabaseTime(firstBan.LastEditedAt),
NormalizeDatabaseTime(firstBan.ExpirationTime),
firstBan.Hidden,
banGroup.Select(ban => ban.RoleId.Replace(BanManager.JobPrefix, null)).ToArray(),
MakePlayerRecord(unbanningAdmin),
NormalizeDatabaseTime(firstBan.Unban?.UnbanTime)));
}
return bans;
}
#endregion
// SQLite returns DateTime as Kind=Unspecified, Npgsql actually knows for sure it's Kind=Utc.
// Normalize DateTimes here so they're always Utc. Thanks.
protected abstract DateTime NormalizeDatabaseTime(DateTime time);
[return: NotNullIfNotNull(nameof(time))]
protected DateTime? NormalizeDatabaseTime(DateTime? time)
{
return time != null ? NormalizeDatabaseTime(time.Value) : time;
}
public async Task<bool> HasPendingModelChanges()
{
await using var db = await GetDb();
return db.DbContext.Database.HasPendingModelChanges();
}
2023-12-10 16:30:12 +01:00
protected abstract Task<DbGuard> GetDb([CallerMemberName] string? name = null);
protected void LogDbOp(string? name)
{
_opsLog.Verbose($"Running DB operation: {name ?? "unknown"}");
}
protected abstract class DbGuard : IAsyncDisposable
{
public abstract ServerDbContext DbContext { get; }
public abstract ValueTask DisposeAsync();
}
}
}